import { NextRequest, NextResponse } from "next/server"; import fs from "fs/promises"; import { createReadStream } from "fs"; import path from "path"; import os from "os"; const DOLCE_API_URL = process.env.DOLCE_API_URL || "http://60.100.99.217:1111"; /** * 임시 파일 저장 및 정리 헬퍼 */ async function saveToTempFile(file: File): Promise<{ filepath: string; cleanup: () => Promise }> { const tempDir = os.tmpdir(); const tempFilePath = path.join(tempDir, `upload-${Date.now()}-${file.name}`); const arrayBuffer = await file.arrayBuffer(); await fs.writeFile(tempFilePath, Buffer.from(arrayBuffer)); return { filepath: tempFilePath, cleanup: async () => { try { await fs.unlink(tempFilePath); } catch (error) { console.error(`임시 파일 삭제 실패: ${tempFilePath}`, error); } }, }; } /** * 스트리밍 방식으로 파일 업로드 * Node.js ReadableStream을 Web ReadableStream으로 변환하여 fetch 사용 */ async function uploadFileStream( filepath: string, uploadId: string, fileId: string, fileSize: number ): Promise { const uploadUrl = `${DOLCE_API_URL}/PWPUploadService.ashx?UploadId=${uploadId}&FileId=${fileId}`; // Node.js ReadableStream 생성 const nodeStream = createReadStream(filepath, { highWaterMark: 64 * 1024, // 64KB 청크로 읽기 }); // Node.js Stream을 Web ReadableStream으로 변환 const webStream = new ReadableStream({ start(controller) { nodeStream.on("data", (chunk: Buffer) => { controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)); }); nodeStream.on("end", () => { controller.close(); }); nodeStream.on("error", (error) => { controller.error(error); }); }, cancel() { nodeStream.destroy(); }, }); // 스트리밍 업로드 const uploadResponse = await fetch(uploadUrl, { method: "POST", headers: { "Content-Type": "application/octet-stream", "Content-Length": fileSize.toString(), }, body: webStream as unknown as BodyInit, // @ts-expect-error - duplex is required for streaming uploads with ReadableStream duplex: "half", }); if (!uploadResponse.ok) { throw new Error( `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}` ); } const fileRelativePath = await uploadResponse.text(); return fileRelativePath; } /** * 상세도면 파일 업로드 API * 스트리밍 처리로 메모리 효율적 업로드 */ export async function POST(request: NextRequest) { const tempFiles: Array<{ filepath: string; cleanup: () => Promise }> = []; try { // FormData 파싱 const formData = await request.formData(); const uploadId = formData.get("uploadId") as string; const userId = formData.get("userId") as string; const fileCount = parseInt(formData.get("fileCount") as string); if (!uploadId || !userId || !fileCount) { return NextResponse.json( { success: false, error: "필수 파라미터가 누락되었습니다" }, { status: 400 } ); } const uploadResults: Array<{ FileId: string; UploadId: string; FileSeq: number; FileName: string; FileRelativePath: string; FileSize: number; FileCreateDT: string; FileWriteDT: string; OwnerUserId: string; }> = []; // 기존 파일 개수 조회 const existingFilesResponse = await fetch( `${DOLCE_API_URL}/PorjectWebProxyService.ashx?service=FileInfoList`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ uploadId: uploadId, }), } ); if (!existingFilesResponse.ok) { throw new Error("기존 파일 조회 실패"); } const existingFilesData = await existingFilesResponse.json(); const startSeq = (existingFilesData.FileInfoListResult?.length || 0) + 1; // 파일 수집 const files: File[] = []; for (let i = 0; i < fileCount; i++) { const file = formData.get(`file_${i}`) as File; if (file) { files.push(file); } } // 각 파일을 임시 디렉터리에 저장 후 스트리밍 업로드 for (let i = 0; i < files.length; i++) { const file = files[i]; const fileId = crypto.randomUUID(); // 임시 파일로 저장 (메모리 압박 감소) const tempFile = await saveToTempFile(file); tempFiles.push(tempFile); // 스트리밍 방식으로 DOLCE API에 업로드 const fileRelativePath = await uploadFileStream( tempFile.filepath, uploadId, fileId, file.size ); 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, }); // 처리 완료된 임시 파일 즉시 삭제 await tempFile.cleanup(); } // 업로드 완료 통지 const resultServiceUrl = `${DOLCE_API_URL}/PWPUploadResultService.ashx?`; const resultResponse = await fetch(resultServiceUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(uploadResults), }); if (!resultResponse.ok) { throw new Error( `업로드 완료 통지 실패: ${resultResponse.status} ${resultResponse.statusText}` ); } const resultText = await resultResponse.text(); if (resultText !== "Success") { throw new Error(`업로드 완료 통지 실패: ${resultText}`); } return NextResponse.json({ success: true, uploadedCount: uploadResults.length, }); } catch (error) { console.error("파일 업로드 실패:", error); // 에러 발생 시 남아있는 임시 파일 모두 정리 for (const tempFile of tempFiles) { await tempFile.cleanup(); } return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다", }, { status: 500 } ); } }