"use server"; import { revalidateTag, revalidatePath, unstable_noStore } from "next/cache"; import db from "@/db/db"; import { getErrorMessage } from "@/lib/handle-error"; import { unstable_cache } from "@/lib/unstable-cache"; import { asc, desc, ilike, inArray, and, or, eq, type SQL, sql } from "drizzle-orm"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { GeneralContractTemplate, generalContractTemplates, users } from "@/db/schema"; import { deleteFile, saveFile, saveDRMFile } from "@/lib/file-stroage"; import { decryptWithServerAction } from "@/components/drm/drmUtils"; import { selectContractTemplates, selectContractTemplatesWithUsers, countContractTemplates, insertContractTemplate, getContractTemplateById as getContractTemplateByIdFromRepo, updateContractTemplate, deleteContractTemplates, findAllContractTemplates } from "./repository"; import { GetContractTemplatesSchema, CreateContractTemplateSchema, UpdateContractTemplateSchema, DeleteContractTemplateSchema } from "./validations"; // ---------------------------------------------------------------------------------------------------- /* HELPER FUNCTION FOR GETTING CURRENT USER ID */ async function getCurrentUserId(): Promise { try { const session = await getServerSession(authOptions); return session?.user?.id ? Number(session.user.id) : 3; // 기본값 3, 실제 환경에서는 적절한 기본값 설정 } catch (error) { console.error('Error getting current user ID:', error); return 3; // 기본값 3 } } // ---------------------------------------------------------------------------------------------------- // ================================================================================= // Contract Template Functions // ================================================================================= export async function getContractTemplates( input: GetContractTemplatesSchema ) { return unstable_cache( async () => { try { const { data, total } = await db.transaction(async (tx) => { // 필터 조건 구성 let whereCondition: any = undefined; if (input.search) { const s = `%${input.search}%`; whereCondition = or( ilike(generalContractTemplates.contractTemplateName, s), ilike(generalContractTemplates.contractTemplateType, s), ilike(generalContractTemplates.fileName, s) ); } // 필터 추가 (기본적으로 모든 상태 표시) let statusCondition: any = undefined; if (input.filters && input.filters.length > 0) { const statusFilter = input.filters.find(f => f.id === 'status'); if (statusFilter && statusFilter.value.length > 0) { // statusFilter.value가 문자열이면 배열로 변환 const statusValues = Array.isArray(statusFilter.value) ? statusFilter.value : [statusFilter.value]; // "ALL"이 포함되어 있으면 상태 필터를 제거 (모든 상태 표시) if (statusValues.includes('ALL')) { statusCondition = undefined; } else { statusCondition = inArray(generalContractTemplates.status, statusValues); } } // 다른 필터들 처리 const otherFilters = input.filters.filter(f => f.id !== 'status' && f.value.length > 0); for (const filter of otherFilters) { let filterCondition: any; // 계약문서명은 부분 일치 검색 (ilike) if (filter.id === 'contractTemplateName') { const searchValue = `%${filter.value}%`; filterCondition = ilike(generalContractTemplates.contractTemplateName, searchValue); } else { // 다른 필터들은 정확히 일치 검색 (inArray) filterCondition = inArray( generalContractTemplates[filter.id as keyof typeof generalContractTemplates] as any, filter.value ); } whereCondition = whereCondition ? and(whereCondition, filterCondition) : filterCondition; } } // 최종 where 조건 if (statusCondition) { if (whereCondition) { whereCondition = and(whereCondition, statusCondition); } else { whereCondition = statusCondition; } } // 정렬 조건 const orderBy = input.sort && input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(generalContractTemplates[item.id as keyof typeof generalContractTemplates] as any) : asc(generalContractTemplates[item.id as keyof typeof generalContractTemplates] as any) ) : [desc(generalContractTemplates.createdAt)]; // 데이터 조회 (사용자 정보 포함) const offset = (input.page - 1) * input.perPage; const dataResult = await selectContractTemplatesWithUsers(tx, { where: whereCondition, orderBy, offset, limit: input.perPage, }); // 총 개수 조회 const totalCount = await countContractTemplates(tx, whereCondition); return { data: dataResult, total: totalCount }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (error) { console.error("getContractTemplates 에러:", error); return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["general-contract-templates"], } )(); } /** * ID로 계약 템플릿 조회 */ export async function getContractTemplateById(id: string) { return unstable_cache( async () => { try { const template = await db .select() .from(generalContractTemplates) .where(eq(generalContractTemplates.id, parseInt(id))) .limit(1); return template[0] || null; } catch (error) { console.error("getContractTemplateById 에러:", error); return null; } }, [id], { revalidate: 3600, tags: ["general-contract-templates"] } )(); } /** * 템플릿 이름 조회 */ export async function getExistingTemplateNamesById(id: number): Promise { const rows = await db .select({ contractTemplateName: generalContractTemplates.contractTemplateName, }) .from(generalContractTemplates) .where(and(eq(generalContractTemplates.status, "ACTIVE"), eq(generalContractTemplates.id, id))) .limit(1); return rows[0]?.contractTemplateName || ""; } /** * 템플릿 파일 저장 서버 액션 */ export async function saveTemplateFile(templateId: number, formData: FormData) { try { const file = formData.get("file") as File; if (!file) { return { error: "파일이 필요합니다." }; } // 기존 템플릿 정보 조회 const existingTemplate = await db .select() .from(generalContractTemplates) .where(eq(generalContractTemplates.id, templateId)) .limit(1); if (existingTemplate.length === 0) { return { error: "템플릿을 찾을 수 없습니다." }; } const template = existingTemplate[0]; if (!template.filePath) { return { error: "파일 경로가 없습니다." }; } // 파일 저장 로직 (DRM 해제 로직 적용) const saveResult = await saveDRMFile( file, decryptWithServerAction, 'general-contract-templates' ); if (!saveResult.success) { return { error: saveResult.error || '파일 저장에 실패했습니다.' }; } // 기존 파일 경로와 다른 경우에만 파일 경로 업데이트 if (template.filePath !== saveResult.publicPath) { // 기존 파일 삭제 if (template.filePath) { const deleted = await deleteFile(template.filePath); if (deleted) { console.log(`✅ 기존 파일 삭제됨: ${template.filePath}`); } else { console.log(`⚠️ 기존 파일 삭제 실패: ${template.filePath}`); } } // DB에 새 파일 경로 업데이트 await db .update(generalContractTemplates) .set({ filePath: saveResult.publicPath, fileName: file.name, updatedAt: new Date(), }) .where(eq(generalContractTemplates.id, templateId)); } else { // 같은 경로인 경우 수정일시만 업데이트 await db .update(generalContractTemplates) .set({ updatedAt: new Date(), }) .where(eq(generalContractTemplates.id, templateId)); } // 캐시 무효화 (목록/상세 모두 고려) revalidatePath(`/evcp/general-contract-template/${templateId}`); revalidateTag("general-contract-templates"); return { success: true, message: "파일이 성공적으로 저장되었습니다." }; } catch (error) { console.error("saveTemplateFile 에러:", error); return { error: error instanceof Error ? error.message : "파일 저장 중 오류가 발생했습니다." }; } } // 새 리비전 생성 (basic-contract의 createBasicContractTemplateRevision 패턴 반영) export async function createGeneralContractTemplateRevision(input: { baseTemplateId: number; contractTemplateType: string; contractTemplateName: string; revision: number; legalReviewRequired: boolean; status: 'ACTIVE' | 'INACTIVE' | 'DISPOSED'; fileName: string; filePath: string; }) { unstable_noStore(); try { // 기본 템플릿 존재 확인 const base = await db .select() .from(generalContractTemplates) .where(eq(generalContractTemplates.id, input.baseTemplateId)) .limit(1); if (base.length === 0) { return { data: null, error: '기본 템플릿을 찾을 수 없습니다.' }; } // 동일 이름/타입에 해당 리비전이 이미 존재하는지 검사 const exists = await db .select({ rev: generalContractTemplates.revision }) .from(generalContractTemplates) .where( and( eq(generalContractTemplates.contractTemplateName, input.contractTemplateName), eq(generalContractTemplates.contractTemplateType, input.contractTemplateType), eq(generalContractTemplates.revision, input.revision) ) ) .limit(1); if (exists.length > 0) { return { data: null, error: `${input.contractTemplateName} v${input.revision} 리비전이 이미 존재합니다.` }; } // 기존 리비전 확인 - baseTemplateId가 있으면 해당 템플릿의 최대 리비전을 찾음 let maxRevision = 0; if (input.baseTemplateId) { const max = await db .select({ rev: generalContractTemplates.revision }) .from(generalContractTemplates) .where(eq(generalContractTemplates.id, input.baseTemplateId)) .limit(1); maxRevision = max[0]?.rev ?? 0; } else { // baseTemplateId가 없으면 이름과 타입으로 찾음 const max = await db .select({ rev: generalContractTemplates.revision }) .from(generalContractTemplates) .where( and( eq(generalContractTemplates.contractTemplateName, input.contractTemplateName), eq(generalContractTemplates.contractTemplateType, input.contractTemplateType) ) ) .orderBy(desc(generalContractTemplates.revision)) .limit(1); maxRevision = max[0]?.rev ?? 0; } if (input.revision <= maxRevision) { return { data: null, error: `새 리비전 번호는 현재 최대 리비전(v${maxRevision})보다 커야 합니다.` }; } const currentUserId = await getCurrentUserId(); const newRow = await db.transaction(async (tx) => { const [row] = await insertContractTemplate(tx, { contractTemplateType: input.contractTemplateType, contractTemplateName: input.contractTemplateName, revision: input.revision, status: input.status, legalReviewRequired: input.legalReviewRequired, fileName: input.fileName, filePath: input.filePath, createdAt: new Date(), createdBy: currentUserId, updatedAt: new Date(), updatedBy: currentUserId, }); return row; }); revalidateTag('general-contract-templates'); return { data: newRow, error: null }; } catch (error) { return { data: null, error: getErrorMessage(error) }; } } // Contract Template 생성 export async function createContractTemplate(input: CreateContractTemplateSchema) { unstable_noStore(); try { // 현재 로그인한 사용자 ID 가져오기 const currentUserId = await getCurrentUserId(); const newTemplate = await db.transaction(async (tx) => { const [row] = await insertContractTemplate(tx, { contractTemplateType: (input as any).contractTemplateType ?? (input as any).contractType, contractTemplateName: (input as any).contractTemplateName ?? (input as any).contractName, revision: input.revision || 1, status: input.status || "ACTIVE", legalReviewRequired: input.legalReviewRequired || false, fileName: input.fileName || null, filePath: input.filePath || null, createdAt: new Date(), createdBy: currentUserId, updatedAt: new Date(), updatedBy: currentUserId, }); return row; }); revalidateTag("general-contract-templates"); revalidatePath("/evcp/general-contract-template"); return { data: newTemplate, error: null }; } catch (error) { console.log(error); return { data: null, error: getErrorMessage(error) }; } } // Contract Template 수정 // Basic Contract 방식과 동일한 통합 업데이트 함수 export async function updateTemplate({ id, formData }: { id: number; formData: FormData; }): Promise<{ success?: boolean; error?: string }> { unstable_noStore(); try { // 필수값 파싱 const contractTemplateType = formData.get("contractTemplateType") as string | null; const contractTemplateName = formData.get("contractTemplateName") as string | null; const legalReviewRequired = formData.get("legalReviewRequired") === "true"; // 리비전 처리: basic-contract와 동일하게 FormData에서 그대로 사용 (없으면 1) if (!contractTemplateType || !contractTemplateName) { return { error: "계약 종류와 문서명은 필수입니다." }; } // 기존 템플릿 조회 const existing = await db .select() .from(generalContractTemplates) .where(eq(generalContractTemplates.id, id)) .limit(1); if (existing.length === 0) { return { error: "템플릿을 찾을 수 없습니다." }; } const prev = existing[0] as any; // 모든 경우에 기존 레코드 업데이트 (새 리비전 생성하지 않음) // 파일 처리 const file = formData.get("file") as File | null; let fileName: string | undefined = undefined; let filePath: string | undefined = undefined; if (file) { // 1) 새 파일 저장 (DRM 해제 로직 적용) const saveResult = await saveDRMFile( file, decryptWithServerAction, 'general-contract-templates' ); if (!saveResult.success) { return { error: saveResult.error || '파일 저장에 실패했습니다.' }; } fileName = file.name; filePath = saveResult.publicPath; // 2) 기존 파일 삭제 if (prev.filePath) { const deleted = await deleteFile(prev.filePath); if (deleted) { console.log(`✅ 기존 파일 삭제됨: ${prev.filePath}`); } else { console.log(`⚠️ 기존 파일 삭제 실패: ${prev.filePath}`); } } } // 모든 경우에 기존 레코드 업데이트 (revision은 위 정책에 따라 결정) const currentUserId = await getCurrentUserId(); // 리비전 처리: FormData에 있으면 사용, 없으면 현재 리비전 + 1 const revisionFromForm = formData.get("revision")?.toString(); let nextRevision: number; if (revisionFromForm) { nextRevision = Number(revisionFromForm) || 1; } else { nextRevision = (prev.revision ?? 0) + 1; // FormData에 없으면 현재 리비전 + 1 } const updateData: Record = { contractTemplateType, contractTemplateName, legalReviewRequired, revision: nextRevision, // 최종 결정된 리비전 사용 updatedAt: new Date(), updatedBy: currentUserId, }; if (fileName && filePath) { updateData.fileName = fileName; updateData.filePath = filePath; } await db.transaction(async (tx) => { await tx .update(generalContractTemplates) .set(updateData) .where(eq(generalContractTemplates.id, id)); }); revalidateTag('general-contract-templates'); revalidatePath('/evcp/general-contract-template'); return { success: true }; } catch (error) { console.error("템플릿 업데이트 오류:", error); return { error: error instanceof Error ? error.message : "템플릿 업데이트 중 오류가 발생했습니다.", }; } } // 기존 함수는 호환성을 위해 유지 export async function updateContractTemplateById( id: number, input: UpdateContractTemplateSchema ) { unstable_noStore(); try { const currentUserId = await getCurrentUserId(); const updatedTemplate = await db.transaction(async (tx) => { const [row] = await updateContractTemplate(tx, id, input, currentUserId); return row; }); revalidateTag("general-contract-templates"); revalidatePath("/evcp/general-contract-template"); return { data: updatedTemplate, error: null }; } catch (error) { console.log(error); return { data: null, error: getErrorMessage(error) }; } } // Contract Template 삭제 export async function removeTemplates({ ids }: { ids: number[]; }): Promise<{ success?: boolean; error?: string }> { if (!ids || ids.length === 0) { return { error: "삭제할 템플릿이 선택되지 않았습니다." }; } // unstable_noStore를 최상단에 배치 unstable_noStore(); try { // 파일 삭제를 위한 템플릿 정보 조회 및 DB 삭제를 직접 트랜잭션으로 처리 // withTransaction 대신 db.transaction 직접 사용 (createContractTemplate와 일관성 유지) const templateFiles: { id: number; filePath: string }[] = []; const result = await db.transaction(async (tx) => { // 각 템플릿의 파일 경로 가져오기 for (const id of ids) { const template = await getContractTemplateByIdFromRepo(tx, id); if (template && template.filePath) { templateFiles.push({ id: template.id, filePath: template.filePath }); } } // DB에서 템플릿 삭제 const { data, error } = await deleteContractTemplates(tx, ids); if (error) { throw new Error(`템플릿 DB 삭제 실패: ${error}`); } return { data }; }); // 파일 시스템 삭제는 트랜잭션 성공 후 수행 for (const template of templateFiles) { const deleted = await deleteFile(template.filePath); if (deleted) { console.log(`✅ 파일 삭제됨: ${template.filePath}`); } else { console.log(`⚠️ 파일 삭제 실패: ${template.filePath}`); } } revalidateTag("general-contract-templates"); revalidateTag("template-status-counts"); // 디버깅을 위한 로그 console.log("캐시 무효화 완료:", ids); return { success: true }; } catch (error) { console.error("템플릿 삭제 중 오류 발생:", error); return { error: error instanceof Error ? error.message : "템플릿 삭제 중 오류가 발생했습니다." }; } } // 모든 활성 Contract Template 목록 (간단한 선택용) export async function getAllActiveContractTemplates() { unstable_noStore(); try { const templates = await db.transaction(async (tx) => { return await findAllContractTemplates(tx); }); return { data: templates, error: null }; } catch (error) { console.log(error); return { data: [], error: getErrorMessage(error) }; } }