summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/evaluation-target-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/evaluation-target-list/table/evaluation-target-table.tsx')
-rw-r--r--lib/evaluation-target-list/table/evaluation-target-table.tsx199
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>