Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
+74
-74
@@ -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) => {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "민원",
|
||||
};
|
||||
+81
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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>;
|
||||
}
|
||||
+4
-3
@@ -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 {
|
||||
|
||||
+61
@@ -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[];
|
||||
}
|
||||
+203
-54
@@ -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",
|
||||
],
|
||||
};
|
||||
|
||||
+6
-10
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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", "**/.*/*"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user