From 74843fe598702a9a55f914f2d2d291368a5abb13 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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 ++++++++++++++++++---
 1 file changed, 204 insertions(+), 33 deletions(-)

(limited to 'components/ship-vendor-document/new-revision-dialog.tsx')

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) => (
@@ -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<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
  * -----------------------------------------------------------------------------------------------*/
@@ -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>
-- 
cgit v1.2.3

