summaryrefslogtreecommitdiff
path: root/lib/qna/validation.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-02 00:45:49 +0000
commit2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch)
treef406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/qna/validation.ts
parent6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff)
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/qna/validation.ts')
-rw-r--r--lib/qna/validation.ts374
1 files changed, 374 insertions, 0 deletions
diff --git a/lib/qna/validation.ts b/lib/qna/validation.ts
new file mode 100644
index 00000000..dd140963
--- /dev/null
+++ b/lib/qna/validation.ts
@@ -0,0 +1,374 @@
+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<QnaViewSelect>().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<ReturnType<typeof searchParamsQnaCache.parse>>;
+export type CreateQnaSchema = z.infer<typeof createQnaSchema>;
+export type UpdateQnaSchema = z.infer<typeof updateQnaSchema>;
+export type CreateAnswerSchema = z.infer<typeof createAnswerSchema>;
+export type UpdateAnswerSchema = z.infer<typeof updateAnswerSchema>;
+export type CreateCommentSchema = z.infer<typeof createCommentSchema>;
+export type UpdateCommentSchema = z.infer<typeof updateCommentSchema>;
+export type BulkQnaActionSchema = z.infer<typeof bulkQnaActionSchema>;
+export type QnaSearchFilterSchema = z.infer<typeof qnaSearchFilterSchema>;
+export type QnaSortSchema = z.infer<typeof qnaSortSchema>;
+export type ExportQnaSchema = z.infer<typeof exportQnaSchema>;
+
+// 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
+ );
+ },
+}; \ No newline at end of file