[st] feat: 통계 검증 상세 화면 작업
This commit is contained in:
@@ -62,20 +62,16 @@ 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]">
|
||||
<Indicators
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
/>
|
||||
</div>
|
||||
<Indicators
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
/>
|
||||
|
||||
<div className="flex-[0.5]">
|
||||
<ScatterChart
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
/>
|
||||
</div>
|
||||
<ScatterChart
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
interface IndicatorsProps {
|
||||
anlsSn: number;
|
||||
@@ -34,8 +35,10 @@ const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="card card-border flex min-h-[10rem] items-center justify-center gap-6 text-[1.4rem] text-muted-foreground">
|
||||
지표 데이터를 불러오는 중...
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +64,7 @@ const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 flex min-h-[10rem] flex-wrap gap-2">
|
||||
<div className="card card-border !m-0 grid min-h-[10rem] grid-cols-4 gap-2">
|
||||
<IndicatorItem label="r 중위수" value={dtl?.rtCnterVl} />
|
||||
<IndicatorItem
|
||||
label="(E_m)E 중위수"
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
@@ -14,7 +9,6 @@ import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-res
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -23,6 +17,9 @@ import {
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
interface RegionalGridProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
@@ -52,6 +49,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
status: "",
|
||||
});
|
||||
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
@@ -99,34 +100,45 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
!appliedFilter.gugun &&
|
||||
!appliedFilter.status
|
||||
) {
|
||||
return showAllOnEmptyFilter ? data : [];
|
||||
}
|
||||
data = showAllOnEmptyFilter ? data : [];
|
||||
} else {
|
||||
if (appliedFilter.sido) {
|
||||
const sidoName = sidoOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.sido,
|
||||
)?.lwpoCdNm;
|
||||
if (sidoName) {
|
||||
data = data.filter((item) => item.sido === sidoName);
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedFilter.sido) {
|
||||
const sidoName = sidoOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.sido,
|
||||
)?.lwpoCdNm;
|
||||
if (sidoName) {
|
||||
data = data.filter((item) => item.sido === sidoName);
|
||||
if (appliedFilter.gugun) {
|
||||
const gugunName = gugunOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.gugun,
|
||||
)?.lwpoCdNm;
|
||||
if (gugunName) {
|
||||
data = data.filter((item) =>
|
||||
gugunName.includes(item.gugun),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedFilter.status) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.otlrYn.toLowerCase() === appliedFilter.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedFilter.gugun) {
|
||||
const gugunName = gugunOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.gugun,
|
||||
)?.lwpoCdNm;
|
||||
if (gugunName) {
|
||||
data = data.filter((item) => gugunName.includes(item.gugun));
|
||||
}
|
||||
}
|
||||
const mappedData = data.map((item) => ({
|
||||
...item,
|
||||
otlrYn: item.otlrYn === "0" ? "정상" : "이상치",
|
||||
}));
|
||||
|
||||
if (appliedFilter.status) {
|
||||
data = data.filter(
|
||||
(item) => item.otlrYn.toLowerCase() === appliedFilter.status,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
setGridRowData(
|
||||
mappedData as unknown as Record<string, string | number>[],
|
||||
);
|
||||
return mappedData;
|
||||
}, [
|
||||
resultData,
|
||||
appliedFilter,
|
||||
@@ -135,60 +147,89 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
gugunOptions,
|
||||
]);
|
||||
|
||||
const columns: ColumnDef<StAnlsRsltResponse>[] = useMemo(
|
||||
() => [
|
||||
{ header: "시도", accessorKey: "sido", size: 80 },
|
||||
{ header: "구군", accessorKey: "gugun", size: 80 },
|
||||
{ header: "행정구역", accessorKey: "admdstNm", size: 100 },
|
||||
{ header: "이전년도", accessorKey: "prvyrVl", size: 80 },
|
||||
{ header: "당해년도", accessorKey: "cryrVl", size: 80 },
|
||||
{
|
||||
header: "R",
|
||||
accessorKey: "chgRt",
|
||||
size: 60,
|
||||
cell: (info) => Number(info.getValue() ?? 0).toFixed(2),
|
||||
},
|
||||
{
|
||||
header: "S",
|
||||
accessorKey: "trsfRt",
|
||||
size: 60,
|
||||
cell: (info) => Number(info.getValue() ?? 0).toFixed(2),
|
||||
},
|
||||
{
|
||||
header: "E",
|
||||
accessorKey: "otlrTrsfRt",
|
||||
size: 60,
|
||||
cell: (info) => Number(info.getValue() ?? 0).toFixed(2),
|
||||
},
|
||||
{
|
||||
header: "상태",
|
||||
accessorKey: "otlrYn",
|
||||
size: 60,
|
||||
cell: (info) => {
|
||||
const val = info.getValue();
|
||||
return (
|
||||
<span
|
||||
className={`font-medium ${val === "0" ? "text-green-600" : "text-red-500"}`}
|
||||
>
|
||||
{val === "0" ? "정상" : "이상치"}
|
||||
</span>
|
||||
);
|
||||
const columnInfo: ColumnInfo = useMemo(
|
||||
() => ({
|
||||
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: "prvyrVl",
|
||||
fieldName: "prvyrVl",
|
||||
header: { text: "이전년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "cryrVl",
|
||||
fieldName: "cryrVl",
|
||||
header: { text: "당해년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "chgRt",
|
||||
fieldName: "chgRt",
|
||||
header: { text: "R" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "trsfRt",
|
||||
fieldName: "trsfRt",
|
||||
header: { text: "S" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrTrsfRt",
|
||||
fieldName: "otlrTrsfRt",
|
||||
header: { text: "E" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrYn",
|
||||
fieldName: "otlrYn",
|
||||
header: { text: "상태" },
|
||||
width: 60,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"sido",
|
||||
"gugun",
|
||||
"admdstNm",
|
||||
"prvyrVl",
|
||||
"cryrVl",
|
||||
"chgRt",
|
||||
"trsfRt",
|
||||
"otlrTrsfRt",
|
||||
"otlrYn",
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
initialState: {
|
||||
pagination: { pageSize: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
onSidoSelect(!val ? null : val);
|
||||
setSelectedGugun("");
|
||||
@@ -258,8 +299,8 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<SelectValue placeholder="상태 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="y">정상</SelectItem>
|
||||
<SelectItem value="n">이상치</SelectItem>
|
||||
<SelectItem value="0">정상</SelectItem>
|
||||
<SelectItem value="1">이상치</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -269,7 +310,9 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>조회</Button>
|
||||
<Button color="green">다운로드</Button>
|
||||
<Button color="green" onClick={() => {}}>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -289,11 +332,12 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
viewPagination={true}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
rowData={gridRowData}
|
||||
setRowData={setGridRowData}
|
||||
isExcelExport={false}
|
||||
isExcelImport={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
ChartJS.register(
|
||||
LinearScale,
|
||||
@@ -231,11 +232,13 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
항목을 선택해주세요
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
차트를 불러오는 중...
|
||||
<div className="flex w-full items-center justify-center py-40">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : chartConfig ? (
|
||||
<div className="h-[40rem] w-full">
|
||||
<div className="h-[48rem] w-full">
|
||||
<Scatter
|
||||
data={chartConfig.data as any}
|
||||
options={chartConfig.options as any}
|
||||
|
||||
@@ -100,6 +100,11 @@ const VerificationList: React.FC<VerificationListProps> = ({
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
columns,
|
||||
state: {
|
||||
rowSelection: selectedVrfcTrgtSn ? { [selectedVrfcTrgtSn]: true } : {},
|
||||
},
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => String(row.vrfcTrgtSn),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ export interface StAnlsDtlResponse {
|
||||
nmlDataNocs: number; // 정상 데이터 건수
|
||||
exclDataNocs: number; // 제외 데이터 건수
|
||||
otlrDataNocs: number; // 이상치 데이터 건수
|
||||
statsCd: string; // 통계 코드
|
||||
statsNm: string; // 통계 항목 명
|
||||
vrfcTrgtNm: string; // 검증대상명
|
||||
statsYr: string; // 통계년도
|
||||
}
|
||||
|
||||
export interface StAnlsDtlSearchParams extends PageableParams {
|
||||
|
||||
Reference in New Issue
Block a user