summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/[lng]/partners/(partners)/swp-document-upload/page.tsx20
-rw-r--r--app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx22
-rw-r--r--lib/swp/actions.ts4
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx36
-rw-r--r--lib/swp/table/swp-table.tsx2
-rw-r--r--lib/swp/vendor-actions.ts405
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
+ });
+}
+