2116 lines
83 KiB
TypeScript
2116 lines
83 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
|
|
|
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
|
import { ValueType } from "realgrid";
|
|
import type { RealGridReact } from "realgrid-react";
|
|
import { flushSync } from "react-dom";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
|
|
import AddArticleButton from "@/components/button/AddArticleButton";
|
|
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
|
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
|
|
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
|
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
|
|
import SaveButton from "@/components/button/SaveButton";
|
|
import SubmitButton from "@/components/button/SumitButton";
|
|
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
|
|
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
|
|
import SearchForm from "@/components/module/SearchForm";
|
|
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
|
|
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
|
import type {
|
|
CellStyleCallback,
|
|
CellStyleCallbackParams,
|
|
CellStyleCallbackResult,
|
|
CellTooltipCallbackParams,
|
|
ColumnConfig,
|
|
ColumnInfo,
|
|
} from "@/components/type/DataGrid.type";
|
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
|
import { doList } from "@/components/util/column/input/DoList";
|
|
import { ExcretaRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn";
|
|
import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn";
|
|
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";
|
|
import type {
|
|
ExcretaRelatedIndustrySaveItem,
|
|
PrivateSewageTreatRelatedIndustrySaveItem,
|
|
PublicInstitutionSewerIndustrySaveItem,
|
|
SewageExcretaRelatedRequestParams,
|
|
SewageExcretaRelatedVerificationResult,
|
|
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface";
|
|
import {
|
|
SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD,
|
|
SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD,
|
|
SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD,
|
|
SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD,
|
|
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface";
|
|
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
|
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";
|
|
|
|
import SubmitDailog from "./SubmitDailog";
|
|
|
|
const SewageExcretaRelatedInpTabList = [
|
|
PublicInstitutionSewerIndustryInpColumn,
|
|
PublicInstitutionExcretaIndustryInpColumn,
|
|
ExcretaRelatedIndustryInpColumn,
|
|
PrivateSewageTreatRelatedIndustryInpColumn,
|
|
] as const;
|
|
|
|
const firstTabKey = PublicInstitutionSewerIndustryInpColumn.tabKey;
|
|
|
|
/** 직제(ognzSn) 행 추가·제외 — ST00120001·ST00120002 */
|
|
const ARTICLE_EDIT_TAB_KEYS = new Set<string>([
|
|
PublicInstitutionSewerIndustryInpColumn.tabKey,
|
|
PublicInstitutionExcretaIndustryInpColumn.tabKey,
|
|
]);
|
|
|
|
/** 해당사항 없음 — ST00120001·ST00120002 */
|
|
const NOT_APPLICABLE_TAB_KEYS = new Set<string>([
|
|
PublicInstitutionSewerIndustryInpColumn.tabKey,
|
|
PublicInstitutionExcretaIndustryInpColumn.tabKey,
|
|
]);
|
|
|
|
/** 항목 추가 행 — 시군구(sggNm) 드롭다운 선택 */
|
|
const SGG_NM_DROPDOWN_TAB_KEYS = new Set<string>([
|
|
PublicInstitutionSewerIndustryInpColumn.tabKey,
|
|
PublicInstitutionExcretaIndustryInpColumn.tabKey,
|
|
]);
|
|
|
|
/** 저장·제출 API 연동 — ST00120001~ST00120004 */
|
|
const SAVE_SUPPORTED_TAB_KEYS = new Set<string>([
|
|
PublicInstitutionSewerIndustryInpColumn.tabKey,
|
|
PublicInstitutionExcretaIndustryInpColumn.tabKey,
|
|
ExcretaRelatedIndustryInpColumn.tabKey,
|
|
PrivateSewageTreatRelatedIndustryInpColumn.tabKey,
|
|
]);
|
|
|
|
const SUBMIT_SUPPORTED_TAB_KEYS = SAVE_SUPPORTED_TAB_KEYS;
|
|
|
|
/** 행정구역 고정 행 탭 — saveList/submitList만 (ST00120003·ST00120004) */
|
|
const DISTRICT_ROW_TAB_KEYS = new Set<string>([
|
|
ExcretaRelatedIndustryInpColumn.tabKey,
|
|
PrivateSewageTreatRelatedIndustryInpColumn.tabKey,
|
|
]);
|
|
|
|
const ARTICLE_TAB_BY_KEY = new Map(
|
|
SewageExcretaRelatedInpTabList.map((tab) => [tab.tabKey, tab]),
|
|
);
|
|
|
|
type TabToolbarSnapshot = {
|
|
showSaveInToolbar: boolean;
|
|
showEnterableToolbarButtons: boolean;
|
|
showActionToolbarCard: boolean;
|
|
};
|
|
|
|
const toGridColumnInfo = (tab: (typeof SewageExcretaRelatedInpTabList)[number]): ColumnInfo => ({
|
|
columns: tab.columns ?? [],
|
|
columnLayout: tab.columnLayout ?? [],
|
|
});
|
|
|
|
function tabRowDataExists(
|
|
rowDataMap: Map<string, Record<string, string | number>[]>,
|
|
tabKey: string,
|
|
): boolean {
|
|
const rows = rowDataMap.get(tabKey);
|
|
return Array.isArray(rows) && rows.length > 0;
|
|
}
|
|
|
|
const computeTabEnterable = (
|
|
statusInfo: SewerageStatusInfo | null | undefined,
|
|
periodEnterable: boolean,
|
|
): boolean => {
|
|
if (statusInfo?.laeSbmsnSttsCd) {
|
|
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
|
|
return false;
|
|
}
|
|
return periodEnterable;
|
|
}
|
|
return periodEnterable;
|
|
};
|
|
|
|
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
|
|
return doList.includes(values?.bsnmCd || "")
|
|
? Boolean(values?.gugun && values.gugun !== "ALL")
|
|
: true;
|
|
}
|
|
|
|
function computeTabToolbarSnapshot(
|
|
statusInfo: SewerageStatusInfo | null,
|
|
hasRowData: boolean,
|
|
treatAsSearched: boolean,
|
|
searchContext: SearchFormValues | null,
|
|
periodEnterable: boolean,
|
|
isAdminInfo: boolean,
|
|
): TabToolbarSnapshot {
|
|
const gugunOk = buildGugunOkForToolbar(searchContext);
|
|
const enterable = computeTabEnterable(statusInfo, periodEnterable);
|
|
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
|
|
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
|
|
const showEnterableToolbarButtons = enterable && gugunOk;
|
|
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
|
|
const showActionToolbarCard =
|
|
(hasRowData || treatAsSearched) &&
|
|
(showSaveInToolbar ||
|
|
showEnterableToolbarButtons ||
|
|
showSubmittedInInputPeriodToolbar ||
|
|
(isSubmitted && isAdminInfo));
|
|
return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard };
|
|
}
|
|
|
|
function createEmptyArticleRow(
|
|
tabKey: string,
|
|
ctpvNm: string,
|
|
sggNm = "",
|
|
): Record<string, string | number> {
|
|
const row: Record<string, string | number> = { ctpvNm, sggNm, __checked: 0 };
|
|
const columns = (ARTICLE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[];
|
|
for (const col of columns) {
|
|
const fieldName = col.fieldName;
|
|
if (
|
|
!fieldName ||
|
|
fieldName === "ctpvNm" ||
|
|
fieldName === "sggNm" ||
|
|
col.editable === false
|
|
) {
|
|
continue;
|
|
}
|
|
if (row[fieldName] === undefined) {
|
|
row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : "";
|
|
}
|
|
}
|
|
return row;
|
|
}
|
|
|
|
function isCheckedRow(row: Record<string, string | number>): boolean {
|
|
const v = row.__checked;
|
|
return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true";
|
|
}
|
|
|
|
type ArticleOgnzSnSet = Set<string>;
|
|
|
|
function extractOgnzSnSet(rows: Record<string, string | number>[]): ArticleOgnzSnSet {
|
|
const set = new Set<string>();
|
|
for (const row of rows) {
|
|
const ognzSn = String(row.ognzSn ?? "").trim();
|
|
if (ognzSn) set.add(ognzSn);
|
|
}
|
|
return set;
|
|
}
|
|
|
|
function isNewArticleRow(row: Record<string, string | number>): boolean {
|
|
const v = row.__isNewArticle;
|
|
return v === 1 || v === "1";
|
|
}
|
|
|
|
function setArticleOgnzSnSetForTab(
|
|
storeRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
tabKey: string,
|
|
rows: Record<string, string | number>[],
|
|
) {
|
|
storeRef.current.set(tabKey, extractOgnzSnSet(rows));
|
|
}
|
|
|
|
function clearArticleOgnzSnTracking(
|
|
persistedRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
preYearRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
) {
|
|
persistedRef.current = new Map();
|
|
preYearRef.current = new Map();
|
|
}
|
|
|
|
/** ST00120001·ST00120002 — deleteList 대상 판별 (`ognzSn` + 전년도·신규 행 구분) */
|
|
function isPersistedPublicInstitutionArticleRow(
|
|
tabKey: string,
|
|
row: Record<string, string | number>,
|
|
persistedRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
preYearRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
): boolean {
|
|
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) return false;
|
|
if (isNewArticleRow(row)) return false;
|
|
const ognzSn = String(row.ognzSn ?? "").trim();
|
|
if (!ognzSn) return false;
|
|
if ((preYearRef.current.get(tabKey) ?? new Set()).has(ognzSn)) return false;
|
|
return (persistedRef.current.get(tabKey) ?? new Set()).has(ognzSn);
|
|
}
|
|
|
|
function syncPublicInstitutionArticleTracking(
|
|
tabKey: string,
|
|
gridRows: Record<string, string | number>[],
|
|
fromPreYear: boolean,
|
|
persistedRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
preYearRef: { current: Map<string, ArticleOgnzSnSet> },
|
|
clearDeleteList: (tabKey: string) => void,
|
|
) {
|
|
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) return;
|
|
if (fromPreYear) {
|
|
setArticleOgnzSnSetForTab(preYearRef, tabKey, gridRows);
|
|
return;
|
|
}
|
|
clearDeleteList(tabKey);
|
|
setArticleOgnzSnSetForTab(persistedRef, tabKey, gridRows);
|
|
}
|
|
|
|
function parseSggListFromResponse(res: ApiResponse<Sgg[]>): Sgg[] {
|
|
if (res.status !== "success" || res.data == null) return [];
|
|
return Array.isArray(res.data) ? res.data : [];
|
|
}
|
|
|
|
function buildSggNmColumn(
|
|
col: ColumnConfig,
|
|
canEdit: boolean,
|
|
sggOptions: Sgg[],
|
|
): ColumnConfig {
|
|
if (!canEdit) {
|
|
return { ...col, editable: false as const, styleName: "rg-cell-readonly" };
|
|
}
|
|
if (sggOptions.length === 0) {
|
|
return { ...col, editable: true, styleName: undefined };
|
|
}
|
|
const labels = sggOptions.map((s) => s.gugun);
|
|
return {
|
|
...col,
|
|
editable: true,
|
|
styleName: undefined,
|
|
editor: {
|
|
type: "dropdown",
|
|
domainOnly: true,
|
|
textReadOnly: true,
|
|
commitOnSelect: true,
|
|
dropDownWhenClick: true,
|
|
values: labels,
|
|
labels,
|
|
},
|
|
values: labels,
|
|
labels,
|
|
lookupDisplay: true,
|
|
};
|
|
}
|
|
|
|
/** 시군구명(`sggNm`) 기준 `admdstCd`·`admdstNm` — 저장 시 그리드 행에서 해석 */
|
|
function resolvePublicInstitutionAdmdst(
|
|
row: Record<string, string | number>,
|
|
sggOptions: Sgg[],
|
|
): { admdstCd: string; admdstNm: string } {
|
|
const sggNm = String(row.sggNm ?? "").trim();
|
|
if (sggNm) {
|
|
const matched = sggOptions.find((s) => s.gugun === sggNm);
|
|
if (matched) {
|
|
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
|
|
}
|
|
}
|
|
const existingCd = String(row.admdstCd ?? "").trim();
|
|
const existingNm = String(row.admdstNm ?? "").trim();
|
|
return { admdstCd: existingCd, admdstNm: existingNm || sggNm };
|
|
}
|
|
|
|
function mergeGridRowsWithMapRows(
|
|
gridRows: Record<string, string | number>[],
|
|
mapRows: Record<string, string | number>[],
|
|
hiddenFields: string[] = [],
|
|
): Record<string, string | number>[] {
|
|
return gridRows.map((gridRow, index) => {
|
|
const mapRow = mapRows[index] ?? {};
|
|
const merged: Record<string, string | number> = { ...mapRow, ...gridRow };
|
|
if (hiddenFields.length === 0) return merged;
|
|
const next = { ...merged };
|
|
for (const field of hiddenFields) {
|
|
const fromGrid = String(gridRow[field] ?? "").trim();
|
|
const fromMap = String(mapRow[field] ?? "").trim();
|
|
next[field] = fromGrid || fromMap;
|
|
}
|
|
return next;
|
|
});
|
|
}
|
|
|
|
/** 그리드 조회 시 `__isNewArticle` 등 메타 필드 유실 방지 */
|
|
function mergeGridRowsWithArticleMeta(
|
|
gridRows: Record<string, string | number>[],
|
|
mapRows: Record<string, string | number>[],
|
|
): Record<string, string | number>[] {
|
|
return gridRows.map((gridRow, index) => {
|
|
const mapRow = mapRows[index];
|
|
if (!mapRow) return gridRow;
|
|
if (!isNewArticleRow(gridRow) && isNewArticleRow(mapRow)) {
|
|
return {
|
|
...gridRow,
|
|
__isNewArticle: mapRow.__isNewArticle as string | number,
|
|
};
|
|
}
|
|
return gridRow;
|
|
});
|
|
}
|
|
|
|
function nullableTrimmedString(
|
|
value: string | number | null | undefined,
|
|
): string | null {
|
|
const s = String(value ?? "").trim();
|
|
return s === "" ? null : s;
|
|
}
|
|
|
|
function toGridNumber(value: string | number | null | undefined): number {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : 0;
|
|
}
|
|
|
|
/** ST00120001·ST00120002 — 그리드 행 → 저장/삭제 API 항목 */
|
|
function buildPublicInstitutionIndustrySaveItem(
|
|
row: Record<string, string | number>,
|
|
sggOptions: Sgg[],
|
|
): PublicInstitutionSewerIndustrySaveItem {
|
|
const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(row, sggOptions);
|
|
return {
|
|
admdstCd,
|
|
admdstNm,
|
|
ognzSn: String(row.ognzSn ?? "").trim(),
|
|
ognzNm: String(row.ognzNm ?? "").trim(),
|
|
pbadmsNope: toGridNumber(row.pbadmsNope),
|
|
engrkCnt: toGridNumber(row.engrkCnt),
|
|
mchnCnt: toGridNumber(row.mchnCnt),
|
|
prvtmCnt: toGridNumber(row.prvtmCnt),
|
|
chmengCnt: toGridNumber(row.chmengCnt),
|
|
envCnt: toGridNumber(row.envCnt),
|
|
enjbEtcCnt: toGridNumber(row.enjbEtcCnt),
|
|
etcCnt: toGridNumber(row.etcCnt),
|
|
rmrk: nullableTrimmedString(row.rmrk),
|
|
};
|
|
}
|
|
|
|
/** ST00120003 — 그리드 행 → 저장/제출 API 항목 */
|
|
function buildExcretaRelatedIndustrySaveItem(
|
|
row: Record<string, string | number>,
|
|
): ExcretaRelatedIndustrySaveItem {
|
|
return {
|
|
admdstCd: String(row.admdstCd ?? "").trim(),
|
|
admdstNm: String(row.admdstNm ?? "").trim(),
|
|
bzentyCnt: toGridNumber(row.bzentyCnt),
|
|
empNope: toGridNumber(row.empNope),
|
|
n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt),
|
|
n4TonUdrCnt: toGridNumber(row.n4TonUdrCnt),
|
|
n8TonUdrCnt: toGridNumber(row.n8TonUdrCnt),
|
|
etcCnt: toGridNumber(row.etcCnt),
|
|
clctCnvncPrfmncQnt: toGridNumber(row.clctCnvncPrfmncQnt),
|
|
rmrk: nullableTrimmedString(row.rmrk),
|
|
};
|
|
}
|
|
|
|
/** ST00120004 — 그리드 행 → 저장/제출 API 항목 */
|
|
function buildPrivateSewageTreatRelatedIndustrySaveItem(
|
|
row: Record<string, string | number>,
|
|
): PrivateSewageTreatRelatedIndustrySaveItem {
|
|
return {
|
|
admdstCd: String(row.admdstCd ?? "").trim(),
|
|
admdstNm: String(row.admdstNm ?? "").trim(),
|
|
designMdsrCnt: toGridNumber(row.designMdsrCnt),
|
|
designMdsrNope: toGridNumber(row.designMdsrNope),
|
|
mfbizCnt: toGridNumber(row.mfbizCnt),
|
|
mfbizMdsrNope: toGridNumber(row.mfbizMdsrNope),
|
|
mngCnt: toGridNumber(row.mngCnt),
|
|
mngMdsrNope: toGridNumber(row.mngMdsrNope),
|
|
rmrk: nullableTrimmedString(row.rmrk),
|
|
};
|
|
}
|
|
|
|
function buildDistrictRowSaveItem(
|
|
tabKey: string,
|
|
row: Record<string, string | number>,
|
|
): ExcretaRelatedIndustrySaveItem | PrivateSewageTreatRelatedIndustrySaveItem {
|
|
if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) {
|
|
return buildExcretaRelatedIndustrySaveItem(row);
|
|
}
|
|
return buildPrivateSewageTreatRelatedIndustrySaveItem(row);
|
|
}
|
|
|
|
function resolveStatsCdForTab(
|
|
tabKey: string,
|
|
tabStatsCdMap: Map<string, string>,
|
|
): string {
|
|
const fromMap = tabStatsCdMap.get(tabKey);
|
|
if (fromMap) return fromMap;
|
|
if (tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey) {
|
|
return SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD;
|
|
}
|
|
if (tabKey === PublicInstitutionExcretaIndustryInpColumn.tabKey) {
|
|
return SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD;
|
|
}
|
|
if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) {
|
|
return SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD;
|
|
}
|
|
if (tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey) {
|
|
return SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD;
|
|
}
|
|
return tabKey;
|
|
}
|
|
|
|
type GridRowWithVerification = Record<string, string | number> & {
|
|
verificationResult?: SewageExcretaRelatedVerificationResult;
|
|
};
|
|
|
|
function mergeCellStyleResults(
|
|
...results: (CellStyleCallbackResult | undefined)[]
|
|
): CellStyleCallbackResult | undefined {
|
|
const styleNames: string[] = [];
|
|
let editable: boolean | undefined;
|
|
let readOnly: boolean | undefined;
|
|
|
|
for (const result of results) {
|
|
if (!result) continue;
|
|
if (typeof result === "string") {
|
|
styleNames.push(result);
|
|
continue;
|
|
}
|
|
if (result.styleName) styleNames.push(result.styleName);
|
|
if (result.editable === false) editable = false;
|
|
if (result.readOnly === true) readOnly = true;
|
|
}
|
|
|
|
const styleName = styleNames.join(" ").trim();
|
|
if (!styleName && editable === undefined && readOnly === undefined) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(styleName ? { styleName } : {}),
|
|
...(editable !== undefined ? { editable } : {}),
|
|
...(readOnly !== undefined ? { readOnly } : {}),
|
|
};
|
|
}
|
|
|
|
function verificationCellStyle({
|
|
fieldName,
|
|
row,
|
|
}: CellStyleCallbackParams): CellStyleCallbackResult | undefined {
|
|
const verification = (row as GridRowWithVerification).verificationResult;
|
|
if (!verification) return;
|
|
const errors = verification[fieldName];
|
|
if (Array.isArray(errors) && errors.length > 0) {
|
|
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
|
|
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
|
}
|
|
}
|
|
|
|
function verificationCellTooltip({
|
|
fieldName,
|
|
row,
|
|
}: CellTooltipCallbackParams): string | undefined {
|
|
const verification = (row as GridRowWithVerification).verificationResult;
|
|
if (!verification) return;
|
|
const errors = verification[fieldName];
|
|
if (Array.isArray(errors) && errors.length > 0) {
|
|
return errors
|
|
.map((e) => {
|
|
const prefix =
|
|
e.vrfcCd === "IMR"
|
|
? "IMR검증 - "
|
|
: e.vrfcCd === "NV"
|
|
? "일반검증 - "
|
|
: "HB검증 - ";
|
|
return `${prefix}${e.rsnExpln}
|
|
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
|
})
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
}
|
|
}
|
|
|
|
function buildSewageExcretaRelatedSearchParam(
|
|
values: SearchFormValues,
|
|
overrides?: { mode?: string },
|
|
): SewageExcretaRelatedRequestParams {
|
|
return {
|
|
statsYr: values.year,
|
|
sewerBzmnCd: values.bsnmCd,
|
|
ctpvNm: values.sido,
|
|
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
|
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
|
};
|
|
}
|
|
|
|
function mapListToGridRows<T extends { verificationResult?: SewageExcretaRelatedVerificationResult }>(
|
|
list: T[] | undefined,
|
|
ctpvNm: string,
|
|
): GridRowWithVerification[] {
|
|
return (list ?? []).map((item) => {
|
|
const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification;
|
|
if (item.verificationResult) {
|
|
row.verificationResult = item.verificationResult;
|
|
}
|
|
return row;
|
|
});
|
|
}
|
|
|
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function waitForImrJobCompleted(
|
|
jobId: string,
|
|
getStatus: (id: string) => Promise<ImrStatusResponse>,
|
|
options?: {
|
|
pollIntervalMs?: number;
|
|
maxAttempts?: number;
|
|
onProgress?: (jobProgress: number) => void;
|
|
},
|
|
): Promise<void> {
|
|
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
|
|
const maxAttempts = options?.maxAttempts ?? 150;
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const res = await getStatus(jobId);
|
|
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
|
|
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
|
|
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
|
|
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
|
|
}
|
|
await sleep(pollIntervalMs);
|
|
}
|
|
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
|
|
}
|
|
|
|
// 하수도 통계 관리 > 12. 하수 및 분뇨관련산업
|
|
const SewageExcretaRelatedInp = () => {
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const { confirm } = useConfirm();
|
|
const isAdminInfo = useAdminStore((state) => state.isAdminInfo);
|
|
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
|
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
|
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
|
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
|
|
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
|
|
|
const [activeTab, setActiveTab] = useState(firstTabKey);
|
|
const [rowDataMap, setRowDataMap] = useState<
|
|
Map<string, Record<string, string | number>[]>
|
|
>(new Map());
|
|
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
|
|
Map<string, SewerageStatusInfo | null>
|
|
>(new Map());
|
|
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
|
|
Map<string, TabToolbarSnapshot>
|
|
>(new Map());
|
|
const [columnGuidesByTab, setColumnGuidesByTab] = useState<
|
|
Record<string, Record<string, string>>
|
|
>({});
|
|
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
|
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(
|
|
null,
|
|
);
|
|
const [hasSearched, setHasSearched] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
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(),
|
|
);
|
|
const [deleteListByTab, setDeleteListByTab] = useState<
|
|
Map<string, Record<string, string | number>[]>
|
|
>(() => new Map());
|
|
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
|
|
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
|
const [showSubmitDialog, setShowSubmitDialog] = useState(false);
|
|
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
|
|
|
|
const searchDoneRef = useRef(false);
|
|
const periodEnterableRef = useRef(true);
|
|
const searchParamRef = useRef<SewageExcretaRelatedRequestParams | null>(null);
|
|
const activeTabRef = useRef(activeTab);
|
|
const loadedTabKeysRef = useRef<Set<string>>(new Set());
|
|
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
|
const getGridRef = useRef<RealGridReact | null>(null);
|
|
/** ST00120001·ST00120002 — 당해 조회 API로 로드된 ognzSn (deleteList 대상) */
|
|
const persistedArticleOgnzSnByTabRef = useRef<Map<string, ArticleOgnzSnSet>>(new Map());
|
|
/** ST00120001·ST00120002 — 전년도(PRE) 조회로 로드된 ognzSn */
|
|
const preYearArticleOgnzSnByTabRef = useRef<Map<string, ArticleOgnzSnSet>>(new Map());
|
|
const sggOptionsSidoRef = useRef<string | null>(null);
|
|
activeTabRef.current = activeTab;
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
void (async () => {
|
|
const entries = await Promise.all(
|
|
SewageExcretaRelatedInpTabList.map(async (tab) => {
|
|
const tabKey = tab.tabKey;
|
|
try {
|
|
const res = await statisticsApi.getColumnGuides(tabKey);
|
|
if (res.status !== "success" || !Array.isArray(res.data)) {
|
|
return [tabKey, {}] as const;
|
|
}
|
|
const next: Record<string, string> = {};
|
|
for (const item of res.data) {
|
|
const key = item.engColNm?.trim();
|
|
const guide = item.columnGuide?.trim();
|
|
if (key && guide) next[key] = guide;
|
|
}
|
|
return [tabKey, next] as const;
|
|
} catch {
|
|
return [tabKey, {}] as const;
|
|
}
|
|
}),
|
|
);
|
|
if (cancelled) return;
|
|
setColumnGuidesByTab(Object.fromEntries(entries));
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const shouldShowNotApplicable = (
|
|
tabKey: string,
|
|
hasRows: boolean,
|
|
statusInfo: SewerageStatusInfo | null | undefined,
|
|
): boolean =>
|
|
NOT_APPLICABLE_TAB_KEYS.has(tabKey) &&
|
|
hasSearched &&
|
|
!hasRows &&
|
|
computeTabEnterable(statusInfo, inputPeriodEnterable);
|
|
|
|
const setIsNotApplicableForTab = useCallback((tabKey: string, nextValue: boolean) => {
|
|
setIsNotApplicableByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, nextValue);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const clearDeleteListForTab = useCallback((tabKey: string) => {
|
|
setDeleteListByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.delete(tabKey);
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
|
if (!sido || sido === "ALL") {
|
|
setSggOptions([]);
|
|
return [];
|
|
}
|
|
try {
|
|
const res = await statisticsApi.getSggAllList(sido);
|
|
const list = parseSggListFromResponse(res);
|
|
setSggOptions(list);
|
|
return list;
|
|
} catch {
|
|
setSggOptions([]);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
const commitToolbarSnapshotForLoadedTab = (
|
|
tabKey: string,
|
|
statusInfo: SewerageStatusInfo | null,
|
|
hasRowData: boolean,
|
|
searchContext: SearchFormValues,
|
|
) => {
|
|
setToolbarSnapshotByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(
|
|
tabKey,
|
|
computeTabToolbarSnapshot(
|
|
statusInfo,
|
|
hasRowData,
|
|
searchDoneRef.current,
|
|
searchContext,
|
|
periodEnterableRef.current,
|
|
isAdminInfo,
|
|
),
|
|
);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const markTabLoaded = (tabKey: string) => {
|
|
loadedTabKeysRef.current.add(tabKey);
|
|
setLoadedTabKeys(new Set(loadedTabKeysRef.current));
|
|
};
|
|
|
|
const getTabGridRows = (tabKey: string): Record<string, string | number>[] => {
|
|
const mapRows = rowDataMap.get(tabKey) ?? [];
|
|
if (tabKey !== activeTabRef.current) return mapRows;
|
|
const gridRows = getGridDataRef.current?.() ?? mapRows;
|
|
if (gridRows.length === 0) return gridRows;
|
|
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey) || mapRows.length === 0) return gridRows;
|
|
return mergeGridRowsWithArticleMeta(gridRows, mapRows);
|
|
};
|
|
|
|
const commitActiveGrid = () => {
|
|
const gridView = getGridRef.current?.gridView as
|
|
| { commit?: (force?: boolean) => void; cancel?: () => void }
|
|
| undefined;
|
|
try {
|
|
gridView?.commit?.(true);
|
|
} catch {
|
|
gridView?.cancel?.();
|
|
}
|
|
};
|
|
|
|
const handleActiveTabChange = async (tabKey: string) => {
|
|
const prevTabKey = activeTabRef.current;
|
|
commitActiveGrid();
|
|
const latestRowsInPrevTab = getTabGridRows(prevTabKey);
|
|
if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) {
|
|
setRowDataMap((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(prevTabKey, latestRowsInPrevTab);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
flushSync(() => {
|
|
activeTabRef.current = tabKey;
|
|
setActiveTab(tabKey);
|
|
});
|
|
|
|
if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return;
|
|
if (loadedTabKeysRef.current.has(tabKey)) return;
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
lastSearchedValues,
|
|
true,
|
|
false,
|
|
);
|
|
} catch {
|
|
alert("데이터 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const applyRowMap = (
|
|
mergeIntoExistingMap: boolean,
|
|
apply: (m: Map<string, Record<string, string | number>[]>) => void,
|
|
) => {
|
|
if (mergeIntoExistingMap) {
|
|
setRowDataMap((prev) => {
|
|
const next = new Map(prev);
|
|
apply(next);
|
|
return next;
|
|
});
|
|
} else {
|
|
const next = new Map<string, Record<string, string | number>[]>();
|
|
apply(next);
|
|
setRowDataMap(next);
|
|
}
|
|
};
|
|
|
|
const loadActiveTabData = async (
|
|
tabKey: string,
|
|
param: SewageExcretaRelatedRequestParams,
|
|
searchContext: SearchFormValues,
|
|
mergeIntoExistingMap: boolean,
|
|
updateLastSearchedValues: boolean,
|
|
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 (!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 =
|
|
await statisticsInputSewageExcretaRelatedApi.getPublicInstitutionSewerIndustryList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, publicInstitutionSewerIndustryList } = response.data;
|
|
const gridRows = mapListToGridRows(
|
|
publicInstitutionSewerIndustryList,
|
|
commonInfo.ctpvNm,
|
|
);
|
|
syncPublicInstitutionArticleTracking(
|
|
tabKey,
|
|
gridRows,
|
|
fromPreYear,
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
clearDeleteListForTab,
|
|
);
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(PublicInstitutionSewerIndustryInpColumn.tabKey, gridRows);
|
|
});
|
|
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);
|
|
markTabLoaded(tabKey);
|
|
return true;
|
|
}
|
|
|
|
if (tabKey === PublicInstitutionExcretaIndustryInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputSewageExcretaRelatedApi.getPublicInstitutionExcretaIndustryList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, publicInstitutionExcretaIndustryList } =
|
|
response.data;
|
|
const gridRows = mapListToGridRows(
|
|
publicInstitutionExcretaIndustryList,
|
|
commonInfo.ctpvNm,
|
|
);
|
|
syncPublicInstitutionArticleTracking(
|
|
tabKey,
|
|
gridRows,
|
|
fromPreYear,
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
clearDeleteListForTab,
|
|
);
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(PublicInstitutionExcretaIndustryInpColumn.tabKey, gridRows);
|
|
});
|
|
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);
|
|
markTabLoaded(tabKey);
|
|
return true;
|
|
}
|
|
|
|
if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputSewageExcretaRelatedApi.getExcretaRelatedIndustryList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, excretaRelatedIndustryList } = response.data;
|
|
const gridRows = mapListToGridRows(excretaRelatedIndustryList, commonInfo.ctpvNm);
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(ExcretaRelatedIndustryInpColumn.tabKey, gridRows);
|
|
});
|
|
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,
|
|
);
|
|
markTabLoaded(tabKey);
|
|
return true;
|
|
}
|
|
|
|
if (tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey) {
|
|
const response =
|
|
await statisticsInputSewageExcretaRelatedApi.getPrivateSewageTreatRelatedIndustryList(
|
|
requestParam,
|
|
);
|
|
if (response.status !== "success" || !response.data) {
|
|
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
|
return false;
|
|
}
|
|
const { commonInfo, statusInfo, privateSewageTreatRelatedIndustryList } =
|
|
response.data;
|
|
const gridRows = mapListToGridRows(
|
|
privateSewageTreatRelatedIndustryList,
|
|
commonInfo.ctpvNm,
|
|
);
|
|
applyRowMap(mergeIntoExistingMap, (m) => {
|
|
m.set(PrivateSewageTreatRelatedIndustryInpColumn.tabKey, gridRows);
|
|
});
|
|
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,
|
|
);
|
|
markTabLoaded(tabKey);
|
|
return true;
|
|
}
|
|
|
|
alert("해당 탭 조회 API가 없습니다.");
|
|
return false;
|
|
};
|
|
|
|
const handleReset = () => {
|
|
setRowDataMap(new Map());
|
|
setTabStatusInfoMap(new Map());
|
|
setToolbarSnapshotByTab(new Map());
|
|
setActiveTab(firstTabKey);
|
|
activeTabRef.current = firstTabKey;
|
|
setLastSearchedValues(null);
|
|
setHasSearched(false);
|
|
setInputPeriodEnterable(true);
|
|
searchDoneRef.current = false;
|
|
periodEnterableRef.current = true;
|
|
searchParamRef.current = null;
|
|
loadedTabKeysRef.current.clear();
|
|
setLoadedTabKeys(new Set());
|
|
setTabStatsCdMap(new Map());
|
|
setIsVerificationFailedByTab(new Map());
|
|
setIsNotApplicableByTab(new Map());
|
|
setDeleteListByTab(new Map());
|
|
clearArticleOgnzSnTracking(
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
);
|
|
setSggOptions([]);
|
|
sggOptionsSidoRef.current = null;
|
|
setVerificationHistoryOpen(false);
|
|
};
|
|
|
|
const handleSearch = async (
|
|
values: SearchFormValues,
|
|
opts?: { forcedTabKey?: string },
|
|
) => {
|
|
if (!values.sido || values.sido === "ALL") {
|
|
alert("시도를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!values.bsnmCd) {
|
|
alert("사업자를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const periodResponse = await statisticsInputSewSupplyRateApi.checkInputPeriod(
|
|
values.year || "",
|
|
);
|
|
if (periodResponse.status !== "success") {
|
|
alert(periodResponse.message);
|
|
return;
|
|
}
|
|
const periods = Array.isArray(periodResponse.data) ? periodResponse.data : [];
|
|
const periodEnterable = isInputPeriodCurrent(periods);
|
|
periodEnterableRef.current = periodEnterable;
|
|
setInputPeriodEnterable(periodEnterable);
|
|
} catch {
|
|
alert("입력 기간 확인 중 오류가 발생했습니다.");
|
|
return;
|
|
}
|
|
|
|
const yearChangedFromLastSearch =
|
|
lastSearchedValues != null &&
|
|
(lastSearchedValues.year ?? "") !== (values.year ?? "");
|
|
|
|
if (yearChangedFromLastSearch) {
|
|
setRowDataMap(new Map());
|
|
setTabStatusInfoMap(new Map());
|
|
setTabStatsCdMap(new Map());
|
|
setIsVerificationFailedByTab(new Map());
|
|
setToolbarSnapshotByTab(new Map());
|
|
setIsNotApplicableByTab(new Map());
|
|
setDeleteListByTab(new Map());
|
|
clearArticleOgnzSnTracking(
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
);
|
|
loadedTabKeysRef.current.clear();
|
|
setLoadedTabKeys(new Set());
|
|
activeTabRef.current = firstTabKey;
|
|
setActiveTab(firstTabKey);
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
searchDoneRef.current = true;
|
|
setHasSearched(true);
|
|
setSearchValues(values);
|
|
|
|
const searchParam = buildSewageExcretaRelatedSearchParam(values);
|
|
searchParamRef.current = searchParam;
|
|
|
|
const loadedSgg = await loadSggOptions(values.sido);
|
|
sggOptionsSidoRef.current = values.sido;
|
|
if (loadedSgg.length === 0) {
|
|
alert("시군구 목록을 불러오지 못했습니다.");
|
|
return;
|
|
}
|
|
|
|
if (opts?.forcedTabKey) {
|
|
activeTabRef.current = opts.forcedTabKey;
|
|
setActiveTab(opts.forcedTabKey);
|
|
}
|
|
|
|
const ok = await loadActiveTabData(
|
|
opts?.forcedTabKey ?? activeTabRef.current,
|
|
searchParam,
|
|
values,
|
|
!yearChangedFromLastSearch,
|
|
true,
|
|
);
|
|
if (!ok) return;
|
|
} catch {
|
|
alert("데이터 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const cnacelSubmitFunc = () => {
|
|
void (async () => {
|
|
try {
|
|
if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return;
|
|
|
|
const ctx = lastSearchedValues ?? searchValues;
|
|
if (!ctx) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
|
|
const userInfo = await getUserInfo();
|
|
const tabKey = activeTabRef.current;
|
|
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
|
|
|
|
const requestData: cancelSubmitRequest = {
|
|
sewerBzmnCd: ctx.bsnmCd ?? "",
|
|
ctpvNm: ctx.sido ?? "",
|
|
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
|
statsYr: ctx.year ?? "",
|
|
mdfrId: userInfo.userUnqId,
|
|
statsCd: statsCdForTab,
|
|
};
|
|
|
|
const response = await statisticsApi.cancelSubmit(requestData);
|
|
|
|
if (response.status === "success") {
|
|
if (searchParamRef.current) {
|
|
setIsLoading(true);
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current,
|
|
ctx,
|
|
true,
|
|
false,
|
|
);
|
|
}
|
|
alert("제출 취소되었습니다.");
|
|
} else {
|
|
alert(response.message ?? "제출 취소 중 오류가 발생했습니다.");
|
|
}
|
|
} catch {
|
|
alert("제출 취소 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const openVerificationHistoryDialog = () => {
|
|
const ctx = lastSearchedValues ?? searchValues;
|
|
const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
|
|
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
setVerificationHistoryOpen(true);
|
|
};
|
|
|
|
const lastYearDataFunc = async () => {
|
|
const values = lastSearchedValues ?? searchValues;
|
|
if (!values?.sido || values.sido === "ALL") {
|
|
alert("시도를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!values.bsnmCd) {
|
|
alert("사업자를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!hasSearched || !searchParamRef.current) {
|
|
alert("먼저 데이터를 조회해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const param = buildSewageExcretaRelatedSearchParam(values, { mode: "PRE" });
|
|
await loadActiveTabData(
|
|
activeTabRef.current,
|
|
param,
|
|
values,
|
|
true,
|
|
false,
|
|
{ mode: "PRE" },
|
|
);
|
|
} catch {
|
|
alert("데이터 조회 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const submitFunc = () => {
|
|
void (async () => {
|
|
const tabKey = activeTabRef.current;
|
|
if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) {
|
|
alert("해당 탭은 제출 기능이 준비되지 않았습니다.");
|
|
return;
|
|
}
|
|
if (!lastSearchedValues || !searchParamRef.current) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
commitActiveGrid();
|
|
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
|
const currentData = getTabGridRows(tabKey);
|
|
if (NOT_APPLICABLE_TAB_KEYS.has(tabKey)) {
|
|
if (!noApplicable && currentData.length === 0) {
|
|
alert("해당사항 없음을 체크해주세요.");
|
|
return;
|
|
}
|
|
} else if (currentData.length === 0) {
|
|
alert("제출할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
if (await confirm("제출 하시겠습니까?")) {
|
|
setShowSubmitDialog(true);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
|
const tabKey = activeTabRef.current;
|
|
if (!SUBMIT_SUPPORTED_TAB_KEYS.has(tabKey)) {
|
|
alert("해당 탭은 제출 기능이 준비되지 않았습니다.");
|
|
return;
|
|
}
|
|
if (!lastSearchedValues || !searchParamRef.current) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
commitActiveGrid();
|
|
|
|
const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey);
|
|
const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
|
|
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
|
const currentData = getTabGridRows(tabKey);
|
|
const mapRows = rowDataMap.get(tabKey) ?? [];
|
|
|
|
const searchContextBase = {
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
ctpvNm: lastSearchedValues.sido ?? "",
|
|
sggNm:
|
|
lastSearchedValues.gugun === "ALL"
|
|
? ""
|
|
: (lastSearchedValues.gugun ?? ""),
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
};
|
|
|
|
let submitList:
|
|
| PublicInstitutionSewerIndustrySaveItem[]
|
|
| ExcretaRelatedIndustrySaveItem[]
|
|
| PrivateSewageTreatRelatedIndustrySaveItem[];
|
|
let deleteList: PublicInstitutionSewerIndustrySaveItem[] = [];
|
|
let sggList: Sgg[] = [];
|
|
|
|
if (isDistrictRowTab) {
|
|
const rowsForSubmit = mergeGridRowsWithMapRows(currentData, mapRows, [
|
|
"admdstCd",
|
|
"admdstNm",
|
|
]);
|
|
if (rowsForSubmit.length === 0) {
|
|
alert("제출할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
submitList =
|
|
tabKey === ExcretaRelatedIndustryInpColumn.tabKey
|
|
? rowsForSubmit.map((row) => buildExcretaRelatedIndustrySaveItem(row))
|
|
: rowsForSubmit.map((row) =>
|
|
buildPrivateSewageTreatRelatedIndustrySaveItem(row),
|
|
);
|
|
} else if (isPublicInstitutionTab) {
|
|
const sido = lastSearchedValues.sido ?? "";
|
|
let list = sggOptions;
|
|
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
|
|
list = await loadSggOptions(sido);
|
|
sggOptionsSidoRef.current = sido;
|
|
}
|
|
if (list.length === 0) {
|
|
alert("시군구 목록을 불러오지 못했습니다.");
|
|
return;
|
|
}
|
|
sggList = list;
|
|
|
|
const rowsForSubmit = mergeGridRowsWithMapRows(currentData, mapRows, [
|
|
"ognzSn",
|
|
"admdstCd",
|
|
"admdstNm",
|
|
"__isNewArticle",
|
|
]);
|
|
|
|
submitList = noApplicable
|
|
? []
|
|
: rowsForSubmit.map((row) =>
|
|
buildPublicInstitutionIndustrySaveItem(row, list),
|
|
);
|
|
deleteList = (deleteListByTab.get(tabKey) ?? [])
|
|
.filter((row) =>
|
|
isPersistedPublicInstitutionArticleRow(
|
|
tabKey,
|
|
row,
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
),
|
|
)
|
|
.map((row) => buildPublicInstitutionIndustrySaveItem(row, list));
|
|
|
|
if (!noApplicable && submitList.length === 0) {
|
|
alert("해당사항 없음을 체크해주세요.");
|
|
return;
|
|
}
|
|
} else {
|
|
alert("해당 탭은 제출 기능이 준비되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
|
|
const statsCdForTab = resolveStatsCdForTab(tabKey, tabStatsCdMap);
|
|
|
|
const submitRequestBase = {
|
|
...searchContextBase,
|
|
laePicNm: submitInfo.laePicNm,
|
|
laePicTelNo: submitInfo.laePicTelNo,
|
|
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
|
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
|
laeIdfrNm: submitInfo.laeIdfrNm,
|
|
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
|
|
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
|
|
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
|
|
...(isPublicInstitutionTab ? { noApplicable } : {}),
|
|
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
|
};
|
|
|
|
const verificationQuery = {
|
|
sewerBzmnCd: searchContextBase.sewerBzmnCd,
|
|
ctpvNm: searchContextBase.ctpvNm,
|
|
sggNm: searchContextBase.sggNm,
|
|
statsYr: searchContextBase.statsYr,
|
|
statsCd: statsCdForTab,
|
|
};
|
|
|
|
const reloadAfterSubmit = async () => {
|
|
if (isPublicInstitutionTab) {
|
|
setDeleteListByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, []);
|
|
return next;
|
|
});
|
|
preYearArticleOgnzSnByTabRef.current.delete(tabKey);
|
|
if (sggList.length > 0) {
|
|
setSggOptions(sggList);
|
|
}
|
|
}
|
|
await loadActiveTabData(
|
|
tabKey,
|
|
searchParamRef.current!,
|
|
lastSearchedValues,
|
|
true,
|
|
false,
|
|
);
|
|
};
|
|
|
|
try {
|
|
showGlobalProgress({ label: "서버에 요청 중...", progress: 0 });
|
|
const userInfo = await getUserInfo();
|
|
setGlobalProgressLabel("일반 검증 진행 중...");
|
|
|
|
const response =
|
|
tabKey === ExcretaRelatedIndustryInpColumn.tabKey
|
|
? await statisticsInputSewageExcretaRelatedApi.submitExcretaRelatedIndustry({
|
|
...submitRequestBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: submitList as ExcretaRelatedIndustrySaveItem[],
|
|
})
|
|
: tabKey === PrivateSewageTreatRelatedIndustryInpColumn.tabKey
|
|
? await statisticsInputSewageExcretaRelatedApi.submitPrivateSewageTreatRelatedIndustry(
|
|
{
|
|
...submitRequestBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList:
|
|
submitList as PrivateSewageTreatRelatedIndustrySaveItem[],
|
|
},
|
|
)
|
|
: tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey
|
|
? await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionSewerIndustry(
|
|
{
|
|
...submitRequestBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: submitList as PublicInstitutionSewerIndustrySaveItem[],
|
|
deleteList,
|
|
},
|
|
)
|
|
: await statisticsInputSewageExcretaRelatedApi.submitPublicInstitutionExcretaIndustry(
|
|
{
|
|
...submitRequestBase,
|
|
userId: userInfo.userUnqId,
|
|
submitList: submitList as PublicInstitutionSewerIndustrySaveItem[],
|
|
deleteList,
|
|
},
|
|
);
|
|
|
|
if (response.status !== "success") {
|
|
alert(response.message ?? "데이터 제출 중 오류가 발생했습니다.");
|
|
return;
|
|
}
|
|
|
|
const finalStatus = response.data?.finalStatus;
|
|
const nvpassed = response.data?.nvpassed;
|
|
|
|
if (finalStatus === "5") {
|
|
setGlobalProgress(50);
|
|
setGlobalProgressLabel("IMR 검증 진행 중...");
|
|
const imrParam = {
|
|
user_id: userInfo.userUnqId,
|
|
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "",
|
|
stats_cd: statsCdForTab,
|
|
stats_yr: lastSearchedValues.year ?? "",
|
|
lae_sbmsn_stts_cd:
|
|
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
|
|
gen_verify_stts_cd: nvpassed ? "1" : "2",
|
|
};
|
|
|
|
try {
|
|
const imrResponse = await statisticsApi.callImrService(imrParam);
|
|
if (imrResponse.job_id) {
|
|
await waitForImrJobCompleted(
|
|
imrResponse.job_id,
|
|
(id) => statisticsApi.getImrStatus(id),
|
|
{
|
|
onProgress: (jobProgress) => {
|
|
setGlobalProgress(50 + Math.round(jobProgress / 2));
|
|
},
|
|
},
|
|
);
|
|
}
|
|
setGlobalProgress(100);
|
|
await reloadAfterSubmit();
|
|
await openVerificationResultDialogIfNeeded(
|
|
verificationQuery,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert },
|
|
);
|
|
} catch (e) {
|
|
await reloadAfterSubmit();
|
|
await openVerificationResultDialogIfNeeded(
|
|
verificationQuery,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert, notifySubmitComplete: false },
|
|
);
|
|
const msg =
|
|
e instanceof Error && e.message
|
|
? e.message
|
|
: "IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.";
|
|
alert(msg);
|
|
}
|
|
} else {
|
|
setGlobalProgress(100);
|
|
await reloadAfterSubmit();
|
|
await openVerificationResultDialogIfNeeded(
|
|
verificationQuery,
|
|
setVerificationResultDialogOpen,
|
|
{ notify: alert },
|
|
);
|
|
}
|
|
} catch {
|
|
alert("데이터 제출 중 오류가 발생했습니다.");
|
|
} finally {
|
|
hideGlobalProgress();
|
|
setShowSubmitDialog(false);
|
|
}
|
|
};
|
|
|
|
const saveFunc = () => {
|
|
void (async () => {
|
|
const tabKey = activeTabRef.current;
|
|
if (!lastSearchedValues) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(lastSearchedValues)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
if (!SAVE_SUPPORTED_TAB_KEYS.has(tabKey)) {
|
|
alert("해당 탭은 저장 기능이 준비되지 않았습니다.");
|
|
return;
|
|
}
|
|
if (!(await confirm("저장 하시겠습니까?"))) return;
|
|
|
|
commitActiveGrid();
|
|
|
|
const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey);
|
|
const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
|
|
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false;
|
|
const currentData = getTabGridRows(tabKey);
|
|
const mapRows = rowDataMap.get(tabKey) ?? [];
|
|
|
|
const baseSaveRequest = {
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
ctpvNm: lastSearchedValues.sido ?? "",
|
|
sggNm:
|
|
lastSearchedValues.gugun === "ALL"
|
|
? ""
|
|
: (lastSearchedValues.gugun ?? ""),
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
|
|
...(isPublicInstitutionTab && noApplicable
|
|
? { noApplicable: true as const }
|
|
: {}),
|
|
};
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
const userInfo = await getUserInfo();
|
|
const userFields = {
|
|
userId: userInfo.userUnqId,
|
|
...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}),
|
|
};
|
|
|
|
let response: { status: string; message?: string };
|
|
let sggList: Sgg[] = [];
|
|
|
|
if (isDistrictRowTab) {
|
|
const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [
|
|
"admdstCd",
|
|
"admdstNm",
|
|
]);
|
|
const saveList = rowsForSave.map((row) =>
|
|
buildDistrictRowSaveItem(tabKey, row),
|
|
);
|
|
if (saveList.length === 0) {
|
|
alert("저장할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
response =
|
|
tabKey === ExcretaRelatedIndustryInpColumn.tabKey
|
|
? await statisticsInputSewageExcretaRelatedApi.saveExcretaRelatedIndustry(
|
|
{
|
|
...baseSaveRequest,
|
|
...userFields,
|
|
saveList: saveList as ExcretaRelatedIndustrySaveItem[],
|
|
},
|
|
)
|
|
: await statisticsInputSewageExcretaRelatedApi.savePrivateSewageTreatRelatedIndustry(
|
|
{
|
|
...baseSaveRequest,
|
|
...userFields,
|
|
saveList:
|
|
saveList as PrivateSewageTreatRelatedIndustrySaveItem[],
|
|
},
|
|
);
|
|
} else if (isPublicInstitutionTab) {
|
|
const sido = lastSearchedValues.sido ?? "";
|
|
let list = sggOptions;
|
|
if (sggOptionsSidoRef.current !== sido || list.length === 0) {
|
|
list = await loadSggOptions(sido);
|
|
sggOptionsSidoRef.current = sido;
|
|
}
|
|
if (list.length === 0) {
|
|
alert("시군구 목록을 불러오지 못했습니다.");
|
|
return;
|
|
}
|
|
sggList = list;
|
|
|
|
const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [
|
|
"ognzSn",
|
|
"admdstCd",
|
|
"admdstNm",
|
|
"__isNewArticle",
|
|
]);
|
|
|
|
const saveList = noApplicable
|
|
? []
|
|
: rowsForSave.map((row) =>
|
|
buildPublicInstitutionIndustrySaveItem(row, list),
|
|
);
|
|
const deleteList = (deleteListByTab.get(tabKey) ?? [])
|
|
.filter((row) =>
|
|
isPersistedPublicInstitutionArticleRow(
|
|
tabKey,
|
|
row,
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
),
|
|
)
|
|
.map((row) => buildPublicInstitutionIndustrySaveItem(row, list));
|
|
|
|
if (!noApplicable && saveList.length === 0) {
|
|
alert("해당사항 없음을 체크해주세요.");
|
|
return;
|
|
}
|
|
|
|
response =
|
|
tabKey === PublicInstitutionSewerIndustryInpColumn.tabKey
|
|
? await statisticsInputSewageExcretaRelatedApi.savePublicInstitutionSewerIndustry(
|
|
{
|
|
...baseSaveRequest,
|
|
...userFields,
|
|
saveList,
|
|
deleteList,
|
|
},
|
|
)
|
|
: await statisticsInputSewageExcretaRelatedApi.savePublicInstitutionExcretaIndustry(
|
|
{
|
|
...baseSaveRequest,
|
|
...userFields,
|
|
saveList,
|
|
deleteList,
|
|
},
|
|
);
|
|
} else {
|
|
alert("해당 탭은 저장 기능이 준비되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
if (response.status === "success") {
|
|
if (isPublicInstitutionTab) {
|
|
setDeleteListByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, []);
|
|
return next;
|
|
});
|
|
preYearArticleOgnzSnByTabRef.current.delete(tabKey);
|
|
if (sggList.length > 0) {
|
|
setSggOptions(sggList);
|
|
}
|
|
}
|
|
|
|
const param =
|
|
searchParamRef.current ??
|
|
buildSewageExcretaRelatedSearchParam(lastSearchedValues);
|
|
await loadActiveTabData(tabKey, param, lastSearchedValues, true, false);
|
|
alert("저장되었습니다.");
|
|
} else {
|
|
alert(response.message ?? "저장 중 오류가 발생했습니다.");
|
|
}
|
|
} catch {
|
|
alert("저장 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
const addArticleFunc = async () => {
|
|
const ctx = lastSearchedValues ?? searchValues;
|
|
if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(ctx)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
const tabKey = activeTabRef.current;
|
|
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
|
|
alert("항목을 추가할 수 없는 탭입니다.");
|
|
return;
|
|
}
|
|
|
|
let list = sggOptions;
|
|
if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) {
|
|
list = await loadSggOptions(ctx.sido);
|
|
sggOptionsSidoRef.current = ctx.sido;
|
|
}
|
|
if (list.length === 0) {
|
|
alert("시군구 목록을 불러오지 못했습니다.");
|
|
return;
|
|
}
|
|
|
|
const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : "";
|
|
commitActiveGrid();
|
|
const currentRows = getTabGridRows(tabKey);
|
|
const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm);
|
|
newRow.__isNewArticle = 1;
|
|
if (sggNm) {
|
|
const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(newRow, list);
|
|
newRow.admdstCd = admdstCd;
|
|
newRow.admdstNm = admdstNm;
|
|
}
|
|
|
|
flushSync(() => {
|
|
setSggOptions(list);
|
|
setRowDataMap((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, [...currentRows, newRow]);
|
|
return next;
|
|
});
|
|
setIsNotApplicableForTab(tabKey, false);
|
|
});
|
|
|
|
commitToolbarSnapshotForLoadedTab(
|
|
tabKey,
|
|
tabStatusInfoMap.get(tabKey) ?? null,
|
|
true,
|
|
ctx,
|
|
);
|
|
};
|
|
|
|
const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => {
|
|
const gridView = getGridRef.current?.gridView as
|
|
| {
|
|
getCheckedItems?: () => number[];
|
|
getCheckedRows?: () => number[];
|
|
}
|
|
| undefined;
|
|
|
|
const checked =
|
|
(typeof gridView?.getCheckedItems === "function"
|
|
? gridView.getCheckedItems()
|
|
: typeof gridView?.getCheckedRows === "function"
|
|
? gridView.getCheckedRows()
|
|
: []) ?? [];
|
|
|
|
return checked.filter(
|
|
(idx) => Number.isInteger(idx) && idx >= 0 && idx < rowCount,
|
|
);
|
|
};
|
|
|
|
const deleteArticleFunc = () => {
|
|
const tabKey = activeTabRef.current;
|
|
if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) {
|
|
alert("항목을 제외할 수 없는 탭입니다.");
|
|
return;
|
|
}
|
|
|
|
const ctx = lastSearchedValues ?? searchValues;
|
|
if (!ctx) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
if (!buildGugunOkForToolbar(ctx)) {
|
|
alert("시군구를 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
commitActiveGrid();
|
|
const currentRows = getTabGridRows(tabKey);
|
|
if (currentRows.length === 0) {
|
|
alert("삭제할 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const checkedIndexes = getCheckedItemIndexesFromGrid(currentRows.length);
|
|
const checkedIndexSet =
|
|
checkedIndexes.length > 0
|
|
? new Set(checkedIndexes)
|
|
: new Set(
|
|
currentRows
|
|
.map((row, idx) => (isCheckedRow(row) ? idx : -1))
|
|
.filter((idx) => idx >= 0),
|
|
);
|
|
|
|
const removedRows = currentRows.filter((_, idx) => checkedIndexSet.has(idx));
|
|
const nextRows = currentRows.filter((_, idx) => !checkedIndexSet.has(idx));
|
|
if (removedRows.length === 0) {
|
|
alert("삭제할 항목을 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
const persistedRemovedRows = removedRows.filter((row) =>
|
|
isPersistedPublicInstitutionArticleRow(
|
|
tabKey,
|
|
row,
|
|
persistedArticleOgnzSnByTabRef,
|
|
preYearArticleOgnzSnByTabRef,
|
|
),
|
|
);
|
|
if (persistedRemovedRows.length > 0) {
|
|
setDeleteListByTab((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, [...(prev.get(tabKey) ?? []), ...persistedRemovedRows]);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
setRowDataMap((prev) => {
|
|
const next = new Map(prev);
|
|
next.set(tabKey, nextRows);
|
|
return next;
|
|
});
|
|
|
|
commitToolbarSnapshotForLoadedTab(
|
|
tabKey,
|
|
tabStatusInfoMap.get(tabKey) ?? null,
|
|
nextRows.length > 0,
|
|
ctx,
|
|
);
|
|
};
|
|
|
|
const columnInfoByTab = useMemo(() => {
|
|
const map = new Map<string, ColumnInfo>();
|
|
for (const tab of SewageExcretaRelatedInpTabList) {
|
|
const tabKey = tab.tabKey;
|
|
const canEdit =
|
|
computeTabEnterable(
|
|
tabStatusInfoMap.get(tabKey) ?? null,
|
|
inputPeriodEnterable,
|
|
) || isAdminInfo;
|
|
const rawColumns = (tab.columns ?? []) as ColumnConfig[];
|
|
const base = canEdit
|
|
? rawColumns
|
|
: rawColumns.map((col) => ({ ...col, editable: false as const }));
|
|
const columns = base.map((col) => {
|
|
let next: ColumnConfig = col;
|
|
if (col.fieldName === "ctpvNm") {
|
|
next = { ...col, editable: false as const, styleName: "rg-cell-readonly" };
|
|
} else if (
|
|
SGG_NM_DROPDOWN_TAB_KEYS.has(tabKey) &&
|
|
col.fieldName === "sggNm"
|
|
) {
|
|
next = buildSggNmColumn(col, canEdit, sggOptions);
|
|
} else if (col.fieldName === "sggNm") {
|
|
next = { ...col, editable: false as const, styleName: "rg-cell-readonly" };
|
|
}
|
|
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
|
return guide ? { ...next, tooltip: guide } : next;
|
|
});
|
|
map.set(tabKey, {
|
|
columns,
|
|
columnLayout: tab.columnLayout ?? [],
|
|
});
|
|
}
|
|
return map;
|
|
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]);
|
|
|
|
const publicInstitutionArticleCellStyleCallback = useCallback<CellStyleCallback>(
|
|
({ fieldName, row }) => {
|
|
if (fieldName !== "sggNm") return undefined;
|
|
if (isNewArticleRow(row)) return undefined;
|
|
return { styleName: "rg-cell-readonly", editable: false };
|
|
},
|
|
[],
|
|
);
|
|
|
|
const resolveGridCellStyleCallback = useCallback(
|
|
(tabKey: string): CellStyleCallback =>
|
|
(params) => {
|
|
const verification = verificationCellStyle(params);
|
|
if (!SGG_NM_DROPDOWN_TAB_KEYS.has(tabKey)) {
|
|
return verification;
|
|
}
|
|
return mergeCellStyleResults(
|
|
verification,
|
|
publicInstitutionArticleCellStyleCallback(params),
|
|
);
|
|
},
|
|
[publicInstitutionArticleCellStyleCallback],
|
|
);
|
|
|
|
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
|
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
|
const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab);
|
|
const activeTabToolbar =
|
|
toolbarSnapshotByTab.get(activeTab) ??
|
|
computeTabToolbarSnapshot(
|
|
activeTabStatus,
|
|
activeTabHasRowData,
|
|
hasSearched,
|
|
searchContextForToolbar,
|
|
inputPeriodEnterable,
|
|
isAdminInfo,
|
|
);
|
|
|
|
const showSaveInToolbar = activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed;
|
|
const showEnterableToolbarButtons =
|
|
activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed;
|
|
const showActionToolbarCard =
|
|
toolbarGroupAllowed &&
|
|
(activeTabToolbar.showActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab)));
|
|
const showArticleEditButtons =
|
|
ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons;
|
|
const showNotApplicableForActiveTab =
|
|
NOT_APPLICABLE_TAB_KEYS.has(activeTab) &&
|
|
loadedTabKeys.has(activeTab) &&
|
|
shouldShowNotApplicable(activeTab, activeTabHasRowData, activeTabStatus);
|
|
const showSecondaryToolbarCard =
|
|
hasSearched &&
|
|
toolbarGroupAllowed &&
|
|
(showSaveInToolbar || showEnterableToolbarButtons);
|
|
const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false;
|
|
const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback(
|
|
(next) => {
|
|
const tabKey = activeTabRef.current;
|
|
const current = isNotApplicableByTab.get(tabKey) ?? false;
|
|
const resolved =
|
|
typeof next === "function"
|
|
? (next as (prevState: boolean) => boolean)(current)
|
|
: next;
|
|
setIsNotApplicableForTab(tabKey, resolved);
|
|
},
|
|
[isNotApplicableByTab, setIsNotApplicableForTab],
|
|
);
|
|
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
|
|
|
|
const verificationQueryContext = (() => {
|
|
const ctx = lastSearchedValues ?? searchValues;
|
|
const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
|
|
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
|
|
return {
|
|
sewerBzmnCd: ctx.bsnmCd,
|
|
ctpvNm: ctx.sido,
|
|
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
|
statsYr: ctx.year,
|
|
statsCd,
|
|
};
|
|
})();
|
|
|
|
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}
|
|
/>
|
|
{showActionToolbarCard && (
|
|
<div className="card !py-0">
|
|
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
|
<VerificationResultButton
|
|
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
|
isVerificationFailed={
|
|
isVerificationFailedByTab.get(activeTab) === true
|
|
}
|
|
/>
|
|
{showEnterableToolbarButtons && (
|
|
<>
|
|
<LastYearDataButton
|
|
getLastYearDataFunc={() => void lastYearDataFunc()}
|
|
/>
|
|
<SubmitButton submitFunc={submitFunc} />
|
|
</>
|
|
)}
|
|
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
|
|
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
|
)}
|
|
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{showSecondaryToolbarCard && (
|
|
<div className="card !py-0">
|
|
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
|
{showNotApplicableForActiveTab && (
|
|
<NotApplicableCheckButton
|
|
isCheck={isNotApplicableForActiveTab}
|
|
setIsCheck={setIsNotApplicableForActiveTab}
|
|
/>
|
|
)}
|
|
{showArticleEditButtons && (
|
|
<>
|
|
<AddArticleButton addArticleFunc={() => void addArticleFunc()} />
|
|
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{isLoading && (
|
|
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
|
<div className="relative size-[5rem]">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={(v) => void handleActiveTabChange(v)}
|
|
className="w-full"
|
|
variant="default"
|
|
>
|
|
<TabsList>
|
|
{SewageExcretaRelatedInpTabList.map((info) => (
|
|
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
|
{info.tabLabel || info.tabKey}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
{SewageExcretaRelatedInpTabList.map((info) => {
|
|
const tabKey = info.tabKey;
|
|
const rowData = rowDataMap.get(tabKey) ?? [];
|
|
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
|
const tabEnterableForGrid =
|
|
computeTabEnterable(tabStatusInfo, inputPeriodEnterable) ||
|
|
isAdminInfo;
|
|
|
|
return (
|
|
<TabsContent key={tabKey} value={tabKey}>
|
|
<DataGrid
|
|
key={`${tabKey}-${tabEnterableForGrid ? "enterable" : "readonly"}`}
|
|
columnInfo={
|
|
columnInfoByTab.get(tabKey) ?? toGridColumnInfo(info)
|
|
}
|
|
rowStartNum={info.rowStartNum}
|
|
excelExportFileSuffix={info.tabLabel || tabKey}
|
|
rowData={rowData}
|
|
setRowData={(updater) => {
|
|
setRowDataMap((prev) => {
|
|
const next = new Map(prev);
|
|
const cur = next.get(tabKey) ?? [];
|
|
const nextRows =
|
|
typeof updater === "function"
|
|
? updater(cur)
|
|
: updater;
|
|
next.set(tabKey, nextRows);
|
|
return next;
|
|
});
|
|
}}
|
|
showHeaderCheckbox={ARTICLE_EDIT_TAB_KEYS.has(tabKey)}
|
|
fixedColumns={info.fixedColumns}
|
|
enableSort={false}
|
|
statusInfo={tabStatusInfo}
|
|
isExcelImport={
|
|
tabRowDataExists(rowDataMap, tabKey) &&
|
|
tabEnterableForGrid &&
|
|
gugunOk
|
|
}
|
|
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
|
|
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
|
|
isExcelExport
|
|
getGridDataRef={
|
|
activeTab === tabKey ? getGridDataRef : undefined
|
|
}
|
|
getGridRef={activeTab === tabKey ? getGridRef : undefined}
|
|
cellStyleCallback={resolveGridCellStyleCallback(tabKey)}
|
|
cellTooltipCallback={verificationCellTooltip}
|
|
/>
|
|
</TabsContent>
|
|
);
|
|
})}
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
<VerificationResultHistoryDialog
|
|
open={verificationHistoryOpen}
|
|
onOpenChange={setVerificationHistoryOpen}
|
|
queryContext={verificationQueryContext}
|
|
onExceptionResetSubmitted={async () => {
|
|
const values = lastSearchedValues ?? searchValues;
|
|
if (!values || !searchParamRef.current) return;
|
|
await loadActiveTabData(
|
|
activeTabRef.current,
|
|
searchParamRef.current,
|
|
values,
|
|
true,
|
|
false,
|
|
);
|
|
}}
|
|
/>
|
|
<ConfirmDialog isOpen={false} />
|
|
<SubmitDailog
|
|
open={showSubmitDialog}
|
|
onOpenChange={setShowSubmitDialog}
|
|
statusInfo={activeTabStatus}
|
|
onConfirm={executeSubmit}
|
|
isLoading={isLoading}
|
|
/>
|
|
<VerificationResultDialog
|
|
open={verificationResultDialogOpen}
|
|
onOpenChange={setVerificationResultDialogOpen}
|
|
queryContext={verificationQueryContext}
|
|
onExceptionSubmitted={async () => {
|
|
const values = lastSearchedValues ?? searchValues;
|
|
if (!values || !searchParamRef.current) return;
|
|
await loadActiveTabData(
|
|
activeTabRef.current,
|
|
searchParamRef.current,
|
|
values,
|
|
true,
|
|
false,
|
|
);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SewageExcretaRelatedInp;
|