diff options
Diffstat (limited to 'lib/dolce/dialogs')
| -rw-r--r-- | lib/dolce/dialogs/add-detail-drawing-dialog.tsx | 136 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog.tsx | 283 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-upload-validation-dialog.tsx | 30 | ||||
| -rw-r--r-- | lib/dolce/dialogs/detail-drawing-dialog.tsx | 53 | ||||
| -rw-r--r-- | lib/dolce/dialogs/upload-files-to-detail-dialog.tsx | 3 |
5 files changed, 334 insertions, 171 deletions
diff --git a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx index 34d06368..48614ecf 100644 --- a/lib/dolce/dialogs/add-detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/add-detail-drawing-dialog.tsx @@ -26,6 +26,12 @@ import { v4 as uuidv4 } from "uuid"; import { useFileUploadWithProgress } from "../hooks/use-file-upload-with-progress"; import { uploadFilesWithProgress } from "../utils/upload-with-progress"; import { FileUploadProgressList } from "../components/file-upload-progress-list"; +import { + getB3DrawingUsageOptions, + getB3RegisterKindOptions, + getB4DrawingUsageOptions, + getB4RegisterKindOptions +} from "../utils/code-translator"; interface AddDetailDrawingDialogProps { open: boolean; @@ -36,38 +42,10 @@ interface AddDetailDrawingDialogProps { userName: string; userEmail: string; onComplete: () => void; - drawingKind: "B3" | "B4"; // 추가 + drawingKind: "B3" | "B4"; + lng?: string; // i18n support } -// B3 벤더의 선택 옵션 -const B3_DRAWING_USAGE_OPTIONS = [ - { value: "APP", label: "APPROVAL (승인용)" }, - { value: "WOR", label: "WORKING (작업용)" }, -]; - -const B3_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string; revisionRule: string }>> = { - APP: [ - { value: "APPR", label: "승인용 도면 (Full)", revisionRule: "예: A, B, C 또는 R00, R01, R02" }, - { value: "APPR-P", label: "승인용 도면 (Partial)", revisionRule: "예: A, B, C 또는 R00, R01, R02" }, - ], - WOR: [ - { value: "WORK", label: "작업용 입수도면 (Full)", revisionRule: "예: A, B, C 또는 R00, R01, R02" }, - { value: "WORK-P", label: "작업용 입수도면 (Partial)", revisionRule: "예: A, B, C 또는 R00, R01, R02" }, - ], -}; - -// B4 벤더(GTT)의 선택 옵션 -const B4_DRAWING_USAGE_OPTIONS = [ - { value: "REC", label: "RECEIVE (입수용)" }, -]; - -const B4_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string; revisionRule: string }>> = { - REC: [ - { value: "RECP", label: "Pre. 도면입수", revisionRule: "예: R00, R01, R02, R03" }, - { value: "RECW", label: "Working 도면입수", revisionRule: "예: R00, R01, R02, R03" }, - ], -}; - export function AddDetailDrawingDialog({ open, onOpenChange, @@ -78,12 +56,29 @@ export function AddDetailDrawingDialog({ userEmail, onComplete, drawingKind, + lng = "ko", }: AddDetailDrawingDialogProps) { const [drawingUsage, setDrawingUsage] = useState<string>(""); const [registerKind, setRegisterKind] = useState<string>(""); const [revision, setRevision] = useState<string>(""); + const [revisionError, setRevisionError] = useState<string>(""); const [isSubmitting, setIsSubmitting] = useState(false); + // 옵션 생성 (다국어 지원) + const drawingUsageOptions = drawingKind === "B3" + ? getB3DrawingUsageOptions(lng) + : getB4DrawingUsageOptions(lng); + + const registerKindOptions = drawingKind === "B3" + ? getB3RegisterKindOptions(drawingUsage, lng).map(opt => ({ + ...opt, + revisionRule: lng === "ko" ? "예: A, B, C 또는 R00, R01, R02" : "e.g. A, B, C or R00, R01, R02" + })) + : getB4RegisterKindOptions(drawingUsage, lng).map(opt => ({ + ...opt, + revisionRule: lng === "ko" ? "예: R00, R01, R02, R03" : "e.g. R00, R01, R02, R03" + })); + // 파일 업로드 훅 사용 (진행도 추적) const { fileProgresses, @@ -96,11 +91,47 @@ export function AddDetailDrawingDialog({ isDragActive, } = useFileUploadWithProgress(); + // Revision 유효성 검증 함수 + const validateRevision = (value: string): string => { + if (!value.trim()) { + return "Revision을 입력하세요"; + } + + const upperValue = value.toUpperCase().trim(); + + // A-Z 패턴 (단일 알파벳) + if (/^[A-Z]$/.test(upperValue)) { + return ""; + } + + // R00-R99 패턴 + if (/^R\d{2}$/.test(upperValue)) { + return ""; + } + + return "올바른 형식이 아닙니다 (A-Z 또는 R00-R99)"; + }; + + // Revision 입력 핸들러 + const handleRevisionChange = (value: string) => { + const processedValue = value.toUpperCase(); + setRevision(processedValue); + + // 값이 있을 때만 validation + if (processedValue.trim()) { + const error = validateRevision(processedValue); + setRevisionError(error); + } else { + setRevisionError(""); + } + }; + // 폼 초기화 const resetForm = () => { setDrawingUsage(""); setRegisterKind(""); setRevision(""); + setRevisionError(""); clearFiles(); }; @@ -119,8 +150,18 @@ export function AddDetailDrawingDialog({ } if (!revision.trim()) { toast.error("Revision을 입력하세요"); + setRevisionError("Revision을 입력하세요"); return; } + + // Revision 형식 검증 + const revisionValidationError = validateRevision(revision); + if (revisionValidationError) { + toast.error(revisionValidationError); + setRevisionError(revisionValidationError); + return; + } + if (files.length === 0) { toast.error("최소 1개 이상의 파일을 첨부해야 합니다"); return; @@ -222,19 +263,21 @@ export function AddDetailDrawingDialog({ const handleDrawingUsageChange = (value: string) => { setDrawingUsage(value); setRegisterKind(""); + setRevision(""); + setRevisionError(""); }; - // 현재 선택 가능한 DrawingUsage 및 RegisterKind 옵션 - const drawingUsageOptions = drawingKind === "B4" ? B4_DRAWING_USAGE_OPTIONS : B3_DRAWING_USAGE_OPTIONS; - const registerKindOptionsMap = drawingKind === "B4" ? B4_REGISTER_KIND_OPTIONS : B3_REGISTER_KIND_OPTIONS; - - const registerKindOptions = drawingUsage - ? registerKindOptionsMap[drawingUsage] || [] - : []; - // 선택된 RegisterKind의 Revision Rule const revisionRule = registerKindOptions.find((opt) => opt.value === registerKind)?.revisionRule || ""; + // 추가 버튼 활성화 조건 + const isFormValid = + drawingUsage.trim() !== "" && + registerKind.trim() !== "" && + revision.trim() !== "" && + !revisionError && + files.length > 0; + return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-2xl"> @@ -302,10 +345,21 @@ export function AddDetailDrawingDialog({ <Label>Revision</Label> <Input value={revision} - onChange={(e) => setRevision(e.target.value)} + onChange={(e) => handleRevisionChange(e.target.value)} placeholder="예: A, B, R00, R01" disabled={!registerKind} + className={revisionError ? "border-red-500 focus-visible:ring-red-500" : ""} /> + {revisionError && ( + <p className="text-sm text-red-500 flex items-center gap-1"> + {revisionError} + </p> + )} + {!revisionError && revision && ( + <p className="text-sm text-green-600 flex items-center gap-1"> + ✓ 올바른 형식입니다 + </p> + )} </div> {/* 파일 업로드 */} @@ -366,7 +420,7 @@ export function AddDetailDrawingDialog({ 전체 제거 </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {files.map((file, index) => ( <div key={index} @@ -400,7 +454,7 @@ export function AddDetailDrawingDialog({ <Button variant="outline" onClick={handleCancel} disabled={isSubmitting}> 취소 </Button> - <Button onClick={handleSubmit} disabled={isSubmitting}> + <Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid}> {isSubmitting ? "처리 중..." : "추가"} </Button> </DialogFooter> diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx index f4816328..1be7f226 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog.tsx @@ -22,6 +22,7 @@ import { import { FolderOpen, Loader2, ChevronRight, ChevronLeft, CheckCircle2 } from "lucide-react"; import { toast } from "sonner"; import { Progress } from "@/components/ui/progress"; +import { useTranslation } from "@/i18n/client"; import { validateB4FileName, B4UploadValidationDialog, @@ -29,10 +30,11 @@ import { } from "./b4-upload-validation-dialog"; import { checkB4MappingStatus, - bulkUploadB4Files, + editDetailDwgReceipt, type MappingCheckItem, type B4BulkUploadResult, } from "../actions"; +import { v4 as uuidv4 } from "uuid"; interface B4BulkUploadDialogProps { open: boolean; @@ -43,20 +45,9 @@ interface B4BulkUploadDialogProps { userEmail: string; vendorCode: string; onUploadComplete?: () => void; + lng: string; } -// B4 GTT 옵션 -const B4_DRAWING_USAGE_OPTIONS = [ - { value: "REC", label: "RECEIVE (입수용)" }, -]; - -const B4_REGISTER_KIND_OPTIONS: Record<string, Array<{ value: string; label: string }>> = { - REC: [ - { value: "RECP", label: "Pre. 도면입수" }, - { value: "RECW", label: "Working 도면입수" }, - ], -}; - type UploadStep = "settings" | "files" | "validation" | "uploading" | "complete"; export function B4BulkUploadDialog({ @@ -68,7 +59,9 @@ export function B4BulkUploadDialog({ userEmail, vendorCode, onUploadComplete, + lng, }: B4BulkUploadDialogProps) { + const { t } = useTranslation(lng, "dolce"); const [currentStep, setCurrentStep] = useState<UploadStep>("settings"); const [drawingUsage, setDrawingUsage] = useState<string>("REC"); const [registerKind, setRegisterKind] = useState<string>(""); @@ -80,6 +73,18 @@ export function B4BulkUploadDialog({ const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState<B4BulkUploadResult | null>(null); + // B4 GTT 옵션 (코드 번역 유틸리티 사용) + const drawingUsageOptions = [ + { value: "REC", label: t("bulkUpload.drawingUsageReceive") }, + ]; + + const registerKindOptionsMap: Record<string, Array<{ value: string; label: string }>> = { + REC: [ + { value: "RECP", label: t("bulkUpload.registerKindRecP") }, + { value: "RECW", label: t("bulkUpload.registerKindRecW") }, + ], + }; + // 다이얼로그 닫을 때 초기화 React.useEffect(() => { if (!open) { @@ -104,12 +109,12 @@ export function B4BulkUploadDialog({ const newFiles = files.filter((f) => !existingNames.has(f.name)); if (newFiles.length === 0) { - toast.error("이미 선택된 파일입니다"); + toast.error(t("bulkUpload.duplicateFileError")); return; } setSelectedFiles((prev) => [...prev, ...newFiles]); - toast.success(`${newFiles.length}개 파일이 선택되었습니다`); + toast.success(t("bulkUpload.filesSelectedSuccess", { count: newFiles.length })); }; // Drag & Drop 핸들러 @@ -152,7 +157,7 @@ export function B4BulkUploadDialog({ // 1단계 완료 (설정) const handleSettingsNext = () => { if (!registerKind) { - toast.error("등록종류를 선택하세요"); + toast.error(t("bulkUpload.selectRegisterKindError")); return; } setCurrentStep("files"); @@ -161,7 +166,7 @@ export function B4BulkUploadDialog({ // 2단계 완료 (파일 선택) const handleFilesNext = () => { if (selectedFiles.length === 0) { - toast.error("파일을 선택해주세요"); + toast.error(t("bulkUpload.selectFilesError")); return; } setCurrentStep("validation"); @@ -219,7 +224,7 @@ export function B4BulkUploadDialog({ return { ...parseResult, mappingStatus: "not_found" as const, - error: "DOLCE 시스템에서 도면을 찾을 수 없습니다", + error: t("validation.notFound"), }; } @@ -228,7 +233,7 @@ export function B4BulkUploadDialog({ return { ...parseResult, mappingStatus: "not_found" as const, - error: "해당 도면번호가 프로젝트에 등록되어 있지 않습니다", + error: t("validation.notRegistered"), }; } @@ -237,7 +242,7 @@ export function B4BulkUploadDialog({ return { ...parseResult, mappingStatus: "not_found" as const, - error: "도면입수(GTT Deliverables)인 도면만 업로드 가능합니다", + error: t("validation.notGttDeliverables"), }; } @@ -255,7 +260,7 @@ export function B4BulkUploadDialog({ } catch (error) { console.error("검증 실패:", error); toast.error( - error instanceof Error ? error.message : "검증 중 오류가 발생했습니다" + error instanceof Error ? error.message : t("bulkUpload.validationError") ); } }; @@ -266,69 +271,146 @@ export function B4BulkUploadDialog({ setCurrentStep("uploading"); setShowValidationDialog(false); - // 진행률 시뮬레이션 - const progressInterval = setInterval(() => { - setUploadProgress((prev) => { - if (prev >= 90) { - clearInterval(progressInterval); - return prev; + try { + console.log(`[B4 일괄 업로드] 시작: ${validFiles.length}개 파일`); + + // 파일을 DrawingNo + RevNo로 그룹화 + const uploadGroups = new Map< + string, + Array<{ + file: File; + drawingNo: string; + revNo: string; + fileName: string; + registerGroupId: number; + }> + >(); + + validFiles.forEach((fileResult) => { + const groupKey = `${fileResult.parsed!.drawingNo}_${fileResult.parsed!.revNo}`; + if (!uploadGroups.has(groupKey)) { + uploadGroups.set(groupKey, []); } - return prev + 10; + uploadGroups.get(groupKey)!.push({ + file: fileResult.file, + drawingNo: fileResult.parsed!.drawingNo, + revNo: fileResult.parsed!.revNo, + fileName: fileResult.file.name, + registerGroupId: fileResult.registerGroupId || 0, + }); }); - }, 500); - try { - // FormData 생성 - const formData = new FormData(); - formData.append("projectNo", projectNo); - formData.append("userId", userId); - formData.append("userName", userName); - formData.append("userEmail", userEmail); - formData.append("vendorCode", vendorCode); - formData.append("registerKind", registerKind); // RegisterKind 추가 - - // 파일 및 메타데이터 추가 - validFiles.forEach((fileResult, index) => { - formData.append(`file_${index}`, fileResult.file); - formData.append(`drawingNo_${index}`, fileResult.parsed!.drawingNo); - formData.append(`revNo_${index}`, fileResult.parsed!.revNo); - formData.append(`fileName_${index}`, fileResult.file.name); - formData.append( - `registerGroupId_${index}`, - String(fileResult.registerGroupId || 0) - ); - }); + console.log(`[B4 일괄 업로드] ${uploadGroups.size}개 그룹으로 묶임`); + + let successCount = 0; + let failCount = 0; + let completedGroups = 0; + + // 각 그룹별로 순차 처리 + for (const [groupKey, files] of uploadGroups.entries()) { + const { drawingNo, revNo, registerGroupId } = files[0]; + + try { + console.log(`[B4 업로드] 그룹 ${groupKey}: ${files.length}개 파일`); + + // 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, + }); + + if (!uploadResponse.ok) { + throw new Error(`파일 업로드 실패: ${uploadResponse.status}`); + } + + const uploadResult = await uploadResponse.json(); + + if (!uploadResult.success) { + throw new Error(uploadResult.error || "파일 업로드 실패"); + } + + console.log(`[B4 업로드] 그룹 ${groupKey} 파일 업로드 완료`); + + // 3. 상세도면 등록 + await editDetailDwgReceipt({ + dwgList: [ + { + Mode: "ADD", + Status: "Draft", + RegisterId: 0, + ProjectNo: projectNo, + Discipline: "", + DrawingKind: "B4", + DrawingNo: drawingNo, + DrawingName: "", + RegisterGroupId: registerGroupId, + RegisterSerialNo: 0, + RegisterKind: registerKind, + DrawingRevNo: revNo, + Category: "TS", + Receiver: null, + Manager: "", + RegisterDesc: "", + UploadId: uploadId, + RegCompanyCode: vendorCode, + }, + ], + userId, + userNm: userName, + vendorCode, + email: userEmail, + }); + + console.log(`[B4 업로드] 그룹 ${groupKey} 상세도면 등록 완료`); + + successCount += files.length; + } catch (error) { + console.error(`[B4 업로드] 그룹 ${groupKey} 실패:`, error); + failCount += files.length; + } - formData.append("fileCount", String(validFiles.length)); + // 진행도 업데이트 + completedGroups++; + const progress = Math.round((completedGroups / uploadGroups.size) * 100); + setUploadProgress(progress); + } - // 서버 액션 호출 - const result: B4BulkUploadResult = await bulkUploadB4Files(formData); + console.log(`[B4 일괄 업로드] ✅ 완료: 성공 ${successCount}, 실패 ${failCount}`); - clearInterval(progressInterval); - setUploadProgress(100); - setUploadResult(result); + const result: B4BulkUploadResult = { + success: true, + successCount, + failCount, + }; - if (result.success) { - setCurrentStep("complete"); - toast.success( - `${result.successCount}/${validFiles.length}개 파일 업로드 완료` - ); - } else { - setCurrentStep("files"); - toast.error(result.error || "업로드 실패"); - } + setUploadResult(result); + setCurrentStep("complete"); + toast.success(t("bulkUpload.uploadSuccessToast", { successCount, total: validFiles.length })); } catch (error) { - console.error("업로드 실패:", error); + console.error("[B4 일괄 업로드] 실패:", error); toast.error( - error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다" + error instanceof Error ? error.message : t("bulkUpload.uploadError") ); + setCurrentStep("files"); } finally { setIsUploading(false); } }; const registerKindOptions = drawingUsage - ? B4_REGISTER_KIND_OPTIONS[drawingUsage] || [] + ? registerKindOptionsMap[drawingUsage] || [] : []; const handleDrawingUsageChange = (value: string) => { @@ -341,11 +423,11 @@ export function B4BulkUploadDialog({ <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-2xl"> <DialogHeader> - <DialogTitle>B4 일괄 업로드</DialogTitle> + <DialogTitle>{t("bulkUpload.title")}</DialogTitle> <DialogDescription> - {currentStep === "settings" && "업로드 설정을 선택하세요"} - {currentStep === "files" && "파일명 형식: [버림] [DrawingNo] [RevNo].[확장자] (예: testfile GTT-DE-007 R01.pdf)"} - {currentStep === "validation" && "파일 검증 중..."} + {currentStep === "settings" && t("bulkUpload.stepSettings")} + {currentStep === "files" && t("bulkUpload.stepFiles")} + {currentStep === "validation" && t("bulkUpload.stepValidation")} </DialogDescription> </DialogHeader> @@ -355,13 +437,13 @@ export function B4BulkUploadDialog({ <> {/* 도면용도 선택 */} <div className="space-y-2"> - <Label>도면용도 (Drawing Usage) *</Label> + <Label>{t("bulkUpload.drawingUsage")} *</Label> <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> <SelectTrigger> - <SelectValue placeholder="도면용도를 선택하세요" /> + <SelectValue placeholder={t("bulkUpload.drawingUsagePlaceholder")} /> </SelectTrigger> <SelectContent> - {B4_DRAWING_USAGE_OPTIONS.map((option) => ( + {drawingUsageOptions.map((option) => ( <SelectItem key={option.value} value={option.value}> {option.label} </SelectItem> @@ -372,14 +454,14 @@ export function B4BulkUploadDialog({ {/* 등록종류 선택 */} <div className="space-y-2"> - <Label>등록종류 (Register Kind) *</Label> + <Label>{t("bulkUpload.registerKind")} *</Label> <Select value={registerKind} onValueChange={setRegisterKind} disabled={!drawingUsage} > <SelectTrigger> - <SelectValue placeholder="등록종류를 선택하세요" /> + <SelectValue placeholder={t("bulkUpload.registerKindPlaceholder")} /> </SelectTrigger> <SelectContent> {registerKindOptions.map((option) => ( @@ -390,7 +472,7 @@ export function B4BulkUploadDialog({ </SelectContent> </Select> <p className="text-sm text-muted-foreground"> - 선택한 등록종류가 모든 파일에 적용됩니다 + {t("bulkUpload.registerKindNote")} </p> </div> </> @@ -436,11 +518,11 @@ export function B4BulkUploadDialog({ }`} > {isDragging - ? "파일을 여기에 놓으세요" - : "클릭하거나 파일을 드래그하여 선택"} + ? t("bulkUpload.fileDropHere") + : t("bulkUpload.fileSelectArea")} </p> <p className="text-xs text-muted-foreground mt-1"> - PDF, DOC, DOCX, XLS, XLSX, DWG, DXF, ZIP + {t("bulkUpload.fileTypes")} </p> </label> </div> @@ -450,17 +532,17 @@ export function B4BulkUploadDialog({ <div className="border rounded-lg p-4"> <div className="flex items-center justify-between mb-3"> <h4 className="text-sm font-medium"> - 선택된 파일 ({selectedFiles.length}개) + {t("bulkUpload.selectedFiles", { count: selectedFiles.length })} </h4> <Button variant="ghost" size="sm" onClick={() => setSelectedFiles([])} > - 전체 제거 + {t("bulkUpload.removeAll")} </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {selectedFiles.map((file, index) => ( <div key={index} @@ -477,7 +559,7 @@ export function B4BulkUploadDialog({ size="sm" onClick={() => handleRemoveFile(index)} > - 제거 + {t("bulkUpload.removeFile")} </Button> </div> ))} @@ -492,7 +574,7 @@ export function B4BulkUploadDialog({ <div className="flex flex-col items-center justify-center py-12"> <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" /> <p className="text-sm text-muted-foreground"> - 파일 검증 중입니다... + {t("bulkUpload.validating")} </p> </div> )} @@ -502,17 +584,23 @@ export function B4BulkUploadDialog({ <div className="space-y-6 py-8"> <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">파일 업로드 중...</h3> + <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploading")}</h3> <p className="text-sm text-muted-foreground"> - 잠시만 기다려주세요 + {t("bulkUpload.uploadingWait")} </p> </div> <div className="space-y-2"> <div className="flex justify-between text-sm"> - <span>진행률</span> + <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> </div> )} @@ -522,16 +610,16 @@ export function B4BulkUploadDialog({ <div className="space-y-6 py-8"> <div className="flex flex-col items-center"> <CheckCircle2 className="h-16 w-16 text-green-500 mb-4" /> - <h3 className="text-lg font-semibold mb-2">업로드 완료!</h3> + <h3 className="text-lg font-semibold mb-2">{t("bulkUpload.uploadComplete")}</h3> <p className="text-sm text-muted-foreground"> - {uploadResult.successCount}개 파일이 성공적으로 업로드되었습니다 + {t("bulkUpload.uploadSuccessMessage", { count: uploadResult.successCount })} </p> </div> {uploadResult.failCount && uploadResult.failCount > 0 && ( <div className="bg-yellow-50 dark:bg-yellow-950/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4"> <p className="text-sm text-yellow-800 dark:text-yellow-200"> - {uploadResult.failCount}개 파일 업로드 실패 + {t("bulkUpload.uploadFailMessage", { count: uploadResult.failCount })} </p> </div> )} @@ -543,7 +631,7 @@ export function B4BulkUploadDialog({ onUploadComplete?.(); }} > - 확인 + {t("bulkUpload.confirmButton")} </Button> </div> </div> @@ -559,13 +647,13 @@ export function B4BulkUploadDialog({ variant="outline" onClick={() => onOpenChange(false)} > - 취소 + {t("bulkUpload.cancelButton")} </Button> <Button onClick={handleSettingsNext} disabled={!registerKind} > - 다음 + {t("bulkUpload.nextButton")} <ChevronRight className="ml-2 h-4 w-4" /> </Button> </> @@ -578,13 +666,13 @@ export function B4BulkUploadDialog({ onClick={() => setCurrentStep("settings")} > <ChevronLeft className="mr-2 h-4 w-4" /> - 이전 + {t("bulkUpload.previousButton")} </Button> <Button onClick={handleFilesNext} disabled={selectedFiles.length === 0} > - 검증 시작 + {t("bulkUpload.validateButton")} <ChevronRight className="ml-2 h-4 w-4" /> </Button> </> @@ -601,6 +689,7 @@ export function B4BulkUploadDialog({ validationResults={validationResults} onConfirmUpload={handleConfirmUpload} isUploading={isUploading} + lng={lng} /> </> ); diff --git a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx index b274d604..f3a7c70a 100644 --- a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx +++ b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx @@ -39,8 +39,11 @@ interface B4UploadValidationDialogProps { /** * B4 파일명 검증 함수 - * 형식: [버림] [DrawingNo] [RevNo].[확장자] - * 예시: "testfile GTT-DE-007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" + * 형식: [버림] [문서번호토큰1] [문서번호토큰2] ... [리비전번호].[확장자] + * 예시: "testfile GTT DE 007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" + * - 첫 번째 토큰은 버림 + * - 마지막 토큰은 RevNo + * - 중간 토큰들을 "-"로 연결하여 DrawingNo 생성 */ export function validateB4FileName(fileName: string): { valid: boolean; @@ -57,23 +60,25 @@ export function validateB4FileName(fileName: string): { }; } - const extension = fileName.substring(lastDotIndex + 1); const nameWithoutExt = fileName.substring(0, lastDotIndex); // 공백으로 분리 const parts = nameWithoutExt.split(" ").filter(p => p.trim() !== ""); - // 최소 3개 파트 필요: [버림], DrawingNo, RevNo + // 최소 3개 파트 필요: [버림], [문서번호토큰], [RevNo] if (parts.length < 3) { return { valid: false, - error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [DrawingNo] [RevNo].[확장자]`, + error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [문서번호토큰들...] [RevNo].[확장자]`, }; } // 첫 번째 토큰은 버림 - const drawingNo = parts[1]; - const revNo = parts[2]; + // 마지막 토큰은 RevNo + // 중간 토큰들을 "-"로 연결하여 DrawingNo 생성 + const revNo = parts[parts.length - 1]; + const drawingTokens = parts.slice(1, parts.length - 1); + const drawingNo = drawingTokens.join("-"); // 필수 항목이 비어있지 않은지 확인 if (!drawingNo || drawingNo.trim() === "") { @@ -307,16 +312,19 @@ export function B4UploadValidationDialog({ 📋 올바른 파일명 형식 </div> <code className="text-xs text-blue-700 dark:text-blue-300"> - [버림] [DrawingNo] [RevNo].[확장자] + [버림] [문서번호토큰1] [문서번호토큰2] ... [RevNo].[확장자] </code> <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> - 예: testfile GTT-DE-007 R01.pdf + 예: 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"> - ※ 첫 번째 단어는 무시되며, 공백으로 구분됩니다 + ※ 첫 번째 단어는 무시됩니다 </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)가 됩니다 </div> </div> </div> diff --git a/lib/dolce/dialogs/detail-drawing-dialog.tsx b/lib/dolce/dialogs/detail-drawing-dialog.tsx index a06c9688..d9df58db 100644 --- a/lib/dolce/dialogs/detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/detail-drawing-dialog.tsx @@ -12,6 +12,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Plus, RefreshCw, Upload, Loader2 } from "lucide-react"; import { toast } from "sonner"; +import { useTranslation } from "@/i18n/client"; import { UnifiedDwgReceiptItem, DetailDwgReceiptItem, @@ -20,7 +21,7 @@ import { fetchFileInfoList, } from "../actions"; import { DrawingListTable } from "../table/drawing-list-table"; -import { detailDrawingColumns } from "../table/detail-drawing-columns"; +import { createDetailDrawingColumns } from "../table/detail-drawing-columns"; import { createFileListColumns } from "../table/file-list-columns"; import { AddDetailDrawingDialog } from "./add-detail-drawing-dialog"; import { UploadFilesToDetailDialog } from "./upload-files-to-detail-dialog"; @@ -34,6 +35,7 @@ interface DetailDrawingDialogProps { userName: string; userEmail: string; drawingKind: "B3" | "B4"; + lng: string; } export function DetailDrawingDialog({ @@ -45,7 +47,9 @@ export function DetailDrawingDialog({ userName, userEmail, drawingKind, + lng, }: DetailDrawingDialogProps) { + const { t } = useTranslation(lng, "dolce"); const [detailDrawings, setDetailDrawings] = useState<DetailDwgReceiptItem[]>([]); const [selectedDetail, setSelectedDetail] = useState<DetailDwgReceiptItem | null>(null); const [files, setFiles] = useState<FileInfoItem[]>([]); @@ -75,11 +79,11 @@ export function DetailDrawingDialog({ } } catch (error) { console.error("상세도면 로드 실패:", error); - toast.error("상세도면 로드에 실패했습니다"); + toast.error(t("detailDialog.detailLoadError")); } finally { setIsLoading(false); } - }, [drawing, selectedDetail]); + }, [drawing, selectedDetail, t]); // 파일 목록 로드 const loadFiles = useCallback(async () => { @@ -94,11 +98,11 @@ export function DetailDrawingDialog({ setFiles(data); } catch (error) { console.error("파일 목록 로드 실패:", error); - toast.error("파일 목록 로드에 실패했습니다"); + toast.error(t("detailDialog.fileLoadError")); } finally { setIsLoadingFiles(false); } - }, [selectedDetail]); + }, [selectedDetail, t]); // 다이얼로그 열릴 때 데이터 로드 useEffect(() => { @@ -120,7 +124,7 @@ export function DetailDrawingDialog({ const handleDownload = async (file: FileInfoItem) => { try { - toast.info("파일 다운로드를 준비 중입니다..."); + toast.info(t("detailDialog.downloadPreparing")); // 파일 생성자의 userId를 사용하여 다운로드 const response = await fetch("/api/dolce/download", { @@ -136,7 +140,7 @@ export function DetailDrawingDialog({ }); if (!response.ok) { - throw new Error("파일 다운로드 실패"); + throw new Error(t("detailDialog.downloadError")); } const blob = await response.blob(); @@ -149,10 +153,10 @@ export function DetailDrawingDialog({ window.URL.revokeObjectURL(url); document.body.removeChild(a); - toast.success("파일 다운로드가 완료되었습니다"); + toast.success(t("detailDialog.downloadSuccess")); } catch (error) { console.error("파일 다운로드 실패:", error); - toast.error("파일 다운로드에 실패했습니다"); + toast.error(t("detailDialog.downloadError")); } }; @@ -170,7 +174,7 @@ export function DetailDrawingDialog({ loadFiles(); }; - const fileColumns = createFileListColumns({ onDownload: handleDownload }); + const fileColumns = createFileListColumns({ onDownload: handleDownload, lng }); // RegisterId + UploadId 조합으로 고유 ID 생성 const getDetailDrawingId = (detail: DetailDwgReceiptItem) => { @@ -188,10 +192,15 @@ export function DetailDrawingDialog({ <DialogContent className="max-w-[95vw] h-[90vh] flex flex-col"> <DialogHeader> <DialogTitle className="flex flex-col gap-1"> - <span>상세도면 정보</span> + <span>{t("detailDialog.title")}</span> {drawing && ( <span className="text-sm font-normal text-muted-foreground"> - {drawing.DrawingNo} | 프로젝트: {drawing.ProjectNo} | Discipline: {drawing.Discipline} | 종류: {drawing.DrawingKind} + {t("detailDialog.subtitle", { + drawingNo: drawing.DrawingNo, + projectNo: drawing.ProjectNo, + discipline: drawing.Discipline, + drawingKind: drawing.DrawingKind + })} </span> )} </DialogTitle> @@ -201,7 +210,7 @@ export function DetailDrawingDialog({ {/* 상단: 상세도면 리스트 */} <Card className="flex-1 overflow-hidden flex flex-col"> <CardHeader className="flex-row items-center justify-between py-3"> - <CardTitle className="text-base">상세도면 목록</CardTitle> + <CardTitle className="text-base">{t("detailDialog.detailListTitle")}</CardTitle> <div className="flex gap-2"> <Button variant="outline" @@ -210,7 +219,7 @@ export function DetailDrawingDialog({ disabled={isLoading} > <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? "animate-spin" : ""}`} /> - 새로고침 + {t("detailDialog.refreshButton")} </Button> {canAddDetailDrawing && ( <Button @@ -219,14 +228,14 @@ export function DetailDrawingDialog({ onClick={() => setAddDialogOpen(true)} > <Plus className="h-4 w-4 mr-2" /> - 상세도면 추가 + {t("detailDialog.addDetailButton")} </Button> )} </div> </CardHeader> <CardContent className="flex-1 overflow-y-auto p-4"> <DrawingListTable<DetailDwgReceiptItem, unknown> - columns={detailDrawingColumns} + columns={createDetailDrawingColumns(lng, t)} data={detailDrawings} onRowClick={setSelectedDetail} selectedRow={selectedDetail || undefined} @@ -239,8 +248,8 @@ export function DetailDrawingDialog({ <Card className="flex-1 overflow-hidden flex flex-col"> <CardHeader className="flex-row items-center justify-between py-3"> <CardTitle className="text-base"> - 첨부파일 목록 - {selectedDetail && ` - Rev. ${selectedDetail.DrawingRevNo}`} + {t("detailDialog.fileListTitle")} + {selectedDetail && t("detailDialog.fileListSubtitle", { revNo: selectedDetail.DrawingRevNo })} </CardTitle> {selectedDetail && canAddDetailDrawing && ( <Button @@ -249,20 +258,20 @@ export function DetailDrawingDialog({ onClick={() => setUploadFilesDialogOpen(true)} > <Upload className="h-4 w-4 mr-2" /> - 파일 업로드 + {t("detailDialog.uploadFilesButton")} </Button> )} </CardHeader> <CardContent className="flex-1 overflow-y-auto p-4"> {!selectedDetail ? ( <div className="h-full flex items-center justify-center text-muted-foreground"> - 상세도면을 선택하세요 + {t("detailDialog.selectDetailDrawing")} </div> ) : isLoadingFiles ? ( <div className="space-y-4"> <div className="flex items-center justify-center gap-2 text-muted-foreground py-8"> <Loader2 className="h-5 w-5 animate-spin" /> - <span>Loading files...</span> + <span>{t("detailDialog.loadingFiles")}</span> </div> <div className="space-y-2"> <Skeleton className="h-10 w-full" /> @@ -292,6 +301,7 @@ export function DetailDrawingDialog({ userEmail={userEmail} onComplete={handleAddComplete} drawingKind={drawingKind} + lng={lng} /> {selectedDetail && ( @@ -303,6 +313,7 @@ export function DetailDrawingDialog({ revNo={selectedDetail.DrawingRevNo} userId={userId} onUploadComplete={handleUploadComplete} + lng={lng} /> )} </> diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx index af73aea6..09f68614 100644 --- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx +++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx @@ -26,6 +26,7 @@ interface UploadFilesToDetailDialogProps { revNo: string; userId: string; onUploadComplete?: () => void; + lng?: string; // i18n support } export function UploadFilesToDetailDialog({ @@ -181,7 +182,7 @@ export function UploadFilesToDetailDialog({ 전체 제거 </Button> </div> - <div className="max-h-48 overflow-auto space-y-2"> + <div className="max-h-60 overflow-y-auto space-y-2"> {selectedFiles.map((file, index) => ( <div key={index} |
