summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/api/swp/download/[fileId]/route.ts160
-rw-r--r--app/api/swp/upload/route.ts138
2 files changed, 111 insertions, 187 deletions
diff --git a/app/api/swp/download/[fileId]/route.ts b/app/api/swp/download/[fileId]/route.ts
index 3af560aa..fe422015 100644
--- a/app/api/swp/download/[fileId]/route.ts
+++ b/app/api/swp/download/[fileId]/route.ts
@@ -1,9 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
-import * as fs from "fs/promises";
-import * as path from "path";
-import { eq } from "drizzle-orm";
-import db from "@/db/db";
-import { swpDocumentFiles } from "@/db/schema/SWP/swp-documents";
+import { downloadDocumentFile } from "@/lib/swp/document-service";
import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils";
// API Route 설정
@@ -11,116 +7,67 @@ export const runtime = "nodejs";
export const maxDuration = 60; // 1분 타임아웃
/**
- * GET /api/swp/download/[fileId]
- * 파일 다운로드 (바이너리 직접 전송)
+ * GET /api/swp/download/[ownDocNo]?projNo=xxx&fileName=xxx
+ * 파일 다운로드 (Full API 기반)
+ *
+ * Query Parameters:
+ * - projNo: 프로젝트 번호
+ * - fileName: 파일명
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ fileId: string }> }
) {
try {
- const { fileId: fileIdString } = await params;
- const fileId = parseInt(fileIdString, 10);
-
- if (isNaN(fileId)) {
- return NextResponse.json(
- { success: false, error: "잘못된 파일 ID입니다." },
- { status: 400 }
- );
- }
-
- debugLog(`[download] 다운로드 시작`, { fileId });
+ const { fileId: ownDocNo } = await params;
+ const { searchParams } = new URL(request.url);
+ const projNo = searchParams.get("projNo");
+ const fileName = searchParams.get("fileName");
- // 1. 파일 정보 조회
- const fileInfo = await db
- .select({
- FILE_NM: swpDocumentFiles.FILE_NM,
- FLD_PATH: swpDocumentFiles.FLD_PATH,
- })
- .from(swpDocumentFiles)
- .where(eq(swpDocumentFiles.id, fileId))
- .limit(1);
+ debugLog(`[download] 다운로드 시작`, { projNo, ownDocNo, fileName });
- if (!fileInfo || fileInfo.length === 0) {
- debugError(`[download] 파일 정보 없음`, { fileId });
+ // 파라미터 검증
+ if (!projNo || !ownDocNo || !fileName) {
+ debugError(`[download] 필수 파라미터 누락`, { projNo, ownDocNo, fileName });
return NextResponse.json(
- { success: false, error: "파일 정보를 찾을 수 없습니다." },
- { status: 404 }
- );
- }
-
- const { FILE_NM, FLD_PATH } = fileInfo[0];
- debugLog(`[download] 파일 정보 조회 완료`, { FILE_NM, FLD_PATH });
-
- if (!FLD_PATH || !FILE_NM) {
- debugError(`[download] 파일 경로 또는 이름 없음`, { FILE_NM, FLD_PATH });
- return NextResponse.json(
- { success: false, error: "파일 경로 또는 파일명이 없습니다." },
- { status: 404 }
- );
- }
-
- // 2. NFS 마운트 경로 확인
- const nfsBasePath = process.env.SWP_MOUNT_DIR;
- if (!nfsBasePath) {
- debugError(`[download] SWP_MOUNT_DIR 환경변수가 설정되지 않았습니다.`);
- return NextResponse.json(
- { success: false, error: "서버 설정 오류: NFS 경로가 설정되지 않았습니다." },
- { status: 500 }
+ {
+ success: false,
+ error: "필수 파라미터가 누락되었습니다. (projNo, ownDocNo, fileName)"
+ },
+ { status: 400 }
);
}
- // 3. 전체 파일 경로 생성
- const normalizedFldPath = FLD_PATH.replace(/\\/g, '/');
- const fullPath = path.join(nfsBasePath, normalizedFldPath, FILE_NM);
+ // document-service의 downloadDocumentFile 사용
+ const result = await downloadDocumentFile(projNo, ownDocNo, fileName);
- debugLog(`[download] 파일 경로`, {
- fileId,
- FILE_NM,
- FLD_PATH,
- normalizedFldPath,
- fullPath,
- });
-
- // 4. 파일 존재 여부 확인
- try {
- await fs.access(fullPath, fs.constants.R_OK);
- } catch (accessError) {
- debugError(`[download] 파일 접근 불가`, { fullPath, error: accessError });
+ if (!result.success || !result.data) {
+ debugError(`[download] 다운로드 실패`, { error: result.error });
return NextResponse.json(
- { success: false, error: `파일을 찾을 수 없습니다: ${FILE_NM}` },
+ {
+ success: false,
+ error: result.error || "파일 다운로드 실패"
+ },
{ status: 404 }
);
}
- // 5. 파일 읽기
- debugLog(`[download] 파일 읽기 시작`, { fullPath });
- const fileBuffer = await fs.readFile(fullPath);
-
- debugLog(`[download] 파일 Buffer 읽기 완료`, {
- bufferLength: fileBuffer.length,
- isBuffer: Buffer.isBuffer(fileBuffer),
- bufferType: typeof fileBuffer,
- constructor: fileBuffer.constructor.name,
- first20Bytes: fileBuffer.slice(0, 20).toString('hex')
- });
-
- // 6. MIME 타입 결정
- const mimeType = getMimeType(FILE_NM);
-
debugSuccess(`[download] 다운로드 성공`, {
- fileName: FILE_NM,
- size: fileBuffer.length,
- mimeType,
+ fileName: result.fileName,
+ size: result.data.length,
+ mimeType: result.mimeType,
});
- // 7. 바이너리 응답 반환
- return new NextResponse(fileBuffer, {
+ // Uint8Array를 Buffer로 변환
+ const buffer = Buffer.from(result.data);
+
+ // 바이너리 응답 반환
+ return new NextResponse(buffer, {
status: 200,
headers: {
- "Content-Type": mimeType,
- "Content-Disposition": `attachment; filename="${encodeURIComponent(FILE_NM)}"`,
- "Content-Length": String(fileBuffer.length),
+ "Content-Type": result.mimeType || "application/octet-stream",
+ "Content-Disposition": `attachment; filename="${encodeURIComponent(result.fileName || fileName)}"`,
+ "Content-Length": String(buffer.length),
},
});
} catch (error) {
@@ -140,33 +87,4 @@ export async function GET(
}
}
-/**
- * MIME 타입 결정
- */
-function getMimeType(fileName: string): string {
- const ext = path.extname(fileName).toLowerCase();
-
- const mimeTypes: Record<string, string> = {
- ".pdf": "application/pdf",
- ".doc": "application/msword",
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ".xls": "application/vnd.ms-excel",
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- ".ppt": "application/vnd.ms-powerpoint",
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
- ".txt": "text/plain",
- ".csv": "text/csv",
- ".jpg": "image/jpeg",
- ".jpeg": "image/jpeg",
- ".png": "image/png",
- ".gif": "image/gif",
- ".zip": "application/zip",
- ".rar": "application/x-rar-compressed",
- ".7z": "application/x-7z-compressed",
- ".dwg": "application/acad",
- ".dxf": "application/dxf",
- };
-
- return mimeTypes[ext] || "application/octet-stream";
-}
diff --git a/app/api/swp/upload/route.ts b/app/api/swp/upload/route.ts
index b38c4ff4..3e15e0a3 100644
--- a/app/api/swp/upload/route.ts
+++ b/app/api/swp/upload/route.ts
@@ -1,11 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import * as fs from "fs/promises";
import * as path from "path";
-import { eq, and } from "drizzle-orm";
-import db from "@/db/db";
-import { swpDocuments } from "@/db/schema/SWP/swp-documents";
import { fetchGetVDRDocumentList, fetchGetExternalInboxList } from "@/lib/swp/api-client";
-import { syncSwpProject } from "@/lib/swp/sync-service";
import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils";
// API Route 설정
@@ -26,21 +22,27 @@ interface InBoxFileInfo {
}
/**
- * 파일명 파싱: [DOC_NO]_[REV_NO]_[STAGE]_[자유-파일명].[확장자]
- * 자유 파일명에는 언더스코어가 포함될 수 있음
+ * 파일명 파싱: [DOC_NO]_[REV_NO]_[STAGE].[확장자] 또는 [DOC_NO]_[REV_NO]_[STAGE]_[자유-파일명].[확장자]
+ * 자유 파일명은 선택사항이며, 포함될 경우 언더스코어를 포함할 수 있음
*/
function parseFileName(fileName: string) {
const lastDotIndex = fileName.lastIndexOf(".");
- const extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex + 1) : "";
- const nameWithoutExt = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
+
+ // 확장자 검증
+ if (lastDotIndex === -1) {
+ throw new Error(`파일 확장자가 없습니다: ${fileName}`);
+ }
+
+ const extension = fileName.substring(lastDotIndex + 1);
+ const nameWithoutExt = fileName.substring(0, lastDotIndex);
const parts = nameWithoutExt.split("_");
- // 최소 4개 파트 필요: docNo, revNo, stage, fileName
- if (parts.length < 4) {
+ // 최소 3개 파트 필요: docNo, revNo, stage (fileName은 선택사항)
+ if (parts.length < 3) {
throw new Error(
`잘못된 파일명 형식입니다: ${fileName}. ` +
- `형식: [DOC_NO]_[REV_NO]_[STAGE]_[파일명].확장자 (언더스코어 최소 3개 필요)`
+ `형식: [DOC_NO]_[REV_NO]_[STAGE].[확장자] (언더스코어 최소 2개 필요)`
);
}
@@ -49,10 +51,29 @@ function parseFileName(fileName: string) {
const revNo = parts[1];
const stage = parts[2];
- // 나머지는 자유 파일명 (언더스코어 포함 가능)
- const customFileName = parts.slice(3).join("_");
+ // 나머지는 자유 파일명 (선택사항, 언더스코어 포함 가능)
+ const customFileName = parts.length > 3 ? parts.slice(3).join("_") : "";
+
+ // 필수 항목이 비어있지 않은지 확인
+ if (!ownDocNo || ownDocNo.trim() === "") {
+ throw new Error(`문서번호(DOC_NO)가 비어있습니다: ${fileName}`);
+ }
+
+ if (!revNo || revNo.trim() === "") {
+ throw new Error(`리비전 번호(REV_NO)가 비어있습니다: ${fileName}`);
+ }
+
+ if (!stage || stage.trim() === "") {
+ throw new Error(`스테이지(STAGE)가 비어있습니다: ${fileName}`);
+ }
- return { ownDocNo, revNo, stage, fileName: customFileName, extension };
+ return {
+ ownDocNo: ownDocNo.trim(),
+ revNo: revNo.trim(),
+ stage: stage.trim(),
+ fileName: customFileName.trim(),
+ extension
+ };
}
/**
@@ -71,22 +92,43 @@ function generateTimestamp(): string {
}
/**
- * CPY_CD 조회
+ * CPY_CD 조회 (API 기반)
+ * GetVDRDocumentList API를 호출하여 해당 프로젝트/벤더의 CPY_CD를 조회
*/
async function getCpyCdForVendor(projNo: string, vndrCd: string): Promise<string> {
- const result = await db
- .select({ CPY_CD: swpDocuments.CPY_CD })
- .from(swpDocuments)
- .where(and(eq(swpDocuments.PROJ_NO, projNo), eq(swpDocuments.VNDR_CD, vndrCd)))
- .limit(1);
+ try {
+ console.log(`[getCpyCdForVendor] API 조회 시작: projNo=${projNo}, vndrCd=${vndrCd}`);
+
+ // GetVDRDocumentList API 호출 (벤더 필터 적용)
+ const documents = await fetchGetVDRDocumentList({
+ proj_no: projNo,
+ doc_gb: "V",
+ vndrCd: vndrCd,
+ });
- if (!result || result.length === 0 || !result[0].CPY_CD) {
- throw new Error(
- `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.`
- );
- }
+ console.log(`[getCpyCdForVendor] API 조회 완료: ${documents.length}개 문서`);
- return result[0].CPY_CD;
+ if (!documents || documents.length === 0) {
+ throw new Error(
+ `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 할당된 문서가 없습니다.`
+ );
+ }
+
+ // 첫 번째 문서에서 CPY_CD 추출
+ const cpyCd = documents[0].CPY_CD;
+
+ if (!cpyCd) {
+ throw new Error(
+ `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.`
+ );
+ }
+
+ console.log(`[getCpyCdForVendor] CPY_CD 확인: ${cpyCd}`);
+ return cpyCd;
+ } catch (error) {
+ console.error("[getCpyCdForVendor] 오류:", error);
+ throw error;
+ }
}
/**
@@ -308,39 +350,8 @@ export async function POST(request: NextRequest) {
await callSaveInBoxList(inBoxFileInfos);
}
- // 업로드 성공 후 동기화 처리 (현재 벤더의 변경사항만)
- if (result.successCount > 0) {
- try {
- console.log(`[upload] 동기화 시작: projNo=${projNo}, vndrCd=${vndrCd}`);
-
- // GetVDRDocumentList 및 GetExternalInboxList API 호출 (벤더 필터 적용)
- const [documents, files] = await Promise.all([
- fetchGetVDRDocumentList({
- proj_no: projNo,
- doc_gb: "V",
- vndrCd: vndrCd, // 현재 벤더만 필터링
- }),
- fetchGetExternalInboxList({
- projNo: projNo,
- vndrCd: vndrCd, // 현재 벤더만 필터링
- }),
- ]);
-
- console.log(`[upload] API 조회 완료: 문서 ${documents.length}개, 파일 ${files.length}개`);
-
- // 동기화 실행
- const syncResult = await syncSwpProject(projNo, documents, files);
-
- if (syncResult.success) {
- console.log(`[upload] 동기화 완료:`, syncResult.stats);
- } else {
- console.warn(`[upload] 동기화 경고:`, syncResult.errors);
- }
- } catch (syncError) {
- // 동기화 실패는 경고로만 처리 (업로드 자체는 성공)
- console.error("[upload] 동기화 실패 (업로드는 성공):", syncError);
- }
- }
+ // ⚠️ Full API 방식으로 전환했으므로 로컬 DB 동기화는 불필요
+ // 업로드 성공 시 SaveInBoxList API 호출만으로 충분 (이미 위에서 완료)
// 결과 메시지 생성
let message: string;
@@ -348,7 +359,7 @@ export async function POST(request: NextRequest) {
if (result.failedCount === 0) {
success = true;
- message = `${result.successCount}개 파일이 성공적으로 업로드 및 동기화되었습니다.`;
+ message = `${result.successCount}개 파일이 성공적으로 업로드되었습니다.`;
} else if (result.successCount === 0) {
success = false;
message = `모든 파일 업로드에 실패했습니다. (${result.failedCount}개)`;
@@ -359,18 +370,13 @@ export async function POST(request: NextRequest) {
console.log(`[upload] 완료:`, { success, message, result });
- // 동기화 완료 정보 추가
- const syncCompleted = result.successCount > 0;
- const syncTimestamp = new Date().toISOString();
-
return NextResponse.json({
success,
message,
successCount: result.successCount,
failedCount: result.failedCount,
details: result.details,
- syncCompleted,
- syncTimestamp,
+ uploadTimestamp: new Date().toISOString(),
affectedVndrCd: vndrCd,
});
} catch (error) {