[st] feat: 통계 검증 상세 화면 작업
This commit is contained in:
@@ -1,18 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import type { AxiosError } from "axios";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
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";
|
||||
import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DataTable,
|
||||
DateRangePicker,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
@@ -29,34 +50,16 @@ import {
|
||||
SelectValue,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
DateRangePicker,
|
||||
} from "@common/components";
|
||||
import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns";
|
||||
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,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "@/entities/statistics/model/analysis-history-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import type { AxiosError } from "axios";
|
||||
import type { ApiErrorResponse } from "@/shared/model";
|
||||
|
||||
import HbVerificationDetail from "./detail/HbVerificationDetail";
|
||||
|
||||
type DateRangeValue = {
|
||||
from?: Date;
|
||||
from: Date;
|
||||
to?: Date;
|
||||
};
|
||||
|
||||
@@ -97,7 +100,6 @@ function toNumber(value: string): number {
|
||||
}
|
||||
|
||||
const HbStatisticsVerification = () => {
|
||||
const router = useRouter();
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -121,6 +123,7 @@ const HbStatisticsVerification = () => {
|
||||
anlsSn: null,
|
||||
});
|
||||
const [selectedStatsCd, setSelectedStatsCd] = useState<string>("");
|
||||
const [selectedAnlsSn, setSelectedAnlsSn] = useState<number | null>(null);
|
||||
|
||||
const defaultHistoryDateRange = useMemo(
|
||||
() => getDefaultHistoryDateRange(),
|
||||
@@ -330,7 +333,7 @@ const HbStatisticsVerification = () => {
|
||||
function handleHistoryRowClick(row: StAnlsMstResponse) {
|
||||
const anlsSn = Number(row.anlsSn ?? 0);
|
||||
if (!anlsSn) return;
|
||||
router.push(`/statistics/verification/${anlsSn}`);
|
||||
setSelectedAnlsSn(anlsSn);
|
||||
}
|
||||
|
||||
function applyHistoryDateSearch(nextRange: DateRangeValue | undefined) {
|
||||
@@ -357,264 +360,298 @@ const HbStatisticsVerification = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card card-border flex flex-col gap-6">
|
||||
{/* 검증 종류 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증 년도
|
||||
</Label>
|
||||
<Select
|
||||
value={formState.verificationYear}
|
||||
onValueChange={(value) =>
|
||||
handleFormChange("verificationYear", value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem key={year} value={year}>
|
||||
{year}년
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증 방식
|
||||
</Label>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
{/* //검증 종류 선택 */}
|
||||
|
||||
{verifyType === "SELECT" && (
|
||||
<div className="flex gap-4">
|
||||
{/* 검증 대상 */}
|
||||
<div className="flex-1">
|
||||
<h4 className="mb-3 rounded bg-gray-100 px-4 py-2 text-[1.4rem] font-medium">
|
||||
검증 대상
|
||||
</h4>
|
||||
<div className="mb-3 flex items-center gap-4 px-4">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
onValueChange={setSelectedStatsCd}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
{selectedAnlsSn ? (
|
||||
<HbVerificationDetail
|
||||
anlsSn={selectedAnlsSn}
|
||||
onBack={() => setSelectedAnlsSn(null)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="card card-border flex flex-col gap-6">
|
||||
{/* 검증 종류 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증 년도
|
||||
</Label>
|
||||
<Select
|
||||
value={formState.verificationYear}
|
||||
onValueChange={(value) =>
|
||||
handleFormChange(
|
||||
"verificationYear",
|
||||
value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem
|
||||
key={year}
|
||||
value={year}
|
||||
>
|
||||
{year}년
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증 방식
|
||||
</Label>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.6rem] w-[120px]"
|
||||
onClick={handleSubmit}
|
||||
disabled={analysisMutation.isPending}
|
||||
>
|
||||
검증 실행
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={() => setSelectedStatsCd("")}
|
||||
className="h-[3.6rem] w-[120px]"
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[16rem] space-y-4 overflow-auto rounded-md border p-4">
|
||||
{verifyType === "SELECT" &&
|
||||
filteredVerificationTargetCodes &&
|
||||
filteredVerificationTargetCodes.length > 0 ? (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{filteredVerificationTargetCodes.map(
|
||||
(item) => (
|
||||
<div
|
||||
key={item.vrfcTrgtSn}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={item.vrfcTrgtSn}
|
||||
checked={selectedTargets.includes(
|
||||
item.vrfcTrgtSn,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
handleTargetToggle(
|
||||
item.vrfcTrgtSn,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={
|
||||
item.vrfcTrgtSn
|
||||
}
|
||||
className="cursor-pointer text-label"
|
||||
>
|
||||
{item.vrfcTrgtNm}
|
||||
</Label>
|
||||
</div>
|
||||
),
|
||||
</div>
|
||||
{/* //검증 종류 선택 */}
|
||||
|
||||
{verifyType === "SELECT" && (
|
||||
<div className="flex gap-4">
|
||||
{/* 검증 대상 */}
|
||||
<div className="flex-1">
|
||||
<h4 className="mb-3 rounded bg-gray-100 px-4 py-2 text-[1.4rem] font-medium">
|
||||
검증 대상
|
||||
</h4>
|
||||
<div className="mb-3 flex items-center gap-4 px-4">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
onValueChange={setSelectedStatsCd}
|
||||
className="w-[24rem]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={() =>
|
||||
setSelectedStatsCd("")
|
||||
}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[16rem] space-y-4 overflow-auto rounded-md border p-4">
|
||||
{verifyType === "SELECT" &&
|
||||
filteredVerificationTargetCodes &&
|
||||
filteredVerificationTargetCodes.length >
|
||||
0 ? (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{filteredVerificationTargetCodes.map(
|
||||
(item) => (
|
||||
<div
|
||||
key={
|
||||
item.vrfcTrgtSn
|
||||
}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={
|
||||
item.vrfcTrgtSn
|
||||
}
|
||||
checked={selectedTargets.includes(
|
||||
item.vrfcTrgtSn,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
handleTargetToggle(
|
||||
item.vrfcTrgtSn,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={
|
||||
item.vrfcTrgtSn
|
||||
}
|
||||
className="cursor-pointer text-label"
|
||||
>
|
||||
{
|
||||
item.vrfcTrgtNm
|
||||
}
|
||||
</Label>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<p className="text-[1.3rem] text-muted-foreground">
|
||||
검증 대상이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<p className="text-[1.3rem] text-muted-foreground">
|
||||
검증 대상이 없습니다.
|
||||
</p>
|
||||
</div>
|
||||
{/* //검증 대상 */}
|
||||
|
||||
{/* 검증 변수 */}
|
||||
<div className="flex-[0.5]">
|
||||
<h4 className="mb-3 rounded bg-gray-100 px-4 py-2 text-[1.4rem] font-medium">
|
||||
검증 변수
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">
|
||||
μ
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.mu}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"mu",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">
|
||||
Percentile
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.percentile}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"percentile",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">
|
||||
A
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.a}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"a",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">
|
||||
C
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.c}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"c",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* //검증 변수 */}
|
||||
</div>
|
||||
</div>
|
||||
{/* //검증 대상 */}
|
||||
|
||||
{/* 검증 변수 */}
|
||||
<div className="flex-[0.5]">
|
||||
<h4 className="mb-3 rounded bg-gray-100 px-4 py-2 text-[1.4rem] font-medium">
|
||||
검증 변수
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">μ</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.mu}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"mu",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">
|
||||
Percentile
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.percentile}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"percentile",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">A</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.a}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"a",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Label className="w-32 text-label">C</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formState.c}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"c",
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* //검증 변수 */}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 px-2 pb-8">
|
||||
<h3 className="mb-4 text-[1.6rem] font-semibold text-title">
|
||||
검증 이력
|
||||
</h3>
|
||||
<div className="card card-border !m-0 !mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-4 px-2">
|
||||
<span className="text-[1.3rem] font-medium text-label">
|
||||
검증일시
|
||||
</span>
|
||||
<DateRangePicker
|
||||
value={historyDateRange as any}
|
||||
defaultValue={defaultHistoryDateRange as any}
|
||||
onValueChange={(value) =>
|
||||
setHistoryDateRange((value as any) ?? undefined)
|
||||
}
|
||||
className="max-w-[26rem]"
|
||||
<div className="mt-10 px-2 pb-8">
|
||||
<h3 className="mb-4 text-[1.6rem] font-semibold text-title">
|
||||
검증 이력
|
||||
</h3>
|
||||
<div className="card card-border !m-0 !mb-2 flex justify-between">
|
||||
<div className="flex items-center gap-4 px-2">
|
||||
<span className="text-[1.3rem] font-medium text-label">
|
||||
검증일시
|
||||
</span>
|
||||
<DateRangePicker
|
||||
value={historyDateRange}
|
||||
defaultValue={defaultHistoryDateRange}
|
||||
onValueChange={(value) =>
|
||||
setHistoryDateRange(
|
||||
value?.from
|
||||
? (value as DateRangeValue)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
className="max-w-[26rem]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={handleHistoryDateSearch}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={handleHistoryDateReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleHistoryRowClick}
|
||||
viewPagination={true}
|
||||
className="[&_td]:!p-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={handleHistoryDateSearch}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
className="h-[3.2rem] px-4 text-[1.3rem]"
|
||||
onClick={handleHistoryDateReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleHistoryRowClick}
|
||||
viewPagination={true}
|
||||
className="[&_td]:!p-0"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={outlierDialog.open} onOpenChange={closeOutlierDialog}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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";
|
||||
|
||||
interface HbVerificationDetailProps {
|
||||
anlsSn: number;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
anlsSn,
|
||||
onBack,
|
||||
}) => {
|
||||
const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedSido, setSelectedSido] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={onBack}
|
||||
className=""
|
||||
>
|
||||
<i className="diveicon di-arrow-back" />
|
||||
<span className="sr-only">목록으로 돌아가기</span>
|
||||
</Button>
|
||||
<h2 className="text-[1.8rem] font-bold text-title">
|
||||
검증 상세
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="h-[20rem] overflow-y-auto">
|
||||
<AreaAVerificationList
|
||||
anlsSn={anlsSn}
|
||||
selectedVrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
onSelect={setSelectedVrfcTrgtSn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card card-border !m-4">
|
||||
<h3 className="p-4 pb-0 text-[1.8rem] font-bold text-title">
|
||||
검증 결과 상세
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<div className="flex-[0.5] p-4">
|
||||
<AreaBRegionalGrid
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
onSidoSelect={setSelectedSido}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-[0.5] flex-col gap-4 p-4 pt-[4.8rem]">
|
||||
<div className="flex-[0.5]">
|
||||
<AreaDIndicators
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-[0.5]">
|
||||
<AreaCScatterChart
|
||||
anlsSn={anlsSn}
|
||||
vrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
selectedSido={selectedSido}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HbVerificationDetail;
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
|
||||
interface IndicatorsProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
}
|
||||
|
||||
const Indicators: React.FC<IndicatorsProps> = ({ anlsSn, vrfcTrgtSn }) => {
|
||||
const { data: chartData, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"chart",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
|
||||
const dtl = chartData?.dtl;
|
||||
|
||||
if (!vrfcTrgtSn) {
|
||||
return (
|
||||
<div className="card card-border flex min-h-[10rem] items-center justify-center gap-6 text-[1.4rem] text-muted-foreground">
|
||||
항목을 선택해주세요
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const IndicatorItem = ({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: number | null | undefined;
|
||||
}) => (
|
||||
<div className="flex flex-1 flex-col gap-2 overflow-hidden rounded border bg-gray-50 p-2">
|
||||
<h4
|
||||
className="truncate text-[1.2rem] font-medium text-label"
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</h4>
|
||||
<div className="truncate text-[1.6rem] font-bold">
|
||||
{value != null ? Number(value).toFixed(4) : "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card card-border !m-0 flex min-h-[10rem] flex-wrap gap-2">
|
||||
<IndicatorItem label="r 중위수" value={dtl?.rtCnterVl} />
|
||||
<IndicatorItem
|
||||
label="(E_m)E 중위수"
|
||||
value={dtl?.lastTrsfScrCnterVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_l)E 하한값"
|
||||
value={dtl?.lastTrsfScrLlmtVl}
|
||||
/>
|
||||
<IndicatorItem
|
||||
label="(E_u)E 상한값"
|
||||
value={dtl?.lastTrsfScrUplmtVl}
|
||||
/>
|
||||
<IndicatorItem label="E_D_L" value={dtl?.llmtVl} />
|
||||
<IndicatorItem label="E_D_U" value={dtl?.uplmtVl} />
|
||||
<IndicatorItem label="이상치 하한선" value={dtl?.otlrLlmtLine} />
|
||||
<IndicatorItem label="이상치 상한선" value={dtl?.otlrUplmtLine} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indicators;
|
||||
@@ -0,0 +1,269 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
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 {
|
||||
Button,
|
||||
DataTable,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
interface RegionalGridProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
selectedSido: string | null;
|
||||
onSidoSelect: (sido: string | null) => void;
|
||||
}
|
||||
|
||||
const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
selectedSido,
|
||||
onSidoSelect,
|
||||
}) => {
|
||||
const [selectedGugun, setSelectedGugun] = useState<string>("ALL");
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>("ALL");
|
||||
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
});
|
||||
|
||||
const sidoOptions = useMemo(() => {
|
||||
if (!regionCodes) return [];
|
||||
return regionCodes.filter(
|
||||
(code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4,
|
||||
);
|
||||
}, [regionCodes]);
|
||||
|
||||
const gugunOptions = useMemo(() => {
|
||||
if (!regionCodes || !selectedSido || selectedSido === "ALL") return [];
|
||||
|
||||
const sidoPrefix = selectedSido;
|
||||
return regionCodes.filter(
|
||||
(code) =>
|
||||
code.lwpoCd.startsWith(sidoPrefix) && code.lwpoCd.length === 7,
|
||||
);
|
||||
}, [regionCodes, selectedSido]);
|
||||
|
||||
const {
|
||||
data: resultData,
|
||||
isLoading,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"list",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getResultList({
|
||||
anlsSn,
|
||||
vrfcTrgtSn: Number(vrfcTrgtSn),
|
||||
page: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
|
||||
|
||||
if (selectedSido && selectedSido !== "ALL") {
|
||||
const sidoName = sidoOptions.find(
|
||||
(opt) => opt.lwpoCd === selectedSido,
|
||||
)?.lwpoCdNm;
|
||||
if (sidoName) {
|
||||
data = data.filter((item) => item.sido === sidoName);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedGugun && selectedGugun !== "ALL") {
|
||||
const gugunName = gugunOptions.find(
|
||||
(opt) => opt.lwpoCd === selectedGugun,
|
||||
)?.lwpoCdNm;
|
||||
if (gugunName) {
|
||||
data = data.filter((item) => item.gugun === gugunName);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedStatus && selectedStatus !== "ALL") {
|
||||
const statusTarget = selectedStatus === "NORMAL" ? "y" : "n";
|
||||
data = data.filter(
|
||||
(item) => item.otlrYn.toLowerCase() === statusTarget,
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [
|
||||
resultData,
|
||||
selectedSido,
|
||||
selectedGugun,
|
||||
selectedStatus,
|
||||
sidoOptions,
|
||||
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();
|
||||
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 "-";
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
initialState: {
|
||||
pagination: { pageSize: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
onSidoSelect(val === "ALL" ? null : val);
|
||||
setSelectedGugun("ALL");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedSido || "ALL"}
|
||||
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}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedGugun}
|
||||
onValueChange={setSelectedGugun}
|
||||
disabled={!selectedSido || selectedSido === "ALL"}
|
||||
>
|
||||
<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}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={selectedStatus}
|
||||
onValueChange={setSelectedStatus}
|
||||
>
|
||||
<SelectTrigger className="w-[10rem] bg-white">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체 상태</SelectItem>
|
||||
<SelectItem value="NORMAL">정상</SelectItem>
|
||||
<SelectItem value="OUTLIER">이상치</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => refetch()}>조회</Button>
|
||||
<Button color="gray">다운로드</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>
|
||||
)}
|
||||
{!isLoading && (!vrfcTrgtSn || rowData.length === 0) && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
{vrfcTrgtSn
|
||||
? "표시할 항목이 없습니다."
|
||||
: "항목을 선택해주세요"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionalGrid;
|
||||
@@ -0,0 +1,253 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Scatter } from "react-chartjs-2";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
ScatterController,
|
||||
Tooltip,
|
||||
} 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 type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
|
||||
ChartJS.register(
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ScatterController,
|
||||
zoomPlugin,
|
||||
);
|
||||
|
||||
interface ScatterChartProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
selectedSido: string | null;
|
||||
}
|
||||
|
||||
const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
selectedSido,
|
||||
}) => {
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
});
|
||||
|
||||
const selectedSidoName = useMemo(() => {
|
||||
if (!selectedSido || !regionCodes) return null;
|
||||
return (
|
||||
regionCodes.find((c) => c.lwpoCd === selectedSido)?.lwpoCdNm || null
|
||||
);
|
||||
}, [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 isLoading = isLoadingGrid || isLoadingChart;
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
if (!resultData || !chartData) return null;
|
||||
|
||||
const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[];
|
||||
const boundaries = chartData.list ?? [];
|
||||
|
||||
const sortedBoundaries = [...boundaries].sort(
|
||||
(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 normalPoints: {
|
||||
x: number;
|
||||
y: number;
|
||||
item: StAnlsRsltResponse;
|
||||
}[] = [];
|
||||
const outlierPoints: {
|
||||
x: number;
|
||||
y: number;
|
||||
item: StAnlsRsltResponse;
|
||||
}[] = [];
|
||||
|
||||
rawPoints.forEach((item) => {
|
||||
if (selectedSidoName && item.sido !== selectedSidoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pt = {
|
||||
x: Number(item.prvyrVl ?? 0),
|
||||
y: Number(item.cryrVl ?? 0),
|
||||
item,
|
||||
};
|
||||
|
||||
if (item.otlrYn.toLowerCase() === "y") {
|
||||
normalPoints.push(pt);
|
||||
} else {
|
||||
outlierPoints.push(pt);
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
datasets: [
|
||||
{
|
||||
label: "상한선",
|
||||
data: upperLineData,
|
||||
type: "line" as const,
|
||||
borderColor: "rgba(255, 99, 132, 0.8)",
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "하한선",
|
||||
data: lowerLineData,
|
||||
type: "line" as const,
|
||||
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,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
{
|
||||
label: "정상",
|
||||
data: normalPoints,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.7)",
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 7,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "xy" as const,
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
enabled: true,
|
||||
},
|
||||
pinch: {
|
||||
enabled: true,
|
||||
},
|
||||
mode: "xy" as const,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
if (context.dataset.type === "line") {
|
||||
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})`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "linear" as const,
|
||||
position: "bottom" as const,
|
||||
title: {
|
||||
display: true,
|
||||
text: "이전년도 값",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear" as const,
|
||||
title: {
|
||||
display: true,
|
||||
text: "당해년도 값",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { data, options };
|
||||
}, [resultData, chartData, selectedSidoName]);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
{!vrfcTrgtSn ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
항목을 선택해주세요
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
차트를 불러오는 중...
|
||||
</div>
|
||||
) : chartConfig ? (
|
||||
<div className="h-[40rem] w-full">
|
||||
<Scatter
|
||||
data={chartConfig.data as any}
|
||||
options={chartConfig.options as any}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
표시할 데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScatterChart;
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
|
||||
import { DataTable } from "@common/components";
|
||||
|
||||
interface VerificationListProps {
|
||||
anlsSn: number;
|
||||
selectedVrfcTrgtSn: string | null;
|
||||
onSelect: (vrfcTrgtSn: string) => void;
|
||||
}
|
||||
|
||||
const VerificationList: React.FC<VerificationListProps> = ({
|
||||
anlsSn,
|
||||
selectedVrfcTrgtSn,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { data: verificationTargetCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"],
|
||||
queryFn: () =>
|
||||
verificationTargetManagementApi.getVerificationTargetCodes(),
|
||||
});
|
||||
|
||||
const targetCodeMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
if (verificationTargetCodes) {
|
||||
verificationTargetCodes.forEach((item) => {
|
||||
map.set(item.vrfcTrgtSn, item.vrfcTrgtNm);
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [verificationTargetCodes]);
|
||||
|
||||
const { data: detailData, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn],
|
||||
queryFn: () =>
|
||||
stAnlsDtlApi.getDetailList({ anlsSn, page: 0, size: 100 }),
|
||||
enabled: !!anlsSn,
|
||||
});
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
return (detailData?.content ?? []) as StAnlsDtlResponse[];
|
||||
}, [detailData]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: "검증 항목",
|
||||
accessorFn: (row: StAnlsDtlResponse) =>
|
||||
targetCodeMap.get(String(row.vrfcTrgtSn)) ||
|
||||
String(row.vrfcTrgtSn),
|
||||
id: "vrfcTrgtNm",
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
header: "μ",
|
||||
accessorKey: "ipcrCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "Percentile",
|
||||
accessorKey: "pctlCrtrVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "A",
|
||||
accessorKey: "scopAjmtCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "C",
|
||||
accessorKey: "thrsCntVl",
|
||||
size: 60,
|
||||
},
|
||||
{
|
||||
header: "정상",
|
||||
accessorKey: "nmlDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
{
|
||||
header: "제외",
|
||||
accessorKey: "exclDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
{
|
||||
header: "이상",
|
||||
accessorKey: "otlrDataNocs",
|
||||
size: 70,
|
||||
},
|
||||
],
|
||||
[targetCodeMap],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rowData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const handleRowClick = (row: StAnlsDtlResponse) => {
|
||||
onSelect(String(row.vrfcTrgtSn));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedVrfcTrgtSn && rowData.length > 0) {
|
||||
const firstTargetSn = rowData[0]?.vrfcTrgtSn;
|
||||
if (firstTargetSn) onSelect(String(firstTargetSn));
|
||||
}
|
||||
}, [selectedVrfcTrgtSn, rowData, onSelect]);
|
||||
|
||||
return (
|
||||
<div className="flex-1 px-4 py-2">
|
||||
{isLoading && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
데이터를 불러오는 중...
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length === 0 && (
|
||||
<div className="p-4 text-center text-[1.3rem] text-muted-foreground">
|
||||
표시할 항목이 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination={false}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationList;
|
||||
Reference in New Issue
Block a user