summaryrefslogtreecommitdiff
path: root/lib/rfqs/table/ParentRfqSelector.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-08 03:08:19 +0000
commit9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch)
treef84750fa6cac954d5e31221fc47a54c655fc06a9 /lib/rfqs/table/ParentRfqSelector.tsx
parent230ce796836c25df26c130dbcd616ef97d12b2ec (diff)
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/rfqs/table/ParentRfqSelector.tsx')
-rw-r--r--lib/rfqs/table/ParentRfqSelector.tsx307
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