diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/vendor-document-list/service.ts | 47 | ||||
| -rw-r--r-- | lib/vendor-document-list/table/add-doc-dialog.tsx | 154 |
2 files changed, 126 insertions, 75 deletions
diff --git a/lib/vendor-document-list/service.ts b/lib/vendor-document-list/service.ts index 356bc792..f3b2b633 100644 --- a/lib/vendor-document-list/service.ts +++ b/lib/vendor-document-list/service.ts @@ -81,11 +81,22 @@ export async function getVendorDocuments(input: GetVendorDcoumentsSchema, id: nu // 입력 스키마 정의 const createDocumentSchema = z.object({ + contractId: z.number(), docNumber: z.string().min(1, "Document number is required"), title: z.string().min(1, "Title is required"), status: z.string(), - stages: z.array(z.string()).min(1, "At least one stage is required"), - contractId: z.number().positive("Contract ID is required") + 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 => { + // 중복된 order 값이 없는지 확인 + const orders = stages.map(s => s.order); + return orders.length === new Set(orders).size; + }, { + message: "Stage orders must be unique" + }) }); export type CreateDocumentInputType = z.infer<typeof createDocumentSchema>; @@ -104,26 +115,26 @@ export async function createDocument(input: CreateDocumentInputType) { contractId: validatedData.contractId, docNumber: validatedData.docNumber, title: validatedData.title, - status: validatedData.status, - // issuedDate는 선택적으로 추가 가능 + status: validatedData.status, }) .returning({ id: documents.id }); - // 2. 스테이지 생성 (문서 ID 연결) - const stageValues = validatedData.stages.map(stageName => ({ + // 2. 스테이지 생성 (순서와 함께) + const stageValues = validatedData.stages.map(stage => ({ documentId: newDocument.id, - stageName: stageName, - // planDate, actualDate는 나중에 설정 가능 + stageName: stage.name, + stageOrder: stage.order, + stageStatus: "PLANNED", // 기본 상태 설정 })); // 스테이지 배열 삽입 await tx.insert(issueStages).values(stageValues); // 성공 결과 반환 - return { - success: true, + return { + success: true, documentId: newDocument.id, - message: "Document and stages created successfully" + message: "Document and stages created successfully" }; }); } catch (error) { @@ -131,17 +142,17 @@ export async function createDocument(input: CreateDocumentInputType) { // Zod 유효성 검사 에러 처리 if (error instanceof z.ZodError) { - return { - success: false, - message: "Validation failed", - errors: error.errors + return { + success: false, + message: "Validation failed", + errors: error.errors }; } // 기타 에러 처리 - return { - success: false, - message: "Failed to create document" + return { + success: false, + message: "Failed to create document" }; } } 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" |
