import { NextRequest, NextResponse } from "next/server"; const DOLCE_API_URL = process.env.DOLCE_API_URL || "http://60.100.99.217:1111"; // Next.js API Route 설정 export const maxDuration = 3600; // 1시간 (대용량 파일 업로드 지원) export const dynamic = "force-dynamic"; /** * 파일을 DOLCE API로 업로드 */ async function uploadFileToDolce( file: File, uploadId: string, fileId: string ): Promise { const uploadUrl = `${DOLCE_API_URL}/PWPUploadService.ashx?UploadId=${uploadId}&FileId=${fileId}`; const startTime = Date.now(); console.log(`[Proxy] 파일 업로드 시작: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`); try { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, 3600000); // 1시간 타임아웃 const uploadResponse = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": "application/octet-stream", "Content-Length": file.size.toString(), }, body: file, // File 객체를 바로 전송 (자동으로 스트리밍됨) signal: controller.signal, // @ts-expect-error - duplex is required for streaming uploads duplex: "half", }); clearTimeout(timeoutId); const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`[Proxy] DOLCE API 응답: ${file.name} (${elapsed}초, HTTP ${uploadResponse.status})`); if (!uploadResponse.ok) { const errorText = await uploadResponse.text(); console.error(`[Proxy] DOLCE API 실패 (${file.name}): HTTP ${uploadResponse.status}`, errorText); throw new Error( `파일 업로드 실패 (${file.name}): ${uploadResponse.status} ${uploadResponse.statusText}` ); } const fileRelativePath = await uploadResponse.text(); if (!fileRelativePath || fileRelativePath.trim() === "") { console.error(`[Proxy] DOLCE API 빈 경로 반환 (${file.name})`); throw new Error(`파일 업로드 실패: 빈 경로 반환 (${file.name})`); } const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1); const speed = (file.size / 1024 / 1024 / (Date.now() - startTime) * 1000).toFixed(2); console.log(`[Proxy] 업로드 완료: ${file.name} (${totalElapsed}초, ${speed}MB/s) → ${fileRelativePath}`); return fileRelativePath; } catch (error) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); if (error instanceof Error && error.name === "AbortError") { console.error(`[Proxy] 타임아웃 (${elapsed}초): ${file.name}`); throw new Error(`업로드 타임아웃 (1시간 초과): ${file.name}`); } console.error(`[Proxy] 업로드 에러 (${elapsed}초): ${file.name}`, error); throw error; } } /** * 기존 파일 목록 조회 */ async function getExistingFileSeq(uploadId: string): Promise { try { const response = await fetch( `${DOLCE_API_URL}/Services/VDCSWebService.svc/FileInfoList`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uploadId: uploadId }), } ); if (response.ok) { const data = await response.json(); let maxSeq = 0; // FileInfoListResult가 배열인지 확인하고 최대 FileSeq 탐색 if (data.FileInfoListResult && Array.isArray(data.FileInfoListResult)) { data.FileInfoListResult.forEach((item: any) => { // FileSeq가 문자열로 오므로 숫자로 변환 (예: "6") const seq = parseInt(item.FileSeq, 10); if (!isNaN(seq) && seq > maxSeq) { maxSeq = seq; } }); } const nextSeq = maxSeq + 1; console.log(`[Proxy] 기존 파일 최대 Seq=${maxSeq}, 새 파일 시작 Seq=${nextSeq}`); return nextSeq; } else { console.warn(`[Proxy] FileInfoList 조회 실패, startSeq=1로 시작`); return 1; } } catch (error) { console.warn(`[Proxy] FileInfoList 조회 에러:`, error); return 1; } } /** * 업로드 완료 통지 (DB 저장) */ async function notifyUploadComplete(uploadResults: Array<{ FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; FileWriteDT: string; OwnerUserId: string; }>): Promise { console.log(`\n[Proxy] ========================================`); console.log(`[Proxy] 업로드 완료 통지 시작 (DB 저장)`); console.log(`[Proxy] PWPUploadResultService 호출: ${uploadResults.length}개 파일 메타데이터 전송`); console.log(`[Proxy] 전송 데이터:`, JSON.stringify(uploadResults, null, 2)); const resultServiceUrl = `${DOLCE_API_URL}/PWPUploadResultService.ashx?`; console.log(`[Proxy] 요청 URL: ${resultServiceUrl}`); const resultResponse = await fetch(resultServiceUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(uploadResults), }); console.log(`[Proxy] PWPUploadResultService HTTP 상태: ${resultResponse.status}`); if (!resultResponse.ok) { const errorText = await resultResponse.text(); console.error(`[Proxy] PWPUploadResultService 실패: HTTP ${resultResponse.status}`, errorText); throw new Error(`업로드 완료 통지 실패: ${resultResponse.status}`); } const resultText = await resultResponse.text(); console.log(`[Proxy] PWPUploadResultService 응답: "${resultText}"`); if (resultText !== "Success") { console.error(`[Proxy] PWPUploadResultService 예상치 못한 응답: "${resultText}"`); throw new Error(`업로드 완료 통지 실패: ${resultText}`); } console.log(`[Proxy] ✅ DB 저장 완료!`); console.log(`[Proxy] ========================================\n`); } /** * 상세도면 파일 업로드 API * * 간단하고 효율적인 구현: * - Next.js의 네이티브 formData API 사용 * - File 객체를 바로 DOLCE API로 전송 (자동 스트리밍) * - 복잡한 이벤트 핸들링 없음 */ export async function POST(request: NextRequest) { try { console.log("[Proxy] 업로드 요청 수신"); // FormData 파싱 (Next.js 네이티브) const formData = await request.formData(); const uploadId = formData.get("uploadId") as string; const userId = formData.get("userId") as string; if (!uploadId || !userId) { return NextResponse.json( { success: false, error: "필수 파라미터가 누락되었습니다" }, { status: 400 } ); } // 파일 수집 const files: File[] = []; for (const [, value] of formData.entries()) { if (value instanceof File) { files.push(value); } } if (files.length === 0) { return NextResponse.json( { success: false, error: "업로드된 파일이 없습니다" }, { status: 400 } ); } console.log(`[Proxy] 총 ${files.length}개 파일 업로드 시작`); // 기존 파일 Seq 조회 const startSeq = await getExistingFileSeq(uploadId); // 파일 업로드 결과 const uploadResults: Array<{ FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; FileWriteDT: string; OwnerUserId: string; }> = []; // 순차 업로드 for (let i = 0; i < files.length; i++) { const file = files[i]; const fileId = crypto.randomUUID(); console.log(`[Proxy] 파일 ${i + 1}/${files.length}: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`); const fileRelativePath = await uploadFileToDolce(file, uploadId, fileId); uploadResults.push({ FileId: fileId, UploadId: uploadId, FileSeq: startSeq + i, FileName: file.name, FileRelativePath: fileRelativePath, FileSize: file.size, FileCreateDT: new Date().toISOString(), FileWriteDT: new Date().toISOString(), OwnerUserId: userId, }); } console.log(`\n[Proxy] 모든 파일 업로드 완료, DB 저장 시작...`); // 업로드 완료 통지 (DB 저장) await notifyUploadComplete(uploadResults); console.log(`[Proxy] ✅ 전체 프로세스 완료: ${uploadResults.length}개 파일 업로드 및 DB 저장 성공`); return NextResponse.json({ success: true, uploadedCount: uploadResults.length, }); } catch (error) { console.error("[Proxy] ❌ 업로드 실패:", error); if (error instanceof Error) { console.error("[Proxy] 에러 스택:", error.stack); } return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다", }, { status: 500 } ); } }