diff options
| -rw-r--r-- | app/[lng]/partners/(partners)/swp-document-upload/page.tsx | 20 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx | 22 | ||||
| -rw-r--r-- | lib/swp/actions.ts | 4 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 36 | ||||
| -rw-r--r-- | lib/swp/table/swp-table.tsx | 2 | ||||
| -rw-r--r-- | lib/swp/vendor-actions.ts | 405 |
6 files changed, 384 insertions, 105 deletions
diff --git a/app/[lng]/partners/(partners)/swp-document-upload/page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx index 25eb52aa..5b8a0be8 100644 --- a/app/[lng]/partners/(partners)/swp-document-upload/page.tsx +++ b/app/[lng]/partners/(partners)/swp-document-upload/page.tsx @@ -2,6 +2,7 @@ import { Suspense } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import VendorDocumentPage from "./vendor-document-page"; +import { Shell } from "@/components/shell"; export const metadata = { title: "문서 조회 및 업로드", @@ -37,21 +38,20 @@ export default async function DocumentUploadPage({ const params = await searchParams; return ( - <div className="container mx-auto py-6 space-y-6"> + <Shell> {/* 헤더 */} - <Card> - <CardHeader> - <CardTitle className="text-2xl">문서 조회 및 업로드</CardTitle> - <CardDescription> - 프로젝트별 할당된 문서를 조회하고 파일을 업로드할 수 있습니다. - </CardDescription> - </CardHeader> - </Card> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + SWP 문서 제출 + </h2> + </div> + </div> {/* 메인 컨텐츠 */} <Suspense fallback={<VendorDocumentSkeleton />}> <VendorDocumentPage searchParams={params} /> </Suspense> - </div> + </Shell> ); }
\ No newline at end of file diff --git a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx index f2469c29..2431259d 100644 --- a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx +++ b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx @@ -12,9 +12,8 @@ import { fetchVendorDocuments, fetchVendorProjects, fetchVendorSwpStats, - type SwpTableFilters, - type SwpDocumentWithStats, } from "@/lib/swp/vendor-actions"; +import { type SwpTableFilters, type SwpDocumentWithStats } from "@/lib/swp/actions"; interface VendorDocumentPageProps { searchParams: { [key: string]: string | string[] | undefined }; @@ -91,9 +90,8 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP } catch (err) { console.error("초기 데이터 로드 실패:", err); setError(err instanceof Error ? err.message : "데이터 로드 실패"); - } finally { - setIsLoading(false); } + setIsLoading(false); // finally 대신 여기서 호출 }; const loadDocuments = async () => { @@ -152,16 +150,16 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP ); } - if (error) { - return ( - <Alert variant="destructive"> - <AlertDescription>{error}</AlertDescription> - </Alert> - ); - } return ( <div className="space-y-6"> + {/* 에러 메시지 */} + {error && ( + <Alert variant="destructive"> + <AlertDescription>{error}</AlertDescription> + </Alert> + )} + {/* 통계 카드 */} <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <Card> @@ -209,7 +207,6 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP filters={filters} onFiltersChange={handleFiltersChange} projects={projects} - mode="vendor" /> </CardHeader> <CardContent> @@ -220,7 +217,6 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP pageSize={pageSize} totalPages={totalPages} onPageChange={handlePageChange} - mode="vendor" /> </CardContent> </Card> diff --git a/lib/swp/actions.ts b/lib/swp/actions.ts index 79c0bafe..694936ab 100644 --- a/lib/swp/actions.ts +++ b/lib/swp/actions.ts @@ -31,7 +31,7 @@ export interface SwpDocumentWithStats { DOC_NO: string; DOC_TITLE: string; PROJ_NO: string; - PROJ_NM: string; + PROJ_NM: string | null; PKG_NO: string | null; VNDR_CD: string | null; CPY_NM: string | null; @@ -140,7 +140,7 @@ export async function fetchSwpDocuments(params: SwpTableParams) { }; } catch (error) { console.error("[fetchSwpDocuments] 오류:", error); - throw new Error("문서 목록 조회 실패"); + throw new Error("문서 목록 조회 실패 [SWP API에서 실패가 발생했습니다. 담당자에게 문의하세요]"); } } diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx index 656dfd4a..7c5f2f2e 100644 --- a/lib/swp/table/swp-table-toolbar.tsx +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -26,14 +26,12 @@ interface SwpTableToolbarProps { filters: SwpTableFilters; onFiltersChange: (filters: SwpTableFilters) => void; projects?: Array<{ PROJ_NO: string; PROJ_NM: string }>; - mode?: "admin" | "vendor"; // admin: SWP 동기화 가능, vendor: 읽기 전용 } export function SwpTableToolbar({ filters, onFiltersChange, projects = [], - mode = "admin", }: SwpTableToolbarProps) { const [isSyncing, startSync] = useTransition(); const [localFilters, setLocalFilters] = useState<SwpTableFilters>(filters); @@ -115,16 +113,14 @@ export function SwpTableToolbar({ {/* 상단 액션 바 */} <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> - {mode === "admin" && ( - <Button - onClick={handleSync} - disabled={isSyncing || !localFilters.projNo} - size="sm" - > - <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} /> - {isSyncing ? "동기화 중..." : "SWP 동기화"} - </Button> - )} + <Button + onClick={handleSync} + disabled={isSyncing || !localFilters.projNo} + size="sm" + > + <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} /> + {isSyncing ? "동기화 중..." : "SWP 동기화"} + </Button> <Button variant="outline" size="sm" disabled> <Download className="h-4 w-4 mr-2" /> @@ -133,7 +129,7 @@ export function SwpTableToolbar({ </div> <div className="text-sm text-muted-foreground"> - {mode === "vendor" ? "문서 조회 및 업로드" : "SWP 문서 관리 시스템"} + SWP 문서 관리 시스템 </div> </div> @@ -166,10 +162,11 @@ export function SwpTableToolbar({ className="w-full justify-between" > {localFilters.projNo ? ( - <span className="truncate"> + <span> {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NO || localFilters.projNo} - {" - "} + {" ["} {projects.find((p) => p.PROJ_NO === localFilters.projNo)?.PROJ_NM} + {"]"} </span> ) : ( <span className="text-muted-foreground">프로젝트 선택</span> @@ -225,10 +222,7 @@ export function SwpTableToolbar({ localFilters.projNo === proj.PROJ_NO ? "opacity-100" : "opacity-0" )} /> - <div className="flex flex-col items-start"> - <span className="font-mono text-sm">{proj.PROJ_NO}</span> - <span className="text-xs text-muted-foreground">{proj.PROJ_NM}</span> - </div> + <span className="font-mono text-sm">{proj.PROJ_NO} [{proj.PROJ_NM}]</span> </Button> ))} {filteredProjects.length === 0 && ( @@ -243,11 +237,13 @@ export function SwpTableToolbar({ ) : ( <Input id="projNo" - placeholder="예: SN2190" + placeholder="계약된 프로젝트가 없습니다" value={localFilters.projNo || ""} onChange={(e) => setLocalFilters({ ...localFilters, projNo: e.target.value }) } + disabled + className="bg-muted" /> )} </div> diff --git a/lib/swp/table/swp-table.tsx b/lib/swp/table/swp-table.tsx index 4024c711..9e8f7f6a 100644 --- a/lib/swp/table/swp-table.tsx +++ b/lib/swp/table/swp-table.tsx @@ -28,7 +28,6 @@ interface SwpTableProps { pageSize: number; totalPages: number; onPageChange: (page: number) => void; - mode?: "admin" | "vendor"; } export function SwpTable({ @@ -38,7 +37,6 @@ export function SwpTable({ pageSize, totalPages, onPageChange, - mode = "admin", }: SwpTableProps) { const [expanded, setExpanded] = useState<ExpandedState>({}); const [revisionData, setRevisionData] = useState<Record<string, RevisionRow[]>>({}); diff --git a/lib/swp/vendor-actions.ts b/lib/swp/vendor-actions.ts index 7d6dfa85..c96cf055 100644 --- a/lib/swp/vendor-actions.ts +++ b/lib/swp/vendor-actions.ts @@ -6,9 +6,14 @@ import db from "@/db/db"; import { vendors } from "@/db/schema/vendors"; import { contracts } from "@/db/schema/contract"; import { projects } from "@/db/schema/projects"; -import { swpDocumentFiles, swpDocumentRevisions } from "@/db/schema/SWP/swp-documents"; -import { eq, and, sql } from "drizzle-orm"; +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"; // ============================================================================ // 벤더 세션 정보 조회 @@ -22,16 +27,22 @@ interface VendorSessionInfo { } export async function getVendorSessionInfo(): Promise<VendorSessionInfo | null> { + 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' + 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({ @@ -43,16 +54,22 @@ export async function getVendorSessionInfo(): Promise<VendorSessionInfo | null> .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; } - return { + const result = { vendorId: vendor[0].id, vendorCode: vendor[0].vendorCode, vendorName: vendor[0].vendorName, companyId, }; + + debugSuccess("벤더 세션 정보 조회 성공", { vendorCode: result.vendorCode }); + return result; } // ============================================================================ @@ -60,28 +77,33 @@ export async function getVendorSessionInfo(): Promise<VendorSessionInfo | null> // ============================================================================ 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, - contract_count: sql<number>`COUNT(DISTINCT ${contracts.id})::int`, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) .where(eq(contracts.vendorId, vendorInfo.vendorId)) - .groupBy(projects.id, projects.code, projects.name) - .orderBy(sql`COUNT(DISTINCT ${contracts.id}) DESC`); + .orderBy(projects.code); + debugSuccess("프로젝트 목록 조회 성공", { count: vendorProjects.length }); return vendorProjects; } catch (error) { + debugError("프로젝트 목록 조회 실패", error); console.error("[fetchVendorProjects] 오류:", error); return []; } @@ -92,10 +114,13 @@ export async function fetchVendorProjects() { // ============================================================================ export async function fetchVendorDocuments(params: SwpTableParams) { + debugProcess("벤더 문서 목록 조회 시작", { page: params.page, pageSize: params.pageSize }); + try { const vendorInfo = await getVendorSessionInfo(); - + if (!vendorInfo) { + debugError("벤더 정보 없음 - 문서 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } @@ -108,11 +133,17 @@ export async function fetchVendorDocuments(params: SwpTableParams) { }, }; + debugLog("SWP 문서 조회 호출", { vendorCode: vendorInfo.vendorCode, filters: vendorParams.filters }); + // 기존 fetchSwpDocuments 재사용 - return await fetchSwpDocuments(vendorParams); + 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("문서 목록 조회 실패"); + throw new Error("문서 목록 조회 실패 [담당자에게 문의하세요]"); } } @@ -130,25 +161,30 @@ export interface FileUploadParams { 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, file } = params; + const { revisionId } = params; + debugLog("리비전 권한 확인 시작", { revisionId }); // 1. 해당 리비전이 벤더에게 제공된 문서인지 확인 const revisionCheck = await db .select({ DOC_NO: swpDocumentRevisions.DOC_NO, VNDR_CD: sql<string>`( - SELECT d."VNDR_CD" - FROM swp.swp_documents d + SELECT d."VNDR_CD" + FROM swp.swp_documents d WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} )`, }) @@ -156,56 +192,51 @@ export async function uploadFileToRevision(params: FileUploadParams) { .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("이 문서에 대한 권한이 없습니다."); } - // 2. 파일 정보 저장 (upsert) - const existingFile = await db - .select({ id: swpDocumentFiles.id }) - .from(swpDocumentFiles) - .where( - and( - eq(swpDocumentFiles.revision_id, revisionId), - eq(swpDocumentFiles.FILE_SEQ, file.FILE_SEQ) - ) - ) - .limit(1); + debugSuccess("리비전 권한 확인 성공"); - if (existingFile[0]) { - // 업데이트 - await db.execute(sql` - UPDATE swp.swp_document_files - SET - "FILE_NM" = ${file.FILE_NM}, - "FILE_SZ" = ${file.FILE_SZ}, - "FLD_PATH" = ${file.FLD_PATH}, - "STAT" = ${file.STAT || null}, - "STAT_NM" = ${file.STAT_NM || null}, - sync_status = 'synced', - updated_at = NOW() - WHERE id = ${existingFile[0].id} - `); - - return { success: true, fileId: existingFile[0].id, action: "updated" }; - } else { - // 삽입 - const result = await db.execute<{ id: number }>(sql` - INSERT INTO swp.swp_document_files - (revision_id, "FILE_NM", "FILE_SEQ", "FILE_SZ", "FLD_PATH", "STAT", "STAT_NM", sync_status) - VALUES - (${revisionId}, ${file.FILE_NM}, ${file.FILE_SEQ}, ${file.FILE_SZ}, ${file.FLD_PATH}, ${file.STAT || null}, ${file.STAT_NM || null}, 'synced') - RETURNING id - `); - - return { success: true, fileId: result.rows[0].id, action: "created" }; - } + 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 : "파일 업로드 실패" @@ -218,10 +249,13 @@ export async function uploadFileToRevision(params: FileUploadParams) { // ============================================================================ export async function fetchVendorSwpStats(projNo?: string) { + debugProcess("벤더 통계 조회 시작", { projNo }); + try { const vendorInfo = await getVendorSessionInfo(); - + if (!vendorInfo) { + debugError("벤더 정보 없음 - 통계 조회 실패"); throw new Error("벤더 정보를 찾을 수 없습니다."); } @@ -233,6 +267,8 @@ export async function fetchVendorSwpStats(projNo?: string) { whereConditions.push(sql`d."PROJ_NO" = ${projNo}`); } + debugLog("통계 SQL 실행", { vendorCode: vendorInfo.vendorCode, projNo }); + const stats = await db.execute<{ total_documents: number; total_revisions: number; @@ -240,7 +276,7 @@ export async function fetchVendorSwpStats(projNo?: string) { uploaded_files: number; last_sync: Date | null; }>(sql` - SELECT + 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, @@ -252,14 +288,24 @@ export async function fetchVendorSwpStats(projNo?: string) { WHERE ${sql.join(whereConditions, sql` AND `)} `); - return stats.rows[0] || { + 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, @@ -271,3 +317,246 @@ export async function fetchVendorSwpStats(projNo?: string) { } } +// ============================================================================ +// SWP 파일 업로드 헬퍼 함수들 +// ============================================================================ + +/** + * 1. SWP 마운트 경로에 파일 저장 + */ +async function saveFileToSwpNetwork( + revisionId: number, + fileInfo: { FILE_NM: string; fileBuffer?: Buffer } +): Promise<string> { + debugProcess("네트워크 파일 저장 시작", { revisionId, fileName: fileInfo.FILE_NM }); + + // 리비전 정보 조회 + const revisionInfo = await db + .select({ + DOC_NO: swpDocumentRevisions.DOC_NO, + REV_NO: swpDocumentRevisions.REV_NO, + PROJ_NO: sql<string>`( + SELECT d."PROJ_NO" FROM swp.swp_documents d + WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} + )`, + VNDR_CD: sql<string>`( + 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<void> { + 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<SwpFileApiResponse[]> { + debugProcess("업데이트된 파일 목록 조회 시작", { revisionId }); + + // 리비전 정보 조회 + const revisionInfo = await db + .select({ + DOC_NO: swpDocumentRevisions.DOC_NO, + PROJ_NO: sql<string>`( + SELECT d."PROJ_NO" FROM swp.swp_documents d + WHERE d."DOC_NO" = ${swpDocumentRevisions.DOC_NO} + )`, + VNDR_CD: sql<string>`( + 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<void> { + 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 + }); +} + |
