This commit is contained in:
2026-04-20 17:46:03 +09:00
47 changed files with 3601 additions and 937 deletions
+74 -74
View File
@@ -1,76 +1,76 @@
{
"name": "st",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@rsone/components": "workspace:*",
"@faker-js/faker": "^9.9.0",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/xlsx": "^0.0.36",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.474.0",
"next": "15.3.8",
"next-qrcode": "^2.5.1",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"realgrid": "^2.9.5",
"realgrid-react": "^0.9.7",
"remixicon": "^4.7.0",
"sonner": "^2.0.1",
"swr": "^2.3.7",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.3.8",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
"name": "st",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@rsone/components": "workspace:*",
"@faker-js/faker": "^9.9.0",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/xlsx": "^0.0.36",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.474.0",
"next": "15.3.8",
"next-qrcode": "^2.5.1",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"realgrid": "^2.9.5",
"realgrid-react": "^0.9.7",
"remixicon": "^4.7.0",
"sonner": "^2.0.1",
"swr": "^2.3.7",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.3.8",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
@@ -1,7 +1,7 @@
import { Button } from "@common/components";
interface ReviewOpinionButtonProps {
reviewOpinionFunc: () => void;
reviewOpinionFunc: () => void | Promise<void>;
}
const ReviewOpinionButton = ({ reviewOpinionFunc }: ReviewOpinionButtonProps) => {
+111 -52
View File
@@ -311,6 +311,7 @@ function buildFieldsFromColumns(
const DataGrid = ({
columnInfo,
rowStartNum,
excelExportFileSuffix,
isExcelImport = true,
isExcelExport = true,
fixedColumns,
@@ -321,10 +322,14 @@ const DataGrid = ({
cellStyleCallback,
cellTooltipCallback,
onRowClick,
onItemChecked,
onItemAllChecked,
onScrollToBottom,
statusInfo,
highlightedRowId,
idFieldName = "id",
enableSort = false,
gridHeight = 65, // vh 단위
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
const gridRef = useRef<RealGridReact>(null);
@@ -340,9 +345,18 @@ const DataGrid = ({
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
cellTooltipCallback,
);
const onScrollToBottomRef = useRef<(() => void) | undefined>(
onScrollToBottom,
);
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
const onItemAllCheckedRef =
useRef<typeof onItemAllChecked>(onItemAllChecked);
rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback;
cellTooltipCallbackRef.current = cellTooltipCallback;
onScrollToBottomRef.current = onScrollToBottom;
onItemCheckedRef.current = onItemChecked;
onItemAllCheckedRef.current = onItemAllChecked;
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
@@ -470,10 +484,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: () => {
@@ -558,10 +576,10 @@ const DataGrid = ({
) {
const cell =
worksheet[
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
XLSX.utils.encode_cell({
r: rowIndex,
c: col,
})
];
if (
cell &&
@@ -637,7 +655,7 @@ const DataGrid = ({
const col = range.s.c + colIndex;
const cell =
worksheet[
XLSX.utils.encode_cell({ r: row, c: col })
XLSX.utils.encode_cell({ r: row, c: col })
];
const headerName =
headers[colIndex] || `Column${colIndex + 1}`;
@@ -761,13 +779,13 @@ const DataGrid = ({
onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
(gridView.displayOptions as DisplayOptions).fitStyle =
GridFitStyle.EVEN;
@@ -937,7 +955,7 @@ const DataGrid = ({
) {
(
column.footer[
index
index
] as FooterItemWithCallback
).valueCallback =
footerItem.valueCallback;
@@ -990,11 +1008,11 @@ const DataGrid = ({
getGridDataRef.current = () => {
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
| undefined;
if (!gv?.getDataSource) return [];
const ds = gv.getDataSource();
@@ -1090,6 +1108,47 @@ const DataGrid = ({
};
}, [onRowClick, cellTooltipCallback, columns]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView || !onScrollToBottom) return;
gridView.onScrollToBottom = () => {
onScrollToBottomRef.current?.();
};
return () => {
if (gridView.onScrollToBottom)
gridView.onScrollToBottom = undefined;
};
}, [onScrollToBottom]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView) return;
if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => {
onItemCheckedRef.current?.(itemIndex, checked);
};
} else {
gridView.onItemChecked = undefined;
}
if (onItemAllChecked) {
gridView.onItemAllChecked = (_grid, checked) => {
onItemAllCheckedRef.current?.(checked);
};
} else {
gridView.onItemAllChecked = undefined;
}
return () => {
if (gridView.onItemChecked) gridView.onItemChecked = undefined;
if (gridView.onItemAllChecked)
gridView.onItemAllChecked = undefined;
};
}, [onItemChecked, onItemAllChecked]);
return (
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
<input
@@ -1105,23 +1164,23 @@ const DataGrid = ({
<span>
진행상황:{" "}
{statusInfo.laeSbmsnSttsCd === null ||
statusInfo.laeSbmsnSttsCd === "0"
statusInfo.laeSbmsnSttsCd === "0"
? "미작성"
: statusInfo.laeSbmsnSttsCd === "1"
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
? "작성중"
: statusInfo.laeSbmsnSttsCd === "2"
? "제출"
: statusInfo.laeSbmsnSttsCd === "3"
? "승인"
: statusInfo.laeSbmsnSttsCd === "4"
? "반려"
: "검증중"}
</span>
<span>
제출일:{" "}
{statusInfo.laeSbmsnYmd
? statusInfo.laeSbmsnYmd.length === 8 &&
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
/^\d{8}$/.test(statusInfo.laeSbmsnYmd)
? `${statusInfo.laeSbmsnYmd.slice(0, 4)}-${statusInfo.laeSbmsnYmd.slice(4, 6)}-${statusInfo.laeSbmsnYmd.slice(6, 8)}`
: statusInfo.laeSbmsnYmd
: "미제출"}
@@ -1159,7 +1218,7 @@ const DataGrid = ({
</Button>
)}
</div>
<div className="h-[65vh]">
<div style={{ height: `${gridHeight}vh` }}>
<RealGridReact
ref={gridRef}
autoGenerateField={true}
@@ -1176,24 +1235,24 @@ const DataGrid = ({
) => boolean;
}
).onSorting = enableSort
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
? createOnSortingHandler(
columns,
setRowData,
sortDirRef,
originalRowsRef,
)
: () => false;
const dataProvider = gridRef.current
?.dataProvider as unknown as
| {
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
| undefined;
if (dataProvider?.setFields) {
dataProvider.setFields(
@@ -1346,14 +1405,14 @@ const DataGrid = ({
})}
footer={
column.footer &&
!Array.isArray(column.footer)
!Array.isArray(column.footer)
? {
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
text: column.footer.text,
expression:
column.footer.expression,
numberFormat:
column.footer.numberFormat,
}
: undefined
}
mergeRule={
+49 -29
View File
@@ -1,52 +1,66 @@
"use client";
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import useSWR from "swr";
import {
Button,
Input,
Label,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
Checkbox,
} from "@common/components";
import { useAlertStore } from "@/shared/lib";
import {
type SearchFormProps,
type SearchFormValues,
defaultSearchFormProps,
} from "../type/SearchForm.type";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type {
ApiResponse,
SearchCondition,
Sgg,
} from "@/entities/statistics/model/statistics-interface";
import {
Button,
Checkbox,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
import {
defaultSearchFormProps,
type SearchFormProps,
type SearchFormValues,
} from "../type/SearchForm.type";
import { doList } from "../util/column/input/DoList";
const getSidoListFetcher = async (
key: [string, string],
): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList({ instSeCd: key[1] });
};
const getSggListFetcher = async (
key: [string, string],
key: [string, string, string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggList(key[1]);
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,
@@ -77,8 +91,10 @@ const SearchForm = (props: SearchFormProps = {}) => {
isDetail: false,
});
const sggKey: [string, string] | null =
values.sido && values.sido !== "ALL" ? ["sgg-list", values.sido] : null;
const sggKey: [string, string, string, string] | null =
values.sido && values.sido !== "ALL"
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""]
: null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey,
getSggListFetcher,
@@ -102,7 +118,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
);
const sggOptions = sggList?.data ?? [];
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
props.onSubmit?.(values);
};
@@ -163,10 +179,14 @@ const SearchForm = (props: SearchFormProps = {}) => {
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}
@@ -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('저장되었습니다.');
@@ -2,13 +2,30 @@
import { useCallback, useEffect, useState } from "react";
import dayjs from "dayjs";
import useSWR from "swr";
import { useAlertStore } from "@/shared/lib";
import { CodeSelectWithAll } from "@/entities/code";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import type {
ApiResponse,
SearchCondition,
Sgg,
StatisticsLog,
StatisticsLogRequest,
} from "@/entities/statistics/model/statistics-interface";
import { Button, Label, LoadingWithAnimation } from "@common/components";
import {
Button,
InputDatePicker,
Label,
LoadingWithAnimation,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import { LogHistoryDetailDialog } from "@/components/statistics/management/LogHistoryDetailDialog";
@@ -18,9 +35,23 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
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 { 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>[]>(
[],
@@ -32,49 +63,148 @@ 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 { 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(`시도 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
},
},
);
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(`시군구 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`);
}
},
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 = {
statsCd: statsCd === "" || statsCd === "ALL" ? "" : statsCd,
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,
}),
};
setIsLoading(true);
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"
? "제출"
: "예외 제출",
}),
);
const response = await statisticsAPi.getLogList(param);
setRowData(toGridRows(convertedData));
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
setPageIndex(Math.max(0, page1Based - 1));
} else {
alert(
response.message || "데이터 조회 중 오류가 발생했습니다.",
);
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],
[
statsCd,
startDate,
endDate,
sido,
gugun,
bsnmCd,
statsDataChgComCd,
alert,
],
);
const handleSearch = async (e: React.SubmitEvent<HTMLFormElement>) => {
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);
@@ -86,14 +216,58 @@ 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>
<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
@@ -104,11 +278,36 @@ const LogHistory = () => {
className="w-[16rem]"
/>
</div>
<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>
@@ -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>
);
};
@@ -1,10 +1,34 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import dayjs from "dayjs";
import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib";
import { CodeSelect } from "@/entities/code";
import {
type ApprovalRequest,
statisticsApprovalApi,
useStAnlsSidoCombo,
useStAnlsSigunguCombo,
useStatsVrfcRptData,
useStatsVrfcRsltData,
} from "@/entities/statistics";
import type { VerifyResultItem } from "@/entities/statistics/model/general-verification-interface";
import type { StatsInputStatusList } from "@/entities/statistics/model/input/statistics-input-status-interface";
import {
Button,
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxValue,
Label,
LoadingWithAnimation,
Select,
@@ -12,259 +36,364 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
useComboboxAnchor,
} from "@common/components";
import {
statisticsVerificationDetailColumnInfo,
statisticsVerificationSummaryColumnInfo,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-columns";
import {
APPROVAL_STATUS_OPTIONS,
DEFAULT_FILTERS,
DEFAULT_SUB_FILTERS,
fetchStatisticsVerificationDetailMock,
fetchStatisticsVerificationSummaryMock,
getSigunguOptions,
getYearOptions,
SIDO_OPTIONS,
STATS_INDICATOR_OPTIONS,
STATUS_OPTIONS,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-mock";
import type {
StatisticsVerificationDetailRow,
StatisticsVerificationFilterState,
StatisticsVerificationSubFilterState,
StatisticsVerificationSummaryRow,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
import ApproveButton from "@/components/button/ApproveButton";
import ReturnButton from "@/components/button/ReturnButton";
import DataGrid from "@/components/module/DataGrid";
import { toGridRows } from "@/components/util/grid/gridFunction";
const getYearOptions = () => {
const currentYear = dayjs().year();
const yearOptionsRange = Array.from(
{ length: currentYear - 2017 },
(_, i) => {
const year = 2017 + i;
return { label: String(year), value: String(year) };
},
);
return yearOptionsRange.map((year) => ({
label: year.label,
value: year.value,
}));
};
function isSameStatsInputStatusRow(
a: StatsInputStatusList,
b: StatsInputStatusList,
): boolean {
return (
a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd
);
}
function isSameVerifyResultRow(
a: VerifyResultItem,
b: VerifyResultItem,
): boolean {
if (a.vrfcSn != null && b.vrfcSn != null) return a.vrfcSn === b.vrfcSn;
return (
a.statsYr === b.statsYr &&
a.statsCd === b.statsCd &&
a.ctpvNm === b.ctpvNm &&
a.sggNm === b.sggNm &&
a.vrfcTrgtNm === b.vrfcTrgtNm &&
a.vrfcMthdCd === b.vrfcMthdCd
);
}
function useGridCheckboxSelection<T>(
sourceRows: T[],
isSameRow: (a: T, b: T) => boolean,
setSelected: Dispatch<SetStateAction<T[]>>,
) {
const handleItemChecked = useCallback(
(itemIndex: number, checked: boolean) => {
const origin = sourceRows[itemIndex];
if (origin === undefined) return;
setSelected((prev) => {
if (checked) {
if (prev.some((r) => isSameRow(r, origin))) return prev;
return [...prev, origin];
}
return prev.filter((r) => !isSameRow(r, origin));
});
},
[sourceRows, isSameRow, setSelected],
);
const handleAllChecked = useCallback(
(checked: boolean) => {
if (checked) setSelected([...sourceRows]);
else setSelected([]);
},
[sourceRows, setSelected],
);
return { handleItemChecked, handleAllChecked };
}
const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
ctpvNm: null,
sggNm: null,
statsYr: String(dayjs().year() - 1),
sewerBzmnCd: null,
statsCd: null,
};
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
...DEFAULT_FILTERS,
prcsYnList: "1,2,3",
pbcpAprvYn: "N",
};
const STATUS_OPTIONS = [
{ label: "수정완료", value: "1" },
{ label: "예외통과", value: "2" },
{ label: "반려", value: "3" },
];
const APPROVAL_STATUS_OPTIONS = [
{ label: "승인", value: "Y" },
{ label: "미승인", value: "N" },
];
function StatisticsVerificationReport() {
const userInfo = useUserStore((state) => state.userInfo);
const { setMessage: alert } = useAlertStore((state) => state);
const yearOptions = useMemo(() => getYearOptions(), []);
const { confirm } = useConfirm();
const [filters, setFilters] =
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
const [subFilters, setSubFilters] =
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
const [summaryRows, setSummaryRows] = useState<
StatisticsVerificationSummaryRow[]
const [submittedMainFilters, setSubmittedMainFilters] =
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
const [submittedDetailParams, setSubmittedDetailParams] = useState<{
main: StatisticsVerificationFilterState;
sub: StatisticsVerificationSubFilterState;
}>({
main: DEFAULT_FILTERS,
sub: DEFAULT_SUB_FILTERS,
});
const [selectedSummaryRows, setSelectedSummaryRows] = useState<
StatsInputStatusList[]
>([]);
const [detailRows, setDetailRows] = useState<
StatisticsVerificationDetailRow[]
const [selectedDetailRows, setSelectedDetailRows] = useState<
VerifyResultItem[]
>([]);
const [summaryGridRows, setSummaryGridRows] = useState<
Record<string, string | number>[]
>([]);
const [detailGridRows, setDetailGridRows] = useState<
Record<string, string | number>[]
>([]);
const [selectedSummaryRow, setSelectedSummaryRow] =
useState<StatisticsVerificationSummaryRow | null>(null);
const [selectedDetailRow, setSelectedDetailRow] =
useState<StatisticsVerificationDetailRow | null>(null);
const [isLoading, setIsLoading] = useState(false);
const sigunguOptions = useMemo(
() => getSigunguOptions(filters.sido),
[filters.sido],
const { data: sidoData } = useStAnlsSidoCombo();
const { data: sigunguData } = useStAnlsSigunguCombo(
filters.sewerBzmnCd ?? "",
);
useEffect(() => {
void handleSearch(filters, subFilters);
}, []);
const statisticsVerificationReportQuery =
useStatsVrfcRptData(submittedMainFilters);
const statisticsVerificationResultQuery = useStatsVrfcRsltData(
submittedDetailParams,
);
async function handleSearch(
const statisticsVerificationReportData =
statisticsVerificationReportQuery.data ?? [];
const statisticsVerificationReportGridRows = useMemo(
() => toGridRows(statisticsVerificationReportData),
[statisticsVerificationReportData],
);
const statisticsVerificationResultData = useMemo(
() =>
statisticsVerificationResultQuery.data?.pages.flatMap(
(page) => page.verifyResultList ?? [],
) ?? [],
[statisticsVerificationResultQuery.data],
);
const statisticsVerificationResultGridRows = useMemo(
() => toGridRows(statisticsVerificationResultData),
[statisticsVerificationResultData],
);
const isLoading =
statisticsVerificationReportQuery.isFetching ||
statisticsVerificationResultQuery.isFetching;
const isAppendingDetail =
statisticsVerificationResultQuery.isFetchingNextPage;
const canLoadMoreDetail = Boolean(
statisticsVerificationResultQuery.hasNextPage,
);
const {
handleItemChecked: handleReportItemChecked,
handleAllChecked: handleReportAllChecked,
} = useGridCheckboxSelection(
statisticsVerificationReportData,
isSameStatsInputStatusRow,
setSelectedSummaryRows,
);
const {
handleItemChecked: handleResultItemChecked,
handleAllChecked: handleResultAllChecked,
} = useGridCheckboxSelection(
statisticsVerificationResultData,
isSameVerifyResultRow,
setSelectedDetailRows,
);
const { mutateAsync: onApproveStats } = useMutationWrapper({
mutationFn: (approvalRequests: ApprovalRequest[]) =>
statisticsApprovalApi.approveStats(approvalRequests),
onSuccess: (data) => {
alert(data.message ?? "승인이 완료되었습니다.");
},
});
useEffect(() => {
if (!statisticsVerificationReportQuery.isError) return;
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
setSelectedSummaryRows([]);
}, [
alert,
statisticsVerificationReportQuery.errorUpdatedAt,
statisticsVerificationReportQuery.isError,
]);
function handleSearch(
nextFilters: StatisticsVerificationFilterState,
nextSubFilters: StatisticsVerificationSubFilterState,
summaryId?: number | null,
) {
setSelectedSummaryRows([]);
setSelectedDetailRows([]);
setSubmittedMainFilters({ ...nextFilters });
setSubmittedDetailParams({
main: { ...nextFilters },
sub: { ...nextSubFilters },
});
}
async function handleLoadMoreDetail() {
if (isLoading || isAppendingDetail || !canLoadMoreDetail) return;
try {
setIsLoading(true);
const [summaryData, detailData] = await Promise.all([
fetchStatisticsVerificationSummaryMock(nextFilters),
fetchStatisticsVerificationDetailMock(
nextFilters,
nextSubFilters,
summaryId,
),
]);
setSummaryRows(summaryData);
setDetailRows(detailData);
setSummaryGridRows(toGridRows(summaryData));
setDetailGridRows(toGridRows(detailData));
if (summaryData.length === 0)
alert("조회된 상단 목록 데이터가 없습니다.");
if (detailData.length === 0)
alert("조회된 상세 목록 데이터가 없습니다.");
await statisticsVerificationResultQuery.fetchNextPage();
} catch {
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
alert("추가 데이터를 불러오는 중 오류가 발생했습니다.");
}
}
function handleDetailSearchByAreaC() {
setSelectedDetailRows([]);
setSubmittedDetailParams({
main: { ...filters },
sub: { ...subFilters },
});
}
function handleFilterChange<
K extends keyof StatisticsVerificationFilterState,
>(key: K, value: StatisticsVerificationFilterState[K]) {
if (key === "sido") {
setFilters((prev) => ({ ...prev, sido: value, sigungu: "ALL" }));
return;
}
setFilters((prev) => ({ ...prev, [key]: value }));
>(key: K) {
return (
value?: StatisticsVerificationFilterState[K],
optionData?: { label: string; value: string },
) => {
if (key === "sewerBzmnCd") {
setFilters((prev) => ({
...prev,
ctpvNm: optionData?.label,
sewerBzmnCd: optionData?.value,
sggNm: "",
}));
return;
}
setFilters((prev) => ({ ...prev, [key]: value }));
};
}
function handleSubFilterChange<
K extends keyof StatisticsVerificationSubFilterState,
>(key: K, value: StatisticsVerificationSubFilterState[K]) {
setSubFilters((prev) => ({ ...prev, [key]: value }));
>(key: K) {
return (value?: StatisticsVerificationSubFilterState[K]) => {
setSubFilters((prev) => ({ ...prev, [key]: value }));
};
}
function handleReset() {
setFilters(DEFAULT_FILTERS);
setSubFilters(DEFAULT_SUB_FILTERS);
setSelectedSummaryRow(null);
setSelectedDetailRow(null);
void handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
setSelectedSummaryRows([]);
setSelectedDetailRows([]);
handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
}
function handleApprove() {
if (!selectedSummaryRow) {
alert("승인할 상단 목록 행을 먼저 선택해주세요.");
return;
async function handleApprove() {
const commonParam = {
statsYr: filters.statsYr,
sewerBzmnCd: filters.sewerBzmnCd ?? "",
statsCd: filters.statsCd ?? "",
mdfrId: userInfo?.userUnqId ?? "",
};
const approvalRequests = selectedSummaryRows.map((row) => ({
...commonParam,
rjctOpnnCn: row.rjctOpnnCn ?? "",
lastCfmtnPicNm: row.laePicNm ?? "",
lastCfmtnPicTelNo: row.laePicTelNo ?? "",
lastCfmtnPicDeptNm: row.laeKbrdrDeptNm ?? "",
lastCfmtnPicJbgdNm: row.laeKbrdrJbgdNm ?? "",
}));
try {
if (await confirm("승인하시겠습니까?")) {
await onApproveStats(approvalRequests);
}
} catch {
alert("승인 중 오류가 발생했습니다.");
}
alert(
`승인 API TBD: ${selectedSummaryRow.sido} ${selectedSummaryRow.sigungu} (${selectedSummaryRow.reportType})`,
);
}
function handleReject() {
if (!selectedDetailRow) {
alert("반려할 상세 항목 행을 먼저 선택해주세요.");
return;
}
alert(
`반려 API TBD: ${selectedDetailRow.sido} ${selectedDetailRow.sigungu} ${selectedDetailRow.detailItem}`,
);
}
function handleDownload() {
if (detailRows.length === 0) {
alert("다운로드할 상세 목록이 없습니다.");
return;
}
const headers = [
"시도",
"시군구",
"지표",
"세부항목",
"검증날짜",
"오류내역",
"입력값",
"비교값",
"상태",
"예외사유",
"반려의견",
"승인여부",
];
const rows = detailRows.map((row) =>
[
row.sido,
row.sigungu,
row.indicator,
row.detailItem,
row.verifyDate,
row.errorDetail,
String(row.inputValue),
String(row.compareValue),
row.status,
row.exceptionReason,
row.returnOpinion,
row.approvalYn,
].join(","),
);
const csv = [headers.join(","), ...rows].join("\n");
const blob = new Blob([`\uFEFF${csv}`], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "statistics-verification-report-detail.csv";
link.click();
URL.revokeObjectURL(url);
}
function handleSummaryRowClick(row: Record<string, string | number>) {
const summaryId = Number(row.summaryId);
const selected = summaryRows.find(
(item) => item.summaryId === summaryId,
);
setSelectedSummaryRow(selected ?? null);
if (!summaryId) return;
void handleSearch(filters, subFilters, summaryId);
}
function handleDetailRowClick(row: Record<string, string | number>) {
const detailId = Number(row.detailId);
const selected = detailRows.find((item) => item.detailId === detailId);
setSelectedDetailRow(selected ?? null);
}
function handleReject() {}
return (
<div className="relative">
<div className="card !py-0">
<div className="grid grid-cols-1 gap-[0.8rem] p-[1.2rem] md:grid-cols-[repeat(4,minmax(0,1fr))_auto]">
<div className="flex justify-between p-2">
<div className="flex items-center gap-6">
<FilterSelect
label="시도"
value={filters.sido}
onChange={(value) => handleFilterChange("sido", value)}
options={SIDO_OPTIONS}
value={filters.sewerBzmnCd ?? ""}
onChange={handleFilterChange("sewerBzmnCd")}
options={
sidoData?.map((item) => ({
label: item.adstNm,
value: item.adstCd,
})) ?? []
}
/>
<FilterSelect
label="시군구"
value={filters.sigungu}
onChange={(value) =>
handleFilterChange("sigungu", value)
value={filters.sggNm ?? ""}
onChange={handleFilterChange("sggNm")}
options={
sigunguData?.map((item) => ({
label: item.adstNm,
value: item.adstCd,
})) ?? []
}
options={sigunguOptions}
/>
<FilterSelect
label="년도"
value={filters.year}
onChange={(value) => handleFilterChange("year", value)}
options={yearOptions}
value={filters.statsYr}
onChange={handleFilterChange("statsYr")}
options={getYearOptions()}
/>
<FilterSelect
label="통계지표"
value={filters.statsIndicator}
onChange={(value) =>
handleFilterChange("statsIndicator", value)
}
options={STATS_INDICATOR_OPTIONS}
/>
<div className="flex items-end justify-end gap-[0.6rem]">
<Button color="green" onClick={handleReset}>
초기화
</Button>
<Button
onClick={() =>
void handleSearch(filters, subFilters)
}
>
조회
</Button>
<div className="flex items-center gap-2">
<Label>통계지표</Label>
<CodeSelect
upCd="STITEM"
value={filters.statsCd ?? ""}
onValueChange={handleFilterChange("statsCd")}
placeholder="선택"
className="w-[32rem]"
/>
</div>
</div>
</div>
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end">
<ApproveButton approveFunc={handleApprove} />
<div className="flex items-end justify-end gap-[0.6rem]">
<Button color="white" onClick={handleReset}>
초기화
</Button>
<Button onClick={() => handleSearch(filters, subFilters)}>
조회
</Button>
<Button
color="green"
onClick={handleApprove}
disabled={selectedSummaryRows.length === 0}
>
승인
</Button>
</div>
</div>
@@ -272,78 +401,161 @@ function StatisticsVerificationReport() {
columnInfo={statisticsVerificationSummaryColumnInfo}
fixedColumns={3}
isExcelImport={false}
onRowClick={handleSummaryRowClick}
rowData={summaryGridRows}
isExcelExport={false}
onItemAllChecked={handleReportAllChecked}
onItemChecked={handleReportItemChecked}
rowData={statisticsVerificationReportGridRows}
rowStartNum={2}
setRowData={setSummaryGridRows}
setRowData={() => {}}
showHeaderCheckbox={true}
gridHeight={30}
/>
<div className="card !py-0">
<div className="m-[0.6rem] flex flex-wrap items-end justify-between gap-[0.8rem]">
<div className="grid w-full grid-cols-1 gap-[0.8rem] md:w-auto md:grid-cols-2">
<FilterSelect
label="상태"
value={subFilters.status}
onChange={(value) =>
handleSubFilterChange("status", value)
}
options={STATUS_OPTIONS}
/>
<FilterSelect
label="승인상태"
value={subFilters.approvalStatus}
onChange={(value) =>
handleSubFilterChange("approvalStatus", value)
}
options={APPROVAL_STATUS_OPTIONS}
/>
</div>
<div className="flex items-end justify-end gap-[0.6rem]">
<Button color="gray" onClick={handleDownload}>
다운로드
</Button>
<ReturnButton returnFunc={handleReject} />
</div>
<h3 className="mt-10 px-2 text-[1.8rem] font-bold text-title">
검증 결과
</h3>
<div className="mt-2 flex flex-wrap items-end justify-between p-2">
<div className="flex items-center gap-6">
<FilterCombobox
label="상태"
value={subFilters.prcsYnList}
onChange={handleSubFilterChange("prcsYnList")}
options={STATUS_OPTIONS}
/>
<FilterSelect
label="승인상태"
value={subFilters.pbcpAprvYn}
onChange={handleSubFilterChange("pbcpAprvYn")}
options={APPROVAL_STATUS_OPTIONS}
/>
</div>
<div className="flex items-end justify-end gap-[0.6rem]">
<Button onClick={handleDetailSearchByAreaC}>조회</Button>
<Button
color="red"
onClick={handleReject}
disabled={selectedDetailRows.length === 0}
>
반려
</Button>
</div>
</div>
<DataGrid
columnInfo={statisticsVerificationDetailColumnInfo}
fixedColumns={4}
isExcelImport={false}
onRowClick={handleDetailRowClick}
rowData={detailGridRows}
rowStartNum={2}
setRowData={setDetailGridRows}
showHeaderCheckbox={true}
/>
<div className="relative">
<DataGrid
columnInfo={statisticsVerificationDetailColumnInfo}
fixedColumns={4}
isExcelImport={false}
onItemAllChecked={handleResultAllChecked}
onItemChecked={handleResultItemChecked}
onScrollToBottom={handleLoadMoreDetail}
rowData={statisticsVerificationResultGridRows}
rowStartNum={2}
setRowData={() => {}}
showHeaderCheckbox={true}
/>
{isLoading && (
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
{(isLoading || isAppendingDetail) && (
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
<div className="relative size-[6rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
</div>
)}
)}
</div>
</div>
);
}
interface FilterSelectProps {
label: string;
onChange: (value: string) => void;
onChange: (
value?: string,
optionData?: { label: string; value: string },
) => void;
options: Array<{ label: string; value: string }>;
value: string;
value?: string;
}
function FilterCombobox({
label,
onChange,
options,
value,
}: FilterSelectProps) {
const anchor = useComboboxAnchor();
const items = useMemo(
() => options.map((option) => option.value),
[options],
);
const selectedValues = useMemo(() => {
if (!value) return [];
return value.split(",").filter(Boolean);
}, [value]);
return (
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap">{label}</Label>
<Combobox
multiple
autoHighlight
items={items}
value={selectedValues}
onValueChange={(next) => {
if (next == null) {
onChange(undefined);
return;
}
const list = Array.isArray(next) ? next : [next];
onChange(list.length > 0 ? list.join(",") : undefined);
}}
>
<ComboboxChips ref={anchor} className="max-w-[26rem]">
<ComboboxValue>
{(values: string[]) => (
<>
{values.map((v) => (
<ComboboxChip key={v}>
{options.find((o) => o.value === v)
?.label ?? v}
</ComboboxChip>
))}
<ComboboxChipsInput placeholder="선택" />
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>검색 결과가 없습니다.</ComboboxEmpty>
<ComboboxList>
{(item: string) => (
<ComboboxItem key={item} value={item}>
{options.find((o) => o.value === item)?.label ??
item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
);
}
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
return (
<div className="flex items-center gap-[0.8rem]">
<Label className="min-w-[4rem] text-[1.3rem]">{label}</Label>
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="h-[3.2rem] min-w-[14rem]">
<SelectValue placeholder="전체" />
<div className="flex items-center gap-2">
<Label className="whitespace-nowrap">{label}</Label>
<Select
value={value}
onValueChange={(value) =>
onChange(
value,
options.find((option) => option.value === value),
)
}
>
<SelectTrigger className="w-[16rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
@@ -357,5 +569,4 @@ function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
);
}
export { StatisticsVerificationReport };
export default StatisticsVerificationReport;
+93 -75
View File
@@ -18,30 +18,30 @@ export type RealGridColumnInfo = {
mergeRule?: { criteria: "value" | string };
sortable?: boolean;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
};
/** setColumns 메서드가 있는 GridView */
@@ -110,21 +110,21 @@ export type DataFieldValueCallback = (
export type ComputedFieldConfig =
| { fields: string[]; type: "sum" | "avg" }
| {
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
export type ColumnConfig = {
name: string;
@@ -147,31 +147,31 @@ export type ColumnConfig = {
valueType?: ValueType;
dataFormat?: string;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
};
export type FooterUserSpan = {
@@ -183,16 +183,16 @@ export type FooterUserSpan = {
export type ColumnLayoutItem =
| string
| {
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
| {
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
export type ColumnInfo = {
columns: ColumnConfig[];
@@ -202,7 +202,10 @@ export type ColumnInfo = {
export type GridTopButtonConfig = {
visible?: boolean;
text: string;
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
onClick?: (helpers: {
openDialog: (title?: string) => void;
closeDialog: () => void;
}) => void;
};
export type ColumnInfoList = {
@@ -225,15 +228,24 @@ export type CellStyleCallbackParams = {
row: Record<string, string | number>;
};
export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined;
export type CellStyleCallback = (
params: CellStyleCallbackParams,
) => string | undefined;
/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */
export type CellTooltipCallbackParams = { row: Record<string, string | number>; fieldName: string };
export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined;
export type CellTooltipCallbackParams = {
row: Record<string, string | number>;
fieldName: string;
};
export type CellTooltipCallback = (
params: CellTooltipCallbackParams,
) => string | undefined;
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;
@@ -252,6 +264,11 @@ export type DataGridProps = {
current: (() => Record<string, string | number>[]) | null;
};
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
/** CheckBar 행 체크 변경 */
onItemChecked?: (itemIndex: number, checked: boolean) => void;
/** CheckBar 헤더 전체 선택/해제 */
onItemAllChecked?: (checked: boolean) => void;
onScrollToBottom?: () => void;
statusInfo?: SewerageSupplyRateStatusInfo | null;
/** 강조 및 스크롤할 행의 ID */
highlightedRowId?: string | number | null;
@@ -263,6 +280,7 @@ export type DataGridProps = {
compactFullWidth?: boolean;
/** CompactDataGrid 전용: 그리드 영역 높이 Tailwind 클래스 */
compactGridHeightClass?: string;
gridHeight?: number;
};
export type TabDataGridProps = {
@@ -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: "주민친화시설",
};
@@ -1,127 +1,169 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
import type {
CheckVerificationNameRequest,
CheckVerificationNameResponse,
CopyVerifyDataRequest,
DeleteVrfcMasterResponse,
GeneralverificationDetailResponse,
GeneralverificationInfoResponse,
GeneralverificationRequest,
GeneralverificationResponse,
GeneralverificationService,
GeneralverificationInfoResponse,
CheckVerificationNameRequest,
CheckVerificationNameResponse,
UpdateVrfcMasterRequest,
UpdateVrfcMasterResponse,
RegistVrfcMasterRequest,
RegistVrfcMasterResponse,
DeleteVrfcMasterResponse,
VrfcOperatorsResponse,
GeneralverificationDetailResponse,
UpdateVrfcMasterRequest,
UpdateVrfcMasterResponse,
VerifyColumnListRequest,
VerifyColumnListResponse,
VerifyDetailRegistRequest,
VerifyDetailUpdateRequest,
CopyVerifyDataRequest,
VerifyResultListRequest,
VerifyResultListResponse,
VrfcOperatorsResponse,
} from "../model/general-verification-interface";
export class GeneralverificationApiService implements GeneralverificationService {
public async getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse> {
const response = await stClient.get<GeneralverificationResponse> (
public async getGeneralverificationList(
param: GeneralverificationRequest,
): Promise<GeneralverificationResponse> {
const response = await stClient.get<GeneralverificationResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`,
{ params: param },
);
return response.data;
}
public async getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse> {
const response = await stClient.get<GeneralverificationInfoResponse> (
public async getGeneralverificationInfo(
param: number,
): Promise<GeneralverificationInfoResponse> {
const response = await stClient.get<GeneralverificationInfoResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`,
);
return response.data;
}
public async checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse> {
const response = await stClient.get<CheckVerificationNameResponse> (
public async checkVerificationName(
param: CheckVerificationNameRequest,
): Promise<CheckVerificationNameResponse> {
const response = await stClient.get<CheckVerificationNameResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`,
{ params: param },
);
return response.data;
}
public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse> {
const response = await stClient.put<UpdateVrfcMasterResponse> (
public async UpdateVrfcMaster(
param: UpdateVrfcMasterRequest,
): Promise<UpdateVrfcMasterResponse> {
const response = await stClient.put<UpdateVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`,
param,
);
return response.data;
}
public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async RegistVrfcMaster(
param: RegistVrfcMasterRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`,
param,
);
return response.data;
}
public async DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse> (
public async DeleteVrfcMaster(
param: number,
): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
);
return response.data;
}
public async getVrfcOperators(): Promise<VrfcOperatorsResponse> {
const response = await stClient.get<VrfcOperatorsResponse> (
const response = await stClient.get<VrfcOperatorsResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`,
);
return response.data;
}
public async getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse> {
const response = await stClient.get<GeneralverificationDetailResponse> (
public async getVrfcDetail(
param: number,
): Promise<GeneralverificationDetailResponse> {
const response = await stClient.get<GeneralverificationDetailResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
);
return response.data;
}
public async getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse> (
param.refStatsCd ?
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
:
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
public async getVerifyColumnList(
param: VerifyColumnListRequest,
): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse>(
param.refStatsCd
? `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
: `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
);
return response.data;
}
public async getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse> (
public async getSelectedVerifyColumnList(
param: string,
): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param}`,
);
return response.data;
}
public async RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async RegistVrfcDetail(
param: VerifyDetailRegistRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
param
param,
);
return response.data;
}
public async UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.put<RegistVrfcMasterResponse> (
public async UpdateVrfcDetail(
param: VerifyDetailUpdateRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.put<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
param
param,
);
return response.data;
}
public async CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async CopyVrfcData(
param: CopyVerifyDataRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`,
);
return response.data;
}
public async getVerifyResultList(
param: VerifyResultListRequest,
): Promise<VerifyResultListResponse> {
const response = await stClient.get<VerifyResultListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
{
params: {
...param.searchDto,
page: param.page,
size: param.size,
},
},
);
return response.data;
}
}
export const generalverificationApiService = new GeneralverificationApiService();
export const generalverificationApiService =
new GeneralverificationApiService();
@@ -1,14 +1,16 @@
import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
StatisticsInputStatusService,
StatsInputStatusListRequestParams,
StatsInputStatusListApiResponse,
StatsInputStatusListRequestParams,
} from "../../model/input/statistics-input-status-interface";
export class StatisticsInputStatusApiService implements StatisticsInputStatusService {
public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse> {
public async getStatisticsInputStatusList(
param: StatsInputStatusListRequestParams,
): Promise<StatsInputStatusListApiResponse> {
const response = await stClient.get<StatsInputStatusListApiResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`,
{ params: param },
@@ -17,4 +19,4 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer
}
}
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
+100 -1
View File
@@ -1,10 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
import type { VerifyResultPageData } from "@/entities/statistics/model/general-verification-interface";
import type {
StatisticsVerificationFilterState,
StatisticsVerificationSubFilterState,
} from "@/entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
import { stAnlsMstApi } from "./statistics-analysis-master-api";
import { stAnlsRsltApi } from "./statistics-analysis-result-api";
const DETAIL_PAGE_SIZE = 100;
interface StatisticsVerificationDetailSubmittedParams {
main: StatisticsVerificationFilterState;
sub: StatisticsVerificationSubFilterState;
}
export const useStAnlsSidoCombo = () => {
return useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
@@ -58,3 +72,88 @@ export const useStAnlsChartData = (
enabled: !!anlsSn && !!vrfcTrgtSn,
});
};
export const useStatsVrfcRptData = (
submittedMainFilters: StatisticsVerificationFilterState | null,
) => {
return useQuery({
queryKey: [
ENDPOINT.ST_SERVICE.INPUT,
"statistics-input-status",
submittedMainFilters?.statsYr ?? "",
submittedMainFilters?.ctpvNm ?? "",
submittedMainFilters?.sggNm ?? "",
submittedMainFilters?.sewerBzmnCd ?? "",
submittedMainFilters?.statsCd ?? "",
],
queryFn: async () => {
if (!submittedMainFilters) return [];
const response =
await StatisticsInputStatusApi.getStatisticsInputStatusList(
submittedMainFilters,
);
if (response.status !== "success")
throw new Error(response.message);
return response.data ?? [];
},
enabled: Boolean(submittedMainFilters),
refetchOnWindowFocus: false,
});
};
export const useStatsVrfcRsltData = (
submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null,
) => {
return useInfiniteQuery({
queryKey: [
ENDPOINT.ST_SERVICE.INPUTVERIFY,
"verify-results",
submittedDetailParams?.main.statsYr ?? "",
submittedDetailParams?.main.ctpvNm ?? "",
submittedDetailParams?.main.sggNm ?? "",
submittedDetailParams?.main.sewerBzmnCd ?? "",
submittedDetailParams?.main.statsCd ?? "",
submittedDetailParams?.sub.prcsYnList ?? "",
submittedDetailParams?.sub.pbcpAprvYn ?? "",
],
initialPageParam: 1,
queryFn: async ({ pageParam }) => {
if (!submittedDetailParams)
return {
verifyResultList: [],
pageInfo: {
currentPage: 0,
size: DETAIL_PAGE_SIZE,
totalElements: 0,
totalPages: 0,
},
} satisfies VerifyResultPageData;
const response =
await generalverificationApiService.getVerifyResultList({
page: pageParam,
size: DETAIL_PAGE_SIZE,
searchDto: {
statsYr: submittedDetailParams.main.statsYr,
ctpvNm: submittedDetailParams.main.ctpvNm,
sggNm: submittedDetailParams.main.sggNm,
sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd,
statsCd: submittedDetailParams.main.statsCd,
prcsYnList: submittedDetailParams.sub.prcsYnList,
pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn,
},
});
if (response.status !== "success")
throw new Error(response.message);
return response.data;
},
getNextPageParam: (lastPage) => {
const { currentPage, totalPages } = lastPage.pageInfo;
if (currentPage + 1 <= totalPages) return currentPage + 1;
return undefined;
},
enabled: Boolean(submittedDetailParams),
refetchOnWindowFocus: false,
});
};
@@ -0,0 +1,22 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
ApprovalRequest,
ApprovalResponse,
StatisticsApprovalService,
} from "../model/statistics-approval-interface";
class StatisticsApprovalApiService implements StatisticsApprovalService {
public async approveStats(
params: ApprovalRequest[],
): Promise<ApprovalResponse> {
const response = await stClient.post<ApprovalResponse>(
`${ENDPOINT.ST_SERVICE.APPROVAL}?type=VRFC&action=APPROVE`,
params,
);
return response.data;
}
}
export const statisticsApprovalApi = new StatisticsApprovalApiService();
@@ -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;
}
}
// 통합 인스턴스 수출
@@ -1,50 +1,69 @@
import { client, stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import { stClient, client } from "@/shared/api/client";
import {
type StatisticsService,
type SearchCondition,
type ApiResponse,
type Sgg,
type cancelSubmitRequest,
type cancelSubmitResponse,
type getReviewOpinionDetailRequest,
type getReviewOpinionDetailResponse,
type ImrRequest,
type SubmitExceptionRequest,
type SubmitExceptionResponse,
type ReviewOpinionSaveRequest,
type ReviewOpinionSaveResponse,
type ReviewReturnSaveRequest,
type ReviewReturnSaveResponse,
type ReviewSubmitRequest,
type ReviewSubmitResponse,
type SearchCondition,
type Sgg,
type StatisticsLogDetailResponse,
type StatisticsLogRequest,
type StatisticsLogResponse,
type StatisticsLogDetailResponse,
type StatisticsService,
type SubmitExceptionRequest,
type SubmitExceptionResponse,
} 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;
}
// 입력 - 제출 취소
public async cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse> {
public async cancelSubmit(
param: cancelSubmitRequest,
): Promise<cancelSubmitResponse> {
const response = await stClient.put<cancelSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/cancel-submit`,
param
param,
);
return response.data;
}
@@ -52,21 +71,25 @@ export class StatisticsAPiService implements StatisticsService {
public async callImrService(param: ImrRequest): Promise<null> {
const response = await client.post<null>(
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
param
param,
);
return response.data;
}
// 예외 제출
public async submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse> {
public async submitException(
param: SubmitExceptionRequest,
): Promise<SubmitExceptionResponse> {
const response = await stClient.post<SubmitExceptionResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/submit-exception`,
param
param,
);
return response.data;
}
public async getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse> {
public async getLogList(
param: StatisticsLogRequest,
): Promise<StatisticsLogResponse> {
const response = await stClient.get<StatisticsLogResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs`,
{ params: param },
@@ -74,12 +97,54 @@ export class StatisticsAPiService implements StatisticsService {
return response.data;
}
public async getLogDetail(param: string): Promise<StatisticsLogDetailResponse> {
public async getLogDetail(
param: string,
): Promise<StatisticsLogDetailResponse> {
const response = await stClient.get<StatisticsLogDetailResponse>(
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/logs/${param}`,
);
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();
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();
+11
View File
@@ -1,3 +1,10 @@
export {
useStAnlsSidoCombo,
useStAnlsSigunguCombo,
useStatsVrfcRptData,
useStatsVrfcRsltData,
} from "./api/queries";
export { statisticsApprovalApi } from "./api/statistics-approval-service";
export {
type AnalysisHistoryListApiResponse,
type AnalysisRequestPayload,
@@ -54,6 +61,10 @@ export {
} from "./model/statistics-analysis/statistics-analysis-result-interface";
export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns";
export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns";
export {
type ApprovalRequest,
type ApprovalResponse,
} from "./model/statistics-approval-interface";
export {
hbArticleColumnInfo,
imrArticleColumnInfo,
@@ -86,7 +86,7 @@ export interface VrfcOperatorsResponse {
opertSn: string;
opertSymbl: string;
opertKornNm: string;
}[]
}[];
}
/** 검증 상세 - 공통 정보 */
@@ -165,48 +165,133 @@ export interface SelectedVerifyColumnListResponseData {
export interface SelectedVerifyColumnListResponse {
status: string;
message?: string;
data: VerifyColumnListResponseData
data: VerifyColumnListResponseData;
}
export interface VerifyDetailRegistData {
trgtStatsCd: string;
refStatsCd: string | null;
kornArtopnEqtnCn: string,
engArtopnEqtnCn: string,
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
cmprOpert: string | null;
eqtnCombSeq: number
eqtnCombSeq: number;
}
export interface VerifyDetailRegistRequest {
vrfcSn: number | null;
detailList: VerifyDetailRegistData[]
detailList: VerifyDetailRegistData[];
rgtrId: string;
}
export interface VerifyDetailUpdateRequest {
vrfcSn: number | null;
detailList: VerifyDetailRegistData[]
mdfrId: string;
detailList: VerifyDetailRegistData[];
mdfrId: string;
}
export interface CopyVerifyDataRequest {
vrfcSn?: number | null;
targetStatsCd : string;
targetStatsCd: string;
rgtrId: string;
}
export interface VerifyResultSearchRequest {
sewerBzmnCd?: string | null;
ctpvNm?: string | null;
sggNm?: string | null;
statsYr: string;
statsCd?: string | null;
vrfcMthdCd?: string;
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
}
export interface VerifyResultListRequest {
searchDto: VerifyResultSearchRequest;
page?: number;
size?: number;
}
export interface VerifyResultItem {
statsYr?: string;
statsCd?: string;
statsNm?: string;
sewerBzmnCd?: string;
ctpvNm?: string;
sggNm?: string;
admdstNm?: string;
vrfcSn?: number;
vrfcMthdCd?: string;
vrfcMthdNm?: string;
vrfcTrgtNm?: string;
regDt?: string;
rsnExpln?: string;
nowVlCn?: string;
compVlCn?: string;
prcsYn?: string;
prcsOpnnExpln?: string;
prcrId?: string;
rjctOpnnCn?: string;
pbcpAprvYn?: string;
mdfrId?: string;
mdfcnDt?: string;
}
export interface VerifyResultPageInfo {
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface VerifyResultPageData {
verifyResultList: VerifyResultItem[];
pageInfo: VerifyResultPageInfo;
}
export interface VerifyResultListResponse {
status: string;
message?: string;
data: VerifyResultPageData;
code?: string;
httpStatus?: number;
}
export interface GeneralverificationService {
getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse>;
getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse>;
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
getGeneralverificationList(
param: GeneralverificationRequest,
): Promise<GeneralverificationResponse>;
getGeneralverificationInfo(
param: number,
): Promise<GeneralverificationInfoResponse>;
checkVerificationName(
param: CheckVerificationNameRequest,
): Promise<CheckVerificationNameResponse>;
UpdateVrfcMaster(
param: UpdateVrfcMasterRequest,
): Promise<UpdateVrfcMasterResponse>;
RegistVrfcMaster(
param: RegistVrfcMasterRequest,
): Promise<RegistVrfcMasterResponse>;
DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse>;
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse>;
getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse>;
RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse>;
UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse>;
CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse>;
}
getVerifyColumnList(
param: VerifyColumnListRequest,
): Promise<VerifyColumnListResponse>;
getSelectedVerifyColumnList(
param: string,
): Promise<VerifyColumnListResponse>;
RegistVrfcDetail(
param: VerifyDetailRegistRequest,
): Promise<RegistVrfcMasterResponse>;
UpdateVrfcDetail(
param: VerifyDetailUpdateRequest,
): Promise<RegistVrfcMasterResponse>;
CopyVrfcData(
param: CopyVerifyDataRequest,
): Promise<RegistVrfcMasterResponse>;
getVerifyResultList(
param: VerifyResultListRequest,
): Promise<VerifyResultListResponse>;
}
@@ -117,7 +117,7 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
header: { text: "당해년도" },
width: 100,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
dataFormat: "#,##0.##",
styleName: "right-column",
},
],
@@ -1,12 +1,11 @@
import type { ApiResponse } from "../statistics-interface";
export interface StatsInputStatusListRequestParams {
ctpvNm?: string;
sggNm?: string;
sewerBzmnCd?: string;
statsYr?: string;
mode?: string;
statsCd?: string;
ctpvNm?: string | null;
sggNm?: string | null;
sewerBzmnCd?: string | null;
statsYr: string;
statsCd?: string | null;
}
export interface StatsInputStatusList {
@@ -0,0 +1,22 @@
export interface ApprovalRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
rjctOpnnCn: string;
lastCfmtnPicNm: string;
lastCfmtnPicTelNo: string;
lastCfmtnPicDeptNm: string;
lastCfmtnPicJbgdNm: string;
}
export interface ApprovalResponse {
resultCode: string;
message: string;
updatedStts: string;
exceptionCnt: number;
}
export interface StatisticsApprovalService {
approveStats(params: ApprovalRequest[]): Promise<ApprovalResponse>;
}
@@ -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[];
}
@@ -4,68 +4,217 @@ const READ_ONLY_STYLE = "rg-cell-readonly";
export const statisticsVerificationSummaryColumnInfo: ColumnInfo = {
columns: [
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
{ name: "reportType", fieldName: "reportType", header: { text: "구분" }, styleName: READ_ONLY_STYLE },
{ name: "progressStatus", fieldName: "progressStatus", header: { text: "진행상태" }, width: 120, styleName: READ_ONLY_STYLE },
{ name: "writerName", fieldName: "writerName", header: { text: "작성자" }, styleName: READ_ONLY_STYLE },
{ name: "writerContact", fieldName: "writerContact", header: { text: "작성자연락처" }, width: 130, styleName: READ_ONLY_STYLE },
{ name: "writerDept", fieldName: "writerDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
{ name: "writerPosition", fieldName: "writerPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
{ name: "approverName", fieldName: "approverName", header: { text: "승인자" }, styleName: READ_ONLY_STYLE },
{ name: "approverDept", fieldName: "approverDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
{ name: "approverPosition", fieldName: "approverPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
{ name: "returnReason", fieldName: "returnReason", header: { text: "반려사유" }, width: 220, styleName: READ_ONLY_STYLE },
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
styleName: READ_ONLY_STYLE,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
styleName: READ_ONLY_STYLE,
},
{
name: "menuTabName",
fieldName: "menuTabName",
header: { text: "구분" },
width: 150,
styleName: READ_ONLY_STYLE,
},
{
name: "locgStatusNm",
fieldName: "locgStatusNm",
header: { text: "진행상태" },
width: 120,
styleName: READ_ONLY_STYLE,
},
{
name: "laePicNm",
fieldName: "laePicNm",
header: { text: "작성자" },
styleName: READ_ONLY_STYLE,
},
{
name: "laePicTelNo",
fieldName: "laePicTelNo",
header: { text: "작성자연락처" },
width: 130,
styleName: READ_ONLY_STYLE,
},
{
name: "laeKbrdrDeptNm",
fieldName: "laeKbrdrDeptNm",
header: { text: "부서" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeKbrdrJbgdNm",
fieldName: "laeKbrdrJbgdNm",
header: { text: "직급" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrNm",
fieldName: "laeIdfrNm",
header: { text: "승인자" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrDeptNm",
fieldName: "laeIdfrDeptNm",
header: { text: "부서" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrJbgdNm",
fieldName: "laeIdfrJbgdNm",
header: { text: "직급" },
styleName: READ_ONLY_STYLE,
},
{
name: "rjctOpnnCn",
fieldName: "rjctOpnnCn",
header: { text: "반려사유" },
width: 220,
styleName: READ_ONLY_STYLE,
},
{
name: "sewerBzmnCd",
fieldName: "sewerBzmnCd",
header: { text: "sewerBzmnCd" },
visible: false,
},
{
name: "statsCd",
fieldName: "statsCd",
header: { text: "statsCd" },
visible: false,
},
],
columnLayout: [
"summaryId",
"sido",
"sigungu",
"reportType",
"progressStatus",
"writerName",
"writerContact",
"writerDept",
"writerPosition",
"approverName",
"approverDept",
"approverPosition",
"returnReason",
"ctpvNm",
"sggNm",
"menuTabName",
"locgStatusNm",
"laePicNm",
"laePicTelNo",
"laeKbrdrDeptNm",
"laeKbrdrJbgdNm",
"laeIdfrNm",
"laeIdfrDeptNm",
"laeIdfrJbgdNm",
"rjctOpnnCn",
"sewerBzmnCd",
"statsCd",
],
};
export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
columns: [
{ name: "detailId", fieldName: "detailId", header: { text: "detailId" }, visible: false },
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
{ name: "indicator", fieldName: "indicator", header: { text: "지표" }, width: 140, styleName: READ_ONLY_STYLE },
{ name: "detailItem", fieldName: "detailItem", header: { text: "세부항목" }, width: 180, styleName: READ_ONLY_STYLE },
{ name: "verifyDate", fieldName: "verifyDate", header: { text: "검증날짜" }, width: 110, styleName: READ_ONLY_STYLE },
{ name: "errorDetail", fieldName: "errorDetail", header: { text: "오류내역" }, width: 200, styleName: READ_ONLY_STYLE },
{ name: "inputValue", fieldName: "inputValue", header: { text: "입력값" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "compareValue", fieldName: "compareValue", header: { text: "비교값" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "status", fieldName: "status", header: { text: "상태" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "exceptionReason", fieldName: "exceptionReason", header: { text: "예외사유" }, width: 170, styleName: READ_ONLY_STYLE },
{ name: "returnOpinion", fieldName: "returnOpinion", header: { text: "반려의견" }, width: 170, styleName: READ_ONLY_STYLE },
{ name: "approvalYn", fieldName: "approvalYn", header: { text: "승인여부" }, width: 90, styleName: READ_ONLY_STYLE },
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
styleName: READ_ONLY_STYLE,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
styleName: READ_ONLY_STYLE,
},
{
name: "statsNm",
fieldName: "statsNm",
header: { text: "지표" },
width: 140,
styleName: READ_ONLY_STYLE,
},
{
name: "vrfcTrgtNm",
fieldName: "vrfcTrgtNm",
header: { text: "세부항목" },
width: 180,
styleName: READ_ONLY_STYLE,
},
{
name: "regDt",
fieldName: "regDt",
header: { text: "검증날짜" },
width: 150,
styleName: READ_ONLY_STYLE,
},
{
name: "rsnExpln",
fieldName: "rsnExpln",
header: { text: "오류내역" },
width: 200,
styleName: READ_ONLY_STYLE,
},
{
name: "nowVlCn",
fieldName: "nowVlCn",
header: { text: "입력값" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "compVlCn",
fieldName: "compVlCn",
header: { text: "비교값" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "prcsYn",
fieldName: "prcsYn",
header: { text: "상태" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "prcsOpnnExpln",
fieldName: "prcsOpnnExpln",
header: { text: "예외사유" },
width: 170,
styleName: READ_ONLY_STYLE,
},
{
name: "rjctOpnnCn",
fieldName: "rjctOpnnCn",
header: { text: "반려의견" },
width: 170,
styleName: READ_ONLY_STYLE,
},
{
name: "pbcpAprvYn",
fieldName: "pbcpAprvYn",
header: { text: "승인여부" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "vrfcSn",
fieldName: "vrfcSn",
header: { text: "vrfcSn" },
visible: false,
},
],
columnLayout: [
"detailId",
"summaryId",
"sido",
"sigungu",
"indicator",
"detailItem",
"verifyDate",
"errorDetail",
"inputValue",
"compareValue",
"status",
"exceptionReason",
"returnOpinion",
"approvalYn",
"ctpvNm",
"sggNm",
"statsNm",
"vrfcTrgtNm",
"regDt",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYn",
"prcsOpnnExpln",
"rjctOpnnCn",
"pbcpAprvYn",
"vrfcSn",
],
};
@@ -1,14 +1,10 @@
export interface StatisticsVerificationFilterState {
sido: string;
sigungu: string;
year: string;
statsIndicator: string;
}
import type { VerifyResultSearchRequest } from "../general-verification-interface";
import type { StatsInputStatusListRequestParams } from "../input/statistics-input-status-interface";
export interface StatisticsVerificationSubFilterState {
status: string;
approvalStatus: string;
}
export type StatisticsVerificationFilterState =
StatsInputStatusListRequestParams;
export type StatisticsVerificationSubFilterState = VerifyResultSearchRequest;
export interface StatisticsVerificationOption {
label: string;
+2 -1
View File
@@ -72,7 +72,8 @@ export const ENDPOINT = {
REVIEW: "/st-management-service/api/v1/review",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats"
TOTAL: "/st-management-service/api/v1/stats",
APPROVAL: "/st-management-service/api/stats/approval",
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
+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;
+41 -41
View File
@@ -1,44 +1,44 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
"exclude": ["**/node_modules", "**/.*/*"]
}
+1
View File
@@ -26,6 +26,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@base-ui/react": "^1.4.0",
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/config": "workspace:*",
@@ -0,0 +1,315 @@
"use client";
import * as React from "react";
import {
Combobox as ComboboxPrimitive,
type ComboboxRootProps,
} from "@base-ui/react";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "../input-group";
const Combobox = ({ ...props }: ComboboxRootProps<string, boolean>) => {
return <ComboboxPrimitive.Root {...props} />;
};
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
);
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
className={cn(className)}
{...props}
render={
<InputGroupButton variant="ghost" size="icon-xs">
<XIcon className="pointer-events-none" />
</InputGroupButton>
}
/>
);
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean;
showClear?: boolean;
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"group/combobox-content max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) ring-foreground/10 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative min-w-[calc(var(--anchor-width)+--spacing(7))] overflow-hidden rounded-sm border-input bg-popover text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none",
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar data-empty:p-0 max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain rounded-sm border border-input p-1",
className,
)}
{...props}
/>
);
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1 pl-1.5 pr-16 text-[1.2rem] hover:bg-accent [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<CheckIcon className="pointer-events-none" />
</span>
}
/>
</ComboboxPrimitive.Item>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
);
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
);
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
);
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"group-data-empty/combobox-content:flex hidden w-full justify-center py-2 text-center text-sm text-muted-foreground",
className,
)}
{...props}
/>
);
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40 flex min-h-[3.2rem] !w-full flex-wrap items-center gap-1 rounded-sm border bg-form bg-clip-padding px-2 py-1.5 text-sm transition-colors focus-within:border-ring",
className,
)}
{...props}
/>
);
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean;
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0 flex h-[2.4rem] w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded-sm bg-muted px-2.5 text-[1.2rem] font-medium text-foreground",
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
render={
<Button
variant="ghost"
size="icon"
className="size-6 !bg-transparent"
>
<XIcon className="pointer-events-none !size-5" />
</Button>
}
/>
)}
</ComboboxPrimitive.Chip>
);
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn(
"h-full min-w-16 flex-1 text-[1.3rem] outline-none",
className,
)}
{...props}
/>
);
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
};
@@ -5,6 +5,7 @@ export * from "./button-group";
export * from "./calendar";
export * from "./carousel";
export * from "./checkbox";
export * from "./combobox";
export * from "./data-table";
export * from "./date-picker";
export * from "./input-date-picker";
@@ -0,0 +1,156 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import { Input } from "../input";
import { Textarea } from "../textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input outline-none transition-colors has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"aria-invalid:ring-0 flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"aria-invalid:ring-0 flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};