From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 25 Mar 2025 15:55:45 +0900 Subject: initial commit --- lib/rfqs/table/BudgetaryRfqSelector.tsx | 261 ++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 lib/rfqs/table/BudgetaryRfqSelector.tsx (limited to 'lib/rfqs/table/BudgetaryRfqSelector.tsx') diff --git a/lib/rfqs/table/BudgetaryRfqSelector.tsx b/lib/rfqs/table/BudgetaryRfqSelector.tsx new file mode 100644 index 00000000..cea53c1d --- /dev/null +++ b/lib/rfqs/table/BudgetaryRfqSelector.tsx @@ -0,0 +1,261 @@ +"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 -- cgit v1.2.3