[st] rename: entities 폴더 정리

This commit is contained in:
2026-03-20 10:41:35 +09:00
parent 512c39cd46
commit 4dcd422815
27 changed files with 1041 additions and 281 deletions
+4 -1
View File
@@ -39,4 +39,7 @@ next-env.d.ts
.env*
!.env.example
*.md
*.md
# next-agents-md
.next-docs/
@@ -15,19 +15,17 @@ import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import type { ApiErrorResponse } from "@/shared/model";
import { CodeSelect } from "@/entities/code";
import { analysisApi } from "@/entities/statistics/api/analysis-api";
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import type {
StAnlsMstResponse,
StAnlsMstSearchParams,
StAnlsOutlierListResponse,
} from "@/entities/statistics/model/analysis-history-interface";
import type {
AnalysisRequestPayload,
AnalysisResponseDto,
} from "@/entities/statistics/model/analysis-interface";
import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns";
StAnlsMstResponse,
StAnlsMstSearchParams,
StAnlsOutlierListResponse,
} from "@/entities/statistics";
import { verificationHistoryColumns } from "@/entities/statistics";
import { analysisApi } from "@/entities/statistics/api/analysis-api";
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface";
import {
Button,
@@ -228,11 +226,7 @@ const HbStatisticsVerification = () => {
state: {
pagination: historyPagination,
},
onPaginationChange: (updater) => {
setHistoryPagination((prev) =>
typeof updater === "function" ? updater(prev) : updater,
);
},
onPaginationChange: setHistoryPagination,
meta: {
onOutlierCellClick: (row: StAnlsMstResponse) => {
setOutlierDialog({ open: true, anlsSn: row.anlsSn });
@@ -0,0 +1,520 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { Line } from "react-chartjs-2";
import { useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import type { ChartOptions } from "chart.js";
import {
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineElement,
PointElement,
Title,
Tooltip as ChartTooltip,
} from "chart.js";
import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config";
import { CodeApi, CodeSelect } from "@/entities/code";
import type {
StAnlsMstResponse,
StAnlsMstSearchParams,
StAnlsRsltResponse,
} from "@/entities/statistics";
import {
imrDetailGridColumnInfo,
imrVerificationHistoryColumns,
} from "@/entities/statistics";
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
import { imrApi } from "@/entities/statistics/api/imr-api";
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
import {
Button,
DataTable,
DateRangePicker,
Label,
LoadingWithAnimation,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import DataGrid from "@/components/module/DataGrid";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
ChartTooltip,
Legend,
);
type DateRangeValue = {
from: Date;
to?: Date;
};
const EMPTY_DATA: never[] = [];
const ImrVerificationHistory = () => {
// Area A Search States
const [searchSido, setSearchSido] = useState<string>("");
const [searchSigungu, setSearchSigungu] = useState<string>("");
const [searchStatsCd, setSearchStatsCd] = useState<string>("");
const [dateRange, setDateRange] = useState<DateRangeValue | undefined>({
from: dayjs().subtract(1, "month").toDate(),
to: dayjs().toDate(),
});
const [appliedSearchParams, setAppliedSearchParams] =
useState<StAnlsMstSearchParams>({
page: 0,
size: 10,
vrfcMthdCd: "IMR",
sort: "anlsSn,desc",
});
// Selected States
const [selectedHistory, setSelectedHistory] =
useState<StAnlsMstResponse | null>(null);
const [selectedDetail, setSelectedDetail] =
useState<StAnlsRsltResponse | null>(null);
// Area B Grid State
const [gridRowData, setGridRowData] = useState<
Record<string, string | number>[]
>([]);
// Area A Pagination
const [historyPagination, setHistoryPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
// Codes for Filter
const { data: lwpoCodes } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
queryFn: () => CodeApi.getCode("LAESECD"),
});
const sidoOptions = useMemo(() => {
if (!lwpoCodes) return [];
return lwpoCodes.filter(
(c) => c.lwpoCd.startsWith("G3") && c.lwpoCd.length === 4,
);
}, [lwpoCodes]);
const sigunguOptions = useMemo(() => {
if (!lwpoCodes || searchSido === "") return [];
return lwpoCodes.filter(
(c) => c.lwpoCd.startsWith(searchSido) && c.lwpoCd.length === 7,
);
}, [lwpoCodes, searchSido]);
// Area A Data Fetching
const { data: historyData } = useQuery({
queryKey: [
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
appliedSearchParams,
historyPagination,
],
queryFn: () =>
analysisHistoryApi.getAnalysisHistoryList({
...appliedSearchParams,
page: historyPagination.pageIndex,
size: historyPagination.pageSize,
}),
});
const historyTable = useReactTable({
data: historyData?.content ?? EMPTY_DATA,
columns: imrVerificationHistoryColumns,
getRowId: (row) => String(row.anlsSn),
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: historyData?.totalPages ?? 0,
state: {
pagination: historyPagination,
rowSelection: selectedHistory
? { [selectedHistory.anlsSn]: true }
: {},
},
onPaginationChange: setHistoryPagination,
});
// Area B Data Fetching
const { data: detailData, isLoading: isDetailLoading } = useQuery({
queryKey: [
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
selectedHistory?.anlsSn,
],
queryFn: () =>
stAnlsRsltApi.getImrResultList(selectedHistory!.anlsSn, {
page: 0,
size: 100,
sort: "rsltSn,asc",
}),
enabled: !!selectedHistory,
});
useEffect(() => {
if (!detailData || !detailData.content) {
setGridRowData([]);
return;
}
const mapped = detailData.content.map((item) => ({
...item,
statsNm: selectedHistory?.vrfcTrgtNm ?? "-",
}));
setGridRowData(mapped as unknown as Record<string, string | number>[]);
}, [detailData, selectedHistory]);
// Area C Data Fetching
const { data: chartData, isLoading: isChartLoading } = useQuery({
queryKey: [
"imr-log",
selectedHistory?.anlsSn,
selectedDetail?.vrfcTrgtSn,
selectedDetail?.admdstCd,
],
queryFn: () =>
imrApi.getOutliersLog({
anlsSn: selectedHistory!.anlsSn,
vrfcTrgtSn: selectedDetail!.vrfcTrgtSn,
admdstCd: selectedDetail!.admdstCd,
}),
enabled: !!selectedHistory && !!selectedDetail,
});
const handleSearch = () => {
const params: StAnlsMstSearchParams = {
vrfcMthdCd: "IMR",
sort: "anlsSn,desc",
};
if (searchSido !== "")
params.sewerBzmnCd =
searchSigungu !== "" ? searchSigungu : searchSido;
if (searchStatsCd) params.statsCd = searchStatsCd;
if (dateRange?.from)
params.verifyDtStart = dayjs(dateRange.from).format("YYYY-MM-DD");
if (dateRange?.to)
params.verifyDtEnd = dayjs(dateRange.to).format("YYYY-MM-DD");
setAppliedSearchParams(params);
setHistoryPagination((prev) => ({ ...prev, pageIndex: 0 }));
setSelectedHistory(null);
setSelectedDetail(null);
};
const handleReset = () => {
setSearchSido("");
setSearchSigungu("");
setSearchStatsCd("");
const defaultRange = {
from: dayjs().subtract(1, "month").toDate(),
to: dayjs().toDate(),
};
setDateRange(defaultRange);
};
const chartOptions: ChartOptions<"line"> = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top" as const,
labels: {
font: {
size: 12,
},
},
},
title: {
display: true,
text: selectedDetail
? `${selectedDetail.fcltNm} - I-MR 분석 차트`
: "I-MR 분석 차트",
font: {
size: 16,
weight: "bold",
},
},
},
scales: {
y: {
beginAtZero: false,
title: {
display: true,
text: "수치",
},
},
x: {
title: {
display: true,
text: "연도",
},
},
},
}),
[selectedDetail],
);
const lineChartData = useMemo(() => {
if (!chartData || !chartData.history) return null;
const labels = chartData.history.map((h) => String(h.year));
const actualData = chartData.history.map((h) => h.x);
const uclData = chartData.history.map((h) => h.ucl);
const lclData = chartData.history.map((h) => h.lcl);
const predictedData = chartData.history.map((h) => h.predicted);
return {
labels,
datasets: [
{
label: "실측치 (X)",
data: actualData,
borderColor: "rgb(75, 192, 192)",
backgroundColor: "rgba(75, 192, 192, 0.5)",
tension: 0.1,
pointRadius: 4,
pointHoverRadius: 6,
},
{
label: "상한선 (UCL)",
data: uclData,
borderColor: "rgb(255, 99, 132)",
backgroundColor: "rgba(255, 99, 132, 0.2)",
borderDash: [5, 5],
pointStyle: "false",
fill: false,
},
{
label: "하한선 (LCL)",
data: lclData,
borderColor: "rgb(54, 162, 235)",
backgroundColor: "rgba(54, 162, 235, 0.2)",
borderDash: [5, 5],
pointStyle: "false",
fill: false,
},
{
label: "예측치 (Predicted)",
data: predictedData,
borderColor: "rgb(153, 102, 255)",
backgroundColor: "rgba(153, 102, 255, 0.5)",
borderDash: [2, 2],
pointStyle: "false",
fill: false,
},
],
};
}, [chartData]);
return (
<div className="flex flex-col gap-4">
{/* 영역 A: 검증 이력 그리드 + 검색 필터 */}
<div className="card card-border bg-white p-6">
<div className="flex flex-wrap items-end gap-6 p-2">
<div className="flex items-center gap-2">
<Label className="text-[1.3rem] font-medium text-label">
시도
</Label>
<Select
value={searchSido}
onValueChange={(val) => {
setSearchSido(val);
setSearchSigungu("");
}}
>
<SelectTrigger className="w-[14rem]">
<SelectValue placeholder="시도 선택" />
</SelectTrigger>
<SelectContent>
{sidoOptions.map((opt) => (
<SelectItem
key={opt.lwpoCd}
value={opt.lwpoCd}
>
{opt.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-[1.3rem] font-medium text-label">
시군구
</Label>
<Select
value={searchSigungu}
onValueChange={setSearchSigungu}
disabled={searchSido === ""}
>
<SelectTrigger className="w-[14rem]">
<SelectValue placeholder="시군구 선택" />
</SelectTrigger>
<SelectContent>
{sigunguOptions.map((opt) => (
<SelectItem
key={opt.lwpoCd}
value={opt.lwpoCd}
>
{opt.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-[1.3rem] font-medium text-label">
검증일시
</Label>
<DateRangePicker
value={dateRange}
onValueChange={(val) =>
setDateRange(
val
? { from: val.from!, to: val.to }
: undefined,
)
}
className="w-[20rem]"
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-[1.3rem] font-medium text-label">
통계항목
</Label>
<CodeSelect
upCd="STITEM"
value={searchStatsCd}
onValueChange={setSearchStatsCd}
placeholder="통계항목 선택"
className="w-[22rem]"
/>
</div>
<div className="ml-auto flex gap-2">
<Button color="gray" onClick={handleReset}>
초기화
</Button>
<Button onClick={handleSearch}>검색</Button>
</div>
</div>
<div className="mt-2 pb-4">
<DataTable
table={historyTable}
variant="secondary"
onRowClick={(row) => {
setSelectedHistory(row);
setSelectedDetail(null);
}}
viewPagination={true}
className="[&_td]:!p-0"
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* 영역 B: 상세 데이터 그리드 */}
<div className="card card-border bg-white p-6">
<div className="flex items-center justify-between">
<h2 className="pl-2 text-[1.6rem] font-bold text-title">
이상치 데이터
</h2>
<Button
color="green"
disabled={
!detailData || detailData.content.length === 0
}
onClick={() => {
if (selectedHistory) {
stAnlsRsltApi.downloadExcel(
selectedHistory.anlsSn,
0,
);
}
}}
>
다운로드
</Button>
</div>
<div className="mt-2 h-[45rem]">
{isDetailLoading ? (
<div className="flex h-full w-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
) : !selectedHistory || gridRowData.length === 0 ? (
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
{!selectedHistory
? "항목을 선택해주세요."
: "표시할 항목이 없습니다."}
</div>
) : (
<DataGrid
columnInfo={imrDetailGridColumnInfo}
rowData={gridRowData}
setRowData={setGridRowData}
idFieldName="rsltSn"
isExcelExport={false}
isExcelImport={false}
highlightedRowId={selectedDetail?.rsltSn}
onRowClick={(e) => {
if (!e.rowId) return;
const row = detailData?.content?.find(
(item) =>
String(item.rsltSn) ===
String(e.rowId),
);
if (row) setSelectedDetail(row);
}}
/>
)}
</div>
</div>
{/* 영역 C: Line Chart */}
<div className="card card-border bg-white p-6">
<h2 className="mb-4 text-[1.6rem] font-bold text-title">
검증 결과 차트
</h2>
<div className="flex h-[45rem] items-center justify-center rounded-lg border bg-slate-50 p-4">
{isChartLoading ? (
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
) : lineChartData ? (
<Line options={chartOptions} data={lineChartData} />
) : (
<div className="text-[1.4rem] text-muted-foreground">
상세 데이터를 선택하면 차트가 표시됩니다.
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default ImrVerificationHistory;
@@ -7,12 +7,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { CodeSelect } from "@/entities/code/ui/code-select";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import {
hbArticleColumnInfo,
imrArticleColumnInfo,
validationArticleColumnInfo,
} from "@/entities/statistics/model/verification-article-column";
} from "@/entities/statistics";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import type {
VerificationTargetManagementResponse,
VerificationTargetManagementSearchParams,
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
import type { StAnlsDtlResponse } from "@/entities/statistics";
import { Button } from "@common/components";
import Indicators from "./Indicators";
@@ -4,10 +4,10 @@ import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "@/entities/code/api/code-service";
import type { StAnlsRsltResponse } from "@/entities/statistics";
import { useStAnlsRsltList } from "@/entities/statistics/api/queries";
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns";
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
import {
Button,
LoadingWithAnimation,
@@ -112,7 +112,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
if (appliedFilter.status) {
data = data.filter(
(item) =>
item.otlrYn.toLowerCase() === appliedFilter.status,
item.otlrYnCd.toLowerCase() === appliedFilter.status,
);
}
}
@@ -132,7 +132,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
const mappedData = data.map((item) => ({
...item,
otlrYn: mapOtlrToLabel(item.otlrYn),
otlrYn: mapOtlrToLabel(item.otlrYnCd),
}));
setGridRowData(
@@ -19,11 +19,11 @@ import zoomPlugin from "chartjs-plugin-zoom";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "@/entities/code/api/code-service";
import type { StAnlsRsltResponse } from "@/entities/statistics";
import {
useStAnlsChartData,
useStAnlsRsltList,
} from "@/entities/statistics/api/queries";
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
import { LoadingWithAnimation } from "@common/components";
import type { AppliedFilter } from "./HbVerificationDetail";
@@ -124,7 +124,7 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
if (
appliedFilter.status &&
item.otlrYn.toLowerCase() !== appliedFilter.status
item.otlrYnCd.toLowerCase() !== appliedFilter.status
) {
return;
}
@@ -135,7 +135,7 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
item,
};
if (item.otlrYn === "0") {
if (item.otlrYnCd === "0") {
normalPoints.push(pt);
} else {
outlierPoints.push(pt);
@@ -1,15 +1,12 @@
import React, { useEffect, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { ENDPOINT } from "@/shared/config";
import type { StAnlsDtlResponse } from "@/entities/statistics";
import { getVerificationListColumns } from "@/entities/statistics";
import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api";
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
import { getVerificationListColumns } from "@/entities/statistics/model/verification-list-columns";
import { DataTable } from "@common/components";
interface VerificationListProps {
@@ -2,40 +2,39 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
AnalysisRequestPayload,
AnalysisResponseDto,
JobStatusDto,
AnalysisResultDto,
} from "../model/analysis-interface";
AnalysisRequestPayload,
AnalysisResponseDto,
JobStatusDto,
AnalysisResultDto,
} from "../model/analysis/analysis-interface";
export class AnalysisApiService {
public async analyze(
payload: AnalysisRequestPayload,
): Promise<AnalysisResponseDto> {
const response = await client.post<AnalysisResponseDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
payload,
);
public async analyze(
payload: AnalysisRequestPayload,
): Promise<AnalysisResponseDto> {
const response = await client.post<AnalysisResponseDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
payload,
);
return response.data;
}
return response.data;
}
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
const response = await client.get<JobStatusDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
);
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
const response = await client.get<JobStatusDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
);
return response.data;
}
return response.data;
}
public async getResult(jobId: string): Promise<AnalysisResultDto> {
const response = await client.get<AnalysisResultDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
);
public async getResult(jobId: string): Promise<AnalysisResultDto> {
const response = await client.get<AnalysisResultDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
);
return response.data;
}
return response.data;
}
}
export const analysisApi = new AnalysisApiService();
@@ -5,7 +5,7 @@ import type {
StAnlsMstListApiResponse,
StAnlsMstSearchParams,
StAnlsOutlierListResponse,
} from "../model/analysis-history-interface";
} from "../model/statistics-analysis/statistics-analysis-master-interface";
export class AnalysisHistoryApiService {
public async getAnalysisHistoryList(
@@ -10,7 +10,7 @@ import type {
ImrListResponse,
ImrLogRequest,
ImrLogResponse,
} from "../model/imr-interface";
} from "../model/imr/imr-interface";
export class ImrApiService {
/**
@@ -1,11 +1,12 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { PageableParams } from "@/shared/model";
import type {
ChartRetDto,
StAnlsRsltListApiResponse,
StAnlsRsltSearchParams,
} from "../model/statistics-analysis-result-interface";
} from "../model/statistics-analysis/statistics-analysis-result-interface";
export class StAnlsRsltApiService {
public async getResultList(
@@ -23,7 +24,7 @@ export class StAnlsRsltApiService {
public async getImrResultList(
anlsSn: number,
params: { page?: number; size?: number },
params: PageableParams,
): Promise<StAnlsRsltListApiResponse> {
const response = await client.get<StAnlsRsltListApiResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/imr/rsltlist/${anlsSn}`,
@@ -62,4 +63,3 @@ export class StAnlsRsltApiService {
}
export const stAnlsRsltApi = new StAnlsRsltApiService();
+59
View File
@@ -0,0 +1,59 @@
export {
type AnalysisHistoryListApiResponse,
type AnalysisRequestPayload,
type AnalysisResponseDto,
type AnalysisResultDto,
type AnalysisSummaryDto,
type JobStatusDto,
type OutlierRecordDto,
} from "./model/analysis/analysis-interface";
export { imrDetailGridColumnInfo } from "./model/imr/imr-detail-grid-column-info";
export { imrDetailGridColumns } from "./model/imr/imr-detail-grid-columns";
export {
type ImrDetailRequest,
type ImrDetailResponse,
type ImrDetailResponseItem,
type ImrDetectRequest,
type ImrDetectResponse,
type ImrListRequest,
type ImrListResponse,
type ImrLogDataParam,
type ImrLogRequest,
type ImrLogResponse,
} from "./model/imr/imr-interface";
export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns";
export {
type StAnlsDtlListApiResponse,
type StAnlsDtlResponse,
type StAnlsDtlSearchParams,
} from "./model/statistics-analysis/statistics-analysis-detail-interface";
export {
type StAnlsMstListApiResponse,
type StAnlsMstResponse,
type StAnlsMstResponseDto,
type StAnlsMstSearchDto,
type StAnlsMstSearchParams,
type StAnlsOutlierDataResponse,
type StAnlsOutlierDataResponseDto,
type StAnlsOutlierListResponse,
type StAnlsOutlierListResponseDto,
} from "./model/statistics-analysis/statistics-analysis-master-interface";
export {
type ChartBoundaryPointDto,
type ChartRetDto,
type StAnlsRsltListApiResponse,
type StAnlsRsltResponse,
type StAnlsRsltSearchParams,
} 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 {
hbArticleColumnInfo,
imrArticleColumnInfo,
validationArticleColumnInfo,
} from "./model/verification-article/verification-article-column";
export {
type HBArticleItem,
type IMRArticleItem,
type ValidationArticleItem,
} from "./model/verification-article/verification-article-interface";
@@ -1,63 +0,0 @@
export interface ImrDetectRequest {
anlsSn: number;
vrfcTrgtSn: number;
statsYr: string;
userId?: string;
}
export interface ImrDetectResponse {
success: boolean;
message: string;
jobId?: string;
}
export interface ImrListRequest {
anlsSn?: number;
vrfcTrgtSn?: number;
statsYr?: string;
}
export interface ImrListResponse {
anlsSn: number;
vrfcTrgtSn: number;
statsYr: string;
admdstCd: string;
admdstNm: string;
modelNm: string;
anlsDt: string;
}
export interface ImrDetailRequest {
anlsSn: number;
vrfcTrgtSn: number;
admdstCd?: string;
}
export interface ImrDetailResponse {
anlsSn: number;
vrfcTrgtSn: number;
details: Array<{
admdstCd: string;
admdstNm: string;
val: number;
isOutlier: boolean;
score: number;
}>;
}
export interface ImrLogRequest {
anlsSn: number;
vrfcTrgtSn: number;
admdstCd: string;
}
export interface ImrLogResponse {
anlsSn: number;
vrfcTrgtSn: number;
admdstCd: string;
logs: Array<{
timestamp: string;
level: string;
message: string;
}>;
}
@@ -0,0 +1,84 @@
import { ValueType } from "realgrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
export const imrDetailGridColumnInfo: ColumnInfo = {
columns: [
{
name: "sido",
fieldName: "sido",
header: { text: "시도" },
width: 80,
},
{
name: "gugun",
fieldName: "gugun",
header: { text: "구군" },
width: 80,
},
{
name: "admdstNm",
fieldName: "admdstNm",
header: { text: "행정구역" },
width: 100,
},
{
name: "admdstCd",
fieldName: "admdstCd",
header: { text: "시설코드" },
width: 100,
},
{
name: "fcltNm",
fieldName: "fcltNm",
header: { text: "시설명" },
width: 150,
},
{
name: "addr",
fieldName: "addr",
header: { text: "주소" },
width: 200,
styleName: "left-column",
},
{
name: "pipeSeCd",
fieldName: "pipeSeCd",
header: { text: "관로구분" },
width: 80,
},
{
name: "vrfcTrgtSn",
fieldName: "vrfcTrgtSn",
header: { text: "항목번호" },
width: 80,
},
{
name: "statsNm",
fieldName: "statsNm",
header: { text: "항목명" },
width: 150,
},
{
name: "cryrVl",
fieldName: "cryrVl",
header: { text: "당해년도" },
width: 100,
valueType: ValueType.NUMBER,
dataFormat: "#,##0",
styleName: "right-column",
},
],
columnLayout: [
"sido",
"gugun",
"admdstNm",
"admdstCd",
"fcltNm",
"addr",
"pipeSeCd",
"vrfcTrgtSn",
"statsNm",
"cryrVl",
],
};
@@ -0,0 +1,78 @@
import { createColumnHelper } from "@tanstack/react-table";
import type { StAnlsRsltResponse } from "../statistics-analysis/statistics-analysis-result-interface";
const columnHelper = createColumnHelper<StAnlsRsltResponse>();
export const imrDetailGridColumns = [
columnHelper.accessor("sido", {
header: "시도",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("gugun", {
header: "구군",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("admdstNm", {
header: "행정구역",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("admdstCd", {
header: "시설코드",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("fcltNm", {
header: "시설명",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("addr", {
header: "주소",
cell: (info) => (
<div
className="max-w-[150px] truncate px-2 py-2"
title={info.getValue() ?? ""}
>
{info.getValue() ?? "-"}
</div>
),
}),
columnHelper.accessor("pipeSeCd", {
header: "관로구분",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("vrfcTrgtSn", {
header: "항목번호",
cell: (info) => (
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.display({
id: "statsNm",
header: "항목명",
cell: (info) => {
const statsNm = (info.table.options.meta as { statsNm?: string })
?.statsNm;
return <div className="px-2 py-2">{statsNm ?? "-"}</div>;
},
}),
columnHelper.accessor("cryrVl", {
header: "당해년도",
cell: (info) => (
<div className="px-2 py-2 text-right">
{info.getValue()?.toLocaleString() ?? "-"}
</div>
),
}),
];
@@ -0,0 +1,119 @@
/**
* 이상치(IMR) 탐지 실행 요청 DTO
*/
export interface ImrDetectRequest {
userId?: string; // 사용자 ID
sewerBzmnCd: string; // 하수도 사업자 코드
statsCd: string; // 통계 탭/항목 코드
statsYr: string; // 분석 기준 통계 연도
}
/**
* 이상치(IMR) 탐지 실행 응답 DTO
*/
export interface ImrDetectResponse {
anlsSn: number; // 분석 테이블 마스터 일련번호
jobId: string; // Celery/비동기 처리를 위한 고유 작업 ID
totalOtlrCnt: number; // 탐지된 총 이상치 건수
statusCode: string; // 처리 상태 코드 (COMPLETED, FAIL, ERROR 등)
errorMsg?: string; // 에러 발생 시 상세 메시지
processingTimeMs: number; // 총 처리 소요 시간 (밀리초)
}
/**
* 이상치 탐지 목록(마스터) 조회 요청 DTO
*/
export interface ImrListRequest {
sewerBzmnCd?: string; // 하수도 사업자 코드
statsCd?: string; // 통계 항목 코드
statsYr?: string; // 통계 연도
}
/**
* 이상치 탐지 목록(마스터) 조회 응답 DTO
*/
export interface ImrListResponse {
anlsSn: number; // 분석 마스터 일련번호
anlsSttsCd: string; // 분석 상태 코드 (COMPLETED, FAIL, ERROR)
nmlDataNocs: number; // 정상 판정 데이터 건수
exclDataNocs: number; // 분석 제외 데이터 건수
otlrDataNocs: number; // 이상치 판정 데이터 건수
}
/**
* 이상치 탐지 상세 조회 요청 DTO
*/
export interface ImrDetailRequest {
anlsSn: number; // 분석 일련번호
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
}
/**
* 이상치 탐지 상세 조회 응답 아이템
*/
export interface ImrDetailResponseItem {
anlsSn: number; // 분석 일련번호
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
admdstCd: string; // 행정동(행정구역) 코드
cryrVl: number; // 당해년도 원천 데이터 값
otlrTrsfRt: number; // 이상치 판단 기준 (변이 척도)
otlrYn: string; // 이상치 여부 플래그 (Y/N)
}
/**
* 이상치 탐지 상세 조회 응답 DTO
*/
export interface ImrDetailResponse {
items: ImrDetailResponseItem[];
}
/**
* 이상치 산출 로그 요청 DTO
*/
export interface ImrLogRequest {
jobUuid?: string; // 작업 고유 식별자
anlsSn: number; // 분석 일련번호
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
admdstCd: string; // 행정동(행정구역) 코드
}
/**
* 이상치 로그 기록용 연도/시퀀스별 파라미터 DTO
*/
export interface ImrLogDataParam {
year: number; // 대상 연도
t: number; // 시간 시퀀스
tDev: number; // 시점 간의 편차
x: number; // 원천 데이터 값
xDev: number; // 원천 데이터 값 편차
prod: number; // t편차와 x편차의 곱
tDevSq: number; // t편차의 제곱
predicted: number; // 예측 정상치
residuals: number; // 잔차
mr: number; // 이동 범위(MR)
ucl: number; // 관리 상한선
lcl: number; // 관리 하한선
judgment: string; // 최종 판정
}
/**
* 이상치 산출 상세 로그 응답 DTO
*/
export interface ImrLogResponse {
sewerBzmnCd: string; // 하수도 사업자 코드
colNm: string; // 영문 컬럼명
vrfcTrgtNm: string; // 분석 대상 지표명
admdstCd: string; // 행정동 코드
regressionA: number; // 회귀식 기울기(a)
regressionB: number; // 회귀식 절편(b)
tMean: number; // 시퀀스 평균값
xMean: number; // 원데이터 평균값
prodMean: number; // 편차 곱의 평균
tDevSqMean: number; // t편차 제곱의 평균
mrMean: number; // 잔차 이동 범위 평균(MR)
uc: number; // 상한 허용 폭(UCL)
lc: number; // 하한 허용 폭(LCL)
controlRange: number; // 관리 전체 폭(CL)
confidenceLevel: string; // 신뢰 구간 표기
history: ImrLogDataParam[]; // 연도별 상세 추적 기록
}
@@ -0,0 +1,119 @@
import { createColumnHelper } from "@tanstack/react-table";
import {
Progress,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@common/components";
import type { StAnlsMstResponse } from "../statistics-analysis/statistics-analysis-master-interface";
const columnHelper = createColumnHelper<StAnlsMstResponse>();
export const imrVerificationHistoryColumns = [
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,
errMsgCn: row.errMsgCn,
}),
{
id: "anlsSttsNm",
header: "진행 상태",
cell: (info) => {
const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt, errMsgCn } =
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>
);
}
if (anlsSttsCd === "FAILED") {
return (
<div className="flex w-full items-center justify-center gap-1">
{anlsSttsNm}
<Tooltip>
<TooltipTrigger>
<i className="diveicon di-info-circle text-destructive" />
</TooltipTrigger>
<TooltipContent className="max-w-[64rem]">
<p className="p-1 text-[1.2rem] leading-6">
{errMsgCn}
</p>
</TooltipContent>
</Tooltip>
</div>
);
}
return anlsSttsNm;
},
},
),
columnHelper.accessor("anlsEndDt", {
header: "종료 일시",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("statsYr", {
header: "검증년도",
cell: (info) => (
<div className="px-2 py-4 text-center">
{info.getValue() ?? "-"}
</div>
),
}),
columnHelper.accessor("sewerBzmnCd", {
header: "하수도사업자",
cell: (info) => (
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("vrfcTrgtNm", {
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 text-right">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("exclDataNocs", {
header: "제외건수",
cell: (info) => (
<div className="px-2 py-4 text-right">{info.getValue() ?? "-"}</div>
),
}),
columnHelper.accessor("otlrDataNocs", {
header: "이상건수",
cell: (info) => {
const value = info.getValue() ?? 0;
return <div className="px-2 py-4 text-right">{value}</div>;
},
}),
];
@@ -47,7 +47,7 @@ export interface StAnlsMstResponseDto {
/**
* 분석 현황 마스터 검색 DTO
*/
export interface StAnlsMstSearchDto extends PageableParams {
export interface StAnlsMstSearchDto extends PageableParams {
/** 분석상태코드 (COMPLETED, FAILED, QUEUED, PROCESSING) */
anlsSttsCd?: string;
/** 작업UUID */
@@ -20,7 +20,7 @@ export interface StAnlsRsltResponse {
chgRt: number | null; // 변경율 (r_i)
trsfRt: number | null; // 변환 비율 (s_i)
otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i)
otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
otlrYnCd: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
}
export interface StAnlsRsltSearchParams extends PageableParams {
@@ -29,7 +29,7 @@ export interface StAnlsRsltSearchParams extends PageableParams {
admdstCd?: string; // 행정구역 코드
admdstNm?: string; // 행정구역 명칭
fcltNm?: string; // 시설명
otlrYn?: string; // 이상치 여부 (y/n)
otlrYnCd?: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
}
export type StAnlsRsltListApiResponse = PageableList<StAnlsRsltResponse>;
@@ -7,7 +7,7 @@ import {
TooltipTrigger,
} from "@common/components";
import type { StAnlsMstResponse } from "./analysis-history-interface";
import type { StAnlsMstResponse } from "./statistics-analysis-master-interface";
const columnHelper = createColumnHelper<StAnlsMstResponse>();
@@ -2,9 +2,10 @@ import React from "react";
import type { ColumnDef } from "@tanstack/react-table";
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
import { Checkbox } from "@common/components";
import type { StAnlsDtlResponse } from "./statistics-analysis-detail-interface";
interface GetVerificationListColumnsParams {
rowData: StAnlsDtlResponse[];
checkedSns: string[];
@@ -1,150 +0,0 @@
import { fakerKO as faker } from "@faker-js/faker";
import type {
HBArticleItem,
IMRArticleItem,
ValidationArticleItem,
} from "./verification-article-interface";
const HB_STATS_ITEMS = [
"하수도 보급률",
"하수관로",
"하수처리시설 및 방류량보고",
"펌프장",
"공공하수처리시설",
"분뇨처리시설",
"개인하수처리시설",
"하수도재정",
"물 재이용",
];
const HB_VERIFICATION_ITEMS = [
"행정구역 보급률",
"유역환경청별 보급률",
"관로연장",
"처리용량",
"방류량",
"펌프용량",
];
const IMR_STATS_ITEMS = [
"하수도 보급률",
"하수관로",
"하수처리시설 및 방류량보고",
"펌프장",
"공공하수처리시설",
"분뇨처리시설",
"개인하수처리시설",
"하수도재정",
"물 재이용",
];
const VALIDATION_STATS_ITEMS = [
"하수관로 유지관리",
"하수저류시설 및 빗물펌프장",
"공공하수처리시설",
"분뇨처리시설",
"개인하수처리시설",
"하수도재정",
];
const VERIFICATION_NAMES = [
"제정별 사업비",
"관로길이",
"공란검증",
"범위검증",
"논리검증",
"유지보수",
];
const TABLE_NAMES = [
"tb_stats_drng_sys",
"tb_stats_ppline",
"tb_stats_fclt",
"tb_stats_pump_stt",
"tb_stats_sewg_trtm",
];
const COLUMN_NAMES = [
"drng_sys_rate",
"ppline_len",
"fclt_cpct",
"pump_cpct",
"trtm_qty",
"popl_cnt",
];
const BASIC_RULES = [
"값 > 0",
"값 >= 0",
"값 != NULL",
"범위: 0-100",
"A > B",
];
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}`;
}
/** H-B 탭 목데이터 생성 */
export function createHBArticleMock(count = 10): HBArticleItem[] {
faker.seed(100);
return Array.from({ length: count }, (_, _index) => ({
statsItmNm: faker.helpers.arrayElement(HB_STATS_ITEMS),
vrfcItmNm: faker.helpers.arrayElement(HB_VERIFICATION_ITEMS),
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
percentileVal: faker.number.float({
min: 0,
max: 100,
fractionDigits: 2,
}),
aVal: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
cVal: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
regNm: faker.person.fullName(),
regDt: formatDateTime(faker.date.recent({ days: 90 })),
}));
}
/** I-MR 탭 목데이터 생성 */
export function createIMRArticleMock(count = 10): IMRArticleItem[] {
faker.seed(200);
return Array.from({ length: count }, (_, _index) => ({
statsItmNm: faker.helpers.arrayElement(IMR_STATS_ITEMS),
vrfcNm: faker.helpers.arrayElement(VERIFICATION_NAMES),
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
regNm: faker.person.fullName(),
regDt: formatDateTime(faker.date.recent({ days: 90 })),
}));
}
/** Validation 탭 목데이터 생성 */
export function createValidationArticleMock(
count = 6,
): ValidationArticleItem[] {
faker.seed(300);
return Array.from({ length: count }, (_, index) => ({
no: `${index + 1}`,
statsItmNm: faker.helpers.arrayElement(VALIDATION_STATS_ITEMS),
vrfcNm: faker.helpers.arrayElement(VERIFICATION_NAMES),
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
cmpTblNm: faker.helpers.arrayElement(TABLE_NAMES),
cmpColNm: faker.helpers.arrayElement(COLUMN_NAMES),
bscRul: faker.helpers.arrayElement(BASIC_RULES),
}));
}
/** H-B 탭 기본 목데이터 */
export const hbArticleMockData = createHBArticleMock(10);
/** I-MR 탭 기본 목데이터 */
export const imrArticleMockData = createIMRArticleMock(10);
/** Validation 탭 기본 목데이터 */
export const validationArticleMockData = createValidationArticleMock(6);