[st] feat: H-B 통계 검증 페이지 API 연결 작업

This commit is contained in:
2026-03-04 15:13:07 +09:00
parent ff5b43e1d2
commit fe3e6de085
6 changed files with 582 additions and 365 deletions
@@ -1,115 +1,343 @@
"use client";
import { useState } from "react";
// import { DataGrid } from "@rsone/components";
import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import {
Button,
Checkbox,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
Input,
Label,
RadioGroup,
RadioGroupItem,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Checkbox,
} from "@common/components";
import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column";
import { verificationHistoryMockData } from "@/entities/statistics/model/verification-history-mock";
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 {
StAnlsMstResponse,
StAnlsOutlierListResponse,
} from "@/entities/statistics/model/analysis-history-interface";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { CodeApi } from "@/entities/code";
interface VerificationFormValues {
type VerifyType = "ALL" | "SELECT";
interface VerificationFormState {
mu: string;
percentile: string;
a: string;
c: string;
verificationYear: string;
checkboxOptions: {
sidoDistribution: boolean;
countyDistribution: boolean;
countyDistributionUserManagement: boolean;
lowerDistribution: boolean;
sidoServiceAvailability: boolean;
individualService: boolean;
legalServiceUsage: boolean;
excessUse: boolean;
lowerOrgStatistics: boolean;
studentStatistics: boolean;
developmentStatistics: boolean;
lowerManagementEnterprise: boolean;
}
interface OutlierDialogState {
open: boolean;
anlsSn: number | null;
}
function getVerificationYearOptions(): string[] {
const currentYear = new Date().getFullYear();
const startYear = 2017;
const endYear = currentYear - 1;
if (endYear < startYear) return [];
return Array.from({ length: endYear - startYear + 1 }, (_, index) =>
String(startYear + index),
);
}
function toNumber(value: string): number {
const parsed = Number(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
function mapHistoryToRow(
item: StAnlsMstResponse,
): Record<string, string | number> {
const executedAt = item.anlsBgngDt ?? "";
const endedAt = item.anlsEndDt ?? "";
const verificationYear =
executedAt && executedAt.length >= 4 ? executedAt.slice(0, 4) : "";
return {
executedAt,
progressStatus: item.anlsSttsNm ?? "",
endedAt,
verificationYear,
mu: 0,
percentile: 0,
a: 0,
c: 0,
target: "",
normalCount: item.nmlDataNocs ?? 0,
excludedCount: item.exclDataNocs ?? 0,
abnormalCount: item.otlrDataNocs ?? 0,
anlsSn: item.anlsSn,
};
}
const HbStatisticsVerification = () => {
const [verificationHistoryRowData, setVerificationHistoryRowData] =
useState<Record<string, string | number>[]>(
verificationHistoryMockData,
);
const { setMessage: alert } = useAlertStore((state) => state);
const queryClient = useQueryClient();
const [formValues, setFormValues] = useState<VerificationFormValues>({
const yearOptions = useMemo(() => getVerificationYearOptions(), []);
const defaultYear: string =
yearOptions.length > 0
? (yearOptions[yearOptions.length - 1] ?? "")
: "";
const [verifyType, setVerifyType] = useState<VerifyType>("ALL");
const [formState, setFormState] = useState<VerificationFormState>({
mu: "0",
percentile: "0",
a: "0",
c: "0",
verificationYear: "2025",
checkboxOptions: {
sidoDistribution: false,
countyDistribution: false,
countyDistributionUserManagement: false,
lowerDistribution: false,
sidoServiceAvailability: false,
individualService: false,
legalServiceUsage: false,
excessUse: false,
lowerOrgStatistics: false,
studentStatistics: false,
developmentStatistics: false,
lowerManagementEnterprise: false,
verificationYear: defaultYear,
});
const [selectedTargets, setSelectedTargets] = useState<string[]>([]);
const [outlierDialog, setOutlierDialog] = useState<OutlierDialogState>({
open: false,
anlsSn: null,
});
const { data: statsItemList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"],
queryFn: async () => {
const response = await CodeApi.getCode("STITEM");
return response;
},
});
const handleCheckboxChange = (
key: keyof VerificationFormValues["checkboxOptions"],
) => {
setFormValues((prev) => ({
...prev,
checkboxOptions: {
...prev.checkboxOptions,
[key]: !prev.checkboxOptions[key],
},
}));
};
const { data: analysisHistory } = useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST],
queryFn: async () => {
const response = await analysisHistoryApi.getAnalysisHistoryList({
page: 0,
size: 10,
});
return response;
},
});
const handleInputChange = (
key: keyof Omit<VerificationFormValues, "checkboxOptions">,
const historyRowData: Record<string, string | number>[] = useMemo(
() =>
(analysisHistory?.content ?? []).map((item) =>
mapHistoryToRow(item as StAnlsMstResponse),
),
[analysisHistory],
);
const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({
queryKey: [
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
"outlier",
outlierDialog.anlsSn,
],
queryFn: async () => {
if (!outlierDialog.anlsSn) return [];
const response = await analysisHistoryApi.getOutlierList(
outlierDialog.anlsSn,
);
return response;
},
enabled: outlierDialog.open && !!outlierDialog.anlsSn,
});
const analysisMutation = useMutation<
AnalysisResponseDto,
Error,
AnalysisRequestPayload
>({
mutationFn: async (payload) => analysisApi.analyze(payload),
onSuccess: () => {
alert("검증을 요청했습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST],
});
},
onError: () => {
alert("검증 실행 중 오류가 발생했습니다.");
},
});
function handleFormChange<Key extends keyof VerificationFormState>(
key: Key,
value: string,
) => {
setFormValues((prev) => ({
) {
setFormState((prev) => ({
...prev,
[key]: value,
}));
};
}
const handleSubmit = () => {
console.log("검증 실행:", formValues);
};
function handleTargetToggle(statsCd: string) {
setSelectedTargets((prev) =>
prev.includes(statsCd)
? prev.filter((cd) => cd !== statsCd)
: [...prev, statsCd],
);
}
const handleReset = () => {
console.log("초기화");
};
async function handleSubmit() {
if (!formState.verificationYear) {
alert("검증년도를 선택해 주세요.");
return;
}
if (verifyType === "SELECT" && selectedTargets.length === 0) {
alert("검증 대상을 1개 이상 선택해 주세요.");
return;
}
const yearNew = toNumber(formState.verificationYear);
const payload: AnalysisRequestPayload = {
dataset:
verifyType === "ALL"
? "ALL"
: selectedTargets.length > 0
? selectedTargets.join(",")
: undefined,
yearNew,
yearOld: yearNew - 1,
mu: toNumber(formState.mu),
percentile: toNumber(formState.percentile),
a: toNumber(formState.a),
c: toNumber(formState.c),
vrfcMthdCd: "HB",
allVerifyYn: verifyType === "ALL" ? "Y" : "N",
};
analysisMutation.mutate(payload);
}
function handleReset() {
setFormState({
mu: "0",
percentile: "0",
a: "0",
c: "0",
verificationYear: defaultYear,
});
setSelectedTargets([]);
}
function handleHistoryRowClick(row: Record<string, string | number>) {
const abnormalCount = Number(row.abnormalCount ?? 0);
const anlsSn = Number(row.anlsSn ?? 0);
if (!anlsSn || abnormalCount <= 0) return;
setOutlierDialog({ open: true, anlsSn });
}
function closeOutlierDialog() {
setOutlierDialog({ open: false, anlsSn: null });
}
return (
<div className="card card-border mt-4">
<div className="p-2">
{/* 통계 검증 섹션 */}
<div className="mb-6">
<h3 className="px-2 text-[1.5rem] font-semibold text-title">
통계 검증
</h3>
<>
<div className="card card-border flex flex-col gap-6">
{/* 검증 종류 선택 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 pl-2">
<span className="text-[1.3rem] font-medium text-label">
검증 방식
</span>
<RadioGroup
value={verifyType}
onValueChange={(value) =>
setVerifyType(value as VerifyType)
}
className="flex items-center gap-4"
>
<label className="flex cursor-pointer items-center gap-2 text-label">
<RadioGroupItem value="ALL" />
<span>일괄 검증</span>
</label>
<label className="flex cursor-pointer items-center gap-2 text-label">
<RadioGroupItem value="SELECT" />
<span>선택 검증</span>
</label>
</RadioGroup>
</div>
<div className="flex justify-center gap-4">
<Button
color="green"
className="h-[3.6rem] w-[120px]"
onClick={handleSubmit}
disabled={analysisMutation.isPending}
>
검증 실행
</Button>
<Button
color="gray"
className="h-[3.6rem] w-[120px]"
type="button"
onClick={handleReset}
>
초기화
</Button>
</div>
</div>
{/* //검증 종류 선택 */}
<div className="flex gap-2">
{/* 왼쪽: 검증 변수 */}
<div className="card card-border flex-[0.5] rounded p-4">
{verifyType === "SELECT" && (
<div className="flex gap-4">
{/* 검증 대상 */}
<div className="flex-1">
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
검증 대상
</h4>
<div className="space-y-4 p-2">
{verifyType === "SELECT" &&
statsItemList &&
statsItemList.length > 0 && (
<div className="grid grid-cols-3 gap-4">
{statsItemList.map((item) => (
<div
key={item.lwpoCd}
className="flex items-center gap-2"
>
<Checkbox
id={item.lwpoCd}
checked={selectedTargets.includes(
item.lwpoCd,
)}
onCheckedChange={() =>
handleTargetToggle(
item.lwpoCd,
)
}
/>
<Label
htmlFor={item.lwpoCd}
className="cursor-pointer text-label"
>
{item.lwpoCdNm}
</Label>
</div>
))}
</div>
)}
</div>
</div>
{/* //검증 대상 */}
{/* 검증 변수 */}
<div className="flex-[0.5]">
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
검증 변수
</h4>
@@ -119,11 +347,11 @@ const HbStatisticsVerification = () => {
<Label className="w-32 text-label">μ</Label>
<Input
type="number"
value={formValues.mu}
onChange={(e) =>
handleInputChange(
value={formState.mu}
onChange={(event) =>
handleFormChange(
"mu",
e.target.value,
event.target.value,
)
}
className="flex-1"
@@ -136,11 +364,11 @@ const HbStatisticsVerification = () => {
</Label>
<Input
type="number"
value={formValues.percentile}
onChange={(e) =>
handleInputChange(
value={formState.percentile}
onChange={(event) =>
handleFormChange(
"percentile",
e.target.value,
event.target.value,
)
}
className="flex-1"
@@ -151,11 +379,11 @@ const HbStatisticsVerification = () => {
<Label className="w-32 text-label">A</Label>
<Input
type="number"
value={formValues.a}
onChange={(e) =>
handleInputChange(
value={formState.a}
onChange={(event) =>
handleFormChange(
"a",
e.target.value,
event.target.value,
)
}
className="flex-1"
@@ -166,11 +394,11 @@ const HbStatisticsVerification = () => {
<Label className="w-32 text-label">C</Label>
<Input
type="number"
value={formValues.c}
onChange={(e) =>
handleInputChange(
value={formState.c}
onChange={(event) =>
handleFormChange(
"c",
e.target.value,
event.target.value,
)
}
className="flex-1"
@@ -182,9 +410,9 @@ const HbStatisticsVerification = () => {
검증 년도
</Label>
<Select
value={formValues.verificationYear}
value={formState.verificationYear}
onValueChange={(value) =>
handleInputChange(
handleFormChange(
"verificationYear",
value,
)
@@ -194,289 +422,86 @@ const HbStatisticsVerification = () => {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2025">
2025년
</SelectItem>
{yearOptions.map((year) => (
<SelectItem
key={year}
value={year}
>
{year}년
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 오른쪽: 검증 대상 */}
<div className="card card-border flex-1 rounded p-4">
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
검증 대상
</h4>
<div className="space-y-4 p-2">
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수도 보급률
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.countyDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"countyDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수관로
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.countyDistributionUserManagement
}
onCheckedChange={() =>
handleCheckboxChange(
"countyDistributionUserManagement",
)
}
/>
<Label className="cursor-pointer text-label">
하수관로 유지관리
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerDistribution
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerDistribution",
)
}
/>
<Label className="cursor-pointer text-label">
하수저류시설 및 빗물펌프장
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoServiceAvailability
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoServiceAvailability",
)
}
/>
<Label className="cursor-pointer text-label">
펌프장
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.individualService
}
onCheckedChange={() =>
handleCheckboxChange(
"individualService",
)
}
/>
<Label className="cursor-pointer text-label">
공공하수처리시설
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.sidoServiceAvailability
}
onCheckedChange={() =>
handleCheckboxChange(
"sidoServiceAvailability",
)
}
/>
<Label className="cursor-pointer text-label">
분뇨처리시설
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.individualService
}
onCheckedChange={() =>
handleCheckboxChange(
"individualService",
)
}
/>
<Label className="cursor-pointer text-label">
개인하수처리시설
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerOrgStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerOrgStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수도재정
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.studentStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"studentStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
물 재이용
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerOrgStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerOrgStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수 및 분뇨찌꺼기(슬러지) 처리
</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.studentStatistics
}
onCheckedChange={() =>
handleCheckboxChange(
"studentStatistics",
)
}
/>
<Label className="cursor-pointer text-label">
하수 및 분뇨관련산업
</Label>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<div className="flex items-center gap-2">
<Checkbox
checked={
formValues.checkboxOptions
.lowerManagementEnterprise
}
onCheckedChange={() =>
handleCheckboxChange(
"lowerManagementEnterprise",
)
}
/>
<Label className="cursor-pointer text-label">
하수도 서비스 수준
</Label>
</div>
</div>
</div>
</div>
{/* //검증 변수 */}
</div>
</div>
{/* 버튼 */}
<div className="mt-6 flex justify-center gap-4 pt-4">
<Button
color="green"
className="h-[3.6rem] w-[120px]"
onClick={handleSubmit}
>
검증 실행
</Button>
<Button
color="gray"
className="h-[3.6rem] w-[120px]"
onClick={handleReset}
>
초기화
</Button>
</div>
{/* 검증 이력 섹션 */}
<div className="mt-8 border-t border-border pt-6">
<h3 className="mb-4 px-2 text-[1.5rem] font-semibold text-title">
검증 이력
</h3>
<DataGrid
columnInfo={verificationHistoryColumnInfo}
rowData={verificationHistoryRowData}
setRowData={setVerificationHistoryRowData}
isExcelImport={false}
isExcelExport={false}
showHeaderCheckbox={false}
/>
</div>
)}
</div>
</div>
<div className="mt-10 px-2">
<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}
onRowClick={handleHistoryRowClick}
cellStyleCallback={({ row, fieldName }) => {
if (fieldName === "abnormalCount") {
const abnormal = Number(row.abnormalCount ?? 0);
if (abnormal > 0) return "rg-cell-verify-error";
}
return "";
}}
/>
</div>
<Dialog open={outlierDialog.open} onOpenChange={closeOutlierDialog}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>이상건수 상세</DialogTitle>
</DialogHeader>
{isLoadingOutlier && (
<p className="px-2 py-4 text-[1.3rem] text-muted-foreground">
이상치 데이터를 불러오는 중입니다.
</p>
)}
{!isLoadingOutlier && (outlierList?.length ?? 0) === 0 && (
<p className="px-2 py-4 text-[1.3rem] text-muted-foreground">
표시할 이상치 데이터가 없습니다.
</p>
)}
{!isLoadingOutlier && (outlierList?.length ?? 0) > 0 && (
<div className="max-h-[40rem] space-y-3 overflow-auto px-1 py-2 text-[1.25rem]">
{outlierList?.map(
(item: StAnlsOutlierListResponse) => (
<div
key={`${item.anlsSn}-${item.vrfcTrgtSn}`}
className="rounded border border-border px-3 py-2"
>
<div className="flex items-center justify-between">
<span className="font-medium">
{item.vrfcTrgtNm}
</span>
<span className="text-sm text-muted-foreground">
이상건수:{" "}
{item.otlrDataNocs ?? 0}
</span>
</div>
</div>
),
)}
</div>
)}
</DialogContent>
</Dialog>
</>
);
};
@@ -0,0 +1,41 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
AnalysisRequestPayload,
AnalysisResponseDto,
JobStatusDto,
AnalysisResultDto,
} from "../model/analysis-interface";
export class AnalysisApiService {
public async analyze(
payload: AnalysisRequestPayload,
): Promise<AnalysisResponseDto> {
const response = await client.post<AnalysisResponseDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
payload,
);
return response.data;
}
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
const response = await client.get<JobStatusDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
);
return response.data;
}
public async getResult(jobId: string): Promise<AnalysisResultDto> {
const response = await client.get<AnalysisResultDto>(
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
);
return response.data;
}
}
export const analysisApi = new AnalysisApiService();
@@ -0,0 +1,36 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
StAnlsMstListApiResponse,
StAnlsMstSearchParams,
StAnlsOutlierListResponse,
} from "../model/analysis-history-interface";
export class AnalysisHistoryApiService {
public async getAnalysisHistoryList(
params: StAnlsMstSearchParams,
): Promise<StAnlsMstListApiResponse> {
const response = await client.get<StAnlsMstListApiResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`,
{
params,
},
);
return response.data;
}
public async getOutlierList(
anlsSn: number,
): Promise<StAnlsOutlierListResponse[]> {
const response = await client.get<StAnlsOutlierListResponse[]>(
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`,
);
return response.data;
}
}
export const analysisHistoryApi = new AnalysisHistoryApiService();
@@ -0,0 +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;
}
export interface StAnlsMstSearchParams {
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;
}
export interface StAnlsOutlierListResponse {
anlsSn: number;
vrfcTrgtSn: number;
vrfcTrgtNm: string;
nmlDataNocs: number;
exclDataNocs: number;
otlrDataNocs: number;
outlierDataList: StAnlsOutlierDataResponse[];
}
export type StAnlsMstListApiResponse = PageableList<StAnlsMstResponse>;
@@ -0,0 +1,64 @@
import type { PageableList } from "@/shared/model";
export interface AnalysisRequestPayload {
dataset?: string;
yearOld: number;
yearNew: number;
mu: number;
a: number;
c: number;
percentile: number;
sido?: string;
sigungu?: string;
vrfcMthdCd?: string;
allVerifyYn?: "Y" | "N";
}
export interface AnalysisResponseDto {
jobId: string;
status: string;
message: string;
queuePosition: number;
createdAt: string;
anlsSn: number;
}
export interface JobStatusDto {
jobId: string;
status: string;
message: string;
queuePosition: number;
progress: number;
startedAt: string | null;
completedAt: string | null;
error: string | null;
anlsSn: number | null;
processingTimeMs: number | null;
}
export interface AnalysisSummaryDto {
// 요약 필드는 Python 분석 서비스 스키마에 따라 확장
[key: string]: unknown;
}
export interface OutlierRecordDto {
// 상세 구조는 Python 분석 서비스 응답에 맞춰 필요 시 확장
[key: string]: unknown;
}
export interface AnalysisResultDto {
jobId: string;
status: string;
createdAt: string;
completedAt: string | null;
summary: AnalysisSummaryDto | null;
multiSummaries: Record<string, AnalysisSummaryDto> | null;
outliers: OutlierRecordDto[];
results: OutlierRecordDto[];
totalCount: number;
cached: boolean;
processingTimeMs: number;
anlsSn: number;
}
export type AnalysisHistoryListApiResponse = PageableList<AnalysisResultDto>;
+2
View File
@@ -72,6 +72,8 @@ export const ENDPOINT = {
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
ANALYSIS: "/statverify-service/api/v1/stat-verify/analysis",
ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst",
},
};