summaryrefslogtreecommitdiff
path: root/lib/dolce/dialogs/b4-upload-validation-dialog.tsx
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-23 16:40:37 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-23 16:40:37 +0900
commitfd4909bba7be8abc1eeab9ae1b4621c66a61604a (patch)
treed375995611de80b55b344b1c536c5a760f06ccb6 /lib/dolce/dialogs/b4-upload-validation-dialog.tsx
parenta2e0785c8749c4d3766ecf3b70edfb7c2fe4df20 (diff)
(김준회) 돌체 재개발 - 1차 (다운로드 오류 수정 필요)
Diffstat (limited to 'lib/dolce/dialogs/b4-upload-validation-dialog.tsx')
-rw-r--r--lib/dolce/dialogs/b4-upload-validation-dialog.tsx353
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>
+ );
+}
+