From 9ceed79cf32c896f8a998399bf1b296506b2cd4a Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 8 Apr 2025 03:08:19 +0000 Subject: 로그인 및 미들웨어 처리. 구조 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfqs/table/BudgetaryRfqSelector.tsx | 261 --------------------------- lib/rfqs/table/ParentRfqSelector.tsx | 307 ++++++++++++++++++++++++++++++++ lib/rfqs/table/add-rfq-dialog.tsx | 227 ++++++++++++++++------- lib/rfqs/table/rfqs-table.tsx | 1 - lib/rfqs/table/update-rfq-sheet.tsx | 169 +++++++++++++++--- 5 files changed, 614 insertions(+), 351 deletions(-) delete mode 100644 lib/rfqs/table/BudgetaryRfqSelector.tsx create mode 100644 lib/rfqs/table/ParentRfqSelector.tsx (limited to 'lib/rfqs/table') diff --git a/lib/rfqs/table/BudgetaryRfqSelector.tsx b/lib/rfqs/table/BudgetaryRfqSelector.tsx deleted file mode 100644 index cea53c1d..00000000 --- a/lib/rfqs/table/BudgetaryRfqSelector.tsx +++ /dev/null @@ -1,261 +0,0 @@ -"use client" - -import * as React from "react" -import { Check, ChevronsUpDown, Loader } from "lucide-react" -import { Button } from "@/components/ui/button" -import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" -import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" -import { cn } from "@/lib/utils" -import { useDebounce } from "@/hooks/use-debounce" -import { getBudgetaryRfqs, type BudgetaryRfq } from "../service" - -interface BudgetaryRfqSelectorProps { - selectedRfqId?: number; - onRfqSelect: (rfq: BudgetaryRfq | null) => void; - placeholder?: string; -} - -export function BudgetaryRfqSelector({ - selectedRfqId, - onRfqSelect, - placeholder = "Budgetary RFQ 선택..." -}: BudgetaryRfqSelectorProps) { - const [searchTerm, setSearchTerm] = React.useState(""); - const debouncedSearchTerm = useDebounce(searchTerm, 300); - - const [open, setOpen] = React.useState(false); - const [loading, setLoading] = React.useState(false); - const [budgetaryRfqs, setBudgetaryRfqs] = React.useState([]); - const [selectedRfq, setSelectedRfq] = React.useState(null); - const [page, setPage] = React.useState(1); - const [hasMore, setHasMore] = React.useState(true); - const [totalCount, setTotalCount] = React.useState(0); - - const listRef = React.useRef(null); - - // 초기 선택된 RFQ가 있을 경우 로드 - React.useEffect(() => { - if (selectedRfqId && open) { - const loadSelectedRfq = async () => { - try { - const result = await getBudgetaryRfqs({ - limit: 1, - // null을 undefined로 변환하여 타입 오류 해결 - projectId: selectedRfq?.projectId ?? undefined - }); - - if ('rfqs' in result && result.rfqs) { - // 옵셔널 체이닝 또는 조건부 검사로 undefined 체크 - const foundRfq = result.rfqs.find(rfq => rfq.id === selectedRfqId); - if (foundRfq) { - setSelectedRfq(foundRfq); - } - } - } catch (error) { - console.error("선택된 RFQ 로드 오류:", error); - } - }; - - if (!selectedRfq || selectedRfq.id !== selectedRfqId) { - loadSelectedRfq(); - } - } - }, [selectedRfqId, open, selectedRfq]); - - // 검색어 변경 시 데이터 리셋 및 재로드 - React.useEffect(() => { - if (open) { - setPage(1); - setHasMore(true); - setBudgetaryRfqs([]); - loadBudgetaryRfqs(1, true); - } - }, [debouncedSearchTerm, open]); - - // 데이터 로드 함수 - const loadBudgetaryRfqs = async (pageToLoad: number, reset = false) => { - if (!open) return; - - setLoading(true); - try { - const limit = 20; // 한 번에 로드할 항목 수 - const result = await getBudgetaryRfqs({ - search: debouncedSearchTerm, - limit, - offset: (pageToLoad - 1) * limit, - }); - - if ('rfqs' in result && result.rfqs) { - if (reset) { - setBudgetaryRfqs(result.rfqs); - } else { - setBudgetaryRfqs(prev => [...prev, ...result.rfqs]); - } - - setTotalCount(result.totalCount); - setHasMore(result.rfqs.length === limit && (pageToLoad * limit) < result.totalCount); - setPage(pageToLoad); - } - } catch (error) { - console.error("Budgetary RFQs 로드 오류:", error); - } finally { - setLoading(false); - } - }; - - // 무한 스크롤 처리 - const handleScroll = () => { - if (listRef.current) { - const { scrollTop, scrollHeight, clientHeight } = listRef.current; - - // 스크롤이 90% 이상 내려갔을 때 다음 페이지 로드 - if (scrollTop + clientHeight >= scrollHeight * 0.9 && !loading && hasMore) { - loadBudgetaryRfqs(page + 1); - } - } - }; - - // RFQ를 프로젝트별로 그룹화하는 함수 - const groupRfqsByProject = (rfqs: BudgetaryRfq[]) => { - const groups: Record = {}; - - // 'No Project' 그룹 기본 생성 - groups['no-project'] = { - projectId: null, - projectCode: null, - projectName: null, - rfqs: [] - }; - - // 프로젝트별로 RFQ 그룹화 - rfqs.forEach(rfq => { - const key = rfq.projectId ? `project-${rfq.projectId}` : 'no-project'; - - if (!groups[key] && rfq.projectId) { - groups[key] = { - projectId: rfq.projectId, - projectCode: rfq.projectCode, - projectName: rfq.projectName, - rfqs: [] - }; - } - - groups[key].rfqs.push(rfq); - }); - - // 필터링된 결과가 있는 그룹만 남기기 - return Object.values(groups).filter(group => group.rfqs.length > 0); - }; - - // 그룹화된 RFQ 목록 - const groupedRfqs = React.useMemo(() => { - return groupRfqsByProject(budgetaryRfqs); - }, [budgetaryRfqs]); - - // RFQ 선택 처리 - const handleRfqSelect = (rfq: BudgetaryRfq | null) => { - setSelectedRfq(rfq); - onRfqSelect(rfq); - setOpen(false); - }; - - return ( - - - - - - - - - 검색 결과가 없습니다 - - - handleRfqSelect(null)} - > - - 선택 안함 - - - - {groupedRfqs.map((group, index) => ( - - {group.rfqs.map((rfq) => ( - handleRfqSelect(rfq)} - > - - {rfq.rfqCode || ""} - - - {rfq.description || ""} - - - ))} - - ))} - - {loading && ( -
- -
- )} - - {!loading && !hasMore && budgetaryRfqs.length > 0 && ( -
- 총 {totalCount}개 중 {budgetaryRfqs.length}개 표시됨 -
- )} -
-
-
-
- ); -} \ No newline at end of file diff --git a/lib/rfqs/table/ParentRfqSelector.tsx b/lib/rfqs/table/ParentRfqSelector.tsx new file mode 100644 index 00000000..0edb1233 --- /dev/null +++ b/lib/rfqs/table/ParentRfqSelector.tsx @@ -0,0 +1,307 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown, Loader } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" +import { cn } from "@/lib/utils" +import { useDebounce } from "@/hooks/use-debounce" +import { getBudgetaryRfqs, type BudgetaryRfq } from "../service" +import { RfqType } from "../validations" + +// ParentRfq 타입 정의 (서비스의 BudgetaryRfq와 호환되어야 함) +interface ParentRfq { + id: number; + rfqCode: string; + description: string | null; + rfqType: RfqType; + projectId: number | null; + projectCode: string | null; + projectName: string | null; +} + +interface ParentRfqSelectorProps { + selectedRfqId?: number; + onRfqSelect: (rfq: ParentRfq | null) => void; + rfqType: RfqType; // 현재 생성 중인 RFQ 타입 + parentRfqTypes: RfqType[]; // 선택 가능한 부모 RFQ 타입 목록 + placeholder?: string; +} + +export function ParentRfqSelector({ + selectedRfqId, + onRfqSelect, + rfqType, + parentRfqTypes, + placeholder = "부모 RFQ 선택..." +}: ParentRfqSelectorProps) { + const [searchTerm, setSearchTerm] = React.useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 300); + + const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [parentRfqs, setParentRfqs] = React.useState([]); + const [selectedRfq, setSelectedRfq] = React.useState(null); + const [page, setPage] = React.useState(1); + const [hasMore, setHasMore] = React.useState(true); + const [totalCount, setTotalCount] = React.useState(0); + + const listRef = React.useRef(null); + + // 타입별로 적절한 검색 placeholder 생성 + const getSearchPlaceholder = () => { + if (rfqType === RfqType.PURCHASE) { + return "BUDGETARY/PURCHASE_BUDGETARY RFQ 검색..."; + } else if (rfqType === RfqType.PURCHASE_BUDGETARY) { + return "BUDGETARY RFQ 검색..."; + } + return "RFQ 코드/설명/프로젝트 검색..."; + }; + + // 초기 선택된 RFQ가 있을 경우 로드 + React.useEffect(() => { + if (selectedRfqId && open) { + const loadSelectedRfq = async () => { + try { + // 단일 RFQ를 id로 조회하는 API 호출 + const result = await getBudgetaryRfqs({ + limit: 1, + rfqId: selectedRfqId + }); + + if ('rfqs' in result && result.rfqs && result.rfqs.length > 0) { + setSelectedRfq(result.rfqs[0] as unknown as ParentRfq); + } + } catch (error) { + console.error("선택된 RFQ 로드 오류:", error); + } + }; + + if (!selectedRfq || selectedRfq.id !== selectedRfqId) { + loadSelectedRfq(); + } + } + }, [selectedRfqId, open, selectedRfq]); + + // 검색어 변경 시 데이터 리셋 및 재로드 + React.useEffect(() => { + if (open) { + setPage(1); + setHasMore(true); + setParentRfqs([]); + loadParentRfqs(1, true); + } + }, [debouncedSearchTerm, open, parentRfqTypes]); + + // 데이터 로드 함수 + const loadParentRfqs = async (pageToLoad: number, reset = false) => { + if (!open || parentRfqTypes.length === 0) return; + + setLoading(true); + try { + const limit = 20; // 한 번에 로드할 항목 수 + const result = await getBudgetaryRfqs({ + search: debouncedSearchTerm, + limit, + offset: (pageToLoad - 1) * limit, + rfqTypes: parentRfqTypes // 현재 RFQ 타입에 맞는 부모 RFQ 타입들로 필터링 + }); + + if ('rfqs' in result && result.rfqs) { + if (reset) { + setParentRfqs(result.rfqs as unknown as ParentRfq[]); + } else { + setParentRfqs(prev => [...prev, ...(result.rfqs as unknown as ParentRfq[])]); + } + + setTotalCount(result.totalCount); + setHasMore(result.rfqs.length === limit && (pageToLoad * limit) < result.totalCount); + setPage(pageToLoad); + } + } catch (error) { + console.error("부모 RFQ 로드 오류:", error); + } finally { + setLoading(false); + } + }; + + // 무한 스크롤 처리 + const handleScroll = () => { + if (listRef.current) { + const { scrollTop, scrollHeight, clientHeight } = listRef.current; + + // 스크롤이 90% 이상 내려갔을 때 다음 페이지 로드 + if (scrollTop + clientHeight >= scrollHeight * 0.9 && !loading && hasMore) { + loadParentRfqs(page + 1); + } + } + }; + + // RFQ를 프로젝트별로 그룹화하는 함수 + const groupRfqsByProject = (rfqs: ParentRfq[]) => { + const groups: Record = {}; + + // 'No Project' 그룹 기본 생성 + groups['no-project'] = { + projectId: null, + projectCode: null, + projectName: null, + rfqs: [] + }; + + // 프로젝트별로 RFQ 그룹화 + rfqs.forEach(rfq => { + const key = rfq.projectId ? `project-${rfq.projectId}` : 'no-project'; + + if (!groups[key] && rfq.projectId) { + groups[key] = { + projectId: rfq.projectId, + projectCode: rfq.projectCode, + projectName: rfq.projectName, + rfqs: [] + }; + } + + groups[key].rfqs.push(rfq); + }); + + // 필터링된 결과가 있는 그룹만 남기기 + return Object.values(groups).filter(group => group.rfqs.length > 0); + }; + + // 그룹화된 RFQ 목록 + const groupedRfqs = React.useMemo(() => { + return groupRfqsByProject(parentRfqs); + }, [parentRfqs]); + + // RFQ 선택 처리 + const handleRfqSelect = (rfq: ParentRfq | null) => { + setSelectedRfq(rfq); + onRfqSelect(rfq); + setOpen(false); + }; + + // RFQ 타입에 따른 표시 형식 + const getRfqTypeLabel = (type: RfqType) => { + switch(type) { + case RfqType.BUDGETARY: + return "BUDGETARY"; + case RfqType.PURCHASE_BUDGETARY: + return "PURCHASE_BUDGETARY"; + case RfqType.PURCHASE: + return "PURCHASE"; + default: + return type; + } + }; + + return ( + + + + + + + + + 검색 결과가 없습니다 + + + handleRfqSelect(null)} + > + + 선택 안함 + + + + {groupedRfqs.map((group, index) => ( + + {group.rfqs.map((rfq) => ( + handleRfqSelect(rfq)} + > + +
+
+ {rfq.rfqCode || ""} + + {getRfqTypeLabel(rfq.rfqType)} + +
+ {rfq.description && ( + + {rfq.description} + + )} +
+
+ ))} +
+ ))} + + {loading && ( +
+ +
+ )} + + {!loading && !hasMore && parentRfqs.length > 0 && ( +
+ 총 {totalCount}개 중 {parentRfqs.length}개 표시됨 +
+ )} +
+
+
+
+ ); +} \ No newline at end of file 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([]) - const [isLoadingBudgetary, setIsLoadingBudgetary] = React.useState(false) - const [budgetarySearchOpen, setBudgetarySearchOpen] = React.useState(false) - const [budgetarySearchTerm, setBudgetarySearchTerm] = React.useState("") - const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState(null) + const [parentRfqs, setParentRfqs] = React.useState([]) + const [isLoadingParents, setIsLoadingParents] = React.useState(false) + const [selectedParentRfq, setSelectedParentRfq] = React.useState(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 ; } + // 타입에 따라 부모 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 ( {/* 모달을 열기 위한 버튼 */} @@ -197,6 +268,9 @@ export function AddRfqDialog({ rfqType = RfqType.PURCHASE }: AddRfqDialogProps) Create New {getTitle()} 새 {getTitle()} 정보를 입력하고 Create 버튼을 누르세요. +
+ {getTypeDescription()} +
@@ -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 && ( ( - Budgetary RFQ (Optional) + {getParentRfqSelectorLabel()} - { - 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 선택..." + } /> +
+ {getParentRfqDescription()} +
)} /> )} - {/* rfqCode */} + {/* rfqCode - 자동 생성되고 읽기 전용 */} RFQ Code - +
+ + {isLoadingRfqCode && ( +
+
+
+ )} +
+
+ RFQ 타입과 현재 날짜를 기준으로 자동 생성됩니다 +
)} diff --git a/lib/rfqs/table/rfqs-table.tsx b/lib/rfqs/table/rfqs-table.tsx index 48c04930..e4ff47d8 100644 --- a/lib/rfqs/table/rfqs-table.tsx +++ b/lib/rfqs/table/rfqs-table.tsx @@ -231,7 +231,6 @@ export function RfqsTable({ promises, rfqType = RfqType.PURCHASE }: RfqsTablePro open={rowAction?.type === "update"} onOpenChange={() => setRowAction(null)} rfq={rowAction?.row.original ?? null} - rfqType={rfqType} /> { rfq: RfqWithItemCount | null - rfqType?: RfqType; } - -interface BudgetaryRfq { +// 부모 RFQ 정보 타입 정의 +interface ParentRfq { id: number; rfqCode: string; description: string | null; + rfqType: RfqType; + projectId: number | null; + projectCode: string | null; + projectName: string | null; } - -export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: UpdateRfqSheetProps) { +export function UpdateRfqSheet({ rfq, ...props }: UpdateRfqSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const { data: session } = useSession() const userId = Number(session?.user?.id || 1) - const [selectedBudgetaryRfq, setSelectedBudgetaryRfq] = React.useState(null) + const [selectedParentRfq, setSelectedParentRfq] = React.useState(null) + + // RFQ의 타입 가져오기 + const rfqType = rfq?.rfqType || RfqType.PURCHASE; + + // 초기 부모 RFQ ID 가져오기 + const initialParentRfqId = rfq?.parentRfqId; + + // 현재 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 []; + } + }; + + // 부모 RFQ 타입들 + const parentRfqTypes = getParentRfqTypes(); + + // 부모 RFQ를 보여줄지 결정 + const shouldShowParentRfqSelector = rfqType === RfqType.PURCHASE || rfqType === RfqType.PURCHASE_BUDGETARY; + + // 타입에 따른 타이틀 생성 + const getTypeTitle = () => { + 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"; + } + }; + + // 타입 설명 가져오기 + const getTypeDescription = () => { + switch(rfqType) { + case RfqType.PURCHASE: + return "실제 구매 발주 전에 가격을 요청"; + case RfqType.BUDGETARY: + return "기술영업 단계에서 입찰가 산정을 위한 견적 요청"; + case RfqType.PURCHASE_BUDGETARY: + return "프로젝트 수주 후, 공식 입찰 전 예산 책정을 위한 가격 요청"; + default: + return ""; + } + }; + // 부모 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 ""; + }; + + // 초기 부모 RFQ 로드 + React.useEffect(() => { + if (initialParentRfqId && shouldShowParentRfqSelector) { + const loadInitialParentRfq = async () => { + try { + const result = await getBudgetaryRfqs({ + rfqId: initialParentRfqId + }); + + if ('rfqs' in result && result.rfqs && result.rfqs.length > 0) { + setSelectedParentRfq(result.rfqs[0] as unknown as ParentRfq); + } + } catch (error) { + console.error("부모 RFQ 로드 오류:", error); + } + }; + + loadInitialParentRfq(); + } + }, [initialParentRfqId, shouldShowParentRfqSelector]); + // RHF setup const form = useForm({ resolver: zodResolver(updateRfqSchema), @@ -70,6 +166,7 @@ export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: Up rfqCode: rfq?.rfqCode ?? "", description: rfq?.description ?? "", projectId: rfq?.projectId, // 프로젝트 ID + parentRfqId: rfq?.parentRfqId, // 부모 RFQ ID dueDate: rfq?.dueDate ?? undefined, // null을 undefined로 변환 status: rfq?.status ?? "DRAFT", createdBy: rfq?.createdBy ?? userId, @@ -77,16 +174,27 @@ export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: Up }); // 프로젝트 선택 처리 - const handleProjectSelect = (project: Project) => { + const handleProjectSelect = (project: Project | null) => { + if (project === null) { + return; + } form.setValue("projectId", project.id); }; + // 부모 RFQ 선택 처리 + const handleParentRfqSelect = (rfq: ParentRfq | null) => { + setSelectedParentRfq(rfq); + form.setValue("parentRfqId", rfq?.id); + }; + async function onSubmit(input: UpdateRfqSchema) { startUpdateTransition(async () => { if (!rfq) return const { error } = await modifyRfq({ ...input, + rfqType: rfqType as RfqType, + }) if (error) { @@ -104,9 +212,12 @@ export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: Up - Update RFQ + Update {getTypeTitle()} - Update the RFQ details and save the changes + Update the {getTypeTitle()} details and save the changes +
+ {getTypeDescription()} +
@@ -122,6 +233,15 @@ export function UpdateRfqSheet({ rfq,rfqType = RfqType.PURCHASE , ...props }: Up )} /> + + {/* Hidden rfqType field */} + {/* ( + + )} + /> */} {/* Project Selector - 재사용 컴포넌트 사용 */} - {/* Budgetary RFQ Selector - 구매용 RFQ 생성 시에만 표시 */} - {rfqType === RfqType.PURCHASE && ( + {/* Parent RFQ Selector - PURCHASE 또는 PURCHASE_BUDGETARY 타입일 때만 표시 */} + {shouldShowParentRfqSelector && ( ( - Budgetary RFQ (Optional) + {getParentRfqSelectorLabel()} - { - setSelectedBudgetaryRfq(rfq as any); - form.setValue("parentRfqId", rfq?.id); - }} - placeholder="Budgetary RFQ 선택..." + onRfqSelect={handleParentRfqSelect} + rfqType={rfqType} + parentRfqTypes={parentRfqTypes} + placeholder={ + rfqType === RfqType.PURCHASE + ? "BUDGETARY 또는 PURCHASE_BUDGETARY RFQ 선택..." + : "BUDGETARY RFQ 선택..." + } /> +
+ {getParentRfqDescription()} +
)} /> )} - {/* rfqCode */} - - {/* dueDate (type="date") */}