diff options
| -rw-r--r-- | app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx | 21 | ||||
| -rw-r--r-- | app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx | 9 | ||||
| -rw-r--r-- | i18n/locales/en/dolce.json | 14 | ||||
| -rw-r--r-- | i18n/locales/ko/dolce.json | 14 | ||||
| -rw-r--r-- | lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx | 415 | ||||
| -rw-r--r-- | lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx | 25 | ||||
| -rw-r--r-- | lib/dolce/dialogs/upload-files-to-detail-dialog.tsx | 252 | ||||
| -rw-r--r-- | lib/dolce/table/detail-drawing-columns.tsx | 2 | ||||
| -rw-r--r-- | lib/dolce/table/drawing-list-table-v2.tsx | 4 | ||||
| -rw-r--r-- | lib/swp/document-service.ts | 46 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-columns.tsx | 20 | ||||
| -rw-r--r-- | lib/swp/vendor-actions.ts | 6 |
12 files changed, 467 insertions, 361 deletions
diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx index f5337c1c..29b41136 100644 --- a/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx +++ b/app/[lng]/partners/(partners)/dolce-upload-v2/dolce-upload-page-v2.tsx @@ -33,8 +33,8 @@ import { drawingListColumns } from "@/lib/dolce/table/drawing-list-columns"; import { createGttDrawingListColumns, DocumentType } from "@/lib/dolce/table/gtt-drawing-list-columns"; import { createDetailDrawingColumns } from "@/lib/dolce/table/detail-drawing-columns"; import { createFileListColumns } from "@/lib/dolce/table/file-list-columns"; -// V2: MatchBatchFileDwg API를 사용하지 않는 새로운 일괄 업로드 (DetailDwgReceiptMgmtEdit 사용) -import { B4BulkUploadDialogV2 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v2"; +// V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) +import { B4BulkUploadDialogV3 } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog-v3"; // V1로 되돌리려면: 위 줄을 주석 처리하고 아래 줄의 주석을 해제하세요 // import { B4BulkUploadDialog } from "@/lib/dolce/dialogs/b4-bulk-upload-dialog"; import { AddAndModifyDetailDrawingDialog } from "@/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog"; @@ -520,14 +520,14 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro </Card> {/* 도면 리스트 테이블 - 항상 렌더링 */} - <Card className="flex-shrink-0" style={{ minHeight: "500px" }}> + <Card className="flex-shrink-0 flex flex-col" style={{ minHeight: "500px" }}> <CardHeader className="py-3"> <CardTitle className="text-base"> {t("drawingList.title")} {filteredDrawings.length > 0 && ` ${t("drawingList.count", { count: filteredDrawings.length })}`} </CardTitle> </CardHeader> - <CardContent className="p-0"> + <CardContent className="p-0 flex-1 min-h-0 flex flex-col"> {!projNo || !vendorInfo ? ( <div className="flex items-center justify-center text-muted-foreground p-8" style={{ minHeight: "400px" }}> <div className="text-center"> @@ -550,7 +550,8 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro onRowClick={handleDrawingClick} selectedRow={selectedDrawing || undefined} getRowId={getDrawingId} - maxHeight="calc(100vh - 600px)" + selectedRow={selectedDrawing || undefined} + getRowId={getDrawingId} minHeight="400px" defaultPageSize={10} /> @@ -612,7 +613,6 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro onRowClick={setSelectedDetail} selectedRow={selectedDetail || undefined} getRowId={getDetailDrawingId} - maxHeight="calc(100vh - 600px)" minHeight="400px" defaultPageSize={10} /> @@ -658,7 +658,6 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro <DrawingListTableV2 columns={fileColumns} data={files} - maxHeight="calc(100vh - 600px)" minHeight="400px" defaultPageSize={10} /> @@ -667,10 +666,10 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro </Card> </div> - {/* B4 일괄 업로드 다이얼로그 (V2) */} - {/* V2: MatchBatchFileDwg API를 사용하지 않는 새로운 방식 */} + {/* B4 일괄 업로드 다이얼로그 (V3) */} + {/* V3: Sync 기능 없이 일괄 업로드 (MatchBatchFileDwg / Edit 사용) */} {vendorInfo && vendorInfo.drawingKind === "B4" && projNo && ( - <B4BulkUploadDialogV2 + <B4BulkUploadDialogV3 open={bulkUploadDialogOpen} onOpenChange={setBulkUploadDialogOpen} projectNo={projNo} @@ -682,7 +681,7 @@ export default function DolceUploadPageV2({ searchParams }: DolceUploadPageV2Pro lng={lng} /> )} - {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV2를 B4BulkUploadDialog로 변경하세요 */} + {/* V1로 되돌리려면: 위의 B4BulkUploadDialogV3를 B4BulkUploadDialog로 변경하세요 */} {/* 상세도면 추가 다이얼로그 */} {vendorInfo && selectedDrawing && ( diff --git a/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx b/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx index 6655606f..9ce7c6c6 100644 --- a/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx +++ b/app/[lng]/partners/(partners)/dolce-upload-v2/page.tsx @@ -53,14 +53,9 @@ export default async function DolceUploadPageWrapper({ <div> <h2 className="text-2xl font-bold tracking-tight"> {lng === "ko" - ? "DOLCE 도면 업로드 V2" - : "DOLCE Drawing Upload V2"} + ? "조선 도면 업로드" + : "Shipbuilding Drawing Upload"} </h2> - <p className="text-muted-foreground"> - {lng === "ko" - ? "설계문서를 조회하고 업로드할 수 있습니다 (분할 레이아웃)" - : "View and upload design documents (Split Layout)"} - </p> </div> </div> diff --git a/i18n/locales/en/dolce.json b/i18n/locales/en/dolce.json index 66d98f8a..adb35efe 100644 --- a/i18n/locales/en/dolce.json +++ b/i18n/locales/en/dolce.json @@ -144,7 +144,12 @@ "selectFilesError": "Please select files", "uploadSuccess": "{{count}} file(s) uploaded successfully", "uploadError": "Upload failed", - "uploadErrorMessage": "An error occurred during upload" + "uploadErrorMessage": "An error occurred during upload", + "confirmTitle": "Confirm File Upload", + "confirmMessage": "Do you want to upload the selected files?", + "backButton": "Back", + "confirmUpload": "Upload", + "nextButton": "Next" }, "addDetailDialog": { "title": "Add Detail Drawing", @@ -180,7 +185,12 @@ "addSuccessPartialUpload": "Detail drawing added but file upload failed: {{error}}", "addSuccess": "Detail drawing added successfully", "addError": "Failed to add detail drawing", - "addErrorMessage": "An error occurred while adding detail drawing" + "addErrorMessage": "An error occurred while adding detail drawing", + "confirmTitle": "Confirmation", + "confirmMessage": "Do you want to submit with the following details?", + "backButton": "Back", + "confirmSubmit": "Submit", + "nextButton": "Next" }, "editDetailDialog": { "title": "Edit Detail Drawing", diff --git a/i18n/locales/ko/dolce.json b/i18n/locales/ko/dolce.json index 94c61d26..e9f7f862 100644 --- a/i18n/locales/ko/dolce.json +++ b/i18n/locales/ko/dolce.json @@ -144,7 +144,12 @@ "selectFilesError": "파일을 선택해주세요", "uploadSuccess": "{{count}}개 파일 업로드 완료", "uploadError": "업로드 실패", - "uploadErrorMessage": "업로드 중 오류가 발생했습니다" + "uploadErrorMessage": "업로드 중 오류가 발생했습니다", + "confirmTitle": "파일 업로드 확인", + "confirmMessage": "선택한 파일을 업로드하시겠습니까?", + "backButton": "뒤로", + "confirmUpload": "업로드", + "nextButton": "다음" }, "addDetailDialog": { "title": "상세도면 추가", @@ -180,7 +185,12 @@ "addSuccessPartialUpload": "상세도면은 추가되었으나 파일 업로드 실패: {{error}}", "addSuccess": "상세도면이 추가되었습니다", "addError": "상세도면 추가에 실패했습니다", - "addErrorMessage": "상세도면 추가 중 오류가 발생했습니다" + "addErrorMessage": "상세도면 추가 중 오류가 발생했습니다", + "confirmTitle": "확인", + "confirmMessage": "아래 내용으로 제출하시겠습니까?", + "backButton": "뒤로", + "confirmSubmit": "제출", + "nextButton": "다음" }, "editDetailDialog": { "title": "상세도면 수정", diff --git a/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx index 673d48d6..0253228b 100644 --- a/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx +++ b/lib/dolce/dialogs/add-and-modify-detail-drawing-dialog.tsx @@ -72,6 +72,8 @@ export function AddAndModifyDetailDrawingDialog({ const [comment, setComment] = useState<string>(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); + // Edit 모드일 때 초기값 설정 useEffect(() => { if (mode === "edit" && detailDrawing && open) { @@ -155,9 +157,10 @@ export function AddAndModifyDetailDrawingDialog({ setRevisionError(""); setComment(""); clearFiles(); + setShowConfirmation(false); }; - // 제출 + // 제출 (확인 단계 포함) const handleSubmit = async () => { // 유효성 검사 if (!registerKind) { @@ -200,6 +203,12 @@ export function AddAndModifyDetailDrawingDialog({ return; } + // 확인 단계가 아니면 확인 단계로 이동 + if (!showConfirmation) { + setShowConfirmation(true); + return; + } + try { setIsSubmitting(true); @@ -212,7 +221,7 @@ export function AddAndModifyDetailDrawingDialog({ dwgList: [ { Mode: "ADD", - Status: "Submitted", + Status: "Standby", RegisterId: 0, ProjectNo: drawing.ProjectNo, Discipline: drawing.Discipline, @@ -329,8 +338,12 @@ export function AddAndModifyDetailDrawingDialog({ }; const handleCancel = () => { - resetForm(); - onOpenChange(false); + if (showConfirmation) { + setShowConfirmation(false); + } else { + resetForm(); + onOpenChange(false); + } }; // DrawingUsage가 변경되면 RegisterKind 초기화 @@ -355,219 +368,279 @@ export function AddAndModifyDetailDrawingDialog({ return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-2xl"> + <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto"> <DialogHeader> <DialogTitle> - {mode === "edit" ? t("editDetailDialog.title") : t("addDetailDialog.title")} + {showConfirmation + ? t("addDetailDialog.confirmTitle", "확인") + : (mode === "edit" ? t("editDetailDialog.title") : t("addDetailDialog.title")) + } </DialogTitle> </DialogHeader> - <div className="space-y-6"> - {/* 도면 정보 표시 */} - {mode === "add" && drawing && ( + {showConfirmation ? ( + <div className="space-y-6"> <Alert> <Info className="h-4 w-4" /> <AlertDescription> - <div className="font-medium">{drawing.DrawingNo}</div> - <div className="text-sm text-muted-foreground">{drawing.DrawingName}</div> + {t("addDetailDialog.confirmMessage", "아래 내용으로 제출하시겠습니까?")} </AlertDescription> </Alert> - )} - {mode === "edit" && detailDrawing && ( - <Alert> - <Info className="h-4 w-4" /> - <AlertDescription> - <div className="font-medium">{detailDrawing.DrawingNo} - Rev. {detailDrawing.DrawingRevNo}</div> - <div className="text-sm text-muted-foreground">{detailDrawing.DrawingName}</div> - </AlertDescription> - </Alert> - )} + <div className="grid grid-cols-2 gap-4 border rounded-lg p-4 bg-muted/20"> + <div className="space-y-1"> + <Label className="text-xs text-muted-foreground">{t("addDetailDialog.drawingUsageLabel")}</Label> + <p className="text-sm font-medium"> + {drawingUsageOptions.find(opt => opt.value === drawingUsage)?.label || drawingUsage} + </p> + </div> + <div className="space-y-1"> + <Label className="text-xs text-muted-foreground">{t("addDetailDialog.registerKindLabel")}</Label> + <p className="text-sm font-medium"> + {registerKindOptions.find(opt => opt.value === registerKind)?.label || registerKind} + </p> + </div> + {drawingUsage !== "CMT" && ( + <div className="space-y-1"> + <Label className="text-xs text-muted-foreground">{t("addDetailDialog.revisionLabel")}</Label> + <p className="text-sm font-medium">{revision}</p> + </div> + )} + <div className="space-y-1 col-span-2"> + <Label className="text-xs text-muted-foreground">{t("addDetailDialog.commentLabel")}</Label> + <p className="text-sm">{comment || "-"}</p> + </div> + </div> + + {files.length > 0 && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.selectedFiles", { count: files.length })}</Label> + <div className="max-h-60 overflow-y-auto space-y-2 border rounded-lg p-2"> + {isSubmitting ? ( + <FileUploadProgressList fileProgresses={fileProgresses} /> + ) : ( + files.map((file, index) => ( + <div key={index} className="flex items-center gap-2 p-2 rounded bg-muted/50 text-sm"> + <FileIcon className="h-4 w-4 text-muted-foreground shrink-0" /> + <span className="truncate flex-1">{file.name}</span> + <span className="text-xs text-muted-foreground whitespace-nowrap"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </span> + </div> + )) + )} + </div> + </div> + )} + </div> + ) : ( + <div className="space-y-6"> + {/* 도면 정보 표시 */} + {mode === "add" && drawing && ( + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + <div className="font-medium">{drawing.DrawingNo}</div> + <div className="text-sm text-muted-foreground">{drawing.DrawingName}</div> + </AlertDescription> + </Alert> + )} + + {mode === "edit" && detailDrawing && ( + <Alert> + <Info className="h-4 w-4" /> + <AlertDescription> + <div className="font-medium">{detailDrawing.DrawingNo} - Rev. {detailDrawing.DrawingRevNo}</div> + <div className="text-sm text-muted-foreground">{detailDrawing.DrawingName}</div> + </AlertDescription> + </Alert> + )} - {/* 도면용도 선택 (Add 모드에서만 표시) */} - {mode === "add" && ( + {/* 도면용도 선택 (Add 모드에서만 표시) */} + {mode === "add" && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.drawingUsageLabel")}</Label> + <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> + <SelectTrigger> + <SelectValue placeholder={t("addDetailDialog.drawingUsagePlaceholder")} /> + </SelectTrigger> + <SelectContent> + {drawingUsageOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + )} + + {/* 등록종류 선택 */} <div className="space-y-2"> - <Label>{t("addDetailDialog.drawingUsageLabel")}</Label> - <Select value={drawingUsage} onValueChange={handleDrawingUsageChange}> + <Label>{t("addDetailDialog.registerKindLabel")}</Label> + <Select + value={registerKind} + onValueChange={setRegisterKind} + disabled={mode === "add" && !drawingUsage} + > <SelectTrigger> - <SelectValue placeholder={t("addDetailDialog.drawingUsagePlaceholder")} /> + <SelectValue placeholder={t("addDetailDialog.registerKindPlaceholder")} /> </SelectTrigger> <SelectContent> - {drawingUsageOptions.map((option) => ( + {registerKindOptions.map((option) => ( <SelectItem key={option.value} value={option.value}> {option.label} </SelectItem> ))} </SelectContent> </Select> + {revisionRule && ( + <p className="text-sm text-muted-foreground"> + {t("addDetailDialog.revisionFormatPrefix")}{revisionRule} + </p> + )} </div> - )} - - {/* 등록종류 선택 */} - <div className="space-y-2"> - <Label>{t("addDetailDialog.registerKindLabel")}</Label> - <Select - value={registerKind} - onValueChange={setRegisterKind} - disabled={mode === "add" && !drawingUsage} - > - <SelectTrigger> - <SelectValue placeholder={t("addDetailDialog.registerKindPlaceholder")} /> - </SelectTrigger> - <SelectContent> - {registerKindOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - {revisionRule && ( - <p className="text-sm text-muted-foreground"> - {t("addDetailDialog.revisionFormatPrefix")}{revisionRule} - </p> + + {/* Revision 입력 */} + {drawingUsage !== "CMT" && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.revisionLabel")}</Label> + <Input + value={revision} + onChange={(e) => handleRevisionChange(e.target.value)} + placeholder={t("addDetailDialog.revisionPlaceholder")} + 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"> + {t("addDetailDialog.revisionValid")} + </p> + )} + </div> )} - </div> - {/* Revision 입력 */} - {drawingUsage !== "CMT" && ( + {/* Comment 입력 */} <div className="space-y-2"> - <Label>{t("addDetailDialog.revisionLabel")}</Label> - <Input - value={revision} - onChange={(e) => handleRevisionChange(e.target.value)} - placeholder={t("addDetailDialog.revisionPlaceholder")} - disabled={!registerKind} - className={revisionError ? "border-red-500 focus-visible:ring-red-500" : ""} + <Label>{t("addDetailDialog.commentLabel")}</Label> + <Textarea + value={comment} + onChange={(e) => setComment(e.target.value)} + placeholder={t("addDetailDialog.commentPlaceholder")} + rows={3} + className="resize-none" /> - {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"> - {t("addDetailDialog.revisionValid")} - </p> - )} + <p className="text-xs text-muted-foreground"> + {t("addDetailDialog.commentMaxLength")} + </p> </div> - )} - - {/* Comment 입력 */} - <div className="space-y-2"> - <Label>{t("addDetailDialog.commentLabel")}</Label> - <Textarea - value={comment} - onChange={(e) => setComment(e.target.value)} - placeholder={t("addDetailDialog.commentPlaceholder")} - rows={3} - className="resize-none" - /> - <p className="text-xs text-muted-foreground"> - {t("addDetailDialog.commentMaxLength")} - </p> - </div> - {/* 파일 업로드 (Add 모드에서만 표시) */} - {mode === "add" && ( - <div className="space-y-2"> - <Label>{t("addDetailDialog.attachmentLabel")}</Label> - <div - {...getRootProps()} - className={` - border-2 border-dashed rounded-lg p-8 text-center cursor-pointer - transition-colors - ${isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25"} - ${files.length > 0 ? "py-4" : ""} - `} - > - <input {...getInputProps()} /> - {files.length === 0 ? ( - <div className="space-y-2"> - <Upload className="h-8 w-8 mx-auto text-muted-foreground" /> - <div> + {/* 파일 업로드 (Add 모드에서만 표시) */} + {mode === "add" && ( + <div className="space-y-2"> + <Label>{t("addDetailDialog.attachmentLabel")}</Label> + <div + {...getRootProps()} + className={` + border-2 border-dashed rounded-lg p-8 text-center cursor-pointer + transition-colors + ${isDragActive ? "border-primary bg-primary/5" : "border-muted-foreground/25"} + ${files.length > 0 ? "py-4" : ""} + `} + > + <input {...getInputProps()} /> + {files.length === 0 ? ( + <div className="space-y-2"> + <Upload className="h-8 w-8 mx-auto text-muted-foreground" /> + <div> + <p className="text-sm font-medium"> + {t("addDetailDialog.dragDropText")} + </p> + <p className="text-xs text-muted-foreground"> + {t("addDetailDialog.fileInfo")} + </p> + </div> + </div> + ) : ( + <div className="space-y-2"> <p className="text-sm font-medium"> - {t("addDetailDialog.dragDropText")} + {t("addDetailDialog.filesSelected", { count: files.length })} </p> <p className="text-xs text-muted-foreground"> - {t("addDetailDialog.fileInfo")} + {t("addDetailDialog.addMoreFiles")} </p> </div> - </div> - ) : ( - <div className="space-y-2"> - <p className="text-sm font-medium"> - {t("addDetailDialog.filesSelected", { count: files.length })} - </p> - <p className="text-xs text-muted-foreground"> - {t("addDetailDialog.addMoreFiles")} - </p> - </div> - )} - </div> + )} + </div> - {/* 선택된 파일 목록 */} - {files.length > 0 && ( - <div className="space-y-2 mt-4"> - {isSubmitting ? ( - // 업로드 중: 진행도 표시 - <FileUploadProgressList fileProgresses={fileProgresses} /> - ) : ( - // 대기 중: 삭제 버튼 표시 - <> - <div className="flex items-center justify-between mb-2"> - <h4 className="text-sm font-medium"> - {t("addDetailDialog.selectedFiles", { count: files.length })} - </h4> - <Button - variant="ghost" - size="sm" - onClick={clearFiles} - > - {t("addDetailDialog.removeAll")} - </Button> - </div> - <div className="max-h-60 overflow-y-auto space-y-2"> - {files.map((file, index) => ( - <div - key={index} - className="flex items-center gap-2 p-2 border rounded-lg bg-muted/50" + {/* 선택된 파일 목록 */} + {files.length > 0 && ( + <div className="space-y-2 mt-4"> + {isSubmitting ? ( + // 업로드 중: 진행도 표시 + <FileUploadProgressList fileProgresses={fileProgresses} /> + ) : ( + // 대기 중: 삭제 버튼 표시 + <> + <div className="flex items-center justify-between mb-2"> + <h4 className="text-sm font-medium"> + {t("addDetailDialog.selectedFiles", { count: files.length })} + </h4> + <Button + variant="ghost" + size="sm" + onClick={clearFiles} > - <FileIcon className="h-4 w-4 text-muted-foreground shrink-0" /> - <div className="flex-1 min-w-0"> - <p className="text-sm truncate">{file.name}</p> - <p className="text-xs text-muted-foreground"> - {(file.size / 1024 / 1024).toFixed(2)} MB - </p> - </div> - <Button - variant="ghost" - size="sm" - onClick={() => removeFile(index)} + {t("addDetailDialog.removeAll")} + </Button> + </div> + <div className="max-h-60 overflow-y-auto space-y-2"> + {files.map((file, index) => ( + <div + key={index} + className="flex items-center gap-2 p-2 border rounded-lg bg-muted/50" > - <X className="h-4 w-4" /> - </Button> - </div> - ))} - </div> - </> - )} + <FileIcon className="h-4 w-4 text-muted-foreground shrink-0" /> + <div className="flex-1 min-w-0"> + <p className="text-sm truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </p> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => removeFile(index)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </> + )} + </div> + )} </div> )} - </div> - )} - </div> + </div> + )} <DialogFooter> <Button variant="outline" onClick={handleCancel} disabled={isSubmitting}> - {t("addDetailDialog.cancelButton")} + {showConfirmation ? t("addDetailDialog.backButton", "뒤로") : t("addDetailDialog.cancelButton")} </Button> <Button onClick={handleSubmit} disabled={isSubmitting || !isFormValid}> {isSubmitting ? t("addDetailDialog.processingButton") - : mode === "edit" - ? t("editDetailDialog.updateButton") - : t("addDetailDialog.addButton") + : showConfirmation + ? t("addDetailDialog.confirmSubmit", "제출") + : t("addDetailDialog.nextButton", "다음") } </Button> </DialogFooter> diff --git a/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx b/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx index ea955420..8bb5dd42 100644 --- a/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx +++ b/lib/dolce/dialogs/b4-bulk-upload-dialog-v3.tsx @@ -301,7 +301,7 @@ export function B4BulkUploadDialogV3({ // Reuse UploadId from the first item's mapping data if available, else generate new const firstItemMapping = groupItems[0].mappingData; // Reuse existing UploadId if present in API response, otherwise generate new one - // The prompt says: "UploadId는 있으면 재활용하고, 없으면 UUID로 만들어줌" + // UploadId는 있으면 재활용하고, 없으면 UUID로 만들어서 사용 const uploadId = firstItemMapping.UploadId || uuidv4(); console.log(`[V3 Dialog] Processing group ${groupKey}, UploadId: ${uploadId}`); @@ -371,30 +371,31 @@ export function B4BulkUploadDialogV3({ const m = item.mappingData; return { CGbn: m.CGbn, - Category: "TS", // Hardcoded as per prompt - CheckBox: "0", + Category: "TS", // Hardcoded fixed value is required! + CheckBox: m.CheckBox, DGbn: m.DGbn, DegreeGbn: m.DegreeGbn, DeptGbn: m.DeptGbn, Discipline: m.Discipline, - DrawingKind: "B4", - DrawingMoveGbn: "도면입수", + DrawingKind: m.DrawingKind, + DrawingMoveGbn: m.DrawingMoveGbn, DrawingName: m.DrawingName, DrawingNo: m.DrawingNo, - DrawingUsage: "입수용", + DrawingUsage: m.DrawingUsage, FileNm: item.file.name, JGbn: m.JGbn, - Manager: m.Manager || "970043", // Fallback/Default - MappingYN: "Y", - NewOrNot: "N", + Manager: m.Manager, + MappingYN: m.MappingYN, + NewOrNot: m.NewOrNot, ProjectNo: projectNo, - RegisterGroup: 0, + RegisterGroup: m.RegisterGroup, RegisterGroupId: m.RegisterGroupId, RegisterKindCode: m.RegisterKindCode, RegisterSerialNo: m.RegisterSerialNo, RevNo: m.RevNo, SGbn: m.SGbn, - UploadId: uploadId // Used for all files in this group + UploadId: uploadId, // Used for all files in this group + status: "Standby", // Hardcoded fixed value is required! }; }); @@ -473,7 +474,7 @@ export function B4BulkUploadDialogV3({ return ( <> <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-2xl"> + <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto"> <DialogHeader> <DialogTitle>{t("bulkUpload.title")} (V3)</DialogTitle> <DialogDescription> diff --git a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx index e8d82129..f21ccc70 100644 --- a/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx +++ b/lib/dolce/dialogs/upload-files-to-detail-dialog.tsx @@ -55,20 +55,29 @@ export function UploadFilesToDetailDialog({ isDragActive, } = useFileUploadWithProgress(); + const [showConfirmation, setShowConfirmation] = useState(false); + // 다이얼로그 닫을 때 초기화 React.useEffect(() => { if (!open) { clearFiles(); + setShowConfirmation(false); } }, [open, clearFiles]); - // 업로드 처리 + // 업로드 처리 (확인 단계 포함) const handleUpload = async () => { if (selectedFiles.length === 0) { toast.error(t("uploadFilesDialog.selectFilesError")); return; } + // 확인 단계가 아니면 확인 단계로 이동 + if (!showConfirmation) { + setShowConfirmation(true); + return; + } + setIsUploading(true); try { @@ -112,117 +121,169 @@ export function UploadFilesToDetailDialog({ } }; + const handleCancel = () => { + if (showConfirmation) { + setShowConfirmation(false); + } else { + onOpenChange(false); + } + }; + return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="max-w-2xl"> + <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto"> <DialogHeader> - <DialogTitle>{t("uploadFilesDialog.title")}</DialogTitle> + <DialogTitle> + {showConfirmation + ? t("uploadFilesDialog.confirmTitle", "파일 업로드 확인") + : t("uploadFilesDialog.title") + } + </DialogTitle> <DialogDescription> {t("uploadFilesDialog.description", { drawingNo, revNo })} </DialogDescription> </DialogHeader> - <div className="space-y-4"> - {/* 안내 메시지 */} - <Alert> - <AlertCircle className="h-4 w-4" /> - <AlertDescription> - {t("uploadFilesDialog.alertMessage")} - </AlertDescription> - </Alert> - - {/* 파일 선택 영역 */} - <div - {...getRootProps()} - className={`border-2 border-dashed rounded-lg p-8 transition-all duration-200 cursor-pointer ${ - isDragActive - ? "border-primary bg-primary/5 scale-[1.02]" - : "border-muted-foreground/30 hover:border-muted-foreground/50" - }`} - > - <input {...getInputProps()} /> - <div className="flex flex-col items-center justify-center"> - <FolderOpen - className={`h-12 w-12 mb-3 transition-colors ${ - isDragActive ? "text-primary" : "text-muted-foreground" - }`} - /> - <p - className={`text-sm transition-colors ${ - isDragActive - ? "text-primary font-medium" - : "text-muted-foreground" - }`} - > - {isDragActive - ? t("uploadFilesDialog.dropHereText") - : t("uploadFilesDialog.dragDropText")} - </p> - <p className="text-xs text-muted-foreground mt-1"> - {t("uploadFilesDialog.fileInfo")} - </p> - </div> - </div> + {showConfirmation ? ( + <div className="space-y-4"> + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + {t("uploadFilesDialog.confirmMessage", "선택한 파일을 업로드하시겠습니까?")} + </AlertDescription> + </Alert> - {/* 선택된 파일 목록 */} - {selectedFiles.length > 0 && ( <div className="border rounded-lg p-4"> - {isUploading ? ( - // 업로드 중: 진행도 표시 - <FileUploadProgressList fileProgresses={fileProgresses} /> - ) : ( - // 대기 중: 삭제 버튼 표시 - <> - <div className="flex items-center justify-between mb-3"> - <h4 className="text-sm font-medium"> - {t("uploadFilesDialog.selectedFiles", { count: selectedFiles.length })} - </h4> - <Button - variant="ghost" - size="sm" - onClick={clearFiles} + <h4 className="text-sm font-medium mb-3"> + {t("uploadFilesDialog.selectedFiles", { count: selectedFiles.length })} + </h4> + <div className="max-h-60 overflow-y-auto space-y-2"> + {isUploading ? ( + <FileUploadProgressList fileProgresses={fileProgresses} /> + ) : ( + selectedFiles.map((file, index) => ( + <div + key={index} + className="flex items-center justify-between p-2 rounded bg-muted/50" > - {t("uploadFilesDialog.removeAll")} - </Button> - </div> - <div className="max-h-60 overflow-y-auto space-y-2"> - {selectedFiles.map((file, index) => ( - <div - key={index} - className="flex items-center justify-between p-2 rounded bg-muted/50" - > - <div className="flex items-center gap-2 flex-1 min-w-0"> - <FileText className="h-4 w-4 text-muted-foreground shrink-0" /> - <div className="flex-1 min-w-0"> - <p className="text-sm truncate">{file.name}</p> - <p className="text-xs text-muted-foreground"> - {(file.size / 1024 / 1024).toFixed(2)} MB - </p> - </div> + <div className="flex items-center gap-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-muted-foreground shrink-0" /> + <div className="flex-1 min-w-0"> + <p className="text-sm truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </p> </div> - <Button - variant="ghost" - size="sm" - onClick={() => removeFile(index)} - > - <X className="h-4 w-4" /> - </Button> </div> - ))} - </div> - </> - )} + </div> + )) + )} + </div> + </div> + </div> + ) : ( + <div className="space-y-4"> + {/* 안내 메시지 */} + <Alert> + <AlertCircle className="h-4 w-4" /> + <AlertDescription> + {t("uploadFilesDialog.alertMessage")} + </AlertDescription> + </Alert> + + {/* 파일 선택 영역 */} + <div + {...getRootProps()} + className={`border-2 border-dashed rounded-lg p-8 transition-all duration-200 cursor-pointer ${ + isDragActive + ? "border-primary bg-primary/5 scale-[1.02]" + : "border-muted-foreground/30 hover:border-muted-foreground/50" + }`} + > + <input {...getInputProps()} /> + <div className="flex flex-col items-center justify-center"> + <FolderOpen + className={`h-12 w-12 mb-3 transition-colors ${ + isDragActive ? "text-primary" : "text-muted-foreground" + }`} + /> + <p + className={`text-sm transition-colors ${ + isDragActive + ? "text-primary font-medium" + : "text-muted-foreground" + }`} + > + {isDragActive + ? t("uploadFilesDialog.dropHereText") + : t("uploadFilesDialog.dragDropText")} + </p> + <p className="text-xs text-muted-foreground mt-1"> + {t("uploadFilesDialog.fileInfo")} + </p> + </div> </div> - )} - </div> + + {/* 선택된 파일 목록 */} + {selectedFiles.length > 0 && ( + <div className="border rounded-lg p-4"> + {isUploading ? ( + // 업로드 중: 진행도 표시 + <FileUploadProgressList fileProgresses={fileProgresses} /> + ) : ( + // 대기 중: 삭제 버튼 표시 + <> + <div className="flex items-center justify-between mb-3"> + <h4 className="text-sm font-medium"> + {t("uploadFilesDialog.selectedFiles", { count: selectedFiles.length })} + </h4> + <Button + variant="ghost" + size="sm" + onClick={clearFiles} + > + {t("uploadFilesDialog.removeAll")} + </Button> + </div> + <div className="max-h-60 overflow-y-auto space-y-2"> + {selectedFiles.map((file, index) => ( + <div + key={index} + className="flex items-center justify-between p-2 rounded bg-muted/50" + > + <div className="flex items-center gap-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-muted-foreground shrink-0" /> + <div className="flex-1 min-w-0"> + <p className="text-sm truncate">{file.name}</p> + <p className="text-xs text-muted-foreground"> + {(file.size / 1024 / 1024).toFixed(2)} MB + </p> + </div> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => removeFile(index)} + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </> + )} + </div> + )} + </div> + )} <DialogFooter> <Button variant="outline" - onClick={() => onOpenChange(false)} + onClick={handleCancel} disabled={isUploading} > - {t("uploadFilesDialog.cancelButton")} + {showConfirmation ? t("uploadFilesDialog.backButton", "뒤로") : t("uploadFilesDialog.cancelButton")} </Button> <Button onClick={handleUpload} @@ -233,10 +294,15 @@ export function UploadFilesToDetailDialog({ <Loader2 className="mr-2 h-4 w-4 animate-spin" /> {t("uploadFilesDialog.uploadingButton")} </> + ) : showConfirmation ? ( + <> + <Upload className="mr-2 h-4 w-4" /> + {t("uploadFilesDialog.confirmUpload", "업로드")} + </> ) : ( <> <Upload className="mr-2 h-4 w-4" /> - {t("uploadFilesDialog.uploadButton", { count: selectedFiles.length })} + {t("uploadFilesDialog.nextButton", "다음")} </> )} </Button> diff --git a/lib/dolce/table/detail-drawing-columns.tsx b/lib/dolce/table/detail-drawing-columns.tsx index 215a0cff..6127a7b6 100644 --- a/lib/dolce/table/detail-drawing-columns.tsx +++ b/lib/dolce/table/detail-drawing-columns.tsx @@ -148,7 +148,7 @@ export function createDetailDrawingColumns( maxSize: 100, cell: ({ row }) => { const status = row.getValue("Status") as string; - const isEditable = status === "Submitted"; + const isEditable = status === "Standby"; return ( <div diff --git a/lib/dolce/table/drawing-list-table-v2.tsx b/lib/dolce/table/drawing-list-table-v2.tsx index 420ed672..e546fa79 100644 --- a/lib/dolce/table/drawing-list-table-v2.tsx +++ b/lib/dolce/table/drawing-list-table-v2.tsx @@ -53,7 +53,7 @@ export function DrawingListTableV2<TData extends DrawingData, TValue>({ onRowClick, selectedRow, getRowId, - maxHeight = "45vh", + maxHeight, minHeight = "400px", defaultPageSize = 10, }: DrawingListTableV2Props<TData, TValue>) { @@ -120,7 +120,7 @@ export function DrawingListTableV2<TData extends DrawingData, TValue>({ minHeight: data.length === 0 ? minHeight : undefined, }} > - <Table className="min-w-max"> + <Table className="w-full"> <TableHeader className="sticky top-0 z-10 bg-background"> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> diff --git a/lib/swp/document-service.ts b/lib/swp/document-service.ts index b89d3442..6d8d7831 100644 --- a/lib/swp/document-service.ts +++ b/lib/swp/document-service.ts @@ -149,9 +149,7 @@ export interface DocumentDetail { * 문서 목록 아이템 (통계 포함) */ export interface DocumentListItem extends SwpDocumentApiResponse { - fileCount: number; - standbyFileCount: number; // STAT=SCW01 - latestFiles: SwpFileApiResponse[]; + // fileCount, standbyFileCount, latestFiles 제거됨 } // ============================================================================ @@ -170,49 +168,21 @@ export async function getDocumentList( debugLog("[getDocumentList] 시작", { projNo, vndrCd }); try { - // 병렬 API 호출 - const [documents, allFiles] = await Promise.all([ - fetchGetVDRDocumentList({ - proj_no: projNo, - doc_gb: "V", - vndrCd: vndrCd, - }), - fetchGetExternalInboxList({ - projNo: projNo, - vndrCd: vndrCd, - }), - ]); + // API 호출 + const documents = await fetchGetVDRDocumentList({ + proj_no: projNo, + doc_gb: "V", + vndrCd: vndrCd, + }); debugLog("[getDocumentList] API 조회 완료", { documents: documents.length, - files: allFiles.length, }); - // 파일을 문서별로 그룹핑 - const filesByDoc = new Map<string, SwpFileApiResponse[]>(); - for (const file of allFiles) { - const docNo = file.OWN_DOC_NO; - if (!filesByDoc.has(docNo)) { - filesByDoc.set(docNo, []); - } - filesByDoc.get(docNo)!.push(file); - } - - // 문서에 파일 통계 추가 + // 문서 목록 반환 (파일 통계 제거) const result = documents.map((doc) => { - const allFiles = filesByDoc.get(doc.OWN_DOC_NO || "") || []; - - // 최신 REV의 파일만 필터링 - const latestRevFiles = allFiles.filter((f) => f.REV_NO === doc.LTST_REV_NO); - const standbyFiles = latestRevFiles.filter((f) => f.STAT === "SCW01"); - return { ...doc, - fileCount: latestRevFiles.length, - standbyFileCount: standbyFiles.length, - latestFiles: latestRevFiles - .sort((a, b) => b.CRTE_DTM.localeCompare(a.CRTE_DTM)) - .slice(0, 5), // 최신 5개만 }; }); diff --git a/lib/swp/table/swp-table-columns.tsx b/lib/swp/table/swp-table-columns.tsx index 91c811c3..261cf960 100644 --- a/lib/swp/table/swp-table-columns.tsx +++ b/lib/swp/table/swp-table-columns.tsx @@ -138,25 +138,7 @@ export const swpDocumentColumns: ColumnDef<DocumentListItem>[] = [ minSize: 100, maxSize: 100, }, - { - id: "stats", - header: "파일", - cell: ({ row }) => ( - <div className="text-center"> - <div className="text-sm font-medium"> - {row.original.fileCount}개 - </div> - {row.original.standbyFileCount > 0 && ( - <div className="text-xs text-yellow-600"> - 대기중 {row.original.standbyFileCount} - </div> - )} - </div> - ), - size: 100, - minSize: 100, - maxSize: 100, - }, + { id: "actions", header: "커버페이지 다운로드", diff --git a/lib/swp/vendor-actions.ts b/lib/swp/vendor-actions.ts index 78521fed..12b7c513 100644 --- a/lib/swp/vendor-actions.ts +++ b/lib/swp/vendor-actions.ts @@ -304,9 +304,9 @@ export async function fetchVendorSwpStats(projNo?: string) { let uploadedFiles = 0; for (const doc of documents) { - totalFiles += doc.fileCount; - // standbyFileCount가 0이 아니면 업로드된 것으로 간주 - uploadedFiles += doc.fileCount - doc.standbyFileCount; + // 파일 통계는 더 이상 계산하지 않음 (API 호출 제거됨) + // totalFiles += doc.fileCount; + // uploadedFiles += doc.fileCount - doc.standbyFileCount; // 리비전 수 추정 (LTST_REV_NO 기반) if (doc.LTST_REV_NO) { |
