"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" 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({ contractId: z.number(), docNumber: z.string().min(1, "Document number is required"), title: z.string().min(1, "Title is required"), status: z.string(), stages: z.array(z.object({ name: z.string().min(1, "Stage name cannot be empty"), order: z.number().int().positive("Order must be a positive integer") })) .min(1, "At least one stage is required") .refine(stages => { // 중복된 order 값이 없는지 확인 const orders = stages.map(s => s.order); return orders.length === new Set(orders).size; }, { message: "Stage orders must be unique" }) }); export type CreateDocumentInputType = z.infer; 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, }) .returning({ id: documents.id }); // 2. 스테이지 생성 (순서와 함께) const stageValues = validatedData.stages.map(stage => ({ documentId: newDocument.id, stageName: stage.name, stageOrder: stage.order, stageStatus: "PLANNED", // 기본 상태 설정 })); // 스테이지 배열 삽입 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; 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; /** * 문서 정보 수정 서버 액션 */ 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" }; } } export async function getContractIdsByVendor(vendorId: number): Promise { try { const contractsData = await db .select({ id: contracts.id }) .from(contracts) .where(eq(contracts.vendorId, vendorId)) .orderBy(contracts.id) return contractsData.map(contract => contract.id) } catch (error) { console.error('Error fetching contract IDs by vendor:', error) return [] } }