diff options
Diffstat (limited to 'components/po-rfq/po-rfq-container.tsx')
| -rw-r--r-- | components/po-rfq/po-rfq-container.tsx | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/components/po-rfq/po-rfq-container.tsx b/components/po-rfq/po-rfq-container.tsx new file mode 100644 index 00000000..e5159242 --- /dev/null +++ b/components/po-rfq/po-rfq-container.tsx @@ -0,0 +1,261 @@ +"use client" + +import { useState, useEffect, useCallback, useRef } from "react" +import { useSearchParams, useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { PanelLeft, PanelLeftClose, PanelLeftOpen } from "lucide-react" + +// shadcn/ui components +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable" + +import { cn } from "@/lib/utils" +import { ProcurementRfqsView } from "@/db/schema" +import { RFQListTable } from "@/lib/procurement-rfqs/table/rfq-table" +import { getPORfqs } from "@/lib/procurement-rfqs/services" +import { RFQFilterSheet } from "./rfq-filter-sheet" +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton" +import { RfqDetailTables } from "./detail-table/rfq-detail-table" + +interface RfqContainerProps { + // 초기 데이터 (필수) + initialData: Awaited<ReturnType<typeof getPORfqs>> + // 서버 액션으로 데이터를 가져오는 함수 + fetchData: (params: any) => Promise<Awaited<ReturnType<typeof getPORfqs>>> +} + +export default function RFQContainer({ + initialData, + fetchData +}: RfqContainerProps) { + const router = useRouter() + const searchParams = useSearchParams() + + // Whether the filter panel is open (now a side panel instead of sheet) + const [isFilterPanelOpen, setIsFilterPanelOpen] = useState(false) + + // 데이터 상태 관리 - 초기 데이터로 시작 + const [data, setData] = useState<Awaited<ReturnType<typeof getPORfqs>>>(initialData) + const [isLoading, setIsLoading] = useState(false) + + // 선택된 문서를 이 state로 관리 + const [selectedRfq, setSelectedRfq] = useState<ProcurementRfqsView | null>(null) + + // 패널 collapse + const [isTopCollapsed, setIsTopCollapsed] = useState(false) + + // 이전 URL 파라미터를 저장하기 위한 ref + const prevParamsRef = useRef<string>(searchParams.toString()) + + // 현재 URL 파라미터로부터 필터 데이터 구성 + const getFilterParams = useCallback(() => { + return { + page: searchParams.get('page') || '1', + perPage: searchParams.get('perPage') || '10', + sort: searchParams.get('sort') || JSON.stringify([{ id: "updatedAt", desc: true }]), + basicFilters: searchParams.get('basicFilters') || null, + basicJoinOperator: searchParams.get('basicJoinOperator') || 'and', + filters: searchParams.get('filters') || null, + joinOperator: searchParams.get('joinOperator') || 'and', + search: searchParams.get('search') || '', + } + }, [searchParams]) + + // 데이터 로드 함수 + const loadData = useCallback(async () => { + try { + setIsLoading(true) + const filterParams = getFilterParams() + + console.log("데이터 로드 시작:", filterParams) + + // 서버 액션으로 데이터 가져오는 함수 + const newData = await fetchData(filterParams) + + console.log("데이터 로드 완료:", newData.data.length, "건") + + setData(newData) + } catch (error) { + console.error("데이터 로드 오류:", error) + } finally { + setIsLoading(false) + } + }, [fetchData, getFilterParams]) + + const refreshData = useCallback(() => { + // 현재 파라미터로 데이터 다시 로드 + loadData(); + }, [loadData]); + + // URL 파라미터 변경 감지 + useEffect(() => { + const currentParams = searchParams.toString() + + // 파라미터가 변경되었을 때만 데이터 로드 + if (currentParams !== prevParamsRef.current) { + console.log("URL 파라미터 변경 감지:", { + previous: prevParamsRef.current, + current: currentParams, + }) + + prevParamsRef.current = currentParams + loadData() + } + }, [searchParams, loadData]) + + // 문서 선택 핸들러 + const handleSelectRfq = (rfq: ProcurementRfqsView | null) => { + setSelectedRfq(rfq) + } + + // 조회 버튼 클릭 핸들러 - RFQFilterSheet에 전달 + // 페이지 리라우팅을 통해 처리하므로 별도 로직 불필요 + const handleSearch = () => { + // Close the panel after search + setIsFilterPanelOpen(false) + } + + const [panelHeight, setPanelHeight] = useState<number>(400) + + // Get active filter count for UI display + const getActiveFilterCount = () => { + try { + const basicFilters = searchParams.get('basicFilters') + return basicFilters ? JSON.parse(basicFilters).length : 0 + } catch (e) { + console.error("Error parsing filters:", e) + return 0 + } + } + + // Filter panel width in pixels + const FILTER_PANEL_WIDTH = 400; + + // Table refresh key - 패널 상태가 변경되면 테이블을 강제로 재렌더링 + const [tableRefreshKey, setTableRefreshKey] = useState(0); + + useEffect(() => { + // 패널 상태가 변경될 때 테이블 강제 재렌더링 + setTableRefreshKey(prev => prev + 1); + }, [isFilterPanelOpen]); + + return ( + <div className="h-[calc(100vh-220px)] w-full overflow-hidden relative"> + {/* Fixed Filter Panel - 가장 왼쪽부터 시작, 전체 높이 맞춤 */} + <div + className={cn( + "fixed left-0 bg-background border-r overflow-hidden flex flex-col transition-all duration-300 ease-in-out z-30", + isFilterPanelOpen ? "border-r" : "border-r-0" + )} + style={{ + width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', + height: 'calc(100vh - 130px)' // 나머지 높이 전체 사용 + }} + > + {/* Filter Content - 제목 포함하여 내부에서 처리 */} + <div className="h-full"> + <RFQFilterSheet + isOpen={isFilterPanelOpen} + onClose={() => setIsFilterPanelOpen(false)} + onSearch={handleSearch} + isLoading={isLoading} + /> + </div> + </div> + + {/* Main Content Panel - 패널이 열릴 때 오른쪽으로 이동 */} + <div + className="h-full overflow-hidden transition-all duration-300 ease-in-out" + style={{ + marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px' + }} + > + {/* Filter Toggle Button - 메인 콘텐츠 상단에 위치 */} + <div className="flex items-center p-4 border-b bg-background pl-0"> + <Button + variant="outline" + size="sm" + type='button' + onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} + className="flex items-center shadow-md" + > + { + isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/> + } + {/* 검색 필터 */} + {getActiveFilterCount() > 0 && ( + <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> + {getActiveFilterCount()} + </span> + )} + </Button> + + {/* 추가적인 헤더 정보나 버튼들을 여기에 배치할 수 있음 */} + <div className="flex-1" /> + <div className="text-sm text-muted-foreground"> + {data && !isLoading && ( + <span>총 {data.total || 0}건</span> + )} + </div> + </div> + + {/* Main Content Area */} + <div className="h-[calc(100%-64px)] w-full overflow-hidden"> + {isLoading ? ( + // 로딩 중 상태 + <DataTableSkeleton + columnCount={6} + searchableColumnCount={1} + filterableColumnCount={2} + cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem", "8rem"]} + shrinkZero + /> + ) : ( + // 데이터 로드 완료 상태 + <ResizablePanelGroup direction="vertical" className="h-full"> + <ResizablePanel + defaultSize={55} + minSize={0} + maxSize={95} + collapsible + collapsedSize={10} + onCollapse={() => setIsTopCollapsed(true)} + onExpand={() => setIsTopCollapsed(false)} + onResize={(size) => { + setPanelHeight(size) + }} + className={cn("overflow-y-auto overflow-x-hidden border-b", isTopCollapsed && "transition-all")} + > + <div className="flex h-full min-h-0 flex-col"> + <RFQListTable + key={tableRefreshKey} // Force re-render when panel toggles + maxHeight={`${panelHeight*0.5}vh`} + data={data} + onSelectRFQ={handleSelectRfq} + onDataRefresh={refreshData} + /> + </div> + </ResizablePanel> + + <ResizableHandle + withHandle + className="pointer-events-none data-[resize-handle]:pointer-events-auto" + /> + + <ResizablePanel + minSize={0} + defaultSize={35} + className="overflow-y-auto overflow-x-hidden" + > + <RfqDetailTables selectedRfq={selectedRfq} /> + </ResizablePanel> + </ResizablePanelGroup> + )} + </div> + </div> + </div> + ) +}
\ No newline at end of file |
