refactor: complete canonical table cleanup
Build & Deploy to K8s / build-and-deploy (push) Failing after 14m3s
Build & Deploy to K8s / build-and-deploy (push) Failing after 14m3s
This commit is contained in:
@@ -1,14 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import type { DataFilterConfig } from "@/types/screen-management";
|
||||
|
||||
/**
|
||||
* useTableData — 통합 table 컴포넌트 데이터 fetch 훅
|
||||
*
|
||||
* entityJoinApi.getTableDataWithJoins() 호출.
|
||||
* 페이지네이션, 정렬, 검색 상태를 관리.
|
||||
*
|
||||
* Phase D.2 (2026-05-20) — `dataFilter` / `excludeFilter` 를 entityJoinApi 에 그대로 전달.
|
||||
* 호출자는 객체 ref 가 매 렌더마다 신규 생성되지 않도록 memoize 하는 책임이 있다 — 내부적으로도
|
||||
* stable JSON string 으로 dep 추적해 ref 변동만으로 fetch 폭주가 나지 않게 한다.
|
||||
*/
|
||||
|
||||
export interface ExcludeFilterPayload {
|
||||
enabled: boolean;
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
sourceColumn: string;
|
||||
filterColumn?: string;
|
||||
filterValue?: any;
|
||||
}
|
||||
|
||||
export interface UseTableDataParams {
|
||||
tableName?: string;
|
||||
page?: number;
|
||||
@@ -17,6 +31,10 @@ export interface UseTableDataParams {
|
||||
sortOrder?: "asc" | "desc";
|
||||
search?: Record<string, any>;
|
||||
enabled?: boolean; // false면 fetch 안 함 (디자인 모드)
|
||||
/** D.2 — enabled 일 때만 entityJoinApi 에 전달 */
|
||||
dataFilter?: DataFilterConfig;
|
||||
/** D.2 — enabled 일 때만 entityJoinApi 에 전달 */
|
||||
excludeFilter?: ExcludeFilterPayload;
|
||||
}
|
||||
|
||||
export interface UseTableDataResult {
|
||||
@@ -36,6 +54,11 @@ export interface UseTableDataResult {
|
||||
toggleSort: (col: string) => void;
|
||||
setSearch: (s: Record<string, any>) => void;
|
||||
refresh: () => void;
|
||||
/**
|
||||
* Phase D.9 (2026-05-20) — DataReceivable.receiveData() 가 local data 를 override.
|
||||
* append/replace/merge 결과를 통째 적용. fetch refresh 전까지 유지. totalOverride 미지정 시 length 사용.
|
||||
*/
|
||||
setLocalData: (next: Record<string, any>[], totalOverride?: number) => void;
|
||||
}
|
||||
|
||||
export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
@@ -47,8 +70,21 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
sortOrder: initialSortOrder = "desc",
|
||||
search: externalSearch,
|
||||
enabled = true,
|
||||
dataFilter,
|
||||
excludeFilter,
|
||||
} = params;
|
||||
|
||||
// D.2 — dataFilter / excludeFilter 객체 ref 가 매 렌더마다 신규여도 dep 으로 안 잡히도록
|
||||
// stable JSON string 으로 변환해 fetchData dep 으로 사용. 호출자 책임 보강.
|
||||
const dataFilterJson = useMemo(
|
||||
() => (dataFilter && (dataFilter as any).enabled ? JSON.stringify(dataFilter) : null),
|
||||
[dataFilter],
|
||||
);
|
||||
const excludeFilterJson = useMemo(
|
||||
() => (excludeFilter && excludeFilter.enabled ? JSON.stringify(excludeFilter) : null),
|
||||
[excludeFilter],
|
||||
);
|
||||
|
||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
@@ -59,14 +95,29 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
const [search, setSearch] = useState<Record<string, any>>(externalSearch || {});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const refreshKey = useRef(0);
|
||||
const initialStateRef = useRef({
|
||||
tableName,
|
||||
page: initialPage,
|
||||
pageSize: initialPageSize,
|
||||
sortBy: initialSortBy,
|
||||
sortOrder: initialSortOrder,
|
||||
});
|
||||
|
||||
// 외부 검색 조건 동기화
|
||||
// 외부 검색 조건 동기화.
|
||||
// D.2: 필터를 모두 clear 해서 externalSearch 가 undefined 로 바뀐 경우에도
|
||||
// 내부 search state 를 비워야 stale 검색 조건이 남지 않는다.
|
||||
useEffect(() => {
|
||||
if (externalSearch) {
|
||||
setSearch(externalSearch);
|
||||
setPage(1);
|
||||
}
|
||||
const nextSearch = externalSearch || {};
|
||||
setSearch((prev) => {
|
||||
const prevKeys = Object.keys(prev);
|
||||
const nextKeys = Object.keys(nextSearch);
|
||||
const changed =
|
||||
prevKeys.length !== nextKeys.length ||
|
||||
nextKeys.some((key) => prev[key] !== nextSearch[key]);
|
||||
if (!changed) return prev;
|
||||
return nextSearch;
|
||||
});
|
||||
setPage(1);
|
||||
}, [externalSearch]);
|
||||
|
||||
// 데이터 fetch
|
||||
@@ -87,6 +138,11 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
sortOrder,
|
||||
search: Object.keys(search).length > 0 ? search : undefined,
|
||||
enableEntityJoin: true,
|
||||
// D.2 — JSON 으로 변환된 stable string 이 dep 이지만 실제 payload 는 원본 객체 사용.
|
||||
dataFilter:
|
||||
dataFilter && (dataFilter as any).enabled ? dataFilter : undefined,
|
||||
excludeFilter:
|
||||
excludeFilter && excludeFilter.enabled ? excludeFilter : undefined,
|
||||
});
|
||||
|
||||
setData(response.data || []);
|
||||
@@ -101,18 +157,51 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [tableName, page, pageSize, sortBy, sortOrder, search, enabled, refreshKey.current]);
|
||||
// dataFilter / excludeFilter 객체 ref 가 아닌 *Json string 만 dep — fetch 폭주 방지
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
tableName,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
search,
|
||||
enabled,
|
||||
dataFilterJson,
|
||||
excludeFilterJson,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
// 테이블 변경 시 페이지 리셋
|
||||
// 테이블 / config 초기값 변경 시 런타임 상태 동기화.
|
||||
// 초기 mount 에서는 useState(initial*) 값이 이미 권위이므로 reset 하지 않는다.
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setSortBy("");
|
||||
setSearch({});
|
||||
}, [tableName]);
|
||||
const prev = initialStateRef.current;
|
||||
const changed =
|
||||
prev.tableName !== tableName ||
|
||||
prev.page !== initialPage ||
|
||||
prev.pageSize !== initialPageSize ||
|
||||
prev.sortBy !== initialSortBy ||
|
||||
prev.sortOrder !== initialSortOrder;
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
initialStateRef.current = {
|
||||
tableName,
|
||||
page: initialPage,
|
||||
pageSize: initialPageSize,
|
||||
sortBy: initialSortBy,
|
||||
sortOrder: initialSortOrder,
|
||||
};
|
||||
|
||||
setPage(initialPage);
|
||||
setPageSize(initialPageSize);
|
||||
setSortBy(initialSortBy);
|
||||
setSortOrder(initialSortOrder);
|
||||
setSearch(externalSearch || {});
|
||||
}, [tableName, initialPage, initialPageSize, initialSortBy, initialSortOrder, externalSearch]);
|
||||
|
||||
const toggleSort = useCallback((col: string) => {
|
||||
setSortBy((prev) => {
|
||||
@@ -126,10 +215,28 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
refreshKey.current += 1;
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const setPageSizeAction = useCallback((s: number) => {
|
||||
setPageSize(s);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const setSearchAction = useCallback((s: Record<string, any>) => {
|
||||
setSearch(s);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const setLocalData = useCallback((next: Record<string, any>[], totalOverride?: number) => {
|
||||
const arr = Array.isArray(next) ? next : [];
|
||||
setData(arr);
|
||||
const t = typeof totalOverride === "number" && totalOverride >= 0 ? totalOverride : arr.length;
|
||||
setTotal(t);
|
||||
const ps = pageSize > 0 ? pageSize : 20;
|
||||
setTotalPages(Math.max(1, Math.ceil(t / ps)));
|
||||
}, [pageSize]);
|
||||
|
||||
return {
|
||||
data,
|
||||
total,
|
||||
@@ -141,10 +248,12 @@ export function useTableData(params: UseTableDataParams): UseTableDataResult {
|
||||
loading,
|
||||
error,
|
||||
setPage,
|
||||
setPageSize: (s: number) => { setPageSize(s); setPage(1); },
|
||||
setPageSize: setPageSizeAction,
|
||||
setSortBy,
|
||||
toggleSort,
|
||||
setSearch: (s: Record<string, any>) => { setSearch(s); setPage(1); },
|
||||
setSearch: setSearchAction,
|
||||
refresh,
|
||||
// Phase D.9 — 외부 receiveData() 가 local override. 다음 fetch 까지 유지.
|
||||
setLocalData,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user