"use client" import * as React from "react" import { useForm, useFieldArray } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { format } from "date-fns" import { CalendarIcon, Plus, Loader2, Trash2, PlusCircle, Check, ChevronsUpDown } from "lucide-react" import { useRouter } from "next/navigation" import { useSession } from "next-auth/react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, } from "@/components/ui/command" import { Calendar } from "@/components/ui/calendar" import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { toast } from "sonner" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { createGeneralRfqAction, getPUsersForFilter, previewGeneralRfqCode } from "../service" // 아이템 스키마 const itemSchema = z.object({ itemCode: z.string().optional(), itemName: z.string().min(1, "자재명을 입력해주세요"), quantity: z.number().min(1, "수량은 1 이상이어야 합니다"), uom: z.string().min(1, "단위를 입력해주세요"), remark: z.string().optional(), }) // 일반견적 생성 폼 스키마 const createGeneralRfqSchema = z.object({ rfqType: z.string().min(1, "견적 종류를 선택해주세요"), customRfqType: z.string().optional(), rfqTitle: z.string().min(1, "견적명을 입력해주세요"), dueDate: z.date({ required_error: "마감일을 선택해주세요", }), picUserId: z.number().min(1, "구매 담당자를 선택해주세요"), remark: z.string().optional(), items: z.array(itemSchema).min(1, "최소 하나의 아이템을 추가해주세요"), }).refine((data) => { if (data.rfqType === "기타") { return data.customRfqType && data.customRfqType.trim().length > 0 } return true }, { message: "견적 종류를 직접 입력해주세요", path: ["customRfqType"], }) type CreateGeneralRfqFormValues = z.infer interface CreateGeneralRfqDialogProps { onSuccess?: () => void; } export function CreateGeneralRfqDialog({ onSuccess }: CreateGeneralRfqDialogProps) { const [open, setOpen] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false) const [users, setUsers] = React.useState>([]) const [isLoadingUsers, setIsLoadingUsers] = React.useState(false) const [userPopoverOpen, setUserPopoverOpen] = React.useState(false) const [userSearchTerm, setUserSearchTerm] = React.useState("") const [previewCode, setPreviewCode] = React.useState("") const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) const userOptionIdsRef = React.useRef>({}) const router = useRouter() const { data: session } = useSession() // 고유 ID 생성 const buttonId = React.useMemo(() => `user-button-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) const popoverContentId = React.useMemo(() => `user-popover-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) const commandId = React.useMemo(() => `user-command-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, []) const userId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null; }, [session]); const form = useForm({ resolver: zodResolver(createGeneralRfqSchema), defaultValues: { rfqType: "", customRfqType: "", rfqTitle: "", dueDate: undefined, picUserId: userId || undefined, remark: "", items: [ { itemCode: "", itemName: "", quantity: 1, uom: "", remark: "", }, ], }, }) const { fields, append, remove } = useFieldArray({ control: form.control, name: "items", }) // 견적 종류 변경 시 customRfqType 필드 초기화 const handleRfqTypeChange = (value: string) => { form.setValue("rfqType", value) if (value !== "기타") { form.setValue("customRfqType", "") } } // RFQ 코드 미리보기 생성 const generatePreviewCode = React.useCallback(async () => { const picUserId = form.watch("picUserId") if (!picUserId) { setPreviewCode("") return } setIsLoadingPreview(true) try { const code = await previewGeneralRfqCode(picUserId) setPreviewCode(code) } catch (error) { console.error("코드 미리보기 오류:", error) setPreviewCode("") } finally { setIsLoadingPreview(false) } }, [form]) // 필드 변경 감지해서 미리보기 업데이트 React.useEffect(() => { const subscription = form.watch((value, { name }) => { if (name === "picUserId") { generatePreviewCode() } }) return () => subscription.unsubscribe() }, [form, generatePreviewCode]) // 사용자 목록 로드 React.useEffect(() => { const loadUsers = async () => { setIsLoadingUsers(true) try { const userList = await getPUsersForFilter() setUsers(userList) } catch (error) { console.log("사용자 목록 로드 오류:", error) toast.error("사용자 목록을 불러오는데 실패했습니다") } finally { setIsLoadingUsers(false) } } loadUsers() }, []) // 세션 사용자 ID로 기본값 설정 React.useEffect(() => { if (userId && !form.getValues("picUserId")) { form.setValue("picUserId", userId) } }, [userId, form]) // 사용자 검색 필터링 const userOptions = React.useMemo(() => { return users.filter((user) => user.name.toLowerCase().includes(userSearchTerm.toLowerCase()) ) }, [users, userSearchTerm]) // 선택된 사용자 찾기 const selectedUser = React.useMemo(() => { const picUserId = form.watch("picUserId") return users.find(user => user.id === picUserId) }, [users, form.watch("picUserId")]) // 사용자 선택 핸들러 const handleSelectUser = (user: { id: number; name: string }) => { form.setValue("picUserId", user.id) setUserPopoverOpen(false) setUserSearchTerm("") } // 다이얼로그 열림/닫힘 처리 및 폼 리셋 const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen) // 다이얼로그가 닫힐 때 폼과 상태 초기화 if (!newOpen) { form.reset({ rfqType: "", customRfqType: "", rfqTitle: "", dueDate: undefined, picUserId: userId || undefined, remark: "", items: [ { itemCode: "", itemName: "", quantity: 1, uom: "", remark: "", }, ], }) setUserSearchTerm("") setUserPopoverOpen(false) setPreviewCode("") setIsLoadingPreview(false) } } const handleCancel = () => { form.reset() setOpen(false) } const onSubmit = async (data: CreateGeneralRfqFormValues) => { if (!userId) { toast.error("로그인이 필요합니다") return } setIsLoading(true) try { // 견적 종류가 "기타"인 경우 customRfqType 사용 const finalRfqType = data.rfqType === "기타" ? data.customRfqType || "기타" : data.rfqType // 서버 액션 호출 const result = await createGeneralRfqAction({ rfqType: finalRfqType, rfqTitle: data.rfqTitle, dueDate: data.dueDate, picUserId: data.picUserId, remark: data.remark || "", items: data.items, createdBy: userId, updatedBy: userId, }) if (result.success) { toast.success(result.message, { description: `RFQ 코드: ${result.data?.rfqCode}`, }) // 다이얼로그 닫기 setOpen(false) // 성공 콜백 실행 if (onSuccess) { onSuccess() } // RFQ 상세 페이지로 이동 (선택사항) // router.push(`/rfq/${result.data?.rfqCode}`) } else { toast.error(result.error || "일반견적 생성에 실패했습니다") } } catch (error) { console.error('일반견적 생성 오류:', error) toast.error("일반견적 생성에 실패했습니다", { description: "알 수 없는 오류가 발생했습니다", }) } finally { setIsLoading(false) } } // 아이템 추가 const handleAddItem = () => { append({ itemCode: "", itemName: "", quantity: 1, uom: "", remark: "", }) } const isCustomRfqType = form.watch("rfqType") === "기타" return ( {/* 고정된 헤더 */} 일반견적 생성 새로운 일반견적을 생성합니다. 필수 정보를 입력해주세요. {/* 스크롤 가능한 컨텐츠 영역 */}
{/* 기본 정보 섹션 */}

