Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-05-11 15:58:59 +09:00
24 changed files with 2221 additions and 270 deletions
+13
View File
@@ -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"] {
@@ -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<SearchFormValues>({
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 = {}) => {
</Select>
</div>
)}
{mergedProps.clsf && (
<div className="flex items-center gap-2">
<Label className="text-label">분류</Label>
<Select
value={values.clsfCd ?? "ALL"}
onValueChange={(value) =>
setValues({
...values,
clsfCd: value as SearchFormClsfCd,
})
}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="INPT">입력</SelectItem>
<SelectItem value="VRFC">검증</SelectItem>
</SelectContent>
</Select>
</div>
)}
{mergedProps.fctName && (
<div className="flex items-center gap-2">
<Label
@@ -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<string, string | number>;
const PRCS_YN_LABEL: Record<string, string> = {
"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}
/>
<VerificationResultHistoryPagination
pageCount={Math.max(pageCount, 1)}
@@ -215,7 +215,7 @@ const VerificationSelectionReport = ({
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
<div className="card card-border flex-1 bg-background !px-0">
<div className="border-b border-border flex justify-between">
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">검증 미통과 내역</h2>
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">이상치 선별 보고서</h2>
<span
role="button"
tabIndex={0}
@@ -0,0 +1,224 @@
"use client";
import { useEffect, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Label,
} from "@common/components";
export interface MainFacilityMappingSaveDialogValues {
prxyBzentyNmCn: string;
/** 위탁비용(백만원/년) */
cnsgnCstVl: number;
wholNope: number;
/** YYYYMMDD~YYYYMMDD */
prxyCtrtCn: string;
}
export interface MainFacilityMappingSaveDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
initialPrxyBzentyNmCn: string;
/** 위탁비용(백만원/년) 초기값 */
initialCnsgnCstVl: number;
initialWholNope: number;
/** 위탁계약기간 초기값 (YYYYMMDD~YYYYMMDD) */
initialPrxyCtrtCn: string;
onConfirm?: (values: MainFacilityMappingSaveDialogValues) => void | Promise<void>;
}
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 (
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent className="max-w-[48rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>저장 정보 입력</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">위탁업체명</Label>
<Input
value={prxyBzentyNmCn}
onChange={(e) => setPrxyBzentyNmCn(e.target.value)}
className="!text-[1.3rem]"
placeholder="위탁사업자명을 입력하세요."
disabled={isSubmitting}
/>
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">위탁비용(백만원/년)</Label>
<Input
type="text"
inputMode="decimal"
value={cnsgnCstInput}
onChange={(e) => {
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}
/>
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">전체 인원</Label>
<Input
type="text"
inputMode="numeric"
value={wholNopeInput}
onChange={(e) => {
const v = e.target.value.replace(/\D/g, "");
setWholNopeInput(v);
}}
className="!text-[1.3rem]"
placeholder="0"
disabled={isSubmitting}
/>
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">위탁계약기간</Label>
<Input
value={prxyCtrtCn}
onChange={(e) => setPrxyCtrtCn(e.target.value)}
className="!text-[1.3rem]"
placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)"
autoComplete="off"
disabled={isSubmitting}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSubmitting}>
취소
</Button>
</DialogClose>
<Button
type="button"
color="primary"
disabled={isSubmitting}
onClick={() => void handleConfirm()}
>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -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';
@@ -25,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';
@@ -32,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';
@@ -95,12 +99,14 @@ async function waitForImrJobCompleted(
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<void> {
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
const maxAttempts = options?.maxAttempts ?? 150;
for (let i = 0; i < maxAttempts; i++) {
const res = await getStatus(jobId);
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === 'COMPLETED' && Number(res.job_prog) === 100) return;
if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
@@ -110,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<boolean>(false);
const [hasSearched, setHasSearched] = useState(false);
const [isNotApplicable, setIsNotApplicable] = useState(false);
@@ -128,9 +154,16 @@ const PublicSewageFacilityInp = () => {
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
const [showDailog, setShowDailog] = useState<boolean>(false);
const [selectedMainFacilitySn, setSelectedMainFacilitySn] = useState<string | null>(null);
const mainFacilityMappingSaveDraftRef = useRef<MainFacilityMappingSaveDraft | null>(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<string, string | number>[]) | null>(null);
const submitFunc = () => {
void (async () => {
@@ -142,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;
@@ -220,72 +254,109 @@ 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 검증 처리 중 오류가 발생했습니다.');
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
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,
};
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));
},
},
);
}
} else {
alert(response.message || '제출 처리 결과를 확인해주세요.');
setGlobalProgress(100);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
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);
}
} else {
setGlobalProgress(100);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
setVerificationResultDialogOpen(true);
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
hideGlobalProgress();
}
};
@@ -296,14 +367,70 @@ const PublicSewageFacilityInp = () => {
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
if (activeTab === 'ST00060002') {
if (!(await confirm('저장 하시겠습니까?'))) return;
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const leftRows = rowDataMap.get('MainFacility') ?? [];
const rightRows = rowDataMap.get('MainFacilityTarget') ?? [];
if (!leftRows.length || !rightRows.length) {
alert('저장할 데이터가 없습니다.');
return;
}
if (!selectedMainFacilitySn) {
alert('좌측에서 중심 처리 시설을 먼저 선택해주세요.');
return;
}
if (activeTab === 'ST00060001') {
const currentData = rowDataMap.get('ST00060001') ?? [];
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;
@@ -321,86 +448,31 @@ const PublicSewageFacilityInp = () => {
saveList,
};
const response = await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(param);
const response =
await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(
param,
);
if (response.status === 'success') {
alert('저장되었습니다.');
setGlobalProgress(50);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
return;
}
if (activeTab === 'ST00060002') {
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 param: PublicSewageMainFacilityMappingSaveRequest = {
statsYr: lastSearchedValues.year ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
mainFcltSn: String(selectedMain.fcltSn ?? ''),
mainFcltNm: String(selectedMain.swtrmtFcltNm ?? ''),
prxyBzentyNmCn: String(selectedMain.prxyBzentyNmCn ?? ''),
cnsgnCstVl: Number(selectedMain.cnsgnCstVl ?? 0),
wholNope: Number(selectedMain.wholNope ?? 0),
prxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''),
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const response = await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param);
if (response.status === 'success') {
setGlobalProgress(100);
alert('저장되었습니다.');
await loadPublicSewageFacilityActiveTabData(
'ST00060002',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
return;
} catch {
alert('데이터 저장 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
alert('현재 탭 저장 API 연동 전입니다.');
} finally {
setIsLoading(false);
return;
}
alert('현재 탭 저장 API 연동 전입니다.');
})();
};
@@ -558,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('시도를 선택해주세요.');
@@ -754,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 (
<>
<div className="relative">
@@ -766,6 +915,9 @@ const PublicSewageFacilityInp = () => {
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
/>
{showEnterableToolbarButtons && !isMainFacilityTab && (
<>
{showIsNotApplicable && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />}
@@ -780,12 +932,13 @@ const PublicSewageFacilityInp = () => {
</div>
</div>
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
{isLoading && !globalProgressVisible && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
</div>
}
)}
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)}>
<TabsList>
{PublicSewageFactInpColumnList.map((tab) => (
@@ -904,6 +1057,9 @@ const PublicSewageFacilityInp = () => {
return next;
});
}}
getGridDataRef={
tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined
}
showHeaderCheckbox={false}
enableSort={false}
/>
@@ -912,6 +1068,11 @@ const PublicSewageFacilityInp = () => {
})}
</Tabs>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
/>
<SubmitDailog
open={showDailog}
onOpenChange={setShowDailog}
@@ -920,6 +1081,26 @@ const PublicSewageFacilityInp = () => {
submitMode="normal"
isLoading={isLoading}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={verificationQueryContext}
/>
<MainFacilityMappingSaveDialog
key={`main-facility-mapping-save-${mainFacilityMappingSaveDialogKey}`}
open={mainFacilityMappingSaveDialogOpen}
onOpenChange={(open) => {
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}
/>
</>
);
};
@@ -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("데이터 제출 중 오류가 발생했습니다.");
@@ -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 || '데이터 제출 중 오류가 발생했습니다.');
@@ -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<string, string | number>): "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<Record<string, string | number>[]>(
[],
);
const [periodDialogOpen, setPeriodDialogOpen] = useState(false);
const [periodDialogMode, setPeriodDialogMode] = useState<"edit" | "create">("edit");
const [createDefaultStatsYr, setCreateDefaultStatsYr] = useState("");
const [periodDialogInitial, setPeriodDialogInitial] =
useState<StatisticsInputPeriodDialogValues | null>(null);
const [searchFormValues, setSearchFormValues] = useState<SearchFormValues | null>(null);
const lastSearchRef = useRef<SearchFormValues | null>(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<string, string | number>) => {
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 (
<>
<SearchForm sido={false} />
<div className="relative">
<SearchForm
sido={false}
gugun={false}
clsf
onSubmit={handleSearch}
onReset={handleReset}
onValuesChange={setSearchFormValues}
/>
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<Button
color="primary"
className="h-[3.2rem] w-[96px]"
type="button"
onClick={handleRegisterClick}
>
<span>등록</span>
</Button>
</div>
</div>
<DataGrid
columnInfo={{ columns, columnLayout }}
rowData={rowData}
enableSort={false}
setRowData={setRowData}
isExcelImport={false}
isExcelExport={false}
onRowClick={handleRowClick}
/>
</>
<StatisticsInputPeriodDialog
open={periodDialogOpen}
mode={periodDialogMode}
createDefaultStatsYr={createDefaultStatsYr}
onOpenChange={(open) => {
setPeriodDialogOpen(open);
if (!open) {
setPeriodDialogInitial(null);
setPeriodDialogMode("edit");
}
}}
initial={periodDialogInitial}
onSaveSuccess={async () => {
const v = lastSearchRef.current;
if (v) await handleSearch(v);
}}
/>
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
);
};
@@ -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<void>;
}
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<Date | undefined>(undefined);
const [inptEndYmd, setInptEndYmd] = useState<Date | undefined>(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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[48rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{isCreate ? "입력 기간 등록" : "입력 관리"}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">연도</Label>
{isCreate ? (
<Input
type="text"
inputMode="numeric"
maxLength={4}
autoComplete="off"
value={statsYr}
onChange={(e) =>
setStatsYr(normalizeStatsYrInput(e.target.value))
}
placeholder="예: 2025"
disabled={isSubmitting}
className="!text-[1.3rem]"
/>
) : (
<Input
value={statsYr}
readOnly
disabled
className="!text-[1.3rem]"
/>
)}
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">분류</Label>
<Select
value={seType ?? undefined}
onValueChange={(v) => setSeType(v as "INPT" | "VRFC")}
disabled={isSubmitting}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INPT">입력</SelectItem>
<SelectItem value="VRFC">검증</SelectItem>
</SelectContent>
</Select>
</div>
{!isCreate && (
<div className="flex flex-col gap-2">
<Label className="text-label">구분</Label>
<Input
type="number"
inputMode="numeric"
min={0}
value={seCdInput}
disabled
className="!text-[1.3rem]"
/>
</div>
)}
<div className="flex flex-col gap-2">
<Label className="text-label">시작일</Label>
<InputDatePicker
value={inptBgngYmd}
onChange={setInptBgngYmd}
className="w-full"
/>
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">종료일</Label>
<InputDatePicker
value={inptEndYmd}
onChange={setInptEndYmd}
className="w-full"
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSubmitting}>
닫기
</Button>
</DialogClose>
{!isCreate && (
<Button
type="button"
color="red"
disabled={isSubmitting}
onClick={() => void handleDelete()}
>
삭제
</Button>
)}
<Button
type="button"
color="green"
disabled={isSubmitting}
onClick={() => void handleSave()}
>
{isCreate ? '등록' : '수정'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,14 +1,16 @@
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 DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
@@ -59,20 +61,51 @@ function extractReviewRowsFromResponseData(
}
}
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<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
/** 탭별 조회 응답의 statsStatusInfo — 활성 탭에 맞춰 버튼 노출 (SewagePipeInp의 tabStatusInfoMap과 동일 패턴) */
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
>(new Map());
const searchParamRef = useRef<SewagePipeReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(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();
@@ -101,6 +134,12 @@ 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);
@@ -108,9 +147,11 @@ const SewagePipeRvw = () => {
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
@@ -141,6 +182,12 @@ 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);
@@ -161,14 +208,19 @@ const SewagePipeRvw = () => {
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={() => { }} />
<ApproveButton approveFunc={() => { }} />
<ReturnButton returnFunc={() => { }} />
<SaveButton saveFunc={() => { }} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={() => {}} />
)}
{showReturnButton && <ReturnButton returnFunc={() => {}} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => {}} />
)}
</div>
</div>
</div>
)}
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
@@ -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<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<>
<SearchForm />
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" />
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<CellColorInfo />
<DataGrid
columnInfo={PublicSewageFacilityColumn}
rowStartNum={2}
isExcelImport={false}
fixedColumns={2}
rowData={rowData}
setRowData={setRowData}
cellStyleCallback={({ row }) => {
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';
}}
/>
</>
);
};
@@ -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);
@@ -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,
@@ -4,10 +4,10 @@ export const MainFacilityColumn = {
columns: [
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명' }, editable: false },
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '소재지' }, editable: false },
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' } },
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' }, editable: false },
{ name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, editable: false },
{ name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' }, editable: false },
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원수(명)' } },
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원수(명)' }, editable: false },
{ name: 'isMainYn', fieldName: 'isMainYn', header: { text: '중심처리시설여부' }, editable: false },
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, editable: false, styleName: 'rg-cell-readonly' },
@@ -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',
@@ -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' },
@@ -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',
],
};
@@ -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 })),
}));
@@ -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<StatsInputPeriodListApiResponse> {
const response = await stClient.get<StatsInputPeriodListApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/list`,
{ params },
);
return response.data;
}
/** 통계 입력 기간 저장 — 경로는 백엔드 스펙에 맞게 수정 */
public async saveStatsInputPeriod(
param: StatsInputPeriodSaveRequest,
): Promise<StatsInputPeriodSaveApiResponse> {
const response = await stClient.post<StatsInputPeriodSaveApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
param,
);
return response.data;
}
/** 통계 입력 기간 삭제 — JSON body. HTTP 메서드·URL은 백엔드 스펙에 맞게 수정 */
public async deleteStatsInputPeriod(
param: StatsInputPeriodDeleteRequest,
): Promise<StatsInputPeriodDeleteApiResponse> {
const response = await stClient.delete<StatsInputPeriodDeleteApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
{ data: param },
);
return response.data;
}
// 입력 - 제출 취소
public async cancelSubmit(
param: cancelSubmitRequest,
@@ -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<SewerageSupplyRateResponse> {
@@ -84,6 +89,16 @@ export class StatisticsTotalApiService {
);
return response.data;
}
/** ST00020005 — 맨홀 시설현황(합계) */
public async getSewagePipePipeMnhleStatus(
params: SewagePipeTotalRequest,
): Promise<FcltyManholeSummaryResponse> {
const response = await stClient.get<FcltyManholeSummaryResponse>(
`${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<PublicSewageInquirySummaryResponse> {
const response = await stClient.get<PublicSewageInquirySummaryResponse>(
`${ENDPOINT.ST_SERVICE.TOTAL}/public-sewage`,
{ params },
);
return response.data;
}
}
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
@@ -151,6 +151,7 @@ export interface PublicSewageMappingTargetsRequestParams {
/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 항목 */
export interface PublicSewageMappingTargetItem {
fcltSn: string;
admdstCd?: string;
admdstNm: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
@@ -181,6 +182,9 @@ export interface PublicSewageMainFacilityMappingSaveItem {
export interface PublicSewageMainFacilityMappingSaveRequest {
statsYr: string;
sewerBzmnCd: string;
/** 좌측에서 선택한 중심 처리 시설의 행정구역 */
admdstCd: string;
admdstNm: string;
mainFcltSn: string;
mainFcltNm: string;
prxyBzentyNmCn: string;
@@ -35,6 +35,46 @@ export interface ApiResponse<T> {
message?: string;
}
/** 통계 입력 관리 등 — 연도·구분·분류(입력/검증)별 입력 기간 한 행 */
export interface StatsInputPeriodItem {
statsYr: string;
seCd: number;
seType: "INPT" | "VRFC";
inptBgngYmd: string;
inptEndYmd: string;
}
/** `data`가 `StatsInputPeriodItem[]` 인 조회 응답 */
export type StatsInputPeriodListApiResponse = ApiResponse<StatsInputPeriodItem[]>;
/** 통계 입력 기간 목록 조회 요청 쿼리 */
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<unknown>;
/** 통계 입력 기간 삭제 요청 (필드 구성은 저장과 동일) */
export type StatsInputPeriodDeleteRequest = StatsInputPeriodSaveRequest;
/** 통계 입력 기간 삭제 응답 */
export type StatsInputPeriodDeleteApiResponse = ApiResponse<unknown>;
/** 하수 입력/보급률 등 조회 `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<ApiResponse<SearchCondition>>;
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
getStatsInputPeriodList(
params?: StatsInputPeriodListRequest,
): Promise<StatsInputPeriodListApiResponse>;
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<ImrResponse>;
submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse>;
@@ -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[]; // 실제 리스트 데이터
}
@@ -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[];
}