diff options
Diffstat (limited to 'lib/b-rfq/summary-table/add-new-rfq-dialog.tsx')
| -rw-r--r-- | lib/b-rfq/summary-table/add-new-rfq-dialog.tsx | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx b/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx new file mode 100644 index 00000000..2333d9cf --- /dev/null +++ b/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx @@ -0,0 +1,523 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { format } from "date-fns" +import { CalendarIcon, Plus, Loader2, Eye } 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 { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Calendar } from "@/components/ui/calendar" +import { Badge } from "@/components/ui/badge" +import { cn } from "@/lib/utils" +import { toast } from "sonner" +import { ProjectSelector } from "@/components/ProjectSelector" +import { createRfqAction, previewNextRfqCode } from "../service" + +export type Project = { + id: number; + projectCode: string; + projectName: string; +} + +// 클라이언트 폼 스키마 (projectId 필수로 변경) +const createRfqSchema = z.object({ + projectId: z.number().min(1, "프로젝트를 선택해주세요"), // 필수로 변경 + dueDate: z.date({ + required_error: "마감일을 선택해주세요", + }), + picCode: z.string().min(1, "구매 담당자 코드를 입력해주세요"), + picName: z.string().optional(), + engPicName: z.string().optional(), + packageNo: z.string().min(1, "패키지 번호를 입력해주세요"), + packageName: z.string().min(1, "패키지명을 입력해주세요"), + remark: z.string().optional(), + projectCompany: z.string().optional(), + projectFlag: z.string().optional(), + projectSite: z.string().optional(), +}) + +type CreateRfqFormValues = z.infer<typeof createRfqSchema> + +interface CreateRfqDialogProps { + onSuccess?: () => void; +} + +export function CreateRfqDialog({ onSuccess }: CreateRfqDialogProps) { + const [open, setOpen] = React.useState(false) + const [isLoading, setIsLoading] = React.useState(false) + const [previewCode, setPreviewCode] = React.useState<string>("") + const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) + const router = useRouter() + const { data: session } = useSession() + + const userId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null; + }, [session]); + + const form = useForm<CreateRfqFormValues>({ + resolver: zodResolver(createRfqSchema), + defaultValues: { + projectId: undefined, + dueDate: undefined, + picCode: "", + picName: "", + engPicName: "", + packageNo: "", + packageName: "", + remark: "", + projectCompany: "", + projectFlag: "", + projectSite: "", + }, + }) + + // picCode 변경 시 미리보기 업데이트 + const watchedPicCode = form.watch("picCode") + + React.useEffect(() => { + if (watchedPicCode && watchedPicCode.length > 0) { + setIsLoadingPreview(true) + const timer = setTimeout(async () => { + try { + const preview = await previewNextRfqCode(watchedPicCode) + setPreviewCode(preview) + } catch (error) { + console.error("미리보기 오류:", error) + setPreviewCode("") + } finally { + setIsLoadingPreview(false) + } + }, 500) // 500ms 디바운스 + + return () => clearTimeout(timer) + } else { + setPreviewCode("") + } + }, [watchedPicCode]) + + // 다이얼로그 열림/닫힘 처리 및 폼 리셋 + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen) + + // 다이얼로그가 닫힐 때 폼과 상태 초기화 + if (!newOpen) { + form.reset() + setPreviewCode("") + setIsLoadingPreview(false) + } + } + + const handleCancel = () => { + form.reset() + setOpen(false) + } + + + const onSubmit = async (data: CreateRfqFormValues) => { + if (!userId) { + toast.error("로그인이 필요합니다") + return + } + + setIsLoading(true) + + try { + // 서버 액션 호출 - Date 객체를 직접 전달 + const result = await createRfqAction({ + projectId: data.projectId, // 이제 항상 값이 있음 + dueDate: data.dueDate, // Date 객체 직접 전달 + picCode: data.picCode, + picName: data.picName || "", + engPicName: data.engPicName || "", + packageNo: data.packageNo, + packageName: data.packageName, + remark: data.remark || "", + projectCompany: data.projectCompany || "", + projectFlag: data.projectFlag || "", + projectSite: data.projectSite || "", + createdBy: userId, + updatedBy: userId, + }) + + if (result.success) { + toast.success(result.message, { + description: `RFQ 코드: ${result.data?.rfqCode}`, + }) + + // 다이얼로그 닫기 (handleOpenChange에서 리셋 처리됨) + setOpen(false) + + // 성공 콜백 실행 + if (onSuccess) { + onSuccess() + } + + } else { + toast.error(result.error || "RFQ 생성에 실패했습니다") + } + + } catch (error) { + console.error('RFQ 생성 오류:', error) + toast.error("RFQ 생성에 실패했습니다", { + description: "알 수 없는 오류가 발생했습니다", + }) + } finally { + setIsLoading(false) + } + } + + const handleProjectSelect = (project: Project | null) => { + if (project === null) { + form.setValue("projectId", undefined as any); // 타입 에러 방지 + return; + } + form.setValue("projectId", project.id); + }; + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + <Button size="sm" variant="outline"> + <Plus className="mr-2 h-4 w-4" /> + 새 RFQ + </Button> + </DialogTrigger> + <DialogContent className="max-w-3xl h-[90vh] flex flex-col"> + {/* 고정된 헤더 */} + <DialogHeader className="flex-shrink-0"> + <DialogTitle>새 RFQ 생성</DialogTitle> + <DialogDescription> + 새로운 RFQ를 생성합니다. 필수 정보를 입력해주세요. + </DialogDescription> + </DialogHeader> + + {/* 스크롤 가능한 컨텐츠 영역 */} + <div className="flex-1 overflow-y-auto px-1"> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-2"> + + {/* 프로젝트 선택 (필수) */} + <FormField + control={form.control} + name="projectId" + render={({ field }) => ( + <FormItem> + <FormLabel> + 프로젝트 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <ProjectSelector + selectedProjectId={field.value} + onProjectSelect={handleProjectSelect} + placeholder="프로젝트 선택..." + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 마감일 (필수) */} + <FormField + control={form.control} + name="dueDate" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel> + 마감일 <span className="text-red-500">*</span> + </FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + className={cn( + "w-full pl-3 text-left font-normal", + !field.value && "text-muted-foreground" + )} + > + {field.value ? ( + format(field.value, "yyyy-MM-dd") + ) : ( + <span>마감일을 선택하세요</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => + date < new Date() || date < new Date("1900-01-01") + } + initialFocus + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {/* 구매 담당자 코드 (필수) + 미리보기 */} + <FormField + control={form.control} + name="picCode" + render={({ field }) => ( + <FormItem> + <FormLabel> + 구매 담당자 코드 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <div className="space-y-2"> + <Input + placeholder="예: P001, P002, MGR01 등" + {...field} + /> + {/* RFQ 코드 미리보기 */} + {previewCode && ( + <div className="flex items-center gap-2 p-2 bg-muted rounded-md"> + <Eye className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm text-muted-foreground"> + 생성될 RFQ 코드: + </span> + <Badge variant="outline" className="font-mono"> + {isLoadingPreview ? "생성 중..." : previewCode} + </Badge> + </div> + )} + </div> + </FormControl> + <FormDescription> + RFQ 코드는 N + 담당자코드 + 시리얼번호(5자리) 형식으로 자동 생성됩니다 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 담당자 정보 (두 개 나란히) */} + <div className="space-y-3"> + <h4 className="text-sm font-medium">담당자 정보</h4> + <div className="grid grid-cols-2 gap-4"> + {/* 구매 담당자 */} + <FormField + control={form.control} + name="picName" + render={({ field }) => ( + <FormItem> + <FormLabel>구매 담당자명</FormLabel> + <FormControl> + <Input + placeholder="구매 담당자명" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 설계 담당자 */} + <FormField + control={form.control} + name="engPicName" + render={({ field }) => ( + <FormItem> + <FormLabel>설계 담당자명</FormLabel> + <FormControl> + <Input + placeholder="설계 담당자명" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + + {/* 패키지 정보 (두 개 나란히) - 필수 */} + <div className="space-y-3"> + <h4 className="text-sm font-medium">패키지 정보</h4> + <div className="grid grid-cols-2 gap-4"> + {/* 패키지 번호 (필수) */} + <FormField + control={form.control} + name="packageNo" + render={({ field }) => ( + <FormItem> + <FormLabel> + 패키지 번호 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="패키지 번호" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 패키지명 (필수) */} + <FormField + control={form.control} + name="packageName" + render={({ field }) => ( + <FormItem> + <FormLabel> + 패키지명 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="패키지명" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + + {/* 프로젝트 상세 정보 */} + <div className="space-y-3"> + <h4 className="text-sm font-medium">프로젝트 상세 정보</h4> + <div className="grid grid-cols-1 gap-3"> + <FormField + control={form.control} + name="projectCompany" + render={({ field }) => ( + <FormItem> + <FormLabel>프로젝트 회사</FormLabel> + <FormControl> + <Input + placeholder="프로젝트 회사명" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <div className="grid grid-cols-2 gap-3"> + <FormField + control={form.control} + name="projectFlag" + render={({ field }) => ( + <FormItem> + <FormLabel>프로젝트 플래그</FormLabel> + <FormControl> + <Input + placeholder="프로젝트 플래그" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="projectSite" + render={({ field }) => ( + <FormItem> + <FormLabel>프로젝트 사이트</FormLabel> + <FormControl> + <Input + placeholder="프로젝트 사이트" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + </div> + + {/* 비고 */} + <FormField + control={form.control} + name="remark" + render={({ field }) => ( + <FormItem> + <FormLabel>비고</FormLabel> + <FormControl> + <Textarea + placeholder="추가 비고사항을 입력하세요" + className="resize-none" + rows={3} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </form> + </Form> + </div> + + {/* 고정된 푸터 */} + <DialogFooter className="flex-shrink-0"> + <Button + type="button" + variant="outline" + onClick={handleCancel} + disabled={isLoading} + > + 취소 + </Button> + <Button + type="submit" + onClick={form.handleSubmit(onSubmit)} + disabled={isLoading} + > + {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {isLoading ? "생성 중..." : "RFQ 생성"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
