summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:08:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-13 07:08:01 +0000
commitc72d0897f7b37843109c86f61d97eba05ba3ca0d (patch)
tree887dd877f3f8beafa92b4d9a7b16c84b4a5795d8 /lib/vendor-document-list/table
parentff902243a658067fae858a615c0629aa2e0a4837 (diff)
(대표님) 20250613 16시 08분 b-rfq, document 등
Diffstat (limited to 'lib/vendor-document-list/table')
-rw-r--r--lib/vendor-document-list/table/bulk-upload-dialog.tsx884
1 files changed, 445 insertions, 439 deletions
diff --git a/lib/vendor-document-list/table/bulk-upload-dialog.tsx b/lib/vendor-document-list/table/bulk-upload-dialog.tsx
index b7021985..b28d3b3d 100644
--- a/lib/vendor-document-list/table/bulk-upload-dialog.tsx
+++ b/lib/vendor-document-list/table/bulk-upload-dialog.tsx
@@ -666,8 +666,9 @@ const canProceedToUpload = matchResult && matchResult.matched.length > 0 && matc
return (
<Dialog open={open} onOpenChange={handleDialogClose}>
- <DialogContent className="sm:max-w-6xl max-h-[90vh] overflow-y-auto">
- <DialogHeader>
+ <DialogContent className="sm:max-w-6xl max-h-[90vh] flex flex-col" style={{maxWidth:900}}>
+ {/* 고정 헤더 */}
+ <DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Files className="w-5 h-5" />
일괄 업로드
@@ -686,476 +687,481 @@ return (
</div>
</DialogHeader>
- {/* 단계별 진행 상태 */}
- <div className="flex items-center gap-2 mb-4">
- {[
- { key: 'template', label: '템플릿' },
- { key: 'files', label: '파일 업로드' },
- { key: 'review', label: '검토' },
- { key: 'upload', label: '업로드' },
- ].map((step, index) => (
- <React.Fragment key={step.key}>
- <div className={`flex items-center gap-1 px-2 py-1 rounded text-xs ${
- currentStep === step.key ? 'bg-primary text-primary-foreground' :
- ['template', 'files', 'review'].indexOf(currentStep) > ['template', 'files', 'review'].indexOf(step.key) ? 'bg-green-100 text-green-700' :
- 'bg-gray-100 text-gray-500'
- }`}>
- {step.label}
- </div>
- {index < 3 && <div className="w-2 h-px bg-gray-300" />}
- </React.Fragment>
- ))}
- </div>
+ {/* 스크롤 가능한 메인 컨텐츠 영역 */}
+ <div className="flex-1 overflow-y-auto px-1">
+ {/* 단계별 진행 상태 */}
+ <div className="flex items-center gap-2 mb-4">
+ {[
+ { key: 'template', label: '템플릿' },
+ { key: 'files', label: '파일 업로드' },
+ { key: 'review', label: '검토' },
+ { key: 'upload', label: '업로드' },
+ ].map((step, index) => (
+ <React.Fragment key={step.key}>
+ <div className={`flex items-center gap-1 px-2 py-1 rounded text-xs ${
+ currentStep === step.key ? 'bg-primary text-primary-foreground' :
+ ['template', 'files', 'review'].indexOf(currentStep) > ['template', 'files', 'review'].indexOf(step.key) ? 'bg-green-100 text-green-700' :
+ 'bg-gray-100 text-gray-500'
+ }`}>
+ {step.label}
+ </div>
+ {index < 3 && <div className="w-2 h-px bg-gray-300" />}
+ </React.Fragment>
+ ))}
+ </div>
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
-
- {/* 1단계: 템플릿 다운로드 및 업로드 */}
- {currentStep === 'template' && (
- <div className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Download className="w-4 h-4" />
- 1단계: 템플릿 다운로드
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <p className="text-sm text-gray-600">
- 현재 문서 목록을 기반으로 업로드 템플릿을 생성합니다.
- 마지막 "fileNames" 칼럼에 업로드할 파일명을 ';'로 구분하여 입력하세요.
- </p>
- <Button type="button" onClick={exportTemplate} className="gap-2">
- <Download className="w-4 h-4" />
- 템플릿 다운로드 ({documents.length}개 문서)
- </Button>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Upload className="w-4 h-4" />
- 작성된 템플릿 업로드
- </CardTitle>
- </CardHeader>
- <CardContent>
- <Dropzone
- maxSize={10e6} // 10MB
- multiple={false}
- accept={{
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
- 'application/vnd.ms-excel': ['.xls']
- }}
- onDropAccepted={handleTemplateDropAccepted}
- disabled={isUploading}
- >
- <DropzoneZone>
- <FormControl>
- <DropzoneInput />
- </FormControl>
- <div className="flex items-center gap-6">
- <FileSpreadsheet className="w-8 h-8 text-gray-400" />
- <div className="grid gap-0.5">
- <DropzoneTitle>작성된 Excel 템플릿을 업로드하세요</DropzoneTitle>
- <DropzoneDescription>
- .xlsx, .xls 파일을 지원합니다
- </DropzoneDescription>
- </div>
- </div>
- </DropzoneZone>
- </Dropzone>
-
- {templateFile && (
- <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg">
- <div className="flex items-center gap-2">
- <CheckCircle2 className="w-4 h-4 text-green-600" />
- <span className="text-sm text-green-700">
- 템플릿 업로드 완료: {templateFile.name}
- </span>
- </div>
- </div>
- )}
- </CardContent>
- </Card>
- </div>
- )}
-
- {/* 2단계: 파일 업로드 */}
- {currentStep === 'files' && (
- <div className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Files className="w-4 h-4" />
- 2단계: 실제 파일들 업로드
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
- <p className="text-sm text-blue-700">
- 템플릿에서 {parsedData.length}개 항목, 총 {parsedData.reduce((sum, item) => sum + item.fileNames.length, 0)}개 파일이 필요합니다.
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+
+ {/* 1단계: 템플릿 다운로드 및 업로드 */}
+ {currentStep === 'template' && (
+ <div className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Download className="w-4 h-4" />
+ 1단계: 템플릿 다운로드
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <p className="text-sm text-gray-600">
+ 현재 문서 목록을 기반으로 업로드 템플릿을 생성합니다.
+ 마지막 "fileNames" 칼럼에 업로드할 파일명을 ';'로 구분하여 입력하세요.
</p>
- </div>
-
- <Dropzone
- maxSize={3e9} // 3GB
- multiple={true}
- onDropAccepted={handleFilesDropAccepted}
- disabled={isUploading}
- >
- <DropzoneZone>
- <FormControl>
- <DropzoneInput />
- </FormControl>
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>실제 파일들을 여기에 드롭하세요</DropzoneTitle>
- <DropzoneDescription>
- 또는 클릭하여 파일들을 선택하세요
- </DropzoneDescription>
+ <Button type="button" onClick={exportTemplate} className="gap-2">
+ <Download className="w-4 h-4" />
+ 템플릿 다운로드 ({documents.length}개 문서)
+ </Button>
+ </CardContent>
+ </Card>
+
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Upload className="w-4 h-4" />
+ 작성된 템플릿 업로드
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <Dropzone
+ maxSize={10e6} // 10MB
+ multiple={false}
+ accept={{
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
+ 'application/vnd.ms-excel': ['.xls']
+ }}
+ onDropAccepted={handleTemplateDropAccepted}
+ disabled={isUploading}
+ >
+ <DropzoneZone>
+ <FormControl>
+ <DropzoneInput />
+ </FormControl>
+ <div className="flex items-center gap-6">
+ <FileSpreadsheet className="w-8 h-8 text-gray-400" />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>작성된 Excel 템플릿을 업로드하세요</DropzoneTitle>
+ <DropzoneDescription>
+ .xlsx, .xls 파일을 지원합니다
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ </Dropzone>
+
+ {templateFile && (
+ <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg">
+ <div className="flex items-center gap-2">
+ <CheckCircle2 className="w-4 h-4 text-green-600" />
+ <span className="text-sm text-green-700">
+ 템플릿 업로드 완료: {templateFile.name}
+ </span>
</div>
</div>
- </DropzoneZone>
- </Dropzone>
-
- {selectedFiles.length > 0 && (
- <div className="mt-4 space-y-2">
- <h6 className="text-sm font-semibold">
- 업로드된 파일 ({selectedFiles.length})
- </h6>
- <ScrollArea className="max-h-[200px]">
- <FileList>
- {selectedFiles.map((file, index) => (
- <FileListItem key={index} className="p-3">
- <FileListHeader>
- <FileListIcon />
- <FileListInfo>
- <FileListName>{file.name}</FileListName>
- <FileListSize>{prettyBytes(file.size)}</FileListSize>
- </FileListInfo>
- <FileListAction
- onClick={() => removeFile(index)}
- disabled={isUploading}
- >
- <X className="h-4 w-4" />
- </FileListAction>
- </FileListHeader>
- </FileListItem>
- ))}
- </FileList>
- </ScrollArea>
- </div>
- )}
- </CardContent>
- </Card>
- </div>
- )}
-
- {/* 3단계: 매칭 결과 검토 */}
- {currentStep === 'review' && matchResult && (
- <div className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <CheckCircle2 className="w-4 h-4" />
- 3단계: 매칭 결과 검토
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-6">
-
- {/* 통합된 매칭 결과 요약 */}
- <div className="grid grid-cols-3 gap-4">
- <div className="p-4 bg-green-50 border border-green-200 rounded-lg text-center">
- <div className="text-2xl font-bold text-green-600">{matchResult.matched.length}</div>
- <div className="text-sm text-green-700">매칭 성공</div>
- </div>
- <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-center">
- <div className="text-2xl font-bold text-yellow-600">{matchResult.unmatched.length}</div>
- <div className="text-sm text-yellow-700">매칭 실패</div>
- </div>
- <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-center">
- <div className="text-2xl font-bold text-red-600">{matchResult.missingFiles.length}</div>
- <div className="text-sm text-red-700">누락된 파일</div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ )}
+
+ {/* 2단계: 파일 업로드 */}
+ {currentStep === 'files' && (
+ <div className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Files className="w-4 h-4" />
+ 2단계: 실제 파일들 업로드
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
+ <p className="text-sm text-blue-700">
+ 템플릿에서 {parsedData.length}개 항목, 총 {parsedData.reduce((sum, item) => sum + item.fileNames.length, 0)}개 파일이 필요합니다.
+ </p>
</div>
- </div>
-
- {/* 통합된 상세 결과 */}
- <div className="border border-gray-200 rounded-lg overflow-hidden">
- {/* 매칭 성공 섹션 */}
- {matchResult.matched.length > 0 && (
- <div className="border-b border-gray-200">
- <div className="p-4 bg-green-50 flex items-center justify-between">
- <h6 className="font-semibold text-green-700 flex items-center gap-2">
- <CheckCircle2 className="w-4 h-4" />
- 매칭 성공 ({matchResult.matched.length}개)
- </h6>
- <Button
- variant="ghost"
- size="sm"
- type="button"
- onClick={(e) => {
- e.preventDefault()
- e.stopPropagation()
- const element = document.getElementById('matched-details')
- if (element) {
- element.style.display = element.style.display === 'none' ? 'block' : 'none'
- }
- }}
- >
- {matchResult.matched.length <= 5 ? '모두보기' : '상세보기'}
- </Button>
- </div>
-
- {/* 미리보기 */}
- <div className="p-4 bg-green-25">
- <div className="space-y-2">
- {matchResult.matched.slice(0, 5).map((match, index) => (
- <div key={index} className="flex items-center justify-between text-sm">
- <span className="font-mono text-green-600 truncate max-w-[300px]" title={match.file.name}>
- {match.file.name}
- </span>
- <span className="text-green-700 ml-4 whitespace-nowrap flex-shrink-0">
- → {match.item.docNumber} Rev.{match.item.revision}
- </span>
- </div>
- ))}
- {matchResult.matched.length > 5 && (
- <div className="text-gray-500 text-center text-sm py-2 border-t border-green-200">
- ... 외 {matchResult.matched.length - 5}개 (상세보기로 확인)
- </div>
- )}
- </div>
- {/* 펼침 상세 내용 */}
- <div id="matched-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-green-200">
- <div className="max-h-64 overflow-y-auto">
- <div className="space-y-2">
- {matchResult.matched.map((match, index) => (
- <div key={index} className="flex items-center justify-between text-sm py-1">
- <span className="font-mono text-green-600 truncate max-w-[300px]" title={match.file.name}>
- {match.file.name}
- </span>
- <span className="text-green-700 ml-4 whitespace-nowrap flex-shrink-0">
- → {match.item.docNumber} ({match.item.stage} Rev.{match.item.revision})
- </span>
- </div>
- ))}
- </div>
- </div>
+ <Dropzone
+ maxSize={3e9} // 3GB
+ multiple={true}
+ onDropAccepted={handleFilesDropAccepted}
+ disabled={isUploading}
+ >
+ <DropzoneZone>
+ <FormControl>
+ <DropzoneInput />
+ </FormControl>
+ <div className="flex items-center gap-6">
+ <DropzoneUploadIcon />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>실제 파일들을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneDescription>
+ 또는 클릭하여 파일들을 선택하세요
+ </DropzoneDescription>
</div>
</div>
+ </DropzoneZone>
+ </Dropzone>
+
+ {selectedFiles.length > 0 && (
+ <div className="mt-4 space-y-2">
+ <h6 className="text-sm font-semibold">
+ 업로드된 파일 ({selectedFiles.length})
+ </h6>
+ <ScrollArea className="max-h-[200px]">
+ <FileList>
+ {selectedFiles.map((file, index) => (
+ <FileListItem key={index} className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{file.name}</FileListName>
+ <FileListSize>{prettyBytes(file.size)}</FileListSize>
+ </FileListInfo>
+ <FileListAction
+ onClick={() => removeFile(index)}
+ disabled={isUploading}
+ >
+ <X className="h-4 w-4" />
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ </ScrollArea>
</div>
)}
+ </CardContent>
+ </Card>
+ </div>
+ )}
- {/* 매칭 실패 섹션 */}
- {matchResult.unmatched.length > 0 && (
- <div className="border-b border-gray-200">
- <div className="p-4 bg-yellow-50 flex items-center justify-between">
- <h6 className="font-semibold text-yellow-700 flex items-center gap-2">
- <AlertCircle className="w-4 h-4" />
- 매칭되지 않은 파일 ({matchResult.unmatched.length}개)
- </h6>
- <Button
- variant="ghost"
- size="sm"
- type="button"
- onClick={() => {
- const element = document.getElementById('unmatched-details')
- if (element) {
- element.style.display = element.style.display === 'none' ? 'block' : 'none'
- }
- }}
- >
- 상세보기
- </Button>
- </div>
-
- <div className="p-4 bg-yellow-25">
- <div className="space-y-1">
- {matchResult.unmatched.slice(0, 3).map((file, index) => (
- <div key={index} className="text-sm text-yellow-600 font-mono truncate max-w-full" title={file.name}>
- {file.name}
- </div>
- ))}
- {matchResult.unmatched.length > 3 && (
- <div className="text-gray-500 text-center text-sm py-2">
- ... 외 {matchResult.unmatched.length - 3}개
- </div>
- )}
+ {/* 3단계: 매칭 결과 검토 */}
+ {currentStep === 'review' && matchResult && (
+ <div className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <CheckCircle2 className="w-4 h-4" />
+ 3단계: 매칭 결과 검토
+ </CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-6">
+
+ {/* 통합된 매칭 결과 요약 */}
+ <div className="grid grid-cols-3 gap-4">
+ <div className="p-4 bg-green-50 border border-green-200 rounded-lg text-center">
+ <div className="text-2xl font-bold text-green-600">{matchResult.matched.length}</div>
+ <div className="text-sm text-green-700">매칭 성공</div>
+ </div>
+ <div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-center">
+ <div className="text-2xl font-bold text-yellow-600">{matchResult.unmatched.length}</div>
+ <div className="text-sm text-yellow-700">매칭 실패</div>
+ </div>
+ <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-center">
+ <div className="text-2xl font-bold text-red-600">{matchResult.missingFiles.length}</div>
+ <div className="text-sm text-red-700">누락된 파일</div>
+ </div>
+ </div>
+
+ {/* 통합된 상세 결과 */}
+ <div className="border border-gray-200 rounded-lg overflow-hidden">
+ {/* 매칭 성공 섹션 */}
+ {matchResult.matched.length > 0 && (
+ <div className="border-b border-gray-200">
+ <div className="p-4 bg-green-50 flex items-center justify-between">
+ <h6 className="font-semibold text-green-700 flex items-center gap-2">
+ <CheckCircle2 className="w-4 h-4" />
+ 매칭 성공 ({matchResult.matched.length}개)
+ </h6>
+ <Button
+ variant="ghost"
+ size="sm"
+ type="button"
+ onClick={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ const element = document.getElementById('matched-details')
+ if (element) {
+ element.style.display = element.style.display === 'none' ? 'block' : 'none'
+ }
+ }}
+ >
+ {matchResult.matched.length <= 5 ? '모두보기' : '상세보기'}
+ </Button>
</div>
+
+ {/* 미리보기 */}
+ <div className="p-4 bg-green-25">
+ <div className="space-y-2">
+ {matchResult.matched.slice(0, 5).map((match, index) => (
+ <div key={index} className="flex items-center justify-between text-sm">
+ <span className="font-mono text-green-600 truncate max-w-[300px]" title={match.file.name}>
+ {match.file.name}
+ </span>
+ <span className="text-green-700 ml-4 whitespace-nowrap flex-shrink-0">
+ → {match.item.docNumber} Rev.{match.item.revision}
+ </span>
+ </div>
+ ))}
+ {matchResult.matched.length > 5 && (
+ <div className="text-gray-500 text-center text-sm py-2 border-t border-green-200">
+ ... 외 {matchResult.matched.length - 5}개 (상세보기로 확인)
+ </div>
+ )}
+ </div>
- <div id="unmatched-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-yellow-200">
- <div className="max-h-40 overflow-y-auto">
- <div className="space-y-1">
- {matchResult.unmatched.map((file, index) => (
- <div key={index} className="text-sm text-yellow-600 font-mono truncate max-w-full" title={file.name}>
- {file.name}
- </div>
- ))}
+ {/* 펼침 상세 내용 */}
+ <div id="matched-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-green-200">
+ <div className="max-h-64 overflow-y-auto">
+ <div className="space-y-2">
+ {matchResult.matched.map((match, index) => (
+ <div key={index} className="flex items-center justify-between text-sm py-1">
+ <span className="font-mono text-green-600 truncate max-w-[300px]" title={match.file.name}>
+ {match.file.name}
+ </span>
+ <span className="text-green-700 ml-4 whitespace-nowrap flex-shrink-0">
+ → {match.item.docNumber} ({match.item.stage} Rev.{match.item.revision})
+ </span>
+ </div>
+ ))}
+ </div>
</div>
</div>
</div>
</div>
- </div>
- )}
+ )}
+
+ {/* 매칭 실패 섹션 */}
+ {matchResult.unmatched.length > 0 && (
+ <div className="border-b border-gray-200">
+ <div className="p-4 bg-yellow-50 flex items-center justify-between">
+ <h6 className="font-semibold text-yellow-700 flex items-center gap-2">
+ <AlertCircle className="w-4 h-4" />
+ 매칭되지 않은 파일 ({matchResult.unmatched.length}개)
+ </h6>
+ <Button
+ variant="ghost"
+ size="sm"
+ type="button"
+ onClick={() => {
+ const element = document.getElementById('unmatched-details')
+ if (element) {
+ element.style.display = element.style.display === 'none' ? 'block' : 'none'
+ }
+ }}
+ >
+ 상세보기
+ </Button>
+ </div>
+
+ <div className="p-4 bg-yellow-25">
+ <div className="space-y-1">
+ {matchResult.unmatched.slice(0, 3).map((file, index) => (
+ <div key={index} className="text-sm text-yellow-600 font-mono truncate max-w-full" title={file.name}>
+ {file.name}
+ </div>
+ ))}
+ {matchResult.unmatched.length > 3 && (
+ <div className="text-gray-500 text-center text-sm py-2">
+ ... 외 {matchResult.unmatched.length - 3}개
+ </div>
+ )}
+ </div>
- {/* 누락된 파일 섹션 */}
- {matchResult.missingFiles.length > 0 && (
- <div>
- <div className="p-4 bg-red-50 flex items-center justify-between">
- <h6 className="font-semibold text-red-700 flex items-center gap-2">
- <X className="w-4 h-4" />
- 누락된 파일 ({matchResult.missingFiles.length}개)
- </h6>
- <Button
- variant="ghost"
- size="sm"
- type="button"
- onClick={() => {
- const element = document.getElementById('missing-details')
- if (element) {
- element.style.display = element.style.display === 'none' ? 'block' : 'none'
- }
- }}
- >
- 상세보기
- </Button>
- </div>
-
- <div className="p-4 bg-red-25">
- <div className="space-y-1">
- {matchResult.missingFiles.slice(0, 3).map((fileName, index) => (
- <div key={index} className="text-sm text-red-600 font-mono truncate max-w-full" title={fileName}>
- {fileName}
- </div>
- ))}
- {matchResult.missingFiles.length > 3 && (
- <div className="text-gray-500 text-center text-sm py-2">
- ... 외 {matchResult.missingFiles.length - 3}개
+ <div id="unmatched-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-yellow-200">
+ <div className="max-h-40 overflow-y-auto">
+ <div className="space-y-1">
+ {matchResult.unmatched.map((file, index) => (
+ <div key={index} className="text-sm text-yellow-600 font-mono truncate max-w-full" title={file.name}>
+ {file.name}
+ </div>
+ ))}
+ </div>
</div>
- )}
+ </div>
+ </div>
+ </div>
+ )}
+
+ {/* 누락된 파일 섹션 */}
+ {matchResult.missingFiles.length > 0 && (
+ <div>
+ <div className="p-4 bg-red-50 flex items-center justify-between">
+ <h6 className="font-semibold text-red-700 flex items-center gap-2">
+ <X className="w-4 h-4" />
+ 누락된 파일 ({matchResult.missingFiles.length}개)
+ </h6>
+ <Button
+ variant="ghost"
+ size="sm"
+ type="button"
+ onClick={() => {
+ const element = document.getElementById('missing-details')
+ if (element) {
+ element.style.display = element.style.display === 'none' ? 'block' : 'none'
+ }
+ }}
+ >
+ 상세보기
+ </Button>
</div>
+
+ <div className="p-4 bg-red-25">
+ <div className="space-y-1">
+ {matchResult.missingFiles.slice(0, 3).map((fileName, index) => (
+ <div key={index} className="text-sm text-red-600 font-mono truncate max-w-full" title={fileName}>
+ {fileName}
+ </div>
+ ))}
+ {matchResult.missingFiles.length > 3 && (
+ <div className="text-gray-500 text-center text-sm py-2">
+ ... 외 {matchResult.missingFiles.length - 3}개
+ </div>
+ )}
+ </div>
- <div id="missing-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-red-200">
- <div className="max-h-40 overflow-y-auto">
- <div className="space-y-1">
- {matchResult.missingFiles.map((fileName, index) => (
- <div key={index} className="text-sm text-red-600 font-mono truncate max-w-full" title={fileName}>
- {fileName}
- </div>
- ))}
+ <div id="missing-details" style={{ display: 'none' }} className="mt-4 pt-4 border-t border-red-200">
+ <div className="max-h-40 overflow-y-auto">
+ <div className="space-y-1">
+ {matchResult.missingFiles.map((fileName, index) => (
+ <div key={index} className="text-sm text-red-600 font-mono truncate max-w-full" title={fileName}>
+ {fileName}
+ </div>
+ ))}
+ </div>
</div>
</div>
</div>
</div>
+ )}
+ </div>
+
+ {/* 업로드 불가 경고 */}
+ {!canProceedToUpload && (
+ <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
+ <div className="flex items-center gap-2">
+ <AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
+ <span className="text-sm text-red-700">
+ 누락된 파일이 있어 업로드를 진행할 수 없습니다. 누락된 파일들을 추가해주세요.
+ </span>
+ </div>
</div>
)}
- </div>
+ </CardContent>
+ </Card>
+
+ {/* 추가 정보 입력 */}
+ <div className="grid grid-cols-1 gap-4">
+ <FormField
+ control={form.control}
+ name="uploaderName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>업로더명</FormLabel>
+ <FormControl>
+ <Input {...field} placeholder="업로더 이름" />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="comment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>코멘트 (선택)</FormLabel>
+ <FormControl>
+ <Textarea {...field} placeholder="일괄 업로드 코멘트" rows={2} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+ )}
- {/* 업로드 불가 경고 */}
- {!canProceedToUpload && (
- <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
+ {/* 4단계: 업로드 진행 */}
+ {currentStep === 'upload' && (
+ <div className="space-y-4">
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg flex items-center gap-2">
+ <Upload className="w-4 h-4" />
+ 4단계: 업로드 진행중
+ </CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-4">
<div className="flex items-center gap-2">
- <AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
- <span className="text-sm text-red-700">
- 누락된 파일이 있어 업로드를 진행할 수 없습니다. 누락된 파일들을 추가해주세요.
- </span>
+ <Loader2 className="h-4 w-4 animate-spin" />
+ <span className="text-sm">{uploadProgress}% 업로드 중...</span>
+ </div>
+ <div className="h-2 w-full bg-muted rounded-full overflow-hidden">
+ <div
+ className="h-full bg-primary rounded-full transition-all"
+ style={{ width: `${uploadProgress}%` }}
+ />
</div>
+ {matchResult && (
+ <p className="text-sm text-gray-600">
+ {matchResult.matched.length}개 파일을 업로드하고 있습니다...
+ </p>
+ )}
</div>
- )}
- </CardContent>
- </Card>
-
- {/* 추가 정보 입력 */}
- <div className="grid grid-cols-1 gap-4">
- <FormField
- control={form.control}
- name="uploaderName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>업로더명</FormLabel>
- <FormControl>
- <Input {...field} placeholder="업로더 이름" />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="comment"
- render={({ field }) => (
- <FormItem>
- <FormLabel>코멘트 (선택)</FormLabel>
- <FormControl>
- <Textarea {...field} placeholder="일괄 업로드 코멘트" rows={2} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
+ </CardContent>
+ </Card>
</div>
- </div>
- )}
-
- {/* 4단계: 업로드 진행 */}
- {currentStep === 'upload' && (
- <div className="space-y-4">
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Upload className="w-4 h-4" />
- 4단계: 업로드 진행중
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="space-y-4">
- <div className="flex items-center gap-2">
- <Loader2 className="h-4 w-4 animate-spin" />
- <span className="text-sm">{uploadProgress}% 업로드 중...</span>
- </div>
- <div className="h-2 w-full bg-muted rounded-full overflow-hidden">
- <div
- className="h-full bg-primary rounded-full transition-all"
- style={{ width: `${uploadProgress}%` }}
- />
- </div>
- {matchResult && (
- <p className="text-sm text-gray-600">
- {matchResult.matched.length}개 파일을 업로드하고 있습니다...
- </p>
- )}
- </div>
- </CardContent>
- </Card>
- </div>
- )}
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={handleDialogClose}
- disabled={isUploading}
- >
- 취소
- </Button>
-
- {currentStep === 'review' && (
- <Button
- type="submit"
- disabled={!canProceedToUpload || isUploading}
- >
- <Upload className="mr-2 h-4 w-4" />
- 일괄 업로드 ({matchResult?.matched.length || 0}개 파일)
- </Button>
)}
- </DialogFooter>
- </form>
- </Form>
+ </form>
+ </Form>
+ </div>
+
+ {/* 고정 푸터 */}
+ <DialogFooter className="flex-shrink-0 pt-4 border-t bg-white">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleDialogClose}
+ disabled={isUploading}
+ >
+ 취소
+ </Button>
+
+ {currentStep === 'review' && (
+ <Button
+ type="submit"
+ disabled={!canProceedToUpload || isUploading}
+ onClick={form.handleSubmit(onSubmit)}
+ >
+ <Upload className="mr-2 h-4 w-4" />
+ 일괄 업로드 ({matchResult?.matched.length || 0}개 파일)
+ </Button>
+ )}
+ </DialogFooter>
</DialogContent>
</Dialog>
)