import { qnaView, type QnaViewSelect } from "@/db/schema"; import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString, parseAsStringEnum, } from "nuqs/server"; import * as z from "zod"; import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"; // Q&A 검색 파라미터 캐시 export const searchParamsQnaCache = createSearchParamsCache({ flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]), page: parseAsInteger.withDefault(1), perPage: parseAsInteger.withDefault(10), sort: getSortingStateParser().withDefault([ { id: "lastActivityAt", desc: true }, // Q&A는 최근 활동순이 기본 ]), // 고급 필터 filters: getFiltersStateParser().withDefault([]), joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"), // 전역 검색 search: parseAsString.withDefault(""), // Q&A 특화 필터 authorDomain: parseAsArrayOf( z.enum(["partners", "tech", "admin"]) ).withDefault([]), vendorType: parseAsArrayOf( z.enum(["vendor", "techVendor"]) ).withDefault([]), hasAnswers: parseAsStringEnum([ "all", "answered", "unanswered" ]).withDefault("all"), myQuestions: parseAsStringEnum([ "all", "mine", "others" ]).withDefault("all"), isPopular: parseAsStringEnum([ "all", "popular", "normal" ]).withDefault("all"), // 날짜 범위 필터 from: parseAsString.withDefault(""), to: parseAsString.withDefault(""), // 답변 수 범위 필터 minAnswers: parseAsInteger.withDefault(0), maxAnswers: parseAsInteger.withDefault(999), // 회사별 필터 (특정 회사 선택) companyNames: parseAsArrayOf(z.string()).withDefault([]), }); // Q&A 생성 스키마 export const createQnaSchema = z.object({ title: z .string() .min(1, "제목을 입력해주세요") .max(255, "제목은 255자 이하로 입력해주세요") .trim(), content: z .string() .min(1, "내용을 입력해주세요") // .max(10000, "내용은 10000자 이하로 입력해주세요") .trim(), category: z .enum(['engineering', 'procurement', 'technical_sales'], { required_error: "카테고리를 선택해주세요", invalid_type_error: "올바른 카테고리를 선택해주세요" }) }); // Q&A 수정 스키마 export const updateQnaSchema = z.object({ title: z .string() .min(1, "제목을 입력해주세요") .max(255, "제목은 255자 이하로 입력해주세요") .trim() .optional(), content: z .string() .min(1, "내용을 입력해주세요") // .max(10000, "내용은 10000자 이하로 입력해주세요") .trim() .optional(), category: z .enum(['engineering', 'procurement', 'technical_sales'], { required_error: "카테고리를 선택해주세요", invalid_type_error: "올바른 카테고리를 선택해주세요" }) .optional(), }); // 답변 생성 스키마 export const createAnswerSchema = z.object({ qnaId: z .number() .positive("유효하지 않은 질문 ID입니다"), content: z .string() .min(1, "답변 내용을 입력해주세요") .max(10000, "답변은 10000자 이하로 입력해주세요") .trim(), }); // 답변 수정 스키마 export const updateAnswerSchema = z.object({ content: z .string() .min(1, "답변 내용을 입력해주세요") .max(10000, "답변은 10000자 이하로 입력해주세요") .trim(), }); // 댓글 생성 스키마 export const createCommentSchema = z.object({ answerId: z .number() .positive("유효하지 않은 답변 ID입니다"), content: z .string() .min(1, "댓글 내용을 입력해주세요") .max(1000, "댓글은 1000자 이하로 입력해주세요") .trim(), parentCommentId: z .number() .positive() .optional(), }); // 댓글 수정 스키마 export const updateCommentSchema = z.object({ content: z .string() .min(1, "댓글 내용을 입력해주세요") .max(1000, "댓글은 1000자 이하로 입력해주세요") .trim(), }); // Q&A 벌크 액션 스키마 export const bulkQnaActionSchema = z.object({ qnaIds: z .array(z.number().positive()) .min(1, "선택된 질문이 없습니다"), action: z.enum([ "delete", "restore", "archive" ]), }); // Q&A 검색 필터 스키마 (고급 검색용) export const qnaSearchFilterSchema = z.object({ title: z.string().optional(), content: z.string().optional(), authorName: z.string().optional(), companyName: z.string().optional(), authorDomain: z.array(z.enum(["partners", "tech", "admin"])).optional(), vendorType: z.array(z.enum(["vendor", "techVendor"])).optional(), hasAnswers: z.enum(["all", "answered", "unanswered"]).optional(), isPopular: z.boolean().optional(), createdFrom: z.date().optional(), createdTo: z.date().optional(), lastActivityFrom: z.date().optional(), lastActivityTo: z.date().optional(), minAnswers: z.number().min(0).optional(), maxAnswers: z.number().min(0).optional(), minComments: z.number().min(0).optional(), maxComments: z.number().min(0).optional(), }); // Q&A 정렬 옵션 스키마 export const qnaSortSchema = z.object({ field: z.enum([ "createdAt", "updatedAt", "lastActivityAt", "title", "authorName", "companyName", "totalAnswers", "totalComments", "answerCount", "isPopular" ]), direction: z.enum(["asc", "desc"]), }); // Q&A 내보내기 스키마 export const exportQnaSchema = z.object({ format: z.enum(["csv", "excel", "pdf"]), fields: z.array(z.enum([ "title", "content", "authorName", "companyName", "createdAt", "totalAnswers", "totalComments", "lastActivityAt" ])).default([ "title", "authorName", "companyName", "createdAt", "totalAnswers" ]), dateRange: z.object({ from: z.date().optional(), to: z.date().optional(), }).optional(), includeAnswers: z.boolean().default(false), includeComments: z.boolean().default(false), }); // 타입 내보내기 export type GetQnaSchema = Awaited>; export type CreateQnaSchema = z.infer; export type UpdateQnaSchema = z.infer; export type CreateAnswerSchema = z.infer; export type UpdateAnswerSchema = z.infer; export type CreateCommentSchema = z.infer; export type UpdateCommentSchema = z.infer; export type BulkQnaActionSchema = z.infer; export type QnaSearchFilterSchema = z.infer; export type QnaSortSchema = z.infer; export type ExportQnaSchema = z.infer; // Q&A 상태 enum (프론트엔드에서 사용) export const QNA_STATUS = { UNANSWERED: "unanswered", ANSWERED: "answered", POPULAR: "popular", ARCHIVED: "archived", } as const; export const QNA_DOMAIN = { PARTNERS: "partners", TECH: "tech", ADMIN: "admin", } as const; export const QNA_VENDOR_TYPE = { VENDOR: "vendor", TECH_VENDOR: "techVendor", } as const; // 필터 옵션 (프론트엔드 드롭다운용) export const QNA_FILTER_OPTIONS = { hasAnswers: [ { value: "all", label: "전체" }, { value: "answered", label: "답변있음" }, { value: "unanswered", label: "답변없음" }, ], authorDomain: [ { value: "partners", label: "협력업체" }, { value: "tech", label: "기술업체" }, { value: "admin", label: "관리자" }, ], vendorType: [ { value: "vendor", label: "일반 벤더" }, { value: "techVendor", label: "기술 벤더" }, ], myQuestions: [ { value: "all", label: "전체 질문" }, { value: "mine", label: "내 질문" }, { value: "others", label: "다른 사람 질문" }, ], isPopular: [ { value: "all", label: "전체" }, { value: "popular", label: "인기 질문" }, { value: "normal", label: "일반 질문" }, ], } as const; // 정렬 옵션 (프론트엔드 드롭다운용) export const QNA_SORT_OPTIONS = [ { value: "lastActivityAt", label: "최근 활동순", direction: "desc" }, { value: "createdAt", label: "최신 등록순", direction: "desc" }, { value: "createdAt", label: "오래된 순", direction: "asc" }, { value: "title", label: "제목순", direction: "asc" }, { value: "totalAnswers", label: "답변 많은 순", direction: "desc" }, { value: "totalComments", label: "댓글 많은 순", direction: "desc" }, { value: "authorName", label: "작성자순", direction: "asc" }, { value: "companyName", label: "회사명순", direction: "asc" }, ] as const; // 유틸리티 함수들 export const qnaValidationUtils = { /** * 검색 파라미터 유효성 검사 */ validateSearchParams: (params: unknown) => { try { return searchParamsQnaCache.parse(params); } catch (error) { console.error("Invalid search parameters:", error); return searchParamsQnaCache.parse({}); // 기본값 반환 } }, /** * 날짜 범위 유효성 검사 */ validateDateRange: (from?: string, to?: string) => { if (!from && !to) return { isValid: true }; const fromDate = from ? new Date(from) : null; const toDate = to ? new Date(to) : null; if (fromDate && toDate && fromDate > toDate) { return { isValid: false, error: "시작 날짜가 종료 날짜보다 늦을 수 없습니다." }; } return { isValid: true }; }, /** * 페이지네이션 범위 계산 */ calculatePagination: (page: number, perPage: number, total: number) => { const totalPages = Math.ceil(total / perPage); const hasNextPage = page < totalPages; const hasPrevPage = page > 1; const startIndex = (page - 1) * perPage + 1; const endIndex = Math.min(page * perPage, total); return { totalPages, hasNextPage, hasPrevPage, startIndex, endIndex, isFirstPage: page === 1, isLastPage: page === totalPages, }; }, /** * 필터 활성 상태 확인 */ hasActiveFilters: (params: GetQnaSchema) => { return !!( params.search || params.authorDomain.length > 0 || params.vendorType.length > 0 || params.hasAnswers !== "all" || params.myQuestions !== "all" || params.isPopular !== "all" || params.from || params.to || params.minAnswers > 0 || params.maxAnswers < 999 || params.companyNames.length > 0 ); }, };