Merge remote-tracking branch 'origin/iipbs/st/dev'

머지 branch 'origin/iipbs/st/dev' into iipbs/st/dev
This commit is contained in:
2026-05-13 14:44:29 +09:00
32 changed files with 2414 additions and 327 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;
}
@@ -71,6 +71,38 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
};
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function realGridHeaderWithTooltip<T extends { text: string; tooltip?: string; showTooltip?: boolean; template?: string }>(
header: T,
): T {
const tip = header.tooltip?.trim();
if (!tip) return header;
return {
...header,
tooltip: tip,
showTooltip: header.showTooltip ?? true,
template:
header.template ??
`<span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;width:100%;"><span>${escapeHtml(header.text)}</span><i class="diveicon di-info-circle" aria-hidden="true"></i></span>`,
};
}
function resolveColumnTooltip(column: {
tooltip?: string;
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
}): string | undefined {
const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
return tip && tip.length > 0 ? tip : undefined;
}
const CompactDataGrid = ({
columnInfo,
fixedColumns,
@@ -88,8 +120,10 @@ const CompactDataGrid = ({
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
const columnsRef = useRef(columns);
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
@@ -155,6 +189,66 @@ const CompactDataGrid = ({
});
gridView.header.height = 60;
if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
(gridView.header as { showTooltip?: boolean }).showTooltip = true;
}
columns.forEach((column) => {
(
gridView as {
setColumnProperty?: (
col: string,
key: string,
value: unknown,
) => void;
}
).setColumnProperty?.(
column.name,
"header",
realGridHeaderWithTooltip({
...column.header,
tooltip: resolveColumnTooltip(column),
}),
);
});
(
gridView as {
onShowHeaderTooltip?: (
grid: unknown,
column: {
name?: string;
fieldName?: string;
header?: { tooltip?: string };
},
value: string,
layout?: {
name?: string;
fieldName?: string;
column?: string | { name?: string; fieldName?: string };
},
isEllipsis?: boolean,
) => string | undefined;
}
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
const currentColumns = columnsRef.current;
const directTip = headerColumn?.header?.tooltip?.trim();
if (directTip) return directTip;
const layoutColumn =
typeof layout?.column === "string"
? layout.column
: layout?.column?.name ?? layout?.column?.fieldName;
const col = currentColumns.find(
(c) =>
c.name === headerColumn?.name ||
c.fieldName === headerColumn?.fieldName ||
c.name === layout?.name ||
c.fieldName === layout?.fieldName ||
c.name === layoutColumn ||
c.fieldName === layoutColumn,
);
return col ? resolveColumnTooltip(col) : undefined;
};
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
@@ -235,7 +329,10 @@ const CompactDataGrid = ({
const colInfo: RealGridColumnInfo = {
name: col.name,
fieldName: col.fieldName,
header: col.header,
header: realGridHeaderWithTooltip({
...col.header,
tooltip: resolveColumnTooltip(col),
}),
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
@@ -427,7 +524,10 @@ const CompactDataGrid = ({
fieldName={column.fieldName}
valueType={column.valueType ? column.valueType : ValueType.TEXT}
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
header={column.header}
header={realGridHeaderWithTooltip({
...column.header,
tooltip: resolveColumnTooltip(column),
})}
editable={column.editable}
{...(column.styleName != null
? { styleName: column.styleName }
+190 -15
View File
@@ -81,6 +81,42 @@ function createNumberComparer(
};
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/**
* RealGrid 헤더 툴팁: `grid.header.showTooltip`이 비어 있으면 `column.header.showTooltip === true`인 컬럼만 표시됨.
* `tooltip`이 있는 헤더는 info-circle 아이콘이 함께 보이도록 template도 자동 구성한다.
*/
function realGridHeaderWithTooltip<T extends { text: string; tooltip?: string; showTooltip?: boolean; template?: string }>(
header: T,
): T {
const tip = header.tooltip?.trim();
if (!tip) return header;
return {
...header,
tooltip: tip,
showTooltip: header.showTooltip ?? true,
template:
header.template ??
`<span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;width:100%;"><span>${escapeHtml(header.text)}</span><i class="diveicon di-info-circle" aria-hidden="true"></i></span>`,
};
}
function resolveColumnTooltip(column: {
tooltip?: string;
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
}): string | undefined {
const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
return tip && tip.length > 0 ? tip : undefined;
}
/** 정렬 필드 값을 숫자로 파싱 (쉼표 제거) */
function toNum(v: unknown): number {
if (v == null) return NaN;
@@ -354,6 +390,7 @@ const DataGrid = ({
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const columnsRef = useRef(columns);
const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({});
const originalRowsRef = useRef<
Record<string, Record<string, string | number>[]>
@@ -372,6 +409,7 @@ const DataGrid = ({
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
const onItemAllCheckedRef =
useRef<typeof onItemAllChecked>(onItemAllChecked);
columnsRef.current = columns;
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
cellTooltipCallbackRef.current = cellTooltipCallback;
@@ -813,6 +851,66 @@ const DataGrid = ({
GridFitStyle.EVEN;
gridView.displayOptions.rowHeight = 30;
gridView.header.height = 60;
if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
(gridView.header as { showTooltip?: boolean }).showTooltip = true;
}
columns.forEach((column) => {
(
gridView as {
setColumnProperty?: (
col: string,
key: string,
value: unknown,
) => void;
}
).setColumnProperty?.(
column.name,
"header",
realGridHeaderWithTooltip({
...column.header,
tooltip: resolveColumnTooltip(column),
}),
);
});
(
gridView as {
onShowHeaderTooltip?: (
grid: unknown,
column: {
name?: string;
fieldName?: string;
header?: { tooltip?: string };
},
value: string,
layout?: {
name?: string;
fieldName?: string;
column?: string | { name?: string; fieldName?: string };
},
isEllipsis?: boolean,
) => string | undefined;
}
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
const currentColumns = columnsRef.current;
const directTip = headerColumn?.header?.tooltip?.trim();
if (directTip) return directTip;
const layoutColumn =
typeof layout?.column === "string"
? layout.column
: layout?.column?.name ?? layout?.column?.fieldName;
const col = currentColumns.find(
(c) =>
c.name === headerColumn?.name ||
c.fieldName === headerColumn?.fieldName ||
c.name === layout?.name ||
c.fieldName === layout?.fieldName ||
c.name === layoutColumn ||
c.fieldName === layoutColumn,
);
return col ? resolveColumnTooltip(col) : undefined;
};
// 헤더 체크박스 표시 여부 설정
gridView.setOptions({
@@ -820,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),
@@ -837,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) {
@@ -862,10 +969,6 @@ const DataGrid = ({
});
}
gridView.setOptions({
summaryMode: "aggregate",
});
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
const checkFooterUserSpans = (layout: unknown[]): boolean => {
return layout.some((item) => {
@@ -888,7 +991,9 @@ const DataGrid = ({
});
};
if (checkFooterUserSpans(columnLayout)) {
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
if (hasFooterUserSpans) {
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
setTimeout(() => {
if (gridRef.current?.gridView && columnLayout) {
@@ -917,7 +1022,7 @@ const DataGrid = ({
const colInfo: RealGridColumnInfo = {
name: col.name,
fieldName: col.fieldName,
header: col.header,
header: realGridHeaderWithTooltip(col.header),
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
@@ -951,6 +1056,10 @@ const DataGrid = ({
(currentGridView as GridViewWithSetColumns).setColumns?.(
columnsWithFooter,
);
applyFooterSettings(currentGridView);
if (hasFooterUserSpans) {
currentGridView.setColumnLayout?.(columnLayout);
}
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
setTimeout(() => {
@@ -991,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);
@@ -1334,6 +1448,48 @@ const DataGrid = ({
setTimeout(() => {
const gv = gridRef.current?.gridView;
if (!gv?.columnByName) return;
// 공식 문서: 헤더 툴팁 = header.showTooltip + onShowHeaderTooltip
(
gv as {
onShowHeaderTooltip?: (
grid: unknown,
column: {
name?: string;
fieldName?: string;
header?: {
tooltip?: string;
};
},
value: string,
layout?: {
name?: string;
fieldName?: string;
column?: string | { name?: string; fieldName?: string };
},
isEllipsis?: boolean,
) => string | undefined;
}
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
const currentColumns = columnsRef.current;
const directTip = headerColumn?.header?.tooltip?.trim();
if (directTip) return directTip;
const layoutColumn =
typeof layout?.column === "string"
? layout.column
: layout?.column?.name ?? layout?.column?.fieldName;
const col = currentColumns.find(
(c) =>
c.name === headerColumn?.name ||
c.fieldName === headerColumn?.fieldName ||
c.name === layout?.name ||
c.fieldName === layout?.fieldName ||
c.name === layoutColumn ||
c.fieldName === layoutColumn,
);
return col ? resolveColumnTooltip(col) : undefined;
};
// 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함)
if (cellTooltipCallbackRef.current) {
(
@@ -1387,6 +1543,22 @@ const DataGrid = ({
renderer?: { showTooltip?: boolean };
} | null;
if (gridCol) {
(
gv as {
setColumnProperty?: (
col: string,
key: string,
value: unknown,
) => void;
}
).setColumnProperty?.(
column.name,
"header",
realGridHeaderWithTooltip({
...column.header,
tooltip: resolveColumnTooltip(column),
}),
);
if (cellTooltipCallbackRef.current) {
(
gv as {
@@ -1456,7 +1628,10 @@ const DataGrid = ({
? column.dataFormat
: "#,##0.000",
})}
header={column.header}
header={realGridHeaderWithTooltip({
...column.header,
tooltip: resolveColumnTooltip(column),
})}
editable={column.editable}
{...(column.width != null && {
width: column.width,
@@ -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,
]);
@@ -13,6 +13,10 @@ import {
DialogHeader,
DialogTitle,
LoadingWithAnimation,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
@@ -20,6 +24,13 @@ 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" | "HB";
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" },
{ value: "HB", label: "HB 검증내역" },
];
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
if (raw === undefined || raw === null) return undefined;
@@ -76,30 +87,40 @@ export function VerificationResultHistoryDialog({
title = "검증 결과 내역",
emptyMessage = "표시할 검증 결과가 없습니다.",
}: VerificationResultHistoryDialogProps) {
const [activeTab, setActiveTab] = useState<VerificationHistoryTabKey>("NV");
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
const [normalPageIndex, setNormalPageIndex] = useState(0);
const [imrPageIndex, setImrPageIndex] = useState(0);
const [hbPageIndex, setHbPageIndex] = useState(0);
const [normalPageCount, setNormalPageCount] = useState(1);
const [imrPageCount, setImrPageCount] = useState(1);
const [hbPageCount, setHbPageCount] = useState(1);
const [normalLoading, setNormalLoading] = useState(false);
const [imrLoading, setImrLoading] = useState(false);
const [hbLoading, setHbLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const resetState = useCallback(() => {
setActiveTab("NV");
setNormalEntries([]);
setImrEntries([]);
setHbEntries([]);
setNormalPageIndex(0);
setImrPageIndex(0);
setHbPageIndex(0);
setNormalPageCount(1);
setImrPageCount(1);
setHbPageCount(1);
setNormalLoading(false);
setImrLoading(false);
setHbLoading(false);
setErrorMessage(null);
}, []);
const fetchVerificationResultPage = useCallback(
async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => {
if (!queryContext) return null;
const param: VerificationResultListRequest = {
sewerBzmnCd: queryContext.sewerBzmnCd,
@@ -126,26 +147,34 @@ export function VerificationResultHistoryDialog({
setErrorMessage(null);
setNormalLoading(true);
setImrLoading(true);
setHbLoading(true);
setNormalPageIndex(0);
setImrPageIndex(0);
setHbPageIndex(0);
try {
const [nvRes, imrRes] = await Promise.all([
const [nvRes, imrRes, hbRes] = await Promise.all([
fetchVerificationResultPage("NV", 0),
fetchVerificationResultPage("IMR", 0),
fetchVerificationResultPage("HB", 0),
]);
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
setImrEntries(imrRes?.data?.verifyResultList ?? []);
setHbEntries(hbRes?.data?.verifyResultList ?? []);
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
} catch {
setNormalEntries([]);
setImrEntries([]);
setHbEntries([]);
setNormalPageCount(1);
setImrPageCount(1);
setHbPageCount(1);
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setNormalLoading(false);
setImrLoading(false);
setHbLoading(false);
}
}, [fetchVerificationResultPage, queryContext]);
@@ -187,6 +216,25 @@ export function VerificationResultHistoryDialog({
[fetchVerificationResultPage, imrPageIndex, queryContext],
);
const handleHbPageChange = useCallback(
async (pageIndex: number) => {
if (pageIndex === hbPageIndex || !queryContext) return;
setErrorMessage(null);
setHbLoading(true);
try {
const response = await fetchVerificationResultPage("HB", pageIndex);
setHbEntries(response?.data?.verifyResultList ?? []);
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHbPageIndex(pageIndex);
} catch {
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
} finally {
setHbLoading(false);
}
},
[fetchVerificationResultPage, hbPageIndex, queryContext],
);
useEffect(() => {
if (!open) {
resetState();
@@ -212,6 +260,7 @@ export function VerificationResultHistoryDialog({
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
SetStateAction<Record<string, string | number>[]>
@@ -269,14 +318,24 @@ export function VerificationResultHistoryDialog({
{errorMessage}
</p>
)}
<section className="rounded-md border border-border">
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
</section>
<section className="rounded-md border border-border">
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
</section>
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default">
<TabsList>
{verificationHistoryTabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
<TabsContent value="NV" className="rounded-md border border-border">
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
</TabsContent>
<TabsContent value="IMR" className="rounded-md border border-border">
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
</TabsContent>
<TabsContent value="HB" className="rounded-md border border-border">
{renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)}
</TabsContent>
</Tabs>
</div>
<DialogFooter>
<DialogClose asChild>
@@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks';
@@ -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;
@@ -131,6 +136,10 @@ type MainFacilityMappingSaveDraft = {
initialPrxyCtrtCn: string;
};
const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
);
// 하수도 통계 관리 > 06. 공공하수처리시설
const PublicSewageFacilityInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
@@ -149,6 +158,8 @@ const PublicSewageFacilityInp = () => {
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
@@ -489,6 +500,36 @@ const PublicSewageFacilityInp = () => {
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
useEffect(() => {
let cancelled = false;
void (async () => {
const entries = await Promise.all(
PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== 'success' || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
})();
return () => {
cancelled = true;
};
}, []);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
statusInfo: SewerageStatusInfo | null,
@@ -783,7 +824,7 @@ const PublicSewageFacilityInp = () => {
const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '',
ctpvNm: lastSearchedValues?.sido ?? '',
sggNm: lastSearchedValues?.gugun ?? '',
sggNm: lastSearchedValues?.gugun === 'ALL' ? '' : lastSearchedValues?.gugun ?? '',
statsYr: lastSearchedValues?.year ?? '',
mdfrId: userInfo.userUnqId,
statsCd: statsCdForCancel,
@@ -811,8 +852,15 @@ const PublicSewageFacilityInp = () => {
const getGridColumnInfo = (
tab: ColumnInfoList,
guideTabKey?: string,
): ColumnInfo => ({
columns: tab.columns ?? [],
columns: (tab.columns ?? []).map((col) => {
const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim();
return {
...col,
tooltip: guide && guide.length > 0 ? guide : undefined,
};
}),
columnLayout: tab.columnLayout ?? [],
});
@@ -955,8 +1003,8 @@ const PublicSewageFacilityInp = () => {
return (
<TabsContent key={tab.tabKey} value={tab.tabKey}>
<SplitDataGrid
leftColumnInfo={getGridColumnInfo(tab.left)}
rightColumnInfo={getGridColumnInfo(tab.right)}
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)}
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)}
leftFixedColumns={tab.left.fixedColumns}
rightFixedColumns={tab.right.fixedColumns}
showHeaderCheckbox={true}
@@ -1062,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>
);
@@ -1085,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}`}
@@ -192,6 +192,9 @@ type GridRowWithVerification = Record<string, string | number> & {
};
const REGIONAL_FACT_TAB_KEY = "ST00020001";
const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)),
);
function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
if (!verification) return false;
@@ -355,6 +358,8 @@ const SewagePipeInp = () => {
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
const [hasSearched, setHasSearched] = useState(false);
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
@@ -417,6 +422,37 @@ const SewagePipeInp = () => {
}
}, [activeTabId, inputFocusNonce]);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
let cancelled = false;
void (async () => {
const entries = await Promise.all(
SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries));
})();
return () => {
cancelled = true;
};
}, [activeTabId]);
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
if (loadedTabKey === activeTabRef.current) {
setStatsCd(nextStatsCd);
@@ -445,6 +481,14 @@ const SewagePipeInp = () => {
};
const columnInfo = useMemo(() => {
const applyColumnGuides = (cols: ColumnConfig[] | undefined, tabKey: string) =>
(cols ?? []).map((col: ColumnConfig) => {
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
return {
...col,
tooltip: guide && guide.length > 0 ? guide : undefined,
};
});
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
const canEdit =
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo;
@@ -458,16 +502,16 @@ const SewagePipeInp = () => {
if (tab.left && tab.right) {
return {
...tab,
left: { ...tab.left, columns: applyEditable(tab.left.columns) },
right: { ...tab.right, columns: applyEditable(tab.right.columns) },
left: { ...tab.left, columns: applyColumnGuides(applyEditable(tab.left.columns), tab.tabKey) },
right: { ...tab.right, columns: applyColumnGuides(applyEditable(tab.right.columns), tab.tabKey) },
};
}
return {
...tab,
columns: applyEditable(tab.columns),
columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey),
};
});
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]);
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = (
tabKey: string,
@@ -866,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);
@@ -1251,7 +1295,7 @@ const SewagePipeInp = () => {
const requestData: cancelSubmitRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '',
ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
@@ -2008,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);
}}
/>
</>
);
@@ -1,4 +1,4 @@
import { useEffect,useLayoutEffect, useMemo, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
@@ -57,6 +57,9 @@ type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult;
};
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
if (!verification) return false;
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
@@ -101,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);
@@ -119,6 +130,8 @@ const SeweragePenetrationRateInp = () => {
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
/** `engColNm` → `columnGuide` (헤더 hover) */
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
const { confirm } = useConfirm();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
@@ -149,6 +162,35 @@ const SeweragePenetrationRateInp = () => {
setSearchFormRemountKey((k) => k + 1);
}, [activeTabId, inputFocusNonce]);
useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
return;
}
let cancelled = false;
void (async () => {
try {
const res = await statisticsApi.getColumnGuides(SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD);
if (cancelled) return;
if (res.status !== "success" || !Array.isArray(res.data)) {
setColumnGuidesByEngCol({});
return;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
setColumnGuidesByEngCol(next);
} catch {
if (!cancelled) setColumnGuidesByEngCol({});
}
})();
return () => {
cancelled = true;
};
}, [activeTabId]);
const { isAdminInfo } = useAdminStore();
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
@@ -167,9 +209,17 @@ const SeweragePenetrationRateInp = () => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
const withHeaderTooltips = base.map((col) => {
const guide = columnGuidesByEngCol[col.fieldName]?.trim();
return {
...col,
tooltip: guide && guide.length > 0 ? guide : undefined,
};
});
return {
columnLayout,
columns: base.map((col) => {
columns: withHeaderTooltips.map((col) => {
if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') };
if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') };
if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') };
@@ -178,7 +228,7 @@ const SeweragePenetrationRateInp = () => {
return col;
}),
};
}, [isEnterable, isAdminInfo, footerFromApi]);
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]);
const handleReset = () => {
setRowData([]);
@@ -276,7 +326,7 @@ const SeweragePenetrationRateInp = () => {
if (!values) return;
postVerificationSearchRef.current = null;
void handleSearch(values);
}, [searchFormRemountKey]);
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
@@ -291,7 +341,7 @@ const SeweragePenetrationRateInp = () => {
...submitInfo,
sewerBzmnCd: searchValues?.bsnmCd ?? '',
ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
statsCd: statsCd,
@@ -336,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);
@@ -396,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'),
@@ -413,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'),
@@ -428,7 +479,7 @@ const SeweragePenetrationRateInp = () => {
const requestData: SewerageSupplyRateSaveRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '',
ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId,
submitList: convertedData,
@@ -542,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'), // 생물학적처리
@@ -562,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'), // 총면적(㎢)
@@ -579,7 +633,7 @@ const SeweragePenetrationRateInp = () => {
const requestData: SewerageSupplyRateSaveRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드
ctpvNm: searchValues?.sido ?? '', // 시도
sggNm: searchValues?.gugun ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '', // 통계 연도
userId: userInfo.userUnqId, // 사용자
saveList: convertedData, // 저장 데이터
@@ -624,7 +678,7 @@ const SeweragePenetrationRateInp = () => {
const requestData: cancelSubmitRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '',
ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId,
statsCd: statsCd,
@@ -777,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>
</>
@@ -8,6 +8,7 @@ import useSWR from "swr";
import { useAlertStore } from "@/shared/lib";
import { CodeSelectWithAll } from "@/entities/code";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { StatisticsLogResponse } from "@/entities/statistics/model/statistics-interface";
import type {
ApiResponse,
SearchCondition,
@@ -129,7 +130,7 @@ const LogHistory = () => {
const selectedBsnm =
value === "ALL"
? (sidoList?.data.sidoList.find((v) => v.sido === sido)
?.bsnmCd ?? "")
?.bsnmCd ?? "")
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? "");
setGugun(value);
setBsnmCd(selectedBsnm);
@@ -150,8 +151,8 @@ const LogHistory = () => {
...(bsnmCd && { sewerBzmnCd: bsnmCd }),
...(statsDataChgComCd &&
statsDataChgComCd !== "ALL" && {
statsDataChgComCd,
}),
statsDataChgComCd,
}),
};
try {
@@ -160,15 +161,19 @@ const LogHistory = () => {
const response = await statisticsApi.getLogList(param);
if (response.status === "success") {
const lostList = response.data?.statsLogList ?? [];
const logListRaw = response.data?.statsLogList ?? [];
const logList = logListRaw.map((l) => ({
...l,
sggNm: String(l.sggNm ?? "").trim() || "-",
}));
setRowData(toGridRows(lostList));
setRowData(toGridRows(logList));
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
setPageIndex(Math.max(0, page1Based - 1));
} else {
alert(
response.message ||
"데이터 조회 중 오류가 발생했습니다.",
"데이터 조회 중 오류가 발생했습니다.",
);
}
} catch {
@@ -331,7 +336,7 @@ const LogHistory = () => {
columnInfo={LogHistoryColumn}
isExcelImport={false}
rowData={rowData}
setRowData={() => {}}
setRowData={() => { }}
onRowClick={handleRowClick}
/>
<LogHistoryDetailDialog
@@ -19,6 +19,7 @@ import type { ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
/** API `YYYYMMDD` 등 → Date */
function seTypeToDisplayLabel(seType: StatsInputPeriodItem["seType"]): string {
if (seType === "INPT") return "입력";
if (seType === "VRFC") return "검증";
@@ -32,8 +33,7 @@ function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC"
return "INPT";
}
/** API `YYYYMMDD` 등 → Date */
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,15 +77,17 @@ const StatisticsInputManagement = () => {
setIsLoading(true);
const params: StatsInputPeriodListRequest = {
statsYr: values.year,
...(values.clsfCd && values.clsfCd !== "ALL"
? { seType: values.clsfCd }
: {}),
};
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),
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
}));
setRowData(toGridRows(rowsForGrid));
} else {
@@ -111,21 +119,47 @@ const StatisticsInputManagement = () => {
setPeriodDialogOpen(true);
};
const handleRowClick = (row: Record<string, string | number>) => {
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),
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[] = [
@@ -140,11 +140,13 @@ export function StatisticsInputPeriodDialog({
const param: StatsInputPeriodSaveRequest = {
statsYr: yr,
seType,
seCd: seCdParsed,
inptBgngYmd: bgng,
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
if (!isCreate) {
param.seCd = seCdParsed;
}
const res = await statisticsApi.saveStatsInputPeriod(param);
if (res.status === "success") {
alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`);
@@ -243,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="선택" />
@@ -1,47 +1,273 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { DataGrid } from "@rsone/components";
import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { PublicSewageFacilityReviewItem } from "@/entities/statistics/model/statistics-public-sewage-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
import ApproveButton from "@/components/button/ApproveButton";
import SubmitButton from "@/components/button/SumitButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
// 하수도 통계 관리 06.공공하수처리시설
const PublicSewageFacilityRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
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 [reviewOpinionContext, setReviewOpinionContext] =
useState<ReviewOpinionDialogContext | null>(null);
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
const [latestProcessHistory, setLatestProcessHistory] = useState("");
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const approveFunc = () => {
console.log('approveFunc');
const columnInfo = useMemo(() => {
const { columns, columnLayout, fixedColumns, rowStartNum } = PublicSewageFacilityInfoList;
return {
columnLayout,
fixedColumns,
rowStartNum,
columns: columns.map((col) => ({
...col,
editable: false as const,
})),
};
}, []);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
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",
};
}, [statsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
const handleReset = () => {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
setLastSearchValues(null);
};
const saveFunc = () => {
console.log('submit');
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
setLastSearchValues(values);
try {
setIsLoading(true);
const response = await statisticsManagementApi.getPublicSewageFacilityReviewList({
ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
swtrmtFcltNm: values.fctName,
});
if (response.status === "success" && response.data) {
const rawList = response.data.publicSewageList;
const convertedData: PublicSewageFacilityReviewItem[] = rawList.map((item) => ({
...item,
impdFcltPstnCn: item.fcltPstnCn ?? "",
prxyBzentyNmCn: item.prxyBzentyNmCn ?? "",
prxyCtrtCn: item.prxyCtrtCn ?? "",
intgPrcsFcltCn: item.intgPrcsFcltCn ?? "",
rmrk: item.rmrk ?? "",
})) as PublicSewageFacilityReviewItem[];
setStatsStatusInfo(response.data.statsStatusInfo);
setReviewOpinionContext({
statsYr: values.year ?? "",
sewerBzmnCd: values.bsnmCd ?? "",
statsCd: response.data.statsStatusInfo.statsCd ?? "",
});
setLatestReviewOpinion(
response.data.statsStatusInfo.splmntOpnnCn ?? "",
);
setLatestProcessHistory(
response.data.statsStatusInfo.wrterOpnnCn ?? "",
);
setRowData(toGridRows(convertedData as unknown as object[]));
} else {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
alert(response.message || "데이터가 없습니다.");
}
} catch (error) {
console.error("공공하수처리시설 조회 중 에러 발생:", error);
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const reviewOpinionFunc = () => {
console.log('reviewOpinionFunc');
const submitFunc = async () => {
if (reviewOpinionContext == 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("제출 중 오류가 발생했습니다.");
}
};
const returnFunc = () => {
console.log('returnFunc');
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
setLatestProcessHistory(row?.wrterOpnnCn ?? "");
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setStatsStatusInfo((prev) => ({
...prev,
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
}));
setLatestReviewOpinion(param.splmntOpnnCn);
setLatestProcessHistory(param.wrterOpnnCn);
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<>
<SearchForm fctName={true} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
<div className="relative">
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
{showActionBar && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && <SubmitButton submitFunc={submitFunc} />}
</div>
</div>
</div>
<DataGrid columnInfo={PublicSewageFacilityInfoList} isExcelImport={false} rowStartNum={3} fixedColumns={3} rowData={rowData} setRowData={setRowData} />
</>
)}
{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>
)}
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
fixedColumns={columnInfo.fixedColumns}
rowStartNum={columnInfo.rowStartNum}
rowData={rowData}
setRowData={setRowData}
/>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
@@ -14,25 +16,47 @@ import {
Textarea,
} from "@common/components";
import type { ReviewReturnSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { useAlertStore } from "@/shared/lib";
export type ReturnReasonDialogContext = Pick<
ReviewReturnSaveRequest,
"statsYr" | "sewerBzmnCd" | "statsCd"
>;
export interface ReturnReasonDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave?: (reason: string) => void | Promise<void>;
context: ReturnReasonDialogContext | null;
onSave?: () => void | Promise<void>;
}
export function ReturnReasonDialog({
open,
onOpenChange,
context,
onSave,
}: ReturnReasonDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
const [reason, setReason] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [reasonError, setReasonError] = useState(false);
useEffect(() => {
if (!open) return;
const resetForm = () => {
setReason("");
setReasonError(false);
};
const handleDialogOpenChange = (nextOpen: boolean) => {
if (!nextOpen) resetForm();
onOpenChange(nextOpen);
};
useEffect(() => {
if (!open) return;
resetForm();
}, [open]);
const handleSave = async () => {
@@ -41,24 +65,65 @@ export function ReturnReasonDialog({
return;
}
if (
context == null ||
context.statsYr === "" ||
context.sewerBzmnCd === "" ||
context.statsCd === ""
) {
alert("시도·시군구·년도로 조회한 뒤 반려 처리할 수 있습니다.");
return;
}
if (!(await confirm("반려 처리 하시겠습니까?"))) {
return;
}
try {
setIsSaving(true);
await onSave?.(reason);
let userInfo: Awaited<ReturnType<typeof getUserInfo>>;
try {
userInfo = await getUserInfo();
} catch {
alert("사용자 정보를 불러오지 못했습니다. 다시 시도해주세요.");
return;
}
const param: ReviewReturnSaveRequest = {
type: "REVIEW",
statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
rjctOpnnCn: reason,
mdfrId: userInfo.userUnqId,
};
const response = await statisticsApi.saveReviewReturn(param);
if (response.resultCode.toLowerCase() !== "success") {
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
return;
}
alert("반려 처리 되었습니다.");
await onSave?.();
resetForm();
onOpenChange(false);
} catch {
alert("반려 처리 중 오류가 발생했습니다.");
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>반려 사유</DialogTitle>
<DialogTitle>반려</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">반려 사유</Label>
<Label className="text-label mb-2">반려 사유</Label>
<Textarea
value={reason}
onChange={(e) => {
@@ -86,11 +151,11 @@ export function ReturnReasonDialog({
</DialogClose>
<Button
type="button"
color="primary"
color="red"
disabled={isSaving}
onClick={() => void handleSave()}
>
저장
반려 제출
</Button>
</DialogFooter>
</DialogContent>
@@ -115,9 +115,9 @@ export function ReviewOpinionDialog({
<DialogHeader>
<DialogTitle>검토의견작성</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-8 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">검토의견</Label>
<Label className="text-label mb-2">검토의견</Label>
<Textarea
value={reviewOpinion}
onChange={(e) => {
@@ -137,7 +137,7 @@ export function ReviewOpinionDialog({
)}
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">처리내역</Label>
<Label className="text-label mb-2">처리내역</Label>
<Textarea
value={processHistory}
onChange={(e) => setProcessHistory(e.target.value)}
@@ -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,11 +1,16 @@
import { useMemo, useRef, useState } from "react";
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 {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
@@ -13,13 +18,58 @@ import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
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 {
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,
@@ -36,6 +86,9 @@ async function fetchSewagePipeReviewTab(
if (tabKey === "ST00020004") {
return statisticsManagementApi.getSewagePipeDiamList(searchParam);
}
if (tabKey === "ST00020005") {
return await statisticsManagementApi.getSewagePipeManholeList(searchParam);
}
return null;
}
@@ -56,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 [];
}
@@ -70,9 +125,14 @@ function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
// 하수도 통계 검토 02.하수관로
const SewagePipeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
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<
Map<string, StatsStatusInfo>
@@ -81,9 +141,53 @@ const SewagePipeRvw = () => {
const searchParamRef = useRef<SewagePipeReviewRequestParams | null>(null);
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const activeStatsStatusInfo = useMemo(
() => 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 {
statsYr: lastSearchValues.year ?? "",
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
statsCd: activeStatsStatusInfo.statsCd ?? "",
};
}, [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 = tabStatsStatusInfoMap.get(activeTab) ?? {};
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
@@ -97,7 +201,7 @@ const SewagePipeRvw = () => {
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [tabStatsStatusInfoMap, activeTab]);
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
@@ -106,6 +210,10 @@ const SewagePipeRvw = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setSubmitDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
@@ -119,6 +227,7 @@ const SewagePipeRvw = () => {
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewagePipeReviewRequestParams = {
statsYr: values.year,
@@ -204,6 +313,79 @@ const SewagePipeRvw = () => {
}
};
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const reviewOpinionFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 검토의견을 작성할 수 있습니다.");
return;
}
try {
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
});
if (detailRes.status !== "success") {
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
return;
}
const row = detailRes.data?.[0];
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: row?.splmntOpnnCn ?? "",
wrterOpnnCn: row?.wrterOpnnCn ?? "",
});
return next;
});
setReviewOpinionOpen(true);
} catch {
alert("검토의견 조회 중 오류가 발생했습니다.");
}
};
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
}
alert(response.message || "저장되었습니다.");
setTabStatsStatusInfoMap((prev) => {
const next = new Map(prev);
next.set(activeTab, {
...(next.get(activeTab) ?? {}),
splmntOpnnCn: param.splmntOpnnCn,
wrterOpnnCn: param.wrterOpnnCn,
});
return next;
});
} catch {
alert("저장 중 오류가 발생했습니다.");
}
};
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
@@ -212,11 +394,11 @@ const SewagePipeRvw = () => {
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showReviewOpinionButton && (
<ReviewOpinionButton reviewOpinionFunc={() => {}} />
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
)}
{showReturnButton && <ReturnButton returnFunc={() => {}} />}
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
{showSubmitButton && (
<SubmitButton submitFunc={() => {}} />
<SubmitButton submitFunc={() => submitFunc()} />
)}
</div>
</div>
@@ -237,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;
@@ -283,6 +465,30 @@ const SewagePipeRvw = () => {
})}
</Tabs>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
onSubmitted={async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
}}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
@@ -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 () => {
@@ -211,36 +208,9 @@ const SeweragePenetrationRateRvw = () => {
const returnFunc = () => setReturnDialogOpen(true);
const handleSaveReturnReason = async (reason: string) => {
if (reviewOpinionContext == null) {
alert("조회 후 반려할 수 있습니다.");
return;
}
if (await confirm('반려 처리 하시겠습니까?')) {
try {
const userInfo = await getUserInfo();
const response = await statisticsApi.saveReviewReturn({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
rjctOpnnCn: reason,
mdfrId: userInfo.userUnqId,
});
if (response.status !== "success") {
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
return;
}
alert(response.message || "반려 처리 되었습니다.");
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
} catch {
alert("반려 처리 중 오류가 발생했습니다.");
}
const handleSaveReturnReason = async () => {
if (lastSearchValues != null) {
await handleSearch(lastSearchValues);
}
};
@@ -285,9 +255,20 @@ 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}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
@@ -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
+5 -3
View File
@@ -17,7 +17,7 @@ export type DataGridStatusBannerInfo = {
export type RealGridColumnInfo = {
name: string;
fieldName: string;
header: { text: string };
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
editable?: boolean;
valueType?: import("realgrid").ValueType;
dataFormat?: string;
@@ -154,7 +154,9 @@ export type ComputedFieldConfig =
export type ColumnConfig = {
name: string;
fieldName: string;
header: { text: string };
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
/** 헤더 hover 시 보여줄 컬럼 가이드 문구 */
tooltip?: string;
/** 계산 필드: fields 합계/평균 등. valueCallback으로 dataProvider.setFields 시 전달 */
computed?: ComputedFieldConfig;
editable?: boolean; // 셀 편집 가능 여부
@@ -210,7 +212,7 @@ export type ColumnLayoutItem =
| {
name: string;
hideChildHeaders?: boolean;
header: { text: string };
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
@@ -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'
],
},
]
@@ -1,142 +1,190 @@
import { ValueType } from "realgrid";
import {
footerAvgDecimalArrayByDivision,
footerDecimal1ArrayByDivision,
footerDecimal3ArrayByDivision,
footerIntArrayByDivision,
} from "@/components/util/grid/footerSummary";
import type { GridView } from "@/components/type/DataGrid.type";
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
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);
if (divisionValues.length <= divisionIndex) {
return withSuffix ? "합계" : "";
}
const label = divisionValues[divisionIndex] ?? "";
return withSuffix ? `${label}합계` : label;
};
// 하수도 통계 관리 06.공공하수처리시설
export const rawPublicSewageFacilityInfoList = {
columns: [
/* 1. 기본 정보 */
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
{ name: 'GBN', fieldName: '구분', header: { text: '구분' } },
{ name: 'ADDR', fieldName: '소재지', header: { text: '소재지' } },
{ name: 'FCT_NM_GROUP', fieldName: '시설명(500m3/일이상/미만)', header: { text: '시설명(500m3/일이상/미만)' } },
{
name: 'ctpvNm',
fieldName: 'ctpvNm',
header: { text: '시도' },
footer: [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
],
},
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
{
name: 'displayYear',
fieldName: 'displayYear',
header: { text: '구분' },
footer: [
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
],
},
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명(500m³/일 이상/미만)' } },
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '소재지' } },
/* 2. 시설용량 (㎥/일) */
{ name: 'FCT_CPC_GROUP', fieldName: '시설용량(500m3/일이상/미만)_용량', header: { text: '시설용량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'FCT_CPC_PHY', fieldName: '물리적_용량', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'FCT_CPC_BIO', fieldName: '생물학적_용량', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'FCT_CPC_ADV', fieldName: '고도_용량', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 3. 유입하수량 (㎥/일) */
{ name: 'IN_QTY_GROUP', fieldName: '유입하수량(500m3/일이상/미만)', header: { text: '유입하수량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_QTY_PHY', fieldName: '물리적_유입', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_QTY_BIO', fieldName: '생물학적_유입', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_QTY_ADV', fieldName: '고도_유입', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwSwagePhysQnt', fieldName: 'inflwSwagePhysQnt', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwSwageBoilQnt', fieldName: 'inflwSwageBoilQnt', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwSwageAltdQnt', fieldName: 'inflwSwageAltdQnt', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 4. 방류량 (㎥/일) */
{ name: 'OUT_QTY_GROUP', fieldName: '방류량(500m3/일이상/미만)', header: { text: '방류량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_QTY_PHY', fieldName: '물리적_방류', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_QTY_BIO', fieldName: '생물학적_방류', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_QTY_ADV', fieldName: '고도_방류', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsQnt', fieldName: 'prcsQnt', header: { text: '방류량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 5. 수질 및 처리효율 */
{ name: 'IN_BOD', fieldName: '유입BOD', header: { text: '유입BOD' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_BOD', fieldName: '방류BOD', header: { text: '방류BOD' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_TOC', fieldName: '유입TOC', header: { text: '유입TOC' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_TOC', fieldName: '방류TOC', header: { text: '방류TOC' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_SS', fieldName: '유입SS', header: { text: '유입SS' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_SS', fieldName: '방류SS', header: { text: '방류SS' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'IN_TN', fieldName: '유입T-N', header: { text: '유입T-N' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'OUT_TN', fieldName: '방류T-N', header: { text: '방류T-N' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'IN_TP', fieldName: '유입T-P', header: { text: '유입T-P' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'OUT_TP', fieldName: '방류T-P', header: { text: '방류T-P' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'IN_COLI', fieldName: '유입총대장균군수', header: { text: '유입총대장균군수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'OUT_COLI', fieldName: '방대총대장균군수', header: { text: '방대총대장균군수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'TRT_EFF', fieldName: '처리효율(%)', header: { text: '처리효율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'TRT_LOD', fieldName: '처리부하량(kgBOD/D)', header: { text: '처리부하량(kgBOD/D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'TRT_MTHD', fieldName: '처리방법', header: { text: '처리방법' } },
{ name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입BOD' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'dschgBodVl', fieldName: 'dschgBodVl', header: { text: '방류BOD' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwTocQnt', fieldName: 'inflwTocQnt', header: { text: '유입TOC' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'dschgTocQnt', fieldName: 'dschgTocQnt', header: { text: '방류TOC' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwSsQnt', fieldName: 'inflwSsQnt', header: { text: '유입SS' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'dschgSsQnt', fieldName: 'dschgSsQnt', header: { text: '방류SS' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'inflwTnVl', fieldName: 'inflwTnVl', header: { text: '유입T-N' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'dschgTnVl', fieldName: 'dschgTnVl', header: { text: '방류T-N' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'inflwTpVl', fieldName: 'inflwTpVl', header: { text: '유입T-P' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'dschgTpVl', fieldName: 'dschgTpVl', header: { text: '방류T-P' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'inflwGrssClfgVl', fieldName: 'inflwGrssClfgVl', header: { text: '유입총대장균군수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류총대장균군수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsRt', fieldName: 'prcsRt', header: { text: '처리효율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' } },
/* 6. 연계처리량 */
{ name: 'LNK_EXC', fieldName: '분뇨', header: { text: '분뇨' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LNK_LVS', fieldName: '축산', header: { text: '축산' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LNK_LCH', fieldName: '침출수', header: { text: '침출수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'LNK_ETC', fieldName: '기타_연계', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'wholHmnQnt', fieldName: 'wholHmnQnt', header: { text: '분뇨' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'wholLvstQnt', fieldName: 'wholLvstQnt', header: { text: '축산' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'wholEfntQnt', fieldName: 'wholEfntQnt', header: { text: '침출수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'wholEtcQnt', fieldName: 'wholEtcQnt', header: { text: '기타' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
/* 7. 상세 시설 정보 */
{ name: 'TEL_NO', fieldName: '전화번호', header: { text: '전화번호' } },
{ name: 'COMP_DT', fieldName: '준공일', header: { text: '준공일' } },
{ name: 'RN_DT', fieldName: '가동개시일', header: { text: '가동개시일' } },
{ name: 'BSNS_CT', fieldName: '사업비(백만원)', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'DSCH_DSF_MTHD', fieldName: '방류수소독방법', header: { text: '방류수소독방법' } },
{ name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' } },
{ name: 'cmptnStrtYmd', fieldName: 'cmptnStrtYmd', header: { text: '준공일' } },
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시일' } },
{ name: 'wholBizVl', fieldName: 'wholBizVl', header: { text: '사업비' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'eflntDsnfMthdExpln', fieldName: 'eflntDsnfMthdExpln', header: { text: '방류수 소독방법' } },
/* 8. 방류수역 */
{ name: 'WTR_SYS', fieldName: '수계', header: { text: '수계' } },
{ name: 'STRM', fieldName: '지류', header: { text: '지류' } },
{ name: 'DET_UNIT_ZONE', fieldName: '세부단위구역', header: { text: '세부단위구역' } },
{ name: 'RGN_GBN', fieldName: '지역구분', header: { text: '지역구분' } },
{ name: 'MID_ZONE_NM', fieldName: '명칭', header: { text: '명칭' } },
{ name: 'MID_ZONE_BOD', fieldName: '목표수질(BOD)', header: { text: '목표수질(BOD)' } },
{ name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' } },
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
{ name: 'dtlUnitZoneCn', fieldName: 'dtlUnitZoneCn', header: { text: '세부단위구역' } },
{ name: 'rgnSeNm', fieldName: 'rgnSeNm', header: { text: '지역구분' } },
{ name: 'msirbCn', fieldName: 'msirbCn', header: { text: '명칭' } },
{ name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질(BOD)' } },
/* 9. 에너지 및 환경 */
{ name: 'ENG_SLF_RT', fieldName: '에너지자립율(%)', header: { text: '에너지자립율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ENG_SLF_QTY', fieldName: '에너지자체생산량(TOE)', header: { text: '에너지자체생산량(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ENG_TOT_SUM', fieldName: '계(TOE)', header: { text: '계(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ENG_TOT_PWR', fieldName: '총전력사용량(KWh)', header: { text: '총전력사용량(KWh)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ENG_TOT_SLF', fieldName: '자체에너지사용량(TOE)', header: { text: '자체에너지사용량(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'CO2_EMISS', fieldName: '하수처리량당CO2배출량(KgCO2/m3)', header: { text: '하수처리량당CO2배출량(KgCO2/m3)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
{ name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지자립률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'anlEngyWholUseQnt', fieldName: 'anlEngyWholUseQnt', header: { text: '계(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '총 전력사용량(kWh)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체 에너지사용량(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
/* 10. 운영 및 위탁 */
{ name: 'OPR_MBY', fieldName: '운영주체', header: { text: '운영주체' } },
{ name: 'ENTRPS_NM', fieldName: '위탁업체명', header: { text: '위탁업체명' } },
{ name: 'ENTRPS_CT', fieldName: '위탁비용(백만원/년)', header: { text: '위탁비용(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'ENTRPS_TERM', fieldName: '위탁계약기간', header: { text: '위탁계약기간' } },
{ name: 'CTR_TRT_FCT', fieldName: '중심처리시설', header: { text: '중심처리시설' } },
{ name: 'EMP_CNT', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'ACC_CNT', fieldName: '안전사고건수', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
{ name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' } },
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' } },
{ name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
{ name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' } },
{ name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' } },
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
],
columnLayout: [
{ name: 'groupA', header: { text: '행정구역' }, items: ['sido', 'sgg'] },
'FCT_NM_GROUP', 'GBN', 'ADDR', 'FCT_CPC_GROUP',
{
name: 'groupA',
hideChildHeaders: false,
header: { text: '행정구역' },
items: [{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'sggNm'],
},
'displayYear', 'swtrmtFcltNm', 'impdFcltPstnCn', 'prcsFcltCpct',
{
name: 'groupB',
header: { text: '시설용량(㎥/일)' },
items: ['FCT_CPC_PHY', 'FCT_CPC_BIO', 'FCT_CPC_ADV'],
items: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'],
},
'IN_QTY_GROUP',
'inflwSwageQnt',
{
name: 'groupC',
header: { text: '유입하수량(㎥/일)' },
items: ['IN_QTY_PHY', 'IN_QTY_BIO', 'IN_QTY_ADV'],
items: ['inflwSwagePhysQnt', 'inflwSwageBoilQnt', 'inflwSwageAltdQnt'],
},
'OUT_QTY_GROUP',
'prcsQnt',
{
name: 'groupD',
header: { text: '방류량(㎥/일)' },
items: ['OUT_QTY_PHY', 'OUT_QTY_BIO', 'OUT_QTY_ADV'],
items: ['prcsPhysCpct', 'prcsBoilCpct', 'prcsAltdCpct'],
},
'IN_BOD', 'OUT_BOD', 'IN_TOC', 'OUT_TOC', 'IN_SS', 'OUT_SS', 'IN_TN', 'OUT_TN', 'IN_TP', 'OUT_TP', 'IN_COLI', 'OUT_COLI',
'TRT_EFF', 'TRT_LOD', 'TRT_MTHD',
'inflwBodVl', 'dschgBodVl', 'inflwTocQnt', 'dschgTocQnt', 'inflwSsQnt', 'dschgSsQnt', 'inflwTnVl', 'dschgTnVl', 'inflwTpVl', 'dschgTpVl', 'inflwGrssClfgVl', 'dschgGrssClfgVl',
'prcsRt', 'prcsLodQnt', 'prcsMthdExpln',
{
name: 'groupE',
header: { text: '연계처리량(㎥/일)' },
items: ['LNK_EXC', 'LNK_LVS', 'LNK_LCH', 'LNK_ETC'],
items: ['wholHmnQnt', 'wholLvstQnt', 'wholEfntQnt', 'wholEtcQnt'],
},
'TEL_NO', 'COMP_DT', 'RN_DT', 'BSNS_CT', 'DSCH_DSF_MTHD',
'telNo', 'cmptnStrtYmd', 'oprtngStrtYmd', 'wholBizVl', 'eflntDsnfMthdExpln',
{
name: 'groupF',
header: { text: '방류수역' },
items: ['WTR_SYS', 'STRM', 'DET_UNIT_ZONE', 'RGN_GBN',
items: ['wtsysNm', 'trbtrNm', 'dtlUnitZoneCn', 'rgnSeNm',
{
name: 'groupG',
header: { text: '중권역' },
items: ['MID_ZONE_NM', 'MID_ZONE_BOD'],
items: ['msirbCn', 'goalWqNo'],
},
],
},
'ENG_SLF_RT', 'ENG_SLF_QTY',
'engySrlneRt', 'engyItslfPrdctnQnt',
{
name: 'groupH',
header: { text: '연간 총 에너지 사용량(TOE)' },
items: ['ENG_TOT_SUM', 'ENG_TOT_PWR', 'ENG_TOT_SLF'],
items: ['anlEngyWholUseQnt', 'anlElpwrUseQnt', 'itslfEngyUseQnt'],
},
'CO2_EMISS', 'OPR_MBY',
'swtrmtEmsnQnt', 'operMnbdCn',
{
name: 'groupI',
header: { text: '위탁현황' },
items: ['ENTRPS_NM', 'ENTRPS_CT', 'ENTRPS_TERM'],
items: ['prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn'],
},
'CTR_TRT_FCT', 'EMP_CNT', 'ACC_CNT', 'RMK'
]
'intgPrcsFcltCn', 'wholNope', 'sftyAcdntEvntcsCnt', 'rmrk'
],
fixedColumns: 3,
rowStartNum: 3
};
// 최종 Export 객체 (일괄 editable: false 적용)
@@ -144,6 +192,7 @@ export const PublicSewageFacilityInfoList = {
...rawPublicSewageFacilityInfoList,
columns: rawPublicSewageFacilityInfoList.columns.map(col => ({
...col,
editable: false
editable: false,
styleName: "rg-cell-readonly"
}))
};
@@ -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,
@@ -30,6 +31,10 @@ import {
import {
type MediationPumpStationApiResponse,
} from "../model/statistics-pumping-station-review-interface";
import {
type PublicSewageFacilityReviewApiResponse,
type PublicSewageFacilityReviewRequestParams,
} from "../model/statistics-public-sewage-facility-review-interface";
export class StatisticsManagementApiService {
@@ -61,6 +66,20 @@ export class StatisticsManagementApiService {
return response.data;
}
/**
* 06. 공공하수처리시설 검토 목록 조회
* @param params (sido, gugun, yr, bsnmCd, fctName)
*/
public async getPublicSewageFacilityReviewList(
params: PublicSewageFacilityReviewRequestParams
): Promise<PublicSewageFacilityReviewApiResponse> {
const response = await stClient.get<PublicSewageFacilityReviewApiResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/public-sewage`,
{ params }
);
return response.data;
}
/**
* 02. 하수관로 지역별 시설현황 목록 조회 (Tab1)
* @param params (sido, gugun, yr, bsnmCd)
@@ -117,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,
@@ -18,12 +20,15 @@ import {
type ReviewSubmitResponse,
type SearchCondition,
type Sgg,
type StatisticsColumnGuideListResponse,
type StatisticsLogDetailResponse,
type StatisticsLogRequest,
type StatisticsLogResponse,
type StatisticsService,
type StatsInputPeriodDeleteApiResponse,
type StatsInputPeriodDeleteRequest,
type StatsInputPeriodDetailRequest,
type StatsInputPeriodDetailResponse,
type StatsInputPeriodListApiResponse,
type StatsInputPeriodListRequest,
type StatsInputPeriodSaveApiResponse,
@@ -80,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,
@@ -158,6 +171,17 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
/** 컬럼 가이드 목록 조회 — GET `.../common/column-guides?statsCd=` */
public async getColumnGuides(
param: string,
): Promise<StatisticsColumnGuideListResponse> {
const response = await stClient.get<StatisticsColumnGuideListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/column-guides`,
{ params: { statsCd: param } },
);
return response.data;
}
public async saveReviewOpinion(
param: ReviewOpinionSaveRequest,
): Promise<ReviewOpinionSaveResponse> {
@@ -181,9 +205,11 @@ export class StatisticsApiService implements StatisticsService {
public async saveReviewReturn(
param: ReviewReturnSaveRequest,
): Promise<ReviewReturnSaveResponse> {
const { type, ...body } = param;
const response = await stClient.post<ReviewReturnSaveResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/return`,
param,
`${ENDPOINT.ST_SERVICE.STATS}/reject`,
body,
{ params: { type } },
);
return response.data;
}
@@ -213,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;
@@ -35,11 +35,23 @@ export interface ApiResponse<T> {
message?: string;
}
/** 통계 컬럼 가이드(영문 컬럼명·한글명·유형·계산 여부·가이드 문구) 한 행 */
export interface StatisticsColumnGuideItem {
engColNm: string;
korColNm: string;
colType: string;
isCalcCol: string;
columnGuide: string;
}
/** 컬럼 가이드 정보 조회 응답 (`data`: 컬럼 가이드 배열) */
export type StatisticsColumnGuideListResponse = ApiResponse<StatisticsColumnGuideItem[]>;
/** 통계 입력 관리 등 — 연도·구분·분류(입력/검증)별 입력 기간 한 행 */
export interface StatsInputPeriodItem {
statsYr: string;
seCd: number;
seType: "INPT" | "VRFC";
seType: string;
inptBgngYmd: string;
inptEndYmd: string;
}
@@ -50,15 +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;
seCd: number;
seCd?: number;
/** 시작일 (백엔드 형식 문자열, 예: YYYYMMDD) */
inptBgngYmd: string;
/** 종료일 (백엔드 형식 문자열, 예: YYYYMMDD) */
@@ -69,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>;
@@ -215,6 +245,10 @@ export interface StatisticsLog {
statsDataChgComCd?: string;
rgtrId?: string;
regDt?: string;
ctpvNm?: string;
sggNm?: string;
statsDataChgComNm?: string;
statsYr?: string;
}
export interface StatisticsLogRequest {
@@ -300,6 +334,7 @@ export interface ReviewOpinionSaveResponse {
}
export interface ReviewReturnSaveRequest {
type: "REVIEW";
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
@@ -308,8 +343,10 @@ export interface ReviewReturnSaveRequest {
}
export interface ReviewReturnSaveResponse {
status: string;
message?: string;
exceptionCnt: number;
message: string;
resultCode: string;
updatedStts: string;
}
export interface ReviewSubmitRequest {
@@ -317,6 +354,8 @@ export interface ReviewSubmitRequest {
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
splmntOpnnCn: string;
wrterOpnnCn: string;
}
export interface ReviewSubmitResponse {
@@ -342,13 +381,31 @@ 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>;
getStatsInputPeriodList(params?: StatsInputPeriodListRequest): Promise<StatsInputPeriodListApiResponse>;
getStatsInputPeriodDetail(param: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse>;
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
@@ -363,4 +420,6 @@ export interface StatisticsService {
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
getImrStatus(param: string): Promise<ImrStatusResponse>;
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse>;
}
@@ -0,0 +1,92 @@
import type { ApiResponse } from "./statistics-interface";
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
export interface PublicSewageFacilityReviewItem {
ctpvNm: string;
sggNm: string;
statsYr: string;
admdstCd: string;
admdstNm: string;
swtrmtFcltNm: string;
displayYear: string;
rowType: "OLD" | "CURR";
fcltPstnCn: string | null;
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 | null;
wholHmnQnt: number;
wholLvstQnt: number;
wholEfntQnt: number;
wholEtcQnt: number;
telNo: string | null;
cmptnStrtYmd: string | null;
oprtngStrtYmd: string | null;
wholBizVl: number;
eflntDsnfMthdExpln: string | null;
wtsysNm: string | null;
trbtrNm: string | null;
dtlUnitZoneCn: string | null;
rgnSeNm: string | null;
msirbCn: string | null;
goalWqNo: string | null;
engySrlneRt: number;
engyItslfPrdctnQnt: number;
anlEngyWholUseQnt: number;
anlElpwrUseQnt: number;
itslfEngyUseQnt: number;
swtrmtEmsnQnt: number;
operMnbdCn: string | null;
prxyBzentyNmCn: string | null;
cnsgnCstVl: number;
prxyCtrtCn: string | null;
intgPrcsFcltCn: string | null;
wholNope: number;
sftyAcdntEvntcsCnt: number;
rmrk: string | null;
}
export interface PublicSewageFacilityReviewResponseData {
publicSewageList: PublicSewageFacilityReviewItem[];
statsStatusInfo: StatsStatusInfo;
}
export interface PublicSewageFacilityReviewRequestParams {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
swtrmtFcltNm?: string;
}
export type PublicSewageFacilityReviewApiResponse =
ApiResponse<PublicSewageFacilityReviewResponseData>;
export interface PublicSewageFacilityReviewService {
getPublicSewageFacilityReviewList(
params: PublicSewageFacilityReviewRequestParams,
): Promise<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;
+1
View File
@@ -70,6 +70,7 @@ export const ENDPOINT = {
COMMON: "/st-management-service/api/v1/common",
INPUT: "/st-input-service/api/v1/input",
REVIEW: "/st-management-service/api/v1/review",
STATS: "/st-management-service/api/stats",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats",