diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-23 16:40:37 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-23 16:40:37 +0900 |
| commit | fd4909bba7be8abc1eeab9ae1b4621c66a61604a (patch) | |
| tree | d375995611de80b55b344b1c536c5a760f06ccb6 /lib/dolce/dialogs/b4-upload-validation-dialog.tsx | |
| parent | a2e0785c8749c4d3766ecf3b70edfb7c2fe4df20 (diff) | |
(김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요)
Diffstat (limited to 'lib/dolce/dialogs/b4-upload-validation-dialog.tsx')
| -rw-r--r-- | lib/dolce/dialogs/b4-upload-validation-dialog.tsx | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/lib/dolce/dialogs/b4-upload-validation-dialog.tsx b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx new file mode 100644 index 00000000..b274d604 --- /dev/null +++ b/lib/dolce/dialogs/b4-upload-validation-dialog.tsx @@ -0,0 +1,353 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { CheckCircle2, XCircle, AlertCircle, Upload } from "lucide-react"; + +export interface ParsedFileInfo { + drawingNo: string; + revNo: string; + fileName: string; +} + +export interface FileValidationResult { + file: File; + valid: boolean; + parsed?: ParsedFileInfo; + error?: string; + mappingStatus?: "available" | "not_found"; + drawingName?: string; + registerGroupId?: number; +} + +interface B4UploadValidationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + validationResults: FileValidationResult[]; + onConfirmUpload: (validFiles: FileValidationResult[]) => void; + isUploading: boolean; +} + +/** + * B4 파일명 검증 함수 + * 형식: [버림] [DrawingNo] [RevNo].[확장자] + * 예시: "testfile GTT-DE-007 R01.pdf" → DrawingNo: "GTT-DE-007", RevNo: "R01" + */ +export function validateB4FileName(fileName: string): { + valid: boolean; + parsed?: ParsedFileInfo; + error?: string; +} { + try { + // 확장자 분리 + const lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex === -1) { + return { + valid: false, + error: "파일 확장자가 없습니다", + }; + } + + const extension = fileName.substring(lastDotIndex + 1); + const nameWithoutExt = fileName.substring(0, lastDotIndex); + + // 공백으로 분리 + const parts = nameWithoutExt.split(" ").filter(p => p.trim() !== ""); + + // 최소 3개 파트 필요: [버림], DrawingNo, RevNo + if (parts.length < 3) { + return { + valid: false, + error: `공백이 최소 2개 있어야 합니다 (현재: ${parts.length - 1}개). 형식: [버림] [DrawingNo] [RevNo].[확장자]`, + }; + } + + // 첫 번째 토큰은 버림 + const drawingNo = parts[1]; + const revNo = parts[2]; + + // 필수 항목이 비어있지 않은지 확인 + if (!drawingNo || drawingNo.trim() === "") { + return { + valid: false, + error: "도면번호(DrawingNo)가 비어있습니다", + }; + } + + if (!revNo || revNo.trim() === "") { + return { + valid: false, + error: "리비전 번호(RevNo)가 비어있습니다", + }; + } + + return { + valid: true, + parsed: { + drawingNo: drawingNo.trim(), + revNo: revNo.trim(), + fileName: fileName, + }, + }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : "알 수 없는 오류", + }; + } +} + +/** + * B4 업로드 전 파일 검증 다이얼로그 + */ +export function B4UploadValidationDialog({ + open, + onOpenChange, + validationResults, + onConfirmUpload, + isUploading, +}: B4UploadValidationDialogProps) { + const validFiles = validationResults.filter((r) => r.valid && r.mappingStatus === "available"); + const notFoundFiles = validationResults.filter((r) => r.valid && r.mappingStatus === "not_found"); + const invalidFiles = validationResults.filter((r) => !r.valid); + + const handleUpload = () => { + if (validFiles.length > 0) { + onConfirmUpload(validFiles); + } + }; + + const handleCancel = () => { + if (!isUploading) { + onOpenChange(false); + } + }; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col"> + <DialogHeader className="flex-shrink-0"> + <DialogTitle>B4 일괄 업로드 검증</DialogTitle> + <DialogDescription> + 선택한 파일의 파일명 형식과 매핑 가능 여부를 검증합니다 + </DialogDescription> + </DialogHeader> + + <div className="space-y-4 overflow-auto flex-1 pr-2"> + {/* 요약 통계 */} + <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-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-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-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-2xl font-bold text-red-600 dark:text-red-400"> + {invalidFiles.length} + </div> + </div> + </div> + + {/* 경고 메시지 */} + {validFiles.length === 0 && ( + <Alert variant="destructive"> + <XCircle className="h-4 w-4" /> + <AlertDescription> + 업로드 가능한 파일이 없습니다. 파일명 형식을 확인하거나 이미 매핑된 파일은 제외해주세요. + </AlertDescription> + </Alert> + )} + + {(notFoundFiles.length > 0 || invalidFiles.length > 0) && validFiles.length > 0 && ( + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + 일부 파일에 문제가 있습니다. 업로드 가능한 {validFiles.length}개 파일만 업로드됩니다. + </AlertDescription> + </Alert> + )} + + {/* 파일 목록 */} + <div className="max-h-[50vh] overflow-auto rounded-md border p-4"> + <div className="space-y-4"> + {/* 업로드 가능 파일 */} + {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}개) + </h4> + {validFiles.map((result, index) => ( + <div + key={index} + className="rounded-lg border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-950/30 p-3" + > + <div className="flex items-start justify-between gap-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm break-all"> + {result.file.name} + </div> + {result.parsed && ( + <div className="flex flex-wrap gap-1 mt-2"> + <Badge variant="outline" className="text-xs"> + 도면: {result.parsed.drawingNo} + </Badge> + <Badge variant="outline" className="text-xs"> + Rev: {result.parsed.revNo} + </Badge> + {result.drawingName && ( + <Badge variant="outline" className="text-xs"> + {result.drawingName} + </Badge> + )} + </div> + )} + </div> + <CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400 shrink-0" /> + </div> + </div> + ))} + </div> + )} + + {/* 도면을 찾을 수 없는 파일 */} + {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}개) + </h4> + {notFoundFiles.map((result, index) => ( + <div + key={index} + className="rounded-lg border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-950/30 p-3" + > + <div className="flex items-start justify-between gap-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm break-all"> + {result.file.name} + </div> + <div className="text-xs text-orange-700 dark:text-orange-300 mt-1"> + ✗ 해당 도면번호가 프로젝트에 등록되어 있지 않습니다 + </div> + {result.parsed && ( + <div className="flex flex-wrap gap-1 mt-2"> + <Badge variant="outline" className="text-xs"> + 도면: {result.parsed.drawingNo} + </Badge> + <Badge variant="outline" className="text-xs"> + Rev: {result.parsed.revNo} + </Badge> + </div> + )} + </div> + <XCircle className="h-5 w-5 text-orange-600 dark:text-orange-400 shrink-0" /> + </div> + </div> + ))} + </div> + )} + + {/* 형식 오류 파일 */} + {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}개) + </h4> + {invalidFiles.map((result, index) => ( + <div + key={index} + className="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/30 p-3" + > + <div className="flex items-start justify-between gap-2"> + <div className="flex-1 min-w-0"> + <div className="font-mono text-sm break-all"> + {result.file.name} + </div> + {result.error && ( + <div className="text-xs text-red-600 dark:text-red-400 mt-1"> + ✗ {result.error} + </div> + )} + </div> + <XCircle className="h-5 w-5 text-red-600 dark:text-red-400 shrink-0" /> + </div> + </div> + ))} + </div> + )} + </div> + </div> + + {/* 형식 안내 */} + <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"> + 📋 올바른 파일명 형식 + </div> + <code className="text-xs text-blue-700 dark:text-blue-300"> + [버림] [DrawingNo] [RevNo].[확장자] + </code> + <div className="text-xs text-blue-600 dark:text-blue-400 mt-1"> + 예: testfile GTT-DE-007 R01.pdf + </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"> + ※ 네 번째 이상의 단어가 있으면 무시됩니다 + </div> + </div> + </div> + + <DialogFooter className="flex-shrink-0"> + <Button + variant="outline" + onClick={handleCancel} + disabled={isUploading} + > + 취소 + </Button> + <Button + onClick={handleUpload} + disabled={validFiles.length === 0 || isUploading} + > + {isUploading ? ( + <> + <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" /> + 업로드 중... + </> + ) : ( + <> + <Upload className="h-4 w-4 mr-2" /> + 업로드 ({validFiles.length}개) + </> + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} + |
