[st] feat: 검증 이력 테이블 수정

This commit is contained in:
2026-03-05 14:10:15 +09:00
parent 64fcc423b5
commit 451d25666a
6 changed files with 168 additions and 231 deletions
@@ -1,11 +1,13 @@
"use client";
import { useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import DataGrid from "@/components/module/DataGrid";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import {
Button,
Checkbox,
DataTable,
Dialog,
DialogContent,
DialogHeader,
@@ -27,14 +29,17 @@ import {
TableRow,
TableCell,
} from "@common/components";
import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column";
import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns";
import { analysisApi } from "@/entities/statistics/api/analysis-api";
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
import type {
AnalysisRequestPayload,
AnalysisResponseDto,
} from "@/entities/statistics/model/analysis-interface";
import type { StAnlsOutlierListResponse } from "@/entities/statistics/model/analysis-history-interface";
import type {
StAnlsMstResponse,
StAnlsOutlierListResponse,
} from "@/entities/statistics/model/analysis-history-interface";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { CodeApi } from "@/entities/code";
@@ -70,6 +75,7 @@ function toNumber(value: string): number {
}
const HbStatisticsVerification = () => {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const queryClient = useQueryClient();
@@ -103,20 +109,34 @@ const HbStatisticsVerification = () => {
const { data: analysisHistory } = useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST],
queryFn: async () => {
const response = await analysisHistoryApi.getAnalysisHistoryList({
queryFn: () =>
analysisHistoryApi.getAnalysisHistoryList({
page: 0,
size: 10,
});
return response;
}),
refetchInterval: (query) => {
const data = query.state.data;
if (!data?.content?.length) return false;
const hasProcessing = (data.content as StAnlsMstResponse[]).some(
(item) => item.anlsSttsCd === "PROCESSING",
);
return hasProcessing ? 3000 : false;
},
});
const historyRowData =
(analysisHistory?.content ?? []) as unknown as Record<
string,
string | number
>[];
const historyRowData: StAnlsMstResponse[] = (analysisHistory?.content ??
[]) as StAnlsMstResponse[];
const table = useReactTable({
data: historyRowData,
columns: verificationHistoryColumns,
getCoreRowModel: getCoreRowModel(),
meta: {
onOutlierCellClick: (row: StAnlsMstResponse) => {
setOutlierDialog({ open: true, anlsSn: row.anlsSn });
},
},
});
const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({
queryKey: [
@@ -212,11 +232,10 @@ const HbStatisticsVerification = () => {
setSelectedTargets([]);
}
function handleHistoryRowClick(row: Record<string, string | number>) {
const abnormalCount = Number(row.otlrDataNocs ?? 0);
function handleHistoryRowClick(row: StAnlsMstResponse) {
const anlsSn = Number(row.anlsSn ?? 0);
if (!anlsSn || abnormalCount <= 0) return;
setOutlierDialog({ open: true, anlsSn });
if (!anlsSn) return;
router.push(`/statistics/verification/${anlsSn}`);
}
function closeOutlierDialog() {
@@ -417,25 +436,16 @@ const HbStatisticsVerification = () => {
)}
</div>
<div className="mt-10 px-2">
<div className="mt-10 px-4 pb-8">
<h3 className="mb-2 px-2 text-[1.5rem] font-semibold text-title">
검증 이력
</h3>
<DataGrid
columnInfo={verificationHistoryColumnInfo}
rowData={historyRowData}
setRowData={() => {}}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
<DataTable
table={table}
variant="secondary"
onRowClick={handleHistoryRowClick}
cellStyleCallback={({ row, fieldName }) => {
if (fieldName === "otlrDataNocs") {
const abnormal = Number(row.otlrDataNocs ?? 0);
if (abnormal > 0) return "rg-cell-verify-error";
}
return "";
}}
viewPagination={true}
className="[&_td]:!p-0"
/>
</div>
@@ -1,49 +1,49 @@
import type { PageableList } from "@/shared/model";
export interface StAnlsMstResponse {
anlsSn: number;
anlsBgngDt: string;
anlsEndDt: string | null;
anlsSttsCd: string;
anlsSttsNm: string;
nmlDataNocs: number;
exclDataNocs: number;
otlrDataNocs: number;
jobUuid: string;
regDt: string;
rgtr: string;
rgtrNm: string;
anlsSn: number;
anlsBgngDt: string;
anlsEndDt: string | null;
anlsSttsCd: string;
anlsSttsNm: string;
anlsPrgrsRt?: number;
nmlDataNocs: number;
exclDataNocs: number;
otlrDataNocs: number;
jobUuid: string;
regDt: string;
rgtr: string;
rgtrNm: string;
}
export interface StAnlsMstSearchParams {
anlsSttsCd?: string;
jobUuid?: string;
page?: number;
size?: number;
sort?: string;
anlsSttsCd?: string;
jobUuid?: string;
page?: number;
size?: number;
sort?: string;
}
export interface StAnlsOutlierDataResponse {
rsltSn: number;
vrfcTrgtSn: number;
admdstCd: string;
sido: string;
gugun: string;
adstDong: string;
addr: string;
cryrVl: number;
otlrTrsfRt: number;
rsltSn: number;
vrfcTrgtSn: number;
admdstCd: string;
sido: string;
gugun: string;
adstDong: string;
addr: string;
cryrVl: number;
otlrTrsfRt: number;
}
export interface StAnlsOutlierListResponse {
anlsSn: number;
vrfcTrgtSn: number;
vrfcTrgtNm: string;
nmlDataNocs: number;
exclDataNocs: number;
otlrDataNocs: number;
outlierDataList: StAnlsOutlierDataResponse[];
anlsSn: number;
vrfcTrgtSn: number;
vrfcTrgtNm: string;
nmlDataNocs: number;
exclDataNocs: number;
otlrDataNocs: number;
outlierDataList: StAnlsOutlierDataResponse[];
}
export type StAnlsMstListApiResponse = PageableList<StAnlsMstResponse>;
@@ -1,72 +0,0 @@
import { ValueType } from "realgrid";
import type { ColumnInfo } from "@rsone/components";
/** 검증 이력 그리드 컬럼 설정 */
export const verificationHistoryColumnInfo: ColumnInfo = {
columns: [
{
name: "anlsBgngDt",
fieldName: "anlsBgngDt",
header: { text: "검증 실행 일시" },
editable: false,
sortdisable: false,
},
{
name: "anlsSttsNm",
fieldName: "anlsSttsNm",
header: { text: "진행 상태" },
editable: false,
sortdisable: false,
},
{
name: "anlsEndDt",
fieldName: "anlsEndDt",
header: { text: "종료 일시" },
editable: false,
sortdisable: false,
},
{
name: "rgtrNm",
fieldName: "rgtrNm",
header: { text: "검증자" },
editable: false,
sortdisable: false,
},
{
name: "nmlDataNocs",
fieldName: "nmlDataNocs",
header: { text: "정상건수" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{
name: "exclDataNocs",
fieldName: "exclDataNocs",
header: { text: "제외건수" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
{
name: "otlrDataNocs",
fieldName: "otlrDataNocs",
header: { text: "이상건수" },
editable: false,
sortdisable: false,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
},
],
columnLayout: [
"anlsBgngDt",
"anlsSttsNm",
"anlsEndDt",
"rgtrNm",
"nmlDataNocs",
"exclDataNocs",
"otlrDataNocs",
],
};
@@ -0,0 +1,93 @@
import { createColumnHelper } from "@tanstack/react-table";
import { Progress } from "@common/components";
import type { StAnlsMstResponse } from "./analysis-history-interface";
const columnHelper = createColumnHelper<StAnlsMstResponse>();
export const verificationHistoryColumns = [
columnHelper.accessor("anlsBgngDt", {
header: "검증 실행 일시",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor(
(row) => ({
anlsSttsCd: row.anlsSttsCd,
anlsSttsNm: row.anlsSttsNm,
anlsPrgrsRt: row.anlsPrgrsRt,
}),
{
id: "anlsSttsNm",
header: "진행 상태",
cell: (info) => {
const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt } = info.getValue();
if (anlsSttsCd === "PROCESSING") {
return (
<div className="flex min-w-[8rem] items-center gap-2">
<Progress
value={anlsPrgrsRt ?? 0}
className="h-2 flex-1"
/>
<span className="text-xs text-muted-foreground">
{anlsPrgrsRt ?? 0}%
</span>
</div>
);
}
return anlsSttsNm;
},
},
),
columnHelper.accessor("anlsEndDt", {
header: "종료 일시",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("rgtrNm", {
header: "검증자",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("nmlDataNocs", {
header: "정상건수",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("exclDataNocs", {
header: "제외건수",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("otlrDataNocs", {
header: "이상건수",
cell: (info) => {
const value = info.getValue() ?? "-";
const row = info.row.original;
const onOutlierCellClick = (
info.table.options.meta as {
onOutlierCellClick?: (row: StAnlsMstResponse) => void;
}
)?.onOutlierCellClick;
if (value > 0 && onOutlierCellClick) {
return (
<button
type="button"
className="block w-full cursor-pointer bg-red-50 py-4 text-destructive underline"
onClick={(e) => {
e.stopPropagation();
onOutlierCellClick(row);
}}
>
{value}
</button>
);
}
return value;
},
}),
];
@@ -1,27 +0,0 @@
/** 검증 이력 한 건 (그리드 행 데이터) */
export interface VerificationHistoryItem {
/** 검증 실행 일시 */
executedAt: string;
/** 진행 상태 */
progressStatus: string;
/** 종료 일시 */
endedAt: string;
/** 검증년도 */
verificationYear: string;
/** μ */
mu: number;
/** Percentile */
percentile: number;
/** A */
a: number;
/** C */
c: number;
/** 검증 대상 */
target: string;
/** 정상건수 */
normalCount: number;
/** 제외건수 */
excludedCount: number;
/** 이상건수 */
abnormalCount: number;
}
@@ -1,67 +0,0 @@
import { fakerKO as faker } from "@faker-js/faker";
import type { VerificationHistoryItem } from "./verification-history-interface";
const VERIFICATION_TARGETS = [
"하수도 보급률",
"하수관로",
"하수관로 유지관리",
"하수저류시설 및 빗물펌프장",
"펌프장",
"공공하수처리시설",
"분뇨처리시설",
"개인하수처리시설",
"하수도재정",
"물 재이용",
"하수도 서비스 수준",
];
const PROGRESS_STATUSES = ["대기", "진행중", "완료", "실패"];
function formatDateTime(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
const h = String(date.getHours()).padStart(2, "0");
const min = String(date.getMinutes()).padStart(2, "0");
return `${y}-${m}-${d} ${h}:${min}`;
}
/** 검증 이력 목데이터 생성 (그리드 행 형식) */
export function createVerificationHistoryMock(
count = 10,
): Record<string, string | number>[] {
faker.seed(42);
return Array.from({ length: count }, () => {
const executedAt = faker.date.recent({ days: 90 });
const durationMinutes = faker.number.int({ min: 1, max: 120 });
const endedAt = new Date(
executedAt.getTime() + durationMinutes * 60 * 1000,
);
const status = faker.helpers.arrayElement(PROGRESS_STATUSES);
const normalCount = faker.number.int({ min: 0, max: 500 });
const excludedCount = faker.number.int({ min: 0, max: 50 });
const abnormalCount = faker.number.int({ min: 0, max: 30 });
const item: VerificationHistoryItem = {
executedAt: formatDateTime(executedAt),
progressStatus: status,
endedAt: formatDateTime(endedAt),
verificationYear: String(executedAt.getFullYear()),
mu: faker.number.float({ min: 0, max: 1, fractionDigits: 3 }),
percentile: faker.number.float({
min: 0,
max: 100,
fractionDigits: 2,
}),
a: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
c: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
target: faker.helpers.arrayElement(VERIFICATION_TARGETS),
normalCount,
excludedCount,
abnormalCount,
};
return { ...item } as Record<string, string | number>;
});
}
/** 검증 이력 기본 목데이터 (10건) */
export const verificationHistoryMockData = createVerificationHistoryMock(10);