diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 2d8eab48c..8a2d8d928 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -29,6 +29,13 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr { @apply !bg-[#fff7bf] !text-[#b45309]; } + /* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */ + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { + @apply !font-semibold !text-[#dc2626]; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { + @apply !font-semibold !text-[#3372cf]; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total { @apply !bg-[#e3eaf2]; } @@ -95,6 +102,12 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-[#545556] !text-[#fff]; } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { + @apply !text-[#f87171]; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { + @apply !text-[#3372cf]; + } } [data-theme="red"] { diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index cc938a282..c759b4a92 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -25,6 +25,7 @@ import { import { defaultSearchFormProps, + type SearchFormClsfCd, type SearchFormProps, type SearchFormValues, } from "../type/SearchForm.type"; @@ -81,6 +82,12 @@ const SearchForm = (props: SearchFormProps = {}) => { }, ); + const initialClsfCd: SearchFormClsfCd = (() => { + const d = props.defaultValues?.clsfCd; + if (d === "ALL" || d === "INPT" || d === "VRFC") return d; + return "ALL"; + })(); + const [values, setValues] = useState({ sido: props.defaultValues?.sido || "", gugun: props.defaultValues?.gugun || "", @@ -90,6 +97,7 @@ const SearchForm = (props: SearchFormProps = {}) => { bsnmCd: props.defaultValues?.bsnmCd || "", isDetail: false, isSummary: props.defaultValues?.isSummary || "", + clsfCd: initialClsfCd, }); const sggKey: [string, string, string, string] | null = @@ -134,6 +142,7 @@ const SearchForm = (props: SearchFormProps = {}) => { bsnmCd: "", isDetail: false, isSummary: "", + clsfCd: "ALL", }; setValues(resetValues); props.onReset?.(); @@ -250,6 +259,29 @@ const SearchForm = (props: SearchFormProps = {}) => { )} + {mergedProps.clsf && ( +
+ + +
+ )} {mergedProps.fctName && (
); diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index 3be4a6a9e..1026fd82d 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -1,6 +1,9 @@ "use client"; import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; + +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; import { Button, Dialog, @@ -11,20 +14,29 @@ import { DialogTitle, LoadingWithAnimation, } from "@common/components"; + import DataGrid from "@/components/module/DataGrid"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; -import type { ColumnInfo } from "@/components/type/DataGrid.type"; -import { statisticsApi } from "@/entities/statistics/api/statistics-service"; -import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; +import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; type VerificationResultGridRow = Record; -const PRCS_YN_LABEL: Record = { - "0": "미통과", - "1": "통과", - "2": "예외처리", - "3": "반려", -}; +function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined { + if (raw === undefined || raw === null) return undefined; + if (typeof raw === "number" && Number.isFinite(raw)) return raw; + const s = String(raw).trim(); + if (s === "") return undefined; + const n = Number.parseInt(s, 10); + return Number.isFinite(n) ? n : undefined; +} + +/** prcsYn 0·4 → 빨간색, 그 외 → 초록색 (처리여부 컬럼) */ +function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined { + if (fieldName !== "prcsYnNm") return undefined; + const code = prcsYnCodeToNumber(row.prcsYn); + if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad"; + return "rg-cell-vrf-prcs-ok"; +} const verificationResultColumnInfo: ColumnInfo = { columns: [ @@ -34,12 +46,13 @@ const verificationResultColumnInfo: ColumnInfo = { { name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false }, { name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false }, { name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false }, - { name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false }, + { name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false }, { name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false }, { name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false }, { name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false }, + { name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false }, ], - columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt"], + columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYnNm", "pbcpAprvYn", "regDt", "mdfcnDt", "prcsYn"], }; export interface VerificationResultHistoryDialogProps { @@ -190,7 +203,8 @@ export function VerificationResultHistoryDialog({ rsnExpln: item.rsnExpln, nowVlCn: item.nowVlCn ?? "-", compVlCn: item.compVlCn ?? "-", - prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn, + prcsYnNm: item.prcsYnNm ?? "-", + prcsYn: item.prcsYn ?? "", pbcpAprvYn: item.pbcpAprvYn, regDt: (item.regDt ?? "").slice(0, 16), mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), @@ -224,6 +238,7 @@ export function VerificationResultHistoryDialog({ isExcelExport={false} showHeaderCheckbox={false} enableSort={false} + cellStyleCallback={verificationPrcsYnNmCellStyle} />
-

검증 미통과 내역

+

이상치 선별 보고서

void; + initialPrxyBzentyNmCn: string; + /** 위탁비용(백만원/년) 초기값 */ + initialCnsgnCstVl: number; + initialWholNope: number; + /** 위탁계약기간 초기값 (YYYYMMDD~YYYYMMDD) */ + initialPrxyCtrtCn: string; + onConfirm?: (values: MainFacilityMappingSaveDialogValues) => void | Promise; +} + +function isValidYmd(ymd: string): boolean { + if (!/^\d{8}$/.test(ymd)) return false; + const y = Number(ymd.slice(0, 4)); + const m = Number(ymd.slice(4, 6)); + const d = Number(ymd.slice(6, 8)); + const dt = new Date(y, m - 1, d); + return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d; +} + +function validatePrxyCtrtCn(value: string): string | null { + const t = value.trim(); + if (!t) return "위탁계약기간을 입력해 주세요."; + if (!/^\d{8}~\d{8}$/.test(t)) { + return "위탁계약기간은 YYYYMMDD~YYYYMMDD 형식으로 입력해 주세요. (예: 20240101~20281231)"; + } + const start = t.slice(0, 8); + const end = t.slice(9, 17); + if (!isValidYmd(start) || !isValidYmd(end)) { + return "시작일 또는 종료일이 유효한 날짜가 아닙니다."; + } + if (start > end) { + return "시작일이 종료일보다 늦을 수 없습니다."; + } + return null; +} + +export function MainFacilityMappingSaveDialog({ + open, + onOpenChange, + initialPrxyBzentyNmCn, + initialCnsgnCstVl, + initialWholNope, + initialPrxyCtrtCn, + onConfirm, +}: MainFacilityMappingSaveDialogProps) { + const { setMessage: alert } = useAlertStore((state) => state); + const [prxyBzentyNmCn, setPrxyBzentyNmCn] = useState(""); + const [cnsgnCstInput, setCnsgnCstInput] = useState(""); + const [wholNopeInput, setWholNopeInput] = useState(""); + const [prxyCtrtCn, setPrxyCtrtCn] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setPrxyBzentyNmCn(initialPrxyBzentyNmCn ?? ""); + const initCost = Number(initialCnsgnCstVl); + setCnsgnCstInput( + initialCnsgnCstVl === null || + initialCnsgnCstVl === undefined || + Number.isNaN(initCost) + ? "" + : String(initCost), + ); + setWholNopeInput( + initialWholNope === null || initialWholNope === undefined || Number.isNaN(Number(initialWholNope)) + ? "" + : String(initialWholNope), + ); + setPrxyCtrtCn((initialPrxyCtrtCn ?? "").trim()); + } + }, [open, initialPrxyBzentyNmCn, initialCnsgnCstVl, initialWholNope, initialPrxyCtrtCn]); + + const handleDialogOpenChange = (nextOpen: boolean) => { + if (!nextOpen && !isSubmitting) { + setPrxyBzentyNmCn(""); + setCnsgnCstInput(""); + setWholNopeInput(""); + setPrxyCtrtCn(""); + } + onOpenChange(nextOpen); + }; + + const handleConfirm = async () => { + const trimmedCost = cnsgnCstInput.trim(); + const cnsgnCstVl = trimmedCost === "" ? 0 : Number.parseFloat(trimmedCost.replace(/,/g, "")); + if (!Number.isFinite(cnsgnCstVl) || cnsgnCstVl < 0) { + alert("위탁비용은 0 이상의 숫자로 입력해 주세요."); + return; + } + const trimmedWhol = wholNopeInput.trim(); + const wholNope = trimmedWhol === "" ? 0 : Number.parseInt(trimmedWhol, 10); + if (!Number.isFinite(wholNope) || wholNope < 0) { + alert("전체 인원은 0 이상의 정수로 입력해 주세요."); + return; + } + const prxyCtrtErr = validatePrxyCtrtCn(prxyCtrtCn); + if (prxyCtrtErr) { + alert(prxyCtrtErr); + return; + } + try { + setIsSubmitting(true); + await onConfirm?.({ + prxyBzentyNmCn: prxyBzentyNmCn.trim(), + cnsgnCstVl, + wholNope, + prxyCtrtCn: prxyCtrtCn.trim(), + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + 저장 정보 입력 + +
+
+ + setPrxyBzentyNmCn(e.target.value)} + className="!text-[1.3rem]" + placeholder="위탁사업자명을 입력하세요." + disabled={isSubmitting} + /> +
+
+ + { + const v = e.target.value.replace(/[^\d.]/g, ""); + const firstDot = v.indexOf("."); + const next = + firstDot === -1 + ? v + : v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, ""); + setCnsgnCstInput(next); + }} + className="!text-[1.3rem]" + placeholder="0" + disabled={isSubmitting} + /> +
+
+ + { + const v = e.target.value.replace(/\D/g, ""); + setWholNopeInput(v); + }} + className="!text-[1.3rem]" + placeholder="0" + disabled={isSubmitting} + /> +
+
+ + setPrxyCtrtCn(e.target.value)} + className="!text-[1.3rem]" + placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)" + autoComplete="off" + disabled={isSubmitting} + /> +
+
+ + + + + + +
+
+ ); +} diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index a48fa76f2..f0334fee8 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -2,7 +2,7 @@ import { 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 { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service'; import { statisticsApi } from '@/entities/statistics/api/statistics-service'; @@ -10,6 +10,8 @@ import type { PublicSewageFacilityRequestParams, PublicSewageFacilitySaveItem, PublicSewageFacilitySaveRequest, + PublicSewageMainFacilityMappingSaveItem, + PublicSewageMainFacilityMappingSaveRequest, PublicSewageMappingTargetsRequestParams, } from '@/entities/statistics/model/input/statistics-public-sewage-facility-input-interface'; import { @@ -23,6 +25,7 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from ' import { getUserInfo } from '../../../../../../packages/common/components/src/api'; +import { MainFacilityMappingSaveDialog } from './MainFacilityMappingSaveDialog'; import SubmitDailog from './SubmitDailog'; import CancelSubmitButton from '@/components/button/CancelSubmitButton'; @@ -30,9 +33,12 @@ import NotApplicableCheckButton from '@/components/button/NotApplicableCheckButt 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 SearchForm from '@/components/module/SearchForm'; import SplitDataGrid from '@/components/module/SplitDataGrid'; +import { VerificationResultDialog } from '@/components/module/VerificationResultDialog'; +import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog'; import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type'; import type { SearchFormValues } from '@/components/type/SearchForm.type'; import { doList } from '@/components/util/column/input/DoList'; @@ -93,12 +99,14 @@ async function waitForImrJobCompleted( 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} 상태로 종료되었습니다.`); @@ -108,11 +116,31 @@ async function waitForImrJobCompleted( throw new Error('IMR 검증 완료 대기 시간이 초과되었습니다.'); } +type MainFacilityMappingSaveDraft = { + statsYr: string; + sewerBzmnCd: string; + admdstCd: string; + admdstNm: string; + mainFcltSn: string; + mainFcltNm: string; + initialCnsgnCstVl: number; + saveList: PublicSewageMainFacilityMappingSaveItem[]; + deleteList: string[]; + initialPrxyBzentyNmCn: string; + initialWholNope: number; + initialPrxyCtrtCn: string; +}; + // 하수도 통계 관리 > 06. 공공하수처리시설 const PublicSewageFacilityInp = () => { const { setMessage: alert } = useAlertStore((state) => state); const { confirm } = useConfirm(); const { isAdminInfo } = useAdminStore(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const globalProgressVisible = useGlobalProgressStore((state) => state.visible); const [isLoading, setIsLoading] = useState(false); const [hasSearched, setHasSearched] = useState(false); const [isNotApplicable, setIsNotApplicable] = useState(false); @@ -126,9 +154,16 @@ const PublicSewageFacilityInp = () => { const [periodCheckList, setPeriodCheckList] = useState([]); const [showDailog, setShowDailog] = useState(false); const [selectedMainFacilitySn, setSelectedMainFacilitySn] = useState(null); + const mainFacilityMappingSaveDraftRef = useRef(null); + const [mainFacilityMappingSaveDialogOpen, setMainFacilityMappingSaveDialogOpen] = useState(false); + const [mainFacilityMappingSaveDialogKey, setMainFacilityMappingSaveDialogKey] = useState(0); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const periodEnterableRef = useRef(true); const searchDoneRef = useRef(false); + /** ST00060001만: RealGrid 편집값이 state에 없을 수 있어 저장/제출 시 그리드에서 직접 읽음 */ + const st60001GridGetRowsRef = useRef<(() => Record[]) | null>(null); const submitFunc = () => { void (async () => { @@ -140,7 +175,8 @@ const PublicSewageFacilityInp = () => { alert('먼저 조회를 진행해주세요.'); return; } - const currentData = rowDataMap.get('ST00060001') ?? []; + const currentData = + st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; if (!currentData || currentData.length === 0) { alert('제출할 데이터가 없습니다.'); return; @@ -199,6 +235,11 @@ const PublicSewageFacilityInp = () => { return noApplicableVisible ? Boolean(isNotApplicable) : false; }; + const isCheckedRow = (row: Record): boolean => { + const v = row.__checked; + return v === 1 || v === '1' || v === 'Y' || v === 'y' || v === 'true'; + }; + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); if (verificationBlocked) { @@ -213,125 +254,225 @@ const PublicSewageFacilityInp = () => { alert('먼저 조회를 진행해주세요.'); return; } - const currentData = rowDataMap.get('ST00060001') ?? []; + const currentData = + st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; if (!currentData || currentData.length === 0) { alert('제출할 데이터가 없습니다.'); return; } - const userInfo = await getUserInfo(); - const submitList = buildFacilitySaveList(currentData); - const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; - const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab; - const requestData = { - statsYr: lastSearchedValues.year ?? '', - ctpvNm: lastSearchedValues.sido ?? '', - sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', - sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', - userId: userInfo.userUnqId, - noApplicable: getNoApplicableForRequest(), - laePicNm: submitInfo.laePicNm, - laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, - laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, - laePicTelNo: submitInfo.laePicTelNo, - laeIdfrNm: submitInfo.laeIdfrNm, - laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, - laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, - laeIdfrTelNo: submitInfo.laeIdfrTelNo, - submitList, - }; - try { - setIsLoading(true); - const response = await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility(requestData); - if (response.status === 'success') { - if (response.data?.finalStatus === '5') { - 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: response.data?.nvpassed ? '1' : '2', - }; - try { - const imrResponse = await statisticsApi.callImrService(imrParam); - if (imrResponse.job_id) { - await waitForImrJobCompleted( - imrResponse.job_id, - (id) => statisticsApi.getImrStatus(id), - ); - } - alert('제출되었습니다.'); - } catch { - alert('제출은 완료되었으나 IMR 검증 처리 중 오류가 발생했습니다.'); - } - } else { - alert(response.message || '제출 처리 결과를 확인해주세요.'); - } - await loadPublicSewageFacilityActiveTabData( - 'ST00060001', - searchParamRef.current, - lastSearchedValues, - ); - } else { - alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); - } - } finally { - setIsLoading(false); - } - }; - - const saveFunc = () => { - void (async () => { - if (activeTab !== 'ST00060001') { - alert('ST00060002 저장 API 연동 전입니다.'); - return; - } - - if (!lastSearchedValues || !searchParamRef.current) { - alert('먼저 조회를 진행해주세요.'); - return; - } - - const currentData = rowDataMap.get('ST00060001') ?? []; - if (!currentData || currentData.length === 0) { - alert('저장할 데이터가 없습니다.'); - return; - } - - if (!(await confirm("저장 하시겠습니까?"))) return; - + showGlobalProgress({ label: '서버에 요청 중...', progress: 0 }); const userInfo = await getUserInfo(); - const saveList = buildFacilitySaveList(currentData); - - const param: PublicSewageFacilitySaveRequest = { + const submitList = buildFacilitySaveList(currentData); + const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; + const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab; + const requestData = { statsYr: lastSearchedValues.year ?? '', ctpvNm: lastSearchedValues.sido ?? '', sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', userId: userInfo.userUnqId, - superAdmin: isAdminInfo ? 'superAdmin' : '', noApplicable: getNoApplicableForRequest(), - saveList, + laePicNm: submitInfo.laePicNm, + laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, + laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, + laePicTelNo: submitInfo.laePicTelNo, + laeIdfrNm: submitInfo.laeIdfrNm, + laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm, + laeIdfrDeptNm: submitInfo.laeIdfrDeptNm, + laeIdfrTelNo: submitInfo.laeIdfrTelNo, + submitList, }; - try { - setIsLoading(true); - const response = await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(param); - if (response.status === 'success') { - alert('저장되었습니다.'); + setGlobalProgressLabel('일반 검증 진행 중...'); + const response = + await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility( + requestData, + ); + + 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 loadPublicSewageFacilityActiveTabData( 'ST00060001', searchParamRef.current, lastSearchedValues, ); - } else { - alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); + setVerificationResultDialogOpen(true); + } catch (e) { + await loadPublicSewageFacilityActiveTabData( + 'ST00060001', + searchParamRef.current, + lastSearchedValues, + ); + setVerificationResultDialogOpen(true); + const msg = + e instanceof Error && e.message + ? e.message + : 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'; + alert(msg); } - } finally { - setIsLoading(false); + } else { + setGlobalProgress(100); + await loadPublicSewageFacilityActiveTabData( + 'ST00060001', + searchParamRef.current, + lastSearchedValues, + ); + setVerificationResultDialogOpen(true); } + } catch { + alert('데이터 제출 중 오류가 발생했습니다.'); + } finally { + hideGlobalProgress(); + } + }; + + const saveFunc = () => { + void (async () => { + if (!lastSearchedValues || !searchParamRef.current) { + alert('먼저 조회를 진행해주세요.'); + return; + } + + if (activeTab === 'ST00060002') { + if (!(await confirm('저장 하시겠습니까?'))) return; + + const leftRows = rowDataMap.get('MainFacility') ?? []; + const rightRows = rowDataMap.get('MainFacilityTarget') ?? []; + if (!leftRows.length || !rightRows.length) { + alert('저장할 데이터가 없습니다.'); + return; + } + if (!selectedMainFacilitySn) { + alert('좌측에서 중심 처리 시설을 먼저 선택해주세요.'); + return; + } + + const selectedMain = leftRows.find((r) => String(r.fcltSn ?? '') === selectedMainFacilitySn); + if (!selectedMain) { + alert('선택된 중심 처리 시설 정보를 찾을 수 없습니다.'); + return; + } + + const saveList: PublicSewageMainFacilityMappingSaveItem[] = rightRows + .filter((r) => isCheckedRow(r)) + .map((r) => ({ + fcltSn: String(r.fcltSn ?? ''), + admdstCd: String(r.admdstCd ?? ''), + admdstNm: String(r.admdstNm ?? ''), + swtrmtFcltCd: String(r.swtrmtFcltCd ?? ''), + swtrmtFcltNm: String(r.swtrmtFcltNm ?? ''), + isMainYn: String(r.isMainYn ?? ''), + })); + + const deleteList = rightRows + .filter((r) => String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r)) + .map((r) => String(r.fcltSn ?? '')) + .filter((sn) => sn !== ''); + + const wholNopeFromRow = Number(selectedMain.wholNope ?? 0); + mainFacilityMappingSaveDraftRef.current = { + statsYr: lastSearchedValues.year ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + admdstCd: String(selectedMain.admdstCd ?? ''), + admdstNm: String(selectedMain.admdstNm ?? ''), + mainFcltSn: String(selectedMain.fcltSn ?? ''), + mainFcltNm: String(selectedMain.swtrmtFcltNm ?? ''), + initialCnsgnCstVl: Number(selectedMain.cnsgnCstVl ?? 0), + saveList, + deleteList, + initialPrxyBzentyNmCn: String(selectedMain.prxyBzentyNmCn ?? ''), + initialWholNope: Number.isFinite(wholNopeFromRow) ? wholNopeFromRow : 0, + initialPrxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''), + }; + setMainFacilityMappingSaveDialogKey((k) => k + 1); + setMainFacilityMappingSaveDialogOpen(true); + return; + } + + if (!(await confirm('저장 하시겠습니까?'))) return; + + if (activeTab === 'ST00060001') { + try { + showGlobalProgress({ label: '저장 진행 중...', progress: 0 }); + const userInfo = await getUserInfo(); + const currentData = + st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; + if (!currentData || currentData.length === 0) { + alert('저장할 데이터가 없습니다.'); + return; + } + + const saveList = buildFacilitySaveList(currentData); + const param: PublicSewageFacilitySaveRequest = { + statsYr: lastSearchedValues.year ?? '', + ctpvNm: lastSearchedValues.sido ?? '', + sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', + sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', + userId: userInfo.userUnqId, + superAdmin: isAdminInfo ? 'superAdmin' : '', + noApplicable: getNoApplicableForRequest(), + saveList, + }; + + const response = + await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility( + param, + ); + if (response.status === 'success') { + setGlobalProgress(50); + await loadPublicSewageFacilityActiveTabData( + 'ST00060001', + searchParamRef.current, + lastSearchedValues, + ); + setGlobalProgress(100); + alert('저장되었습니다.'); + } else { + alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 저장 중 오류가 발생했습니다.'); + } finally { + hideGlobalProgress(); + } + return; + } + + alert('현재 탭 저장 API 연동 전입니다.'); })(); }; @@ -489,6 +630,59 @@ const PublicSewageFacilityInp = () => { } }; + const handleMainFacilityMappingSaveConfirm = async (input: { + prxyBzentyNmCn: string; + cnsgnCstVl: number; + wholNope: number; + prxyCtrtCn: string; + }) => { + const draft = mainFacilityMappingSaveDraftRef.current; + if (!draft || !searchParamRef.current || !lastSearchedValues) { + setMainFacilityMappingSaveDialogOpen(false); + mainFacilityMappingSaveDraftRef.current = null; + return; + } + try { + showGlobalProgress({ label: '저장 진행 중...', progress: 0 }); + const userInfo = await getUserInfo(); + const param: PublicSewageMainFacilityMappingSaveRequest = { + statsYr: draft.statsYr, + sewerBzmnCd: draft.sewerBzmnCd, + admdstCd: draft.admdstCd, + admdstNm: draft.admdstNm, + mainFcltSn: draft.mainFcltSn, + mainFcltNm: draft.mainFcltNm, + prxyBzentyNmCn: input.prxyBzentyNmCn, + cnsgnCstVl: input.cnsgnCstVl, + wholNope: input.wholNope, + prxyCtrtCn: input.prxyCtrtCn, + userId: userInfo.userUnqId, + saveList: draft.saveList, + deleteList: draft.deleteList, + }; + const response = + await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param); + if (response.status === 'success') { + setGlobalProgress(50); + setMainFacilityMappingSaveDialogOpen(false); + mainFacilityMappingSaveDraftRef.current = null; + await loadPublicSewageFacilityActiveTabData( + 'ST00060002', + searchParamRef.current, + lastSearchedValues, + ); + setGlobalProgress(100); + alert('저장되었습니다.'); + } else { + alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 저장 중 오류가 발생했습니다.'); + } finally { + hideGlobalProgress(); + } + }; + const handleSearch = async (values: SearchFormValues) => { if (!values.sido || values.sido === 'ALL') { alert('시도를 선택해주세요.'); @@ -685,6 +879,30 @@ const PublicSewageFacilityInp = () => { const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar; const isMainFacilityTab = activeTab === 'ST00060002'; + const openVerificationHistoryDialog = () => { + const ctx = lastSearchedValues ?? searchValues; + const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? activeTab; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) { + alert('먼저 조회를 진행해주세요.'); + return; + } + setVerificationHistoryOpen(true); + }; + + const verificationQueryContext = + (() => { + const ctx = lastSearchedValues ?? searchValues; + const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab; + if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; + return { + sewerBzmnCd: ctx.bsnmCd, + ctpvNm: ctx.sido, + sggNm: ctx.gugun === 'ALL' ? '' : (ctx.gugun ?? ''), + statsYr: ctx.year, + statsCd, + }; + })(); + return ( <>
@@ -697,6 +915,9 @@ const PublicSewageFacilityInp = () => { {showActionToolbarCard && (
+ {showEnterableToolbarButtons && !isMainFacilityTab && ( <> {showIsNotApplicable && } @@ -711,12 +932,13 @@ const PublicSewageFacilityInp = () => {
)} - {isLoading &&
-
- + {isLoading && !globalProgressVisible && ( +
+
+ +
-
- } + )} void handleActiveTabChange(v)}> {PublicSewageFactInpColumnList.map((tab) => ( @@ -753,6 +975,38 @@ const PublicSewageFacilityInp = () => { ? leftMainFacilityCellStyleCallback : undefined } + rightOnItemChecked={ + tab.tabKey === 'ST00060002' + ? (itemIndex, checked) => { + setRowDataMap((prev) => { + const next = new Map(prev); + const rows = [...(next.get('MainFacilityTarget') ?? [])]; + if (!rows[itemIndex]) return prev; + rows[itemIndex] = { + ...rows[itemIndex], + __checked: checked ? 'Y' : '', + }; + next.set('MainFacilityTarget', rows); + return next; + }); + } + : undefined + } + rightOnItemAllChecked={ + tab.tabKey === 'ST00060002' + ? (checked) => { + setRowDataMap((prev) => { + const next = new Map(prev); + const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({ + ...r, + __checked: checked ? 'Y' : '', + })); + next.set('MainFacilityTarget', rows); + return next; + }); + } + : undefined + } setLeftRowData={(updater) => { setRowDataMap((prev) => { const next = new Map(prev); @@ -803,6 +1057,9 @@ const PublicSewageFacilityInp = () => { return next; }); }} + getGridDataRef={ + tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined + } showHeaderCheckbox={false} enableSort={false} /> @@ -811,6 +1068,11 @@ const PublicSewageFacilityInp = () => { })}
+ { submitMode="normal" isLoading={isLoading} /> + + { + setMainFacilityMappingSaveDialogOpen(open); + if (!open) { + mainFacilityMappingSaveDraftRef.current = null; + } + }} + initialPrxyBzentyNmCn={mainFacilityMappingSaveDraftRef.current?.initialPrxyBzentyNmCn ?? ''} + initialCnsgnCstVl={mainFacilityMappingSaveDraftRef.current?.initialCnsgnCstVl ?? 0} + initialWholNope={mainFacilityMappingSaveDraftRef.current?.initialWholNope ?? 0} + initialPrxyCtrtCn={mainFacilityMappingSaveDraftRef.current?.initialPrxyCtrtCn ?? ''} + onConfirm={handleMainFacilityMappingSaveConfirm} + /> ); }; diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index e73f398c2..04ef09dce 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -1,71 +1,74 @@ -import { useState, useMemo, useRef, useEffect, useLayoutEffect } from "react"; +import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -import DataGrid from "@/components/module/DataGrid"; -import SplitDataGrid from "@/components/module/SplitDataGrid"; -import GridTopActionButton from "@/components/module/GridTopActionButton"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; -import VerificationResultButton from "@/components/button/VerificationResultButton"; -import CancelSubmitButton from "@/components/button/CancelSubmitButton"; -import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; -import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; -import SubmitDailog from "./SubmitDailog"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; -import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type"; -import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { STATISTICS_INPUT_URL_PATH, verificationSearchToFormValues, } from "@/shared/lib/stats-cd-input-navigation"; -import { useAdminStore } from "@/store/admin-store"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; -import { doList } from "@/components/util/column/input/DoList"; import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { SeweragePipeInputFactInstallStatusSaveRequest, SeweragePipeInputFactInstallStatusSubmitRequest, SeweragePipeInputManholeStatusItem, - SeweragePipeInputManholeStatusSaveYearlyStatItem, SeweragePipeInputManholeStatusSaveRequest, + SeweragePipeInputManholeStatusSaveYearlyStatItem, SeweragePipeInputManholeStatusSubmitRequest, SeweragePipeInputPipeDiameterStatusSaveRequest, SeweragePipeInputPipeDiameterStatusSubmitRequest, SeweragePipeInputPipeTypeStatusItem, SeweragePipeInputPipeTypeStatusSaveRequest, SeweragePipeInputPipeTypeStatusSubmitRequest, - SeweragePipeInputRegionalFactStatusSaveRequest, SeweragePipeInputRegionalFactStatusSaveItem, + SeweragePipeInputRegionalFactStatusSaveRequest, SeweragePipeInputRegionalFactStatusSubmitRequest, SeweragePipeInputSubmitResponse, SeweragePipeInputVerificationResult, SeweragePipeRequestParams, } from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; +import { + type CheckInputPeriodItem, + getVerificationPeriodActionBlockedMessage, + isInputPeriodCurrent, +} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import type { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, SubmitExceptionRequest, } from "@/entities/statistics/model/statistics-interface"; -import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; -import { - type CheckInputPeriodItem, - getVerificationPeriodActionBlockedMessage, - isInputPeriodCurrent, -} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-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 GridTopActionButton from "@/components/module/GridTopActionButton"; +import SearchForm from "@/components/module/SearchForm"; +import SplitDataGrid from "@/components/module/SplitDataGrid"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { ColumnConfig,ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { doList } from "@/components/util/column/input/DoList"; import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn"; import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn"; import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn"; import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; +import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; -import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; -import { getUserInfo } from "../../../../../../packages/common/components/src/api"; -import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; @@ -740,7 +743,7 @@ const SewagePipeInp = () => { if (!job) return; postVerificationSearchRef.current = null; void handleSearch(job.values, { forcedTabKey: job.tabKey }); - // eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회 + }, [searchFormRemountKey]); const handleActiveTabChange = async (tabKey: string) => { @@ -1209,6 +1212,7 @@ const SewagePipeInp = () => { true, false, ); + setVerificationResultDialogOpen(true); const msg = e instanceof Error && e.message ? e.message @@ -1224,7 +1228,7 @@ const SewagePipeInp = () => { true, false, ); - alert("제출되었습니다."); + setVerificationResultDialogOpen(true); } } catch { alert("데이터 제출 중 오류가 발생했습니다."); diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 071d86bca..4db880b03 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -1,52 +1,54 @@ -import { useState, useMemo, useRef, useLayoutEffect, useEffect } from "react"; +import { useEffect,useLayoutEffect, useMemo, useRef, useState } from "react"; -import SearchForm from "@/components/module/SearchForm"; -// import { DataGrid } from "@rsone/components"; -import DataGrid from "@/components/module/DataGrid"; -import SubmitButton from "@/components/button/SumitButton"; -import SaveButton from "@/components/button/SaveButton"; -import LastYearDataButton from "@/components/button/LastYearDataButton"; -import CancelSubmitButton from "@/components/button/CancelSubmitButton"; -import VerificationResultButton from "@/components/button/VerificationResultButton"; -import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; -import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; -import { LoadingWithAnimation } from "@common/components"; -import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; -import SubmitDailog from "./SubmitDailog"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { STATISTICS_INPUT_URL_PATH, verificationSearchToFormValues, } from "@/shared/lib/stats-cd-input-navigation"; -import { useAdminStore } from "@/store/admin-store"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; -import { toGridRows } from "@/components/util/grid/gridFunction"; -import { getFooterSummary } from "@/components/util/grid/footerSummary"; -import { getUserInfo } from "../../../../../../packages/common/components/src/api"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; - -import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { type CheckInputPeriodItem, + getVerificationPeriodActionBlockedMessage, + isInputPeriodCurrent, type SewerageSupplyRateItem, type SewerageSupplyRateRequestParams, type SewerageSupplyRateSaveItem, - type SewerageSupplyRateVerificationResult, type SewerageSupplyRateSaveRequest, - getVerificationPeriodActionBlockedMessage, - isInputPeriodCurrent, + type SewerageSupplyRateVerificationResult, } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import type { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, SubmitExceptionRequest, } from "@/entities/statistics/model/statistics-interface"; -import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; -import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; +import { LoadingWithAnimation } 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 "@rsone/components"; +import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { doList } from "@/components/util/column/input/DoList"; +import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; +import { getFooterSummary } from "@/components/util/grid/footerSummary"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; @@ -274,7 +276,7 @@ const SeweragePenetrationRateInp = () => { if (!values) return; postVerificationSearchRef.current = null; void handleSearch(values); - // eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회 + }, [searchFormRemountKey]); const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => { @@ -475,6 +477,7 @@ const SeweragePenetrationRateInp = () => { } catch (e) { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); setIsLoading(false); + setVerificationResultDialogOpen(true); const msg = e instanceof Error && e.message ? e.message @@ -485,7 +488,7 @@ const SeweragePenetrationRateInp = () => { } else { setGlobalProgress(100); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); - alert('제출되었습니다.'); + setVerificationResultDialogOpen(true); } } else { alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); diff --git a/apps/st/src/components/statistics/management/NoticeTalkSending.tsx b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx index ff0cae557..df0076a76 100644 --- a/apps/st/src/components/statistics/management/NoticeTalkSending.tsx +++ b/apps/st/src/components/statistics/management/NoticeTalkSending.tsx @@ -8,14 +8,15 @@ import { type RowSelectionState, useReactTable, } from "@tanstack/react-table"; +import type { AxiosError } from "axios"; -import { client } from "@/shared/api/client"; -import { ENDPOINT } from "@/shared/config"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import type { ApiErrorResponse } from "@/shared/model"; import { type AdstZoneResponseDto, type BusinessSendRequest, type BusinessSendResponse, + noticeTalkSendingApi, type SelectedRecipient, type SewerStUserItem, useNtftalkTemplateList, @@ -122,10 +123,26 @@ function NtitlkSndng() { const [previewRecipient, setPreviewRecipient] = useState(null); const [isPreviewOpen, setIsPreviewOpen] = useState(false); - const [isSending, setIsSending] = useState(false); const [userListRowSelection, setUserListRowSelection] = useState({}); + const sendMutation = useMutationWrapper< + BusinessSendResponse, + AxiosError, + BusinessSendRequest + >({ + mutationFn: (payload) => noticeTalkSendingApi.sendBusiness(payload), + onSuccess: (data) => { + if (!data.success) { + alert(data.message || "알림톡 발송 요청에 실패했습니다."); + return; + } + alert(data.message || "알림톡 발송이 요청되었습니다."); + setSelectedRecipients([]); + setCommonVariableValues({}); + }, + }); + const { data: templatePageData } = useNtftalkTemplateList(); const templates = templatePageData?.content ?? []; @@ -321,7 +338,7 @@ function NtitlkSndng() { }), ); if (missingVariable) { - alert("필수 변수값이 입력되지 않은 발송 대상이 있습니다."); + alert("변수값이 입력되지 않은 발송 대상이 있습니다."); return; } if ( @@ -331,34 +348,13 @@ function NtitlkSndng() { ) return; - const payload: BusinessSendRequest = { + sendMutation.mutate({ templateCode: selectedTemplateCode, callback: "032-590-4000", recipients: selectedRecipients.map((recipient) => getMergedVariables(recipient, commonVariableValues), ), - }; - - try { - setIsSending(true); - const response = await client.post( - `${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/business`, - payload, - ); - const data = response.data; - if (!data.success) { - alert(data.message || "알림톡 발송 요청에 실패했습니다."); - return; - } - - alert(data.message || `알림톡 발송이 요청되었습니다.`); - setSelectedRecipients([]); - setCommonVariableValues({}); - } catch { - alert("알림톡 발송 중 오류가 발생했습니다."); - } finally { - setIsSending(false); - } + }); }; return ( @@ -567,7 +563,8 @@ function NtitlkSndng() { color="green" onClick={handleSend} disabled={ - isSending || selectedRecipients.length === 0 + sendMutation.isPending || + selectedRecipients.length === 0 } > 발송 diff --git a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx index 8626b4a83..0d6d88429 100644 --- a/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx +++ b/apps/st/src/components/statistics/management/StatisticsInputManagement.tsx @@ -1,84 +1,230 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + StatsInputPeriodItem, + StatsInputPeriodListRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { Button, LoadingWithAnimation } from "@common/components"; + +import { + StatisticsInputPeriodDialog, + type StatisticsInputPeriodDialogValues, +} from "./StatisticsInputPeriodDialog"; import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; import type { ColumnConfig } from "@/components/type/DataGrid.type"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; + +function seTypeToDisplayLabel(seType: StatsInputPeriodItem["seType"]): string { + if (seType === "INPT") return "입력"; + if (seType === "VRFC") return "검증"; + return seType; +} + +function rowSeTypeToCode(row: Record): "INPT" | "VRFC" { + const v = String(row.seType ?? ""); + if (v === "INPT" || v === "입력") return "INPT"; + if (v === "VRFC" || v === "검증") return "VRFC"; + return "INPT"; +} + +/** API `YYYYMMDD` 등 → Date */ +function parseYmdToDate(ymd: unknown): Date | undefined { + const s = String(ymd ?? "").replace(/\D/g, ""); + if (s.length !== 8) return undefined; + const y = Number(s.slice(0, 4)); + const m = Number(s.slice(4, 6)); + const d = Number(s.slice(6, 8)); + if (!y || m < 1 || m > 12 || d < 1 || d > 31) return undefined; + const dt = new Date(y, m - 1, d); + if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) { + return undefined; + } + return dt; +} const StatisticsInputManagement = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const [isLoading, setIsLoading] = useState(false); const [rowData, setRowData] = useState[]>( [], ); + const [periodDialogOpen, setPeriodDialogOpen] = useState(false); + const [periodDialogMode, setPeriodDialogMode] = useState<"edit" | "create">("edit"); + const [createDefaultStatsYr, setCreateDefaultStatsYr] = useState(""); + const [periodDialogInitial, setPeriodDialogInitial] = + useState(null); + const [searchFormValues, setSearchFormValues] = useState(null); + const lastSearchRef = useRef(null); + + const handleSearch = async (values: SearchFormValues) => { + if (!values.year) { + alert("년도를 선택해주세요."); + return; + } + lastSearchRef.current = values; + try { + setIsLoading(true); + const params: StatsInputPeriodListRequest = { + statsYr: values.year, + }; + if (values.clsfCd && values.clsfCd !== "ALL") { + params.seType = values.clsfCd; + } + const res = await statisticsApi.getStatsInputPeriodList(params); + if (res.status === "success" && Array.isArray(res.data)) { + const rowsForGrid = res.data.map((row) => ({ + ...row, + seType: seTypeToDisplayLabel(row.seType), + })); + setRowData(toGridRows(rowsForGrid)); + } else { + setRowData([]); + alert(res.message || "데이터 조회에 실패했습니다."); + } + } catch { + setRowData([]); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + setRowData([]); + }; + + const handleRegisterClick = () => { + const yr = + searchFormValues?.year ?? lastSearchRef.current?.year ?? ""; + if (!yr) { + alert("검색 폼에서 년도를 선택해 주세요."); + return; + } + setPeriodDialogMode("create"); + setCreateDefaultStatsYr(yr); + setPeriodDialogInitial(null); + setPeriodDialogOpen(true); + }; + + const handleRowClick = (row: Record) => { + const seCdRaw = row.seCd; + const seCdNum = + typeof seCdRaw === "number" + ? seCdRaw + : parseInt(String(seCdRaw ?? ""), 10); + setPeriodDialogMode("edit"); + setPeriodDialogInitial({ + statsYr: String(row.statsYr ?? ""), + seType: rowSeTypeToCode(row), + seCd: Number.isFinite(seCdNum) ? seCdNum : 0, + inptBgngYmd: parseYmdToDate(row.inptBgngYmd), + inptEndYmd: parseYmdToDate(row.inptEndYmd), + }); + setPeriodDialogOpen(true); + }; const columns: ColumnConfig[] = [ { - name: "sido", - fieldName: "시도", - header: { text: "시도" }, - mergeCells: true, + name: "statsYr", + fieldName: "statsYr", + header: { text: "연도" }, editable: false, - sortdisable: true, }, { - name: "sgg", - fieldName: "시군구", - header: { text: "시군구" }, - mergeCells: true, + name: "seType", + fieldName: "seType", + header: { text: "분류" }, editable: false, - sortdisable: true, }, { - name: "구분", - fieldName: "구분", + name: "seCd", + fieldName: "seCd", header: { text: "구분" }, editable: false, - sortdisable: true, }, { - name: "광역자치단체", - fieldName: "광역자치단체", - header: { text: "광역자치단체" }, + name: "inptBgngYmd", + fieldName: "inptBgngYmd", + header: { text: "시작일" }, editable: false, - sortdisable: true, }, { - name: "공단", - fieldName: "공단", - header: { text: "공단" }, + name: "inptEndYmd", + fieldName: "inptEndYmd", + header: { text: "종료일" }, editable: false, - sortdisable: true, - }, - { - name: "처리내역", - fieldName: "처리내역", - header: { text: "처리내역" }, - editable: false, - sortdisable: true, }, ]; const columnLayout = [ - "sido", - "sgg", - "구분", - { - name: "groupA", - hideChildHeaders: false, - header: { text: "검토내역" }, - items: ["광역자치단체", "공단", "처리내역"], - }, + "statsYr", + "seType", + "seCd", + "inptBgngYmd", + "inptEndYmd", ]; return ( - <> - +
+ +
+
+ +
+
- + { + setPeriodDialogOpen(open); + if (!open) { + setPeriodDialogInitial(null); + setPeriodDialogMode("edit"); + } + }} + initial={periodDialogInitial} + onSaveSuccess={async () => { + const v = lastSearchRef.current; + if (v) await handleSearch(v); + }} + /> + {isLoading && ( +
+
+ +
+
+ )} +
); }; diff --git a/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx new file mode 100644 index 000000000..06a70a10d --- /dev/null +++ b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx @@ -0,0 +1,315 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +import { useAlertStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + StatsInputPeriodDeleteRequest, + StatsInputPeriodSaveRequest, +} from "@/entities/statistics/model/statistics-interface"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + InputDatePicker, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; + +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; + +export type StatisticsInputPeriodDialogValues = { + statsYr: string; + seType: "INPT" | "VRFC"; + seCd: number; + inptBgngYmd: Date | undefined; + inptEndYmd: Date | undefined; +}; + +export type StatisticsInputPeriodDialogMode = "edit" | "create"; + +function normalizeStatsYrInput(raw: string): string { + return raw.replace(/\D/g, "").slice(0, 4); +} + +function formatDateToYmd(d: Date | undefined): string | null { + if (!d || Number.isNaN(d.getTime())) return null; + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}${m}${day}`; +} + +export interface StatisticsInputPeriodDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + /** 행 클릭(수정) 시에만 전달. 등록 모드에서는 null */ + initial: StatisticsInputPeriodDialogValues | null; + mode: StatisticsInputPeriodDialogMode; + /** mode가 create일 때 SearchForm의 연도 */ + createDefaultStatsYr?: string; + /** 저장 성공 후 목록 재조회 등 */ + onSaveSuccess?: () => void | Promise; +} + +export function StatisticsInputPeriodDialog({ + open, + onOpenChange, + initial, + mode, + createDefaultStatsYr, + onSaveSuccess, +}: StatisticsInputPeriodDialogProps) { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); + const [statsYr, setStatsYr] = useState(""); + const [seType, setSeType] = useState<"INPT" | "VRFC" | null>(null); + const [seCdInput, setSeCdInput] = useState(""); + const [inptBgngYmd, setInptBgngYmd] = useState(undefined); + const [inptEndYmd, setInptEndYmd] = useState(undefined); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (!open) return; + if (mode === "create") { + setStatsYr(normalizeStatsYrInput(createDefaultStatsYr ?? "")); + setSeType(null); + setSeCdInput("0"); + setInptBgngYmd(undefined); + setInptEndYmd(undefined); + return; + } + if (!initial) return; + setStatsYr(normalizeStatsYrInput(String(initial.statsYr ?? ""))); + setSeType(initial.seType); + setSeCdInput(String(Number.isFinite(initial.seCd) ? initial.seCd : 0)); + setInptBgngYmd(initial.inptBgngYmd); + setInptEndYmd(initial.inptEndYmd); + }, [open, mode, initial, createDefaultStatsYr]); + + const handleSave = async () => { + const bgng = formatDateToYmd(inptBgngYmd); + const end = formatDateToYmd(inptEndYmd); + if (!bgng || !end) { + alert("시작일과 종료일을 선택해주세요."); + return; + } + if (bgng > end) { + alert("시작일은 종료일보다 늦을 수 없습니다."); + return; + } + if (seType === null) { + alert("분류를 선택해주세요."); + return; + } + const seCdParsed = + mode === "create" + ? 0 + : (() => { + const n = parseInt(seCdInput, 10); + return Number.isFinite(n) && n >= 0 ? n : NaN; + })(); + if (mode === "edit" && !Number.isFinite(seCdParsed)) { + alert("구분 값이 올바르지 않습니다."); + return; + } + const yr = normalizeStatsYrInput(statsYr.trim()); + if (mode === "create" && !/^\d{4}$/.test(yr)) { + alert("연도는 숫자 4자리로 입력해 주세요."); + return; + } + if (mode === "edit" && !yr) { + alert("연도가 없습니다."); + return; + } + try { + setIsSubmitting(true); + const userInfo = await getUserInfo(); + const param: StatsInputPeriodSaveRequest = { + statsYr: yr, + seType, + seCd: seCdParsed, + inptBgngYmd: bgng, + inptEndYmd: end, + rgtrId: userInfo.userUnqId, + }; + const res = await statisticsApi.saveStatsInputPeriod(param); + if (res.status === "success") { + alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`); + onOpenChange(false); + await onSaveSuccess?.(); + } else { + alert(res.message || "저장에 실패했습니다."); + } + } catch { + alert("저장 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + + const handleDelete = async () => { + if (mode === "create" || !initial) return; + if (!(await confirm("삭제하시겠습니까?"))) return; + + const bgng = formatDateToYmd(initial.inptBgngYmd); + const end = formatDateToYmd(initial.inptEndYmd); + if (!bgng || !end) { + alert("삭제할 기간 정보가 없습니다."); + return; + } + const yr = normalizeStatsYrInput(String(initial.statsYr ?? "")); + if (!yr) { + alert("연도가 없습니다."); + return; + } + const seCd = + Number.isFinite(initial.seCd) && initial.seCd >= 0 ? initial.seCd : 0; + + try { + setIsSubmitting(true); + const userInfo = await getUserInfo(); + const param: StatsInputPeriodDeleteRequest = { + statsYr: yr, + seType: initial.seType, + seCd, + inptBgngYmd: bgng, + inptEndYmd: end, + rgtrId: userInfo.userUnqId, + }; + const res = await statisticsApi.deleteStatsInputPeriod(param); + if (res.status === "success") { + alert("삭제되었습니다."); + onOpenChange(false); + await onSaveSuccess?.(); + } else { + alert(res.message || "삭제에 실패했습니다."); + } + } catch { + alert("삭제 중 오류가 발생했습니다."); + } finally { + setIsSubmitting(false); + } + }; + + const isCreate = mode === "create"; + + return ( + + + + {isCreate ? "입력 기간 등록" : "입력 관리"} + +
+
+ + {isCreate ? ( + + setStatsYr(normalizeStatsYrInput(e.target.value)) + } + placeholder="예: 2025" + disabled={isSubmitting} + className="!text-[1.3rem]" + /> + ) : ( + + )} +
+
+ + +
+ {!isCreate && ( +
+ + +
+ )} +
+ + +
+
+ + +
+
+ + + + + {!isCreate && ( + + )} + + +
+
+ ); +} diff --git a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx index 50ec7c929..295802cdc 100644 --- a/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeMaintainRvw.tsx @@ -9,8 +9,8 @@ import ApproveButton from "@/components/button/ApproveButton"; import ReturnButton from "@/components/button/ReturnButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import SaveButton from "@/components/button/SaveButton"; -import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn"; @@ -40,10 +40,12 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] { return []; } if (tabKey === "ST00030001") { - return (((responseData as { mtcStatusList?: unknown[] }).mtcStatusList) ?? []) as object[]; + const list = (responseData as { sewerPipeMtcList?: unknown }).sewerPipeMtcList; + return Array.isArray(list) ? (list as object[]) : []; } if (tabKey === "ST00030002") { - return (((responseData as { drdgStatusList?: unknown[] }).drdgStatusList) ?? []) as object[]; + const list = (responseData as { sewerPipeDrdgList?: unknown }).sewerPipeDrdgList; + return Array.isArray(list) ? (list as object[]) : []; } return []; } diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 0f6a96e2f..a18ccde3e 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -1,16 +1,18 @@ -import { useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useAlertStore } from "@/shared/lib"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; -import type { SewagePipeReviewRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { + SewagePipeReviewRequestParams, + StatsStatusInfo, +} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; -import ApproveButton from "@/components/button/ApproveButton"; +import SubmitButton from "@/components/button/SumitButton"; import ReturnButton from "@/components/button/ReturnButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; -import SaveButton from "@/components/button/SaveButton"; -import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; +import SearchForm from "@/components/module/SearchForm"; import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn"; @@ -37,20 +39,73 @@ async function fetchSewagePipeReviewTab( return null; } +function extractReviewRowsFromResponseData( + tabKey: string, + data: unknown, +): object[] { + if (Array.isArray(data)) return data as object[]; + if (!data || typeof data !== "object") return []; + + const d = data as Record; + switch (tabKey) { + case "ST00020001": + return Array.isArray(d.regionalStatusList) ? (d.regionalStatusList as object[]) : []; + case "ST00020002": + return Array.isArray(d.facilityStatusList) ? (d.facilityStatusList as object[]) : []; + case "ST00020003": + return Array.isArray(d.pipeTypeList) ? (d.pipeTypeList as object[]) : []; + case "ST00020004": + return Array.isArray(d.pipeDiamList) ? (d.pipeDiamList as object[]) : []; + default: + return []; + } +} + +function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null { + if (!data || typeof data !== "object") return null; + const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo; + return ssi && typeof ssi === "object" ? ssi : null; +} + // 하수도 통계 검토 02.하수관로 const SewagePipeRvw = () => { const { setMessage: alert } = useAlertStore((state) => state); const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState(firstTabKey); + /** 탭별 조회 응답의 statsStatusInfo — 활성 탭에 맞춰 버튼 노출 (SewagePipeInp의 tabStatusInfoMap과 동일 패턴) */ + const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState< + Map + >(new Map()); const searchParamRef = useRef(null); const searchDoneRef = useRef(false); const lazyTabLoggedRef = useRef>(new Set()); + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const statsStatusInfo = tabStatsStatusInfoMap.get(activeTab) ?? {}; + const submit = + statsStatusInfo.submitStatusCd != null + ? String(statsStatusInfo.submitStatusCd) + : ""; + const prvn = + statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [tabStatsStatusInfoMap, activeTab]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + const handleReset = () => { setRowDataMap(new Map()); setActiveTab(firstTabKey); + setTabStatsStatusInfoMap(new Map()); searchParamRef.current = null; searchDoneRef.current = false; lazyTabLoggedRef.current.clear(); @@ -79,15 +134,24 @@ const SewagePipeRvw = () => { const response = await fetchSewagePipeReviewTab(activeTab, searchParam); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(activeTab, ssi ?? {}); + return next; + }); + const rows = extractReviewRowsFromResponseData(activeTab, response.data); setRowDataMap((prev) => { const next = new Map(prev); - next.set(activeTab, toGridRows(response.data as object[])); + next.set(activeTab, toGridRows(rows)); return next; }); } else { + setTabStatsStatusInfoMap(new Map()); alert(response?.message || "데이터 조회 중 오류가 발생했습니다."); } } catch { + setTabStatsStatusInfoMap(new Map()); alert("데이터 조회 중 오류가 발생했습니다."); } finally { setIsLoading(false); @@ -118,9 +182,16 @@ const SewagePipeRvw = () => { lazyTabLoggedRef.current.add(tabKey); if (response?.status === "success" && response.data) { + const ssi = extractStatsStatusInfo(response.data); + setTabStatsStatusInfoMap((prev) => { + const next = new Map(prev); + next.set(tabKey, ssi ?? {}); + return next; + }); + const rows = extractReviewRowsFromResponseData(tabKey, response.data); setRowDataMap((prev) => { const next = new Map(prev); - next.set(tabKey, toGridRows(response.data as object[])); + next.set(tabKey, toGridRows(rows)); return next; }); } else { @@ -137,14 +208,19 @@ const SewagePipeRvw = () => {
-
-
- { }} /> - { }} /> - { }} /> - { }} /> + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + {}} /> + )} + {showReturnButton && {}} />} + {showSubmitButton && ( + {}} /> + )} +
-
+ )} {isLoading && (
diff --git a/apps/st/src/components/statistics/total/PublicSewageFacilityTot.tsx b/apps/st/src/components/statistics/total/PublicSewageFacilityTot.tsx index 6eb3f2991..280db0894 100644 --- a/apps/st/src/components/statistics/total/PublicSewageFacilityTot.tsx +++ b/apps/st/src/components/statistics/total/PublicSewageFacilityTot.tsx @@ -1,9 +1,79 @@ +import { useState } from "react"; + +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { PublicSewageFacilityColumn } from "@/components/util/column/total/PublicSewageFacilityColumn"; +import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; +import { toGridRows } from "@/components/util/grid/gridFunction"; +import { useAlertStore } from "@/shared/lib"; + import SearchForm from "@/components/module/SearchForm"; +import DataGrid from "@/components/module/DataGrid"; +import CellColorInfo from "./CellColorInfo"; +import { LoadingWithAnimation } from "@common/components"; const PublicSewageFacilityTot = () => { + const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const { setMessage: alert } = useAlertStore((state) => state); + + const handleSearch = async (values: SearchFormValues) => { + try { + setIsLoading(true); + + const isSummaryEnabled = values.isSummary !== 'ALL'; + const summaryYearCnt = isSummaryEnabled ? Number(values.isSummary) : undefined; + + const param = { + ctpvNm: values.sido === 'ALL' ? '' : values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + rowDataYn: values.isDetail, + summaryYn: isSummaryEnabled, + ...(isSummaryEnabled ? { summaryYearCnt } : {}), + }; + + const response = await statisticsTotalStatusApi.getSewagePublicFacillityTotal(param); + + if (response.status === 'success') { + setRowData(toGridRows(response.data)); + } else { + alert(response.message || '데이터 조회 중 오류가 발생했습니다.'); + } + } catch (error) { + console.log(error); + alert('데이터 조회 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + return ( <> - + + {isLoading && ( +
+
+ +
+
+ )} + + { + const rowType = String(row.rowType ?? ''); + if (rowType === 'DETAIL') return undefined; + if (rowType === 'BIZ_SUM') return 'rg-cell-rowtype-biz-sum'; + if (rowType === 'SUMMARY') return 'rg-cell-rowtype-summary'; + return 'rg-cell-rowtype-other'; + }} + /> ); }; diff --git a/apps/st/src/components/statistics/total/SewagePipeTot.tsx b/apps/st/src/components/statistics/total/SewagePipeTot.tsx index 2553fce19..061405229 100644 --- a/apps/st/src/components/statistics/total/SewagePipeTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeTot.tsx @@ -25,6 +25,7 @@ function buildSewagePipeTotSearchParam(values: SearchFormValues): SewagePipeTota sewerBzmnCd: values.bsnmCd, statsYr: values.year, rowDataYn: values.isDetail, + summaryYn: isSummaryEnabled, ...(isSummaryEnabled ? { summaryYearCnt } : {}), }; } @@ -106,6 +107,15 @@ const SewagePipeTot = () => { alert(response.message || "데이터 조회 중 오류가 발생했습니다."); return false; } + if (tabKey === "ST00020005") { + const response = await statisticsTotalStatusApi.getSewagePipePipeMnhleStatus(param); + if (response.status === "success" && response.data) { + applyRows(toGridRows(response.data as object[])); + return true; + } + alert(response.message || "데이터 조회 중 오류가 발생했습니다."); + return false; + } return false; }; @@ -120,7 +130,8 @@ const SewagePipeTot = () => { activeTab === "ST00020001" || activeTab === "ST00020002" || activeTab === "ST00020003" || - activeTab === "ST00020004" + activeTab === "ST00020004" || + activeTab === "ST00020005" ) { ok = await loadSewagePipeTotTabData(activeTab, values, true); } @@ -151,7 +162,8 @@ const SewagePipeTot = () => { tabKey === "ST00020001" || tabKey === "ST00020002" || tabKey === "ST00020003" || - tabKey === "ST00020004" + tabKey === "ST00020004" || + tabKey === "ST00020005" ) { try { setIsLoading(true); diff --git a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx index 2206b5d3b..4dd37c89d 100644 --- a/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx +++ b/apps/st/src/components/statistics/verification/HbStatisticsVerification.tsx @@ -114,10 +114,10 @@ export default function HbStatisticsVerification() { const [verifyType, setVerifyType] = useState("ALL"); const [formState, setFormState] = useState({ - mu: "0", - percentile: "0", - a: "0", - c: "0", + mu: "", + percentile: "", + a: "", + c: "", verificationYear: defaultYear, }); const [selectedTargets, setSelectedTargets] = useState([]); @@ -559,11 +559,14 @@ export default function HbStatisticsVerification() {
-
-
-
-
diff --git a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx index ce9b1d26e..eb56ef7d8 100644 --- a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx +++ b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Search } from "lucide-react"; import { CodeSelect } from "@/entities/code/ui/code-select"; import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service"; @@ -368,7 +367,7 @@ export function VerificationTargetDialog({ type="number" min="0" max="0.5" - step="0.1" + step="0.01" value={ field.value ?? "" @@ -385,7 +384,7 @@ export function VerificationTargetDialog({ : null, ) } - placeholder="(0 ~ 0.5, 0.1 단위)" + placeholder="(0 ~ 0.5, 0.01 단위)" />
)} diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 1fef46df3..3519ce772 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -369,6 +369,10 @@ export type SplitDataGridProps = { leftOnRowClick?: (rowData: Record) => void; /** 좌측 분할 그리드 셀 스타일 콜백 */ leftCellStyleCallback?: CellStyleCallback; + /** 우측 분할 그리드 행 체크 변경 */ + rightOnItemChecked?: (itemIndex: number, checked: boolean) => void; + /** 우측 분할 그리드 헤더 전체 체크 변경 */ + rightOnItemAllChecked?: (checked: boolean) => void; /** 전체 정렬 기능 on/off (기본값: false) */ enableSort?: boolean; }; diff --git a/apps/st/src/components/type/SearchForm.type.ts b/apps/st/src/components/type/SearchForm.type.ts index 79ca5d118..18ce8aea7 100644 --- a/apps/st/src/components/type/SearchForm.type.ts +++ b/apps/st/src/components/type/SearchForm.type.ts @@ -1,3 +1,6 @@ +/** 통계 분류: 전체 / 입력 / 검증 */ +export type SearchFormClsfCd = "ALL" | "INPT" | "VRFC"; + export type SearchFormValues = { sido?: string; gugun?: string; @@ -7,6 +10,8 @@ export type SearchFormValues = { bsnmCd?: string; isDetail?: boolean; isSummary?: string; + /** 분류: 전체(ALL), 입력(INPT), 검증(VRFC) */ + clsfCd?: SearchFormClsfCd; }; export type SearchFormProps = { @@ -16,6 +21,8 @@ export type SearchFormProps = { year?: boolean; order?: boolean; fctName?: boolean; + /** 분류(입력/검증) 셀렉트 표시 */ + clsf?: boolean; userMenual?: boolean; isDetail?: boolean; isSummary?: boolean; @@ -33,6 +40,7 @@ export const defaultSearchFormProps: Required< year: true, order: false, fctName: false, + clsf: false, userMenual: false, isDetail: false, isSummary: false, diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts index cd85731e6..14cb5281d 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/MainFacilityInpColumn.ts @@ -46,6 +46,7 @@ export const MainFacilityTargetColumn = { { name: 'fcltSn', fieldName: 'fcltSn', header: { text: '처리시설일련번호' }, visible: false, editable: false, valueType: ValueType.TEXT, styleName: 'rg-cell-readonly' }, { name: 'swtrmtFcltCd', fieldName: 'swtrmtFcltCd', header: { text: '하수처리시설코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, { name: 'mainFcltSn', fieldName: 'mainFcltSn', header: { text: '연결된중심시설일련번호' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, { name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, visible: false, editable: false, styleName: 'rg-cell-readonly' }, @@ -56,6 +57,7 @@ export const MainFacilityTargetColumn = { 'mappedYn', 'fcltSn', 'swtrmtFcltCd', + 'admdstCd', 'admdstNm', 'mainFcltSn', 'isMainYn', diff --git a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts index 8cb23828f..fa2f73317 100644 --- a/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts +++ b/apps/st/src/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactColumn.ts @@ -21,9 +21,9 @@ export const PublicSewageFactColumn = { { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-total' }, // (m³/일) - { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, - { name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, // 유입하수량 { name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' }, diff --git a/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts new file mode 100644 index 000000000..f4c0ddae2 --- /dev/null +++ b/apps/st/src/components/util/column/total/PublicSewageFacilityColumn.ts @@ -0,0 +1,298 @@ +import { ValueType } from "realgrid"; +/** + * 하수도 통계 관리 > 06. 공공하수처리시설 + * 단일 Tab + */ +export const PublicSewageFacilityColumn = { + columns: [ + // 행정구역 + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false }, + + // 시설명 (500㎥/일 이상/미만) + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명\n(500㎥/일\n이상/미만)' }, editable: false }, + + // 소재지 + { name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false }, + + // 시설용량 (㎥/일) + { + name: 'prcsFcltCpct', + fieldName: 'prcsFcltCpct', + header: { text: ' 시설용량 500㎥/일\n이상/미만' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + { + name: 'prcsFcltPhysCpct', + fieldName: 'prcsFcltPhysCpct', + header: { text: '물리적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'prcsFcltBoilCpct', + fieldName: 'prcsFcltBoilCpct', + header: { text: '생물학적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'prcsFcltAltdCpct', + fieldName: 'prcsFcltAltdCpct', + header: { text: '고도' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + + // 유입하수량 (㎥/일) + { + name: 'inflwSwageQnt', + fieldName: 'inflwSwageQnt', + header: { text: '유입하수량 500㎥/일\n이상/미만' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'inflwSwagePhysQnt', + fieldName: 'inflwSwagePhysQnt', + header: { text: '물리적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'inflwSwageBoilQnt', + fieldName: 'inflwSwageBoilQnt', + header: { text: '생물학적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'inflwSwageAltdQnt', + fieldName: 'inflwSwageAltdQnt', + header: { text: '고도' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + + // 방류량 (㎥/일) + { + name: 'prcsQnt', + fieldName: 'prcsQnt', + header: { text: '방류량 500㎥/일\n이상/미만' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'prcsPhysCpct', + fieldName: 'prcsPhysCpct', + header: { text: '물리적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'prcsBoilCpct', + fieldName: 'prcsBoilCpct', + header: { text: '생물학적' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + { + name: 'prcsAltdCpct', + fieldName: 'prcsAltdCpct', + header: { text: '고도' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.00', + }, + + // 수질 정보 - 유입/방류 교차 + { name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입\nBOD' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgBodVl', fieldName: 'dschgBodVl', header: { text: '방류\nBOD' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwTocQnt', fieldName: 'inflwTocQnt', header: { text: '유입\nTOC' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgTocQnt', fieldName: 'dschgTocQnt', header: { text: '방류\nTOC' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwSsQnt', fieldName: 'inflwSsQnt', header: { text: '유입\nSS' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgSsQnt', fieldName: 'dschgSsQnt', header: { text: '방류\nSS' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwTnVl', fieldName: 'inflwTnVl', header: { text: '유입\nT-N' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgTnVl', fieldName: 'dschgTnVl', header: { text: '방류\nT-N' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwTpVl', fieldName: 'inflwTpVl', header: { text: '유입\nT-P' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgTpVl', fieldName: 'dschgTpVl', header: { text: '방류\nT-P' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'inflwGrssClfgVl', fieldName: 'inflwGrssClfgVl', header: { text: '유입\n총대장균군수' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류\n총대장균군수' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + + // 처리 효율 및 방법 + { + name: 'prcsRt', + fieldName: 'prcsRt', + header: { text: '처리\n효율(%)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0.0', + }, + { + name: 'prcsLodQnt', + fieldName: 'prcsLodQnt', + header: { text: '처리부하량\n(kg BOD/D)' }, + editable: false, + valueType: ValueType.NUMBER, + dataFormat: '#,##0', + }, + { name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리\n방법' }, editable: false }, + + // 연계처리량 (㎥/일) + { name: 'wholHmnQnt', fieldName: 'wholHmnQnt', header: { text: '분뇨' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wholLvstQnt', fieldName: 'wholLvstQnt', header: { text: '축산' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wholEfntQnt', fieldName: 'wholEfntQnt', header: { text: '침출수' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'wholEtcQnt', fieldName: 'wholEtcQnt', header: { text: '기타' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + + // 부가 정보 + { name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' }, editable: false }, + { name: 'cmptnStrtYmd', fieldName: 'cmptnStrtYmd', header: { text: '준공일' }, editable: false }, + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시일' }, editable: false }, + { name: 'wholBizVl', fieldName: 'wholBizVl', header: { text: '사업비\n(백만원)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'eflntDsnfMthdExpln', fieldName: 'eflntDsnfMthdExpln', header: { text: '방류수\n소독방법' }, editable: false }, + + // 방류수역 정보 + { name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' }, editable: false }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' }, editable: false }, + { name: 'dtlUnitZoneCn', fieldName: 'dtlUnitZoneCn', header: { text: '세부단위구역' }, editable: false }, + { name: 'rgnSeNm', fieldName: 'rgnSeNm', header: { text: '지역구분' }, editable: false }, + + // 중권역 + { name: 'msirbCn', fieldName: 'msirbCn', header: { text: '명칭' }, editable: false }, + { name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질\n(BOD)' }, editable: false }, + + // 에너지 + { name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지\n자립율(%)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, + { name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지\n자체생산량\n(TOE)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'anlEngyWholUseQnt', fieldName: 'anlEngyWholUseQnt', header: { text: '계(TOE)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '총전력사용량\n(kWh)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체에너지\n사용량(TOE)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당\nCO2배출량\n(㎏CO2/㎥)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + + // 운영주체 + { name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체\n(자체/공기업/\n민간대행)' }, editable: false }, + + // 위탁현황 + { name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁\n업체명' }, editable: false }, + { name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용\n(백만원/년)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' }, + { name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간\n(yyyy-mm-dd\n~\nyyyy-mm-dd)' }, editable: false }, + { 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: [ + { + name: 'groupA', + hideChildHeaders: false, + header: { text: '행정구역' }, + items: ['ctpvNm', 'sggNm', 'admdstNm'], + }, + 'swtrmtFcltNm', + 'fcltPstnCn', + 'prcsFcltCpct', + { + name: 'groupCapacity', + hideChildHeaders: false, + header: { text: '(㎥/일)' }, + items: [ 'prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'], + }, + 'inflwSwageQnt', + { + name: 'groupInflw', + hideChildHeaders: false, + header: { text: '(㎥/일)' }, + items: [ 'inflwSwagePhysQnt', 'inflwSwageBoilQnt', 'inflwSwageAltdQnt'], + }, + 'prcsQnt', + { + name: 'groupPrcs', + hideChildHeaders: false, + header: { text: '(㎥/일)' }, + items: [ 'prcsPhysCpct', 'prcsBoilCpct', 'prcsAltdCpct'], + }, + 'inflwBodVl', + 'dschgBodVl', + 'inflwTocQnt', + 'dschgTocQnt', + 'inflwSsQnt', + 'dschgSsQnt', + 'inflwTnVl', + 'dschgTnVl', + 'inflwTpVl', + 'dschgTpVl', + 'inflwGrssClfgVl', + 'dschgGrssClfgVl', + 'prcsRt', + 'prcsLodQnt', + 'prcsMthdExpln', + { + name: 'groupLinkPrcs', + hideChildHeaders: false, + header: { text: '연계처리량(㎥/일)\n(500㎥/일 이상/미만)' }, + items: ['wholHmnQnt', 'wholLvstQnt', 'wholEfntQnt', 'wholEtcQnt'], + }, + 'telNo', + 'cmptnStrtYmd', + 'oprtngStrtYmd', + 'wholBizVl', + 'eflntDsnfMthdExpln', + { + name: 'groupDschgWater', + hideChildHeaders: false, + header: { text: '방류수역' }, + items: [ + 'wtsysNm', + 'trbtrNm', + 'dtlUnitZoneCn', + 'rgnSeNm', + { + name: 'groupMsirbCn', + hideChildHeaders: false, + header: { text: '중권역' }, + items: ['msirbCn', 'goalWqNo'], + }, + ], + }, + 'engySrlneRt', + 'engyItslfPrdctnQnt', + { + name: 'groupEngyUse', + hideChildHeaders: false, + header: { text: '연간 총 에너지 사용량(TOE)' }, + items: ['anlEngyWholUseQnt', 'anlElpwrUseQnt', 'itslfEngyUseQnt'], + }, + 'swtrmtEmsnQnt', + 'operMnbdCn', + { + name: 'groupCnsgn', + hideChildHeaders: false, + header: { text: '위탁현황' }, + items: ['prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn'], + }, + 'intgPrcsFcltCn', + 'wholNope', + 'sftyAcdntEvntcsCnt', + 'admdstCd', + 'rowType', + 'statsYr', + ], +}; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts index 9c78c5b0e..e2a30d9e0 100644 --- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts +++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts @@ -706,7 +706,7 @@ const rawSewagePipeTotList = [ tabKey: "ST00020004", tabLabel: "관경별 현황", rowStartNum: 2, - fixedColumns: 3, + fixedColumns: 1, columns: [ { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, @@ -794,9 +794,238 @@ const rawSewagePipeTotList = [ "etcPdmtPrlg", ], }, + + { + tabKey: "ST00020005", + tabLabel: "맨홀 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, + { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, + { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false }, + { name: "emphsMngSeNm", fieldName: "emphsMngSeNm", header: { text: "집중강우\n중점관리\n구역구분" }, editable: false }, + + { name: "totWholMnhlCnt", fieldName: "totWholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "totStep1MnhlCnt", fieldName: "totStep1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "totStep2MnhlCnt", fieldName: "totStep2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "totStep3MnhlCnt", fieldName: "totStep3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "totStep4MnhlCnt", fieldName: "totStep4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "totStep5MnhlCnt", fieldName: "totStep5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2015년 이전 (yy14) ────────────────────────────────── + { name: "yy14WholMnhlCnt", fieldName: "yy14WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy14Step1MnhlCnt", fieldName: "yy14Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy14Step2MnhlCnt", fieldName: "yy14Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy14Step3MnhlCnt", fieldName: "yy14Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy14Step4MnhlCnt", fieldName: "yy14Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy14Step5MnhlCnt", fieldName: "yy14Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2015년 (yy15) ─────────────────────────────────────── + { name: "yy15WholMnhlCnt", fieldName: "yy15WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy15Step1MnhlCnt", fieldName: "yy15Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy15Step2MnhlCnt", fieldName: "yy15Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy15Step3MnhlCnt", fieldName: "yy15Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy15Step4MnhlCnt", fieldName: "yy15Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy15Step5MnhlCnt", fieldName: "yy15Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2016년 (yy16) ─────────────────────────────────────── + { name: "yy16WholMnhlCnt", fieldName: "yy16WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy16Step1MnhlCnt", fieldName: "yy16Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy16Step2MnhlCnt", fieldName: "yy16Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy16Step3MnhlCnt", fieldName: "yy16Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy16Step4MnhlCnt", fieldName: "yy16Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy16Step5MnhlCnt", fieldName: "yy16Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2017년 (yy17) ─────────────────────────────────────── + { name: "yy17WholMnhlCnt", fieldName: "yy17WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy17Step1MnhlCnt", fieldName: "yy17Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy17Step2MnhlCnt", fieldName: "yy17Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy17Step3MnhlCnt", fieldName: "yy17Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy17Step4MnhlCnt", fieldName: "yy17Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy17Step5MnhlCnt", fieldName: "yy17Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2018년 (yy18) ─────────────────────────────────────── + { name: "yy18WholMnhlCnt", fieldName: "yy18WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy18Step1MnhlCnt", fieldName: "yy18Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy18Step2MnhlCnt", fieldName: "yy18Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy18Step3MnhlCnt", fieldName: "yy18Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy18Step4MnhlCnt", fieldName: "yy18Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy18Step5MnhlCnt", fieldName: "yy18Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2019년 (yy19) ─────────────────────────────────────── + { name: "yy19WholMnhlCnt", fieldName: "yy19WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy19Step1MnhlCnt", fieldName: "yy19Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy19Step2MnhlCnt", fieldName: "yy19Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy19Step3MnhlCnt", fieldName: "yy19Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy19Step4MnhlCnt", fieldName: "yy19Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy19Step5MnhlCnt", fieldName: "yy19Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2020년 (yy20) ─────────────────────────────────────── + { name: "yy20WholMnhlCnt", fieldName: "yy20WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy20Step1MnhlCnt", fieldName: "yy20Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy20Step2MnhlCnt", fieldName: "yy20Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy20Step3MnhlCnt", fieldName: "yy20Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy20Step4MnhlCnt", fieldName: "yy20Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy20Step5MnhlCnt", fieldName: "yy20Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2021년 (yy21) ─────────────────────────────────────── + { name: "yy21WholMnhlCnt", fieldName: "yy21WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy21Step1MnhlCnt", fieldName: "yy21Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy21Step2MnhlCnt", fieldName: "yy21Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy21Step3MnhlCnt", fieldName: "yy21Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy21Step4MnhlCnt", fieldName: "yy21Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy21Step5MnhlCnt", fieldName: "yy21Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2022년 (yy22) ─────────────────────────────────────── + { name: "yy22WholMnhlCnt", fieldName: "yy22WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy22Step1MnhlCnt", fieldName: "yy22Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy22Step2MnhlCnt", fieldName: "yy22Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy22Step3MnhlCnt", fieldName: "yy22Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy22Step4MnhlCnt", fieldName: "yy22Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy22Step5MnhlCnt", fieldName: "yy22Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2023년 (yy23) ─────────────────────────────────────── + { name: "yy23WholMnhlCnt", fieldName: "yy23WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy23Step1MnhlCnt", fieldName: "yy23Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy23Step2MnhlCnt", fieldName: "yy23Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy23Step3MnhlCnt", fieldName: "yy23Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy23Step4MnhlCnt", fieldName: "yy23Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy23Step5MnhlCnt", fieldName: "yy23Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2024년 (yy24) ─────────────────────────────────────── + { name: "yy24WholMnhlCnt", fieldName: "yy24WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy24Step1MnhlCnt", fieldName: "yy24Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy24Step2MnhlCnt", fieldName: "yy24Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy24Step3MnhlCnt", fieldName: "yy24Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy24Step4MnhlCnt", fieldName: "yy24Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy24Step5MnhlCnt", fieldName: "yy24Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + // ── 2025년 (yy25) ─────────────────────────────────────── + { name: "yy25WholMnhlCnt", fieldName: "yy25WholMnhlCnt", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy25Step1MnhlCnt", fieldName: "yy25Step1MnhlCnt", header: { text: "주철" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy25Step2MnhlCnt", fieldName: "yy25Step2MnhlCnt", header: { text: "주철(조화)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy25Step3MnhlCnt", fieldName: "yy25Step3MnhlCnt", header: { text: "콘크리트" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy25Step4MnhlCnt", fieldName: "yy25Step4MnhlCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + { name: "yy25Step5MnhlCnt", fieldName: "yy25Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false }, + + { name: "rmrk", fieldName: "rmrk", header: { text: "비고" }, editable: false }, + ], + columnLayout: [ + { name: "addrGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm"] }, + "emphsMngSeNm", + { + name: "totGroup", + header: { text: "연도별총계" }, + items: [ + { name: "totMtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["totWholMnhlCnt", "totStep1MnhlCnt", "totStep2MnhlCnt", "totStep3MnhlCnt", "totStep4MnhlCnt"] }, + "totStep5MnhlCnt", + ], + }, + { + name: "yy14Group", + header: { text: "2015년 이전" }, // ← _14Y + items: [ + { name: "yy14MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy14WholMnhlCnt", "yy14Step1MnhlCnt", "yy14Step2MnhlCnt", "yy14Step3MnhlCnt", "yy14Step4MnhlCnt"] }, + "yy14Step5MnhlCnt", + ], + }, + { + name: "yy15Group", + header: { text: "2015년" }, // ← _15Y + items: [ + { name: "yy15MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy15WholMnhlCnt", "yy15Step1MnhlCnt", "yy15Step2MnhlCnt", "yy15Step3MnhlCnt", "yy15Step4MnhlCnt"] }, + "yy15Step5MnhlCnt", + ], + }, + { + name: "yy16Group", + header: { text: "2016년" }, // ← _16Y + items: [ + { name: "yy16MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy16WholMnhlCnt", "yy16Step1MnhlCnt", "yy16Step2MnhlCnt", "yy16Step3MnhlCnt", "yy16Step4MnhlCnt"] }, + "yy16Step5MnhlCnt", + ], + }, + { + name: "yy17Group", + header: { text: "2017년" }, + items: [ + { name: "yy17MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy17WholMnhlCnt", "yy17Step1MnhlCnt", "yy17Step2MnhlCnt", "yy17Step3MnhlCnt", "yy17Step4MnhlCnt"] }, + "yy17Step5MnhlCnt", + ], + }, + { + name: "yy18Group", + header: { text: "2018년" }, + items: [ + { name: "yy18MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy18WholMnhlCnt", "yy18Step1MnhlCnt", "yy18Step2MnhlCnt", "yy18Step3MnhlCnt", "yy18Step4MnhlCnt"] }, + "yy18Step5MnhlCnt", + ], + }, + { + name: "yy19Group", + header: { text: "2019년" }, + items: [ + { name: "yy19MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy19WholMnhlCnt", "yy19Step1MnhlCnt", "yy19Step2MnhlCnt", "yy19Step3MnhlCnt", "yy19Step4MnhlCnt"] }, + "yy19Step5MnhlCnt", + ], + }, + { + name: "yy20Group", + header: { text: "2020년" }, + items: [ + { name: "yy20MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy20WholMnhlCnt", "yy20Step1MnhlCnt", "yy20Step2MnhlCnt", "yy20Step3MnhlCnt", "yy20Step4MnhlCnt"] }, + "yy20Step5MnhlCnt", + ], + }, + { + name: "yy21Group", + header: { text: "2021년" }, + items: [ + { name: "yy21MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy21WholMnhlCnt", "yy21Step1MnhlCnt", "yy21Step2MnhlCnt", "yy21Step3MnhlCnt", "yy21Step4MnhlCnt"] }, + "yy21Step5MnhlCnt", + ], + }, + { + name: "yy22Group", + header: { text: "2022년" }, + items: [ + { name: "yy22MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy22WholMnhlCnt", "yy22Step1MnhlCnt", "yy22Step2MnhlCnt", "yy22Step3MnhlCnt", "yy22Step4MnhlCnt"] }, + "yy22Step5MnhlCnt", + ], + }, + { + name: "yy23Group", + header: { text: "2023년" }, + items: [ + { name: "yy23MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy23WholMnhlCnt", "yy23Step1MnhlCnt", "yy23Step2MnhlCnt", "yy23Step3MnhlCnt", "yy23Step4MnhlCnt"] }, + "yy23Step5MnhlCnt", + ], + }, + { + name: "yy24Group", + header: { text: "2024년" }, + items: [ + { name: "yy24MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy24WholMnhlCnt", "yy24Step1MnhlCnt", "yy24Step2MnhlCnt", "yy24Step3MnhlCnt", "yy24Step4MnhlCnt"] }, + "yy24Step5MnhlCnt", + ], + }, + { + name: "yy25Group", + header: { text: "2025년" }, + items: [ + { name: "yy25MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy25WholMnhlCnt", "yy25Step1MnhlCnt", "yy25Step2MnhlCnt", "yy25Step3MnhlCnt", "yy25Step4MnhlCnt"] }, + "yy25Step5MnhlCnt", + ], + }, + "rmrk", +], +}, + ]; export const SewagePipeColumnInfoListTot = rawSewagePipeTotList.map((tab) => ({ ...tab, columns: tab.columns.map((col) => ({ ...col, editable: false as const })), })); + diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts index 6adccc917..78dd39e9c 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts @@ -28,7 +28,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe } public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`, params, ); @@ -36,7 +36,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe } public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`, params, ); diff --git a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts index d77b875c3..29b766b06 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-public-sewage-facility-input-service.ts @@ -8,6 +8,8 @@ import type { PublicSewageFacilitySaveResponse, PublicSewageFacilitySubmitRequest, PublicSewageFacilitySubmitResponse, + PublicSewageMainFacilityMappingSaveRequest, + PublicSewageMainFacilityMappingSaveResponse, PublicSewageMainFacilityResponse, PublicSewageMappingTargetsRequestParams, PublicSewageMappingTargetsResponse, @@ -41,10 +43,20 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics return response.data; } + public async saveMainFacilityMapping( + param: PublicSewageMainFacilityMappingSaveRequest, + ): Promise { + const response = await stClient.put( + `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/save`, + param, + ); + return response.data; + } + public async savePublicSewageFacility( param: PublicSewageFacilitySaveRequest, ): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/save`, param, ); @@ -54,7 +66,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics public async submitPublicSewageFacility( param: PublicSewageFacilitySubmitRequest, ): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/submit`, param, ); diff --git a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index 956abbd6b..24b271824 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -101,7 +101,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/save`, param, ); @@ -109,7 +109,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`, param, ); @@ -117,7 +117,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/submit`, param, ); @@ -125,7 +125,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/save`, param, ); @@ -133,7 +133,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async submitFactInstallStatus(param: SeweragePipeInputFactInstallStatusSubmitRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/submit`, param, ); @@ -141,7 +141,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/save`, param, ); @@ -149,7 +149,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async submitPipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSubmitRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/submit`, param, ); @@ -157,7 +157,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/save`, param, ); @@ -165,7 +165,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async submitPipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSubmitRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/submit`, param, ); @@ -173,7 +173,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/save`, param, ); @@ -181,7 +181,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS } public async submitManholeStatus(param: SeweragePipeInputManholeStatusSubmitRequest): Promise { - const response = await stClient.post( + const response = await stClient.put( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/submit`, param, ); diff --git a/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts b/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts index 476c8fecd..a770b45e2 100644 --- a/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts +++ b/apps/st/src/entities/statistics/api/notice-talk/notice-talk-sending-service.ts @@ -3,6 +3,8 @@ import { ENDPOINT } from "@/shared/config"; import type { PageableList } from "@/shared/model"; import type { + BusinessSendRequest, + BusinessSendResponse, NtftalkTemplate, TemplateVariablesResponse, } from "../../model/notice-talk/notice-talk-sending-interface"; @@ -38,6 +40,16 @@ export class NoticeTalkSendingApiService { ); return response.data; } + + async sendBusiness( + payload: BusinessSendRequest, + ): Promise { + const response = await client.post( + `${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/business`, + payload, + ); + return response.data; + } } export const noticeTalkSendingApi = new NoticeTalkSendingApiService(); diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index 37cd9e3a6..7b4ee34d1 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -8,26 +8,32 @@ import { type getReviewOpinionDetailRequest, type getReviewOpinionDetailResponse, type ImrRequest, + type ImrResponse, + type ImrStatusResponse, type ReviewOpinionSaveRequest, type ReviewOpinionSaveResponse, type ReviewReturnSaveRequest, type ReviewReturnSaveResponse, type ReviewSubmitRequest, type ReviewSubmitResponse, - type VerificationResultListRequest, - type VerificationResultListResponse, type SearchCondition, type Sgg, type StatisticsLogDetailResponse, type StatisticsLogRequest, type StatisticsLogResponse, type StatisticsService, + type StatsInputPeriodDeleteApiResponse, + type StatsInputPeriodDeleteRequest, + type StatsInputPeriodListApiResponse, + type StatsInputPeriodListRequest, + type StatsInputPeriodSaveApiResponse, + type StatsInputPeriodSaveRequest, type SubmitExceptionRequest, type SubmitExceptionResponse, - type ImrResponse, - type ImrStatusResponse, type SubmitVerifyExceptionRequest, type SubmitVerifyExceptionResponse, + type VerificationResultListRequest, + type VerificationResultListResponse, } from "../model/statistics-interface"; export class StatisticsApiService implements StatisticsService { @@ -63,6 +69,39 @@ export class StatisticsApiService implements StatisticsService { return response.data; } + /** 통계 입력 기간(연도·분류별) 목록 조회 — 경로는 백엔드 스펙에 맞게 수정 */ + public async getStatsInputPeriodList( + params?: StatsInputPeriodListRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/list`, + { params }, + ); + return response.data; + } + + /** 통계 입력 기간 저장 — 경로는 백엔드 스펙에 맞게 수정 */ + public async saveStatsInputPeriod( + param: StatsInputPeriodSaveRequest, + ): Promise { + const response = await stClient.post( + `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, + param, + ); + return response.data; + } + + /** 통계 입력 기간 삭제 — JSON body. HTTP 메서드·URL은 백엔드 스펙에 맞게 수정 */ + public async deleteStatsInputPeriod( + param: StatsInputPeriodDeleteRequest, + ): Promise { + const response = await stClient.delete( + `${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`, + { data: param }, + ); + return response.data; + } + // 입력 - 제출 취소 public async cancelSubmit( param: cancelSubmitRequest, diff --git a/apps/st/src/entities/statistics/api/statistics-total-service.ts b/apps/st/src/entities/statistics/api/statistics-total-service.ts index 402fd016c..e5efbb39b 100644 --- a/apps/st/src/entities/statistics/api/statistics-total-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-total-service.ts @@ -11,6 +11,7 @@ import type { SewagePipeFactInstallStatusResponse, SewagePipePipeDiamStatusResponse, SewagePipePipeTypeFacilityStatusResponse, + FcltyManholeSummaryResponse, SewagePipeTotalRequest, SewagePipeTotalResponse, } from "../model/statistics-total/statistics-sewage-pipe-total-interface"; @@ -19,6 +20,10 @@ import type { SewerageServiceLevelRequest, SewerageServiceLevelResidentFriendlyFactResponse } from "../model/statistics-total/statistics-service-level-total-interface"; +import type { + PublicSewageInquirySummaryResponse +} from "../model/statistics-total/statistics-sewage-public-total-interface"; + export class StatisticsTotalApiService { public async getSewerageSupplyRateList(params: SewerageSupplyRateRequest): Promise { @@ -84,6 +89,16 @@ export class StatisticsTotalApiService { ); return response.data; } + /** ST00020005 — 맨홀 시설현황(합계) */ + public async getSewagePipePipeMnhleStatus( + params: SewagePipeTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/sewer-pipe/manhle-status`, + { params }, + ); + return response.data; + } /** ST00030001 — 하수관로 유지관리 > 개보수(합계) */ public async getSewagePipeMaintainRepairTotal( @@ -106,6 +121,17 @@ export class StatisticsTotalApiService { ); return response.data; } + + /** ST00060001 —공공하수처리시설 */ + public async getSewagePublicFacillityTotal( + params: SewagePipeMaintainTotalRequest, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.TOTAL}/public-sewage`, + { params }, + ); + return response.data; + } } export const statisticsTotalStatusApi = new StatisticsTotalApiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/imr/imr-detail-grid-column-info.ts b/apps/st/src/entities/statistics/model/imr/imr-detail-grid-column-info.ts index ebdc3e018..4fe4c554a 100644 --- a/apps/st/src/entities/statistics/model/imr/imr-detail-grid-column-info.ts +++ b/apps/st/src/entities/statistics/model/imr/imr-detail-grid-column-info.ts @@ -11,10 +11,18 @@ const IMR_VISIBLE_REGION_COLUMNS = [ "cryrVl", ] as const; -/** ST00020003, ST00020005: 시도·구군·행정구역·관종구분 */ -const IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS = [ +/** ST00020003: 시도·구군·행정구역·관종구분·매설년도 */ +const IMR_VISIBLE_REGION_AND_PIPE_COLUMNS = [ ...IMR_VISIBLE_REGION_COLUMNS, "pipeSeNm", + "lyngYr", +] as const; + +/** ST00020005: 시도·구군·행정구역·중점관리구역구분·맨홀기준년도 */ +const IMR_VISIBLE_REGION_AND_MANH_COLUMNS = [ + ...IMR_VISIBLE_REGION_COLUMNS, + "emphsMngSeNm", + "manhYr", ] as const; /** ST00060001, ST00100001: 시도·구군·행정구역·시설코드·시설명·주소 */ @@ -40,8 +48,8 @@ export const IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD: Record< string, readonly string[] > = { - ST00020003: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS], - ST00020005: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS], + ST00020003: [...IMR_VISIBLE_REGION_AND_PIPE_COLUMNS], + ST00020005: [...IMR_VISIBLE_REGION_AND_MANH_COLUMNS], ST00060001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS], ST00100001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS], ST00040001: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS], @@ -80,6 +88,18 @@ export const imrDetailGridColumnInfo: ColumnInfo = { header: { text: "행정구역" }, width: 100, }, + { + name: "emphsMngSeNm", + fieldName: "emphsMngSeNm", + header: { text: "중점관리구역구분" }, + width: 80, + }, + { + name: "manhYr", + fieldName: "manhYr", + header: { text: "맨홀기준년도" }, + width: 100, + }, { name: "admdstCd", fieldName: "admdstCd", @@ -105,6 +125,12 @@ export const imrDetailGridColumnInfo: ColumnInfo = { header: { text: "관종구분" }, width: 80, }, + { + name: "lyngYr", + fieldName: "lyngYr", + header: { text: "매설년도" }, + width: 100, + }, { name: "vrfcTrgtSn", fieldName: "vrfcTrgtSn", @@ -126,10 +152,13 @@ export const imrDetailGridColumnInfo: ColumnInfo = { "sido", "gugun", "adstDong", + "emphsMngSeNm", + "manhYr", "admdstCd", "fcltNm", "addr", "pipeSeNm", + "lyngYr", "vrfcTrgtSn", "cryrVl", ], diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index 19c86e20c..efda5e1aa 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -151,6 +151,7 @@ export interface PublicSewageMappingTargetsRequestParams { /** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 항목 */ export interface PublicSewageMappingTargetItem { fcltSn: string; + admdstCd?: string; admdstNm: string; swtrmtFcltCd: string; swtrmtFcltNm: string; @@ -167,6 +168,40 @@ export interface PublicSewageMappingTargetsResponse { data: PublicSewageMappingTargetItem[]; } +/** ST00060002 중심처리시설 매핑 저장 — saveList 항목 */ +export interface PublicSewageMainFacilityMappingSaveItem { + fcltSn: string; + admdstCd: string; + admdstNm: string; + swtrmtFcltCd: string; + swtrmtFcltNm: string; + isMainYn: string; +} + +/** ST00060002 중심처리시설 매핑 저장 API 요청 */ +export interface PublicSewageMainFacilityMappingSaveRequest { + statsYr: string; + sewerBzmnCd: string; + /** 좌측에서 선택한 중심 처리 시설의 행정구역 */ + admdstCd: string; + admdstNm: string; + mainFcltSn: string; + mainFcltNm: string; + prxyBzentyNmCn: string; + cnsgnCstVl: number; + wholNope: number; + prxyCtrtCn: string; + userId: string; + saveList: PublicSewageMainFacilityMappingSaveItem[]; + deleteList: string[]; +} + +export interface PublicSewageMainFacilityMappingSaveResponse { + status: string; + message?: string; + data?: string; +} + /** ST00060001 공공하수처리시설 저장 — saveList 항목 */ export interface PublicSewageFacilitySaveItem { admdstCd: string; @@ -249,6 +284,7 @@ export interface StatisticsInputPublicSewageFacilityService { getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise; getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise; getMappingTargets(param: PublicSewageMappingTargetsRequestParams): Promise; + saveMainFacilityMapping(param: PublicSewageMainFacilityMappingSaveRequest): Promise; savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise; submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise; } diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 841db1ff3..9ad90befb 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -35,6 +35,46 @@ export interface ApiResponse { message?: string; } +/** 통계 입력 관리 등 — 연도·구분·분류(입력/검증)별 입력 기간 한 행 */ +export interface StatsInputPeriodItem { + statsYr: string; + seCd: number; + seType: "INPT" | "VRFC"; + inptBgngYmd: string; + inptEndYmd: string; +} + +/** `data`가 `StatsInputPeriodItem[]` 인 조회 응답 */ +export type StatsInputPeriodListApiResponse = ApiResponse; + +/** 통계 입력 기간 목록 조회 요청 쿼리 */ +export interface StatsInputPeriodListRequest { + statsYr?: string; + /** 전체 / 입력 / 검증 (SearchForm `clsfCd`와 동일) */ + seType?: "ALL" | "INPT" | "VRFC"; +} + +/** 통계 입력 기간 등록·수정 등 요청 본문 */ +export interface StatsInputPeriodSaveRequest { + statsYr: string; + seType: string; + seCd: number; + /** 시작일 (백엔드 형식 문자열, 예: YYYYMMDD) */ + inptBgngYmd: string; + /** 종료일 (백엔드 형식 문자열, 예: YYYYMMDD) */ + inptEndYmd: string; + rgtrId: string; +} + +/** 통계 입력 기간 저장 응답 */ +export type StatsInputPeriodSaveApiResponse = ApiResponse; + +/** 통계 입력 기간 삭제 요청 (필드 구성은 저장과 동일) */ +export type StatsInputPeriodDeleteRequest = StatsInputPeriodSaveRequest; + +/** 통계 입력 기간 삭제 응답 */ +export type StatsInputPeriodDeleteApiResponse = ApiResponse; + /** 하수 입력/보급률 등 조회 `data.statusInfo` 공통 (LAE 제출·담당자 정보) */ export interface SewerageStatusInfo { laePicNm: string | null; @@ -116,6 +156,7 @@ export interface VerificationResultReportItem { nowVlCn: string; compVlCn: string | null; prcsYn: string; + prcsYnNm: string; prcsOpnnExpln: string | null; prcrId: string | null; rjctOpnnCn: string | null; @@ -305,6 +346,11 @@ export interface StatisticsService { getSidoList(params?: { instSeCd?: string }): Promise>; getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; + getStatsInputPeriodList( + params?: StatsInputPeriodListRequest, + ): Promise; + saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise; + deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise; cancelSubmit(param: cancelSubmitRequest): Promise; callImrService(param: ImrRequest): Promise; submitException(param: SubmitExceptionRequest): Promise; diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts index 330403c73..29202d9c8 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface.ts @@ -32,7 +32,8 @@ export interface SewerPipeMtcItem { } export interface SewerPipeMtcResponseData { - mtcStatusList: SewerPipeMtcItem[]; + sewerPipeMtcList: SewerPipeMtcItem[]; + statsStatusInfo: StatsStatusInfo; } export interface SewerPipeMtcRequestParams { @@ -96,7 +97,8 @@ export interface SewerPipeDrdgItem { export interface SewerPipeDrdgResponseData { - drdgStatusList: SewerPipeDrdgItem[]; + sewerPipeDrdgList: SewerPipeDrdgItem[]; + statsStatusInfo: StatsStatusInfo; } @@ -104,4 +106,13 @@ export type SewerPipeDrdgApiResponse = ApiResponse; export interface SewagePipeMtcService { getSewerPipeMtcDrdgItem(params: SewerPipeMtcRequestParams): Promise; +} + +export interface StatsStatusInfo { + statsCd?: string; + submitStatusCd?: string; + prvnStatusCd?: string; + rjctOpnnCn?: string; + splmntOpnnCn?: string; + wrterOpnnCn?: string; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts index b059aa90c..6e81a41bc 100644 --- a/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-sewage-pipe-review-interface.ts @@ -63,7 +63,8 @@ export interface SewagePipeReviewrRegionalItem { } export interface SewagePipeReviewResponseData { - sewagePipeList: SewagePipeReviewrRegionalItem[]; + regionalStatusList: SewagePipeReviewrRegionalItem[]; + statsStatusInfo: StatsStatusInfo; } export interface SewagePipeReviewRequestParams { @@ -73,7 +74,7 @@ export interface SewagePipeReviewRequestParams { sewerBzmnCd?: string; } -export type SewagePipeReviewRegionalApiResponse = ApiResponse & { +export type SewagePipeReviewRegionalApiResponse = ApiResponse & { message: string; }; @@ -131,7 +132,12 @@ export interface SewagePipeReviewFacilityItem { } -export type SewagePipeFacilityApiResponse = ApiResponse & { +export interface SewagePipeFacilityResponseData { + facilityStatusList: SewagePipeReviewFacilityItem[]; + statsStatusInfo: StatsStatusInfo; +} + +export type SewagePipeFacilityApiResponse = ApiResponse & { message: string; }; @@ -165,10 +171,11 @@ export interface SewerPipeTypeItem { export interface SewerPipeTypeResponseData { - pipeTypeStatusList: SewerPipeTypeItem[]; + pipeTypeList: SewerPipeTypeItem[]; + statsStatusInfo: StatsStatusInfo; } -export type SewerPipeTypeApiResponse = ApiResponse; +export type SewerPipeTypeApiResponse = ApiResponse; export interface SewagePipeTypeService { @@ -225,9 +232,23 @@ export interface SewerPipeDiamItem { } -export type SewerPipeDiamApiResponse = ApiResponse; +export interface SewerPipeDiamResponseData { + pipeDiamList: SewerPipeDiamItem[]; + statsStatusInfo: StatsStatusInfo; +} + +export type SewerPipeDiamApiResponse = ApiResponse; export interface SewagePipeDiamService { getSewagePipeDiamList(params: SewagePipeReviewRequestParams): Promise; +} + +export interface StatsStatusInfo { + statsCd?: string; + submitStatusCd?: string; + prvnStatusCd?: string; + rjctOpnnCn?: string; + splmntOpnnCn?: string; + wrterOpnnCn?: string; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts index d9a7825cf..8643d41d6 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts @@ -4,6 +4,8 @@ export interface SewagePipeTotalRequest { statsYr?: string; sewerBzmnCd?: string; rowDataYn?: boolean; + summaryYn?: boolean; + summaryYearCnt?: number; } export interface SewagePipeTotalItem { @@ -234,3 +236,130 @@ export interface SewagePipePipeDiamStatusResponse { message?: string; data: SewagePipePipeDiamStatusItem[]; } + +/** + * 맨홀 현황 응답 아이템 인터페이스 + * DTO의 yy14(2014년 이전) ~ yy25(2024년) 필드를 모두 명시적으로 선언 + */ +export interface FcltyManholeSummaryItem { + /* --- 기본 정보 및 행 제어 --- */ + ctpvNm: string; // 시도명 + sggNm: string; // 시군구명 + admdstNm: string; // 행정구역명 + admdstCd: string; // 행정구역코드 + statsYr: string; // 통계연도 + rowType: string; // 데이터 구분 (OLD, CURR, SUMMARY 등) + emphsMngSeCd: string; // 중점관리구역구분코드 + emphsMngSeNm: string; // 중점관리구역구분명 + + totWholMnhlCnt: number; + totStep1MnhlCnt: number; + totStep2MnhlCnt: number; + totStep3MnhlCnt: number; + totStep4MnhlCnt: number; + totStep5MnhlCnt: number; + /* --- yy14 (2014년 이전) --- */ + yy14WholMnhlCnt: number; // 합계 + yy14Step1MnhlCnt: number; // 1단계 + yy14Step2MnhlCnt: number; // 2단계 + yy14Step3MnhlCnt: number; // 3단계 + yy14Step4MnhlCnt: number; // 4단계 + yy14Step5MnhlCnt: number; // 5단계 + + /* --- yy15 (2014년) --- */ + yy15WholMnhlCnt: number; + yy15Step1MnhlCnt: number; + yy15Step2MnhlCnt: number; + yy15Step3MnhlCnt: number; + yy15Step4MnhlCnt: number; + yy15Step5MnhlCnt: number; + + /* --- yy16 (2015년) --- */ + yy16WholMnhlCnt: number; + yy16Step1MnhlCnt: number; + yy16Step2MnhlCnt: number; + yy16Step3MnhlCnt: number; + yy16Step4MnhlCnt: number; + yy16Step5MnhlCnt: number; + + /* --- yy17 (2016년) --- */ + yy17WholMnhlCnt: number; + yy17Step1MnhlCnt: number; + yy17Step2MnhlCnt: number; + yy17Step3MnhlCnt: number; + yy17Step4MnhlCnt: number; + yy17Step5MnhlCnt: number; + + /* --- yy18 (2017년) --- */ + yy18WholMnhlCnt: number; + yy18Step1MnhlCnt: number; + yy18Step2MnhlCnt: number; + yy18Step3MnhlCnt: number; + yy18Step4MnhlCnt: number; + yy18Step5MnhlCnt: number; + + /* --- yy19 (2018년) --- */ + yy19WholMnhlCnt: number; + yy19Step1MnhlCnt: number; + yy19Step2MnhlCnt: number; + yy19Step3MnhlCnt: number; + yy19Step4MnhlCnt: number; + yy19Step5MnhlCnt: number; + + /* --- yy20 (2019년) --- */ + yy20WholMnhlCnt: number; + yy20Step1MnhlCnt: number; + yy20Step2MnhlCnt: number; + yy20Step3MnhlCnt: number; + yy20Step4MnhlCnt: number; + yy20Step5MnhlCnt: number; + + /* --- yy21 (2020년) --- */ + yy21WholMnhlCnt: number; + yy21Step1MnhlCnt: number; + yy21Step2MnhlCnt: number; + yy21Step3MnhlCnt: number; + yy21Step4MnhlCnt: number; + yy21Step5MnhlCnt: number; + + /* --- yy22 (2021년) --- */ + yy22WholMnhlCnt: number; + yy22Step1MnhlCnt: number; + yy22Step2MnhlCnt: number; + yy22Step3MnhlCnt: number; + yy22Step4MnhlCnt: number; + yy22Step5MnhlCnt: number; + + /* --- yy23 (2022년) --- */ + yy23WholMnhlCnt: number; + yy23Step1MnhlCnt: number; + yy23Step2MnhlCnt: number; + yy23Step3MnhlCnt: number; + yy23Step4MnhlCnt: number; + yy23Step5MnhlCnt: number; + + /* --- yy24 (2023년) --- */ + yy24WholMnhlCnt: number; + yy24Step1MnhlCnt: number; + yy24Step2MnhlCnt: number; + yy24Step3MnhlCnt: number; + yy24Step4MnhlCnt: number; + yy24Step5MnhlCnt: number; + + /* --- yy25 (2024년) --- */ + yy25WholMnhlCnt: number; + yy25Step1MnhlCnt: number; + yy25Step2MnhlCnt: number; + yy25Step3MnhlCnt: number; + yy25Step4MnhlCnt: number; + yy25Step5MnhlCnt: number; +} + +/** + * 맨홀 현황 API 공통 응답 규격 + */ +export interface FcltyManholeSummaryResponse { + status: string; // success / error 등 + message?: string; // 오류 메시지 등 + data: FcltyManholeSummaryItem[]; // 실제 리스트 데이터 +} diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-public-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-public-total-interface.ts new file mode 100644 index 000000000..34edcebf9 --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-public-total-interface.ts @@ -0,0 +1,103 @@ +export interface PublicSewageInquirySummaryRequest { + ctpvNm?: string; + sggNm?: string; + statsYr?: string; + swtrmtFcltNm?: string; + sewerBzmnCd?: string; + rowDataYn?: boolean; + capacityGbn?: string; + summaryYn?: boolean; + summaryYearCnt?: number; +} + +export interface PublicSewageInquirySummaryItem { + // 기본 정보 및 행정구역 + ctpvNm: string; + sggNm: string; + statsYr: string; + admdstCd: string; + admdstNm: string; + swtrmtFcltNm: string; + rowType: string; + fcltPstnCn: string; + + // 시설 용량 (㎥/일) + prcsFcltCpct: number; + prcsFcltPhysCpct: number; + prcsFcltBoilCpct: number; + prcsFcltAltdCpct: number; + + // 유입 및 처리량 (㎥/일) + inflwSwageQnt: number; + inflwSwagePhysQnt: number; + inflwSwageBoilQnt: number; + inflwSwageAltdQnt: number; + + prcsQnt: number; + prcsPhysCpct: number; + prcsBoilCpct: number; + prcsAltdCpct: number; + + // 수질 정보 (유입/방류) + inflwBodVl: number; + dschgBodVl: number; + inflwTocQnt: number; + dschgTocQnt: number; + inflwSsQnt: number; + dschgSsQnt: number; + inflwTnVl: number; + dschgTnVl: number; + inflwTpVl: number; + dschgTpVl: number; + inflwGrssClfgVl: number; + dschgGrssClfgVl: number; + + // 처리 효율 및 방법 + prcsRt: number; + prcsLodQnt: number; + prcsMthdExpln: string; + + // 연계 처리량 (㎥/일) + wholHmnQnt: number; + wholLvstQnt: number; + wholEfntQnt: number; + wholEtcQnt: number; + + // 부가 정보 + telNo: string; + cmptnStrtYmd: string; + oprtngStrtYmd: string; + wholBizVl: number; + eflntDsnfMthdExpln: string; + + // 방류수역 정보 + wtsysNm: string; + trbtrNm: string; + dtlUnitZoneCn: string; + rgnSeNm: string; + msirbCn: string; + goalWqNo: string; + + // 에너지 및 위탁현황 + engySrlneRt: number; + engyItslfPrdctnQnt: number; + anlEngyWholUseQnt: number; + anlElpwrUseQnt: number; + itslfEngyUseQnt: number; + swtrmtEmsnQnt: number; + operMnbdCn: string; + + // 위탁현황 + prxyBzentyNmCn: string; + cnsgnCstVl: number; + prxyCtrtCn: string; + intgPrcsFcltCn: string; + wholNope: number; + sftyAcdntEvntcsCnt: number; +} + +export interface PublicSewageInquirySummaryResponse { + status: string; + message: string; + data: PublicSewageInquirySummaryItem[]; +} \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/verification-article/verification-article-column.ts b/apps/st/src/entities/statistics/model/verification-article/verification-article-column.ts index e9e237338..7f45b5632 100644 --- a/apps/st/src/entities/statistics/model/verification-article/verification-article-column.ts +++ b/apps/st/src/entities/statistics/model/verification-article/verification-article-column.ts @@ -34,7 +34,7 @@ export const hbArticleColumnInfo: ColumnInfo = { { name: "ipcrCntVl", fieldName: "ipcrCntVl", - header: { text: "μ" }, + header: { text: "중요도 계수(μ)" }, editable: false, sortdisable: false, valueType: ValueType.NUMBER, @@ -43,7 +43,7 @@ export const hbArticleColumnInfo: ColumnInfo = { { name: "pctlCrtrVl", fieldName: "pctlCrtrVl", - header: { text: "Percentile" }, + header: { text: "백분위수 기준값(pct)" }, editable: false, sortdisable: false, valueType: ValueType.NUMBER, @@ -52,7 +52,7 @@ export const hbArticleColumnInfo: ColumnInfo = { { name: "scopCntCntVl", fieldName: "scopCntCntVl", - header: { text: "A" }, + header: { text: "스케일 조정 계수(A)" }, editable: false, sortdisable: false, valueType: ValueType.NUMBER, @@ -61,7 +61,7 @@ export const hbArticleColumnInfo: ColumnInfo = { { name: "thrsCntVl", fieldName: "thrsCntVl", - header: { text: "C" }, + header: { text: "임계 계수(c)" }, editable: false, sortdisable: false, valueType: ValueType.NUMBER,