summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/vendor-document-list/service.ts47
-rw-r--r--lib/vendor-document-list/table/add-doc-dialog.tsx154
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"