diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-13 07:08:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-13 07:08:01 +0000 |
| commit | c72d0897f7b37843109c86f61d97eba05ba3ca0d (patch) | |
| tree | 887dd877f3f8beafa92b4d9a7b16c84b4a5795d8 /lib/vendor-document-list/table | |
| parent | ff902243a658067fae858a615c0629aa2e0a4837 (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.tsx | 884 |
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> ) |
