diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/[lng]/partners/(partners)/dolce-upload/dolce-upload-page.tsx | 110 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/dolce-upload/page.tsx | 19 | ||||
| -rw-r--r-- | app/api/dolce/upload-files/route.ts | 325 | ||||
| -rw-r--r-- | app/api/revisions/max-serial-no/route.ts | 13 |
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 조회 |
