[st] feat: 입력 저장/제출 로직 수정, 통계입력관리, 검토 반려/제출

This commit is contained in:
2026-05-13 13:18:24 +09:00
parent 0fd2466f67
commit f94b18a64d
26 changed files with 1234 additions and 241 deletions
+39
View File
@@ -36,6 +36,38 @@
.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-stats-status-pending {
@apply !text-gray-500;
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-submitted {
@apply !font-semibold
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-approved {
@apply !font-semibold
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected {
@apply !font-semibold !text-[#ff6c64];
white-space: nowrap !important;
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected > * {
display: inline-flex !important;
align-items: center !important;
justify-content: center;
width: max-content;
margin: 0 auto;
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected > *::after {
content: "";
display: inline-block;
margin-left: 0.25rem;
vertical-align: middle;
width: 0.6125rem;
height: 0.6125rem;
border-radius: 9999px;
background: #ff6c64;
box-shadow: 0 0 0 0 rgba(255, 108, 100, 0.75);
animation: rg-cell-stats-status-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total {
@apply !bg-[#e3eaf2];
}
@@ -141,6 +173,13 @@
}
}
@keyframes rg-cell-stats-status-dot-ping {
75%,
100% {
box-shadow: 0 0 0 0.5rem rgba(255, 108, 100, 0);
}
}
.tabs-content {
margin-top: 0.6rem !important;
}
+29 -13
View File
@@ -918,9 +918,17 @@ const DataGrid = ({
rowIndicator: { visible: false },
});
// footer가 있는 컬럼이 있으면 footer 높이 설정
const hasFooter = columns.some((col) => col.footer);
if (hasFooter) {
const applyFooterSettings = (
targetGridView: typeof gridView,
) => {
const hasFooter = columns.some((col) => col.footer);
if (!hasFooter) return;
targetGridView.setOptions({
summaryMode: "aggregate",
footer: { visible: true },
});
// footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행)
const hasArrayFooter = columns.some(
(col) => col.footer && Array.isArray(col.footer),
@@ -935,23 +943,24 @@ const DataGrid = ({
);
});
if (hasArrayFooter || hasMultiRowFooter) {
// footer 배열의 길이를 확인하여 행 수 결정
const arrayFooterLengths = columns
.filter((col) => col.footer && Array.isArray(col.footer))
.map((col) => (col.footer as unknown[]).length);
const maxFooterLength =
arrayFooterLengths.length > 0
? Math.max(...arrayFooterLengths)
: 2; // 기본값 2
: 2;
const footerHeights = Array.from(
{ length: maxFooterLength },
() => ({ height: 40 }),
);
gridView.setFooters?.(footerHeights);
targetGridView.setFooters?.(footerHeights);
} else {
gridView.footer.height = 40;
targetGridView.footer.height = 40;
}
}
};
applyFooterSettings(gridView);
// 고정 컬럼 설정
if (fixedColumns !== undefined && fixedColumns > 0) {
@@ -960,10 +969,6 @@ const DataGrid = ({
});
}
gridView.setOptions({
summaryMode: "aggregate",
});
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
const checkFooterUserSpans = (layout: unknown[]): boolean => {
return layout.some((item) => {
@@ -986,7 +991,9 @@ const DataGrid = ({
});
};
if (checkFooterUserSpans(columnLayout)) {
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
if (hasFooterUserSpans) {
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
if (gridRef.current?.gridView && columnLayout) {
@@ -1049,6 +1056,10 @@ const DataGrid = ({
(currentGridView as GridViewWithSetColumns).setColumns?.(
columnsWithFooter,
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
setTimeout(() => {
@@ -1089,6 +1100,11 @@ const DataGrid = ({
);
}
});
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
currentGridView.refresh?.();
}, 50);
} catch (e) {
console.warn("Failed to set columns with footer array:", e);
@@ -109,6 +109,8 @@ export interface VerificationResultDialogProps {
queryContext?: VerificationResultDialogQueryContext | null;
title?: string;
emptyMessage?: string;
/** 예외 제출 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */
onExceptionSubmitted?: () => void | Promise<void>;
}
export function VerificationResultDialog({
@@ -117,6 +119,7 @@ export function VerificationResultDialog({
queryContext,
title = "검증 결과",
emptyMessage = "표시할 검증 결과가 없습니다.",
onExceptionSubmitted,
}: VerificationResultDialogProps) {
/** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */
const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => {
@@ -385,6 +388,11 @@ export function VerificationResultDialog({
});
if (response.status === "success") {
alert("예외 제출 처리되었습니다.");
try {
await onExceptionSubmitted?.();
} catch {
alert("예외 제출은 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.");
}
onOpenChange(false);
} else {
alert(response.message ?? "예외 제출 처리 중 오류가 발생했습니다.");
@@ -396,9 +404,10 @@ export function VerificationResultDialog({
alert,
imrCheckedIndices,
imrRows,
loadInitial,
normalCheckedIndices,
normalRows,
onExceptionSubmitted,
onOpenChange,
stableQueryContext,
]);
@@ -24,12 +24,12 @@ import { VerificationResultHistoryPagination } from "@/components/module/Verific
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
type VerificationResultGridRow = Record<string, string | number>;
type VerificationHistoryTabKey = "NV" | "IMR" | "HR";
type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" },
{ value: "HR", label: "HR 검증내역" },
{ value: "HB", label: "HB 검증내역" },
];
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
@@ -90,32 +90,32 @@ export function VerificationResultHistoryDialog({
const [activeTab, setActiveTab] = useState<VerificationHistoryTabKey>("NV");
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
const [hrEntries, setHrEntries] = useState<VerificationResultReportItem[]>([]);
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
const [normalPageIndex, setNormalPageIndex] = useState(0);
const [imrPageIndex, setImrPageIndex] = useState(0);
const [hrPageIndex, setHrPageIndex] = useState(0);
const [hbPageIndex, setHbPageIndex] = useState(0);
const [normalPageCount, setNormalPageCount] = useState(1);
const [imrPageCount, setImrPageCount] = useState(1);
const [hrPageCount, setHrPageCount] = useState(1);
const [hbPageCount, setHbPageCount] = useState(1);
const [normalLoading, setNormalLoading] = useState(false);
const [imrLoading, setImrLoading] = useState(false);
const [hrLoading, setHrLoading] = useState(false);
const [hbLoading, setHbLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const resetState = useCallback(() => {
setActiveTab("NV");
setNormalEntries([]);
setImrEntries([]);
setHrEntries([]);
setHbEntries([]);
setNormalPageIndex(0);
setImrPageIndex(0);
setHrPageIndex(0);
setHbPageIndex(0);
setNormalPageCount(1);
setImrPageCount(1);
setHrPageCount(1);
setHbPageCount(1);
setNormalLoading(false);
setImrLoading(false);
setHrLoading(false);
setHbLoading(false);
setErrorMessage(null);
}, []);
@@ -147,34 +147,34 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setNormalLoading(true);
setImrLoading(true);
setHrLoading(true);
setHbLoading(true);
setNormalPageIndex(0);
setImrPageIndex(0);
setHrPageIndex(0);
setHbPageIndex(0);
try {
const [nvRes, imrRes, hrRes] = await Promise.all([
const [nvRes, imrRes, hbRes] = await Promise.all([
fetchVerificationResultPage("NV", 0),
fetchVerificationResultPage("IMR", 0),
fetchVerificationResultPage("HR", 0),
fetchVerificationResultPage("HB", 0),
]);
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
setImrEntries(imrRes?.data?.verifyResultList ?? []);
setHrEntries(hrRes?.data?.verifyResultList ?? []);
setHbEntries(hbRes?.data?.verifyResultList ?? []);
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
setHrPageCount(hrRes?.data?.pageInfo?.totalPages ?? 1);
setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
} catch {
setNormalEntries([]);
setImrEntries([]);
setHrEntries([]);
setHbEntries([]);
setNormalPageCount(1);
setImrPageCount(1);
setHrPageCount(1);
setHbPageCount(1);
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setNormalLoading(false);
setImrLoading(false);
setHrLoading(false);
setHbLoading(false);
}
}, [fetchVerificationResultPage, queryContext]);
@@ -216,23 +216,23 @@ export function VerificationResultHistoryDialog({
[fetchVerificationResultPage, imrPageIndex, queryContext],
);
const handleHrPageChange = useCallback(
const handleHbPageChange = useCallback(
async (pageIndex: number) => {
if (pageIndex === hrPageIndex || !queryContext) return;
if (pageIndex === hbPageIndex || !queryContext) return;
setErrorMessage(null);
setHrLoading(true);
setHbLoading(true);
try {
const response = await fetchVerificationResultPage("HR", pageIndex);
setHrEntries(response?.data?.verifyResultList ?? []);
setHrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHrPageIndex(pageIndex);
const response = await fetchVerificationResultPage("HB", pageIndex);
setHbEntries(response?.data?.verifyResultList ?? []);
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHbPageIndex(pageIndex);
} catch {
setErrorMessage("HR 검증내역 조회 중 오류가 발생했습니다.");
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
} finally {
setHrLoading(false);
setHbLoading(false);
}
},
[fetchVerificationResultPage, hrPageIndex, queryContext],
[fetchVerificationResultPage, hbPageIndex, queryContext],
);
useEffect(() => {
@@ -260,7 +260,7 @@ export function VerificationResultHistoryDialog({
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
const hrRows = useMemo(() => mapRows(hrEntries), [hrEntries]);
const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
SetStateAction<Record<string, string | number>[]>
@@ -332,8 +332,8 @@ export function VerificationResultHistoryDialog({
<TabsContent value="IMR" className="rounded-md border border-border">
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
</TabsContent>
<TabsContent value="HR" className="rounded-md border border-border">
{renderGrid(hrRows, hrPageIndex, hrPageCount, handleHrPageChange, hrLoading)}
<TabsContent value="HB" className="rounded-md border border-border">
{renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)}
</TabsContent>
</Tabs>
</div>
@@ -18,6 +18,7 @@ import {
type CheckInputPeriodItem,
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 } from '@/entities/statistics/model/statistics-interface';
@@ -46,6 +47,10 @@ import { PublicSewageFactInpColumnList } from '@/components/util/column/input/Pu
import { toGridRows } from '@/components/util/grid/gridFunction';
import { useAdminStore } from '@/store/admin-store';
type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult;
};
type TabToolbarSnapshot = {
showSaveInToolbar: boolean;
showEnterableToolbarButtons: boolean;
@@ -1105,6 +1110,32 @@ const PublicSewageFacilityInp = () => {
}
showHeaderCheckbox={false}
enableSort={false}
cellStyleCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
const hasImr = errors.some((e) => e.vrfcCd === "IMR");
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
}}
cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return;
const errors = verification[fieldName];
if (Array.isArray(errors) && errors.length > 0) {
return errors
.map((e) => {
const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : "일반검증 - ";
const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln}(검증 기준값: ${comp} / 현재 입력값: ${now})`;
})
.filter(Boolean)
.join("\n");
}
}}
/>
</TabsContent>
);
@@ -1128,6 +1159,11 @@ const PublicSewageFacilityInp = () => {
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
queryContext={verificationQueryContext}
onExceptionSubmitted={async () => {
const ctx = lastSearchedValues ?? searchValues;
if (!ctx || !searchParamRef.current) return;
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
}}
/>
<MainFacilityMappingSaveDialog
key={`main-facility-mapping-save-${mainFacilityMappingSaveDialogKey}`}
@@ -910,14 +910,14 @@ const SewagePipeInp = () => {
return;
}
if (!isException) {
const currentData = getTabGridRowsForVerification(tabKey);
const rowsWithVerificationErrors = currentData.filter((d) =>
hasRegionalFactVerificationErrors((d as GridRowWithVerification).verificationResult),
);
if (rowsWithVerificationErrors.length > 0) {
alert("데이터에 오류가 있습니다.\n수정 후 제출해주세요.");
return;
}
// const currentData = getTabGridRowsForVerification(tabKey);
// const rowsWithVerificationErrors = currentData.filter((d) =>
// hasRegionalFactVerificationErrors((d as GridRowWithVerification).verificationResult),
// );
// if (rowsWithVerificationErrors.length > 0) {
// alert("데이터에 오류가 있습니다.\n수정 후 제출해주세요.");
// return;
// }
if (await confirm("제출 하시겠습니까?")) {
setIsExceptionSubmitFlow(false);
setShowDailog(true);
@@ -2052,6 +2052,12 @@ const SewagePipeInp = () => {
}
: null
}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
const refreshedParam = buildSewagePipeSearchParam(values);
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
}}
/>
</>
);
@@ -104,6 +104,14 @@ function getSewerageSupplyRateParams(
};
}
function normalizeGridString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
return value.trim() === "" ? null : value;
}
// 하수도 통계 관리 > 01. 하수도 보급률
const SeweragePenetrationRateInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
@@ -378,15 +386,15 @@ const SeweragePenetrationRateInp = () => {
const submitFunc = async (isException: boolean) => {
if (!isException) {
const isError = rowData.filter((d) =>
hasVerificationErrors((d as GridRowWithVerification).verificationResult)
);
// const isError = rowData.filter((d) =>
// hasVerificationErrors((d as GridRowWithVerification).verificationResult)
// );
if (isError.length > 0) {
alert(`데이터에 오류가 있습니다.
수정 후 제출해주세요.`);
return;
}
// if (isError.length > 0) {
// alert(`데이터에 오류가 있습니다.
// 수정 후 제출해주세요.`);
// return;
// }
if (await confirm('제출 하시겠습니까?')) {
setIsExceptionSubmitFlow(false);
setShowDailog(true);
@@ -438,11 +446,12 @@ const SeweragePenetrationRateInp = () => {
};
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
sggNm: (d.sggNm as string) || null,
admdstCd: d.admdstCd as string,
admdstNm: d.admdstNm as string,
trbtrNm: d.trbtrNm as string,
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null),
sggNm: normalizeGridString(d.sggNm),
admdstCd: normalizeGridString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm),
trbtrNm: normalizeGridString(d.trbtrNm),
spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음' ? null : normalizeGridString(d.spcCtpnRgnCn),
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
@@ -455,7 +464,7 @@ const SeweragePenetrationRateInp = () => {
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
rmrk: (d.rmrk as string) ?? '',
rmrk: normalizeGridString(d.rmrk),
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
wholArea: formatNumber(d.wholArea, 'wholArea'),
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
@@ -584,11 +593,14 @@ const SeweragePenetrationRateInp = () => {
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
// 행정구역
sggNm: (d.sggNm as string) || null, // 시군구
admdstCd: d.admdstCd as string, // 행정코드
admdstNm: d.admdstNm as string, // 행정구역명
trbtrNm: d.trbtrNm as string, // 지류
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), // 특별대책지역
sggNm: normalizeGridString(d.sggNm), // 시군구
admdstCd: normalizeGridString(d.admdstCd), // 행정코드
admdstNm: normalizeGridString(d.admdstNm), // 행정구역명
trbtrNm: normalizeGridString(d.trbtrNm), // 지류
spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음'
? null
: normalizeGridString(d.spcCtpnRgnCn), // 특별대책지역
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
@@ -604,7 +616,7 @@ const SeweragePenetrationRateInp = () => {
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
rmrk: (d.rmrk as string) ?? '', // 비고
rmrk: normalizeGridString(d.rmrk), // 비고
// 합계·소계(계산필드 저장용)
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
@@ -819,6 +831,11 @@ const SeweragePenetrationRateInp = () => {
}
: null
}
onExceptionSubmitted={async () => {
const values = lastSearchedValues ?? searchValues;
if (!values) return;
await handleSearch(values);
}}
/>
</>
);
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { RealGridReact } from "realgrid-react";
import useSWR, { useSWRConfig } from "swr";
import { useAlertStore } from "@/shared/lib";
@@ -13,6 +14,7 @@ import RejectButton from "@/components/button/RejectButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useTabStore } from "@/store/tab-store";
@@ -32,6 +34,10 @@ const StatsInputStatusInp = () => {
);
const [searchParams, setSearchParams] =
useState<StatsInputStatusListRequestParams | null>(null);
const [lastSearchedValues, setLastSearchedValues] =
useState<SearchFormValues | null>(null);
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
const gridRef = useRef<RealGridReact | null>(null);
const swrKey = searchParams
? (["stats-input-status", searchParams] as const)
@@ -45,12 +51,16 @@ const StatsInputStatusInp = () => {
onSuccess: (data) => {
if (data.status === "success" && data.data) {
setRowData(toGridRows(data.data));
setHasCompletedSearch(true);
} else {
setHasCompletedSearch(false);
setRowData([]);
alert(data.message ?? "조회에 실패했습니다.");
}
},
onError: () => {
alert("데이터 조회 중 오류가 발생했습니다.");
setHasCompletedSearch(false);
setRowData([]);
},
},
@@ -73,6 +83,8 @@ const StatsInputStatusInp = () => {
sggNm: values.gugun === "ALL" ? "" : values.gugun,
statsYr: values.year ?? "",
};
setHasCompletedSearch(false);
setLastSearchedValues(values);
setSearchParams(params);
mutate(["stats-input-status", params]);
};
@@ -80,6 +92,8 @@ const StatsInputStatusInp = () => {
const handleReset = () => {
setSearchParams(null);
setRowData([]);
setLastSearchedValues(null);
setHasCompletedSearch(false);
};
const rejectFunc = () => {
@@ -87,19 +101,44 @@ const StatsInputStatusInp = () => {
};
const approveFunc = () => {
const checkedRows =
gridRef.current?.gridView?.getCheckedRows?.(true, false, true) ?? [];
if (rowData.length === 0 || checkedRows.length !== rowData.length) {
alert('모든 통계 항목을 체크해주세요.');
return;
}
const hasInvalidStatus = rowData.some((row) =>
["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
);
if (hasInvalidStatus) {
alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
return;
}
// 승인 요청 로직
};
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
: true;
const showActionToolbarCard =
hasCompletedSearch && rowData.length > 0 && gugunOkOrNotDoList;
return (
<>
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<RejectButton rejectFunc={rejectFunc} />
<ApproveButton approveFunc={approveFunc} />
{showActionToolbarCard && (
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<RejectButton rejectFunc={rejectFunc} />
<ApproveButton approveFunc={approveFunc} />
</div>
</div>
</div>
)}
{isLoading && (
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
<div className="relative size-[5rem]">
@@ -115,6 +154,27 @@ const StatsInputStatusInp = () => {
setRowData={setRowData}
isExcelImport={false}
showHeaderCheckbox={true}
getGridRef={gridRef}
cellStyleCallback={({ row, fieldName }) => {
if (fieldName !== "laeSbmsnSttsNm") {
return undefined;
}
const statusCd = String(row.laeSbmsnSttsCd ?? "");
if (statusCd === "0" || statusCd === "1") {
return "rg-cell-stats-status-pending";
}
if (statusCd === "2") {
return "rg-cell-stats-status-submitted";
}
if (statusCd === "3") {
return "rg-cell-stats-status-approved";
}
if (statusCd === "4") {
return "rg-cell-stats-status-rejected";
}
return undefined;
}}
/>
</div>
</>
@@ -33,7 +33,7 @@ function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC"
return "INPT";
}
function parseYmdToDate(ymd: unknown): Date | undefined {
function parseYmdToDate(ymd: string): Date | undefined {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return undefined;
const y = Number(s.slice(0, 4));
@@ -47,6 +47,12 @@ function parseYmdToDate(ymd: unknown): Date | undefined {
return dt;
}
function formatYmdForDisplay(ymd: string): string {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return String(ymd ?? "");
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
}
const StatisticsInputManagement = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [isLoading, setIsLoading] = useState(false);
@@ -71,16 +77,17 @@ const StatisticsInputManagement = () => {
setIsLoading(true);
const params: StatsInputPeriodListRequest = {
statsYr: values.year,
seType:
values.clsfCd && values.clsfCd !== "ALL"
? values.clsfCd
: "",
...(values.clsfCd && values.clsfCd !== "ALL"
? { 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),
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
}));
setRowData(toGridRows(rowsForGrid));
} else {
@@ -112,27 +119,47 @@ const StatisticsInputManagement = () => {
setPeriodDialogOpen(true);
};
const handleRowClick = (row: Record<string, string | number>) => {
const inptYmdSnRaw = row.inptYmdSn;
const inptYmdSnNum =
typeof inptYmdSnRaw === "number"
? inptYmdSnRaw
: parseInt(String(inptYmdSnRaw ?? ""), 10);
const handleRowClick = async (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),
inptYmdSn: Number.isFinite(inptYmdSnNum) ? inptYmdSnNum : 0,
seCd: Number.isFinite(seCdNum) ? seCdNum : 0,
inptBgngYmd: parseYmdToDate(row.inptBgngYmd),
inptEndYmd: parseYmdToDate(row.inptEndYmd),
});
setPeriodDialogOpen(true);
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
const statsYr = String(row.statsYr ?? "");
const seType = rowSeTypeToCode(row);
try {
setIsLoading(true);
const res = await statisticsApi.getStatsInputPeriodDetail({
statsYr,
seType,
seCd,
});
if (res.status !== "success" || !res.data) {
alert(res.message || "상세 정보를 조회하지 못했습니다.");
return;
}
const detail = res.data;
setPeriodDialogMode("edit");
setPeriodDialogInitial({
statsYr: String(detail.statsYr ?? statsYr),
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
seCd:
typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
? detail.seCd
: seCd,
inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
inptEndYmd: parseYmdToDate(detail.inptEndYmd),
});
setPeriodDialogOpen(true);
} catch {
alert("상세 정보 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const columns: ColumnConfig[] = [
@@ -166,13 +193,6 @@ const StatisticsInputManagement = () => {
header: { text: "종료일" },
editable: false,
},
{
name: "inptYmdSn",
fieldName: "inptYmdSn",
header: { text: "inptYmdSn" },
visible: false,
editable: false,
},
];
const columnLayout = [
@@ -33,7 +33,6 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap
export type StatisticsInputPeriodDialogValues = {
statsYr: string;
seType: "INPT" | "VRFC";
inptYmdSn: number;
seCd: number;
inptBgngYmd: Date | undefined;
inptEndYmd: Date | undefined;
@@ -138,22 +137,17 @@ export function StatisticsInputPeriodDialog({
try {
setIsSubmitting(true);
const userInfo = await getUserInfo();
const inptYmdSn =
mode === "create"
? 0
: (initial && Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0);
const param: StatsInputPeriodSaveRequest = {
statsYr: yr,
seType,
inptYmdSn,
seCd: seCdParsed,
inptBgngYmd: bgng,
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
const res = isCreate
? await statisticsApi.saveStatsInputPeriod(param)
: await statisticsApi.updateStatsInputPeriod(param);
if (!isCreate) {
param.seCd = seCdParsed;
}
const res = await statisticsApi.saveStatsInputPeriod(param);
if (res.status === "success") {
alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`);
onOpenChange(false);
@@ -192,8 +186,6 @@ export function StatisticsInputPeriodDialog({
const param: StatsInputPeriodDeleteRequest = {
statsYr: yr,
seType: initial.seType,
inptYmdSn:
Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0,
seCd,
inptBgngYmd: bgng,
inptEndYmd: end,
@@ -253,7 +245,7 @@ export function StatisticsInputPeriodDialog({
<Select
value={seType ?? undefined}
onValueChange={(v) => setSeType(v as "INPT" | "VRFC")}
disabled={isSubmitting}
disabled={isSubmitting || mode === "edit"}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="선택" />
@@ -91,7 +91,7 @@ const PublicSewageFacilityRvw = () => {
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
fctName: values.fctName,
swtrmtFcltNm: values.fctName,
});
if (response.status === "success" && response.data) {
@@ -99,12 +99,12 @@ export function ReturnReasonDialog({
};
const response = await statisticsApi.saveReviewReturn(param);
if (response.status !== "success") {
if (response.resultCode.toLowerCase() !== "success") {
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
return;
}
alert(response.message || "반려 처리 되었습니다.");
alert("반려 처리 되었습니다.");
await onSave?.();
resetForm();
onOpenChange(false);
@@ -0,0 +1,196 @@
"use client";
import { useEffect, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Label,
LoadingWithAnimation,
Textarea,
} from "@common/components";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewerInfoResponse,
ReviewSubmitRequest,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore } from "@/shared/lib";
export interface ReviewSubmitDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
context: ReviewerInfoRequest | null;
onSubmitted?: () => void | Promise<void>;
}
export function ReviewSubmitDialog({
open,
onOpenChange,
context,
onSubmitted,
}: ReviewSubmitDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state);
const [reviewerInfo, setReviewerInfo] =
useState<ReviewerInfoResponse["data"] | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!open) {
setReviewerInfo(null);
setIsLoading(false);
return;
}
if (context == null) {
setReviewerInfo(null);
return;
}
let cancelled = false;
const loadReviewerInfo = async () => {
try {
setIsLoading(true);
const response = await statisticsApi.getReviewerInfo(context);
if (cancelled) return;
if (response.status !== "success" || !response.data) {
alert("제출 정보를 불러오지 못했습니다.");
setReviewerInfo(null);
return;
}
setReviewerInfo(response.data);
} catch {
if (!cancelled) {
alert("제출 정보를 불러오지 못했습니다.");
setReviewerInfo(null);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
};
void loadReviewerInfo();
return () => {
cancelled = true;
};
}, [open, context, alert]);
const handleConfirm = async () => {
if (context == null || reviewerInfo == null) {
alert("제출 정보를 확인한 뒤 다시 시도해주세요.");
return;
}
try {
setIsSubmitting(true);
const userInfo = await getUserInfo();
const submitParam: ReviewSubmitRequest = {
statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo.wrterOpnnCn ?? "",
};
const response = await statisticsApi.submitReview(submitParam);
if (response.status !== "success") {
alert(response.message || "제출에 실패했습니다.");
return;
}
alert(response.message || "제출되었습니다.");
onOpenChange(false);
await onSubmitted?.();
} catch {
alert("제출 중 오류가 발생했습니다.");
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[64rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>제출 정보</DialogTitle>
</DialogHeader>
{(isLoading || isSubmitting) && (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">검토자명</Label>
<Input value={reviewerInfo?.userNm ?? "-"} readOnly />
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">연락처</Label>
<Input value={reviewerInfo?.telOffrm ?? "-"} readOnly />
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">부서</Label>
<Input value={reviewerInfo?.dpmNm ?? "-"} readOnly />
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">직책</Label>
<Input value={reviewerInfo?.cps ?? "-"} readOnly />
</div>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">검토의견</Label>
<Textarea
value={reviewerInfo?.splmntOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
<div className="col-span-2 flex flex-col gap-2">
<Label className="text-label">처리내역</Label>
<Textarea
value={reviewerInfo?.wrterOpnnCn ?? "-"}
readOnly
className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
color="primary"
onClick={() => void handleConfirm()}
disabled={isLoading || isSubmitting || reviewerInfo == null}
>
제출
</Button>
<Button
type="button"
color="gray"
onClick={() => onOpenChange(false)}
disabled={isLoading || isSubmitting}
>
취소
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,15 +1,16 @@
import { useMemo, useRef, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewagePipeReviewRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
@@ -18,17 +19,57 @@ import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type {
ColumnConfig,
ColumnInfo,
ColumnInfoList,
ColumnLayoutItem,
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001";
function cloneColumnLayoutItem(item: ColumnLayoutItem): ColumnLayoutItem {
if (typeof item === "string") return item;
if ("column" in item) {
return {
column: item.column,
...(item.footerUserSpans && {
footerUserSpans: item.footerUserSpans.map((span) => ({ ...span })),
}),
};
}
return {
...item,
header: { ...item.header },
items: item.items.map((child) =>
typeof child === "string" ? child : cloneColumnLayoutItem(child),
),
...(item.footerUserSpans && {
footerUserSpans: item.footerUserSpans.map((span) => ({ ...span })),
}),
};
}
function cloneColumnConfig(col: ColumnConfig): ColumnConfig {
return {
...col,
header: { ...col.header },
...(col.footer && {
footer: Array.isArray(col.footer)
? col.footer.map((item) => ({ ...item }))
: { ...col.footer },
}),
};
}
async function fetchSewagePipeReviewTab(
tabKey: string,
searchParam: SewagePipeReviewRequestParams,
@@ -45,6 +86,9 @@ async function fetchSewagePipeReviewTab(
if (tabKey === "ST00020004") {
return statisticsManagementApi.getSewagePipeDiamList(searchParam);
}
if (tabKey === "ST00020005") {
return await statisticsManagementApi.getSewagePipeManholeList(searchParam);
}
return null;
}
@@ -65,6 +109,8 @@ function extractReviewRowsFromResponseData(
return Array.isArray(d.pipeTypeList) ? (d.pipeTypeList as object[]) : [];
case "ST00020004":
return Array.isArray(d.pipeDiamList) ? (d.pipeDiamList as object[]) : [];
case "ST00020005":
return Array.isArray(d.manholeList) ? (d.manholeList as object[]) : [];
default:
return [];
}
@@ -79,12 +125,13 @@ function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
// 하수도 통계 검토 02.하수관로
const SewagePipeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const userInfo = useUserStore((state) => state.userInfo);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
/** 탭별 조회 응답의 statsStatusInfo — 활성 탭에 맞춰 버튼 노출 (SewagePipeInp의 tabStatusInfoMap과 동일 패턴) */
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
@@ -98,6 +145,17 @@ const SewagePipeRvw = () => {
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
[tabStatsStatusInfoMap, activeTab],
);
const columnInfo = useMemo<ColumnInfoList[]>(
() =>
SewagePipeColumnInfoListRvw.map((tab) => ({
...tab,
columns: (tab.columns ?? []).map((col) => cloneColumnConfig(col)),
columnLayout: (tab.columnLayout ?? []).map((item) =>
cloneColumnLayoutItem(item),
),
})),
[],
);
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
if (lastSearchValues == null) return null;
return {
@@ -108,6 +166,25 @@ const SewagePipeRvw = () => {
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchValues == null) return null;
const statsYr = lastSearchValues.year ?? "";
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
const statsCd = activeStatsStatusInfo.statsCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
statsCd,
instSeCd,
};
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = activeStatsStatusInfo;
@@ -135,6 +212,7 @@ const SewagePipeRvw = () => {
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
@@ -235,37 +313,13 @@ const SewagePipeRvw = () => {
}
};
const submitFunc = async () => {
if (reviewOpinionContext == null) {
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
if (!(await confirm("제출 하시겠습니까?"))) {
return;
}
try {
const userInfo = await getUserInfo();
const response = await statisticsApi.submitReview({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
mdfrId: userInfo.userUnqId,
});
if (response.status !== "success") {
alert(response.message || "제출에 실패했습니다.");
return;
}
alert(response.message || "제출되었습니다.");
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
} catch {
alert("제출 중 오류가 발생했습니다.");
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
@@ -344,7 +398,7 @@ const SewagePipeRvw = () => {
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={submitFunc} />
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
@@ -365,13 +419,13 @@ const SewagePipeRvw = () => {
variant="default"
>
<TabsList>
{SewagePipeColumnInfoListRvw.map((info) => (
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{SewagePipeColumnInfoListRvw.map((info) => {
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
@@ -419,6 +473,16 @@ const SewagePipeRvw = () => {
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
@@ -6,13 +6,15 @@ import ReturnButton from "@/components/button/ReturnButton";
import SubmitButton from "@/components/button/SumitButton";
import { LoadingWithAnimation } from "@common/components";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import type {
ReviewOpinionSaveRequest,
ReviewerInfoRequest,
} from "@/entities/statistics/model/statistics-interface";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
@@ -25,17 +27,18 @@ import {
// 하수도 통계 검토 01.하수도 보급률
const SeweragePenetrationRateRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [statsStatusInfo, setStatsStatusInfo] = useState<statsStatusInfo>({});
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewOpinionContext, setReviewOpinionContext] =
useState<ReviewOpinionDialogContext | null>(null);
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
const [latestProcessHistory, setLatestProcessHistory] = useState("");
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const { confirm } = useConfirm();
const columnInfo = useMemo(() => {
const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList;
@@ -66,6 +69,24 @@ const SeweragePenetrationRateRvw = () => {
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (reviewOpinionContext == null) {
return null;
}
const instSeCd = userInfo?.instSeCd ?? "";
if (!instSeCd) {
return null;
}
return {
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
instSeCd,
};
}, [reviewOpinionContext, userInfo?.instSeCd]);
const handleReset = () => {
setRowData([]);
setStatsStatusInfo({});
@@ -129,37 +150,13 @@ const SeweragePenetrationRateRvw = () => {
}
};
const submitFunc = async () => {
if (reviewOpinionContext == null) {
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
if (!(await confirm("제출 하시겠습니까?"))) {
return;
}
try {
const userInfo = await getUserInfo();
const response = await statisticsApi.submitReview({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
mdfrId: userInfo.userUnqId,
});
if (response.status !== "success") {
alert(response.message || "제출에 실패했습니다.");
return;
}
alert(response.message || "제출되었습니다.");
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
} catch {
alert("제출 중 오류가 발생했습니다.");
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
@@ -258,6 +255,16 @@ const SeweragePenetrationRateRvw = () => {
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
@@ -4,6 +4,7 @@ import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/re
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import ApproveButton from "@/components/button/ApproveButton";
import { doList } from "@/components/util/column/input/DoList";
import {
Button,
Dialog,
@@ -39,6 +40,8 @@ const StatsInputStatusRvw = () => {
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchedYear, setSearchedYear] = useState<string>("");
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
const [historySearchInfo, setHistorySearchInfo] = useState<{
ctpvNm: string;
@@ -85,6 +88,8 @@ const StatsInputStatusRvw = () => {
const handleReset = () => {
setRowData([]);
setSearchedYear("");
setLastSearchedValues(null);
setHasCompletedSearch(false);
};
const handleSearch = async (values: SearchFormValues) => {
@@ -96,6 +101,8 @@ const StatsInputStatusRvw = () => {
try {
setIsLoading(true);
setSearchedYear(values.year ?? "");
setLastSearchedValues(values);
setHasCompletedSearch(false);
const response = await statisticsManagementApi.getStatsInputStatusList({
ctpvNm: values.sido,
@@ -115,11 +122,15 @@ const StatsInputStatusRvw = () => {
setRowData(toGridRows(convertedData));
} else {
setRowData([]);
setHasCompletedSearch(false);
alert(response.message || "조회된 데이터가 없습니다.");
return;
}
setHasCompletedSearch(true);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
setRowData([]);
setHasCompletedSearch(false);
} finally {
setIsLoading(false);
}
@@ -127,6 +138,12 @@ const StatsInputStatusRvw = () => {
const approveFunc = () => console.log('approveFunc');
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
: true;
const showActionToolbarCard =
hasCompletedSearch && rowData.length > 0 && gugunOkOrNotDoList;
const handleRowClick = async (row: Record<string, string | number>) => {
setHistorySearchInfo({
ctpvNm: String(row.ctpvNm ?? ""),
@@ -157,11 +174,13 @@ const StatsInputStatusRvw = () => {
<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]">
<ApproveButton approveFunc={approveFunc} />
{showActionToolbarCard && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ApproveButton approveFunc={approveFunc} />
</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">
@@ -180,6 +199,43 @@ const StatsInputStatusRvw = () => {
rowData={rowData}
setRowData={setRowData}
onRowClick={handleRowClick}
cellStyleCallback={({ row, fieldName }) => {
if (fieldName === "submitStatusNm") {
const statusCd = String(row.submitStatusCd ?? "");
if (statusCd === "0" || statusCd === "1") {
return "rg-cell-stats-status-pending";
}
if (statusCd === "2") {
return "rg-cell-stats-status-submitted";
}
if (statusCd === "3") {
return "rg-cell-stats-status-approved";
}
if (statusCd === "4") {
return "rg-cell-stats-status-rejected";
}
return undefined;
} else if (fieldName === "prvnStatusNm") {
const statusCd = String(row.prvnStatusCd ?? "");
if (statusCd === "0" || statusCd === "1") {
return "rg-cell-stats-status-pending";
}
if (statusCd === "2") {
return "rg-cell-stats-status-submitted";
}
if (statusCd === "3") {
return "rg-cell-stats-status-approved";
}
if (statusCd === "4") {
return "rg-cell-stats-status-rejected";
}
return undefined;
} else {
return undefined;
}
}}
/>
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
<DialogContent
@@ -10,8 +10,7 @@ export const StatsInputStatusInpColumn = {
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false, editable: false },
{ name: 'menuTabName', fieldName: 'menuTabName', header: { text: '구분' }, editable: false, width: 220 },
{ name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false },
// { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'laeSbmsnSttsNm', fieldName: 'laeSbmsnSttsNm', header: { text: '진행상황' }, editable: false },
{ name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false },
{ name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false },
{ name: 'laeKbrdrDeptNm', fieldName: 'laeKbrdrDeptNm', header: { text: '부서' }, editable: false },
@@ -20,6 +19,7 @@ export const StatsInputStatusInpColumn = {
{ name: 'laeIdfrDeptNm', fieldName: 'laeIdfrDeptNm', header: { text: '부서' }, editable: false },
{ name: 'laeIdfrJbgdNm', fieldName: 'laeIdfrJbgdNm', header: { text: '직제' }, editable: false },
{ name: 'rjctOpnnCn', fieldName: 'rjctOpnnCn', header: { text: '반려사유' }, editable: false },
{ name: 'laeSbmsnSttsCd', fieldName: 'laeSbmsnSttsCd', header: { text: '진행상황' }, editable: false, visible: false },
],
columnLayout: [
@@ -32,8 +32,7 @@ export const StatsInputStatusInpColumn = {
hideChildHeaders: false,
header: { text: '하수도사업자' },
items: [
'locgStatusNm',
// 'inputRate',
'laeSbmsnSttsNm',
'laePicNm',
'laePicTelNo',
'laeKbrdrDeptNm',
@@ -41,7 +40,8 @@ export const StatsInputStatusInpColumn = {
'laeIdfrNm',
'laeIdfrDeptNm',
'laeIdfrJbgdNm',
'rjctOpnnCn'
'rjctOpnnCn',
'laeSbmsnSttsCd'
],
},
]
@@ -12,7 +12,7 @@ import {
const currentYear = dayjs().year() - 1;
const startYear = 1996;
const totalCount = currentYear - startYear + 1 + 1;
const totalCount = currentYear - startYear + 1;
// 하수도 통계 관리 02. 하수관로
const rawList = [
@@ -398,7 +398,7 @@ const rawList = [
...Array.from({ length: totalCount }, (_, i) => {
const dtoNum = totalCount - i;
const dtoNum = i + 1;
const wholField = `yy${dtoNum}PrlgWhol`;
const lyngField = `yy${dtoNum}LyngPrlg`;
@@ -426,7 +426,8 @@ const rawList = [
{ name: "summaryGroup", header: { text: "총계" }, items: ["pipeByPrstWhol", "lyngPrlg", "dmltnPrlg"] },
...Array.from({ length: totalCount }, (_, i) => {
const dtoNum = totalCount - i;
// const dtoNum = totalCount - i;
const dtoNum = i + 1;
const yearLabel = i === 0 ? "1996년이전" : `${startYear + (i - 1)}년`;
return {
@@ -550,7 +551,311 @@ const rawList = [
'etcPdmtPrlg',
'rmrk'
]
}
},
{
tabKey: "ST00020005",
tabLabel: "맨홀 현황",
rowStartNum: 3,
fixedColumns: 3,
columns: [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
mergeCells: true,
sortdisable: true,
footer: [
{
valueCallback: (grid: GridView) => {
const divisionIndex = 0;
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + "합계" : "합계";
}
},
{
valueCallback: (grid: GridView) => {
const divisionIndex = 1;
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, "displayYear");
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + "합계" : "합계";
}
}
]
},
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, mergeCells: true, sortdisable: true },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, mergeCells: true, sortdisable: true },
{ name: "emphsMngSeNm", fieldName: "emphsMngSeNm", header: { text: "집중강우\n중점관리\n구역구분" }, mergeCells: true },
{
name: 'displayYear',
fieldName: 'displayYear',
header: { text: '구분' },
sortdisable: true,
footer: [
{
valueCallback: (grid: GridView) => {
const divisionIndex = 0;
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, 'displayYear');
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
}
},
{
valueCallback: (grid: GridView) => {
const divisionIndex = 1;
const dataSource = grid.getDataSource();
const uniqueValues = new Set<string>();
for (let i = 0; i < dataSource.getRowCount(); i++) {
const val = dataSource.getValue(i, 'displayYear');
if (val) uniqueValues.add(String(val));
}
const divisionValues = Array.from(uniqueValues);
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
}
}
]
},
{ name: "totalWholMnhlCnt", fieldName: "totalWholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "totalStep1MnhlCnt", fieldName: "totalStep1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "totalStep2MnhlCnt", fieldName: "totalStep2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "totalStep3MnhlCnt", fieldName: "totalStep3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "totalStep4MnhlCnt", fieldName: "totalStep4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "totalStep5MnhlCnt", fieldName: "totalStep5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2015년 이전 (yy14) ──────────────────────────────────
{ name: "yy14WholMnhlCnt", fieldName: "yy14WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy14Step1MnhlCnt", fieldName: "yy14Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy14Step2MnhlCnt", fieldName: "yy14Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy14Step3MnhlCnt", fieldName: "yy14Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy14Step4MnhlCnt", fieldName: "yy14Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy14Step5MnhlCnt", fieldName: "yy14Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2015년 (yy15) ───────────────────────────────────────
{ name: "yy15WholMnhlCnt", fieldName: "yy15WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy15Step1MnhlCnt", fieldName: "yy15Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy15Step2MnhlCnt", fieldName: "yy15Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy15Step3MnhlCnt", fieldName: "yy15Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy15Step4MnhlCnt", fieldName: "yy15Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy15Step5MnhlCnt", fieldName: "yy15Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2016년 (yy16) ───────────────────────────────────────
{ name: "yy16WholMnhlCnt", fieldName: "yy16WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy16Step1MnhlCnt", fieldName: "yy16Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy16Step2MnhlCnt", fieldName: "yy16Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy16Step3MnhlCnt", fieldName: "yy16Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy16Step4MnhlCnt", fieldName: "yy16Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy16Step5MnhlCnt", fieldName: "yy16Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2017년 (yy17) ───────────────────────────────────────
{ name: "yy17WholMnhlCnt", fieldName: "yy17WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy17Step1MnhlCnt", fieldName: "yy17Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy17Step2MnhlCnt", fieldName: "yy17Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy17Step3MnhlCnt", fieldName: "yy17Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy17Step4MnhlCnt", fieldName: "yy17Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy17Step5MnhlCnt", fieldName: "yy17Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2018년 (yy18) ───────────────────────────────────────
{ name: "yy18WholMnhlCnt", fieldName: "yy18WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy18Step1MnhlCnt", fieldName: "yy18Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy18Step2MnhlCnt", fieldName: "yy18Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy18Step3MnhlCnt", fieldName: "yy18Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy18Step4MnhlCnt", fieldName: "yy18Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy18Step5MnhlCnt", fieldName: "yy18Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2019년 (yy19) ───────────────────────────────────────
{ name: "yy19WholMnhlCnt", fieldName: "yy19WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy19Step1MnhlCnt", fieldName: "yy19Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy19Step2MnhlCnt", fieldName: "yy19Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy19Step3MnhlCnt", fieldName: "yy19Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy19Step4MnhlCnt", fieldName: "yy19Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy19Step5MnhlCnt", fieldName: "yy19Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2020년 (yy20) ───────────────────────────────────────
{ name: "yy20WholMnhlCnt", fieldName: "yy20WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy20Step1MnhlCnt", fieldName: "yy20Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy20Step2MnhlCnt", fieldName: "yy20Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy20Step3MnhlCnt", fieldName: "yy20Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy20Step4MnhlCnt", fieldName: "yy20Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy20Step5MnhlCnt", fieldName: "yy20Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2021년 (yy21) ───────────────────────────────────────
{ name: "yy21WholMnhlCnt", fieldName: "yy21WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy21Step1MnhlCnt", fieldName: "yy21Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy21Step2MnhlCnt", fieldName: "yy21Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy21Step3MnhlCnt", fieldName: "yy21Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy21Step4MnhlCnt", fieldName: "yy21Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy21Step5MnhlCnt", fieldName: "yy21Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2022년 (yy22) ───────────────────────────────────────
{ name: "yy22WholMnhlCnt", fieldName: "yy22WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy22Step1MnhlCnt", fieldName: "yy22Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy22Step2MnhlCnt", fieldName: "yy22Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy22Step3MnhlCnt", fieldName: "yy22Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy22Step4MnhlCnt", fieldName: "yy22Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy22Step5MnhlCnt", fieldName: "yy22Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2023년 (yy23) ───────────────────────────────────────
{ name: "yy23WholMnhlCnt", fieldName: "yy23WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy23Step1MnhlCnt", fieldName: "yy23Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy23Step2MnhlCnt", fieldName: "yy23Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy23Step3MnhlCnt", fieldName: "yy23Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy23Step4MnhlCnt", fieldName: "yy23Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy23Step5MnhlCnt", fieldName: "yy23Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2024년 (yy24) ───────────────────────────────────────
{ name: "yy24WholMnhlCnt", fieldName: "yy24WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy24Step1MnhlCnt", fieldName: "yy24Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy24Step2MnhlCnt", fieldName: "yy24Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy24Step3MnhlCnt", fieldName: "yy24Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy24Step4MnhlCnt", fieldName: "yy24Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy24Step5MnhlCnt", fieldName: "yy24Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
// ── 2025년 (yy25) ───────────────────────────────────────
{ name: "yy25WholMnhlCnt", fieldName: "yy25WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step1MnhlCnt", fieldName: "yy25Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step2MnhlCnt", fieldName: "yy25Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step3MnhlCnt", fieldName: "yy25Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step4MnhlCnt", fieldName: "yy25Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "yy25Step5MnhlCnt", fieldName: "yy25Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" }, editable: false },
],
columnLayout: [
{
name: "addrGroup",
header: { text: "행정구역" },
items: [
{ column: "ctpvNm", footerUserSpans: [{ colspan: 5 }, { colspan: 5 }] },
"sggNm",
"admdstNm",
],
},
"emphsMngSeNm",
"displayYear",
{
name: "totGroup",
header: { text: "총계" },
items: [
{ name: "totMtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["totalWholMnhlCnt", "totalStep1MnhlCnt", "totalStep2MnhlCnt", "totalStep3MnhlCnt", "totalStep4MnhlCnt"] },
"totalStep5MnhlCnt",
],
},
{
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 SewagePipeColumnInfoListRvw = rawList.map(tab => ({
@@ -38,6 +38,5 @@ export const StatsInputStatusInfoList = {
columns: StatsInputStatusInfoColumnRaw.columns.map(col => ({
...col,
editable: false,
styleName: "rg-cell-readonly"
}))
};
@@ -17,6 +17,7 @@ import {
type SewagePipeFacilityApiResponse,
type SewerPipeTypeApiResponse,
type SewerPipeDiamApiResponse,
type SewagePipeManholeApiResponse,
} from "../model/statistics-sewage-pipe-review-interface";
import {
type SewerPipeMtcApiResponse,
@@ -135,6 +136,20 @@ export class StatisticsManagementApiService {
return response.data;
}
/**
* 02. 하수관로 맨홀 현황 조회 (Tab 5)
* @param params (sido, gugun, yr, bsnmCd)
*/
public async getSewagePipeManholeList(
params: SewagePipeReviewRequestParams
): Promise<SewagePipeManholeApiResponse> {
const response = await stClient.get<SewagePipeManholeApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/manhole`,
{ params }
);
return response.data;
}
/**
* 03. 하수관로 유지관리 개보수 현황 조회
* @param params (yr, bsnmCd, sido, gugun)
@@ -10,6 +10,8 @@ import {
type ImrRequest,
type ImrResponse,
type ImrStatusResponse,
type ReviewerInfoRequest,
type ReviewerInfoResponse,
type ReviewOpinionSaveRequest,
type ReviewOpinionSaveResponse,
type ReviewReturnSaveRequest,
@@ -25,6 +27,8 @@ import {
type StatisticsService,
type StatsInputPeriodDeleteApiResponse,
type StatsInputPeriodDeleteRequest,
type StatsInputPeriodDetailRequest,
type StatsInputPeriodDetailResponse,
type StatsInputPeriodListApiResponse,
type StatsInputPeriodListRequest,
type StatsInputPeriodSaveApiResponse,
@@ -81,6 +85,14 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
public async getStatsInputPeriodDetail(params: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse> {
const response = await stClient.get<StatsInputPeriodDetailResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/detail`,
{ params },
);
return response.data;
}
/** 통계 입력 기간 저장 — 경로는 백엔드 스펙에 맞게 수정 */
public async saveStatsInputPeriod(
param: StatsInputPeriodSaveRequest,
@@ -92,17 +104,6 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
/** 통계 입력 기간 수정 — 경로는 저장과 동일, 메서드는 PUT */
public async updateStatsInputPeriod(
param: StatsInputPeriodSaveRequest,
): Promise<StatsInputPeriodSaveApiResponse> {
const response = await stClient.put<StatsInputPeriodSaveApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
param,
);
return response.data;
}
/** 통계 입력 기간 삭제 — JSON body. HTTP 메서드·URL은 백엔드 스펙에 맞게 수정 */
public async deleteStatsInputPeriod(
param: StatsInputPeriodDeleteRequest,
@@ -238,6 +239,14 @@ export class StatisticsApiService implements StatisticsService {
);
return response.data;
}
public async getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse> {
const response = await stClient.get<ReviewerInfoResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/reviewer-info`,
{ params: param },
);
return response.data;
}
}
export const statisticsApi = new StatisticsApiService();
@@ -15,8 +15,8 @@ export interface StatsInputStatusList {
statsCd?: string;
sewerBzmnCd?: string;
menuTabName?: string;
locgStatusNm?: string;
inputRate?: number;
laeSbmsnSttsNm?: string;
laeSbmsnSttsCd?: string;
laePicNm?: string;
laePicTelNo?: string;
laeKbrdrDeptNm?: string;
@@ -87,9 +87,9 @@ export interface SewerageSupplyRateRequestParams {
/** 하수도 보급률 저장 API 요청 body (한 행) */
export interface SewerageSupplyRateSaveItem {
sggNm: string | null;
admdstCd: string;
admdstNm: string;
trbtrNm: string;
admdstCd: string | null;
admdstNm: string | null;
trbtrNm: string | null;
spcCtpnRgnCn: string | null;
ts1SwtrmtPpltnCnt: number;
ts2SwtrmtPpltnCnt: number;
@@ -103,7 +103,7 @@ export interface SewerageSupplyRateSaveItem {
drtwPrcsPpltnCnt: number;
spctnkPpltnCnt: number;
staaExcpArea: number;
rmrk: string;
rmrk: string | null;
wholPpltnCnt: number;
wholArea: number;
wholSwtrmtPpltnCnt: number;
@@ -51,10 +51,9 @@ export type StatisticsColumnGuideListResponse = ApiResponse<StatisticsColumnGuid
export interface StatsInputPeriodItem {
statsYr: string;
seCd: number;
seType: "INPT" | "VRFC";
seType: string;
inptBgngYmd: string;
inptEndYmd: string;
inptYmdSn: number | null;
}
/** `data`가 `StatsInputPeriodItem[]` 인 조회 응답 */
@@ -63,16 +62,26 @@ export type StatsInputPeriodListApiResponse = ApiResponse<StatsInputPeriodItem[]
/** 통계 입력 기간 목록 조회 요청 쿼리 */
export interface StatsInputPeriodListRequest {
statsYr?: string;
/** 전체 / 입력 / 검증 (SearchForm `clsfCd`와 동일) */
seType?: "" | "ALL" | "INPT" | "VRFC";
seType?: string;
}
export interface StatsInputPeriodDetailRequest {
statsYr: string;
seType: string;
seCd: number;
}
export interface StatsInputPeriodDetailResponse {
status: string;
message?: string;
data: StatsInputPeriodItem;
}
/** 통계 입력 기간 등록·수정 등 요청 본문 */
export interface StatsInputPeriodSaveRequest {
statsYr: string;
seType: string;
inptYmdSn: number;
seCd: number;
seCd?: number;
/** 시작일 (백엔드 형식 문자열, 예: YYYYMMDD) */
inptBgngYmd: string;
/** 종료일 (백엔드 형식 문자열, 예: YYYYMMDD) */
@@ -83,8 +92,15 @@ export interface StatsInputPeriodSaveRequest {
/** 통계 입력 기간 저장 응답 */
export type StatsInputPeriodSaveApiResponse = ApiResponse<unknown>;
/** 통계 입력 기간 삭제 요청 (필드 구성은 저장과 동일) */
export type StatsInputPeriodDeleteRequest = StatsInputPeriodSaveRequest;
/** 통계 입력 기간 삭제 요청 */
export interface StatsInputPeriodDeleteRequest {
statsYr: string;
seType: string;
seCd: number;
inptBgngYmd: string;
inptEndYmd: string;
rgtrId: string;
}
/** 통계 입력 기간 삭제 응답 */
export type StatsInputPeriodDeleteApiResponse = ApiResponse<unknown>;
@@ -327,8 +343,10 @@ export interface ReviewReturnSaveRequest {
}
export interface ReviewReturnSaveResponse {
status: string;
message?: string;
exceptionCnt: number;
message: string;
resultCode: string;
updatedStts: string;
}
export interface ReviewSubmitRequest {
@@ -336,6 +354,8 @@ export interface ReviewSubmitRequest {
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
splmntOpnnCn: string;
wrterOpnnCn: string;
}
export interface ReviewSubmitResponse {
@@ -361,17 +381,32 @@ export interface SubmitVerifyExceptionResponse {
message?: string;
}
export interface ReviewerInfoRequest {
sewerBzmnCd: string;
statsYr: string;
statsCd: string;
instSeCd : string;
}
export interface ReviewerInfoResponse {
status: string;
data: {
userNm: string;
telOffrm: string;
dpmNm: string;
cps: string;
splmntOpnnCn: string;
wrterOpnnCn: string;
}
}
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>;
/** 입력 기간 등록 (POST) */
getStatsInputPeriodList(params?: StatsInputPeriodListRequest): Promise<StatsInputPeriodListApiResponse>;
getStatsInputPeriodDetail(param: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse>;
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
/** 입력 기간 수정 (PUT) */
updateStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<ImrResponse>;
@@ -385,6 +420,6 @@ export interface StatisticsService {
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
getImrStatus(param: string): Promise<ImrStatusResponse>;
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
/** `INPUTCOMMON/column-guides` — 통계코드 등 단일 쿼리 문자열 */
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse>;
}
@@ -79,7 +79,7 @@ export interface PublicSewageFacilityReviewRequestParams {
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
fctName?: string;
swtrmtFcltNm?: string;
}
export type PublicSewageFacilityReviewApiResponse =
@@ -244,6 +244,118 @@ export interface SewagePipeDiamService {
getSewagePipeDiamList(params: SewagePipeReviewRequestParams): Promise<SewerPipeDiamApiResponse>;
}
/**
* 02. 하수관로 맨홀 현황 아이템 (Tab 5)
*/
export interface SewagePipeReviewManholeItem {
ctpvNm: string;
sggNm: string;
admdstNm: string;
admdstCd: string;
statsYr: string;
displayYear: string;
rowType: 'OLD' | 'CURR';
emphsMngSeCd: string;
emphsMngSeNm?: string;
yy14WholMnhlCnt: number;
yy14Step1MnhlCnt: number;
yy14Step2MnhlCnt: number;
yy14Step3MnhlCnt: number;
yy14Step4MnhlCnt: number;
yy14Step5MnhlCnt: number;
yy15WholMnhlCnt: number;
yy15Step1MnhlCnt: number;
yy15Step2MnhlCnt: number;
yy15Step3MnhlCnt: number;
yy15Step4MnhlCnt: number;
yy15Step5MnhlCnt: number;
yy16WholMnhlCnt: number;
yy16Step1MnhlCnt: number;
yy16Step2MnhlCnt: number;
yy16Step3MnhlCnt: number;
yy16Step4MnhlCnt: number;
yy16Step5MnhlCnt: number;
yy17WholMnhlCnt: number;
yy17Step1MnhlCnt: number;
yy17Step2MnhlCnt: number;
yy17Step3MnhlCnt: number;
yy17Step4MnhlCnt: number;
yy17Step5MnhlCnt: number;
yy18WholMnhlCnt: number;
yy18Step1MnhlCnt: number;
yy18Step2MnhlCnt: number;
yy18Step3MnhlCnt: number;
yy18Step4MnhlCnt: number;
yy18Step5MnhlCnt: number;
yy19WholMnhlCnt: number;
yy19Step1MnhlCnt: number;
yy19Step2MnhlCnt: number;
yy19Step3MnhlCnt: number;
yy19Step4MnhlCnt: number;
yy19Step5MnhlCnt: number;
yy20WholMnhlCnt: number;
yy20Step1MnhlCnt: number;
yy20Step2MnhlCnt: number;
yy20Step3MnhlCnt: number;
yy20Step4MnhlCnt: number;
yy20Step5MnhlCnt: number;
yy21WholMnhlCnt: number;
yy21Step1MnhlCnt: number;
yy21Step2MnhlCnt: number;
yy21Step3MnhlCnt: number;
yy21Step4MnhlCnt: number;
yy21Step5MnhlCnt: number;
yy22WholMnhlCnt: number;
yy22Step1MnhlCnt: number;
yy22Step2MnhlCnt: number;
yy22Step3MnhlCnt: number;
yy22Step4MnhlCnt: number;
yy22Step5MnhlCnt: number;
yy23WholMnhlCnt: number;
yy23Step1MnhlCnt: number;
yy23Step2MnhlCnt: number;
yy23Step3MnhlCnt: number;
yy23Step4MnhlCnt: number;
yy23Step5MnhlCnt: number;
yy24WholMnhlCnt: number;
yy24Step1MnhlCnt: number;
yy24Step2MnhlCnt: number;
yy24Step3MnhlCnt: number;
yy24Step4MnhlCnt: number;
yy24Step5MnhlCnt: number;
yy25WholMnhlCnt: number;
yy25Step1MnhlCnt: number;
yy25Step2MnhlCnt: number;
yy25Step3MnhlCnt: number;
yy25Step4MnhlCnt: number;
yy25Step5MnhlCnt: number;
rmrk: string | null;
}
export interface SewagePipeManholeResponseData {
manholeList: SewagePipeReviewManholeItem[];
statsStatusInfo: StatsStatusInfo;
}
export type SewagePipeManholeApiResponse = ApiResponse<SewagePipeManholeResponseData>;
export interface SewagePipeManholeService {
getSewagePipeManholeList(params: SewagePipeReviewRequestParams): Promise<SewagePipeManholeApiResponse>;
}
export interface StatsStatusInfo {
statsCd?: string;
submitStatusCd?: string;