"use server"; import { revalidatePath, revalidateTag } from "next/cache"; import db from "@/db/db"; import { biddingProjects, NewBiddingProject } from "@/db/schema/projects"; import { plftbEstmProjMast } from "@/db/schema/NONSAP/nonsap"; import { eq, desc } from "drizzle-orm"; import { updateBiddingProjectSchema, type UpdateBiddingProjectSchema } from "./validation"; /** * NONSAP에서 견적프로젝트 데이터를 가져와서 biddingProjects 테이블에 upsert하는 서버 액션 */ export async function syncProjectsFromNonSap() { try { // 1. NONSAP에서 IF_TRGT_YN = 'Y'인 데이터를 가져오되, // 같은 ESTM_PROJ_NO에 대해 REV_NO가 가장 높은 것만 가져오기 const nonsapProjects = await db .select() .from(plftbEstmProjMast) .where(eq(plftbEstmProjMast.IF_TRGT_YN, "Y")) .orderBy(desc(plftbEstmProjMast.REV_NO)); if (nonsapProjects.length === 0) { return { success: true, message: "동기화할 프로젝트가 없습니다.", syncedCount: 0, }; } // 2. 프로젝트 번호별로 가장 높은 REV_NO만 유지 const latestProjects = new Map(); for (const project of nonsapProjects) { const existingProject = latestProjects.get(project.ESTM_PROJ_NO); if ( !existingProject || (project.REV_NO && project.REV_NO > existingProject.REV_NO) ) { latestProjects.set(project.ESTM_PROJ_NO, project); } } let syncedCount = 0; const errors: string[] = []; // 3. 각 프로젝트를 biddingProjects 형식으로 변환하고 upsert for (const nonsapProject of latestProjects.values()) { try { // pspid가 없으면 해당 프로젝트는 건너뛰기 if (!nonsapProject.ESTM_PROJ_NO?.trim()) { console.warn(`프로젝트 번호가 없는 데이터 건너뛰기:`, nonsapProject); continue; } // 컬럼 매핑 및 타입 변환 const biddingProject: NewBiddingProject = { pspid: nonsapProject.ESTM_PROJ_NO.substring(0, 8), // 해양 TOP은 8자리만 사용 projNm: nonsapProject.ESTM_PROJ_NM?.substring(0, 90) || null, // 90자 제한 sector: nonsapProject.BIZ_CLS?.substring(0, 1) || null, // 1자 제한 projMsrm: nonsapProject.SERS_CNT ? (() => { const numValue = Number(nonsapProject.SERS_CNT); return isNaN(numValue) ? null : String(Math.min(Math.max(numValue, 0), 999)); })() : null, // precision 3,0 제한 (0~999, 숫자가 아니면 null) kunnr: nonsapProject.OWNER_CD?.substring(0, 10) || null, // 10자 제한 kunnrNm: null, // NONSAP에 없음 cls1: nonsapProject.CLS_1?.substring(0, 10) || null, // 10자 제한 cls1Nm: null, // NONSAP에 없음 ptype: nonsapProject.SKND_CD?.substring(0, 3) || null, // 10 -> 3자 제한 ptypeNm: null, // NONSAP에 없음 pmodelCd: nonsapProject.SHTYPE_CD?.substring(0, 10) || null, // 10자 제한 pmodelNm: null, // NONSAP에 없음 pmodelSz: nonsapProject.SHTYPE_SIZE ? String(nonsapProject.SHTYPE_SIZE).substring(0, 20) : null, // numeric to varchar(20) pmodelUom: nonsapProject.SHTYPE_UOM?.substring(0, 5) || null, // 10 -> 5자 제한 txt04: nonsapProject.ESTM_TYPE?.substring(0, 4) || null, // 1 -> 4자 제한 txt30: null, // NONSAP에 없음 estmPm: null, // NONSAP에 없음 pjtType: "TOP", // NONSAP에서 오는 것은 해양 탑으로 고정 }; // upsert 처리 (pspid를 기준으로) const existingProject = await db.query.biddingProjects.findFirst({ where: eq(biddingProjects.pspid, biddingProject.pspid), }); if (existingProject) { // 업데이트 - 시트에서 수정 가능한 필드들은 제외하고 NONSAP 원본 데이터만 업데이트 await db .update(biddingProjects) .set({ // NONSAP 원본 데이터만 업데이트 (시트 수정 필드 제외) pspid: biddingProject.pspid, projNm: biddingProject.projNm, sector: biddingProject.sector, projMsrm: biddingProject.projMsrm, kunnr: biddingProject.kunnr, // kunnrNm: 시트에서 수정 가능하므로 제외 cls1: biddingProject.cls1, // cls1Nm: 시트에서 수정 가능하므로 제외 ptype: biddingProject.ptype, // ptypeNm: 시트에서 수정 가능하므로 제외 pmodelCd: biddingProject.pmodelCd, // pmodelNm: 시트에서 수정 가능하므로 제외 pmodelSz: biddingProject.pmodelSz, pmodelUom: biddingProject.pmodelUom, txt04: biddingProject.txt04, // txt30: 시트에서 수정 가능하므로 제외 // estmPm: 시트에서 수정 가능하므로 제외 pjtType: biddingProject.pjtType, updatedAt: new Date(), }) .where(eq(biddingProjects.pspid, biddingProject.pspid)); } else { // 신규 등록 await db.insert(biddingProjects).values(biddingProject); } syncedCount++; } catch (error) { const errorMsg = `프로젝트 ${nonsapProject.ESTM_PROJ_NO} 동기화 실패: ${ error instanceof Error ? error.message : "알 수 없는 오류" }`; errors.push(errorMsg); console.error(errorMsg, error); } } // 4. 관련 페이지 재검증 (공격적으로) revalidatePath("/ko/evcp/bid-projects"); // 국제화 경로 revalidatePath("/en/evcp/bid-projects"); // 영어 경로 revalidatePath("/ko/evcp/vendor-info/projects"); // 국제화 기존 경로 revalidatePath("/en/evcp/vendor-info/projects"); // 영어 기존 경로 // 관련 태그들 재검증 revalidateTag("bidding-projects"); revalidateTag("projects"); revalidateTag("bid-projects-list"); revalidateTag("nonsap-projects"); revalidateTag("vendor-info"); return { success: true, message: `${syncedCount}개 프로젝트가 성공적으로 동기화되었습니다.${ errors.length > 0 ? ` (${errors.length}개 오류 발생)` : "" }`, syncedCount, errors: errors.length > 0 ? errors : undefined, }; } catch (error) { console.error("NONSAP 프로젝트 동기화 오류:", error); return { success: false, error: error instanceof Error ? error.message : "프로젝트 동기화에 실패했습니다.", }; } } /** * 견적 프로젝트 정보를 업데이트하는 서버 액션 * @param input 업데이트할 프로젝트 데이터 */ export async function updateBiddingProject(input: UpdateBiddingProjectSchema) { try { // 입력 데이터 검증 const validatedInput = updateBiddingProjectSchema.parse(input); // 프로젝트 존재 여부 확인 const existingProject = await db.query.biddingProjects.findFirst({ where: eq(biddingProjects.id, validatedInput.id), }); if (!existingProject) { return { success: false, error: "프로젝트를 찾을 수 없습니다.", }; } // TOP 타입만 수정 가능하도록 제한 if (existingProject.pjtType !== "TOP") { return { success: false, error: "해양 TOP 프로젝트만 수정할 수 있습니다.", }; } // 업데이트 실행 await db .update(biddingProjects) .set({ projNm: validatedInput.projNm, kunnrNm: validatedInput.kunnrNm, cls1Nm: validatedInput.cls1Nm, ptypeNm: validatedInput.ptypeNm, pmodelNm: validatedInput.pmodelNm, pmodelSz: validatedInput.pmodelSz, txt30: validatedInput.txt30, estmPm: validatedInput.estmPm, updatedAt: new Date(), }) .where(eq(biddingProjects.id, validatedInput.id)); // 관련 페이지 재검증 (공격적으로) revalidatePath("/evcp/bid-projects"); // 해당 페이지 revalidatePath("/evcp"); // 상위 경로 revalidatePath("/"); // 루트 경로 revalidatePath("/ko/evcp/bid-projects"); // 국제화 경로 revalidatePath("/en/evcp/bid-projects"); // 영어 경로 // 관련 태그들 재검증 revalidateTag("bidding-projects"); revalidateTag("projects"); revalidateTag("bid-projects-list"); return { success: true, message: "프로젝트가 성공적으로 업데이트되었습니다.", }; } catch (error) { console.error("프로젝트 업데이트 오류:", error); return { success: false, error: error instanceof Error ? error.message : "프로젝트 업데이트에 실패했습니다.", }; } }