"use server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import db from "@/db/db"; import { vendors } from "@/db/schema/vendors"; import { contracts } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; import { swpDocumentRevisions, swpDocumentFiles } from "@/db/schema/SWP/swp-documents"; import { eq, sql, and } from "drizzle-orm"; import { fetchSwpDocuments, type SwpTableParams } from "./actions"; import { fetchGetExternalInboxList } from "./api-client"; import type { SwpFileApiResponse } from "./sync-service"; import fs from "fs/promises"; import path from "path"; import { debugLog, debugError, debugSuccess, debugProcess, debugWarn } from "@/lib/debug-utils"; // ============================================================================ // 벤더 세션 정보 조회 // ============================================================================ interface VendorSessionInfo { vendorId: number; vendorCode: string; vendorName: string; companyId: number; } export async function getVendorSessionInfo(): Promise { debugProcess("벤더 세션 정보 조회 시작"); const session = await getServerSession(authOptions); debugLog("세션 조회 완료", { hasSession: !!session, hasCompanyId: !!session?.user?.companyId }); if (!session?.user?.companyId) { debugWarn("세션 또는 companyId 없음"); return null; } const companyId = typeof session.user.companyId === 'string' ? parseInt(session.user.companyId, 10) : session.user.companyId as number; debugLog("벤더 정보 조회 시작", { companyId }); // vendors 테이블에서 companyId로 벤더 정보 조회 const vendor = await db .select({ id: vendors.id, vendorCode: vendors.vendorCode, vendorName: vendors.vendorName, }) .from(vendors) .where(eq(vendors.id, companyId)) .limit(1); debugLog("벤더 정보 조회 완료", { found: !!vendor[0], vendorCode: vendor[0]?.vendorCode }); if (!vendor[0] || !vendor[0].vendorCode) { debugWarn("벤더 정보 또는 벤더 코드 없음", { vendor: vendor[0] }); return null; } const result = { vendorId: vendor[0].id, vendorCode: vendor[0].vendorCode, vendorName: vendor[0].vendorName, companyId, }; debugSuccess("벤더 세션 정보 조회 성공", { vendorCode: result.vendorCode }); return result; } // ============================================================================ // 벤더의 프로젝트 목록 조회 // ============================================================================ export async function fetchVendorProjects() { debugProcess("벤더 프로젝트 목록 조회 시작"); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 프로젝트 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } debugLog("프로젝트 목록 DB 조회 시작", { vendorId: vendorInfo.vendorId }); // contracts 테이블에서 해당 벤더의 계약들의 프로젝트 조회 const vendorProjects = await db .selectDistinct({ PROJ_NO: projects.code, PROJ_NM: projects.name, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .where(eq(contracts.vendorId, vendorInfo.vendorId)) .orderBy(projects.code); debugSuccess("프로젝트 목록 조회 성공", { count: vendorProjects.length }); return vendorProjects; } catch (error) { debugError("프로젝트 목록 조회 실패", error); console.error("[fetchVendorProjects] 오류:", error); return []; } } // ============================================================================ // 벤더 필터링된 문서 목록 조회 // ============================================================================ export async function fetchVendorDocuments(params: SwpTableParams) { debugProcess("벤더 문서 목록 조회 시작", { page: params.page, pageSize: params.pageSize }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 문서 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } // 벤더 코드를 필터에 자동 추가 const vendorParams: SwpTableParams = { ...params, filters: { ...params.filters, vndrCd: vendorInfo.vendorCode, // 벤더 코드 필터 강제 적용 }, }; debugLog("SWP 문서 조회 호출", { vendorCode: vendorInfo.vendorCode, filters: vendorParams.filters }); // 기존 fetchSwpDocuments 재사용 const result = await fetchSwpDocuments(vendorParams); debugSuccess("문서 목록 조회 성공", { total: result.total, dataCount: result.data.length }); return result; } catch (error) { debugError("문서 목록 조회 실패", error); console.error("[fetchVendorDocuments] 오류:", error); throw new Error("문서 목록 조회 실패 [담당자에게 문의하세요]"); } } // ============================================================================ // 파일 업로드 // ============================================================================ export interface FileUploadParams { revisionId: number; file: { FILE_NM: string; FILE_SEQ: string; FILE_SZ: string; FLD_PATH: string; STAT?: string; STAT_NM?: string; }; fileBuffer?: Buffer; // 실제 파일 데이터 추가 } export async function uploadFileToRevision(params: FileUploadParams) { debugProcess("파일 업로드 시작", { revisionId: params.revisionId, fileName: params.file.FILE_NM }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 파일 업로드 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } const { revisionId } = params; debugLog("리비전 권한 확인 시작", { revisionId }); // 1. 해당 리비전이 벤더에게 제공된 문서인지 확인 const revisionCheck = await db .select({ DOC_NO: swpDocumentRevisions.DOC_NO, VNDR_CD: sql`( SELECT d."VNDR_CD" FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, }) .from(swpDocumentRevisions) .where(eq(swpDocumentRevisions.id, revisionId)) .limit(1); debugLog("리비전 조회 결과", { found: !!revisionCheck[0], docNo: revisionCheck[0]?.DOC_NO }); if (!revisionCheck[0]) { debugError("리비전 없음", { revisionId }); throw new Error("리비전을 찾을 수 없습니다."); } // 벤더 코드가 일치하는지 확인 if (revisionCheck[0].VNDR_CD !== vendorInfo.vendorCode) { debugError("권한 없음", { expected: vendorInfo.vendorCode, actual: revisionCheck[0].VNDR_CD, docNo: revisionCheck[0].DOC_NO }); throw new Error("이 문서에 대한 권한이 없습니다."); } debugSuccess("리비전 권한 확인 성공"); const { revisionId: revId, file, fileBuffer } = params; // 1. SWP 마운트 경로에 파일 저장 debugProcess("파일 저장 단계 시작"); await saveFileToSwpNetwork(revId, { FILE_NM: file.FILE_NM, fileBuffer: fileBuffer, }); // 2. 파일 저장 API 호출 (메타데이터 전송) debugProcess("API 호출 단계 시작"); await callSwpFileSaveApi(revId, file); // 3. 파일 리스트 API 호출 (업데이트된 파일 목록 조회) debugProcess("파일 목록 조회 단계 시작"); const updatedFiles = await fetchUpdatedFileList(revId); debugLog("업데이트된 파일 목록", { count: updatedFiles.length }); // 4. 파일 목록 DB 동기화 (새 파일들 추가) debugProcess("DB 동기화 단계 시작"); await syncSwpDocumentFiles(revId, updatedFiles); debugSuccess("파일 업로드 완료", { fileName: file.FILE_NM, revisionId }); return { success: true, fileId: 0, action: "uploaded" }; } catch (error) { debugError("파일 업로드 실패", error); console.error("[uploadFileToRevision] 오류:", error); throw new Error( error instanceof Error ? error.message : "파일 업로드 실패" ); } } // ============================================================================ // 벤더 통계 조회 // ============================================================================ export async function fetchVendorSwpStats(projNo?: string) { debugProcess("벤더 통계 조회 시작", { projNo }); try { const vendorInfo = await getVendorSessionInfo(); if (!vendorInfo) { debugError("벤더 정보 없음 - 통계 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } const whereConditions = [ sql`d."VNDR_CD" = ${vendorInfo.vendorCode}`, ]; if (projNo) { whereConditions.push(sql`d."PROJ_NO" = ${projNo}`); } debugLog("통계 SQL 실행", { vendorCode: vendorInfo.vendorCode, projNo }); const stats = await db.execute<{ total_documents: number; total_revisions: number; total_files: number; uploaded_files: number; last_sync: Date | null; }>(sql` SELECT COUNT(DISTINCT d."DOC_NO")::int as total_documents, COUNT(DISTINCT r.id)::int as total_revisions, COUNT(f.id)::int as total_files, COUNT(CASE WHEN f."FLD_PATH" IS NOT NULL AND f."FLD_PATH" != '' THEN 1 END)::int as uploaded_files, MAX(d.last_synced_at) as last_sync FROM swp.swp_documents d LEFT JOIN swp.swp_document_revisions r ON d."DOC_NO" = r."DOC_NO" LEFT JOIN swp.swp_document_files f ON r.id = f.revision_id WHERE ${sql.join(whereConditions, sql` AND `)} `); const result = stats.rows[0] || { total_documents: 0, total_revisions: 0, total_files: 0, uploaded_files: 0, last_sync: null, }; debugSuccess("통계 조회 성공", { documents: result.total_documents, revisions: result.total_revisions, files: result.total_files, uploaded: result.uploaded_files }); return result; } catch (error) { debugError("통계 조회 실패", error); console.error("[fetchVendorSwpStats] 오류:", error); return { total_documents: 0, total_revisions: 0, total_files: 0, uploaded_files: 0, last_sync: null, }; } } // ============================================================================ // SWP 파일 업로드 헬퍼 함수들 // ============================================================================ /** * 1. SWP 마운트 경로에 파일 저장 */ async function saveFileToSwpNetwork( revisionId: number, fileInfo: { FILE_NM: string; fileBuffer?: Buffer } ): Promise { debugProcess("네트워크 파일 저장 시작", { revisionId, fileName: fileInfo.FILE_NM }); // 리비전 정보 조회 const revisionInfo = await db .select({ DOC_NO: swpDocumentRevisions.DOC_NO, REV_NO: swpDocumentRevisions.REV_NO, PROJ_NO: sql`( SELECT d."PROJ_NO" FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, VNDR_CD: sql`( SELECT d."VNDR_CD" FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, }) .from(swpDocumentRevisions) .where(eq(swpDocumentRevisions.id, revisionId)) .limit(1); debugLog("리비전 정보 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); if (!revisionInfo[0]) { debugError("리비전 정보 없음"); throw new Error("리비전 정보를 찾을 수 없습니다"); } const { PROJ_NO, VNDR_CD, DOC_NO, REV_NO } = revisionInfo[0]; // SWP 마운트 경로 생성 const mountDir = process.env.SWP_MOUNT_DIR || "/mnt/swp-smb-dir"; const targetDir = path.join(mountDir, PROJ_NO, VNDR_CD, DOC_NO, REV_NO); debugLog("파일 저장 경로 생성", { mountDir, targetDir }); // 디렉토리 생성 await fs.mkdir(targetDir, { recursive: true }); debugLog("디렉토리 생성 완료"); // 파일 저장 const targetPath = path.join(targetDir, fileInfo.FILE_NM); if (fileInfo.fileBuffer) { await fs.writeFile(targetPath, fileInfo.fileBuffer); debugSuccess("파일 저장 완료", { fileName: fileInfo.FILE_NM, targetPath, size: fileInfo.fileBuffer.length }); } else { debugWarn("파일 버퍼 없음", { fileName: fileInfo.FILE_NM }); } return targetPath; } /** * 2. 파일 저장 API 호출 (메타데이터 전송) */ async function callSwpFileSaveApi( revisionId: number, fileInfo: FileUploadParams['file'] ): Promise { debugProcess("SWP 파일 저장 API 호출 시작", { revisionId, fileName: fileInfo.FILE_NM }); // TODO: SWP 파일 저장 API 구현 // buyer-system의 sendToInBox 패턴 참고 debugLog("메타데이터 전송", { fileName: fileInfo.FILE_NM, fileSeq: fileInfo.FILE_SEQ, filePath: fileInfo.FLD_PATH }); // 임시 구현: 실제로는 SWP SaveFile API 등을 호출해야 함 // 예: SaveFile, UploadFile API 등 debugWarn("SWP 파일 저장 API가 아직 구현되지 않음 - 임시 스킵"); } /** * 3. 파일 리스트 API 호출 (업데이트된 파일 목록 조회) */ async function fetchUpdatedFileList(revisionId: number): Promise { debugProcess("업데이트된 파일 목록 조회 시작", { revisionId }); // 리비전 정보 조회 const revisionInfo = await db .select({ DOC_NO: swpDocumentRevisions.DOC_NO, PROJ_NO: sql`( SELECT d."PROJ_NO" FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, VNDR_CD: sql`( SELECT d."VNDR_CD" FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, }) .from(swpDocumentRevisions) .where(eq(swpDocumentRevisions.id, revisionId)) .limit(1); debugLog("리비전 정보 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); if (!revisionInfo[0]) { debugError("리비전 정보 없음"); throw new Error("리비전 정보를 찾을 수 없습니다"); } const { PROJ_NO, VNDR_CD } = revisionInfo[0]; debugLog("SWP 파일 목록 API 호출", { projNo: PROJ_NO, vndrCd: VNDR_CD }); // SWP API에서 업데이트된 파일 목록 조회 const files = await fetchGetExternalInboxList({ projNo: PROJ_NO, vndrCd: VNDR_CD, }); debugSuccess("파일 목록 조회 완료", { count: files.length }); return files; } /** * 4. 파일 목록 DB 동기화 (새 파일들 추가) */ async function syncSwpDocumentFiles( revisionId: number, apiFiles: SwpFileApiResponse[] ): Promise { debugProcess("DB 동기화 시작", { revisionId, fileCount: apiFiles.length }); // 리비전 정보에서 DOC_NO 가져오기 const revisionInfo = await db .select({ DOC_NO: swpDocumentRevisions.DOC_NO, }) .from(swpDocumentRevisions) .where(eq(swpDocumentRevisions.id, revisionId)) .limit(1); debugLog("리비전 DOC_NO 조회", { found: !!revisionInfo[0], docNo: revisionInfo[0]?.DOC_NO }); if (!revisionInfo[0]) { debugError("리비전 정보 없음"); throw new Error("리비전 정보를 찾을 수 없습니다"); } const { DOC_NO } = revisionInfo[0]; let processedCount = 0; let updatedCount = 0; let insertedCount = 0; for (const apiFile of apiFiles) { try { processedCount++; // 기존 파일 확인 const existingFile = await db .select({ id: swpDocumentFiles.id }) .from(swpDocumentFiles) .where( and( eq(swpDocumentFiles.revision_id, revisionId), eq(swpDocumentFiles.FILE_SEQ, apiFile.FILE_SEQ || "1") ) ) .limit(1); const fileData = { DOC_NO: DOC_NO, FILE_NM: apiFile.FILE_NM, FILE_SEQ: apiFile.FILE_SEQ || "1", FILE_SZ: apiFile.FILE_SZ || "0", FLD_PATH: apiFile.FLD_PATH, STAT: apiFile.STAT || null, STAT_NM: apiFile.STAT_NM || null, ACTV_NO: apiFile.ACTV_NO || null, IDX: apiFile.IDX || null, CRTER: apiFile.CRTER || null, CRTE_DTM: apiFile.CRTE_DTM || null, CHGR: apiFile.CHGR || null, CHG_DTM: apiFile.CHG_DTM || null, sync_status: 'synced' as const, last_synced_at: new Date(), updated_at: new Date(), }; if (existingFile[0]) { // 기존 파일 업데이트 await db .update(swpDocumentFiles) .set({ ...fileData, updated_at: new Date(), }) .where(eq(swpDocumentFiles.id, existingFile[0].id)); updatedCount++; debugLog("파일 업데이트", { fileName: apiFile.FILE_NM, fileSeq: apiFile.FILE_SEQ }); } else { // 새 파일 추가 await db.insert(swpDocumentFiles).values({ revision_id: revisionId, DOC_NO: DOC_NO, FILE_NM: apiFile.FILE_NM, FILE_SEQ: apiFile.FILE_SEQ || "1", FILE_SZ: apiFile.FILE_SZ || "0", FLD_PATH: apiFile.FLD_PATH, STAT: apiFile.STAT || null, STAT_NM: apiFile.STAT_NM || null, ACTV_NO: apiFile.ACTV_NO || null, IDX: apiFile.IDX || null, CRTER: apiFile.CRTER || null, CRTE_DTM: apiFile.CRTE_DTM || null, CHGR: apiFile.CHGR || null, CHG_DTM: apiFile.CHG_DTM || null, sync_status: 'synced' as const, last_synced_at: new Date(), updated_at: new Date(), }); insertedCount++; debugLog("파일 추가", { fileName: apiFile.FILE_NM, fileSeq: apiFile.FILE_SEQ }); } } catch (error) { debugError("파일 동기화 실패", { fileName: apiFile.FILE_NM, error }); console.error(`파일 동기화 실패: ${apiFile.FILE_NM}`, error); // 개별 파일 실패는 전체 프로세스를 중단하지 않음 } } debugSuccess("DB 동기화 완료", { processed: processedCount, updated: updatedCount, inserted: insertedCount }); }