"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" interface ManualCreateEvaluationTargetDialogProps { open: boolean onOpenChange: (open: boolean) => void onSuccess?: () => void onDataChange?: () => void } export function ManualCreateEvaluationTargetDialog({ open, onOpenChange, onSuccess, onDataChange }: 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 createEvaluationTargetSchema = z.object({ evaluationYear: z.number().min(2020).max(2030), division: z.enum(["PLANT", "SHIP"]), vendorId: z.number().min(0), // 0도 허용, 클라이언트에서 검증 materialType: z.enum(["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) 제거, 나중에 클라이언트에서 필터링 }) ), }).refine((data) => { // 벤더가 선택되어야 함 if (data.vendorId === 0) { return false; } // 최소 1명의 담당자가 지정되어야 함 (reviewerUserId > 0) const validReviewers = data.reviewers .filter(r => r.reviewerUserId > 0) .map((r, i) => ({ departmentCode: r.departmentCode || departments[i]?.code, // 없으면 보충 reviewerUserId: r.reviewerUserId, })); return validReviewers.length > 0; }, { message: "벤더를 선택하고 최소 1명의 담당자를 지정해주세요.", path: ["vendorId"] }) type CreateEvaluationTargetFormValues = z.infer const form = useForm({ resolver: zodResolver(createEvaluationTargetSchema), defaultValues: { evaluationYear: getDefaultEvaluationYear(), division: "SHIP", vendorId: 0, // 임시로 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 defaultReviewers = departments.map(dept => ({ departmentCode: dept.code, // ✅ 반드시 포함 reviewerUserId: 0, })); form.setValue("reviewers", defaultReviewers, { shouldValidate: false }); } }, [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: "SHIP", vendorId: 0, materialType: "EQUIPMENT", adminComment: "", ldClaimCount: 0, ldClaimAmount: 0, ldClaimCurrency: "KRW", reviewers: [], }) setDepartments([]) setVendors([]) setReviewers([]) setVendorSearch("") setReviewerSearches({}) setReviewerOpens({}) onSuccess?.() // 기존 방식 (table.resetRowSelection, router.refresh 등) onDataChange?.() // 새로운 방식 (클라이언트 상태 업데이트) 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: "SHIP", 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을 전체 콘텐츠를 감싸도록 수정 */}
console.log('❌ validation errors:', errors) )} className="flex flex-col flex-1 min-h-0" id="evaluation-target-form" > {/* 스크롤 가능한 콘텐츠 영역 */}
{/* 기본 정보 */} 기본 정보
{/* 평가년도 */} ( 평가년도 field.onChange(parseInt(e.target.value))} /> )} /> {/* 구분 */} ( 구분 )} />
{/* 벤더 선택 */} ( 벤더 {isLoadingVendors ? (
로딩 중...
) : ( "검색 결과가 없습니다." )}
{vendors.map((vendor) => ( { field.onChange(vendor.id) setVendorOpen(false) }} >
{vendor.vendorCode} {vendor.vendorName}
))}
)} /> {/* 자재구분 */} ( 자재구분 )} /> {/* 관리자 의견 */} ( 관리자 의견 (선택사항)