[st] fix: 로그 조회, SearchForm, DataGrid 수정

This commit is contained in:
2026-04-20 10:39:25 +09:00
parent 4a26dae6b4
commit c950652f34
28 changed files with 1742 additions and 302 deletions
@@ -1,7 +1,7 @@
import { Button } from "@common/components";
interface ReviewOpinionButtonProps {
reviewOpinionFunc: () => void;
reviewOpinionFunc: () => void | Promise<void>;
}
const ReviewOpinionButton = ({ reviewOpinionFunc }: ReviewOpinionButtonProps) => {
+6 -1
View File
@@ -311,6 +311,7 @@ function buildFieldsFromColumns(
const DataGrid = ({
columnInfo,
rowStartNum,
excelExportFileSuffix,
isExcelImport = true,
isExcelExport = true,
fixedColumns,
@@ -470,10 +471,14 @@ const DataGrid = ({
gridView.setColumnLayout?.(normalizedLayout);
}
const exportBaseName = excelExportFileSuffix
? `${menuName}_${excelExportFileSuffix}`
: menuName;
gridView.exportGrid({
type: "excel",
target: "local",
fileName: `${menuName}.xlsx`,
fileName: `${exportBaseName}.xlsx`,
footer: "hidden",
allColumns: true, // 숨김(visible: false) 컬럼도 엑셀에 포함
done: () => {
+18 -10
View File
@@ -16,28 +16,36 @@ import {
}
from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { type SearchFormProps, type SearchFormValues, defaultSearchFormProps } from "../type/SearchForm.type";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type { ApiResponse, SearchCondition, Sgg } from "@/entities/statistics/model/statistics-interface";
import { doList } from "../util/column/input/DoList";
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
const getSidoListFetcher = async (key: [string, string]): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList({ instSeCd: key[1] });
};
const getSggListFetcher = async (key: [string, string]): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggList(key[1]);
const getSggListFetcher = async (key: [string, string, string, string]): Promise<ApiResponse<Sgg[]>> => {
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
return statisticsAPi.getSggAllList(key[1], { instSeCd: key[2], gugunSkipYn });
};
const SearchForm = (props: SearchFormProps = {}) => {
const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const mergedProps = { ...defaultSearchFormProps, ...props };
const instSeCd = userInfo?.instSeCd ?? "";
const sidoSelectText =
props.sidoSelectText && props.sidoSelectText.trim() !== ""
? props.sidoSelectText
: "선택";
const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1;
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"sido-list",
["sido-list", instSeCd],
getSidoListFetcher,
{
revalidateOnFocus: false,
@@ -68,9 +76,9 @@ const SearchForm = (props: SearchFormProps = {}) => {
isDetail: false,
});
const sggKey: [string, string] | null =
const sggKey: [string, string, string, string] | null =
values.sido && values.sido !== "ALL"
? ["sgg-list", values.sido]
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""]
: null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey,
@@ -152,10 +160,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
</Label>
<Select value={values.sido} onValueChange={handleSidoChange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택"></SelectValue>
<SelectValue placeholder={sidoSelectText}></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">선택</SelectItem>
<SelectItem value="ALL">{sidoSelectText}</SelectItem>
{sidoList?.data?.sidoList?.map((sido) => (
<SelectItem key={sido.adstDstrCd} value={sido.sido}>
{sido.sido}
@@ -1,14 +1,16 @@
import { useState, useMemo, useRef } from "react";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import DataGrid from "@/components/module/DataGrid";
import SplitDataGrid from "@/components/module/SplitDataGrid";
import GridTopActionButton from "@/components/module/GridTopActionButton";
import SubmitButton from "@/components/button/SumitButton";
import SaveButton from "@/components/button/SaveButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { ColumnInfo, ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { useAlertStore } from "@/shared/lib";
import { useAdminStore } from "@/store/admin-store";
@@ -17,20 +19,13 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { doList } from "@/components/util/column/input/DoList";
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
import type {
SeweragePipeInputAdminAreaConnectionItem,
SeweragePipeInputFacilityConnectionItem,
SeweragePipeInputFactInstallStatusItem,
SeweragePipeInputManholeStatusItem,
SeweragePipeInputPipeDiameterStatusItem,
SeweragePipeInputPipeTypeStatusItem,
SeweragePipeInputRegionalFactStatusItem,
SeweragePipeInputRegionalFactStatusSaveRequest,
SeweragePipeInputStatusInfo,
SeweragePipeRequestParams,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { LoadingWithAnimation } from "@common/components";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
const applyEnterableFromStatus = (
@@ -62,6 +57,23 @@ function buildSewagePipeSearchParam(
};
}
/** ST00020006(연결)은 행이 좌·우 그리드 키에만 있음 */
function tabRowDataExists(
m: Map<string, Record<string, string | number>[]>,
tabKey: string,
): boolean {
if (tabKey === "ST00020006") {
const left = m.get("treatmentPlantConnectionInp");
const right = m.get("adminAreaConnectionInp");
return (
(Array.isArray(left) && left.length > 0) ||
(Array.isArray(right) && right.length > 0)
);
}
const rows = m.get(tabKey);
return Array.isArray(rows) && rows.length > 0;
}
// 하수도 통계 관리 > 02. 하수관로
const SewagePipeInp = () => {
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
@@ -128,7 +140,7 @@ const SewagePipeInp = () => {
});
}, [isEnterable, isAdminInfo]);
/** mergeIntoExistingMap: true면 지난연도(PRE) 등 — 현재 탭만 갱신, 나머지 탭 캐시 유지 */
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
const loadSewagePipeActiveTabData = async (
tabKey: string,
searchParam: SeweragePipeRequestParams,
@@ -290,7 +302,6 @@ const SewagePipeInp = () => {
const checkEnterable = periodResponse.data;
periodEnterableRef.current = checkEnterable;
setRowDataMap(new Map());
lazyTabLoggedRef.current.clear();
setIsLoading(true);
@@ -301,7 +312,7 @@ const SewagePipeInp = () => {
setHasSearched(true);
setLastSearchedYear(values.year ?? null);
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, false, true);
await loadSewagePipeActiveTabData(activeTab, searchParam, values, checkEnterable, true, true);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
@@ -312,8 +323,7 @@ const SewagePipeInp = () => {
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const activeTabData = rowDataMap.get(tabKey);
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
const hasActiveTabData = tabRowDataExists(rowDataMap, tabKey);
const canLazyLoad =
searchParamRef.current &&
@@ -324,86 +334,20 @@ const SewagePipeInp = () => {
if (!canLazyLoad) return;
const searchParam = searchParamRef.current;
if (!searchParam) return;
const values = lastSearchedValues ?? searchValues;
if (!searchParam || !values) return;
try {
setIsLoading(true);
let response;
if (tabKey === 'ST00020001') {
response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam);
} else if (tabKey === 'ST00020002') {
response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam);
} else if (tabKey === 'ST00020003') {
response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam);
} else if (tabKey === 'ST00020004') {
response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam);
} else if (tabKey === 'ST00020005') {
response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam);
} else if (tabKey === 'ST00020006') {
response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam);
}
lazyTabLoggedRef.current.add(tabKey);
if (response?.status === 'success') {
setStatusInfo(response.data.statusInfo);
setStatsCd(response.data.commonInfo.statsCd);
applyEnterableFromStatus(response.data.statusInfo, periodEnterableRef.current, setIsEnterable);
setRowDataMap(prev => {
const next = new Map(prev);
if (tabKey === 'ST00020001') {
const commonInfo = response.data.commonInfo;
const convertedData = (response.data.regionalFactStatusList ?? []).map((d: SeweragePipeInputRegionalFactStatusItem) => ({
ctpvNm: commonInfo.ctpvNm,
...d
}));
next.set(tabKey, toGridRows(convertedData));
} else if (tabKey === 'ST00020002') {
const commonInfo = response.data.commonInfo;
const convertedData = (response.data.factInstallStatusList ?? []).map((d: SeweragePipeInputFactInstallStatusItem) => ({
ctpvNm: commonInfo.ctpvNm,
...d
}));
next.set(tabKey, toGridRows(convertedData));
} else if (tabKey === 'ST00020003') {
const commonInfo = response.data.commonInfo;
const convertedData = (response.data.pipeTypeStatusList ?? []).map((d: SeweragePipeInputPipeTypeStatusItem) => ({
ctpvNm: commonInfo.ctpvNm,
...d
}));
next.set(tabKey, toGridRows(convertedData));
} else if (tabKey === 'ST00020004') {
const commonInfo = response.data.commonInfo;
const convertedData = (response.data.pipeDiameterStatusList ?? []).map((d: SeweragePipeInputPipeDiameterStatusItem) => ({
ctpvNm: commonInfo.ctpvNm,
...d
}));
next.set(tabKey, toGridRows(convertedData));
} else if (tabKey === 'ST00020005') {
const commonInfo = response.data.commonInfo;
const convertedData = (response.data.manholeStatusList ?? []).map((d: SeweragePipeInputManholeStatusItem) => ({
ctpvNm: commonInfo.ctpvNm,
...d
}));
next.set(tabKey, toGridRows(convertedData));
} else if (tabKey === 'ST00020006') {
const commonInfo = response.data.commonInfo;
const facilityConvertedData = (response.data.facilityConnectionList ?? []).map((d: SeweragePipeInputFacilityConnectionItem) => ({
...d
}));
const adminConvertedData = (response.data.adminAreaConnectionList ?? []).map((d: SeweragePipeInputAdminAreaConnectionItem) => ({
...d,
ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
}));
next.set('treatmentPlantConnectionInp', toGridRows(facilityConvertedData));
next.set('adminAreaConnectionInp', toGridRows(adminConvertedData));
}
return next;
});
} else {
alert(response?.message || '데이터 조회 중 오류가 발생했습니다.');
}
const ok = await loadSewagePipeActiveTabData(
tabKey,
searchParam,
values,
periodEnterableRef.current,
true,
false,
);
if (ok) lazyTabLoggedRef.current.add(tabKey);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
@@ -543,8 +487,9 @@ const SewagePipeInp = () => {
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
/** TabDataGrid는 rowDataMap 사용 — 행이 하나라도 있을 때만 툴바 카드 의미 있음 */
/** rowDataMap — 행이 하나라도 있을 때만 툴바·엑셀 의미 있음 */
const hasRowData = Array.from(rowDataMap.values()).some((rows) => rows.length > 0);
const excelToolbarEnabled = hasRowData && isEnterable && gugunOkOrNotDoList;
const showActionToolbarCard =
(hasRowData || hasSearched) &&
@@ -573,16 +518,132 @@ const SewagePipeInp = () => {
<LoadingWithAnimation color="primary" />
</div>
</div>}
<TabDataGrid
columnInfo={columnInfo}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
isExcelImport={hasRowData && isEnterable && gugunOkOrNotDoList}
facilitySearchStatsYr={lastSearchedYear}
getGridDataRef={getGridDataRef}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (info.isSplitGrid && info.left && info.right) {
const leftKey = info.left.tabKey || `${tabKey}_left`;
const rightKey = info.right.tabKey || `${tabKey}_right`;
const leftRowData = rowDataMap.get(leftKey) ?? [];
const rightRowData = rowDataMap.get(rightKey) ?? [];
const splitTopButton = info.right.topButton;
const showSplitTopButton =
Boolean(splitTopButton?.text) &&
(splitTopButton?.visible ?? true) &&
excelToolbarEnabled;
const leftColumnInfo: ColumnInfo = {
columns: info.left.columns ?? [],
columnLayout: info.left.columnLayout ?? [],
};
const rightColumnInfo: ColumnInfo = {
columns: info.right.columns ?? [],
columnLayout: info.right.columnLayout ?? [],
};
return (
<TabsContent key={tabKey} value={tabKey}>
<div className="flex flex-col gap-[0.6rem]">
<div className="flex justify-end gap-[0.6rem]">
{showSplitTopButton && (
<GridTopActionButton
text={splitTopButton?.text ?? ""}
onClick={splitTopButton?.onClick}
statsYr={lastSearchedYear ?? undefined}
/>
)}
</div>
<SplitDataGrid
leftColumnInfo={leftColumnInfo}
rightColumnInfo={rightColumnInfo}
leftFixedColumns={info.left.fixedColumns}
rightFixedColumns={info.right.fixedColumns}
showHeaderCheckbox={false}
enableSort={false}
leftRowData={leftRowData}
rightRowData={rightRowData}
setLeftRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(leftKey) ?? [];
const n =
typeof updater === "function"
? updater(cur)
: updater;
next.set(leftKey, n);
return next;
});
}}
setRightRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(rightKey) ?? [];
const n =
typeof updater === "function"
? updater(cur)
: updater;
next.set(rightKey, n);
return next;
});
}}
/>
</div>
</TabsContent>
);
}
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={excelToolbarEnabled}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
getGridDataRef={tabKey === "ST00020001" ? getGridDataRef : undefined}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</>
);
};
@@ -465,22 +465,22 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
if (response.status === 'success') {
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
stats_cd: statsCd,
stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'),
};
// const imrParam = {
// user_id: userInfo.userUnqId,
// sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
// stats_cd: statsCd,
// stats_yr: searchValues?.year ?? '',
// lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'),
// };
try {
await statisticsAPi.callImrService(imrParam);
} catch {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
setIsLoading(false);
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
return;
}
// try {
// await statisticsAPi.callImrService(imrParam);
// } catch {
// await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
// setIsLoading(false);
// alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
// return;
// }
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
alert('저장되었습니다.');
@@ -1,28 +1,57 @@
"use client";
import { useCallback, useState, useEffect } from 'react';
import { useCallback, useState, useEffect } from "react";
import useSWR from "swr";
import dayjs from "dayjs";
import {
Button,
InputDatePicker,
Label,
LoadingWithAnimation,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import DataGrid from '@/components/module/DataGrid';
import { CodeSelectWithAll } from '@/entities/code';
import DataGrid from "@/components/module/DataGrid";
import { CodeSelectWithAll } from "@/entities/code";
import { statisticsAPi } from '@/entities/statistics/api/statistics-service';
import type { StatisticsLogRequest, StatisticsLog } from '@/entities/statistics/model/statistics-interface';
import { LogHistoryDetailDialog } from '@/components/statistics/management/LogHistoryDetailDialog';
import { toGridRows } from '@/components/util/grid/gridFunction';
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type {
ApiResponse,
SearchCondition,
Sgg,
StatisticsLog,
StatisticsLogRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LogHistoryDetailDialog } from "@/components/statistics/management/LogHistoryDetailDialog";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAlertStore } from "@/shared/lib";
import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn';
import { LogHistoryTablePagination } from '@/components/statistics/management/LogHistoryTablePagination';
import { LogHistoryColumn } from "@/components/util/column/management/LogHistoryColumn";
import { LogHistoryTablePagination } from "@/components/statistics/management/LogHistoryTablePagination";
const PAGE_SIZE = 100;
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
};
const getSggListFetcher = async (
key: [string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggList(key[1]);
};
const LogHistory = () => {
// const { setMessage: alert } = useAlertStore((state) => state);
const [statsCd, setStatsCd] = useState<string>('');
const { setMessage: alert } = useAlertStore((state) => state);
const [statsCd, setStatsCd] = useState<string>("");
const [sido, setSido] = useState<string>("ALL");
const [gugun, setGugun] = useState<string>("ALL");
const [bsnmCd, setBsnmCd] = useState<string>("");
const [statsDataChgComCd, setStatsDataChgComCd] = useState<string>("ALL");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [pageIndex, setPageIndex] = useState(0);
@@ -32,44 +61,141 @@ const LogHistory = () => {
string,
string | number
> | null>(null);
const [startDate, setStartDate] = useState<Date | undefined>(() =>
dayjs().subtract(1, "month").startOf("day").toDate(),
);
const [endDate, setEndDate] = useState<Date | undefined>(() =>
dayjs().endOf("day").toDate(),
);
const loadLogs = useCallback(async (page1Based: number) => {
const param: StatisticsLogRequest = {
statsCd: (statsCd === '' || statsCd === 'ALL') ? '' : statsCd,
page: page1Based,
size: PAGE_SIZE,
};
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
"log-history-sido-list",
getSidoListFetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
dedupingInterval: 2000,
shouldRetryOnError: false,
onSuccess: (data) => {
if (data.status !== "success") {
alert(`시도 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
}
},
onError: () => {
alert(`시도 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
},
},
);
try {
setIsLoading(true);
const response = await statisticsAPi.getLogList(param);
if (response.status === 'success') {
const lostList = response.data?.statsLogList ?? [];
const convertedData: StatisticsLog[] = lostList.map((item: StatisticsLog) => ({
...item,
dmndFwkNm: item.dmndFwkNm === 'save' ? '저장' : (item.dmndFwkNm === 'submit' ? '제출' : '예외 제출'),
}));
setRowData(toGridRows(convertedData));
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
setPageIndex(Math.max(0, page1Based - 1));
} else {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
const sggKey: [string, string] | null =
sido && sido !== "ALL" ? ["log-history-sgg-list", sido] : null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(sggKey, getSggListFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateIfStale: false,
dedupingInterval: 2000,
shouldRetryOnError: false,
onSuccess: (data) => {
if (data.status !== "success") {
alert(`시군구 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}, [statsCd]);
},
onError: () => {
alert(`시군구 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
},
});
const sggOptions = sggList?.data ?? [];
const handleSidoChange = (value: string) => {
const selectedBsnm =
sidoList?.data.sidoList.find((v) => v.sido === value)?.bsnmCd ?? "";
setSido(value);
setGugun("ALL");
setBsnmCd(selectedBsnm);
};
const handleSggChange = (value: string) => {
const listForBsnm = sggList?.data ?? [];
const selectedBsnm =
value === "ALL"
? (sidoList?.data.sidoList.find((v) => v.sido === sido)?.bsnmCd ??
"")
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? "");
setGugun(value);
setBsnmCd(selectedBsnm);
};
const loadLogs = useCallback(
async (page1Based: number) => {
const param: StatisticsLogRequest = {
page: page1Based,
size: PAGE_SIZE,
startDate: startDate
? dayjs(startDate).format("YYYY-MM-DD")
: "",
endDate: endDate ? dayjs(endDate).format("YYYY-MM-DD") : "",
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
...(sido && sido !== "ALL" && { ctpvNm: sido }),
...(gugun && gugun !== "ALL" && { sggNm: gugun }),
...(bsnmCd && { sewerBzmnCd: bsnmCd }),
...(statsDataChgComCd &&
statsDataChgComCd !== "ALL" && {
statsDataChgComCd,
}),
};
try {
setIsLoading(true);
const response = await statisticsAPi.getLogList(param);
if (response.status === "success") {
const lostList = response.data?.statsLogList ?? [];
setRowData(toGridRows(lostList));
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
setPageIndex(Math.max(0, page1Based - 1));
} else {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
},
[
statsCd,
startDate,
endDate,
sido,
gugun,
bsnmCd,
statsDataChgComCd,
alert,
],
);
const handleSearch = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await loadLogs(1);
};
const handleReset = () => {
setStatsCd("");
setSido("ALL");
setGugun("ALL");
setBsnmCd("");
setStatsDataChgComCd("ALL");
setStartDate(dayjs().subtract(1, "month").startOf("day").toDate());
setEndDate(dayjs().endOf("day").toDate());
};
const handleRowClick = (row: Record<string, string | number>) => {
setSelectedLogRow(row);
setDetailDialogOpen(true);
@@ -81,18 +207,54 @@ const LogHistory = () => {
};
useEffect(() => {
loadLogs(1);
void loadLogs(1);
}, []);
return (
<div className="relative">
<div className="card card-border">
<form onSubmit={handleSearch}>
<div className="flex items-center gap-8 p-2">
<div className="flex flex-wrap items-center gap-8 p-2">
<div className="flex items-center gap-2">
<Label className="text-label">
통계항목
</Label>
<Label className="text-label">시도</Label>
<Select value={sido} onValueChange={handleSidoChange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
{sidoList?.data?.sidoList?.map((item) => (
<SelectItem
key={item.adstDstrCd}
value={item.sido}
>
{item.sido}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-label">시군구</Label>
<Select value={gugun} onValueChange={handleSggChange}>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
{sggOptions.map((sgg) => (
<SelectItem
key={sgg.adstDstrCd}
value={sgg.gugun}
>
{sgg.gugun}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-label">통계항목</Label>
<CodeSelectWithAll
upCd="STITEM"
value={statsCd}
@@ -101,8 +263,37 @@ const LogHistory = () => {
className="w-[16rem]"
/>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button color="green" className="h-[3.2rem] w-[96px]" type="button">
<div className="flex items-center gap-2">
<Label className="text-label">요청 기능</Label>
<CodeSelectWithAll
upCd="STATSDATACHG"
value={statsDataChgComCd}
onValueChange={setStatsDataChgComCd}
placeholder="전체"
className="w-[16rem]"
/>
</div>
<div className="flex items-center gap-2">
<Label className="mx-2 text-label">시작일</Label>
<InputDatePicker
value={startDate}
onChange={setStartDate}
className="w-[14rem]"
/>
<Label className="mx-2 text-label">종료일</Label>
<InputDatePicker
value={endDate}
onChange={setEndDate}
className="w-[14rem]"
/>
</div>
<div className="ml-auto flex items-center gap-2">
<Button
color="green"
className="h-[3.2rem] w-[96px]"
type="button"
onClick={handleReset}
>
초기화
</Button>
<Button className="h-[3.2rem] w-[96px]" type="submit">
@@ -112,11 +303,13 @@ const LogHistory = () => {
</div>
</form>
</div>
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
{isLoading && (
<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>}
)}
<DataGrid
columnInfo={LogHistoryColumn}
isExcelImport={false}
@@ -140,4 +333,4 @@ const LogHistory = () => {
);
};
export default LogHistory;
export default LogHistory;
@@ -0,0 +1,99 @@
"use client";
import { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Label,
Textarea,
} from "@common/components";
export interface ReturnReasonDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave?: (reason: string) => void | Promise<void>;
}
export function ReturnReasonDialog({
open,
onOpenChange,
onSave,
}: ReturnReasonDialogProps) {
const [reason, setReason] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [reasonError, setReasonError] = useState(false);
useEffect(() => {
if (!open) return;
setReason("");
setReasonError(false);
}, [open]);
const handleSave = async () => {
if (reason.trim() === "") {
setReasonError(true);
return;
}
try {
setIsSaving(true);
await onSave?.(reason);
onOpenChange(false);
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>반려 사유</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">반려 사유</Label>
<Textarea
value={reason}
onChange={(e) => {
setReason(e.target.value);
if (reasonError) {
setReasonError(false);
}
}}
className="min-h-[12rem] resize-y !text-[1.3rem] !leading-6"
placeholder="반려 사유를 입력하세요."
disabled={isSaving}
/>
{reasonError && (
<p className="text-[1.2rem] text-red-500">
반려 사유를 작성해주세요.
</p>
)}
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSaving}>
취소
</Button>
</DialogClose>
<Button
type="button"
color="primary"
disabled={isSaving}
onClick={() => void handleSave()}
>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,168 @@
"use client";
import { useEffect, useState } from "react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Label,
Textarea,
} from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { useAlertStore } from "@/shared/lib";
export type ReviewOpinionDialogContext = Pick<
ReviewOpinionSaveRequest,
"statsYr" | "sewerBzmnCd" | "statsCd"
>;
export interface ReviewOpinionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
/** 조회 성공 후의 통계연도·사업자코드·통계코드 (없으면 저장 불가) */
context: ReviewOpinionDialogContext | null;
/** 다이얼로그 열릴 때 검토의견 초기값 */
initialReviewOpinion?: string;
/** 다이얼로그 열릴 때 처리내역 초기값 */
initialProcessHistory?: string;
/** 저장 시 생성된 param 전달 (API 호출 등) */
onSave?: (param: ReviewOpinionSaveRequest) => void | Promise<void>;
}
export function ReviewOpinionDialog({
open,
onOpenChange,
context,
initialReviewOpinion = "",
initialProcessHistory = "",
onSave,
}: ReviewOpinionDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state);
const [reviewOpinion, setReviewOpinion] = useState("");
const [processHistory, setProcessHistory] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [reviewOpinionError, setReviewOpinionError] = useState(false);
const resetForm = () => {
setReviewOpinion("");
setProcessHistory("");
setReviewOpinionError(false);
};
const handleDialogOpenChange = (nextOpen: boolean) => {
if (!nextOpen) resetForm();
onOpenChange(nextOpen);
};
useEffect(() => {
if (!open) return;
setReviewOpinion(initialReviewOpinion);
setProcessHistory(initialProcessHistory);
}, [open, initialReviewOpinion, initialProcessHistory]);
const handleSave = async () => {
if (reviewOpinion.trim() === "") {
setReviewOpinionError(true);
return;
}
if (
context == null ||
context.statsYr === "" ||
context.sewerBzmnCd === ""
) {
alert("시도·시군구·년도로 조회한 뒤 검토의견을 저장할 수 있습니다.");
return;
}
try {
setIsSaving(true);
let userInfo: Awaited<ReturnType<typeof getUserInfo>>;
try {
userInfo = await getUserInfo();
} catch {
alert("사용자 정보를 불러오지 못했습니다. 다시 시도해주세요.");
return;
}
const param: ReviewOpinionSaveRequest = {
statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
splmntOpnnCn: reviewOpinion,
wrterOpnnCn: processHistory,
mdfrId: userInfo.userUnqId,
};
await onSave?.(param);
resetForm();
onOpenChange(false);
} catch {
/* 저장 실패 시 부모에서 alert 후 throw — 다이얼로그는 유지 */
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>검토의견작성</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 py-2">
<div className="flex flex-col gap-2">
<Label className="text-label">검토의견</Label>
<Textarea
value={reviewOpinion}
onChange={(e) => {
setReviewOpinion(e.target.value);
if (reviewOpinionError) {
setReviewOpinionError(false);
}
}}
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
placeholder="검토의견을 입력하세요."
disabled={isSaving}
/>
{reviewOpinionError && (
<p className="text-[1.2rem] text-red-500">
검토의견을 작성해주세요.
</p>
)}
</div>
<div className="flex flex-col gap-2">
<Label className="text-label">처리내역</Label>
<Textarea
value={processHistory}
onChange={(e) => setProcessHistory(e.target.value)}
className="min-h-[10rem] resize-y !text-[1.3rem] !leading-6"
placeholder="처리내역을 입력하세요."
disabled={isSaving}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray" disabled={isSaving}>
취소
</Button>
</DialogClose>
<Button
type="button"
color="primary"
disabled={isSaving}
onClick={() => void handleSave()}
>
저장
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -1,17 +1,17 @@
import { useMemo, useRef,useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SewerPipeMtcRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
import { LoadingWithAnimation } from "@common/components";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SaveButton from "@/components/button/SaveButton";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import type { ColumnConfig,ColumnInfoList } from "@/components/type/DataGrid.type";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
@@ -79,8 +79,8 @@ const SewagePipeMaintainRvw = () => {
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
alert('시도를 선택해주세요.');
if (!values.sido || values.sido === "ALL") {
alert("시도를 선택해주세요.");
return;
}
@@ -91,26 +91,27 @@ const SewagePipeMaintainRvw = () => {
statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun
sggNm: values.gugun === "ALL" ? "" : values.gugun,
};
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoggedRef.current.clear();
setRowDataMap(new Map());
const response = await fetchSewagePipeMaintainReviewTab(activeTab, searchParam);
if (response?.status === 'success' && response.data) {
if (response?.status === "success" && response.data) {
const listData = getListDataByTab(activeTab, response.data);
setRowDataMap(
new Map([[activeTab, toGridRows(listData)]]),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(listData));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
@@ -177,14 +178,60 @@ const SewagePipeMaintainRvw = () => {
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
);
};
@@ -1,17 +1,17 @@
import { useMemo, useRef,useState } from "react";
import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SewagePipeReviewRequestParams } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { LoadingWithAnimation } from "@common/components";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SaveButton from "@/components/button/SaveButton";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import type { ColumnConfig,ColumnInfoList } from "@/components/type/DataGrid.type";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } 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";
@@ -48,17 +48,6 @@ const SewagePipeRvw = () => {
const searchDoneRef = useRef(false);
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
const columnInfo = useMemo(() => {
return SewagePipeColumnInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
}));
}, []);
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(firstTabKey);
@@ -86,14 +75,15 @@ const SewagePipeRvw = () => {
searchParamRef.current = searchParam;
searchDoneRef.current = true;
lazyTabLoggedRef.current.clear();
setRowDataMap(new Map());
const response = await fetchSewagePipeReviewTab(activeTab, searchParam);
if (response?.status === "success" && response.data) {
setRowDataMap(
new Map([[activeTab, toGridRows(response.data as object[])]]),
);
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(activeTab, toGridRows(response.data as object[]));
return next;
});
} else {
alert(response?.message || "데이터 조회 중 오류가 발생했습니다.");
}
@@ -163,14 +153,60 @@ const SewagePipeRvw = () => {
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={(v) => void handleActiveTabChange(v)}
className="w-full"
variant="default"
>
<TabsList>
{SewagePipeColumnInfoListRvw.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{SewagePipeColumnInfoListRvw.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
);
};
@@ -5,13 +5,22 @@ import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
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 { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
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 { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import {
ReviewOpinionDialog,
type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog";
// 하수도 통계 검토 01.하수도 보급률
const SeweragePenetrationRateRvw = () => {
@@ -19,6 +28,14 @@ const SeweragePenetrationRateRvw = () => {
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 { confirm } = useConfirm();
const columnInfo = useMemo(() => {
const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList;
@@ -35,14 +52,14 @@ const SeweragePenetrationRateRvw = () => {
}, []);
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
const lae = statsStatusInfo.laeSbmsnSttsCd != null ? String(statsStatusInfo.laeSbmsnSttsCd) : "";
const pic = statsStatusInfo.statPicSbmsnSttsCd != null ? String(statsStatusInfo.statPicSbmsnSttsCd) : "";
const laeIs3 = lae === "3";
const reviewSubmitVisible = laeIs3 && (pic === "0" || pic === "1");
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: laeIs3 && pic !== "3",
showReturnButton: submitIs3 && prvn !== "3",
};
}, [statsStatusInfo]);
@@ -52,6 +69,10 @@ const SeweragePenetrationRateRvw = () => {
const handleReset = () => {
setRowData([]);
setStatsStatusInfo({});
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
setLastSearchValues(null);
};
const handleSearch = async (values: SearchFormValues) => {
@@ -60,6 +81,7 @@ const SeweragePenetrationRateRvw = () => {
alert('시도를 선택해주세요.');
return;
}
setLastSearchValues(values);
try {
setIsLoading(true);
@@ -80,9 +102,23 @@ const SeweragePenetrationRateRvw = () => {
}));
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));
} else {
setRowData([]);
setReviewOpinionContext(null);
setLatestReviewOpinion("");
setLatestProcessHistory("");
alert(response.message || "데이터가 없습니다.");
}
} catch (error) {
@@ -93,11 +129,120 @@ const SeweragePenetrationRateRvw = () => {
}
};
const submitFunc = () => console.log('submit');
const submitFunc = async () => {
if (reviewOpinionContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
if (!(await confirm("제출 하시겠습니까?"))) {
return;
}
const returnFunc = () => console.log('returnFunc');
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];
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 (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("반려 처리 중 오류가 발생했습니다.");
}
}
};
return (
<div className="relative">
@@ -131,6 +276,20 @@ const SeweragePenetrationRateRvw = () => {
rowData={rowData}
setRowData={setRowData}
/>
<ReviewOpinionDialog
open={reviewOpinionOpen}
onOpenChange={setReviewOpinionOpen}
context={reviewOpinionContext}
initialReviewOpinion={latestReviewOpinion}
initialProcessHistory={latestProcessHistory}
onSave={saveReviewOpinion}
/>
<ReturnReasonDialog
open={returnDialogOpen}
onOpenChange={setReturnDialogOpen}
onSave={handleSaveReturnReason}
/>
</div>
);
};
@@ -1,23 +1,54 @@
import { useState, useMemo } from "react";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
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 ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { LoadingWithAnimation } from "@common/components";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
LoadingWithAnimation,
DataTable
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { ReviewHistoryResponse } from "@/entities/statistics/model/statistics-input-review-interface";
import { toGridRows } from "@/components/util/grid/gridFunction";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
const checkPrvnStatusCd = (code: string) => {
return (code === '0' || code === '1') ? '미작성' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '반려'));
};
const checkSubmitStatusCd = (code: string) => {
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
};
// 하수도 통계 검토 00. 통계 입력 현황
const StatsInputStatusRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [searchedYear, setSearchedYear] = useState<string>("");
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
const [historySearchInfo, setHistorySearchInfo] = useState<{
ctpvNm: string;
sggNm: string;
tabNm: string;
}>({
ctpvNm: '',
sggNm: '',
tabNm: '',
});
const columnInfo = useMemo(() => {
const { columns, columnLayout } = StatsInputStatusInfoList;
@@ -30,8 +61,30 @@ const StatsInputStatusRvw = () => {
};
}, []);
const reviewHistoryColumns = useMemo(() => {
const col = createColumnHelper<ReviewHistoryRow>();
return [
col.accessor("logDt", {
header: "등록일",
cell: (info) => info.getValue(),
}),
col.accessor("historyContent", {
header: "내용",
cell: (info) => info.getValue(),
}),
];
}, []);
const historyTable = useReactTable({
data: historyRowData,
columns: reviewHistoryColumns,
getCoreRowModel: getCoreRowModel(),
getRowId: (_row, index) => String(index),
});
const handleReset = () => {
setRowData([]);
setSearchedYear("");
};
const handleSearch = async (values: SearchFormValues) => {
@@ -42,24 +95,29 @@ const StatsInputStatusRvw = () => {
try {
setIsLoading(true);
setSearchedYear(values.year ?? "");
const response = await statisticsManagementApi.getStatsInputStatusList({
ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
statsYr: values.year,
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
statsYr: values.year,
sewerBzmnCd: values.bsnmCd
});
if (response.status === 'success' && response.data) {
const rawList = response.data.sewerageInputList ?? [];
const convertedData = rawList.map((r) => ({
...r,
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || '0'),
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || '0')
}));
setRowData(toGridRows(rawList));
setRowData(toGridRows(convertedData));
} else {
setRowData([]);
alert(response.message || "조회된 데이터가 없습니다.");
}
} catch (error) {
console.error("입력현황 조회 중 에러 발생:", error);
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
setRowData([]);
} finally {
@@ -68,20 +126,40 @@ const StatsInputStatusRvw = () => {
};
const approveFunc = () => console.log('approveFunc');
const saveFunc = () => console.log('submit');
const reviewOpinionFunc = () => console.log('reviewOpinionFunc');
const returnFunc = () => console.log('returnFunc');
const handleRowClick = async (row: Record<string, string | number>) => {
setHistorySearchInfo({
ctpvNm: String(row.ctpvNm ?? ""),
sggNm: String(row.sggNm ?? ""),
tabNm: String(row.tabNm ?? ""),
});
const params = {
statsYr: searchedYear,
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
statsCd: String(row.statsCd ?? ""),
};
try {
const response = await statisticsManagementApi.getReviewHistory(params);
if (response.status === 'success') {
setHistoryRowData(response.data);
setReviewHistoryOpen(true);
} else {
alert(response.message || '검토 이력 조회 중 오류가 발생했습니다.');
}
} catch {
alert('검토 이력 조회 중 오류가 발생했습니다.');
}
};
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
<ApproveButton approveFunc={approveFunc} />
<ReturnButton returnFunc={returnFunc} />
<SaveButton saveFunc={saveFunc} />
</div>
</div>
@@ -96,12 +174,43 @@ const StatsInputStatusRvw = () => {
<DataGrid
columnInfo={columnInfo}
isExcelImport={false}
showHeaderCheckbox={true}
rowStartNum={2}
showHeaderCheckbox={true}
rowStartNum={2}
fixedColumns={3}
rowData={rowData}
setRowData={setRowData}
onRowClick={handleRowClick}
/>
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
<DialogContent
className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4"
aria-describedby={undefined}
>
<DialogHeader className="shrink-0">
<DialogTitle>
{(historySearchInfo.sggNm ? `${historySearchInfo.ctpvNm} ${historySearchInfo.sggNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력` : `${historySearchInfo.ctpvNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력`)}
</DialogTitle>
</DialogHeader>
{historyRowData.length === 0 ?
<div className="text-2xl h-40 flex items-center justify-center">
검토 이력이 없습니다.
</div>
:
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-auto">
<DataTable table={historyTable} />
</div>
}
<DialogFooter className="shrink-0">
<Button
type="button"
color="gray"
onClick={() => setReviewHistoryOpen(false)}
>
닫기
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
@@ -2,20 +2,22 @@ import { useMemo, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { LoadingWithAnimation } from "@common/components";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001";
const SewagePipeTot = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState("ST00020001");
const [activeTab, setActiveTab] = useState(firstTabKey);
const columnInfo = useMemo(
() =>
@@ -32,11 +34,11 @@ const SewagePipeTot = () => {
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab("ST00020001");
setActiveTab(firstTabKey);
};
const handleSearch = async (values: SearchFormValues) => {
setActiveTab("ST00020001");
setActiveTab(firstTabKey);
setRowDataMap(new Map());
try {
@@ -76,20 +78,66 @@ const SewagePipeTot = () => {
</div>
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
cellStyleCallback={({ row }) => {
const rowType = String(row.rowType ?? "");
if (rowType === "DETAIL") return undefined;
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
return "rg-cell-rowtype-other";
}}
/>
<div className="flex flex-col gap-[0.6rem] mx-[0.6rem]">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full"
variant="default"
>
<TabsList>
{columnInfo.map((info) => (
<TabsTrigger key={info.tabKey} value={info.tabKey}>
{info.tabLabel || info.tabKey}
</TabsTrigger>
))}
</TabsList>
{columnInfo.map((info) => {
const tabKey = info.tabKey;
if (!info.columns || !info.columnLayout) {
return null;
}
const rowData = rowDataMap.get(tabKey) ?? [];
const gridColumnInfo: ColumnInfo = {
columns: info.columns,
columnLayout: info.columnLayout,
};
return (
<TabsContent key={tabKey} value={tabKey}>
<DataGrid
columnInfo={gridColumnInfo}
rowStartNum={info.rowStartNum}
excelExportFileSuffix={info.tabLabel || tabKey}
rowData={rowData}
setRowData={(updater) => {
setRowDataMap((prev) => {
const next = new Map(prev);
const cur = next.get(tabKey) ?? [];
const n =
typeof updater === "function" ? updater(cur) : updater;
next.set(tabKey, n);
return next;
});
}}
isExcelImport={false}
isExcelExport
showHeaderCheckbox={false}
fixedColumns={info.fixedColumns}
enableSort={false}
cellStyleCallback={({ row }) => {
const rowType = String(row.rowType ?? "");
if (rowType === "DETAIL") return undefined;
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
return "rg-cell-rowtype-other";
}}
/>
</TabsContent>
);
})}
</Tabs>
</div>
</div>
);
};
@@ -44,7 +44,7 @@ const SeweragePenetrationRateTot = () => {
return (
<>
<SearchForm isDetail={true} onSubmit={handleSearch} />
<SearchForm isDetail={true} onSubmit={handleSearch} sidoSelectText="전체" />
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
@@ -1,10 +1,123 @@
import SearchForm from "../../module/SearchForm";
import { useEffect, useMemo, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import SearchForm from "@/components/module/SearchForm";
import TabDataGrid from "@/components/module/TabDataGrid";
import { LoadingWithAnimation } from "@common/components";
import type { ColumnConfig, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ComplaintInpColumn } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
import { ResidentFriendlyFactTotColumn } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
const SewerageServiceLevelInpTabList = [
ComplaintInpColumn,
ResidentFriendlyFactTotColumn,
];
const SewerageServiceLevelTot = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState(ComplaintInpColumn.tabKey);
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
const columnInfo = useMemo(
() =>
SewerageServiceLevelInpTabList.map((tab: ColumnInfoList) => ({
...tab,
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
})),
})),
[],
);
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab(ComplaintInpColumn.tabKey);
setLastSearchValues(null);
};
const fetchByTab = async (tabKey: string, values: SearchFormValues) => {
try {
setIsLoading(true);
const param = {
ctpvNm: values.sido === "ALL" ? "" : values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun,
sewerBzmnCd: values.bsnmCd,
statsYr: values.year,
rowDataYn: values.isDetail,
};
let response;
if (tabKey === ResidentFriendlyFactTotColumn.tabKey) {
response =
await statisticsTotalStatusApi.getSewerageServiceLevelResidentFriendlyFact(param);
} else {
response =
await statisticsTotalStatusApi.getSewerageServiceLevelComplaint(param);
}
if (response.status === "success" && response.data) {
setRowDataMap((prev) => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
return next;
});
} else {
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
}
} catch {
alert("데이터 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const handleSearch = async (values: SearchFormValues) => {
setLastSearchValues(values);
setRowDataMap(new Map());
await fetchByTab(activeTab, values);
};
useEffect(() => {
if (lastSearchValues == null) return;
if (rowDataMap.has(activeTab)) return;
void fetchByTab(activeTab, lastSearchValues);
}, [activeTab]);
return (
<>
<SearchForm />
</>
<div className="relative">
<SearchForm isDetail={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" />
{isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
<TabDataGrid
columnInfo={columnInfo}
isExcelImport={false}
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
cellStyleCallback={({ row }) => {
const rowType = String(row.rowType ?? "");
if (rowType === "DETAIL") return undefined;
if (rowType === "BIZ_SUM") return "rg-cell-rowtype-biz-sum";
return "rg-cell-rowtype-other";
}}
/>
</div>
);
};
@@ -234,6 +234,8 @@ export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string
export type DataGridProps = {
columnInfo: ColumnInfo;
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
/** 있으면보내기 파일명: `{메뉴명}_{suffix}.xlsx` (TabDataGrid의 탭 라벨 접미사와 동일) */
excelExportFileSuffix?: string;
isExcelImport?: boolean; // 엑셀 가져오기 유무
isExcelExport?: boolean;
onRowsChange?: (rows: Record<string, string | number>[]) => void;
@@ -10,6 +10,7 @@ export type SearchFormValues = {
export type SearchFormProps = {
sido?: boolean;
sidoSelectText?: string;
gugun?: boolean;
year?: boolean;
order?: boolean;
@@ -22,7 +23,9 @@ export type SearchFormProps = {
defaultValues?: SearchFormValues;
};
export const defaultSearchFormProps: Required<Omit<SearchFormProps, 'onSubmit' | 'onReset' | 'onValuesChange' | 'defaultValues'>> = {
export const defaultSearchFormProps: Required<
Omit<SearchFormProps, "onSubmit" | "onReset" | "onValuesChange" | "defaultValues" | "sidoSelectText">
> = {
sido: true,
gugun: true,
year: true,
@@ -2,20 +2,26 @@ import { ValueType } from "realgrid";
export const LogHistoryColumn = {
columns: [
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false },
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
{ name: 'statsYr', fieldName: 'statsYr', header: { text: '연도' }, editable: false },
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false },
{ name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false },
{ name: 'dmndFwkNm', fieldName: 'dmndFwkNm', header: { text: '요청 기능명' }, editable: false },
{ name: 'statsDataChgComNm', fieldName: 'statsDataChgComNm', header: { text: '요청 기능명' }, editable: false },
{ name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false, visible: false },
{ name: 'regDt', fieldName: 'regDt', header: { text: '등록일' }, editable: false },
],
columnLayout: [
'statsLogSn',
'ctpvNm',
'sggNm',
'statsYr',
'menuNm',
'tblNm',
'dmndFwkNm',
'statsDataChgComNm',
'rgtrId',
'regDt'
'regDt',
'statsLogSn',
]
};
@@ -0,0 +1,77 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 1: 민원
export const ComplaintInpColumn = {
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } },
{
name: "swtrmtPpltnCnt",
fieldName: "swtrmtPpltnCnt",
header: { text: "하수처리장민원건수(건)" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{
name: "whol",
fieldName: "whol",
header: { text: "합계" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{ name: "odorCnt", fieldName: "odorCnt", header: { text: "악취" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "swagePplCnt", fieldName: "swagePplCnt", header: { text: "하수관거" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "dschgWqCnt", fieldName: "dschgWqCnt", header: { text: "방류수수질" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "ldsCnt", fieldName: "ldsCnt", header: { text: "퇴적토" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "plltnAcdntCnt", fieldName: "plltnAcdntCnt", header: { text: "오염사고" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "sewerCrgAmt", fieldName: "sewerCrgAmt", header: { text: "하수도요금" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{
name: "crtrPpltnCvlcptEvntcsCnt",
fieldName: "crtrPpltnCvlcptEvntcsCnt",
header: { text: "인구10만명당 민원건수(건)" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0.00",
},
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
{ name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false },
{ name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false },
{ name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false },
].map((col) => ({ ...col, editable: false })),
columnLayout: [
{
name: "groupA",
hideChildHeaders: false,
header: { text: "행정구역" },
items: ["ctpvNm", "sggNm", "admdstNm"],
},
{
name: "groupB",
hideChildHeaders: false,
header: { text: "민원건수(건)" },
items: [
"whol",
"odorCnt",
"swagePplCnt",
"dschgWqCnt",
"ldsCnt",
"plltnAcdntCnt",
"sewerCrgAmt",
"etcCnt",
],
},
"swtrmtPpltnCnt",
"crtrPpltnCvlcptEvntcsCnt",
"rmrk",
"statsYr",
"admdstCd",
"rowType",
],
rowStartNum: 2,
fixedColumns: 1,
tabKey: "ST00130001",
tabLabel: "민원",
};
@@ -0,0 +1,81 @@
import { ValueType } from "realgrid";
// 하수도 통계 관리 > 13. 하수도 서비스 수준 > tab 2: 주민친화시설
export const ResidentFriendlyFactTotColumn = {
columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" } },
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" } },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" } },
{ name: "swtrmtFcltNm", fieldName: "swtrmtFcltNm", header: { text: "처리장명" } },
{
name: "fcltCpct",
fieldName: "fcltCpct",
header: { text: "시설용량(m³/일)" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{
name: "tmplntArea",
fieldName: "tmplntArea",
header: { text: "처리장면적(㎡)" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0.00",
},
{
name: "whol",
fieldName: "whol",
header: { text: "총계" },
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{ name: "brkFcltArea", fieldName: "brkFcltArea", header: { text: "휴게시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "childFrshpSpacArea", fieldName: "childFrshpSpacArea", header: { text: "어린이놀이공간" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "bdySportsFcltArea", fieldName: "bdySportsFcltArea", header: { text: "체력단련시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "sportsFcltArea", fieldName: "sportsFcltArea", header: { text: "운동시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "pkltArea", fieldName: "pkltArea", header: { text: "주차장" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "eduMultiFcltArea", fieldName: "eduMultiFcltArea", header: { text: "교육 및 문화시설" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "wtlndMakeArea", fieldName: "wtlndMakeArea", header: { text: "습지조성(생태공원)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "etcCnt", fieldName: "etcCnt", header: { text: "기타" }, valueType: ValueType.NUMBER, dataFormat: "#,##0" },
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" } },
{ name: "statsYr", fieldName: "statsYr", header: { text: "통계연도" }, visible: false },
{ name: "admdstCd", fieldName: "admdstCd", header: { text: "행정코드" }, visible: false },
{ name: "rowType", fieldName: "rowType", header: { text: "행타입" }, visible: false },
].map((col) => ({ ...col, editable: false })),
columnLayout: [
{
name: "groupA",
hideChildHeaders: false,
header: { text: "행정구역" },
items: ["ctpvNm", "sggNm", "admdstNm"],
},
"swtrmtFcltNm",
"fcltCpct",
"tmplntArea",
{
name: "groupB",
hideChildHeaders: false,
header: { text: "주민친화시설 면적(㎡)" },
items: [
"whol",
"brkFcltArea",
"childFrshpSpacArea",
"bdySportsFcltArea",
"sportsFcltArea",
"pkltArea",
"eduMultiFcltArea",
"wtlndMakeArea",
"etcCnt",
],
},
"rmrk",
"statsYr",
"admdstCd",
"rowType",
],
rowStartNum: 2,
fixedColumns: 3,
tabKey: "ST00130002",
tabLabel: "주민친화시설",
};
@@ -2,8 +2,10 @@ import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import {
type StatsInputStatusReviewApiResponse
import type {
StatsInputStatusReviewApiResponse,
ReviewHistoryRequest,
ReviewHistoryResponse,
} from "../model/statistics-input-review-interface";
import {
type SewerageSupplyRateReviewRequestParams,
@@ -190,6 +192,14 @@ export class StatisticsManagementApiService {
);
return response.data;
}
public async getReviewHistory(params: ReviewHistoryRequest): Promise<ReviewHistoryResponse> {
const response = await stClient.get<ReviewHistoryResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/history`,
{ params }
);
return response.data;
}
}
// 통합 인스턴스 수출
@@ -14,28 +14,37 @@ import {
type StatisticsLogRequest,
type StatisticsLogResponse,
type StatisticsLogDetailResponse,
type ReviewOpinionSaveRequest,
type ReviewOpinionSaveResponse,
type getReviewOpinionDetailRequest,
type getReviewOpinionDetailResponse,
type ReviewReturnSaveRequest,
type ReviewReturnSaveResponse,
type ReviewSubmitRequest,
type ReviewSubmitResponse,
} from "../model/statistics-interface";
export class StatisticsAPiService implements StatisticsService {
public async getSidoList(): Promise<ApiResponse<SearchCondition>> {
public async getSidoList(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>> {
const response = await stClient.get<ApiResponse<SearchCondition>>(
`${ENDPOINT.ST_SERVICE.COMMON}/search-condition`,
{ params },
);
return response.data;
}
public async getSggList(sidoName: string): Promise<ApiResponse<Sgg[]>> {
public async getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>> {
const response = await stClient.get<ApiResponse<Sgg[]>>(
`${ENDPOINT.ST_SERVICE.COMMON}/sgg`,
{ params: { sidoName } },
{ params: { sidoName, ...params } },
);
return response.data;
}
public async getSggAllList(sidoName: string): Promise<ApiResponse<Sgg[]>> {
public async getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>> {
const response = await stClient.get<ApiResponse<Sgg[]>>(
`${ENDPOINT.ST_SERVICE.COMMON}/sgg-all`,
{ params: { sidoName } },
{ params: { sidoName, ...params } },
);
return response.data;
}
@@ -80,6 +89,38 @@ export class StatisticsAPiService implements StatisticsService {
);
return response.data;
}
public async saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise<ReviewOpinionSaveResponse> {
const response = await stClient.post<ReviewOpinionSaveResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/opinion-save`,
param
);
return response.data;
}
public async getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse> {
const response = await stClient.get<getReviewOpinionDetailResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/opinion-select`,
{ params: param },
);
return response.data;
}
public async saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse> {
const response = await stClient.post<ReviewReturnSaveResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/return`,
param
);
return response.data;
}
public async submitReview(param: ReviewSubmitRequest): Promise<ReviewSubmitResponse> {
const response = await stClient.post<ReviewSubmitResponse>(
`${ENDPOINT.ST_SERVICE.REVIEW}/submit`,
param
);
return response.data;
}
}
export const statisticsAPi = new StatisticsAPiService();
@@ -3,6 +3,11 @@ import { stClient } from "@/shared/api/client";
import type { SewerageSupplyRateRequest, SewerageSupplyRateResponse } from "../model/statistics-total/statistics-sewage-supply-rate-total-interface";
import type { SewagePipeTotalRequest, SewagePipeTotalResponse } from "../model/statistics-total/statistics-sewage-pipe-total-interface";
import type {
SewerageServiceLevelComplaintResponse,
SewerageServiceLevelRequest,
SewerageServiceLevelResidentFriendlyFactResponse
} from "../model/statistics-total/statistics-service-level-total-interface";
export class StatisticsTotalApiService {
public async getSewerageSupplyRateList(params: SewerageSupplyRateRequest): Promise<SewerageSupplyRateResponse> {
@@ -20,6 +25,22 @@ export class StatisticsTotalApiService {
);
return response.data;
}
public async getSewerageServiceLevelComplaint(params: SewerageServiceLevelRequest): Promise<SewerageServiceLevelComplaintResponse> {
const response = await stClient.get<SewerageServiceLevelComplaintResponse> (
`${ENDPOINT.ST_SERVICE.TOTAL}/sewage-complaint`,
{ params: params }
);
return response.data;
}
public async getSewerageServiceLevelResidentFriendlyFact(params: SewerageServiceLevelRequest): Promise<SewerageServiceLevelResidentFriendlyFactResponse> {
const response = await stClient.get<SewerageServiceLevelResidentFriendlyFactResponse> (
`${ENDPOINT.ST_SERVICE.TOTAL}/resident-friendly-facility`,
{ params: params }
);
return response.data;
}
}
export const statisticsTotalStatusApi = new StatisticsTotalApiService();
@@ -27,6 +27,21 @@ export type StatsInputStatusReviewApiResponse = ApiResponse<StatsInputStatusRevi
};
export interface ReviewHistoryRequest {
statsCd: string;
statsYr: string;
sewerBzmnCd: string;
}
export interface ReviewHistoryResponse {
status: string;
message?: string;
data: {
logDt: string;
historyContent: string;
}[]
}
export interface StatsInputStatusReviewService {
getStatsInputStatusList(params: {
yr?: string;
@@ -34,4 +49,5 @@ export interface StatsInputStatusReviewService {
sido?: string;
gugun?: string;
}): Promise<StatsInputStatusReviewApiResponse>;
getReviewHistory(param: ReviewHistoryRequest): Promise<ReviewHistoryResponse>;
}
@@ -71,7 +71,7 @@ export interface StatisticsLog {
menuNm?: string;
tabNm?: string;
tblNm?: string;
dmndFwkNm?: string;
statsDataChgComCd?: string;
rgtrId?: string;
regDt?: string;
}
@@ -80,6 +80,15 @@ export interface StatisticsLogRequest {
page: number;
size: number;
statsCd?: string;
startDate?: string;
endDate?: string;
/** 시도명 */
ctpvNm?: string;
/** 시군구명 */
sggNm?: string;
sewerBzmnCd?: string;
/** 요청 기능(통계 데이터 변경 공통코드) */
statsDataChgComCd?: string;
}
export interface StatisticsLogResponse {
@@ -114,13 +123,77 @@ export interface StatisticsLogDetailResponse {
data?: StatisticsLogDetailRow[];
}
export interface getReviewOpinionDetailRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
}
export interface ReviewOpinionDetailItem {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
splmntOpnnCn: string | null;
wrterOpnnCn: string | null;
}
export interface getReviewOpinionDetailResponse {
status: string;
message?: string;
data?: ReviewOpinionDetailItem[];
}
/** 검토의견 저장 API body */
export interface ReviewOpinionSaveRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
splmntOpnnCn: string;
wrterOpnnCn: string;
mdfrId: string;
}
export interface ReviewOpinionSaveResponse {
status: string;
message?: string;
}
export interface ReviewReturnSaveRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
rjctOpnnCn: string;
mdfrId: string;
}
export interface ReviewReturnSaveResponse {
status: string;
message?: string;
}
export interface ReviewSubmitRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
}
export interface ReviewSubmitResponse {
status: string;
message?: string;
}
export interface StatisticsService {
getSidoList(): Promise<ApiResponse<SearchCondition>>;
getSggList(sidoName: string): Promise<ApiResponse<Sgg[]>>;
getSggAllList(sidoName: string): Promise<ApiResponse<Sgg[]>>;
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[]>>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<null>;
submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse>;
getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse>;
getLogDetail(param: string): Promise<StatisticsLogDetailResponse>;
saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise<ReviewOpinionSaveResponse>;
getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse>;
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
submitReview(param :ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
}
@@ -55,10 +55,11 @@ export interface SewerageSupplyRateReviewRequestParams {
export interface statsStatusInfo {
statsCd?: string;
laeSbmsnSttsCd?: string;
statPicSbmsnSttsCd?: string;
submitStatusCd?: string;
prvnStatusCd?: string;
rjctOpnnCn?: string;
splmntOpnnCn?: string
splmntOpnnCn?: string;
wrterOpnnCn?: string;
}
export interface SewerageSupplyRateReviewResponseData {
@@ -0,0 +1,61 @@
export interface SewerageServiceLevelRequest {
ctpvNm?: string;
sggNm?: string;
statsYr?: string;
sewerBzmnCd?: string;
rowDataYn?: boolean;
}
export interface SewerageServiceLevelComplaintTotalItem {
statsYr: string;
ctpvNm: string;
sggNm: string | null;
admdstCd: string;
admdstNm: string;
rowType: string;
swtrmtPpltnCnt: number;
whol: number;
odorCnt: number;
swagePplCnt: number;
dschgWqCnt: number;
ldsCnt: number;
plltnAcdntCnt: number;
sewerCrgAmt: number;
etcCnt: number;
crtrPpltnCvlcptEvntcsCnt: number;
rmrk: string | null;
}
export interface SewerageServiceLevelComplaintResponse {
status: string;
message?: string;
data: SewerageServiceLevelComplaintTotalItem[];
}
export interface SewerageServiceLevelResidentFriendlyFactTotalItem {
statsYr: string;
ctpvNm: string;
sggNm: string;
admdstCd: string;
admdstNm: string;
rowType: string;
swtrmtFcltNm: string;
fcltCpct: number;
tmplntArea: number;
whol: number;
brkFcltArea: number;
childFrshpSpacArea: number;
bdySportsFcltArea: number;
sportsFcltArea: number;
pkltArea: number;
eduMultiFcltArea: number;
wtlndMakeArea: number;
etcCnt: number;
rmrk: string;
}
export interface SewerageServiceLevelResidentFriendlyFactResponse {
status: string;
message?: string;
data: SewerageServiceLevelResidentFriendlyFactTotalItem[];
}
+2
View File
@@ -23,6 +23,8 @@ export interface IUserInfo {
addr: string;
daddr: string;
laeSeCd: string;
/** 기관 구분 코드 — 통계 공통 조회 등 query `instSeCd`로 전달 */
instSeCd?: string;
ogdpSeCd: string;
aprvYn: string;
autzrId: string;