summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx110
-rw-r--r--app/[lng]/partners/(partners)/dolce-upload/page.tsx19
-rw-r--r--app/api/dolce/upload-files/route.ts325
-rw-r--r--app/api/revisions/max-serial-no/route.ts13
4 files changed, 263 insertions, 204 deletions
diff --git a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx
index db8d528b..43800838 100644
--- a/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx
+++ b/app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
+import { useParams } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton";
@@ -16,6 +17,7 @@ import {
} from "@/components/ui/select";
import { InfoIcon, RefreshCw, Search, Upload } from "lucide-react";
import { toast } from "sonner";
+import { useTranslation } from "@/i18n/client";
import {
UnifiedDwgReceiptItem,
fetchDwgReceiptList,
@@ -33,6 +35,10 @@ interface DolceUploadPageProps {
}
export default function DolceUploadPage({ searchParams }: DolceUploadPageProps) {
+ const params = useParams();
+ const lng = params?.lng as string;
+ const { t } = useTranslation(lng, "dolce");
+
// URL에서 초기 프로젝트 코드
const initialProjNo = (searchParams.projNo as string) || "";
@@ -78,7 +84,7 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
fetchVendorProjects(),
]);
- setVendorInfo(vendorInfoData);
+ setVendorInfo(vendorInfoData as typeof vendorInfo);
setProjects(projectsData);
// 초기 프로젝트가 있으면 도면 로드
@@ -92,12 +98,12 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
}
} catch (err) {
console.error("초기 데이터 로드 실패:", err);
- setError(err instanceof Error ? err.message : "데이터 로드 실패");
- toast.error("데이터 로드 실패");
+ setError(err instanceof Error ? err.message : t("page.initialLoadError"));
+ toast.error(t("page.initialLoadError"));
} finally {
setIsLoading(false);
}
- }, [initialProjNo]);
+ }, [initialProjNo, t]);
// 도면 목록 조회
const loadDrawings = useCallback(async () => {
@@ -114,21 +120,28 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
});
setDrawings(drawingsData);
- toast.success("도면 목록을 갱신했습니다");
+ toast.success(t("page.drawingLoadSuccess"));
} catch (err) {
console.error("도면 로드 실패:", err);
- setError(err instanceof Error ? err.message : "도면 로드 실패");
- toast.error("도면 로드 실패");
+ setError(err instanceof Error ? err.message : t("page.drawingLoadError"));
+ toast.error(t("page.drawingLoadError"));
} finally {
setIsRefreshing(false);
}
- }, [projNo, vendorInfo]);
+ }, [projNo, vendorInfo, t]);
// 초기 데이터 로드
useEffect(() => {
loadInitialData();
}, [loadInitialData]);
+ // 프로젝트 변경 시 자동 검색
+ useEffect(() => {
+ if (projNo && vendorInfo) {
+ loadDrawings();
+ }
+ }, [projNo, vendorInfo, loadDrawings]);
+
// 도면 클릭 핸들러
const handleDrawingClick = (drawing: UnifiedDwgReceiptItem) => {
setSelectedDrawing(drawing);
@@ -227,7 +240,7 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertDescription>
- 프로젝트를 선택하여 도면 목록을 조회하세요.
+ {t("page.selectProject")}
</AlertDescription>
</Alert>
)}
@@ -235,16 +248,16 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
{/* 필터 카드 */}
<Card>
<CardHeader>
- <CardTitle>검색 필터</CardTitle>
+ <CardTitle>{t("filter.title")}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 프로젝트 선택 */}
<div className="space-y-2">
- <Label>프로젝트</Label>
+ <Label>{t("filter.project")}</Label>
<Select value={projNo} onValueChange={setProjNo}>
<SelectTrigger>
- <SelectValue placeholder="프로젝트를 선택하세요" />
+ <SelectValue placeholder={t("filter.projectPlaceholder")} />
</SelectTrigger>
<SelectContent>
{projects.map((project) => (
@@ -258,77 +271,69 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
{/* 도면번호 검색 */}
<div className="space-y-2">
- <Label>도면번호 (Drawing No)</Label>
+ <Label>{t("filter.drawingNo")}</Label>
<Input
value={drawingNo}
onChange={(e) => setDrawingNo(e.target.value)}
- placeholder="도면번호 입력"
+ placeholder={t("filter.drawingNoPlaceholder")}
/>
</div>
{/* 도면명 검색 */}
<div className="space-y-2">
- <Label>도면명 (Drawing Name)</Label>
+ <Label>{t("filter.drawingName")}</Label>
<Input
value={drawingName}
onChange={(e) => setDrawingName(e.target.value)}
- placeholder="도면명 입력"
+ placeholder={t("filter.drawingNamePlaceholder")}
/>
</div>
{/* 설계공종 검색 */}
<div className="space-y-2">
- <Label>설계공종 (Discipline)</Label>
+ <Label>{t("filter.discipline")}</Label>
<Input
value={discipline}
onChange={(e) => setDiscipline(e.target.value)}
- placeholder="설계공종 입력"
+ placeholder={t("filter.disciplinePlaceholder")}
/>
</div>
{/* 담당자명 검색 (클라이언트 필터) */}
<div className="space-y-2">
- <Label>담당자명 (Manager)</Label>
+ <Label>{t("filter.manager")}</Label>
<Input
value={manager}
onChange={(e) => setManager(e.target.value)}
- placeholder="담당자명 입력"
+ placeholder={t("filter.managerPlaceholder")}
/>
</div>
{/* B4(GTT) 전용: Document Type 필터 */}
{vendorInfo?.drawingKind === "B4" && (
<div className="space-y-2">
- <Label>Document Type</Label>
+ <Label>{t("filter.documentType")}</Label>
<Select value={documentType} onValueChange={(value) => setDocumentType(value as DocumentType)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
- <SelectItem value="ALL">ALL (전체)</SelectItem>
- <SelectItem value="GTT_DELIVERABLES">GTT Deliverables (도면입수)</SelectItem>
- <SelectItem value="SHI_INPUT">SHI Input Document (도면제출)</SelectItem>
+ <SelectItem value="ALL">{t("filter.documentTypeAll")}</SelectItem>
+ <SelectItem value="GTT_DELIVERABLES">{t("filter.documentTypeGttDeliverables")}</SelectItem>
+ <SelectItem value="SHI_INPUT">{t("filter.documentTypeSHIInput")}</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
- <div className="flex gap-2 mt-4">
+ <div className="flex gap-2 mt-4 justify-end">
<Button
onClick={handleSearch}
disabled={!projNo || isRefreshing}
- className="flex-1"
>
<Search className="h-4 w-4 mr-2" />
- 검색
- </Button>
- <Button
- variant="outline"
- onClick={handleRefresh}
- disabled={!projNo || isRefreshing}
- >
- <RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
+ {t("filter.searchButton")}
</Button>
{/* B4 벤더인 경우에만 일괄 업로드 버튼 표시 */}
{vendorInfo?.drawingKind === "B4" && (
@@ -338,9 +343,16 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
disabled={!projNo || isRefreshing}
>
<Upload className="h-4 w-4 mr-2" />
- 일괄 업로드
+ {t("filter.bulkUploadButton")}
</Button>
)}
+ <Button
+ variant="outline"
+ onClick={handleRefresh}
+ disabled={!projNo || isRefreshing}
+ >
+ <RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
+ </Button>
</div>
</CardContent>
</Card>
@@ -350,24 +362,20 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
<Card>
<CardHeader>
<CardTitle>
- 도면 리스트
- {filteredDrawings.length > 0 && ` (${filteredDrawings.length}건)`}
+ {t("drawingList.title")}
+ {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`}
</CardTitle>
</CardHeader>
<CardContent className="overflow-x-auto">
- {vendorInfo.drawingKind === "B4" ? (
- <DrawingListTable
- columns={createGttDrawingListColumns({ documentType })}
- data={filteredDrawings}
- onRowClick={handleDrawingClick}
- />
- ) : (
- <DrawingListTable
- columns={drawingListColumns}
- data={filteredDrawings}
- onRowClick={handleDrawingClick}
- />
- )}
+ <DrawingListTable
+ columns={
+ vendorInfo.drawingKind === "B4"
+ ? (createGttDrawingListColumns({ documentType, lng, t }) as unknown as typeof drawingListColumns)
+ : (drawingListColumns(lng, t) as unknown as typeof drawingListColumns)
+ }
+ data={filteredDrawings}
+ onRowClick={handleDrawingClick}
+ />
</CardContent>
</Card>
)}
@@ -383,6 +391,7 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
userName={vendorInfo.userName}
userEmail={vendorInfo.email}
drawingKind={vendorInfo.drawingKind}
+ lng={lng}
/>
)}
@@ -397,6 +406,7 @@ export default function DolceUploadPage({ searchParams }: DolceUploadPageProps)
userEmail={vendorInfo.email}
vendorCode={vendorInfo.vendorCode}
onUploadComplete={handleBulkUploadComplete}
+ lng={lng}
/>
)}
</div>
diff --git a/app/[lng]/partners/(partners)/dolce-upload/page.tsx b/app/[lng]/partners/(partners)/dolce-upload/page.tsx
index d44e71b6..4d7b1a74 100644
--- a/app/[lng]/partners/(partners)/dolce-upload/page.tsx
+++ b/app/[lng]/partners/(partners)/dolce-upload/page.tsx
@@ -5,8 +5,8 @@ import DolceUploadPage from "./dolce-upload-page";
import { Shell } from "@/components/shell";
export const metadata = {
- title: "DOLCE 업로드",
- description: "설계문서 업로드 및 관리",
+ title: "조선 벤더문서 업로드(DOLCE)",
+ description: "조선 설계문서 업로드 및 관리",
};
// ============================================================================
@@ -31,11 +31,14 @@ function DolceUploadSkeleton() {
}
export default async function DolceUploadPageWrapper({
+ params,
searchParams,
}: {
+ params: Promise<{ lng: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
- const params = await searchParams;
+ const { lng } = await params;
+ const resolvedParams = await searchParams;
return (
<Shell>
@@ -43,17 +46,21 @@ export default async function DolceUploadPageWrapper({
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold tracking-tight">
- DOLCE 문서 업로드
+ {lng === "ko"
+ ? "DOLCE 도면 업로드"
+ : "DOLCE Drawing Upload"}
</h2>
<p className="text-muted-foreground">
- 설계문서를 조회하고 업로드할 수 있습니다
+ {lng === "ko"
+ ? "설계문서를 조회하고 업로드할 수 있습니다"
+ : "View and upload design documents"}
</p>
</div>
</div>
{/* 메인 컨텐츠 */}
<Suspense fallback={<DolceUploadSkeleton />}>
- <DolceUploadPage searchParams={params} />
+ <DolceUploadPage searchParams={resolvedParams} />
</Suspense>
</Shell>
);
diff --git a/app/api/dolce/upload-files/route.ts b/app/api/dolce/upload-files/route.ts
index 1d302cb2..898f9b2a 100644
--- a/app/api/dolce/upload-files/route.ts
+++ b/app/api/dolce/upload-files/route.ts
@@ -1,114 +1,204 @@
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";
+// Next.js API Route 설정
+export const maxDuration = 3600; // 1시간 (대용량 파일 업로드 지원)
+export const dynamic = "force-dynamic";
+
/**
- * 임시 파일 저장 및 정리 헬퍼
+ * 파일을 DOLCE API로 업로드
*/
-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);
+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] 파일 업로드 시작: ${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<number> {
+ try {
+ const response = await fetch(
+ `${DOLCE_API_URL}/PorjectWebProxyService.ashx?service=FileInfoList`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ uploadId }),
}
- },
- };
+ );
+
+ 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;
+ }
}
/**
- * 스트리밍 방식으로 파일 업로드
- * Node.js ReadableStream을 Web ReadableStream으로 변환하여 fetch 사용
+ * 업로드 완료 통지 (DB 저장)
*/
-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 청크로 읽기
- });
+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?`;
- // 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();
- },
- });
+ console.log(`[Proxy] 요청 URL: ${resultServiceUrl}`);
- // 스트리밍 업로드
- const uploadResponse = await fetch(uploadUrl, {
+ const resultResponse = await fetch(resultServiceUrl, {
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",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(uploadResults),
});
-
- if (!uploadResponse.ok) {
- throw new Error(
- `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}`
- );
+
+ 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 fileRelativePath = await uploadResponse.text();
- return fileRelativePath;
+
+ 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) {
- const tempFiles: Array<{ filepath: string; cleanup: () => Promise<void> }> = [];
-
try {
- // FormData 파싱
+ 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;
- const fileCount = parseInt(formData.get("fileCount") as string);
- if (!uploadId || !userId || !fileCount) {
+ 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;
@@ -121,52 +211,14 @@ export async function POST(request: NextRequest) {
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);
+ console.log(`[Proxy] 파일 ${i + 1}/${files.length}: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
- // 스트리밍 방식으로 DOLCE API에 업로드
- const fileRelativePath = await uploadFileStream(
- tempFile.filepath,
- uploadId,
- fileId,
- file.size
- );
+ const fileRelativePath = await uploadFileToDolce(file, uploadId, fileId);
uploadResults.push({
FileId: fileId,
@@ -179,45 +231,26 @@ export async function POST(request: NextRequest) {
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}`
- );
- }
+ console.log(`\n[Proxy] 모든 파일 업로드 완료, DB 저장 시작...`);
+
+ // 업로드 완료 통지 (DB 저장)
+ await notifyUploadComplete(uploadResults);
- const resultText = await resultResponse.text();
- if (resultText !== "Success") {
- throw new Error(`업로드 완료 통지 실패: ${resultText}`);
- }
+ console.log(`[Proxy] ✅ 전체 프로세스 완료: ${uploadResults.length}개 파일 업로드 및 DB 저장 성공`);
return NextResponse.json({
success: true,
uploadedCount: uploadResults.length,
});
} catch (error) {
- console.error("파일 업로드 실패:", error);
+ console.error("[Proxy] ❌ 업로드 실패:", error);
- // 에러 발생 시 남아있는 임시 파일 모두 정리
- for (const tempFile of tempFiles) {
- await tempFile.cleanup();
+ if (error instanceof Error) {
+ console.error("[Proxy] 에러 스택:", error.stack);
}
-
+
return NextResponse.json(
{
success: false,
diff --git a/app/api/revisions/max-serial-no/route.ts b/app/api/revisions/max-serial-no/route.ts
index 0681b66d..6cdf18b6 100644
--- a/app/api/revisions/max-serial-no/route.ts
+++ b/app/api/revisions/max-serial-no/route.ts
@@ -18,8 +18,8 @@ export async function GET(request: NextRequest) {
debugLog('1. Input documentId:', documentId)
- if (!documentId) {
- debugLog('2. documentId is missing, returning 400')
+ if (!documentId || documentId === 'undefined' || documentId === 'null') {
+ debugLog('2. documentId is missing or invalid, returning 400')
return NextResponse.json(
{ error: 'documentId is required' },
{ status: 400 }
@@ -27,6 +27,15 @@ export async function GET(request: NextRequest) {
}
const parsedDocumentId = parseInt(documentId)
+
+ if (isNaN(parsedDocumentId)) {
+ debugError('3. Invalid documentId - cannot parse to integer:', documentId)
+ return NextResponse.json(
+ { error: 'Invalid documentId format' },
+ { status: 400 }
+ )
+ }
+
debugLog('3. Parsed documentId:', parsedDocumentId)
// 1. 내부 DB에서 최대 serialNo 조회