This commit is contained in:
2026-08-13 17:21:09 +09:00
34 changed files with 1872 additions and 511 deletions
@@ -2,15 +2,17 @@ import { Button } from "@common/components";
interface SubmitButtonProps {
submitFunc: () => void;
/** 버튼 문구 (기본: 제출) */
text?: string;
}
const SubmitButton = ({ submitFunc }: SubmitButtonProps) => {
const SubmitButton = ({ submitFunc, text = "제출" }: SubmitButtonProps) => {
return (
<Button className="h-[3.2rem] w-[96px]" onClick={submitFunc}>
<Button className="h-[3.2rem] min-w-[96px] px-4" onClick={submitFunc}>
<i className="diveicon di-upload" />
<span>제출</span>
<span>{text}</span>
</Button>
);
};
export default SubmitButton;
export default SubmitButton;
+23
View File
@@ -23,6 +23,15 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
@apply !bg-[#fff7bf] !text-[#b45309];
}
/* 총계 footer 검증 오류 (commonInfo.verificationResult) */
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
}
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr {
@apply !bg-[#fff7bf] !text-[#b45309];
}
/* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
@apply !font-semibold !text-[#dc2626];
@@ -405,6 +414,13 @@
background-color: #ffe6e6 !important;
color: #c00 !important;
}
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error {
background-color: #ffe6e6 !important;
color: #c00 !important;
}
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
@@ -412,6 +428,13 @@
background-color: #fff7bf !important;
color: #b45309 !important;
}
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
background-color: #fff7bf !important;
color: #b45309 !important;
}
/* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
+267 -34
View File
@@ -24,6 +24,12 @@ import type {
} from "../type/DataGrid.type";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
import {
formatVerificationTooltip,
getVerificationErrorsForField,
getVerificationStyleName,
type VerificationResultLikeMap,
} from "@/components/util/input/verification-result-display";
import {
buildExcelExportFileBaseName,
getExcelImportColumnFieldNames,
@@ -55,6 +61,34 @@ function mergeCellStyleCallbackResult(
};
}
/** 컬럼 footer.styleName 또는 footerVerificationResult(fieldName) → footer styleName */
function resolveFooterVerificationStyleName(
col: ColumnConfig,
footerVerificationResult: VerificationResultLikeMap | null | undefined,
): string | undefined {
if (col.footer && !Array.isArray(col.footer) && col.footer.styleName) {
return col.footer.styleName;
}
const errors = getVerificationErrorsForField(
footerVerificationResult,
col.fieldName,
col.name !== col.fieldName ? [col.name] : undefined,
);
return getVerificationStyleName(errors);
}
function resolveFooterVerificationTooltip(
col: ColumnConfig,
footerVerificationResult: VerificationResultLikeMap | null | undefined,
): string | undefined {
const errors = getVerificationErrorsForField(
footerVerificationResult,
col.fieldName,
col.name !== col.fieldName ? [col.name] : undefined,
);
return formatVerificationTooltip(errors);
}
type GridColumnWithStyleCallback = {
styleCallback?: (
grid: unknown,
@@ -782,7 +816,7 @@ const DataGrid = ({
excelExportFileSuffix,
excelExportSearchContext,
isExcelImport = true,
excelImportSkipNonEditable = true,
excelImportSkipNonEditable = false,
excelImportSewerBzmnCd,
isExcelExport = true,
fixedColumns,
@@ -793,6 +827,7 @@ const DataGrid = ({
getGridRef,
cellStyleCallback,
cellTooltipCallback,
footerVerificationResult = null,
onCellEdited,
onRowClick,
onDataCellClick,
@@ -824,6 +859,9 @@ const DataGrid = ({
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
cellTooltipCallback,
);
const footerVerificationResultRef = useRef<
VerificationResultLikeMap | null | undefined
>(footerVerificationResult);
const onCellEditedRef = useRef<typeof onCellEdited>(onCellEdited);
const onDataCellClickRef = useRef<typeof onDataCellClick>(onDataCellClick);
const onScrollToBottomRef = useRef<(() => void) | undefined>(
@@ -848,6 +886,7 @@ const DataGrid = ({
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
cellTooltipCallbackRef.current = cellTooltipCallback;
footerVerificationResultRef.current = footerVerificationResult;
onScrollToBottomRef.current = onScrollToBottom;
onItemCheckedRef.current = onItemChecked;
onItemAllCheckedRef.current = onItemAllChecked;
@@ -855,6 +894,7 @@ const DataGrid = ({
onDataCellClickRef.current = onDataCellClick;
const hasVerificationErrors = rowsHaveVerificationErrors(rowData);
// 총계 footer(commonInfo.verificationResult)는 행 우선보기 대상이 아님
const showVerifyPriorityToggle = hasVerificationErrors;
// 조회 등으로 rowData가 교체되면 우선보기 해제 (자체 setRowData는 스킵)
@@ -1338,10 +1378,14 @@ useEffect(() => {
const preserveFieldNames = excelImportSkipNonEditable
? getNonEditableColumnFieldNames(columns, columnLayout)
: [];
setRowData((prev) =>
mergeExcelImportWithExistingRows(prev, dataRows, {
preserveFieldNames,
}),
// 우선보기 해제 직후 rowDataRef가 원본 순서 — functional prev는 아직 정렬 상태일 수 있음
skipVerifyPriorityResetRef.current = true;
setRowData(
mergeExcelImportWithExistingRows(
rowDataRef.current,
dataRows,
{ preserveFieldNames },
),
);
} catch (error) {
console.error("엑셀 파일 파싱 오류:", error);
@@ -1703,12 +1747,26 @@ useEffect(() => {
if (col.footer) {
if (Array.isArray(col.footer)) {
colInfo.footer = col.footer;
colInfo.footer = col.footer.map((item) => ({
...item,
styleName:
item.styleName ??
resolveFooterVerificationStyleName(
col,
footerVerificationResultRef.current,
) ??
"",
}));
} else {
colInfo.footer = {
text: col.footer.text,
expression: col.footer.expression,
numberFormat: col.footer.numberFormat,
styleName:
resolveFooterVerificationStyleName(
col,
footerVerificationResultRef.current,
) ?? "",
};
}
}
@@ -1797,7 +1855,7 @@ useEffect(() => {
}
}
// footer가 단일 객체이고 valueCallback이 있는 경우
// footer가 단일 객체이고 valueCallback/styleName이 있는 경우
columns.forEach((col) => {
if (!col.footer || Array.isArray(col.footer)) return;
@@ -1812,6 +1870,13 @@ useEffect(() => {
}
).valueCallback = col.footer.valueCallback;
}
const styleName =
resolveFooterVerificationStyleName(
col,
footerVerificationResultRef.current,
) ?? "";
(column.footer as { styleName?: string }).styleName =
styleName;
} catch (e) {
console.warn(
`Error setting footer valueCallback for column ${col.name}:`,
@@ -1871,8 +1936,45 @@ useEffect(() => {
headerHeight,
fitStyle,
enableSort,
footerVerificationResult,
]);
/** footerVerificationResult만 바뀐 경우에도 총계 스타일 즉시 재적용/초기화 */
useEffect(() => {
const gridView = gridRef.current?.gridView as
| {
columnByName?: (name: string) => {
footer?: { styleName?: string } | Array<{ styleName?: string }>;
} | null;
refresh?: () => void;
}
| undefined;
if (!gridView?.columnByName) return;
columns.forEach((col) => {
if (!col.footer) return;
try {
const column = gridView.columnByName?.(col.name);
if (!column?.footer) return;
const styleName =
resolveFooterVerificationStyleName(
col,
footerVerificationResult,
) ?? "";
if (Array.isArray(column.footer)) {
column.footer.forEach((item) => {
item.styleName = styleName;
});
} else {
column.footer.styleName = styleName;
}
} catch {
/* ignore */
}
});
gridView.refresh?.();
}, [footerVerificationResult, columns]);
// 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식)
useEffect(() => {
const gridView = gridRef.current?.gridView;
@@ -1960,9 +2062,15 @@ useEffect(() => {
};
}, [onRowClick, onDataCellClick, cellTooltipCallback, columns]);
// 빈 셀은 RealGrid hover 툴팁이 안 뜸 → React 오버레이로만 보완 (값 있는 셀은 기존 onShowTooltip)
// RealGrid 네이티브 툴팁은 데이터 셀만 지원 → 빈 셀·총계 footer는 React 오버레이로 보완
useEffect(() => {
if (!cellTooltipCallback) {
const needsOverlayTooltip =
Boolean(cellTooltipCallback) ||
Boolean(
footerVerificationResult &&
Object.keys(footerVerificationResult).length > 0,
);
if (!needsOverlayTooltip) {
emptyCellHoverKeyRef.current = "";
setEmptyCellHoverTooltip(null);
return;
@@ -1976,6 +2084,28 @@ useEffect(() => {
setEmptyCellHoverTooltip(null);
};
const isFooterCellType = (cellType: string | undefined) => {
if (!cellType) return false;
const normalized = cellType.toLowerCase();
return (
normalized === "footer" ||
normalized === "summary" ||
normalized === "groupfooter"
);
};
const resolveColumnFromIndex = (index: {
column?: string;
fieldName?: string;
}) =>
columnsRef.current.find(
(c) =>
c.name === index.column ||
c.fieldName === index.column ||
c.name === index.fieldName ||
c.fieldName === index.fieldName,
);
const handleMouseMove = (ev: MouseEvent) => {
if (emptyCellHoverRafRef.current != null) {
cancelAnimationFrame(emptyCellHoverRafRef.current);
@@ -2012,12 +2142,72 @@ useEffect(() => {
ev.clientY - rect.top,
);
} catch {
index = null;
}
const elAtPoint = document.elementFromPoint(
ev.clientX,
ev.clientY,
);
const footerDom =
elAtPoint instanceof Element
? elAtPoint.closest(
".rg-footer-cell, .rg-footers .rg-cell, .rg-footer .rg-cell",
)
: null;
const isFooter =
Boolean(footerDom) ||
isFooterCellType(index?.cellType) ||
index?.itemIndex === -1;
if (isFooter) {
if (!footerVerificationResultRef.current) {
clearEmptyCellHoverTooltip();
return;
}
const col = index
? resolveColumnFromIndex(index)
: undefined;
if (!col) {
clearEmptyCellHoverTooltip();
return;
}
const fieldName = col.fieldName;
const msg = resolveFooterVerificationTooltip(
col,
footerVerificationResultRef.current,
);
if (!msg) {
clearEmptyCellHoverTooltip();
return;
}
const normalizedMessage = msg.replace(/\n[ \t]+/g, "\n");
const key = `footer:${fieldName}`;
emptyCellHoverKeyRef.current = key;
setEmptyCellHoverTooltip({
message: normalizedMessage,
x: ev.clientX + 12,
y: ev.clientY + 16,
key,
});
return;
}
if (!index) {
clearEmptyCellHoverTooltip();
return;
}
const col = resolveColumnFromIndex(index);
const fieldName =
col?.fieldName ?? index.fieldName ?? index.column ?? "";
if (!cellTooltipCallbackRef.current) {
clearEmptyCellHoverTooltip();
return;
}
if (
!index ||
(index.cellType != null && index.cellType !== "data") ||
index.itemIndex == null ||
index.itemIndex < 0
@@ -2032,15 +2222,6 @@ useEffect(() => {
return;
}
const col = columnsRef.current.find(
(c) =>
c.name === index.column ||
c.fieldName === index.column ||
c.name === index.fieldName ||
c.fieldName === index.fieldName,
);
const fieldName =
col?.fieldName ?? index.fieldName ?? index.column ?? "";
if (!fieldName) {
clearEmptyCellHoverTooltip();
return;
@@ -2061,8 +2242,6 @@ useEffect(() => {
return;
}
// 값 있는 셀 RealGrid 툴팁과 같이: 본문 다음 줄에 (검증 기준값...)이 오도록
// 템플릿 리터럴 들여쓰기 공백은 제거
const normalizedMessage = msg.replace(/\n[ \t]+/g, "\n");
const key = `${index.itemIndex}:${fieldName}`;
@@ -2096,7 +2275,7 @@ useEffect(() => {
}
clearEmptyCellHoverTooltip();
};
}, [cellTooltipCallback]);
}, [cellTooltipCallback, footerVerificationResult]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
@@ -2433,19 +2612,36 @@ useEffect(() => {
index: {
itemIndex: number;
column: string;
cellType?: string;
},
value: string,
) => string | undefined;
}
).onShowTooltip = (_grid, index, value) => {
const row =
rowDataRef.current[index?.itemIndex];
if (row == null) return value;
const column = index.column;
const col = columns.find(
(c) => c.name === column,
);
const fieldName = col?.fieldName ?? column;
// 총계 footer: cellType footer 또는 itemIndex === -1
const isFooter =
index?.cellType === "footer" ||
index?.itemIndex === -1;
if (
isFooter &&
col &&
footerVerificationResultRef.current
) {
const footerMsg =
resolveFooterVerificationTooltip(
col,
footerVerificationResultRef.current,
);
if (footerMsg) return footerMsg;
}
const row =
rowDataRef.current[index?.itemIndex];
if (row == null) return value;
const custom =
cellTooltipCallbackRef.current?.({
row: row as Record<
@@ -2459,6 +2655,33 @@ useEffect(() => {
? custom
: undefined;
};
} else if (footerVerificationResultRef.current) {
(
gv as {
onShowTooltip?: (
grid: unknown,
index: {
itemIndex: number;
column: string;
cellType?: string;
},
value: string,
) => string | undefined;
}
).onShowTooltip = (_grid, index) => {
const isFooter =
index?.cellType === "footer" ||
index?.itemIndex === -1;
if (!isFooter) return undefined;
const col = columns.find(
(c) => c.name === index.column,
);
if (!col) return undefined;
return resolveFooterVerificationTooltip(
col,
footerVerificationResultRef.current,
);
};
}
columns.forEach((column) => {
const gridCol = gv.columnByName(
@@ -2487,7 +2710,7 @@ useEffect(() => {
gv as GridViewWithSetColumnProperty,
column,
);
if (cellTooltipCallbackRef.current) {
if (cellTooltipCallbackRef.current || footerVerificationResultRef.current) {
(
gv as {
setColumnProperty?: (
@@ -2588,13 +2811,22 @@ useEffect(() => {
footer={
column.footer &&
!Array.isArray(column.footer)
? {
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
? (() => {
const footerStyleName =
resolveFooterVerificationStyleName(
column,
footerVerificationResult,
) ?? "";
return {
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer
.numberFormat,
styleName: footerStyleName,
};
})()
: undefined
}
mergeRule={
@@ -2610,10 +2842,11 @@ useEffect(() => {
{emptyCellHoverTooltip &&
createPortal(
<div
className="rg-tooltip pointer-events-none !visible fixed z-[9999] w-max=[60rem]"
className="rg-tooltip pointer-events-none !visible fixed z-[9999] max-w-[60rem]"
style={{
left: emptyCellHoverTooltip.x,
top: emptyCellHoverTooltip.y,
whiteSpace: "pre-line",
}}
role="tooltip"
>
@@ -282,7 +282,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
const TabDataGrid = ({
columnInfo,
isExcelImport = true,
excelImportSkipNonEditable = true,
excelImportSkipNonEditable = false,
excelImportSewerBzmnCd,
excelExportSearchContext,
isExcelExport = true,
@@ -40,6 +40,7 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -109,7 +110,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk;
@@ -339,6 +340,8 @@ const ExcretaTreatmentFacilityInp = () => {
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -406,7 +409,7 @@ const ExcretaTreatmentFacilityInp = () => {
const columnInfo = useMemo(() => {
const enterable =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = ExcretaTreatFactInpColumn.columns ?? [];
const base = enterable
? rawColumns
@@ -434,7 +437,7 @@ const ExcretaTreatmentFacilityInp = () => {
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
const canEditGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const getCurrentRows = (): Record<string, string | number>[] =>
gridGetRowsRef.current?.() ?? rowData;
@@ -477,6 +480,7 @@ const ExcretaTreatmentFacilityInp = () => {
setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
setFooterVerificationResult(commonInfo.verificationResult ?? null);
if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) {
@@ -536,6 +540,7 @@ const ExcretaTreatmentFacilityInp = () => {
setStatusInfo(null);
setStatsCd("");
setIsVerificationFailed(false);
setFooterVerificationResult(null);
setIsNotApplicable(false);
setInputPeriodEnterable(true);
setSggOptions([]);
@@ -984,7 +989,7 @@ const ExcretaTreatmentFacilityInp = () => {
computeTabEnterable(statusInfo, inputPeriodEnterable);
const tabEnterableForGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const verificationQueryContext = (() => {
@@ -1076,6 +1081,7 @@ const ExcretaTreatmentFacilityInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={verificationCellTooltip}
/>
</div>
@@ -58,7 +58,11 @@ import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/p
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
const PrivateSewageFacilityInpTabList = [
@@ -134,7 +138,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -343,6 +347,9 @@ const PrivateSewageFacilityInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -458,6 +465,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -486,6 +496,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -514,6 +527,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -531,6 +547,7 @@ const PrivateSewageFacilityInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -573,6 +590,7 @@ const PrivateSewageFacilityInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -1067,10 +1085,10 @@ const PrivateSewageFacilityInp = () => {
for (const tab of PrivateSewageFacilityInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -1213,10 +1231,10 @@ const PrivateSewageFacilityInp = () => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
@@ -1255,6 +1273,9 @@ const PrivateSewageFacilityInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
@@ -51,7 +51,8 @@ import { PublicSewageFactInpColumnList } from '@/components/util/column/input/Pu
import { toGridRows } from '@/components/util/grid/gridFunction';
import { fetchColumnGuidesRecord } from '@/components/util/grid/columnGuides';
import { useTabStore } from '@/store/tab-store';
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
import { patchVerificationFailedByTab, patchFooterVerificationByTab } from '@/components/util/input/verification-failed-flag';
import type { VerificationResultLikeMap } from '@/components/util/input/verification-result-display';
import { useAdminStore } from '@/store/admin-store';
type GridRowWithVerification = Record<string, string | number> & {
@@ -94,7 +95,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -201,6 +202,9 @@ const PublicSewageFacilityInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
@@ -750,6 +754,9 @@ const PublicSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', gridRows);
@@ -797,6 +804,9 @@ const PublicSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, response.data.commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacility', leftGridRows);
@@ -959,6 +969,7 @@ const PublicSewageFacilityInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -1022,6 +1033,7 @@ const PublicSewageFacilityInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setInputPeriodEnterable(true);
@@ -1080,13 +1092,16 @@ const PublicSewageFacilityInp = () => {
const getGridColumnInfo = (
tab: ColumnInfoList,
guideTabKey?: string,
canEdit = true,
): ColumnInfo => ({
columns: (tab.columns ?? []).map((col) => {
const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim();
return {
const withGuide = {
...col,
tooltip: guide && guide.length > 0 ? guide : undefined,
};
if (canEdit) return withGuide;
return { ...withGuide, editable: false as const };
}),
columnLayout: tab.columnLayout ?? [],
});
@@ -1164,7 +1179,7 @@ const PublicSewageFacilityInp = () => {
isMainFacilityTab &&
tabRowDataExists('ST00060002') &&
gugunOkOrNotDoList &&
(isAdminInfo || inputPeriodEnterable);
inputPeriodEnterable;
const showSaveInToolbar =
toolbarGroupAllowed && (baseShowSaveInToolbar || showMainFacilitySaveInToolbar);
const showEnterableToolbarButtons =
@@ -1261,14 +1276,28 @@ const PublicSewageFacilityInp = () => {
</TabsList>
{PublicSewageFactInpColumnList.map((tab) => {
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null;
const tabEnterableForGrid =
(computeTabEnterable(tabStatus, inputPeriodEnterable) ||
isAdminInfo) &&
inputPeriodEnterable;
if (tab.isSplitGrid && tab.left && tab.right) {
const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`;
const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`;
return (
<TabsContent key={tab.tabKey} value={tab.tabKey}>
<SplitDataGrid
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)}
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)}
leftColumnInfo={getGridColumnInfo(
tab.left,
tab.tabKey,
tabEnterableForGrid,
)}
rightColumnInfo={getGridColumnInfo(
tab.right,
tab.tabKey,
tabEnterableForGrid,
)}
leftFixedColumns={tab.left.fixedColumns}
rightFixedColumns={tab.right.fixedColumns}
showHeaderCheckbox={true}
@@ -1348,11 +1377,10 @@ const PublicSewageFacilityInp = () => {
);
}
const gridColumnInfo = getGridColumnInfo(tab);
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null;
const tabEnterableForGrid = computeTabEnterable(
tabStatus,
inputPeriodEnterable,
const gridColumnInfo = getGridColumnInfo(
tab,
undefined,
tabEnterableForGrid,
);
const excelToolbarEnabledForTab =
tabRowDataExists(tab.tabKey) &&
@@ -1362,7 +1390,7 @@ const PublicSewageFacilityInp = () => {
return (
<TabsContent key={tab.tabKey} value={tab.tabKey}>
<DataGrid
key={tab.tabKey}
key={`${tab.tabKey}-${tabEnterableForGrid ? 'enterable' : 'readonly'}`}
columnInfo={gridColumnInfo}
rowStartNum={tab.rowStartNum}
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
@@ -1399,6 +1427,9 @@ const PublicSewageFacilityInp = () => {
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
}}
footerVerificationResult={
footerVerificationByTab.get(tab.tabKey) ?? null
}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
@@ -40,6 +40,7 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -109,7 +110,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk;
@@ -334,6 +335,8 @@ const PumpingStationInp = () => {
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -401,7 +404,7 @@ const PumpingStationInp = () => {
const columnInfo = useMemo(() => {
const enterable =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = PumpingStationInpColumn.columns ?? [];
const base = enterable
? rawColumns
@@ -429,7 +432,7 @@ const PumpingStationInp = () => {
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
const canEditGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const getCurrentRows = (): Record<string, string | number>[] =>
gridGetRowsRef.current?.() ?? rowData;
@@ -469,6 +472,7 @@ const PumpingStationInp = () => {
setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
setFooterVerificationResult(commonInfo.verificationResult ?? null);
if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) {
@@ -528,6 +532,7 @@ const PumpingStationInp = () => {
setStatusInfo(null);
setStatsCd("");
setIsVerificationFailed(false);
setFooterVerificationResult(null);
setIsNotApplicable(false);
setInputPeriodEnterable(true);
setSggOptions([]);
@@ -968,7 +973,7 @@ const PumpingStationInp = () => {
computeTabEnterable(statusInfo, inputPeriodEnterable);
const tabEnterableForGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const verificationQueryContext = (() => {
@@ -1060,6 +1065,7 @@ const PumpingStationInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={verificationCellTooltip}
/>
</div>
@@ -38,7 +38,11 @@ import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/colum
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -169,7 +173,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -622,6 +626,9 @@ const SewageExcretaRelatedInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
@@ -866,6 +873,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -909,6 +919,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -940,6 +953,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -974,6 +990,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -1005,6 +1024,7 @@ const SewageExcretaRelatedInp = () => {
setLoadedTabKeys(new Set());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map());
clearArticleOgnzSnTracking(
@@ -1055,6 +1075,7 @@ const SewageExcretaRelatedInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map());
@@ -1811,10 +1832,10 @@ const SewageExcretaRelatedInp = () => {
for (const tab of SewageExcretaRelatedInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -2013,8 +2034,8 @@ const SewageExcretaRelatedInp = () => {
const rowData = rowDataMap.get(tabKey) ?? [];
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
const tabEnterableForGrid =
computeTabEnterable(tabStatusInfo, inputPeriodEnterable) ||
isAdminInfo;
(computeTabEnterable(tabStatusInfo, inputPeriodEnterable) ||
isAdminInfo) && inputPeriodEnterable;
return (
<TabsContent key={tabKey} value={tabKey}>
@@ -2055,6 +2076,9 @@ const SewageExcretaRelatedInp = () => {
}
getGridRef={activeTab === tabKey ? getGridRef : undefined}
cellStyleCallback={resolveGridCellStyleCallback(tabKey)}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
/>
</TabsContent>
@@ -52,7 +52,11 @@ import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -355,7 +359,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk;
@@ -669,6 +673,9 @@ const SewageExcretaSlurgeInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -927,6 +934,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -964,6 +974,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -999,6 +1012,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -1039,6 +1055,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -1060,6 +1079,7 @@ const SewageExcretaSlurgeInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -1118,6 +1138,7 @@ const SewageExcretaSlurgeInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
loadedTabKeysRef.current.clear();
setLoadedTabKeys(new Set());
@@ -1876,10 +1897,10 @@ const SewageExcretaSlurgeInp = () => {
for (const tab of SewageExcretaSlurgeInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -2154,6 +2175,9 @@ const SewageExcretaSlurgeInp = () => {
activeTab === tabKey ? getGridRef : undefined
}
cellStyleCallback={resolveGridCellStyleCallback(tabKey)}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
onCellEdited={
tabKey === DigestorOperatingStatusInpColumn.tabKey
@@ -72,8 +72,10 @@ import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewa
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -435,7 +437,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -465,6 +467,9 @@ const SewagePipeInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */
@@ -566,6 +571,7 @@ const SewagePipeInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setLastSearchedYear(null);
setLastSearchedValues(null);
setHasSearched(false);
@@ -594,7 +600,7 @@ const SewagePipeInp = () => {
});
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
const canEdit =
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo;
(computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const applyEditable = (cols: ColumnConfig[] | undefined) =>
tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY
? applyPipeTypeStatusEditableColumns(
@@ -703,6 +709,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -747,6 +756,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -773,6 +785,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -800,6 +815,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -826,6 +844,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -859,6 +880,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -912,6 +936,7 @@ const SewagePipeInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setStatsCd("");
setToolbarSnapshotByTab(new Map());
}
@@ -2193,7 +2218,9 @@ const SewagePipeInp = () => {
}}
isExcelImport={excelToolbarEnabledForTab}
excelImportSkipNonEditable={
tabKey === PIPE_TYPE_STATUS_TAB_KEY ? !isAdminInfo : true
tabKey === PIPE_TYPE_STATUS_TAB_KEY
? !isAdminInfo
: false
}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
@@ -2224,6 +2251,9 @@ const SewagePipeInp = () => {
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
}}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
@@ -58,7 +58,11 @@ import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewa
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -128,7 +132,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk;
@@ -291,6 +295,9 @@ const SewagePipeMaintainInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -435,6 +442,9 @@ const SewagePipeMaintainInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -471,6 +481,9 @@ const SewagePipeMaintainInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -866,6 +879,7 @@ const SewagePipeMaintainInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
setLastSearchedValues(null);
@@ -911,6 +925,7 @@ const SewagePipeMaintainInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -986,10 +1001,10 @@ const SewagePipeMaintainInp = () => {
for (const tab of SewagePipeMaintainInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = tab.columns ?? [];
const base = canEdit
? rawColumns
@@ -1152,10 +1167,10 @@ const SewagePipeMaintainInp = () => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
@@ -1192,6 +1207,9 @@ const SewagePipeMaintainInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
@@ -45,7 +45,11 @@ import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageR
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import {
formatCompactYmdForGrid,
normalizeOxYnValue,
@@ -138,7 +142,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk;
@@ -937,6 +941,9 @@ const SewageReservoirPumpInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
useState(false);
@@ -1079,6 +1086,9 @@ const SewageReservoirPumpInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(values);
}
@@ -1124,6 +1134,9 @@ const SewageReservoirPumpInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(values);
}
@@ -1191,6 +1204,7 @@ const SewageReservoirPumpInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setLastSearchedValues(null);
setSggOptions([]);
@@ -1241,6 +1255,7 @@ const SewageReservoirPumpInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
loadedTabKeysRef.current.clear();
setActiveTab(firstTabKey);
@@ -1428,10 +1443,10 @@ const SewageReservoirPumpInp = () => {
for (const tab of SewageReservoirPumpInpTabList) {
const tabKey = tab.tabKey ?? "";
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
const isRainfallPumpTab = tabKey === rainfallPumpTabKey;
const rawColumns = tab.columns ?? [];
@@ -1596,10 +1611,10 @@ const SewageReservoirPumpInp = () => {
{SewageReservoirPumpInpTabList.map((tab) => {
const tabKey = tab.tabKey ?? "";
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
@@ -1658,6 +1673,9 @@ const SewageReservoirPumpInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
/>
</TabsContent>
@@ -63,7 +63,11 @@ import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewera
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
const SewerageFinanceInpTabList = [
@@ -133,7 +137,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -372,6 +376,9 @@ const SewerageFinanceInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -484,6 +491,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -510,6 +520,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -536,6 +549,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -562,6 +578,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -579,6 +598,7 @@ const SewerageFinanceInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -621,6 +641,7 @@ const SewerageFinanceInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -1139,10 +1160,10 @@ const SewerageFinanceInp = () => {
for (const tab of SewerageFinanceInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -1285,10 +1306,10 @@ const SewerageFinanceInp = () => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
@@ -1327,6 +1348,9 @@ const SewerageFinanceInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
@@ -51,6 +51,7 @@ import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input
import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -60,6 +61,28 @@ type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult;
};
/** commonInfo 푸터 필드명 → 그리드 fieldName */
const FOOTER_VERIFY_FIELD_ALIASES: Record<string, string> = {
sewerBzmnWholPpltnCnt: "pbadmsWholPpltnCnt",
sewerBzmnDomplWholPpltnCnt: "domplWholPpltnCnt",
sewerBzmnFrgnrWholPpltnCnt: "frgnrWholPpltnCnt",
sewerBzmnArea: "refArea",
};
const remapCommonInfoVerificationToGridFields = (
result: SewerageSupplyRateVerificationResult | null | undefined,
): VerificationResultLikeMap | null => {
if (!result) return null;
const remapped: VerificationResultLikeMap = {};
for (const [key, errors] of Object.entries(result)) {
const gridField = FOOTER_VERIFY_FIELD_ALIASES[key] ?? key;
remapped[gridField] = errors;
// API 키로도 조회 가능하도록 유지
if (gridField !== key) remapped[key] = errors;
}
return remapped;
};
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
@@ -137,6 +160,8 @@ const SeweragePenetrationRateInp = () => {
/** `engColNm` → `columnGuide` (헤더 hover) */
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const { confirm } = useConfirm();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
@@ -208,11 +233,14 @@ const SeweragePenetrationRateInp = () => {
(footerFromApi && footerFromApi[fieldName] != null ? footerFromApi[fieldName] : null) ?? getFooterSummary(grid, column),
});
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
// 관리자는 제출/승인 상태에서도 편집 가능. 단, 입력기간 외에는 불가.
const columnInfo = useMemo(() => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
const canEdit =
(isEnterable || isAdminInfo) && isInputPeriodCurrent(periodCheckList);
const base = canEdit
? columns
: columns.map((col) => ({ ...col, editable: false as const }));
const withHeaderTooltips = base.map((col) => {
const guide = columnGuidesByEngCol[col.fieldName]?.trim();
@@ -233,7 +261,7 @@ const SeweragePenetrationRateInp = () => {
return col;
}),
};
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]);
}, [isEnterable, isAdminInfo, periodCheckList, footerFromApi, columnGuidesByEngCol]);
const handleReset = () => {
setRowData([]);
@@ -242,6 +270,7 @@ const SeweragePenetrationRateInp = () => {
setLastSearchedYear(null);
setLastSearchedValues(null);
setFooterFromApi(null);
setFooterVerificationResult(null);
setVerificationSearchDefaults(null);
setIsVerificationFailed(false);
setSearchFormRemountKey((k) => k + 1);
@@ -278,6 +307,11 @@ const SeweragePenetrationRateInp = () => {
}
: null
);
setFooterVerificationResult(
remapCommonInfoVerificationToGridFields(
commonInfo.verificationResult,
),
);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
@@ -299,6 +333,7 @@ const SeweragePenetrationRateInp = () => {
setRowData([]);
setStatusInfo(null);
setFooterFromApi(null);
setFooterVerificationResult(null);
} finally {
setIsLoading(false);
}
@@ -755,7 +790,8 @@ const SeweragePenetrationRateInp = () => {
const inputPeriodCurrent = isInputPeriodCurrent(periodCheckList);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
const baseShowSaveInToolbar =
(isEnterable || isAdminInfo) && inputPeriodCurrent && gugunOkOrNotDoList;
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
const showSubmittedInInputPeriodToolbar =
isSubmitted && inputPeriodCurrent && gugunOkOrNotDoList;
@@ -805,7 +841,7 @@ const SeweragePenetrationRateInp = () => {
</div>
}
<DataGrid
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
key={((isEnterable || isAdminInfo) && inputPeriodCurrent) ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
@@ -826,6 +862,7 @@ const SeweragePenetrationRateInp = () => {
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
}
}}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
@@ -26,7 +26,11 @@ import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/se
import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -132,7 +136,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -304,6 +308,9 @@ const SewerageServiceLevelInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -420,6 +427,9 @@ const SewerageServiceLevelInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
@@ -455,6 +465,9 @@ const SewerageServiceLevelInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
@@ -478,6 +491,7 @@ const SewerageServiceLevelInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -523,6 +537,7 @@ const SewerageServiceLevelInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setIsNotApplicable(false);
activeTabRef.current = firstTabKey;
@@ -1001,10 +1016,10 @@ const SewerageServiceLevelInp = () => {
for (const tab of SewerageServiceLevelInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -1188,10 +1203,10 @@ const SewerageServiceLevelInp = () => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
@@ -1230,6 +1245,9 @@ const SewerageServiceLevelInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
@@ -42,8 +42,10 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { useTabStore } from "@/store/tab-store";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -173,7 +175,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard =
@@ -539,6 +541,9 @@ const WaterReuseInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -713,6 +718,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
@@ -741,6 +749,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
@@ -769,6 +780,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
@@ -799,6 +813,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
@@ -830,6 +847,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
@@ -887,6 +907,7 @@ const WaterReuseInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -934,6 +955,7 @@ const WaterReuseInp = () => {
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map());
setDeleteListByTab(new Map());
loadedTabKeysRef.current.clear();
@@ -1905,10 +1927,10 @@ const WaterReuseInp = () => {
for (const tab of WaterReuseInpTabList) {
const tabKey = tab.tabKey;
const canEdit =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit
? rawColumns
@@ -2138,10 +2160,10 @@ const WaterReuseInp = () => {
const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid =
computeTabEnterable(
(computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable,
) || isAdminInfo;
) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
@@ -2183,6 +2205,9 @@ const WaterReuseInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport
cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip}
getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined
@@ -29,6 +29,11 @@ export interface ReviewSubmitDialogProps {
onOpenChange: (open: boolean) => void;
context: ReviewerInfoRequest | null;
onSubmitted?: () => void | Promise<void>;
/**
* true면 검토의견·처리내역 UI를 숨기고
* submit body에 statsCd / splmntOpnnCn / wrterOpnnCn을 넣지 않음
*/
hideOpinionFields?: boolean;
}
export function ReviewSubmitDialog({
@@ -36,6 +41,7 @@ export function ReviewSubmitDialog({
onOpenChange,
context,
onSubmitted,
hideOpinionFields = false,
}: ReviewSubmitDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state);
const [reviewerInfo, setReviewerInfo] =
@@ -98,14 +104,20 @@ export function ReviewSubmitDialog({
setIsSubmitting(true);
const userInfo = await getUserInfo();
const submitParam: ReviewSubmitRequest = {
statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "",
};
const submitParam: ReviewSubmitRequest = hideOpinionFields
? {
statsYr: context.statsYr,
sewerBzmnCd: [context.sewerBzmnCd],
mdfrId: userInfo.userUnqId,
}
: {
statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "",
};
const response = await statisticsApi.submitReview(submitParam);
if (response.status !== "success") {
@@ -153,22 +165,26 @@ export function ReviewSubmitDialog({
<Label className="text-label">직책</Label>
<Input value={reviewerInfo?.cps ?? "-"} readOnly />
</div>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">검토의견</Label>
<Textarea
value={reviewerInfo?.splmntOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">처리내역</Label>
<Textarea
value={reviewerInfo?.wrterOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
{!hideOpinionFields && (
<>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">검토의견</Label>
<Textarea
value={reviewerInfo?.splmntOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">처리내역</Label>
<Textarea
value={reviewerInfo?.wrterOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
</>
)}
</div>
<DialogFooter>
<Button
@@ -6,6 +6,8 @@ import DataGrid from "@/components/module/DataGrid";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import SearchForm from "@/components/module/SearchForm";
import ApproveButton from "@/components/button/ApproveButton";
import SubmitButton from "@/components/button/SumitButton";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import { doList } from "@/components/util/column/input/DoList";
import {
Button,
@@ -18,9 +20,12 @@ import {
DataTable
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
import type {
ReviewerInfoRequest,
StatsInputStatusApproveItem,
} from "@/entities/statistics/model/statistics-interface";
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type {
@@ -55,6 +60,7 @@ const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
// 하수도 통계 검토 00. 통계 입력 현황
const StatsInputStatusRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const { confirm } = useConfirm();
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
@@ -63,6 +69,7 @@ const StatsInputStatusRvw = () => {
const [searchedYear, setSearchedYear] = useState<string>("");
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
const [historySearchInfo, setHistorySearchInfo] = useState<{
ctpvNm: string;
@@ -112,6 +119,7 @@ const StatsInputStatusRvw = () => {
setLastSearchedValues(null);
setHasCompletedSearch(false);
setIsApproveLoading(false);
setSubmitDialogOpen(false);
};
const handleSearch = async (values: SearchFormValues) => {
@@ -242,13 +250,74 @@ const StatsInputStatusRvw = () => {
[rowData],
);
const showActionToolbarCard =
/** 진행상태(prvn) 코드가 모두 제출완료(2) */
const allPrvnStatusSubmitted = useMemo(
() =>
rowData.length > 0 &&
rowData.every((row) => String(row.prvnStatusCd ?? "") === "2"),
[rowData],
);
const showSubmitButton =
hasCompletedSearch &&
rowData.length > 0 &&
gugunOkOrNotDoList &&
!allPrvnStatusApproved &&
!allPrvnStatusSubmitted;
const showApproveButton =
hasCompletedSearch &&
rowData.length > 0 &&
gugunOkOrNotDoList &&
allSubmitStatusApproved &&
!allPrvnStatusApproved;
const showActionToolbarCard = showSubmitButton || showApproveButton;
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchedValues == null) return null;
const statsYr = lastSearchedValues.year ?? "";
const sewerBzmnCd = lastSearchedValues.bsnmCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
instSeCd,
};
}, [lastSearchedValues, userInfo?.instSeCd]);
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const refreshListAfterSubmit = async () => {
if (!lastSearchedValues) return;
try {
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
ctpvNm: lastSearchedValues.sido,
sggNm: lastSearchedValues.gugun === "ALL" ? "" : lastSearchedValues.gugun,
statsYr: lastSearchedValues.year ?? "",
sewerBzmnCd: lastSearchedValues.bsnmCd,
});
if (listResponse.status === "success" && listResponse.data) {
const rawList = listResponse.data.sewerageInputList ?? [];
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
}
} catch {
/* 목록 갱신 실패는 별도 안내 없이 생략 */
}
};
const openReviewHistoryForRow = useCallback(
async (row: Record<string, string | number>) => {
if (!searchedYear) {
@@ -297,7 +366,10 @@ const StatsInputStatusRvw = () => {
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ApproveButton approveFunc={approveFunc} />
{showSubmitButton && (
<SubmitButton submitFunc={submitFunc} text="일괄 제출" />
)}
{showApproveButton && <ApproveButton approveFunc={approveFunc} />}
</div>
</div>
)}
@@ -357,6 +429,13 @@ const StatsInputStatusRvw = () => {
}
}}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
hideOpinionFields
onSubmitted={() => void refreshListAfterSubmit()}
/>
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
<DialogContent
className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4"
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ import {
Textarea,
Skeleton,
Input,
Checkbox,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
@@ -32,11 +33,13 @@ interface FomulaDialogProps {
data?: VerifyDetailResponseDto | null;
trgtStatsCd?: string;
trgtStatsNm?: string;
/** 바로 앞 수식 연산자가 like / !like 일 때 '사용자 직접 입력' 체크박스 표시 */
allowLikeDirectInput?: boolean;
/** 확인 클릭 시 검사 통과 후 호출. 해당 행에 반영 후 다이얼로그 닫기는 부모에서 처리 */
onConfirm?: (payload: FomulaDialogConfirmPayload) => void;
}
const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onConfirm }: FomulaDialogProps) => {
const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, allowLikeDirectInput = false, onConfirm }: FomulaDialogProps) => {
const [columnInfo, setColumnInfo] = useState<VerifyColumnListResponseData | null>(null);
const [selectedColumnInfo, setSelectedColumnInfo] = useState<SelectedVerifyColumnListResponseData | null>(null);
const [showCommonColumns, setShowCommonColumns] = useState<boolean>(true);
@@ -50,6 +53,8 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
/** 수식 = 토큰 배열. 한글명 중복 시 출처(공통/대상/참조)별 engColNm 보존 */
const [formulaDisplaySegments, setFormulaDisplaySegments] = useState<string[]>([]);
const [formulaValueSegments, setFormulaValueSegments] = useState<string[]>([]);
/** like / !like 수식에서 '사용자 직접 입력' 체크 여부 */
const [userDirectInput, setUserDirectInput] = useState(false);
/** 확인 클릭 시 검사 실패 메시지 (Textarea 아래 빨간 글씨) */
const [formulaSaveError, setFormulaSaveError] = useState<string | null>(null);
/** 참조 통계 변경 시 안내 메시지 */
@@ -57,6 +62,8 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
const formulaTextareaRef = useRef<HTMLTextAreaElement | null>(null);
const isUserDirectInputMode = allowLikeDirectInput && userDirectInput;
const { setMessage: alert } = useAlertStore((state) => state);
/** 다이얼로그에서 쓰는 기호 → 제출용 value. 백엔드 규격에 맞게 수정 가능 */
@@ -112,6 +119,36 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
return { display, value };
};
/** 직접 입력 저장값의 양쪽 백틱 제거 (편집용) */
const stripBacktickWrap = (value: string) => {
if (value.length >= 2 && value.startsWith("`") && value.endsWith("`")) {
return value.slice(1, -1);
}
return value;
};
/** 직접 입력 저장 시 백틱으로 감싸기 (이미 감싸진 경우 유지) */
const wrapWithBacktick = (value: string) => {
if (!value) return value;
if (value.startsWith("`") && value.endsWith("`")) return value;
return `\`${value}\``;
};
const handleUserDirectInputChange = (checked: boolean) => {
setFormulaSaveError(null);
setUserDirectInput(checked);
const current = formulaDisplaySegments.join("");
if (checked) {
const editable = stripBacktickWrap(current);
setFormulaDisplaySegments(editable ? [editable] : []);
setFormulaValueSegments(editable ? [editable] : []);
return;
}
const { display, value } = parseDisplayToSegments(current);
setFormulaDisplaySegments(display);
setFormulaValueSegments(value);
};
const handleFormulaKey = (val: string) => {
setFormulaSaveError(null);
if (val === 'backspace') {
@@ -257,6 +294,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
setRefStatsCdLocal('');
setFormulaDisplaySegments([]);
setFormulaValueSegments([]);
setUserDirectInput(false);
setFormulaSaveError(null);
setRefStatsChangeNotice(null);
}
@@ -264,12 +302,22 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
useEffect(() => {
if (open) {
const str = data?.kornArtopnEqtnCn ?? '';
const { display, value } = parseDisplayToSegments(str);
setFormulaDisplaySegments(display);
setFormulaValueSegments(value);
const raw = data?.kornArtopnEqtnCn ?? '';
const wasBacktickWrapped =
raw.length >= 2 && raw.startsWith("`") && raw.endsWith("`");
const str = stripBacktickWrap(raw);
if (wasBacktickWrapped && allowLikeDirectInput) {
setFormulaDisplaySegments(str ? [str] : []);
setFormulaValueSegments(str ? [str] : []);
setUserDirectInput(true);
} else {
const { display, value } = parseDisplayToSegments(str);
setFormulaDisplaySegments(display);
setFormulaValueSegments(value);
setUserDirectInput(false);
}
}
}, [open, data?.kornArtopnEqtnCn, data?.vrfcDtlSn]);
}, [open, data?.kornArtopnEqtnCn, data?.vrfcDtlSn, allowLikeDirectInput]);
/**
* 재오픈 직후에는 columnInfo가 아직 없어서 parseDisplayToSegments가 컬럼명을 매칭 못하고
@@ -278,8 +326,8 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
* engColNm 치환이 반영되도록 보정한다.
*/
useEffect(() => {
if (!open) return;
const src = data?.kornArtopnEqtnCn ?? '';
if (!open || isUserDirectInputMode) return;
const src = stripBacktickWrap(data?.kornArtopnEqtnCn ?? '');
if (!src) return;
const displayJoined = formulaDisplaySegments.join('');
@@ -318,12 +366,14 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
]);
useEffect(() => {
if (!open || columnInfo?.kornArtopnEqtnCn == null) return;
if (data?.kornArtopnEqtnCn !== columnInfo.kornArtopnEqtnCn) return;
const { display, value } = parseDisplayToSegments(columnInfo.kornArtopnEqtnCn);
if (!open || isUserDirectInputMode || columnInfo?.kornArtopnEqtnCn == null) return;
const strippedColumn = stripBacktickWrap(columnInfo.kornArtopnEqtnCn);
const strippedData = stripBacktickWrap(data?.kornArtopnEqtnCn ?? "");
if (strippedData !== strippedColumn) return;
const { display, value } = parseDisplayToSegments(strippedColumn);
setFormulaDisplaySegments(display);
setFormulaValueSegments(value);
}, [open, data?.kornArtopnEqtnCn, columnInfo?.kornArtopnEqtnCn]);
}, [open, data?.kornArtopnEqtnCn, columnInfo?.kornArtopnEqtnCn, isUserDirectInputMode]);
const handleChangeRefStatsCd = (next: string) => {
if (!open) return;
@@ -360,6 +410,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
if (!nextOpen) {
setFormulaDisplaySegments([]);
setFormulaValueSegments([]);
setUserDirectInput(false);
setFormulaSaveError(null);
setSelectedColumnInfo(null);
setRefStatsCdLocal('');
@@ -463,20 +514,44 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
</div>
</div>
<div className="w-full flex flex-col gap-2">
<h3 className="font-bold text-title text-[14px]">수식 미리보기</h3>
<div className="flex items-center gap-3">
<h3 className="font-bold text-title text-[14px]">수식 미리보기</h3>
{allowLikeDirectInput && (
<label className="flex items-center gap-2 text-[13px] text-gray-700 cursor-pointer select-none">
<Checkbox
id="userDirectInput"
checked={userDirectInput}
onCheckedChange={(checked) =>
handleUserDirectInputChange(checked === true)
}
/>
사용자 직접 입력
</label>
)}
</div>
<Textarea
className="w-full flex-1 bg-gray-100 resize-none !text-3xl p-4 !leading-loose text-gray-600 font-bold"
className={`w-full flex-1 resize-none !text-3xl p-4 !leading-loose font-bold ${
isUserDirectInputMode
? "bg-white text-gray-900"
: "bg-gray-100 text-gray-600"
}`}
id="formulaDisplayInput"
ref={formulaTextareaRef}
value={formulaDisplaySegments.join('')}
onChange={(e) => {
setFormulaSaveError(null);
if (isUserDirectInputMode) {
const raw = e.target.value;
setFormulaDisplaySegments(raw ? [raw] : []);
setFormulaValueSegments(raw ? [raw] : []);
return;
}
const { display, value } = parseDisplayToSegments(e.target.value);
setFormulaDisplaySegments(display);
setFormulaValueSegments(value);
}}
rows={4}
readOnly
readOnly={!isUserDirectInputMode}
/>
{formulaSaveError && (
<p className="text-sm text-red-600 mt-1" role="alert">
@@ -516,6 +591,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
value={c.engColNm}
size="sm"
color="lightGray"
disabled={isUserDirectInputMode}
onClick={() => handleFormulaColumn(c.kornColNm, c.engColNm)}
>
{c.kornColNm}
@@ -558,6 +634,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
type="button"
value={t.engColNm}
size="sm"
disabled={isUserDirectInputMode}
onClick={() => handleFormulaColumn(t.kornColNm, t.engColNm)}
>
{t.kornColNm}
@@ -619,6 +696,7 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
value={r.engColNm}
size="sm"
color="sky"
disabled={isUserDirectInputMode}
onClick={() => handleFormulaColumn(r.kornColNm, r.engColNm)}
>
{r.kornColNm}
@@ -636,6 +714,23 @@ const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onCo
size="lg"
type="button"
onClick={() => {
if (isUserDirectInputMode) {
setFormulaSaveError(null);
const raw = formulaDisplaySegments.join("");
const wrapped = wrapWithBacktick(raw);
const isEmptyFormula = raw.trim().length === 0;
onConfirm?.({
kornArtopnEqtnCn: isEmptyFormula ? raw : wrapped,
engArtopnEqtnCn: isEmptyFormula ? raw : wrapped,
refStatsCd: isEmptyFormula
? null
: refStatsCdLocal
? refStatsCdLocal
: null,
});
onOpenChange?.(false);
return;
}
const err = validateFormulaForSave(formulaValueSegments);
if (err) {
setFormulaSaveError(err);
@@ -27,6 +27,14 @@ const zoneClassName: Record<ClauseZone, string> = {
"zone-result": "bg-[#fff4ea]",
};
/** 저장용 백틱 래핑은 숨기고 사용자에게만 표시 */
const stripBacktickWrap = (value: string) => {
if (value.length >= 2 && value.startsWith("`") && value.endsWith("`")) {
return value.slice(1, -1);
}
return value;
};
const FormulaComponents = ({
data,
index,
@@ -38,6 +46,9 @@ const FormulaComponents = ({
handleFomulaDialog,
}: FormulaComponentsProps) => {
const cmprOpert = data?.cmprOpert;
const displayFormula = data
? stripBacktickWrap(data.kornArtopnEqtnCn ?? "")
: "";
return (
<div
@@ -56,8 +67,8 @@ const FormulaComponents = ({
className="h-[3.6rem] flex-1 !cursor-pointer rounded-md border-[#d8dce3] text-[13px]"
placeholder="클릭하여 수식을 등록해주세요."
id={`formulaInput-${index ?? 0}`}
value={data ? data.kornArtopnEqtnCn : ""}
title={data ? data.kornArtopnEqtnCn : ""}
value={displayFormula}
title={displayFormula}
onClick={() => handleFomulaDialog?.(true, data, index)}
readOnly
/>
@@ -20,12 +20,14 @@ interface OperatorsComponentsProps {
cmprOpert?: string | null;
onValueChange?: (value: string) => void;
placeholder?: string;
opertSeCd?: string;
}
const OperatorsComponents = ({
cmprOpert,
onValueChange,
placeholder = "연산자 없음",
opertSeCd = "opertSeCd=CMPR"
}: OperatorsComponentsProps) => {
const [operators, setOperators] = useState<OperatorItem[]>([]);
const [localValue, setLocalValue] = useState<string>(cmprOpert ?? "");
@@ -36,13 +38,13 @@ const OperatorsComponents = ({
useEffect(() => {
const fetchOperators = async () => {
const response = await generalverificationApiService.getVrfcOperators();
const response = await generalverificationApiService.getVrfcOperators(opertSeCd);
if (response.status === 'success' && response.data) {
setOperators(response.data);
}
};
fetchOperators();
}, []);
void fetchOperators();
}, [opertSeCd]);
// onValueChange가 있으면 부모가 제어(controlled), 없으면 내부 state로 표시
const isControlled = onValueChange !== undefined;
@@ -73,4 +75,4 @@ const OperatorsComponents = ({
);
};
export default OperatorsComponents;
export default OperatorsComponents;
+20 -2
View File
@@ -30,6 +30,7 @@ export type RealGridColumnInfo = {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
styleName?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
@@ -42,6 +43,7 @@ export type RealGridColumnInfo = {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
styleName?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
@@ -262,6 +264,7 @@ export type ColumnConfig = {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
styleName?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
@@ -275,6 +278,7 @@ export type ColumnConfig = {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
styleName?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
@@ -400,7 +404,7 @@ export type DataGridProps = {
/** 있으면보내기 파일명: `{메뉴명}_{suffix}.xlsx` (TabDataGrid의 탭 라벨 접미사와 동일) */
excelExportFileSuffix?: string;
isExcelImport?: boolean; // 엑셀 가져오기 유무
/** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */
/** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 false — 보내기한 값을 그대로 반영) */
excelImportSkipNonEditable?: boolean;
/** 엑셀 import 시 admdstCd 앞 2자리와 비교할 조회 sewerBzmnCd(SearchForm bsnmCd) */
excelImportSewerBzmnCd?: string;
@@ -418,6 +422,20 @@ export type DataGridProps = {
cellStyleCallback?: CellStyleCallback;
/** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */
cellTooltipCallback?: CellTooltipCallback;
/**
* commonInfo.verificationResult — 컬럼 fieldName(또는 name) 키로 총계 footer 강조/툴팁.
* 컬럼 footer.styleName이 있으면 그쪽이 우선.
* ※ 행 단위 「검증 오류 항목 우선보기」에는 포함되지 않음.
*/
footerVerificationResult?: Record<
string,
Array<{
vrfcCd: string;
rsnExpln: string;
nowVlCn?: string;
compVlCn?: string;
}>
> | null;
/** 셀 편집 완료 시 호출 (RealGrid onEditRowChanged) */
onCellEdited?: CellEditedCallback;
/** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
@@ -458,7 +476,7 @@ export type TabDataGridProps = {
columnInfo: ColumnInfoList[];
rows?: Record<string, string | number>[];
isExcelImport?: boolean; // 엑셀 가져오기 유무
/** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 true) */
/** 엑셀 가져오기 시 editable:false 컬럼 값 무시 (기본 false — 보내기한 값을 그대로 반영) */
excelImportSkipNonEditable?: boolean;
/** 엑셀 import 시 admdstCd 앞 2자리와 비교할 조회 sewerBzmnCd(SearchForm bsnmCd) */
excelImportSewerBzmnCd?: string;
@@ -190,7 +190,7 @@ export const RegionalFactStatusInpColumn = {
{ name: 'exlnOtflCnt', fieldName: 'exlnOtflCnt', header: { text: '우수토구(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
// 하수도대장 전산화
{ name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'sewerLdgrEdpPrlg', fieldName: 'sewerLdgrEdpPrlg', header: { text: '하수도대장전산화연장(m)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
{
name: 'sewerLdgrEdpRt',
fieldName: 'sewerLdgrEdpRt',
@@ -199,7 +199,7 @@ export const RegionalFactStatusInpColumn = {
editable: false,
styleName: 'rg-cell-total',
valueType: ValueType.NUMBER,
dataFormat: '#,##0.00',
dataFormat: '#,##0.0',
computed: {
numeratorFields: ['sewerLdgrEdpPrlg'],
denominatorFields: ['cbsysSqrePrlg', 'cbsysCiclPrlg', 'cbsysOpnPrlg', 'cbsysGttrPrlg', 'drtwPplSqrePrlg', 'drtwPplCiclPrlg', 'exlnPplSqrePrlg', 'exlnPplCiclPrlg', 'exlnPplOpnPrlg', 'exlnPplGttrPrlg'],
@@ -14,42 +14,58 @@ function truncateToFirstDecimal(value: number): number {
return Math.floor(value * 10) / 10;
}
/** computed sum용 스케일 (표시 최대 소수 3자리 + 여유) */
const SUM_DECIMAL_SCALE = 1e6;
/**
* IEEE 754 가산 잡음 제거.
* 통계 표시 자릿수(대개 0~3)보다 충분히 큰 자리로 반올림해
* 112.229999… → 112.23 처럼 만들어 RealGrid `#,##0.000` 표시 오차를 막는다.
* 스케일 정수 합을 Number로 되돌릴 때 생기는 IEEE 오차 보정.
* 예: 9003630000/1e6 → 9003.62999… (RealGrid trunc 시 9003.62)
* 아주 작은 양수를 더해 표시가 9003.63이 되게 한다.
*/
function sanitizeFloatNoise(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.round(value * 1e6) / 1e6;
function numberFromScaledSum(scaledSum: number): number {
if (scaledSum === 0) return 0;
const raw = scaledSum / SUM_DECIMAL_SCALE;
if (scaledSum % SUM_DECIMAL_SCALE === 0) return raw;
return raw + Math.sign(raw) * 1e-12;
}
/**
* 각 값을 소수 6자리 정수로 올린 뒤 더한 다음 다시 나눈다.
* 예: 2712.5 + 6291.13 → (2712500000 + 6291130000) / 1e6 = 9003.63
*/
function sumViaScaledIntegers(nums: number[]): number {
let scaledSum = 0;
for (const n of nums) {
if (!Number.isFinite(n)) continue;
scaledSum += Math.round(n * SUM_DECIMAL_SCALE);
}
return numberFromScaledSum(scaledSum);
}
/** 합계용 valueCallback 생성 */
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
let sum = 0;
const nums: number[] = [];
for (const f of fieldNames) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sum += toNum(values[idx]);
if (idx >= 0) nums.push(toNum(values[idx]));
}
return sanitizeFloatNoise(sum);
return sumViaScaledIntegers(nums);
};
}
/** 평균용 valueCallback 생성 */
export function createAvgValueCallback(fieldNames: string[]): DataFieldValueCallback {
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
let sum = 0;
let count = 0;
const nums: number[] = [];
for (const f of fieldNames) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) {
const val = toNum(values[idx]);
sum += val;
count += 1;
nums.push(toNum(values[idx]));
}
}
return count > 0 ? sum / count : 0;
if (nums.length === 0) return 0;
return sumViaScaledIntegers(nums) / nums.length;
};
}
@@ -58,12 +74,12 @@ function sumFieldValues(
fieldNamesArr: string[],
values: unknown[],
): number {
let sum = 0;
const nums: number[] = [];
for (const f of fields) {
const idx = fieldNamesArr.indexOf(f);
if (idx >= 0) sum += toNum(values[idx]);
if (idx >= 0) nums.push(toNum(values[idx]));
}
return sum;
return sumViaScaledIntegers(nums);
}
/** halfSumOfSums 한 건 — (sum(first) + sum(second)) / 2, 소수 내림 */
@@ -30,7 +30,7 @@ function flattenColumnLayoutNames(layout: ColumnLayoutItem[]): string[] {
}
export type ExcelImportColumnFieldNamesOptions = {
/** false면 editable:false 컬럼도 import (기본 true — 읽기전용·계산 컬럼은 엑셀 값 무시) */
/** false면 editable:false 컬럼도 import (기본 false — 보내기한 값을 그대로 반영) */
skipNonEditable?: boolean;
};
@@ -61,7 +61,8 @@ export function getExcelImportColumnFieldNames(
columnLayout?: ColumnLayoutItem[],
options?: ExcelImportColumnFieldNamesOptions,
): string[] {
const skipNonEditable = options?.skipNonEditable !== false;
/** true일 때만 읽기전용 컬럼 스킵. 기본(미지정/false)은 보내기 값 그대로 반영 */
const skipNonEditable = options?.skipNonEditable === true;
const excludedNames = new Set(
columns.filter((c) => c.excludeFromExcelExport).map((c) => c.name),
);
@@ -137,7 +138,10 @@ export function mergeExcelImportWithExistingRows(
const existingRow = existingRows[index];
if (!existingRow) return { ...importedRow };
const merged: Record<string, string | number> = { ...existingRow, ...importedRow };
const merged: Record<string, string | number> = {
...existingRow,
...importedRow,
};
if (preserveSet) {
for (const fieldName of preserveSet) {
const preserved = existingRow[fieldName];
@@ -146,6 +150,8 @@ export function mergeExcelImportWithExistingRows(
}
}
}
// 엑셀 반영 후에는 이전 검증오류 메타를 제거 (cellStyleCallback 스타일/툴팁 초기화)
delete (merged as { verificationResult?: unknown }).verificationResult;
return merged;
});
}
@@ -1,5 +1,7 @@
import type { StatisticsInputCommonInfoBase } from "@/entities/statistics/model/statistics-interface";
import type { VerificationResultLikeMap } from "./verification-result-display";
export function isVerificationFailedFromCommonInfo(
commonInfo: Pick<StatisticsInputCommonInfoBase, "isVerificationFailed"> | null | undefined,
): boolean {
@@ -15,3 +17,17 @@ export function patchVerificationFailedByTab(
next.set(tabKey, isVerificationFailedFromCommonInfo(commonInfo));
return next;
}
/** commonInfo.verificationResult → 탭별 총계 footer 검증 (우선보기 대상 아님) */
export function patchFooterVerificationByTab(
prev: Map<string, VerificationResultLikeMap | null>,
tabKey: string,
commonInfo:
| { verificationResult?: VerificationResultLikeMap | null }
| null
| undefined,
): Map<string, VerificationResultLikeMap | null> {
const next = new Map(prev);
next.set(tabKey, commonInfo?.verificationResult ?? null);
return next;
}
@@ -0,0 +1,63 @@
/** 셀/푸터 검증 오류 한 건 (입력 조회 verificationResult 항목) */
export type VerificationResultLikeItem = {
vrfcCd: string;
rsnExpln: string;
nowVlCn?: string;
compVlCn?: string;
};
export type VerificationResultLikeMap = Record<
string,
VerificationResultLikeItem[]
>;
/** 검증 오류 배열 → RealGrid styleName */
export function getVerificationStyleName(
errors: VerificationResultLikeItem[] | null | undefined,
): string | undefined {
if (!Array.isArray(errors) || errors.length === 0) return undefined;
const hasImr = errors.some(
(e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB",
);
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
/** 검증 오류 배열 → 툴팁/메시지 문구 */
export function formatVerificationTooltip(
errors: VerificationResultLikeItem[] | null | undefined,
): string | undefined {
if (!Array.isArray(errors) || errors.length === 0) return undefined;
const text = errors
.map((e) => {
const prefix =
e.vrfcCd === "IMR"
? "시계열 검증 - "
: e.vrfcCd === "NV"
? "일반검증 - "
: "모집단 검증 - ";
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln}\n(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
.filter(Boolean)
.join("\n");
return text.length > 0 ? text : undefined;
}
/** fieldName 및 alias 중 첫 매칭 오류 배열 */
export function getVerificationErrorsForField(
result: VerificationResultLikeMap | null | undefined,
fieldName: string,
aliases?: readonly string[],
): VerificationResultLikeItem[] | undefined {
if (!result) return undefined;
const direct = result[fieldName];
if (Array.isArray(direct) && direct.length > 0) return direct;
if (aliases) {
for (const alias of aliases) {
const found = result[alias];
if (Array.isArray(found) && found.length > 0) return found;
}
}
return undefined;
}
@@ -89,9 +89,9 @@ export class GeneralverificationApiService implements GeneralverificationService
return response.data;
}
public async getVrfcOperators(): Promise<VrfcOperatorsResponse> {
public async getVrfcOperators(param: string): Promise<VrfcOperatorsResponse> {
const response = await stClient.get<VrfcOperatorsResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`,
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators?${param}`,
);
return response.data;
}
@@ -279,7 +279,7 @@ export class StatisticsApiService implements StatisticsService {
const response = await stClient.post<StatsInputStatusApproveResponse>(
`${ENDPOINT.ST_SERVICE.STATS}/approve`,
param.list,
{ params: { type } },
withAuthHeader({ params: { type } }),
);
return response.data;
}
@@ -290,6 +290,7 @@ export class StatisticsApiService implements StatisticsService {
const response = await stClient.post<ReviewSubmitResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/submit`,
param,
withAuthHeader(),
);
return response.data;
}
@@ -112,6 +112,8 @@ export interface VerifyDetailResponseDto {
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
cmprOpert: string | null;
/** group by 여부 (Y/N) */
grpByYn: string | null;
}
export interface VerifyDetailData {
@@ -175,6 +177,8 @@ export interface VerifyDetailRegistData {
engArtopnEqtnCn: string;
cmprOpert: string | null;
eqtnCombSeq: number;
/** group by 여부 (Y/N) */
grpByYn: string;
}
export interface VerifyDetailRegistRequest {
@@ -280,7 +284,7 @@ export interface GeneralverificationService {
vrfcSn: number,
userUnqId: string,
): Promise<DeleteVrfcMasterResponse>;
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
getVrfcOperators(param: string): Promise<VrfcOperatorsResponse>;
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
getVerifyColumnList(
param: VerifyColumnListRequest,
@@ -1,5 +1,16 @@
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 검증 결과 한 건 (필드별 메시지) */
export interface VerificationResultItem {
vrfcCd: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string;
}
/** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */
export type SewerageSupplyRateVerificationResult = Record<string, VerificationResultItem[]>;
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateCommonInfo extends StatisticsInputCommonInfoBase {
sewerBzmnArea: number;
@@ -12,19 +23,10 @@ export interface SewerageSupplyRateCommonInfo extends StatisticsInputCommonInfoB
ctpvNm: string;
statsYr: string;
statsCd: string;
/** 푸터(합계) 등 commonInfo 필드 검증 결과 */
verificationResult?: SewerageSupplyRateVerificationResult | null;
}
/** 검증 결과 한 건 (필드별 메시지) */
export interface VerificationResultItem {
vrfcCd: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string;
}
/** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */
export type SewerageSupplyRateVerificationResult = Record<string, VerificationResultItem[]>;
/** 하수도 보급률 목록 API 응답 한 행 (실제 응답 필드만) */
export interface SewerageSupplyRateItem {
admdstCd: string;
@@ -378,11 +378,13 @@ export interface ReviewInputSaveRequest {
export interface ReviewSubmitRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
/** 항목 단위 제출: string / 통계 입력 현황 일괄 제출: string[] */
sewerBzmnCd: string | string[];
mdfrId: string;
splmntOpnnCn: string;
wrterOpnnCn: string;
/** 통계항목 단위 제출 시 사용 (통계 입력 현황 일괄 제출에는 미사용) */
statsCd?: string;
splmntOpnnCn?: string;
wrterOpnnCn?: string;
}
export interface ReviewSubmitResponse {
@@ -435,7 +437,8 @@ export interface SubmitVerifyExceptionResponse {
export interface ReviewerInfoRequest {
sewerBzmnCd: string;
statsYr: string;
statsCd: string;
/** 통계항목 단위 조회 시 사용 (통계 입력 현황 일괄 제출에는 미사용) */
statsCd?: string;
instSeCd : string;
}