Merge branch 'iipbs/st/dev'

This commit is contained in:
2026-05-11 15:59:24 +09:00
38 changed files with 2536 additions and 321 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
@@ -14,6 +14,8 @@ const SplitDataGrid = ({
setRightRowData,
leftOnRowClick,
leftCellStyleCallback,
rightOnItemChecked,
rightOnItemAllChecked,
enableSort = false,
}: SplitDataGridProps) => {
return (
@@ -35,6 +37,8 @@ const SplitDataGrid = ({
enableSort={enableSort}
rowData={rightRowData}
setRowData={setRightRowData}
onItemChecked={rightOnItemChecked}
onItemAllChecked={rightOnItemAllChecked}
/>
</div>
);
@@ -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';
@@ -10,6 +10,8 @@ import type {
PublicSewageFacilityRequestParams,
PublicSewageFacilitySaveItem,
PublicSewageFacilitySaveRequest,
PublicSewageMainFacilityMappingSaveItem,
PublicSewageMainFacilityMappingSaveRequest,
PublicSewageMappingTargetsRequestParams,
} from '@/entities/statistics/model/input/statistics-public-sewage-facility-input-interface';
import {
@@ -23,6 +25,7 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '
import { getUserInfo } from '../../../../../../packages/common/components/src/api';
import { MainFacilityMappingSaveDialog } from './MainFacilityMappingSaveDialog';
import SubmitDailog from './SubmitDailog';
import CancelSubmitButton from '@/components/button/CancelSubmitButton';
@@ -30,9 +33,12 @@ import NotApplicableCheckButton from '@/components/button/NotApplicableCheckButt
import SaveButton from '@/components/button/SaveButton';
import SubmitButton from '@/components/button/SumitButton';
import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton';
import VerificationResultButton from '@/components/button/VerificationResultButton';
import DataGrid from '@/components/module/DataGrid';
import SearchForm from '@/components/module/SearchForm';
import SplitDataGrid from '@/components/module/SplitDataGrid';
import { VerificationResultDialog } from '@/components/module/VerificationResultDialog';
import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog';
import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
@@ -93,12 +99,14 @@ async function waitForImrJobCompleted(
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<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} 상태로 종료되었습니다.`);
@@ -108,11 +116,31 @@ async function waitForImrJobCompleted(
throw new Error('IMR 검증 완료 대기 시간이 초과되었습니다.');
}
type MainFacilityMappingSaveDraft = {
statsYr: string;
sewerBzmnCd: string;
admdstCd: string;
admdstNm: string;
mainFcltSn: string;
mainFcltNm: string;
initialCnsgnCstVl: number;
saveList: PublicSewageMainFacilityMappingSaveItem[];
deleteList: string[];
initialPrxyBzentyNmCn: string;
initialWholNope: number;
initialPrxyCtrtCn: string;
};
// 하수도 통계 관리 > 06. 공공하수처리시설
const PublicSewageFacilityInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [hasSearched, setHasSearched] = useState(false);
const [isNotApplicable, setIsNotApplicable] = useState(false);
@@ -126,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 () => {
@@ -140,7 +175,8 @@ const PublicSewageFacilityInp = () => {
alert('먼저 조회를 진행해주세요.');
return;
}
const currentData = rowDataMap.get('ST00060001') ?? [];
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('제출할 데이터가 없습니다.');
return;
@@ -199,6 +235,11 @@ const PublicSewageFacilityInp = () => {
return noApplicableVisible ? Boolean(isNotApplicable) : false;
};
const isCheckedRow = (row: Record<string, string | number>): boolean => {
const v = row.__checked;
return v === 1 || v === '1' || v === 'Y' || v === 'y' || v === 'true';
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) {
@@ -213,125 +254,225 @@ const PublicSewageFacilityInp = () => {
alert('먼저 조회를 진행해주세요.');
return;
}
const currentData = rowDataMap.get('ST00060001') ?? [];
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('제출할 데이터가 없습니다.');
return;
}
const userInfo = await getUserInfo();
const submitList = buildFacilitySaveList(currentData);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
noApplicable: getNoApplicableForRequest(),
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
submitList,
};
try {
setIsLoading(true);
const response = await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility(requestData);
if (response.status === 'success') {
if (response.data?.finalStatus === '5') {
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '',
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: response.data?.nvpassed ? '1' : '2',
};
try {
const imrResponse = await statisticsApi.callImrService(imrParam);
if (imrResponse.job_id) {
await waitForImrJobCompleted(
imrResponse.job_id,
(id) => statisticsApi.getImrStatus(id),
);
}
alert('제출되었습니다.');
} catch {
alert('제출은 완료되었으나 IMR 검증 처리 중 오류가 발생했습니다.');
}
} else {
alert(response.message || '제출 처리 결과를 확인해주세요.');
}
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
}
} finally {
setIsLoading(false);
}
};
const saveFunc = () => {
void (async () => {
if (activeTab !== 'ST00060001') {
alert('ST00060002 저장 API 연동 전입니다.');
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert('먼저 조회를 진행해주세요.');
return;
}
const currentData = rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('저장할 데이터가 없습니다.');
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
const userInfo = await getUserInfo();
const saveList = buildFacilitySaveList(currentData);
const param: PublicSewageFacilitySaveRequest = {
const submitList = buildFacilitySaveList(currentData);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? 'superAdmin' : '',
noApplicable: getNoApplicableForRequest(),
saveList,
laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
laePicTelNo: submitInfo.laePicTelNo,
laeIdfrNm: submitInfo.laeIdfrNm,
laeIdfrJbgdNm: submitInfo.laeIdfrJbgdNm,
laeIdfrDeptNm: submitInfo.laeIdfrDeptNm,
laeIdfrTelNo: submitInfo.laeIdfrTelNo,
submitList,
};
try {
setIsLoading(true);
const response = await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(param);
if (response.status === 'success') {
alert('저장되었습니다.');
setGlobalProgressLabel('일반 검증 진행 중...');
const response =
await statisticsInputPublicSewageFacilityApiService.submitPublicSewageFacility(
requestData,
);
if (response.status !== 'success') {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
return;
}
const finalStatus = response.data?.finalStatus;
const nvpassed = response.data?.nvpassed;
if (finalStatus === '5') {
setGlobalProgress(50);
setGlobalProgressLabel('IMR 검증 진행 중...');
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '',
stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: nvpassed ? '1' : '2',
};
try {
const imrResponse = await statisticsApi.callImrService(imrParam);
if (imrResponse.job_id) {
await waitForImrJobCompleted(
imrResponse.job_id,
(id) => statisticsApi.getImrStatus(id),
{
onProgress: (jobProgress) => {
setGlobalProgress(50 + Math.round(jobProgress / 2));
},
},
);
}
setGlobalProgress(100);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
setVerificationResultDialogOpen(true);
} catch (e) {
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
setVerificationResultDialogOpen(true);
const msg =
e instanceof Error && e.message
? e.message
: 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.';
alert(msg);
}
} finally {
setIsLoading(false);
} else {
setGlobalProgress(100);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
setVerificationResultDialogOpen(true);
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
};
const saveFunc = () => {
void (async () => {
if (!lastSearchedValues || !searchParamRef.current) {
alert('먼저 조회를 진행해주세요.');
return;
}
if (activeTab === 'ST00060002') {
if (!(await confirm('저장 하시겠습니까?'))) return;
const leftRows = rowDataMap.get('MainFacility') ?? [];
const rightRows = rowDataMap.get('MainFacilityTarget') ?? [];
if (!leftRows.length || !rightRows.length) {
alert('저장할 데이터가 없습니다.');
return;
}
if (!selectedMainFacilitySn) {
alert('좌측에서 중심 처리 시설을 먼저 선택해주세요.');
return;
}
const selectedMain = leftRows.find((r) => String(r.fcltSn ?? '') === selectedMainFacilitySn);
if (!selectedMain) {
alert('선택된 중심 처리 시설 정보를 찾을 수 없습니다.');
return;
}
const saveList: PublicSewageMainFacilityMappingSaveItem[] = rightRows
.filter((r) => isCheckedRow(r))
.map((r) => ({
fcltSn: String(r.fcltSn ?? ''),
admdstCd: String(r.admdstCd ?? ''),
admdstNm: String(r.admdstNm ?? ''),
swtrmtFcltCd: String(r.swtrmtFcltCd ?? ''),
swtrmtFcltNm: String(r.swtrmtFcltNm ?? ''),
isMainYn: String(r.isMainYn ?? ''),
}));
const deleteList = rightRows
.filter((r) => String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r))
.map((r) => String(r.fcltSn ?? ''))
.filter((sn) => sn !== '');
const wholNopeFromRow = Number(selectedMain.wholNope ?? 0);
mainFacilityMappingSaveDraftRef.current = {
statsYr: lastSearchedValues.year ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
admdstCd: String(selectedMain.admdstCd ?? ''),
admdstNm: String(selectedMain.admdstNm ?? ''),
mainFcltSn: String(selectedMain.fcltSn ?? ''),
mainFcltNm: String(selectedMain.swtrmtFcltNm ?? ''),
initialCnsgnCstVl: Number(selectedMain.cnsgnCstVl ?? 0),
saveList,
deleteList,
initialPrxyBzentyNmCn: String(selectedMain.prxyBzentyNmCn ?? ''),
initialWholNope: Number.isFinite(wholNopeFromRow) ? wholNopeFromRow : 0,
initialPrxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''),
};
setMainFacilityMappingSaveDialogKey((k) => k + 1);
setMainFacilityMappingSaveDialogOpen(true);
return;
}
if (!(await confirm('저장 하시겠습니까?'))) return;
if (activeTab === 'ST00060001') {
try {
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
const userInfo = await getUserInfo();
const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!currentData || currentData.length === 0) {
alert('저장할 데이터가 없습니다.');
return;
}
const saveList = buildFacilitySaveList(currentData);
const param: PublicSewageFacilitySaveRequest = {
statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId,
superAdmin: isAdminInfo ? 'superAdmin' : '',
noApplicable: getNoApplicableForRequest(),
saveList,
};
const response =
await statisticsInputPublicSewageFacilityApiService.savePublicSewageFacility(
param,
);
if (response.status === 'success') {
setGlobalProgress(50);
await loadPublicSewageFacilityActiveTabData(
'ST00060001',
searchParamRef.current,
lastSearchedValues,
);
setGlobalProgress(100);
alert('저장되었습니다.');
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 저장 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
return;
}
alert('현재 탭 저장 API 연동 전입니다.');
})();
};
@@ -489,6 +630,59 @@ const PublicSewageFacilityInp = () => {
}
};
const handleMainFacilityMappingSaveConfirm = async (input: {
prxyBzentyNmCn: string;
cnsgnCstVl: number;
wholNope: number;
prxyCtrtCn: string;
}) => {
const draft = mainFacilityMappingSaveDraftRef.current;
if (!draft || !searchParamRef.current || !lastSearchedValues) {
setMainFacilityMappingSaveDialogOpen(false);
mainFacilityMappingSaveDraftRef.current = null;
return;
}
try {
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
const userInfo = await getUserInfo();
const param: PublicSewageMainFacilityMappingSaveRequest = {
statsYr: draft.statsYr,
sewerBzmnCd: draft.sewerBzmnCd,
admdstCd: draft.admdstCd,
admdstNm: draft.admdstNm,
mainFcltSn: draft.mainFcltSn,
mainFcltNm: draft.mainFcltNm,
prxyBzentyNmCn: input.prxyBzentyNmCn,
cnsgnCstVl: input.cnsgnCstVl,
wholNope: input.wholNope,
prxyCtrtCn: input.prxyCtrtCn,
userId: userInfo.userUnqId,
saveList: draft.saveList,
deleteList: draft.deleteList,
};
const response =
await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param);
if (response.status === 'success') {
setGlobalProgress(50);
setMainFacilityMappingSaveDialogOpen(false);
mainFacilityMappingSaveDraftRef.current = null;
await loadPublicSewageFacilityActiveTabData(
'ST00060002',
searchParamRef.current,
lastSearchedValues,
);
setGlobalProgress(100);
alert('저장되었습니다.');
} else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 저장 중 오류가 발생했습니다.');
} finally {
hideGlobalProgress();
}
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
@@ -685,6 +879,30 @@ const PublicSewageFacilityInp = () => {
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar;
const isMainFacilityTab = activeTab === 'ST00060002';
const openVerificationHistoryDialog = () => {
const ctx = lastSearchedValues ?? searchValues;
const activeTabStatsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) {
alert('먼저 조회를 진행해주세요.');
return;
}
setVerificationHistoryOpen(true);
};
const verificationQueryContext =
(() => {
const ctx = lastSearchedValues ?? searchValues;
const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return {
sewerBzmnCd: ctx.bsnmCd,
ctpvNm: ctx.sido,
sggNm: ctx.gugun === 'ALL' ? '' : (ctx.gugun ?? ''),
statsYr: ctx.year,
statsCd,
};
})();
return (
<>
<div className="relative">
@@ -697,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} />}
@@ -711,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) => (
@@ -753,6 +975,38 @@ const PublicSewageFacilityInp = () => {
? leftMainFacilityCellStyleCallback
: undefined
}
rightOnItemChecked={
tab.tabKey === 'ST00060002'
? (itemIndex, checked) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])];
if (!rows[itemIndex]) return prev;
rows[itemIndex] = {
...rows[itemIndex],
__checked: checked ? 'Y' : '',
};
next.set('MainFacilityTarget', rows);
return next;
});
}
: undefined
}
rightOnItemAllChecked={
tab.tabKey === 'ST00060002'
? (checked) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({
...r,
__checked: checked ? 'Y' : '',
}));
next.set('MainFacilityTarget', rows);
return next;
});
}
: undefined
}
setLeftRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
@@ -803,6 +1057,9 @@ const PublicSewageFacilityInp = () => {
return next;
});
}}
getGridDataRef={
tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined
}
showHeaderCheckbox={false}
enableSort={false}
/>
@@ -811,6 +1068,11 @@ const PublicSewageFacilityInp = () => {
})}
</Tabs>
</div>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={verificationQueryContext}
/>
<SubmitDailog
open={showDailog}
onOpenChange={setShowDailog}
@@ -819,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 || '데이터 제출 중 오류가 발생했습니다.');
@@ -8,14 +8,15 @@ import {
type RowSelectionState,
useReactTable,
} from "@tanstack/react-table";
import type { AxiosError } from "axios";
import { client } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
import type { ApiErrorResponse } from "@/shared/model";
import {
type AdstZoneResponseDto,
type BusinessSendRequest,
type BusinessSendResponse,
noticeTalkSendingApi,
type SelectedRecipient,
type SewerStUserItem,
useNtftalkTemplateList,
@@ -122,10 +123,26 @@ function NtitlkSndng() {
const [previewRecipient, setPreviewRecipient] =
useState<SelectedRecipient | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [isSending, setIsSending] = useState(false);
const [userListRowSelection, setUserListRowSelection] =
useState<RowSelectionState>({});
const sendMutation = useMutationWrapper<
BusinessSendResponse,
AxiosError<ApiErrorResponse>,
BusinessSendRequest
>({
mutationFn: (payload) => noticeTalkSendingApi.sendBusiness(payload),
onSuccess: (data) => {
if (!data.success) {
alert(data.message || "알림톡 발송 요청에 실패했습니다.");
return;
}
alert(data.message || "알림톡 발송이 요청되었습니다.");
setSelectedRecipients([]);
setCommonVariableValues({});
},
});
const { data: templatePageData } = useNtftalkTemplateList();
const templates = templatePageData?.content ?? [];
@@ -321,7 +338,7 @@ function NtitlkSndng() {
}),
);
if (missingVariable) {
alert("필수 변수값이 입력되지 않은 발송 대상이 있습니다.");
alert("변수값이 입력되지 않은 발송 대상이 있습니다.");
return;
}
if (
@@ -331,34 +348,13 @@ function NtitlkSndng() {
)
return;
const payload: BusinessSendRequest = {
sendMutation.mutate({
templateCode: selectedTemplateCode,
callback: "032-590-4000",
recipients: selectedRecipients.map((recipient) =>
getMergedVariables(recipient, commonVariableValues),
),
};
try {
setIsSending(true);
const response = await client.post<BusinessSendResponse>(
`${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/business`,
payload,
);
const data = response.data;
if (!data.success) {
alert(data.message || "알림톡 발송 요청에 실패했습니다.");
return;
}
alert(data.message || `알림톡 발송이 요청되었습니다.`);
setSelectedRecipients([]);
setCommonVariableValues({});
} catch {
alert("알림톡 발송 중 오류가 발생했습니다.");
} finally {
setIsSending(false);
}
});
};
return (
@@ -567,7 +563,8 @@ function NtitlkSndng() {
color="green"
onClick={handleSend}
disabled={
isSending || selectedRecipients.length === 0
sendMutation.isPending ||
selectedRecipients.length === 0
}
>
발송
@@ -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>
);
}
@@ -9,8 +9,8 @@ import ApproveButton from "@/components/button/ApproveButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SaveButton from "@/components/button/SaveButton";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
@@ -40,10 +40,12 @@ function getListDataByTab(tabKey: string, responseData: unknown): object[] {
return [];
}
if (tabKey === "ST00030001") {
return (((responseData as { mtcStatusList?: unknown[] }).mtcStatusList) ?? []) as object[];
const list = (responseData as { sewerPipeMtcList?: unknown }).sewerPipeMtcList;
return Array.isArray(list) ? (list as object[]) : [];
}
if (tabKey === "ST00030002") {
return (((responseData as { drdgStatusList?: unknown[] }).drdgStatusList) ?? []) as object[];
const list = (responseData as { sewerPipeDrdgList?: unknown }).sewerPipeDrdgList;
return Array.isArray(list) ? (list as object[]) : [];
}
return [];
}
@@ -1,16 +1,18 @@
import { useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SewagePipeReviewRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
SewagePipeReviewRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SaveButton from "@/components/button/SaveButton";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
@@ -37,20 +39,73 @@ async function fetchSewagePipeReviewTab(
return null;
}
function extractReviewRowsFromResponseData(
tabKey: string,
data: unknown,
): object[] {
if (Array.isArray(data)) return data as object[];
if (!data || typeof data !== "object") return [];
const d = data as Record<string, unknown>;
switch (tabKey) {
case "ST00020001":
return Array.isArray(d.regionalStatusList) ? (d.regionalStatusList as object[]) : [];
case "ST00020002":
return Array.isArray(d.facilityStatusList) ? (d.facilityStatusList as object[]) : [];
case "ST00020003":
return Array.isArray(d.pipeTypeList) ? (d.pipeTypeList as object[]) : [];
case "ST00020004":
return Array.isArray(d.pipeDiamList) ? (d.pipeDiamList as object[]) : [];
default:
return [];
}
}
function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
if (!data || typeof data !== "object") return null;
const ssi = (data as { statsStatusInfo?: StatsStatusInfo }).statsStatusInfo;
return ssi && typeof ssi === "object" ? ssi : null;
}
// 하수도 통계 검토 02.하수관로
const SewagePipeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<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();
@@ -79,15 +134,24 @@ const SewagePipeRvw = () => {
const response = await fetchSewagePipeReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, ssi ?? {});
return next;
});
const rows = extractReviewRowsFromResponseData(activeTab, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(response.data as object[]));
next.set(activeTab, toGridRows(rows));
return next;
});
} else {
setTabStatsStatusInfoMap(new Map());
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
setTabStatsStatusInfoMap(new Map());
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
@@ -118,9 +182,16 @@ const SewagePipeRvw = () => {
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === "success" && response.data) {
const ssi = extractStatsStatusInfo(response.data);
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(tabKey, ssi ?? {});
return next;
});
const rows = extractReviewRowsFromResponseData(tabKey, response.data);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
next.set(tabKey, toGridRows(rows));
return next;
});
} else {
@@ -137,14 +208,19 @@ const SewagePipeRvw = () => {
<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);
@@ -114,10 +114,10 @@ export default function HbStatisticsVerification() {
const [verifyType, setVerifyType] = useState<VerifyType>("ALL");
const [formState, setFormState] = useState<VerificationFormState>({
mu: "0",
percentile: "0",
a: "0",
c: "0",
mu: "",
percentile: "",
a: "",
c: "",
verificationYear: defaultYear,
});
const [selectedTargets, setSelectedTargets] = useState<string[]>([]);
@@ -559,11 +559,14 @@ export default function HbStatisticsVerification() {
<div className="space-y-3">
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
μ
<Label className="w-48 whitespace-nowrap text-label">
중요도 계수(μ)
</Label>
<Input
type="number"
min="0"
max="1"
step="0.1"
value={formState.mu}
onChange={(event) =>
handleFormChange(
@@ -572,15 +575,19 @@ export default function HbStatisticsVerification() {
)
}
className="flex-1"
placeholder="(0 ~ 1, 0.1 단위)"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
Percentile
<Label className="w-48 text-label">
백분위수 기준값(pct)
</Label>
<Input
type="number"
min="0"
max="0.5"
step="0.01"
value={formState.percentile}
onChange={(event) =>
handleFormChange(
@@ -589,15 +596,19 @@ export default function HbStatisticsVerification() {
)
}
className="flex-1"
placeholder="(0 ~ 0.5, 0.01 단위)"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
A
<Label className="w-48 text-label">
스케일 조정 계수(A)
</Label>
<Input
type="number"
min="0"
max="1"
step="0.01"
value={formState.a}
onChange={(event) =>
handleFormChange(
@@ -606,15 +617,19 @@ export default function HbStatisticsVerification() {
)
}
className="flex-1"
placeholder="(0 ~ 1, 0.01 단위)"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
C
<Label className="w-48 text-label">
임계 계수(c)
</Label>
<Input
type="number"
min="0"
max="100"
step="1"
value={formState.c}
onChange={(event) =>
handleFormChange(
@@ -623,6 +638,7 @@ export default function HbStatisticsVerification() {
)
}
className="flex-1"
placeholder="(0 ~ 100, 1 단위)"
/>
</div>
</div>
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Search } from "lucide-react";
import { CodeSelect } from "@/entities/code/ui/code-select";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
@@ -368,7 +367,7 @@ export function VerificationTargetDialog({
type="number"
min="0"
max="0.5"
step="0.1"
step="0.01"
value={
field.value ??
""
@@ -385,7 +384,7 @@ export function VerificationTargetDialog({
: null,
)
}
placeholder="(0 ~ 0.5, 0.1 단위)"
placeholder="(0 ~ 0.5, 0.01 단위)"
/>
</div>
)}
@@ -369,6 +369,10 @@ export type SplitDataGridProps = {
leftOnRowClick?: (rowData: Record<string, string | number>) => void;
/** 좌측 분할 그리드 셀 스타일 콜백 */
leftCellStyleCallback?: CellStyleCallback;
/** 우측 분할 그리드 행 체크 변경 */
rightOnItemChecked?: (itemIndex: number, checked: boolean) => void;
/** 우측 분할 그리드 헤더 전체 체크 변경 */
rightOnItemAllChecked?: (checked: boolean) => void;
/** 전체 정렬 기능 on/off (기본값: false) */
enableSort?: boolean;
};
@@ -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,
@@ -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 })),
}));
@@ -28,7 +28,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe
}
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveResponse> {
const response = await stClient.post<SewerageSupplyRateSaveResponse>(
const response = await stClient.put<SewerageSupplyRateSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`,
params,
);
@@ -36,7 +36,7 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe
}
public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSubmitResponse> {
const response = await stClient.post<SewerageSupplyRateSubmitResponse>(
const response = await stClient.put<SewerageSupplyRateSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`,
params,
);
@@ -8,6 +8,8 @@ import type {
PublicSewageFacilitySaveResponse,
PublicSewageFacilitySubmitRequest,
PublicSewageFacilitySubmitResponse,
PublicSewageMainFacilityMappingSaveRequest,
PublicSewageMainFacilityMappingSaveResponse,
PublicSewageMainFacilityResponse,
PublicSewageMappingTargetsRequestParams,
PublicSewageMappingTargetsResponse,
@@ -41,10 +43,20 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics
return response.data;
}
public async saveMainFacilityMapping(
param: PublicSewageMainFacilityMappingSaveRequest,
): Promise<PublicSewageMainFacilityMappingSaveResponse> {
const response = await stClient.put<PublicSewageMainFacilityMappingSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/main-facility/save`,
param,
);
return response.data;
}
public async savePublicSewageFacility(
param: PublicSewageFacilitySaveRequest,
): Promise<PublicSewageFacilitySaveResponse> {
const response = await stClient.post<PublicSewageFacilitySaveResponse>(
const response = await stClient.put<PublicSewageFacilitySaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/save`,
param,
);
@@ -54,7 +66,7 @@ export class StatisticsInputPublicSewageFacilityApiService implements Statistics
public async submitPublicSewageFacility(
param: PublicSewageFacilitySubmitRequest,
): Promise<PublicSewageFacilitySubmitResponse> {
const response = await stClient.post<PublicSewageFacilitySubmitResponse>(
const response = await stClient.put<PublicSewageFacilitySubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/public-sewage-facility/facility/submit`,
param,
);
@@ -101,7 +101,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async saveFactConnection(param: SeweragePipeFactConnectionSaveRequest): Promise<SeweragePipeFactConnectionSaveResponse> {
const response = await stClient.post<SeweragePipeFactConnectionSaveResponse>(
const response = await stClient.put<SeweragePipeFactConnectionSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-connection-status/popup/save`,
param,
);
@@ -109,7 +109,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveResponse>(
const response = await stClient.put<SeweragePipeInputRegionalFactStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`,
param,
);
@@ -117,7 +117,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async submitRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSubmitRequest): Promise<SeweragePipeInputRegionalFactStatusSubmitResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSubmitResponse>(
const response = await stClient.put<SeweragePipeInputRegionalFactStatusSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/submit`,
param,
);
@@ -125,7 +125,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async saveFactInstallStatus(param: SeweragePipeInputFactInstallStatusSaveRequest): Promise<SeweragePipeInputFactInstallStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputFactInstallStatusSaveResponse>(
const response = await stClient.put<SeweragePipeInputFactInstallStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/save`,
param,
);
@@ -133,7 +133,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async submitFactInstallStatus(param: SeweragePipeInputFactInstallStatusSubmitRequest): Promise<SeweragePipeInputFactInstallStatusSubmitResponse> {
const response = await stClient.post<SeweragePipeInputFactInstallStatusSubmitResponse>(
const response = await stClient.put<SeweragePipeInputFactInstallStatusSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/facility-install-status/submit`,
param,
);
@@ -141,7 +141,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async savePipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSaveRequest): Promise<SeweragePipeInputPipeTypeStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputPipeTypeStatusSaveResponse>(
const response = await stClient.put<SeweragePipeInputPipeTypeStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/save`,
param,
);
@@ -149,7 +149,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async submitPipeTypeStatus(param: SeweragePipeInputPipeTypeStatusSubmitRequest): Promise<SeweragePipeInputSubmitResponse> {
const response = await stClient.post<SeweragePipeInputSubmitResponse>(
const response = await stClient.put<SeweragePipeInputSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-type-status/submit`,
param,
);
@@ -157,7 +157,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async savePipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSaveRequest): Promise<SeweragePipeInputPipeDiameterStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputPipeDiameterStatusSaveResponse>(
const response = await stClient.put<SeweragePipeInputPipeDiameterStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/save`,
param,
);
@@ -165,7 +165,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async submitPipeDiameterStatus(param: SeweragePipeInputPipeDiameterStatusSubmitRequest): Promise<SeweragePipeInputSubmitResponse> {
const response = await stClient.post<SeweragePipeInputSubmitResponse>(
const response = await stClient.put<SeweragePipeInputSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/pipe-diameter-status/submit`,
param,
);
@@ -173,7 +173,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async saveManholeStatus(param: SeweragePipeInputManholeStatusSaveRequest): Promise<SeweragePipeInputManholeStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputManholeStatusSaveResponse>(
const response = await stClient.put<SeweragePipeInputManholeStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/save`,
param,
);
@@ -181,7 +181,7 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
}
public async submitManholeStatus(param: SeweragePipeInputManholeStatusSubmitRequest): Promise<SeweragePipeInputSubmitResponse> {
const response = await stClient.post<SeweragePipeInputSubmitResponse>(
const response = await stClient.put<SeweragePipeInputSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/manhole-status/submit`,
param,
);
@@ -3,6 +3,8 @@ import { ENDPOINT } from "@/shared/config";
import type { PageableList } from "@/shared/model";
import type {
BusinessSendRequest,
BusinessSendResponse,
NtftalkTemplate,
TemplateVariablesResponse,
} from "../../model/notice-talk/notice-talk-sending-interface";
@@ -38,6 +40,16 @@ export class NoticeTalkSendingApiService {
);
return response.data;
}
async sendBusiness(
payload: BusinessSendRequest,
): Promise<BusinessSendResponse> {
const response = await client.post<BusinessSendResponse>(
`${ENDPOINT.NTFTALK_SERVICE.ATA_SEND}/business`,
payload,
);
return response.data;
}
}
export const noticeTalkSendingApi = new NoticeTalkSendingApiService();
@@ -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();
@@ -11,10 +11,18 @@ const IMR_VISIBLE_REGION_COLUMNS = [
"cryrVl",
] as const;
/** ST00020003, ST00020005: 시도·구군·행정구역·관종구분 */
const IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS = [
/** ST00020003: 시도·구군·행정구역·관종구분·매설년도 */
const IMR_VISIBLE_REGION_AND_PIPE_COLUMNS = [
...IMR_VISIBLE_REGION_COLUMNS,
"pipeSeNm",
"lyngYr",
] as const;
/** ST00020005: 시도·구군·행정구역·중점관리구역구분·맨홀기준년도 */
const IMR_VISIBLE_REGION_AND_MANH_COLUMNS = [
...IMR_VISIBLE_REGION_COLUMNS,
"emphsMngSeNm",
"manhYr",
] as const;
/** ST00060001, ST00100001: 시도·구군·행정구역·시설코드·시설명·주소 */
@@ -40,8 +48,8 @@ export const IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD: Record<
string,
readonly string[]
> = {
ST00020003: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS],
ST00020005: [...IMR_VISIBLE_REGION_AND_PIPE_SE_COLUMNS],
ST00020003: [...IMR_VISIBLE_REGION_AND_PIPE_COLUMNS],
ST00020005: [...IMR_VISIBLE_REGION_AND_MANH_COLUMNS],
ST00060001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS],
ST00100001: [...IMR_VISIBLE_FACILITY_FULL_COLUMNS],
ST00040001: [...IMR_VISIBLE_FACILITY_NAME_ADDR_COLUMNS],
@@ -80,6 +88,18 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
header: { text: "행정구역" },
width: 100,
},
{
name: "emphsMngSeNm",
fieldName: "emphsMngSeNm",
header: { text: "중점관리구역구분" },
width: 80,
},
{
name: "manhYr",
fieldName: "manhYr",
header: { text: "맨홀기준년도" },
width: 100,
},
{
name: "admdstCd",
fieldName: "admdstCd",
@@ -105,6 +125,12 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
header: { text: "관종구분" },
width: 80,
},
{
name: "lyngYr",
fieldName: "lyngYr",
header: { text: "매설년도" },
width: 100,
},
{
name: "vrfcTrgtSn",
fieldName: "vrfcTrgtSn",
@@ -126,10 +152,13 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
"sido",
"gugun",
"adstDong",
"emphsMngSeNm",
"manhYr",
"admdstCd",
"fcltNm",
"addr",
"pipeSeNm",
"lyngYr",
"vrfcTrgtSn",
"cryrVl",
],
@@ -151,6 +151,7 @@ export interface PublicSewageMappingTargetsRequestParams {
/** ST00060002 메인시설 클릭 시 우측 매핑대상 조회 항목 */
export interface PublicSewageMappingTargetItem {
fcltSn: string;
admdstCd?: string;
admdstNm: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
@@ -167,6 +168,40 @@ export interface PublicSewageMappingTargetsResponse {
data: PublicSewageMappingTargetItem[];
}
/** ST00060002 중심처리시설 매핑 저장 — saveList 항목 */
export interface PublicSewageMainFacilityMappingSaveItem {
fcltSn: string;
admdstCd: string;
admdstNm: string;
swtrmtFcltCd: string;
swtrmtFcltNm: string;
isMainYn: string;
}
/** ST00060002 중심처리시설 매핑 저장 API 요청 */
export interface PublicSewageMainFacilityMappingSaveRequest {
statsYr: string;
sewerBzmnCd: string;
/** 좌측에서 선택한 중심 처리 시설의 행정구역 */
admdstCd: string;
admdstNm: string;
mainFcltSn: string;
mainFcltNm: string;
prxyBzentyNmCn: string;
cnsgnCstVl: number;
wholNope: number;
prxyCtrtCn: string;
userId: string;
saveList: PublicSewageMainFacilityMappingSaveItem[];
deleteList: string[];
}
export interface PublicSewageMainFacilityMappingSaveResponse {
status: string;
message?: string;
data?: string;
}
/** ST00060001 공공하수처리시설 저장 — saveList 항목 */
export interface PublicSewageFacilitySaveItem {
admdstCd: string;
@@ -249,6 +284,7 @@ export interface StatisticsInputPublicSewageFacilityService {
getPublicSewageFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageFacilityResponse>;
getPublicSewageMainFacility(param: PublicSewageFacilityRequestParams): Promise<PublicSewageMainFacilityResponse>;
getMappingTargets(param: PublicSewageMappingTargetsRequestParams): Promise<PublicSewageMappingTargetsResponse>;
saveMainFacilityMapping(param: PublicSewageMainFacilityMappingSaveRequest): Promise<PublicSewageMainFacilityMappingSaveResponse>;
savePublicSewageFacility(param: PublicSewageFacilitySaveRequest): Promise<PublicSewageFacilitySaveResponse>;
submitPublicSewageFacility(param: PublicSewageFacilitySubmitRequest): Promise<PublicSewageFacilitySubmitResponse>;
}
@@ -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>;
@@ -32,7 +32,8 @@ export interface SewerPipeMtcItem {
}
export interface SewerPipeMtcResponseData {
mtcStatusList: SewerPipeMtcItem[];
sewerPipeMtcList: SewerPipeMtcItem[];
statsStatusInfo: StatsStatusInfo;
}
export interface SewerPipeMtcRequestParams {
@@ -96,7 +97,8 @@ export interface SewerPipeDrdgItem {
export interface SewerPipeDrdgResponseData {
drdgStatusList: SewerPipeDrdgItem[];
sewerPipeDrdgList: SewerPipeDrdgItem[];
statsStatusInfo: StatsStatusInfo;
}
@@ -104,4 +106,13 @@ export type SewerPipeDrdgApiResponse = ApiResponse<SewerPipeDrdgResponseData>;
export interface SewagePipeMtcService {
getSewerPipeMtcDrdgItem(params: SewerPipeMtcRequestParams): Promise<SewerPipeDrdgApiResponse>;
}
export interface StatsStatusInfo {
statsCd?: string;
submitStatusCd?: string;
prvnStatusCd?: string;
rjctOpnnCn?: string;
splmntOpnnCn?: string;
wrterOpnnCn?: string;
}
@@ -63,7 +63,8 @@ export interface SewagePipeReviewrRegionalItem {
}
export interface SewagePipeReviewResponseData {
sewagePipeList: SewagePipeReviewrRegionalItem[];
regionalStatusList: SewagePipeReviewrRegionalItem[];
statsStatusInfo: StatsStatusInfo;
}
export interface SewagePipeReviewRequestParams {
@@ -73,7 +74,7 @@ export interface SewagePipeReviewRequestParams {
sewerBzmnCd?: string;
}
export type SewagePipeReviewRegionalApiResponse = ApiResponse<SewagePipeReviewrRegionalItem[]> & {
export type SewagePipeReviewRegionalApiResponse = ApiResponse<SewagePipeReviewResponseData> & {
message: string;
};
@@ -131,7 +132,12 @@ export interface SewagePipeReviewFacilityItem {
}
export type SewagePipeFacilityApiResponse = ApiResponse<SewagePipeReviewFacilityItem[]> & {
export interface SewagePipeFacilityResponseData {
facilityStatusList: SewagePipeReviewFacilityItem[];
statsStatusInfo: StatsStatusInfo;
}
export type SewagePipeFacilityApiResponse = ApiResponse<SewagePipeFacilityResponseData> & {
message: string;
};
@@ -165,10 +171,11 @@ export interface SewerPipeTypeItem {
export interface SewerPipeTypeResponseData {
pipeTypeStatusList: SewerPipeTypeItem[];
pipeTypeList: SewerPipeTypeItem[];
statsStatusInfo: StatsStatusInfo;
}
export type SewerPipeTypeApiResponse = ApiResponse<SewerPipeTypeItem[]>;
export type SewerPipeTypeApiResponse = ApiResponse<SewerPipeTypeResponseData>;
export interface SewagePipeTypeService {
@@ -225,9 +232,23 @@ export interface SewerPipeDiamItem {
}
export type SewerPipeDiamApiResponse = ApiResponse<SewerPipeDiamItem[]>;
export interface SewerPipeDiamResponseData {
pipeDiamList: SewerPipeDiamItem[];
statsStatusInfo: StatsStatusInfo;
}
export type SewerPipeDiamApiResponse = ApiResponse<SewerPipeDiamResponseData>;
export interface SewagePipeDiamService {
getSewagePipeDiamList(params: SewagePipeReviewRequestParams): Promise<SewerPipeDiamApiResponse>;
}
export interface StatsStatusInfo {
statsCd?: string;
submitStatusCd?: string;
prvnStatusCd?: string;
rjctOpnnCn?: string;
splmntOpnnCn?: string;
wrterOpnnCn?: string;
}
@@ -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[];
}
@@ -34,7 +34,7 @@ export const hbArticleColumnInfo: ColumnInfo = {
{
name: "ipcrCntVl",
fieldName: "ipcrCntVl",
header: { text: "μ" },
header: { text: "중요도 계수(μ)" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
@@ -43,7 +43,7 @@ export const hbArticleColumnInfo: ColumnInfo = {
{
name: "pctlCrtrVl",
fieldName: "pctlCrtrVl",
header: { text: "Percentile" },
header: { text: "백분위수 기준값(pct)" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
@@ -52,7 +52,7 @@ export const hbArticleColumnInfo: ColumnInfo = {
{
name: "scopCntCntVl",
fieldName: "scopCntCntVl",
header: { text: "A" },
header: { text: "스케일 조정 계수(A)" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
@@ -61,7 +61,7 @@ export const hbArticleColumnInfo: ColumnInfo = {
{
name: "thrsCntVl",
fieldName: "thrsCntVl",
header: { text: "C" },
header: { text: "임계 계수(c)" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,