"use server"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import db from "@/db/db"; import { vendors, users, contracts, projects } from "@/db/schema"; import { eq } from "drizzle-orm"; const DOLCE_API_URL = process.env.DOLCE_API_URL || "http://60.100.99.217:1111"; // ============================================================================ // 타입 정의 // ============================================================================ // B3 벤더 도면 아이템 export interface DwgReceiptItem { AppDwg_PlanDate: string; AppDwg_ResultDate: string; CreateDt: string; CreateUserENM: string | null; CreateUserId: string | null; CreateUserNo: string; Discipline: string; DrawingKind: string; DrawingMoveGbn: string; DrawingName: string; DrawingNo: string; Manager: string; ManagerENM: string; ManagerNo: string; ProjectNo: string; RegisterGroup: number; RegisterGroupId: number; VendorCode: string; VendorName: string; WorDwg_PlanDate: string; WorDwg_ResultDate: string; } // B4 (GTT) 벤더 도면 아이템 export interface GttDwgReceiptItem { CGbn: string | null; CreateDt: string; CreateUserENM: string; CreateUserId: string; CreateUserNo: string; DGbn: string | null; DegreeGbn: string | null; DeptGbn: string | null; Discipline: string; DrawingKind: string; DrawingMoveGbn: string; // "도면제출" 또는 "도면입수" DrawingName: string; DrawingNo: string; GTTInput_PlanDate: string | null; GTTInput_ResultDate: string | null; GTTPreDwg_PlanDate: string | null; GTTPreDwg_ResultDate: string | null; GTTWorkingDwg_PlanDate: string | null; GTTWorkingDwg_ResultDate: string | null; JGbn: string | null; Manager: string; ManagerENM: string; ManagerNo: string; ProjectNo: string; RegisterGroup: number; RegisterGroupId: number; SGbn: string | null; SHIDrawingNo: string | null; // Added ENM fields CategoryENM?: string; DrawingUsageENM?: string; RegisterKindENM?: string; } // 통합 도면 아이템 타입 export type UnifiedDwgReceiptItem = DwgReceiptItem | GttDwgReceiptItem; // 타입 가드는 클라이언트에서 직접 DrawingKind === "B4" 체크로 대체 // (서버 액션 export 함수는 반드시 async여야 하므로 타입 가드는 export 불가) export interface DetailDwgReceiptItem { Category: string; CategoryENM: string; CategoryNM: string; CreateDt: string; CreateUserENM: string; CreateUserId: string; CreateUserNM: string; Discipline: string; DrawingKind: string; DrawingName: string; DrawingNo: string; DrawingRevNo: string; DrawingUsage: string; DrawingUsageENM: string | null; DrawingUsageNM: string; Manager: string; Mode: string | null; OFDC_NO: string; ProjectNo: string; Receiver: string | null; RegCompanyCode: string | null; RegCompanyENM: string | null; RegCompanyNM: string | null; RegisterDesc: string; RegisterGroup: number; RegisterGroupId: number; RegisterId: number; RegisterKind: string; RegisterKindENM: string | null; RegisterKindNM: string; RegisterSerialNo: number; SHINote: string | null; Status: string; UploadId: string; } export interface FileInfoItem { CreateDt: string; CreateUserId: string; Deleted: string; FileDescription: string; FileId: string; FileName: string; FileRelativePath: string; FileSeq: string; FileServerId: string; FileSize: string; FileTitle: string | null; FileWriteDT: string; OwnerUserId: string; SourceDrmYn: string | null; SystemId: string; TableName: string; UploadId: string; UseYn: string; } export interface DetailDwgEditRequest { Mode: "ADD" | "MOD" | "DEL"; Status: string; RegisterId: number; ProjectNo: string; Discipline: string; DrawingKind: string; DrawingNo: string; DrawingName: string; RegisterGroupId: number; RegisterSerialNo: number; RegisterKind: string; DrawingRevNo: string | null; Category: string; Receiver: string | null; Manager: string; RegisterDesc: string; UploadId: string; RegCompanyCode: string; } // ============================================================================ // 유틸리티 함수 // ============================================================================ export async function dolceApiCall(endpoint: string, body: Record): Promise { const url = `${DOLCE_API_URL}/Services/VDCSWebService.svc/${endpoint}`; console.log(`[DOLCE API] Calling ${endpoint}:`, JSON.stringify(body, null, 2)); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`DOLCE API error (${endpoint}): ${response.status} ${response.statusText}`); } const data = await response.json(); console.log(`[DOLCE API] Response from ${endpoint}:`, JSON.stringify(data, null, 2)); return data; } // ============================================================================ // 서버 액션 // ============================================================================ /** * 1. 도면 리스트 조회 (B3 및 B4 통합) */ export async function fetchDwgReceiptList(params: { project: string; drawingKind: string; drawingMoveGbn?: string; drawingNo?: string; drawingName?: string; drawingVendor?: string; discipline?: string; }): Promise { try { const response = await dolceApiCall<{ DwgReceiptMgmtResult: { FMEADwgList: unknown[]; GTTDwgList: GttDwgReceiptItem[]; VendorDwgList: DwgReceiptItem[]; }; }>("DwgReceiptMgmt", { project: params.project, drawingKind: params.drawingKind, drawingMoveGbn: params.drawingMoveGbn || "", drawingNo: params.drawingNo || "", drawingName: params.drawingName || "", drawingVendor: params.drawingVendor || "", discipline: params.discipline || "", }); // B4(GTT)인 경우 GTTDwgList 반환, B3인 경우 VendorDwgList 반환 if (params.drawingKind === "B4") { return response.DwgReceiptMgmtResult.GTTDwgList; } else { return response.DwgReceiptMgmtResult.VendorDwgList; } } catch (error) { console.error("도면 리스트 조회 실패:", error); throw error; } } /** * 2. 상세도면 리스트 조회 */ export async function fetchDetailDwgReceiptList(params: { project: string; drawingNo: string; discipline: string; drawingKind: string; userId?: string; }): Promise { try { const response = await dolceApiCall<{ DetailDwgReceiptMgmtResult: DetailDwgReceiptItem[]; }>("DetailDwgReceiptMgmt", { project: params.project, drawingNo: params.drawingNo, discipline: params.discipline, drawingKind: params.drawingKind, userId: params.userId || "", }); return response.DetailDwgReceiptMgmtResult; } catch (error) { console.error("상세도면 리스트 조회 실패:", error); throw error; } } /** * 3. 개별 상세도면 파일 리스트 조회 */ export async function fetchFileInfoList(uploadId: string): Promise { try { const response = await dolceApiCall<{ FileInfoListResult: FileInfoItem[]; }>("FileInfoList", { uploadId, }); // UseYn이 "True"인 파일만 반환 return response.FileInfoListResult.filter((file) => file.UseYn === "True"); } catch (error) { console.error("파일 리스트 조회 실패:", error); throw error; } } /** * 4. 상세도면 추가/수정 * * 참고: DetailDwgReceiptMmgtEditResult는 실제 성공 건수를 정확히 반영하지 않음 * (1개 추가되어도 0을 반환하는 경우 있음) * API 호출이 성공하면 요청한 건수가 처리된 것으로 간주 */ export async function editDetailDwgReceipt(params: { dwgList: DetailDwgEditRequest[]; userId: string; userNm: string; vendorCode: string; email: string; }): Promise { try { const response = await dolceApiCall<{ DetailDwgReceiptMmgtEditResult: number; }>("DetailDwgReceiptMgmtEdit", { DwgList: params.dwgList, UserID: params.userId, UserNM: params.userNm, VENDORCODE: params.vendorCode, EMAIL: params.email, }); // 응답값이 신뢰할 수 없으므로 로그만 남김 if (response.DetailDwgReceiptMmgtEditResult !== params.dwgList.length) { console.warn( `[DOLCE API] DetailDwgReceiptMmgtEditResult 불일치: 요청=${params.dwgList.length}, 응답=${response.DetailDwgReceiptMmgtEditResult}` ); } // API 호출 성공 시 요청한 건수 반환 (응답값 무시) return params.dwgList.length; } catch (error) { console.error("상세도면 수정 실패:", error); throw error; } } /** * 5. 파일 다운로드 (프록시) */ export async function downloadDolceFile(params: { fileId: string; userId: string; fileName: string; }): Promise<{ blob: Blob; fileName: string }> { try { const { createDolceDownloadKey } = await import("./crypto-utils"); // DES 암호화된 키 생성 const encryptedKey = createDolceDownloadKey( params.fileId, params.userId, params.fileName ); const url = `${DOLCE_API_URL}/Download.aspx`; // 디버깅용 상세 로그 const plainText = `${params.fileId}↔${params.userId}↔${params.fileName}`; console.log("[DOLCE] 파일 다운로드 상세:", { fileId: params.fileId, userId: params.userId, fileName: params.fileName, plainText, encryptedKey, fullUrl: `${url}?key=${encryptedKey}`, }); // 주의: encodeURIComponent 사용하지 않음! // C#에서는 +를 |||로 변환 후 그대로 URL에 사용 const response = await fetch(`${url}?key=${encryptedKey}`, { method: "GET", }); console.log("[DOLCE] 응답 상태:", response.status, response.statusText); console.log("[DOLCE] Content-Type:", response.headers.get("content-type")); if (!response.ok) { const errorText = await response.text(); console.error("[DOLCE] 에러 응답:", errorText); throw new Error(`File download failed: ${response.status} - ${errorText}`); } // HTML/텍스트 응답인 경우 에러일 수 있음 const contentType = response.headers.get("content-type") || ""; if (contentType.includes("text") || contentType.includes("html")) { const errorText = await response.text(); console.error("[DOLCE] 텍스트 응답 (에러):", errorText); throw new Error(`Unexpected response: ${errorText.substring(0, 200)}`); } const blob = await response.blob(); console.log("[DOLCE] 다운로드 성공:", blob.size, "bytes"); return { blob, fileName: params.fileName, }; } catch (error) { console.error("[DOLCE] 파일 다운로드 실패:", error); throw error; } } /** * 벤더 세션 정보 조회 */ export async function getVendorSessionInfo() { try { const session = await getServerSession(authOptions); if (!session?.user) { throw new Error("Login required"); } // DB에서 사용자 정보 조회 const userId = parseInt(session.user.id); const userInfo = await db.query.users.findFirst({ where: eq(users.id, userId), columns: { id: true, name: true, email: true, companyId: true, }, }); if (!userInfo || !userInfo.companyId) { throw new Error("Vendor information not found"); } // 벤더 정보 조회 const vendorInfo = await db.query.vendors.findFirst({ where: eq(vendors.id, userInfo.companyId), columns: { id: true, vendorCode: true, vendorName: true, }, }); if (!vendorInfo) { throw new Error("Vendor information not found"); } // GTT 벤더 확인 (A0016193) const drawingKind = vendorInfo.vendorCode === "A0016193" ? "B4" : "B3"; return { userId: String(userInfo.id), userName: userInfo.name || "", email: userInfo.email || "", vendorCode: vendorInfo.vendorCode || "", vendorName: vendorInfo.vendorName || "", drawingKind, // B3 또는 B4 }; } catch (error) { console.error("세션 정보 조회 실패:", error); throw error; } } /** * 벤더의 프로젝트 목록 조회 (계약이 있는 프로젝트) */ export async function fetchVendorProjects(): Promise< Array<{ code: string; name: string }> > { try { const vendorInfo = await getVendorSessionInfo(); // 벤더와 계약이 있는 프로젝트 목록 조회 const projectList = await db .selectDistinct({ code: projects.code, name: projects.name, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .innerJoin(vendors, eq(contracts.vendorId, vendors.id)) .where(eq(vendors.vendorCode, vendorInfo.vendorCode)); return projectList.map((p) => ({ code: p.code || "", name: p.name || "", })); } catch (error) { console.error("프로젝트 목록 조회 실패:", error); throw error; } } /** * 6. 파일 삭제 (FileInfoDeleteEdit) */ export async function deleteFileInfo(params: { fileId: string; uploadId: string; }): Promise { try { const response = await dolceApiCall<{ FileInfoDeleteEditResult: number; }>("FileInfoDeleteEdit", { FileId: params.fileId, UploadId: params.uploadId, }); return response.FileInfoDeleteEditResult; } catch (error) { console.error("파일 삭제 실패:", error); throw error; } } // ============================================================================ // B4 일괄 업로드 관련 // ============================================================================ /** * MatchBatchFileDwg 매핑 체크 아이템 */ export interface MappingCheckItem { DrawingNo: string; RevNo: string; FileNm: string; } /** * MatchBatchFileDwg 응답 아이템 */ export interface MappingCheckResult { CGbn: string | null; Category: string | null; CheckBox: string; CreateDt: string | null; CreateUserId: string | null; DGbn: string | null; DegreeGbn: string | null; DeptGbn: string | null; Discipline: string | null; DrawingKind: string; DrawingMoveGbn: string | null; DrawingName: string | null; DrawingNo: string; DrawingUsage: string | null; FileNm: string; JGbn: string | null; Manager: string | null; MappingYN: "Y" | "N"; NewOrNot: string | null; ProjectNo: string; RegisterGroup: number; RegisterGroupId: number; RegisterKindCode: string | null; RegisterSerialNo: number; RevNo: string | null; SGbn: string | null; Status: string | null; UploadId: string | null; } /** * B4 일괄 업로드 결과 */ export interface B4BulkUploadResult { success: boolean; successCount?: number; failCount?: number; error?: string; results?: Array<{ drawingNo: string; revNo: string; fileName: string; success: boolean; error?: string; }>; } /** * B4 파일 매핑 상태 확인 (MatchBatchFileDwg) */ export async function checkB4MappingStatus( projectNo: string, fileList: MappingCheckItem[] ): Promise { try { const response = await dolceApiCall<{ MatchBatchFileDwgResult: MappingCheckResult[]; }>("MatchBatchFileDwg", { ProjectNo: projectNo, DrawingKind: "B4", drawingMoveGbn: "도면입수", FileList: fileList.map((item) => ({ ProjectNo: projectNo, DrawingKind: "B4", DrawingNo: item.DrawingNo, RevNo: item.RevNo, FileNm: item.FileNm, })), }); return response.MatchBatchFileDwgResult; } catch (error) { console.error("매핑 상태 확인 실패:", error); throw error; } } /** * 상세도면에 파일 업로드 결과 */ export interface UploadFilesResult { success: boolean; uploadedCount?: number; error?: string; } /** * 상세도면에 파일 업로드 (기존 UploadId에 파일 추가) */ export async function uploadFilesToDetailDrawing( formData: FormData ): Promise { try { const uploadId = formData.get("uploadId") as string; const userId = formData.get("userId") as string; const fileCount = parseInt(formData.get("fileCount") as string); if (!uploadId || !userId || !fileCount) { throw new Error("Required parameters are missing"); } const uploadResults: Array<{ FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; FileWriteDT: string; OwnerUserId: string; }> = []; // 기존 파일 개수 조회 (FileSeq를 이어서 사용하기 위함) const existingFiles = await dolceApiCall<{ FileInfoListResult: Array<{ FileSeq: string }>; }>("FileInfoList", { uploadId: uploadId, }); const startSeq = existingFiles.FileInfoListResult.length + 1; // 각 파일 업로드 for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; if (!file) continue; const fileId = crypto.randomUUID(); // 파일을 ArrayBuffer로 변환 const arrayBuffer = await file.arrayBuffer(); // PWPUploadService.ashx 호출 const uploadUrl = `${DOLCE_API_URL}/PWPUploadService.ashx?UploadId=${uploadId}&FileId=${fileId}`; const uploadResponse = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": "application/octet-stream", }, body: arrayBuffer, }); if (!uploadResponse.ok) { throw new Error( `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}` ); } const fileRelativePath = await uploadResponse.text(); uploadResults.push({ FileId: fileId, UploadId: uploadId, FileSeq: startSeq + i, FileName: file.name, FileRelativePath: fileRelativePath, FileSize: file.size, FileCreateDT: new Date().toISOString(), FileWriteDT: new Date().toISOString(), OwnerUserId: userId, }); } // 업로드 완료 통지 (PWPUploadResultService.ashx) const resultServiceUrl = `${DOLCE_API_URL}/PWPUploadResultService.ashx?`; const resultResponse = await fetch(resultServiceUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(uploadResults), }); if (!resultResponse.ok) { throw new Error( `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}` ); } const resultText = await resultResponse.text(); if (resultText !== "Success") { throw new Error(`Upload notification failed: ${resultText}`); } return { success: true, uploadedCount: fileCount, }; } catch (error) { console.error("파일 업로드 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * B4 매핑 정보 일괄 저장 (MatchBatchFileDwgEdit) */ export interface B4MappingSaveItem { CGbn: string | null; Category: string | null; CheckBox: string; DGbn: string | null; DegreeGbn: string | null; DeptGbn: string | null; Discipline: string | null; DrawingKind: string; DrawingMoveGbn: string | null; DrawingName: string | null; DrawingNo: string; DrawingUsage: string | null; FileNm: string; JGbn: string | null; Manager: string | null; MappingYN: "Y" | "N"; NewOrNot: string | null; ProjectNo: string; RegisterGroup: number; RegisterGroupId: number; RegisterKindCode: string | null; RegisterSerialNo: number; RevNo: string | null; SGbn: string | null; UploadId: string; } export async function saveB4MappingBatch( mappingSaveLists: B4MappingSaveItem[], userInfo: { userId: string; userName: string; vendorCode: string; email: string; } ): Promise { try { const response = await dolceApiCall<{ MatchBatchFileDwgEditResult: number; }>("MatchBatchFileDwgEdit", { mappingSaveLists, UserID: userInfo.userId, UserNM: userInfo.userName, VENDORCODE: userInfo.vendorCode, EMAIL: userInfo.email, }); return response.MatchBatchFileDwgEditResult; } catch (error) { console.error("B4 매핑 정보 저장 실패:", error); throw error; } } /** * B4 파일명 파싱 (validateB4FileName과 동일한 로직) * 형식: [버림] [문서번호토큰1] [문서번호토큰2] ... [리비전번호].[확장자] * 예시: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" */ export async function parseB4FileName(fileName: string): Promise<{ valid: boolean; drawingNo?: string; revNo?: string; error?: string; }> { try { const lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex === -1) { return { valid: false, error: "File extension is missing" }; } const nameWithoutExt = fileName.substring(0, lastDotIndex); const parts = nameWithoutExt.split(" ").filter((p) => p.trim() !== ""); if (parts.length < 3) { return { valid: false, error: `At least 2 spaces required (current: ${parts.length - 1})`, }; } const revNo = parts[parts.length - 1]; const drawingTokens = parts.slice(1, parts.length - 1); const drawingNo = drawingTokens.join("-"); if (!drawingNo || !revNo) { return { valid: false, error: "Drawing number or revision number is empty" }; } return { valid: true, drawingNo: drawingNo.trim(), revNo: revNo.trim() }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * B4 일괄 업로드를 위한 상세도면 준비 (V2) * * 반환값: DrawingNo + RevNo별 UploadId 정보 * 파일 업로드는 클라이언트에서 직접 처리 */ export interface B4DetailDrawingInfo { drawingNo: string; revNo: string; uploadId: string; isNew: boolean; // 새로 생성된 상세도면인지 여부 drawingName?: string; discipline?: string; } export async function prepareB4DetailDrawingsV2(params: { projectNo: string; userId: string; userNm: string; email: string; vendorCode: string; registerKind: string; drawingRevisions: Array<{ drawingNo: string; revNo: string }>; }): Promise<{ success: boolean; detailDrawings?: B4DetailDrawingInfo[]; error?: string; }> { try { console.log("[V2 Prepare] 상세도면 준비 시작"); const { projectNo, userId, userNm, email, vendorCode, registerKind, drawingRevisions } = params; const detailDrawings: B4DetailDrawingInfo[] = []; // 중복 제거: 동일한 DrawingNo + RevNo 조합은 1번만 처리 const uniqueRevisions = Array.from( new Map( drawingRevisions.map((r) => [`${r.drawingNo}_${r.revNo}`, r]) ).values() ); if (uniqueRevisions.length !== drawingRevisions.length) { console.warn( `[V2 Prepare] 중복 제거: ${drawingRevisions.length}개 → ${uniqueRevisions.length}개` ); } console.log(`[V2 Prepare] 처리할 리비전: ${uniqueRevisions.length}개`); // DrawingNo별로 그룹화 const drawingNoSet = new Set(uniqueRevisions.map((r) => r.drawingNo)); const drawingInfoMap = new Map(); // 1. 기본 도면 정보 조회 for (const drawingNo of drawingNoSet) { try { const dwgList = await fetchDwgReceiptList({ project: projectNo, drawingKind: "B4", drawingMoveGbn: "도면입수", drawingNo: drawingNo, }); const dwgInfo = dwgList.find( (d) => (d as GttDwgReceiptItem).DrawingNo === drawingNo ) as GttDwgReceiptItem | undefined; if (dwgInfo) { drawingInfoMap.set(drawingNo, dwgInfo); } } catch (error) { console.error(`[V2 Prepare] 도면 정보 조회 실패: ${drawingNo}`, error); } } // 2. 각 RevNo별로 상세도면 확인/생성 (중복 제거된 리스트 사용) for (const { drawingNo, revNo } of uniqueRevisions) { try { const drawingInfo = drawingInfoMap.get(drawingNo); if (!drawingInfo) { throw new Error(`Drawing information not found: ${drawingNo}`); } console.log(`[V2 Prepare] 처리 중: ${drawingNo} Rev.${revNo}`); // 기존 상세도면 조회 const detailDwgList = await fetchDetailDwgReceiptList({ project: projectNo, drawingNo: drawingNo, discipline: drawingInfo.Discipline, drawingKind: "B4", userId: userId, }); // 해당 RevNo의 상세도면 찾기 const existingDetail = detailDwgList.find( (d) => d.DrawingRevNo === revNo ); let uploadId: string; let isNew = false; if (existingDetail) { // 1. 기존 상세도면이 있는 경우: 해당 uploadId 재사용 uploadId = existingDetail.UploadId; isNew = false; console.log( `[V2 Prepare] ✓ 기존 상세도면 재사용: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}` ); } else { // 2. 상세도면이 없는 경우: 새로 1번만 생성 uploadId = crypto.randomUUID(); isNew = true; console.log( `[V2 Prepare] ✓ 새 상세도면 생성: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}` ); const category = detailDwgList.length > 0 ? detailDwgList[0].Category : "NORM"; const addRequest: DetailDwgEditRequest = { Mode: "ADD", Status: "01", RegisterId: 0, ProjectNo: projectNo, Discipline: drawingInfo.Discipline, DrawingKind: "B4", DrawingNo: drawingNo, DrawingName: drawingInfo.DrawingName, RegisterGroupId: drawingInfo.RegisterGroupId, RegisterSerialNo: drawingInfo.RegisterGroup, RegisterKind: registerKind, DrawingRevNo: revNo, Category: category, Receiver: null, Manager: drawingInfo.Manager || "970043", RegisterDesc: "", UploadId: uploadId, RegCompanyCode: vendorCode, }; await editDetailDwgReceipt({ dwgList: [addRequest], userId: userId, userNm: userNm, vendorCode: vendorCode, email: email, }); console.log(`[V2 Prepare] ✓ DetailDwgReceiptMgmtEdit 호출 완료`); } detailDrawings.push({ drawingNo, revNo, uploadId, isNew, drawingName: drawingInfo.DrawingName, discipline: drawingInfo.Discipline, }); } catch (error) { console.error( `[V2 Prepare] ✗ 상세도면 준비 실패: ${drawingNo} Rev.${revNo}`, error ); throw error; } } const newCount = detailDrawings.filter((d) => d.isNew).length; const existingCount = detailDrawings.length - newCount; console.log( `[V2 Prepare] ✓ 완료: 총 ${detailDrawings.length}개 (신규: ${newCount}개, 기존: ${existingCount}개)` ); return { success: true, detailDrawings, }; } catch (error) { console.error("[V2 Prepare] 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * B4 파일 일괄 업로드 V2 (레거시 - 사용 안 함) * * @deprecated 서버 액션에서 fetch 호출 시 상대 경로 문제로 사용 중단 * 대신 prepareB4DetailDrawingsV2 + 클라이언트 uploadFilesWithProgress 사용 */ export async function bulkUploadB4FilesV2( formData: FormData ): Promise { try { console.log("[V2] B4 일괄 업로드 시작"); // FormData에서 메타데이터 추출 const projectNo = formData.get("projectNo") as string; const userId = formData.get("userId") as string; const userNm = formData.get("userNm") as string; const email = formData.get("email") as string; const vendorCode = formData.get("vendorCode") as string; const registerKind = formData.get("registerKind") as string; const fileCount = parseInt(formData.get("fileCount") as string); if (!projectNo || !userId || !userNm || !email || !vendorCode || !registerKind || !fileCount) { throw new Error("Required parameters are missing"); } console.log(`[V2] 프로젝트: ${projectNo}, 사용자: ${userId}, 파일 수: ${fileCount}`); const results: Array<{ drawingNo: string; revNo: string; fileName: string; success: boolean; error?: string; }> = []; let successCount = 0; let failCount = 0; // 1단계: 파일 수집 및 파싱 interface ParsedFile { file: File; drawingNo: string; revNo: string; fileName: string; } const parsedFiles: ParsedFile[] = []; for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; if (!file) continue; const parseResult = await parseB4FileName(file.name); if (!parseResult.valid || !parseResult.drawingNo || !parseResult.revNo) { results.push({ drawingNo: "", revNo: "", fileName: file.name, success: false, error: parseResult.error || "Failed to parse filename", }); failCount++; continue; } parsedFiles.push({ file, drawingNo: parseResult.drawingNo, revNo: parseResult.revNo, fileName: file.name, }); } console.log(`[V2] 파싱 완료: ${parsedFiles.length}개 성공, ${failCount}개 실패`); // 2단계: DrawingNo별로 기본 도면 정보 조회 const drawingNoSet = new Set(parsedFiles.map((f) => f.drawingNo)); const drawingInfoMap = new Map(); for (const drawingNo of drawingNoSet) { try { const dwgList = await fetchDwgReceiptList({ project: projectNo, drawingKind: "B4", drawingMoveGbn: "도면입수", drawingNo: drawingNo, }); const dwgInfo = dwgList.find( (d) => (d as GttDwgReceiptItem).DrawingNo === drawingNo ) as GttDwgReceiptItem | undefined; if (dwgInfo) { drawingInfoMap.set(drawingNo, dwgInfo); console.log(`[V2] 도면 정보 조회 완료: ${drawingNo}`); } else { console.warn(`[V2] 도면 정보 없음: ${drawingNo}`); } } catch (error) { console.error(`[V2] 도면 정보 조회 실패: ${drawingNo}`, error); } } // 3단계: DrawingNo + RevNo로 그룹화 const uploadGroups = new Map< string, { drawingNo: string; revNo: string; files: File[]; drawingInfo?: GttDwgReceiptItem; } >(); for (const parsed of parsedFiles) { const groupKey = `${parsed.drawingNo}_${parsed.revNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, { drawingNo: parsed.drawingNo, revNo: parsed.revNo, files: [], drawingInfo: drawingInfoMap.get(parsed.drawingNo), }); } uploadGroups.get(groupKey)!.files.push(parsed.file); } console.log(`[V2] ${uploadGroups.size}개 그룹으로 묶임`); // 4단계: 각 그룹별로 처리 for (const [groupKey, group] of uploadGroups.entries()) { const { drawingNo, revNo, files, drawingInfo } = group; try { console.log(`[V2] 그룹 처리 시작: ${groupKey} (${files.length}개 파일)`); // 도면 정보가 없으면 실패 if (!drawingInfo) { throw new Error(`Drawing information not found: ${drawingNo}`); } // 4-1. 기존 상세도면 조회 const detailDwgList = await fetchDetailDwgReceiptList({ project: projectNo, drawingNo: drawingNo, discipline: drawingInfo.Discipline, drawingKind: "B4", userId: userId, }); console.log(`[V2] 기존 상세도면: ${detailDwgList.length}개`); // 4-2. 해당 RevNo의 상세도면 찾기 const existingDetail = detailDwgList.find( (d) => d.DrawingRevNo === revNo ); let uploadId: string; if (existingDetail) { // 기존 상세도면이 있으면 해당 UploadId 사용 uploadId = existingDetail.UploadId; console.log(`[V2] 기존 상세도면 사용: ${revNo}, UploadId: ${uploadId}`); } else { // 4-3. 없으면 새로 생성 (ADD) uploadId = crypto.randomUUID(); // 기존 상세도면이 있으면 거기서 Category 가져오기, 없으면 기본값 const category = detailDwgList.length > 0 ? detailDwgList[0].Category : "NORM"; const registerDesc = ""; console.log(`[V2] 새 상세도면 생성: ${revNo}, UploadId: ${uploadId}`); const addRequest: DetailDwgEditRequest = { Mode: "ADD", Status: "01", RegisterId: 0, ProjectNo: projectNo, Discipline: drawingInfo.Discipline, DrawingKind: "B4", DrawingNo: drawingNo, DrawingName: drawingInfo.DrawingName, RegisterGroupId: drawingInfo.RegisterGroupId, RegisterSerialNo: drawingInfo.RegisterGroup, RegisterKind: registerKind, DrawingRevNo: revNo, Category: category, Receiver: null, Manager: drawingInfo.Manager || "970043", RegisterDesc: registerDesc, UploadId: uploadId, RegCompanyCode: vendorCode, }; await editDetailDwgReceipt({ dwgList: [addRequest], userId: userId, userNm: userNm, vendorCode: vendorCode, email: email, }); console.log(`[V2] 상세도면 ADD 완료: ${groupKey}`); } // 4-4. 파일 업로드 console.log(`[V2] 파일 업로드 시작: ${files.length}개 파일`); const uploadFormData = new FormData(); uploadFormData.append("uploadId", uploadId); uploadFormData.append("userId", userId); uploadFormData.append("fileCount", String(files.length)); files.forEach((file, index) => { uploadFormData.append(`file_${index}`, file); }); // Next.js API Route 호출 (프록시) const uploadResponse = await fetch("/api/dolce/upload-files", { method: "POST", body: uploadFormData, }); if (!uploadResponse.ok) { const errorData = await uploadResponse.json(); throw new Error(errorData.error || `File upload failed: ${uploadResponse.status}`); } const uploadResult = await uploadResponse.json(); if (!uploadResult.success) { throw new Error(uploadResult.error || "File upload failed"); } console.log(`[V2] 파일 업로드 완료: ${groupKey}`); // 성공 처리 for (const file of files) { results.push({ drawingNo, revNo, fileName: file.name, success: true, }); successCount++; } } catch (error) { // 실패 처리 const errorMessage = error instanceof Error ? error.message : "Unknown error"; console.error(`[V2] 그룹 처리 실패: ${groupKey}`, error); for (const file of group.files) { results.push({ drawingNo: group.drawingNo, revNo: group.revNo, fileName: file.name, success: false, error: errorMessage, }); failCount++; } } } console.log(`[V2] 일괄 업로드 완료: 성공 ${successCount}, 실패 ${failCount}`); return { success: successCount > 0, successCount, failCount, results, }; } catch (error) { console.error("[V2] 일괄 업로드 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } } /** * B4 파일 일괄 업로드 * 주의: formData를 사용하여 대용량 파일 처리 */ export async function bulkUploadB4Files( formData: FormData ): Promise { try { // FormData에서 메타데이터 추출 const projectNo = formData.get("projectNo") as string; const userId = formData.get("userId") as string; const fileCount = parseInt(formData.get("fileCount") as string); const registerKind = formData.get("registerKind") as string; if (!projectNo || !userId || !fileCount || !registerKind) { throw new Error("Required parameters are missing"); } const results: Array<{ drawingNo: string; revNo: string; fileName: string; success: boolean; error?: string; }> = []; let successCount = 0; let failCount = 0; // 파일별로 그룹화 (DrawingNo + RevNo 기준) const uploadGroups = new Map< string, Array<{ file: File; drawingNo: string; revNo: string; fileName: string; registerGroupId: number; }> >(); for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; const drawingNo = formData.get(`drawingNo_${i}`) as string; const revNo = formData.get(`revNo_${i}`) as string; const fileName = formData.get(`fileName_${i}`) as string; const registerGroupId = parseInt( formData.get(`registerGroupId_${i}`) as string ); if (!file || !drawingNo || !revNo) { continue; } const groupKey = `${drawingNo}_${revNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, []); } uploadGroups.get(groupKey)!.push({ file, drawingNo, revNo, fileName, registerGroupId, }); } // 각 그룹별로 업로드 처리 for (const [, files] of uploadGroups.entries()) { const { drawingNo, revNo, registerGroupId } = files[0]; try { // 1. UploadId 생성 (그룹당 하나) const uploadId = crypto.randomUUID(); // 2. 파일 업로드 (PWPUploadService.ashx) const uploadResults: Array<{ FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; FileWriteDT: string; OwnerUserId: string; }> = []; for (let i = 0; i < files.length; i++) { const fileInfo = files[i]; const fileId = crypto.randomUUID(); // 파일을 ArrayBuffer로 변환 const arrayBuffer = await fileInfo.file.arrayBuffer(); // PWPUploadService.ashx 호출 const uploadUrl = `${DOLCE_API_URL}/PWPUploadService.ashx?UploadId=${uploadId}&FileId=${fileId}`; const uploadResponse = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": "application/octet-stream", }, body: arrayBuffer, }); if (!uploadResponse.ok) { throw new Error( `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}` ); } const fileRelativePath = await uploadResponse.text(); uploadResults.push({ FileId: fileId, UploadId: uploadId, FileSeq: i + 1, FileName: fileInfo.file.name, FileRelativePath: fileRelativePath, FileSize: fileInfo.file.size, FileCreateDT: new Date().toISOString(), FileWriteDT: new Date().toISOString(), OwnerUserId: userId, }); } // 3. 업로드 완료 통지 (PWPUploadResultService.ashx) const resultServiceUrl = `${DOLCE_API_URL}/PWPUploadResultService.ashx?`; const resultResponse = await fetch(resultServiceUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(uploadResults), }); if (!resultResponse.ok) { throw new Error( `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}` ); } const resultText = await resultResponse.text(); if (resultText !== "Success") { throw new Error(`Upload notification failed: ${resultText}`); } // 4. 매핑 현황 재조회 (MatchBatchFileDwg) const mappingCheckResults = await checkB4MappingStatus(projectNo, [ { DrawingNo: drawingNo, RevNo: revNo, FileNm: files[0].fileName, }, ]); const mappingData = mappingCheckResults[0]; if (!mappingData || mappingData.RegisterGroupId === 0) { throw new Error("Mapping information not found"); } // 5. 매핑 정보 저장 (MatchBatchFileDwgEdit) await dolceApiCall("MatchBatchFileDwgEdit", { mappingSaveLists: [ { CGbn: mappingData.CGbn, Category: mappingData.Category, CheckBox: "0", DGbn: mappingData.DGbn, DegreeGbn: mappingData.DegreeGbn, DeptGbn: mappingData.DeptGbn, Discipline: mappingData.Discipline, DrawingKind: "B4", DrawingMoveGbn: "도면입수", DrawingName: mappingData.DrawingName, DrawingNo: drawingNo, DrawingUsage: "입수용", FileNm: files[0].fileName, JGbn: mappingData.JGbn, Manager: mappingData.Manager || "970043", MappingYN: "Y", NewOrNot: "N", ProjectNo: projectNo, RegisterGroup: 0, RegisterGroupId: registerGroupId, RegisterKindCode: registerKind, // 사용자가 선택한 RegisterKind 사용 RegisterSerialNo: mappingData.RegisterSerialNo, RevNo: revNo, SGbn: mappingData.SGbn, UploadId: uploadId, }, ], UserID: parseInt(userId), }); // 성공 처리 for (const fileInfo of files) { results.push({ drawingNo, revNo, fileName: fileInfo.fileName, success: true, }); successCount++; } } catch (error) { // 실패 처리 const errorMessage = error instanceof Error ? error.message : "Unknown error"; for (const fileInfo of files) { results.push({ drawingNo, revNo, fileName: fileInfo.fileName, success: false, error: errorMessage, }); failCount++; } } } return { success: successCount > 0, successCount, failCount, results, }; } catch (error) { console.error("일괄 업로드 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error", }; } }