From 624cfcf4edb106e6cf0b041d9437ceaa94b6a46d Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Thu, 2 Oct 2025 18:02:11 +0900
Subject: (디버깅) 돌체 디버깅 - serialNo, 변경사항 카운트
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ship-vendor-document/edit-revision-dialog.tsx | 137 ++++-------
.../ship-vendor-document/new-revision-dialog.tsx | 268 ++++-----------------
.../ship-vendor-document/revision-validation.tsx | 266 ++++++++++++++++++++
.../user-vendor-document-table-container.tsx | 6 +-
4 files changed, 364 insertions(+), 313 deletions(-)
create mode 100644 components/ship-vendor-document/revision-validation.tsx
(limited to 'components')
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({
Revision
-
+ {drawingKind === 'B3' ? (
+
+ ) : (
+ <>
+ {
+ const upperValue = e.target.value.toUpperCase()
+ if (upperValue.length <= 3) {
+ field.onChange(upperValue)
+ }
+ }}
+ />
+
+ {revisionGuide.helpText}
+
+ >
+ )}
-
- {revisionGuide}
-
)}
@@ -495,7 +444,7 @@ export function EditRevisionDialog({
{watchedUsage === "Comments" && (
- Automatically set to "Comments" for this usage
+ Automatically set to "Comments" for this usage
)}
@@ -574,7 +523,7 @@ export function EditRevisionDialog({
Attachments ({revision.attachments.length})
- {revision.attachments.map((file, index) => (
+ {revision.attachments.map((file) => (
{
- // 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
) => {
- 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
@@ -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({
Drawing Type: {drawingKind} | Serial No: {nextSerialNo}
{isLoadingSerialNo && (
-
+ <>
+
+
Loading...
+ >
)}
+ {/* 디버그용 임시 표시 */}
+
+ Debug: nextSerialNo={nextSerialNo}, isLoading={isLoadingSerialNo}
+
)}
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) => {
+ 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 (
+
+
+
+
+
+ {revisionGuide.helpText}
+
+ {revisionGuide.examples.map((example, idx) => (
+
• {example}
+ ))}
+
+
+
+
+ )
+}
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) {
--
cgit v1.2.3