diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
| commit | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch) | |
| tree | f84750fa6cac954d5e31221fc47a54c655fc06a9 /lib/rfqs/table/add-rfq-dialog.tsx | |
| parent | 230ce796836c25df26c130dbcd616ef97d12b2ec (diff) | |
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/rfqs/table/add-rfq-dialog.tsx')
| -rw-r--r-- | lib/rfqs/table/add-rfq-dialog.tsx | 227 |
1 files changed, 161 insertions, 66 deletions
diff --git a/lib/rfqs/table/add-rfq-dialog.tsx b/lib/rfqs/table/add-rfq-dialog.tsx index 45390cd0..41055608 100644 --- a/lib/rfqs/table/add-rfq-dialog.tsx +++ b/lib/rfqs/table/add-rfq-dialog.tsx @@ -3,38 +3,29 @@ import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" -import { Check, ChevronsUpDown } from "lucide-react" import { toast } from "sonner" import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" -import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" import { useSession } from "next-auth/react" import { createRfqSchema, type CreateRfqSchema, RfqType } from "../validations" -import { createRfq, getBudgetaryRfqs } from "../service" +import { createRfq, generateNextRfqCode, getBudgetaryRfqs } from "../service" import { ProjectSelector } from "@/components/ProjectSelector" import { type Project } from "../service" -import { cn } from "@/lib/utils" -import { BudgetaryRfqSelector } from "./BudgetaryRfqSelector" -import { type BudgetaryRfq as ServiceBudgetaryRfq } from "../service"; +import { ParentRfqSelector } from "./ParentRfqSelector" // 부모 RFQ 정보 타입 정의 -interface BudgetaryRfq { +interface ParentRfq { id: number; rfqCode: string; description: string | null; + rfqType: RfqType; + projectId: number | null; + projectCode: string | null; + projectName: string | null; } interface AddRfqDialogProps { @@ -44,11 +35,10 @@ interface AddRfqDialogProps { export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) { const [open, setOpen] = React.useState(false) const { data: session, status } = useSession() - const [budgetaryRfqs, setBudgetaryRfqs] = React.useState<BudgetaryRfq[]>([]) - const [isLoadingBudgetary, setIsLoadingBudgetary] = React.useState(false) - const [budgetarySearchOpen, setBudgetarySearchOpen] = React.useState(false) - const [budgetarySearchTerm, setBudgetarySearchTerm] = React.useState("") - const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState<BudgetaryRfq | null>(null) + const [parentRfqs, setParentRfqs] = React.useState<ParentRfq[]>([]) + const [isLoadingParents, setIsLoadingParents] = React.useState(false) + const [selectedParentRfq, setSelectedParentRfq] = React.useState<ParentRfq | null>(null) + const [isLoadingRfqCode, setIsLoadingRfqCode] = React.useState(false) // Get the user ID safely, ensuring it's a valid number const userId = React.useMemo(() => { @@ -64,9 +54,30 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) // RfqType에 따른 타이틀 생성 const getTitle = () => { - return rfqType === RfqType.PURCHASE - ? "Purchase RFQ" - : "Budgetary RFQ"; + switch(rfqType) { + case RfqType.PURCHASE: + return "Purchase RFQ"; + case RfqType.BUDGETARY: + return "Budgetary RFQ"; + case RfqType.PURCHASE_BUDGETARY: + return "Purchase Budgetary RFQ"; + default: + return "RFQ"; + } + }; + + // RfqType 설명 가져오기 + const getTypeDescription = () => { + switch(rfqType) { + case RfqType.PURCHASE: + return "실제 구매 발주 전에 가격을 요청"; + case RfqType.BUDGETARY: + return "기술영업 단계에서 입찰가 산정을 위한 견적 요청"; + case RfqType.PURCHASE_BUDGETARY: + return "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 가격 요청"; + default: + return ""; + } }; // RHF + Zod @@ -92,40 +103,79 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) } }, [status, userId, form]); - // Budgetary RFQ 목록 로드 (Purchase RFQ 생성 시만) + // 다이얼로그가 열릴 때 자동으로 RFQ 코드 생성 React.useEffect(() => { - if (rfqType === RfqType.PURCHASE && open) { - const loadBudgetaryRfqs = async () => { - setIsLoadingBudgetary(true); + if (open) { + const generateRfqCode = async () => { + setIsLoadingRfqCode(true); try { - const result = await getBudgetaryRfqs(); - if ('rfqs' in result) { - setBudgetaryRfqs(result.rfqs as unknown as BudgetaryRfq[]); - } else if ('error' in result) { - console.error("Budgetary RFQs 로드 오류:", result.error); + // 서버 액션 호출 + const result = await generateNextRfqCode(rfqType); + + if (result.error) { + toast.error(`RFQ 코드 생성 실패: ${result.error}`); + return; } + + // 생성된 코드를 폼에 설정 + form.setValue("rfqCode", result.code); } catch (error) { - console.error("Budgetary RFQs 로드 오류:", error); + console.error("RFQ 코드 생성 오류:", error); + toast.error("RFQ 코드 생성에 실패했습니다"); } finally { - setIsLoadingBudgetary(false); + setIsLoadingRfqCode(false); } }; - - loadBudgetaryRfqs(); + + generateRfqCode(); } - }, [rfqType, open]); + }, [open, rfqType, form]); + + // 현재 RFQ 타입에 따라 선택 가능한 부모 RFQ 타입들 결정 + const getParentRfqTypes = (): RfqType[] => { + switch(rfqType) { + case RfqType.PURCHASE: + // PURCHASE는 BUDGETARY와 PURCHASE_BUDGETARY를 부모로 가질 수 있음 + return [RfqType.BUDGETARY, RfqType.PURCHASE_BUDGETARY]; + case RfqType.PURCHASE_BUDGETARY: + // PURCHASE_BUDGETARY는 BUDGETARY만 부모로 가질 수 있음 + return [RfqType.BUDGETARY]; + default: + return []; + } + }; - // 검색어로 필터링된 Budgetary RFQ 목록 - const filteredBudgetaryRfqs = React.useMemo(() => { - if (!budgetarySearchTerm.trim()) return budgetaryRfqs; + // 선택 가능한 부모 RFQ 목록 로드 + React.useEffect(() => { + if ((rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_BUDGETARY) && open) { + const loadParentRfqs = async () => { + setIsLoadingParents(true); + try { + // 현재 RFQ 타입에 따라 선택 가능한, 부모가 될 수 있는 RFQ 타입들 가져오기 + const parentTypes = getParentRfqTypes(); + + // 부모 RFQ 타입이 있을 때만 API 호출 + if (parentTypes.length > 0) { + const result = await getBudgetaryRfqs({ + rfqTypes: parentTypes // 서비스에 rfqTypes 파라미터 추가 필요 + }); + + if ('rfqs' in result) { + setParentRfqs(result.rfqs as unknown as ParentRfq[]); + } else if ('error' in result) { + console.error("부모 RFQ 로드 오류:", result.error); + } + } + } catch (error) { + console.error("부모 RFQ 로드 오류:", error); + } finally { + setIsLoadingParents(false); + } + }; - const lowerSearch = budgetarySearchTerm.toLowerCase(); - return budgetaryRfqs.filter( - rfq => - rfq.rfqCode.toLowerCase().includes(lowerSearch) || - (rfq.description && rfq.description.toLowerCase().includes(lowerSearch)) - ); - }, [budgetaryRfqs, budgetarySearchTerm]); + loadParentRfqs(); + } + }, [rfqType, open]); // 프로젝트 선택 처리 const handleProjectSelect = (project: Project | null) => { @@ -136,11 +186,10 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) form.setValue("projectId", project.id); }; - // Budgetary RFQ 선택 처리 - const handleBudgetaryRfqSelect = (rfq: BudgetaryRfq) => { - setSelectedBudgetaryRfq(rfq); - form.setValue("parentRfqId", rfq.id); - setBudgetarySearchOpen(false); + // 부모 RFQ 선택 처리 + const handleParentRfqSelect = (rfq: ParentRfq | null) => { + setSelectedParentRfq(rfq); + form.setValue("parentRfqId", rfq?.id); }; async function onSubmit(data: CreateRfqSchema) { @@ -166,14 +215,14 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) toast.success("RFQ가 성공적으로 생성되었습니다."); form.reset(); - setSelectedBudgetaryRfq(null); + setSelectedParentRfq(null); setOpen(false); } function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset(); - setSelectedBudgetaryRfq(null); + setSelectedParentRfq(null); } setOpen(nextOpen); } @@ -183,6 +232,28 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) return <Button variant="outline" size="sm" disabled>Loading...</Button>; } + // 타입에 따라 부모 RFQ 선택 필드를 보여줄지 결정 + const shouldShowParentRfqSelector = rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_BUDGETARY; + + // 부모 RFQ 선택기 레이블 및 설명 가져오기 + const getParentRfqSelectorLabel = () => { + if (rfqType === RfqType.PURCHASE) { + return "부모 RFQ (BUDGETARY/PURCHASE_BUDGETARY)"; + } else if (rfqType === RfqType.PURCHASE_BUDGETARY) { + return "부모 RFQ (BUDGETARY)"; + } + return "부모 RFQ"; + }; + + const getParentRfqDescription = () => { + if (rfqType === RfqType.PURCHASE) { + return "BUDGETARY 또는 PURCHASE_BUDGETARY 타입의 RFQ를 부모로 선택할 수 있습니다."; + } else if (rfqType === RfqType.PURCHASE_BUDGETARY) { + return "BUDGETARY 타입의 RFQ만 부모로 선택할 수 있습니다."; + } + return ""; + }; + return ( <Dialog open={open} onOpenChange={handleDialogOpenChange}> {/* 모달을 열기 위한 버튼 */} @@ -197,6 +268,9 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) <DialogTitle>Create New {getTitle()}</DialogTitle> <DialogDescription> 새 {getTitle()} 정보를 입력하고 <b>Create</b> 버튼을 누르세요. + <div className="mt-1 text-xs text-muted-foreground"> + {getTypeDescription()} + </div> </DialogDescription> </DialogHeader> @@ -231,31 +305,37 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) )} /> - {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */} - {rfqType === RfqType.PURCHASE && ( + {/* Parent RFQ Selector - PURCHASE 또는 PURCHASE_BUDGETARY 타입일 때만 표시 */} + {shouldShowParentRfqSelector && ( <FormField control={form.control} name="parentRfqId" render={({ field }) => ( <FormItem> - <FormLabel>Budgetary RFQ (Optional)</FormLabel> + <FormLabel>{getParentRfqSelectorLabel()}</FormLabel> <FormControl> - <BudgetaryRfqSelector + <ParentRfqSelector selectedRfqId={field.value as number | undefined} - onRfqSelect={(rfq) => { - setSelectedBudgetaryRfq(rfq as any); - form.setValue("parentRfqId", rfq?.id); - }} - placeholder="Budgetary RFQ 선택..." + onRfqSelect={handleParentRfqSelect} + rfqType={rfqType} + parentRfqTypes={getParentRfqTypes()} + placeholder={ + rfqType === RfqType.PURCHASE + ? "BUDGETARY 또는 PURCHASE_BUDGETARY RFQ 선택..." + : "BUDGETARY RFQ 선택..." + } /> </FormControl> + <div className="text-xs text-muted-foreground mt-1"> + {getParentRfqDescription()} + </div> <FormMessage /> </FormItem> )} /> )} - {/* rfqCode */} + {/* rfqCode - 자동 생성되고 읽기 전용 */} <FormField control={form.control} name="rfqCode" @@ -263,8 +343,23 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) <FormItem> <FormLabel>RFQ Code</FormLabel> <FormControl> - <Input placeholder="e.g. RFQ-2025-001" {...field} /> + <div className="flex"> + <Input + placeholder="자동으로 생성 중..." + {...field} + disabled={true} + className="bg-muted" + /> + {isLoadingRfqCode && ( + <div className="ml-2 flex items-center"> + <div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div> + </div> + )} + </div> </FormControl> + <div className="text-xs text-muted-foreground mt-1"> + RFQ 타입과 현재 날짜를 기준으로 자동 생성됩니다 + </div> <FormMessage /> </FormItem> )} |
