diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-24 11:06:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-24 11:06:32 +0000 |
| commit | 1dc24d48e52f2e490f5603ceb02842586ecae533 (patch) | |
| tree | 8fca2c5b5b52cc10557b5ba6e55b937ae3c57cf6 /lib/evaluation-target-list/table/evaluation-target-table.tsx | |
| parent | ed0d6fcc98f671280c2ccde797b50693da88152e (diff) | |
(대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업 피드백 반영
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-target-table.tsx')
| -rw-r--r-- | lib/evaluation-target-list/table/evaluation-target-table.tsx | 199 |
1 files changed, 153 insertions, 46 deletions
diff --git a/lib/evaluation-target-list/table/evaluation-target-table.tsx b/lib/evaluation-target-list/table/evaluation-target-table.tsx index 5560d3ff..c65a7815 100644 --- a/lib/evaluation-target-list/table/evaluation-target-table.tsx +++ b/lib/evaluation-target-list/table/evaluation-target-table.tsx @@ -1,6 +1,6 @@ // ============================================================================ // components/evaluation-targets-table.tsx (CLIENT COMPONENT) -// ─ 완전본 ─ evaluation-targets-columns.tsx 는 별도 +// ─ 정리된 버전 ─ // ============================================================================ "use client"; @@ -18,14 +18,14 @@ import type { } from "@/types/table"; import { useDataTable } from "@/hooks/use-data-table"; import { DataTable } from "@/components/data-table/data-table"; -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; // ✅ 확장된 버전 사용 import { getEvaluationTargets, getEvaluationTargetsStats } from "../service"; import { cn } from "@/lib/utils"; import { useTablePresets } from "@/components/data-table/use-table-presets"; import { TablePresetManager } from "@/components/data-table/data-table-preset"; import { getEvaluationTargetsColumns } from "./evaluation-targets-columns"; import { EvaluationTargetsTableToolbarActions } from "./evaluation-targets-toolbar-actions"; -import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"; +import { EvaluationTargetFilterSheet } from "./evaluation-targets-filter-sheet"; // ✅ 폼 기반 필터 시트 import { EvaluationTargetWithDepartments } from "@/db/schema"; import { EditEvaluationTargetSheet } from "./update-evaluation-target"; import { @@ -33,6 +33,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { useRouter } from "next/navigation"; // ✅ 라우터 추가 /* -------------------------------------------------------------------------- */ /* Process Guide Popover */ @@ -239,11 +240,93 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false); const searchParams = useSearchParams(); + // ✅ 외부 필터 상태 (폼에서 전달받은 필터) + const [externalFilters, setExternalFilters] = React.useState<any[]>([]); + const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and"); + + // ✅ 폼에서 전달받은 필터를 처리하는 핸들러 + const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => { + console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator); + setExternalFilters(filters); + setExternalJoinOperator(joinOperator); + // 필터 적용 후 패널 닫기 + setIsFilterPanelOpen(false); + }, []); + + + const searchString = React.useMemo( + () => searchParams.toString(), + [searchParams] + ); + + const getSearchParam = React.useCallback( + (key: string, def = "") => + new URLSearchParams(searchString).get(key) ?? def, + [searchString] + ); + + + // ✅ URL 필터 변경 감지 및 데이터 새로고침 + React.useEffect(() => { + const refetchData = async () => { + try { + setIsDataLoading(true); + + // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성 + const currentFilters = getSearchParam("filters"); + const currentJoinOperator = getSearchParam("joinOperator", "and"); + const currentPage = parseInt(getSearchParam("page", "1")); + const currentPerPage = parseInt(getSearchParam("perPage", "10")); + const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }]; + const currentSearch = getSearchParam("search", ""); + + const searchParams = { + filters: currentFilters ? JSON.parse(currentFilters) : [], + joinOperator: currentJoinOperator as "and" | "or", + page: currentPage, + perPage: currentPerPage, + sort: currentSort, + search: currentSearch, + evaluationYear: evaluationYear + }; + + console.log("=== 새 데이터 요청 ===", searchParams); + + // 서버 액션 직접 호출 + const newData = await getEvaluationTargets(searchParams); + setTableData(newData); + + console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건"); + } catch (error) { + console.error("데이터 새로고침 오류:", error); + } finally { + setIsDataLoading(false); + } + }; + + /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */ + + // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용) + const timeoutId = setTimeout(() => { + // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침 + const hasChanges = getSearchParam("filters") || + getSearchParam("search") || + getSearchParam("page") !== "1" || + getSearchParam("perPage") !== "10" || + getSearchParam("sort"); + + if (hasChanges) { + refetchData(); + } + }, 300); // 디바운스 시간 단축 + + return () => clearTimeout(timeoutId); + }, [searchString, evaluationYear, getSearchParam]); + /* --------------------------- layout refs --------------------------- */ const containerRef = React.useRef<HTMLDivElement>(null); const [containerTop, setContainerTop] = React.useState(0); - // RFQ 패턴으로 변경: State를 통한 위치 관리 const updateContainerBounds = React.useCallback(() => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); @@ -267,25 +350,16 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: }; }, [updateContainerBounds]); - /* ---------------------- 데이터 프리패치 ---------------------- */ - const [promiseData] = React.use(promises); - const tableData = promiseData; + /* ---------------------- 데이터 상태 관리 ---------------------- */ + // 초기 데이터 설정 + const [initialPromiseData] = React.use(promises); + + // ✅ 테이블 데이터 상태 추가 + const [tableData, setTableData] = React.useState(initialPromiseData); + const [isDataLoading, setIsDataLoading] = React.useState(false); - console.log(tableData) - /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */ - const searchString = React.useMemo( - () => searchParams.toString(), // query가 바뀔 때만 새로 계산 - [searchParams] - ); - const getSearchParam = React.useCallback( - (key: string, def = "") => - new URLSearchParams(searchString).get(key) ?? def, - [searchString] - ); - - // 제네릭 함수는 useCallback 밖에서 정의 const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => { try { const value = getSearchParam(key); @@ -295,9 +369,9 @@ export function EvaluationTargetsTable({ promises, evaluationYear, className }: } }, [getSearchParam]); -const parseSearchParam = <T,>(key: string, defaultValue: T): T => { - return parseSearchParamHelper(key, defaultValue); -}; + const parseSearchParam = <T,>(key: string, defaultValue: T): T => { + return parseSearchParamHelper(key, defaultValue); + }; /* ---------------------- 초기 설정 ---------------------------- */ const initialSettings = React.useMemo(() => ({ @@ -306,15 +380,13 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }], filters: parseSearchParam("filters", []), joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and", - basicFilters: parseSearchParam("basicFilters", []), - basicJoinOperator: (getSearchParam("basicJoinOperator") as "and" | "or") || "and", search: getSearchParam("search", ""), columnVisibility: {}, columnOrder: [], pinnedColumns: { left: [], right: ["actions"] }, groupBy: [], expandedRows: [], - }), [getSearchParam]); + }), [getSearchParam, parseSearchParam]); /* --------------------- 프리셋 훅 ------------------------------ */ const { @@ -336,14 +408,6 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { /* --------------------- 컬럼 ------------------------------ */ const columns = React.useMemo(() => getEvaluationTargetsColumns({ setRowAction }), [setRowAction]); -// const columns =[ -// { accessorKey: "vendorCode", header: "벤더 코드" }, -// { accessorKey: "vendorName", header: "벤더명" }, -// { accessorKey: "status", header: "상태" }, -// { accessorKey: "evaluationYear", header: "평가년도" }, -// { accessorKey: "division", header: "구분" } -// ]; - /* 기본 필터 */ const filterFields: DataTableFilterField<EvaluationTargetWithDepartments>[] = [ @@ -355,13 +419,36 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { /* 고급 필터 */ const advancedFilterFields: DataTableAdvancedFilterField<EvaluationTargetWithDepartments>[] = [ { id: "evaluationYear", label: "평가년도", type: "number" }, - { id: "division", label: "구분", type: "select", options: [ { label: "해양", value: "OCEAN" }, { label: "조선", value: "SHIPYARD" } ] }, + { id: "division", label: "구분", type: "select", options: [ + { label: "해양", value: "PLANT" }, + { label: "조선", value: "SHIP" } + ]}, { id: "vendorCode", label: "벤더 코드", type: "text" }, { id: "vendorName", label: "벤더명", type: "text" }, - { id: "domesticForeign", label: "내외자", type: "select", options: [ { label: "내자", value: "DOMESTIC" }, { label: "외자", value: "FOREIGN" } ] }, - { id: "materialType", label: "자재구분", type: "select", options: [ { label: "기자재", value: "EQUIPMENT" }, { label: "벌크", value: "BULK" }, { label: "기/벌", value: "EQUIPMENT_BULK" } ] }, - { id: "status", label: "상태", type: "select", options: [ { label: "검토 중", value: "PENDING" }, { label: "확정", value: "CONFIRMED" }, { label: "제외", value: "EXCLUDED" } ] }, - { id: "consensusStatus", label: "의견 일치", type: "select", options: [ { label: "일치", value: "true" }, { label: "불일치", value: "false" }, { label: "검토 중", value: "null" } ] }, + { id: "domesticForeign", label: "내외자", type: "select", options: [ + { label: "내자", value: "DOMESTIC" }, + { label: "외자", value: "FOREIGN" } + ]}, + { id: "materialType", label: "자재구분", type: "select", options: [ + { label: "기자재", value: "EQUIPMENT" }, + { label: "벌크", value: "BULK" }, + { label: "기자재/벌크", value: "EQUIPMENT_BULK" } + ]}, + { id: "status", label: "상태", type: "select", options: [ + { label: "검토 중", value: "PENDING" }, + { label: "확정", value: "CONFIRMED" }, + { label: "제외", value: "EXCLUDED" } + ]}, + { id: "consensusStatus", label: "의견 일치", type: "select", options: [ + { label: "일치", value: "true" }, + { label: "불일치", value: "false" }, + { label: "검토 중", value: "null" } + ]}, + { id: "orderReviewerName", label: "발주 담당자명", type: "text" }, + { id: "procurementReviewerName", label: "조달 담당자명", type: "text" }, + { id: "qualityReviewerName", label: "품질 담당자명", type: "text" }, + { id: "designReviewerName", label: "설계 담당자명", type: "text" }, + { id: "csReviewerName", label: "CS 담당자명", type: "text" }, { id: "adminComment", label: "관리자 의견", type: "text" }, { id: "consolidatedComment", label: "종합 의견", type: "text" }, { id: "confirmedAt", label: "확정일", type: "date" }, @@ -398,10 +485,15 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { }); /* ---------------------- helper ------------------------------ */ - const getActiveBasicFilterCount = React.useCallback(() => { + const getActiveFilterCount = React.useCallback(() => { try { - const f = getSearchParam("basicFilters"); - return f ? JSON.parse(f).length : 0; + // URL에서 현재 필터 수 확인 + const filtersParam = getSearchParam("filters"); + if (filtersParam) { + const filters = JSON.parse(filtersParam); + return Array.isArray(filters) ? filters.length : 0; + } + return 0; } catch { return 0; } @@ -427,7 +519,7 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { <EvaluationTargetFilterSheet isOpen={isFilterPanelOpen} onClose={() => setIsFilterPanelOpen(false)} - onSearch={() => setIsFilterPanelOpen(false)} + onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달 isLoading={false} /> </div> @@ -451,9 +543,9 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { className="flex items-center shadow-sm" > {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />} - {getActiveBasicFilterCount() > 0 && ( + {getActiveFilterCount() > 0 && ( <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> - {getActiveBasicFilterCount()} + {getActiveFilterCount()} </span> )} </Button> @@ -468,12 +560,27 @@ const parseSearchParam = <T,>(key: string, defaultValue: T): T => { </div> {/* Table */} - <div className="flex-1 overflow-hidden" style={{ height: "calc(100vh - 500px)" }}> + <div className="flex-1 overflow-hidden relative" style={{ height: "calc(100vh - 500px)" }}> + {isDataLoading && ( + <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center"> + <div className="flex items-center gap-2 text-sm text-muted-foreground"> + <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" /> + 필터링 중... + </div> + </div> + )} <DataTable table={table} className="h-full"> + {/* ✅ 확장된 DataTableAdvancedToolbar 사용 */} <DataTableAdvancedToolbar table={table} filterFields={advancedFilterFields} + debounceMs={300} shallow={false} + externalFilters={externalFilters} + externalJoinOperator={externalJoinOperator} + onFiltersChange={(filters, joinOperator) => { + console.log("=== 필터 변경 감지 ===", filters, joinOperator); + }} > <div className="flex items-center gap-2"> <TablePresetManager<EvaluationTargetWithDepartments> |
