[st] feat: 입력-지난검증결과 수정 및 컬럼 툴팁 정보 추가, 검토 - 2,6 메뉴 작업중, 관리 - 입력관리 작업중

This commit is contained in:
2026-05-12 10:30:00 +09:00
parent b0a35d6832
commit 0fd2466f67
21 changed files with 1295 additions and 201 deletions
@@ -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 }
+161 -2
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({
@@ -917,7 +1015,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,
@@ -1334,6 +1432,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 +1527,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 +1612,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,
@@ -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" | "HR";
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" },
{ value: "HR", label: "HR 검증내역" },
];
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 [hrEntries, setHrEntries] = useState<VerificationResultReportItem[]>([]);
const [normalPageIndex, setNormalPageIndex] = useState(0);
const [imrPageIndex, setImrPageIndex] = useState(0);
const [hrPageIndex, setHrPageIndex] = useState(0);
const [normalPageCount, setNormalPageCount] = useState(1);
const [imrPageCount, setImrPageCount] = useState(1);
const [hrPageCount, setHrPageCount] = useState(1);
const [normalLoading, setNormalLoading] = useState(false);
const [imrLoading, setImrLoading] = useState(false);
const [hrLoading, setHrLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const resetState = useCallback(() => {
setActiveTab("NV");
setNormalEntries([]);
setImrEntries([]);
setHrEntries([]);
setNormalPageIndex(0);
setImrPageIndex(0);
setHrPageIndex(0);
setNormalPageCount(1);
setImrPageCount(1);
setHrPageCount(1);
setNormalLoading(false);
setImrLoading(false);
setHrLoading(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);
setHrLoading(true);
setNormalPageIndex(0);
setImrPageIndex(0);
setHrPageIndex(0);
try {
const [nvRes, imrRes] = await Promise.all([
const [nvRes, imrRes, hrRes] = await Promise.all([
fetchVerificationResultPage("NV", 0),
fetchVerificationResultPage("IMR", 0),
fetchVerificationResultPage("HR", 0),
]);
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
setImrEntries(imrRes?.data?.verifyResultList ?? []);
setHrEntries(hrRes?.data?.verifyResultList ?? []);
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
setHrPageCount(hrRes?.data?.pageInfo?.totalPages ?? 1);
} catch {
setNormalEntries([]);
setImrEntries([]);
setHrEntries([]);
setNormalPageCount(1);
setImrPageCount(1);
setHrPageCount(1);
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
} finally {
setNormalLoading(false);
setImrLoading(false);
setHrLoading(false);
}
}, [fetchVerificationResultPage, queryContext]);
@@ -187,6 +216,25 @@ export function VerificationResultHistoryDialog({
[fetchVerificationResultPage, imrPageIndex, queryContext],
);
const handleHrPageChange = useCallback(
async (pageIndex: number) => {
if (pageIndex === hrPageIndex || !queryContext) return;
setErrorMessage(null);
setHrLoading(true);
try {
const response = await fetchVerificationResultPage("HR", pageIndex);
setHrEntries(response?.data?.verifyResultList ?? []);
setHrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHrPageIndex(pageIndex);
} catch {
setErrorMessage("HR 검증내역 조회 중 오류가 발생했습니다.");
} finally {
setHrLoading(false);
}
},
[fetchVerificationResultPage, hrPageIndex, 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 hrRows = useMemo(() => mapRows(hrEntries), [hrEntries]);
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="HR" className="rounded-md border border-border">
{renderGrid(hrRows, hrPageIndex, hrPageCount, handleHrPageChange, hrLoading)}
</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';
@@ -131,6 +131,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 +153,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 +495,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 +819,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 +847,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 +998,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}
@@ -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,
@@ -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,
@@ -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);
@@ -119,6 +122,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 +154,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 +201,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 +220,7 @@ const SeweragePenetrationRateInp = () => {
return col;
}),
};
}, [isEnterable, isAdminInfo, footerFromApi]);
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]);
const handleReset = () => {
setRowData([]);
@@ -276,7 +318,7 @@ const SeweragePenetrationRateInp = () => {
if (!values) return;
postVerificationSearchRef.current = null;
void handleSearch(values);
}, [searchFormRemountKey]);
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
@@ -291,7 +333,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,
@@ -428,7 +470,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,
@@ -579,7 +621,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 +666,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,
@@ -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,7 +33,6 @@ function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC"
return "INPT";
}
/** API `YYYYMMDD` 등 → Date */
function parseYmdToDate(ymd: unknown): Date | undefined {
const s = String(ymd ?? "").replace(/\D/g, "");
if (s.length !== 8) return undefined;
@@ -71,10 +71,11 @@ const StatisticsInputManagement = () => {
setIsLoading(true);
const params: StatsInputPeriodListRequest = {
statsYr: values.year,
seType:
values.clsfCd && values.clsfCd !== "ALL"
? 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) => ({
@@ -112,6 +113,11 @@ const StatisticsInputManagement = () => {
};
const handleRowClick = (row: Record<string, string | number>) => {
const inptYmdSnRaw = row.inptYmdSn;
const inptYmdSnNum =
typeof inptYmdSnRaw === "number"
? inptYmdSnRaw
: parseInt(String(inptYmdSnRaw ?? ""), 10);
const seCdRaw = row.seCd;
const seCdNum =
typeof seCdRaw === "number"
@@ -121,6 +127,7 @@ const StatisticsInputManagement = () => {
setPeriodDialogInitial({
statsYr: String(row.statsYr ?? ""),
seType: rowSeTypeToCode(row),
inptYmdSn: Number.isFinite(inptYmdSnNum) ? inptYmdSnNum : 0,
seCd: Number.isFinite(seCdNum) ? seCdNum : 0,
inptBgngYmd: parseYmdToDate(row.inptBgngYmd),
inptEndYmd: parseYmdToDate(row.inptEndYmd),
@@ -159,6 +166,13 @@ const StatisticsInputManagement = () => {
header: { text: "종료일" },
editable: false,
},
{
name: "inptYmdSn",
fieldName: "inptYmdSn",
header: { text: "inptYmdSn" },
visible: false,
editable: false,
},
];
const columnLayout = [
@@ -33,6 +33,7 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap
export type StatisticsInputPeriodDialogValues = {
statsYr: string;
seType: "INPT" | "VRFC";
inptYmdSn: number;
seCd: number;
inptBgngYmd: Date | undefined;
inptEndYmd: Date | undefined;
@@ -137,15 +138,22 @@ export function StatisticsInputPeriodDialog({
try {
setIsSubmitting(true);
const userInfo = await getUserInfo();
const inptYmdSn =
mode === "create"
? 0
: (initial && Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0);
const param: StatsInputPeriodSaveRequest = {
statsYr: yr,
seType,
inptYmdSn,
seCd: seCdParsed,
inptBgngYmd: bgng,
inptEndYmd: end,
rgtrId: userInfo.userUnqId,
};
const res = await statisticsApi.saveStatsInputPeriod(param);
const res = isCreate
? await statisticsApi.saveStatsInputPeriod(param)
: await statisticsApi.updateStatsInputPeriod(param);
if (res.status === "success") {
alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`);
onOpenChange(false);
@@ -184,6 +192,8 @@ export function StatisticsInputPeriodDialog({
const param: StatsInputPeriodDeleteRequest = {
statsYr: yr,
seType: initial.seType,
inptYmdSn:
Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0,
seCd,
inptBgngYmd: bgng,
inptEndYmd: end,
@@ -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,
fctName: 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.status !== "success") {
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
return;
}
alert(response.message || "반려 처리 되었습니다.");
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)}
@@ -1,11 +1,15 @@
import { useMemo, useRef, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewagePipeReviewRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
@@ -13,6 +17,11 @@ 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 { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
@@ -70,9 +79,13 @@ function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
// 하수도 통계 검토 02.하수관로
const SewagePipeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm();
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 [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
/** 탭별 조회 응답의 statsStatusInfo — 활성 탭에 맞춰 버튼 노출 (SewagePipeInp의 tabStatusInfoMap과 동일 패턴) */
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
Map<string, StatsStatusInfo>
@@ -81,9 +94,23 @@ 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 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 { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const statsStatusInfo = tabStatsStatusInfoMap.get(activeTab) ?? {};
const statsStatusInfo = activeStatsStatusInfo;
const submit =
statsStatusInfo.submitStatusCd != null
? String(statsStatusInfo.submitStatusCd)
@@ -97,7 +124,7 @@ const SewagePipeRvw = () => {
showSubmitButton: reviewSubmitVisible,
showReturnButton: submitIs3 && prvn !== "3",
};
}, [tabStatsStatusInfoMap, activeTab]);
}, [activeStatsStatusInfo]);
const showActionBar =
showReviewOpinionButton || showReturnButton || showSubmitButton;
@@ -106,6 +133,9 @@ const SewagePipeRvw = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
setTabStatsStatusInfoMap(new Map());
setReviewOpinionOpen(false);
setReturnDialogOpen(false);
setLastSearchValues(null);
searchParamRef.current = null;
searchDoneRef.current = false;
lazyTabLoggedRef.current.clear();
@@ -119,6 +149,7 @@ const SewagePipeRvw = () => {
try {
setIsLoading(true);
setLastSearchValues(values);
const searchParam: SewagePipeReviewRequestParams = {
statsYr: values.year,
@@ -204,6 +235,103 @@ const SewagePipeRvw = () => {
}
};
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 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 +340,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>
@@ -283,6 +411,20 @@ const SewagePipeRvw = () => {
})}
</Tabs>
</div>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
);
};
@@ -211,36 +211,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);
}
};
@@ -288,6 +261,7 @@ const SeweragePenetrationRateRvw = () => {
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
context={reviewOpinionContext}
onSave={handleSaveReturnReason}
/>
</div>
+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 병합 설정
}
@@ -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"
}))
};
@@ -30,6 +30,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 +65,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)
@@ -18,6 +18,7 @@ import {
type ReviewSubmitResponse,
type SearchCondition,
type Sgg,
type StatisticsColumnGuideListResponse,
type StatisticsLogDetailResponse,
type StatisticsLogRequest,
type StatisticsLogResponse,
@@ -91,6 +92,17 @@ export class StatisticsApiService implements StatisticsService {
return response.data;
}
/** 통계 입력 기간 수정 — 경로는 저장과 동일, 메서드는 PUT */
public async updateStatsInputPeriod(
param: StatsInputPeriodSaveRequest,
): Promise<StatsInputPeriodSaveApiResponse> {
const response = await stClient.put<StatsInputPeriodSaveApiResponse>(
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info`,
param,
);
return response.data;
}
/** 통계 입력 기간 삭제 — JSON body. HTTP 메서드·URL은 백엔드 스펙에 맞게 수정 */
public async deleteStatsInputPeriod(
param: StatsInputPeriodDeleteRequest,
@@ -158,6 +170,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 +204,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;
}
@@ -35,6 +35,18 @@ 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;
@@ -42,6 +54,7 @@ export interface StatsInputPeriodItem {
seType: "INPT" | "VRFC";
inptBgngYmd: string;
inptEndYmd: string;
inptYmdSn: number | null;
}
/** `data`가 `StatsInputPeriodItem[]` 인 조회 응답 */
@@ -51,13 +64,14 @@ export type StatsInputPeriodListApiResponse = ApiResponse<StatsInputPeriodItem[]
export interface StatsInputPeriodListRequest {
statsYr?: string;
/** 전체 / 입력 / 검증 (SearchForm `clsfCd`와 동일) */
seType?: "ALL" | "INPT" | "VRFC";
seType?: "" | "ALL" | "INPT" | "VRFC";
}
/** 통계 입력 기간 등록·수정 등 요청 본문 */
export interface StatsInputPeriodSaveRequest {
statsYr: string;
seType: string;
inptYmdSn: number;
seCd: number;
/** 시작일 (백엔드 형식 문자열, 예: YYYYMMDD) */
inptBgngYmd: string;
@@ -215,6 +229,10 @@ export interface StatisticsLog {
statsDataChgComCd?: string;
rgtrId?: string;
regDt?: string;
ctpvNm?: string;
sggNm?: string;
statsDataChgComNm?: string;
statsYr?: string;
}
export interface StatisticsLogRequest {
@@ -300,6 +318,7 @@ export interface ReviewOpinionSaveResponse {
}
export interface ReviewReturnSaveRequest {
type: "REVIEW";
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
@@ -349,7 +368,10 @@ export interface StatisticsService {
getStatsInputPeriodList(
params?: StatsInputPeriodListRequest,
): Promise<StatsInputPeriodListApiResponse>;
/** 입력 기간 등록 (POST) */
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
/** 입력 기간 수정 (PUT) */
updateStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<ImrResponse>;
@@ -363,4 +385,6 @@ export interface StatisticsService {
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
getImrStatus(param: string): Promise<ImrStatusResponse>;
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
/** `INPUTCOMMON/column-guides` — 통계코드 등 단일 쿼리 문자열 */
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
}
@@ -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;
fctName?: string;
}
export type PublicSewageFacilityReviewApiResponse =
ApiResponse<PublicSewageFacilityReviewResponseData>;
export interface PublicSewageFacilityReviewService {
getPublicSewageFacilityReviewList(
params: PublicSewageFacilityReviewRequestParams,
): Promise<PublicSewageFacilityReviewApiResponse>;
}
+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",