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/ParentRfqSelector.tsx | |
| parent | 230ce796836c25df26c130dbcd616ef97d12b2ec (diff) | |
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/rfqs/table/ParentRfqSelector.tsx')
| -rw-r--r-- | lib/rfqs/table/ParentRfqSelector.tsx | 307 |
1 files changed, 307 insertions, 0 deletions
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<ParentRfq[]>([]); + const [selectedRfq, setSelectedRfq] = React.useState<ParentRfq | null>(null); + const [page, setPage] = React.useState(1); + const [hasMore, setHasMore] = React.useState(true); + const [totalCount, setTotalCount] = React.useState(0); + + const listRef = React.useRef<HTMLDivElement>(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<string, { + projectId: number | null; + projectCode: string | null; + projectName: string | null; + rfqs: ParentRfq[]; + }> = {}; + + // '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 ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={open} + className="w-full justify-between" + > + {selectedRfq + ? `${selectedRfq.rfqCode || ""} - ${selectedRfq.description || ""}` + : placeholder} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[400px] p-0"> + <Command> + <CommandInput + placeholder={getSearchPlaceholder()} + value={searchTerm} + onValueChange={setSearchTerm} + /> + <CommandList + className="max-h-[300px]" + ref={listRef} + onScroll={handleScroll} + > + <CommandEmpty>검색 결과가 없습니다</CommandEmpty> + + <CommandGroup> + <CommandItem + value="none" + onSelect={() => handleRfqSelect(null)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + !selectedRfq + ? "opacity-100" + : "opacity-0" + )} + /> + <span className="font-medium">선택 안함</span> + </CommandItem> + </CommandGroup> + + {groupedRfqs.map((group, index) => ( + <CommandGroup + key={`group-${group.projectId || index}`} + heading={ + group.projectId + ? `${group.projectCode || ""} - ${group.projectName || ""}` + : "프로젝트 없음" + } + > + {group.rfqs.map((rfq) => ( + <CommandItem + key={rfq.id} + value={`${rfq.rfqCode || ""} ${rfq.description || ""}`} + onSelect={() => handleRfqSelect(rfq)} + > + <Check + className={cn( + "mr-2 h-4 w-4", + selectedRfq?.id === rfq.id + ? "opacity-100" + : "opacity-0" + )} + /> + <div className="flex flex-col"> + <div className="flex items-center"> + <span className="font-medium">{rfq.rfqCode || ""}</span> + <span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-slate-100 text-slate-700"> + {getRfqTypeLabel(rfq.rfqType)} + </span> + </div> + {rfq.description && ( + <span className="text-sm text-gray-500 truncate"> + {rfq.description} + </span> + )} + </div> + </CommandItem> + ))} + </CommandGroup> + ))} + + {loading && ( + <div className="py-2 text-center"> + <Loader className="h-4 w-4 animate-spin mx-auto" /> + </div> + )} + + {!loading && !hasMore && parentRfqs.length > 0 && ( + <div className="py-2 text-center text-sm text-muted-foreground"> + 총 {totalCount}개 중 {parentRfqs.length}개 표시됨 + </div> + )} + </CommandList> + </Command> + </PopoverContent> + </Popover> + ); +}
\ No newline at end of file |
