[st] - 검토단 작업 완료 및 입력6-1, 10-1 운영데이터업데이트 기능 추가, 텝 영역 우클릭 후 텝 닫기 기능 추가

This commit is contained in:
2026-06-30 14:23:08 +09:00
parent 162e18142c
commit afdc06638f
52 changed files with 7723 additions and 1591 deletions
+106 -59
View File
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, type MutableRefObject } from "react";
import JSZip from "jszip";
import { ValueType } from "realgrid";
@@ -14,6 +14,7 @@ import type {
CellStyleCallbackParams,
CellStyleCallbackResult,
CellTooltipCallback,
ColumnConfig,
ColumnLayoutItem,
DataGridProps,
FooterItemWithCallback,
@@ -48,6 +49,63 @@ function mergeCellStyleCallbackResult(
};
}
type GridColumnWithStyleCallback = {
styleCallback?: (
grid: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
};
function applyCellStyleCallbacksToColumns(
gridView: unknown,
columns: ColumnConfig[],
rowDataRef: MutableRefObject<Record<string, string | number>[]>,
cellStyleCallbackRef: MutableRefObject<CellStyleCallback | undefined>,
) {
if (!cellStyleCallbackRef.current || !gridView) return;
const gv = gridView as {
columnByName?: (name: string) => GridColumnWithStyleCallback | null | undefined;
};
if (typeof gv.columnByName !== "function") return;
columns.forEach((column) => {
const baseStyleName = column.styleName;
let gridCol: GridColumnWithStyleCallback | null | undefined;
try {
// columnByName은 GridView 인스턴스 메서드 — this 바인딩을 위해 gv에서 직접 호출
gridCol = gv.columnByName!(column.name);
} catch {
return;
}
if (!gridCol) return;
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1;
const fieldName =
dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName;
const data = rowDataRef.current;
if (itemIndex < 0 || itemIndex >= data.length) {
return mergeCellStyleCallbackResult(baseStyleName, undefined);
}
const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(baseStyleName, extra);
};
});
}
// RealGrid 타입 확장
declare module "realgrid" {
interface GridView {
@@ -725,9 +783,16 @@ const DataGrid = ({
}, [highlightedRowId, rowData, idFieldName]);
useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return;
gridRef.current.gridView.refresh();
}, [cellStyleCallback, rowData]);
const gv = gridRef.current?.gridView;
if (!gv || !cellStyleCallback) return;
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
(gv as { refresh?: () => void }).refresh?.();
}, [cellStyleCallback, columns, rowData]);
const { setMessage: alert } = useAlertStore((state) => state);
const activeTabId = useTabStore((state) => state.activeTabId);
@@ -1049,6 +1114,10 @@ const DataGrid = ({
const gridView = gridRef.current.gridView;
const dataProvider = gridRef.current.dataProvider;
const footerTimerIds: ReturnType<typeof setTimeout>[] = [];
const scheduleFooterTask = (task: () => void, delayMs: number) => {
footerTimerIds.push(setTimeout(task, delayMs));
};
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
const columnsFieldsSig = columns.map((c) => c.fieldName).join("\u0001");
@@ -1189,8 +1258,10 @@ const DataGrid = ({
});
const applyFooterSettings = (
targetGridView: typeof gridView,
targetGridView: typeof gridView | null | undefined,
) => {
if (!targetGridView?.setOptions) return;
const hasFooter = columns.some((col) => col.footer);
if (!hasFooter) return;
@@ -1308,7 +1379,7 @@ const DataGrid = ({
};
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
scheduleFooterTask(() => {
if (gridRef.current?.gridView && columnLayout) {
const safeLayout = sanitizeColumnLayout(columnLayout as unknown[]);
if (safeLayout.length === 0) return;
@@ -1327,10 +1398,9 @@ const DataGrid = ({
// footer 설정 (배열 또는 단일 객체 모두 처리)
// setFooters 호출 후 footer 배열 설정
setTimeout(() => {
scheduleFooterTask(() => {
const currentGridView = gridRef.current?.gridView;
if (!currentGridView || !currentGridView.columnByName) {
console.warn("GridView not available for footer setup");
if (!currentGridView?.columnByName) {
return;
}
@@ -1385,19 +1455,28 @@ const DataGrid = ({
col,
),
);
applyCellStyleCallbacksToColumns(
currentGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
setTimeout(() => {
scheduleFooterTask(() => {
const liveGridView = gridRef.current?.gridView;
if (!liveGridView?.columnByName) return;
columns.forEach((col) => {
if (!col.footer || !Array.isArray(col.footer))
return;
try {
const column = currentGridView.columnByName(
const column = liveGridView.columnByName(
col.name,
);
if (!column) return;
@@ -1429,11 +1508,17 @@ const DataGrid = ({
);
}
});
applyFooterSettings(currentGridView);
applyCellStyleCallbacksToColumns(
liveGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(liveGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
liveGridView.setColumnLayout?.(columnLayout);
}
currentGridView.refresh?.();
liveGridView.refresh?.();
}, 50);
} catch (e) {
console.warn("Failed to set columns with footer array:", e);
@@ -1495,6 +1580,7 @@ const DataGrid = ({
}
return () => {
footerTimerIds.forEach(clearTimeout);
if (getGridDataRef) getGridDataRef.current = null;
};
}, [
@@ -1939,19 +2025,9 @@ const DataGrid = ({
};
}
columns.forEach((column) => {
const baseStyleName = column.styleName;
const gridCol = gv.columnByName(
column.name,
) as {
styleCallback?: (
g: unknown,
dataCell: {
itemIndex?: number;
index?: { itemIndex?: number };
dataField?: string;
fieldName?: string;
},
) => CellStyleCallbackResult;
renderer?: { showTooltip?: boolean };
} | null;
if (gridCol) {
@@ -1990,43 +2066,14 @@ const DataGrid = ({
{ showTooltip: true },
);
}
gridCol.styleCallback = (grid, dataCell) => {
const itemIndex =
dataCell?.index?.itemIndex ??
dataCell?.itemIndex ??
-1;
const fieldName =
dataCell?.dataField ??
dataCell?.fieldName ??
column.fieldName;
const data = rowDataRef.current;
if (
itemIndex < 0 ||
itemIndex >= data.length
) {
return mergeCellStyleCallbackResult(
baseStyleName,
undefined,
);
}
const row = data[itemIndex] as Record<
string,
string | number
>;
const extra =
cellStyleCallbackRef.current?.({
rowIndex: itemIndex,
fieldName,
row,
grid: grid as CellStyleCallbackParams["grid"],
});
return mergeCellStyleCallbackResult(
baseStyleName,
extra,
);
};
}
});
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
gv.refresh();
}, 0);
}}
@@ -234,6 +234,7 @@ export function VerificationResultHistoryDialog({
};
const checkedVrfcByTabRef = useRef(checkedVrfcByTab);
checkedVrfcByTabRef.current = checkedVrfcByTab;
const loadedTabKeysRef = useRef<Set<VerificationHistoryTabKey>>(new Set());
const resetState = useCallback(() => {
setActiveTab("NV");
@@ -243,6 +244,7 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setResetSubmitting(false);
setCheckedVrfcByTab(createEmptyCheckedVrfcByTab());
loadedTabKeysRef.current.clear();
}, []);
const fetchVerificationResultPage = useCallback(
@@ -279,10 +281,10 @@ export function VerificationResultHistoryDialog({
);
const loadTabPage = useCallback(
async (tab: VerificationHistoryTabKey, pageIndex: number) => {
async (tab: VerificationHistoryTabKey, pageIndex: number): Promise<boolean> => {
if (!stableQueryContext) {
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
return;
return false;
}
setErrorMessage(null);
setLoadingByTab((prev) => ({ ...prev, [tab]: true }));
@@ -296,10 +298,12 @@ export function VerificationResultHistoryDialog({
? Math.max(0, Number(apiCurrentPage) - 1)
: pageIndex;
applyPageResponse(tab, resolvedPageIndex, list, totalPages);
return true;
} catch {
const tabLabel =
verificationHistoryTabs.find((t) => t.value === tab)?.label ?? tab;
setErrorMessage(`${tabLabel} 조회 중 오류가 발생했습니다.`);
return false;
} finally {
setLoadingByTab((prev) => ({ ...prev, [tab]: false }));
}
@@ -314,40 +318,27 @@ export function VerificationResultHistoryDialog({
}
setErrorMessage(null);
setTabPages(initialTabPageState());
setLoadingByTab({ NV: true, IMR: true, HB: true });
try {
const results = await Promise.all(
verificationHistoryTabs.map((tab) =>
fetchVerificationResultPage(tab.value, 0),
),
);
setEntriesByTab({
NV: results[0]?.data?.verifyResultList ?? [],
IMR: results[1]?.data?.verifyResultList ?? [],
HB: results[2]?.data?.verifyResultList ?? [],
});
setTabPages({
NV: {
pageIndex: 0,
pageCount: Math.max(results[0]?.data?.pageInfo?.totalPages ?? 1, 1),
},
IMR: {
pageIndex: 0,
pageCount: Math.max(results[1]?.data?.pageInfo?.totalPages ?? 1, 1),
},
HB: {
pageIndex: 0,
pageCount: Math.max(results[2]?.data?.pageInfo?.totalPages ?? 1, 1),
},
});
} catch {
setEntriesByTab({ NV: [], IMR: [], HB: [] });
setTabPages(initialTabPageState());
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setLoadingByTab({ NV: false, IMR: false, HB: false });
setEntriesByTab({ NV: [], IMR: [], HB: [] });
loadedTabKeysRef.current.clear();
const loaded = await loadTabPage("NV", 0);
if (loaded) {
loadedTabKeysRef.current.add("NV");
}
}, [fetchVerificationResultPage, stableQueryContext]);
}, [loadTabPage, stableQueryContext]);
const handleActiveTabChange = useCallback(
(tab: VerificationHistoryTabKey) => {
setActiveTab(tab);
if (loadedTabKeysRef.current.has(tab)) return;
void (async () => {
const loaded = await loadTabPage(tab, 0);
if (loaded) {
loadedTabKeysRef.current.add(tab);
}
})();
},
[loadTabPage],
);
const handleTabPageChange = useCallback(
(tab: VerificationHistoryTabKey, pageIndex: number) => {
@@ -577,7 +568,7 @@ export function VerificationResultHistoryDialog({
)}
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)}
onValueChange={(value) => handleActiveTabChange(value as VerificationHistoryTabKey)}
className="w-full"
variant="default"
>