summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-03-26 00:37:41 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-03-26 00:37:41 +0000
commite0dfb55c5457aec489fc084c4567e791b4c65eb1 (patch)
tree68543a65d88f5afb3a0202925804103daa91bc6f /lib/vendor-document-list/service.ts
3/25 까지의 대표님 작업사항
Diffstat (limited to 'lib/vendor-document-list/service.ts')
-rw-r--r--lib/vendor-document-list/service.ts284
1 files changed, 284 insertions, 0 deletions
diff --git a/lib/vendor-document-list/service.ts b/lib/vendor-document-list/service.ts
new file mode 100644
index 00000000..75c9b6cd
--- /dev/null
+++ b/lib/vendor-document-list/service.ts
@@ -0,0 +1,284 @@
+"use server"
+
+import { eq, SQL } from "drizzle-orm"
+import db from "@/db/db"
+import { documents, documentStagesView, issueStages } from "@/db/schema/vendorDocu"
+import { contracts } from "@/db/schema/vendorData"
+import { GetVendorDcoumentsSchema } from "./validations"
+import { unstable_cache } from "@/lib/unstable-cache";
+import { filterColumns } from "@/lib/filter-columns";
+import { getErrorMessage } from "@/lib/handle-error";
+import { asc, desc, ilike, inArray, and, gte, lte, not, or } from "drizzle-orm";
+import { countVendorDocuments, selectVendorDocuments } from "./repository"
+import path from "path";
+import fs from "fs/promises";
+import { v4 as uuidv4 } from "uuid"
+import { z } from "zod"
+import { revalidateTag, unstable_noStore ,revalidatePath} from "next/cache";
+
+/**
+ * 특정 vendorId에 속한 문서 목록 조회
+ */
+export async function getVendorDocuments(input: GetVendorDcoumentsSchema, id: number) {
+
+ return unstable_cache(
+ async () => {
+ try {
+ const offset = (input.page - 1) * input.perPage;
+
+ // advancedTable 모드면 filterColumns()로 where 절 구성
+ const advancedWhere = filterColumns({
+ table: documentStagesView,
+ filters: input.filters,
+ joinOperator: input.joinOperator,
+ });
+
+ let globalWhere
+ if (input.search) {
+ const s = `%${input.search}%`
+ globalWhere = or(ilike(documentStagesView.title, s), ilike(documentStagesView.docNumber, s)
+ )
+ // 필요시 여러 칼럼 OR조건 (status, priority, etc)
+ }
+
+ const finalWhere = and(advancedWhere, globalWhere, eq(documentStagesView.contractId, id));
+ const orderBy =
+ input.sort.length > 0
+ ? input.sort.map((item) =>
+ item.desc ? desc(documentStagesView[item.id]) : asc(documentStagesView[item.id])
+ )
+ : [asc(documentStagesView.createdAt)];
+
+ // 트랜잭션 내부에서 Repository 호출
+ const { data, total } = await db.transaction(async (tx) => {
+ const data = await selectVendorDocuments(tx, {
+ where: finalWhere,
+ orderBy,
+ offset,
+ limit: input.perPage,
+ });
+ const total = await countVendorDocuments(tx, finalWhere);
+ return { data, total };
+ });
+
+ const pageCount = Math.ceil(total / input.perPage);
+
+
+ return { data, pageCount };
+ } catch (err) {
+ // 에러 발생 시 디폴트
+ return { data: [], pageCount: 0 };
+ }
+ },
+ [JSON.stringify(input), String(id)], // 캐싱 키
+ {
+ revalidate: 3600,
+ tags: [`vendor-docuemnt-list-${id}`],
+ }
+ )();
+}
+
+
+// 입력 스키마 정의
+const createDocumentSchema = z.object({
+ docNumber: z.string().min(1, "Document number is required"),
+ title: z.string().min(1, "Title is required"),
+ status: z.string(),
+ stages: z.array(z.string()).min(1, "At least one stage is required"),
+ contractId: z.number().positive("Contract ID is required")
+});
+
+export type CreateDocumentInputType = z.infer<typeof createDocumentSchema>;
+
+export async function createDocument(input: CreateDocumentInputType) {
+ try {
+ // 입력 유효성 검증
+ const validatedData = createDocumentSchema.parse(input);
+
+ // 트랜잭션 사용하여 문서와 스테이지 동시 생성
+ return await db.transaction(async (tx) => {
+ // 1. 문서 생성
+ const [newDocument] = await tx
+ .insert(documents)
+ .values({
+ contractId: validatedData.contractId,
+ docNumber: validatedData.docNumber,
+ title: validatedData.title,
+ status: validatedData.status,
+ // issuedDate는 선택적으로 추가 가능
+ })
+ .returning({ id: documents.id });
+
+ // 2. 스테이지 생성 (문서 ID 연결)
+ const stageValues = validatedData.stages.map(stageName => ({
+ documentId: newDocument.id,
+ stageName: stageName,
+ // planDate, actualDate는 나중에 설정 가능
+ }));
+
+ // 스테이지 배열 삽입
+ await tx.insert(issueStages).values(stageValues);
+
+ // 성공 결과 반환
+ return {
+ success: true,
+ documentId: newDocument.id,
+ message: "Document and stages created successfully"
+ };
+ });
+ } catch (error) {
+ console.error("Error creating document:", error);
+
+ // Zod 유효성 검사 에러 처리
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ message: "Validation failed",
+ errors: error.errors
+ };
+ }
+
+ // 기타 에러 처리
+ return {
+ success: false,
+ message: "Failed to create document"
+ };
+ }
+}
+
+// 캐시 무효화 함수
+export async function invalidateDocumentCache(contractId: number) {
+ revalidatePath(`/partners/document-list/${contractId}`);
+ // 추가로 tag 기반 캐시도 무효화할 수 있음
+ revalidateTag(`vendor-docuemnt-list-${contractId}`);
+}
+
+const removeDocumentsSchema = z.object({
+ ids: z.array(z.number()).min(1, "At least one document ID is required")
+});
+export type RemoveDocumentsInputType = z.infer<typeof removeDocumentsSchema>;
+
+export async function removeDocuments(input: RemoveDocumentsInputType) {
+ try {
+ // 입력 유효성 검증
+ const validatedData = removeDocumentsSchema.parse(input);
+
+ // 먼저 삭제할 문서의 contractId를 일반 select 쿼리로 가져옴
+ const [result] = await db
+ .select({ contractId: documents.contractId })
+ .from(documents)
+ .where(eq(documents.id, validatedData.ids[0]))
+ .limit(1);
+
+ const contractId = result?.contractId;
+
+ // 트랜잭션 사용하여 문서 삭제
+ await db.transaction(async (tx) => {
+ // documents 테이블에서 삭제 (cascade 옵션으로 연결된 issueStages도 함께 삭제)
+ await tx
+ .delete(documents)
+ .where(inArray(documents.id, validatedData.ids));
+ });
+
+ // 캐시 무효화
+ if (contractId) {
+ await invalidateDocumentCache(contractId);
+ }
+
+ return { success: true };
+ } catch (error) {
+ console.error("Error removing documents:", error);
+
+ // Zod 유효성 검사 에러 처리
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ error: "Validation failed: " + error.errors.map(e => e.message).join(', ')
+ };
+ }
+
+ // 기타 에러 처리
+ return {
+ success: false,
+ error: "Failed to remove documents"
+ };
+ }
+}
+
+// 입력 스키마 정의
+const modifyDocumentSchema = z.object({
+ id: z.number().positive("Document ID is required"),
+ contractId: z.number().positive("Contract ID is required"),
+ docNumber: z.string().min(1, "Document number is required"),
+ title: z.string().min(1, "Title is required"),
+ status: z.string().min(1, "Status is required"),
+ description: z.string().optional(),
+ remarks: z.string().optional()
+});
+
+export type ModifyDocumentInputType = z.infer<typeof modifyDocumentSchema>;
+
+/**
+ * 문서 정보 수정 서버 액션
+ */
+export async function modifyDocument(input: ModifyDocumentInputType) {
+ try {
+ // 입력 유효성 검증
+ const validatedData = modifyDocumentSchema.parse(input);
+
+ // 업데이트할 문서 데이터 준비
+ const updateData = {
+ docNumber: validatedData.docNumber,
+ title: validatedData.title,
+ status: validatedData.status,
+ description: validatedData.description,
+ remarks: validatedData.remarks,
+ updatedAt: new Date() // 수정 시간 업데이트
+ };
+
+ // 트랜잭션 사용하여 문서 업데이트
+ const [updatedDocument] = await db.transaction(async (tx) => {
+ // documents 테이블 업데이트
+ return tx
+ .update(documents)
+ .set(updateData)
+ .where(eq(documents.id, validatedData.id))
+ .returning({ id: documents.id });
+ });
+
+ // 문서가 존재하지 않는 경우 처리
+ if (!updatedDocument) {
+ return {
+ success: false,
+ error: "Document not found"
+ };
+ }
+
+ // 캐시 무효화
+ await invalidateDocumentCache(validatedData.contractId);
+
+ // 성공 결과 반환
+ return {
+ success: true,
+ documentId: updatedDocument.id,
+ message: "Document updated successfully"
+ };
+
+ } catch (error) {
+ console.error("Error updating document:", error);
+
+ // Zod 유효성 검사 에러 처리
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ error: "Validation failed: " + error.errors.map(e => e.message).join(', ')
+ };
+ }
+
+ // 기타 에러 처리
+ return {
+ success: false,
+ error: getErrorMessage(error) || "Failed to update document"
+ };
+ }
+}