"use server"; import { fetchDwgReceiptList as fetchDwgOriginal, fetchDetailDwgReceiptList as fetchDetailOriginal, fetchFileInfoList as fetchFileOriginal, getVendorSessionInfo as getVendorSessionInfoOriginal, fetchVendorProjects as fetchVendorProjectsOriginal, downloadDolceFile as downloadDolceFileOriginal, // 타입들 재사용 DwgReceiptItem, GttDwgReceiptItem, UnifiedDwgReceiptItem, DetailDwgReceiptItem, FileInfoItem, DetailDwgEditRequest, B4MappingSaveItem } from "@/lib/dolce/actions"; import db from "@/db/db"; import { dolceSyncList } from "@/db/schema/dolce/dolce"; import { eq, and, desc } from "drizzle-orm"; import { saveToLocalBuffer, syncItem, getLocalFile, deleteLocalItem, deleteLocalFileFromItem } from "./sync-service"; // 타입 재-export export type { DwgReceiptItem, GttDwgReceiptItem, UnifiedDwgReceiptItem, DetailDwgReceiptItem, FileInfoItem, DetailDwgEditRequest }; // Re-export 함수들을 명시적인 async 함수로 래핑 export async function getVendorSessionInfo() { return getVendorSessionInfoOriginal(); } export async function fetchVendorProjects() { return fetchVendorProjectsOriginal(); } export async function downloadDolceFile(params: { fileId: string; userId: string; fileName: string; }) { return downloadDolceFileOriginal(params); } // ============================================================================ // 조회 액션 (로컬 데이터 병합) // ============================================================================ /** * 1. 도면 리스트 조회 (변경 없음 - 도면 리스트 자체는 외부 시스템 기준) */ export async function fetchDwgReceiptList(params: { project: string; drawingKind: string; drawingMoveGbn?: string; drawingNo?: string; drawingName?: string; drawingVendor?: string; discipline?: string; }) { return fetchDwgOriginal(params); } /** * 2. 상세도면 리스트 조회 (로컬 임시 저장 건 병합) */ export async function fetchDetailDwgReceiptListV2(params: { project: string; drawingNo: string; discipline: string; drawingKind: string; userId?: string; }): Promise { // 1. 외부 API 조회 const originalList = await fetchDetailOriginal(params); // 2. 로컬 DB 조회 (미동기화된 ADD_DETAIL 건) // projectNo와 drawingNo로 필터링 const localItems = await db.query.dolceSyncList.findMany({ where: and( eq(dolceSyncList.projectNo, params.project), eq(dolceSyncList.drawingNo, params.drawingNo), eq(dolceSyncList.type, "ADD_DETAIL"), eq(dolceSyncList.isSynced, false) ), orderBy: [desc(dolceSyncList.createdAt)], }); // 3. 로컬 데이터를 DetailDwgReceiptItem 형식으로 변환하여 추가 const mergedList = [...originalList]; for (const item of localItems) { const payload = item.payload as { meta: { dwgList: DetailDwgEditRequest[] } }; const dwgRequest = payload.meta.dwgList[0]; // 보통 1개씩 요청함 if (dwgRequest) { // 임시 객체 생성 const tempItem: DetailDwgReceiptItem = { Category: dwgRequest.Category, CategoryENM: dwgRequest.Category, // 임시: 코드값 사용 CategoryNM: dwgRequest.Category, // 임시 CreateDt: item.createdAt.toISOString(), CreateUserENM: "", CreateUserId: item.userId, CreateUserNM: "", // 이름은 별도 조회 필요하나 생략 Discipline: dwgRequest.Discipline, DrawingKind: dwgRequest.DrawingKind, DrawingName: dwgRequest.DrawingName, DrawingNo: dwgRequest.DrawingNo, DrawingRevNo: dwgRequest.DrawingRevNo || "", DrawingUsage: "TEMP", // 임시 표시 DrawingUsageENM: "Temporary Saved", DrawingUsageNM: "임시저장", Manager: dwgRequest.Manager, Mode: "ADD", OFDC_NO: "", ProjectNo: dwgRequest.ProjectNo, Receiver: dwgRequest.Receiver, RegCompanyCode: dwgRequest.RegCompanyCode, RegCompanyENM: "", RegCompanyNM: "", RegisterDesc: dwgRequest.RegisterDesc, RegisterGroup: dwgRequest.RegisterGroupId, RegisterGroupId: dwgRequest.RegisterGroupId, RegisterId: 0, // 임시 ID RegisterKind: dwgRequest.RegisterKind, RegisterKindENM: dwgRequest.RegisterKind, // 임시: 코드값 사용 RegisterKindNM: dwgRequest.RegisterKind, RegisterSerialNo: dwgRequest.RegisterSerialNo, SHINote: null, Status: "EVCP Saved", // 작성중 UploadId: dwgRequest.UploadId, }; // 리스트 상단에 추가 (혹은 날짜순 정렬) mergedList.unshift(tempItem); } } return mergedList; } /** * 3. 파일 리스트 조회 (로컬 임시 저장 건 병합) */ export async function fetchFileInfoListV2(uploadId: string): Promise { // 1. 외부 API 조회 const originalList = await fetchFileOriginal(uploadId); // 2. 로컬 DB 조회 (이 uploadId에 대해 추가된 파일들) // ADD_DETAIL(새 도면 생성 시 파일) 또는 ADD_FILE(기존 도면에 파일 추가) 모두 해당될 수 있음. // upload_id 컬럼으로 조회 const localItems = await db.query.dolceSyncList.findMany({ where: and( eq(dolceSyncList.uploadId, uploadId), eq(dolceSyncList.isSynced, false) ), }); const mergedList = [...originalList]; for (const item of localItems) { const payload = item.payload as { files: Array<{ originalName: string, size: number, localPath: string }> }; if (payload.files && payload.files.length > 0) { payload.files.forEach((file, index) => { const tempFile: FileInfoItem = { CreateDt: item.createdAt.toISOString(), CreateUserId: item.userId, Deleted: "False", FileDescription: "Local Temp File", FileId: `LOCAL_${item.id}_${index}`, // 로컬 파일 식별자 FileName: file.originalName, FileRelativePath: file.localPath, // 로컬 경로 (다운로드 시 구분 필요) FileSeq: String(9999 + index), // 임시 시퀀스 FileServerId: "LOCAL", FileSize: String(file.size), FileTitle: null, FileWriteDT: item.createdAt.toISOString(), OwnerUserId: item.userId, SourceDrmYn: "N", SystemId: "EVCP", TableName: "Temp", UploadId: uploadId, UseYn: "True" }; mergedList.push(tempFile); }); } } return mergedList; } // ============================================================================ // 저장 액션 (로컬 버퍼링) // ============================================================================ /** * 4. 상세도면 추가/수정 (로컬 저장) */ export async function editDetailDwgReceiptV2( formData: FormData ): Promise<{ success: boolean, syncId: string }> { try { const dwgListJson = formData.get("dwgList") as string; const userId = formData.get("userId") as string; const userNm = formData.get("userNm") as string; const vendorCode = formData.get("vendorCode") as string; const email = formData.get("email") as string; if (!dwgListJson || !userId) { throw new Error("Required parameters are missing"); } const dwgList = JSON.parse(dwgListJson) as DetailDwgEditRequest[]; const request = dwgList[0]; // 보통 1건 처리 const type = request.Mode === "ADD" ? "ADD_DETAIL" : "MOD_DETAIL"; // FormData에서 파일 추출 const files: File[] = []; // file_0, file_1 ... 형식으로 전송된다고 가정 const fileCount = parseInt((formData.get("fileCount") as string) || "0"); for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`); if (file instanceof File) { files.push(file); } } const savedItem = await saveToLocalBuffer({ type, projectNo: request.ProjectNo, userId, userName: userNm, // [추가] vendorCode: vendorCode, // [추가] drawingNo: request.DrawingNo, uploadId: request.UploadId, metaData: { dwgList, userId, userNm, vendorCode, email }, files }); return { success: true, syncId: savedItem.id }; } catch (error) { console.error("상세도면 로컬 저장 실패:", error); throw error; } } /** * 5. 파일 추가 (로컬 저장) */ export async function uploadFilesToDetailDrawingV2( formData: FormData ): Promise<{ success: boolean, syncId?: string, error?: string }> { try { const uploadId = formData.get("uploadId") as string; const userId = formData.get("userId") as string; const fileCount = parseInt(formData.get("fileCount") as string); const projectNo = formData.get("projectNo") as string; const vendorCode = formData.get("vendorCode") as string; // [추가] 메타데이터 추출 const drawingNo = formData.get("drawingNo") as string; const revNo = formData.get("revNo") as string; const drawingName = formData.get("drawingName") as string; const discipline = formData.get("discipline") as string; const registerKind = formData.get("registerKind") as string; if (!uploadId || !userId) { throw new Error("Required parameters are missing"); } const files: File[] = []; for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; if (file) files.push(file); } const savedItem = await saveToLocalBuffer({ type: "ADD_FILE", projectNo: projectNo || "UNKNOWN", userId, vendorCode, drawingNo: drawingNo || undefined, // [추가] DB drawingNo 컬럼 저장 uploadId, metaData: { uploadId, userId, vendorCode, // [추가] drawingNo, revNo, drawingName, discipline, registerKind }, files }); return { success: true, syncId: savedItem.id }; } catch (error) { console.error("파일 로컬 저장 실패:", error); return { success: false, error: error instanceof Error ? error.message : "Unknown error" }; } } /** * 6. 동기화 실행 액션 */ export async function syncDolceItem(id: string) { return syncItem(id); } /** * 7. 미동기화 아이템 목록 조회 (내것만 - Sync 버튼 카운트용) */ export async function fetchPendingSyncItems(params: { projectNo: string; userId: string; }): Promise> { try { const items = await db.query.dolceSyncList.findMany({ where: and( eq(dolceSyncList.projectNo, params.projectNo), eq(dolceSyncList.userId, params.userId), eq(dolceSyncList.isSynced, false) ), orderBy: [desc(dolceSyncList.createdAt)], }); return items.map((item) => { let desc = ""; if (item.type === "ADD_DETAIL" || item.type === "MOD_DETAIL") { desc = `${item.type === "ADD_DETAIL" ? "Add" : "Mod"} Drawing ${item.drawingNo}`; } else if (item.type === "ADD_FILE") { desc = `Add Files to ${item.uploadId}`; } else if (item.type === "B4_BULK") { desc = `Bulk Upload ${item.drawingNo}`; } return { id: item.id, type: item.type, desc, }; }); } catch (error) { console.error("미동기화 목록 조회 실패:", error); return []; } } // 상세 동기화 정보 인터페이스 export interface PendingSyncItemDetail { id: string; // syncId type: string; createdAt: Date; userId: string; userName: string; // 도면 정보 drawingNo: string; drawingName: string; discipline: string; // 상세도면 정보 revision: string; registerKind: string; // 접수종류 // 파일 정보 files: Array<{ name: string; size: number; }>; } /** * 8. 프로젝트 전체 미동기화 아이템 목록 조회 (동기화 다이얼로그용 - 상세 정보 포함) */ export async function fetchProjectPendingSyncItems(params: { projectNo: string; currentUserId: string; currentVendorCode: string; }): Promise<{ myItems: PendingSyncItemDetail[]; otherItems: PendingSyncItemDetail[]; }> { try { // 1. 내 아이템 조회 (userId 이용) const myItemsPromise = db.query.dolceSyncList.findMany({ where: and( eq(dolceSyncList.projectNo, params.projectNo), eq(dolceSyncList.userId, params.currentUserId), eq(dolceSyncList.isSynced, false) ), orderBy: [desc(dolceSyncList.createdAt)], }); // 2. 같은 벤더의 다른 사용자 아이템 조회 (vendorCode 이용, userId 제외) const otherItemsPromise = db.query.dolceSyncList.findMany({ where: and( eq(dolceSyncList.projectNo, params.projectNo), eq(dolceSyncList.vendorCode, params.currentVendorCode), eq(dolceSyncList.isSynced, false) ), orderBy: [desc(dolceSyncList.createdAt)], }); const [myDbItems, otherDbItems] = await Promise.all([myItemsPromise, otherItemsPromise]); // 아이템 상세 정보 파싱 헬퍼 const parseItem = (item: typeof dolceSyncList.$inferSelect): PendingSyncItemDetail => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const payload = item.payload as any; // eslint-disable-next-line @typescript-eslint/no-explicit-any const files = payload.files?.map((f: any) => ({ name: f.originalName, size: f.size })) || []; const meta = payload.meta || {}; let drawingNo = item.drawingNo || ""; let drawingName = ""; let discipline = ""; let revision = ""; let registerKind = ""; if (item.type === "ADD_DETAIL" || item.type === "MOD_DETAIL") { const dwgRequest = meta.dwgList?.[0]; if (dwgRequest) { drawingNo = dwgRequest.DrawingNo; drawingName = dwgRequest.DrawingName; discipline = dwgRequest.Discipline; revision = dwgRequest.DrawingRevNo || ""; registerKind = dwgRequest.RegisterKind || ""; } } else if (item.type === "ADD_FILE") { // ADD_FILE의 경우 meta에 저장된 정보 사용 drawingNo = meta.drawingNo || item.drawingNo || ""; drawingName = meta.drawingName || ""; discipline = meta.discipline || ""; revision = meta.revNo || ""; registerKind = meta.registerKind || ""; } else if (item.type === "B4_BULK") { // B4_BULK의 경우 첫 번째 매핑 정보 사용 (보통 같은 도면) const firstMapping = meta.mappingSaveLists?.[0]; if (firstMapping) { drawingNo = firstMapping.DrawingNo; drawingName = firstMapping.DrawingName; discipline = firstMapping.Discipline; revision = firstMapping.RevNo; registerKind = firstMapping.RegisterKindCode || ""; } } return { id: item.id, type: item.type, createdAt: item.createdAt, userId: item.userId, userName: item.userName || item.userId, drawingNo, drawingName, discipline, revision, registerKind, files, }; }; // 내 아이템 매핑 const myItems = myDbItems.map(parseItem); // 다른 사용자 아이템 매핑 const otherItems = otherDbItems .filter(item => item.userId !== params.currentUserId) .map(parseItem); return { myItems, otherItems }; } catch (error) { console.error("프로젝트 미동기화 목록 조회 실패:", error); return { myItems: [], otherItems: [] }; } } // B4 일괄 업로드 (로컬 저장 버전) // ============================================================================ /** * B4 일괄 업로드 V3 (로컬 저장) */ export async function bulkUploadB4FilesV3( formData: FormData ): Promise<{ success: boolean, syncIds: string[], error?: string }> { try { // 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 fileCount = parseInt(formData.get("fileCount") as string); const syncIds: string[] = []; // 그룹핑: UploadId 기준 (하나의 상세도면에 여러 파일이 들어갈 수 있음) const groups = new Map(); for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; const mappingJson = formData.get(`mappingData_${i}`) as string; if (!file || !mappingJson) continue; const mapping = JSON.parse(mappingJson); const uploadId = mapping.UploadId; if (!groups.has(uploadId)) { groups.set(uploadId, { files: [], mappings: [], drawingNo: mapping.DrawingNo }); } groups.get(uploadId)!.files.push(file); groups.get(uploadId)!.mappings.push(mapping); } // 각 그룹(상세도면 단위)별로 로컬 저장 for (const [uploadId, group] of groups.entries()) { const savedItem = await saveToLocalBuffer({ type: "B4_BULK", projectNo, userId, userName: userNm, // [추가] vendorCode: vendorCode, // [추가] drawingNo: group.drawingNo, uploadId, metaData: { mappingSaveLists: group.mappings, userInfo: { userId, userName: userNm, vendorCode, email } }, files: group.files }); syncIds.push(savedItem.id); } return { success: true, syncIds }; } catch (error) { console.error("B4 일괄 업로드 로컬 저장 실패:", error); return { success: false, syncIds: [], error: error instanceof Error ? error.message : "Unknown error" }; } } /** * 9. 로컬 파일 다운로드 (Buffer 반환) */ export async function downloadLocalFile(fileId: string) { return getLocalFile(fileId); } /** * 10. 로컬 상세도면 삭제 */ export async function deleteLocalDetailDrawing(uploadId: string) { try { // Find the item by uploadId and type // We assume one ADD_DETAIL per uploadId for now (as per prepareB4DetailDrawingsV2 logic) const item = await db.query.dolceSyncList.findFirst({ where: and( eq(dolceSyncList.uploadId, uploadId), eq(dolceSyncList.type, "ADD_DETAIL"), eq(dolceSyncList.isSynced, false) ) }); if (item) { await deleteLocalItem(item.id); return { success: true }; } return { success: false, error: "Item not found" }; } catch (e) { console.error("Failed to delete local drawing", e); return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; } } /** * 11. 로컬 파일 삭제 */ export async function deleteLocalFile(fileId: string) { try { await deleteLocalFileFromItem(fileId); return { success: true }; } catch (e) { console.error("Failed to delete local file", e); return { success: false, error: e instanceof Error ? e.message : "Unknown error" }; } }