diff options
Diffstat (limited to 'app/api/dolce/upload-files')
| -rw-r--r-- | app/api/dolce/upload-files/route.ts | 229 |
1 files changed, 229 insertions, 0 deletions
diff --git a/app/api/dolce/upload-files/route.ts b/app/api/dolce/upload-files/route.ts new file mode 100644 index 00000000..1d302cb2 --- /dev/null +++ b/app/api/dolce/upload-files/route.ts @@ -0,0 +1,229 @@ +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<void> }> { + 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<string> { + 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<void> }> = []; + + 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 } + ); + } +} |
