From 5d14a673d9ad9223a5b5a8f87bbdf6e1594d7751 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 2 Jun 2025 06:06:01 +0000
Subject: (대표님) 20250602 오후 개발사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/vendor-document-list/table/add-doc-dialog.tsx | 154 ++++++++++++++--------
 1 file changed, 97 insertions(+), 57 deletions(-)

(limited to 'lib/vendor-document-list/table/add-doc-dialog.tsx')

diff --git a/lib/vendor-document-list/table/add-doc-dialog.tsx b/lib/vendor-document-list/table/add-doc-dialog.tsx
index 9bedc810..9ea07abd 100644
--- a/lib/vendor-document-list/table/add-doc-dialog.tsx
+++ b/lib/vendor-document-list/table/add-doc-dialog.tsx
@@ -4,7 +4,7 @@ import * as React from "react"
 import { useForm } from "react-hook-form"
 import { zodResolver } from "@hookform/resolvers/zod"
 import { z } from "zod"
-import { Plus, X } from "lucide-react"
+import { Plus, X, ChevronUp, ChevronDown } from "lucide-react"
 import { useRouter } from "next/navigation"
 
 import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
@@ -21,15 +21,31 @@ import {
 import { useToast } from "@/hooks/use-toast"
 import { createDocument, CreateDocumentInputType, invalidateDocumentCache } from "../service"
 
-// Zod 스키마 정의 - 빈 문자열 방지 로직 추가
+// 스테이지 객체로 변경
+type StageItem = {
+    name: string;
+    order: number;
+}
+
+// Zod 스키마 정의 - 스테이지 구조 변경
 const createDocumentSchema = z.object({
     docNumber: z.string().min(1, "Document number is required"),
     title: z.string().min(1, "Title is required"),
-    stages: z.array(z.string().min(1, "Stage name cannot be empty"))
+    stages: z.array(z.object({
+        name: z.string().min(1, "Stage name cannot be empty"),
+        order: z.number().int().positive("Order must be a positive integer")
+    }))
         .min(1, "At least one stage is required")
-        .refine(stages => !stages.some(stage => stage.trim() === ""), {
+        .refine(stages => !stages.some(stage => stage.name.trim() === ""), {
             message: "Stage names cannot be empty"
         })
+        .refine(stages => {
+            // 중복된 order 값이 없는지 확인
+            const orders = stages.map(s => s.order);
+            return orders.length === new Set(orders).size;
+        }, {
+            message: "Stage orders must be unique"
+        })
 });
 
 type CreateDocumentSchema = z.infer<typeof createDocumentSchema>;
@@ -37,7 +53,7 @@ type CreateDocumentSchema = z.infer<typeof createDocumentSchema>;
 interface AddDocumentListDialogProps {
     projectType: "ship" | "plant";
     contractId: number;
-    onSuccess?: () => void; // ✅ onSuccess 콜백 추가
+    onSuccess?: () => void;
 }
 
 export function AddDocumentListDialog({ projectType, contractId, onSuccess }: AddDocumentListDialogProps) {
@@ -46,10 +62,13 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
     const router = useRouter();
     const { toast } = useToast()
     
-    // 기본 스테이지 설정
-    const defaultStages = projectType === "ship" 
-        ? ["For Approval", "For Working"] 
-        : [""];
+    // 기본 스테이지 설정 - 객체 배열로 변경
+    const defaultStages: StageItem[] = projectType === "ship" 
+        ? [
+            { name: "For Approval", order: 1 },
+            { name: "For Working", order: 2 }
+          ] 
+        : [{ name: "", order: 1 }];
     
     // react-hook-form 설정
     const form = useForm<CreateDocumentSchema>({
@@ -61,25 +80,45 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
         },
     });
     
-    // 식물 유형일 때 단계 추가 기능
+    // 스테이지 추가 기능
     const addStage = () => {
         const currentStages = form.getValues().stages;
-        form.setValue('stages', [...currentStages, ""], { shouldValidate: true });
+        const nextOrder = Math.max(...currentStages.map(s => s.order), 0) + 1;
+        form.setValue('stages', [...currentStages, { name: "", order: nextOrder }], { shouldValidate: true });
     };
     
-    // 식물 유형일 때 단계 제거 기능
+    // 스테이지 제거 기능
     const removeStage = (index: number) => {
         const currentStages = form.getValues().stages;
         const newStages = currentStages.filter((_, i) => i !== index);
-        form.setValue('stages', newStages, { shouldValidate: true });
+        // 순서 재정렬
+        const reorderedStages = newStages.map((stage, i) => ({ ...stage, order: i + 1 }));
+        form.setValue('stages', reorderedStages, { shouldValidate: true });
+    };
+    
+    // 스테이지 순서 이동 기능
+    const moveStage = (index: number, direction: 'up' | 'down') => {
+        const currentStages = [...form.getValues().stages];
+        const targetIndex = direction === 'up' ? index - 1 : index + 1;
+        
+        if (targetIndex < 0 || targetIndex >= currentStages.length) return;
+        
+        // 스테이지 위치 교환
+        [currentStages[index], currentStages[targetIndex]] = [currentStages[targetIndex], currentStages[index]];
+        
+        // order 값 재정렬
+        const reorderedStages = currentStages.map((stage, i) => ({ ...stage, order: i + 1 }));
+        form.setValue('stages', reorderedStages, { shouldValidate: true });
     };
     
     async function onSubmit(data: CreateDocumentSchema) {
         try {
             setIsSubmitting(true);
             
-            // 빈 문자열 필터링 (추가 안전장치)
-            const filteredStages = data.stages.filter(stage => stage.trim() !== "");
+            // 빈 문자열 필터링 및 순서 정렬
+            const filteredStages = data.stages
+                .filter(stage => stage.name.trim() !== "")
+                .sort((a, b) => a.order - b.order);
             
             if (filteredStages.length === 0) {
                 toast({
@@ -90,23 +129,23 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
                 return;
             }
             
-            // 서버 액션 호출 - status를 "pending"으로 설정
+            // 서버로 전달할 데이터 형태 변환
             const result = await createDocument({
-                ...data,
-                stages: filteredStages, // 필터링된 단계 사용
-                status: "pending", // status 필드 추가
-                contractId, // 계약 ID 추가
+                docNumber: data.docNumber,
+                title: data.title,
+                stages: filteredStages, // { name, order } 객체 배열로 전달
+                status: "pending",
+                contractId,
             } as CreateDocumentInputType);
             
             if (result.success) {
-                // ✅ 캐시 무효화 시도 (에러가 나더라도 계속 진행)
+                // 캐시 무효화 시도
                 try {
                     await invalidateDocumentCache(contractId);
                 } catch (cacheError) {
                     console.warn('Cache invalidation failed:', cacheError);
                 }
                 
-                // 토스트 메시지
                 toast({
                     title: "Success",
                     description: "Document created successfully",
@@ -121,18 +160,15 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
                 });
                 setOpen(false);
                 
-                // ✅ 성공 콜백 호출 (부모 컴포넌트에서 추가 처리 가능)
                 if (onSuccess) {
                     onSuccess();
                 }
                 
-                // ✅ 라우터 새로고침 (약간의 지연을 두고 실행)
                 setTimeout(() => {
                     router.refresh();
                 }, 100);
                 
             } else {
-                // 실패 시 에러 토스트
                 toast({
                     title: "Error",
                     description: result.message || "Failed to create document",
@@ -151,23 +187,6 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
         }
     }
     
-    // 제출 전 유효성 검사
-    const validateBeforeSubmit = async () => {
-        // 빈 스테이지 검사
-        const stages = form.getValues().stages;
-        const hasEmptyStage = stages.some(stage => stage.trim() === "");
-        
-        if (hasEmptyStage) {
-            form.setError("stages", {
-                type: "manual",
-                message: "Stage names cannot be empty"
-            });
-            return false;
-        }
-        
-        return true;
-    };
-    
     function handleDialogOpenChange(nextOpen: boolean) {
         if (!nextOpen) {
             form.reset({
@@ -181,7 +200,6 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
     
     return (
         <Dialog open={open} onOpenChange={handleDialogOpenChange}>
-            {/* 모달을 열기 위한 버튼 */}
             <DialogTrigger asChild>
                 <Button variant="default" size="sm">
                     <Plus className="size-4 mr-1"/>
@@ -197,20 +215,8 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
                     </DialogDescription>
                 </DialogHeader>
                 
-                {/* shadcn/ui Form을 이용해 react-hook-form과 연결 */}
                 <Form {...form}>
-                    <form onSubmit={form.handleSubmit(onSubmit, async (errors) => {
-                        // 추가 유효성 검사 수행
-                        console.error("Form errors:", errors);
-                        const stages = form.getValues().stages;
-                        if (stages.some(stage => stage.trim() === "")) {
-                            toast({
-                                title: "Error",
-                                description: "Stage names cannot be empty",
-                                variant: "destructive",
-                            });
-                        }
-                    })} className="space-y-4">
+                    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
                         {/* 문서 번호 필드 */}
                         <FormField
                             control={form.control}
@@ -260,9 +266,41 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
                             
                             {form.watch("stages").map((stage, index) => (
                                 <div key={index} className="flex items-center gap-2 mb-2">
+                                    {/* 순서 표시 */}
+                                    <div className="flex flex-col items-center">
+                                        <span className="text-xs text-muted-foreground font-medium w-6 text-center">
+                                            {stage.order}
+                                        </span>
+                                        {projectType === "plant" && (
+                                            <div className="flex flex-col">
+                                                <Button
+                                                    type="button"
+                                                    variant="ghost"
+                                                    size="sm"
+                                                    onClick={() => moveStage(index, 'up')}
+                                                    disabled={index === 0}
+                                                    className="h-4 w-4 p-0"
+                                                >
+                                                    <ChevronUp className="h-3 w-3" />
+                                                </Button>
+                                                <Button
+                                                    type="button"
+                                                    variant="ghost"
+                                                    size="sm"
+                                                    onClick={() => moveStage(index, 'down')}
+                                                    disabled={index === form.watch("stages").length - 1}
+                                                    className="h-4 w-4 p-0"
+                                                >
+                                                    <ChevronDown className="h-3 w-3" />
+                                                </Button>
+                                            </div>
+                                        )}
+                                    </div>
+                                    
+                                    {/* 스테이지 이름 입력 */}
                                     <FormField
                                         control={form.control}
-                                        name={`stages.${index}`}
+                                        name={`stages.${index}.name`}
                                         render={({ field }) => (
                                             <FormItem className="flex-1">
                                                 <FormControl>
@@ -276,6 +314,8 @@ export function AddDocumentListDialog({ projectType, contractId, onSuccess }: Ad
                                             </FormItem>
                                         )}
                                     />
+                                    
+                                    {/* 제거 버튼 */}
                                     {projectType === "plant" && index > 0 && (
                                         <Button
                                             type="button"
-- 
cgit v1.2.3

