"use server"; import { revalidateTag, 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, gte, lte, not, or, sql, eq, isNull, ne, isNotNull, count } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; import { basicContract, BasicContractTemplate, basicContractTemplates, basicContractView, vendors, type BasicContractTemplate as DBBasicContractTemplate, } from "@/db/schema"; import { GetBasicContractTemplatesSchema, CreateBasicContractTemplateSchema, GetBasciContractsSchema, } from "./validations"; import { insertBasicContractTemplate, selectBasicContractTemplates, countBasicContractTemplates, deleteBasicContractTemplates, getBasicContractTemplateById, selectBasicContracts, countBasicContracts, findAllTemplates } from "./repository"; import { revalidatePath } from 'next/cache'; import { sendEmail } from "../mail/sendEmail"; import { headers } from 'next/headers'; import { filterColumns } from "@/lib/filter-columns"; import { differenceInDays, addYears, isBefore } from "date-fns"; import { deleteFile, saveFile } from "@/lib/file-stroage"; // 템플릿 추가 export async function addTemplate( templateData: FormData | Omit ): Promise<{ success: boolean; data?: BasicContractTemplate; error?: string }> { try { // FormData인 경우 파일 추출 및 저장 처리 if (templateData instanceof FormData) { const templateName = templateData.get("templateName") as string; // 문자열을 숫자로 변환 (FormData의 get은 항상 string|File을 반환) const status = templateData.get("status") as "ACTIVE" | "INACTIVE"; const file = templateData.get("file") as File; // 유효성 검사 if (!templateName) { return { success: false, error: "템플릿 이름은 필수입니다." }; } if (!file) { return { success: false, error: "파일은 필수입니다." }; } const saveResult = await saveFile({file, directory:"basicContract/template" }); if (!saveResult.success) { return { success: false, error: saveResult.error }; } // DB에 저장할 데이터 구성 const formattedData = { templateName, status, fileName: file.name, filePath: saveResult.publicPath! }; // DB에 저장 const { data, error } = await createBasicContractTemplate(formattedData); if (error) { // DB 저장 실패 시 파일 삭제 await deleteFile(saveResult.publicPath!); return { success: false, error }; } return { success: true, data: data || undefined }; } // 기존 객체 형태인 경우 (호환성 유지) else { const formattedData = { ...templateData, status: templateData.status as "ACTIVE" | "INACTIVE", // validityPeriod가 없으면 기본값 12개월 사용 validityPeriod: templateData.validityPeriod || 12, }; const { data, error } = await createBasicContractTemplate(formattedData); if (error) { return { success: false, error }; } return { success: true, data: data || undefined }; } } catch (error) { console.error("Template add error:", error); return { success: false, error: error instanceof Error ? error.message : "템플릿 추가 중 오류가 발생했습니다." }; } } // 기본 계약서 템플릿 목록 조회 (서버 액션) export async function getBasicContractTemplates( input: GetBasicContractTemplatesSchema ) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; const { data, total } = await db.transaction(async (tx) => { // 간소화된 구현 - 실제 filterColumns 함수는 더 복잡할 수 있습니다 let whereCondition = undefined; if (input.search) { const s = `%${input.search}%`; whereCondition = or( ilike(basicContractTemplates.templateName, s), ilike(basicContractTemplates.fileName, s), ilike(basicContractTemplates.status, s) ); } const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc( // eslint-disable-next-line @typescript-eslint/no-explicit-any basicContractTemplates[ item.id as keyof typeof basicContractTemplates ] as any ) : asc( // eslint-disable-next-line @typescript-eslint/no-explicit-any basicContractTemplates[ item.id as keyof typeof basicContractTemplates ] as any ) ) : [desc(basicContractTemplates.createdAt)]; const dataResult = await selectBasicContractTemplates(tx, { where: whereCondition, orderBy, offset, limit: input.perPage, }); const totalCount = await countBasicContractTemplates( tx, whereCondition ); return { data: dataResult, total: totalCount }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (error) { console.error("getBasicContractTemplates 에러:", error); return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], { revalidate: 3600, tags: ["basic-contract-templates"], } )(); } // 템플릿 생성 (서버 액션) export async function createBasicContractTemplate(input: CreateBasicContractTemplateSchema) { unstable_noStore(); try { const newTemplate = await db.transaction(async (tx) => { const [row] = await insertBasicContractTemplate(tx, { templateName: input.templateName, revision: input.revision || 1, legalReviewRequired: input.legalReviewRequired, shipBuildingApplicable: input.shipBuildingApplicable, windApplicable: input.windApplicable, pcApplicable: input.pcApplicable, nbApplicable: input.nbApplicable, rcApplicable: input.rcApplicable, gyApplicable: input.gyApplicable, sysApplicable: input.sysApplicable, infraApplicable: input.infraApplicable, status: input.status || "ACTIVE", // 📝 null 처리 추가 fileName: input.fileName || null, filePath: input.filePath || null, }); return row; }); return { data: newTemplate, error: null }; } catch (error) { console.log(error); return { data: null, error: getErrorMessage(error) }; } } //서명 계약서 저장, 김기만 프로님 추가 코드 export const saveSignedContract = async ( fileBuffer: ArrayBuffer, templateName: string, tableRowId: number ): Promise<{ result: true } | { result: false; error: string }> => { try { const originalName = `${tableRowId}_${templateName}`; // ArrayBuffer를 File 객체로 변환 const file = new File([fileBuffer], originalName); // ✅ 서명된 계약서 저장 // 개발: /project/public/basicContract/signed/ // 프로덕션: /nas_evcp/basicContract/signed/ const saveResult = await saveFile({file,directory: "basicContract/signed" ,originalName:originalName}); if (!saveResult.success) { return { result: false, error: saveResult.error! }; } console.log(`✅ 서명된 계약서 저장됨: ${saveResult.filePath}`); await db.transaction(async (tx) => { await tx .update(basicContract) .set({ status: "COMPLETED", fileName: originalName, filePath: saveResult.publicPath, // 웹 접근 경로 저장 }) .where(eq(basicContract.id, tableRowId)); }); // 캐시 무효화 revalidateTag("basic-contract-requests"); revalidateTag("template-status-counts"); return { result: true }; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류"; return { result: false, error: errorMessage }; } }; interface RemoveTemplatesProps { ids: number[]; } interface TemplateFile { id: number; filePath: string; } export async function removeTemplates({ ids }: RemoveTemplatesProps): Promise<{ success?: boolean; error?: string }> { if (!ids || ids.length === 0) { return { error: "삭제할 템플릿이 선택되지 않았습니다." }; } // unstable_noStore를 최상단에 배치 unstable_noStore(); try { // 파일 삭제를 위한 템플릿 정보 조회 및 DB 삭제를 직접 트랜잭션으로 처리 // withTransaction 대신 db.transaction 직접 사용 (createBasicContractTemplate와 일관성 유지) const templateFiles: TemplateFile[] = []; const result = await db.transaction(async (tx) => { // 각 템플릿의 파일 경로 가져오기 for (const id of ids) { const { data: template, error } = await getBasicContractTemplateById(tx, id); if (template && template.filePath) { templateFiles.push({ id: template.id, filePath: template.filePath }); } } // DB에서 템플릿 삭제 const { data, error } = await deleteBasicContractTemplates(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("basic-contract-templates"); revalidateTag("template-status-counts"); // 디버깅을 위한 로그 console.log("캐시 무효화 완료:", ids); return { success: true }; } catch (error) { console.error("템플릿 삭제 중 오류 발생:", error); return { error: error instanceof Error ? error.message : "템플릿 삭제 중 오류가 발생했습니다." }; } } interface UpdateTemplateParams { id: number; formData: FormData; } const SCOPE_KEYS = [ "shipBuildingApplicable", "windApplicable", "pcApplicable", "nbApplicable", "rcApplicable", "gyApplicable", "sysApplicable", "infraApplicable", ] as const; function getBool(fd: FormData, key: string, defaultValue = false) { const v = fd.get(key); if (v === null) return defaultValue; return v === "true"; } export async function updateTemplate({ id, formData }: UpdateTemplateParams): Promise<{ success?: boolean; error?: string }> { unstable_noStore(); try { // 필수값 const templateName = formData.get("templateName") as string | null; if (!templateName) { return { error: "템플릿 이름은 필수입니다." }; } // 선택/추가 필드 파싱 const revisionStr = formData.get("revision")?.toString() ?? "1"; const revision = Number(revisionStr) || 1; const legalReviewRequired = getBool(formData, "legalReviewRequired", false); // status는 프런트에서 ACTIVE만 넣고 있으나, 없으면 기존값 유지 or 기본값 설정 const status = (formData.get("status") as "ACTIVE" | "INACTIVE" | null) ?? "ACTIVE"; // validityPeriod가 이제 필요없다면 제거하시고, 사용한다면 파싱 그대로 const validityPeriodStr = formData.get("validityPeriod")?.toString(); const validityPeriod = validityPeriodStr ? Number(validityPeriodStr) : undefined; // Scope booleans const scopeData: Record = {}; for (const key of SCOPE_KEYS) { scopeData[key] = getBool(formData, key, false); } // 파일 처리 const file = formData.get("file") as File | null; let fileName: string | undefined = undefined; let filePath: string | undefined = undefined; if (file) { // 1) 새 파일 저장 const saveResult = await saveFile({ file, directory: "basicContract/template" }); if (!saveResult.success) { return { success: false, error: saveResult.error }; } fileName = file.name; filePath = saveResult.publicPath; // 2) 기존 파일 삭제 const existingTemplate = await db.query.basicContractTemplates.findFirst({ where: eq(basicContractTemplates.id, id), }); if (existingTemplate?.filePath) { const deleted = await deleteFile(existingTemplate.filePath); if (deleted) { console.log(`✅ 기존 파일 삭제됨: ${existingTemplate.filePath}`); } else { console.log(`⚠️ 기존 파일 삭제 실패: ${existingTemplate.filePath}`); } } } // 업데이트할 데이터 구성 const updateData: Record = { templateName, revision, legalReviewRequired, status, updatedAt: new Date(), ...scopeData, }; if (validityPeriod !== undefined) { updateData.validityPeriod = validityPeriod; } if (fileName && filePath) { updateData.fileName = fileName; updateData.filePath = filePath; } // DB 업데이트 await db.transaction(async (tx) => { await tx .update(basicContractTemplates) .set(updateData) .where(eq(basicContractTemplates.id, id)); }); // 캐시 무효화 revalidateTag("basic-contract-templates"); revalidateTag("template-status-counts"); revalidateTag("templates"); return { success: true }; } catch (error) { console.error("템플릿 업데이트 오류:", error); return { error: error instanceof Error ? error.message : "템플릿 업데이트 중 오류가 발생했습니다.", }; } } interface RequestBasicContractInfoProps { vendorIds: number[]; requestedBy: number; templateId: number; } export async function requestBasicContractInfo({ vendorIds, requestedBy, templateId }: RequestBasicContractInfoProps): Promise<{ success?: boolean; error?: string }> { unstable_noStore(); if (!vendorIds || vendorIds.length === 0) { return { error: "요청할 협력업체가 선택되지 않았습니다." }; } if (!templateId) { return { error: "계약서 템플릿이 선택되지 않았습니다." }; } try { // 1. 선택된 템플릿 정보 가져오기 const template = await db.query.basicContractTemplates.findFirst({ where: eq(basicContractTemplates.id, templateId) }); if (!template) { return { error: "선택한 템플릿을 찾을 수 없습니다." }; } // 2. 협력업체 정보 가져오기 const vendorList = await db .select() .from(vendors) .where(inArray(vendors.id, vendorIds)); if (!vendorList || vendorList.length === 0) { return { error: "선택한 협력업체 정보를 찾을 수 없습니다." }; } // 3. 각 협력업체에 대해 기본계약 레코드 생성 및 이메일 발송 const results = await Promise.all( vendorList.map(async (vendor) => { if (!vendor.email) return; // 이메일이 없으면 스킵 try { // 3-1. basic_contract 테이블에 레코드 추가 const [newContract] = await db .insert(basicContract) .values({ templateId: template.id, vendorId: vendor.id, requestedBy: requestedBy, status: "PENDING", fileName: template.fileName, // 템플릿 파일 이름 사용 filePath: template.filePath, // 템플릿 파일 경로 사용 }) .returning(); // 3-2. 협력업체에 이메일 발송 const subject = `[${process.env.COMPANY_NAME || '회사명'}] 기본계약서 서명 요청`; const headersList = await headers(); const host = headersList.get('host') || 'localhost:3000'; // 로그인 또는 서명 페이지 URL 생성 const baseUrl = `http://${host}` const loginUrl = `${baseUrl}/partners/basic-contract`; // 사용자 언어 설정 (기본값은 한국어) const userLang = "ko"; // 이메일 발송 await sendEmail({ to: vendor.email, subject, template: "contract-sign-request", // 이메일 템플릿 이름 context: { vendorName: vendor.vendorName, contractId: newContract.id, templateName: template.templateName, loginUrl, language: userLang, }, }); return { vendorId: vendor.id, success: true }; } catch (err) { console.error(`협력업체 ${vendor.id} 처리 중 오류:`, err); return { vendorId: vendor.id, success: false, error: getErrorMessage(err) }; } }) ); // 4. 실패한 케이스가 있는지 확인 const failedVendors = results.filter(r => r && !r.success); if (failedVendors.length > 0) { console.error("일부 협력업체 처리 실패:", failedVendors); if (failedVendors.length === vendorIds.length) { // 모든 협력업체 처리 실패 return { error: "모든 협력업체에 대한 처리가 실패했습니다." }; } else { // 일부 협력업체만 처리 실패 return { success: true, error: `${results.length - failedVendors.length}개 협력업체 처리 성공, ${failedVendors.length}개 처리 실패` }; } } // 5. 캐시 무효화 revalidateTag("basic-contract-requests"); return { success: true }; } catch (error) { console.error("기본계약서 요청 중 오류 발생:", error); return { error: error instanceof Error ? error.message : "기본계약서 요청 처리 중 오류가 발생했습니다." }; } } export async function getBasicContracts(input: GetBasciContractsSchema) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // const advancedTable = input.flags.includes("advancedTable"); const advancedTable = true; // advancedTable 모드면 filterColumns()로 where 절 구성 const advancedWhere = filterColumns({ table: basicContractView, filters: input.filters, joinOperator: input.joinOperator, }); let globalWhere if (input.search) { const s = `%${input.search}%` globalWhere = or(ilike(basicContractView.templateName, s), ilike(basicContractView.vendorName, s) , ilike(basicContractView.vendorCode, s) , ilike(basicContractView.vendorEmail, s) , ilike(basicContractView.status, s) ) // 필요시 여러 칼럼 OR조건 (status, priority, etc) } const finalWhere = and( // advancedWhere or your existing conditions advancedWhere, globalWhere // and()함수로 결합 or or() 등으로 결합 ) // 아니면 ilike, inArray, gte 등으로 where 절 구성 const where = finalWhere const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(basicContractView[item.id]) : asc(basicContractView[item.id]) ) : [asc(basicContractView.createdAt)]; // 트랜잭션 내부에서 Repository 호출 const { data, total } = await db.transaction(async (tx) => { const data = await selectBasicContracts(tx, { where, orderBy, offset, limit: input.perPage, }); const total = await countBasicContracts(tx, where); return { data, total }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { // 에러 발생 시 디폴트 return { data: [], pageCount: 0 }; } }, [JSON.stringify(input)], // 캐싱 키 { revalidate: 3600, tags: ["basicContractView"], // revalidateTag("basicContractView") 호출 시 무효화 } )(); } export async function getBasicContractsByVendorId( input: GetBasciContractsSchema, vendorId: number ) { return unstable_cache( async () => { try { const offset = (input.page - 1) * input.perPage; // const advancedTable = input.flags.includes("advancedTable"); const advancedTable = true; // advancedTable 모드면 filterColumns()로 where 절 구성 const advancedWhere = filterColumns({ table: basicContractView, filters: input.filters, joinOperator: input.joinOperator, }); let globalWhere; if (input.search) { const s = `%${input.search}%`; globalWhere = or( ilike(basicContractView.templateName, s), ilike(basicContractView.vendorName, s), ilike(basicContractView.vendorCode, s), ilike(basicContractView.vendorEmail, s), ilike(basicContractView.status, s) ); // 필요시 여러 칼럼 OR조건 (status, priority, etc) } // 벤더 ID 필터링 조건 추가 const vendorCondition = eq(basicContractView.vendorId, vendorId); const finalWhere = and( // 항상 벤더 ID 조건을 포함 vendorCondition, // 기존 조건들 advancedWhere, globalWhere ); const where = finalWhere; const orderBy = input.sort.length > 0 ? input.sort.map((item) => item.desc ? desc(basicContractView[item.id]) : asc(basicContractView[item.id]) ) : [asc(basicContractView.createdAt)]; // 트랜잭션 내부에서 Repository 호출 const { data, total } = await db.transaction(async (tx) => { const data = await selectBasicContracts(tx, { where, orderBy, offset, limit: input.perPage, }); const total = await countBasicContracts(tx, where); return { data, total }; }); const pageCount = Math.ceil(total / input.perPage); return { data, pageCount }; } catch (err) { // 에러 발생 시 디폴트 return { data: [], pageCount: 0 }; } }, [JSON.stringify(input), String(vendorId)], // 캐싱 키에 vendorId 추가 { revalidate: 3600, tags: ["basicContractView-vendor"], // revalidateTag("basicContractView") 호출 시 무효화 } )(); } export async function getAllTemplates(): Promise { try { return await findAllTemplates(); } catch (err) { throw new Error("Failed to get templates"); } } interface VendorTemplateStatus { vendorId: number; vendorName: string; templateId: number; templateName: string; status: string; createdAt: Date; isExpired: boolean; // 요청이 오래되었는지 (예: 30일 이상) isUpdated: boolean; // 템플릿이 업데이트되었는지 } /** * 협력업체와 템플릿 조합에 대한 계약 요청 상태를 확인합니다. */ // 계약 상태 확인 API 함수 export async function checkContractRequestStatus( vendorIds: number[], templateIds: number[] ) { try { // 각 협력업체-템플릿 조합에 대한 최신 계약 요청 상태 확인 const requests = await db .select({ id: basicContract.id, vendorId: basicContract.vendorId, templateId: basicContract.templateId, status: basicContract.status, createdAt: basicContract.createdAt, updatedAt: basicContract.updatedAt, // completedAt 필드 추가 필요 completedAt: basicContract.completedAt, // 계약 완료 날짜 }) .from(basicContract) .where( and( inArray(basicContract.vendorId, vendorIds), inArray(basicContract.templateId, templateIds) ) ) .orderBy(desc(basicContract.createdAt)); // 협력업체 정보 가져오기 const vendorData = await db .select({ id: vendors.id, vendorName: vendors.vendorName, }) .from(vendors) .where(inArray(vendors.id, vendorIds)); // 템플릿 정보 가져오기 const templateData = await db .select({ id: basicContractTemplates.id, templateName: basicContractTemplates.templateName, updatedAt: basicContractTemplates.updatedAt, validityPeriod: basicContractTemplates.validityPeriod, // 템플릿별 유효기간(개월) }) .from(basicContractTemplates) .where(inArray(basicContractTemplates.id, templateIds)); // 데이터 가공 - 협력업체별, 템플릿별로 상태 매핑 const vendorMap = new Map(vendorData.map(v => [v.id, v])); const templateMap = new Map(templateData.map(t => [t.id, t])); const uniqueRequests = new Map(); // 각 협력업체-템플릿 조합에 대해 가장 최근 요청만 사용 requests.forEach(req => { const key = `${req.vendorId}-${req.templateId}`; if (!uniqueRequests.has(key)) { uniqueRequests.set(key, req); } }); // 인터페이스를 임포트하거나 이 함수 내에서/위에서 재정의 interface VendorTemplateStatus { vendorId: number; vendorName: string; templateId: number; templateName: string; status: string; createdAt: Date; completedAt?: Date; isExpired: boolean; isUpdated: boolean; isContractExpired: boolean; } // 명시적 타입 지정 const statusData: VendorTemplateStatus[] = []; // 요청 만료 기준 - 30일 const REQUEST_EXPIRATION_DAYS = 30; // 기본 계약 유효기간 - 12개월 (템플릿별로 다르게 설정 가능) const DEFAULT_CONTRACT_VALIDITY_MONTHS = 12; const now = new Date(); // 모든 협력업체-템플릿 조합에 대해 상태 확인 vendorIds.forEach(vendorId => { templateIds.forEach(templateId => { const key = `${vendorId}-${templateId}`; const request = uniqueRequests.get(key); const vendor = vendorMap.get(vendorId); const template = templateMap.get(templateId); if (!vendor || !template) return; let status = "NONE"; // 기본 상태: 요청 없음 let createdAt = new Date(); let completedAt = null; let isExpired = false; let isUpdated = false; let isContractExpired = false; if (request) { status = request.status; createdAt = request.createdAt; completedAt = request.completedAt; // 요청이 오래되었는지 확인 (PENDING 상태일 때만 적용) if (status === "PENDING") { isExpired = differenceInDays(now, createdAt) > REQUEST_EXPIRATION_DAYS; } // 요청 이후 템플릿이 업데이트되었는지 확인 if (template.updatedAt && request.createdAt) { isUpdated = template.updatedAt > request.createdAt; } // 계약 유효기간 만료 확인 (COMPLETED 상태이고 completedAt이 있는 경우) if (status === "COMPLETED" && completedAt) { // 템플릿별 유효기간 또는 기본값 사용 const validityMonths = template.validityPeriod || DEFAULT_CONTRACT_VALIDITY_MONTHS; // 계약 만료일 계산 (완료일 + 유효기간) const expiryDate = addYears(completedAt, validityMonths / 12); // 현재 날짜가 만료일 이후인지 확인 isContractExpired = isBefore(expiryDate, now); } } statusData.push({ vendorId, vendorName: vendor.vendorName, templateId, templateName: template.templateName, status, createdAt, completedAt, isExpired, isUpdated, isContractExpired, }); }); }); return { data: statusData }; } catch (error) { console.error("계약 상태 확인 중 오류:", error); return { data: [], error: error instanceof Error ? error.message : "계약 상태 확인 중 오류가 발생했습니다." }; } } /** * ID로 기본계약서 템플릿 조회 */ export async function getBasicContractTemplateByIdService(id: string) { try { const templateId = parseInt(id); if (isNaN(templateId)) { return null; } const templates = await db .select() .from(basicContractTemplates) .where(eq(basicContractTemplates.id, templateId)) .limit(1); if (templates.length === 0) { return null; } return templates[0]; } catch (error) { console.error("템플릿 조회 오류:", error); return null; } } /** * 템플릿 파일 저장 서버 액션 */ 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(basicContractTemplates) .where(eq(basicContractTemplates.id, templateId)) .limit(1); if (existingTemplate.length === 0) { return { error: "템플릿을 찾을 수 없습니다." }; } const template = existingTemplate[0]; // 파일 저장 로직 (실제 파일 시스템에 저장) const { writeFile, mkdir } = await import("fs/promises"); const { join } = await import("path"); const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // 기존 파일 경로 사용 (덮어쓰기) const uploadPath = join(process.cwd(), "public", template.filePath.replace(/^\//, "")); // 디렉토리 확인 및 생성 const dirPath = uploadPath.substring(0, uploadPath.lastIndexOf('/')); await mkdir(dirPath, { recursive: true }); // 파일 저장 await writeFile(uploadPath, buffer); // 데이터베이스 업데이트 (수정일시만 업데이트) await db .update(basicContractTemplates) .set({ updatedAt: new Date(), }) .where(eq(basicContractTemplates.id, templateId)); // 캐시 무효화 revalidatePath(`/evcp/basic-contract-template/${templateId}`); revalidateTag("basic-contract-templates"); return { success: true, message: "템플릿이 성공적으로 저장되었습니다." }; } catch (error) { console.error("템플릿 저장 오류:", error); return { error: "저장 중 오류가 발생했습니다." }; } } /** * 템플릿 페이지 새로고침 서버 액션 */ export async function refreshTemplatePage(templateId: string) { revalidatePath(`/evcp/basic-contract-template/${templateId}`); revalidateTag("basic-contract-templates"); } // 새 리비전 생성 함수 export async function createBasicContractTemplateRevision(input: CreateRevisionSchema) { unstable_noStore(); try { // 기본 템플릿 존재 확인 const baseTemplate = await db .select() .from(basicContractTemplates) .where(eq(basicContractTemplates.id, input.baseTemplateId)) .limit(1); if (baseTemplate.length === 0) { return { data: null, error: "기본 템플릿을 찾을 수 없습니다." }; } // 같은 템플릿 이름에 해당 리비전이 이미 존재하는지 확인 const existingRevision = await db .select() .from(basicContractTemplates) .where( and( eq(basicContractTemplates.templateName, input.templateName), eq(basicContractTemplates.revision, input.revision) ) ) .limit(1); if (existingRevision.length > 0) { return { data: null, error: `${input.templateName} v${input.revision} 리비전이 이미 존재합니다.` }; } // 새 리비전이 기존 리비전들보다 큰 번호인지 확인 const maxRevision = await db .select({ maxRev: basicContractTemplates.revision }) .from(basicContractTemplates) .where(eq(basicContractTemplates.templateName, input.templateName)) .orderBy(desc(basicContractTemplates.revision)) .limit(1); if (maxRevision.length > 0 && input.revision <= maxRevision[0].maxRev) { return { data: null, error: `새 리비전 번호는 현재 최대 리비전(v${maxRevision[0].maxRev})보다 커야 합니다.` }; } const newRevision = await db.transaction(async (tx) => { const [row] = await insertBasicContractTemplate(tx, { templateName: input.templateName, revision: input.revision, legalReviewRequired: input.legalReviewRequired, shipBuildingApplicable: input.shipBuildingApplicable, windApplicable: input.windApplicable, pcApplicable: input.pcApplicable, nbApplicable: input.nbApplicable, rcApplicable: input.rcApplicable, gyApplicable: input.gyApplicable, sysApplicable: input.sysApplicable, infraApplicable: input.infraApplicable, status: "ACTIVE", fileName: input.fileName, filePath: input.filePath, validityPeriod: null, }); return row; }); return { data: newRevision, error: null }; } catch (error) { return { data: null, error: getErrorMessage(error) }; } } // 1) 전체 basicContractTemplates 조회 export async function getALLBasicContractTemplates() { return db .select() .from(basicContractTemplates) .where(eq(basicContractTemplates.status,"ACTIVE")) .orderBy(desc(basicContractTemplates.createdAt)); } // 2) 등록된 templateName만 중복 없이 가져오기 export async function getExistingTemplateNames(): Promise { const rows = await db .select({ templateName: basicContractTemplates.templateName, }) .from(basicContractTemplates) .where(eq(basicContractTemplates.status,"ACTIVE")) .groupBy(basicContractTemplates.templateName); return rows.map((r) => r.templateName); } export async function getExistingTemplateNamesById(id:number): Promise { const rows = await db .select({ templateName: basicContractTemplates.templateName, }) .from(basicContractTemplates) .where(and(eq(basicContractTemplates.status,"ACTIVE"),eq(basicContractTemplates.id,id))) .limit(1) return rows[0].templateName; }