diff options
Diffstat (limited to 'lib/dolce/dialogs')
| -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 |
6 files changed, 654 insertions, 262 deletions
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> |
