summaryrefslogtreecommitdiff
path: root/lib/rfqs/table/BudgetaryRfqSelector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rfqs/table/BudgetaryRfqSelector.tsx')
-rw-r--r--lib/rfqs/table/BudgetaryRfqSelector.tsx261
1 files changed, 261 insertions, 0 deletions
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<BudgetaryRfq[]>([]);
+ const [selectedRfq, setSelectedRfq] = React.useState<BudgetaryRfq | 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);
+
+ // 초기 선택된 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<string, {
+ projectId: number | null;
+ projectCode: string | null;
+ projectName: string | null;
+ rfqs: BudgetaryRfq[];
+ }> = {};
+
+ // '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 (
+ <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="Budgetary RFQ 코드/설명/프로젝트 검색..."
+ 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"
+ )}
+ />
+ <span className="font-medium">{rfq.rfqCode || ""}</span>
+ <span className="ml-2 text-gray-500 truncate">
+ - {rfq.description || ""}
+ </span>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ ))}
+
+ {loading && (
+ <div className="py-2 text-center">
+ <Loader className="h-4 w-4 animate-spin mx-auto" />
+ </div>
+ )}
+
+ {!loading && !hasMore && budgetaryRfqs.length > 0 && (
+ <div className="py-2 text-center text-sm text-muted-foreground">
+ 총 {totalCount}개 중 {budgetaryRfqs.length}개 표시됨
+ </div>
+ )}
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ );
+} \ No newline at end of file