Merge remote-tracking branch 'origin/iipbs/st/dev'

This commit is contained in:
2026-07-02 18:31:43 +09:00
66 changed files with 1615 additions and 206 deletions
@@ -0,0 +1,24 @@
.verification-result-btn-wrap {
position: relative;
display: inline-flex;
}
.verification-result-btn-failed-dot {
position: absolute;
left: 0.35rem;
top: 0.35rem;
width: 0.55rem;
height: 0.55rem;
border-radius: 9999px;
background: #ff6c64;
box-shadow: 0 0 0 0 rgba(255, 108, 100, 0.75);
animation: verification-result-btn-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
pointer-events: none;
}
@keyframes verification-result-btn-dot-ping {
75%,
100% {
box-shadow: 0 0 0 0.5rem rgba(255, 108, 100, 0);
}
}
@@ -1,16 +1,34 @@
import { Button } from "@common/components";
import "./VerificationResultButton.css";
interface VerificationResultButtonProps {
VerificationResultButtonFunc: () => void;
isVerificationFailed?: boolean;
}
const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => {
const VerificationResultButton = ({
VerificationResultButtonFunc,
isVerificationFailed = false,
}: VerificationResultButtonProps) => {
return (
<Button color="white" className="h-[3.2rem]" onClick={VerificationResultButtonFunc}>
<i className="diveicon di-list" />
<span>지난 검증 결과</span>
</Button>
<span className="verification-result-btn-wrap">
<Button
color="white"
className="h-[3.2rem]"
onClick={VerificationResultButtonFunc}
>
<i className="diveicon di-list" />
<span>지난 검증 결과</span>
</Button>
{isVerificationFailed && (
<span
className="verification-result-btn-failed-dot"
aria-hidden
/>
)}
</span>
);
};
export default VerificationResultButton;
export default VerificationResultButton;
+24
View File
@@ -166,6 +166,30 @@
@apply !bg-white !text-[#333];
}
/* 하수관로 검토: CURR 구분 이후 미입력 — OLD 있음(노랑) / OLD 없음(하늘) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-warn.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-warn {
@apply !bg-[#feee85] !text-[#333];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-review-curr-empty-new.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new,
.rg-grid .rg-body .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-rowtype-curr.rg-cell-review-curr-empty-new {
@apply !bg-[#bae6fd] !text-[#333];
}
/*
* 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용.
* 클래스 이중화로 specificity를 한 단계 올려 동일 specificity의 다른 셀 클래스
+35 -17
View File
@@ -39,10 +39,10 @@ function mergeCellStyleCallbackResult(
return baseStyleName ? { styleName: baseStyleName } : {};
}
if (typeof extra === "string") {
const styleName = [extra, baseStyleName].filter(Boolean).join(" ");
const styleName = [baseStyleName, extra].filter(Boolean).join(" ");
return styleName ? { styleName } : {};
}
const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" ");
const styleName = [baseStyleName, extra.styleName].filter(Boolean).join(" ");
return {
...extra,
...(styleName ? { styleName } : {}),
@@ -812,13 +812,24 @@ useEffect(() => {
useLayoutEffect(() => {
const gv = gridRef.current?.gridView;
if (!gv || !cellStyleCallback) return;
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
(gv as { refresh?: () => void }).refresh?.();
const apply = () => {
applyCellStyleCallbacksToColumns(
gv,
columns,
rowDataRef,
cellStyleCallbackRef,
);
(gv as { refresh?: () => void }).refresh?.();
};
apply();
// footer setColumns / setColumnLayout 비동기 처리 이후 styleCallback 재적용
const timerId = window.setTimeout(apply, 300);
return () => {
window.clearTimeout(timerId);
};
}, [cellStyleCallback, columns, rowData]);
const { setMessage: alert } = useAlertStore((state) => state);
@@ -1416,6 +1427,13 @@ useEffect(() => {
| ((layout: unknown) => void)
| undefined
)?.(safeLayout);
applyCellStyleCallbacksToColumns(
gridRef.current.gridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
gridRef.current.gridView.refresh?.();
} catch (e) {
console.warn("setColumnLayout skipped due to invalid columns:", e);
}
@@ -1482,16 +1500,16 @@ useEffect(() => {
col,
),
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
applyCellStyleCallbacksToColumns(
currentGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
scheduleFooterTask(() => {
@@ -1535,16 +1553,16 @@ useEffect(() => {
);
}
});
applyFooterSettings(liveGridView);
if (hasFooterUserSpans) {
liveGridView.setColumnLayout?.(columnLayout);
}
applyCellStyleCallbacksToColumns(
liveGridView,
columns,
rowDataRef,
cellStyleCallbackRef,
);
applyFooterSettings(liveGridView);
if (hasFooterUserSpans) {
liveGridView.setColumnLayout?.(columnLayout);
}
liveGridView.refresh?.();
}, 50);
} catch (e) {
@@ -38,6 +38,7 @@ import type {
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 { 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";
@@ -59,6 +60,8 @@ import type {
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -325,6 +328,7 @@ const ExcretaTreatmentFacilityInp = () => {
const [hasSearched, setHasSearched] = useState(false);
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -454,11 +458,17 @@ const ExcretaTreatmentFacilityInp = () => {
excretaTreatmentFacilityList,
} = response.data;
const list = excretaTreatmentFacilityList ?? [];
const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm);
setRowData(mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm));
setRowData(gridRows);
setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd);
setIsNotApplicable(false);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicable(false);
}
setSearchLoaded(true);
return true;
};
@@ -512,6 +522,7 @@ const ExcretaTreatmentFacilityInp = () => {
setHasSearched(false);
setStatusInfo(null);
setStatsCd("");
setIsVerificationFailed(false);
setIsNotApplicable(false);
setInputPeriodEnterable(true);
setSggOptions([]);
@@ -974,10 +985,18 @@ const ExcretaTreatmentFacilityInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY,
onAutoSearch: (values) => handleSearch(values),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
@@ -988,6 +1007,7 @@ const ExcretaTreatmentFacilityInp = () => {
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={isVerificationFailed}
/>
{showEnterableToolbarButtons && (
<>
@@ -3,6 +3,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/api/input/statistics-input-private-sewage-facility-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -53,6 +55,7 @@ import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/inpu
import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn";
import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
const PrivateSewageFacilityInpTabList = [
@@ -338,6 +341,9 @@ const PrivateSewageFacilityInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -462,6 +468,9 @@ const PrivateSewageFacilityInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -487,6 +496,9 @@ const PrivateSewageFacilityInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -512,6 +524,9 @@ const PrivateSewageFacilityInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -528,6 +543,7 @@ const PrivateSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -539,7 +555,10 @@ const PrivateSewageFacilityInp = () => {
periodEnterableRef.current = true;
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -566,11 +585,17 @@ const PrivateSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
@@ -578,7 +603,7 @@ const PrivateSewageFacilityInp = () => {
searchParamRef.current = searchParam;
await loadActiveTabData(
activeTabRef.current,
opts?.forcedTabKey ?? activeTabRef.current,
searchParam,
values,
!yearChangedFromLastSearch,
@@ -1131,10 +1156,23 @@ const PrivateSewageFacilityInp = () => {
}
};
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
@@ -1144,6 +1182,9 @@ const PrivateSewageFacilityInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -3,6 +3,8 @@ import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from
import { useConfirm } from '@frontend-opensource/use-react-hooks';
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib';
import { STATISTICS_INPUT_URL_PATH } from '@/shared/lib/stats-cd-input-navigation';
import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics-input-pending-navigation';
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
@@ -46,6 +48,7 @@ import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
import { useAdminStore } from '@/store/admin-store';
type GridRowWithVerification = Record<string, string | number> & {
@@ -165,6 +168,9 @@ const PublicSewageFacilityInp = () => {
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
@@ -685,8 +691,10 @@ const PublicSewageFacilityInp = () => {
if (list.length === 0) {
setShowIsNotApplicable(true);
setIsNotApplicable(true);
} else {
setShowIsNotApplicable(false);
setIsNotApplicable(false);
}
const gridRows = toGridRows(convertedData);
@@ -700,6 +708,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('ST00060001', gridRows);
@@ -744,6 +755,9 @@ const PublicSewageFacilityInp = () => {
next.set(tabKey, statsCd);
return next;
});
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set('MainFacility', leftGridRows);
@@ -874,7 +888,10 @@ const PublicSewageFacilityInp = () => {
}
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
return;
@@ -902,6 +919,7 @@ const PublicSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -910,16 +928,17 @@ const PublicSewageFacilityInp = () => {
setLastSearchedValues(values);
searchDoneRef.current = true;
setHasSearched(true);
setIsNotApplicableByTab((prev) => {
const next = new Map(prev);
next.set('ST00060001', false);
return next;
});
setIsLoading(true);
const param = buildPublicSewageFacilityParams(values);
searchParamRef.current = param;
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
const tabKeyToLoad =
opts?.forcedTabKey ??
(yearChangedFromLastSearch ? firstTabKey : activeTabRef.current);
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
await loadPublicSewageFacilityActiveTabData(tabKeyToLoad, param, values);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
@@ -963,6 +982,7 @@ const PublicSewageFacilityInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setInputPeriodEnterable(true);
@@ -1138,10 +1158,23 @@ const PublicSewageFacilityInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
@@ -1153,6 +1186,9 @@ const PublicSewageFacilityInp = () => {
{!isMainFacilityTab && (
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
)}
{showEnterableToolbarButtons && !isMainFacilityTab && (
@@ -38,6 +38,7 @@ import type {
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 { 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";
@@ -58,6 +59,8 @@ import type {
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -320,6 +323,7 @@ const PumpingStationInp = () => {
const [hasSearched, setHasSearched] = useState(false);
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -445,11 +449,18 @@ const PumpingStationInp = () => {
const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } =
response.data;
const list = pumpingStationList ?? [];
const gridRows = mapPumpingStationListToGridRows(list, commonInfo.ctpvNm);
setRowData(mapPumpingStationListToGridRows(list, commonInfo.ctpvNm));
setPumpingStationDeleteList([]);
setRowData(gridRows);
setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd);
setIsNotApplicable(false);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicable(false);
}
setSearchLoaded(true);
return true;
};
@@ -503,6 +514,7 @@ const PumpingStationInp = () => {
setHasSearched(false);
setStatusInfo(null);
setStatsCd("");
setIsVerificationFailed(false);
setIsNotApplicable(false);
setInputPeriodEnterable(true);
setSggOptions([]);
@@ -957,10 +969,18 @@ const PumpingStationInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.PUMPING_STATION,
onAutoSearch: (values) => handleSearch(values),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
@@ -971,6 +991,7 @@ const PumpingStationInp = () => {
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={isVerificationFailed}
/>
{showEnterableToolbarButtons && (
<>
@@ -35,6 +35,7 @@ import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/co
import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn";
import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
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";
@@ -61,6 +62,8 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -613,6 +616,9 @@ const SewageExcretaRelatedInp = () => {
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [loadedTabKeys, setLoadedTabKeys] = useState<Set<string>>(new Set());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(),
);
@@ -823,7 +829,16 @@ const SewageExcretaRelatedInp = () => {
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
const fromPreYear = overrides?.mode === "PRE";
setIsNotApplicableForTab(tabKey, false);
const applyNotApplicableForLoadedTab = (
gridRows: readonly Record<string, string | number>[],
) => {
if (!NOT_APPLICABLE_TAB_KEYS.has(tabKey)) return;
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
};
if (tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey) {
const response =
@@ -852,6 +867,9 @@ const SewageExcretaRelatedInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -859,6 +877,7 @@ const SewageExcretaRelatedInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -891,6 +910,9 @@ const SewageExcretaRelatedInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -898,6 +920,7 @@ const SewageExcretaRelatedInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -918,6 +941,9 @@ const SewageExcretaRelatedInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -949,6 +975,9 @@ const SewageExcretaRelatedInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -979,6 +1008,7 @@ const SewageExcretaRelatedInp = () => {
loadedTabKeysRef.current.clear();
setLoadedTabKeys(new Set());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map());
clearArticleOgnzSnTracking(
@@ -990,7 +1020,10 @@ const SewageExcretaRelatedInp = () => {
setVerificationHistoryOpen(false);
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -1025,6 +1058,7 @@ const SewageExcretaRelatedInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map());
@@ -1054,8 +1088,13 @@ const SewageExcretaRelatedInp = () => {
return;
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
const ok = await loadActiveTabData(
activeTabRef.current,
opts?.forcedTabKey ?? activeTabRef.current,
searchParam,
values,
!yearChangedFromLastSearch,
@@ -1894,10 +1933,23 @@ const SewageExcretaRelatedInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
@@ -1907,6 +1959,9 @@ const SewageExcretaRelatedInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -49,6 +49,7 @@ import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/colu
import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
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";
@@ -70,6 +71,8 @@ import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statisti
import type { ApiResponse, cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, Sgg } from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -660,6 +663,9 @@ const SewageExcretaSlurgeInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -895,7 +901,15 @@ const SewageExcretaSlurgeInp = () => {
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
const fromPreYear = overrides?.mode === "PRE";
setIsNotApplicableForTab(tabKey, false);
const applyNotApplicableForLoadedTab = (
gridRows: readonly Record<string, string | number>[],
) => {
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
};
if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) {
const response =
@@ -914,6 +928,9 @@ const SewageExcretaSlurgeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -921,6 +938,7 @@ const SewageExcretaSlurgeInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -947,6 +965,9 @@ const SewageExcretaSlurgeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -954,6 +975,7 @@ const SewageExcretaSlurgeInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -978,6 +1000,9 @@ const SewageExcretaSlurgeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -985,6 +1010,7 @@ const SewageExcretaSlurgeInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -1014,6 +1040,9 @@ const SewageExcretaSlurgeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(
tabKey,
@@ -1021,6 +1050,7 @@ const SewageExcretaSlurgeInp = () => {
gridRows.length > 0,
searchContext,
);
applyNotApplicableForLoadedTab(gridRows);
markTabLoaded(tabKey);
return true;
}
@@ -1033,6 +1063,7 @@ const SewageExcretaSlurgeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -1056,7 +1087,10 @@ const SewageExcretaSlurgeInp = () => {
sggOptionsSidoRef.current = null;
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -1087,6 +1121,7 @@ const SewageExcretaSlurgeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
loadedTabKeysRef.current.clear();
setLoadedTabKeys(new Set());
@@ -1113,13 +1148,20 @@ const SewageExcretaSlurgeInp = () => {
return;
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
const ok = await loadActiveTabData(
activeTabRef.current,
opts?.forcedTabKey ?? activeTabRef.current,
searchParam,
values,
!yearChangedFromLastSearch,
true,
buildEmptyDataAlertOverrides(activeTabRef.current),
buildEmptyDataAlertOverrides(
opts?.forcedTabKey ?? activeTabRef.current,
),
);
if (!ok) return;
} catch {
@@ -1988,10 +2030,23 @@ const SewageExcretaSlurgeInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName
onSubmit={handleSearch}
onReset={handleReset}
@@ -2002,6 +2057,9 @@ const SewageExcretaSlurgeInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -69,6 +69,9 @@ import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePi
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -360,6 +363,9 @@ const SewagePipeInp = () => {
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */
@@ -470,6 +476,7 @@ const SewagePipeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setLastSearchedYear(null);
setLastSearchedValues(null);
setHasSearched(false);
@@ -582,6 +589,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020001', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -623,6 +633,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020002', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -646,6 +659,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -670,6 +686,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020004', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -693,6 +712,9 @@ const SewagePipeInp = () => {
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -723,6 +745,9 @@ const SewagePipeInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values);
commitToolbarSnapshotForLoadedTab(
@@ -774,6 +799,7 @@ const SewagePipeInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setStatsCd("");
setToolbarSnapshotByTab(new Map());
}
@@ -1896,6 +1922,9 @@ const SewagePipeInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -56,6 +56,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -291,6 +292,9 @@ const SewagePipeMaintainInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -441,6 +445,9 @@ const SewagePipeMaintainInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -474,6 +481,9 @@ const SewagePipeMaintainInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(searchContext);
}
@@ -868,6 +878,7 @@ const SewagePipeMaintainInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
setLastSearchedValues(null);
@@ -912,6 +923,7 @@ const SewagePipeMaintainInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
@@ -1109,6 +1121,9 @@ const SewagePipeMaintainInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -42,6 +42,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import {
formatCompactYmdForGrid,
normalizeOxYnValue,
@@ -69,6 +70,8 @@ import type {
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -926,6 +929,9 @@ const SewageReservoirPumpInp = () => {
Map<string, TabToolbarSnapshot>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
useState(false);
@@ -1046,8 +1052,7 @@ const SewageReservoirPumpInp = () => {
updateLastSearchedValues: boolean,
overrides?: { mode?: string },
): Promise<boolean> => {
// 서버 데이터로 새로 불러오는 시점이므로 현재 탭의 "해당없음" 체크는 초기화
setIsNotApplicableForTab(tabKey, false);
const fromPreYear = overrides?.mode === "PRE";
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
const response =
await statisticsInputSewageReservoirPumpApi.getSewageReservoirList(
@@ -1072,6 +1077,9 @@ const SewageReservoirPumpInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(values);
}
@@ -1081,6 +1089,11 @@ const SewageReservoirPumpInp = () => {
gridRows.length > 0,
values,
);
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -1109,6 +1122,9 @@ const SewageReservoirPumpInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) {
setLastSearchedValues(values);
}
@@ -1118,6 +1134,11 @@ const SewageReservoirPumpInp = () => {
gridRows.length > 0,
values,
);
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -1170,6 +1191,7 @@ const SewageReservoirPumpInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setLastSearchedValues(null);
setSggOptions([]);
@@ -1183,7 +1205,10 @@ const SewageReservoirPumpInp = () => {
setActiveTab(firstTabKey);
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -1216,6 +1241,7 @@ const SewageReservoirPumpInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
loadedTabKeysRef.current.clear();
setActiveTab(firstTabKey);
@@ -1233,8 +1259,13 @@ const SewageReservoirPumpInp = () => {
return;
}
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
if (yearChangedFromLastSearch) {
const tabKeyToLoad =
opts?.forcedTabKey ??
(yearChangedFromLastSearch ? firstTabKey : activeTabRef.current);
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
} else if (yearChangedFromLastSearch) {
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
@@ -1484,10 +1515,23 @@ const SewageReservoirPumpInp = () => {
};
})();
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
fctName={true}
onSubmit={handleSearch}
onReset={handleReset}
@@ -1498,6 +1542,9 @@ const SewageReservoirPumpInp = () => {
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -3,6 +3,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/input/statistics-input-sewerage-finance-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -58,6 +60,7 @@ import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerage
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn";
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
const SewerageFinanceInpTabList = [
@@ -367,6 +370,9 @@ const SewerageFinanceInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -482,6 +488,9 @@ const SewerageFinanceInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -505,6 +514,9 @@ const SewerageFinanceInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -528,6 +540,9 @@ const SewerageFinanceInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -551,6 +566,9 @@ const SewerageFinanceInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true;
@@ -567,6 +585,7 @@ const SewerageFinanceInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -578,7 +597,10 @@ const SewerageFinanceInp = () => {
periodEnterableRef.current = true;
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -605,11 +627,17 @@ const SewerageFinanceInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
@@ -617,7 +645,7 @@ const SewerageFinanceInp = () => {
searchParamRef.current = searchParam;
await loadActiveTabData(
activeTabRef.current,
opts?.forcedTabKey ?? activeTabRef.current,
searchParam,
values,
!yearChangedFromLastSearch,
@@ -1194,10 +1222,23 @@ const SewerageFinanceInp = () => {
}
};
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
@@ -1207,6 +1248,9 @@ const SewerageFinanceInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -49,6 +49,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
@@ -131,6 +132,7 @@ const SeweragePenetrationRateInp = () => {
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [isVerificationFailed, setIsVerificationFailed] = useState(false);
/** `engColNm` → `columnGuide` (헤더 hover) */
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
@@ -240,6 +242,7 @@ const SeweragePenetrationRateInp = () => {
setLastSearchedValues(null);
setFooterFromApi(null);
setVerificationSearchDefaults(null);
setIsVerificationFailed(false);
setSearchFormRemountKey((k) => k + 1);
};
@@ -275,6 +278,7 @@ const SeweragePenetrationRateInp = () => {
: null
);
setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (statusInfo && statusInfo.laeSbmsnSttsCd) {
if (['2', '3'].includes(statusInfo.laeSbmsnSttsCd)) {
setIsEnterable(false);
@@ -779,6 +783,7 @@ const SeweragePenetrationRateInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={isVerificationFailed}
/>
{showEnterableToolbarButtons && (
<>
@@ -23,6 +23,7 @@ import { doList } from "@/components/util/column/input/DoList";
import { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn";
import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
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";
@@ -44,6 +45,8 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -300,6 +303,9 @@ const SewerageServiceLevelInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -425,6 +431,9 @@ const SewerageServiceLevelInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
@@ -457,9 +466,17 @@ const SewerageServiceLevelInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
if (!fromPreYear) {
setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicable(false);
}
return true;
}
@@ -474,6 +491,7 @@ const SewerageServiceLevelInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -488,7 +506,10 @@ const SewerageServiceLevelInp = () => {
publicSewageFacilitiesAvailableRef.current = false;
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -515,12 +536,20 @@ const SewerageServiceLevelInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setIsNotApplicable(false);
activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey);
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current;
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
@@ -528,12 +557,12 @@ const SewerageServiceLevelInp = () => {
searchParamRef.current = searchParam;
await loadActiveTabData(
activeTabRef.current,
tabForLoad,
searchParam,
values,
!yearChangedFromLastSearch,
true,
buildEmptyDataAlertOverrides(activeTabRef.current),
buildEmptyDataAlertOverrides(tabForLoad),
);
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
@@ -1091,10 +1120,23 @@ const SewerageServiceLevelInp = () => {
}
};
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
@@ -1104,6 +1146,9 @@ const SewerageServiceLevelInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -38,6 +38,9 @@ import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/wat
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import {
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
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";
@@ -71,6 +74,8 @@ import type {
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store";
@@ -533,6 +538,9 @@ const WaterReuseInp = () => {
Map<string, SewerageStatusInfo | null>
>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean>
>(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot>
>(new Map());
@@ -677,8 +685,19 @@ const WaterReuseInp = () => {
overrides?: { mode?: string },
): Promise<boolean> => {
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
const fromPreYear = overrides?.mode === "PRE";
const applyNotApplicableForLoadedTab = (
gridRows: readonly Record<string, string | number>[],
) => {
if (!NO_APPLICABLE_TAB_KEYS.has(tabKey)) return;
if (!fromPreYear) {
setIsNotApplicableForTab(tabKey, gridRows.length === 0);
} else if (gridRows.length > 0) {
setIsNotApplicableForTab(tabKey, false);
}
};
setIsNotApplicableForTab(tabKey, false);
if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
setDeleteListByTab((prev) => {
const next = new Map(prev);
@@ -706,8 +725,12 @@ const WaterReuseInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -730,8 +753,12 @@ const WaterReuseInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -754,8 +781,12 @@ const WaterReuseInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -780,8 +811,12 @@ const WaterReuseInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -807,8 +842,12 @@ const WaterReuseInp = () => {
});
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows);
loadedTabKeysRef.current.add(tabKey);
return true;
}
@@ -862,6 +901,7 @@ const WaterReuseInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey;
@@ -878,7 +918,10 @@ const WaterReuseInp = () => {
sggOptionsSidoRef.current = null;
};
const handleSearch = async (values: SearchFormValues) => {
const handleSearch = async (
values: SearchFormValues,
opts?: { forcedTabKey?: string },
) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
@@ -905,6 +948,7 @@ const WaterReuseInp = () => {
setRowDataMap(new Map());
setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map());
setToolbarSnapshotByTab(new Map());
setDeleteListByTab(new Map());
loadedTabKeysRef.current.clear();
@@ -912,6 +956,13 @@ const WaterReuseInp = () => {
setActiveTab(firstTabKey);
}
if (opts?.forcedTabKey) {
activeTabRef.current = opts.forcedTabKey;
setActiveTab(opts.forcedTabKey);
}
const tabForLoad = opts?.forcedTabKey ?? activeTabRef.current;
setIsLoading(true);
searchDoneRef.current = true;
setHasSearched(true);
@@ -920,13 +971,13 @@ const WaterReuseInp = () => {
const loadedSgg = await loadSggOptions(values.sido);
sggOptionsSidoRef.current = values.sido;
if (ARTICLE_EDIT_TAB_KEYS.has(activeTabRef.current) && loadedSgg.length === 0) {
if (ARTICLE_EDIT_TAB_KEYS.has(tabForLoad) && loadedSgg.length === 0) {
alert("시군구 목록을 불러오지 못했습니다.");
return;
}
await loadActiveTabData(
activeTabRef.current,
tabForLoad,
searchParam,
values,
!yearChangedFromLastSearch,
@@ -2009,10 +2060,23 @@ const WaterReuseInp = () => {
}
};
const { verificationSearchDefaults, searchFormRemountKey } =
useStatisticsInputPendingNavigation({
urlPath: STATISTICS_INPUT_URL_PATH.WATER_REUSE,
onPending: (pending) => {
activeTabRef.current = pending.tabKey;
setActiveTab(pending.tabKey);
},
onAutoSearch: (values, tabKey) =>
handleSearch(values, { forcedTabKey: tabKey }),
});
return (
<>
<div className="relative">
<SearchForm
key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchValues}
@@ -2022,6 +2086,9 @@ const WaterReuseInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={
isVerificationFailedByTab.get(activeTab) === true
}
/>
{showEnterableToolbarButtons && (
<>
@@ -165,6 +165,8 @@ const LogHistory = () => {
const logList = logListRaw.map((l) => ({
...l,
sggNm: String(l.sggNm ?? "").trim() || "-",
ctpvNm: String(l.ctpvNm ?? "").trim() || "-",
statsYr: String(l.statsYr ?? "").trim() || "-",
}));
setRowData(toGridRows(logList));
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { GridFitStyle } from "realgrid";
import {
@@ -15,17 +15,25 @@ import {
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type {
ColumnInfo,
DataCellClickCallbackParams,
} from "@/components/type/DataGrid.type";
import {
alignLogDetailRowsToColumns,
buildLogHistoryDetailColumnInfo,
emptyLogHistoryDetailColumnInfo,
LOG_DETAIL_DETAILS_FIELD,
LOG_DETAIL_DETAILS_RAW_FIELD,
type LogDetailVerificationItem,
prepareLogDetailRowsForGrid,
} from "@/components/util/column/management/LogHistoryDetailColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface";
import { UserApi } from "@/entities/user";
import { useAlertStore } from "@/shared/lib";
import { LogHistoryDetailsNestedDialog } from "./LogHistoryDetailsNestedDialog";
export interface LogHistoryDetailDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -45,11 +53,19 @@ export function LogHistoryDetailDialog({
const [columnInfo, setColumnInfo] = useState<ColumnInfo>(
emptyLogHistoryDetailColumnInfo,
);
const [userName, setUserName] = useState("");
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedDetails, setSelectedDetails] = useState<
LogDetailVerificationItem[]
>([]);
useEffect(() => {
if (!open) {
setRowData([]);
setColumnInfo(emptyLogHistoryDetailColumnInfo);
setUserName("");
setDetailsDialogOpen(false);
setSelectedDetails([]);
setIsLoading(false);
return;
}
@@ -57,23 +73,42 @@ export function LogHistoryDetailDialog({
if (!row) return;
const sn = row.statsLogSn;
const rgtrId =
row.rgtrId != null && String(row.rgtrId).trim() !== ""
? String(row.rgtrId).trim()
: "";
let cancelled = false;
void (async () => {
setIsLoading(true);
setRowData([]);
setUserName("");
try {
const response = await statisticsApi.getLogDetail(
sn !== undefined && sn !== null ? String(sn) : "",
);
const userLookupPromise =
rgtrId !== ""
? UserApi.findByUserUnqId(rgtrId).catch(() => {
return null;
})
: Promise.resolve(null);
const [response, userResponse] = await Promise.all([
statisticsApi.getLogDetail(
sn !== undefined && sn !== null ? String(sn) : "",
),
userLookupPromise,
]);
if (userResponse) {
setUserName(userResponse.userName ?? "");
}
if (cancelled) return;
if (response.status !== "success") {
alert(
response.message ??
"로그 상세 정보를 불러오는데 실패했습니다.",
"로그 상세 정보를 불러오는데 실패했습니다.",
);
return;
}
@@ -83,7 +118,7 @@ export function LogHistoryDetailDialog({
: [];
const nextColumnInfo = buildLogHistoryDetailColumnInfo(list);
const nextRows = alignLogDetailRowsToColumns(
toGridRows(list as StatisticsLogDetailRow[]),
prepareLogDetailRowsForGrid(list),
nextColumnInfo,
);
setColumnInfo(nextColumnInfo);
@@ -102,6 +137,25 @@ export function LogHistoryDetailDialog({
};
}, [open, row, alert]);
const handleDataCellClick = useCallback(
({ fieldName, row }: DataCellClickCallbackParams) => {
if (fieldName !== LOG_DETAIL_DETAILS_FIELD) return;
const raw = row[LOG_DETAIL_DETAILS_RAW_FIELD];
if (raw == null || String(raw).trim() === "") return;
try {
const parsed = JSON.parse(String(raw)) as unknown;
if (!Array.isArray(parsed) || parsed.length === 0) return;
setSelectedDetails(parsed as LogDetailVerificationItem[]);
setDetailsDialogOpen(true);
} catch {
alert("상세 데이터를 불러오는데 실패했습니다.");
}
},
[alert],
);
const gridRemountKey = useMemo(
() => columnInfo.columns.map((c) => c.fieldName).join("\u0001"),
[columnInfo],
@@ -110,13 +164,14 @@ export function LogHistoryDetailDialog({
const showGrid = !isLoading && rowData.length > 0;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-[80%]"
aria-describedby={undefined}
>
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-[80%]"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>로그 상세 정보</DialogTitle>
<DialogTitle>로그 상세 정보(기능 실행자: {userName})</DialogTitle>
</DialogHeader>
<div className="relative min-h-[32rem]">
{isLoading && (
@@ -136,7 +191,8 @@ export function LogHistoryDetailDialog({
isExcelExport={false}
enableSort={false}
gridHeight={55}
fitStyle={GridFitStyle.NONE}
fitStyle={GridFitStyle.EVEN}
onDataCellClick={handleDataCellClick}
/>
) : (
!isLoading && (
@@ -153,7 +209,13 @@ export function LogHistoryDetailDialog({
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
<LogHistoryDetailsNestedDialog
open={detailsDialogOpen}
onOpenChange={setDetailsDialogOpen}
items={selectedDetails}
/>
</>
);
}
@@ -0,0 +1,98 @@
"use client";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@common/components";
import type { LogDetailVerificationItem } from "@/components/util/column/management/LogHistoryDetailColumn";
export interface LogHistoryDetailsNestedDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
items: LogDetailVerificationItem[];
}
export function LogHistoryDetailsNestedDialog({
open,
onOpenChange,
items,
}: LogHistoryDetailsNestedDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-[72rem]"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>검증 상세</DialogTitle>
</DialogHeader>
<div className="max-h-[60vh] space-y-4 overflow-y-auto pr-1">
{items.length === 0 ? (
<p className="py-8 text-center text-[1.3rem] text-muted-foreground">
표시할 상세 데이터가 없습니다.
</p>
) : (
items.map((item, index) => (
<div
key={`${item.vrfcDtlSn ?? "detail"}-${index}`}
className="rounded-lg border border-slate-200 p-4"
>
<div className="mb-3 grid grid-cols-2 gap-x-6 gap-y-2 text-[1.3rem]">
<p>
<span className="font-semibold">
조합순번:
</span>{" "}
{item.eqtnCombSeq ?? "-"}
</p>
<p>
<span className="font-semibold">
검증상세순번:
</span>{" "}
{item.vrfcDtlSn ?? "-"}
</p>
<p>
<span className="font-semibold">
비교연산:
</span>{" "}
{item.cmprOpert ?? "-"}
</p>
<p>
<span className="font-semibold">
참조통계코드:
</span>{" "}
{item.refStatsCd ?? "-"}
</p>
<p className="col-span-2">
<span className="font-semibold">
한글 수식:
</span>{" "}
{item.kornArtopnEqtnCn ?? "-"}
</p>
<p className="col-span-2">
<span className="font-semibold">
영문 수식:
</span>{" "}
{item.engArtopnEqtnCn ?? "-"}
</p>
</div>
</div>
))
)}
</div>
<DialogFooter>
<DialogClose asChild>
<Button size="lg" color="white" type="button">
닫기
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -33,6 +33,12 @@ function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC"
return "INPT";
}
function parseExtSchdlId(value: string | number | undefined): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
const parsed = parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) ? parsed : undefined;
}
function parseYmdToDate(ymd: string): Date | undefined {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return undefined;
@@ -132,6 +138,7 @@ const StatisticsInputManagement = () => {
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
const statsYr = String(row.statsYr ?? "");
const seType = rowSeTypeToCode(row);
const extSchdlId = parseExtSchdlId(row.extSchdlId);
try {
setIsLoading(true);
@@ -139,6 +146,7 @@ const StatisticsInputManagement = () => {
statsYr,
seType,
seCd,
...(extSchdlId != null ? { extSchdlId } : {}),
});
if (res.status !== "success" || !res.data) {
@@ -147,8 +155,10 @@ const StatisticsInputManagement = () => {
}
const detail = res.data;
const detailExtSchdlId = parseExtSchdlId(detail.extSchdlId) ?? extSchdlId;
setPeriodDialogMode("edit");
setPeriodDialogInitial({
extSchdlId: detailExtSchdlId,
statsYr: String(detail.statsYr ?? statsYr),
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
seCd:
@@ -31,6 +31,7 @@ import {
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
export type StatisticsInputPeriodDialogValues = {
extSchdlId?: number;
statsYr: string;
seType: "INPT" | "VRFC";
seCd: number;
@@ -146,6 +147,12 @@ export function StatisticsInputPeriodDialog({
};
if (!isCreate) {
param.seCd = seCdParsed;
if (
initial?.extSchdlId != null &&
Number.isFinite(initial.extSchdlId)
) {
param.extSchdlId = initial.extSchdlId;
}
}
const res = await statisticsApi.saveStatsInputPeriod(param);
if (res.status === "success") {
@@ -191,6 +198,9 @@ export function StatisticsInputPeriodDialog({
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
if (initial.extSchdlId != null && Number.isFinite(initial.extSchdlId)) {
param.extSchdlId = initial.extSchdlId;
}
const res = await statisticsApi.deleteStatsInputPeriod(param);
if (res.status === "success") {
alert("삭제되었습니다.");
@@ -30,7 +30,9 @@ import {
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn";
import { buildReviewRowTypeCurrCellStyleExcludeMergeCells, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0];
@@ -68,8 +70,8 @@ const ExcretaTreatmentFacilityRvw = () => {
}, []);
const cellStyleCallback = useMemo(
() => buildReviewRowTypeCurrCellStyleExcludeMergeCells(columnInfo.columns),
[columnInfo],
() => buildReviewCellStyleCallback(columnInfo.columns, rowData),
[columnInfo, rowData],
);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
@@ -274,6 +276,7 @@ const ExcretaTreatmentFacilityRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<DataGrid
columnInfo={columnInfo}
rowStartNum={columnInfo.rowStartNum}
@@ -30,7 +30,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001";
@@ -86,8 +88,8 @@ const PrivateSewageFacilityRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -349,6 +351,7 @@ const PrivateSewageFacilityRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -22,7 +22,9 @@ import {
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { buildReviewRowTypeCurrCellStyleExcludeMergeCells, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
@@ -56,8 +58,8 @@ const PublicSewageFacilityRvw = () => {
}, []);
const cellStyleCallback = useMemo(
() => buildReviewRowTypeCurrCellStyleExcludeMergeCells(columnInfo.columns),
[columnInfo],
() => buildReviewCellStyleCallback(columnInfo.columns, rowData),
[columnInfo, rowData],
);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
@@ -248,15 +250,18 @@ const PublicSewageFacilityRvw = () => {
</div>
)}
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={cellStyleCallback}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={cellStyleCallback}
/>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
@@ -28,7 +28,9 @@ import {
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn";
import { buildReviewRowTypeCurrCellStyleExcludeMergeCells, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
/**
* 하수도 통계 검토 05.중계펌프장
@@ -63,8 +65,8 @@ const PumpingStationRvw = () => {
}, []);
const cellStyleCallback = useMemo(
() => buildReviewRowTypeCurrCellStyleExcludeMergeCells(columnInfo.columns),
[columnInfo],
() => buildReviewCellStyleCallback(columnInfo.columns, rowData),
[columnInfo, rowData],
);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
@@ -132,7 +134,7 @@ const PumpingStationRvw = () => {
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
pumpstNm: values.fctName,
fcltNm: values.fctName,
});
if (response.status === "success" && response.data) {
@@ -264,6 +266,7 @@ const PumpingStationRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<DataGrid
columnInfo={columnInfo}
rowStartNum={columnInfo.rowStartNum}
@@ -0,0 +1,16 @@
const ReviewEmptyCurrLegend = () => (
<div className="flex justify-end">
<div className="flex items-center gap-[1.2rem] text-[1.4rem] bg-white w-fit py-2 px-4 border border-slate-200/80 rounded-md">
<div className="flex items-center gap-x-2">
<div className="w-8 h-6 bg-[#feee85] border border-slate-200" />
<span>전년도만 존재</span>
</div>
<div className="flex items-center gap-x-2">
<div className="w-8 h-6 bg-[#bae6fd] border border-slate-200" />
<span>현년도만 존재</span>
</div>
</div>
</div>
);
export default ReviewEmptyCurrLegend;
@@ -30,7 +30,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001";
@@ -89,8 +91,8 @@ const SewageExcretaRelatedRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -354,6 +356,7 @@ const SewageExcretaRelatedRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -30,7 +30,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001";
@@ -89,8 +91,8 @@ const SewageExcretaSlurgeRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -355,6 +357,7 @@ const SewageExcretaSlurgeRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -27,7 +27,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001";
@@ -104,8 +106,8 @@ const SewagePipeMaintainRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -367,6 +369,7 @@ const SewagePipeMaintainRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -32,7 +32,9 @@ import type {
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001";
@@ -158,8 +160,8 @@ const SewagePipeRvw = () => {
);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -418,6 +420,7 @@ const SewagePipeRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -25,7 +25,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001";
@@ -52,7 +54,7 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] {
const data = responseData as Record<string, unknown>;
if (tabKey === "ST00040001") {
const list = data.sewerReservoirList ?? data.facilitiesList;
const list = data.sewerReservoirList ?? data.reservoirList;
return Array.isArray(list) ? (list as object[]) : [];
}
if (tabKey === "ST00040002") {
@@ -108,8 +110,8 @@ const SewageReservoirPumpRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const activeStatsCd = activeStatsStatusInfo.statsCd ?? activeTab;
@@ -373,6 +375,7 @@ const SewageReservoirPumpRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -30,7 +30,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001";
@@ -89,8 +91,8 @@ const SewerageFinanceRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -352,6 +354,7 @@ const SewerageFinanceRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -18,7 +18,9 @@ import type {
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { buildReviewRowTypeCurrCellStyleExcludeMergeCells, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
@@ -55,8 +57,8 @@ const SeweragePenetrationRateRvw = () => {
}, []);
const cellStyleCallback = useMemo(
() => buildReviewRowTypeCurrCellStyleExcludeMergeCells(columnInfo.columns),
[columnInfo],
() => buildReviewCellStyleCallback(columnInfo.columns, rowData),
[columnInfo, rowData],
);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
@@ -243,15 +245,18 @@ const SeweragePenetrationRateRvw = () => {
</div>
)}
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={cellStyleCallback}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={cellStyleCallback}
/>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
@@ -34,7 +34,9 @@ import type {
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageServiceLevelInfoListRvw } from "@/components/util/column/review/SewerageServiceLevelRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewerageServiceLevelInfoListRvw[0]?.tabKey ?? "ST00130001";
@@ -87,8 +89,8 @@ const SewerageServiceLevelRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -353,6 +355,7 @@ const SewerageServiceLevelRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -30,7 +30,9 @@ import {
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn";
import { buildReviewRowTypeCurrCellStyleCallbackByTab, toGridRows } from "@/components/util/grid/gridFunction";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001";
@@ -92,8 +94,8 @@ const WaterReuseRvw = () => {
}, []);
const reviewCellStyleCallbackByTab = useMemo(
() => buildReviewRowTypeCurrCellStyleCallbackByTab(columnInfo),
[columnInfo],
() => buildReviewCellStyleCallbackByTab(columnInfo, rowDataMap),
[columnInfo, rowDataMap],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
@@ -356,6 +358,7 @@ const WaterReuseRvw = () => {
)}
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<ReviewEmptyCurrLegend />
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
@@ -63,11 +63,14 @@ const CopyDialog = ({
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const response = await generalverificationApiService.CopyVrfcData({
vrfcSn: row?.vrfcSn ?? null,
targetStatsCd: selectedCode ?? '',
rgtrId: userInfo.userUnqId
});
const response = await generalverificationApiService.CopyVrfcData(
{
vrfcSn: row?.vrfcSn ?? null,
targetStatsCd: selectedCode ?? '',
rgtrId: userInfo.userUnqId,
},
userInfo.userUnqId,
);
if (response.status === 'success') {
alert('복사되었습니다.');
@@ -177,7 +177,12 @@ const RegistDialog = memo(
if (await confirm(deleteMsg)) {
try {
const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn!);
const userInfo = await getUserInfo();
const response =
await generalverificationApiService.DeleteVrfcMaster(
vrfcSn!,
userInfo.userUnqId,
);
if (response.status === 'success') {
alert('삭제되었습니다.');
+101 -6
View File
@@ -1,8 +1,11 @@
/**
* statistics/input 화면과 탭(·분할 그리드) 키 매칭용 상수.
* 각 컴포넌트에 실제로 쓰이는 `tabKey`를 모두 포함합니다.
* 각 컴포넌트에 실제로 쓰이는 `tabKey`·`statsCd`를 모두 포함합니다.
*/
/** `SeweragePenetrationRateInp.tsx` — 단일 그리드 */
export const SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS = ["ST00010001"] as const;
/** `SewagePipeInp.tsx` — 메인 탭 + `ConnectionStatusInpColumn` 좌·우 그리드 서브 키 */
export const SEWAGE_PIPE_INP_TAB_KEYS = [
"ST00020001",
@@ -18,16 +21,108 @@ export const SEWAGE_PIPE_INP_TAB_KEYS = [
/** `SewagePipeMaintainInp.tsx` — 보수·준설 탭 */
export const SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS = ["ST00030001", "ST00030002"] as const;
/** `SeweragePenetrationRateInp.tsx` — 단일 그리드(통계코드) */
export const SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS = ["ST00010001"] as const;
/** `SewageReservoirPumpInp.tsx` */
export const SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS = ["ST00040001", "ST00040002"] as const;
/** `PumpingStationInp.tsx` */
export const PUMPING_STATION_INP_TAB_KEYS = ["ST00050001"] as const;
/** `PublicSewageFacilityInp.tsx` */
export const PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS = ["ST00060001", "ST00060002"] as const;
/** `ExcretaTreatmentFacilityInp.tsx` */
export const EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS = ["ST00070001"] as const;
/**
* `PrivateSewageFacilityInp.tsx`
* ST00080003은 tabKey(`privateSewageDesigStatusInp`)와 다름 — `INPUT_TAB_KEY_BY_STATS_CD` 참고
*/
export const PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS = [
"ST00080001",
"ST00080002",
"ST00080003",
] as const;
/** `SewerageFinanceInp.tsx` */
export const SEWERAGE_FINANCE_INP_TAB_KEYS = [
"ST00090001",
"ST00090002",
"ST00090003",
"ST00090004",
] as const;
/** `WaterReuseInp.tsx` */
export const WATER_REUSE_INP_TAB_KEYS = [
"ST00100001",
"ST00100002",
"ST00100003",
"ST00100004",
"ST00100005",
] as const;
/** `SewageExcretaSlurgeInp.tsx` */
export const SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS = [
"ST00110001",
"ST00110002",
"ST00110003",
"ST00110004",
] as const;
/** `SewageExcretaRelatedInp.tsx` */
export const SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS = [
"ST00120001",
"ST00120002",
"ST00120003",
"ST00120004",
] as const;
/** `SewerageServiceLevelInp.tsx` */
export const SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS = [
"ST00130001",
"ST00130002",
] as const;
/** statsCd와 입력 화면 activeTab(tabKey)이 다른 경우 */
export const INPUT_TAB_KEY_BY_STATS_CD: Record<string, string> = {
ST00080003: "privateSewageDesigStatusInp",
};
export type SeweragePenetrationRateInpTabKey =
(typeof SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS)[number];
export type SewagePipeInpTabKey = (typeof SEWAGE_PIPE_INP_TAB_KEYS)[number];
export type SewagePipeMaintainInpTabKey = (typeof SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS)[number];
export type SeweragePenetrationRateInpTabKey = (typeof SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS)[number];
export type SewagePipeMaintainInpTabKey =
(typeof SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS)[number];
export type SewageReservoirPumpInpTabKey =
(typeof SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS)[number];
export type PumpingStationInpTabKey = (typeof PUMPING_STATION_INP_TAB_KEYS)[number];
export type PublicSewageFacilityInpTabKey =
(typeof PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS)[number];
export type ExcretaTreatmentFacilityInpTabKey =
(typeof EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS)[number];
export type PrivateSewageFacilityInpTabKey =
(typeof PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS)[number];
export type SewerageFinanceInpTabKey = (typeof SEWERAGE_FINANCE_INP_TAB_KEYS)[number];
export type WaterReuseInpTabKey = (typeof WATER_REUSE_INP_TAB_KEYS)[number];
export type SewageExcretaSlurgeInpTabKey =
(typeof SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS)[number];
export type SewageExcretaRelatedInpTabKey =
(typeof SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS)[number];
export type SewerageServiceLevelInpTabKey =
(typeof SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS)[number];
/** 컴포넌트 파일명 기준 묶음 (메뉴·라우트 매칭 시 참고) */
export const STATISTICS_INPUT_TAB_KEYS_BY_SCREEN = {
SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
SewagePipeInp: SEWAGE_PIPE_INP_TAB_KEYS,
SewagePipeMaintainInp: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
SeweragePenetrationRateInp: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
SewageReservoirPumpInp: SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS,
PumpingStationInp: PUMPING_STATION_INP_TAB_KEYS,
PublicSewageFacilityInp: PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS,
ExcretaTreatmentFacilityInp: EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS,
PrivateSewageFacilityInp: PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS,
SewerageFinanceInp: SEWERAGE_FINANCE_INP_TAB_KEYS,
WaterReuseInp: WATER_REUSE_INP_TAB_KEYS,
SewageExcretaSlurgeInp: SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS,
SewageExcretaRelatedInp: SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS,
SewerageServiceLevelInp: SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS,
} as const;
@@ -3,6 +3,28 @@ import { ValueType } from "realgrid";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface";
export const LOG_DETAIL_DETAILS_FIELD = "details";
export const LOG_DETAIL_DETAILS_RAW_FIELD = "__detailsRaw";
export interface LogDetailVerificationColumn {
tblNm: string;
metaSn: number;
fieldNm: string;
engColNm: string;
dataScpCd: string;
kornColNm: string;
}
export interface LogDetailVerificationItem {
columns: LogDetailVerificationColumn[];
cmprOpert: string | null;
vrfcDtlSn: number;
refStatsCd: string | null;
eqtnCombSeq: number;
engArtopnEqtnCn: string;
kornArtopnEqtnCn: string;
}
const COMMON_FIELD_ORDER = ["rmrk", "sggNm", "admdstCd", "admdstNm"] as const;
const FIELD_HEADER: Record<string, string> = {
@@ -10,8 +32,71 @@ const FIELD_HEADER: Record<string, string> = {
sggNm: "시군구",
admdstCd: "행정구역코드",
admdstNm: "행정구역명",
[LOG_DETAIL_DETAILS_FIELD]: "상세",
};
function parseLogDetailsValue(
value: unknown,
): LogDetailVerificationItem[] | null {
if (value == null || value === "") return null;
if (Array.isArray(value)) {
return value as LogDetailVerificationItem[];
}
if (typeof value === "string") {
try {
const parsed: unknown = JSON.parse(value);
return Array.isArray(parsed)
? (parsed as LogDetailVerificationItem[])
: null;
} catch {
return null;
}
}
return null;
}
/** details JSON 배열을 그리드 버튼 셀 + 숨김 raw 필드로 변환 */
export function prepareLogDetailRowsForGrid(
rows: StatisticsLogDetailRow[],
): Record<string, string | number>[] {
return rows.map((row) => {
const next: Record<string, string | number> = {};
for (const [key, value] of Object.entries(row)) {
if (key === LOG_DETAIL_DETAILS_FIELD) {
const parsed = parseLogDetailsValue(value);
if (parsed && parsed.length > 0) {
next[LOG_DETAIL_DETAILS_RAW_FIELD] = JSON.stringify(parsed);
next[LOG_DETAIL_DETAILS_FIELD] = "상세보기";
} else {
next[LOG_DETAIL_DETAILS_RAW_FIELD] = "";
next[LOG_DETAIL_DETAILS_FIELD] =
value === null || value === undefined
? ""
: typeof value === "object"
? ""
: String(value);
}
continue;
}
next[key] =
value === null || value === undefined
? ""
: (value as string | number);
}
if (
LOG_DETAIL_DETAILS_FIELD in row &&
!(LOG_DETAIL_DETAILS_RAW_FIELD in next)
) {
next[LOG_DETAIL_DETAILS_RAW_FIELD] = "";
}
return next;
});
}
function collectFieldNames(rows: StatisticsLogDetailRow[]): string[] {
const keys = new Set<string>();
for (const row of rows) {
@@ -47,6 +132,16 @@ function buildColumns(rows: StatisticsLogDetailRow[]): ColumnConfig[] {
return fieldNames.map((fieldName) => {
const numeric = isNumericField(rows, fieldName);
if (fieldName === LOG_DETAIL_DETAILS_FIELD) {
return {
name: fieldName,
fieldName,
header: { text: FIELD_HEADER[fieldName] ?? fieldName },
editable: false,
width: 100,
styleName: "rg-cell-outline-action-btn",
};
}
return {
name: fieldName,
fieldName,
@@ -60,13 +155,33 @@ function buildColumns(rows: StatisticsLogDetailRow[]): ColumnConfig[] {
});
}
function appendHiddenDetailsRawColumn(columns: ColumnConfig[]): ColumnConfig[] {
const hasDetails = columns.some(
(column) => column.fieldName === LOG_DETAIL_DETAILS_FIELD,
);
if (!hasDetails) return columns;
return [
...columns,
{
name: LOG_DETAIL_DETAILS_RAW_FIELD,
fieldName: LOG_DETAIL_DETAILS_RAW_FIELD,
header: { text: "" },
editable: false,
visible: false,
},
];
}
export function buildLogHistoryDetailColumnInfo(
rows: StatisticsLogDetailRow[],
): ColumnInfo {
const columns = buildColumns(rows);
const columns = appendHiddenDetailsRawColumn(buildColumns(rows));
return {
columns,
columnLayout: columns.map((c) => c.fieldName),
columnLayout: columns
.filter((column) => column.visible !== false)
.map((column) => column.fieldName),
};
}
@@ -33,16 +33,19 @@ export const rawPublicSewageFacilityInfoList = {
name: 'ctpvNm',
fieldName: 'ctpvNm',
header: { text: '시도' },
mergeCells: true,
sortdisable: true,
footer: [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
],
},
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true },
{
name: 'displayYear',
fieldName: 'displayYear',
header: { text: '구분' },
sortdisable: true,
footer: [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
@@ -61,6 +61,26 @@ export function buildReviewRowTypeCurrCellStyleCallbackByTab(
return map;
}
/** 여러 cellStyleCallback 결과를 styleName으로 병합 */
export function composeCellStyleCallbacks(
...callbacks: CellStyleCallback[]
): CellStyleCallback {
return (params) => {
const styleNames: string[] = [];
for (const cb of callbacks) {
const result = cb(params);
if (!result) continue;
if (typeof result === "string") {
styleNames.push(result);
} else if (result.styleName) {
styleNames.push(result.styleName);
}
}
const styleName = styleNames.join(" ").trim();
return styleName ? styleName : undefined;
};
}
/**
* 객체 배열을 그리드 row 형식으로 변환 (null/undefined → "").
* 어떤 엔티티 타입이든 공통 사용 가능.
@@ -0,0 +1,183 @@
import type { CellStyleCallback, ColumnConfig } from "@/components/type/DataGrid.type";
import {
buildReviewRowTypeCurrCellStyleExcludeMergeCells,
composeCellStyleCallbacks,
} from "@/components/util/grid/gridFunction";
const DISPLAY_YEAR_FIELD = "displayYear";
/** CURR 미입력 + OLD(전년) 데이터 있음 */
export const REVIEW_CURR_EMPTY_WARN_STYLE = "rg-cell-review-curr-empty-warn";
/** CURR 미입력 + OLD 행 없음 */
export const REVIEW_CURR_EMPTY_NEW_STYLE = "rg-cell-review-curr-empty-new";
function getColumnFieldNames(columns: readonly Pick<ColumnConfig, "fieldName">[]): string[] {
return columns.map((col) => col.fieldName).filter(Boolean);
}
function getFieldNamesBeforeDivision(columns: readonly Pick<ColumnConfig, "fieldName">[]): string[] {
const fields = getColumnFieldNames(columns);
const divisionIndex = fields.indexOf(DISPLAY_YEAR_FIELD);
if (divisionIndex <= 0) return [];
return fields.slice(0, divisionIndex);
}
function getFieldNamesAfterDivision(columns: readonly Pick<ColumnConfig, "fieldName">[]): string[] {
const fields = getColumnFieldNames(columns);
const divisionIndex = fields.indexOf(DISPLAY_YEAR_FIELD);
if (divisionIndex < 0 || divisionIndex >= fields.length - 1) return [];
return fields.slice(divisionIndex + 1);
}
function getDivisionHighlightFieldNames(
columns: readonly Pick<ColumnConfig, "fieldName" | "name">[],
): Set<string> {
const divisionColumn = columns.find((col) => col.fieldName === DISPLAY_YEAR_FIELD);
return new Set(
[DISPLAY_YEAR_FIELD, divisionColumn?.name, divisionColumn?.fieldName].filter(
Boolean,
) as string[],
);
}
function buildRowGroupKey(
row: Record<string, string | number>,
groupKeyFields: readonly string[],
): string {
return groupKeyFields.map((field) => String(row[field] ?? "")).join("\0");
}
export function isEmptyOrZeroGridValue(value: string | number | undefined): boolean {
if (value === null || value === undefined || value === "") return true;
if (typeof value === "number") return value === 0;
const trimmed = String(value).trim();
if (trimmed === "") return true;
const numeric = Number(trimmed.replace(/,/g, ""));
return !Number.isNaN(numeric) && numeric === 0;
}
export function isRowEmptyOrZeroAfterDivision(
row: Record<string, string | number>,
dataFieldNames: readonly string[],
): boolean {
if (dataFieldNames.length === 0) return false;
return dataFieldNames.every((field) => isEmptyOrZeroGridValue(row[field]));
}
type EmptyCurrHighlight =
| typeof REVIEW_CURR_EMPTY_WARN_STYLE
| typeof REVIEW_CURR_EMPTY_NEW_STYLE;
function buildOldRowByGroupKey(
rows: readonly Record<string, string | number>[],
groupKeyFields: readonly string[],
): Map<string, Record<string, string | number>> {
const map = new Map<string, Record<string, string | number>>();
for (const row of rows) {
if (String(row.rowType ?? "") !== "OLD") continue;
map.set(buildRowGroupKey(row, groupKeyFields), row);
}
return map;
}
function buildOldRowIndexByGroupKey(
rows: readonly Record<string, string | number>[],
groupKeyFields: readonly string[],
): Map<string, number> {
const map = new Map<string, number>();
rows.forEach((row, index) => {
if (String(row.rowType ?? "") !== "OLD") return;
map.set(buildRowGroupKey(row, groupKeyFields), index);
});
return map;
}
function resolveEmptyCurrHighlight(
currRow: Record<string, string | number>,
oldRowByGroupKey: ReadonlyMap<string, Record<string, string | number>>,
groupKeyFields: readonly string[],
dataFieldNames: readonly string[],
): EmptyCurrHighlight | undefined {
if (String(currRow.rowType ?? "") !== "CURR") return undefined;
if (!isRowEmptyOrZeroAfterDivision(currRow, dataFieldNames)) return undefined;
const groupKey = buildRowGroupKey(currRow, groupKeyFields);
const oldRow = oldRowByGroupKey.get(groupKey);
if (!oldRow) {
return REVIEW_CURR_EMPTY_NEW_STYLE;
}
if (isRowEmptyOrZeroAfterDivision(oldRow, dataFieldNames)) {
return undefined;
}
return REVIEW_CURR_EMPTY_WARN_STYLE;
}
/**
* 검토 그리드 — CURR 행의 구분 이후 값이 모두 비어있거나 0일 때
* 구분(displayYear): OLD 있음 → OLD·CURR 노란색, OLD 없음 → CURR 하늘색, 둘 다 없음 → 미적용
*/
export function buildReviewEmptyCurrCellStyleCallback(
columns: readonly Pick<ColumnConfig, "fieldName" | "name">[],
rows: readonly Record<string, string | number>[],
): CellStyleCallback {
const groupKeyFields = getFieldNamesBeforeDivision(columns);
const dataFieldNames = getFieldNamesAfterDivision(columns);
const highlightFieldNames = getDivisionHighlightFieldNames(columns);
const oldRowByGroupKey = buildOldRowByGroupKey(rows, groupKeyFields);
const oldRowIndexByGroupKey = buildOldRowIndexByGroupKey(rows, groupKeyFields);
const highlightByRowIndex = new Map<number, EmptyCurrHighlight>();
rows.forEach((row, index) => {
if (String(row.rowType ?? "") !== "CURR") return;
const highlight = resolveEmptyCurrHighlight(
row,
oldRowByGroupKey,
groupKeyFields,
dataFieldNames,
);
if (!highlight) return;
highlightByRowIndex.set(index, highlight);
if (highlight === REVIEW_CURR_EMPTY_WARN_STYLE) {
const oldIndex = oldRowIndexByGroupKey.get(buildRowGroupKey(row, groupKeyFields));
if (oldIndex !== undefined) {
highlightByRowIndex.set(oldIndex, highlight);
}
}
});
return ({ rowIndex, fieldName }) => {
if (highlightFieldNames.size === 0 || !highlightFieldNames.has(fieldName)) {
return undefined;
}
return highlightByRowIndex.get(rowIndex);
};
}
export function buildReviewCellStyleCallback(
columns: readonly Pick<ColumnConfig, "fieldName" | "name" | "mergeCells">[],
rows: readonly Record<string, string | number>[],
): CellStyleCallback {
const base = buildReviewRowTypeCurrCellStyleExcludeMergeCells(columns);
const emptyCurr = buildReviewEmptyCurrCellStyleCallback(columns, rows);
return composeCellStyleCallbacks(base, emptyCurr);
}
export function buildReviewCellStyleCallbackByTab(
tabs: readonly {
tabKey: string;
columns?: readonly Pick<ColumnConfig, "fieldName" | "name" | "mergeCells">[];
}[],
rowDataMap: ReadonlyMap<string, readonly Record<string, string | number>[]>,
): Map<string, CellStyleCallback> {
const map = new Map<string, CellStyleCallback>();
for (const tab of tabs) {
const columns = tab.columns ?? [];
const rows = rowDataMap.get(tab.tabKey) ?? [];
map.set(tab.tabKey, buildReviewCellStyleCallback(columns, rows));
}
return map;
}
@@ -0,0 +1,17 @@
import type { StatisticsInputCommonInfoBase } from "@/entities/statistics/model/statistics-interface";
export function isVerificationFailedFromCommonInfo(
commonInfo: Pick<StatisticsInputCommonInfoBase, "isVerificationFailed"> | null | undefined,
): boolean {
return commonInfo?.isVerificationFailed === true;
}
export function patchVerificationFailedByTab(
prev: Map<string, boolean>,
tabKey: string,
commonInfo: Pick<StatisticsInputCommonInfoBase, "isVerificationFailed"> | null | undefined,
): Map<string, boolean> {
const next = new Map(prev);
next.set(tabKey, isVerificationFailedFromCommonInfo(commonInfo));
return next;
}
@@ -75,10 +75,16 @@ export class GeneralverificationApiService implements GeneralverificationService
}
public async DeleteVrfcMaster(
param: number,
vrfcSn: number,
userUnqId: string,
): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${vrfcSn}`,
{
headers: {
userId: userUnqId,
},
},
);
return response.data;
}
@@ -141,9 +147,16 @@ export class GeneralverificationApiService implements GeneralverificationService
public async CopyVrfcData(
param: CopyVerifyDataRequest,
userUnqId: string,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`,
undefined,
{
headers: {
userId: userUnqId,
},
},
);
return response.data;
}
@@ -258,7 +258,7 @@ export class StatisticsManagementApiService {
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
pumpstNm?: string;
fcltNm?: string;
}
): Promise<MediationPumpStationApiResponse> {
const response = await stClient.get<MediationPumpStationApiResponse>(
@@ -93,7 +93,7 @@ export class StatisticsApiService implements StatisticsService {
): Promise<StatsInputPeriodListApiResponse> {
const response = await stClient.get<StatsInputPeriodListApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/list`,
{ params },
withAuthHeader({ params }),
);
return response.data;
}
@@ -101,7 +101,7 @@ export class StatisticsApiService implements StatisticsService {
public async getStatsInputPeriodDetail(params: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse> {
const response = await stClient.get<StatsInputPeriodDetailResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/detail`,
{ params },
withAuthHeader({ params }),
);
return response.data;
}
@@ -113,6 +113,7 @@ export class StatisticsApiService implements StatisticsService {
const response = await stClient.post<StatsInputPeriodSaveApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
param,
withAuthHeader(),
);
return response.data;
}
@@ -123,7 +124,7 @@ export class StatisticsApiService implements StatisticsService {
): Promise<StatsInputPeriodDeleteApiResponse> {
const response = await stClient.delete<StatsInputPeriodDeleteApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
{ data: param },
withAuthHeader({ data: param }),
);
return response.data;
}
@@ -275,7 +275,10 @@ export interface GeneralverificationService {
RegistVrfcMaster(
param: RegistVrfcMasterRequest,
): Promise<RegistVrfcMasterResponse>;
DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse>;
DeleteVrfcMaster(
vrfcSn: number,
userUnqId: string,
): Promise<DeleteVrfcMasterResponse>;
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
getVerifyColumnList(
@@ -292,6 +295,7 @@ export interface GeneralverificationService {
): Promise<RegistVrfcMasterResponse>;
CopyVrfcData(
param: CopyVerifyDataRequest,
userUnqId: string,
): Promise<RegistVrfcMasterResponse>;
getVerifyResultList(
param: VerifyResultListRequest,
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 분뇨처리시설(ST00070001) — `ExcretaTreatFactInpColumn` */
export const EXCRETA_TREATMENT_FACILITY_STATS_CD = "ST00070001" as const;
@@ -18,7 +18,7 @@ export type ExcretaTreatmentFacilityVerificationResult = Record<
>;
/** 분뇨처리시설 조회 `data.commonInfo` */
export interface ExcretaTreatmentFacilityCommonInfo {
export interface ExcretaTreatmentFacilityCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 처리용량별 오수처리시설 현황(ST00080001) — `SewageTreatFactStatusInpColumn.tabKey` */
export const SEWAGE_TREAT_FACT_STATUS_STATS_CD = "ST00080001" as const;
@@ -18,7 +18,7 @@ export type PrivateSewageFacilityVerificationResult = Record<
>;
/** ST00080001 조회 `data.commonInfo` */
export interface SewageTreatFactStatusCommonInfo {
export interface SewageTreatFactStatusCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -78,7 +78,7 @@ export interface SewageTreatFactStatusRequestParams {
export const SEPTIC_TANK_STATUS_STATS_CD = "ST00080002" as const;
/** ST00080002 조회 `data.commonInfo` */
export interface SepticTankStatusCommonInfo {
export interface SepticTankStatusCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -156,7 +156,7 @@ export interface SepticTankStatusRequestParams {
export const PRIVATE_SEWAGE_DESIG_STATUS_STATS_CD = "ST00080003" as const;
/** ST00080003 조회 `data.commonInfo` */
export interface PrivateSewageDesigStatusCommonInfo {
export interface PrivateSewageDesigStatusCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 펌프장(ST00050001) — `PumpingStationInpColumn` tabKey */
export const PUMPING_STATION_STATS_CD = "ST00050001" as const;
@@ -18,7 +18,7 @@ export type PumpingStationVerificationResult = Record<
>;
/** 펌프장 조회 `data.commonInfo` */
export interface PumpingStationCommonInfo {
export interface PumpingStationCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 하수·분뇨관련산업 — 공공기관 하수분야(ST00120001) `PublicInstitutionSewerIndustryInpColumn.tabKey` */
export const SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD = "ST00120001" as const;
@@ -18,7 +18,7 @@ export type SewageExcretaRelatedVerificationResult = Record<
>;
/** 조회 `data.commonInfo` */
export interface SewageExcretaRelatedCommonInfo {
export interface SewageExcretaRelatedCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 슬러지 처리 — 찌꺼기(슬러지)처리시설현황(ST00110001) `SlurgeProcessingFacStatusInpColumn.tabKey` */
export const SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD = "ST00110001" as const;
@@ -27,7 +27,7 @@ export type SewageExcretaSlurgeVerificationResult = Record<
>;
/** 조회 `data.commonInfo` */
export interface SewageExcretaSlurgeCommonInfo {
export interface SewageExcretaSlurgeCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
export interface SewagePipeMaintainRepairVerificationResultItem {
vrfcCd: string;
@@ -13,7 +13,7 @@ export type SewagePipeMaintainRepairVerificationResult = Record<
SewagePipeMaintainRepairVerificationResultItem[]
>;
export interface SewagePipeMaintainRepairCommonInfo {
export interface SewagePipeMaintainRepairCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 하수저류시설 Tab1 — `SewageReservoirInpColumn.tabKey` */
export const SEWAGE_RESERVOIR_STATS_CD = "ST00040001" as const;
@@ -18,7 +18,7 @@ export type SewageReservoirVerificationResult = Record<
>;
/** 하수저류시설 조회 `data.commonInfo` */
export interface SewageReservoirCommonInfo {
export interface SewageReservoirCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -86,7 +86,7 @@ export type SewageReservoirApiResponse = ApiResponse<SewageReservoirResponseData
};
/** 빗물펌프장 Tab2 `data.commonInfo` */
export interface RainfallPumpStationCommonInfo {
export interface RainfallPumpStationCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 하수도재정 결산(ST00090001) — `SewerageFinanceSettleInpColumn.tabKey` */
export const SEWERAGE_FINANCE_SETTLE_STATS_CD = "ST00090001" as const;
@@ -18,7 +18,7 @@ export type SewerageFinanceVerificationResult = Record<
>;
/** ST00090001 조회 `data.commonInfo` */
export interface SewerageFinanceSettleCommonInfo {
export interface SewerageFinanceSettleCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -202,7 +202,7 @@ export interface SewerageFinanceSubmitResponse {
export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const;
/** ST00090002 조회 `data.commonInfo` */
export interface SewerageChargeCommonInfo {
export interface SewerageChargeCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -292,7 +292,7 @@ export type SewerageChargeSubmitItem = SewerageChargeSaveItem;
export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const;
/** ST00090003 조회 `data.commonInfo` */
export interface SewerageUsageFeeCommonInfo {
export interface SewerageUsageFeeCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -416,7 +416,7 @@ export type SewerageUsageFeeSubmitItem = SewerageUsageFeeSaveItem;
export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const;
/** ST00090004 조회 `data.commonInfo` */
export interface CauserBurdenCommonInfo {
export interface CauserBurdenCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 하수도 서비스 수준 — 민원(ST00130001) `ComplaintInpColumn.tabKey` */
export const SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD = "ST00130001" as const;
@@ -18,7 +18,7 @@ export type SewerageServiceLevelVerificationResult = Record<
>;
/** ST00130001 조회 `data.commonInfo` */
export interface SewerageServiceLevelComplaintCommonInfo {
export interface SewerageServiceLevelComplaintCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -72,7 +72,7 @@ export interface SewerageServiceLevelComplaintRequestParams {
export const SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD = "ST00130002" as const;
/** ST00130002 조회 `data.commonInfo` */
export interface SewerageServiceLevelResidentFriendlyFactCommonInfo {
export interface SewerageServiceLevelResidentFriendlyFactCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
/** 물 재이용 — 공공하수처리수 재이용(ST00100001) `PublicSewageTreatWaterReuseInpColumn.tabKey` */
export const WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD = "ST00100001" as const;
@@ -30,7 +30,7 @@ export type WaterReuseVerificationResult = Record<
>;
/** 조회 `data.commonInfo` */
export interface WaterReuseCommonInfo {
export interface WaterReuseCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { SewerageStatusInfo } from "../statistics-interface";
import type { SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
export interface PublicSewageFacilityRequestParams {
ctpvNm?: string;
@@ -22,7 +22,7 @@ export type PublicSewageFacilityVerificationResult = Record<
PublicSewageFacilityVerifyResultItem[]
>;
export interface PublicSewageFacilityCommonInfo {
export interface PublicSewageFacilityCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,4 +1,4 @@
import type { SewerageStatusInfo } from "../statistics-interface";
import type { SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
export interface SeweragePipeRequestParams {
ctpvNm?: string;
@@ -20,7 +20,7 @@ export interface VerifyResultItem {
/** 필드명 -> 검증 결과 배열 */
export type SeweragePipeInputVerificationResult = Record<string, VerifyResultItem[]>;
export interface SeweragePipeInputCommonInfo {
export interface SeweragePipeInputCommonInfo extends StatisticsInputCommonInfoBase {
statsYr: string;
ctpvNm: string;
sewerBzmnCd: string;
@@ -1,7 +1,7 @@
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
import type { ApiResponse, SewerageStatusInfo, StatisticsInputCommonInfoBase } from "../statistics-interface";
// 하수도 보급률 데이터 항목
export interface SewerageSupplyRateCommonInfo {
export interface SewerageSupplyRateCommonInfo extends StatisticsInputCommonInfoBase {
sewerBzmnArea: number;
sewerBzmnCd: string;
sewerBzmnFrgnrWholPpltnCnt: number;
@@ -44,7 +44,7 @@ export interface ExcretaTreatmentFacilityReviewItem {
}
export interface ExcretaTreatmentFacilityReviewResponseData {
excretaTreatmentFacilityList: ExcretaTreatmentFacilityReviewItem[];
excretaTreatmentList: ExcretaTreatmentFacilityReviewItem[];
statsStatusInfo?: StatsStatusInfo;
}
@@ -65,7 +65,7 @@ export function extractExcretaTreatmentFacilityReviewList(
): ExcretaTreatmentFacilityReviewItem[] {
if (!data) return [];
if (Array.isArray(data)) return data;
return data.excretaTreatmentFacilityList ?? [];
return data.excretaTreatmentList ?? [];
}
export function extractExcretaTreatmentFacilityReviewStatsStatusInfo(
@@ -49,6 +49,7 @@ export type StatisticsColumnGuideListResponse = ApiResponse<StatisticsColumnGuid
/** 통계 입력 관리 등 — 연도·구분·분류(입력/검증)별 입력 기간 한 행 */
export interface StatsInputPeriodItem {
extSchdlId?: number;
statsYr: string;
seCd: number;
seType: string;
@@ -69,6 +70,7 @@ export interface StatsInputPeriodDetailRequest {
statsYr: string;
seType: string;
seCd: number;
extSchdlId?: number;
}
export interface StatsInputPeriodDetailResponse {
@@ -79,6 +81,7 @@ export interface StatsInputPeriodDetailResponse {
/** 통계 입력 기간 등록·수정 등 요청 본문 */
export interface StatsInputPeriodSaveRequest {
extSchdlId?: number;
statsYr: string;
seType: string;
seCd?: number;
@@ -94,6 +97,7 @@ export type StatsInputPeriodSaveApiResponse = ApiResponse<unknown>;
/** 통계 입력 기간 삭제 요청 */
export interface StatsInputPeriodDeleteRequest {
extSchdlId?: number;
statsYr: string;
seType: string;
seCd: number;
@@ -105,6 +109,11 @@ export interface StatsInputPeriodDeleteRequest {
/** 통계 입력 기간 삭제 응답 */
export type StatsInputPeriodDeleteApiResponse = ApiResponse<unknown>;
/** 입력 조회 `data.commonInfo` 공통 필드 */
export interface StatisticsInputCommonInfoBase {
isVerificationFailed: boolean;
}
/** 하수 입력/보급률 등 조회 `data.statusInfo` 공통 (LAE 제출·담당자 정보) */
export interface SewerageStatusInfo {
laePicNm: string | null;
@@ -84,6 +84,6 @@ export interface MediationPumpStationService {
sewerBzmnCd?: string;
ctpvNm?: string;
sggNm?: string;
pumpstNm?: string;
fcltNm?: string;
}): Promise<MediationPumpStationApiResponse>;
}
@@ -3,9 +3,20 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doNameList } from "@/components/util/column/input/DoList";
import { menuMatchesStatisticsTabId } from "@/shared/lib/statistics-menu-url";
import {
EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS,
INPUT_TAB_KEY_BY_STATS_CD,
PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS,
PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS,
PUMPING_STATION_INP_TAB_KEYS,
SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS,
SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS,
SEWAGE_PIPE_INP_TAB_KEYS,
SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS,
SEWERAGE_FINANCE_INP_TAB_KEYS,
SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS,
WATER_REUSE_INP_TAB_KEYS,
} from "@/components/util/MenuMatchList";
/** 검증 결과에서 입력 화면 자동 조회에 사용하는 조건 */
@@ -35,15 +46,21 @@ export function verificationSearchToFormValues(
/** `statistics-page` 동적 import 경로의 파일명(확장자 제외)과 일치해야 함 */
export const STATISTICS_INPUT_URL_PATH = {
SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp",
SEWAGE_PIPE: "SewagePipeInp",
SEWAGE_PIPE_MAINTAIN: "SewagePipeMaintainInp",
SEWERAGE_PENETRATION_RATE: "SeweragePenetrationRateInp",
SEWAGE_RESERVOIR_PUMP: "SewageReservoirPumpInp",
PUMPING_STATION: "PumpingStationInp",
PUBLIC_SEWAGE_FACILITY: "PublicSewageFacilityInp",
EXCRETA_TREATMENT_FACILITY: "ExcretaTreatmentFacilityInp",
PRIVATE_SEWAGE_FACILITY: "PrivateSewageFacilityInp",
SEWERAGE_FINANCE: "SewerageFinanceInp",
WATER_REUSE: "WaterReuseInp",
SEWAGE_EXCRETA_SLURGE: "SewageExcretaSlurgeInp",
SEWAGE_EXCRETA_RELATED: "SewageExcretaRelatedInp",
SEWERAGE_SERVICE_LEVEL: "SewerageServiceLevelInp",
} as const;
/** `PublicSewageFacilityInp.tsx` — 공공하수처리시설 탭 */
const PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS = ["ST00060001"] as const;
function normalizeSewagePipeTabKey(tabKey: string): string {
if (
tabKey === "adminAreaConnectionInp" ||
@@ -54,20 +71,66 @@ function normalizeSewagePipeTabKey(tabKey: string): string {
return tabKey;
}
function resolveInputTabKey(urlPath: string, statsCd: string): string {
if (urlPath === STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) {
return normalizeSewagePipeTabKey(statsCd);
}
return INPUT_TAB_KEY_BY_STATS_CD[statsCd] ?? statsCd;
}
const NAV_ROWS: { urlPath: string; tabKeys: readonly string[] }[] = [
{ urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE, tabKeys: SEWAGE_PIPE_INP_TAB_KEYS },
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN,
tabKeys: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE,
tabKeys: SEWERAGE_PENETRATION_RATE_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE,
tabKeys: SEWAGE_PIPE_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN,
tabKeys: SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP,
tabKeys: SEWAGE_RESERVOIR_PUMP_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.PUMPING_STATION,
tabKeys: PUMPING_STATION_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY,
tabKeys: PUBLIC_SEWAGE_FACILITY_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY,
tabKeys: EXCRETA_TREATMENT_FACILITY_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY,
tabKeys: PRIVATE_SEWAGE_FACILITY_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE,
tabKeys: SEWERAGE_FINANCE_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.WATER_REUSE,
tabKeys: WATER_REUSE_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE,
tabKeys: SEWAGE_EXCRETA_SLURGE_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED,
tabKeys: SEWAGE_EXCRETA_RELATED_INP_TAB_KEYS,
},
{
urlPath: STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL,
tabKeys: SEWERAGE_SERVICE_LEVEL_INP_TAB_KEYS,
},
];
/**
@@ -83,11 +146,10 @@ export function resolveInputPageNavigationFromStatsCd(statsCd: string): {
for (const row of NAV_ROWS) {
if (row.tabKeys.includes(trimmed)) {
const tabKey =
row.urlPath === STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE
? normalizeSewagePipeTabKey(trimmed)
: trimmed;
return { urlPath: row.urlPath, tabKey };
return {
urlPath: row.urlPath,
tabKey: resolveInputTabKey(row.urlPath, trimmed),
};
}
}
return null;
@@ -0,0 +1,71 @@
"use client";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import {
verificationSearchToFormValues,
type StatisticsInputPendingSearch,
} from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
type PendingNavigation = {
tabKey: string;
search: StatisticsInputPendingSearch | null;
};
type UseStatisticsInputPendingNavigationOptions = {
urlPath: string;
onPending?: (pending: PendingNavigation) => void;
onAutoSearch?: (
values: SearchFormValues,
tabKey: string,
) => void | Promise<void>;
};
/** 이상치 선별 보고서 등에서 넘어온 입력 화면·탭·검색조건을 적용합니다. */
export function useStatisticsInputPendingNavigation({
urlPath,
onPending,
onAutoSearch,
}: UseStatisticsInputPendingNavigationOptions) {
const [verificationSearchDefaults, setVerificationSearchDefaults] =
useState<SearchFormValues | null>(null);
const [searchFormRemountKey, setSearchFormRemountKey] = useState(0);
const autoSearchRef = useRef<{
values: SearchFormValues;
tabKey: string;
} | null>(null);
const handlersRef = useRef({ onPending, onAutoSearch });
handlersRef.current = { onPending, onAutoSearch };
const activeTabId = useTabStore((s) => s.activeTabId);
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
useLayoutEffect(() => {
if (activeTabId !== urlPath) return;
const pending = useStatisticsInputFocusStore
.getState()
.takePendingForUrl(urlPath);
if (!pending) return;
handlersRef.current.onPending?.(pending);
if (pending.search) {
const values = verificationSearchToFormValues(pending.search);
setVerificationSearchDefaults(values);
autoSearchRef.current = { values, tabKey: pending.tabKey };
setSearchFormRemountKey((k) => k + 1);
}
}, [activeTabId, inputFocusNonce, urlPath]);
useEffect(() => {
const job = autoSearchRef.current;
if (!job) return;
autoSearchRef.current = null;
void handlersRef.current.onAutoSearch?.(job.values, job.tabKey);
}, [searchFormRemountKey]);
return { verificationSearchDefaults, searchFormRemountKey };
}