summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-24 10:41:46 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-24 10:41:46 +0900
commit26365ef08588d53b8c5d9c7cfaefb244536e6743 (patch)
tree979841b791d1c3925eb1c3efa3f901f2c0bef193 /app
parentfd4909bba7be8abc1eeab9ae1b4621c66a61604a (diff)
(김준회) 돌체 재개발 - 2차: 업로드 개선, 다운로드 오류 수정
Diffstat (limited to 'app')
-rw-r--r--app/api/dolce/upload-files/route.ts229
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 }
+ );
+ }
+}