[st] feat: 통계 검증 리포트 화면 작업

This commit is contained in:
2026-04-20 16:08:14 +09:00
parent dbcbd5f8b2
commit 16c8fe716d
22 changed files with 2362 additions and 1111 deletions
+74 -74
View File
@@ -1,76 +1,76 @@
{
"name": "st",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@rsone/components": "workspace:*",
"@faker-js/faker": "^9.9.0",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/xlsx": "^0.0.36",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.474.0",
"next": "15.3.8",
"next-qrcode": "^2.5.1",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"realgrid": "^2.9.5",
"realgrid-react": "^0.9.7",
"remixicon": "^4.7.0",
"sonner": "^2.0.1",
"swr": "^2.3.7",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.3.8",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
"name": "st",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/components": "workspace:*",
"@common/config": "workspace:*",
"@common/network": "workspace:*",
"@rsone/components": "workspace:*",
"@faker-js/faker": "^9.9.0",
"@fontsource-variable/noto-sans-kr": "^5.2.5",
"@frontend-opensource/use-react-hooks": "^1.1.4",
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/interaction": "^6.1.17",
"@fullcalendar/react": "^6.1.17",
"@fullcalendar/timegrid": "^6.1.17",
"@hookform/resolvers": "^4.1.3",
"@minoru/react-dnd-treeview": "3.5.1-alpha.1",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^5.1.1",
"@types/xlsx": "^0.0.36",
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13",
"js-cookie": "^3.0.5",
"jszip": "^3.10.1",
"lucide-react": "^0.474.0",
"next": "15.3.8",
"next-qrcode": "^2.5.1",
"next-themes": "^0.4.6",
"react-chartjs-2": "^5.3.0",
"react-dnd": "^16.0.1",
"react-hook-form": "^7.54.2",
"realgrid": "^2.9.5",
"realgrid-react": "^0.9.7",
"remixicon": "^4.7.0",
"sonner": "^2.0.1",
"swr": "^2.3.7",
"tailwindcss-animate": "^1.0.7",
"tus-js-client": "^4.3.1",
"xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"eslint": "^9",
"eslint-config-next": "15.3.8",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"steiger": "^0.5.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
+105 -51
View File
@@ -321,10 +321,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 +344,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(() => {
@@ -558,10 +571,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 +650,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 +774,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 +950,7 @@ const DataGrid = ({
) {
(
column.footer[
index
index
] as FooterItemWithCallback
).valueCallback =
footerItem.valueCallback;
@@ -990,11 +1003,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 +1103,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 +1159,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 +1213,7 @@ const DataGrid = ({
</Button>
)}
</div>
<div className="h-[65vh]">
<div style={{ height: `${gridHeight}vh` }}>
<RealGridReact
ref={gridRef}
autoGenerateField={true}
@@ -1176,24 +1230,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 +1400,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,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;
+91 -75
View File
@@ -18,30 +18,30 @@ export type RealGridColumnInfo = {
mergeRule?: { criteria: "value" | string };
sortable?: boolean;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>;
};
/** setColumns 메서드가 있는 GridView */
@@ -110,21 +110,21 @@ export type DataFieldValueCallback = (
export type ComputedFieldConfig =
| { fields: string[]; type: "sum" | "avg" }
| {
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "pblswrgPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
numeratorFields: string[];
denominatorFields: string[];
type: "adtrtPopltnSuprtRate";
}
| {
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
numeratorFields: string[];
denominatorFields: string[];
thirdFields: string[];
type: "swrgInstlrtRate";
};
export type ColumnConfig = {
name: string;
@@ -147,31 +147,31 @@ export type ColumnConfig = {
valueType?: ValueType;
dataFormat?: string;
footer?:
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
| {
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
multiRow?: boolean; // 여러 행 footer 사용 여부
}
| Array<{
text?: string;
expression?: "sum" | "avg" | "count" | "max" | "min";
numberFormat?: string;
valueCallback?: (
grid: GridView,
column: DataColumn,
footerIndex: number,
columnFooter: ColumnFooter,
value: number | string | undefined,
) => number | string;
}>; // RealGrid에서 footer를 배열로 설정할 수 있음
};
export type FooterUserSpan = {
@@ -183,16 +183,16 @@ export type FooterUserSpan = {
export type ColumnLayoutItem =
| string
| {
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
name: string;
hideChildHeaders?: boolean;
header: { text: string };
items: (string | ColumnLayoutItem)[];
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
}
| {
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
column: string; // 컬럼 이름 (footerUserSpans 사용 시)
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
};
export type ColumnInfo = {
columns: ColumnConfig[];
@@ -202,7 +202,10 @@ export type ColumnInfo = {
export type GridTopButtonConfig = {
visible?: boolean;
text: string;
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
onClick?: (helpers: {
openDialog: (title?: string) => void;
closeDialog: () => void;
}) => void;
};
export type ColumnInfoList = {
@@ -225,11 +228,18 @@ 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;
@@ -252,6 +262,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 +278,7 @@ export type DataGridProps = {
compactFullWidth?: boolean;
/** CompactDataGrid 전용: 그리드 영역 높이 Tailwind 클래스 */
compactGridHeightClass?: string;
gridHeight?: number;
};
export type TabDataGridProps = {
@@ -1,127 +1,169 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
import type {
CheckVerificationNameRequest,
CheckVerificationNameResponse,
CopyVerifyDataRequest,
DeleteVrfcMasterResponse,
GeneralverificationDetailResponse,
GeneralverificationInfoResponse,
GeneralverificationRequest,
GeneralverificationResponse,
GeneralverificationService,
GeneralverificationInfoResponse,
CheckVerificationNameRequest,
CheckVerificationNameResponse,
UpdateVrfcMasterRequest,
UpdateVrfcMasterResponse,
RegistVrfcMasterRequest,
RegistVrfcMasterResponse,
DeleteVrfcMasterResponse,
VrfcOperatorsResponse,
GeneralverificationDetailResponse,
UpdateVrfcMasterRequest,
UpdateVrfcMasterResponse,
VerifyColumnListRequest,
VerifyColumnListResponse,
VerifyDetailRegistRequest,
VerifyDetailUpdateRequest,
CopyVerifyDataRequest,
VerifyResultListRequest,
VerifyResultListResponse,
VrfcOperatorsResponse,
} from "../model/general-verification-interface";
export class GeneralverificationApiService implements GeneralverificationService {
public async getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse> {
const response = await stClient.get<GeneralverificationResponse> (
public async getGeneralverificationList(
param: GeneralverificationRequest,
): Promise<GeneralverificationResponse> {
const response = await stClient.get<GeneralverificationResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`,
{ params: param },
);
return response.data;
}
public async getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse> {
const response = await stClient.get<GeneralverificationInfoResponse> (
public async getGeneralverificationInfo(
param: number,
): Promise<GeneralverificationInfoResponse> {
const response = await stClient.get<GeneralverificationInfoResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`,
);
return response.data;
}
public async checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse> {
const response = await stClient.get<CheckVerificationNameResponse> (
public async checkVerificationName(
param: CheckVerificationNameRequest,
): Promise<CheckVerificationNameResponse> {
const response = await stClient.get<CheckVerificationNameResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`,
{ params: param },
);
return response.data;
}
public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse> {
const response = await stClient.put<UpdateVrfcMasterResponse> (
public async UpdateVrfcMaster(
param: UpdateVrfcMasterRequest,
): Promise<UpdateVrfcMasterResponse> {
const response = await stClient.put<UpdateVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`,
param,
);
return response.data;
}
public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async RegistVrfcMaster(
param: RegistVrfcMasterRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`,
param,
);
return response.data;
}
public async DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse> (
public async DeleteVrfcMaster(
param: number,
): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
);
return response.data;
}
public async getVrfcOperators(): Promise<VrfcOperatorsResponse> {
const response = await stClient.get<VrfcOperatorsResponse> (
const response = await stClient.get<VrfcOperatorsResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`,
);
return response.data;
}
public async getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse> {
const response = await stClient.get<GeneralverificationDetailResponse> (
public async getVrfcDetail(
param: number,
): Promise<GeneralverificationDetailResponse> {
const response = await stClient.get<GeneralverificationDetailResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
);
return response.data;
}
public async getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse> (
param.refStatsCd ?
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
:
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
public async getVerifyColumnList(
param: VerifyColumnListRequest,
): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse>(
param.refStatsCd
? `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
: `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
);
return response.data;
}
public async getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse> (
public async getSelectedVerifyColumnList(
param: string,
): Promise<VerifyColumnListResponse> {
const response = await stClient.get<VerifyColumnListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param}`,
);
return response.data;
}
public async RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async RegistVrfcDetail(
param: VerifyDetailRegistRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
param
param,
);
return response.data;
}
public async UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.put<RegistVrfcMasterResponse> (
public async UpdateVrfcDetail(
param: VerifyDetailUpdateRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.put<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
param
param,
);
return response.data;
}
public async CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
public async CopyVrfcData(
param: CopyVerifyDataRequest,
): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`,
);
return response.data;
}
public async getVerifyResultList(
param: VerifyResultListRequest,
): Promise<VerifyResultListResponse> {
const response = await stClient.get<VerifyResultListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
{
params: {
...param.searchDto,
page: param.page,
size: param.size,
},
},
);
return response.data;
}
}
export const generalverificationApiService = new GeneralverificationApiService();
export const generalverificationApiService =
new GeneralverificationApiService();
@@ -1,14 +1,16 @@
import { ENDPOINT } from "@/shared/config";
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
StatisticsInputStatusService,
StatsInputStatusListRequestParams,
StatsInputStatusListApiResponse,
StatsInputStatusListRequestParams,
} from "../../model/input/statistics-input-status-interface";
export class StatisticsInputStatusApiService implements StatisticsInputStatusService {
public async getStatisticsInputStatusList(param: StatsInputStatusListRequestParams): Promise<StatsInputStatusListApiResponse> {
public async getStatisticsInputStatusList(
param: StatsInputStatusListRequestParams,
): Promise<StatsInputStatusListApiResponse> {
const response = await stClient.get<StatsInputStatusListApiResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status`,
{ params: param },
@@ -17,4 +19,4 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer
}
}
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
export const StatisticsInputStatusApi = new StatisticsInputStatusApiService();
+100 -1
View File
@@ -1,10 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
import type { VerifyResultPageData } from "@/entities/statistics/model/general-verification-interface";
import type {
StatisticsVerificationFilterState,
StatisticsVerificationSubFilterState,
} from "@/entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
import { stAnlsMstApi } from "./statistics-analysis-master-api";
import { stAnlsRsltApi } from "./statistics-analysis-result-api";
const DETAIL_PAGE_SIZE = 100;
interface StatisticsVerificationDetailSubmittedParams {
main: StatisticsVerificationFilterState;
sub: StatisticsVerificationSubFilterState;
}
export const useStAnlsSidoCombo = () => {
return useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
@@ -58,3 +72,88 @@ export const useStAnlsChartData = (
enabled: !!anlsSn && !!vrfcTrgtSn,
});
};
export const useStatsVrfcRptData = (
submittedMainFilters: StatisticsVerificationFilterState | null,
) => {
return useQuery({
queryKey: [
ENDPOINT.ST_SERVICE.INPUT,
"statistics-input-status",
submittedMainFilters?.statsYr ?? "",
submittedMainFilters?.ctpvNm ?? "",
submittedMainFilters?.sggNm ?? "",
submittedMainFilters?.sewerBzmnCd ?? "",
submittedMainFilters?.statsCd ?? "",
],
queryFn: async () => {
if (!submittedMainFilters) return [];
const response =
await StatisticsInputStatusApi.getStatisticsInputStatusList(
submittedMainFilters,
);
if (response.status !== "success")
throw new Error(response.message);
return response.data ?? [];
},
enabled: Boolean(submittedMainFilters),
refetchOnWindowFocus: false,
});
};
export const useStatsVrfcRsltData = (
submittedDetailParams: StatisticsVerificationDetailSubmittedParams | null,
) => {
return useInfiniteQuery({
queryKey: [
ENDPOINT.ST_SERVICE.INPUTVERIFY,
"verify-results",
submittedDetailParams?.main.statsYr ?? "",
submittedDetailParams?.main.ctpvNm ?? "",
submittedDetailParams?.main.sggNm ?? "",
submittedDetailParams?.main.sewerBzmnCd ?? "",
submittedDetailParams?.main.statsCd ?? "",
submittedDetailParams?.sub.prcsYnList ?? "",
submittedDetailParams?.sub.pbcpAprvYn ?? "",
],
initialPageParam: 1,
queryFn: async ({ pageParam }) => {
if (!submittedDetailParams)
return {
verifyResultList: [],
pageInfo: {
currentPage: 0,
size: DETAIL_PAGE_SIZE,
totalElements: 0,
totalPages: 0,
},
} satisfies VerifyResultPageData;
const response =
await generalverificationApiService.getVerifyResultList({
page: pageParam,
size: DETAIL_PAGE_SIZE,
searchDto: {
statsYr: submittedDetailParams.main.statsYr,
ctpvNm: submittedDetailParams.main.ctpvNm,
sggNm: submittedDetailParams.main.sggNm,
sewerBzmnCd: submittedDetailParams.main.sewerBzmnCd,
statsCd: submittedDetailParams.main.statsCd,
prcsYnList: submittedDetailParams.sub.prcsYnList,
pbcpAprvYn: submittedDetailParams.sub.pbcpAprvYn,
},
});
if (response.status !== "success")
throw new Error(response.message);
return response.data;
},
getNextPageParam: (lastPage) => {
const { currentPage, totalPages } = lastPage.pageInfo;
if (currentPage + 1 <= totalPages) return currentPage + 1;
return undefined;
},
enabled: Boolean(submittedDetailParams),
refetchOnWindowFocus: false,
});
};
@@ -0,0 +1,22 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
ApprovalRequest,
ApprovalResponse,
StatisticsApprovalService,
} from "../model/statistics-approval-interface";
class StatisticsApprovalApiService implements StatisticsApprovalService {
public async approveStats(
params: ApprovalRequest[],
): Promise<ApprovalResponse> {
const response = await stClient.post<ApprovalResponse>(
`${ENDPOINT.ST_SERVICE.APPROVAL}?type=VRFC&action=APPROVE`,
params,
);
return response.data;
}
}
export const statisticsApprovalApi = new StatisticsApprovalApiService();
@@ -1,19 +1,19 @@
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 ImrRequest,
type SubmitExceptionRequest,
type SubmitExceptionResponse,
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 {
@@ -41,10 +41,12 @@ export class StatisticsAPiService implements StatisticsService {
}
// 입력 - 제출 취소
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 +54,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,7 +80,9 @@ 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}`,
);
@@ -82,4 +90,4 @@ export class StatisticsAPiService implements StatisticsService {
}
}
export const statisticsAPi = new StatisticsAPiService();
export const statisticsAPi = new StatisticsAPiService();
+11
View File
@@ -1,3 +1,10 @@
export {
useStAnlsSidoCombo,
useStAnlsSigunguCombo,
useStatsVrfcRptData,
useStatsVrfcRsltData,
} from "./api/queries";
export { statisticsApprovalApi } from "./api/statistics-approval-service";
export {
type AnalysisHistoryListApiResponse,
type AnalysisRequestPayload,
@@ -54,6 +61,10 @@ export {
} from "./model/statistics-analysis/statistics-analysis-result-interface";
export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns";
export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns";
export {
type ApprovalRequest,
type ApprovalResponse,
} from "./model/statistics-approval-interface";
export {
hbArticleColumnInfo,
imrArticleColumnInfo,
@@ -86,7 +86,7 @@ export interface VrfcOperatorsResponse {
opertSn: string;
opertSymbl: string;
opertKornNm: string;
}[]
}[];
}
/** 검증 상세 - 공통 정보 */
@@ -165,48 +165,133 @@ export interface SelectedVerifyColumnListResponseData {
export interface SelectedVerifyColumnListResponse {
status: string;
message?: string;
data: VerifyColumnListResponseData
data: VerifyColumnListResponseData;
}
export interface VerifyDetailRegistData {
trgtStatsCd: string;
refStatsCd: string | null;
kornArtopnEqtnCn: string,
engArtopnEqtnCn: string,
kornArtopnEqtnCn: string;
engArtopnEqtnCn: string;
cmprOpert: string | null;
eqtnCombSeq: number
eqtnCombSeq: number;
}
export interface VerifyDetailRegistRequest {
vrfcSn: number | null;
detailList: VerifyDetailRegistData[]
detailList: VerifyDetailRegistData[];
rgtrId: string;
}
export interface VerifyDetailUpdateRequest {
vrfcSn: number | null;
detailList: VerifyDetailRegistData[]
mdfrId: string;
detailList: VerifyDetailRegistData[];
mdfrId: string;
}
export interface CopyVerifyDataRequest {
vrfcSn?: number | null;
targetStatsCd : string;
targetStatsCd: string;
rgtrId: string;
}
export interface VerifyResultSearchRequest {
sewerBzmnCd?: string | null;
ctpvNm?: string | null;
sggNm?: string | null;
statsYr: string;
statsCd?: string | null;
vrfcMthdCd?: string;
prcsYnList?: string; // 검증 상태 리스트 (0: 미검증, 1: 수정완료, 2:예외통과, 3: 반려)
pbcpAprvYn?: string; // 공단 승인 여부 (Y/N)
}
export interface VerifyResultListRequest {
searchDto: VerifyResultSearchRequest;
page?: number;
size?: number;
}
export interface VerifyResultItem {
statsYr?: string;
statsCd?: string;
statsNm?: string;
sewerBzmnCd?: string;
ctpvNm?: string;
sggNm?: string;
admdstNm?: string;
vrfcSn?: number;
vrfcMthdCd?: string;
vrfcMthdNm?: string;
vrfcTrgtNm?: string;
regDt?: string;
rsnExpln?: string;
nowVlCn?: string;
compVlCn?: string;
prcsYn?: string;
prcsOpnnExpln?: string;
prcrId?: string;
rjctOpnnCn?: string;
pbcpAprvYn?: string;
mdfrId?: string;
mdfcnDt?: string;
}
export interface VerifyResultPageInfo {
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface VerifyResultPageData {
verifyResultList: VerifyResultItem[];
pageInfo: VerifyResultPageInfo;
}
export interface VerifyResultListResponse {
status: string;
message?: string;
data: VerifyResultPageData;
code?: string;
httpStatus?: number;
}
export interface GeneralverificationService {
getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse>;
getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse>;
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
getGeneralverificationList(
param: GeneralverificationRequest,
): Promise<GeneralverificationResponse>;
getGeneralverificationInfo(
param: number,
): Promise<GeneralverificationInfoResponse>;
checkVerificationName(
param: CheckVerificationNameRequest,
): Promise<CheckVerificationNameResponse>;
UpdateVrfcMaster(
param: UpdateVrfcMasterRequest,
): Promise<UpdateVrfcMasterResponse>;
RegistVrfcMaster(
param: RegistVrfcMasterRequest,
): Promise<RegistVrfcMasterResponse>;
DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse>;
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse>;
getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse>;
RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse>;
UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse>;
CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse>;
}
getVerifyColumnList(
param: VerifyColumnListRequest,
): Promise<VerifyColumnListResponse>;
getSelectedVerifyColumnList(
param: string,
): Promise<VerifyColumnListResponse>;
RegistVrfcDetail(
param: VerifyDetailRegistRequest,
): Promise<RegistVrfcMasterResponse>;
UpdateVrfcDetail(
param: VerifyDetailUpdateRequest,
): Promise<RegistVrfcMasterResponse>;
CopyVrfcData(
param: CopyVerifyDataRequest,
): Promise<RegistVrfcMasterResponse>;
getVerifyResultList(
param: VerifyResultListRequest,
): Promise<VerifyResultListResponse>;
}
@@ -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>;
}
@@ -4,68 +4,217 @@ const READ_ONLY_STYLE = "rg-cell-readonly";
export const statisticsVerificationSummaryColumnInfo: ColumnInfo = {
columns: [
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
{ name: "reportType", fieldName: "reportType", header: { text: "구분" }, styleName: READ_ONLY_STYLE },
{ name: "progressStatus", fieldName: "progressStatus", header: { text: "진행상태" }, width: 120, styleName: READ_ONLY_STYLE },
{ name: "writerName", fieldName: "writerName", header: { text: "작성자" }, styleName: READ_ONLY_STYLE },
{ name: "writerContact", fieldName: "writerContact", header: { text: "작성자연락처" }, width: 130, styleName: READ_ONLY_STYLE },
{ name: "writerDept", fieldName: "writerDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
{ name: "writerPosition", fieldName: "writerPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
{ name: "approverName", fieldName: "approverName", header: { text: "승인자" }, styleName: READ_ONLY_STYLE },
{ name: "approverDept", fieldName: "approverDept", header: { text: "부서" }, styleName: READ_ONLY_STYLE },
{ name: "approverPosition", fieldName: "approverPosition", header: { text: "직급" }, styleName: READ_ONLY_STYLE },
{ name: "returnReason", fieldName: "returnReason", header: { text: "반려사유" }, width: 220, styleName: READ_ONLY_STYLE },
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
styleName: READ_ONLY_STYLE,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
styleName: READ_ONLY_STYLE,
},
{
name: "menuTabName",
fieldName: "menuTabName",
header: { text: "구분" },
width: 150,
styleName: READ_ONLY_STYLE,
},
{
name: "locgStatusNm",
fieldName: "locgStatusNm",
header: { text: "진행상태" },
width: 120,
styleName: READ_ONLY_STYLE,
},
{
name: "laePicNm",
fieldName: "laePicNm",
header: { text: "작성자" },
styleName: READ_ONLY_STYLE,
},
{
name: "laePicTelNo",
fieldName: "laePicTelNo",
header: { text: "작성자연락처" },
width: 130,
styleName: READ_ONLY_STYLE,
},
{
name: "laeKbrdrDeptNm",
fieldName: "laeKbrdrDeptNm",
header: { text: "부서" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeKbrdrJbgdNm",
fieldName: "laeKbrdrJbgdNm",
header: { text: "직급" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrNm",
fieldName: "laeIdfrNm",
header: { text: "승인자" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrDeptNm",
fieldName: "laeIdfrDeptNm",
header: { text: "부서" },
styleName: READ_ONLY_STYLE,
},
{
name: "laeIdfrJbgdNm",
fieldName: "laeIdfrJbgdNm",
header: { text: "직급" },
styleName: READ_ONLY_STYLE,
},
{
name: "rjctOpnnCn",
fieldName: "rjctOpnnCn",
header: { text: "반려사유" },
width: 220,
styleName: READ_ONLY_STYLE,
},
{
name: "sewerBzmnCd",
fieldName: "sewerBzmnCd",
header: { text: "sewerBzmnCd" },
visible: false,
},
{
name: "statsCd",
fieldName: "statsCd",
header: { text: "statsCd" },
visible: false,
},
],
columnLayout: [
"summaryId",
"sido",
"sigungu",
"reportType",
"progressStatus",
"writerName",
"writerContact",
"writerDept",
"writerPosition",
"approverName",
"approverDept",
"approverPosition",
"returnReason",
"ctpvNm",
"sggNm",
"menuTabName",
"locgStatusNm",
"laePicNm",
"laePicTelNo",
"laeKbrdrDeptNm",
"laeKbrdrJbgdNm",
"laeIdfrNm",
"laeIdfrDeptNm",
"laeIdfrJbgdNm",
"rjctOpnnCn",
"sewerBzmnCd",
"statsCd",
],
};
export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
columns: [
{ name: "detailId", fieldName: "detailId", header: { text: "detailId" }, visible: false },
{ name: "summaryId", fieldName: "summaryId", header: { text: "summaryId" }, visible: false },
{ name: "sido", fieldName: "sido", header: { text: "시도" }, styleName: READ_ONLY_STYLE },
{ name: "sigungu", fieldName: "sigungu", header: { text: "시군구" }, styleName: READ_ONLY_STYLE },
{ name: "indicator", fieldName: "indicator", header: { text: "지표" }, width: 140, styleName: READ_ONLY_STYLE },
{ name: "detailItem", fieldName: "detailItem", header: { text: "세부항목" }, width: 180, styleName: READ_ONLY_STYLE },
{ name: "verifyDate", fieldName: "verifyDate", header: { text: "검증날짜" }, width: 110, styleName: READ_ONLY_STYLE },
{ name: "errorDetail", fieldName: "errorDetail", header: { text: "오류내역" }, width: 200, styleName: READ_ONLY_STYLE },
{ name: "inputValue", fieldName: "inputValue", header: { text: "입력값" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "compareValue", fieldName: "compareValue", header: { text: "비교값" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "status", fieldName: "status", header: { text: "상태" }, width: 90, styleName: READ_ONLY_STYLE },
{ name: "exceptionReason", fieldName: "exceptionReason", header: { text: "예외사유" }, width: 170, styleName: READ_ONLY_STYLE },
{ name: "returnOpinion", fieldName: "returnOpinion", header: { text: "반려의견" }, width: 170, styleName: READ_ONLY_STYLE },
{ name: "approvalYn", fieldName: "approvalYn", header: { text: "승인여부" }, width: 90, styleName: READ_ONLY_STYLE },
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
styleName: READ_ONLY_STYLE,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
styleName: READ_ONLY_STYLE,
},
{
name: "statsNm",
fieldName: "statsNm",
header: { text: "지표" },
width: 140,
styleName: READ_ONLY_STYLE,
},
{
name: "vrfcTrgtNm",
fieldName: "vrfcTrgtNm",
header: { text: "세부항목" },
width: 180,
styleName: READ_ONLY_STYLE,
},
{
name: "regDt",
fieldName: "regDt",
header: { text: "검증날짜" },
width: 150,
styleName: READ_ONLY_STYLE,
},
{
name: "rsnExpln",
fieldName: "rsnExpln",
header: { text: "오류내역" },
width: 200,
styleName: READ_ONLY_STYLE,
},
{
name: "nowVlCn",
fieldName: "nowVlCn",
header: { text: "입력값" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "compVlCn",
fieldName: "compVlCn",
header: { text: "비교값" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "prcsYn",
fieldName: "prcsYn",
header: { text: "상태" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "prcsOpnnExpln",
fieldName: "prcsOpnnExpln",
header: { text: "예외사유" },
width: 170,
styleName: READ_ONLY_STYLE,
},
{
name: "rjctOpnnCn",
fieldName: "rjctOpnnCn",
header: { text: "반려의견" },
width: 170,
styleName: READ_ONLY_STYLE,
},
{
name: "pbcpAprvYn",
fieldName: "pbcpAprvYn",
header: { text: "승인여부" },
width: 90,
styleName: READ_ONLY_STYLE,
},
{
name: "vrfcSn",
fieldName: "vrfcSn",
header: { text: "vrfcSn" },
visible: false,
},
],
columnLayout: [
"detailId",
"summaryId",
"sido",
"sigungu",
"indicator",
"detailItem",
"verifyDate",
"errorDetail",
"inputValue",
"compareValue",
"status",
"exceptionReason",
"returnOpinion",
"approvalYn",
"ctpvNm",
"sggNm",
"statsNm",
"vrfcTrgtNm",
"regDt",
"rsnExpln",
"nowVlCn",
"compVlCn",
"prcsYn",
"prcsOpnnExpln",
"rjctOpnnCn",
"pbcpAprvYn",
"vrfcSn",
],
};
@@ -1,14 +1,10 @@
export interface StatisticsVerificationFilterState {
sido: string;
sigungu: string;
year: string;
statsIndicator: string;
}
import type { VerifyResultSearchRequest } from "../general-verification-interface";
import type { StatsInputStatusListRequestParams } from "../input/statistics-input-status-interface";
export interface StatisticsVerificationSubFilterState {
status: string;
approvalStatus: string;
}
export type StatisticsVerificationFilterState =
StatsInputStatusListRequestParams;
export type StatisticsVerificationSubFilterState = VerifyResultSearchRequest;
export interface StatisticsVerificationOption {
label: string;
+2 -1
View File
@@ -72,7 +72,8 @@ export const ENDPOINT = {
REVIEW: "/st-management-service/api/v1/review",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats"
TOTAL: "/st-management-service/api/v1/stats",
APPROVAL: "/st-management-service/api/stats/approval",
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
+41 -41
View File
@@ -1,44 +1,44 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": [
".eslintrc.*",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.js",
"**/*.cjs",
"**/*.mjs",
"**/*.jsx",
"**/*.json",
".next/types/**/*.ts"
],
"exclude": ["**/node_modules", "**/.*/*"]
"exclude": ["**/node_modules", "**/.*/*"]
}
+1
View File
@@ -26,6 +26,7 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@base-ui/react": "^1.4.0",
"@common/assets": "workspace:*",
"@common/auth": "workspace:*",
"@common/config": "workspace:*",
@@ -0,0 +1,315 @@
"use client";
import * as React from "react";
import {
Combobox as ComboboxPrimitive,
type ComboboxRootProps,
} from "@base-ui/react";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "../input-group";
const Combobox = ({ ...props }: ComboboxRootProps<string, boolean>) => {
return <ComboboxPrimitive.Root {...props} />;
};
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}
function ComboboxTrigger({
className,
children,
...props
}: ComboboxPrimitive.Trigger.Props) {
return (
<ComboboxPrimitive.Trigger
data-slot="combobox-trigger"
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
{...props}
>
{children}
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</ComboboxPrimitive.Trigger>
);
}
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
return (
<ComboboxPrimitive.Clear
data-slot="combobox-clear"
className={cn(className)}
{...props}
render={
<InputGroupButton variant="ghost" size="icon-xs">
<XIcon className="pointer-events-none" />
</InputGroupButton>
}
/>
);
}
function ComboboxInput({
className,
children,
disabled = false,
showTrigger = true,
showClear = false,
...props
}: ComboboxPrimitive.Input.Props & {
showTrigger?: boolean;
showClear?: boolean;
}) {
return (
<InputGroup className={cn("w-auto", className)}>
<ComboboxPrimitive.Input
render={<InputGroupInput disabled={disabled} />}
{...props}
/>
<InputGroupAddon align="inline-end">
{showTrigger && (
<InputGroupButton
size="icon-xs"
variant="ghost"
data-slot="input-group-button"
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
disabled={disabled}
/>
)}
{showClear && <ComboboxClear disabled={disabled} />}
</InputGroupAddon>
{children}
</InputGroup>
);
}
function ComboboxContent({
className,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
anchor,
...props
}: ComboboxPrimitive.Popup.Props &
Pick<
ComboboxPrimitive.Positioner.Props,
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
>) {
return (
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
data-chips={!!anchor}
className={cn(
"group/combobox-content max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) ring-foreground/10 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative min-w-[calc(var(--anchor-width)+--spacing(7))] overflow-hidden rounded-sm border-input bg-popover text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none",
className,
)}
{...props}
/>
</ComboboxPrimitive.Positioner>
</ComboboxPrimitive.Portal>
);
}
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
return (
<ComboboxPrimitive.List
data-slot="combobox-list"
className={cn(
"no-scrollbar data-empty:p-0 max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain rounded-sm border border-input p-1",
className,
)}
{...props}
/>
);
}
function ComboboxItem({
className,
children,
...props
}: ComboboxPrimitive.Item.Props) {
return (
<ComboboxPrimitive.Item
data-slot="combobox-item"
className={cn(
"outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1 pl-1.5 pr-16 text-[1.2rem] hover:bg-accent [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
<ComboboxPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<CheckIcon className="pointer-events-none" />
</span>
}
/>
</ComboboxPrimitive.Item>
);
}
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
return (
<ComboboxPrimitive.Group
data-slot="combobox-group"
className={cn(className)}
{...props}
/>
);
}
function ComboboxLabel({
className,
...props
}: ComboboxPrimitive.GroupLabel.Props) {
return (
<ComboboxPrimitive.GroupLabel
data-slot="combobox-label"
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
);
}
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
return (
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
);
}
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
return (
<ComboboxPrimitive.Empty
data-slot="combobox-empty"
className={cn(
"group-data-empty/combobox-content:flex hidden w-full justify-center py-2 text-center text-sm text-muted-foreground",
className,
)}
{...props}
/>
);
}
function ComboboxSeparator({
className,
...props
}: ComboboxPrimitive.Separator.Props) {
return (
<ComboboxPrimitive.Separator
data-slot="combobox-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function ComboboxChips({
className,
...props
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props) {
return (
<ComboboxPrimitive.Chips
data-slot="combobox-chips"
className={cn(
"focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40 flex min-h-[3.2rem] !w-full flex-wrap items-center gap-1 rounded-sm border bg-form bg-clip-padding px-2 py-1.5 text-sm transition-colors focus-within:border-ring",
className,
)}
{...props}
/>
);
}
function ComboboxChip({
className,
children,
showRemove = true,
...props
}: ComboboxPrimitive.Chip.Props & {
showRemove?: boolean;
}) {
return (
<ComboboxPrimitive.Chip
data-slot="combobox-chip"
className={cn(
"has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0 flex h-[2.4rem] w-fit items-center justify-center gap-1.5 whitespace-nowrap rounded-sm bg-muted px-2.5 text-[1.2rem] font-medium text-foreground",
className,
)}
{...props}
>
{children}
{showRemove && (
<ComboboxPrimitive.ChipRemove
className="-ml-1 opacity-50 hover:opacity-100"
data-slot="combobox-chip-remove"
render={
<Button
variant="ghost"
size="icon"
className="size-6 !bg-transparent"
>
<XIcon className="pointer-events-none !size-5" />
</Button>
}
/>
)}
</ComboboxPrimitive.Chip>
);
}
function ComboboxChipsInput({
className,
...props
}: ComboboxPrimitive.Input.Props) {
return (
<ComboboxPrimitive.Input
data-slot="combobox-chip-input"
className={cn(
"h-full min-w-16 flex-1 text-[1.3rem] outline-none",
className,
)}
{...props}
/>
);
}
function useComboboxAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
export {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxGroup,
ComboboxLabel,
ComboboxCollection,
ComboboxEmpty,
ComboboxSeparator,
ComboboxChips,
ComboboxChip,
ComboboxChipsInput,
ComboboxTrigger,
ComboboxValue,
useComboboxAnchor,
};
@@ -5,6 +5,7 @@ export * from "./button-group";
export * from "./calendar";
export * from "./carousel";
export * from "./checkbox";
export * from "./combobox";
export * from "./data-table";
export * from "./date-picker";
export * from "./input-date-picker";
@@ -0,0 +1,156 @@
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
import { Button } from "../button";
import { Input } from "../input";
import { Textarea } from "../textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input outline-none transition-colors has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className,
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
},
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
},
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className,
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"aria-invalid:ring-0 flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"aria-invalid:ring-0 flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
className,
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
+534 -473
View File
File diff suppressed because it is too large Load Diff