summaryrefslogtreecommitdiff
path: root/app/api/dolce/upload-files/route.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/dolce/upload-files/route.ts')
-rw-r--r--app/api/dolce/upload-files/route.ts418
1 files changed, 151 insertions, 267 deletions
diff --git a/app/api/dolce/upload-files/route.ts b/app/api/dolce/upload-files/route.ts
index e547f217..898f9b2a 100644
--- a/app/api/dolce/upload-files/route.ts
+++ b/app/api/dolce/upload-files/route.ts
@@ -1,120 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
-import formidable from "formidable";
-import { Readable } from "stream";
-import type { IncomingMessage } from "http";
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 maxDuration = 3600; // 1시간 (대용량 파일 업로드 지원)
export const dynamic = "force-dynamic";
-// body parser 설정 (대용량 파일 지원)
-export const config = {
- api: {
- bodyParser: false, // formidable이 직접 파싱하도록 비활성화
- responseLimit: false, // 응답 크기 제한 없음
- },
-};
-
-/**
- * NextRequest를 Node.js IncomingMessage 스타일로 변환
- * Next.js의 request.body를 직접 스트림으로 사용 (메모리 로드 없음)
- */
-function convertToNodeRequest(request: NextRequest) {
- // Next.js의 ReadableStream을 Node.js Readable로 변환
- const webReadableStream = request.body;
-
- if (!webReadableStream) {
- throw new Error("Request body is null");
- }
-
- // Web ReadableStream을 Node.js Readable로 변환
- // Node.js 17+의 Readable.fromWeb 사용
- // @ts-expect-error - Web Streams API와 Node.js Streams의 타입 차이
- const nodeReadable = Readable.fromWeb(webReadableStream);
-
- // Node.js IncomingMessage 필수 속성 추가
- const nodeRequest = nodeReadable as Readable & {
- headers: Record<string, string>;
- method: string;
- url: string;
- httpVersion: string;
- httpVersionMajor: number;
- httpVersionMinor: number;
- };
-
- nodeRequest.headers = Object.fromEntries(request.headers.entries());
- nodeRequest.method = request.method;
- nodeRequest.url = request.url;
- nodeRequest.httpVersion = "1.1";
- nodeRequest.httpVersionMajor = 1;
- nodeRequest.httpVersionMinor = 1;
-
- return nodeRequest;
-}
-
/**
- * 스트림을 DOLCE API로 즉시 전송 (임시 저장 없음)
+ * 파일을 DOLCE API로 업로드
*/
-async function uploadStreamToDolce(
- fileStream: NodeJS.ReadableStream,
- fileName: string,
- fileSize: number,
+async function uploadFileToDolce(
+ file: File,
uploadId: string,
fileId: string
): Promise<string> {
const uploadUrl = `${DOLCE_API_URL}/PWPUploadService.ashx?UploadId=${uploadId}&FileId=${fileId}`;
const startTime = Date.now();
- console.log(`[Proxy] 파일 스트리밍 시작: ${fileName} (${(fileSize / 1024 / 1024).toFixed(2)}MB)`);
-
- let bytesUploaded = 0;
- const logInterval = setInterval(() => {
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
- const progress = fileSize > 0 ? ((bytesUploaded / fileSize) * 100).toFixed(1) : "0.0";
- console.log(`[Proxy] 진행 중: ${fileName} - ${progress}% (${elapsed}초)`);
- }, 10000); // 10초마다 로그
-
- // 진행도 추적을 위한 PassThrough 스트림
- const { PassThrough } = await import("stream");
- const progressStream = new PassThrough();
-
- fileStream.on("data", (chunk: Buffer) => {
- bytesUploaded += chunk.length;
- });
-
- fileStream.pipe(progressStream);
-
- // Web ReadableStream으로 변환
- const webStream = new ReadableStream({
- start(controller) {
- progressStream.on("data", (chunk: Buffer) => {
- controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
- });
-
- progressStream.on("end", () => {
- clearInterval(logInterval);
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
- console.log(`[Proxy] 스트림 전송 완료: ${fileName} (${elapsed}초)`);
- controller.close();
- });
-
- progressStream.on("error", (error) => {
- clearInterval(logInterval);
- console.error(`[Proxy] 스트림 에러: ${fileName}`, error);
- controller.error(error);
- });
- },
- cancel() {
- clearInterval(logInterval);
- progressStream.destroy();
- // fileStream 정리
- if (fileStream && "destroy" in fileStream && typeof fileStream.destroy === "function") {
- fileStream.destroy();
- }
- },
- });
+ console.log(`[Proxy] 파일 업로드 시작: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
try {
const controller = new AbortController();
@@ -126,195 +29,147 @@ async function uploadStreamToDolce(
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
- "Content-Length": fileSize.toString(),
+ "Content-Length": file.size.toString(),
},
- body: webStream as unknown as BodyInit,
+ body: file, // File 객체를 바로 전송 (자동으로 스트리밍됨)
signal: controller.signal,
- // @ts-expect-error - duplex is required for streaming uploads with ReadableStream
+ // @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 응답: ${fileName} (${elapsed}초, HTTP ${uploadResponse.status})`);
+ console.log(`[Proxy] DOLCE API 응답: ${file.name} (${elapsed}초, HTTP ${uploadResponse.status})`);
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
- console.error(`[Proxy] DOLCE API 실패 (${fileName}): HTTP ${uploadResponse.status}`, errorText);
+ console.error(`[Proxy] DOLCE API 실패 (${file.name}): HTTP ${uploadResponse.status}`, errorText);
throw new Error(
- `파일 업로드 실패 (${fileName}): ${uploadResponse.status} ${uploadResponse.statusText}`
+ `파일 업로드 실패 (${file.name}): ${uploadResponse.status} ${uploadResponse.statusText}`
);
}
const fileRelativePath = await uploadResponse.text();
if (!fileRelativePath || fileRelativePath.trim() === "") {
- console.error(`[Proxy] DOLCE API 빈 경로 반환 (${fileName})`);
- throw new Error(`파일 업로드 실패: 빈 경로 반환 (${fileName})`);
+ console.error(`[Proxy] DOLCE API 빈 경로 반환 (${file.name})`);
+ throw new Error(`파일 업로드 실패: 빈 경로 반환 (${file.name})`);
}
const totalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
- const speed = (fileSize / 1024 / 1024 / (Date.now() - startTime) * 1000).toFixed(2);
- console.log(`[Proxy] 업로드 완료: ${fileName} (${totalElapsed}초, ${speed}MB/s) → ${fileRelativePath}`);
+ 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) {
- clearInterval(logInterval);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (error instanceof Error && error.name === "AbortError") {
- console.error(`[Proxy] 타임아웃 (${elapsed}초): ${fileName}`);
- throw new Error(`업로드 타임아웃 (1시간 초과): ${fileName}`);
+ console.error(`[Proxy] 타임아웃 (${elapsed}초): ${file.name}`);
+ throw new Error(`업로드 타임아웃 (1시간 초과): ${file.name}`);
}
- console.error(`[Proxy] 업로드 에러 (${elapsed}초): ${fileName}`, error);
+ console.error(`[Proxy] 업로드 에러 (${elapsed}초): ${file.name}`, error);
throw error;
}
}
/**
- * 상세도면 파일 업로드 API (스트리밍 Proxy)
- *
- * 진정한 Proxy 모드:
- * - 클라이언트로부터 받는 즉시 DOLCE API로 전달
- * - 임시 파일 저장 없음 (메모리 효율)
- * - 진행도가 실시간으로 정확하게 반영
+ * 기존 파일 목록 조회
*/
-export async function POST(request: NextRequest) {
+async function getExistingFileSeq(uploadId: string): Promise<number> {
try {
- console.log("[Proxy] 업로드 요청 수신");
+ const response = await fetch(
+ `${DOLCE_API_URL}/PorjectWebProxyService.ashx?service=FileInfoList`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ uploadId }),
+ }
+ );
- // NextRequest를 Node.js request로 변환 (스트리밍)
- const nodeRequest = convertToNodeRequest(request);
+ if (response.ok) {
+ const data = await response.json();
+ const existingCount = data.FileInfoListResult?.length || 0;
+ console.log(`[Proxy] 기존 파일=${existingCount}개, 새 파일 시작 Seq=${existingCount + 1}`);
+ return existingCount + 1;
+ } else {
+ console.warn(`[Proxy] FileInfoList 조회 실패, startSeq=1로 시작`);
+ return 1;
+ }
+ } catch (error) {
+ console.warn(`[Proxy] FileInfoList 조회 에러:`, error);
+ return 1;
+ }
+}
- // formidable 설정 (스트리밍 모드)
- const form = formidable({
- maxFileSize: 1024 * 1024 * 1024, // 1GB
- maxTotalFileSize: 10 * 1024 * 1024 * 1024, // 10GB
- allowEmptyFiles: false,
- // 파일을 디스크에 저장하지 않음
- enabledPlugins: [],
- });
+/**
+ * 업로드 완료 통지 (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<void> {
+ 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),
+ });
- // 파일 메타데이터 수집
- const uploadResults: Array<{
- FileId: string;
- UploadId: string;
- FileSeq: number;
- FileName: string;
- FileRelativePath: string;
- FileSize: number;
- FileCreateDT: string;
- FileWriteDT: string;
- OwnerUserId: string;
- }> = [];
+ console.log(`[Proxy] PWPUploadResultService HTTP 상태: ${resultResponse.status}`);
- let uploadId: string | null = null;
- let userId: string | null = null;
- let currentFileIndex = 0;
- let startSeq = 1;
-
- // formidable 이벤트 기반 처리
- await new Promise<void>((resolve, reject) => {
- // 필드(메타데이터) 처리
- form.on("field", (name, value) => {
- if (name === "uploadId") uploadId = value;
- if (name === "userId") userId = value;
- });
+ if (!resultResponse.ok) {
+ const errorText = await resultResponse.text();
+ console.error(`[Proxy] PWPUploadResultService 실패: HTTP ${resultResponse.status}`, errorText);
+ throw new Error(`업로드 완료 통지 실패: ${resultResponse.status}`);
+ }
- // 파일 스트림 처리 (받는 즉시 DOLCE로 전송)
- form.on("fileBegin", (formName, file) => {
- console.log(`[Proxy] 파일 수신 시작: ${file.originalFilename || "unknown"} (${formName})`);
- });
+ const resultText = await resultResponse.text();
+ console.log(`[Proxy] PWPUploadResultService 응답: "${resultText}"`);
- form.on("file", async (formName, file) => {
- try {
- if (!uploadId || !userId) {
- throw new Error("uploadId 또는 userId가 누락되었습니다");
- }
-
- // 첫 파일일 때 기존 파일 조회
- if (currentFileIndex === 0) {
- try {
- const existingFilesResponse = await fetch(
- `${DOLCE_API_URL}/PorjectWebProxyService.ashx?service=FileInfoList`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ uploadId }),
- }
- );
-
- if (existingFilesResponse.ok) {
- const existingFilesData = await existingFilesResponse.json();
- startSeq = (existingFilesData.FileInfoListResult?.length || 0) + 1;
- console.log(`[Proxy] 기존 파일=${startSeq - 1}개, 새 파일 시작 Seq=${startSeq}`);
- } else {
- console.warn(`[Proxy] FileInfoList 조회 실패, startSeq=1로 시작`);
- }
- } catch (error) {
- console.warn(`[Proxy] FileInfoList 조회 에러:`, error);
- }
- }
-
- const fileId = crypto.randomUUID();
- const fileName = file.originalFilename || `file_${currentFileIndex}`;
- const fileSize = file.size;
-
- console.log(`[Proxy] 파일 ${currentFileIndex + 1}: ${fileName} (${(fileSize / 1024 / 1024).toFixed(2)}MB)`);
-
- // 이 시점에서 파일은 이미 디스크에 저장되어 있음 (formidable 기본 동작)
- // 파일을 읽어서 DOLCE API로 전송
- const fs = await import("fs");
- const fileStream = fs.createReadStream(file.filepath);
-
- const fileRelativePath = await uploadStreamToDolce(
- fileStream,
- fileName,
- fileSize,
- uploadId,
- fileId
- );
-
- uploadResults.push({
- FileId: fileId,
- UploadId: uploadId,
- FileSeq: startSeq + currentFileIndex,
- FileName: fileName,
- FileRelativePath: fileRelativePath,
- FileSize: fileSize,
- FileCreateDT: new Date().toISOString(),
- FileWriteDT: new Date().toISOString(),
- OwnerUserId: userId,
- });
-
- // 임시 파일 삭제
- try {
- await fs.promises.unlink(file.filepath);
- } catch (error) {
- console.error(`[Proxy] 임시 파일 삭제 실패: ${file.filepath}`, error);
- }
-
- currentFileIndex++;
- } catch (error) {
- reject(error);
- }
- });
+ if (resultText !== "Success") {
+ console.error(`[Proxy] PWPUploadResultService 예상치 못한 응답: "${resultText}"`);
+ throw new Error(`업로드 완료 통지 실패: ${resultText}`);
+ }
- form.on("error", (err) => {
- console.error("[Proxy] formidable 에러:", err);
- reject(err);
- });
+ console.log(`[Proxy] ✅ DB 저장 완료!`);
+ console.log(`[Proxy] ========================================\n`);
+}
- form.on("end", () => {
- console.log("[Proxy] formidable 파싱 완료");
- resolve();
- });
+/**
+ * 상세도면 파일 업로드 API
+ *
+ * 간단하고 효율적인 구현:
+ * - Next.js의 네이티브 formData API 사용
+ * - File 객체를 바로 DOLCE API로 전송 (자동 스트리밍)
+ * - 복잡한 이벤트 핸들링 없음
+ */
+export async function POST(request: NextRequest) {
+ try {
+ console.log("[Proxy] 업로드 요청 수신");
- // 파싱 시작 (타입 캐스팅: formidable은 실제로는 Readable 스트림만 필요)
- form.parse(nodeRequest as unknown as IncomingMessage);
- });
+ // 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(
@@ -323,38 +178,67 @@ export async function POST(request: NextRequest) {
);
}
- if (uploadResults.length === 0) {
+ // 파일 수집
+ 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] PWPUploadResultService 호출: ${uploadResults.length}개 파일 메타데이터 전송`);
+ console.log(`[Proxy] 총 ${files.length}개 파일 업로드 시작`);
- const resultServiceUrl = `${DOLCE_API_URL}/PWPUploadResultService.ashx?`;
- const resultResponse = await fetch(resultServiceUrl, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(uploadResults),
- });
+ // 기존 파일 Seq 조회
+ const startSeq = await getExistingFileSeq(uploadId);
- 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);
+ // 파일 업로드 결과
+ const uploadResults: Array<{
+ FileId: string;
+ UploadId: string;
+ FileSeq: number;
+ FileName: string;
+ FileRelativePath: string;
+ FileSize: number;
+ FileCreateDT: string;
+ FileWriteDT: string;
+ OwnerUserId: string;
+ }> = [];
- if (resultText !== "Success") {
- console.error(`[Proxy] PWPUploadResultService 예상치 못한 응답: "${resultText}"`);
- throw new Error(`업로드 완료 통지 실패: ${resultText}`);
+ // 순차 업로드
+ 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(`[Proxy] ✅ 완료: ${uploadResults.length}개 파일 업로드 성공`);
+ console.log(`\n[Proxy] 모든 파일 업로드 완료, DB 저장 시작...`);
+
+ // 업로드 완료 통지 (DB 저장)
+ await notifyUploadComplete(uploadResults);
+
+ console.log(`[Proxy] ✅ 전체 프로세스 완료: ${uploadResults.length}개 파일 업로드 및 DB 저장 성공`);
return NextResponse.json({
success: true,