From 95bbe9c583ff841220da1267630e7b2025fc36dc Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 19 Jun 2025 09:44:28 +0000 Subject: (대표님) 20250619 1844 KST 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manual-create-evaluation-target-dialog.tsx | 772 +++++++++++++++++++++ 1 file changed, 772 insertions(+) create mode 100644 lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx (limited to 'lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx') diff --git a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx new file mode 100644 index 00000000..5704cba1 --- /dev/null +++ b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx @@ -0,0 +1,772 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Check, ChevronsUpDown, Loader2, Search } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" + +import { + createEvaluationTarget, + getAvailableVendors, + getAvailableReviewers, + getDepartmentInfo, + type CreateEvaluationTargetInput, +} from "../service" +import { EVALUATION_TARGET_FILTER_OPTIONS, getDefaultEvaluationYear } from "../validation" +import { useSession } from "next-auth/react" + +// 폼 스키마 정의 +const createEvaluationTargetSchema = z.object({ + evaluationYear: z.number().min(2020).max(2030), + division: z.enum(["OCEAN", "SHIPYARD"]), + vendorId: z.number().min(1, "벤더를 선택해주세요"), + materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]), + adminComment: z.string().optional(), + // L/D 클레임 정보 + ldClaimCount: z.number().min(0).optional(), + ldClaimAmount: z.number().min(0).optional(), + ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(), + reviewers: z.array( + z.object({ + departmentCode: z.string(), + reviewerUserId: z.number().min(1, "담당자를 선택해주세요"), + }) + ).min(1, "최소 1명의 담당자를 지정해주세요"), +}) + +type CreateEvaluationTargetFormValues = z.infer + +interface ManualCreateEvaluationTargetDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ManualCreateEvaluationTargetDialog({ + open, + onOpenChange, +}: ManualCreateEvaluationTargetDialogProps) { + const router = useRouter() + const [isSubmitting, setIsSubmitting] = React.useState(false) + const { data: session } = useSession() + const userId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : 1; + }, [session]); + + // 벤더 관련 상태 + const [vendors, setVendors] = React.useState>([]) + const [vendorSearch, setVendorSearch] = React.useState("") + const [vendorOpen, setVendorOpen] = React.useState(false) + const [isLoadingVendors, setIsLoadingVendors] = React.useState(false) + + // 담당자 관련 상태 + const [reviewers, setReviewers] = React.useState>([]) + const [isLoadingReviewers, setIsLoadingReviewers] = React.useState(false) + + // 각 부서별 담당자 선택 상태 + const [reviewerSearches, setReviewerSearches] = React.useState>({}) + const [reviewerOpens, setReviewerOpens] = React.useState>({}) + + // 부서 정보 상태 + const [departments, setDepartments] = React.useState>([]) + + const form = useForm({ + resolver: zodResolver(createEvaluationTargetSchema), + defaultValues: { + evaluationYear: getDefaultEvaluationYear(), + division: "OCEAN", + vendorId: 0, + materialType: "EQUIPMENT", + adminComment: "", + ldClaimCount: 0, + ldClaimAmount: 0, + ldClaimCurrency: "KRW", + reviewers: [], // 초기에는 빈 배열, useEffect에서 설정 + }, + }) + + // 부서 정보 로드 + const loadDepartments = React.useCallback(async () => { + try { + const departmentList = await getDepartmentInfo() + setDepartments(departmentList) + } catch (error) { + console.error("Error loading departments:", error) + toast.error("부서 정보를 불러오는데 실패했습니다.") + } + }, []) // form 의존성 제거 + + // 벤더 목록 로드 + const loadVendors = React.useCallback(async (search?: string) => { + setIsLoadingVendors(true) + try { + const vendorList = await getAvailableVendors(search) + setVendors(vendorList) + } catch (error) { + console.error("Error loading vendors:", error) + toast.error("벤더 목록을 불러오는데 실패했습니다.") + } finally { + setIsLoadingVendors(false) + } + }, []) + + // 담당자 목록 로드 + const loadReviewers = React.useCallback(async () => { + setIsLoadingReviewers(true) + try { + const reviewerList = await getAvailableReviewers() + setReviewers(reviewerList) + } catch (error) { + console.error("Error loading reviewers:", error) + toast.error("담당자 목록을 불러오는데 실패했습니다.") + } finally { + setIsLoadingReviewers(false) + } + }, []) + + // 다이얼로그 열릴 때 데이터 로드 + React.useEffect(() => { + if (open) { + loadDepartments() + loadVendors() + loadReviewers() + } + }, [open]) // 함수 의존성 제거 + + // 부서 정보가 로드되면 reviewers 기본값 설정 + React.useEffect(() => { + if (departments.length > 0 && open) { + const currentReviewers = form.getValues("reviewers") + + // 이미 설정되어 있으면 다시 설정하지 않음 + if (currentReviewers.length === 0) { + const defaultReviewers = departments.map(dept => ({ + departmentCode: dept.code, + reviewerUserId: 0, + })) + form.setValue('reviewers', defaultReviewers) + } + } + }, [departments, open]) // form 의존성 제거하고 조건 추가 + + console.log(departments) + + // 벤더 검색 + React.useEffect(() => { + const timeoutId = setTimeout(() => { + if (vendorSearch || vendorOpen) { + loadVendors(vendorSearch) + } + }, 300) + + return () => clearTimeout(timeoutId) + }, [vendorSearch, vendorOpen]) // loadVendors 의존성 제거 + + // 폼 제출 + async function onSubmit(data: CreateEvaluationTargetFormValues) { + console.log("Form submitted with data:", data) // 디버깅용 + setIsSubmitting(true) + try { + // 담당자가 지정되지 않은 부서 제외 + const validReviewers = data.reviewers.filter(r => r.reviewerUserId > 0) + + if (validReviewers.length === 0) { + toast.error("최소 1명의 담당자를 지정해주세요.") + return + } + + const input: CreateEvaluationTargetInput = { + ...data, + reviewers: validReviewers, + } + + console.log(input,"client") + + const result = await createEvaluationTarget(input, userId) + + if (result.success) { + toast.success(result.message) + onOpenChange(false) + // 폼과 상태 초기화 + form.reset({ + evaluationYear: getDefaultEvaluationYear(), + division: "OCEAN", + vendorId: 0, + materialType: "EQUIPMENT", + adminComment: "", + ldClaimCount: 0, + ldClaimAmount: 0, + ldClaimCurrency: "KRW", + reviewers: [], + }) + setDepartments([]) + setVendors([]) + setReviewers([]) + setVendorSearch("") + setReviewerSearches({}) + setReviewerOpens({}) + router.refresh() + } else { + toast.error(result.error || "평가 대상 생성에 실패했습니다.") + } + } catch (error) { + console.error("Error creating evaluation target:", error) + toast.error("평가 대상 생성 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + } + } + + // 다이얼로그 닫기 핸들러 + const handleOpenChange = (open: boolean) => { + onOpenChange(open) + + // 다이얼로그가 닫힐 때 상태 초기화 + if (!open) { + form.reset({ + evaluationYear: getDefaultEvaluationYear(), + division: "OCEAN", + vendorId: 0, + materialType: "EQUIPMENT", + adminComment: "", + ldClaimCount: 0, + ldClaimAmount: 0, + ldClaimCurrency: "KRW", + reviewers: [], + }) + setDepartments([]) + setVendors([]) + setReviewers([]) + setVendorSearch("") + setReviewerSearches({}) + setReviewerOpens({}) + } + } + + // 선택된 벤더 정보 + const selectedVendor = vendors.find(v => v.id === form.watch("vendorId")) + + // 현재 선택된 자재구분 + const currentMaterialType = form.watch("materialType") + + // BULK 자재일 때 비활성화할 부서 코드들 + const BULK_DISABLED_DEPARTMENTS = ["DESIGN_EVAL", "CS_EVAL"] // 설계(DES), CS 부서 코드 (실제 코드에 맞게 수정 필요) + + // 담당자 검색 필터링 + const getFilteredReviewers = (search: string) => { + if (!search) return reviewers + return reviewers.filter(reviewer => + reviewer.name.toLowerCase().includes(search.toLowerCase()) || + reviewer.email.toLowerCase().includes(search.toLowerCase()) + ) + } + + // 부서가 비활성화되어야 하는지 확인 + const isDepartmentDisabled = (departmentCode: string) => { + return currentMaterialType === "BULK" && BULK_DISABLED_DEPARTMENTS.includes(departmentCode) + } + + // 자재구분 변경 시 BULK 비활성화 부서들의 담당자 초기화 + React.useEffect(() => { + if (currentMaterialType === "BULK") { + const currentReviewers = form.getValues("reviewers") + const updatedReviewers = currentReviewers.map(reviewer => { + if (BULK_DISABLED_DEPARTMENTS.includes(reviewer.departmentCode)) { + return { ...reviewer, reviewerUserId: 0 } + } + return reviewer + }) + form.setValue("reviewers", updatedReviewers) + } + }, [currentMaterialType, form]) + + return ( + + + {/* 고정 헤더 */} + + 평가 대상 수동 생성 + + 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다. + + + + {/* Form을 전체 콘텐츠를 감싸도록 수정 */} +
+ + {/* 스크롤 가능한 콘텐츠 영역 */} +
+
+ {/* 기본 정보 */} + + + 기본 정보 + + +
+ {/* 평가년도 */} + ( + + 평가년도 + + field.onChange(parseInt(e.target.value))} + /> + + + + )} + /> + + {/* 구분 */} + ( + + 구분 + + + + )} + /> +
+ + {/* 벤더 선택 */} + ( + + 벤더 + + + + + + + + + + + + {isLoadingVendors ? ( +
+ + 로딩 중... +
+ ) : ( + "검색 결과가 없습니다." + )} +
+ + {vendors.map((vendor) => ( + { + field.onChange(vendor.id) + setVendorOpen(false) + }} + > + +
+ {vendor.vendorCode} + {vendor.vendorName} +
+
+ ))} +
+
+
+
+
+ +
+ )} + /> + + {/* 자재구분 */} + ( + + 자재구분 + + + + )} + /> + + {/* 관리자 의견 */} + ( + + 관리자 의견 (선택사항) + +