diff --git a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx index 7b8d16a5d..72f344291 100644 --- a/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx +++ b/apps/st/src/components/statistics/verification/detail/RegionalGrid.tsx @@ -5,6 +5,7 @@ import { ValueType } from "realgrid"; import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; +import { useStAnlsRsltList } from "@/entities/statistics/api/queries"; import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { @@ -80,22 +81,10 @@ const RegionalGrid: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { data: resultData, isLoading } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "list", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => - stAnlsRsltApi.getResultList({ - anlsSn, - vrfcTrgtSn: Number(vrfcTrgtSn), - page: 0, - size: 1000, - }), - enabled: !!vrfcTrgtSn, - }); + const { data: resultData, isLoading } = useStAnlsRsltList( + anlsSn, + vrfcTrgtSn, + ); const rowData = useMemo(() => { let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; diff --git a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx index 2da038fcd..1b2fb5127 100644 --- a/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx +++ b/apps/st/src/components/statistics/verification/detail/ScatterChart.tsx @@ -4,6 +4,8 @@ import { Scatter } from "react-chartjs-2"; import { useQuery } from "@tanstack/react-query"; import { Chart as ChartJS, + type ChartData, + type ChartOptions, Legend, LinearScale, LineController, @@ -11,12 +13,16 @@ import { PointElement, ScatterController, Tooltip, + type TooltipItem, } from "chart.js"; import zoomPlugin from "chartjs-plugin-zoom"; import { ENDPOINT } from "@/shared/config"; import { CodeApi } from "@/entities/code/api/code-service"; -import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api"; +import { + useStAnlsChartData, + useStAnlsRsltList, +} from "@/entities/statistics/api/queries"; import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface"; import { LoadingWithAnimation } from "@common/components"; @@ -37,6 +43,15 @@ interface ScatterChartProps { selectedSido: string | null; } +interface LinePoint { + x: number; + y: number; +} + +interface DataPoint extends LinePoint { + item: StAnlsRsltResponse; +} + const ScatterChart: React.FC = ({ anlsSn, vrfcTrgtSn, @@ -54,110 +69,79 @@ const ScatterChart: React.FC = ({ ); }, [regionCodes, selectedSido]); - const { data: resultData, isLoading: isLoadingGrid } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "list", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => - stAnlsRsltApi.getResultList({ - anlsSn, - vrfcTrgtSn: Number(vrfcTrgtSn), - page: 0, - size: 1000, - }), - enabled: !!anlsSn && !!vrfcTrgtSn, - }); - - const { data: chartData, isLoading: isLoadingChart } = useQuery({ - queryKey: [ - ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, - "chart", - anlsSn, - vrfcTrgtSn, - ], - queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), - enabled: !!anlsSn && !!vrfcTrgtSn, - }); + const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList( + anlsSn, + vrfcTrgtSn, + ); + const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData( + anlsSn, + vrfcTrgtSn, + ); const isLoading = isLoadingGrid || isLoadingChart; const chartConfig = useMemo(() => { if (!resultData || !chartData) return null; - const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[]; + const rawPoints = resultData.content ?? []; const boundaries = chartData.list ?? []; const sortedBoundaries = [...boundaries].sort( - (a, b) => a.xPrev - b.xPrev, + (a, b) => a.xprev - b.xprev, ); - const upperLineData = sortedBoundaries.map((b) => ({ - x: b.xPrev, - y: b.yUpper, - })); - const lowerLineData = sortedBoundaries.map((b) => ({ - x: b.xPrev, - y: b.yLower, + + const upperLineData: LinePoint[] = sortedBoundaries.map((b) => ({ + x: b.yupper, + y: b.xprev, })); - const normalPoints: { - x: number; - y: number; - item: StAnlsRsltResponse; - }[] = []; - const outlierPoints: { - x: number; - y: number; - item: StAnlsRsltResponse; - }[] = []; + const lowerLineData: LinePoint[] = sortedBoundaries.map((b) => ({ + x: b.xprev, + y: b.ylower, + })); + + const normalPoints: DataPoint[] = []; + const outlierPoints: DataPoint[] = []; rawPoints.forEach((item) => { if (selectedSidoName && item.sido !== selectedSidoName) { return; } - const pt = { + const pt: DataPoint = { x: Number(item.prvyrVl ?? 0), y: Number(item.cryrVl ?? 0), item, }; - if (item.otlrYn.toLowerCase() === "y") { + if (item.otlrYn === "0") { normalPoints.push(pt); } else { outlierPoints.push(pt); } }); - const data = { + const data: ChartData<"scatter"> = { datasets: [ { label: "상한선", data: upperLineData, - type: "line" as const, + showLine: true, borderColor: "rgba(255, 99, 132, 0.8)", - borderWidth: 2, - fill: false, pointRadius: 0, - tension: 0.1, }, { label: "하한선", data: lowerLineData, - type: "line" as const, + showLine: true, borderColor: "rgba(54, 162, 235, 0.8)", - borderWidth: 2, - fill: false, pointRadius: 0, - tension: 0.1, }, { label: "이상치", data: outlierPoints, backgroundColor: "rgba(255, 99, 132, 0.7)", - pointRadius: 5, + pointRadius: 4, pointHoverRadius: 8, }, { @@ -165,19 +149,19 @@ const ScatterChart: React.FC = ({ data: normalPoints, backgroundColor: "rgba(75, 192, 192, 0.7)", pointRadius: 4, - pointHoverRadius: 7, + pointHoverRadius: 8, }, ], }; - const options = { + const options: ChartOptions<"scatter"> = { responsive: true, maintainAspectRatio: false, plugins: { zoom: { pan: { enabled: true, - mode: "xy" as const, + mode: "xy", }, zoom: { wheel: { @@ -186,36 +170,34 @@ const ScatterChart: React.FC = ({ pinch: { enabled: true, }, - mode: "xy" as const, + mode: "xy", }, }, tooltip: { callbacks: { - label: (context: any) => { - if (context.dataset.type === "line") { - return `${context.dataset.label}: (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`; + label: (context: TooltipItem<"scatter">) => { + const dataset = context.dataset; + if (dataset.showLine) { + return `${context.dataset.label}: (${context?.parsed?.x?.toFixed(2) ?? "-"}, ${context?.parsed?.y?.toFixed(2) ?? "-"})`; } - const pt = context.raw as { - x: number; - y: number; - item: StAnlsRsltResponse; - }; - return `${pt.item.sido} ${pt.item.gugun}: 이전(${pt.x}), 당해(${pt.y})`; + + const pt = (context.raw as DataPoint) ?? {}; + return `${pt?.item?.sido ?? "시/도"} ${pt?.item?.gugun ?? "구/군"}: 이전(${pt?.x ?? "-"}), 당해(${pt?.y ?? "-"})`; }, }, }, }, scales: { x: { - type: "linear" as const, - position: "bottom" as const, + type: "linear", + position: "bottom", title: { display: true, text: "이전년도 값", }, }, y: { - type: "linear" as const, + type: "linear", title: { display: true, text: "당해년도 값", @@ -242,8 +224,8 @@ const ScatterChart: React.FC = ({ ) : chartConfig ? (
) : ( diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts new file mode 100644 index 000000000..bd8510421 --- /dev/null +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -0,0 +1,43 @@ +import { useQuery } from "@tanstack/react-query"; + +import { ENDPOINT } from "@/shared/config"; + +import { stAnlsRsltApi } from "./statistics-analysis-result-api"; + +export const useStAnlsRsltList = ( + anlsSn: number, + vrfcTrgtSn: string | null, +) => { + return useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "list", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => + stAnlsRsltApi.getResultList({ + anlsSn, + vrfcTrgtSn: Number(vrfcTrgtSn), + page: 0, + size: 1000, + }), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); +}; + +export const useStAnlsChartData = ( + anlsSn: number, + vrfcTrgtSn: string | null, +) => { + return useQuery({ + queryKey: [ + ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT, + "chart", + anlsSn, + vrfcTrgtSn, + ], + queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)), + enabled: !!anlsSn && !!vrfcTrgtSn, + }); +}; diff --git a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts index 399be99e8..a2a3e5e84 100644 --- a/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-analysis-result-interface.ts @@ -35,9 +35,9 @@ export interface StAnlsRsltSearchParams extends PageableParams { export type StAnlsRsltListApiResponse = PageableList; export interface ChartBoundaryPointDto { - xPrev: number; // 전년도 값 (x축) - yLower: number; // 이상치 하한 당년값 (y축) - yUpper: number; // 이상치 상한 당년값 (y축) + xprev: number; // 전년도 값 (x축) + ylower: number; // 이상치 하한 당년값 (y축) + yupper: number; // 이상치 상한 당년값 (y축) } export interface ChartRetDto {