diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-25 15:42:00 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-25 15:42:00 +0900 |
| commit | 4586f2cd95f1cd7112cbec80399da8817df0d289 (patch) | |
| tree | 48572a71cb01d3850defb6cab6c616ca545f5f25 | |
| parent | 25b2561bf17128b96f023c977efb5cb51da0b4aa (diff) | |
(김준회) dolce: i18n 적용, 상세도면 수정 적용
| -rw-r--r-- | app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx | 4 | ||||
| -rw-r--r-- | lib/dolce/actions.ts | 258 | ||||
| -rw-r--r-- | lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx (renamed from lib/dolce/dialogs/add-detail-drawing-dialog.tsx) | 381 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx | 258 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 108 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-upload-validation-dialog.tsx | 99 | ||||
| -rw-r--r-- | lib/dolce/dialogs/detail-drawing-dialog.tsx | 35 | ||||
| -rw-r--r-- | lib/dolce/dialogs/upload-files-to-detail-dialog.tsx | 35 | ||||
| -rw-r--r-- | lib/dolce/table/detail-drawing-columns.tsx | 112 |
9 files changed, 976 insertions, 314 deletions
diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx index 79f1b147..f5337c1c 100644 --- a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx +++ b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx @@ -37,7 +37,7 @@ import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; import { B4BulkUploadDialogV2 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v2"; // V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 // import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; -import { AddDetailDrawingDialog } from "@/lib/dolce/dialogs/add-detail-drawing-dialog"; +import { AddAndModifyDetailDrawingDialog } from "@/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog"; import { UploadFilesToDetailDialog } from "@/lib/dolce/dialogs/upload-files-to-detail-dialog"; interface DolceUploadPageV2Props { @@ -686,7 +686,7 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro {/* 상세도면 추가 다이얼로그 */} {vendorInfo && selectedDrawing && ( - <AddDetailDrawingDialog + <AddAndModifyDetailDrawingDialog open={addDialogOpen} onOpenChange={setAddDialogOpen} drawing={selectedDrawing} diff --git a/lib/dolce/actions.ts b/lib/dolce/actions.ts index 552a9a6a..8c5dfa1b 100644 --- a/lib/dolce/actions.ts +++ b/lib/dolce/actions.ts @@ -172,7 +172,7 @@ async function dolceApiCall<T>(endpoint: string, body: Record<string, unknown>): }); if (!response.ok) { - throw new Error(`DOLCE API 오류 (${endpoint}): ${response.status} ${response.statusText}`); + throw new Error(`DOLCE API error (${endpoint}): ${response.status} ${response.statusText}`); } const data = await response.json(); @@ -356,7 +356,7 @@ export async function downloadDolceFile(params: { if (!response.ok) { const errorText = await response.text(); console.error("[DOLCE] 에러 응답:", errorText); - throw new Error(`파일 다운로드 실패: ${response.status} - ${errorText}`); + throw new Error(`File download failed: ${response.status} - ${errorText}`); } // HTML/텍스트 응답인 경우 에러일 수 있음 @@ -364,7 +364,7 @@ export async function downloadDolceFile(params: { if (contentType.includes("text") || contentType.includes("html")) { const errorText = await response.text(); console.error("[DOLCE] 텍스트 응답 (에러):", errorText); - throw new Error(`예상치 못한 응답: ${errorText.substring(0, 200)}`); + throw new Error(`Unexpected response: ${errorText.substring(0, 200)}`); } const blob = await response.blob(); @@ -387,7 +387,7 @@ export async function getVendorSessionInfo() { try { const session = await getServerSession(authOptions); if (!session?.user) { - throw new Error("로그인이 필요합니다"); + throw new Error("Login required"); } // DB에서 사용자 정보 조회 @@ -403,7 +403,7 @@ export async function getVendorSessionInfo() { }); if (!userInfo || !userInfo.companyId) { - throw new Error("벤더 정보를 찾을 수 없습니다"); + throw new Error("Vendor information not found"); } // 벤더 정보 조회 @@ -417,7 +417,7 @@ export async function getVendorSessionInfo() { }); if (!vendorInfo) { - throw new Error("벤더 정보를 찾을 수 없습니다"); + throw new Error("Vendor information not found"); } // GTT 벤더 확인 (A0016193) @@ -582,7 +582,7 @@ export async function uploadFilesToDetailDrawing( const fileCount = parseInt(formData.get("fileCount") as string); if (!uploadId || !userId || !fileCount) { - throw new Error("필수 파라미터가 누락되었습니다"); + throw new Error("Required parameters are missing"); } const uploadResults: Array<{ @@ -629,7 +629,7 @@ export async function uploadFilesToDetailDrawing( if (!uploadResponse.ok) { throw new Error( - `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}` + `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}` ); } @@ -661,13 +661,13 @@ export async function uploadFilesToDetailDrawing( if (!resultResponse.ok) { throw new Error( - `업로드 완료 통지 실패: ${resultResponse.status} ${resultResponse.statusText}` + `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}` ); } const resultText = await resultResponse.text(); if (resultText !== "Success") { - throw new Error(`업로드 완료 통지 실패: ${resultText}`); + throw new Error(`Upload notification failed: ${resultText}`); } return { @@ -678,7 +678,7 @@ export async function uploadFilesToDetailDrawing( console.error("파일 업로드 실패:", error); return { success: false, - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "Unknown error", }; } } @@ -747,7 +747,7 @@ async function parseB4FileName(fileName: string): Promise<{ try { const lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex === -1) { - return { valid: false, error: "파일 확장자가 없습니다" }; + return { valid: false, error: "File extension is missing" }; } const nameWithoutExt = fileName.substring(0, lastDotIndex); @@ -756,7 +756,7 @@ async function parseB4FileName(fileName: string): Promise<{ if (parts.length < 3) { return { valid: false, - error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개)`, + error: `At least 2 spaces required (current: ${parts.length - 1})`, }; } @@ -765,30 +765,212 @@ async function parseB4FileName(fileName: string): Promise<{ const drawingNo = drawingTokens.join("-"); if (!drawingNo || !revNo) { - return { valid: false, error: "도면번호 또는 리비전번호가 비어있습니다" }; + return { valid: false, error: "Drawing number or revision number is empty" }; } return { valid: true, drawingNo: drawingNo.trim(), revNo: revNo.trim() }; } catch (error) { return { valid: false, - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "Unknown error", }; } } /** - * B4 파일 일괄 업로드 V2 + * B4 일괄 업로드를 위한 상세도면 준비 (V2) * - * MatchBatchFileDwg/MatchBatchFileDwgEdit API 대신 - * DetailDwgReceiptMgmtEdit API와 업로드 서비스만 사용 + * 반환값: DrawingNo + RevNo별 UploadId 정보 + * 파일 업로드는 클라이언트에서 직접 처리 + */ +export interface B4DetailDrawingInfo { + drawingNo: string; + revNo: string; + uploadId: string; + isNew: boolean; // 새로 생성된 상세도면인지 여부 + drawingName?: string; + discipline?: string; +} + +export async function prepareB4DetailDrawingsV2(params: { + projectNo: string; + userId: string; + userNm: string; + email: string; + vendorCode: string; + registerKind: string; + drawingRevisions: Array<{ drawingNo: string; revNo: string }>; +}): Promise<{ + success: boolean; + detailDrawings?: B4DetailDrawingInfo[]; + error?: string; +}> { + try { + console.log("[V2 Prepare] 상세도면 준비 시작"); + + const { projectNo, userId, userNm, email, vendorCode, registerKind, drawingRevisions } = params; + + const detailDrawings: B4DetailDrawingInfo[] = []; + + // 중복 제거: 동일한 DrawingNo + RevNo 조합은 1번만 처리 + const uniqueRevisions = Array.from( + new Map( + drawingRevisions.map((r) => [`${r.drawingNo}_${r.revNo}`, r]) + ).values() + ); + + if (uniqueRevisions.length !== drawingRevisions.length) { + console.warn( + `[V2 Prepare] 중복 제거: ${drawingRevisions.length}개 → ${uniqueRevisions.length}개` + ); + } + + console.log(`[V2 Prepare] 처리할 리비전: ${uniqueRevisions.length}개`); + + // DrawingNo별로 그룹화 + const drawingNoSet = new Set(uniqueRevisions.map((r) => r.drawingNo)); + const drawingInfoMap = new Map<string, GttDwgReceiptItem>(); + + // 1. 기본 도면 정보 조회 + for (const drawingNo of drawingNoSet) { + try { + const dwgList = await fetchDwgReceiptList({ + project: projectNo, + drawingKind: "B4", + drawingMoveGbn: "도면입수", + drawingNo: drawingNo, + }); + + const dwgInfo = dwgList.find( + (d) => (d as GttDwgReceiptItem).DrawingNo === drawingNo + ) as GttDwgReceiptItem | undefined; + + if (dwgInfo) { + drawingInfoMap.set(drawingNo, dwgInfo); + } + } catch (error) { + console.error(`[V2 Prepare] 도면 정보 조회 실패: ${drawingNo}`, error); + } + } + + // 2. 각 RevNo별로 상세도면 확인/생성 (중복 제거된 리스트 사용) + for (const { drawingNo, revNo } of uniqueRevisions) { + try { + const drawingInfo = drawingInfoMap.get(drawingNo); + if (!drawingInfo) { + throw new Error(`Drawing information not found: ${drawingNo}`); + } + + console.log(`[V2 Prepare] 처리 중: ${drawingNo} Rev.${revNo}`); + + // 기존 상세도면 조회 + const detailDwgList = await fetchDetailDwgReceiptList({ + project: projectNo, + drawingNo: drawingNo, + discipline: drawingInfo.Discipline, + drawingKind: "B4", + userId: userId, + }); + + // 해당 RevNo의 상세도면 찾기 + const existingDetail = detailDwgList.find( + (d) => d.DrawingRevNo === revNo + ); + + let uploadId: string; + let isNew = false; + + if (existingDetail) { + // 1. 기존 상세도면이 있는 경우: 해당 uploadId 재사용 + uploadId = existingDetail.UploadId; + isNew = false; + console.log( + `[V2 Prepare] ✓ 기존 상세도면 재사용: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}` + ); + } else { + // 2. 상세도면이 없는 경우: 새로 1번만 생성 + uploadId = crypto.randomUUID(); + isNew = true; + console.log( + `[V2 Prepare] ✓ 새 상세도면 생성: ${drawingNo} Rev.${revNo}, UploadId: ${uploadId}` + ); + + const category = detailDwgList.length > 0 ? detailDwgList[0].Category : "NORM"; + + const addRequest: DetailDwgEditRequest = { + Mode: "ADD", + Status: "01", + RegisterId: 0, + ProjectNo: projectNo, + Discipline: drawingInfo.Discipline, + DrawingKind: "B4", + DrawingNo: drawingNo, + DrawingName: drawingInfo.DrawingName, + RegisterGroupId: drawingInfo.RegisterGroupId, + RegisterSerialNo: drawingInfo.RegisterGroup, + RegisterKind: registerKind, + DrawingRevNo: revNo, + Category: category, + Receiver: null, + Manager: drawingInfo.Manager || "970043", + RegisterDesc: "", + UploadId: uploadId, + RegCompanyCode: vendorCode, + }; + + await editDetailDwgReceipt({ + dwgList: [addRequest], + userId: userId, + userNm: userNm, + vendorCode: vendorCode, + email: email, + }); + + console.log(`[V2 Prepare] ✓ DetailDwgReceiptMgmtEdit 호출 완료`); + } + + detailDrawings.push({ + drawingNo, + revNo, + uploadId, + isNew, + drawingName: drawingInfo.DrawingName, + discipline: drawingInfo.Discipline, + }); + } catch (error) { + console.error( + `[V2 Prepare] ✗ 상세도면 준비 실패: ${drawingNo} Rev.${revNo}`, + error + ); + throw error; + } + } + + const newCount = detailDrawings.filter((d) => d.isNew).length; + const existingCount = detailDrawings.length - newCount; + + console.log( + `[V2 Prepare] ✓ 완료: 총 ${detailDrawings.length}개 (신규: ${newCount}개, 기존: ${existingCount}개)` + ); + + return { + success: true, + detailDrawings, + }; + } catch (error) { + console.error("[V2 Prepare] 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } +} + +/** + * B4 파일 일괄 업로드 V2 (레거시 - 사용 안 함) * - * 프로세스: - * 1. 파일명 파싱하여 DrawingNo, RevNo 추출 - * 2. 기존 도면 정보 조회 (fetchDwgReceiptList) - * 3. 기존 상세도면 조회 (fetchDetailDwgReceiptList) - * 4. 없으면 ADD, 있으면 기존 UploadId 사용 - * 5. 파일 업로드 (/api/dolce/upload-files) + * @deprecated 서버 액션에서 fetch 호출 시 상대 경로 문제로 사용 중단 + * 대신 prepareB4DetailDrawingsV2 + 클라이언트 uploadFilesWithProgress 사용 */ export async function bulkUploadB4FilesV2( formData: FormData @@ -806,7 +988,7 @@ export async function bulkUploadB4FilesV2( const fileCount = parseInt(formData.get("fileCount") as string); if (!projectNo || !userId || !userNm || !email || !vendorCode || !registerKind || !fileCount) { - throw new Error("필수 파라미터가 누락되었습니다"); + throw new Error("Required parameters are missing"); } console.log(`[V2] 프로젝트: ${projectNo}, 사용자: ${userId}, 파일 수: ${fileCount}`); @@ -843,7 +1025,7 @@ export async function bulkUploadB4FilesV2( revNo: "", fileName: file.name, success: false, - error: parseResult.error || "파일명 파싱 실패", + error: parseResult.error || "Failed to parse filename", }); failCount++; continue; @@ -922,7 +1104,7 @@ export async function bulkUploadB4FilesV2( // 도면 정보가 없으면 실패 if (!drawingInfo) { - throw new Error(`도면 정보를 찾을 수 없습니다: ${drawingNo}`); + throw new Error(`Drawing information not found: ${drawingNo}`); } // 4-1. 기존 상세도면 조회 @@ -1009,13 +1191,13 @@ export async function bulkUploadB4FilesV2( if (!uploadResponse.ok) { const errorData = await uploadResponse.json(); - throw new Error(errorData.error || `파일 업로드 실패: ${uploadResponse.status}`); + throw new Error(errorData.error || `File upload failed: ${uploadResponse.status}`); } const uploadResult = await uploadResponse.json(); if (!uploadResult.success) { - throw new Error(uploadResult.error || "파일 업로드 실패"); + throw new Error(uploadResult.error || "File upload failed"); } console.log(`[V2] 파일 업로드 완료: ${groupKey}`); @@ -1033,7 +1215,7 @@ export async function bulkUploadB4FilesV2( } catch (error) { // 실패 처리 const errorMessage = - error instanceof Error ? error.message : "알 수 없는 오류"; + error instanceof Error ? error.message : "Unknown error"; console.error(`[V2] 그룹 처리 실패: ${groupKey}`, error); @@ -1062,7 +1244,7 @@ export async function bulkUploadB4FilesV2( console.error("[V2] 일괄 업로드 실패:", error); return { success: false, - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "Unknown error", }; } } @@ -1082,7 +1264,7 @@ export async function bulkUploadB4Files( const registerKind = formData.get("registerKind") as string; if (!projectNo || !userId || !fileCount || !registerKind) { - throw new Error("필수 파라미터가 누락되었습니다"); + throw new Error("Required parameters are missing"); } const results: Array<{ @@ -1176,7 +1358,7 @@ export async function bulkUploadB4Files( if (!uploadResponse.ok) { throw new Error( - `파일 업로드 실패: ${uploadResponse.status} ${uploadResponse.statusText}` + `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}` ); } @@ -1208,13 +1390,13 @@ export async function bulkUploadB4Files( if (!resultResponse.ok) { throw new Error( - `업로드 완료 통지 실패: ${resultResponse.status} ${resultResponse.statusText}` + `Upload notification failed: ${resultResponse.status} ${resultResponse.statusText}` ); } const resultText = await resultResponse.text(); if (resultText !== "Success") { - throw new Error(`업로드 완료 통지 실패: ${resultText}`); + throw new Error(`Upload notification failed: ${resultText}`); } // 4. 매핑 현황 재조회 (MatchBatchFileDwg) @@ -1228,7 +1410,7 @@ export async function bulkUploadB4Files( const mappingData = mappingCheckResults[0]; if (!mappingData || mappingData.RegisterGroupId === 0) { - throw new Error("매핑 정보를 찾을 수 없습니다"); + throw new Error("Mapping information not found"); } // 5. 매핑 정보 저장 (MatchBatchFileDwgEdit) @@ -1278,7 +1460,7 @@ export async function bulkUploadB4Files( } catch (error) { // 실패 처리 const errorMessage = - error instanceof Error ? error.message : "알 수 없는 오류"; + error instanceof Error ? error.message : "Unknown error"; for (const fileInfo of files) { results.push({ @@ -1303,7 +1485,7 @@ export async function bulkUploadB4Files( console.error("일괄 업로드 실패:", error); return { success: false, - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "Unknown error", }; } } diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx index 48614ecf..87819693 100644 --- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Dialog, DialogContent, @@ -19,9 +19,11 @@ import { SelectValue, } from "@/components/ui/select"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Textarea } from "@/components/ui/textarea"; import { Upload, X, FileIcon, Info } from "lucide-react"; import { toast } from "sonner"; -import { UnifiedDwgReceiptItem, editDetailDwgReceipt } from "../actions"; +import { useTranslation } from "@/i18n/client"; +import { UnifiedDwgReceiptItem, DetailDwgReceiptItem, editDetailDwgReceipt } from "../actions"; import { v4 as uuidv4 } from "uuid"; import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress"; import { uploadFilesWithProgress } from "../utils/upload-with-progress"; @@ -33,7 +35,7 @@ import { getB4RegisterKindOptions } from "../utils/code-translator"; -interface AddDetailDrawingDialogProps { +interface AddAndModifyDetailDrawingDialogProps { open: boolean; onOpenChange: (open: boolean) => void; drawing: UnifiedDwgReceiptItem | null; @@ -43,10 +45,12 @@ interface AddDetailDrawingDialogProps { userEmail: string; onComplete: () => void; drawingKind: "B3" | "B4"; - lng?: string; // i18n support + lng: string; + mode?: "add" | "edit"; + detailDrawing?: DetailDwgReceiptItem | null; } -export function AddDetailDrawingDialog({ +export function AddAndModifyDetailDrawingDialog({ open, onOpenChange, drawing, @@ -56,14 +60,31 @@ export function AddDetailDrawingDialog({ userEmail, onComplete, drawingKind, - lng = "ko", -}: AddDetailDrawingDialogProps) { + lng, + mode = "add", + detailDrawing = null, +}: AddAndModifyDetailDrawingDialogProps) { + const { t } = useTranslation(lng, "dolce"); const [drawingUsage, setDrawingUsage] = useState<string>(""); const [registerKind, setRegisterKind] = useState<string>(""); const [revision, setRevision] = useState<string>(""); const [revisionError, setRevisionError] = useState<string>(""); + const [comment, setComment] = useState<string>(""); const [isSubmitting, setIsSubmitting] = useState(false); + // Edit 모드일 때 초기값 설정 + useEffect(() => { + if (mode === "edit" && detailDrawing && open) { + setDrawingUsage(detailDrawing.DrawingUsage || ""); + setRegisterKind(detailDrawing.RegisterKind || ""); + setRevision(detailDrawing.DrawingRevNo || ""); + setComment(detailDrawing.RegisterDesc || ""); + } else if (mode === "add" && open) { + // Add 모드로 열릴 때는 초기화 + resetForm(); + } + }, [mode, detailDrawing, open]); + // 옵션 생성 (다국어 지원) const drawingUsageOptions = drawingKind === "B3" ? getB3DrawingUsageOptions(lng) @@ -94,7 +115,7 @@ export function AddDetailDrawingDialog({ // Revision 유효성 검증 함수 const validateRevision = (value: string): string => { if (!value.trim()) { - return "Revision을 입력하세요"; + return t("addDetailDialog.revisionRequired"); } const upperValue = value.toUpperCase().trim(); @@ -109,7 +130,7 @@ export function AddDetailDrawingDialog({ return ""; } - return "올바른 형식이 아닙니다 (A-Z 또는 R00-R99)"; + return t("addDetailDialog.revisionInvalidFormat"); }; // Revision 입력 핸들러 @@ -132,25 +153,20 @@ export function AddDetailDrawingDialog({ setRegisterKind(""); setRevision(""); setRevisionError(""); + setComment(""); clearFiles(); }; // 제출 const handleSubmit = async () => { - if (!drawing) return; - // 유효성 검사 - if (!drawingUsage) { - toast.error("도면용도를 선택하세요"); - return; - } if (!registerKind) { - toast.error("등록종류를 선택하세요"); + toast.error(t("addDetailDialog.selectRegisterKindError")); return; } if (!revision.trim()) { - toast.error("Revision을 입력하세요"); - setRevisionError("Revision을 입력하세요"); + toast.error(t("addDetailDialog.selectRevisionError")); + setRevisionError(t("addDetailDialog.revisionRequired")); return; } @@ -162,93 +178,148 @@ export function AddDetailDrawingDialog({ return; } - if (files.length === 0) { - toast.error("최소 1개 이상의 파일을 첨부해야 합니다"); + // Add 모드일 때만 파일 필수 + if (mode === "add") { + if (!drawing) return; + if (!drawingUsage) { + toast.error(t("addDetailDialog.selectDrawingUsageError")); + return; + } + if (files.length === 0) { + toast.error(t("addDetailDialog.selectFilesError")); + return; + } + } + + // Edit 모드일 때는 detailDrawing 필수 + if (mode === "edit" && !detailDrawing) { + toast.error(t("editDetailDialog.editError")); return; } try { setIsSubmitting(true); - // 파일 업로드 ID 생성 - const uploadId = uuidv4(); - - // 상세도면 추가 - const result = await editDetailDwgReceipt({ - dwgList: [ - { - Mode: "ADD", - Status: "Draft", - RegisterId: 0, - ProjectNo: drawing.ProjectNo, - Discipline: drawing.Discipline, - DrawingKind: drawing.DrawingKind, - DrawingNo: drawing.DrawingNo, - DrawingName: drawing.DrawingName, - RegisterGroupId: drawing.RegisterGroupId, - RegisterSerialNo: 0, // 자동 증가 - RegisterKind: registerKind, - DrawingRevNo: revision, - Category: "TS", // To SHI (벤더가 SHI에게 제출) - Receiver: null, - Manager: "", - RegisterDesc: "", - UploadId: uploadId, - RegCompanyCode: vendorCode, - }, - ], - userId, - userNm: userName, - vendorCode, - email: userEmail, - }); - - if (result > 0) { - // 파일 업로드 처리 (상세도면 추가 후) - if (files.length > 0) { - toast.info(`${files.length}개 파일 업로드를 진행합니다...`); - - // 모든 파일 상태를 uploading으로 변경 - files.forEach((_, index) => { - updateFileProgress(index, 0, "uploading"); - }); - - const uploadResult = await uploadFilesWithProgress({ - uploadId, - userId, - files, - callbacks: { - onProgress: (fileIndex, progress) => { - updateFileProgress(fileIndex, progress, "uploading"); - }, - onFileComplete: (fileIndex) => { - updateFileProgress(fileIndex, 100, "completed"); - }, - onFileError: (fileIndex, error) => { - updateFileProgress(fileIndex, 0, "error", error); - }, + if (mode === "add" && drawing) { + // 파일 업로드 ID 생성 + const uploadId = uuidv4(); + + // 상세도면 추가 + const result = await editDetailDwgReceipt({ + dwgList: [ + { + Mode: "ADD", + Status: "Submitted", + RegisterId: 0, + ProjectNo: drawing.ProjectNo, + Discipline: drawing.Discipline, + DrawingKind: drawing.DrawingKind, + DrawingNo: drawing.DrawingNo, + DrawingName: drawing.DrawingName, + RegisterGroupId: drawing.RegisterGroupId, + RegisterSerialNo: 0, // 자동 증가 + RegisterKind: registerKind, + DrawingRevNo: revision, + Category: "TS", // To SHI (벤더가 SHI에게 제출) + Receiver: null, + Manager: "", + RegisterDesc: comment, + UploadId: uploadId, + RegCompanyCode: vendorCode, }, - }); - - if (uploadResult.success) { - toast.success(`상세도면 추가 및 ${uploadResult.uploadedCount}개 파일 업로드 완료`); + ], + userId, + userNm: userName, + vendorCode, + email: userEmail, + }); + + if (result > 0) { + // 파일 업로드 처리 (상세도면 추가 후) + if (files.length > 0) { + toast.info(t("addDetailDialog.uploadingFiles", { count: files.length })); + + // 모든 파일 상태를 uploading으로 변경 + files.forEach((_, index) => { + updateFileProgress(index, 0, "uploading"); + }); + + const uploadResult = await uploadFilesWithProgress({ + uploadId, + userId, + files, + callbacks: { + onProgress: (fileIndex, progress) => { + updateFileProgress(fileIndex, progress, "uploading"); + }, + onFileComplete: (fileIndex) => { + updateFileProgress(fileIndex, 100, "completed"); + }, + onFileError: (fileIndex, error) => { + updateFileProgress(fileIndex, 0, "error", error); + }, + }, + }); + + if (uploadResult.success) { + toast.success(t("addDetailDialog.addSuccessWithUpload", { count: uploadResult.uploadedCount })); + } else { + toast.warning(t("addDetailDialog.addSuccessPartialUpload", { error: uploadResult.error })); + } } else { - toast.warning(`상세도면은 추가되었으나 파일 업로드 실패: ${uploadResult.error}`); + toast.success(t("addDetailDialog.addSuccess")); } + + // API 호출 성공 시 무조건 다이얼로그 닫기 (파일 업로드 성공 여부와 무관) + resetForm(); + onComplete(); + onOpenChange(false); } else { - toast.success("상세도면이 추가되었습니다"); + toast.error(t("addDetailDialog.addError")); + } + } else if (mode === "edit" && detailDrawing) { + // 상세도면 수정 + const result = await editDetailDwgReceipt({ + dwgList: [ + { + Mode: "MOD", + Status: detailDrawing.Status, + RegisterId: detailDrawing.RegisterId, + ProjectNo: detailDrawing.ProjectNo, + Discipline: detailDrawing.Discipline, + DrawingKind: detailDrawing.DrawingKind, + DrawingNo: detailDrawing.DrawingNo, + DrawingName: detailDrawing.DrawingName, + RegisterGroupId: detailDrawing.RegisterGroupId, + RegisterSerialNo: detailDrawing.RegisterSerialNo, + RegisterKind: registerKind, + DrawingRevNo: revision, + Category: detailDrawing.Category, + Receiver: detailDrawing.Receiver, + Manager: detailDrawing.Manager, + RegisterDesc: comment, + UploadId: detailDrawing.UploadId, + RegCompanyCode: detailDrawing.RegCompanyCode || vendorCode, + }, + ], + userId, + userNm: userName, + vendorCode, + email: userEmail, + }); + + if (result > 0) { + toast.success(t("editDetailDialog.editSuccess")); + resetForm(); + onComplete(); + onOpenChange(false); + } else { + toast.error(t("editDetailDialog.editError")); } - - // API 호출 성공 시 무조건 다이얼로그 닫기 (파일 업로드 성공 여부와 무관) - resetForm(); - onComplete(); - onOpenChange(false); - } else { - toast.error("상세도면 추가에 실패했습니다"); } } catch (error) { - console.error("상세도면 추가 실패:", error); - toast.error("상세도면 추가 중 오류가 발생했습니다"); + console.error("상세도면 처리 실패:", error); + toast.error(mode === "add" ? t("addDetailDialog.addErrorMessage") : t("editDetailDialog.editErrorMessage")); } finally { setIsSubmitting(false); } @@ -270,24 +341,29 @@ export function AddDetailDrawingDialog({ // 선택된 RegisterKind의 Revision Rule const revisionRule = registerKindOptions.find((opt) => opt.value === registerKind)?.revisionRule || ""; - // 추가 버튼 활성화 조건 - const isFormValid = - drawingUsage.trim() !== "" && - registerKind.trim() !== "" && - revision.trim() !== "" && - !revisionError && - files.length > 0; + // 버튼 활성화 조건 + const isFormValid = mode === "add" + ? drawingUsage.trim() !== "" && + registerKind.trim() !== "" && + revision.trim() !== "" && + !revisionError && + files.length > 0 + : registerKind.trim() !== "" && + revision.trim() !== "" && + !revisionError; return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-2xl"> <DialogHeader> - <DialogTitle>상세도면 추가</DialogTitle> + <DialogTitle> + {mode === "edit" ? t("editDetailDialog.title") : t("addDetailDialog.title")} + </DialogTitle> </DialogHeader> <div className="space-y-6"> {/* 도면 정보 표시 */} - {drawing && ( + {mode === "add" && drawing && ( <Alert> <Info className="h-4 w-4" /> <AlertDescription> @@ -297,33 +373,45 @@ export function AddDetailDrawingDialog({ </Alert> )} - {/* 도면용도 선택 */} - <div className="space-y-2"> - <Label>도면용도 (Drawing Usage)</Label> - <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> - <SelectTrigger> - <SelectValue placeholder="도면용도를 선택하세요" /> - </SelectTrigger> - <SelectContent> - {drawingUsageOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> + {mode === "edit" && detailDrawing && ( + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + <div className="font-medium">{detailDrawing.DrawingNo} - Rev. {detailDrawing.DrawingRevNo}</div> + <div className="text-sm text-muted-foreground">{detailDrawing.DrawingName}</div> + </AlertDescription> + </Alert> + )} + + {/* 도면용도 선택 (Add 모드에서만 표시) */} + {mode === "add" && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.drawingUsageLabel")}</Label> + <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> + <SelectTrigger> + <SelectValue placeholder={t("addDetailDialog.drawingUsagePlaceholder")} /> + </SelectTrigger> + <SelectContent> + {drawingUsageOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + )} {/* 등록종류 선택 */} <div className="space-y-2"> - <Label>등록종류 (Register Kind)</Label> + <Label>{t("addDetailDialog.registerKindLabel")}</Label> <Select value={registerKind} onValueChange={setRegisterKind} - disabled={!drawingUsage} + disabled={mode === "add" && !drawingUsage} > <SelectTrigger> - <SelectValue placeholder="등록종류를 선택하세요" /> + <SelectValue placeholder={t("addDetailDialog.registerKindPlaceholder")} /> </SelectTrigger> <SelectContent> {registerKindOptions.map((option) => ( @@ -335,18 +423,18 @@ export function AddDetailDrawingDialog({ </Select> {revisionRule && ( <p className="text-sm text-muted-foreground"> - Revision 입력 형식: {revisionRule} + {t("addDetailDialog.revisionFormatPrefix")}{revisionRule} </p> )} </div> {/* Revision 입력 */} <div className="space-y-2"> - <Label>Revision</Label> + <Label>{t("addDetailDialog.revisionLabel")}</Label> <Input value={revision} onChange={(e) => handleRevisionChange(e.target.value)} - placeholder="예: A, B, R00, R01" + placeholder={t("addDetailDialog.revisionPlaceholder")} disabled={!registerKind} className={revisionError ? "border-red-500 focus-visible:ring-red-500" : ""} /> @@ -357,14 +445,30 @@ export function AddDetailDrawingDialog({ )} {!revisionError && revision && ( <p className="text-sm text-green-600 flex items-center gap-1"> - ✓ 올바른 형식입니다 + {t("addDetailDialog.revisionValid")} </p> )} </div> - {/* 파일 업로드 */} + {/* Comment 입력 */} <div className="space-y-2"> - <Label>첨부파일 (필수) *</Label> + <Label>{t("addDetailDialog.commentLabel")}</Label> + <Textarea + value={comment} + onChange={(e) => setComment(e.target.value)} + placeholder={t("addDetailDialog.commentPlaceholder")} + rows={3} + className="resize-none" + /> + <p className="text-xs text-muted-foreground"> + {t("addDetailDialog.commentMaxLength")} + </p> + </div> + + {/* 파일 업로드 (Add 모드에서만 표시) */} + {mode === "add" && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.attachmentLabel")}</Label> <div {...getRootProps()} className={` @@ -380,20 +484,20 @@ export function AddDetailDrawingDialog({ <Upload className="h-8 w-8 mx-auto text-muted-foreground" /> <div> <p className="text-sm font-medium"> - 파일을 드래그하거나 클릭하여 선택 + {t("addDetailDialog.dragDropText")} </p> <p className="text-xs text-muted-foreground"> - 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일) + {t("addDetailDialog.fileInfo")} </p> </div> </div> ) : ( <div className="space-y-2"> <p className="text-sm font-medium"> - {files.length}개 파일 선택됨 + {t("addDetailDialog.filesSelected", { count: files.length })} </p> <p className="text-xs text-muted-foreground"> - 추가로 파일을 드래그하거나 클릭하여 더 추가할 수 있습니다 + {t("addDetailDialog.addMoreFiles")} </p> </div> )} @@ -410,14 +514,14 @@ export function AddDetailDrawingDialog({ <> <div className="flex items-center justify-between mb-2"> <h4 className="text-sm font-medium"> - 선택된 파일 ({files.length}개) + {t("addDetailDialog.selectedFiles", { count: files.length })} </h4> <Button variant="ghost" size="sm" onClick={clearFiles} > - 전체 제거 + {t("addDetailDialog.removeAll")} </Button> </div> <div className="max-h-60 overflow-y-auto space-y-2"> @@ -447,15 +551,21 @@ export function AddDetailDrawingDialog({ )} </div> )} - </div> + </div> + )} </div> <DialogFooter> <Button variant="outline" onClick={handleCancel} disabled={isSubmitting}> - 취소 + {t("addDetailDialog.cancelButton")} </Button> <Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid}> - {isSubmitting ? "처리 중..." : "추가"} + {isSubmitting + ? t("addDetailDialog.processingButton") + : mode === "edit" + ? t("editDetailDialog.updateButton") + : t("addDetailDialog.addButton") + } </Button> </DialogFooter> </DialogContent> @@ -463,3 +573,4 @@ export function AddDetailDrawingDialog({ ); } + diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx index 3207c00b..ba5673ef 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog-v2.tsx @@ -30,10 +30,13 @@ import { } from "./b4-upload-validation-dialog"; import { fetchDwgReceiptList, - bulkUploadB4FilesV2, + prepareB4DetailDrawingsV2, type B4BulkUploadResult, type GttDwgReceiptItem, } from "../actions"; +import { uploadFilesWithProgress } from "../utils/upload-with-progress"; +import { FileUploadProgressList } from "../components/file-upload-progress-list"; +import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress"; interface B4BulkUploadDialogV2Props { open: boolean; @@ -71,6 +74,7 @@ export function B4BulkUploadDialogV2({ const [isDragging, setIsDragging] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null); + const [fileProgresses, setFileProgresses] = useState<FileUploadProgress[]>([]); // B4 GTT 옵션 const drawingUsageOptions = [ @@ -96,6 +100,7 @@ export function B4BulkUploadDialogV2({ setIsDragging(false); setUploadProgress(0); setUploadResult(null); + setFileProgresses([]); } }, [open]); @@ -271,7 +276,7 @@ export function B4BulkUploadDialogV2({ } }; - // 업로드 확인 (V2: bulkUploadB4FilesV2 사용) + // 업로드 확인 (V2: prepareB4DetailDrawingsV2 + uploadFilesWithProgress 사용) const handleConfirmUpload = async (validFiles: FileValidationResult[]) => { setIsUploading(true); setCurrentStep("uploading"); @@ -280,35 +285,224 @@ export function B4BulkUploadDialogV2({ try { console.log(`[V2 Dialog] 업로드 시작: ${validFiles.length}개 파일`); - // FormData 구성 - const formData = new FormData(); - formData.append("projectNo", projectNo); - formData.append("userId", userId); - formData.append("userNm", userName); - formData.append("email", userEmail); - formData.append("vendorCode", vendorCode); - formData.append("registerKind", registerKind); - formData.append("fileCount", String(validFiles.length)); + // 0단계: 모든 파일에 대한 진행도 상태 초기화 + const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({ + file: fileResult.file, + progress: 0, + status: "pending" as const, + })); + setFileProgresses(initialProgresses); + // 파일 인덱스 맵 생성 (파일명 기반) + const fileIndexMap = new Map<string, number>(); validFiles.forEach((fileResult, index) => { - formData.append(`file_${index}`, fileResult.file); + fileIndexMap.set(fileResult.file.name, index); }); - // 업로드 프로그레스 시뮬레이션 - const progressInterval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 90) return 90; - return prev + 10; - }); - }, 500); + // 1단계: DrawingNo + RevNo별로 그룹화 + // - 동일한 Drawing/Revision에 속하는 파일들을 하나의 그룹으로 묶음 + // - 이렇게 하면 같은 리비전의 상세도면을 1번만 생성/조회함 + const uploadGroups = new Map< + string, + { + drawingNo: string; + revNo: string; + files: File[]; + fileIndices: number[]; // 전체 배열에서의 인덱스 + } + >(); + + validFiles.forEach((fileResult, index) => { + const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; + if (!uploadGroups.has(groupKey)) { + uploadGroups.set(groupKey, { + drawingNo: fileResult.parsed!.drawingNo, + revNo: fileResult.parsed!.revNo, + files: [], + fileIndices: [], + }); + } + uploadGroups.get(groupKey)!.files.push(fileResult.file); + uploadGroups.get(groupKey)!.fileIndices.push(index); + }); + + console.log( + `[V2 Dialog] ${uploadGroups.size}개 리비전 그룹 생성 (${validFiles.length}개 파일)` + ); + + // 2단계: 상세도면 준비 (서버 액션) + // - 각 리비전별로 상세도면 존재 여부 확인 + // - 기존 상세도면이 있으면 uploadId 재사용 + // - 없으면 새로 생성 (1번만!) + const drawingRevisions = Array.from(uploadGroups.values()).map((group) => ({ + drawingNo: group.drawingNo, + revNo: group.revNo, + })); + + console.log(`[V2 Dialog] 상세도면 준비 요청: ${drawingRevisions.length}개 리비전`); + + const prepareResult = await prepareB4DetailDrawingsV2({ + projectNo, + userId, + userNm: userName, + email: userEmail, + vendorCode, + registerKind, + drawingRevisions, + }); + + if (!prepareResult.success || !prepareResult.detailDrawings) { + throw new Error(prepareResult.error || "상세도면 준비 실패"); + } + + const newDrawings = prepareResult.detailDrawings.filter((d) => d.isNew); + const existingDrawings = prepareResult.detailDrawings.filter((d) => !d.isNew); + + console.log( + `[V2 Dialog] 상세도면 준비 완료: 총 ${prepareResult.detailDrawings.length}개 ` + + `(기존 ${existingDrawings.length}개 재사용, 신규 ${newDrawings.length}개 생성)` + ); + + // 3단계: 각 그룹별로 파일 업로드 + // - 준비된 uploadId를 사용하여 파일 업로드 + const detailDrawingMap = new Map( + prepareResult.detailDrawings.map((d) => [`${d.drawingNo}_${d.revNo}`, d]) + ); + + let successCount = 0; + let failCount = 0; + let completedGroups = 0; + const results: B4BulkUploadResult["results"] = []; + + for (const [groupKey, group] of uploadGroups.entries()) { + try { + const detailDrawing = detailDrawingMap.get(groupKey); + if (!detailDrawing) { + throw new Error(`상세도면 정보를 찾을 수 없습니다: ${groupKey}`); + } - // V2 함수 호출 - const result = await bulkUploadB4FilesV2(formData); + console.log( + `[V2 Dialog] 그룹 ${groupKey} 업로드 시작\n` + + ` - 파일 수: ${group.files.length}개\n` + + ` - UploadId: ${detailDrawing.uploadId}\n` + + ` - 상태: ${detailDrawing.isNew ? "신규 생성" : "기존 재사용"}` + ); + + // 그룹 내 모든 파일 상태를 uploading으로 변경 + setFileProgresses((prev) => + prev.map((fp, index) => + group.fileIndices.includes(index) + ? { ...fp, status: "uploading" as const } + : fp + ) + ); + + // uploadFilesWithProgress 사용 (클라이언트 fetch) + const uploadResult = await uploadFilesWithProgress({ + uploadId: detailDrawing.uploadId, + userId: userId, + files: group.files, + callbacks: { + onProgress: (fileIndexInGroup, progress) => { + // 그룹 내 파일 인덱스를 전체 인덱스로 변환 + const globalFileIndex = group.fileIndices[fileIndexInGroup]; + + // 개별 파일 진행도 업데이트 + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, progress, status: "uploading" as const } + : fp + ) + ); + + // 전체 진행도 계산 + const groupProgress = (completedGroups / uploadGroups.size) * 100; + const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size); + setUploadProgress(Math.round(groupProgress + currentGroupProgress)); + }, + onFileComplete: (fileIndexInGroup) => { + const globalFileIndex = group.fileIndices[fileIndexInGroup]; + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, progress: 100, status: "completed" as const } + : fp + ) + ); + }, + onFileError: (fileIndexInGroup, error) => { + const globalFileIndex = group.fileIndices[fileIndexInGroup]; + console.error(`[V2 Dialog] 파일 ${globalFileIndex} 업로드 실패:`, error); + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, status: "error" as const, error } + : fp + ) + ); + }, + }, + }); - clearInterval(progressInterval); - setUploadProgress(100); + if (uploadResult.success) { + console.log( + `[V2 Dialog] ✓ 그룹 ${groupKey} 업로드 완료 (${group.files.length}개 파일)` + ); + successCount += group.files.length; + + // 성공 결과 추가 + group.files.forEach((file) => { + results?.push({ + drawingNo: group.drawingNo, + revNo: group.revNo, + fileName: file.name, + success: true, + }); + }); + } else { + throw new Error(uploadResult.error || "파일 업로드 실패"); + } + } catch (error) { + console.error( + `[V2 Dialog] ✗ 그룹 ${groupKey} 업로드 실패 (${group.files.length}개 파일):`, + error + ); + const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; + failCount += group.files.length; + + // 실패 결과 추가 + group.files.forEach((file) => { + results?.push({ + drawingNo: group.drawingNo, + revNo: group.revNo, + fileName: file.name, + success: false, + error: errorMessage, + }); + }); + } - console.log("[V2 Dialog] 업로드 완료:", result); + completedGroups++; + setUploadProgress(Math.round((completedGroups / uploadGroups.size) * 100)); + } + + console.log( + `[V2 Dialog] ========================================\n` + + `[V2 Dialog] 일괄 업로드 최종 결과\n` + + `[V2 Dialog] - 총 파일 수: ${validFiles.length}개\n` + + `[V2 Dialog] - 성공: ${successCount}개\n` + + `[V2 Dialog] - 실패: ${failCount}개\n` + + `[V2 Dialog] - 처리된 리비전: ${uploadGroups.size}개\n` + + `[V2 Dialog] ========================================` + ); + + const result: B4BulkUploadResult = { + success: successCount > 0, + successCount, + failCount, + results, + }; setUploadResult(result); setCurrentStep("complete"); @@ -506,7 +700,7 @@ export function B4BulkUploadDialogV2({ {/* 4단계: 업로드 진행 중 */} {currentStep === "uploading" && ( - <div className="space-y-6 py-8"> + <div className="space-y-6 py-4"> <div className="flex flex-col items-center"> <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" /> <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3> @@ -514,18 +708,22 @@ export function B4BulkUploadDialogV2({ {t("bulkUpload.uploadingWait")} </p> </div> + + {/* 전체 진행도 */} <div className="space-y-2"> <div className="flex justify-between text-sm"> <span>{t("bulkUpload.uploadProgress")}</span> <span>{uploadProgress}%</span> </div> <Progress value={uploadProgress} className="h-2" /> - {uploadProgress >= 90 && uploadProgress < 100 && ( - <p className="text-xs text-muted-foreground text-center pt-2"> - {t("bulkUpload.uploadingToServer")} - </p> - )} </div> + + {/* 개별 파일 진행도 리스트 */} + {fileProgresses.length > 0 && ( + <div className="border rounded-lg p-4 max-h-96 overflow-y-auto"> + <FileUploadProgressList fileProgresses={fileProgresses} /> + </div> + )} </div> )} diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx index 21647e63..b7b25fca 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx @@ -36,6 +36,9 @@ import { type B4MappingSaveItem, } from "../actions"; import { v4 as uuidv4 } from "uuid"; +import { uploadFilesWithProgress } from "../utils/upload-with-progress"; +import { FileUploadProgressList } from "../components/file-upload-progress-list"; +import type { FileUploadProgress } from "../hooks/use-file-upload-with-progress"; interface B4BulkUploadDialogProps { open: boolean; @@ -73,6 +76,7 @@ export function B4BulkUploadDialog({ const [isDragging, setIsDragging] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null); + const [fileProgresses, setFileProgresses] = useState<FileUploadProgress[]>([]); // B4 GTT 옵션 (코드 번역 유틸리티 사용) const drawingUsageOptions = [ @@ -98,6 +102,7 @@ export function B4BulkUploadDialog({ setIsDragging(false); setUploadProgress(0); setUploadResult(null); + setFileProgresses([]); } }, [open]); @@ -275,6 +280,14 @@ export function B4BulkUploadDialog({ try { console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`); + // 0단계: 모든 파일에 대한 진행도 상태 초기화 + const initialProgresses: FileUploadProgress[] = validFiles.map((fileResult) => ({ + file: fileResult.file, + progress: 0, + status: "pending" as const, + })); + setFileProgresses(initialProgresses); + // 파일을 DrawingNo + RevNo로 그룹화 const uploadGroups = new Map< string, @@ -284,10 +297,11 @@ export function B4BulkUploadDialog({ revNo: string; fileName: string; registerGroupId: number; + fileIndex: number; // 전체 배열에서의 인덱스 }> >(); - validFiles.forEach((fileResult) => { + validFiles.forEach((fileResult, index) => { const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; if (!uploadGroups.has(groupKey)) { uploadGroups.set(groupKey, []); @@ -298,6 +312,7 @@ export function B4BulkUploadDialog({ revNo: fileResult.parsed!.revNo, fileName: fileResult.file.name, registerGroupId: fileResult.registerGroupId || 0, + fileIndex: index, }); }); @@ -317,27 +332,63 @@ export function B4BulkUploadDialog({ // 1. UploadId 생성 const uploadId = uuidv4(); - // 2. 파일 업로드 (공통 API 사용) - const formData = new FormData(); - formData.append("uploadId", uploadId); - formData.append("userId", userId); - formData.append("fileCount", String(files.length)); - - files.forEach((fileInfo, index) => { - formData.append(`file_${index}`, fileInfo.file); - }); - - const uploadResponse = await fetch("/api/dolce/upload-files", { - method: "POST", - body: formData, + // 그룹 내 모든 파일 상태를 uploading으로 변경 + setFileProgresses((prev) => + prev.map((fp, index) => + files.some((f) => f.fileIndex === index) + ? { ...fp, status: "uploading" as const } + : fp + ) + ); + + // 2. 파일 업로드 (uploadFilesWithProgress 사용) + const uploadResult = await uploadFilesWithProgress({ + uploadId, + userId, + files: files.map((f) => f.file), + callbacks: { + onProgress: (fileIndexInGroup, progress) => { + // 그룹 내 파일 인덱스를 전체 인덱스로 변환 + const globalFileIndex = files[fileIndexInGroup].fileIndex; + + // 개별 파일 진행도 업데이트 + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, progress, status: "uploading" as const } + : fp + ) + ); + + // 전체 진행도 계산 + const groupProgress = (completedGroups / uploadGroups.size) * 100; + const currentGroupProgress = (progress / 100) * (100 / uploadGroups.size); + setUploadProgress(Math.round(groupProgress + currentGroupProgress)); + }, + onFileComplete: (fileIndexInGroup) => { + const globalFileIndex = files[fileIndexInGroup].fileIndex; + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, progress: 100, status: "completed" as const } + : fp + ) + ); + }, + onFileError: (fileIndexInGroup, error) => { + const globalFileIndex = files[fileIndexInGroup].fileIndex; + console.error(`[B4 업로드] 파일 ${globalFileIndex} 업로드 실패:`, error); + setFileProgresses((prev) => + prev.map((fp, index) => + index === globalFileIndex + ? { ...fp, status: "error" as const, error } + : fp + ) + ); + }, + }, }); - if (!uploadResponse.ok) { - throw new Error(`파일 업로드 실패: ${uploadResponse.status}`); - } - - const uploadResult = await uploadResponse.json(); - if (!uploadResult.success) { throw new Error(uploadResult.error || "파일 업로드 실패"); } @@ -599,7 +650,7 @@ export function B4BulkUploadDialog({ {/* 4단계: 업로드 진행 중 */} {currentStep === "uploading" && ( - <div className="space-y-6 py-8"> + <div className="space-y-6 py-4"> <div className="flex flex-col items-center"> <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" /> <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3> @@ -607,19 +658,22 @@ export function B4BulkUploadDialog({ {t("bulkUpload.uploadingWait")} </p> </div> + + {/* 전체 진행도 */} <div className="space-y-2"> <div className="flex justify-between text-sm"> <span>{t("bulkUpload.uploadProgress")}</span> <span>{uploadProgress}%</span> </div> <Progress value={uploadProgress} className="h-2" /> - {/* 90% 이상일 때 추가 안내 메시지 */} - {uploadProgress >= 90 && uploadProgress < 100 && ( - <p className="text-xs text-muted-foreground text-center pt-2"> - {t("bulkUpload.uploadingToServer")} - </p> - )} </div> + + {/* 개별 파일 진행도 리스트 */} + {fileProgresses.length > 0 && ( + <div className="border rounded-lg p-4 max-h-96 overflow-y-auto"> + <FileUploadProgressList fileProgresses={fileProgresses} /> + </div> + )} </div> )} diff --git a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx index f3a7c70a..05c1efd7 100644 --- a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx +++ b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx @@ -38,12 +38,12 @@ interface B4UploadValidationDialogProps { } /** - * B4 파일명 검증 함수 - * 형식: [버림] [문서번호토큰1] [문서번호토큰2] ... [리비전번호].[확장자] - * 예시: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" - * - 첫 번째 토큰은 버림 - * - 마지막 토큰은 RevNo - * - 중간 토큰들을 "-"로 연결하여 DrawingNo 생성 + * B4 file name validation function + * Format: [ignore] [document_number_token1] [document_number_token2] ... [revision_number].[extension] + * Example: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" + * - First token is ignored + * - Last token is RevNo + * - Middle tokens are joined with "-" to create DrawingNo */ export function validateB4FileName(fileName: string): { valid: boolean; @@ -51,47 +51,47 @@ export function validateB4FileName(fileName: string): { error?: string; } { try { - // 확장자 분리 + // Separate extension const lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex === -1) { return { valid: false, - error: "파일 확장자가 없습니다", + error: "File has no extension", }; } const nameWithoutExt = fileName.substring(0, lastDotIndex); - // 공백으로 분리 + // Split by spaces const parts = nameWithoutExt.split(" ").filter(p => p.trim() !== ""); - // 최소 3개 파트 필요: [버림], [문서번호토큰], [RevNo] + // At least 3 parts required: [ignore], [document_number_token], [RevNo] if (parts.length < 3) { return { valid: false, - error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [문서번호토큰들...] [RevNo].[확장자]`, + error: `At least 2 spaces required (current: ${parts.length - 1}). Format: [ignore] [document_tokens...] [RevNo].[extension]`, }; } - // 첫 번째 토큰은 버림 - // 마지막 토큰은 RevNo - // 중간 토큰들을 "-"로 연결하여 DrawingNo 생성 + // First token is ignored + // Last token is RevNo + // Middle tokens are joined with "-" to create DrawingNo const revNo = parts[parts.length - 1]; const drawingTokens = parts.slice(1, parts.length - 1); const drawingNo = drawingTokens.join("-"); - // 필수 항목이 비어있지 않은지 확인 + // Check that required fields are not empty if (!drawingNo || drawingNo.trim() === "") { return { valid: false, - error: "도면번호(DrawingNo)가 비어있습니다", + error: "Drawing number (DrawingNo) is empty", }; } if (!revNo || revNo.trim() === "") { return { valid: false, - error: "리비전 번호(RevNo)가 비어있습니다", + error: "Revision number (RevNo) is empty", }; } @@ -106,13 +106,13 @@ export function validateB4FileName(fileName: string): { } catch (error) { return { valid: false, - error: error instanceof Error ? error.message : "알 수 없는 오류", + error: error instanceof Error ? error.message : "Unknown error", }; } } /** - * B4 업로드 전 파일 검증 다이얼로그 + * B4 file validation dialog before upload */ export function B4UploadValidationDialog({ open, @@ -141,45 +141,45 @@ export function B4UploadValidationDialog({ <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col"> <DialogHeader className="flex-shrink-0"> - <DialogTitle>B4 일괄 업로드 검증</DialogTitle> + <DialogTitle>B4 Bulk Upload Validation</DialogTitle> <DialogDescription> - 선택한 파일의 파일명 형식과 매핑 가능 여부를 검증합니다 + Validates file name format and mapping availability for selected files </DialogDescription> </DialogHeader> <div className="space-y-4 overflow-auto flex-1 pr-2"> - {/* 요약 통계 */} + {/* Summary statistics */} <div className="grid grid-cols-4 gap-3"> <div className="rounded-lg border p-3"> - <div className="text-sm text-muted-foreground">전체</div> + <div className="text-sm text-muted-foreground">Total</div> <div className="text-2xl font-bold">{validationResults.length}</div> </div> <div className="rounded-lg border p-3 bg-green-50 dark:bg-green-950/30"> - <div className="text-sm text-green-600 dark:text-green-400">업로드 가능</div> + <div className="text-sm text-green-600 dark:text-green-400">Ready to Upload</div> <div className="text-2xl font-bold text-green-600 dark:text-green-400"> {validFiles.length} </div> </div> <div className="rounded-lg border p-3 bg-orange-50 dark:bg-orange-950/30"> - <div className="text-sm text-orange-600 dark:text-orange-400">도면 없음</div> + <div className="text-sm text-orange-600 dark:text-orange-400">Drawing Not Found</div> <div className="text-2xl font-bold text-orange-600 dark:text-orange-400"> {notFoundFiles.length} </div> </div> <div className="rounded-lg border p-3 bg-red-50 dark:bg-red-950/30"> - <div className="text-sm text-red-600 dark:text-red-400">형식 오류</div> + <div className="text-sm text-red-600 dark:text-red-400">Format Error</div> <div className="text-2xl font-bold text-red-600 dark:text-red-400"> {invalidFiles.length} </div> </div> </div> - {/* 경고 메시지 */} + {/* Warning messages */} {validFiles.length === 0 && ( <Alert variant="destructive"> <XCircle className="h-4 w-4" /> <AlertDescription> - 업로드 가능한 파일이 없습니다. 파일명 형식을 확인하거나 이미 매핑된 파일은 제외해주세요. + No files available for upload. Please check the file name format or exclude already mapped files. </AlertDescription> </Alert> )} @@ -188,20 +188,20 @@ export function B4UploadValidationDialog({ <Alert> <AlertCircle className="h-4 w-4" /> <AlertDescription> - 일부 파일에 문제가 있습니다. 업로드 가능한 {validFiles.length}개 파일만 업로드됩니다. + Some files have issues. Only {validFiles.length} file(s) will be uploaded. </AlertDescription> </Alert> )} - {/* 파일 목록 */} + {/* File list */} <div className="max-h-[50vh] overflow-auto rounded-md border p-4"> <div className="space-y-4"> - {/* 업로드 가능 파일 */} + {/* Files ready to upload */} {validFiles.length > 0 && ( <div className="space-y-2"> <h4 className="text-sm font-semibold text-green-600 dark:text-green-400 flex items-center gap-2"> <CheckCircle2 className="h-4 w-4" /> - 업로드 가능 ({validFiles.length}개) + Ready to Upload ({validFiles.length}) </h4> {validFiles.map((result, index) => ( <div @@ -216,7 +216,7 @@ export function B4UploadValidationDialog({ {result.parsed && ( <div className="flex flex-wrap gap-1 mt-2"> <Badge variant="outline" className="text-xs"> - 도면: {result.parsed.drawingNo} + Drawing: {result.parsed.drawingNo} </Badge> <Badge variant="outline" className="text-xs"> Rev: {result.parsed.revNo} @@ -236,12 +236,12 @@ export function B4UploadValidationDialog({ </div> )} - {/* 도면을 찾을 수 없는 파일 */} + {/* Files with drawing not found */} {notFoundFiles.length > 0 && ( <div className="space-y-2 mt-4"> <h4 className="text-sm font-semibold text-orange-600 dark:text-orange-400 flex items-center gap-2"> <XCircle className="h-4 w-4" /> - 도면을 찾을 수 없음 ({notFoundFiles.length}개) + Drawing Not Found ({notFoundFiles.length}) </h4> {notFoundFiles.map((result, index) => ( <div @@ -254,12 +254,12 @@ export function B4UploadValidationDialog({ {result.file.name} </div> <div className="text-xs text-orange-700 dark:text-orange-300 mt-1"> - ✗ 해당 도면번호가 프로젝트에 등록되어 있지 않습니다 + ✗ This drawing number is not registered in the project </div> {result.parsed && ( <div className="flex flex-wrap gap-1 mt-2"> <Badge variant="outline" className="text-xs"> - 도면: {result.parsed.drawingNo} + Drawing: {result.parsed.drawingNo} </Badge> <Badge variant="outline" className="text-xs"> Rev: {result.parsed.revNo} @@ -274,12 +274,12 @@ export function B4UploadValidationDialog({ </div> )} - {/* 형식 오류 파일 */} + {/* Files with format errors */} {invalidFiles.length > 0 && ( <div className="space-y-2 mt-4"> <h4 className="text-sm font-semibold text-red-600 dark:text-red-400 flex items-center gap-2"> <XCircle className="h-4 w-4" /> - 파일명 형식 오류 ({invalidFiles.length}개) + File Name Format Error ({invalidFiles.length}) </h4> {invalidFiles.map((result, index) => ( <div @@ -306,25 +306,22 @@ export function B4UploadValidationDialog({ </div> </div> - {/* 형식 안내 */} + {/* Format guide */} <div className="rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-3"> <div className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1"> - 📋 올바른 파일명 형식 + 📋 Correct File Name Format </div> <code className="text-xs text-blue-700 dark:text-blue-300"> - [버림] [문서번호토큰1] [문서번호토큰2] ... [RevNo].[확장자] + [fileName(without blanks)] [document_token1] [document_token2] ... [RevNo].[extension] </code> <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> - 예: testfile GTT DE 007 R01.pdf → DrawingNo: GTT-DE-007, Rev: R01 + Example: testfile GTT DE 007 R01.pdf → DrawingNo: GTT-DE-007, Rev: R01 </div> <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> - ※ 첫 번째 단어는 무시됩니다 + ※ The last word is the revision number (RevNo) </div> <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> - ※ 마지막 단어는 리비전 번호(RevNo)입니다 - </div> - <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> - ※ 중간의 모든 단어는 "-"로 연결되어 문서번호(DrawingNo)가 됩니다 + ※ All middle words are connected with "-" to become the document number (DrawingNo) </div> </div> </div> @@ -335,7 +332,7 @@ export function B4UploadValidationDialog({ onClick={handleCancel} disabled={isUploading} > - 취소 + Cancel </Button> <Button onClick={handleUpload} @@ -344,12 +341,12 @@ export function B4UploadValidationDialog({ {isUploading ? ( <> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" /> - 업로드 중... + Uploading... </> ) : ( <> <Upload className="h-4 w-4 mr-2" /> - 업로드 ({validFiles.length}개) + Upload ({validFiles.length}) </> )} </Button> diff --git a/lib/dolce/dialogs/detail-drawing-dialog.tsx b/lib/dolce/dialogs/detail-drawing-dialog.tsx index d9df58db..afe4a4c2 100644 --- a/lib/dolce/dialogs/detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/detail-drawing-dialog.tsx @@ -23,7 +23,7 @@ import { import { DrawingListTable } from "../table/drawing-list-table"; import { createDetailDrawingColumns } from "../table/detail-drawing-columns"; import { createFileListColumns } from "../table/file-list-columns"; -import { AddDetailDrawingDialog } from "./add-detail-drawing-dialog"; +import { AddAndModifyDetailDrawingDialog } from "./add-and-modify-detail-drawing-dialog"; import { UploadFilesToDetailDialog } from "./upload-files-to-detail-dialog"; interface DetailDrawingDialogProps { @@ -56,6 +56,8 @@ export function DetailDrawingDialog({ const [isLoading, setIsLoading] = useState(false); const [isLoadingFiles, setIsLoadingFiles] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [editingDetailDrawing, setEditingDetailDrawing] = useState<DetailDwgReceiptItem | null>(null); const [uploadFilesDialogOpen, setUploadFilesDialogOpen] = useState(false); // 상세도면 목록 로드 @@ -169,6 +171,17 @@ export function DetailDrawingDialog({ loadDetailDrawings(); }; + const handleEditClick = (detailDrawing: DetailDwgReceiptItem) => { + setEditingDetailDrawing(detailDrawing); + setEditDialogOpen(true); + }; + + const handleEditComplete = () => { + setEditDialogOpen(false); + setEditingDetailDrawing(null); + loadDetailDrawings(); + }; + const handleUploadComplete = () => { setUploadFilesDialogOpen(false); loadFiles(); @@ -235,7 +248,7 @@ export function DetailDrawingDialog({ </CardHeader> <CardContent className="flex-1 overflow-y-auto p-4"> <DrawingListTable<DetailDwgReceiptItem, unknown> - columns={createDetailDrawingColumns(lng, t)} + columns={createDetailDrawingColumns(lng, t, handleEditClick)} data={detailDrawings} onRowClick={setSelectedDetail} selectedRow={selectedDetail || undefined} @@ -291,7 +304,7 @@ export function DetailDrawingDialog({ </DialogContent> </Dialog> - <AddDetailDrawingDialog + <AddAndModifyDetailDrawingDialog open={addDialogOpen} onOpenChange={setAddDialogOpen} drawing={drawing} @@ -302,6 +315,22 @@ export function DetailDrawingDialog({ onComplete={handleAddComplete} drawingKind={drawingKind} lng={lng} + mode="add" + /> + + <AddAndModifyDetailDrawingDialog + open={editDialogOpen} + onOpenChange={setEditDialogOpen} + drawing={drawing} + vendorCode={vendorCode} + userId={userId} + userName={userName} + userEmail={userEmail} + onComplete={handleEditComplete} + drawingKind={drawingKind} + lng={lng} + mode="edit" + detailDrawing={editingDetailDrawing} /> {selectedDetail && ( diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx index 09f68614..e8d82129 100644 --- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx +++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx @@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Upload, FolderOpen, Loader2, X, FileText, AlertCircle } from "lucide-react"; import { toast } from "sonner"; +import { useTranslation } from "@/i18n/client"; import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress"; import { uploadFilesWithProgress, type UploadResult } from "../utils/upload-with-progress"; import { FileUploadProgressList } from "../components/file-upload-progress-list"; @@ -26,7 +27,7 @@ interface UploadFilesToDetailDialogProps { revNo: string; userId: string; onUploadComplete?: () => void; - lng?: string; // i18n support + lng: string; } export function UploadFilesToDetailDialog({ @@ -37,7 +38,9 @@ export function UploadFilesToDetailDialog({ revNo, userId, onUploadComplete, + lng, }: UploadFilesToDetailDialogProps) { + const { t } = useTranslation(lng, "dolce"); const [isUploading, setIsUploading] = useState(false); // 파일 업로드 훅 사용 (진행도 추적) @@ -62,7 +65,7 @@ export function UploadFilesToDetailDialog({ // 업로드 처리 const handleUpload = async () => { if (selectedFiles.length === 0) { - toast.error("파일을 선택해주세요"); + toast.error(t("uploadFilesDialog.selectFilesError")); return; } @@ -93,16 +96,16 @@ export function UploadFilesToDetailDialog({ }); if (result.success) { - toast.success(`${result.uploadedCount}개 파일 업로드 완료`); + toast.success(t("uploadFilesDialog.uploadSuccess", { count: result.uploadedCount })); onOpenChange(false); onUploadComplete?.(); } else { - toast.error(result.error || "업로드 실패"); + toast.error(result.error || t("uploadFilesDialog.uploadError")); } } catch (error) { console.error("업로드 실패:", error); toast.error( - error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다" + error instanceof Error ? error.message : t("uploadFilesDialog.uploadErrorMessage") ); } finally { setIsUploading(false); @@ -113,9 +116,9 @@ export function UploadFilesToDetailDialog({ <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-2xl"> <DialogHeader> - <DialogTitle>파일 업로드</DialogTitle> + <DialogTitle>{t("uploadFilesDialog.title")}</DialogTitle> <DialogDescription> - {drawingNo} - Rev. {revNo}에 파일을 업로드합니다 + {t("uploadFilesDialog.description", { drawingNo, revNo })} </DialogDescription> </DialogHeader> @@ -124,7 +127,7 @@ export function UploadFilesToDetailDialog({ <Alert> <AlertCircle className="h-4 w-4" /> <AlertDescription> - 선택한 상세도면의 UploadId에 파일을 추가합니다. 파일 업로드 후 자동으로 메타데이터가 저장됩니다. + {t("uploadFilesDialog.alertMessage")} </AlertDescription> </Alert> @@ -152,11 +155,11 @@ export function UploadFilesToDetailDialog({ }`} > {isDragActive - ? "파일을 여기에 놓으세요" - : "클릭하거나 파일을 드래그하여 선택"} + ? t("uploadFilesDialog.dropHereText") + : t("uploadFilesDialog.dragDropText")} </p> <p className="text-xs text-muted-foreground mt-1"> - 여러 파일을 한 번에 업로드할 수 있습니다 (최대 1GB/파일) + {t("uploadFilesDialog.fileInfo")} </p> </div> </div> @@ -172,14 +175,14 @@ export function UploadFilesToDetailDialog({ <> <div className="flex items-center justify-between mb-3"> <h4 className="text-sm font-medium"> - 선택된 파일 ({selectedFiles.length}개) + {t("uploadFilesDialog.selectedFiles", { count: selectedFiles.length })} </h4> <Button variant="ghost" size="sm" onClick={clearFiles} > - 전체 제거 + {t("uploadFilesDialog.removeAll")} </Button> </div> <div className="max-h-60 overflow-y-auto space-y-2"> @@ -219,7 +222,7 @@ export function UploadFilesToDetailDialog({ onClick={() => onOpenChange(false)} disabled={isUploading} > - 취소 + {t("uploadFilesDialog.cancelButton")} </Button> <Button onClick={handleUpload} @@ -228,12 +231,12 @@ export function UploadFilesToDetailDialog({ {isUploading ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - 업로드 중... + {t("uploadFilesDialog.uploadingButton")} </> ) : ( <> <Upload className="mr-2 h-4 w-4" /> - 업로드 ({selectedFiles.length}개) + {t("uploadFilesDialog.uploadButton", { count: selectedFiles.length })} </> )} </Button> diff --git a/lib/dolce/table/detail-drawing-columns.tsx b/lib/dolce/table/detail-drawing-columns.tsx index c082333d..747173af 100644 --- a/lib/dolce/table/detail-drawing-columns.tsx +++ b/lib/dolce/table/detail-drawing-columns.tsx @@ -3,13 +3,25 @@ import { ColumnDef } from "@tanstack/react-table"; import { DetailDwgReceiptItem } from "../actions"; import { formatDolceDateTime } from "../utils/date-formatter"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Edit } from "lucide-react"; // DOLCE API ENM 필드가 제대로 번역되지 않아 직접 매핑 const DRAWING_USAGE_MAP: Record<string, { ko: string; en: string }> = { APP: { ko: "승인용", en: "Approval" }, WOR: { ko: "작업용", en: "Working" }, + // 참조용은 eVCP에서 사용하지 않으나 추가해둠 + REF: { ko: "참조용", en: "Reference" }, REC: { ko: "입수용", en: "GTT→SHI" }, SUB: { ko: "제출용", en: "SHI→GTT" }, + CMT: { ko: "코멘트", en: "Comment" }, }; const REGISTER_KIND_MAP: Record<string, { ko: string; en: string }> = { @@ -19,6 +31,19 @@ const REGISTER_KIND_MAP: Record<string, { ko: string; en: string }> = { WORP: { ko: "작업용 입수도면(Partial)", en: "For Working(Partial)" }, RECW: { ko: "Working 도면입수(GTT→SHI)", en: "Working Dwg(GTT→SHI)" }, RECP: { ko: "Pre. 도면입수(GTT→SHI)", en: "Pre. Dwg(GTT→SHI)" }, + GSUB: { ko: "SHI 입력 문서 (SHI→GTT)", en: "SHI Input Document(SHI→GTT)" }, + CMTQ: { ko: "Comment-TQ", en: "Comment-TQ" }, + CMTM: { ko: "MARK-UP", en: "MARK-UP" }, + // FMEA는 미사용 코드 + "FMEA-R1": { ko: "FMEA 1st Receipt", en: "FMEA 1st Receipt" }, + "FMEA-R2": { ko: "FMEA 2nd Receipt", en: "FMEA 2nd Receipt" }, + "FMEA-1": { ko: "FMEA 1st Submission", en: "FMEA 1st Submission" }, + "FMEA-2": { ko: "FMEA 2nd Submission", en: "FMEA 2nd Submission" }, + // 참조용 도면도 미사용 코드, 번역 받아야 적용 가능 + PREF: { ko: "본선용 참조도면", en: "본선용 참조도면" }, + RREF: { ko: "실적선 참조도면", en: "실적선 참조도면" }, + "PREP-P": { ko: "본선용 참조도면(Partial)", en: "본선용 참조도면(Partial)" }, + "RREP-P": { ko: "실적선 참조도면(Partial)", en: "실적선 참조도면(Partial)" }, }; // 카테고리는 API에서 ENM이 제공되는 것으로 가정 (필요시 추가) @@ -36,11 +61,39 @@ const translateRegisterKind = (code: string | null, lng: string): string => { return lng === "en" ? mapped.en : mapped.ko; }; +// Comment Dialog Component +function CommentDialog({ comment }: { comment: string }) { + const [open, setOpen] = useState(false); + + return ( + <> + <Button + variant="ghost" + className="h-auto py-1 px-2 text-left justify-start font-normal hover:bg-accent" + onClick={() => setOpen(true)} + > + <div className="truncate max-w-[200px]">{comment || "-"}</div> + </Button> + <Dialog open={open} onOpenChange={setOpen}> + <DialogContent className="max-w-2xl"> + <DialogHeader> + <DialogTitle>Comment</DialogTitle> + </DialogHeader> + <div className="max-h-96 overflow-y-auto"> + <p className="whitespace-pre-wrap text-sm">{comment || "코멘트가 없습니다."}</p> + </div> + </DialogContent> + </Dialog> + </> + ); +} + // 다국어 지원 컬럼 생성 함수 export function createDetailDrawingColumns( lng: string, // eslint-disable-next-line @typescript-eslint/no-explicit-any - t: any + t: any, + onEdit?: (detailDrawing: DetailDwgReceiptItem) => void ): ColumnDef<DetailDwgReceiptItem>[] { return [ { @@ -78,17 +131,18 @@ export function createDetailDrawingColumns( return <div>{lng === "en" ? (categoryENM || categoryNM) : (categoryNM || categoryENM)}</div>; }, }, - { - accessorKey: "DrawingUsage", - header: t("detailDrawing.columns.drawingUsage"), - minSize: 100, - cell: ({ row }) => { - // API 응답의 DrawingUsage는 코드값이므로 직접 매핑하여 번역 - const usageCode = row.getValue("DrawingUsage") as string; - const translated = translateDrawingUsage(usageCode, lng); - return <div>{translated}</div>; - }, - }, + // RegisterKind 로 DrawingUsage를 알 수 있으므로 주석 처리 + // { + // accessorKey: "DrawingUsage", + // header: t("detailDrawing.columns.drawingUsage"), + // minSize: 100, + // cell: ({ row }) => { + // // API 응답의 DrawingUsage는 코드값이므로 직접 매핑하여 번역 + // const usageCode = row.getValue("DrawingUsage") as string; + // const translated = translateDrawingUsage(usageCode, lng); + // return <div>{translated}</div>; + // }, + // }, { accessorKey: "RegisterKind", header: t("detailDrawing.columns.registerKind"), @@ -119,5 +173,39 @@ export function createDetailDrawingColumns( return <div className="text-sm text-muted-foreground">{formatDolceDateTime(date)}</div>; }, }, + { + accessorKey: "RegisterDesc", + header: "Comment", + minSize: 200, + maxSize: 200, + cell: ({ row }) => { + const comment = row.getValue("RegisterDesc") as string; + return <CommentDialog comment={comment} />; + }, + }, + { + id: "actions", + header: t("detailDrawing.columns.actions"), + minSize: 100, + maxSize: 100, + cell: ({ row }) => { + const status = row.getValue("Status") as string; + const isEditable = status === "Submitted"; + + return ( + <div className="flex items-center justify-center"> + <Button + variant="ghost" + size="sm" + disabled={!isEditable || !onEdit} + onClick={() => onEdit && onEdit(row.original)} + title={!isEditable ? t("editDetailDialog.statusSubmittedOnly") : t("editDetailDialog.editButton")} + > + <Edit className="h-4 w-4" /> + </Button> + </div> + ); + }, + }, ]; } |
