From 46cf24c193c1b837fd9c45b65e3045c1f9b10acc Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 23 Jun 2026 10:20:20 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[st]=20-=20=EC=9E=85=EB=A0=A5=EB=8B=A8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85=EC=A4=91,=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=9A=94=EC=95=BD=ED=91=9C=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/st/summary-excel/download/route.ts | 63 + .../src/components/module/CompactDataGrid.tsx | 5 +- apps/st/src/components/module/DataGrid.tsx | 8 + apps/st/src/components/module/SearchForm.tsx | 30 +- apps/st/src/components/module/TabDataGrid.tsx | 5 +- .../statistics/input/SewerageFinanceInp.tsx | 257 +- .../input/SewerageServiceLevelInp.tsx | 1229 +++++++++- .../statistics/input/WaterReuseInp.tsx | 2168 ++++++++++++++++- .../total/SewerageAggregateSummary.tsx | 233 ++ apps/st/src/components/type/DataGrid.type.ts | 6 + .../st/src/components/type/SearchForm.type.ts | 5 +- .../column/input/ExcretaTreatFactInpColumn.ts | 2 +- .../sewagePipe/ConnectionStatusInpColumn.ts | 4 +- .../sewerageFinance/CauserBurdenInpColumn.ts | 4 +- .../SewerageChargeInpColumn.ts | 4 +- .../SewerageFinanceSettleInpColumn.ts | 4 +- .../SewerageUsageFeeInpColumn.ts | 4 +- .../ComplaintInpColumn.ts | 117 +- .../ResidentFriendlyFactInpColumn.ts | 119 +- .../PublicSewageTreatWaterReuseInpColumn.ts | 241 +- .../RainFallUtilFactStatusInpColumn.ts | 194 +- .../RecycledWaterStatusInpColumn.ts | 196 +- ...atWaterReuseBusinessApprStatusInpColumn.ts | 60 +- ...eatWaterReuseDesignConstStatusInpColumn.ts | 60 +- .../total/PublicSewageFacilityColumn.ts | 8 - .../total/SeweragePentrationRateTotColumn.ts | 5 - .../util/grid/computedFieldCallbacks.ts | 19 + ...atistics-input-sewerage-finance-service.ts | 50 + ...cs-input-sewerage-service-level-service.ts | 94 + .../statistics-input-water-reuse-service.ts | 204 ++ ...tics-sewerage-aggregate-summary-service.ts | 104 + ...istics-input-sewerage-finance-interface.ts | 95 +- ...-input-sewerage-service-level-interface.ts | 280 +++ .../statistics-input-water-reuse-interface.ts | 584 +++++ ...cs-sewerage-aggregate-summary-interface.ts | 38 + 35 files changed, 5977 insertions(+), 522 deletions(-) create mode 100644 apps/st/app/api/st/summary-excel/download/route.ts create mode 100644 apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts create mode 100644 apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts diff --git a/apps/st/app/api/st/summary-excel/download/route.ts b/apps/st/app/api/st/summary-excel/download/route.ts new file mode 100644 index 000000000..b1fda0d6b --- /dev/null +++ b/apps/st/app/api/st/summary-excel/download/route.ts @@ -0,0 +1,63 @@ +import { cookies } from "next/headers"; +import { NextResponse } from "next/server"; + +import { + AUTH_USER_ID, + CLAIM_NAME, + SITE_ID, + ST_SERVICE_URL, +} from "@/shared/config"; +import { SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME } from "@/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface"; + +const BACKEND_DOWNLOAD_URL = `${ST_SERVICE_URL}/st-management-service/api/v1/summary-excel/download`; + +/** middleware `/server` rewrite는 장시간 blob 응답에서 연결이 끊길 수 있어 전용 프록시 사용 */ +export const dynamic = "force-dynamic"; +export const maxDuration = 3600; + +export async function POST(request: Request) { + const cookieStore = await cookies(); + const token = cookieStore.get(CLAIM_NAME)?.value; + const userId = cookieStore.get(AUTH_USER_ID)?.value; + + const headers: Record = { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + Accept: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + }; + if (token) headers[CLAIM_NAME] = token; + if (userId) headers[AUTH_USER_ID] = userId; + + let body: string; + try { + body = await request.text(); + } catch { + return NextResponse.json({ message: "요청 본문을 읽을 수 없습니다." }, { status: 400 }); + } + + let backendRes: Response; + try { + backendRes = await fetch(BACKEND_DOWNLOAD_URL, { + method: "POST", + headers, + body, + cache: "no-store", + }); + } catch { + return NextResponse.json( + { message: "엑셀 서버에 연결할 수 없습니다." }, + { status: 502 }, + ); + } + + const responseHeaders = new Headers(); + const contentType = backendRes.headers.get("content-type"); + const contentDisposition = backendRes.headers.get("content-disposition"); + if (contentType) responseHeaders.set("Content-Type", contentType); + if (contentDisposition) responseHeaders.set("Content-Disposition", contentDisposition); + + return new NextResponse(backendRes.body, { + status: backendRes.status, + headers: responseHeaders, + }); +} diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 110d331c6..985e98ea6 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -40,13 +40,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT }; if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; - const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; + const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0; const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; - if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) { + const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 517d26ef5..9cc364c04 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -521,6 +521,7 @@ function buildFieldsFromColumns( const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && + "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && @@ -557,6 +558,12 @@ function buildFieldsFromColumns( col.computed.type === "bodPrcsRt" && "inflwField" in col.computed && "dschgField" in col.computed; + const hasSumPerTenThousand = + col.computed.type === "sumPerTenThousand" && + "numeratorFields" in col.computed && + col.computed.numeratorFields.length > 0 && + "denominatorField" in col.computed && + Boolean(col.computed.denominatorField); const hasQuotientRatioPercent = col.computed.type === "quotientRatioPercent" && "numeratorQuotient" in col.computed && @@ -573,6 +580,7 @@ function buildFieldsFromColumns( hasSumOfMaxGroups || hasPumpGrossPpqty || hasBodPrcsRt || + hasSumPerTenThousand || hasQuotientRatioPercent ) { const computedCallback = getComputedValueCallback( diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 58faea987..0b816a231 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -137,11 +137,11 @@ const SearchForm = (props: SearchFormProps = {}) => { prev.sido ? prev : { - ...prev, - sido: first.sido, - gugun: "", - bsnmCd: first.bsnmCd, - }, + ...prev, + sido: first.sido, + gugun: "", + bsnmCd: first.bsnmCd, + }, ); }, [sidoList, props.defaultValues?.sido, mergedProps.sido]); @@ -398,16 +398,18 @@ const SearchForm = (props: SearchFormProps = {}) => { )}
- + {mergedProps.isReset && ( + + )}
diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index cfed25cd9..a1fb327a8 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -220,13 +220,14 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT }; if (col.computed) { const hasFields = "fields" in col.computed && col.computed.fields.length > 0; - const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && col.computed.denominatorFields.length > 0; + const hasNumeratorDenominator = "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorFields" in col.computed && col.computed.denominatorFields.length > 0; const hasMaxOfFieldAndSum = "baseField" in col.computed && "sumFields" in col.computed && col.computed.sumFields.length > 0; const hasDifferenceOfSums = "minuendFields" in col.computed && "subtrahendFields" in col.computed && col.computed.minuendFields.length > 0; const hasHalfSumOfSums = col.computed.type === "halfSumOfSums" && "firstSumFields" in col.computed && "secondSumFields" in col.computed && col.computed.firstSumFields.length > 0 && col.computed.secondSumFields.length > 0; const hasSumOfTwoHalfSumOfSums = col.computed.type === "sumOfTwoHalfSumOfSums" && "firstHalfSum" in col.computed && "secondHalfSum" in col.computed && col.computed.firstHalfSum.firstSumFields.length > 0 && col.computed.firstHalfSum.secondSumFields.length > 0 && col.computed.secondHalfSum.firstSumFields.length > 0 && col.computed.secondHalfSum.secondSumFields.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; - if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups) { + const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); + if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; } } diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index fb2ebf09b..939e219d0 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; 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"; @@ -10,30 +10,43 @@ import type { CauserBurdenItem, CauserBurdenSaveItem, CauserBurdenSaveRequest, + CauserBurdenSubmitRequest, SewerageChargeItem, SewerageChargeSaveItem, SewerageChargeSaveRequest, + SewerageChargeSubmitRequest, SewerageFinanceRequestParams, SewerageFinanceSettleItem, SewerageFinanceSettleSaveItem, SewerageFinanceSettleSaveRequest, + SewerageFinanceSettleSubmitRequest, + SewerageFinanceSubmitResponse, SewerageFinanceVerificationResult, SewerageUsageFeeItem, SewerageUsageFeeSaveItem, SewerageUsageFeeSaveRequest, + SewerageUsageFeeSubmitRequest, } from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface"; import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; -import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SewerageStatusInfo, +} from "@/entities/statistics/model/statistics-interface"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import SubmitDailog from "./SubmitDailog"; + import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; 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 { 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"; @@ -306,11 +319,40 @@ function buildCauserBurdenSaveList( })); } +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + 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 검증 완료 대기 시간이 초과되었습니다."); +} + // 하수도 통계 관리 > 09. 하수도재정 const SewerageFinanceInp = () => { const { setMessage: alert } = useAlertStore((state) => state); const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(firstTabKey); @@ -333,6 +375,7 @@ const SewerageFinanceInp = () => { >({}); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); const searchParamRef = useRef(null); const searchDoneRef = useRef(false); @@ -702,7 +745,208 @@ const SewerageFinanceInp = () => { }; const submitFunc = () => { - // 제출 로직 + void (async () => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } = + resolveSewerageFinanceTab(tabKey); + if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const currentData = getTabGridRows(tabKey); + if (!currentData.length) { + alert("제출할 데이터가 없습니다."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; + const adminOpt = isAdminInfo ? { superAdmin: "superAdmin" as const } : {}; + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + false, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: SewerageFinanceSubmitResponse; + + if (isSettleTab) { + const param: SewerageFinanceSettleSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageFinanceSettleSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputSewerageFinanceApi.submitSewerageFinanceSettle( + param, + ); + } else if (isChargeTab) { + const param: SewerageChargeSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageChargeSaveList(currentData), + ...adminOpt, + }; + response = await statisticsInputSewerageFinanceApi.submitSewerageCharge(param); + } else if (isUsageFeeTab) { + const param: SewerageUsageFeeSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildSewerageUsageFeeSaveList(currentData), + ...adminOpt, + }; + response = + await statisticsInputSewerageFinanceApi.submitSewerageUsageFee(param); + } else { + const param: CauserBurdenSubmitRequest = { + ...submitInfoBase, + userId: userInfo.userUnqId, + submitList: buildCauserBurdenSaveList(currentData), + ...adminOpt, + }; + response = await statisticsInputSewerageFinanceApi.submitCauserBurden(param); + } + + 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 reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + 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 reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + setShowSubmitDialog(false); + } }; const saveFunc = () => { @@ -1071,6 +1315,13 @@ const SewerageFinanceInp = () => { ); }} /> + = { + [ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, + [ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, +}; + +type GridRowWithVerification = Record & { + verificationResult?: SewerageServiceLevelVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof SewerageServiceLevelInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + 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 buildSewerageServiceLevelSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewerageServiceLevelComplaintRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function mapServiceLevelListToGridRows< + T extends { verificationResult?: SewerageServiceLevelVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +function normalizeGridRmrk(value: unknown): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value : String(value); + return s.trim() === "" ? null : s; +} + +function buildColumnFormatNumber( + columns: { fieldName?: string; dataFormat?: string }[], +) { + const columnFormatMap = new Map(); + columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + + return (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === "") return 0; + const num = + typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; +} + +function buildComplaintSaveList( + currentData: Record[], +): SewerageServiceLevelComplaintSaveItem[] { + const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"), + dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"), + odorCnt: formatNumber(d.odorCnt, "odorCnt"), + ldsCnt: formatNumber(d.ldsCnt, "ldsCnt"), + plltnAcdntCnt: formatNumber(d.plltnAcdntCnt, "plltnAcdntCnt"), + swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"), + sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"), + etcCnt: formatNumber(d.etcCnt, "etcCnt"), + rmrk: normalizeGridRmrk(d.rmrk), + whol: formatNumber(d.whol, "whol"), + crtrPpltnCvlcptEvntcsCnt: formatNumber( + d.crtrPpltnCvlcptEvntcsCnt, + "crtrPpltnCvlcptEvntcsCnt", + ), + })); +} + +function buildResidentFriendlyFactSaveList( + currentData: Record[], +): SewerageServiceLevelResidentFriendlyFactSaveItem[] { + const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns); + + return currentData.map((d) => ({ + admdstCd: normalizeGridString(d.admdstCd), + admdstNm: normalizeGridString(d.admdstNm), + swtrmtFcltCd: normalizeGridString(d.swtrmtFcltCd), + swtrmtFcltNm: normalizeGridString(d.swtrmtFcltNm), + fcltSn: normalizeGridString(d.fcltSn), + tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"), + brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"), + childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"), + bdySportsFcltArea: formatNumber(d.bdySportsFcltArea, "bdySportsFcltArea"), + sportsFcltArea: formatNumber(d.sportsFcltArea, "sportsFcltArea"), + pkltArea: formatNumber(d.pkltArea, "pkltArea"), + eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"), + wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"), + etcCnt: formatNumber(d.etcCnt, "etcCnt"), + rmrk: normalizeGridRmrk(d.rmrk), + })); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + 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 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 13. 하수도 서비스 수준 const SewerageServiceLevelInp = () => { - - // tab List - const SewerageServiceLevelInpTabList = [ - ComplaintInpColumn, - ResidentFriendlyFactInpColumn - ]; + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); - const submitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [isNotApplicable, setIsNotApplicable] = useState(false); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [searchLoaded, setSearchLoaded] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + 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 getLastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewerageServiceLevelComplaintRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + if (tabKey === ComplaintInpColumn.tabKey) { + const response = + await statisticsInputSewerageServiceLevelApi.getComplaintList(requestParam); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, complaintList } = response.data; + const gridRows = mapServiceLevelListToGridRows( + (complaintList ?? []) as SewerageServiceLevelComplaintItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ComplaintInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + setSearchLoaded(true); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { + const response = + await statisticsInputSewerageServiceLevelApi.getResidentFriendlyFactList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, residentFriendlyFactList } = response.data; + const gridRows = mapServiceLevelListToGridRows( + (residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[], + commonInfo.ctpvNm, + ); + + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ResidentFriendlyFactInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + setSearchLoaded(true); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []; + + const handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setSearchLoaded(false); + setIsNotApplicable(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + 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 periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setIsNotApplicable(false); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewerageServiceLevelSearchParam(values); + searchParamRef.current = searchParam; + + await loadActiveTabData( + activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleActiveTabChange = async (tabKey: string) => { + const prevTabKey = activeTabRef.current; + const latestRowsInPrevTab = getTabGridRows(prevTabKey); + if (latestRowsInPrevTab.length > 0 || rowDataMap.has(prevTabKey)) { + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(prevTabKey, latestRowsInPrevTab); + return next; + }); + } + + activeTabRef.current = tabKey; + setActiveTab(tabKey); + setIsNotApplicable(false); + + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildSewerageServiceLevelSearchParam(values, { mode: "PRE" }); + await loadActiveTabData( + activeTabRef.current, + param, + values, + true, + false, + { mode: "PRE" }, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; const submitFunc = () => { - // 제출 로직 + void (async () => { + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const tabKey = activeTabRef.current; + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + const latestRows = getTabGridRows(tabKey); + const noApplicable = isResidentTab && isNotApplicable; + if (latestRows.length === 0) { + if (isResidentTab && !noApplicable) { + alert("해당사항 없음을 체크해주세요."); + return; + } + if (!isResidentTab) { + alert("제출할 데이터가 없습니다."); + return; + } + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const isComplaintTab = tabKey === ComplaintInpColumn.tabKey; + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + + if (!isComplaintTab && !isResidentTab) { + alert("제출할 수 없는 탭입니다."); + return; + } + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + const latestRows = getTabGridRows(tabKey); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, latestRows); + return next; + }); + + const noApplicable = isResidentTab && isNotApplicable; + const complaintSubmitList = buildComplaintSaveList(latestRows); + const residentSubmitList = noApplicable + ? [] + : buildResidentFriendlyFactSaveList(latestRows); + + if (isComplaintTab && complaintSubmitList.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + if (isResidentTab && !noApplicable && residentSubmitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = + tabStatsCdMap.get(tabKey) ?? + (isComplaintTab + ? SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD + : SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD); + + const submitInfoBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + }; + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadSubmittedTab = async () => { + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + false, + false, + ); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + const response = isComplaintTab + ? await statisticsInputSewerageServiceLevelApi.submitComplaintList({ + ...submitInfoBase, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + submitList: complaintSubmitList, + }) + : await statisticsInputSewerageServiceLevelApi.submitResidentFriendlyFactList({ + ...submitInfoBase, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + submitList: residentSubmitList, + }); + + 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 reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } catch (e) { + await reloadSubmittedTab(); + 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 reloadSubmittedTab(); + await openVerificationResultDialogIfNeeded( + verificationQuery, + setVerificationResultDialogOpen, + { notify: alert }, + ); + } + } catch { + alert("데이터 제출 중 오류가 발생했습니다."); + } finally { + hideGlobalProgress(); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; + const latestRows = getTabGridRows(tabKey); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, latestRows); + return next; + }); + + const noApplicable = isResidentTab && isNotApplicable; + const baseRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + + if (tabKey === ComplaintInpColumn.tabKey) { + const saveList = buildComplaintSaveList(latestRows); + if (saveList.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + const response = + await statisticsInputSewerageServiceLevelApi.saveComplaintList({ + ...baseRequest, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + saveList, + }); + + if (response.status !== "success") { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + return; + } + } else if (tabKey === ResidentFriendlyFactInpColumn.tabKey) { + const saveList = noApplicable + ? [] + : buildResidentFriendlyFactSaveList(latestRows); + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const response = + await statisticsInputSewerageServiceLevelApi.saveResidentFriendlyFactList( + { + ...baseRequest, + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + noApplicable, + saveList, + }, + ); + + if (response.status !== "success") { + alert(response.message || "데이터 저장 중 오류가 발생했습니다."); + return; + } + } else { + alert("저장할 수 없는 탭입니다."); + return; + } + + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + alert("저장되었습니다."); + } catch { + alert("데이터 저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const openVerificationHistoryDialog = () => { + setVerificationHistoryOpen(true); + }; + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewerageServiceLevelInpTabList) { + 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) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return guide ? { ...col, tooltip: guide } : col; + }); + map.set(tabKey, { + columns, + columnLayout: tab.columnLayout ?? [], + }); + } + return map; + }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); + + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabRowData = rowDataMap.get(activeTab) ?? []; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; + const showNotApplicableForResidentTab = + activeTab === ResidentFriendlyFactInpColumn.tabKey && + searchLoaded && + hasSearched && + activeTabRowData.length === 0 && + computeTabEnterable(activeTabStatus, inputPeriodEnterable); + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons || showNotApplicableForResidentTab); + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(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 verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | 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"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): 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"); + } + }; return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForResidentTab && ( + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewerageServiceLevelInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewerageServiceLevelInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + 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={false} + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + /> + + ); + })} +
-
-
- -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> ); }; -export default SewerageServiceLevelInp; \ No newline at end of file +export default SewerageServiceLevelInp; diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index 26798d169..2df784f6c 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -1,92 +1,2154 @@ -import { useState } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type Dispatch, + type SetStateAction, +} from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +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 SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton" 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 UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import DataGrid from "@/components/module/DataGrid"; +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 { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; import { PublicSewageTreatWaterReuseInpColumn } from "@/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn"; -import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn"; import { RainFallUtilFactStatusInpColumn } from "@/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn"; -import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn"; +import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn"; 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 { formatCompactYmdForGrid, toCompactYmd } 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"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import { + WATER_REUSE_BUSINESS_APPR_STATS_CD, + WATER_REUSE_DESIGN_CONST_STATS_CD, + WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + WATER_REUSE_RECYCLED_WATER_STATS_CD, + type PublicSewageTreatWaterReuseItem, + type PublicSewageTreatWaterReuseSaveItem, + type RainFallUtilFactStatusItem, + type RainFallUtilFactStatusSaveItem, + type RecycledWaterStatusItem, + type RecycledWaterStatusSaveItem, + type TreatWaterReuseBusinessApprStatusItem, + type TreatWaterReuseBusinessApprStatusSaveItem, + type TreatWaterReuseDesignConstStatusItem, + type TreatWaterReuseDesignConstStatusSaveItem, + type WaterReuseRequestParams, + type WaterReuseVerificationResult, +} from "@/entities/statistics/model/input/statistics-input-water-reuse-interface"; +import type { + ApiResponse, + ImrStatusResponse, + Sgg, + SewerageStatusInfo, + cancelSubmitRequest, +} 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 { 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 WaterReuseInpTabList = [ + PublicSewageTreatWaterReuseInpColumn, + RecycledWaterStatusInpColumn, + RainFallUtilFactStatusInpColumn, + TreatWaterReuseDesignConstStatusInpColumn, + TreatWaterReuseBusinessApprStatusInpColumn, +] as const; + +const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey; + +const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [ + WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + WATER_REUSE_RECYCLED_WATER_STATS_CD, + WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + WATER_REUSE_DESIGN_CONST_STATS_CD, + WATER_REUSE_BUSINESS_APPR_STATS_CD, +] as const; + +const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record = { + [PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, + [RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD, + [RainFallUtilFactStatusInpColumn.tabKey]: WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD, + [TreatWaterReuseDesignConstStatusInpColumn.tabKey]: WATER_REUSE_DESIGN_CONST_STATS_CD, + [TreatWaterReuseBusinessApprStatusInpColumn.tabKey]: WATER_REUSE_BUSINESS_APPR_STATS_CD, +}; + +const ARTICLE_EDIT_TAB_KEYS = new Set([ + RecycledWaterStatusInpColumn.tabKey, + RainFallUtilFactStatusInpColumn.tabKey, + TreatWaterReuseDesignConstStatusInpColumn.tabKey, + TreatWaterReuseBusinessApprStatusInpColumn.tabKey, +]); + +const NO_APPLICABLE_TAB_KEYS = ARTICLE_EDIT_TAB_KEYS; + +const WATER_REUSE_TAB_BY_KEY = new Map( + WaterReuseInpTabList.map((tab) => [tab.tabKey, tab]), +); + +type GridRowWithVerification = Record & { + verificationResult?: WaterReuseVerificationResult; +}; + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +const toGridColumnInfo = (tab: (typeof WaterReuseInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + 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 buildWaterReuseSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): WaterReuseRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function parseSggListFromResponse(res: ApiResponse): 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 }; + } + if (sggOptions.length === 0) { + return { ...col, editable: true }; + } + const labels = sggOptions.map((s) => s.gugun); + return { + ...col, + editable: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values: labels, + labels, + }, + values: labels, + labels, + lookupDisplay: true, + }; +} + +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; +} + +function resolveArticleAdmdst( + row: Record, + sggOptions: Sgg[], +): { admdstCd: string; admdstNm: string } { + const existingCd = String(row.admdstCd ?? "").trim(); + const existingNm = String(row.admdstNm ?? "").trim(); + if (existingCd && existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm }; + } + const sggNm = String(row.sggNm ?? "").trim(); + const matched = sggOptions.find((s) => s.gugun === sggNm); + if (matched) { + return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; + } + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; +} + +function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null { + if ( + tabKey === RecycledWaterStatusInpColumn.tabKey || + tabKey === RainFallUtilFactStatusInpColumn.tabKey + ) { + return "fcltSn"; + } + if ( + tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey || + tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey + ) { + return "bplcSn"; + } + return null; +} + +function isPersistedArticleRow( + tabKey: string, + row: Record, +): boolean { + const snField = getArticleRowSnField(tabKey); + if (!snField) return false; + return String(row[snField] ?? "").trim() !== ""; +} + +function buildRecycledWaterStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): RecycledWaterStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + bldgSeCn: String(row.bldgSeCn ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + cmptnStrtYmd: toCompactYmd(row.cmptnStrtYmd), + oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd), + archWholArea: toGridNumber(row.archWholArea), + prcsCpct: toGridNumber(row.prcsCpct), + wtrUseQnt: toGridNumber(row.wtrUseQnt), + ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt), + sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt), + mnUsgCn: String(row.mnUsgCn ?? "").trim(), + ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildRainFallUtilFactStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): RainFallUtilFactStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + bdstTypCn: String(row.bdstTypCn ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + cmptYmd: toCompactYmd(row.cmptYmd), + instlAmt: toGridNumber(row.instlAmt), + rcptMynCn: String(row.rcptMynCn ?? "").trim(), + filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(), + impdCpct: toGridNumber(row.impdCpct), + anlUseQnt: toGridNumber(row.anlUseQnt), + exlnUtlzUsgExpln: String(row.exlnUtlzUsgExpln ?? "").trim(), + anlOperAmt: toGridNumber(row.anlOperAmt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildTreatWaterReuseDesignConstStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): TreatWaterReuseDesignConstStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + bplcSn: String(row.bplcSn ?? "").trim(), + bplcNm: String(row.bplcNm ?? "").trim(), + rprsvNm: String(row.rprsvNm ?? "").trim(), + bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + coRegYmd: toCompactYmd(row.coRegYmd), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildTreatWaterReuseBusinessApprStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): TreatWaterReuseBusinessApprStatusSaveItem { + const { admdstCd, admdstNm } = resolveArticleAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + bplcSn: String(row.bplcSn ?? "").trim(), + bplcNm: String(row.bplcNm ?? "").trim(), + rprsvNm: String(row.rprsvNm ?? "").trim(), + bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + coRegYmd: toCompactYmd(row.coRegYmd), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +function buildPublicSewageTreatWaterReuseSaveItem( + row: Record, +): PublicSewageTreatWaterReuseSaveItem { + return { + admdstCd: String(row.admdstCd ?? "").trim(), + admdstNm: String(row.admdstNm ?? row.sggNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + wholPrcsQnt: toGridNumber(row.wholPrcsQnt), + lglYn: String(row.lglYn ?? "").trim(), + instlYn: String(row.instlYn ?? "").trim(), + oprtngStrtYmd: toCompactYmd(row.oprtngStrtYmd), + instlAprvYmd: toCompactYmd(row.instlAprvYmd), + rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct), + mtcnsNm: String(row.mtcnsNm ?? "").trim(), + ruswtPplLen: toGridNumber(row.ruswtPplLen), + ruswtPumpLen: toGridNumber(row.ruswtPumpLen), + rmrk: nullableTrimmedString(row.rmrk), + rtlztnRt: toGridNumber(row.rtlztnRt), + }; +} + +function createEmptyArticleRow( + tabKey: string, + ctpvNm: string, + sggNm = "", +): GridRowWithVerification { + const row: GridRowWithVerification = { ctpvNm, sggNm }; + const columns = (WATER_REUSE_TAB_BY_KEY.get(tabKey)?.columns ?? []) as ColumnConfig[]; + for (const col of columns) { + const fieldName = col.fieldName; + if (!fieldName || fieldName === "ctpvNm" || fieldName === "sggNm") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +function mapPublicSewageTreatWaterReuseListToGridRows( + list: PublicSewageTreatWaterReuseItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + sncnYmd: formatCompactYmdForGrid(item.sncnYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + instlAprvYmd: formatCompactYmdForGrid(item.instlAprvYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapRecycledWaterStatusListToGridRows( + list: RecycledWaterStatusItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + cmptnStrtYmd: formatCompactYmdForGrid(item.cmptnStrtYmd), + oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapRainFallUtilFactStatusListToGridRows( + list: RainFallUtilFactStatusItem[], + ctpvNm: string, +): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + cmptYmd: formatCompactYmdForGrid(item.cmptYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapTreatWaterReuseStatusListToGridRows< + T extends { coRegYmd: string; verificationResult?: WaterReuseVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([ + { + ctpvNm, + ...rest, + coRegYmd: formatCompactYmdForGrid(item.coRegYmd), + }, + ])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +function mapWaterReuseListToGridRows< + T extends { verificationResult?: WaterReuseVerificationResult }, +>(list: T[], ctpvNm: string): GridRowWithVerification[] { + return list.map((item) => { + const { verificationResult, ...rest } = item; + const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; + if (verificationResult) row.verificationResult = verificationResult; + return row; + }); +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + 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 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 10. 물 재이용 const WaterReuseInp = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + 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); - // tab list - const WaterReuseInpTabList = [ - PublicSewageTreatWaterReuseInpColumn, - RecycledWaterStatusInpColumn, - RainFallUtilFactStatusInpColumn, - TreatWaterReuseDesignConstStatusInpColumn, - TreatWaterReuseBusinessApprStatusInpColumn - ]; - const SubmitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState[]>>( + new Map(), + ); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [sggOptions, setSggOptions] = useState([]); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + + const searchParamRef = useRef(null); + const sggOptionsSidoRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + const loadedTabKeysRef = useRef>(new Set()); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + WATER_REUSE_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => { + try { + const res = await statisticsApi.getColumnGuides(statsCd); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [statsCd, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [statsCd, next] as const; + } catch { + return [statsCd, {}] as const; + } + }), + ); + if (cancelled) return; + const guidesByStatsCd = Object.fromEntries(entries); + setColumnGuidesByTab( + Object.fromEntries( + Object.entries(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( + ([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}], + ), + ), + ); + })(); + return () => { + cancelled = true; + }; + }, []); + + const loadSggOptions = useCallback(async (sido: string): Promise => { + 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 shouldShowNotApplicable = ( + tabKey: string, + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + NO_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 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 getLastYearDataFunc = () => { - // 전연도 데이터 가져오기 로직 + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: WaterReuseRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + + setIsNotApplicableForTab(tabKey, false); + if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + } + + if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getPublicSewageTreatWaterReuseList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, publicSewageTreatWaterReuseList } = response.data; + const gridRows = mapPublicSewageTreatWaterReuseListToGridRows( + (publicSewageTreatWaterReuseList ?? []) as PublicSewageTreatWaterReuseItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(PublicSewageTreatWaterReuseInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const response = await statisticsInputWaterReuseApi.getRecycledWaterStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, recycledWaterList } = response.data; + const gridRows = mapRecycledWaterStatusListToGridRows( + (recycledWaterList ?? []) as RecycledWaterStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(RecycledWaterStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const response = await statisticsInputWaterReuseApi.getRainFallUtilFactStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, rainFallUtilFactStatusList } = response.data; + const gridRows = mapRainFallUtilFactStatusListToGridRows( + (rainFallUtilFactStatusList ?? []) as RainFallUtilFactStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(RainFallUtilFactStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getTreatWaterReuseDesignConstStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, treatWaterReuseDesignConstStatusList } = response.data; + const gridRows = mapTreatWaterReuseStatusListToGridRows( + (treatWaterReuseDesignConstStatusList ?? + []) as TreatWaterReuseDesignConstStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(TreatWaterReuseDesignConstStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const response = + await statisticsInputWaterReuseApi.getTreatWaterReuseBusinessApprStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, treatWaterReuseBusinessApprStatusList } = + response.data; + const gridRows = mapTreatWaterReuseStatusListToGridRows( + (treatWaterReuseBusinessApprStatusList ?? + []) as TreatWaterReuseBusinessApprStatusItem[], + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(TreatWaterReuseBusinessApprStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); + loadedTabKeysRef.current.add(tabKey); + return true; + } + + alert("해당 탭 조회 API가 없습니다."); + return false; + }; + + const getTabGridRows = (tabKey: string): Record[] => + tabKey === activeTabRef.current + ? (getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []) + : (rowDataMap.get(tabKey) ?? []); + + const commitActiveGrid = () => { + const gridView = getGridRef.current?.gridView as + | { commit?: (force?: boolean) => void; cancel?: () => void } + | undefined; + try { + gridView?.commit?.(true); + } catch { + gridView?.cancel?.(); + } + }; + + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; + }; + + const getCheckedItemIndexesFromGrid = (rowCount: number): number[] => { + if (!getGridRef.current?.gridView) return []; + + const gridView = getGridRef.current.gridView as { + getCheckedItems?: () => number[]; + getCheckedRows?: () => number[]; + }; + + 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 handleReset = () => { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setIsNotApplicableByTab(new Map()); + setDeleteListByTab(new Map()); + setInputPeriodEnterable(true); + searchParamRef.current = null; + searchDoneRef.current = false; + periodEnterableRef.current = true; + loadedTabKeysRef.current.clear(); + setSggOptions([]); + sggOptionsSidoRef.current = null; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + 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 periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setDeleteListByTab(new Map()); + loadedTabKeysRef.current.clear(); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildWaterReuseSearchParam(values); + searchParamRef.current = searchParam; + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (ARTICLE_EDIT_TAB_KEYS.has(activeTabRef.current) && loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + await loadActiveTabData( + activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + 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; + }); + } + + activeTabRef.current = tabKey; + setActiveTab(tabKey); + + if (!hasSearched || !lastSearchedValues || !searchParamRef.current) return; + if (loadedTabKeysRef.current.has(tabKey)) return; + if (tabRowDataExists(rowDataMap, tabKey)) return; + + try { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + true, + false, + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const getLastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + + try { + setIsLoading(true); + const param = buildWaterReuseSearchParam(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; + const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + if (!isPublicSewageTab && !isArticleTab) { + alert("제출 API 연동 준비 중입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + if (isPublicSewageTab) { + const currentData = getTabGridRows(tabKey); + if (currentData.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + } else { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + if (!noApplicable && currentData.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + } + + if (await confirm("제출 하시겠습니까?")) { + setShowSubmitDialog(true); + } + })(); + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + const tabKey = activeTabRef.current; + const isPublicSewageTab = tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); + if (!isPublicSewageTab && !isArticleTab) { + alert("제출 API 연동 준비 중입니다."); + return; + } + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + + commitActiveGrid(); + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + + let articleSggOptions: Sgg[] | null = null; + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForTab = + tabStatsCdMap.get(tabKey) ?? + WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? + tabKey; + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForTab, + }; + + const reloadAfterSubmit = async () => { + if (isArticleTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (articleSggOptions) { + setSggOptions(articleSggOptions); + } + } + const param = + searchParamRef.current ?? + buildWaterReuseSearchParam(lastSearchedValues); + await loadActiveTabData(tabKey, param, lastSearchedValues, false, false); + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + + let response: { + status: string; + message?: string; + data?: { finalStatus: string; nvpassed: boolean }; + }; + + if (isPublicSewageTab) { + const currentData = getTabGridRows(tabKey); + const submitList = currentData.map(buildPublicSewageTreatWaterReuseSaveItem); + if (submitList.length === 0) { + alert("제출할 데이터가 없습니다."); + return; + } + + response = await statisticsInputWaterReuseApi.submitPublicSewageTreatWaterReuse({ + ...submitRequestBase, + userId: userInfo.userUnqId, + noApplicable: false, + submitList, + }); + } else { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + 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; + } + articleSggOptions = list; + + const articleSubmitRequest = { + ...submitRequestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + }; + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildRecycledWaterStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRecycledWaterStatusSaveItem(row, list)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = await statisticsInputWaterReuseApi.submitRecycledWaterStatus({ + ...articleSubmitRequest, + submitList, + deleteList, + }); + } else if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildRainFallUtilFactStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = await statisticsInputWaterReuseApi.submitRainFallUtilFactStatus({ + ...articleSubmitRequest, + submitList, + deleteList, + }); + } else if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputWaterReuseApi.submitTreatWaterReuseDesignConstStatus( + { + ...articleSubmitRequest, + submitList, + deleteList, + }, + ); + } else if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const submitList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + + if (!noApplicable && submitList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputWaterReuseApi.submitTreatWaterReuseBusinessApprStatus( + { + ...articleSubmitRequest, + submitList, + deleteList, + }, + ); + } else { + alert("제출 API 연동 준비 중입니다."); + return; + } + } + + 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(); + } }; const saveFunc = () => { - // 저장 로직 + void (async () => { + const tabKey = activeTabRef.current; + if (!lastSearchedValues) { + alert("먼저 조회를 진행해주세요."); + return; + } + if (!buildGugunOkForToolbar(lastSearchedValues)) { + alert("시군구를 선택해주세요."); + return; + } + if (!(await confirm("저장 하시겠습니까?"))) return; + + commitActiveGrid(); + + const requestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), + }; + + const reloadTabAfterSave = async () => { + const param = + searchParamRef.current ?? + buildWaterReuseSearchParam(lastSearchedValues); + await loadActiveTabData( + tabKey, + param, + lastSearchedValues, + false, + false, + ); + }; + + if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) { + const currentData = getTabGridRows(tabKey); + if (currentData.length === 0) { + alert("저장할 데이터가 없습니다."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.savePublicSewageTreatWaterReuse({ + ...requestBase, + userId: userInfo.userUnqId, + noApplicable: false, + saveList: currentData.map(buildPublicSewageTreatWaterReuseSaveItem), + }); + + if (response.status === "success") { + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === RecycledWaterStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + 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; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildRecycledWaterStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRecycledWaterStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = await statisticsInputWaterReuseApi.saveRecycledWaterStatus({ + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + 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; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildRainFallUtilFactStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveRainFallUtilFactStatus({ + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + 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; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseDesignConstStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveTreatWaterReuseDesignConstStatus( + { + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }, + ); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + return; + } + + if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + + 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; + } + + const saveList = noApplicable + ? [] + : currentData.map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRow(tabKey, row)) + .map((row) => + buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const response = + await statisticsInputWaterReuseApi.saveTreatWaterReuseBusinessApprStatus( + { + ...requestBase, + userId: userInfo.userUnqId, + ...(noApplicable ? { noApplicable: true } : {}), + saveList, + deleteList, + }, + ); + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + setSggOptions(list); + await reloadTabAfterSave(); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + } + })(); }; const updateOperateDataButtonFunc = () => { - // 운영 데이터 업데이트 로직 - tab1. 공공하수처리수 재이용 + alert("운영 데이터 업데이트 API 연동 준비 중입니다."); }; - const addArticleFunc = () => { - console.log('save'); - // 항목 추가 로직 + const addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 추가할 수 없는 탭입니다."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + 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; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm); + + flushSync(() => { + setSggOptions(list); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, [...currentRows, newRow]); + return next; + }); + }); + + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; const deleteArticleFunc = () => { - console.log('save'); - // 항목 제외 로직 + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + 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) => + isPersistedArticleRow(tabKey, row), + ); + + 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; + }); + + const ctx = lastSearchedValues ?? searchValues; + if (ctx) { + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + nextRows.length > 0, + ctx, + ); + } }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); + }; + + const openVerificationHistoryDialog = () => { + setVerificationHistoryOpen(true); + }; + + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of WaterReuseInpTabList) { + 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 }; + } + if ( + tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey && + col.fieldName === "sggNm" + ) { + next = { + ...col, + editable: false as const, + styleName: "rg-cell-readonly", + }; + } else if ( + ARTICLE_EDIT_TAB_KEYS.has(tabKey) && + col.fieldName === "sggNm" + ) { + next = buildSggNmColumn(col, canEdit, sggOptions); + } + 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 activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabRowData = rowDataMap.get(activeTab) ?? []; + const activeTabToolbar = + toolbarSnapshotByTab.get(activeTab) ?? + computeTabToolbarSnapshot( + activeTabStatus, + tabRowDataExists(rowDataMap, activeTab), + hasSearched, + lastSearchedValues, + inputPeriodEnterable, + isAdminInfo, + ); + + const showSaveInToolbar = + activeTabToolbar.showSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + + const isPublicSewageTreatTab = activeTab === PublicSewageTreatWaterReuseInpColumn.tabKey; + const isArticleEditTab = ARTICLE_EDIT_TAB_KEYS.has(activeTab); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const showNotApplicableForActiveTab = + loadedTabKeysRef.current.has(activeTab) && + shouldShowNotApplicable( + activeTab, + tabRowDataExists(rowDataMap, activeTab), + activeTabStatus, + ); + const setIsNotApplicableForActiveTab: Dispatch> = 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 verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(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 verificationCellStyle = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): string | 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"; + } + }; + + const verificationCellTooltip = ({ + fieldName, + row, + }: { + fieldName: string; + row: Record; + }): 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"); + } + }; return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void getLastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {isPublicSewageTreatTab && showEnterableToolbarButtons && ( + + )} + {isArticleEditTab && showEnterableToolbarButtons && ( + <> + + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {WaterReuseInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {WaterReuseInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabEnterableForGrid = + computeTabEnterable( + tabStatusInfoMap.get(tabKey) ?? null, + inputPeriodEnterable, + ) || isAdminInfo; + const gugunOk = buildGugunOkForToolbar(lastSearchedValues); + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + 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) && + tabEnterableForGrid + } + fixedColumns={info.fixedColumns} + enableSort={false} + statusInfo={tabStatusInfo} + isExcelImport={ + tabRowDataExists(rowDataMap, tabKey) && + tabEnterableForGrid && + gugunOk + } + isExcelExport + cellStyleCallback={verificationCellStyle} + cellTooltipCallback={verificationCellTooltip} + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={ + activeTab === tabKey ? getGridRef : undefined + } + /> + + ); + })} +
-
-
- - - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} /> + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + ); }; -export default WaterReuseInp; \ No newline at end of file +export default WaterReuseInp; diff --git a/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx b/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx new file mode 100644 index 000000000..994285f7b --- /dev/null +++ b/apps/st/src/components/statistics/total/SewerageAggregateSummary.tsx @@ -0,0 +1,233 @@ +import { useEffect, useRef, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import type { RealGridReact } from "realgrid-react"; + +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; +import { statisticsSewerageAggregateSummaryApi } from "@/entities/statistics/api/statistics-sewerage-aggregate-summary-service"; +import type { SewerageAggregateSummaryDownloadRequest } from "@/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface"; +import { LoadingWithAnimation } from "@common/components"; + +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +const columnInfo = { + columns: [ + { + name: "statsCd", + fieldName: "statsCd", + header: { text: "통계항목코드" }, + editable: false, + visible: false, + }, + { name: "tabNm", fieldName: "tabNm", header: { text: "통계항목" }, editable: false }, + ], + columnLayout: ["statsCd", "tabNm"], +}; + +type GridViewWithCheckApi = { + getCheckedRows?: (refresh?: boolean) => number[]; + getCheckedItems?: (refresh?: boolean) => number[]; +}; + +const EXCEL_DOWNLOAD_PROGRESS_MAX = 98; +const EXCEL_DOWNLOAD_PROGRESS_STEP = 1; +const EXCEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 500; +const EXCEL_DOWNLOAD_PROGRESS_LABEL = + "엑셀 파일을 생성 중입니다.\n창을 닫거나 이동하지 말고 잠시만 기다려주세요."; + +const SewerageAggregateSummary = () => { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + const downloadProgressTimerRef = useRef | null>(null); + + useEffect(() => { + let cancelled = false; + + void (async () => { + try { + setIsLoading(true); + const response = + await statisticsSewerageAggregateSummaryApi.getAggregateSummaryTabList(); + + if (cancelled) return; + + if (response.status === "success") { + setRowData(toGridRows(response.data ?? [])); + } else { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + } + } catch { + if (!cancelled) { + alert("데이터 조회 중 오류가 발생했습니다."); + } + } finally { + if (!cancelled) { + setIsLoading(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [alert]); + + useEffect(() => { + return () => { + if (downloadProgressTimerRef.current !== null) { + clearInterval(downloadProgressTimerRef.current); + } + }; + }, []); + + const stopDownloadProgressTimer = () => { + if (downloadProgressTimerRef.current !== null) { + clearInterval(downloadProgressTimerRef.current); + downloadProgressTimerRef.current = null; + } + }; + + const startDownloadProgress = () => { + stopDownloadProgressTimer(); + let currentProgress = 0; + + showGlobalProgress({ + label: EXCEL_DOWNLOAD_PROGRESS_LABEL, + progress: 0, + }); + + downloadProgressTimerRef.current = setInterval(() => { + currentProgress = Math.min( + currentProgress + EXCEL_DOWNLOAD_PROGRESS_STEP, + EXCEL_DOWNLOAD_PROGRESS_MAX, + ); + setGlobalProgress(currentProgress); + + if (currentProgress >= EXCEL_DOWNLOAD_PROGRESS_MAX) { + stopDownloadProgressTimer(); + } + }, EXCEL_DOWNLOAD_PROGRESS_INTERVAL_MS); + }; + + const handleDownload = async (values: SearchFormValues) => { + const gridView = getGridRef.current?.gridView as GridViewWithCheckApi | undefined; + const checkedIndexes = + gridView?.getCheckedRows?.(false) ?? gridView?.getCheckedItems?.(false) ?? []; + + const allRows = getGridDataRef.current?.() ?? rowData; + const statsCdList = checkedIndexes + .map((idx) => normalizeGridString(allRows[idx]?.statsCd)) + .filter((cd) => cd.length > 0); + + if (statsCdList.length === 0) { + alert("내려받을 통계항목을 선택해주세요."); + return; + } + + if (!values.year) { + alert("통계연도를 선택해주세요."); + return; + } + + if ( + !(await confirm( + `선택한 데이터 및 사용자 네트워크 환경에 따라 최대 1시간까지 시간이 소요될 수 있습니다. + 진행하시겠습니까?` + )) + ) { + return; + } + + const summaryYearCnt = + values.isSummary && values.isSummary !== "ALL" + ? Number(values.isSummary) + : 0; + + const request: SewerageAggregateSummaryDownloadRequest = { + statsCdList, + statsYr: values.year, + ctpvNm: values.sido === "ALL" ? "" : (values.sido ?? ""), + sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), + sewerBzmnCd: values.bsnmCd ?? "", + summaryYearCnt: Number.isFinite(summaryYearCnt) ? summaryYearCnt : 0, + }; + + try { + startDownloadProgress(); + const { blob, fileName } = + await statisticsSewerageAggregateSummaryApi.downloadAggregateSummary(request); + + stopDownloadProgressTimer(); + setGlobalProgress(100); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName || `하수도통계 집계 요약표_${values.year}.xlsx`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + await new Promise((resolve) => setTimeout(resolve, 400)); + alert("저장되었습니다."); + } catch (error) { + const message = + error instanceof Error && error.message + ? error.message + : "엑셀 내려받기 중 오류가 발생했습니다."; + alert(message); + } finally { + stopDownloadProgressTimer(); + hideGlobalProgress(); + } + }; + + return ( +
+ void handleDownload(values)} + isReset={false} + /> + {isLoading && ( +
+
+ +
+
+ )} + +
+ ); +}; + +function normalizeGridString(value: unknown): string { + if (value === null || value === undefined) return ""; + return typeof value === "string" ? value : String(value); +} + +export default SewerageAggregateSummary; \ No newline at end of file diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 6a77cde08..d44b32a81 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -173,6 +173,12 @@ export type ComputedFieldConfig = dschgField: string; type: "bodPrcsRt"; } + | { + /** Σ(numeratorFields) / denominatorField × 10,000 (건/10,000명) */ + numeratorFields: string[]; + denominatorField: string; + type: "sumPerTenThousand"; + } | { /** (quotient[0]/quotient[1]) / (denominatorQuotient[0]/denominatorQuotient[1]) × 100 */ numeratorQuotient: [string, string]; diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index 18ce8aea7..62c7fab7a 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -30,10 +30,12 @@ export type SearchFormProps = { onReset?: () => void; onValuesChange?: (values: SearchFormValues) => void; defaultValues?: SearchFormValues; + searchButtonText?: string; + isReset?: boolean; }; export const defaultSearchFormProps: Required< - Omit + Omit > = { sido: true, gugun: true, @@ -44,4 +46,5 @@ export const defaultSearchFormProps: Required< userMenual: false, isDetail: false, isSummary: false, + isReset: true, }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts index ac73c825c..ecaef2339 100644 --- a/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts +++ b/apps/st/src/components/util/column/input/ExcretaTreatFactInpColumn.ts @@ -8,7 +8,7 @@ import { ValueType } from "realgrid"; export const ExcretaTreatFactInpColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 기본 정보 diff --git a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts index c28f12bc7..9b41b2202 100644 --- a/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewagePipe/ConnectionStatusInpColumn.ts @@ -10,7 +10,7 @@ export const AdminAreaConnectionInpColumn = { { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false }, { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false }, // 연결여부 - { name: 'isConnection', fieldName: 'isConnection', header: { text: '연결여부' }, editable: false }, + { name: 'isAreaConnection', fieldName: 'isAreaConnection', header: { text: '연결여부' }, editable: false }, // 연결하수처리장 { name: 'connectionBscFctNmList', fieldName: 'connectionBscFctNmList', header: { text: '연결하수처리장' }, editable: false }, { @@ -28,7 +28,7 @@ export const AdminAreaConnectionInpColumn = { header: { text: '행정구역' }, items: ['ctpvNm', 'sggNm', 'admdstNm'], }, - 'isConnection', + 'isAreaConnection', 'connectionBscFctNmList', 'admdstCd' ], diff --git a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts index 7ae03817e..9a7087cbc 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/CauserBurdenInpColumn.ts @@ -5,8 +5,8 @@ import { ValueType } from "realgrid"; export const CauserBurdenInpColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 부과액 { name: 'levyEvntcsCnt', fieldName: 'levyEvntcsCnt', header: { text: '건수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts index b44fad586..b6bdb0f4f 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts @@ -5,8 +5,8 @@ import { ValueType } from "realgrid"; export const SewerageChargeInpColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 요금 { name: 'anlLevyQnt', fieldName: 'anlLevyQnt', header: { text: '연간조정량(m³)(A)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts index 5db0443df..d75f5a06d 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn.ts @@ -5,8 +5,8 @@ import { ValueType } from "realgrid"; export const SewerageFinanceSettleInpColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 재정별 사업비 { diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts index 2cd90446b..e26f4dbc6 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts @@ -5,8 +5,8 @@ import { ValueType } from "realgrid"; export const SewerageUsageFeeInpColumn = { columns: [ // 행정구역 - { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false }, - { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 총계 > 부과액 { diff --git a/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts b/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts index 68624e658..b98e431c7 100644 --- a/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn.ts @@ -1,80 +1,105 @@ import { footerInt, footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +const COMPLAINT_TYPE_COUNT_FIELDS = [ + 'dschgWqCnt', + 'odorCnt', + 'ldsCnt', + 'plltnAcdntCnt', + 'swagePplCnt', + 'sewerCrgAmt', + 'etcCnt', +] as const; + // 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원 export const ComplaintInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ADST_DONG', fieldName: '행정구역명', header: { text: '행정구역명' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리인구(명) - { name: 'SWGE_PROC_POPLTN', fieldName: '하수처리인구(명)', header: { text: '하수처리인구(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swtrmtPpltnCnt', fieldName: 'swtrmtPpltnCnt', header: { text: '하수처리인구(명)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 민원유형(건) - { name: 'CVPL_CM', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_BSML', fieldName: '악취', header: { text: '악취' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_DCWTR_QLTWTR', fieldName: '하수관로', header: { text: '하수관로' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_FIVW', fieldName: '배수설비', header: { text: '배수설비' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_POLLUTN_ACDNT', fieldName: '맨홀', header: { text: '맨홀' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_SSOS', fieldName: '정화조', header: { text: '정화조' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_CHRGE', fieldName: '하수도요금', header: { text: '하수도요금' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CVPL_UNKD', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'whol', + fieldName: 'whol', + header: { text: '총계' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: [...COMPLAINT_TYPE_COUNT_FIELDS], + type: 'sum' as const, + }, + }, + { name: 'dschgWqCnt', fieldName: 'dschgWqCnt', header: { text: '악취' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'odorCnt', fieldName: 'odorCnt', header: { text: '하수관로' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ldsCnt', fieldName: 'ldsCnt', header: { text: '배수설비' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'plltnAcdntCnt', fieldName: 'plltnAcdntCnt', header: { text: '맨홀' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swagePplCnt', fieldName: 'swagePplCnt', header: { text: '정화조' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sewerCrgAmt', fieldName: 'sewerCrgAmt', header: { text: '하수도요금' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - // 기준인구당 민원건수(건/10,000명) - { name: 'STDR_POPLTN_PER_CVPL', fieldName: '기준인구당민원건수(건/10,000명)', header: { text: '기준인구당민원건수(건/10,000명)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + // 기준인구당 민원건수(건/10,000명) = 민원유형 합계 / 하수처리인구 × 10,000 + { + name: 'crtrPpltnCvlcptEvntcsCnt', + fieldName: 'crtrPpltnCvlcptEvntcsCnt', + header: { text: '기준인구당민원건수(건/10,000명)' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorFields: [...COMPLAINT_TYPE_COUNT_FIELDS], + denominatorField: 'swtrmtPpltnCnt', + type: 'sumPerTenThousand' as const, + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_SWGE_PROC_POPLTN', fieldName: 'CHK_SWGE_PROC_POPLTN', header: { text: 'CHK_SWGE_PROC_POPLTN' }, visible: false }, - { name: 'CHK_CVPL_CM', fieldName: 'CHK_CVPL_CM', header: { text: 'CHK_CVPL_CM' }, visible: false }, - { name: 'CHK_CVPL_BSML', fieldName: 'CHK_CVPL_BSML', header: { text: 'CHK_CVPL_BSML' }, visible: false }, - { name: 'CHK_CVPL_DCWTR_QLTWTR', fieldName: 'CHK_CVPL_DCWTR_QLTWTR', header: { text: 'CHK_CVPL_DCWTR_QLTWTR' }, visible: false }, - { name: 'CHK_CVPL_FIVW', fieldName: 'CHK_CVPL_FIVW', header: { text: 'CHK_CVPL_FIVW' }, visible: false }, - { name: 'CHK_CVPL_POLLUTN_ACDNT', fieldName: 'CHK_CVPL_POLLUTN_ACDNT', header: { text: 'CHK_CVPL_POLLUTN_ACDNT' }, visible: false }, - { name: 'CHK_CVPL_SSOS', fieldName: 'CHK_CVPL_SSOS', header: { text: 'CHK_CVPL_SSOS' }, visible: false }, - { name: 'CHK_CVPL_CHRGE', fieldName: 'CHK_CVPL_CHRGE', header: { text: 'CHK_CVPL_CHRGE' }, visible: false }, - { name: 'CHK_CVPL_UNKD', fieldName: 'CHK_CVPL_UNKD', header: { text: 'CHK_CVPL_UNKD' }, visible: false }, - { name: 'CHK_STDR_POPLTN_PER_CVPL', fieldName: 'CHK_STDR_POPLTN_PER_CVPL', header: { text: 'CHK_STDR_POPLTN_PER_CVPL' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'SWGE_PROC_POPLTN', + 'swtrmtPpltnCnt', { name: 'group_complaint_type', header: { text: '민원유형(건)' }, items: [ - 'CVPL_CM', - 'CVPL_BSML', - 'CVPL_DCWTR_QLTWTR', - 'CVPL_FIVW', - 'CVPL_POLLUTN_ACDNT', - 'CVPL_SSOS', - 'CVPL_CHRGE', - 'CVPL_UNKD' + 'whol', + 'dschgWqCnt', + 'odorCnt', + 'ldsCnt', + 'plltnAcdntCnt', + 'swagePplCnt', + 'sewerCrgAmt', + 'etcCnt' ] }, - 'STDR_POPLTN_PER_CVPL', - 'RMK' + 'crtrPpltnCvlcptEvntcsCnt', + 'rmrk', + 'admdstCd', + 'admdstNm' ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'complaintInp', + tabKey: 'ST00130001', tabLabel: '민원' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts b/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts index 3f59e6b07..960b01d04 100644 --- a/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn.ts @@ -1,89 +1,100 @@ import { footerInt, footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +const RESIDENT_FRIENDLY_FACILITY_AREA_FIELDS = [ + 'brkFcltArea', + 'childFrshpSpacArea', + 'bdySportsFcltArea', + 'sportsFcltArea', + 'pkltArea', + 'eduMultiFcltArea', + 'wtlndMakeArea', + 'etcCnt', +] as const; + // 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설 export const ResidentFriendlyFactInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 시설 기본 정보 - { name: 'BSC_FCT_NM', fieldName: '처리장명', header: { text: '처리장명' }, editable: false }, - { name: 'FCT_CPC', fieldName: '시설용량(m³/일)', header: { text: '시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(m³/일)' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 처리장면적(㎡) - { name: 'PRCPLT_AR', fieldName: '처리장면적(㎡)', header: { text: '처리장면적(㎡)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'tmplntArea', fieldName: 'tmplntArea', header: { text: '처리장면적(㎡)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 주민친화시설 면적(㎡) - { name: 'TOTAL_AR', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'RESTRM_AR', fieldName: '휴게시설', header: { text: '휴게시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHILDPLAY_AR', fieldName: '어린이놀이공간', header: { text: '어린이놀이공간' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ALSFC_AR', fieldName: '체력단련시설', header: { text: '체력단련시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'PLGRND_AR', fieldName: '운동시설', header: { text: '운동시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'PRKPLCE_AR', fieldName: '주차장', header: { text: '주차장' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CTLSTT_AR', fieldName: '교육 및 문화시설', header: { text: '교육 및 문화시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'NRPRK_AR', fieldName: '습지조성(생태공원)', header: { text: '습지조성(생태공원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ETC_AR', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'whol', + fieldName: 'whol', + header: { text: '총계' }, + editable: false, + styleName: 'rg-cell-total', + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + computed: { + fields: [...RESIDENT_FRIENDLY_FACILITY_AREA_FIELDS], + type: 'sum' as const, + }, + }, + { name: 'brkFcltArea', fieldName: 'brkFcltArea', header: { text: '휴게시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'childFrshpSpacArea', fieldName: 'childFrshpSpacArea', header: { text: '어린이놀이공간' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'bdySportsFcltArea', fieldName: 'bdySportsFcltArea', header: { text: '체력단련시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'sportsFcltArea', fieldName: 'sportsFcltArea', header: { text: '운동시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pkltArea', fieldName: 'pkltArea', header: { text: '주차장' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'eduMultiFcltArea', fieldName: 'eduMultiFcltArea', header: { text: '교육 및 문화시설' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wtlndMakeArea', fieldName: 'wtlndMakeArea', header: { text: '습지조성(생태공원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 시설 정보 - { name: 'STPN_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정동코드', header: { text: '행정동코드' }, visible: false }, - { name: 'BSC_FCT_CD', fieldName: '기초시설코드', header: { text: '기초시설코드' }, visible: false }, - - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 활용율 필드 - { name: 'PRCUSE_RT', fieldName: '주민친화시설활용율(%)', header: { text: '주민친화시설활용율(%)' }, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_PRCPLT_AR', fieldName: 'CHK_PRCPLT_AR', header: { text: 'CHK_PRCPLT_AR' }, visible: false }, - { name: 'CHK_TOTAL_AR', fieldName: 'CHK_TOTAL_AR', header: { text: 'CHK_TOTAL_AR' }, visible: false }, - { name: 'CHK_RESTRM_AR', fieldName: 'CHK_RESTRM_AR', header: { text: 'CHK_RESTRM_AR' }, visible: false }, - { name: 'CHK_CHILDPLAY_AR', fieldName: 'CHK_CHILDPLAY_AR', header: { text: 'CHK_CHILDPLAY_AR' }, visible: false }, - { name: 'CHK_ALSFC_AR', fieldName: 'CHK_ALSFC_AR', header: { text: 'CHK_ALSFC_AR' }, visible: false }, - { name: 'CHK_PLGRND_AR', fieldName: 'CHK_PLGRND_AR', header: { text: 'CHK_PLGRND_AR' }, visible: false }, - { name: 'CHK_PRKPLCE_AR', fieldName: 'CHK_PRKPLCE_AR', header: { text: 'CHK_PRKPLCE_AR' }, visible: false }, - { name: 'CHK_CTLSTT_AR', fieldName: 'CHK_CTLSTT_AR', header: { text: 'CHK_CTLSTT_AR' }, visible: false }, - { name: 'CHK_NRPRK_AR', fieldName: 'CHK_NRPRK_AR', header: { text: 'CHK_NRPRK_AR' }, visible: false }, - { name: 'CHK_ETC_AR', fieldName: 'CHK_ETC_AR', header: { text: 'CHK_ETC_AR' }, visible: false }, - { name: 'CHK_PRCUSE_RT', fieldName: 'CHK_PRCUSE_RT', header: { text: 'CHK_PRCUSE_RT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', - 'FCT_CPC', - 'PRCPLT_AR', + 'swtrmtFcltNm', + 'fcltCpct', + 'tmplntArea', { name: 'group_resident_friendly_facility', header: { text: '주민친화시설 면적(㎡)' }, items: [ - 'TOTAL_AR', - 'RESTRM_AR', - 'CHILDPLAY_AR', - 'ALSFC_AR', - 'PLGRND_AR', - 'PRKPLCE_AR', - 'CTLSTT_AR', - 'NRPRK_AR', - 'ETC_AR' + 'whol', + 'brkFcltArea', + 'childFrshpSpacArea', + 'bdySportsFcltArea', + 'sportsFcltArea', + 'pkltArea', + 'eduMultiFcltArea', + 'wtlndMakeArea', + 'etcCnt' ] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd', ], rowStartNum: 2, fixedColumns: 3, - tabKey: 'residentFriendlyFactInp', + tabKey: 'ST00130002', tabLabel: '주민친화시설' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts index 0e6da19f4..db1a72a59 100644 --- a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts @@ -1,176 +1,231 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 법적의무대상(`lglYn`) */ +export const PUBLIC_SEWAGE_TREAT_WATER_REUSE_LGL_YN_VALUES = ["대상", "미대상"] as const; + +/** 설치유무(`instlYn`) */ +export const PUBLIC_SEWAGE_TREAT_WATER_REUSE_INSTL_YN_VALUES = ["유", "무"] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 10. 물 재이용 > tab 1: 공공하수처리수 재이용 export const PublicSewageTreatWaterReuseInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장 - { name: 'BSC_FCT_NM', fieldName: '시설명', header: { text: '시설명' }, editable: false }, - { name: 'EQP_PRMS_DT', fieldName: '인가일(년/월/일)', header: { text: '인가일(년/월/일)' }, editable: false }, - { name: 'FCT_CPC', fieldName: '시설용량(㎥/일)', header: { text: '시설용량(㎥/일)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'TPUT_TOTAL', fieldName: '연간하수처리량(천톤/년)', header: { text: '연간하수처리량(천톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_LAW_YN', fieldName: '법적의무대상(대상/미대상)', header: { text: '법적의무대상(대상/미대상)' } }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false, styleName: 'rg-cell-readonly' }, + { + name: 'sncnYmd', + fieldName: 'sncnYmd', + header: { text: '인가일(년/월/일)' }, + datetimeFormat: 'yyyy-MM-dd', + editable: false, + styleName: 'rg-cell-readonly', + }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(㎥/일)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, + { name: 'wholPrcsQnt', fieldName: 'wholPrcsQnt', header: { text: '연간하수처리량(천톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + dropdownColumn( + { + name: 'lglYn', + fieldName: 'lglYn', + header: { text: '법적의무대상(대상/미대상)' }, + }, + PUBLIC_SEWAGE_TREAT_WATER_REUSE_LGL_YN_VALUES, + ), // 재이용시설 > 설치유무 - { name: 'INST_YN', fieldName: '설치유무(유/무)', header: { text: '설치유무(유/무)' } }, + dropdownColumn( + { + name: 'instlYn', + fieldName: 'instlYn', + header: { text: '설치유무(유/무)' }, + }, + PUBLIC_SEWAGE_TREAT_WATER_REUSE_INSTL_YN_VALUES, + ), // 재이용시설 > 재이용시설 설치시 - { name: 'RSWTR_RN_DT', fieldName: '가동개시일(yyyy-mm-dd)', header: { text: '가동개시일(yyyy-mm-dd)' } }, - { name: 'RSWTR_PRMS_DT', fieldName: '설치승인일(yyyy-mm-dd)', header: { text: '설치승인일(yyyy-mm-dd)' } }, + { + name: 'oprtngStrtYmd', + fieldName: 'oprtngStrtYmd', + header: { text: '가동개시일(yyyy-mm-dd)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, + { + name: 'instlAprvYmd', + fieldName: 'instlAprvYmd', + header: { text: '설치승인일(yyyy-mm-dd)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 재이용시설 > 재이용시설 설치시 > 재처리시설 - { name: 'RSWTR_CPC', fieldName: '용량(톤/일)', header: { text: '용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_TRT_NM', fieldName: '공법/기자재', header: { text: '공법/기자재' } }, + { name: 'rprcsFcltCpct', fieldName: 'rprcsFcltCpct', header: { text: '용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'mtcnsNm', fieldName: 'mtcnsNm', header: { text: '공법/기자재' } }, // 재이용시설 > 재이용시설 설치시 - { name: 'RSWTR_PIPE', fieldName: '공급관로길이(km)', header: { text: '공급관로길이(km)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RSWTR_PUMP', fieldName: '공급펌프(톤/일)', header: { text: '공급펌프(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtPplLen', fieldName: 'ruswtPplLen', header: { text: '공급관로길이(km)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtPumpLen', fieldName: 'ruswtPumpLen', header: { text: '공급펌프(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 하수처리수 재이용현황 > 총계 - { name: 'USE_AMNT_TOTAL', fieldName: '총계', header: { text: '총계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wholUseQnt', fieldName: 'wholUseQnt', header: { text: '총계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 하수처리수 재이용현황 > 장내용수 - { name: 'USE_AMNT_D01', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S01', fieldName: '세척수', header: { text: '세척수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S02', fieldName: '냉각수', header: { text: '냉각수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S03', fieldName: '청소수', header: { text: '청소수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S04', fieldName: '조경용수', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // (레거시) 식수대 - { name: 'USE_AMNT_S05', fieldName: '희석용수', header: { text: '희석용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S06', fieldName: '장내중수도', header: { text: '장내중수도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S07', fieldName: '장내기타용수', header: { text: '장내기타용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ithlWshwtrQnt', fieldName: 'ithlWshwtrQnt', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ithlClnQty', fieldName: 'ithlClnQty', header: { text: '세척수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlFrzgQty', fieldName: 'ithlFrzgQty', header: { text: '냉각수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlLdhcUswtrQnt', fieldName: 'ithlLdhcUswtrQnt', header: { text: '청소수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlDltnUswtrQnt', fieldName: 'ithlDltnUswtrQnt', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlRuswtrQnt', fieldName: 'ithlRuswtrQnt', header: { text: '희석용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlEtcQnt', fieldName: 'ithlEtcQnt', header: { text: '장내중수도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ithlUnsdQnt', fieldName: 'ithlUnsdQnt', header: { text: '장내기타용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 하수처리수 재이용현황 > 장외용수 - { name: 'USE_AMNT_D02', fieldName: '계', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S11', fieldName: '청소·화장실용수', header: { text: '청소·화장실용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S12', fieldName: '세척·살수용수', header: { text: '세척·살수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S13', fieldName: '조경용수', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S14', fieldName: '친수용수', header: { text: '친수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S15', fieldName: '하천등유지용수', header: { text: '하천등유지용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S16', fieldName: '농업용수', header: { text: '농업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S17', fieldName: '지하수충전', header: { text: '지하수충전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'USE_AMNT_S18', fieldName: '공업용수', header: { text: '공업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otcntrUswtrWhol', fieldName: 'otcntrUswtrWhol', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otcntrClnRstrmQnt', fieldName: 'otcntrClnRstrmQnt', header: { text: '청소·화장실용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrWshwtrQnt', fieldName: 'otcntrWshwtrQnt', header: { text: '세척·살수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrLdhcUswtrQnt', fieldName: 'otcntrLdhcUswtrQnt', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrHdrplUswtrQnt', fieldName: 'otcntrHdrplUswtrQnt', header: { text: '친수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'otcntrRvrKeepQnt', fieldName: 'otcntrRvrKeepQnt', header: { text: '하천등유지용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'agrcltUswtrPstnQnt', fieldName: 'agrcltUswtrPstnQnt', header: { text: '농업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'ugwtrChargQnt', fieldName: 'ugwtrChargQnt', header: { text: '지하수충전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, + { name: 'mdsrUswtrQnt', fieldName: 'mdsrUswtrQnt', header: { text: '공업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 재이용률 - { name: 'REUSE_RT', fieldName: '재이용률(%)', header: { text: '재이용률(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'rtlztnRt', fieldName: 'rtlztnRt', header: { text: '재이용률(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_RSWTR_LAW_YN', fieldName: 'CHK_RSWTR_LAW_YN', header: { text: 'CHK_RSWTR_LAW_YN' }, visible: false }, - { name: 'CHK_INST_YN', fieldName: 'CHK_INST_YN', header: { text: 'CHK_INST_YN' }, visible: false }, - { name: 'CHK_RSWTR_RN_DT', fieldName: 'CHK_RSWTR_RN_DT', header: { text: 'CHK_RSWTR_RN_DT' }, visible: false }, - { name: 'CHK_RSWTR_PRMS_DT', fieldName: 'CHK_RSWTR_PRMS_DT', header: { text: 'CHK_RSWTR_PRMS_DT' }, visible: false }, - { name: 'CHK_RSWTR_CPC', fieldName: 'CHK_RSWTR_CPC', header: { text: 'CHK_RSWTR_CPC' }, visible: false }, - { name: 'CHK_RSWTR_TRT_NM', fieldName: 'CHK_RSWTR_TRT_NM', header: { text: 'CHK_RSWTR_TRT_NM' }, visible: false }, - { name: 'CHK_RSWTR_PIPE', fieldName: 'CHK_RSWTR_PIPE', header: { text: 'CHK_RSWTR_PIPE' }, visible: false }, - { name: 'CHK_RSWTR_PUMP', fieldName: 'CHK_RSWTR_PUMP', header: { text: 'CHK_RSWTR_PUMP' }, visible: false }, - { name: 'CHK_TPUT_TOTAL', fieldName: 'CHK_TPUT_TOTAL', header: { text: 'CHK_TPUT_TOTAL' }, visible: false }, - { name: 'CHK_USE_AMNT_S01', fieldName: 'CHK_USE_AMNT_S01', header: { text: 'CHK_USE_AMNT_S01' }, visible: false }, - { name: 'CHK_USE_AMNT_S02', fieldName: 'CHK_USE_AMNT_S02', header: { text: 'CHK_USE_AMNT_S02' }, visible: false }, - { name: 'CHK_USE_AMNT_S03', fieldName: 'CHK_USE_AMNT_S03', header: { text: 'CHK_USE_AMNT_S03' }, visible: false }, - { name: 'CHK_USE_AMNT_S04', fieldName: 'CHK_USE_AMNT_S04', header: { text: 'CHK_USE_AMNT_S04' }, visible: false }, - { name: 'CHK_USE_AMNT_S05', fieldName: 'CHK_USE_AMNT_S05', header: { text: 'CHK_USE_AMNT_S05' }, visible: false }, - { name: 'CHK_USE_AMNT_S06', fieldName: 'CHK_USE_AMNT_S06', header: { text: 'CHK_USE_AMNT_S06' }, visible: false }, - { name: 'CHK_USE_AMNT_S07', fieldName: 'CHK_USE_AMNT_S07', header: { text: 'CHK_USE_AMNT_S07' }, visible: false }, - { name: 'CHK_USE_AMNT_S11', fieldName: 'CHK_USE_AMNT_S11', header: { text: 'CHK_USE_AMNT_S11' }, visible: false }, - { name: 'CHK_USE_AMNT_S12', fieldName: 'CHK_USE_AMNT_S12', header: { text: 'CHK_USE_AMNT_S12' }, visible: false }, - { name: 'CHK_USE_AMNT_S13', fieldName: 'CHK_USE_AMNT_S13', header: { text: 'CHK_USE_AMNT_S13' }, visible: false }, - { name: 'CHK_USE_AMNT_S14', fieldName: 'CHK_USE_AMNT_S14', header: { text: 'CHK_USE_AMNT_S14' }, visible: false }, - { name: 'CHK_USE_AMNT_S15', fieldName: 'CHK_USE_AMNT_S15', header: { text: 'CHK_USE_AMNT_S15' }, visible: false }, - { name: 'CHK_USE_AMNT_S16', fieldName: 'CHK_USE_AMNT_S16', header: { text: 'CHK_USE_AMNT_S16' }, visible: false }, - { name: 'CHK_USE_AMNT_S17', fieldName: 'CHK_USE_AMNT_S17', header: { text: 'CHK_USE_AMNT_S17' }, visible: false }, - { name: 'CHK_USE_AMNT_S18', fieldName: 'CHK_USE_AMNT_S18', header: { text: 'CHK_USE_AMNT_S18' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: ['ctpvNm', 'sggNm'] }, { name: 'group_facility', header: { text: '하수처리장' }, items: [ - 'BSC_FCT_NM', - 'EQP_PRMS_DT', - 'FCT_CPC', - 'RSWTR_LAW_YN' + 'swtrmtFcltNm', + 'sncnYmd', + 'prcsFcltCpct', + 'lglYn' ] }, { name: 'group_reuse_facility', header: { text: '재이용시설' }, items: [ - 'INST_YN', + 'instlYn', { name: 'group_reuse_install', header: { text: '재이용시설 설치시' }, items: [ - 'RSWTR_RN_DT', - 'RSWTR_PRMS_DT', + 'oprtngStrtYmd', + 'instlAprvYmd', { name: 'group_treatment_facility', header: { text: '처리시설' }, items: [ - 'RSWTR_CPC', - 'RSWTR_TRT_NM' + 'rprcsFcltCpct', + 'mtcnsNm' ] }, - 'RSWTR_PIPE', - 'RSWTR_PUMP' + 'ruswtPplLen', + 'ruswtPumpLen' ] } ] }, - 'TPUT_TOTAL', + 'wholPrcsQnt', { name: 'group_reuse_current', header: { text: '하수처리수 재이용현황(천톤/년)' }, items: [ - 'USE_AMNT_TOTAL', + 'wholUseQnt', { name: 'group_indoor_use', header: { text: '장 내 용 수' }, items: [ - 'USE_AMNT_D01', - 'USE_AMNT_S01', - 'USE_AMNT_S02', - 'USE_AMNT_S03', - 'USE_AMNT_S04', - 'USE_AMNT_S05', - 'USE_AMNT_S06', - 'USE_AMNT_S07' + 'ithlWshwtrQnt', + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt' ] }, { name: 'group_outdoor_use', header: { text: '장 외 용 수' }, items: [ - 'USE_AMNT_D02', - 'USE_AMNT_S11', - 'USE_AMNT_S12', - 'USE_AMNT_S13', - 'USE_AMNT_S14', - 'USE_AMNT_S15', - 'USE_AMNT_S16', - 'USE_AMNT_S17', - 'USE_AMNT_S18' + 'otcntrUswtrWhol', + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt' ] } ] }, - 'REUSE_RT', - 'RMK' + 'rtlztnRt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 4, fixedColumns: 0, - tabKey: 'publicSewageTreatWaterReuseInp', + tabKey: 'ST00100001', tabLabel: '공공하수처리수 재이용' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts index 10dc53ccd..9455637b0 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts @@ -1,120 +1,166 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 건축물용도(`bdstTypCn`) */ +export const RAINFALL_UTIL_FACT_BDST_TYP_VALUES = [ + "종합운동장", + "실내체육관", + "공공업무시설", + "공공기관청사", + "공동주택", + "학교", + "대규모점포", + "골프장", + "기타", +] as const; + +/** 법적의무대상여부(`lglTrgtYn`) */ +export const RAINFALL_UTIL_FACT_LGL_TRGT_YN_VALUES = ["대상", "미대상"] as const; + +/** 이용용도(`exlnUtlzUsgExpln`) */ +export const RAINFALL_UTIL_FACT_EXLN_UTLZ_USG_VALUES = [ + "청소화장실용수", + "세척살수용수", + "조경용수", + "친수용수", + "농업용수", + "공업용수", + "기타", +] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 10. 물 재이용 > tab 3: 빗물이용시설현황 export const RainFallUtilFactStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ST_RWR_REUSE_FCT_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 시설명 - { name: 'RWR_REUSE_FCT_NM', fieldName: '시설명', header: { text: '시설명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } }, // 건축물용도 - { name: 'BILD_TP', fieldName: '건축물용도', header: { text: '건축물용도' } }, + dropdownColumn( + { + name: 'bdstTypCn', + fieldName: 'bdstTypCn', + header: { text: '건축물용도' }, + }, + RAINFALL_UTIL_FACT_BDST_TYP_VALUES, + ), // 위치(주소) - { name: 'ADDR', fieldName: '위치(주소)', header: { text: '위치(주소)' } }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치(주소)' } }, // 법적의무대상여부(대상/미대상) - { name: 'LAW_YN', fieldName: '법적의무대상여부(대상/미대상)', header: { text: '법적의무대상여부(대상/미대상)' } }, + dropdownColumn( + { + name: 'lglTrgtYn', + fieldName: 'lglTrgtYn', + header: { text: '법적의무대상여부(대상/미대상)' }, + }, + RAINFALL_UTIL_FACT_LGL_TRGT_YN_VALUES, + ), // 설치완료일 yyyy-mm-dd - { name: 'RN_DT', fieldName: '설치완료일 yyyy-mm-dd', header: { text: '설치완료일 yyyy-mm-dd' } }, + { + name: 'cmptYmd', + fieldName: 'cmptYmd', + header: { text: '설치완료일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 설치비(백만원) - { name: 'INSTL_CT', fieldName: '설치비(백만원)', header: { text: '설치비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'instlAmt', fieldName: 'instlAmt', header: { text: '설치비(백만원)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 집수면 종류 - { name: 'CCM', fieldName: '집수면 종류', header: { text: '집수면 종류' } }, + { name: 'rcptMynCn', fieldName: 'rcptMynCn', header: { text: '집수면 종류' } }, // 여과등처리시설유무(유:처리시설종류,무:미설치) - { name: 'TRTFC_ENNC', fieldName: '여과등처리시설유무(유:처리시설종류,무:미설치)', header: { text: '여과등처리시설유무(유:처리시설종류,무:미설치)' } }, + { name: 'filtrPrcsFcltYnExpln', fieldName: 'filtrPrcsFcltYnExpln', header: { text: '여과등처리시설유무(유:처리시설종류,무:미설치)' } }, // 저류조용량(m³) - { name: 'SGBS_CPCTY', fieldName: '저류조용량(m³)', header: { text: '저류조용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'impdCpct', fieldName: 'impdCpct', header: { text: '저류조용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 연간이용량(m³/년) - { name: 'FYER_USEQTY', fieldName: '연간이용량(m³/년)', header: { text: '연간이용량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlUseQnt', fieldName: 'anlUseQnt', header: { text: '연간이용량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 이용용도 - { name: 'RWR_PRCUSE_PRPPOS', fieldName: '이용용도', header: { text: '이용용도' } }, + dropdownColumn( + { + name: 'exlnUtlzUsgExpln', + fieldName: 'exlnUtlzUsgExpln', + header: { text: '이용용도' }, + }, + RAINFALL_UTIL_FACT_EXLN_UTLZ_USG_VALUES, + ), // 연간운영비(백만원/년) - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlOperAmt', fieldName: 'anlOperAmt', header: { text: '연간운영비(백만원/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '작성자의견', header: { text: '작성자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 용도별 이용량 (화면에 표시 안 함) - { name: 'FYER_USE_01', fieldName: '빗물사용 1', header: { text: '빗물사용 1' }, visible: false }, - { name: 'FYER_USE_02', fieldName: '빗물사용 2', header: { text: '빗물사용 2' }, visible: false }, - { name: 'FYER_USE_03', fieldName: '빗물사용 3', header: { text: '빗물사용 3' }, visible: false }, - { name: 'FYER_USE_04', fieldName: '빗물사용 4', header: { text: '빗물사용 4' }, visible: false }, - { name: 'FYER_USE_05', fieldName: '빗물사용 5', header: { text: '빗물사용 5' }, visible: false }, - { name: 'FYER_USE_06', fieldName: '빗물사용 6', header: { text: '빗물사용 6' }, visible: false }, - { name: 'FYER_USE_07', fieldName: '빗물사용 7', header: { text: '빗물사용 7' }, visible: false }, - - // Hidden: 인가일 (화면에 표시 안 함) - { name: 'PERMIT_DT', fieldName: '인가일', header: { text: '인가일' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_RWR_REUSE_FCT_NM', fieldName: 'CHK_RWR_REUSE_FCT_NM', header: { text: 'CHK_RWR_REUSE_FCT_NM' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_RN_DT', fieldName: 'CHK_RN_DT', header: { text: 'CHK_RN_DT' }, visible: false }, - { name: 'CHK_INSTL_CT', fieldName: 'CHK_INSTL_CT', header: { text: 'CHK_INSTL_CT' }, visible: false }, - { name: 'CHK_CCM', fieldName: 'CHK_CCM', header: { text: 'CHK_CCM' }, visible: false }, - { name: 'CHK_CCM_AR', fieldName: 'CHK_CCM_AR', header: { text: 'CHK_CCM_AR' }, visible: false }, - { name: 'CHK_TRTFC_ENNC', fieldName: 'CHK_TRTFC_ENNC', header: { text: 'CHK_TRTFC_ENNC' }, visible: false }, - { name: 'CHK_SGBS_CPCTY', fieldName: 'CHK_SGBS_CPCTY', header: { text: 'CHK_SGBS_CPCTY' }, visible: false }, - { name: 'CHK_FYER_USEQTY', fieldName: 'CHK_FYER_USEQTY', header: { text: 'CHK_FYER_USEQTY' }, visible: false }, - { name: 'CHK_RWR_PRCUSE_PRPPOS', fieldName: 'CHK_RWR_PRCUSE_PRPPOS', header: { text: 'CHK_RWR_PRCUSE_PRPPOS' }, visible: false }, - { name: 'CHK_FYER_OPER_CT', fieldName: 'CHK_FYER_OPER_CT', header: { text: 'CHK_FYER_OPER_CT' }, visible: false }, - { name: 'CHK_LAW_YN', fieldName: 'CHK_LAW_YN', header: { text: 'CHK_LAW_YN' }, visible: false }, - { name: 'CHK_BILD_TP', fieldName: 'CHK_BILD_TP', header: { text: 'CHK_BILD_TP' }, visible: false }, - { name: 'CHK_PERMIT_DT', fieldName: 'CHK_PERMIT_DT', header: { text: 'CHK_PERMIT_DT' }, visible: false }, - { name: 'CHK_FYER_USE_01', fieldName: 'CHK_FYER_USE_01', header: { text: 'CHK_FYER_USE_01' }, visible: false }, - { name: 'CHK_FYER_USE_02', fieldName: 'CHK_FYER_USE_02', header: { text: 'CHK_FYER_USE_02' }, visible: false }, - { name: 'CHK_FYER_USE_03', fieldName: 'CHK_FYER_USE_03', header: { text: 'CHK_FYER_USE_03' }, visible: false }, - { name: 'CHK_FYER_USE_04', fieldName: 'CHK_FYER_USE_04', header: { text: 'CHK_FYER_USE_04' }, visible: false }, - { name: 'CHK_FYER_USE_05', fieldName: 'CHK_FYER_USE_05', header: { text: 'CHK_FYER_USE_05' }, visible: false }, - { name: 'CHK_FYER_USE_06', fieldName: 'CHK_FYER_USE_06', header: { text: 'CHK_FYER_USE_06' }, visible: false }, - { name: 'CHK_FYER_USE_07', fieldName: 'CHK_FYER_USE_07', header: { text: 'CHK_FYER_USE_07' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: ['ctpvNm', 'sggNm'] }, - 'RWR_REUSE_FCT_NM', - 'BILD_TP', - 'ADDR', - 'LAW_YN', - 'RN_DT', - 'INSTL_CT', - 'CCM', - 'TRTFC_ENNC', - 'SGBS_CPCTY', - 'FYER_USEQTY', - 'RWR_PRCUSE_PRPPOS', - 'FYER_OPER_CT', - 'RMK' + 'fcltNm', + 'bdstTypCn', + 'fcltPstnCn', + 'lglTrgtYn', + 'cmptYmd', + 'instlAmt', + 'rcptMynCn', + 'filtrPrcsFcltYnExpln', + 'impdCpct', + 'anlUseQnt', + 'exlnUtlzUsgExpln', + 'anlOperAmt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'rainFallUtilFactStatusInp', + tabKey: 'ST00100003', tabLabel: '빗물이용시설현황' -}; \ No newline at end of file +}; diff --git a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts index 5ec78024c..ee62e6c82 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts @@ -1,109 +1,191 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +/** 시설물용도/개발사업종류(`bldgSeCn`) */ +export const RECYCLED_WATER_BLDG_SE_VALUES = [ + "숙박업", + "목욕장업", + "공장", + "발전시설", + "대규모점포", + "물류시설", + "운수시설", + "업무시설", + "교정시설", + "방송국", + "전신전화국", + "기타제반법령에의한시설물", + "관광단지의개발사업", + "도시개발사업", + "산업단지개발사업", + "택지개발사업", + "기타", +] as const; + +/** 법적의무대상여부(`lglTrgtYn`) */ +export const RECYCLED_WATER_LGL_TRGT_YN_VALUES = ["대상", "미대상"] as const; + +/** 중수도용도(`mnUsgCn`) */ +export const RECYCLED_WATER_MN_USG_VALUES = [ + "청소화장실용수", + "세척살수용수", + "조경용수", + "친수용수", + "하천등유지용수", + "공업용수", +] as const; + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 10. 물 재이용 > tab 2: 중수도 현황 export const RecycledWaterStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'RUSWTR_FCT_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 건축물명/개발사업명 - { name: 'BILD_NM', fieldName: '건축물명/개발사업명', header: { text: '건축물명/개발사업명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '건축물명/개발사업명' } }, // 시설물용도/개발사업종류 - { name: 'BILD_TP', fieldName: '시설물용도/개발사업종류', header: { text: '시설물용도/개발사업종류' } }, + dropdownColumn( + { + name: 'bldgSeCn', + fieldName: 'bldgSeCn', + header: { text: '시설물용도/개발사업종류' }, + }, + RECYCLED_WATER_BLDG_SE_VALUES, + ), // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '주소' } }, // 법적의무대상여부(대상/미대상) - { name: 'LAW_YN', fieldName: '법적의무대상여부(대상/미대상)', header: { text: '법적의무대상여부(대상/미대상)' } }, + dropdownColumn( + { + name: 'lglTrgtYn', + fieldName: 'lglTrgtYn', + header: { text: '법적의무대상여부(대상/미대상)' }, + }, + RECYCLED_WATER_LGL_TRGT_YN_VALUES, + ), // 설치완료일(년-월-일) - { name: 'COMP_DT', fieldName: '설치완료일(년-월-일)', header: { text: '설치완료일(년-월-일)' } }, + { + name: 'cmptnStrtYmd', + fieldName: 'cmptnStrtYmd', + header: { text: '설치완료일(년-월-일)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 가동개시일(년-월-일) - { name: 'RN_DT', fieldName: '가동개시일(년-월-일)', header: { text: '가동개시일(년-월-일)' } }, + { + name: 'oprtngStrtYmd', + fieldName: 'oprtngStrtYmd', + header: { text: '가동개시일(년-월-일)' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 시설현황 - { name: 'TOTAR', fieldName: '건축연면적(m²)', header: { text: '건축연면적(m²)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'PROC_CPCTY', fieldName: '처리용량(m³/일)', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'archWholArea', fieldName: 'archWholArea', header: { text: '건축연면적(m²)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsCpct', fieldName: 'prcsCpct', header: { text: '처리용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 물 사용량(m³/일) - { name: 'WATER_USE_CPCTY', fieldName: '물 사용량(m³/일)', header: { text: '물 사용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'wtrUseQnt', fieldName: 'wtrUseQnt', header: { text: '물 사용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 중수도이용량(m³/일) - { name: 'RUSWTR_USE_CPCTY', fieldName: '중수도이용량(m³/일)', header: { text: '중수도이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtrUtztnQnt', fieldName: 'ruswtrUtztnQnt', header: { text: '중수도이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 대체수이용량(m³/일) - { name: 'SUB_USE_CPCTY', fieldName: '대체수이용량(m³/일)', header: { text: '대체수이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'sbstCntUtztnQnt', fieldName: 'sbstCntUtztnQnt', header: { text: '대체수이용량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 중수도용도 - { name: 'RUSWTR_PRPOS', fieldName: '중수도용도', header: { text: '중수도용도' } }, + dropdownColumn( + { + name: 'mnUsgCn', + fieldName: 'mnUsgCn', + header: { text: '중수도용도' }, + }, + RECYCLED_WATER_MN_USG_VALUES, + ), // 연간운영비(백만원/년) - { name: 'FYER_OPER_CT', fieldName: '연간운영비(백만원/년)', header: { text: '연간운영비(백만원/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ruswtrOperAmt', fieldName: 'ruswtrOperAmt', header: { text: '연간운영비(백만원/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 사용자의견, 검토자의견 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_BILD_NM', fieldName: 'CHK_BILD_NM', header: { text: 'CHK_BILD_NM' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_TOTAR', fieldName: 'CHK_TOTAR', header: { text: 'CHK_TOTAR' }, visible: false }, - { name: 'CHK_PROC_CPCTY', fieldName: 'CHK_PROC_CPCTY', header: { text: 'CHK_PROC_CPCTY' }, visible: false }, - { name: 'CHK_RUSWTR_USE_CPCTY', fieldName: 'CHK_RUSWTR_USE_CPCTY', header: { text: 'CHK_RUSWTR_USE_CPCTY' }, visible: false }, - { name: 'CHK_COMP_DT', fieldName: 'CHK_COMP_DT', header: { text: 'CHK_COMP_DT' }, visible: false }, - { name: 'CHK_RN_DT', fieldName: 'CHK_RN_DT', header: { text: 'CHK_RN_DT' }, visible: false }, - { name: 'CHK_RUSWTR_PRPOS', fieldName: 'CHK_RUSWTR_PRPOS', header: { text: 'CHK_RUSWTR_PRPOS' }, visible: false }, - { name: 'CHK_LAW_YN', fieldName: 'CHK_LAW_YN', header: { text: 'CHK_LAW_YN' }, visible: false }, - { name: 'CHK_BILD_TP', fieldName: 'CHK_BILD_TP', header: { text: 'CHK_BILD_TP' }, visible: false }, - { name: 'CHK_PERMIT_DT', fieldName: 'CHK_PERMIT_DT', header: { text: 'CHK_PERMIT_DT' }, visible: false }, - { name: 'CHK_FYER_OPER_CT', fieldName: 'CHK_FYER_OPER_CT', header: { text: 'CHK_FYER_OPER_CT' }, visible: false }, - { name: 'CHK_WATER_USE_CPCTY', fieldName: 'CHK_WATER_USE_CPCTY', header: { text: 'CHK_WATER_USE_CPCTY' }, visible: false }, - { name: 'CHK_SUB_USE_CPCTY', fieldName: 'CHK_SUB_USE_CPCTY', header: { text: 'CHK_SUB_USE_CPCTY' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: ['ctpvNm', 'sggNm'] }, - 'BILD_NM', - 'BILD_TP', - 'ADDR', - 'LAW_YN', - 'COMP_DT', - 'RN_DT', + 'fcltNm', + 'bldgSeCn', + 'fcltPstnCn', + 'lglTrgtYn', + 'cmptnStrtYmd', + 'oprtngStrtYmd', { name: 'group_facility_status', header: { text: '시설현황' }, items: [ - 'TOTAR', - 'PROC_CPCTY' + 'archWholArea', + 'prcsCpct' ] }, - 'WATER_USE_CPCTY', - 'RUSWTR_USE_CPCTY', - 'SUB_USE_CPCTY', - 'RUSWTR_PRPOS', - 'FYER_OPER_CT', - 'RMK' + 'wtrUseQnt', + 'ruswtrUtztnQnt', + 'sbstCntUtztnQnt', + 'mnUsgCn', + 'ruswtrOperAmt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'recycledWaterStatusInp', + tabKey: 'ST00100002', tabLabel: '중수도 현황' -}; \ No newline at end of file +}; diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts index 7144c1edc..7a31a2162 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts @@ -2,56 +2,58 @@ export const TreatWaterReuseBusinessApprStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'SIDO_CD', fieldName: '시도코드', header: { text: '시도코드' }, visible: false }, - { name: 'REUSE_CMPN_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 상호(명칭) - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, + { name: 'bplcNm', fieldName: 'bplcNm', header: { text: '상호(명칭)' } }, // 대표자 - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, + { name: 'rprsvNm', fieldName: 'rprsvNm', header: { text: '대표자' } }, // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'bplcPstnCn', fieldName: 'bplcPstnCn', header: { text: '주소' } }, // 인가일 - { name: 'CMPN_RGSDE', fieldName: '인가일 yyyy-mm-dd', header: { text: '인가일 yyyy-mm-dd' } }, + { + name: 'coRegYmd', + fieldName: 'coRegYmd', + header: { text: '인가일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CMPNM', fieldName: 'CHK_CMPNM', header: { text: 'CHK_CMPNM' }, visible: false }, - { name: 'CHK_RPRSNTV', fieldName: 'CHK_RPRSNTV', header: { text: 'CHK_RPRSNTV' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_CMPN_RGSDE', fieldName: 'CHK_CMPN_RGSDE', header: { text: 'CHK_CMPN_RGSDE' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'bplcSn', fieldName: 'bplcSn', header: { text: '사업장일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: ['ctpvNm', 'sggNm'] }, - 'CMPNM', - 'RPRSNTV', - 'ADDR', - 'CMPN_RGSDE', - 'RMK' + 'bplcNm', + 'rprsvNm', + 'bplcPstnCn', + 'coRegYmd', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'bplcSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'treatWaterReuseBusinessApprStatusInp', + tabKey: 'ST00100005', tabLabel: '하·폐수처리수 재이용사업 인가현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts index 15a3b12b2..4b93706a1 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts @@ -2,56 +2,58 @@ export const TreatWaterReuseDesignConstStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false }, // editable 임의로 - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'SIDO_CD', fieldName: '시도코드', header: { text: '시도코드' }, visible: false }, - { name: 'PLCST_CMPNY_SEQ', fieldName: '일련번호', header: { text: '일련번호' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } }, // 상호(명칭) - { name: 'CMPNM', fieldName: '상호(명칭)', header: { text: '상호(명칭)' } }, + { name: 'bplcNm', fieldName: 'bplcNm', header: { text: '상호(명칭)' } }, // 대표자 - { name: 'RPRSNTV', fieldName: '대표자', header: { text: '대표자' } }, + { name: 'rprsvNm', fieldName: 'rprsvNm', header: { text: '대표자' } }, // 주소 - { name: 'ADDR', fieldName: '주소', header: { text: '주소' } }, + { name: 'bplcPstnCn', fieldName: 'bplcPstnCn', header: { text: '주소' } }, // 등록일 yyyy-mm-dd - { name: 'CMPN_RGSDE', fieldName: '등록일 yyyy-mm-dd', header: { text: '등록일 yyyy-mm-dd' } }, + { + name: 'coRegYmd', + fieldName: 'coRegYmd', + header: { text: '등록일 yyyy-mm-dd' }, + datetimeFormat: 'yyyy-MM-dd', + editor: { + type: 'date' as const, + dropDownWhenClick: true, + datetimeFormat: 'yyyy-MM-dd', + }, + }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CMPNM', fieldName: 'CHK_CMPNM', header: { text: 'CHK_CMPNM' }, visible: false }, - { name: 'CHK_RPRSNTV', fieldName: 'CHK_RPRSNTV', header: { text: 'CHK_RPRSNTV' }, visible: false }, - { name: 'CHK_ADDR', fieldName: 'CHK_ADDR', header: { text: 'CHK_ADDR' }, visible: false }, - { name: 'CHK_CMPN_RGSDE', fieldName: 'CHK_CMPN_RGSDE', header: { text: 'CHK_CMPN_RGSDE' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'bplcSn', fieldName: 'bplcSn', header: { text: '사업장일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: ['ctpvNm', 'sggNm'] }, - 'CMPNM', - 'RPRSNTV', - 'ADDR', - 'CMPN_RGSDE', - 'RMK' + 'bplcNm', + 'rprsvNm', + 'bplcPstnCn', + 'coRegYmd', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'bplcSn', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'treatWaterReuseDesignConstStatusInp', + tabKey: 'ST00100004', tabLabel: '하·폐수처리수 재이용시설 설계시공업 등록현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts index f4c0ddae2..83b4ad7a1 100644 --- a/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts +++ b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts @@ -192,11 +192,6 @@ export const PublicSewageFacilityColumn = { { name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' }, editable: false }, { name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수\n(명)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, { name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // hidden - { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false }, - { name: 'rowType', fieldName: 'rowType', header: { text: '데이터구분' }, visible: false }, - { name: 'statsYr', fieldName: 'statsYr', header: { text: '통계연도' }, visible: false }, ].map((col) => ({ ...col, editable: false })), columnLayout: [ @@ -291,8 +286,5 @@ export const PublicSewageFacilityColumn = { 'intgPrcsFcltCn', 'wholNope', 'sftyAcdntEvntcsCnt', - 'admdstCd', - 'rowType', - 'statsYr', ], }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts index 01b224322..c4cff90cb 100644 --- a/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts +++ b/apps/st/src/components/util/column/total/SeweragePentrationRateTotColumn.ts @@ -139,9 +139,6 @@ export const SeweragePentrationRateTotColumn = { // 비고 { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - - { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false }, - { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false }, ].map((col) => ({ ...col, editable: false })), columnLayout: [ @@ -205,7 +202,5 @@ export const SeweragePentrationRateTotColumn = { 'sewerInstlRt', 'rurPsplyRt', 'rmrk', - 'swagePplPsplyRt', - 'admdstCd', ] }; \ No newline at end of file diff --git a/apps/st/src/components/util/grid/computedFieldCallbacks.ts b/apps/st/src/components/util/grid/computedFieldCallbacks.ts index f8c59f37a..43ce816f6 100644 --- a/apps/st/src/components/util/grid/computedFieldCallbacks.ts +++ b/apps/st/src/components/util/grid/computedFieldCallbacks.ts @@ -299,6 +299,20 @@ export function createQuotientRatioPercentValueCallback( }; } +/** Σ(numeratorFields) / denominatorField × 10,000 — 기준인구당 건수(건/10,000명) */ +export function createSumPerTenThousandValueCallback( + numeratorFields: string[], + denominatorField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const sum = sumFieldValues(numeratorFields, fieldNamesArr, values); + const denIdx = fieldNamesArr.indexOf(denominatorField); + const den = denIdx >= 0 ? toNum(values[denIdx]) : 0; + if (den === 0) return 0; + return truncateToFirstDecimal((sum / den) * 10000); + }; +} + /** 나눗셈: fields[0] / fields[1] (분모 0이면 0) */ export function createQuotientValueCallback(fields: string[]): DataFieldValueCallback { const numeratorField = fields[0] ?? ""; @@ -365,5 +379,10 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField return createPumpGrossPpqtyValueCallback(config.pumpGroups); case 'bodPrcsRt': return createBodPrcsRtValueCallback(config.inflwField, config.dschgField); + case 'sumPerTenThousand': + return createSumPerTenThousandValueCallback( + config.numeratorFields, + config.denominatorField, + ); } } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts index 8f1d97e13..1bac7543c 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts @@ -7,18 +7,24 @@ import { type CauserBurdenRequestParams, type CauserBurdenSaveRequest, type CauserBurdenSaveResponse, + type CauserBurdenSubmitRequest, type SewerageChargeApiResponse, type SewerageChargeRequestParams, type SewerageChargeSaveRequest, type SewerageChargeSaveResponse, + type SewerageChargeSubmitRequest, type SewerageFinanceSettleApiResponse, type SewerageFinanceSettleRequestParams, type SewerageFinanceSettleSaveRequest, type SewerageFinanceSettleSaveResponse, + type SewerageFinanceSettleSubmitRequest, + type SewerageFinanceSettleSubmitResponse, + type SewerageFinanceSubmitResponse, type SewerageUsageFeeApiResponse, type SewerageUsageFeeRequestParams, type SewerageUsageFeeSaveRequest, type SewerageUsageFeeSaveResponse, + type SewerageUsageFeeSubmitRequest, type StatisticsInputSewerageFinanceService, } from "../../model/input/statistics-input-sewerage-finance-interface"; @@ -49,6 +55,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도재정 결산 제출 — ST00090001 */ + public async submitSewerageFinanceSettle( + request: SewerageFinanceSettleSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/settle/submit`, + request, + ); + return response.data; + } + /** 하수도 요금 조회 — ST00090002 */ public async getSewerageChargeList( params: SewerageChargeRequestParams, @@ -71,6 +88,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도 요금 제출 — ST00090002 */ + public async submitSewerageCharge( + request: SewerageChargeSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/charge/submit`, + request, + ); + return response.data; + } + /** 하수도 사용료 조회 — ST00090003 */ public async getSewerageUsageFeeList( params: SewerageUsageFeeRequestParams, @@ -93,6 +121,17 @@ export class StatisticsInputSewerageFinanceApiService return response.data; } + /** 하수도 사용료 제출 — ST00090003 */ + public async submitSewerageUsageFee( + request: SewerageUsageFeeSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/usage-fee/submit`, + request, + ); + return response.data; + } + /** 원인자 부담금 조회 — ST00090004 */ public async getCauserBurdenList( params: CauserBurdenRequestParams, @@ -114,6 +153,17 @@ export class StatisticsInputSewerageFinanceApiService ); return response.data; } + + /** 원인자 부담금 제출 — ST00090004 */ + public async submitCauserBurden( + request: CauserBurdenSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/causer-burden/submit`, + request, + ); + return response.data; + } } export const statisticsInputSewerageFinanceApi = diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts new file mode 100644 index 000000000..478ad0696 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-service-level-service.ts @@ -0,0 +1,94 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type SewerageServiceLevelComplaintApiResponse, + type SewerageServiceLevelComplaintRequestParams, + type SewerageServiceLevelComplaintSaveRequest, + type SewerageServiceLevelComplaintSaveResponse, + type SewerageServiceLevelComplaintSubmitRequest, + type SewerageServiceLevelResidentFriendlyFactApiResponse, + type SewerageServiceLevelResidentFriendlyFactRequestParams, + type SewerageServiceLevelResidentFriendlyFactSaveRequest, + type SewerageServiceLevelResidentFriendlyFactSaveResponse, + type SewerageServiceLevelResidentFriendlyFactSubmitRequest, + type SewerageServiceLevelSubmitResponse, + type StatisticsInputSewerageServiceLevelService, +} from "../../model/input/statistics-input-sewerage-service-level-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-service-level`; + +export class StatisticsInputSewerageServiceLevelApiService + implements StatisticsInputSewerageServiceLevelService +{ + /** 민원 조회 — ST00130001 */ + public async getComplaintList( + params: SewerageServiceLevelComplaintRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/complaint`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 주민친화시설 조회 — ST00130002 */ + public async getResidentFriendlyFactList( + params: SewerageServiceLevelResidentFriendlyFactRequestParams, + ): Promise { + const response = + await stClient.get( + `${BASE}/resident-friendly-fact`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 민원 저장 — ST00130001 */ + public async saveComplaintList( + request: SewerageServiceLevelComplaintSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/complaint/save`, + request, + ); + return response.data; + } + + /** 주민친화시설 저장 — ST00130002 */ + public async saveResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/resident-friendly-fact/save`, + request, + ); + return response.data; + } + + /** 민원 제출 — ST00130001 */ + public async submitComplaintList( + request: SewerageServiceLevelComplaintSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/complaint/submit`, + request, + ); + return response.data; + } + + /** 주민친화시설 제출 — ST00130002 */ + public async submitResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/resident-friendly-fact/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputSewerageServiceLevelApi = + new StatisticsInputSewerageServiceLevelApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts new file mode 100644 index 000000000..4622da5c8 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-water-reuse-service.ts @@ -0,0 +1,204 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import { + type PublicSewageTreatWaterReuseApiResponse, + type PublicSewageTreatWaterReuseSaveRequest, + type PublicSewageTreatWaterReuseSaveResponse, + type PublicSewageTreatWaterReuseSubmitRequest, + type PublicSewageTreatWaterReuseSubmitResponse, + type RainFallUtilFactStatusApiResponse, + type RainFallUtilFactStatusSaveRequest, + type RainFallUtilFactStatusSaveResponse, + type RainFallUtilFactStatusSubmitRequest, + type RainFallUtilFactStatusSubmitResponse, + type RecycledWaterStatusApiResponse, + type RecycledWaterStatusSaveRequest, + type RecycledWaterStatusSaveResponse, + type RecycledWaterStatusSubmitRequest, + type RecycledWaterStatusSubmitResponse, + type StatisticsInputWaterReuseService, + type TreatWaterReuseBusinessApprStatusApiResponse, + type TreatWaterReuseBusinessApprStatusSaveRequest, + type TreatWaterReuseBusinessApprStatusSaveResponse, + type TreatWaterReuseBusinessApprStatusSubmitRequest, + type TreatWaterReuseBusinessApprStatusSubmitResponse, + type TreatWaterReuseDesignConstStatusApiResponse, + type TreatWaterReuseDesignConstStatusSaveRequest, + type TreatWaterReuseDesignConstStatusSaveResponse, + type TreatWaterReuseDesignConstStatusSubmitRequest, + type TreatWaterReuseDesignConstStatusSubmitResponse, + type WaterReuseRequestParams, +} from "../../model/input/statistics-input-water-reuse-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/water-reuse`; + +export class StatisticsInputWaterReuseApiService implements StatisticsInputWaterReuseService { + /** 공공하수처리수 재이용 조회 — ST00100001 */ + public async getPublicSewageTreatWaterReuseList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-sewage`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공하수처리수 재이용 저장 — ST00100001 */ + public async savePublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-sewage/save`, + request, + ); + return response.data; + } + + /** 공공하수처리수 재이용 제출 — ST00100001 */ + public async submitPublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-sewage/submit`, + request, + ); + return response.data; + } + + /** 중수도 현황 조회 — ST00100002 */ + public async getRecycledWaterStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/recycled-water`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 중수도 현황 저장 — ST00100002 */ + public async saveRecycledWaterStatus( + request: RecycledWaterStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/recycled-water/save`, + request, + ); + return response.data; + } + + /** 중수도 현황 제출 — ST00100002 */ + public async submitRecycledWaterStatus( + request: RecycledWaterStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/recycled-water/submit`, + request, + ); + return response.data; + } + + /** 빗물이용시설현황 조회 — ST00100003 */ + public async getRainFallUtilFactStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/rainfall-util-fact-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 빗물이용시설현황 저장 — ST00100003 */ + public async saveRainFallUtilFactStatus( + request: RainFallUtilFactStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/rainfall-util-fact-status/save`, + request, + ); + return response.data; + } + + /** 빗물이용시설현황 제출 — ST00100003 */ + public async submitRainFallUtilFactStatus( + request: RainFallUtilFactStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/rainfall-util-fact-status/submit`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 조회 — ST00100004 */ + public async getTreatWaterReuseDesignConstStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/treat-water-reuse-design-const-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 저장 — ST00100004 */ + public async saveTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-design-const-status/save`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 제출 — ST00100004 */ + public async submitTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-design-const-status/submit`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 조회 — ST00100005 */ + public async getTreatWaterReuseBusinessApprStatusList( + params: WaterReuseRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/treat-water-reuse-business-appr-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 저장 — ST00100005 */ + public async saveTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-business-appr-status/save`, + request, + ); + return response.data; + } + + /** 하·폐수처리수 재이용사업 인가현황 제출 — ST00100005 */ + public async submitTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/treat-water-reuse-business-appr-status/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputWaterReuseApi = new StatisticsInputWaterReuseApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts b/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts new file mode 100644 index 000000000..1cbade954 --- /dev/null +++ b/apps/st/src/entities/statistics/api/statistics-sewerage-aggregate-summary-service.ts @@ -0,0 +1,104 @@ +import { stClient } from "@/shared/api/client"; + +import { + SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + type SewerageAggregateSummaryDownloadRequest, + type SewerageAggregateSummaryDownloadResult, + type SewerageAggregateSummaryTabListResponse, + type StatisticsSewerageAggregateSummaryService, +} from "../model/statistics-total/statistics-sewerage-aggregate-summary-interface"; + +const INDICATORS_URL = "/st-management-service/api/v1/summary-excel/indicators"; +/** middleware `/server` rewrite 대신 장시간 blob용 API Route 프록시 */ +const DOWNLOAD_URL = "/api/st/summary-excel/download"; +/** 확인 안내와 동일 — 최대 1시간 대기 */ +const DOWNLOAD_TIMEOUT_MS = 3_600_000; + +function parseDownloadFileName(contentDisposition: string): string | undefined { + if (!contentDisposition) return undefined; + + const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); + if (utf8Match?.[1]) { + return decodeURIComponent(utf8Match[1].trim()); + } + + const plainMatch = contentDisposition.match(/filename\s*=\s*"?([^";]+)"?/i); + return plainMatch?.[1]?.trim(); +} + +async function toExcelBlob(data: Blob): Promise { + if (data.type === SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME) { + return data; + } + return new Blob([data], { type: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME }); +} + +async function parseBlobErrorMessage(data: Blob): Promise { + const fallback = "엑셀 내려받기 중 오류가 발생했습니다."; + try { + const text = await data.text(); + const json = JSON.parse(text) as { message?: string }; + return json.message?.trim() || fallback; + } catch { + return fallback; + } +} + +export class StatisticsSewerageAggregateSummaryApiService + implements StatisticsSewerageAggregateSummaryService +{ + /** 하수도 통계 총괄표 — 통계항목(탭) 목록 */ + public async getAggregateSummaryTabList(): Promise { + const response = + await stClient.get(INDICATORS_URL); + return response.data; + } + + /** 하수도 통계 총괄표 — 엑셀 내려받기 (`Content-Type`: xlsx, `Content-Disposition`: 파일명) */ + public async downloadAggregateSummary( + request: SewerageAggregateSummaryDownloadRequest, + ): Promise { + try { + const response = await fetch(DOWNLOAD_URL, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + Accept: SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME, + }, + body: JSON.stringify(request), + signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS), + }); + + const contentType = String(response.headers.get("content-type") ?? ""); + const contentDisposition = String(response.headers.get("content-disposition") ?? ""); + const data = await response.blob(); + + if (!response.ok) { + throw new Error(await parseBlobErrorMessage(data)); + } + + if (contentType.includes("application/json")) { + throw new Error(await parseBlobErrorMessage(data)); + } + + return { + blob: await toExcelBlob(data), + fileName: parseDownloadFileName(contentDisposition), + }; + } catch (error) { + if (error instanceof DOMException && error.name === "TimeoutError") { + throw new Error( + "엑셀 생성 시간이 초과되었습니다. 선택 항목을 줄이거나 잠시 후 다시 시도해주세요.", + ); + } + if (error instanceof Error) { + throw error; + } + throw new Error("엑셀 내려받기 중 오류가 발생했습니다."); + } + } +} + +export const statisticsSewerageAggregateSummaryApi = + new StatisticsSewerageAggregateSummaryApiService(); diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts index e56a52858..ca47d1a5e 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -143,6 +143,61 @@ export interface SewerageFinanceSettleSaveResponse { data?: string; } +/** ST00090001 제출 API 요청 */ +export interface SewerageFinanceSettleSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + submitList: SewerageFinanceSettleSaveItem[]; +} + +export interface SewerageFinanceSettleSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** 하수도재정 입력 제출 공통 본문 (`submitList` 제외) */ +export interface SewerageFinanceSubmitRequestBase { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; +} + +export interface SewerageFinanceSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + /** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */ export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const; @@ -193,7 +248,7 @@ export interface SewerageChargeRequestParams { } /** - * ST00090002 저장 API `saveList[]` 한 행. + * ST00090002 저장·제출 API `saveList` / `submitList[]` 한 행. * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). */ export interface SewerageChargeSaveItem { @@ -226,6 +281,13 @@ export interface SewerageChargeSaveResponse { data?: string; } +/** ST00090002 제출 API 요청 (`submitList` 행은 `SewerageChargeSaveItem`과 동일) */ +export interface SewerageChargeSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: SewerageChargeSaveItem[]; +} + +export type SewerageChargeSubmitItem = SewerageChargeSaveItem; + /** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */ export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const; @@ -293,7 +355,7 @@ export interface SewerageUsageFeeRequestParams { } /** - * ST00090003 저장 API `saveList[]` 한 행. + * ST00090003 저장·제출 API `saveList` / `submitList[]` 한 행. * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). */ export interface SewerageUsageFeeSaveItem { @@ -343,6 +405,13 @@ export interface SewerageUsageFeeSaveResponse { data?: string; } +/** ST00090003 제출 API 요청 (`submitList` 행은 `SewerageUsageFeeSaveItem`과 동일) */ +export interface SewerageUsageFeeSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: SewerageUsageFeeSaveItem[]; +} + +export type SewerageUsageFeeSubmitItem = SewerageUsageFeeSaveItem; + /** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */ export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const; @@ -395,8 +464,9 @@ export interface CauserBurdenRequestParams { } /** - * ST00090004 저장 API `saveList[]` 한 행. + * ST00090004 저장·제출 API `saveList` / `submitList[]` 한 행. * `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). + * `trgtByWholClctnEvntcsCnt`는 조회·그리드 전용이며 저장·제출 요청에는 포함하지 않음. */ export interface CauserBurdenSaveItem { admdstCd: string; @@ -429,6 +499,13 @@ export interface CauserBurdenSaveResponse { data?: string; } +/** ST00090004 제출 API 요청 (`submitList` 행은 `CauserBurdenSaveItem`과 동일) */ +export interface CauserBurdenSubmitRequest extends SewerageFinanceSubmitRequestBase { + submitList: CauserBurdenSaveItem[]; +} + +export type CauserBurdenSubmitItem = CauserBurdenSaveItem; + /** 하수도재정 조회 공통 요청 파라미터 */ export interface SewerageFinanceRequestParams { ctpvNm?: string; @@ -445,22 +522,34 @@ export interface StatisticsInputSewerageFinanceService { saveSewerageFinanceSettle( request: SewerageFinanceSettleSaveRequest, ): Promise; + submitSewerageFinanceSettle( + request: SewerageFinanceSettleSubmitRequest, + ): Promise; getSewerageChargeList( params: SewerageChargeRequestParams, ): Promise; saveSewerageCharge( request: SewerageChargeSaveRequest, ): Promise; + submitSewerageCharge( + request: SewerageChargeSubmitRequest, + ): Promise; getSewerageUsageFeeList( params: SewerageUsageFeeRequestParams, ): Promise; saveSewerageUsageFee( request: SewerageUsageFeeSaveRequest, ): Promise; + submitSewerageUsageFee( + request: SewerageUsageFeeSubmitRequest, + ): Promise; getCauserBurdenList( params: CauserBurdenRequestParams, ): Promise; saveCauserBurden( request: CauserBurdenSaveRequest, ): Promise; + submitCauserBurden( + request: CauserBurdenSubmitRequest, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts new file mode 100644 index 000000000..f331aa558 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts @@ -0,0 +1,280 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 하수도 서비스 수준 — 민원(ST00130001) `ComplaintInpColumn.tabKey` */ +export const SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD = "ST00130001" as const; + +/** 검증 결과 한 건 */ +export interface SewerageServiceLevelVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewerageServiceLevelVerificationResult = Record< + string, + SewerageServiceLevelVerificationResultItem[] +>; + +/** ST00130001 조회 `data.commonInfo` */ +export interface SewerageServiceLevelComplaintCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +/** + * ST00130001 조회 `data.complaintList[]` 한 행. + * `ComplaintInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageServiceLevelComplaintItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + swtrmtPpltnCnt: number; + whol: number; + dschgWqCnt: number; + odorCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + swagePplCnt: number; + sewerCrgAmt: number; + etcCnt: number; + crtrPpltnCvlcptEvntcsCnt: number; + rmrk: string | null; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +export interface SewerageServiceLevelComplaintResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageServiceLevelComplaintCommonInfo; + complaintList: SewerageServiceLevelComplaintItem[]; +} + +export type SewerageServiceLevelComplaintApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00130001 조회 요청 파라미터 */ +export interface SewerageServiceLevelComplaintRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** 하수도 서비스 수준 — 주민친화시설(ST00130002) `ResidentFriendlyFactInpColumn.tabKey` */ +export const SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD = "ST00130002" as const; + +/** ST00130002 조회 `data.commonInfo` */ +export interface SewerageServiceLevelResidentFriendlyFactCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +/** + * ST00130002 조회 `data.residentFriendlyFactList[]` 한 행. + * `ResidentFriendlyFactInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewerageServiceLevelResidentFriendlyFactItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: number; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltCpct: number; + tmplntArea: number; + whol: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewerageServiceLevelVerificationResult; +} + +export interface SewerageServiceLevelResidentFriendlyFactResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewerageServiceLevelResidentFriendlyFactCommonInfo; + residentFriendlyFactList: SewerageServiceLevelResidentFriendlyFactItem[]; +} + +export type SewerageServiceLevelResidentFriendlyFactApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00130002 조회 요청 파라미터 */ +export interface SewerageServiceLevelResidentFriendlyFactRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** ST00130001 저장 `saveList[]` 한 행 */ +export interface SewerageServiceLevelComplaintSaveItem { + admdstCd: string; + admdstNm: string; + swtrmtPpltnCnt: number; + dschgWqCnt: number; + odorCnt: number; + ldsCnt: number; + plltnAcdntCnt: number; + swagePplCnt: number; + sewerCrgAmt: number; + etcCnt: number; + rmrk: string | null; + whol: number; + crtrPpltnCvlcptEvntcsCnt: number; +} + +/** ST00130001 저장 API 요청 */ +export interface SewerageServiceLevelComplaintSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + saveList: SewerageServiceLevelComplaintSaveItem[]; +} + +export interface SewerageServiceLevelComplaintSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00130002 저장 `saveList[]` 한 행 */ +export interface SewerageServiceLevelResidentFriendlyFactSaveItem { + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltSn: string; + tmplntArea: number; + brkFcltArea: number; + childFrshpSpacArea: number; + bdySportsFcltArea: number; + sportsFcltArea: number; + pkltArea: number; + eduMultiFcltArea: number; + wtlndMakeArea: number; + etcCnt: number; + rmrk: string | null; +} + +/** ST00130002 저장 API 요청 */ +export interface SewerageServiceLevelResidentFriendlyFactSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable: boolean; + saveList: SewerageServiceLevelResidentFriendlyFactSaveItem[]; +} + +export interface SewerageServiceLevelResidentFriendlyFactSaveResponse { + status: string; + message?: string; + data?: string; +} + +/** ST00130001 제출 `submitList[]` 한 행 */ +export type SewerageServiceLevelComplaintSubmitItem = SewerageServiceLevelComplaintSaveItem; + +/** ST00130001 제출 API 요청 */ +export interface SewerageServiceLevelComplaintSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + submitList: SewerageServiceLevelComplaintSubmitItem[]; +} + +/** ST00130002 제출 `submitList[]` 한 행 */ +export type SewerageServiceLevelResidentFriendlyFactSubmitItem = + SewerageServiceLevelResidentFriendlyFactSaveItem; + +/** ST00130002 제출 API 요청 */ +export interface SewerageServiceLevelResidentFriendlyFactSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + noApplicable: boolean; + submitList: SewerageServiceLevelResidentFriendlyFactSubmitItem[]; +} + +export interface SewerageServiceLevelSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputSewerageServiceLevelService { + /** 민원 조회 — ST00130001 */ + getComplaintList( + params: SewerageServiceLevelComplaintRequestParams, + ): Promise; + /** 주민친화시설 조회 — ST00130002 */ + getResidentFriendlyFactList( + params: SewerageServiceLevelResidentFriendlyFactRequestParams, + ): Promise; + /** 민원 저장 — ST00130001 */ + saveComplaintList( + request: SewerageServiceLevelComplaintSaveRequest, + ): Promise; + /** 주민친화시설 저장 — ST00130002 */ + saveResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSaveRequest, + ): Promise; + /** 민원 제출 — ST00130001 */ + submitComplaintList( + request: SewerageServiceLevelComplaintSubmitRequest, + ): Promise; + /** 주민친화시설 제출 — ST00130002 */ + submitResidentFriendlyFactList( + request: SewerageServiceLevelResidentFriendlyFactSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts new file mode 100644 index 000000000..e3eb5a16c --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts @@ -0,0 +1,584 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 물 재이용 — 공공하수처리수 재이용(ST00100001) `PublicSewageTreatWaterReuseInpColumn.tabKey` */ +export const WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD = "ST00100001" as const; + +/** 물 재이용 — 중수도 현황(ST00100002) `RecycledWaterStatusInpColumn.tabKey` */ +export const WATER_REUSE_RECYCLED_WATER_STATS_CD = "ST00100002" as const; + +/** 물 재이용 — 빗물이용시설현황(ST00100003) `RainFallUtilFactStatusInpColumn.tabKey` */ +export const WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD = "ST00100003" as const; + +/** 물 재이용 — 하·폐수처리수 재이용시설 설계시공업 등록현황(ST00100004) */ +export const WATER_REUSE_DESIGN_CONST_STATS_CD = "ST00100004" as const; + +/** 물 재이용 — 하·폐수처리수 재이용사업 인가현황(ST00100005) */ +export const WATER_REUSE_BUSINESS_APPR_STATS_CD = "ST00100005" as const; + +/** 검증 결과 한 건 */ +export interface WaterReuseVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type WaterReuseVerificationResult = Record< + string, + WaterReuseVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface WaterReuseCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: WaterReuseVerificationResult; +} + +/** 물 재이용 조회 요청 파라미터 */ +export interface WaterReuseRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00100001 조회 `data.publicSewageTreatWaterReuseList[]` 한 행. + * `PublicSewageTreatWaterReuseInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicSewageTreatWaterReuseItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + sncnYmd: string; + prcsFcltCpct: number; + wholPrcsQnt: number; + lglYn: string; + instlYn: string; + oprtngStrtYmd: string; + instlAprvYmd: string; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + wholUseQnt: number; + ithlWshwtrQnt: number; + ithlClnQty: number; + ithlFrzgQty: number; + ithlLdhcUswtrQnt: number; + ithlDltnUswtrQnt: number; + ithlRuswtrQnt: number; + ithlEtcQnt: number; + ithlUnsdQnt: number; + otcntrUswtrWhol: number; + otcntrClnRstrmQnt: number; + otcntrWshwtrQnt: number; + otcntrLdhcUswtrQnt: number; + otcntrHdrplUswtrQnt: number; + otcntrRvrKeepQnt: number; + agrcltUswtrPstnQnt: number; + ugwtrChargQnt: number; + mdsrUswtrQnt: number; + rtlztnRt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface PublicSewageTreatWaterReuseResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + publicSewageTreatWaterReuseList: PublicSewageTreatWaterReuseItem[]; +} + +export type PublicSewageTreatWaterReuseApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100001 저장 — `saveList` 항목 */ +export interface PublicSewageTreatWaterReuseSaveItem { + admdstCd: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + wholPrcsQnt: number; + lglYn: string; + instlYn: string; + oprtngStrtYmd: string; + instlAprvYmd: string; + rprcsFcltCpct: number; + mtcnsNm: string; + ruswtPplLen: number; + ruswtPumpLen: number; + rmrk: string | null; + rtlztnRt: number; +} + +/** ST00100001 저장 API 요청 */ +export interface PublicSewageTreatWaterReuseSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: PublicSewageTreatWaterReuseSaveItem[]; +} + +export type PublicSewageTreatWaterReuseSaveResponse = ApiResponse & { + message?: string; +}; + +export type PublicSewageTreatWaterReuseSubmitItem = PublicSewageTreatWaterReuseSaveItem; + +/** ST00100001 제출 API 요청 (`submitList` 행은 저장과 동일) */ +export interface PublicSewageTreatWaterReuseSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: PublicSewageTreatWaterReuseSubmitItem[]; +} + +export interface PublicSewageTreatWaterReuseSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100002 조회 `data.recycledWaterList[]` 한 행. + * `RecycledWaterStatusInpColumn` fieldName과 동일. + */ +export interface RecycledWaterStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + bldgSeCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + archWholArea: number; + prcsCpct: number; + wtrUseQnt: number; + ruswtrUtztnQnt: number; + sbstCntUtztnQnt: number; + mnUsgCn: string; + ruswtrOperAmt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface RecycledWaterStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + recycledWaterList: RecycledWaterStatusItem[]; +} + +export type RecycledWaterStatusApiResponse = ApiResponse & { + message: string; +}; + +/** ST00100002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type RecycledWaterStatusSaveItem = Omit< + RecycledWaterStatusItem, + "sggNm" | "verificationResult" +>; + +/** ST00100002 저장 API 요청 */ +export interface RecycledWaterStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RecycledWaterStatusSaveItem[]; + deleteList: RecycledWaterStatusSaveItem[]; +} + +export type RecycledWaterStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type RecycledWaterStatusSubmitItem = RecycledWaterStatusSaveItem; + +/** ST00100002 제출 API 요청 */ +export interface RecycledWaterStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: RecycledWaterStatusSubmitItem[]; + deleteList: RecycledWaterStatusSubmitItem[]; +} + +export interface RecycledWaterStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100003 조회 `data.rainFallUtilFactStatusList[]` 한 행. + * `RainFallUtilFactStatusInpColumn` fieldName과 동일. + */ +export interface RainFallUtilFactStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + bdstTypCn: string; + fcltPstnCn: string; + lglTrgtYn: string; + cmptYmd: string; + instlAmt: number; + rcptMynCn: string; + filtrPrcsFcltYnExpln: string; + impdCpct: number; + anlUseQnt: number; + exlnUtlzUsgExpln: string; + anlOperAmt: number; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface RainFallUtilFactStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + rainFallUtilFactStatusList: RainFallUtilFactStatusItem[]; +} + +export type RainFallUtilFactStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100003 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type RainFallUtilFactStatusSaveItem = Omit< + RainFallUtilFactStatusItem, + "sggNm" | "verificationResult" +>; + +/** ST00100003 저장 API 요청 */ +export interface RainFallUtilFactStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: RainFallUtilFactStatusSaveItem[]; + deleteList: RainFallUtilFactStatusSaveItem[]; +} + +export type RainFallUtilFactStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type RainFallUtilFactStatusSubmitItem = RainFallUtilFactStatusSaveItem; + +/** ST00100003 제출 API 요청 */ +export interface RainFallUtilFactStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: RainFallUtilFactStatusSubmitItem[]; + deleteList: RainFallUtilFactStatusSubmitItem[]; +} + +export interface RainFallUtilFactStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100004 조회 `data.treatWaterReuseDesignConstStatusList[]` 한 행. + * `TreatWaterReuseDesignConstStatusInpColumn` fieldName과 동일. + */ +export interface TreatWaterReuseDesignConstStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bplcSn: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface TreatWaterReuseDesignConstStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + treatWaterReuseDesignConstStatusList: TreatWaterReuseDesignConstStatusItem[]; +} + +export type TreatWaterReuseDesignConstStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100004 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type TreatWaterReuseDesignConstStatusSaveItem = Omit< + TreatWaterReuseDesignConstStatusItem, + "sggNm" | "verificationResult" +>; + +/** ST00100004 저장 API 요청 */ +export interface TreatWaterReuseDesignConstStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: TreatWaterReuseDesignConstStatusSaveItem[]; + deleteList: TreatWaterReuseDesignConstStatusSaveItem[]; +} + +export type TreatWaterReuseDesignConstStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type TreatWaterReuseDesignConstStatusSubmitItem = + TreatWaterReuseDesignConstStatusSaveItem; + +/** ST00100004 제출 API 요청 */ +export interface TreatWaterReuseDesignConstStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: TreatWaterReuseDesignConstStatusSubmitItem[]; + deleteList: TreatWaterReuseDesignConstStatusSubmitItem[]; +} + +export interface TreatWaterReuseDesignConstStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00100005 조회 `data.treatWaterReuseBusinessApprStatusList[]` 한 행. + * `TreatWaterReuseBusinessApprStatusInpColumn` fieldName과 동일. + */ +export interface TreatWaterReuseBusinessApprStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bplcSn: string; + bplcNm: string; + rprsvNm: string; + bplcPstnCn: string; + coRegYmd: string; + rmrk: string | null; + verificationResult?: WaterReuseVerificationResult; +} + +export interface TreatWaterReuseBusinessApprStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: WaterReuseCommonInfo; + treatWaterReuseBusinessApprStatusList: TreatWaterReuseBusinessApprStatusItem[]; +} + +export type TreatWaterReuseBusinessApprStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00100005 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ +export type TreatWaterReuseBusinessApprStatusSaveItem = Omit< + TreatWaterReuseBusinessApprStatusItem, + "sggNm" | "verificationResult" +>; + +/** ST00100005 저장 API 요청 */ +export interface TreatWaterReuseBusinessApprStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: TreatWaterReuseBusinessApprStatusSaveItem[]; + deleteList: TreatWaterReuseBusinessApprStatusSaveItem[]; +} + +export type TreatWaterReuseBusinessApprStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type TreatWaterReuseBusinessApprStatusSubmitItem = + TreatWaterReuseBusinessApprStatusSaveItem; + +/** ST00100005 제출 API 요청 */ +export interface TreatWaterReuseBusinessApprStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: TreatWaterReuseBusinessApprStatusSubmitItem[]; + deleteList: TreatWaterReuseBusinessApprStatusSubmitItem[]; +} + +export interface TreatWaterReuseBusinessApprStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputWaterReuseService { + /** 공공하수처리수 재이용 조회 — ST00100001 */ + getPublicSewageTreatWaterReuseList( + params: WaterReuseRequestParams, + ): Promise; + /** 공공하수처리수 재이용 저장 — ST00100001 */ + savePublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSaveRequest, + ): Promise; + /** 공공하수처리수 재이용 제출 — ST00100001 */ + submitPublicSewageTreatWaterReuse( + request: PublicSewageTreatWaterReuseSubmitRequest, + ): Promise; + /** 중수도 현황 조회 — ST00100002 */ + getRecycledWaterStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 중수도 현황 저장 — ST00100002 */ + saveRecycledWaterStatus( + request: RecycledWaterStatusSaveRequest, + ): Promise; + /** 중수도 현황 제출 — ST00100002 */ + submitRecycledWaterStatus( + request: RecycledWaterStatusSubmitRequest, + ): Promise; + /** 빗물이용시설현황 조회 — ST00100003 */ + getRainFallUtilFactStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 빗물이용시설현황 저장 — ST00100003 */ + saveRainFallUtilFactStatus( + request: RainFallUtilFactStatusSaveRequest, + ): Promise; + /** 빗물이용시설현황 제출 — ST00100003 */ + submitRainFallUtilFactStatus( + request: RainFallUtilFactStatusSubmitRequest, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 조회 — ST00100004 */ + getTreatWaterReuseDesignConstStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 저장 — ST00100004 */ + saveTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSaveRequest, + ): Promise; + /** 하·폐수처리수 재이용시설 설계시공업 등록현황 제출 — ST00100004 */ + submitTreatWaterReuseDesignConstStatus( + request: TreatWaterReuseDesignConstStatusSubmitRequest, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 조회 — ST00100005 */ + getTreatWaterReuseBusinessApprStatusList( + params: WaterReuseRequestParams, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 저장 — ST00100005 */ + saveTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSaveRequest, + ): Promise; + /** 하·폐수처리수 재이용사업 인가현황 제출 — ST00100005 */ + submitTreatWaterReuseBusinessApprStatus( + request: TreatWaterReuseBusinessApprStatusSubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts new file mode 100644 index 000000000..798506ee1 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewerage-aggregate-summary-interface.ts @@ -0,0 +1,38 @@ +/** 하수도 통계 총괄표 — 통계항목(탭) 목록 API `data[]` 한 행 */ +export interface SewerageAggregateSummaryTabItem { + statsCd: string; + tabNm: string; +} + +export interface SewerageAggregateSummaryTabListResponse { + status: string; + message: string; + data: SewerageAggregateSummaryTabItem[]; +} + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API `Content-Type` */ +export const SEWERAGE_AGGREGATE_SUMMARY_EXCEL_MIME = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" as const; + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API 응답 */ +export interface SewerageAggregateSummaryDownloadResult { + blob: Blob; + fileName?: string; +} + +/** 하수도 통계 총괄표 — 엑셀 내려받기 API 요청 */ +export interface SewerageAggregateSummaryDownloadRequest { + statsCdList: string[]; + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + summaryYearCnt: number; +} + +export interface StatisticsSewerageAggregateSummaryService { + getAggregateSummaryTabList(): Promise; + downloadAggregateSummary( + request: SewerageAggregateSummaryDownloadRequest, + ): Promise; +} From 162e18142c4c784f6779a014958647a75fbc09b6 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 25 Jun 2026 18:02:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[st]=20-=20=EC=9E=85=EB=A0=A5=EB=8B=A8=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/module/CompactDataGrid.tsx | 49 +- apps/st/src/components/module/DataGrid.tsx | 52 +- apps/st/src/components/module/TabDataGrid.tsx | 34 +- .../input/PrivateSewageFacilityInp.tsx | 6 +- .../statistics/input/PumpingStationInp.tsx | 24 +- .../input/SewageExcretaRelatedInp.tsx | 2079 +++++++++++++++- .../input/SewageExcretaSlurgeInp.tsx | 2169 ++++++++++++++++- .../input/SewerageServiceLevelInp.tsx | 39 +- .../statistics/input/WaterReuseInp.tsx | 12 - apps/st/src/components/type/DataGrid.type.ts | 24 +- .../ExcretaRelatedIndustryInpColumn.ts | 85 +- ...vateSewageTreatRelatedIndustryInpColumn.ts | 69 +- ...blicInstitutionExcretaIndustryInpColumn.ts | 123 +- ...PublicInstitutionSewerIndustryInpColumn.ts | 125 +- .../DigestorOperatingStatusInpColumn.ts | 163 +- .../ExcretaSlurgeProcessingStatusInpColumn.ts | 125 +- ...ewageSlurgeProcessingFacStatusInpColumn.ts | 73 +- .../SlurgeProcessingFacStatusInpColumn.ts | 367 ++- .../PublicSewageTreatWaterReuseInpColumn.ts | 102 +- .../RainFallUtilFactStatusInpColumn.ts | 5 +- .../RecycledWaterStatusInpColumn.ts | 5 +- ...atWaterReuseBusinessApprStatusInpColumn.ts | 5 +- ...eatWaterReuseDesignConstStatusInpColumn.ts | 5 +- .../util/grid/computedFieldCallbacks.ts | 25 + ...cs-input-sewage-excreta-related-service.ts | 163 ++ ...ics-input-sewage-excreta-slurge-service.ts | 182 ++ ...-input-sewage-excreta-related-interface.ts | 328 +++ ...s-input-sewage-excreta-slurge-interface.ts | 539 ++++ 28 files changed, 6395 insertions(+), 582 deletions(-) create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts create mode 100644 apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts create mode 100644 apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index 985e98ea6..a8605c52e 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -3,9 +3,27 @@ import { ValueType } from "realgrid" import RealGrid, { GridFitStyle } from "realgrid" import { RealGridReact, RGDataColumn } from "realgrid-react" -import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback } from "../type/DataGrid.type" +import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback, CellStyleCallbackResult, CellStyleCallbackParams } from "../type/DataGrid.type" import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [extra, baseStyleName].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} + // RealGrid 타입 확장 declare module "realgrid" { interface GridView { @@ -411,18 +429,33 @@ const CompactDataGrid = ({ columns.forEach((column) => { const baseStyleName = column.styleName; const gridCol = currentGridView.columnByName(column.name) as { - styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string; + styleCallback?: ( + g: unknown, + dataCell: { + itemIndex?: number; + index?: { itemIndex?: number }; + dataField?: string; + fieldName?: string; + }, + ) => CellStyleCallbackResult; } | null; if (gridCol) { - gridCol.styleCallback = (_g, dataCell) => { + gridCol.styleCallback = (grid, dataCell) => { const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; - const fieldName = (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? ''; + const fieldName = + (dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName) ?? ""; const data = rowDataRef.current; - if (itemIndex < 0 || itemIndex >= data.length) return baseStyleName ?? ''; + if (itemIndex < 0 || itemIndex >= data.length) { + return mergeCellStyleCallbackResult(baseStyleName, undefined); + } const row = data[itemIndex] as Record; - const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row }); - if (!extra) return baseStyleName ?? ''; - return baseStyleName ? `${extra} ${baseStyleName}` : extra; + const extra = cellStyleCallbackRef.current?.({ + rowIndex: itemIndex, + fieldName, + row, + grid: grid as CellStyleCallbackParams["grid"], + }); + return mergeCellStyleCallbackResult(baseStyleName, extra); }; } }); diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 9cc364c04..957318ab9 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -11,6 +11,8 @@ import { Button } from "@common/components"; import type { CellStyleCallback, + CellStyleCallbackParams, + CellStyleCallbackResult, CellTooltipCallback, ColumnLayoutItem, DataGridProps, @@ -28,6 +30,24 @@ import { } from "@/components/util/gridExcelExport"; import { useTabStore } from "@/store/tab-store"; +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [extra, baseStyleName].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} + // RealGrid 타입 확장 declare module "realgrid" { interface GridView { @@ -570,6 +590,14 @@ function buildFieldsFromColumns( "denominatorQuotient" in col.computed && col.computed.numeratorQuotient.length === 2 && col.computed.denominatorQuotient.length === 2; + const hasQuotientTimesMultiplierPercent = + col.computed.type === "quotientTimesMultiplierPercent" && + "numeratorField" in col.computed && + "denominatorField" in col.computed && + "multiplierField" in col.computed && + Boolean(col.computed.numeratorField) && + Boolean(col.computed.denominatorField) && + Boolean(col.computed.multiplierField); if ( hasFields || hasNumeratorDenominator || @@ -581,7 +609,8 @@ function buildFieldsFromColumns( hasPumpGrossPpqty || hasBodPrcsRt || hasSumPerTenThousand || - hasQuotientRatioPercent + hasQuotientRatioPercent || + hasQuotientTimesMultiplierPercent ) { const computedCallback = getComputedValueCallback( col.computed, @@ -1922,7 +1951,7 @@ const DataGrid = ({ dataField?: string; fieldName?: string; }, - ) => string; + ) => CellStyleCallbackResult; renderer?: { showTooltip?: boolean }; } | null; if (gridCol) { @@ -1961,7 +1990,7 @@ const DataGrid = ({ { showTooltip: true }, ); } - gridCol.styleCallback = (_g, dataCell) => { + gridCol.styleCallback = (grid, dataCell) => { const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? @@ -1974,8 +2003,12 @@ const DataGrid = ({ if ( itemIndex < 0 || itemIndex >= data.length - ) - return baseStyleName ?? ""; + ) { + return mergeCellStyleCallbackResult( + baseStyleName, + undefined, + ); + } const row = data[itemIndex] as Record< string, string | number @@ -1985,11 +2018,12 @@ const DataGrid = ({ rowIndex: itemIndex, fieldName, row, + grid: grid as CellStyleCallbackParams["grid"], }); - if (!extra) return baseStyleName ?? ""; - return baseStyleName - ? `${extra} ${baseStyleName}` - : extra; + return mergeCellStyleCallbackResult( + baseStyleName, + extra, + ); }; } }); diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index a1fb327a8..6b7eacc87 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -10,9 +10,27 @@ import { useAlertStore } from "@/shared/lib"; import { Tabs, TabsContent,TabsList, TabsTrigger } from "@common/components"; import { Button } from "@common/components"; -import type { CellStyleCallback,ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type" +import type { CellStyleCallback, CellStyleCallbackParams, CellStyleCallbackResult, ColumnConfig, ColumnInfo, ColumnLayoutItem, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo, TabDataGridProps } from "../type/DataGrid.type" import GridTopActionButton from "./GridTopActionButton"; + +function mergeCellStyleCallbackResult( + baseStyleName: string | undefined, + extra: CellStyleCallbackResult | undefined, +): CellStyleCallbackResult { + if (!extra) { + return baseStyleName ? { styleName: baseStyleName } : {}; + } + if (typeof extra === "string") { + const styleName = [extra, baseStyleName].filter(Boolean).join(" "); + return styleName ? { styleName } : {}; + } + const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); + return { + ...extra, + ...(styleName ? { styleName } : {}), + }; +} import SplitDataGrid from "./SplitDataGrid"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" @@ -73,10 +91,12 @@ function safeGridViewRefresh(gv: { refresh?: () => void } | null | undefined) { function safeColumnByName( gv: { columnByName?: (name: string) => unknown } | null | undefined, name: string, -): { styleCallback?: (g: unknown, dataCell: Record) => string } | null { +): { styleCallback?: (g: unknown, dataCell: Record) => CellStyleCallbackResult } | null { if (!gv?.columnByName) return null; try { - const col = gv.columnByName(name) as { styleCallback?: (g: unknown, dataCell: Record) => string } | null; + const col = gv.columnByName(name) as { + styleCallback?: (g: unknown, dataCell: Record) => CellStyleCallbackResult; + } | null; return col ?? null; } catch { return null; @@ -296,7 +316,7 @@ const TabDataGrid = ({ const gridCol = safeColumnByName(gv, column.name); if (!gridCol) continue; const baseStyleName = column.styleName; - gridCol.styleCallback = (_g, dataCell) => { + gridCol.styleCallback = (grid, dataCell) => { const dc = dataCell as { itemIndex?: number; index?: { itemIndex?: number }; @@ -307,16 +327,16 @@ const TabDataGrid = ({ const fieldName = dc?.dataField ?? dc?.fieldName ?? column.fieldName; const data = rowDataMapRef.current.get(tabKey) || []; if (itemIndex < 0 || itemIndex >= data.length) { - return baseStyleName ?? ""; + return mergeCellStyleCallbackResult(baseStyleName, undefined); } const row = data[itemIndex] as Record; const extra = cellStyleCallbackRef.current?.({ rowIndex: itemIndex, fieldName, row, + grid: grid as CellStyleCallbackParams["grid"], }); - if (!extra) return baseStyleName ?? ""; - return baseStyleName ? `${extra} ${baseStyleName}` : extra; + return mergeCellStyleCallbackResult(baseStyleName, extra); }; attachedCount += 1; } diff --git a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx index de789139c..946e17aa3 100644 --- a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx @@ -1140,8 +1140,8 @@ const PrivateSewageFacilityInp = () => { onValuesChange={setSearchValues} /> {showActionToolbarCard && ( -
-
+
+
@@ -1150,7 +1150,7 @@ const PrivateSewageFacilityInp = () => { void lastYearDataFunc()} /> - + )} {activeTabStatus?.laeSbmsnSttsCd === "2" && ( diff --git a/apps/st/src/components/statistics/input/PumpingStationInp.tsx b/apps/st/src/components/statistics/input/PumpingStationInp.tsx index 0a4be8402..421405505 100644 --- a/apps/st/src/components/statistics/input/PumpingStationInp.tsx +++ b/apps/st/src/components/statistics/input/PumpingStationInp.tsx @@ -967,7 +967,7 @@ const PumpingStationInp = () => { onValuesChange={setSearchValues} /> {showActionToolbarCard && ( -
+
{ void getLastYearDataFunc()} /> - + )} {statusInfo?.laeSbmsnSttsCd === "2" && ( )} {showSaveInToolbar && } -
-
+
+
)} {showArticleToolbarCard && ( -
+
{showNotApplicable && ( { setIsCheck={setIsNotApplicable} /> )} - - -
-
+ + +
+
)} {isLoading && (
@@ -1008,14 +1008,14 @@ const PumpingStationInp = () => {
)} - } columnInfo={columnInfo} rowStartNum={PumpingStationInpColumn.rowStartNum} fixedColumns={PumpingStationInpColumn.fixedColumns} - rowData={rowData} - setRowData={setRowData} + rowData={rowData} + setRowData={setRowData} getGridDataRef={gridGetRowsRef} showHeaderCheckbox statusInfo={statusInfo} diff --git a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx index b4470e438..f9f722b4f 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx @@ -1,81 +1,2062 @@ -import { useState } from "react"; +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 SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; import AddArticleButton from "@/components/button/AddArticleButton"; +import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton"; - -import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn" -import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn" +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 { 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 { 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 { 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([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 해당사항 없음 — ST00120001·ST00120002 */ +const NOT_APPLICABLE_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 항목 추가 행 — 시군구(sggNm) 드롭다운 선택 */ +const SGG_NM_DROPDOWN_TAB_KEYS = new Set([ + PublicInstitutionSewerIndustryInpColumn.tabKey, + PublicInstitutionExcretaIndustryInpColumn.tabKey, +]); + +/** 저장·제출 API 연동 — ST00120001~ST00120004 */ +const SAVE_SUPPORTED_TAB_KEYS = new Set([ + 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([ + 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[]>, + 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 { + const row: Record = { 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): boolean { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; +} + +type ArticleOgnzSnSet = Set; + +function extractOgnzSnSet(rows: Record[]): ArticleOgnzSnSet { + const set = new Set(); + for (const row of rows) { + const ognzSn = String(row.ognzSn ?? "").trim(); + if (ognzSn) set.add(ognzSn); + } + return set; +} + +function isNewArticleRow(row: Record): boolean { + const v = row.__isNewArticle; + return v === 1 || v === "1"; +} + +function setArticleOgnzSnSetForTab( + storeRef: { current: Map }, + tabKey: string, + rows: Record[], +) { + storeRef.current.set(tabKey, extractOgnzSnSet(rows)); +} + +function clearArticleOgnzSnTracking( + persistedRef: { current: Map }, + preYearRef: { current: Map }, +) { + persistedRef.current = new Map(); + preYearRef.current = new Map(); +} + +/** ST00120001·ST00120002 — deleteList 대상 판별 (`ognzSn` + 전년도·신규 행 구분) */ +function isPersistedPublicInstitutionArticleRow( + tabKey: string, + row: Record, + persistedRef: { current: Map }, + preYearRef: { current: Map }, +): 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[], + fromPreYear: boolean, + persistedRef: { current: Map }, + preYearRef: { current: Map }, + 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[] { + 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, + 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[], + mapRows: Record[], + hiddenFields: string[] = [], +): Record[] { + return gridRows.map((gridRow, index) => { + const mapRow = mapRows[index] ?? {}; + const merged: Record = { ...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[], + mapRows: Record[], +): Record[] { + 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, + 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, +): 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, +): 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, +): ExcretaRelatedIndustrySaveItem | PrivateSewageTreatRelatedIndustrySaveItem { + if (tabKey === ExcretaRelatedIndustryInpColumn.tabKey) { + return buildExcretaRelatedIndustrySaveItem(row); + } + return buildPrivateSewageTreatRelatedIndustrySaveItem(row); +} + +function resolveStatsCdForTab( + tabKey: string, + tabStatsCdMap: Map, +): 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 & { + 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( + 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((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + 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 = () => { - - // tab list - const SewageExcretaRelatedInpTabList = [ - PublicInstitutionSewerIndustryInpColumn, - PublicInstitutionExcretaIndustryInpColumn, - ExcretaRelatedIndustryInpColumn, - PrivateSewageTreatRelatedIndustryInpColumn - ]; + 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 submitExceptionFunc = () => { - // 예외 제출 로직 + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState< + Map[]> + >(new Map()); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState( + null, + ); + const [hasSearched, setHasSearched] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [loadedTabKeys, setLoadedTabKeys] = useState>(new Set()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + const [sggOptions, setSggOptions] = useState([]); + 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(null); + const activeTabRef = useRef(activeTab); + const loadedTabKeysRef = useRef>(new Set()); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + /** ST00120001·ST00120002 — 당해 조회 API로 로드된 ognzSn (deleteList 대상) */ + const persistedArticleOgnzSnByTabRef = useRef>(new Map()); + /** ST00120001·ST00120002 — 전년도(PRE) 조회로 로드된 ognzSn */ + const preYearArticleOgnzSnByTabRef = useRef>(new Map()); + const sggOptionsSidoRef = useRef(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 = {}; + 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 => { + 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 lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + const markTabLoaded = (tabKey: string) => { + loadedTabKeysRef.current.add(tabKey); + setLoadedTabKeys(new Set(loadedTabKeysRef.current)); + }; + + const getTabGridRows = (tabKey: string): Record[] => { + 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[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const loadActiveTabData = async ( + tabKey: string, + param: SewageExcretaRelatedRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { mode?: string }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + 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)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + 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)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + 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)); + 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)); + 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()); + setIsNotApplicableByTab(new Map()); + setDeleteListByTab(new Map()); + clearArticleOgnzSnTracking( + persistedArticleOgnzSnByTabRef, + preYearArticleOgnzSnByTabRef, + ); + setSggOptions([]); + sggOptionsSidoRef.current = null; + setVerificationHistoryOpen(false); + }; + + const handleSearch = async (values: SearchFormValues) => { + 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()); + 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; + } + + const ok = await loadActiveTabData( + 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, + false, + 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, + false, + 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, false, false); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - // 항목 추가 로직 + 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; + } + + const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + if (noApplicable) { + 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; + }); + }); + + 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 = () => { - console.log('save'); - // 항목 제외 로직 + 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 [rowDataMap, setRowDataMap] = useState[]>>(new Map()); + const columnInfoByTab = useMemo(() => { + const map = new Map(); + 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( + ({ 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> = 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, + }; + })(); return ( <> - -
-
- - - - -
-
-
-
- - -
-
- + + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {showArticleEditButtons && ( + <> + void addArticleFunc()} /> + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewageExcretaRelatedInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {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 ( + + { + 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 + } + isExcelExport + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={activeTab === tabKey ? getGridRef : undefined} + cellStyleCallback={resolveGridCellStyleCallback(tabKey)} + cellTooltipCallback={verificationCellTooltip} + /> + + ); + })} + +
+
+ { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> + + + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + true, + false, + ); + }} + /> ); }; -export default SewageExcretaRelatedInp; \ No newline at end of file +export default SewageExcretaRelatedInp; diff --git a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx index 6df8ce665..902fea894 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx @@ -1,82 +1,2155 @@ -import { useState } from "react"; +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 SearchForm from "@/components/module/SearchForm"; -import TabDataGrid from "@/components/module/TabDataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; 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 { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn"; -import { DigestorOperatingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn"; -import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn"; +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 { 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 SubmitDailog from "./SubmitDailog"; +import type { + CellEditedCallback, + CellStyleCallback, + CellStyleCallbackParams, + CellStyleCallbackResult, + CellTooltipCallbackParams, + ColumnConfig, + ColumnInfo, +} from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { + DigestorOperatingStatusInpColumn, + DIGESTOR_OPERATING_STATUS_EN_YN_FIELD, + isDigestorOperatingRowDisabled, +} from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn"; import { ExcretaSlurgeProcessingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn"; +import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn"; +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 { 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"; +import { + SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD, + SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD, + SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD, + SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface"; +import type { + SewageExcretaSlurgeRequestParams, + DigestorOperatingStatusSaveItem, + ExcretaSlurgeProcessingStatusSaveItem, + SewageExcretaSlurgeVerificationResult, + SewageSlurgeProcessingFacStatusSaveItem, + SlurgeProcessingFacStatusSaveItem, +} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-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 { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { useAdminStore } from "@/store/admin-store"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +type GridRowWithVerification = Record & { + verificationResult?: SewageExcretaSlurgeVerificationResult; +}; + +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 buildSewageExcretaSlurgeSearchParam( + values: SearchFormValues, + overrides?: { mode?: string }, +): SewageExcretaSlurgeRequestParams { + return { + statsYr: values.year, + sewerBzmnCd: values.bsnmCd, + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + fcltNm: values.fctName?.trim() || undefined, + ...(overrides?.mode ? { mode: overrides.mode } : {}), + }; +} + +function mapListToGridRows( + 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 SewageExcretaSlurgeInpTabList = [ + SlurgeProcessingFacStatusInpColumn, + DigestorOperatingStatusInpColumn, + SewageSlurgeProcessingFacStatusInpColumn, + ExcretaSlurgeProcessingStatusInpColumn, +] as const; + +const firstTabKey = SlurgeProcessingFacStatusInpColumn.tabKey; + +/** 항목 추가·제외 대상 탭 — ST00110003 (ST00110001·ST00110002·ST00110004 제외) */ +const ARTICLE_EDIT_TAB_KEYS = new Set([ + SewageSlurgeProcessingFacStatusInpColumn.tabKey, +]); + +/** ST00110003 — 시군구(sggNm) 드롭다운 선택 */ +const SGG_NM_DROPDOWN_TAB_KEYS = new Set([ + SewageSlurgeProcessingFacStatusInpColumn.tabKey, +]); + +const EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG = + "06. 공공하수처리시설 데이터가 없으면 전년도 데이터 조회 및 항목 추가가 불가능 합니다."; + +const EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG = + "07. 분뇨처리시설 데이터가 없으면 전년도 데이터 조회가 불가능 합니다."; + +/** ST00110001~ST00110004 — 저장 API 연동 탭 */ +const SAVE_SUPPORTED_TAB_KEYS = new Set([ + SlurgeProcessingFacStatusInpColumn.tabKey, + DigestorOperatingStatusInpColumn.tabKey, + SewageSlurgeProcessingFacStatusInpColumn.tabKey, + ExcretaSlurgeProcessingStatusInpColumn.tabKey, +]); + +function mergeHiddenFieldsForSubmitTab(tabKey: string): string[] { + if ( + tabKey === SlurgeProcessingFacStatusInpColumn.tabKey || + tabKey === DigestorOperatingStatusInpColumn.tabKey + ) { + return ["swtrmtFcltCd", "fcltSn", "admdstCd", "admdstNm"]; + } + if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + return ["fcltSn", "admdstCd", "admdstNm"]; + } + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + return ["fcltSn", "admdstCd", "admdstNm", "fcltPstnCn"]; + } + return []; +} + +/** ST00110001~ST00110004 — 제출 API 연동 탭 */ +const SUBMIT_SUPPORTED_TAB_KEYS = new Set([ + SlurgeProcessingFacStatusInpColumn.tabKey, + DigestorOperatingStatusInpColumn.tabKey, + SewageSlurgeProcessingFacStatusInpColumn.tabKey, + ExcretaSlurgeProcessingStatusInpColumn.tabKey, +]); + +const ARTICLE_TAB_BY_KEY = new Map( + SewageExcretaSlurgeInpTabList.map((tab) => [tab.tabKey, tab]), +); + +function createEmptyArticleRow( + tabKey: string, + ctpvNm: string, + sggNm = "", +): Record { + const row: Record = { ctpvNm, sggNm }; + 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") continue; + row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : ""; + } + return row; +} + +type ArticleFcltSnSet = Set; + +function extractFcltSnSet(rows: Record[]): ArticleFcltSnSet { + const set = new Set(); + for (const row of rows) { + const fcltSn = String(row.fcltSn ?? "").trim(); + if (fcltSn) set.add(fcltSn); + } + return set; +} + +function setArticleFcltSnSet( + storeRef: { current: ArticleFcltSnSet }, + rows: Record[], +) { + storeRef.current = extractFcltSnSet(rows); +} + +function isNewArticleRow(row: Record): boolean { + const v = row.__isNewArticle; + return v === 1 || v === "1"; +} + +function clearSlurgeFacStatusArticleFcltSnTracking( + persistedRef: { current: ArticleFcltSnSet }, + preYearRef: { current: ArticleFcltSnSet }, +) { + persistedRef.current = new Set(); + preYearRef.current = new Set(); +} + +/** + * ST00110001 — deleteList 대상 판별. + * fcltSn만으로는 전년도(PRE)·항목추가 행과 구분 불가 → __isNewArticle + preYear ref 사용. + */ +function isPersistedSlurgeFacStatusArticleRow( + row: Record, + persistedRef: { current: ArticleFcltSnSet }, + preYearRef: { current: ArticleFcltSnSet }, +): boolean { + if (isNewArticleRow(row)) return false; + const fcltSn = String(row.fcltSn ?? "").trim(); + if (!fcltSn) return false; + if (preYearRef.current.has(fcltSn)) return false; + return persistedRef.current.has(fcltSn); +} + +/** ST00110003 등 — 전년도 조회 시 fcltSn 미제공, fcltSn 유무로 판단 */ +function isPersistedArticleRowByFcltSn(row: Record): boolean { + if (isNewArticleRow(row)) return false; + return String(row.fcltSn ?? "").trim() !== ""; +} + +const toGridColumnInfo = (tab: (typeof SewageExcretaSlurgeInpTabList)[number]): ColumnInfo => ({ + columns: tab.columns ?? [], + columnLayout: tab.columnLayout ?? [], +}); + +function tabRowDataExists( + rowDataMap: Map[]>, + tabKey: string, +): boolean { + const rows = rowDataMap.get(tabKey); + return Array.isArray(rows) && rows.length > 0; +} + +type TabToolbarSnapshot = { + showSaveInToolbar: boolean; + showEnterableToolbarButtons: boolean; + showActionToolbarCard: boolean; +}; + +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 parseSggListFromResponse(res: ApiResponse): 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, + }; +} + +function buildEmptyDataAlertOverrides(tabKey: string): + | { alertIfDigestorEmpty?: boolean; alertIfExcretaSlurgeEmpty?: boolean } + | undefined { + if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + return { alertIfDigestorEmpty: true }; + } + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + return { alertIfExcretaSlurgeEmpty: true }; + } + return undefined; +} + +function mergeGridRowsWithMapRows( + gridRows: Record[], + mapRows: Record[], + hiddenFields: string[] = [], +): Record[] { + return gridRows.map((gridRow, index) => { + const mapRow = mapRows[index] ?? {}; + const merged: Record = { ...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; + }); +} + +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; +} + +function resolveSlurgeFacStatusAdmdst( + row: Record, + 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(); + if (existingCd || existingNm) { + return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + } + return { admdstCd: "", admdstNm: sggNm }; +} + +/** 그리드 행 → ST00110001 저장/삭제 API 항목 */ +function buildSlurgeProcessingFacStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): SlurgeProcessingFacStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + itslfQnt: toGridNumber(row.itslfQnt), + otsdInflwQnt: toGridNumber(row.otsdInflwQnt), + transfTmplntNm: String(row.transfTmplntNm ?? "").trim(), + transfPrcsQnt: toGridNumber(row.transfPrcsQnt), + srpFncRt: toGridNumber(row.srpFncRt), + itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt), + itslfTs1IncnrQnt: toGridNumber(row.itslfTs1IncnrQnt), + itslfTs1AltdQnt: toGridNumber(row.itslfTs1AltdQnt), + itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt), + itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt), + itslfTs2CmpstgQnt: toGridNumber(row.itslfTs2CmpstgQnt), + cmpsPrcsFcltBzentyNm: String(row.cmpsPrcsFcltBzentyNm ?? "").trim(), + incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm), + incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt), + incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt), + incnrPrcsFcltNm: String(row.incnrPrcsFcltNm ?? "").trim(), + stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm), + stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt), + stbznPrcsFcltNm: String(row.stbznPrcsFcltNm ?? "").trim(), + dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm), + dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt), + dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt), + dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt), + dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt), + dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt), + dryPrcsFcltNm: String(row.dryPrcsFcltNm ?? "").trim(), + fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt), + fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt), + fuelPrcsFcltNm: String(row.fuelPrcsFcltNm ?? "").trim(), + otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt), + otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt), + otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt), + otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt), + otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt), + otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt), + otsdCnsgnFcltNm: String(row.otsdCnsgnFcltNm ?? "").trim(), + cparTs1Whol: toGridNumber(row.cparTs1Whol), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110002 저장 API 항목 */ +function buildDigestorOperatingStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): DigestorOperatingStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + const base = { + admdstCd, + admdstNm, + swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + fcltSn: String(row.fcltSn ?? "").trim(), + digstnEnYn: String(row.digstnEnYn ?? "").trim(), + }; + + if (isDigestorOperatingRowDisabled(row)) { + return { + ...base, + digstnEnYn: "N", + digstnCpct: 0, + actlDrvnCpct: 0, + digstnGasInputQnt: 0, + digstnGasInputSldDnst: 0, + digstnTnkOprtngDayCnt: 0, + ocrnGasQnt: 0, + digstnGasUtztnNtslQnt: 0, + digstnGasUtztnEtgtQnt: 0, + digstnGasUtztnItslfQnt: 0, + digstnGasUtztnEtcQnt: 0, + surplsGasQnt: 0, + rmrk: null, + }; + } + + return { + ...base, + digstnCpct: toGridNumber(row.digstnCpct), + actlDrvnCpct: toGridNumber(row.actlDrvnCpct), + digstnGasInputQnt: toGridNumber(row.digstnGasInputQnt), + digstnGasInputSldDnst: toGridNumber(row.digstnGasInputSldDnst), + digstnTnkOprtngDayCnt: toGridNumber(row.digstnTnkOprtngDayCnt), + ocrnGasQnt: toGridNumber(row.ocrnGasQnt), + digstnGasUtztnNtslQnt: toGridNumber(row.digstnGasUtztnNtslQnt), + digstnGasUtztnEtgtQnt: toGridNumber(row.digstnGasUtztnEtgtQnt), + digstnGasUtztnItslfQnt: toGridNumber(row.digstnGasUtztnItslfQnt), + digstnGasUtztnEtcQnt: toGridNumber(row.digstnGasUtztnEtcQnt), + surplsGasQnt: toGridNumber(row.surplsGasQnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110003 저장/삭제 API 항목 */ +function buildSewageSlurgeProcessingFacStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): SewageSlurgeProcessingFacStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + prcsMthExpln: String(row.prcsMthExpln ?? "").trim(), + fcltCpct: toGridNumber(row.fcltCpct), + anlPrcsQnt: toGridNumber(row.anlPrcsQnt), + dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr), + anlOprtngDayCnt: toGridNumber(row.anlOprtngDayCnt), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +/** 그리드 행 → ST00110004 저장 API 항목 */ +function buildExcretaSlurgeProcessingStatusSaveItem( + row: Record, + sggOptions: Sgg[], +): ExcretaSlurgeProcessingStatusSaveItem { + const { admdstCd, admdstNm } = resolveSlurgeFacStatusAdmdst(row, sggOptions); + return { + admdstCd, + admdstNm, + fcltSn: String(row.fcltSn ?? "").trim(), + fcltNm: String(row.fcltNm ?? "").trim(), + fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + rcycQnt: toGridNumber(row.rcycQnt), + olandLfvlm: toGridNumber(row.olandLfvlm), + incnrQnt: toGridNumber(row.incnrQnt), + oceanInputQnt: toGridNumber(row.oceanInputQnt), + fuelQnt: toGridNumber(row.fuelQnt), + etcQnt: toGridNumber(row.etcQnt), + nondspsQnt: toGridNumber(row.nondspsQnt), + swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + rmrk: nullableTrimmedString(row.rmrk), + }; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + 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 검증 완료 대기 시간이 초과되었습니다."); +} // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 const SewageExcretaSlurgeInp = () => { - - // tab list - const SewageExcretaSlurgeInpTabList = [ - SlurgeProcessingFacStatusInpColumn, - DigestorOperatingStatusInpColumn, - SewageSlurgeProcessingFacStatusInpColumn, - ExcretaSlurgeProcessingStatusInpColumn - ]; + const { isAdminInfo } = useAdminStore(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + 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 submitExceptionFunc = () => { - // 예외 제출 로직 + const [isLoading, setIsLoading] = useState(false); + const [activeTab, setActiveTab] = useState(firstTabKey); + const [rowDataMap, setRowDataMap] = useState< + Map[]> + >(new Map()); + const [isNotApplicableByTab, setIsNotApplicableByTab] = useState>( + () => new Map(), + ); + const [searchValues, setSearchValues] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); + const [tabStatusInfoMap, setTabStatusInfoMap] = useState< + Map + >(new Map()); + const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< + Map + >(new Map()); + const [sggOptions, setSggOptions] = useState([]); + const [loadedTabKeys, setLoadedTabKeys] = useState>(() => new Set()); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [showSubmitDialog, setShowSubmitDialog] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [deleteListByTab, setDeleteListByTab] = useState< + Map[]> + >(() => new Map()); + const [columnGuidesByTab, setColumnGuidesByTab] = useState< + Record> + >({}); + + const searchParamRef = useRef(null); + const searchDoneRef = useRef(false); + const periodEnterableRef = useRef(true); + const loadedTabKeysRef = useRef>(new Set()); + const sggOptionsSidoRef = useRef(null); + /** ST00110002 당해 조회 결과 유무(공공하수처리시설 연동) — 전년도 조회 가능 여부 */ + const publicSewageFacilitiesAvailableRef = useRef(false); + /** ST00110004 당해 조회 결과 유무(분뇨처리시설 연동 데이터) — 전년도 조회 가능 여부 */ + const excretaTreatmentFacilitiesAvailableRef = useRef(false); + const activeTabRef = useRef(activeTab); + const getGridDataRef = useRef<(() => Record[]) | null>(null); + const getGridRef = useRef(null); + /** ST00110001 — 당해 연도 조회 API로 로드된 fcltSn (deleteList 대상) */ + const persistedArticleFcltSnRef = useRef(new Set()); + /** ST00110001 — 전년도(PRE) 조회로 로드된 fcltSn */ + const preYearArticleFcltSnRef = useRef(new Set()); + activeTabRef.current = activeTab; + + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SewageExcretaSlurgeInpTabList.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 = {}; + 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 = ( + hasRows: boolean, + statusInfo: SewerageStatusInfo | null | undefined, + ): boolean => + 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 loadSggOptions = useCallback(async (sido: string): Promise => { + 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 markTabLoaded = (tabKey: string) => { + loadedTabKeysRef.current.add(tabKey); + setLoadedTabKeys(new Set(loadedTabKeysRef.current)); }; - const lastYearDataFunc = () => { - // 지난연도 데이터 가져오기 로직 + 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 getTabGridRows = (tabKey: string): Record[] => + tabKey === activeTabRef.current + ? (getGridDataRef.current?.() ?? rowDataMap.get(tabKey) ?? []) + : (rowDataMap.get(tabKey) ?? []); + + 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, + buildEmptyDataAlertOverrides(tabKey), + ); + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const applyRowMap = ( + mergeIntoExistingMap: boolean, + apply: (m: Map[]>) => void, + ) => { + if (mergeIntoExistingMap) { + setRowDataMap((prev) => { + const next = new Map(prev); + apply(next); + return next; + }); + } else { + const next = new Map[]>(); + apply(next); + setRowDataMap(next); + } + }; + + const syncSlurgeFacStatusArticleFcltSnTracking = ( + gridRows: Record[], + fromPreYear: boolean, + ) => { + if (fromPreYear) { + setArticleFcltSnSet(preYearArticleFcltSnRef, gridRows); + return; + } + setArticleFcltSnSet(persistedArticleFcltSnRef, gridRows); + }; + + const isPersistedArticleRowForTab = useCallback( + (tabKey: string, row: Record) => { + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + return isPersistedSlurgeFacStatusArticleRow( + row, + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + } + return isPersistedArticleRowByFcltSn(row); + }, + [], + ); + + const loadActiveTabData = async ( + tabKey: string, + param: SewageExcretaSlurgeRequestParams, + searchContext: SearchFormValues, + mergeIntoExistingMap: boolean, + updateLastSearchedValues: boolean, + overrides?: { + mode?: string; + alertIfDigestorEmpty?: boolean; + alertIfExcretaSlurgeEmpty?: boolean; + }, + ): Promise => { + const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; + + setIsNotApplicableForTab(tabKey, false); + + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getSlurgeProcessingFacStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, slurgeProcessingFacStatusList } = response.data; + const gridRows = mapListToGridRows(slurgeProcessingFacStatusList, commonInfo.ctpvNm); + syncSlurgeFacStatusArticleFcltSnTracking(gridRows, fromPreYear); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SlurgeProcessingFacStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getDigestorOperatingStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, digestorOperatingStatusList } = response.data; + const gridRows = mapListToGridRows(digestorOperatingStatusList, commonInfo.ctpvNm); + if (!fromPreYear) { + publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfDigestorEmpty) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + } + } + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(DigestorOperatingStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getSewageSlurgeProcessingFacStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, sewageSlurgeProcessingFacStatusList } = + response.data; + const gridRows = mapListToGridRows( + sewageSlurgeProcessingFacStatusList, + commonInfo.ctpvNm, + ); + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(SewageSlurgeProcessingFacStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + if (updateLastSearchedValues) setLastSearchedValues(searchContext); + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + gridRows.length > 0, + searchContext, + ); + markTabLoaded(tabKey); + return true; + } + + if (tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey) { + const response = + await statisticsInputSewageExcretaSlurgeApi.getExcretaSlurgeProcessingStatusList( + requestParam, + ); + if (response.status !== "success" || !response.data) { + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } + const { commonInfo, statusInfo, excretaSlurgeProcessingStatusList } = response.data; + const gridRows = mapListToGridRows( + excretaSlurgeProcessingStatusList, + commonInfo.ctpvNm, + ); + if (!fromPreYear) { + excretaTreatmentFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfExcretaSlurgeEmpty) { + alert(EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG); + } + } + applyRowMap(mergeIntoExistingMap, (m) => { + m.set(ExcretaSlurgeProcessingStatusInpColumn.tabKey, gridRows); + }); + setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo)); + setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); + 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()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + setActiveTab(firstTabKey); + activeTabRef.current = firstTabKey; + setLastSearchedValues(null); + setHasSearched(false); + setInputPeriodEnterable(true); + searchParamRef.current = null; + setIsNotApplicableByTab(new Map()); + searchDoneRef.current = false; + periodEnterableRef.current = true; + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + setSggOptions([]); + publicSewageFacilitiesAvailableRef.current = false; + excretaTreatmentFacilitiesAvailableRef.current = false; + setDeleteListByTab(new Map()); + clearSlurgeFacStatusArticleFcltSnTracking( + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + sggOptionsSidoRef.current = null; + }; + + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + 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 periodOk = isInputPeriodCurrent(periods); + periodEnterableRef.current = periodOk; + setInputPeriodEnterable(periodOk); + } catch { + alert("입력 기간 확인 중 오류가 발생했습니다."); + return; + } + + const yearChangedFromLastSearch = + lastSearchedValues != null && + (lastSearchedValues.year ?? "") !== (values.year ?? ""); + + if (yearChangedFromLastSearch) { + setRowDataMap(new Map()); + setTabStatusInfoMap(new Map()); + setTabStatsCdMap(new Map()); + setToolbarSnapshotByTab(new Map()); + loadedTabKeysRef.current.clear(); + setLoadedTabKeys(new Set()); + setDeleteListByTab(new Map()); + clearSlurgeFacStatusArticleFcltSnTracking( + persistedArticleFcltSnRef, + preYearArticleFcltSnRef, + ); + activeTabRef.current = firstTabKey; + setActiveTab(firstTabKey); + } + + try { + setIsLoading(true); + searchDoneRef.current = true; + setHasSearched(true); + const searchParam = buildSewageExcretaSlurgeSearchParam(values); + searchParamRef.current = searchParam; + + const loadedSgg = await loadSggOptions(values.sido); + sggOptionsSidoRef.current = values.sido; + if (loadedSgg.length === 0) { + alert("시군구 목록을 불러오지 못했습니다."); + return; + } + + const ok = await loadActiveTabData( + activeTabRef.current, + searchParam, + values, + !yearChangedFromLastSearch, + true, + buildEmptyDataAlertOverrides(activeTabRef.current), + ); + if (!ok) return; + } catch { + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const lastYearDataFunc = async () => { + const values = lastSearchedValues ?? searchValues; + if (!values?.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + if (!hasSearched) { + alert("먼저 데이터를 조회해주세요."); + return; + } + if ( + activeTabRef.current === DigestorOperatingStatusInpColumn.tabKey && + !publicSewageFacilitiesAvailableRef.current + ) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + return; + } + if ( + activeTabRef.current === ExcretaSlurgeProcessingStatusInpColumn.tabKey && + !excretaTreatmentFacilitiesAvailableRef.current + ) { + alert(EMPTY_EXCRETA_TREATMENT_FACILITIES_MSG); + return; + } + + try { + setIsLoading(true); + const param = buildSewageExcretaSlurgeSearchParam(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 (!noApplicable && 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 noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + 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; + } + + const isSlurgeFacTab = tabKey === SlurgeProcessingFacStatusInpColumn.tabKey; + const isDigestorTab = tabKey === DigestorOperatingStatusInpColumn.tabKey; + const isSewageSlurgeFacTab = tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey; + const isExcretaSlurgeTab = tabKey === ExcretaSlurgeProcessingStatusInpColumn.tabKey; + const rowsForSubmit = mergeGridRowsWithMapRows( + currentData, + mapRows, + mergeHiddenFieldsForSubmitTab(tabKey), + ); + + const slurgeSubmitList = + noApplicable || !isSlurgeFacTab + ? [] + : rowsForSubmit.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)); + const digestorSubmitList = + noApplicable || !isDigestorTab + ? [] + : rowsForSubmit.map((row) => buildDigestorOperatingStatusSaveItem(row, list)); + const sewageSlurgeFacSubmitList = + noApplicable || !isSewageSlurgeFacTab + ? [] + : rowsForSubmit.map((row) => + buildSewageSlurgeProcessingFacStatusSaveItem(row, list), + ); + const excretaSlurgeSubmitList = + noApplicable || !isExcretaSlurgeTab + ? [] + : rowsForSubmit.map((row) => + buildExcretaSlurgeProcessingStatusSaveItem(row, list), + ); + const slurgeDeleteList = isSlurgeFacTab + ? (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)) + : []; + const sewageSlurgeFacDeleteList = isSewageSlurgeFacTab + ? (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list)) + : []; + + if ( + !noApplicable && + ((isSlurgeFacTab && slurgeSubmitList.length === 0) || + (isDigestorTab && digestorSubmitList.length === 0) || + (isSewageSlurgeFacTab && sewageSlurgeFacSubmitList.length === 0) || + (isExcretaSlurgeTab && excretaSlurgeSubmitList.length === 0)) + ) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; + const statsCdForSubmit = + tabStatsCdMap.get(tabKey) ?? + (isDigestorTab + ? SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD + : isSewageSlurgeFacTab + ? SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD + : isExcretaSlurgeTab + ? SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD + : SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD); + + const verificationQuery = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + statsCd: statsCdForSubmit, + }; + + const reloadAfterSubmit = async () => { + if (isSlurgeFacTab || isSewageSlurgeFacTab) { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (isSlurgeFacTab) { + preYearArticleFcltSnRef.current = new Set(); + } + } + setSggOptions(list); + await loadActiveTabData( + tabKey, + searchParamRef.current!, + lastSearchedValues, + false, + false, + ); + }; + + const submitRequestBase = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + userId: "", + laePicNm: submitInfo.laePicNm, + laePicTelNo: submitInfo.laePicTelNo, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + noApplicable, + }; + + try { + showGlobalProgress({ label: "서버에 요청 중...", progress: 0 }); + const userInfo = await getUserInfo(); + setGlobalProgressLabel("일반 검증 진행 중..."); + const response = isSlurgeFacTab + ? await statisticsInputSewageExcretaSlurgeApi.submitSlurgeProcessingFacStatus({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: slurgeSubmitList, + deleteList: slurgeDeleteList, + }) + : isDigestorTab + ? await statisticsInputSewageExcretaSlurgeApi.submitDigestorOperatingStatus({ + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: digestorSubmitList, + }) + : isSewageSlurgeFacTab + ? await statisticsInputSewageExcretaSlurgeApi.submitSewageSlurgeProcessingFacStatus( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: sewageSlurgeFacSubmitList, + deleteList: sewageSlurgeFacDeleteList, + }, + ) + : await statisticsInputSewageExcretaSlurgeApi.submitExcretaSlurgeProcessingStatus( + { + ...submitRequestBase, + userId: userInfo.userUnqId, + submitList: excretaSlurgeSubmitList, + }, + ); + + 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: statsCdForSubmit, + 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 cnacelSubmitFunc = () => { + void (async () => { + try { + if (!(await confirm("제출된 내용을 취소하시겠습니까?"))) return; + + if (!lastSearchedValues || !searchParamRef.current) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const userInfo = await getUserInfo(); + const tabKey = activeTabRef.current; + const statsCdForTab = tabStatsCdMap.get(tabKey); + if (!statsCdForTab) { + alert("먼저 조회를 진행해주세요."); + return; + } + + const requestData: cancelSubmitRequest = { + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + statsYr: lastSearchedValues.year ?? "", + mdfrId: userInfo.userUnqId, + statsCd: statsCdForTab, + }; + + const response = await statisticsApi.cancelSubmit(requestData); + + if (response.status === "success") { + setIsLoading(true); + await loadActiveTabData( + tabKey, + searchParamRef.current, + lastSearchedValues, + false, + false, + ); + alert("제출 취소되었습니다."); + } else { + alert(response.message ?? "제출 취소 중 오류가 발생했습니다."); + } + } catch { + alert("제출 취소 중 오류가 발생했습니다."); + } finally { + setIsLoading(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 noApplicable = isNotApplicableByTab.get(tabKey) ?? false; + const currentData = getTabGridRows(tabKey); + const mapRows = rowDataMap.get(tabKey) ?? []; + + 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; + } + + const baseSaveRequest = { + statsYr: lastSearchedValues.year ?? "", + ctpvNm: lastSearchedValues.sido ?? "", + sggNm: + lastSearchedValues.gugun === "ALL" + ? "" + : (lastSearchedValues.gugun ?? ""), + sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", + ...(noApplicable ? { noApplicable: true } : {}), + }; + + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const userFields = { + userId: userInfo.userUnqId, + ...(isAdminInfo ? { superAdmin: "superAdmin" as const } : {}), + }; + + let response: { status: string; message?: string }; + + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "swtrmtFcltCd", + "fcltSn", + "admdstCd", + "admdstNm", + ]); + + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildSlurgeProcessingFacStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveSlurgeProcessingFacStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ); + } else if (tabKey === DigestorOperatingStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "swtrmtFcltCd", + "fcltSn", + "admdstCd", + "admdstNm", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildDigestorOperatingStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveDigestorOperatingStatus({ + ...baseSaveRequest, + ...userFields, + saveList, + }); + } else if (tabKey === SewageSlurgeProcessingFacStatusInpColumn.tabKey) { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "fcltSn", + "admdstCd", + "admdstNm", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildSewageSlurgeProcessingFacStatusSaveItem(row, list), + ); + const deleteList = (deleteListByTab.get(tabKey) ?? []) + .filter((row) => isPersistedArticleRowForTab(tabKey, row)) + .map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list)); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveSewageSlurgeProcessingFacStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + deleteList, + }, + ); + } else { + const rowsForSave = mergeGridRowsWithMapRows(currentData, mapRows, [ + "fcltSn", + "admdstCd", + "admdstNm", + "fcltPstnCn", + ]); + const saveList = noApplicable + ? [] + : rowsForSave.map((row) => + buildExcretaSlurgeProcessingStatusSaveItem(row, list), + ); + + if (!noApplicable && saveList.length === 0) { + alert("해당사항 없음을 체크해주세요."); + return; + } + + response = + await statisticsInputSewageExcretaSlurgeApi.saveExcretaSlurgeProcessingStatus( + { + ...baseSaveRequest, + ...userFields, + saveList, + }, + ); + } + + if (response.status === "success") { + setDeleteListByTab((prev) => { + const next = new Map(prev); + next.set(tabKey, []); + return next; + }); + if (tabKey === SlurgeProcessingFacStatusInpColumn.tabKey) { + preYearArticleFcltSnRef.current = new Set(); + } + setSggOptions(list); + + const param = + searchParamRef.current ?? + buildSewageExcretaSlurgeSearchParam(lastSearchedValues); + await loadActiveTabData(tabKey, param, lastSearchedValues, false, false); + alert("저장되었습니다."); + } else { + alert(response.message ?? "저장 중 오류가 발생했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + })(); }; - const addArticleFunc = () => { - // 항목 추가 로직 + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === "1" || v === "Y" || v === "y" || v === "true"; + }; + + 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 addArticleFunc = async () => { + const ctx = lastSearchedValues ?? searchValues; + if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") { + alert("먼저 조회를 진행해주세요."); + return; + } + + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + alert("항목을 추가할 수 없는 탭입니다."); + return; + } + + const sggNm = ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : ""; + + 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; + } + + commitActiveGrid(); + const currentRows = getTabGridRows(tabKey); + const newRow = createEmptyArticleRow(tabKey, ctx.sido, sggNm); + if (ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + newRow.__isNewArticle = 1; + } + + flushSync(() => { + setSggOptions(list); + setRowDataMap((prev) => { + const next = new Map(prev); + next.set(tabKey, [...currentRows, newRow]); + return next; + }); + }); + + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx); }; const deleteArticleFunc = () => { - // 항목 제외 로직 + const tabKey = activeTabRef.current; + if (!ARTICLE_EDIT_TAB_KEYS.has(tabKey)) { + 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) => + isPersistedArticleRowForTab(tabKey, row), + ); + 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; + }); + + const ctx = lastSearchedValues ?? searchValues; + if (ctx) { + const statusInfo = tabStatusInfoMap.get(tabKey) ?? null; + commitToolbarSnapshotForLoadedTab( + tabKey, + statusInfo, + nextRows.length > 0, + ctx, + ); + } }; - const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); - const [isNotApplicable, setIsNotApplicable] = useState(false); + const columnInfoByTab = useMemo(() => { + const map = new Map(); + for (const tab of SewageExcretaSlurgeInpTabList) { + 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" || col.fieldName === "swtrmtFcltNm") { + 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, + sggOptions, + hasSearched, + lastSearchedValues, + searchValues, + columnGuidesByTab, + ]); + + const digestorOperatingCellStyleCallback = useCallback( + ({ row, fieldName, rowIndex, grid }) => { + if (fieldName === DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) { + return undefined; + } + if (!isDigestorOperatingRowDisabled(row, grid, rowIndex)) { + return undefined; + } + return { styleName: "rg-cell-readonly", editable: false }; + }, + [], + ); + + const handleDigestorCellEdited = useCallback( + ({ fieldName }) => { + if (fieldName !== DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) { + return; + } + // 편집 중 setRowDataMap → setRows/refresh와 충돌하지 않도록 편집 완료 후 갱신 + window.setTimeout(() => { + getGridRef.current?.gridView?.refresh?.(); + }, 0); + }, + [], + ); + + const resolveGridCellStyleCallback = useCallback( + (tabKey: string): CellStyleCallback => + (params) => { + const verification = verificationCellStyle(params); + if (tabKey !== DigestorOperatingStatusInpColumn.tabKey) { + return verification; + } + return mergeCellStyleResults( + verification, + digestorOperatingCellStyleCallback(params), + ); + }, + [digestorOperatingCellStyleCallback], + ); + + const searchContextForToolbar = lastSearchedValues ?? searchValues; + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab); + const activeTabToolbar = computeTabToolbarSnapshot( + activeTabStatus, + activeTabHasRowData, + hasSearched, + searchContextForToolbar, + inputPeriodEnterable, + isAdminInfo, + ); + + const { + showSaveInToolbar: baseShowSaveInToolbar, + showEnterableToolbarButtons: baseShowEnterableToolbarButtons, + showActionToolbarCard: baseShowActionToolbarCard, + } = activeTabToolbar; + + const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed; + const showEnterableToolbarButtons = + baseShowEnterableToolbarButtons && toolbarGroupAllowed; + const showActionToolbarCard = + toolbarGroupAllowed && + (baseShowActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab))); + const showArticleEditButtons = + ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons; + const showNotApplicableForActiveTab = + loadedTabKeys.has(activeTab) && + shouldShowNotApplicable(activeTabHasRowData, activeTabStatus); + const showSecondaryToolbarCard = + hasSearched && + toolbarGroupAllowed && + (showSaveInToolbar || showEnterableToolbarButtons); + const isNotApplicableForActiveTab = isNotApplicableByTab.get(activeTab) ?? false; + const setIsNotApplicableForActiveTab: Dispatch> = 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 openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const activeTabStatsCd = tabStatsCdMap.get(activeTab); + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(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, + }; + })(); return ( <> - -
-
- - - - +
+ + {showActionToolbarCard && ( +
+
+ + {showEnterableToolbarButtons && ( + <> + void lastYearDataFunc()} + /> + + + )} + {activeTabStatus?.laeSbmsnSttsCd === "2" && ( + + )} + {showSaveInToolbar && } +
+
+ )} + {showSecondaryToolbarCard && ( +
+
+ {showNotApplicableForActiveTab && ( + + )} + {showArticleEditButtons && ( + <> + + + + )} +
+
+ )} + {isLoading && ( +
+
+ +
+
+ )} +
+ void handleActiveTabChange(v)} + className="w-full" + variant="default" + > + + {SewageExcretaSlurgeInpTabList.map((info) => ( + + {info.tabLabel || info.tabKey} + + ))} + + {SewageExcretaSlurgeInpTabList.map((info) => { + const tabKey = info.tabKey; + const rowData = rowDataMap.get(tabKey) ?? []; + const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; + + return ( + + { + 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)} + isExcelExport + getGridDataRef={ + activeTab === tabKey ? getGridDataRef : undefined + } + getGridRef={ + activeTab === tabKey ? getGridRef : undefined + } + cellStyleCallback={resolveGridCellStyleCallback(tabKey)} + cellTooltipCallback={verificationCellTooltip} + onCellEdited={ + tabKey === DigestorOperatingStatusInpColumn.tabKey + ? handleDigestorCellEdited + : undefined + } + /> + + ); + })} +
-
-
- - - -
-
- { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + false, + false, + ); + }} /> + { + const values = lastSearchedValues ?? searchValues; + if (!values || !searchParamRef.current) return; + await loadActiveTabData( + activeTabRef.current, + searchParamRef.current, + values, + false, + false, + ); + }} + /> + + ); }; diff --git a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx index 1c90c2a62..abd48918a 100644 --- a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx @@ -37,7 +37,11 @@ import { type SewerageServiceLevelResidentFriendlyFactSaveItem, type SewerageServiceLevelVerificationResult, } from "@/entities/statistics/model/input/statistics-input-sewerage-service-level-interface"; -import type { SewerageStatusInfo, cancelSubmitRequest, ImrStatusResponse } from "@/entities/statistics/model/statistics-interface"; +import type { + SewerageStatusInfo, + cancelSubmitRequest, + ImrStatusResponse, +} 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 { ConfirmDialog } from "@/shared/ui/confirm-dialog"; @@ -62,6 +66,18 @@ const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record & { verificationResult?: SewerageServiceLevelVerificationResult; }; @@ -299,6 +315,8 @@ const SewerageServiceLevelInp = () => { const periodEnterableRef = useRef(true); const activeTabRef = useRef(activeTab); const getGridDataRef = useRef<(() => Record[]) | null>(null); + /** ST00130002 당해 조회 결과 유무(공공하수처리시설 연동) — 전년도 조회 가능 여부 */ + const publicSewageFacilitiesAvailableRef = useRef(false); activeTabRef.current = activeTab; useEffect(() => { @@ -384,9 +402,10 @@ const SewerageServiceLevelInp = () => { searchContext: SearchFormValues, mergeIntoExistingMap: boolean, updateLastSearchedValues: boolean, - overrides?: { mode?: string }, + overrides?: { mode?: string; alertIfResidentFriendlyEmpty?: boolean }, ): Promise => { const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; + const fromPreYear = overrides?.mode === "PRE"; if (tabKey === ComplaintInpColumn.tabKey) { const response = @@ -426,6 +445,12 @@ const SewerageServiceLevelInp = () => { (residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[], commonInfo.ctpvNm, ); + if (!fromPreYear) { + publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; + if (gridRows.length === 0 && overrides?.alertIfResidentFriendlyEmpty) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + } + } applyRowMap(mergeIntoExistingMap, (m) => { m.set(ResidentFriendlyFactInpColumn.tabKey, gridRows); @@ -460,6 +485,7 @@ const SewerageServiceLevelInp = () => { searchParamRef.current = null; searchDoneRef.current = false; periodEnterableRef.current = true; + publicSewageFacilitiesAvailableRef.current = false; }; const handleSearch = async (values: SearchFormValues) => { @@ -507,6 +533,7 @@ const SewerageServiceLevelInp = () => { values, !yearChangedFromLastSearch, true, + buildEmptyDataAlertOverrides(activeTabRef.current), ); } catch { alert("데이터 조회 중 오류가 발생했습니다."); @@ -541,6 +568,7 @@ const SewerageServiceLevelInp = () => { lastSearchedValues, true, false, + buildEmptyDataAlertOverrides(tabKey), ); } catch { alert("데이터 조회 중 오류가 발생했습니다."); @@ -559,6 +587,13 @@ const SewerageServiceLevelInp = () => { alert("먼저 데이터를 조회해주세요."); return; } + if ( + activeTabRef.current === ResidentFriendlyFactInpColumn.tabKey && + !publicSewageFacilitiesAvailableRef.current + ) { + alert(EMPTY_PUBLIC_SEWAGE_FACILITIES_MSG); + return; + } try { setIsLoading(true); diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index 2df784f6c..a5726399a 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -481,17 +481,6 @@ function mapTreatWaterReuseStatusListToGridRows< }); } -function mapWaterReuseListToGridRows< - T extends { verificationResult?: WaterReuseVerificationResult }, ->(list: T[], ctpvNm: string): GridRowWithVerification[] { - return list.map((item) => { - const { verificationResult, ...rest } = item; - const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; - if (verificationResult) row.verificationResult = verificationResult; - return row; - }); -} - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function waitForImrJobCompleted( @@ -1871,7 +1860,6 @@ const WaterReuseInp = () => { }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]); const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; - const activeTabRowData = rowDataMap.get(activeTab) ?? []; const activeTabToolbar = toolbarSnapshotByTab.get(activeTab) ?? computeTabToolbarSnapshot( diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index d44b32a81..1c9fc9bae 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -184,6 +184,13 @@ export type ComputedFieldConfig = numeratorQuotient: [string, string]; denominatorQuotient: [string, string]; type: "quotientRatioPercent"; + } + | { + /** (numeratorField / denominatorField) × multiplierField × 100 */ + numeratorField: string; + denominatorField: string; + multiplierField: string; + type: "quotientTimesMultiplierPercent"; }; export type ColumnConfig = { @@ -306,15 +313,30 @@ export type ColumnInfoList = { }; // 개별 셀 스타일을 동적으로 결정하기 위한 콜백 (예: 검증 오류 셀 강조) +export type CellStyleCallbackGrid = { + getValue?: (itemIndex: number, fieldName: string) => unknown; +}; + export type CellStyleCallbackParams = { rowIndex: number; fieldName: string; row: Record; + /** styleCallback 시점의 그리드 값 조회(행 단위 동적 스타일용) */ + grid?: CellStyleCallbackGrid; }; +/** RealGrid styleCallback — 문자열(styleName) 또는 editable 등 동적 스타일 객체 */ +export type CellStyleCallbackResult = + | string + | { + styleName?: string; + editable?: boolean; + readOnly?: boolean; + }; + export type CellStyleCallback = ( params: CellStyleCallbackParams, -) => string | undefined; +) => CellStyleCallbackResult | undefined; /** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */ export type CellTooltipCallbackParams = { diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts index a2b645123..e8e0991f0 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn.ts @@ -5,74 +5,75 @@ import { ValueType } from "realgrid"; export const ExcretaRelatedIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_DSTR_CD', fieldName: '행정구역코드', header: { text: '행정구역코드' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, - { name: 'ADST_DONG', fieldName: '행정동', header: { text: '행정동' }, visible: false }, - { name: 'RGN_NM', fieldName: '지역명', header: { text: '지역명' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 업체수(개소) - { name: 'ECSCS_SM', fieldName: '업체수(개소)', header: { text: '업체수(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'bzentyCnt', fieldName: 'bzentyCnt', header: { text: '업체수(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 종사인원(명) - { name: 'ECSCS_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'empNope', fieldName: 'empNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 시설(차량) 확보현황(대수) - { name: 'VHCLE_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'THREE_BELOW', fieldName: '3톤이하', header: { text: '3톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'FOUR_BELOW', fieldName: '4.5톤이하', header: { text: '4.5톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'EIGHT_BELOW', fieldName: '8톤이하', header: { text: '8톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'fcltSecrPrstCnt', + fieldName: 'fcltSecrPrstCnt', + header: { text: '계' }, + editable: false, footer: + footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['n3TonUdrCnt', 'n4TonUdrCnt', 'n8TonUdrCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, + { name: 'n3TonUdrCnt', fieldName: 'n3TonUdrCnt', header: { text: '3톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'n4TonUdrCnt', fieldName: 'n4TonUdrCnt', header: { text: '4.5톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'n8TonUdrCnt', fieldName: 'n8TonUdrCnt', header: { text: '8톤이하' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 수집·운반 실적(m³/년) - { name: 'COLCT_TRSP_ACMSLT', fieldName: '수집·운반 실적(m³/년)', header: { text: '수집·운반 실적(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'clctCnvncPrfmncQnt', fieldName: 'clctCnvncPrfmncQnt', header: { text: '수집·운반 실적(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_ECSCS_SM', fieldName: 'CHK_ECSCS_SM', header: { text: 'CHK_ECSCS_SM' }, visible: false }, - { name: 'CHK_ECSCS_BSMN', fieldName: 'CHK_ECSCS_BSMN', header: { text: 'CHK_ECSCS_BSMN' }, visible: false }, - { name: 'CHK_VHCLE_SM', fieldName: 'CHK_VHCLE_SM', header: { text: 'CHK_VHCLE_SM' }, visible: false }, - { name: 'CHK_THREE_BELOW', fieldName: 'CHK_THREE_BELOW', header: { text: 'CHK_THREE_BELOW' }, visible: false }, - { name: 'CHK_FOUR_BELOW', fieldName: 'CHK_FOUR_BELOW', header: { text: 'CHK_FOUR_BELOW' }, visible: false }, - { name: 'CHK_EIGHT_BELOW', fieldName: 'CHK_EIGHT_BELOW', header: { text: 'CHK_EIGHT_BELOW' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, - { name: 'CHK_COLCT_TRSP_ACMSLT', fieldName: 'CHK_COLCT_TRSP_ACMSLT', header: { text: 'CHK_COLCT_TRSP_ACMSLT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'ECSCS_SM', - 'ECSCS_BSMN', + 'bzentyCnt', + 'empNope', { name: 'group_vehicle_status', header: { text: '시설(차량) 확보현황(대수)' }, items: [ - 'VHCLE_SM', - 'THREE_BELOW', - 'FOUR_BELOW', - 'EIGHT_BELOW', - 'ETC' + 'fcltSecrPrstCnt', + 'n3TonUdrCnt', + 'n4TonUdrCnt', + 'n8TonUdrCnt', + 'etcCnt' ] }, - 'COLCT_TRSP_ACMSLT', - 'RMK' + 'clctCnvncPrfmncQnt', + 'rmrk', + 'admdstCd', + 'admdstNm', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'excretaRelatedIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120003', tabLabel: '분뇨수집·운반업' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts index 5e61f6afe..53fa25308 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn.ts @@ -5,82 +5,59 @@ import { ValueType } from "realgrid"; export const PrivateSewageTreatRelatedIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_DONG', fieldName: '행정동', header: { text: '행정동' }, visible: false }, - { name: 'RGN_NM', fieldName: '지역명', header: { text: '지역명' }, visible: false }, - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 개인하수처리시설 설계·시공업 - { name: 'CNSTRCT_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CNSTRCT_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'designMdsrCnt', fieldName: 'designMdsrCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'designMdsrNope', fieldName: 'designMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 개인하수처리시설 제조업 - { name: 'MNFCTUR_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MNFCTUR_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mfbizCnt', fieldName: 'mfbizCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mfbizMdsrNope', fieldName: 'mfbizMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 개인하수처리시설 관리업 - { name: 'MGMT_ENTRPS', fieldName: '업소수', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MGMT_BSMN', fieldName: '종사인원(명)', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mngCnt', fieldName: 'mngCnt', header: { text: '업소수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mngMdsrNope', fieldName: 'mngMdsrNope', header: { text: '종사인원(명)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: 추가 필드 (화면에 미노출이, DB 저장) - { name: 'CNSTRCT_FCT_CNT', fieldName: '설계시공시설개수', header: { text: '설계시공시설개수' }, visible: false }, - { name: 'CNSTRCT_FCT_CPCTY', fieldName: '설계시공시설용량', header: { text: '설계시공시설용량' }, visible: false }, - { name: 'MNFCTUR_STTY_CNT', fieldName: '제조정원', header: { text: '제조정원' }, visible: false }, - { name: 'MNFCTUR_WRRTN_CNT', fieldName: '제조권원', header: { text: '제조권원' }, visible: false }, - { name: 'MGMT_STTY_CNT', fieldName: '관리정원', header: { text: '관리정원' }, visible: false }, - { name: 'MGMT_WRRTN_CNT', fieldName: '관리권원', header: { text: '관리권원' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_CNSTRCT_ENTRPS', fieldName: 'CHK_CNSTRCT_ENTRPS', header: { text: 'CHK_CNSTRCT_ENTRPS' }, visible: false }, - { name: 'CHK_CNSTRCT_BSMN', fieldName: 'CHK_CNSTRCT_BSMN', header: { text: 'CHK_CNSTRCT_BSMN' }, visible: false }, - { name: 'CHK_CNSTRCT_FCT_CNT', fieldName: 'CHK_CNSTRCT_FCT_CNT', header: { text: 'CHK_CNSTRCT_FCT_CNT' }, visible: false }, - { name: 'CHK_CNSTRCT_FCT_CPCTY', fieldName: 'CHK_CNSTRCT_FCT_CPCTY', header: { text: 'CHK_CNSTRCT_FCT_CPCTY' }, visible: false }, - { name: 'CHK_MNFCTUR_ENTRPS', fieldName: 'CHK_MNFCTUR_ENTRPS', header: { text: 'CHK_MNFCTUR_ENTRPS' }, visible: false }, - { name: 'CHK_MNFCTUR_BSMN', fieldName: 'CHK_MNFCTUR_BSMN', header: { text: 'CHK_MNFCTUR_BSMN' }, visible: false }, - { name: 'CHK_MNFCTUR_STTY_CNT', fieldName: 'CHK_MNFCTUR_STTY_CNT', header: { text: 'CHK_MNFCTUR_STTY_CNT' }, visible: false }, - { name: 'CHK_MNFCTUR_WRRTN_CNT', fieldName: 'CHK_MNFCTUR_WRRTN_CNT', header: { text: 'CHK_MNFCTUR_WRRTN_CNT' }, visible: false }, - { name: 'CHK_MGMT_ENTRPS', fieldName: 'CHK_MGMT_ENTRPS', header: { text: 'CHK_MGMT_ENTRPS' }, visible: false }, - { name: 'CHK_MGMT_BSMN', fieldName: 'CHK_MGMT_BSMN', header: { text: 'CHK_MGMT_BSMN' }, visible: false }, - { name: 'CHK_MGMT_STTY_CNT', fieldName: 'CHK_MGMT_STTY_CNT', header: { text: 'CHK_MGMT_STTY_CNT' }, visible: false }, - { name: 'CHK_MGMT_WRRTN_CNT', fieldName: 'CHK_MGMT_WRRTN_CNT', header: { text: 'CHK_MGMT_WRRTN_CNT' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_design_construct', header: { text: '개인하수처리시설 설계·시공업' }, - items: ['CNSTRCT_ENTRPS', 'CNSTRCT_BSMN'] + items: ['designMdsrCnt', 'designMdsrNope'] }, { name: 'group_manufacture', header: { text: '개인하수처리시설 제조업' }, - items: ['MNFCTUR_ENTRPS', 'MNFCTUR_BSMN'] + items: ['mfbizCnt', 'mfbizMdsrNope'] }, { name: 'group_management', header: { text: '개인하수처리시설 관리업' }, - items: ['MGMT_ENTRPS', 'MGMT_BSMN'] + items: ['mngCnt', 'mngMdsrNope'] }, - 'RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'privateSewageTreatRelatedIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120004', tabLabel: '개인하수처리관련업' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts index 8df679707..7df3d801c 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn.ts @@ -5,86 +5,107 @@ import { ValueType } from "realgrid"; export const PublicInstitutionExcretaIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 직제 - { name: 'OCGNZ', fieldName: '직 제', header: { text: '직 제' } }, + { name: 'ognzNm', fieldName: 'ognzNm', header: { text: '직 제' } }, // 직원총수 - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'empGrssCnt', + fieldName: 'empGrssCnt', + header: { text: '직원총수' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, // 행정직 - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pbadmsNope', fieldName: 'pbadmsNope', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기술직 - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // 기능직 (hidden) - { name: 'TCNSRV', fieldName: '기능직', header: { text: '기능직' }, visible: false }, + { + name: 'techWholNope', + fieldName: 'techWholNope', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt'], + type: 'sum' as const, + }, + }, + { name: 'engrkCnt', fieldName: 'engrkCnt', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mchnCnt', fieldName: 'mchnCnt', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'prvtmCnt', fieldName: 'prvtmCnt', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'chmengCnt', fieldName: 'chmengCnt', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'envCnt', fieldName: 'envCnt', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'enjbEtcCnt', fieldName: 'enjbEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기타 - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_OCGNZ', fieldName: 'CHK_OCGNZ', header: { text: 'CHK_OCGNZ' }, visible: false }, - { name: 'CHK_TOT_WTR_QRM', fieldName: 'CHK_TOT_WTR_QRM', header: { text: 'CHK_TOT_WTR_QRM' }, visible: false }, - { name: 'CHK_ADMINIST_JSSFC', fieldName: 'CHK_ADMINIST_JSSFC', header: { text: 'CHK_ADMINIST_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_JSSFC_SM', fieldName: 'CHK_TCHNLGY_JSSFC_SM', header: { text: 'CHK_TCHNLGY_JSSFC_SM' }, visible: false }, - { name: 'CHK_ENGRK_JSSFC', fieldName: 'CHK_ENGRK_JSSFC', header: { text: 'CHK_ENGRK_JSSFC' }, visible: false }, - { name: 'CHK_MCHN_JSSFC', fieldName: 'CHK_MCHN_JSSFC', header: { text: 'CHK_MCHN_JSSFC' }, visible: false }, - { name: 'CHK_ELCTY_JSSFC', fieldName: 'CHK_ELCTY_JSSFC', header: { text: 'CHK_ELCTY_JSSFC' }, visible: false }, - { name: 'CHK_CHMSTRY_JSSFC', fieldName: 'CHK_CHMSTRY_JSSFC', header: { text: 'CHK_CHMSTRY_JSSFC' }, visible: false }, - { name: 'CHK_ENVRN_JSSFC', fieldName: 'CHK_ENVRN_JSSFC', header: { text: 'CHK_ENVRN_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_ETC', fieldName: 'CHK_TCHNLGY_ETC', header: { text: 'CHK_TCHNLGY_ETC' }, visible: false }, - { name: 'CHK_TCNSRV', fieldName: 'CHK_TCNSRV', header: { text: 'CHK_TCNSRV' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'ognzSn', fieldName: 'ognzSn', header: { text: '조직일련번호' }, visible: false }, + { + name: '__isNewArticle', + fieldName: '__isNewArticle', + header: { text: '신규항목' }, + visible: false, + editable: false, + valueType: ValueType.NUMBER, + }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'OCGNZ', - 'TOT_WTR_QRM', - 'ADMINIST_JSSFC', + 'ognzNm', + 'empGrssCnt', + 'pbadmsNope', { name: 'group_technology', header: { text: '기술직' }, items: [ - 'TCHNLGY_JSSFC_SM', - 'ENGRK_JSSFC', - 'MCHN_JSSFC', - 'ELCTY_JSSFC', - 'CHMSTRY_JSSFC', - 'ENVRN_JSSFC', - 'TCHNLGY_ETC' + 'techWholNope', + 'engrkCnt', + 'mchnCnt', + 'prvtmCnt', + 'chmengCnt', + 'envCnt', + 'enjbEtcCnt' ] }, - 'ETC', - 'RMK' + 'etcCnt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'ognzSn', + '__isNewArticle', ], rowStartNum: 2, fixedColumns: 0, - tabKey: 'publicInstitutionExcretaIndustryInp', + tabKey: 'ST00120002', tabLabel: '공공기관 분뇨분야' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts index 9df115260..176c11a1a 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn.ts @@ -5,86 +5,107 @@ import { ValueType } from "realgrid"; export const PublicInstitutionSewerIndustryInpColumn = { columns: [ // 행정구역 - { name: 'SIDO', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'GUGUN', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, - - // 행정코드 (hidden) - { name: 'ADST_CD', fieldName: '행정코드', header: { text: '행정코드' }, visible: false }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 직제 - { name: 'OCGNZ', fieldName: '직 제', header: { text: '직 제' } }, + { name: 'ognzNm', fieldName: 'ognzNm', header: { text: '직 제' } }, // 직원총수 - { name: 'TOT_WTR_QRM', fieldName: '직원총수', header: { text: '직원총수' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'empGrssCnt', + fieldName: 'empGrssCnt', + header: { text: '직원총수' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt', 'etcCnt'], + type: 'sum' as const, + }, + }, // 행정직 - { name: 'ADMINIST_JSSFC', fieldName: '행정직', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'pbadmsNope', fieldName: 'pbadmsNope', header: { text: '행정직' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기술직 - { name: 'TCHNLGY_JSSFC_SM', fieldName: '계', header: { text: '계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENGRK_JSSFC', fieldName: '토목', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'MCHN_JSSFC', fieldName: '기계', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ELCTY_JSSFC', fieldName: '전기', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'CHMSTRY_JSSFC', fieldName: '화공', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'ENVRN_JSSFC', fieldName: '환경', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'TCHNLGY_ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - - // 기능직 (hidden) - { name: 'TCNSRV', fieldName: '기능직', header: { text: '기능직' }, visible: false }, + { + name: 'techWholNope', + fieldName: 'techWholNope', + header: { text: '계' }, + editable: false, + footer: footerInt, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + styleName: 'rg-cell-total', + computed: { + fields: ['engrkCnt', 'mchnCnt', 'prvtmCnt', 'chmengCnt', 'envCnt', 'enjbEtcCnt'], + type: 'sum' as const, + }, + }, + { name: 'engrkCnt', fieldName: 'engrkCnt', header: { text: '토목' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'mchnCnt', fieldName: 'mchnCnt', header: { text: '기계' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'prvtmCnt', fieldName: 'prvtmCnt', header: { text: '전기' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'chmengCnt', fieldName: 'chmengCnt', header: { text: '화공' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'envCnt', fieldName: 'envCnt', header: { text: '환경' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'enjbEtcCnt', fieldName: 'enjbEtcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 기타 - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'etcCnt', fieldName: 'etcCnt', header: { text: '기타' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 필드 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields : 데이터 변경 여부를 판단하는 필드로, UI에서 숨김 처리 - { name: 'CHK_OCGNZ', fieldName: 'CHK_OCGNZ', header: { text: 'CHK_OCGNZ' }, visible: false }, - { name: 'CHK_TOT_WTR_QRM', fieldName: 'CHK_TOT_WTR_QRM', header: { text: 'CHK_TOT_WTR_QRM' }, visible: false }, - { name: 'CHK_ADMINIST_JSSFC', fieldName: 'CHK_ADMINIST_JSSFC', header: { text: 'CHK_ADMINIST_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_JSSFC_SM', fieldName: 'CHK_TCHNLGY_JSSFC_SM', header: { text: 'CHK_TCHNLGY_JSSFC_SM' }, visible: false }, - { name: 'CHK_ENGRK_JSSFC', fieldName: 'CHK_ENGRK_JSSFC', header: { text: 'CHK_ENGRK_JSSFC' }, visible: false }, - { name: 'CHK_MCHN_JSSFC', fieldName: 'CHK_MCHN_JSSFC', header: { text: 'CHK_MCHN_JSSFC' }, visible: false }, - { name: 'CHK_ELCTY_JSSFC', fieldName: 'CHK_ELCTY_JSSFC', header: { text: 'CHK_ELCTY_JSSFC' }, visible: false }, - { name: 'CHK_CHMSTRY_JSSFC', fieldName: 'CHK_CHMSTRY_JSSFC', header: { text: 'CHK_CHMSTRY_JSSFC' }, visible: false }, - { name: 'CHK_ENVRN_JSSFC', fieldName: 'CHK_ENVRN_JSSFC', header: { text: 'CHK_ENVRN_JSSFC' }, visible: false }, - { name: 'CHK_TCHNLGY_ETC', fieldName: 'CHK_TCHNLGY_ETC', header: { text: 'CHK_TCHNLGY_ETC' }, visible: false }, - { name: 'CHK_TCNSRV', fieldName: 'CHK_TCNSRV', header: { text: 'CHK_TCNSRV' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'ognzSn', fieldName: 'ognzSn', header: { text: '조직일련번호' }, visible: false }, + { + name: '__isNewArticle', + fieldName: '__isNewArticle', + header: { text: '신규항목' }, + visible: false, + editable: false, + valueType: ValueType.NUMBER, + }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO', 'GUGUN'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'OCGNZ', - 'TOT_WTR_QRM', - 'ADMINIST_JSSFC', + 'ognzNm', + 'empGrssCnt', + 'pbadmsNope', { name: 'group_technology', header: { text: '기술직' }, items: [ - 'TCHNLGY_JSSFC_SM', - 'ENGRK_JSSFC', - 'MCHN_JSSFC', - 'ELCTY_JSSFC', - 'CHMSTRY_JSSFC', - 'ENVRN_JSSFC', - 'TCHNLGY_ETC' + 'techWholNope', + 'engrkCnt', + 'mchnCnt', + 'prvtmCnt', + 'chmengCnt', + 'envCnt', + 'enjbEtcCnt' ] }, - 'ETC', - 'RMK' + 'etcCnt', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'ognzSn', + '__isNewArticle', ], rowStartNum: 2, - fixedColumns: 1, - tabKey: 'publicInstitutionSewerIndustryInp', + fixedColumns: 0, + tabKey: 'ST00120001', tabLabel: '공공기관 하수분야' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts index c6b70ea6c..247fdd5aa 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn.ts @@ -1,91 +1,158 @@ +import type { ColumnConfig } from "@/components/type/DataGrid.type"; import { footerDecimal1 } from "@/components/util/grid/footerSummary"; import { ValueType } from "realgrid"; +export const DIGESTOR_OPERATING_STATUS_YN_VALUES = ["Y", "N"] as const; +export const DIGESTOR_OPERATING_STATUS_EN_YN_FIELD = "digstnEnYn" as const; + +/** 소화조 유/무가 N이면 해당 행의 입력 셀을 잠근다(digstnEnYn 제외). */ +export function isDigestorOperatingRowDisabled( + row: Record, + grid?: { getValue?: (itemIndex: number, fieldName: string) => unknown }, + rowIndex?: number, +): boolean { + const live = + grid?.getValue != null && rowIndex != null && rowIndex >= 0 + ? grid.getValue(rowIndex, DIGESTOR_OPERATING_STATUS_EN_YN_FIELD) + : undefined; + const raw = + live !== undefined && live !== null + ? live + : row[DIGESTOR_OPERATING_STATUS_EN_YN_FIELD]; + return String(raw ?? "").trim().toUpperCase() === "N"; +} + +function dropdownColumn( + col: Pick & + Partial, + options: readonly string[], +): ColumnConfig { + const values = [...options]; + return { + ...col, + values, + labels: values, + lookupDisplay: true, + editor: { + type: "dropdown", + domainOnly: true, + textReadOnly: true, + commitOnSelect: true, + dropDownWhenClick: true, + values, + labels: values, + }, + }; +} + // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 > tab 2. 소화조 운영현황 export const DigestorOperatingStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장명 - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수처리장명' }, editable: false }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리장명' }, editable: false }, // 소화조 운영현황 - { name: 'DGSTR_FNNC', fieldName: '소화조 유/무(Y/N)선택', header: { text: '소화조 유/무(Y/N)선택' } }, - { name: 'DGSTR_CPC', fieldName: '설계용량(m³)', header: { text: '설계용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_OPR_CPC', fieldName: '실제운전용량(m³)', header: { text: '실제운전용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_IN', fieldName: '투입량(m³/일)', header: { text: '투입량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_TS', fieldName: '투입 TS 농도(%)', header: { text: '투입 TS 농도(%)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_YRSTP_RN_DYN', fieldName: '가동일수(연간)', header: { text: '가동일수(연간)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_OCCUR_GAS', fieldName: '발생가스량(m³/일)', header: { text: '발생가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + dropdownColumn( + { + name: 'digstnEnYn', + fieldName: 'digstnEnYn', + header: { text: '소화조 유/무(Y/N)선택' }, + }, + DIGESTOR_OPERATING_STATUS_YN_VALUES, + ), + { name: 'digstnCpct', fieldName: 'digstnCpct', header: { text: '설계용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'actlDrvnCpct', fieldName: 'actlDrvnCpct', header: { text: '실제운전용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasInputQnt', fieldName: 'digstnGasInputQnt', header: { text: '투입량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasInputSldDnst', fieldName: 'digstnGasInputSldDnst', header: { text: '투입 TS 농도(%)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnTnkOprtngDayCnt', fieldName: 'digstnTnkOprtngDayCnt', header: { text: '가동일수(연간)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'ocrnGasQnt', fieldName: 'ocrnGasQnt', header: { text: '발생가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 소화조 운영현황 > 소화가스 이용현황(m³/일) - { name: 'DGSTR_SM', fieldName: '계', header: { text: '계' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_SLE', fieldName: '판매', header: { text: '판매' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ELTGNR', fieldName: '발전', header: { text: '발전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ONSLF', fieldName: '자체활용', header: { text: '자체활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'DGSTR_ETC', fieldName: '기타(CNG등)', header: { text: '기타(CNG등)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'digstnGasUtztnWholPrst', + fieldName: 'digstnGasUtztnWholPrst', + header: { text: '계' }, + footer: footerDecimal1, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'digstnGasUtztnNtslQnt', + 'digstnGasUtztnEtgtQnt', + 'digstnGasUtztnItslfQnt', + 'digstnGasUtztnEtcQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'digstnGasUtztnNtslQnt', fieldName: 'digstnGasUtztnNtslQnt', header: { text: '판매' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnEtgtQnt', fieldName: 'digstnGasUtztnEtgtQnt', header: { text: '발전' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnItslfQnt', fieldName: 'digstnGasUtztnItslfQnt', header: { text: '자체활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'digstnGasUtztnEtcQnt', fieldName: 'digstnGasUtztnEtcQnt', header: { text: '기타(CNG등)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 소화조 운영현황 > 잉여가스량 - { name: 'DGSTR_GAS', fieldName: '잉여가스량(m³/일)', header: { text: '잉여가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'surplsGasQnt', fieldName: 'surplsGasQnt', header: { text: '잉여가스량(m³/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'DGSTR_RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields for change detection - { name: 'CHK_DGSTR_FNNC', fieldName: 'CHK_DGSTR_FNNC', header: { text: 'CHK_DGSTR_FNNC' }, visible: false }, - { name: 'CHK_DGSTR_IN', fieldName: 'CHK_DGSTR_IN', header: { text: 'CHK_DGSTR_IN' }, visible: false }, - { name: 'CHK_DGSTR_TS', fieldName: 'CHK_DGSTR_TS', header: { text: 'CHK_DGSTR_TS' }, visible: false }, - { name: 'CHK_DGSTR_YRSTP_RN_DYN', fieldName: 'CHK_DGSTR_YRSTP_RN_DYN', header: { text: 'CHK_DGSTR_YRSTP_RN_DYN' }, visible: false }, - { name: 'CHK_DGSTR_OCCUR_GAS', fieldName: 'CHK_DGSTR_OCCUR_GAS', header: { text: 'CHK_DGSTR_OCCUR_GAS' }, visible: false }, - { name: 'CHK_DGSTR_CPC', fieldName: 'CHK_DGSTR_CPC', header: { text: 'CHK_DGSTR_CPC' }, visible: false }, - { name: 'CHK_DGSTR_OPR_CPC', fieldName: 'CHK_DGSTR_OPR_CPC', header: { text: 'CHK_DGSTR_OPR_CPC' }, visible: false }, - { name: 'CHK_DGSTR_SM', fieldName: 'CHK_DGSTR_SM', header: { text: 'CHK_DGSTR_SM' }, visible: false }, - { name: 'CHK_DGSTR_SLE', fieldName: 'CHK_DGSTR_SLE', header: { text: 'CHK_DGSTR_SLE' }, visible: false }, - { name: 'CHK_DGSTR_ELTGNR', fieldName: 'CHK_DGSTR_ELTGNR', header: { text: 'CHK_DGSTR_ELTGNR' }, visible: false }, - { name: 'CHK_DGSTR_ONSLF', fieldName: 'CHK_DGSTR_ONSLF', header: { text: 'CHK_DGSTR_ONSLF' }, visible: false }, - { name: 'CHK_DGSTR_ETC', fieldName: 'CHK_DGSTR_ETC', header: { text: 'CHK_DGSTR_ETC' }, visible: false }, - { name: 'CHK_DGSTR_GAS', fieldName: 'CHK_DGSTR_GAS', header: { text: 'CHK_DGSTR_GAS' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', + 'swtrmtFcltNm', { name: 'group_digestor_operating', header: { text: '소화조 운영현황' }, items: [ - 'DGSTR_FNNC', - 'DGSTR_CPC', - 'DGSTR_OPR_CPC', - 'DGSTR_IN', - 'DGSTR_TS', - 'DGSTR_YRSTP_RN_DYN', - 'DGSTR_OCCUR_GAS', + 'digstnEnYn', + 'digstnCpct', + 'actlDrvnCpct', + 'digstnGasInputQnt', + 'digstnGasInputSldDnst', + 'digstnTnkOprtngDayCnt', + 'ocrnGasQnt', { name: 'group_gas_usage', header: { text: '소화가스 이용현황(m³/일)' }, items: [ - 'DGSTR_SM', - 'DGSTR_SLE', - 'DGSTR_ELTGNR', - 'DGSTR_ONSLF', - 'DGSTR_ETC' + 'digstnGasUtztnWholPrst', + 'digstnGasUtztnNtslQnt', + 'digstnGasUtztnEtgtQnt', + 'digstnGasUtztnItslfQnt', + 'digstnGasUtztnEtcQnt' ] }, - 'DGSTR_GAS' + 'surplsGasQnt' ] }, - 'DGSTR_RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 3, fixedColumns: 0, - tabKey: 'digestorOperatingStatusInp', + tabKey: 'ST00110002', tabLabel: '소화조 운영현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts index 7edfb7a40..60f9b14d0 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn.ts @@ -5,79 +5,116 @@ import { ValueType } from "realgrid"; export const ExcretaSlurgeProcessingStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, editable: false, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 분뇨처리장명 - { name: 'ECSCS_NM', fieldName: '분 뇨 처리장명', header: { text: '분 뇨 처리장명' }, editable: false }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '분뇨 처리장명' }, editable: false }, // 발생량 - { name: 'OCTY', fieldName: '발생량 (A)=(B)+(C)', header: { text: '발생량 (A)=(B)+(C)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'gnam', + fieldName: 'gnam', + header: { text: '발생량 (A)=(B)+(C)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt', + 'nondspsQnt' + ], + type: 'sum' as const, + }, + }, // 처 분 량(톤/년) - { name: 'PROCESS_CPCTY_SM', fieldName: '계(B)', header: { text: '계(B)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'RUSE', fieldName: '재활용', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'LAND_RCLM', fieldName: '육상매립', header: { text: '육상매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'INCNR', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OCEAN_DPSL', fieldName: '해양투기', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'FUEL', fieldName: '연료화', header: { text: '연료화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ETC', fieldName: '기타', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholDspsQnt', + fieldName: 'wholDspsQnt', + header: { text: '계(B)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt', + ], + type: 'sum' as const, + }, + }, + { name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'olandLfvlm', fieldName: 'olandLfvlm', header: { text: '육상매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrQnt', fieldName: 'incnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'oceanInputQnt', fieldName: 'oceanInputQnt', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelQnt', fieldName: 'fuelQnt', header: { text: '연료화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 미처분량(이월량)(C) - { name: 'CYFD_CPCTY', fieldName: '미처분량(이월량)(C)', header: { text: '미처분량(이월량)(C)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'nondspsQnt', fieldName: 'nondspsQnt', header: { text: '미처분량(이월량)(C)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 타시설연계여부 - { name: 'OUT_BSC_FCT_NM', fieldName: '타시설연계여부', header: { text: '타시설연계여부' } }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '타시설연계여부' } }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: 의견 - { name: 'WRTER_OPINION', fieldName: '사용자의견', header: { text: '사용자의견' }, visible: false }, - { name: 'CHCKER_OPINION', fieldName: '검토자의견', header: { text: '검토자의견' }, visible: false }, - - // Hidden: Check fields for change detection - { name: 'CHK_ECSCS_NM', fieldName: 'CHK_ECSCS_NM', header: { text: 'CHK_ECSCS_NM' }, visible: false }, - { name: 'CHK_OCTY', fieldName: 'CHK_OCTY', header: { text: 'CHK_OCTY' }, visible: false }, - { name: 'CHK_PROCESS_CPCTY_SM', fieldName: 'CHK_PROCESS_CPCTY_SM', header: { text: 'CHK_PROCESS_CPCTY_SM' }, visible: false }, - { name: 'CHK_RUSE', fieldName: 'CHK_RUSE', header: { text: 'CHK_RUSE' }, visible: false }, - { name: 'CHK_LAND_RCLM', fieldName: 'CHK_LAND_RCLM', header: { text: 'CHK_LAND_RCLM' }, visible: false }, - { name: 'CHK_INCNR', fieldName: 'CHK_INCNR', header: { text: 'CHK_INCNR' }, visible: false }, - { name: 'CHK_OCEAN_DPSL', fieldName: 'CHK_OCEAN_DPSL', header: { text: 'CHK_OCEAN_DPSL' }, visible: false }, - { name: 'CHK_FUEL', fieldName: 'CHK_FUEL', header: { text: 'CHK_FUEL' }, visible: false }, - { name: 'CHK_ETC', fieldName: 'CHK_ETC', header: { text: 'CHK_ETC' }, visible: false }, - { name: 'CHK_CYFD_CPCTY', fieldName: 'CHK_CYFD_CPCTY', header: { text: 'CHK_CYFD_CPCTY' }, visible: false }, - { name: 'CHK_OUT_BSC_FCT_NM', fieldName: 'CHK_OUT_BSC_FCT_NM', header: { text: 'CHK_OUT_BSC_FCT_NM' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '시설물위치내용' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'ECSCS_NM', - 'OCTY', + 'fcltNm', + 'gnam', { name: 'group_process_amount', header: { text: '처 분 량(톤/년)' }, items: [ - 'PROCESS_CPCTY_SM', - 'RUSE', - 'LAND_RCLM', - 'INCNR', - 'OCEAN_DPSL', - 'FUEL', - 'ETC' + 'wholDspsQnt', + 'rcycQnt', + 'olandLfvlm', + 'incnrQnt', + 'oceanInputQnt', + 'fuelQnt', + 'etcQnt' ] }, - 'CYFD_CPCTY', - 'OUT_BSC_FCT_NM', - 'RMK' + 'nondspsQnt', + 'swtrmtFcltNm', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'fcltPstnCn' ], rowStartNum: 2, fixedColumns: 2, - tabKey: 'excretaSlurgeProcessingStatusInp', + tabKey: 'ST00110004', tabLabel: '분뇨찌꺼기(슬러지)처리현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts index dedf4af98..2c779ccd3 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn.ts @@ -5,22 +5,37 @@ import { ValueType } from "realgrid"; export const SewageSlurgeProcessingFacStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, styleName: 'rg-cell-readonly' }, // 찌꺼기처리시설명 - { name: 'SLG_FCT_NM', fieldName: '찌꺼기처리시설명', header: { text: '찌꺼기처리시설명' } }, + { name: 'fcltNm', fieldName: 'fcltNm', header: { text: '찌꺼기처리시설명' } }, // 처리시설 운영현황 - { name: 'TRT_METHOD', fieldName: '처리방식', header: { text: '처리방식' } }, - { name: 'SLG_FCT_CPC', fieldName: '시설용량(톤/일)', header: { text: '시설용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OPR_CPC', fieldName: '연간처리량(톤/년)', header: { text: '연간처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'ODY_RN_TIME', fieldName: '일평균가동시간(1~24)', header: { text: '일평균가동시간(1~24)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'YRSTP_RN_DYN', fieldName: '연중 가동일(1~330)', header: { text: '연중 가동일(1~330)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'OPR_RT', fieldName: '가동율(%)', header: { text: '가동율(%)' }, footer: footerDecimal1, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsMthExpln', fieldName: 'prcsMthExpln', header: { text: '처리방식' } }, + { name: 'fcltCpct', fieldName: 'fcltCpct', header: { text: '시설용량(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'anlPrcsQnt', fieldName: 'anlPrcsQnt', header: { text: '연간처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dayAvgOprtngHr', fieldName: 'dayAvgOprtngHr', header: { text: '일평균가동시간(1~24)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'anlOprtngDayCnt', fieldName: 'anlOprtngDayCnt', header: { text: '연중 가동일(1~330)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { + name: 'oprtngRt', + fieldName: 'oprtngRt', + header: { text: '가동율(%)' }, + footer: footerDecimal1, + editable: false, + styleName: 'rg-cell-total', + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + computed: { + numeratorField: 'anlPrcsQnt', + denominatorField: 'fcltCpct', + multiplierField: 'anlOprtngDayCnt', + type: 'quotientTimesMultiplierPercent' as const, + }, + }, // 비고 - { name: 'SLG_FCT_RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, // Hidden: 기본 정보 { name: 'STPN_SEQ', fieldName: 'STPN_SEQ', header: { text: 'STPN_SEQ' }, visible: false }, @@ -30,40 +45,42 @@ export const SewageSlurgeProcessingFacStatusInpColumn = { { name: 'BSNM_CD', fieldName: 'BSNM_CD', header: { text: 'BSNM_CD' }, visible: false }, { name: 'BSC_FCT_CD', fieldName: 'BSC_FCT_CD', header: { text: 'BSC_FCT_CD' }, visible: false }, - // Hidden: Check fields for change detection - { name: 'CHK_SLG_FCT_NM', fieldName: 'CHK_SLG_FCT_NM', header: { text: 'CHK_SLG_FCT_NM' }, visible: false }, - { name: 'CHK_TRT_METHOD', fieldName: 'CHK_TRT_METHOD', header: { text: 'CHK_TRT_METHOD' }, visible: false }, - { name: 'CHK_SLG_FCT_CPC', fieldName: 'CHK_SLG_FCT_CPC', header: { text: 'CHK_SLG_FCT_CPC' }, visible: false }, - { name: 'CHK_OPR_CPC', fieldName: 'CHK_OPR_CPC', header: { text: 'CHK_OPR_CPC' }, visible: false }, - { name: 'CHK_ODY_RN_TIME', fieldName: 'CHK_ODY_RN_TIME', header: { text: 'CHK_ODY_RN_TIME' }, visible: false }, - { name: 'CHK_YRSTP_RN_DYN', fieldName: 'CHK_YRSTP_RN_DYN', header: { text: 'CHK_YRSTP_RN_DYN' }, visible: false }, - { name: 'CHK_OPR_RT', fieldName: 'CHK_OPR_RT', header: { text: 'CHK_OPR_RT' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'SLG_FCT_NM', + 'fcltNm', { name: 'group_processing_fac_operating', header: { text: '처리시설 운영현황' }, items: [ - 'TRT_METHOD', - 'SLG_FCT_CPC', - 'OPR_CPC', - 'ODY_RN_TIME', - 'YRSTP_RN_DYN', - 'OPR_RT' + 'prcsMthExpln', + 'fcltCpct', + 'anlPrcsQnt', + 'dayAvgOprtngHr', + 'anlOprtngDayCnt', + 'oprtngRt' ] }, - 'SLG_FCT_RMK' + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', ], rowStartNum: 2, fixedColumns: 1, - tabKey: 'sewageSlurgeProcessingFacStatusInp', + tabKey: 'ST00110003', tabLabel: '하수찌꺼기 처리시설 운영현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts index 364c4e378..1d3e11f0d 100644 --- a/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn.ts @@ -5,212 +5,357 @@ import { ValueType } from "realgrid"; export const SlurgeProcessingFacStatusInpColumn = { columns: [ // 행정구역 - { name: 'SIDO_CD', fieldName: '시도', header: { text: '시도' }, editable: false }, - { name: 'ADST_CD', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, // 하수처리장명 - { name: 'BSC_FCT_NM', fieldName: '하수처리장명', header: { text: '하수처리장명' }, }, + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '하수처리장명' }, editable: false, styleName: 'rg-cell-readonly' }, // 찌꺼기발생량(톤/년, 탈수기준) - { name: 'TOT_AMNT', fieldName: '합계', header: { text: '합계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'P_AMNT', fieldName: '자체', header: { text: '자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'OUT_AMNT', fieldName: '외부유입량', header: { text: '외부유입량' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'gnamWhol', + fieldName: 'gnamWhol', + header: { text: '합계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['itslfQnt', 'otsdInflwQnt'], + type: 'sum' as const, + }, + }, + { name: 'itslfQnt', fieldName: 'itslfQnt', header: { text: '자체' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdInflwQnt', fieldName: 'otsdInflwQnt', header: { text: '외부유입량' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 이송처리장 - { name: 'OUT_FCT_NM', fieldName: '이송처리장', header: { text: '이송처리장' } }, + { name: 'transfTmplntNm', fieldName: 'transfTmplntNm', header: { text: '이송처리장' } }, // 이송처리량 - 쿼리에서 못찾음 (임의) - { name: 'a', fieldName: '이송처리량(m³/년)', header: { text: '이송처리량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'transfPrcsQnt', fieldName: 'transfPrcsQnt', header: { text: '이송처리량(m³/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 함수율 - { name: 'DSP_MOIRAT', fieldName: '함수율(%, 탈수기준)', header: { text: '함수율(%, 탈수기준)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'srpFncRt', fieldName: 'srpFncRt', header: { text: '함수율(%, 탈수기준)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 찌꺼기처리량합계 - { name: 'DSP_SM', fieldName: '찌꺼기처리량합계(톤/년)', header: { text: '찌꺼기처리량합계(톤/년)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholSrpPrcsQnt', + fieldName: 'wholSrpPrcsQnt', + header: { text: '찌꺼기처리량합계(톤/년)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', + 'itslfTs1AltdQnt', + 'itslfTs1DryQnt', + 'itslfTs1FuelQnt', + 'itslfTs2CmpstgQnt', + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + 'cparTs1Whol', + ], + type: 'sum' as const, + } + }, // 자체 찌꺼기 처리량 > 자체찌꺼기처리량소계 - { name: 'DSP_SELF_SM', fieldName: '자체찌꺼기처리량소계', header: { text: '자체찌꺼기처리량소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'itslfTs1Whol', + fieldName: 'itslfTs1Whol', + header: { text: '자체찌꺼기처리량소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', + 'itslfTs1AltdQnt', + 'itslfTs1DryQnt', + 'itslfTs1FuelQnt', + 'itslfTs2CmpstgQnt', + ], + type: 'sum' as const, + } + }, // 자체 찌꺼기 처리량 > 매립 - { name: 'O_DSP_RCLM', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1RclmQnt', fieldName: 'itslfTs1RclmQnt', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 소각(1차) - { name: 'O_DSP_INCNR', fieldName: '소각(1차)', header: { text: '소각(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1IncnrQnt', fieldName: 'itslfTs1IncnrQnt', header: { text: '소각(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 소각 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'b1', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b2', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b3', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b4', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'b5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'incnrPrcsWhol', + fieldName: 'incnrPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'incnrPrcsLfvlm', + 'incnrPrcsPrdctRmtrlQnt', + 'incnrPrcsFmlyrQnt', + ], + type: 'sum' as const, + } + }, + { name: 'incnrPrcsLfvlm', fieldName: 'incnrPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsPrdctRmtrlQnt', fieldName: 'incnrPrcsPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsFmlyrQnt', fieldName: 'incnrPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'incnrPrcsFcltNm', fieldName: 'incnrPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 고화(1차) - 쿼리에 없음 (임의 생성) - { name: 'c1', fieldName: '고화(1차)', header: { text: '고화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1AltdQnt', fieldName: 'itslfTs1AltdQnt', header: { text: '고화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 고화 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'c2', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c3', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c4', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'c5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'stbznPrcsWhol', + fieldName: 'stbznPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'stbznPrcsLfvlm', + 'stbznPrcsFmlyrQnt', + ], + type: 'sum' as const, + } + }, + { name: 'stbznPrcsLfvlm', fieldName: 'stbznPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'stbznPrcsFmlyrQnt', fieldName: 'stbznPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'stbznPrcsFcltNm', fieldName: 'stbznPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 건조(1차) - { name: 'DSP_DESIC', fieldName: '건조(1차)', header: { text: '건조(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1DryQnt', fieldName: 'itslfTs1DryQnt', header: { text: '건조(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 건조 후 처리(2차) - 쿼리에 없음 - { name: 'd1', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd2', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd3', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd4', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd5', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd6', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd7', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'd8', fieldName: '시설(업체명)', header: { text: '시설(업체명)' }, editable: true }, + { + name: 'dryPrcsWhol', + fieldName: 'dryPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'dryPrcsLfvlm', + 'dryPrcsIncnrQnt', + 'dryPrcsFuelQnt', + 'dryPrcsPrdctRmtrlQnt', + 'dryPrcsFmlyrQnt', + 'dryPrcsCmpstgQnt', + ], + type: 'sum' as const, + } + }, + { name: 'dryPrcsLfvlm', fieldName: 'dryPrcsLfvlm', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsIncnrQnt', fieldName: 'dryPrcsIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFuelQnt', fieldName: 'dryPrcsFuelQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsPrdctRmtrlQnt', fieldName: 'dryPrcsPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFmlyrQnt', fieldName: 'dryPrcsFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsCmpstgQnt', fieldName: 'dryPrcsCmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'dryPrcsFcltNm', fieldName: 'dryPrcsFcltNm', header: { text: '시설(업체명)' }, editable: true }, // 자체 찌꺼기 처리량 > 탄화(1차) - 쿼리에 없음 (임의 생성) - { name: 'e1', fieldName: '탄화(1차)', header: { text: '탄화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs1FuelQnt', fieldName: 'itslfTs1FuelQnt', header: { text: '탄화(1차)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 탄화 후 처리(2차) - 쿼리에 없음 (임의 생성) - { name: 'e2', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e3', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e4', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'e5', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'fuelPrcsWhol', + fieldName: 'fuelPrcsWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: ['fuelPrcsIncnrQnt', 'fuelPrcsFuelQnt'], + type: 'sum' as const, + } + }, + { name: 'fuelPrcsIncnrQnt', fieldName: 'fuelPrcsIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelPrcsFuelQnt', fieldName: 'fuelPrcsFuelQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'fuelPrcsFcltNm', fieldName: 'fuelPrcsFcltNm', header: { text: '시설(업체명)' } }, // 자체 찌꺼기 처리량 > 퇴비화 - 쿼리에 없음 (임의 생성) - { name: 'f1', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'itslfTs2CmpstgQnt', fieldName: 'itslfTs2CmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 자체 찌꺼기 처리량 > 퇴비화 > 시설(업체명) - 쿼리에 없음 (임의 생성) - { name: 'f2', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { name: 'cmpsPrcsFcltBzentyNm', fieldName: 'cmpsPrcsFcltBzentyNm', header: { text: '시설(업체명)' } }, // 외부위탁 처리량(톤/년) - { name: 'DSP_CONSIGNMENT', fieldName: '소계', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g1', fieldName: '매립', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g2', fieldName: '소각', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g3', fieldName: '연료', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g4', fieldName: '제품원료', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g5', fieldName: '복토재', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g6', fieldName: '퇴비화', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'g7', fieldName: '시설(업체명)', header: { text: '시설(업체명)' } }, + { + name: 'otsdCnsgnWhol', + fieldName: 'otsdCnsgnWhol', + header: { text: '소계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + ], + type: 'sum' as const, + } + }, + { name: 'otsdCnsgnRclmQnt', fieldName: 'otsdCnsgnRclmQnt', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnIncnrQnt', fieldName: 'otsdCnsgnIncnrQnt', header: { text: '소각' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFuelPwplQnt', fieldName: 'otsdCnsgnFuelPwplQnt', header: { text: '연료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnPrdctRmtrlQnt', fieldName: 'otsdCnsgnPrdctRmtrlQnt', header: { text: '제품원료' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFmlyrQnt', fieldName: 'otsdCnsgnFmlyrQnt', header: { text: '복토재' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnCmpstgQnt', fieldName: 'otsdCnsgnCmpstgQnt', header: { text: '퇴비화' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'otsdCnsgnFcltNm', fieldName: 'otsdCnsgnFcltNm', header: { text: '시설(업체명)' } }, // 수도권광역위탁처리량 - { name: 'DSP_WIDE', fieldName: '수도권광역위탁처리량(톤/년)', header: { text: '수도권광역위탁처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'cparTs1Whol', fieldName: 'cparTs1Whol', header: { text: '수도권광역위탁처리량(톤/년)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 비고 - { name: 'RMK', fieldName: '비고', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - // Hidden: Check fields - { name: 'CHK_TOT_AMNT', fieldName: 'CHK_TOT_AMNT', header: { text: 'CHK_TOT_AMNT' }, visible: false }, - { name: 'CHK_P_AMNT', fieldName: 'CHK_P_AMNT', header: { text: 'CHK_P_AMNT' }, visible: false }, - { name: 'CHK_OUT_AMNT', fieldName: 'CHK_OUT_AMNT', header: { text: 'CHK_OUT_AMNT' }, visible: false }, - { name: 'CHK_DSP_MOIRAT', fieldName: 'CHK_DSP_MOIRAT', header: { text: 'CHK_DSP_MOIRAT' }, visible: false }, - { name: 'CHK_DSP_SM', fieldName: 'CHK_DSP_SM', header: { text: 'CHK_DSP_SM' }, visible: false }, - { name: 'CHK_DSP_SELF_SM', fieldName: 'CHK_DSP_SELF_SM', header: { text: 'CHK_DSP_SELF_SM' }, visible: false }, - { name: 'CHK_O_DSP_RCLM', fieldName: 'CHK_O_DSP_RCLM', header: { text: 'CHK_O_DSP_RCLM' }, visible: false }, - { name: 'CHK_O_DSP_INCNR', fieldName: 'CHK_O_DSP_INCNR', header: { text: 'CHK_O_DSP_INCNR' }, visible: false }, - { name: 'CHK_DSP_DESIC', fieldName: 'CHK_DSP_DESIC', header: { text: 'CHK_DSP_DESIC' }, visible: false }, - { name: 'CHK_DSP_CONSIGNMENT', fieldName: 'CHK_DSP_CONSIGNMENT', header: { text: 'CHK_DSP_CONSIGNMENT' }, visible: false }, - { name: 'CHK_DSP_WIDE', fieldName: 'CHK_DSP_WIDE', header: { text: 'CHK_DSP_WIDE' }, visible: false }, - { name: 'CHK_OUT_FCT_NM', fieldName: 'CHK_OUT_FCT_NM', header: { text: 'CHK_OUT_FCT_NM' }, visible: false }, + // hidden + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false }, + { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설물일련번호' }, visible: false }, + { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '처리장코드' }, visible: false }, ], columnLayout: [ { name: 'group_admin', header: { text: '행정구역' }, - items: ['SIDO_CD', 'ADST_CD'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, - 'BSC_FCT_NM', + 'swtrmtFcltNm', { name: 'group_occur_amount', header: { text: '찌꺼기발생량(톤/년, 탈수기준)' }, items: [ - 'TOT_AMNT', - 'P_AMNT', - 'OUT_AMNT' + 'gnamWhol', + 'itslfQnt', + 'otsdInflwQnt' ] }, - 'OUT_FCT_NM', - 'a', - 'DSP_MOIRAT', - 'DSP_SM', + 'transfTmplntNm', + 'transfPrcsQnt', + 'srpFncRt', + 'wholSrpPrcsQnt', { name: 'group_self_treatment', header: { text: '자체 찌꺼기 처리량(톤/년)' }, items: [ - 'DSP_SELF_SM', - 'O_DSP_RCLM', - 'O_DSP_INCNR', + 'itslfTs1Whol', + 'itslfTs1RclmQnt', + 'itslfTs1IncnrQnt', { name: 'group_incinerate_2nd', header: { text: '소각 후 처리(2차)' }, items: [ - 'b1', - 'b2', - 'b3', - 'b4', - 'b5' + 'incnrPrcsWhol', + 'incnrPrcsLfvlm', + 'incnrPrcsPrdctRmtrlQnt', + 'incnrPrcsFmlyrQnt', + 'incnrPrcsFcltNm' ] }, - 'c1', + 'itslfTs1AltdQnt', { name: 'group_solidify_2nd', header: { text: '고화 후 처리(2차)' }, items: [ - 'c2', - 'c3', - 'c4', - 'c5' + 'stbznPrcsWhol', + 'stbznPrcsLfvlm', + 'stbznPrcsFmlyrQnt', + 'stbznPrcsFcltNm' ] }, - 'DSP_DESIC', + 'itslfTs1DryQnt', { name: 'group_drying_2nd', header: { text: '건조 후 처리(2차)' }, items: [ - 'd1', - 'd2', - 'd3', - 'd4', - 'd5', - 'd6', - 'd7', - 'd8' + 'dryPrcsWhol', + 'dryPrcsLfvlm', + 'dryPrcsIncnrQnt', + 'dryPrcsFuelQnt', + 'dryPrcsPrdctRmtrlQnt', + 'dryPrcsFmlyrQnt', + 'dryPrcsCmpstgQnt', + 'dryPrcsFcltNm' ] }, - 'e1', + 'itslfTs1FuelQnt', { name: 'group_carbonize_2nd', header: { text: '탄화 후 처리(2차)' }, items: [ - 'e2', - 'e3', - 'e4', - 'e5' + 'fuelPrcsWhol', + 'fuelPrcsIncnrQnt', + 'fuelPrcsFuelQnt', + 'fuelPrcsFcltNm' ] }, - 'f1', - 'f2' + 'itslfTs2CmpstgQnt', + 'cmpsPrcsFcltBzentyNm' ] }, { name: 'group_external_treatment', header: { text: '외부위탁 처리량(톤/년)' }, items: [ - 'DSP_CONSIGNMENT', - 'g1', - 'g2', - 'g3', - 'g4', - 'g5', - 'g6', - 'g7' + 'otsdCnsgnWhol', + 'otsdCnsgnRclmQnt', + 'otsdCnsgnIncnrQnt', + 'otsdCnsgnFuelPwplQnt', + 'otsdCnsgnPrdctRmtrlQnt', + 'otsdCnsgnFmlyrQnt', + 'otsdCnsgnCmpstgQnt', + 'otsdCnsgnFcltNm' ] }, - 'DSP_WIDE', - 'RMK' + 'cparTs1Whol', + 'rmrk', + 'admdstCd', + 'admdstNm', + 'fcltSn', + 'swtrmtFcltCd' ], rowStartNum: 3, fixedColumns: 2, - tabKey: 'slurgeProcessingFacStatusInp', + tabKey: 'ST00110001', tabLabel: '찌꺼기(슬러지)처리시설현황' }; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts index db1a72a59..709cc473d 100644 --- a/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn.ts @@ -31,6 +31,25 @@ function dropdownColumn( }; } +/** 하수처리수 재이용현황(천톤/년) — 장내·장외 세부 항목 합산 필드 */ +const PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS = [ + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt', + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt', +] as const; + // 하수도 통계 관리 > 10. 물 재이용 > tab 1: 공공하수처리수 재이용 export const PublicSewageTreatWaterReuseInpColumn = { columns: [ @@ -102,10 +121,44 @@ export const PublicSewageTreatWaterReuseInpColumn = { { name: 'ruswtPumpLen', fieldName: 'ruswtPumpLen', header: { text: '공급펌프(톤/일)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 하수처리수 재이용현황 > 총계 - { name: 'wholUseQnt', fieldName: 'wholUseQnt', header: { text: '총계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'wholUseQnt', + fieldName: 'wholUseQnt', + header: { text: '총계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [...PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS], + type: 'sum' as const, + }, + }, // 하수처리수 재이용현황 > 장내용수 - { name: 'ithlWshwtrQnt', fieldName: 'ithlWshwtrQnt', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'ithlWshwtrQnt', + fieldName: 'ithlWshwtrQnt', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'ithlClnQty', + 'ithlFrzgQty', + 'ithlLdhcUswtrQnt', + 'ithlDltnUswtrQnt', + 'ithlRuswtrQnt', + 'ithlEtcQnt', + 'ithlUnsdQnt', + ], + type: 'sum' as const, + }, + }, { name: 'ithlClnQty', fieldName: 'ithlClnQty', header: { text: '세척수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, { name: 'ithlFrzgQty', fieldName: 'ithlFrzgQty', header: { text: '냉각수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, { name: 'ithlLdhcUswtrQnt', fieldName: 'ithlLdhcUswtrQnt', header: { text: '청소수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, @@ -115,7 +168,29 @@ export const PublicSewageTreatWaterReuseInpColumn = { { name: 'ithlUnsdQnt', fieldName: 'ithlUnsdQnt', header: { text: '장내기타용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 하수처리수 재이용현황 > 장외용수 - { name: 'otcntrUswtrWhol', fieldName: 'otcntrUswtrWhol', header: { text: '계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'otcntrUswtrWhol', + fieldName: 'otcntrUswtrWhol', + header: { text: '계' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + fields: [ + 'otcntrClnRstrmQnt', + 'otcntrWshwtrQnt', + 'otcntrLdhcUswtrQnt', + 'otcntrHdrplUswtrQnt', + 'otcntrRvrKeepQnt', + 'agrcltUswtrPstnQnt', + 'ugwtrChargQnt', + 'mdsrUswtrQnt', + ], + type: 'sum' as const, + }, + }, { name: 'otcntrClnRstrmQnt', fieldName: 'otcntrClnRstrmQnt', header: { text: '청소·화장실용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, { name: 'otcntrWshwtrQnt', fieldName: 'otcntrWshwtrQnt', header: { text: '세척·살수용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, { name: 'otcntrLdhcUswtrQnt', fieldName: 'otcntrLdhcUswtrQnt', header: { text: '조경용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, @@ -126,7 +201,21 @@ export const PublicSewageTreatWaterReuseInpColumn = { { name: 'mdsrUswtrQnt', fieldName: 'mdsrUswtrQnt', header: { text: '공업용수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly', editable: false }, // 재이용률 - { name: 'rtlztnRt', fieldName: 'rtlztnRt', header: { text: '재이용률(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { + name: 'rtlztnRt', + fieldName: 'rtlztnRt', + header: { text: '재이용률(%)' }, + editable: false, + footer: footerDecimal1, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + styleName: 'rg-cell-total', + computed: { + numeratorFields: [...PUBLIC_SEWAGE_REUSE_USE_QNT_SUM_FIELDS], + denominatorFields: ['wholPrcsQnt'], + type: 'pblswrgPopltnSuprtRate' as const, + }, + }, // 비고 { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, @@ -142,7 +231,10 @@ export const PublicSewageTreatWaterReuseInpColumn = { { name: 'group_admin', header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, { name: 'group_facility', diff --git a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts index 9455637b0..f5189c746 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn.ts @@ -139,7 +139,10 @@ export const RainFallUtilFactStatusInpColumn = { { name: 'group_admin', header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'fcltNm', 'bdstTypCn', diff --git a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts index ee62e6c82..4930c51d2 100644 --- a/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn.ts @@ -157,7 +157,10 @@ export const RecycledWaterStatusInpColumn = { { name: 'group_admin', header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'fcltNm', 'bldgSeCn', diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts index 7a31a2162..0a2225dc8 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn.ts @@ -40,7 +40,10 @@ export const TreatWaterReuseBusinessApprStatusInpColumn = { { name: 'group_admin', header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'bplcNm', 'rprsvNm', diff --git a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts index 4b93706a1..69fc6311c 100644 --- a/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts +++ b/apps/st/src/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn.ts @@ -40,7 +40,10 @@ export const TreatWaterReuseDesignConstStatusInpColumn = { { name: 'group_admin', header: { text: '행정구역' }, - items: ['ctpvNm', 'sggNm'] + items: [ + { column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] }, + 'sggNm', + ], }, 'bplcNm', 'rprsvNm', diff --git a/apps/st/src/components/util/grid/computedFieldCallbacks.ts b/apps/st/src/components/util/grid/computedFieldCallbacks.ts index 43ce816f6..0506bee8a 100644 --- a/apps/st/src/components/util/grid/computedFieldCallbacks.ts +++ b/apps/st/src/components/util/grid/computedFieldCallbacks.ts @@ -328,6 +328,25 @@ export function createQuotientValueCallback(fields: string[]): DataFieldValueCal }; } +/** (numerator / denominator) × multiplier × 100 — 분모 0이면 0 */ +export function createQuotientTimesMultiplierPercentValueCallback( + numeratorField: string, + denominatorField: string, + multiplierField: string, +): DataFieldValueCallback { + return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => { + const getVal = (field: string) => { + const idx = fieldNamesArr.indexOf(field); + return idx >= 0 ? toNum(values[idx]) : 0; + }; + const denominator = getVal(denominatorField); + if (denominator === 0) return 0; + const quotient = getVal(numeratorField) / denominator; + const multiplier = getVal(multiplierField); + return truncateToFirstDecimal(quotient * multiplier * 100); + }; +} + /** 연산 타입에 맞는 valueCallback 반환 */ export function getComputedValueCallback(config: ComputedFieldConfig): DataFieldValueCallback { switch (config.type) { @@ -384,5 +403,11 @@ export function getComputedValueCallback(config: ComputedFieldConfig): DataField config.numeratorFields, config.denominatorField, ); + case 'quotientTimesMultiplierPercent': + return createQuotientTimesMultiplierPercentValueCallback( + config.numeratorField, + config.denominatorField, + config.multiplierField, + ); } } diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts new file mode 100644 index 000000000..61baeca8e --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-related-service.ts @@ -0,0 +1,163 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import type { + ExcretaRelatedIndustryApiResponse, + ExcretaRelatedIndustrySaveRequest, + ExcretaRelatedIndustrySubmitRequest, + PrivateSewageTreatRelatedIndustryApiResponse, + PrivateSewageTreatRelatedIndustrySaveRequest, + PrivateSewageTreatRelatedIndustrySubmitRequest, + PublicInstitutionExcretaIndustryApiResponse, + PublicInstitutionExcretaIndustrySaveRequest, + PublicInstitutionExcretaIndustrySubmitRequest, + PublicInstitutionSewerIndustryApiResponse, + PublicInstitutionSewerIndustrySaveRequest, + PublicInstitutionSewerIndustrySubmitRequest, + SewageExcretaRelatedRequestParams, + SewageExcretaRelatedSaveResponse, + SewageExcretaRelatedSubmitResponse, + StatisticsInputSewageExcretaRelatedService, +} from "../../model/input/statistics-input-sewage-excreta-related-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewage-excreta-related`; + +export class StatisticsInputSewageExcretaRelatedApiService + implements StatisticsInputSewageExcretaRelatedService +{ + /** 공공기관 하수분야 조회 — ST00120001 */ + public async getPublicInstitutionSewerIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-institution-sewer-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공기관 하수분야 저장 — ST00120001 */ + public async savePublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-sewer-industry/save`, + request, + ); + return response.data; + } + + /** 공공기관 하수분야 제출 — ST00120001 */ + public async submitPublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-sewer-industry/submit`, + request, + ); + return response.data; + } + + /** 공공기관 분뇨분야 조회 — ST00120002 */ + public async getPublicInstitutionExcretaIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/public-institution-excreta-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 공공기관 분뇨분야 저장 — ST00120002 */ + public async savePublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-excreta-industry/save`, + request, + ); + return response.data; + } + + /** 공공기관 분뇨분야 제출 — ST00120002 */ + public async submitPublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/public-institution-excreta-industry/submit`, + request, + ); + return response.data; + } + + /** 분뇨수집·운반업 조회 — ST00120003 */ + public async getExcretaRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/excreta-related-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨수집·운반업 저장 — ST00120003 */ + public async saveExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-related-industry/save`, + request, + ); + return response.data; + } + + /** 분뇨수집·운반업 제출 — ST00120003 */ + public async submitExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-related-industry/submit`, + request, + ); + return response.data; + } + + /** 개인하수처리관련업 조회 — ST00120004 */ + public async getPrivateSewageTreatRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/private-sewage-treat-related-industry`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 개인하수처리관련업 저장 — ST00120004 */ + public async savePrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/private-sewage-treat-related-industry/save`, + request, + ); + return response.data; + } + + /** 개인하수처리관련업 제출 — ST00120004 */ + public async submitPrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/private-sewage-treat-related-industry/submit`, + request, + ); + return response.data; + } +} + +export const statisticsInputSewageExcretaRelatedApi = + new StatisticsInputSewageExcretaRelatedApiService(); diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts new file mode 100644 index 000000000..4d47e0e24 --- /dev/null +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service.ts @@ -0,0 +1,182 @@ +import { stClient } from "@/shared/api/client"; +import { withAuthHeader } from "@/shared/api/with-auth-header"; +import { ENDPOINT } from "@/shared/config"; + +import type { + DigestorOperatingStatusApiResponse, + DigestorOperatingStatusSaveRequest, + DigestorOperatingStatusSaveResponse, + DigestorOperatingStatusSubmitRequest, + DigestorOperatingStatusSubmitResponse, + ExcretaSlurgeProcessingStatusApiResponse, + ExcretaSlurgeProcessingStatusSaveRequest, + ExcretaSlurgeProcessingStatusSaveResponse, + ExcretaSlurgeProcessingStatusSubmitRequest, + ExcretaSlurgeProcessingStatusSubmitResponse, + PublicSewageFacilitiesListApiResponse, + PublicSewageFacilitiesListRequest, + SewageExcretaSlurgeRequestParams, + SewageSlurgeProcessingFacStatusApiResponse, + SewageSlurgeProcessingFacStatusSaveRequest, + SewageSlurgeProcessingFacStatusSaveResponse, + SewageSlurgeProcessingFacStatusSubmitRequest, + SewageSlurgeProcessingFacStatusSubmitResponse, + SlurgeProcessingFacStatusApiResponse, + SlurgeProcessingFacStatusSaveRequest, + SlurgeProcessingFacStatusSaveResponse, + SlurgeProcessingFacStatusSubmitRequest, + SlurgeProcessingFacStatusSubmitResponse, + StatisticsInputSewageExcretaSlurgeService, +} from "../../model/input/statistics-input-sewage-excreta-slurge-interface"; + +const BASE = `${ENDPOINT.ST_SERVICE.INPUT}/sewage-excreta-slurge`; + +export class StatisticsInputSewageExcretaSlurgeApiService + implements StatisticsInputSewageExcretaSlurgeService +{ + /** 찌꺼기(슬러지)처리시설현황 조회 — ST00110001 */ + public async getSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/slurge-processing-fac-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 찌꺼기(슬러지)처리시설현황 저장 — ST00110001 */ + public async saveSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/slurge-processing-fac-status/save`, + request, + ); + return response.data; + } + + /** 찌꺼기(슬러지)처리시설현황 제출 — ST00110001 */ + public async submitSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/slurge-processing-fac-status/submit`, + request, + ); + return response.data; + } + + /** 소화조 운영현황 조회 — ST00110002 */ + public async getDigestorOperatingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/digestor-operating-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 소화조 운영현황 저장 — ST00110002 */ + public async saveDigestorOperatingStatus( + request: DigestorOperatingStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/digestor-operating-status/save`, + request, + ); + return response.data; + } + + /** 소화조 운영현황 제출 — ST00110002 */ + public async submitDigestorOperatingStatus( + request: DigestorOperatingStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/digestor-operating-status/submit`, + request, + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 조회 — ST00110003 */ + public async getSewageSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/sewage-slurge-processing-fac-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 저장 — ST00110003 */ + public async saveSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-slurge-processing-fac-status/save`, + request, + ); + return response.data; + } + + /** 하수찌꺼기 처리시설 운영현황 제출 — ST00110003 */ + public async submitSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/sewage-slurge-processing-fac-status/submit`, + request, + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 조회 — ST00110004 */ + public async getExcretaSlurgeProcessingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise { + const response = await stClient.get( + `${BASE}/excreta-slurge-processing-status`, + withAuthHeader({ params }), + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 저장 — ST00110004 */ + public async saveExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSaveRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-slurge-processing-status/save`, + request, + ); + return response.data; + } + + /** 분뇨찌꺼기(슬러지)처리현황 제출 — ST00110004 */ + public async submitExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSubmitRequest, + ): Promise { + const response = await stClient.put( + `${BASE}/excreta-slurge-processing-status/submit`, + request, + ); + return response.data; + } + + /** 공공하수처리시설 목록 조회 */ + public async getPublicSewageFacilitiesList( + params: PublicSewageFacilitiesListRequest, + ): Promise { + const response = await stClient.get( + `${BASE}/public-sewage-facilities`, + withAuthHeader({ params }), + ); + return response.data; + } +} + +export const statisticsInputSewageExcretaSlurgeApi = + new StatisticsInputSewageExcretaSlurgeApiService(); diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts new file mode 100644 index 000000000..f2db92d5a --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts @@ -0,0 +1,328 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 하수·분뇨관련산업 — 공공기관 하수분야(ST00120001) `PublicInstitutionSewerIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD = "ST00120001" as const; + +/** 검증 결과 한 건 */ +export interface SewageExcretaRelatedVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewageExcretaRelatedVerificationResult = Record< + string, + SewageExcretaRelatedVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface SewageExcretaRelatedCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +/** 하수·분뇨관련산업 조회 요청 파라미터 */ +export interface SewageExcretaRelatedRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + mode?: string; +} + +/** + * ST00120001 조회 `data.publicInstitutionSewerIndustryList[]` 한 행. + * `PublicInstitutionSewerIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicInstitutionSewerIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + ognzSn: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PublicInstitutionSewerIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + publicInstitutionSewerIndustryList: PublicInstitutionSewerIndustryItem[]; +} + +export type PublicInstitutionSewerIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** 하수·분뇨관련산업 — 공공기관 분뇨분야(ST00120002) `PublicInstitutionExcretaIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PUBLIC_INSTITUTION_EXCRETA_STATS_CD = "ST00120002" as const; + +/** + * ST00120002 조회 `data.publicInstitutionExcretaIndustryList[]` 한 행. + * `PublicInstitutionExcretaIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PublicInstitutionExcretaIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + ognzSn: string; + ognzNm: string; + empGrssCnt: number; + pbadmsNope: number; + techWholNope: number; + engrkCnt: number; + mchnCnt: number; + prvtmCnt: number; + chmengCnt: number; + envCnt: number; + enjbEtcCnt: number; + etcCnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PublicInstitutionExcretaIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + publicInstitutionExcretaIndustryList: PublicInstitutionExcretaIndustryItem[]; +} + +export type PublicInstitutionExcretaIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** 하수·분뇨관련산업 — 분뇨수집·운반업(ST00120003) `ExcretaRelatedIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_EXCRETA_RELATED_INDUSTRY_STATS_CD = "ST00120003" as const; + +/** + * ST00120003 조회 `data.excretaRelatedIndustryList[]` 한 행. + * `ExcretaRelatedIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaRelatedIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + bzentyCnt: number; + empNope: number; + fcltSecrPrstCnt: number; + n3TonUdrCnt: number; + n4TonUdrCnt: number; + n8TonUdrCnt: number; + etcCnt: number; + clctCnvncPrfmncQnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface ExcretaRelatedIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + excretaRelatedIndustryList: ExcretaRelatedIndustryItem[]; +} + +export type ExcretaRelatedIndustryApiResponse = ApiResponse & { + message: string; +}; + +/** 하수·분뇨관련산업 — 개인하수처리관련업(ST00120004) `PrivateSewageTreatRelatedIndustryInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_PRIVATE_SEWAGE_TREAT_RELATED_INDUSTRY_STATS_CD = "ST00120004" as const; + +/** + * ST00120004 조회 `data.privateSewageTreatRelatedIndustryList[]` 한 행. + * `PrivateSewageTreatRelatedIndustryInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface PrivateSewageTreatRelatedIndustryItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + designMdsrCnt: number; + designMdsrNope: number; + mfbizCnt: number; + mfbizMdsrNope: number; + mngCnt: number; + mngMdsrNope: number; + rmrk: string | null; + verificationResult?: SewageExcretaRelatedVerificationResult; +} + +export interface PrivateSewageTreatRelatedIndustryResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaRelatedCommonInfo; + privateSewageTreatRelatedIndustryList: PrivateSewageTreatRelatedIndustryItem[]; +} + +export type PrivateSewageTreatRelatedIndustryApiResponse = + ApiResponse & { + message: string; + }; + +/** ST00120001·ST00120002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·계산필드·검증결과 제외) */ +export type PublicInstitutionSewerIndustrySaveItem = Omit< + PublicInstitutionSewerIndustryItem, + "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" +>; + +export type PublicInstitutionExcretaIndustrySaveItem = Omit< + PublicInstitutionExcretaIndustryItem, + "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" +>; + +/** ST00120003·ST00120004 저장 — `saveList` 항목 (`sggNm`·계산필드·검증결과 제외) */ +export type ExcretaRelatedIndustrySaveItem = Omit< + ExcretaRelatedIndustryItem, + "sggNm" | "verificationResult" | "fcltSecrPrstCnt" +>; + +export type PrivateSewageTreatRelatedIndustrySaveItem = Omit< + PrivateSewageTreatRelatedIndustryItem, + "sggNm" | "verificationResult" +>; + +export interface SewageExcretaRelatedSaveRequestBase { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; +} + +export interface PublicInstitutionSewerIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + noApplicable?: boolean; + saveList: PublicInstitutionSewerIndustrySaveItem[]; + deleteList: PublicInstitutionSewerIndustrySaveItem[]; +} + +export interface PublicInstitutionExcretaIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + noApplicable?: boolean; + saveList: PublicInstitutionExcretaIndustrySaveItem[]; + deleteList: PublicInstitutionExcretaIndustrySaveItem[]; +} + +export interface ExcretaRelatedIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + saveList: ExcretaRelatedIndustrySaveItem[]; +} + +export interface PrivateSewageTreatRelatedIndustrySaveRequest extends SewageExcretaRelatedSaveRequestBase { + saveList: PrivateSewageTreatRelatedIndustrySaveItem[]; +} + +export type SewageExcretaRelatedSaveResponse = ApiResponse & { + message?: string; +}; + +export interface SewageExcretaRelatedSubmitRequestBase extends SewageExcretaRelatedSaveRequestBase { + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; +} + +export type PublicInstitutionSewerIndustrySubmitItem = PublicInstitutionSewerIndustrySaveItem; + +export interface PublicInstitutionSewerIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + noApplicable?: boolean; + submitList: PublicInstitutionSewerIndustrySubmitItem[]; + deleteList: PublicInstitutionSewerIndustrySubmitItem[]; +} + +export type PublicInstitutionExcretaIndustrySubmitItem = PublicInstitutionExcretaIndustrySaveItem; + +export interface PublicInstitutionExcretaIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + noApplicable?: boolean; + submitList: PublicInstitutionExcretaIndustrySubmitItem[]; + deleteList: PublicInstitutionExcretaIndustrySubmitItem[]; +} + +export type ExcretaRelatedIndustrySubmitItem = ExcretaRelatedIndustrySaveItem; + +export interface ExcretaRelatedIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + submitList: ExcretaRelatedIndustrySubmitItem[]; +} + +export type PrivateSewageTreatRelatedIndustrySubmitItem = PrivateSewageTreatRelatedIndustrySaveItem; + +export interface PrivateSewageTreatRelatedIndustrySubmitRequest extends SewageExcretaRelatedSubmitRequestBase { + submitList: PrivateSewageTreatRelatedIndustrySubmitItem[]; +} + +export interface SewageExcretaRelatedSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +export interface StatisticsInputSewageExcretaRelatedService { + /** 공공기관 하수분야 조회 — ST00120001 */ + getPublicInstitutionSewerIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 공공기관 하수분야 저장 — ST00120001 */ + savePublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySaveRequest, + ): Promise; + /** 공공기관 하수분야 제출 — ST00120001 */ + submitPublicInstitutionSewerIndustry( + request: PublicInstitutionSewerIndustrySubmitRequest, + ): Promise; + /** 공공기관 분뇨분야 조회 — ST00120002 */ + getPublicInstitutionExcretaIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 공공기관 분뇨분야 저장 — ST00120002 */ + savePublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySaveRequest, + ): Promise; + /** 공공기관 분뇨분야 제출 — ST00120002 */ + submitPublicInstitutionExcretaIndustry( + request: PublicInstitutionExcretaIndustrySubmitRequest, + ): Promise; + /** 분뇨수집·운반업 조회 — ST00120003 */ + getExcretaRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 분뇨수집·운반업 저장 — ST00120003 */ + saveExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySaveRequest, + ): Promise; + /** 분뇨수집·운반업 제출 — ST00120003 */ + submitExcretaRelatedIndustry( + request: ExcretaRelatedIndustrySubmitRequest, + ): Promise; + /** 개인하수처리관련업 조회 — ST00120004 */ + getPrivateSewageTreatRelatedIndustryList( + params: SewageExcretaRelatedRequestParams, + ): Promise; + /** 개인하수처리관련업 저장 — ST00120004 */ + savePrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySaveRequest, + ): Promise; + /** 개인하수처리관련업 제출 — ST00120004 */ + submitPrivateSewageTreatRelatedIndustry( + request: PrivateSewageTreatRelatedIndustrySubmitRequest, + ): Promise; +} diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts new file mode 100644 index 000000000..4c575fe84 --- /dev/null +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts @@ -0,0 +1,539 @@ +import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface"; + +/** 슬러지 처리 — 찌꺼기(슬러지)처리시설현황(ST00110001) `SlurgeProcessingFacStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD = "ST00110001" as const; + +/** 슬러지 처리 — 소화조 운영현황(ST00110002) `DigestorOperatingStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD = "ST00110002" as const; + +/** 슬러지 처리 — 하수찌꺼기 처리시설 운영현황(ST00110003) `SewageSlurgeProcessingFacStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD = "ST00110003" as const; + +/** 슬러지 처리 — 분뇨찌꺼기(슬러지)처리현황(ST00110004) `ExcretaSlurgeProcessingStatusInpColumn.tabKey` */ +export const SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD = "ST00110004" as const; + +/** 검증 결과 한 건 */ +export interface SewageExcretaSlurgeVerificationResultItem { + vrfcCd: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string; +} + +/** 필드명 → 검증 결과 배열 */ +export type SewageExcretaSlurgeVerificationResult = Record< + string, + SewageExcretaSlurgeVerificationResultItem[] +>; + +/** 조회 `data.commonInfo` */ +export interface SewageExcretaSlurgeCommonInfo { + statsYr: string; + ctpvNm: string; + sewerBzmnCd: string; + statsCd: string; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +/** 슬러지 처리 조회 요청 파라미터 */ +export interface SewageExcretaSlurgeRequestParams { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsYr?: string; + fcltNm?: string; + mode?: string; +} + +/** + * ST00110001 조회 `data.slurgeProcessingFacStatusList[]` 한 행. + * `SlurgeProcessingFacStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SlurgeProcessingFacStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltNm: string; + swtrmtFcltCd: string; + gnamWhol: number; + itslfQnt: number; + otsdInflwQnt: number; + transfTmplntNm: string; + transfPrcsQnt: number; + srpFncRt: number; + wholSrpPrcsQnt: number; + itslfTs1Whol: number; + itslfTs1RclmQnt: number; + itslfTs1IncnrQnt: number; + incnrPrcsWhol: number; + incnrPrcsLfvlm: number; + incnrPrcsPrdctRmtrlQnt: number; + incnrPrcsFmlyrQnt: number; + incnrPrcsFcltNm: string; + itslfTs1AltdQnt: number; + stbznPrcsWhol: number; + stbznPrcsLfvlm: number; + stbznPrcsFmlyrQnt: number; + stbznPrcsFcltNm: string; + itslfTs1DryQnt: number; + dryPrcsWhol: number; + dryPrcsLfvlm: number; + dryPrcsIncnrQnt: number; + dryPrcsFuelQnt: number; + dryPrcsPrdctRmtrlQnt: number; + dryPrcsFmlyrQnt: number; + dryPrcsCmpstgQnt: number; + dryPrcsFcltNm: string; + itslfTs1FuelQnt: number; + fuelPrcsWhol: number; + fuelPrcsIncnrQnt: number; + fuelPrcsFuelQnt: number; + fuelPrcsFcltNm: string; + itslfTs2CmpstgQnt: number; + cmpsPrcsFcltBzentyNm: string; + otsdCnsgnWhol: number; + otsdCnsgnRclmQnt: number; + otsdCnsgnIncnrQnt: number; + otsdCnsgnFuelPwplQnt: number; + otsdCnsgnPrdctRmtrlQnt: number; + otsdCnsgnFmlyrQnt: number; + otsdCnsgnCmpstgQnt: number; + otsdCnsgnFcltNm: string; + cparTs1Whol: number; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface SlurgeProcessingFacStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + slurgeProcessingFacStatusList: SlurgeProcessingFacStatusItem[]; +} + +export type SlurgeProcessingFacStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110001 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type SlurgeProcessingFacStatusSaveItem = Omit< + SlurgeProcessingFacStatusItem, + | "sggNm" + | "verificationResult" + | "gnamWhol" + | "wholSrpPrcsQnt" + | "itslfTs1Whol" + | "incnrPrcsWhol" + | "stbznPrcsWhol" + | "dryPrcsWhol" + | "fuelPrcsWhol" + | "otsdCnsgnWhol" +>; + +/** ST00110001 저장 API 요청 */ +export interface SlurgeProcessingFacStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SlurgeProcessingFacStatusSaveItem[]; + deleteList: SlurgeProcessingFacStatusSaveItem[]; +} + +export type SlurgeProcessingFacStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type SlurgeProcessingFacStatusSubmitItem = SlurgeProcessingFacStatusSaveItem; + +/** ST00110001 제출 API 요청 */ +export interface SlurgeProcessingFacStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SlurgeProcessingFacStatusSubmitItem[]; + deleteList: SlurgeProcessingFacStatusSubmitItem[]; +} + +export interface SlurgeProcessingFacStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110002 조회 `data.digestorOperatingStatusList[]` 한 행. + * `DigestorOperatingStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface DigestorOperatingStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + digstnEnYn: string; + digstnCpct: number; + actlDrvnCpct: number; + digstnGasInputQnt: number; + digstnGasInputSldDnst: number; + digstnTnkOprtngDayCnt: number; + ocrnGasQnt: number; + digstnGasUtztnWholPrst: number; + digstnGasUtztnNtslQnt: number; + digstnGasUtztnEtgtQnt: number; + digstnGasUtztnItslfQnt: number; + digstnGasUtztnEtcQnt: number; + surplsGasQnt: number; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface DigestorOperatingStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + digestorOperatingStatusList: DigestorOperatingStatusItem[]; +} + +export type DigestorOperatingStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110002 저장 — `saveList` 항목. + * `sggNm`·계산필드·검증결과는 저장 요청에서 제외. + */ +export type DigestorOperatingStatusSaveItem = Omit< + DigestorOperatingStatusItem, + "sggNm" | "verificationResult" | "digstnGasUtztnWholPrst" +>; + +/** ST00110002 저장 API 요청 */ +export interface DigestorOperatingStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: DigestorOperatingStatusSaveItem[]; + deleteList?: DigestorOperatingStatusSaveItem[]; +} + +export type DigestorOperatingStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type DigestorOperatingStatusSubmitItem = DigestorOperatingStatusSaveItem; + +/** ST00110002 제출 API 요청 */ +export interface DigestorOperatingStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: DigestorOperatingStatusSubmitItem[]; +} + +export interface DigestorOperatingStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110003 조회 `data.sewageSlurgeProcessingFacStatusList[]` 한 행. + * `SewageSlurgeProcessingFacStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface SewageSlurgeProcessingFacStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + prcsMthExpln: string; + fcltCpct: number; + anlPrcsQnt: number; + dayAvgOprtngHr: number; + anlOprtngDayCnt: number; + oprtngRt: number; + rmrk: string | null; + fcltPstnCn: string; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface SewageSlurgeProcessingFacStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + sewageSlurgeProcessingFacStatusList: SewageSlurgeProcessingFacStatusItem[]; +} + +export type SewageSlurgeProcessingFacStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110003 저장 — `saveList` / `deleteList` 항목. + * `sggNm`·계산필드·검증결과·`fcltPstnCn`은 저장 요청에서 제외. + */ +export type SewageSlurgeProcessingFacStatusSaveItem = Omit< + SewageSlurgeProcessingFacStatusItem, + "sggNm" | "verificationResult" | "oprtngRt" | "fcltPstnCn" +>; + +/** ST00110003 저장 API 요청 */ +export interface SewageSlurgeProcessingFacStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: SewageSlurgeProcessingFacStatusSaveItem[]; + deleteList: SewageSlurgeProcessingFacStatusSaveItem[]; +} + +export type SewageSlurgeProcessingFacStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type SewageSlurgeProcessingFacStatusSubmitItem = + SewageSlurgeProcessingFacStatusSaveItem; + +/** ST00110003 제출 API 요청 */ +export interface SewageSlurgeProcessingFacStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: SewageSlurgeProcessingFacStatusSubmitItem[]; + deleteList: SewageSlurgeProcessingFacStatusSubmitItem[]; +} + +export interface SewageSlurgeProcessingFacStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** + * ST00110004 조회 `data.excretaSlurgeProcessingStatusList[]` 한 행. + * `ExcretaSlurgeProcessingStatusInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 `commonInfo.ctpvNm` 시도명). + */ +export interface ExcretaSlurgeProcessingStatusItem { + admdstCd: string; + sggNm: string; + admdstNm: string; + fcltSn: string; + fcltNm: string; + fcltPstnCn: string; + gnam: number; + wholDspsQnt: number; + rcycQnt: number; + olandLfvlm: number; + incnrQnt: number; + oceanInputQnt: number; + fuelQnt: number; + etcQnt: number; + nondspsQnt: number; + swtrmtFcltNm: string; + rmrk: string | null; + verificationResult?: SewageExcretaSlurgeVerificationResult; +} + +export interface ExcretaSlurgeProcessingStatusResponseData { + statusInfo: SewerageStatusInfo; + commonInfo: SewageExcretaSlurgeCommonInfo; + excretaSlurgeProcessingStatusList: ExcretaSlurgeProcessingStatusItem[]; +} + +export type ExcretaSlurgeProcessingStatusApiResponse = + ApiResponse & { + message: string; + }; + +/** + * ST00110004 저장 — `saveList` 항목. + * `sggNm`·계산필드(`gnam`, `wholDspsQnt`)·검증결과는 저장 요청에서 제외. + */ +export type ExcretaSlurgeProcessingStatusSaveItem = Omit< + ExcretaSlurgeProcessingStatusItem, + "sggNm" | "verificationResult" | "gnam" | "wholDspsQnt" +>; + +/** ST00110004 저장 API 요청 */ +export interface ExcretaSlurgeProcessingStatusSaveRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + superAdmin?: string; + noApplicable?: boolean; + saveList: ExcretaSlurgeProcessingStatusSaveItem[]; +} + +export type ExcretaSlurgeProcessingStatusSaveResponse = ApiResponse & { + message?: string; +}; + +export type ExcretaSlurgeProcessingStatusSubmitItem = ExcretaSlurgeProcessingStatusSaveItem; + +/** ST00110004 제출 API 요청 */ +export interface ExcretaSlurgeProcessingStatusSubmitRequest { + statsYr: string; + ctpvNm: string; + sggNm: string; + sewerBzmnCd: string; + userId: string; + laePicNm: string; + laePicTelNo: string; + laeKbrdrDeptNm: string; + laeKbrdrJbgdNm: string; + laeIdfrNm: string; + laeIdfrTelNo: string; + laeIdfrDeptNm: string; + laeIdfrJbgdNm: string; + superAdmin?: string; + noApplicable?: boolean; + submitList: ExcretaSlurgeProcessingStatusSubmitItem[]; +} + +export interface ExcretaSlurgeProcessingStatusSubmitResponse { + status: string; + message?: string; + data?: { + finalStatus: string; + nvpassed: boolean; + }; +} + +/** `public-sewage-facilities` 조회 요청 */ +export interface PublicSewageFacilitiesListRequest { + statsYr: string; + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; +} + +/** `public-sewage-facilities` 조회 `data[]` 한 행 */ +export interface PublicSewageFacilityItem { + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + fcltSn: string; +} + +export type PublicSewageFacilitiesListApiResponse = ApiResponse & { + message: string; +}; + +export interface StatisticsInputSewageExcretaSlurgeService { + /** 찌꺼기(슬러지)처리시설현황 조회 — ST00110001 */ + getSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 찌꺼기(슬러지)처리시설현황 저장 — ST00110001 */ + saveSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSaveRequest, + ): Promise; + /** 찌꺼기(슬러지)처리시설현황 제출 — ST00110001 */ + submitSlurgeProcessingFacStatus( + request: SlurgeProcessingFacStatusSubmitRequest, + ): Promise; + /** 소화조 운영현황 조회 — ST00110002 */ + getDigestorOperatingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 소화조 운영현황 저장 — ST00110002 */ + saveDigestorOperatingStatus( + request: DigestorOperatingStatusSaveRequest, + ): Promise; + /** 소화조 운영현황 제출 — ST00110002 */ + submitDigestorOperatingStatus( + request: DigestorOperatingStatusSubmitRequest, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 조회 — ST00110003 */ + getSewageSlurgeProcessingFacStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 저장 — ST00110003 */ + saveSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSaveRequest, + ): Promise; + /** 하수찌꺼기 처리시설 운영현황 제출 — ST00110003 */ + submitSewageSlurgeProcessingFacStatus( + request: SewageSlurgeProcessingFacStatusSubmitRequest, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 조회 — ST00110004 */ + getExcretaSlurgeProcessingStatusList( + params: SewageExcretaSlurgeRequestParams, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 저장 — ST00110004 */ + saveExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSaveRequest, + ): Promise; + /** 분뇨찌꺼기(슬러지)처리현황 제출 — ST00110004 */ + submitExcretaSlurgeProcessingStatus( + request: ExcretaSlurgeProcessingStatusSubmitRequest, + ): Promise; + /** 공공하수처리시설 목록 조회 */ + getPublicSewageFacilitiesList( + params: PublicSewageFacilitiesListRequest, + ): Promise; +}