[st] feat: 통계 검증 상세 화면 작업
This commit is contained in:
@@ -26,5 +26,9 @@ pnpm-lock.yaml
|
||||
settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
@@ -180,6 +180,7 @@ const HbStatisticsVerification = () => {
|
||||
const params: StAnlsMstSearchParams = {
|
||||
page: historyPagination.pageIndex,
|
||||
size: historyPagination.pageSize,
|
||||
sort: "anlsSn,desc",
|
||||
};
|
||||
|
||||
if (appliedHistoryDateRange?.from) {
|
||||
@@ -455,6 +456,7 @@ const HbStatisticsVerification = () => {
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={selectedStatsCd}
|
||||
onValueChange={setSelectedStatsCd}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useState } from "react";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import AreaDIndicators from "./Indicators";
|
||||
import AreaBRegionalGrid from "./RegionalGrid";
|
||||
import AreaCScatterChart from "./ScatterChart";
|
||||
import AreaAVerificationList from "./VerificationList";
|
||||
import Indicators from "./Indicators";
|
||||
import RegionalGrid from "./RegionalGrid";
|
||||
import ScatterChart from "./ScatterChart";
|
||||
import VerificationList from "./VerificationList";
|
||||
|
||||
interface HbVerificationDetailProps {
|
||||
anlsSn: number;
|
||||
@@ -41,7 +41,7 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="h-[20rem] overflow-y-auto">
|
||||
<AreaAVerificationList
|
||||
<VerificationList
|
||||
anlsSn={anlsSn}
|
||||
selectedVrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
onSelect={setSelectedVrfcTrgtSn}
|
||||
@@ -54,7 +54,7 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<div className="flex-[0.5] p-4">
|
||||
<AreaBRegionalGrid
|
||||
<RegionalGrid
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
@@ -63,14 +63,14 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
</div>
|
||||
<div className="flex flex-[0.5] flex-col gap-4 p-4 pt-[4.8rem]">
|
||||
<div className="flex-[0.5]">
|
||||
<AreaDIndicators
|
||||
<Indicators
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-[0.5]">
|
||||
<AreaCScatterChart
|
||||
<ScatterChart
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
@@ -29,14 +30,27 @@ interface RegionalGridProps {
|
||||
onSidoSelect: (sido: string | null) => void;
|
||||
}
|
||||
|
||||
interface AppliedFilter {
|
||||
sido: string | null;
|
||||
gugun: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
selectedSido,
|
||||
onSidoSelect,
|
||||
}) => {
|
||||
const [selectedGugun, setSelectedGugun] = useState<string>("ALL");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("ALL");
|
||||
const [selectedGugun, setSelectedGugun] = useState<string>("");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("");
|
||||
const [showAllOnEmptyFilter, setShowAllOnEmptyFilter] =
|
||||
useState<boolean>(true);
|
||||
const [appliedFilter, setAppliedFilter] = useState<AppliedFilter>({
|
||||
sido: null,
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
@@ -51,7 +65,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
}, [regionCodes]);
|
||||
|
||||
const gugunOptions = useMemo(() => {
|
||||
if (!regionCodes || !selectedSido || selectedSido === "ALL") return [];
|
||||
if (!regionCodes || !selectedSido) return [];
|
||||
|
||||
const sidoPrefix = selectedSido;
|
||||
return regionCodes.filter(
|
||||
@@ -60,11 +74,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
);
|
||||
}, [regionCodes, selectedSido]);
|
||||
|
||||
const {
|
||||
data: resultData,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
const { data: resultData, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"list",
|
||||
@@ -78,43 +88,49 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
page: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
enabled: false,
|
||||
enabled: !!vrfcTrgtSn,
|
||||
});
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
|
||||
|
||||
if (selectedSido && selectedSido !== "ALL") {
|
||||
if (
|
||||
!appliedFilter.sido &&
|
||||
!appliedFilter.gugun &&
|
||||
!appliedFilter.status
|
||||
) {
|
||||
return showAllOnEmptyFilter ? data : [];
|
||||
}
|
||||
|
||||
if (appliedFilter.sido) {
|
||||
const sidoName = sidoOptions.find(
|
||||
(opt) => opt.lwpoCd === selectedSido,
|
||||
(opt) => opt.lwpoCd === appliedFilter.sido,
|
||||
)?.lwpoCdNm;
|
||||
if (sidoName) {
|
||||
data = data.filter((item) => item.sido === sidoName);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedGugun && selectedGugun !== "ALL") {
|
||||
if (appliedFilter.gugun) {
|
||||
const gugunName = gugunOptions.find(
|
||||
(opt) => opt.lwpoCd === selectedGugun,
|
||||
(opt) => opt.lwpoCd === appliedFilter.gugun,
|
||||
)?.lwpoCdNm;
|
||||
if (gugunName) {
|
||||
data = data.filter((item) => item.gugun === gugunName);
|
||||
data = data.filter((item) => gugunName.includes(item.gugun));
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedStatus && selectedStatus !== "ALL") {
|
||||
const statusTarget = selectedStatus === "NORMAL" ? "y" : "n";
|
||||
if (appliedFilter.status) {
|
||||
data = data.filter(
|
||||
(item) => item.otlrYn.toLowerCase() === statusTarget,
|
||||
(item) => item.otlrYn.toLowerCase() === appliedFilter.status,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [
|
||||
resultData,
|
||||
selectedSido,
|
||||
selectedGugun,
|
||||
selectedStatus,
|
||||
appliedFilter,
|
||||
showAllOnEmptyFilter,
|
||||
sidoOptions,
|
||||
gugunOptions,
|
||||
]);
|
||||
@@ -150,19 +166,13 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
size: 60,
|
||||
cell: (info) => {
|
||||
const val = info.getValue();
|
||||
if (val === "y")
|
||||
return (
|
||||
<span className="font-medium text-green-600">
|
||||
정상
|
||||
</span>
|
||||
);
|
||||
if (val === "n")
|
||||
return (
|
||||
<span className="font-medium text-red-500">
|
||||
이상치
|
||||
</span>
|
||||
);
|
||||
return "-";
|
||||
return (
|
||||
<span
|
||||
className={`font-medium ${val === "0" ? "text-green-600" : "text-red-500"}`}
|
||||
>
|
||||
{val === "0" ? "정상" : "이상치"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -180,8 +190,29 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
});
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
onSidoSelect(val === "ALL" ? null : val);
|
||||
setSelectedGugun("ALL");
|
||||
onSidoSelect(!val ? null : val);
|
||||
setSelectedGugun("");
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
setAppliedFilter({
|
||||
sido: selectedSido,
|
||||
gugun: selectedGugun,
|
||||
status: selectedStatus,
|
||||
});
|
||||
setShowAllOnEmptyFilter(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
onSidoSelect(null);
|
||||
setSelectedGugun("");
|
||||
setSelectedStatus("");
|
||||
setAppliedFilter({
|
||||
sido: null,
|
||||
gugun: "",
|
||||
status: "",
|
||||
});
|
||||
setShowAllOnEmptyFilter(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -189,14 +220,13 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedSido || "ALL"}
|
||||
value={selectedSido || ""}
|
||||
onValueChange={handleSidoChange}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
<SelectValue placeholder="시도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체 (시도)</SelectItem>
|
||||
{sidoOptions.map((opt) => (
|
||||
<SelectItem key={opt.lwpoCd} value={opt.lwpoCd}>
|
||||
{opt.lwpoCdNm}
|
||||
@@ -207,13 +237,12 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<Select
|
||||
value={selectedGugun}
|
||||
onValueChange={setSelectedGugun}
|
||||
disabled={!selectedSido || selectedSido === "ALL"}
|
||||
disabled={!selectedSido}
|
||||
>
|
||||
<SelectTrigger className="w-[12rem] bg-white">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체 (시군구)</SelectItem>
|
||||
{gugunOptions.map((opt) => (
|
||||
<SelectItem key={opt.lwpoCd} value={opt.lwpoCd}>
|
||||
{opt.lwpoCdNm}
|
||||
@@ -226,41 +255,47 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
onValueChange={setSelectedStatus}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] bg-white">
|
||||
<SelectValue placeholder="상태" />
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체 상태</SelectItem>
|
||||
<SelectItem value="NORMAL">정상</SelectItem>
|
||||
<SelectItem value="OUTLIER">이상치</SelectItem>
|
||||
<SelectItem value="y">정상</SelectItem>
|
||||
<SelectItem value="n">이상치</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => refetch()}>조회</Button>
|
||||
<Button color="gray">다운로드</Button>
|
||||
<Button color="white" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>조회</Button>
|
||||
<Button color="green">다운로드</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
viewPagination={true}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
데이터를 불러오는 중...
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && (!vrfcTrgtSn || rowData.length === 0) && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
{vrfcTrgtSn
|
||||
? "표시할 항목이 없습니다."
|
||||
: "항목을 선택해주세요"}
|
||||
: "항목을 선택해주세요."}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
viewPagination={true}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api";
|
||||
@@ -47,50 +51,47 @@ const VerificationList: React.FC<VerificationListProps> = ({
|
||||
return (detailData?.content ?? []) as StAnlsDtlResponse[];
|
||||
}, [detailData]);
|
||||
|
||||
const columns = useMemo(
|
||||
const columns: ColumnDef<StAnlsDtlResponse>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: "통계 년도",
|
||||
accessorKey: "statsYr",
|
||||
},
|
||||
{
|
||||
header: "통계 항목",
|
||||
accessorKey: "statsNm",
|
||||
},
|
||||
{
|
||||
header: "검증 항목",
|
||||
accessorFn: (row: StAnlsDtlResponse) =>
|
||||
targetCodeMap.get(String(row.vrfcTrgtSn)) ||
|
||||
String(row.vrfcTrgtSn),
|
||||
id: "vrfcTrgtNm",
|
||||
size: 150,
|
||||
accessorKey: "vrfcTrgtNm",
|
||||
},
|
||||
{
|
||||
header: "μ",
|
||||
accessorKey: "ipcrCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "Percentile",
|
||||
accessorKey: "pctlCrtrVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "A",
|
||||
accessorKey: "scopAjmtCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "C",
|
||||
accessorKey: "thrsCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "정상",
|
||||
header: "정상건수",
|
||||
accessorKey: "nmlDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
{
|
||||
header: "제외",
|
||||
header: "제외건수",
|
||||
accessorKey: "exclDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
{
|
||||
header: "이상",
|
||||
header: "이상건수",
|
||||
accessorKey: "otlrDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
],
|
||||
[targetCodeMap],
|
||||
|
||||
@@ -2,35 +2,34 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
StAnlsMstListApiResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
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,
|
||||
},
|
||||
);
|
||||
public async getAnalysisHistoryList(
|
||||
params: StAnlsMstSearchParams,
|
||||
): Promise<StAnlsMstListApiResponse> {
|
||||
const response = await client.get<StAnlsMstListApiResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getOutlierList(
|
||||
anlsSn: number,
|
||||
): Promise<StAnlsOutlierListResponse[]> {
|
||||
const response = await client.get<StAnlsOutlierListResponse[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`,
|
||||
);
|
||||
public async getOutlierList(
|
||||
anlsSn: number,
|
||||
): Promise<StAnlsOutlierListResponse[]> {
|
||||
const response = await client.get<StAnlsOutlierListResponse[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const analysisHistoryApi = new AnalysisHistoryApiService();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PageableList } from "@/shared/model";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface StAnlsMstResponse {
|
||||
anlsSn: number;
|
||||
@@ -16,12 +16,9 @@ export interface StAnlsMstResponse {
|
||||
rgtrNm: string;
|
||||
}
|
||||
|
||||
export interface StAnlsMstSearchParams {
|
||||
export interface StAnlsMstSearchParams extends PageableParams {
|
||||
anlsSttsCd?: string;
|
||||
jobUuid?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
verifyDtStart?: string;
|
||||
verifyDtEnd?: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface StAnlsRsltResponse {
|
||||
chgRt: number | null; // 변경율 (r_i)
|
||||
trsfRt: number | null; // 변환 비율 (s_i)
|
||||
otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i)
|
||||
otlrYn: string; // 이상치 여부 (y/n)
|
||||
otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치)
|
||||
}
|
||||
|
||||
export interface StAnlsRsltSearchParams extends PageableParams {
|
||||
|
||||
Reference in New Issue
Block a user