기본 정보

{/* 견적 종류 */}
( 견적 종류 * )} /> {/* 기타 견적 종류 입력 필드 */} {isCustomRfqType && ( ( 견적 종류 직접 입력 * )} /> )}
{/* 마감일 */} ( 마감일 * date < new Date() || date < new Date("1900-01-01") } initialFocus /> )} />
{/* 견적명 */} ( 견적명 * 견적의 목적이나 내용을 간단명료하게 입력해주세요 )} /> {/* 구매 담당자 - 검색 가능한 셀렉터로 변경 */} ( 구매 담당자 * { e.stopPropagation(); // 이벤트 전파 차단 const target = e.currentTarget; target.scrollTop += e.deltaY; // 직접 스크롤 처리 }} > 담당자를 찾을 수 없습니다. {userOptions.map((user, userIndex) => { if (!userOptionIdsRef.current[user.id]) { userOptionIdsRef.current[user.id] = `user-${user.id}-${Date.now()}-${Math.random() .toString(36) .slice(2, 9)}` } const optionId = userOptionIdsRef.current[user.id] return ( handleSelectUser(user)} value={user.name} className="truncate" title={user.name} > {user.name} ) })} {/* RFQ 코드 미리보기 */} {previewCode && (
예상 RFQ 코드: {previewCode} {isLoadingPreview && ( )}
)}
)} /> {/* 비고 */} ( 비고