From 74843fe598702a9a55f914f2d2d291368a5abb13 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 1 Oct 2025 10:31:23 +0000 Subject: (대표님) dolce 수정, spreadjs 수정 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ship-vendor-document/new-revision-dialog.tsx | 237 ++++++++++++++++++--- .../user-vendor-document-table-container.tsx | 168 ++++++++++----- 2 files changed, 320 insertions(+), 85 deletions(-) (limited to 'components/ship-vendor-document') 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({ {files.length > 0 && ( -
+

Selected Files ({files.length})

{files.map((file, index) => ( @@ -258,6 +329,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) => { + 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 ( +
+ + + + +
{revisionGuide.helpText}
+
+ {revisionGuide.examples.map((example, idx) => ( +
• {example}
+ ))} +
+
+
+
+ ) +} + /* ------------------------------------------------------------------------------------------------- * Main Dialog Component * -----------------------------------------------------------------------------------------------*/ @@ -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("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 ( - + {/* 고정 헤더 */} @@ -451,6 +600,12 @@ export function NewRevisionDialog({ {documentTitle && (
Document: {documentTitle}
+
+ Drawing Type: {drawingKind} | Serial No: {nextSerialNo} + {isLoadingSerialNo && ( + + )} +
)}
@@ -513,7 +668,7 @@ export function NewRevisionDialog({ /> )} - {/* 리비전 */} + {/* 리비전 입력 */} Revision - + {drawingKind === 'B3' ? ( + + ) : ( + <> + { + const upperValue = e.target.value.toUpperCase() + if (upperValue.length <= 3) { + field.onChange(upperValue) + } + }} + /> +
+ {revisionGuide.helpText} +
+ + )}
-
- {revisionGuide} -
)} @@ -617,7 +788,7 @@ export function NewRevisionDialog({ )} - +
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 ( )} {processStatus === 'partially-processed' && ( -
)} @@ -333,7 +343,7 @@ function RevisionTable({ {/* ✅ 처리된 파일 수 표시 */} {processStatus === 'partially-processed' && ( - ({revision.attachments.filter(att => + ({revision.attachments.filter(att => att.dolceFilePath && att.dolceFilePath.trim() !== '' ).length} processed) @@ -354,21 +364,20 @@ function RevisionTable({ )} - + {/* ✅ 수정 버튼 */} - - {/* ✅ 삭제 버튼 */} +