summaryrefslogtreecommitdiff
path: root/lib/swp
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-24 15:18:36 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-24 15:18:36 +0900
commit3e85fbde628bb556e5f6a93cafd9ba4d53031461 (patch)
tree165726e29fe8a11a437dea06217ec4b202219e23 /lib/swp
parent912e2bc761d7d57bd33d68cb5050da43dea59636 (diff)
(김준회) fix: SWP 초기 개발건 오류 수정
Diffstat (limited to 'lib/swp')
-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
4 files changed, 365 insertions, 82 deletions
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
+ });
+}
+