diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-01 10:31:23 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-01 10:31:23 +0000 |
| commit | 74843fe598702a9a55f914f2d2d291368a5abb13 (patch) | |
| tree | a88abdaf039f51dd843e0416321f08877b17ea75 /components | |
| parent | 33e8452331c301430191b3506825ebaf3edac93a (diff) | |
(대표님) dolce 수정, spreadjs 수정 등
Diffstat (limited to 'components')
| -rw-r--r-- | components/form-data/form-data-table.tsx | 6 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 63 | ||||
| -rw-r--r-- | components/ship-vendor-document/new-revision-dialog.tsx | 237 | ||||
| -rw-r--r-- | components/ship-vendor-document/user-vendor-document-table-container.tsx | 168 |
4 files changed, 386 insertions, 88 deletions
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 591ba66a..3d8b1438 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -1,7 +1,7 @@ "use client"; import * as React from "react"; -import { useParams, useRouter } from "next/navigation"; +import { useParams, useRouter, usePathname } from "next/navigation"; import { useTranslation } from "@/i18n/client"; import { ClientDataTable } from "../client-data-table/data-table"; @@ -99,6 +99,7 @@ export default function DynamicTable({ const router = useRouter(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "engineering"); + const pathname = usePathname(); const [rowAction, setRowAction] = React.useState<DataTableRowAction<GenericData> | null>(null); @@ -114,6 +115,7 @@ export default function DynamicTable({ const [formStats, setFormStats] = React.useState<FormStatusByVendor | null>(null); const [isLoadingStats, setIsLoadingStats] = React.useState(true); + const isEVCPPath = pathname.includes('evcp'); React.useEffect(() => { const fetchFormStats = async () => { @@ -672,6 +674,7 @@ export default function DynamicTable({ return ( <> + {!isEVCPPath && ( <div className="mb-6"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-6"> {/* Tag Count */} @@ -807,6 +810,7 @@ export default function DynamicTable({ </Card> </div> </div> + )} <ClientDataTable data={tableData} diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 91d5672c..af1a3dca 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -197,7 +197,38 @@ export function TemplateViewDialog({ }, []); React.useEffect(() => { - if (!templateData) return; + // 템플릿 데이터가 없거나 빈 배열인 경우 기본 GRD_LIST 템플릿 생성 + if (!templateData || (Array.isArray(templateData) && templateData.length === 0)) { + // columnsJSON이 있으면 기본 GRD_LIST 템플릿 생성 + if (columnsJSON && columnsJSON.length > 0) { + const defaultGrdTemplate: TemplateItem = { + TMPL_ID: 'DEFAULT_GRD_LIST', + NAME: 'Default Grid View', + TMPL_TYPE: 'GRD_LIST', + SPR_LST_SETUP: { + ACT_SHEET: '', + HIDN_SHEETS: [], + DATA_SHEETS: [] + }, + GRD_LST_SETUP: { + REG_TYPE_ID: 'DEFAULT', + SPR_ITM_IDS: [], + ATTS: [] + }, + SPR_ITM_LST_SETUP: { + ACT_SHEET: '', + HIDN_SHEETS: [], + DATA_SHEETS: [] + } + }; + + setAvailableTemplates([defaultGrdTemplate]); + setSelectedTemplateId('DEFAULT_GRD_LIST'); + setTemplateType('GRD_LIST'); + console.log('📋 Created default GRD_LIST template'); + } + return; + } let templates: TemplateItem[]; if (Array.isArray(templateData)) { @@ -207,6 +238,34 @@ export function TemplateViewDialog({ } const validTemplates = templates.filter(isValidTemplate); + + // 유효한 템플릿이 없지만 columnsJSON이 있으면 기본 GRD_LIST 추가 + if (validTemplates.length === 0 && columnsJSON && columnsJSON.length > 0) { + const defaultGrdTemplate: TemplateItem = { + TMPL_ID: 'DEFAULT_GRD_LIST', + NAME: 'Default Grid View', + TMPL_TYPE: 'GRD_LIST', + SPR_LST_SETUP: { + ACT_SHEET: '', + HIDN_SHEETS: [], + DATA_SHEETS: [] + }, + GRD_LST_SETUP: { + REG_TYPE_ID: 'DEFAULT', + SPR_ITM_IDS: [], + ATTS: [] + }, + SPR_ITM_LST_SETUP: { + ACT_SHEET: '', + HIDN_SHEETS: [], + DATA_SHEETS: [] + } + }; + + validTemplates.push(defaultGrdTemplate); + console.log('📋 Added default GRD_LIST template to empty template list'); + } + setAvailableTemplates(validTemplates); if (validTemplates.length > 0 && !selectedTemplateId) { @@ -215,7 +274,7 @@ export function TemplateViewDialog({ setSelectedTemplateId(firstTemplate.TMPL_ID); setTemplateType(templateTypeToSet); } - }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]); + }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType, columnsJSON]); const handleTemplateChange = (templateId: string) => { const template = availableTemplates.find(t => t.TMPL_ID === templateId); diff --git a/components/ship-vendor-document/new-revision-dialog.tsx b/components/ship-vendor-document/new-revision-dialog.tsx index 1ffcf630..3ec58d1d 100644 --- a/components/ship-vendor-document/new-revision-dialog.tsx +++ b/components/ship-vendor-document/new-revision-dialog.tsx @@ -9,7 +9,8 @@ import { DialogContent, DialogDescription, DialogHeader, - DialogTitle,DialogFooter + DialogTitle, + DialogFooter } from "@/components/ui/dialog" import { Form, @@ -35,10 +36,12 @@ import { FileText, X, Loader2, - CheckCircle + CheckCircle, + Info } from "lucide-react" import { toast } from "sonner" import { useSession } from "next-auth/react" +import { Alert, AlertDescription } from "@/components/ui/alert" // 기존 메인 컴포넌트에서 추가할 import // import { NewRevisionDialog } from "./new-revision-dialog" @@ -50,12 +53,26 @@ import { useSession } from "next-auth/react" // 파일 검증 스키마 const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB +// B3 리비전 검증 함수 +const validateB3Revision = (value: string) => { + // B3 리비전 패턴: 단일 알파벳(A-Z) 또는 R01-R99 + const alphabetPattern = /^[A-Z]$/ + const numericPattern = /^R(0[1-9]|[1-9][0-9])$/ + + return alphabetPattern.test(value) || numericPattern.test(value) +} + +// B4 리비전 검증 함수 +const validateB4Revision = (value: string) => { + // B4 리비전 패턴: R01-R99 + const numericPattern = /^R(0[1-9]|[1-9][0-9])$/ + return numericPattern.test(value) +} // drawingKind에 따른 동적 스키마 생성 const createRevisionUploadSchema = (drawingKind: string) => { const baseSchema = { usage: z.string().min(1, "Please select a usage"), - revision: z.string().min(1, "Please enter a revision").max(50, "Revision must be 50 characters or less"), comment: z.string().optional(), attachments: z .array(z.instanceof(File)) @@ -63,22 +80,37 @@ const createRevisionUploadSchema = (drawingKind: string) => { .refine( (files) => files.every((file) => file.size <= MAX_FILE_SIZE), "File size must be 50MB or less" - ) - // .refine( - // (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)), - // "Unsupported file format" - // ), + ), } + // B3와 B4에 따른 리비전 검증 추가 + const revisionField = drawingKind === 'B3' + ? z.string() + .min(1, "Please enter a revision") + .max(3, "Revision must be 3 characters or less") + .refine( + validateB3Revision, + "Invalid format. Use A-Z or R01-R99" + ) + : z.string() + .min(1, "Please enter a revision") + .max(3, "Revision must be 3 characters or less") + .refine( + validateB4Revision, + "Invalid format. Use R01-R99" + ) + // B3인 경우에만 usageType 필드 추가 if (drawingKind === 'B3') { return z.object({ ...baseSchema, + revision: revisionField, usageType: z.string().min(1, "Please select a usage type"), }) } else { return z.object({ ...baseSchema, + revision: revisionField, usageType: z.string().optional(), }) } @@ -118,7 +150,6 @@ const getUsageTypeOptions = (usage: string) => { return [ { value: "Full", label: "Full" }, { value: "Partial", label: "Partial" }, - ] case 'Working': return [ @@ -128,7 +159,6 @@ const getUsageTypeOptions = (usage: string) => { case 'Comments': return [ { value: "Comments", label: "Comments" }, - ] default: return [] @@ -136,8 +166,49 @@ const getUsageTypeOptions = (usage: string) => { } // 리비전 형식 가이드 생성 -const getRevisionGuide = () => { - return "Enter in R01, R02, R03... format" +const getRevisionGuide = (drawingKind: string) => { + if (drawingKind === 'B3') { + return { + placeholder: "e.g., A, B, C or R01, R02", + helpText: "Use single letter (A-Z) or R01-R99 format", + examples: [ + "A, B, C, ... Z (alphabetic revisions)", + "R01, R02, ... R99 (numeric revisions)" + ] + } + } + return { + placeholder: "e.g., R01, R02, R03", + helpText: "Enter in R01, R02, R03... format", + examples: ["R01, R02, R03, ... R99"] + } +} + +// B3 리비전 자동 포맷팅 함수 +const formatB3RevisionInput = (value: string): string => { + // 입력값을 대문자로 변환 + const upperValue = value.toUpperCase() + + // 단일 알파벳인 경우 + if (/^[A-Z]$/.test(upperValue)) { + return upperValue + } + + // R로 시작하는 경우 + if (upperValue.startsWith('R')) { + // R 뒤의 숫자 추출 + const numPart = upperValue.slice(1).replace(/\D/g, '') + if (numPart) { + const num = parseInt(numPart, 10) + // 1-99 범위 체크 + if (num >= 1 && num <= 99) { + // 01-09는 0을 붙이고, 10-99는 그대로 + return `R${num.toString().padStart(2, '0')}` + } + } + } + + return upperValue } interface NewRevisionDialogProps { @@ -146,7 +217,7 @@ interface NewRevisionDialogProps { documentId: number documentTitle?: string drawingKind: string - onSuccess?: (result?: any) => void // ✅ result 파라미터 추가 + onSuccess?: (result?: any) => void } /* ------------------------------------------------------------------------------------------------- @@ -221,7 +292,7 @@ function FileUploadArea({ </div> {files.length > 0 && ( - <div className="space-y-2 max-h-40 overflow-y-auto overscroll-contain pr-2"> + <div className="space-y-2 max-h-40 overflow-y-auto overscroll-contain pr-2"> <p className="text-sm font-medium">Selected Files ({files.length})</p> <div className="max-h-40 overflow-y-auto space-y-2"> {files.map((file, index) => ( @@ -259,6 +330,56 @@ function FileUploadArea({ } /* ------------------------------------------------------------------------------------------------- + * Revision Input Component for B3 + * -----------------------------------------------------------------------------------------------*/ +function B3RevisionInput({ + value, + onChange, + error +}: { + value: string + onChange: (value: string) => void + error?: string +}) { + const [inputValue, setInputValue] = React.useState(value) + + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const rawValue = e.target.value + const formattedValue = formatB3RevisionInput(rawValue) + + // 길이 제한 (알파벳은 1자, R숫자는 3자) + if (rawValue.length <= 3) { + setInputValue(formattedValue) + onChange(formattedValue) + } + } + + const revisionGuide = getRevisionGuide('B3') + + return ( + <div className="space-y-2"> + <Input + value={inputValue} + onChange={handleInputChange} + placeholder={revisionGuide.placeholder} + className={error ? "border-red-500" : ""} + /> + <Alert className="bg-blue-50 border-blue-200"> + <Info className="h-4 w-4 text-blue-600" /> + <AlertDescription className="text-xs space-y-1"> + <div className="font-medium text-blue-900">{revisionGuide.helpText}</div> + <div className="text-blue-700"> + {revisionGuide.examples.map((example, idx) => ( + <div key={idx}>• {example}</div> + ))} + </div> + </AlertDescription> + </Alert> + </div> + ) +} + +/* ------------------------------------------------------------------------------------------------- * Main Dialog Component * -----------------------------------------------------------------------------------------------*/ export function NewRevisionDialog({ @@ -272,6 +393,34 @@ export function NewRevisionDialog({ const [isUploading, setIsUploading] = React.useState(false) const [uploadProgress, setUploadProgress] = React.useState(0) const { data: session } = useSession() + const [nextSerialNo, setNextSerialNo] = React.useState<string>("1") + const [isLoadingSerialNo, setIsLoadingSerialNo] = React.useState(false) + + // Serial No 조회 + const fetchNextSerialNo = React.useCallback(async () => { + setIsLoadingSerialNo(true) + try { + const response = await fetch(`/api/revisions/max-serial-no?documentId=${documentId}`) + if (response.ok) { + const data = await response.json() + setNextSerialNo(String(data.nextSerialNo)) + } + } catch (error) { + console.error('Failed to fetch serial no:', error) + // 에러 시 기본값 1 사용 + setNextSerialNo("1") + } finally { + setIsLoadingSerialNo(false) + } + }, [documentId]) + + // Dialog 열릴 때 Serial No 조회 + React.useEffect(() => { + if (open && documentId) { + fetchNextSerialNo() + } + }, [open, documentId, fetchNextSerialNo]) + const userName = React.useMemo(() => { return session?.user?.name ? session.user.name : null; @@ -319,8 +468,8 @@ export function NewRevisionDialog({ // 리비전 가이드 텍스트 const revisionGuide = React.useMemo(() => { - return getRevisionGuide() - }, []) + return getRevisionGuide(drawingKind) + }, [drawingKind]) const handleDialogClose = () => { if (!isUploading) { @@ -337,6 +486,7 @@ export function NewRevisionDialog({ try { const formData = new FormData() formData.append("documentId", String(documentId)) + formData.append("serialNo", nextSerialNo) // 추가 formData.append("usage", data.usage) formData.append("revision", data.revision) formData.append("uploaderName", userName || "evcp") @@ -365,7 +515,7 @@ export function NewRevisionDialog({ setUploadProgress(progress) }, 300) - const response = await fetch('/api/revision-upload-ship', { // ✅ 올바른 API 엔드포인트 사용 + const response = await fetch('/api/revision-upload-ship', { method: 'POST', body: formData, }) @@ -389,7 +539,7 @@ export function NewRevisionDialog({ setTimeout(() => { handleDialogClose() - onSuccess?.(result) // ✅ API 응답 결과를 콜백에 전달 + onSuccess?.(result) }, 1000) } catch (error) { @@ -400,22 +550,22 @@ export function NewRevisionDialog({ if (error instanceof Error) { const message = error.message.toLowerCase() - // 파일명 관련 에러 (보안상 허용) + // 파일명 관련 에러 if (message.includes("안전하지 않은 파일명") || message.includes("unsafe filename") || message.includes("filename") && message.includes("invalid")) { userMessage = "File name contains invalid characters. Please avoid using < > : \" ' | ? * in file names. filename can't start with '..'." } - // 파일명 길이 에러 (보안상 허용) + // 파일명 길이 에러 else if (message.includes("파일명이 너무 깁니다") || message.includes("filename too long") || message.includes("파일명") && message.includes("길이")) { userMessage = "File name is too long. Please use a shorter name (max 255 characters)." } - // 파일 크기 에러 (보안상 허용) + // 파일 크기 에러 else if (message.includes("파일 크기가 너무 큽니다") || message.includes("file size") || message.includes("1gb limit") || message.includes("exceeds") && message.includes("limit")) { userMessage = "File size is too large. Please use files smaller than 1GB." } - // 클라이언트측 네트워크 에러 (기존과 같이 처리) + // 클라이언트측 네트워크 에러 else if (message.includes("network") || message.includes("fetch") || message.includes("connection") || message.includes("timeout")) { userMessage = "Network error occurred. Please check your connection and try again." @@ -426,7 +576,6 @@ export function NewRevisionDialog({ message.includes("security") || message.includes("validation")) { userMessage = "Please try again later. If the problem persists, please contact the administrator." } - // 그 외는 일반적인 메시지 else { userMessage = "Please try again later. If the problem persists, please contact the administrator." } @@ -441,7 +590,7 @@ export function NewRevisionDialog({ return ( <Dialog open={open} onOpenChange={handleDialogClose}> - <DialogContent className="max-w-2xl h-[90vh] flex flex-col overflow-hidden" style={{maxHeight:'90vh'}}> + <DialogContent className="max-w-2xl h-[90vh] flex flex-col overflow-hidden" style={{maxHeight:'90vh'}}> {/* 고정 헤더 */} <DialogHeader className="flex-shrink-0 pb-4 border-b"> <DialogTitle className="flex items-center gap-2"> @@ -451,6 +600,12 @@ export function NewRevisionDialog({ {documentTitle && ( <DialogDescription className="text-sm space-y-1"> <div>Document: {documentTitle}</div> + <div className="text-xs text-muted-foreground"> + Drawing Type: {drawingKind} | Serial No: {nextSerialNo} + {isLoadingSerialNo && ( + <Loader2 className="inline-block ml-2 h-3 w-3 animate-spin" /> + )} + </div> </DialogDescription> )} </DialogHeader> @@ -513,7 +668,7 @@ export function NewRevisionDialog({ /> )} - {/* 리비전 */} + {/* 리비전 입력 */} <FormField control={form.control} name="revision" @@ -521,14 +676,30 @@ export function NewRevisionDialog({ <FormItem> <FormLabel className="required">Revision</FormLabel> <FormControl> - <Input - placeholder={revisionGuide} - {...field} - /> + {drawingKind === 'B3' ? ( + <B3RevisionInput + value={field.value} + onChange={field.onChange} + error={form.formState.errors.revision?.message} + /> + ) : ( + <> + <Input + placeholder={revisionGuide.placeholder} + {...field} + onChange={(e) => { + const upperValue = e.target.value.toUpperCase() + if (upperValue.length <= 3) { + field.onChange(upperValue) + } + }} + /> + <div className="text-xs text-muted-foreground mt-1"> + {revisionGuide.helpText} + </div> + </> + )} </FormControl> - <div className="text-xs text-muted-foreground mt-1"> - {revisionGuide} - </div> <FormMessage /> </FormItem> )} @@ -617,7 +788,7 @@ export function NewRevisionDialog({ </> )} </Button> - </DialogFooter> + </DialogFooter> </form> </Form> </DialogContent> diff --git a/components/ship-vendor-document/user-vendor-document-table-container.tsx b/components/ship-vendor-document/user-vendor-document-table-container.tsx index 7fac34a9..775dac47 100644 --- a/components/ship-vendor-document/user-vendor-document-table-container.tsx +++ b/components/ship-vendor-document/user-vendor-document-table-container.tsx @@ -40,6 +40,16 @@ import { useRouter } from 'next/navigation' import { AddAttachmentDialog } from "./add-attachment-dialog" // ✅ import 추가 import { EditRevisionDialog } from "./edit-revision-dialog" // ✅ 추가 import { downloadFile } from "@/lib/file-download" // ✅ 공용 다운로드 함수 import +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog" /* ------------------------------------------------------------------------------------------------- * Types & Constants @@ -172,12 +182,12 @@ function RevisionTable({ // ✅ 리비전 수정 가능 여부 확인 함수 const canEditRevision = React.useCallback((revision: RevisionInfo) => { // 첨부파일이 없으면 수정 가능 - if ((!revision.attachments || revision.attachments.length === 0)&&revision.uploaderType ==="vendor") { + if ((!revision.attachments || revision.attachments.length === 0) && revision.uploaderType === "vendor") { return true } // 모든 첨부파일의 dolceFilePath가 null이거나 빈값이어야 수정 가능 - return revision.attachments.every(attachment => + return revision.attachments.every(attachment => !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' ) }, []) @@ -188,7 +198,7 @@ function RevisionTable({ return 'no-files' } - const processedCount = revision.attachments.filter(attachment => + const processedCount = revision.attachments.filter(attachment => attachment.dolceFilePath && attachment.dolceFilePath.trim() !== '' ).length @@ -241,7 +251,7 @@ function RevisionTable({ {revisions.map((revision) => { const canEdit = canEditRevision(revision) const processStatus = getRevisionProcessStatus(revision) - + return ( <TableRow key={revision.id} @@ -264,14 +274,14 @@ function RevisionTable({ {revision.revision} {/* ✅ 처리 상태 인디케이터 */} {processStatus === 'fully-processed' && ( - <div - className="w-2 h-2 bg-blue-500 rounded-full" + <div + className="w-2 h-2 bg-blue-500 rounded-full" title="All files processed" /> )} {processStatus === 'partially-processed' && ( - <div - className="w-2 h-2 bg-yellow-500 rounded-full" + <div + className="w-2 h-2 bg-yellow-500 rounded-full" title="Some files processed" /> )} @@ -333,7 +343,7 @@ function RevisionTable({ {/* ✅ 처리된 파일 수 표시 */} {processStatus === 'partially-processed' && ( <span className="text-xs text-muted-foreground"> - ({revision.attachments.filter(att => + ({revision.attachments.filter(att => att.dolceFilePath && att.dolceFilePath.trim() !== '' ).length} processed) </span> @@ -354,21 +364,20 @@ function RevisionTable({ <Eye className="h-4 w-4" /> </Button> )} - + {/* ✅ 수정 버튼 */} <Button variant="ghost" size="sm" onClick={() => onEditRevision(revision)} - className={`h-8 px-2 ${ - canEdit - ? 'text-blue-600 hover:text-blue-700 hover:bg-blue-50' + className={`h-8 px-2 ${canEdit + ? 'text-blue-600 hover:text-blue-700 hover:bg-blue-50' : 'text-muted-foreground cursor-not-allowed' - }`} + }`} disabled={!canEdit} title={ - canEdit - ? 'Edit revision' + canEdit + ? 'Edit revision' : 'Cannot edit - some files have been processed' } > @@ -390,17 +399,23 @@ function RevisionTable({ function AttachmentTable({ attachments, onDownloadFile, - onDeleteFile, // ✅ 삭제 함수 prop 추가 + onDeleteFile, }: { attachments: AttachmentInfo[] onDownloadFile: (attachment: AttachmentInfo) => void - onDeleteFile: (attachment: AttachmentInfo) => Promise<void> // ✅ 삭제 함수 추가 + onDeleteFile: (attachment: AttachmentInfo) => Promise<void> }) { const { selectedRevisionId, allData, setAllData } = React.useContext(DocumentSelectionContext) const [addAttachmentDialogOpen, setAddAttachmentDialogOpen] = React.useState(false) - const [deletingFileId, setDeletingFileId] = React.useState<number | null>(null) // ✅ 삭제 중인 파일 ID + const [deletingFileId, setDeletingFileId] = React.useState<number | null>(null) const router = useRouter() + // ✅ AlertDialog 상태 추가 + const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false) + const [fileToDelete, setFileToDelete] = React.useState<AttachmentInfo | null>(null) + const [errorAlertOpen, setErrorAlertOpen] = React.useState(false) + const [errorMessage, setErrorMessage] = React.useState('') + // 선택된 리비전 정보 가져오기 const selectedRevisionInfo = React.useMemo(() => { if (!selectedRevisionId || !allData) return null @@ -425,34 +440,48 @@ function AttachmentTable({ // ✅ 삭제 가능 여부 확인 함수 const canDeleteFile = React.useCallback((attachment: AttachmentInfo) => { + // rejected 상태의 리비전에 속한 첨부파일은 무조건 삭제 가능 + if (selectedRevisionInfo && + selectedRevisionInfo.revisionStatus && + selectedRevisionInfo.revisionStatus.toLowerCase() === 'rejected') { + return true + } + + // 그 외의 경우는 기존 로직대로: dolceFilePath가 없거나 빈값인 경우만 삭제 가능 return !attachment.dolceFilePath || attachment.dolceFilePath.trim() === '' - }, []) + }, [selectedRevisionInfo]) - // ✅ 파일 삭제 핸들러 - const handleDeleteFile = React.useCallback(async (attachment: AttachmentInfo) => { + // ✅ 삭제 요청 핸들러 (확인 다이얼로그 표시) + const handleDeleteRequest = React.useCallback((attachment: AttachmentInfo) => { if (!canDeleteFile(attachment)) { - alert('This file cannot be deleted because it has been processed by the system.') + setErrorMessage('This file cannot be deleted because it has been processed by the system.') + setErrorAlertOpen(true) return } - const confirmDelete = window.confirm( - `Are you sure you want to delete "${attachment.fileName}"?\nThis action cannot be undone.` - ) - - if (!confirmDelete) return + setFileToDelete(attachment) + setDeleteConfirmOpen(true) + }, [canDeleteFile]) + + // ✅ 실제 삭제 수행 핸들러 + const handleConfirmDelete = React.useCallback(async () => { + if (!fileToDelete) return try { - setDeletingFileId(attachment.id) - await onDeleteFile(attachment) + setDeletingFileId(fileToDelete.id) + setDeleteConfirmOpen(false) + await onDeleteFile(fileToDelete) } catch (error) { console.error('Delete file error:', error) - alert(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`) + setErrorMessage(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`) + setErrorAlertOpen(true) } finally { setDeletingFileId(null) + setFileToDelete(null) } - }, [canDeleteFile, onDeleteFile]) + }, [fileToDelete, onDeleteFile]) - // 첨부파일 업로드 성공 핸들러 + // 첨부파일 업로드 성공 핸들러 (기존 코드 유지) const handleAttachmentUploadSuccess = React.useCallback((uploadResult?: any) => { if (!selectedRevisionId || !allData || !uploadResult?.data) { console.log('🔄 Full refresh') @@ -467,7 +496,7 @@ function AttachmentTable({ revisionId: selectedRevisionId, fileName: file.fileName, filePath: file.filePath, - dolceFilePath: null, // ✅ 새 파일은 dolceFilePath가 없음 + dolceFilePath: null, fileSize: file.fileSize, fileType: file.fileType || null, createdAt: new Date(), @@ -484,7 +513,6 @@ function AttachmentTable({ for (const stage of stages) { const revisionIndex = stage.revisions.findIndex(r => r.id === selectedRevisionId) if (revisionIndex !== -1) { - // 해당 리비전의 첨부파일 배열에 새 파일들 추가 stage.revisions[revisionIndex] = { ...stage.revisions[revisionIndex], attachments: [...stage.revisions[revisionIndex].attachments, ...newAttachments] @@ -501,7 +529,6 @@ function AttachmentTable({ setAllData(updatedData) console.log('✅ AttachmentTable update complete') - // 메인 테이블도 업데이트 (약간의 지연 후) setTimeout(() => { router.refresh() }, 1500) @@ -518,7 +545,6 @@ function AttachmentTable({ <CardHeader> <div className="flex items-center justify-between"> <CardTitle className="text-lg">Attachments</CardTitle> - {/* + 버튼 */} {selectedRevisionId && selectedRevisionInfo && ( <Button onClick={handleAddAttachment} @@ -551,7 +577,6 @@ function AttachmentTable({ ? 'Please select a revision' : 'No attached files'} </span> - {/* 리비전이 선택된 경우 추가 버튼 표시 */} {selectedRevisionId && selectedRevisionInfo && ( <Button onClick={handleAddAttachment} @@ -581,7 +606,6 @@ function AttachmentTable({ : `${(file.fileSize / 1024).toFixed(1)}KB` : '-'} </div> - {/* ✅ dolceFilePath 상태 표시 */} {file.dolceFilePath && file.dolceFilePath.trim() !== '' && ( <div className="text-xs text-blue-600 font-medium"> Processed @@ -591,7 +615,6 @@ function AttachmentTable({ </TableCell> <TableCell> <div className="flex items-center gap-1"> - {/* 다운로드 버튼 */} <Button variant="ghost" size="sm" @@ -601,21 +624,21 @@ function AttachmentTable({ > <Download className="h-4 w-4" /> </Button> - - {/* ✅ 삭제 버튼 */} + <Button variant="ghost" size="sm" - onClick={() => handleDeleteFile(file)} - className={`h-8 px-2 ${ - canDeleteFile(file) - ? 'text-red-600 hover:text-red-700 hover:bg-red-50' + onClick={() => handleDeleteRequest(file)} + className={`h-8 px-2 ${canDeleteFile(file) + ? 'text-red-600 hover:text-red-700 hover:bg-red-50' : 'text-muted-foreground cursor-not-allowed' - }`} + }`} disabled={!canDeleteFile(file) || deletingFileId === file.id} title={ - canDeleteFile(file) - ? 'Delete file' + canDeleteFile(file) + ? selectedRevisionInfo?.revisionStatus?.toLowerCase() === 'rejected' + ? 'Delete file (rejected revision)' + : 'Delete file' : 'Cannot delete processed file' } > @@ -635,6 +658,47 @@ function AttachmentTable({ </CardContent> </Card> + {/* ✅ 삭제 확인 다이얼로그 */} + <AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>Delete File</AlertDialogTitle> + <AlertDialogDescription> + Are you sure you want to delete "{fileToDelete?.fileName}"? + This action cannot be undone. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel onClick={() => setFileToDelete(null)}> + Cancel + </AlertDialogCancel> + <AlertDialogAction + onClick={handleConfirmDelete} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Delete + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + + {/* ✅ 에러 메시지 다이얼로그 */} + <AlertDialog open={errorAlertOpen} onOpenChange={setErrorAlertOpen}> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>Error</AlertDialogTitle> + <AlertDialogDescription> + {errorMessage} + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogAction onClick={() => setErrorMessage('')}> + OK + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> + </AlertDialog> + {/* AddAttachmentDialog */} {selectedRevisionInfo && ( <AddAttachmentDialog @@ -666,7 +730,7 @@ function SubTables() { const isCancelled = React.useRef(false) const [newRevisionDialogOpen, setNewRevisionDialogOpen] = React.useState(false) - + // ✅ 리비전 수정 다이얼로그 상태 const [editRevisionDialogOpen, setEditRevisionDialogOpen] = React.useState(false) const [editingRevision, setEditingRevision] = React.useState<RevisionInfo | null>(null) @@ -770,7 +834,7 @@ function SubTables() { try { // 파일 경로 처리 let downloadPath = attachment.filePath - + // 공용 다운로드 함수 사용 (보안 검증, 파일 체크 모두 포함) const result = await downloadFile(downloadPath, attachment.fileName, { action: 'download', @@ -784,7 +848,7 @@ function SubTables() { } catch (error) { console.error('File download error:', error) - + // fallback: API 엔드포인트를 통한 다운로드 시도 try { const queryParam = attachment.id |
