diff options
Diffstat (limited to 'lib/vendor-document-list/service.ts')
| -rw-r--r-- | lib/vendor-document-list/service.ts | 284 |
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" + }; + } +} |
