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; +}