diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-02 18:02:11 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-02 18:02:11 +0900 |
| commit | 624cfcf4edb106e6cf0b041d9437ceaa94b6a46d (patch) | |
| tree | c8b81d4969c994aa8f15aea308c24de1f972ce96 | |
| parent | ed412083c785fc1fed7a2490f84f72a665c846be (diff) | |
(디버깅) 돌체 디버깅 - serialNo, 변경사항 카운트
| -rw-r--r-- | app/api/revision-upload-ship/route.ts | 23 | ||||
| -rw-r--r-- | app/api/revisions/max-serial-no/route.ts | 71 | ||||
| -rw-r--r-- | components/ship-vendor-document/edit-revision-dialog.tsx | 137 | ||||
| -rw-r--r-- | components/ship-vendor-document/new-revision-dialog.tsx | 268 | ||||
| -rw-r--r-- | components/ship-vendor-document/revision-validation.tsx | 266 | ||||
| -rw-r--r-- | components/ship-vendor-document/user-vendor-document-table-container.tsx | 6 |
6 files changed, 433 insertions, 338 deletions
diff --git a/app/api/revision-upload-ship/route.ts b/app/api/revision-upload-ship/route.ts index ccfa2e59..26105efd 100644 --- a/app/api/revision-upload-ship/route.ts +++ b/app/api/revision-upload-ship/route.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from "next/server" import { revalidateTag } from "next/cache" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" import db from "@/db/db" import { @@ -21,6 +23,15 @@ import { export async function POST(request: NextRequest) { try { + // 세션 정보 가져오기 + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + + const currentUserId = Number(session.user.id) + const currentUserName = session.user.name || session.user.email || "unknown" + const formData = await request.formData() /* ------- 파라미터 파싱 ------- */ @@ -166,8 +177,8 @@ export async function POST(request: NextRequest) { "UPDATE", updated, existingRev, - undefined, - uploaderName ?? undefined, + currentUserId, // 세션에서 가져온 실제 user ID + currentUserName, // 세션에서 가져온 실제 user name [targetSystem] ) } else { @@ -197,8 +208,8 @@ export async function POST(request: NextRequest) { "CREATE", newRev, undefined, - undefined, - uploaderName ?? undefined, + currentUserId, // 세션에서 가져온 실제 user ID + currentUserName, // 세션에서 가져온 실제 user name [targetSystem] ) } @@ -252,8 +263,8 @@ export async function POST(request: NextRequest) { "CREATE", att, undefined, - undefined, - uploaderName ?? undefined, + currentUserId, // 세션에서 가져온 실제 user ID + currentUserName, // 세션에서 가져온 실제 user name [targetSystem] ) } diff --git a/app/api/revisions/max-serial-no/route.ts b/app/api/revisions/max-serial-no/route.ts index b202956a..c0bfe5c3 100644 --- a/app/api/revisions/max-serial-no/route.ts +++ b/app/api/revisions/max-serial-no/route.ts @@ -1,48 +1,81 @@ import { NextRequest, NextResponse } from 'next/server' import db from '@/db/db' -import { revisions, issueStages } from '@/db/schema' -import { eq, and, sql, desc } from 'drizzle-orm' +import { revisions, issueStages } from '@/db/schema/vendorDocu' +import { eq, sql } from 'drizzle-orm' +import { alias } from 'drizzle-orm/pg-core' +import { debugLog, debugError, debugSuccess } from '@/lib/debug-utils' export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) const documentId = searchParams.get('documentId') - + + debugLog('1. Input documentId:', documentId) + if (!documentId) { + debugLog('2. documentId is missing, returning 400') return NextResponse.json( { error: 'documentId is required' }, { status: 400 } ) } + const parsedDocumentId = parseInt(documentId) + debugLog('3. Parsed documentId:', parsedDocumentId) + // 해당 document의 모든 issueStages와 연결된 revisions에서 최대 serialNo 조회 + const r = alias(revisions, 'r') + const is = alias(issueStages, 'is') + + debugLog('4. Created aliases - r:', r, 'is:', is) + const maxSerialResult = await db .select({ - maxSerialNo: sql<number>` - GREATEST( - COALESCE(MAX(CAST(r.serial_no AS INTEGER)), 0), - COALESCE(MAX(CAST(r.register_serial_no_max AS INTEGER)), 0) - ) - `.as('max_serial_no') + maxSerial: sql<number>` + COALESCE(MAX(CAST(${r.serialNo} AS INTEGER)), 0) + `, + maxRegisterSerial: sql<number>` + COALESCE(MAX(CAST(${r.registerSerialNoMax} AS INTEGER)), 0) + ` }) - .from(revisions.as('r')) + .from(r) .innerJoin( - issueStages.as('is'), - eq(revisions.issueStageId, issueStages.id) + is, + eq(r.issueStageId, is.id) ) - .where(eq(issueStages.documentId, parseInt(documentId))) + .where(eq(is.documentId, parsedDocumentId)) + + debugLog('5. Query result:', maxSerialResult) + debugLog('6. Query result length:', maxSerialResult.length) + debugLog('7. First result item:', maxSerialResult[0]) + + const maxSerialValue = maxSerialResult[0]?.maxSerial || 0 + const maxRegisterSerialValue = maxSerialResult[0]?.maxRegisterSerial || 0 + + debugLog('8. maxSerial value:', maxSerialValue) + debugLog('9. maxRegisterSerial value:', maxRegisterSerialValue) + + const maxSerialNo = Math.max(maxSerialValue, maxRegisterSerialValue) + + debugSuccess('10. Final maxSerialNo:', maxSerialNo) + debugSuccess('11. Next serialNo:', maxSerialNo + 1) - const maxSerialNo = maxSerialResult[0]?.maxSerialNo || 0 - - return NextResponse.json({ + return NextResponse.json({ maxSerialNo, nextSerialNo: maxSerialNo + 1, - documentId: documentId + documentId: documentId, + debug: { + parsedDocumentId, + queryResult: maxSerialResult, + maxSerialValue, + maxRegisterSerialValue + } }) } catch (error) { - console.error('Error fetching max serial no:', error) + debugError('Error fetching max serial no:', error) + debugError('Error stack:', error?.stack) return NextResponse.json( - { error: 'Failed to fetch max serial number' }, + { error: 'Failed to fetch max serial number', details: error?.message }, { status: 500 } ) } diff --git a/components/ship-vendor-document/edit-revision-dialog.tsx b/components/ship-vendor-document/edit-revision-dialog.tsx index 2b8735e7..9ca4c65d 100644 --- a/components/ship-vendor-document/edit-revision-dialog.tsx +++ b/components/ship-vendor-document/edit-revision-dialog.tsx @@ -31,17 +31,23 @@ import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { - Edit, - FileText, - Loader2, +import { + Edit, + FileText, + Loader2, AlertTriangle, Trash2, - CheckCircle, - Clock + CheckCircle } from "lucide-react" import { toast } from "sonner" import { updateRevisionAction, deleteRevisionAction } from "@/lib/vendor-document-list/enhanced-document-service" // ✅ 서버 액션 import +import { + createEditRevisionSchema, + getUsageOptions, + getUsageTypeOptions, + getRevisionGuide, + B3RevisionInput +} from "./revision-validation" /* ------------------------------------------------------------------------------------------------- * Schema & Types @@ -84,88 +90,16 @@ interface AttachmentInfo { updatedAt: Date } -// drawingKind에 따른 동적 스키마 생성 (수정용) -const createEditRevisionSchema = (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"), // ✅ revision 필드 추가 - comment: z.string().optional(), - } - // B3인 경우에만 usageType 필드 추가 - if (drawingKind === 'B3') { - return z.object({ - ...baseSchema, - usageType: z.string().min(1, "Please select a usage type"), - }) - } else { - return z.object({ - ...baseSchema, - usageType: z.string().optional(), - }) - } -} -// drawingKind에 따른 용도 옵션 -const getUsageOptions = (drawingKind: string) => { - switch (drawingKind) { - case 'B3': - return [ - { value: "Approval", label: "Approval" }, - { value: "Working", label: "Working" }, - { value: "Comments", label: "Comments" }, - ] - case 'B4': - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - case 'B5': - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - default: - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - } -} -// B3 전용 용도 타입 옵션 -const getUsageTypeOptions = (usage: string) => { - switch (usage) { - case 'Approval': - return [ - { value: "Full", label: "Full" }, - { value: "Partial", label: "Partial" }, - ] - case 'Working': - return [ - { value: "Full", label: "Full" }, - { value: "Partial", label: "Partial" }, - ] - case 'Comments': - return [ - { value: "Comments", label: "Comments" }, - ] - default: - return [] - } -} - -// ✅ 리비전 가이드 생성 (NewRevisionDialog와 동일) -const getRevisionGuide = () => { - return "Enter in R01, R02, R03... format" -} interface EditRevisionDialogProps { open: boolean onOpenChange: (open: boolean) => void revision: RevisionInfo | null drawingKind?: string - onSuccess: (action: 'update' | 'delete', result?: any) => void + onSuccess: (action: 'update' | 'delete', result?: unknown) => void } /* ------------------------------------------------------------------------------------------------- @@ -302,8 +236,8 @@ export function EditRevisionDialog({ // ✅ 리비전 가이드 텍스트 const revisionGuide = React.useMemo(() => { - return getRevisionGuide() - }, []) + return getRevisionGuide(drawingKind) + }, [drawingKind]) // revision이 변경될 때 폼 데이터 초기화 React.useEffect(() => { @@ -324,7 +258,6 @@ export function EditRevisionDialog({ form.setValue("usageType", "Comments") } else { // Comments가 아닌 경우, 초기 로드가 아니라면 초기화 - const currentValue = form.getValues("usageType") if (revision && watchedUsage !== revision.usage) { form.setValue("usageType", "") } @@ -471,15 +404,31 @@ export function EditRevisionDialog({ <FormItem> <FormLabel className="required">Revision</FormLabel> <FormControl> - <Input - placeholder={revisionGuide} - disabled={!canEdit} - {...field} - /> + {drawingKind === 'B3' ? ( + <B3RevisionInput + value={field.value} + onChange={field.onChange} + error={form.formState.errors.revision?.message} + /> + ) : ( + <> + <Input + placeholder={revisionGuide.placeholder} + disabled={!canEdit} + {...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> )} @@ -495,7 +444,7 @@ export function EditRevisionDialog({ <Select onValueChange={field.onChange} value={field.value} disabled={!canEdit}> <FormControl> <SelectTrigger> - <SelectValue placeholder="Select usage" /> + <SelectValue placeholder='Select usage' /> </SelectTrigger> </FormControl> <SelectContent> @@ -526,7 +475,7 @@ export function EditRevisionDialog({ > <FormControl> <SelectTrigger> - <SelectValue placeholder="Select usage type" /> + <SelectValue placeholder='Select usage type' /> </SelectTrigger> </FormControl> <SelectContent> @@ -539,7 +488,7 @@ export function EditRevisionDialog({ </Select> {watchedUsage === "Comments" && ( <div className="text-xs text-muted-foreground mt-1"> - Automatically set to "Comments" for this usage + Automatically set to "Comments" for this usage </div> )} <FormMessage /> @@ -574,7 +523,7 @@ export function EditRevisionDialog({ <div className="space-y-2"> <FormLabel>Attachments ({revision.attachments.length})</FormLabel> <div className="space-y-2 max-h-32 overflow-y-auto border rounded-lg p-3"> - {revision.attachments.map((file, index) => ( + {revision.attachments.map((file) => ( <div key={file.id} className="flex items-center justify-between p-2 bg-muted/50 rounded text-sm" diff --git a/components/ship-vendor-document/new-revision-dialog.tsx b/components/ship-vendor-document/new-revision-dialog.tsx index 3ec58d1d..91694827 100644 --- a/components/ship-vendor-document/new-revision-dialog.tsx +++ b/components/ship-vendor-document/new-revision-dialog.tsx @@ -31,17 +31,22 @@ import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" -import { - Upload, - FileText, - X, - Loader2, - CheckCircle, - Info +import { + Upload, + FileText, + X, + Loader2, + CheckCircle } from "lucide-react" import { toast } from "sonner" import { useSession } from "next-auth/react" -import { Alert, AlertDescription } from "@/components/ui/alert" +import { + createUploadRevisionSchema, + getUsageOptions, + getUsageTypeOptions, + getRevisionGuide, + B3RevisionInput +} from "./revision-validation" // 기존 메인 컴포넌트에서 추가할 import // import { NewRevisionDialog } from "./new-revision-dialog" @@ -50,166 +55,12 @@ import { Alert, AlertDescription } from "@/components/ui/alert" * Schema & Types * -----------------------------------------------------------------------------------------------*/ -// 파일 검증 스키마 -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"), - comment: z.string().optional(), - attachments: z - .array(z.instanceof(File)) - .min(1, "Please upload at least 1 file") - .refine( - (files) => files.every((file) => file.size <= MAX_FILE_SIZE), - "File size must be 50MB or less" - ), - } - // 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(), - }) - } -} -// drawingKind에 따른 용도 옵션 -const getUsageOptions = (drawingKind: string) => { - switch (drawingKind) { - case 'B3': - return [ - { value: "Approval", label: "Approval" }, - { value: "Working", label: "Working" }, - { value: "Comments", label: "Comments" }, - ] - case 'B4': - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - case 'B5': - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - default: - return [ - { value: "Pre", label: "Pre" }, - { value: "Working", label: "Working" }, - ] - } -} -// B3 전용 용도 타입 옵션 -const getUsageTypeOptions = (usage: string) => { - switch (usage) { - case 'Approval': - return [ - { value: "Full", label: "Full" }, - { value: "Partial", label: "Partial" }, - ] - case 'Working': - return [ - { value: "Full", label: "Full" }, - { value: "Partial", label: "Partial" }, - ] - case 'Comments': - return [ - { value: "Comments", label: "Comments" }, - ] - default: - return [] - } -} - -// 리비전 형식 가이드 생성 -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 { open: boolean @@ -217,7 +68,7 @@ interface NewRevisionDialogProps { documentId: number documentTitle?: string drawingKind: string - onSuccess?: (result?: any) => void + onSuccess?: (result?: unknown) => void } /* ------------------------------------------------------------------------------------------------- @@ -329,55 +180,6 @@ 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 @@ -398,15 +200,30 @@ export function NewRevisionDialog({ // Serial No 조회 const fetchNextSerialNo = React.useCallback(async () => { + console.log('🔍 fetchNextSerialNo called with documentId:', documentId) setIsLoadingSerialNo(true) try { - const response = await fetch(`/api/revisions/max-serial-no?documentId=${documentId}`) + const apiUrl = `/api/revisions/max-serial-no?documentId=${documentId}` + console.log('🔍 Calling API:', apiUrl) + + const response = await fetch(apiUrl) + console.log('🔍 API Response status:', response.status) + if (response.ok) { const data = await response.json() - setNextSerialNo(String(data.nextSerialNo)) + console.log('🔍 API Response data:', data) + console.log('🔍 data.nextSerialNo:', data.nextSerialNo) + + const serialNoString = String(data.nextSerialNo) + console.log('🔍 Setting nextSerialNo to:', serialNoString) + + setNextSerialNo(serialNoString) + console.log('🔍 nextSerialNo state updated') + } else { + console.error('🔍 API call failed with status:', response.status) } } catch (error) { - console.error('Failed to fetch serial no:', error) + console.error('❌ Failed to fetch serial no:', error) // 에러 시 기본값 1 사용 setNextSerialNo("1") } finally { @@ -416,8 +233,12 @@ export function NewRevisionDialog({ // Dialog 열릴 때 Serial No 조회 React.useEffect(() => { + console.log('🎯 useEffect triggered - open:', open, 'documentId:', documentId) if (open && documentId) { + console.log('🎯 Calling fetchNextSerialNo') fetchNextSerialNo() + } else { + console.log('🎯 Conditions not met for fetchNextSerialNo') } }, [open, documentId, fetchNextSerialNo]) @@ -427,7 +248,7 @@ export function NewRevisionDialog({ }, [session]); // drawingKind에 따른 동적 스키마 및 옵션 생성 - const revisionUploadSchema = React.useMemo(() => createRevisionUploadSchema(drawingKind), [drawingKind]) + const revisionUploadSchema = React.useMemo(() => createUploadRevisionSchema(drawingKind), [drawingKind]) const usageOptions = React.useMemo(() => getUsageOptions(drawingKind), [drawingKind]) const showUsageType = drawingKind === 'B3' @@ -480,6 +301,10 @@ export function NewRevisionDialog({ } const onSubmit = async (data: RevisionUploadSchema) => { + console.log('🚀 onSubmit called with data:', data) + console.log('🚀 Current nextSerialNo state:', nextSerialNo) + console.log('🚀 documentId:', documentId) + setIsUploading(true) setUploadProgress(0) @@ -487,6 +312,8 @@ export function NewRevisionDialog({ const formData = new FormData() formData.append("documentId", String(documentId)) formData.append("serialNo", nextSerialNo) // 추가 + console.log('🚀 Appending serialNo to formData:', nextSerialNo) + formData.append("usage", data.usage) formData.append("revision", data.revision) formData.append("uploaderName", userName || "evcp") @@ -603,8 +430,15 @@ export function NewRevisionDialog({ <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" /> + <> + <Loader2 className="inline-block ml-2 h-3 w-3 animate-spin" /> + <span className="ml-1">Loading...</span> + </> )} + {/* 디버그용 임시 표시 */} + <div className="mt-1 text-xs text-orange-600"> + Debug: nextSerialNo={nextSerialNo}, isLoading={isLoadingSerialNo} + </div> </div> </DialogDescription> )} diff --git a/components/ship-vendor-document/revision-validation.tsx b/components/ship-vendor-document/revision-validation.tsx new file mode 100644 index 00000000..4ff621a0 --- /dev/null +++ b/components/ship-vendor-document/revision-validation.tsx @@ -0,0 +1,266 @@ +import React from "react" +import { z } from "zod" +import { Input } from "@/components/ui/input" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Info } from "lucide-react" + +// 파일 검증 스키마 +export const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB + +// B3 리비전 검증 함수 +export 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 리비전 검증 함수 +export const validateB4Revision = (value: string) => { + // B4 리비전 패턴: R01-R99 + const numericPattern = /^R(0[1-9]|[1-9][0-9])$/ + return numericPattern.test(value) +} + +// 리비전 검증 함수 (drawingKind에 따라) +export const validateRevision = (value: string, drawingKind: string) => { + if (drawingKind === 'B3') { + return validateB3Revision(value) + } else { + return validateB4Revision(value) + } +} + +// 리비전 수정용 스키마 생성 +export const createEditRevisionSchema = (drawingKind: string) => { + const baseSchema = { + usage: z.string().min(1, "Please select a usage"), + comment: z.string().optional(), + } + + // drawingKind에 따른 리비전 검증 추가 + 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(), + }) + } +} + +// 리비전 업로드용 스키마 생성 +export const createUploadRevisionSchema = (drawingKind: string) => { + const baseSchema = { + usage: z.string().min(1, "Please select a usage"), + comment: z.string().optional(), + attachments: z + .array(z.instanceof(File)) + .min(1, "Please upload at least 1 file") + .refine( + (files) => files.every((file) => file.size <= MAX_FILE_SIZE), + "File size must be 50MB or less" + ), + } + + // drawingKind에 따른 리비전 검증 추가 + 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(), + }) + } +} + +// drawingKind에 따른 용도 옵션 +export const getUsageOptions = (drawingKind: string) => { + switch (drawingKind) { + case 'B3': + return [ + { value: "Approval", label: "Approval" }, + { value: "Working", label: "Working" }, + { value: "Comments", label: "Comments" }, + ] + case 'B4': + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + case 'B5': + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + default: + return [ + { value: "Pre", label: "Pre" }, + { value: "Working", label: "Working" }, + ] + } +} + +// B3 전용 용도 타입 옵션 +export const getUsageTypeOptions = (usage: string) => { + switch (usage) { + case 'Approval': + return [ + { value: "Full", label: "Full" }, + { value: "Partial", label: "Partial" }, + ] + case 'Working': + return [ + { value: "Full", label: "Full" }, + { value: "Partial", label: "Partial" }, + ] + case 'Comments': + return [ + { value: "Comments", label: "Comments" }, + ] + default: + return [] + } +} + +// 리비전 형식 가이드 생성 +export 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 리비전 자동 포맷팅 함수 +export 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 +} + +// B3 리비전 입력 컴포넌트 +export function B3RevisionInput({ + value, + onChange, + error +}: { + value: string + onChange: (value: string) => void + error?: string +}) { + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const rawValue = e.target.value.toUpperCase() + + // 길이 제한 (알파벳은 1자, R숫자는 3자) + if (rawValue.length <= 3) { + onChange(rawValue) + } + } + + const handleBlur = () => { + // blur 시점에 포맷팅 적용 + const formattedValue = formatB3RevisionInput(value) + onChange(formattedValue) + } + + const revisionGuide = getRevisionGuide('B3') + + return ( + <div className="space-y-2"> + <Input + value={value} + onChange={handleInputChange} + onBlur={handleBlur} + 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> + ) +} 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 775dac47..0c3390d1 100644 --- a/components/ship-vendor-document/user-vendor-document-table-container.tsx +++ b/components/ship-vendor-document/user-vendor-document-table-container.tsx @@ -967,6 +967,7 @@ function SubTables() { reviewerId: null, reviewerName: null, reviewComments: null, + serialNo: uploadResult.data.serialNo || null, // ✅ serialNo 추가 createdAt: new Date(), updatedAt: new Date(), stageName: uploadResult.data.stage, @@ -997,11 +998,12 @@ function SubTables() { ) if (targetStage) { - // 기존 revision과 중복 체크 (같은 revision, usage, usageType) + // 기존 revision과 중복 체크 (같은 revision, usage, usageType, serialNo) const isDuplicate = targetStage.revisions.some(rev => rev.revision === newRevision.revision && rev.usage === newRevision.usage && - rev.usageType === newRevision.usageType + rev.usageType === newRevision.usageType && + rev.serialNo === newRevision.serialNo ) if (!isDuplicate) { |
