[st] feat: 통계 검증 상세 화면 작업
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"@types/xlsx": "^0.0.36",
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.8",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.card {
|
||||
margin: 0.5rem !important;
|
||||
padding: 0.5rem !important;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
StAnlsDtlListApiResponse,
|
||||
StAnlsDtlResponse,
|
||||
StAnlsDtlSearchParams,
|
||||
} from "../model/statistics-analysis-detail-interface";
|
||||
|
||||
export class StAnlsDtlApiService {
|
||||
public async getDetailList(
|
||||
params: StAnlsDtlSearchParams,
|
||||
): Promise<StAnlsDtlListApiResponse> {
|
||||
const response = await client.get<StAnlsDtlListApiResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getDetail(
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: number,
|
||||
): Promise<StAnlsDtlResponse> {
|
||||
const response = await client.get<StAnlsDtlResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL}/${anlsSn}/${vrfcTrgtSn}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const stAnlsDtlApi = new StAnlsDtlApiService();
|
||||
@@ -0,0 +1,37 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ChartRetDto,
|
||||
StAnlsRsltListApiResponse,
|
||||
StAnlsRsltSearchParams,
|
||||
} from "../model/statistics-analysis-result-interface";
|
||||
|
||||
export class StAnlsRsltApiService {
|
||||
public async getResultList(
|
||||
params: StAnlsRsltSearchParams,
|
||||
): Promise<StAnlsRsltListApiResponse> {
|
||||
const response = await client.get<StAnlsRsltListApiResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getChartData(
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: number,
|
||||
): Promise<ChartRetDto> {
|
||||
const response = await client.get<ChartRetDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/chartList/${anlsSn}/${vrfcTrgtSn}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const stAnlsRsltApi = new StAnlsRsltApiService();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface StAnlsDtlResponse {
|
||||
anlsSn: number; // 분석 일련번호
|
||||
vrfcTrgtSn: number; // 검증 대상 일련번호
|
||||
ipcrCntVl: number | null; // 중요도 개수 값 (mu)
|
||||
pctlCrtrVl: number | null; // 백분위 기준 값 (percentile)
|
||||
scopAjmtCntVl: number | null; // 범위 조정 개수 값 (A)
|
||||
thrsCntVl: number | null; // 임계 개수 값 (C)
|
||||
rtCnterVl: number | null; // 비율 중앙값
|
||||
lastTrsfScrCnterVl: number | null; // 최종 변환 점수 중앙값
|
||||
lastTrsfScrLlmtVl: number | null; // 최종 변환 점수 하한값
|
||||
lastTrsfScrUplmtVl: number | null; // 최종 변환 점수 상한값
|
||||
uplmtVl: number | null; // 상한값
|
||||
llmtVl: number | null; // 하한값
|
||||
otlrUplmtLine: number | null; // 이상치 상한선
|
||||
otlrLlmtLine: number | null; // 이상치 하한선
|
||||
nmlDataNocs: number; // 정상 데이터 건수
|
||||
exclDataNocs: number; // 제외 데이터 건수
|
||||
otlrDataNocs: number; // 이상치 데이터 건수
|
||||
}
|
||||
|
||||
export interface StAnlsDtlSearchParams extends PageableParams {
|
||||
anlsSn?: number; // 분석 일련번호
|
||||
vrfcTrgtSn?: number; // 검증 대상 일련번호
|
||||
}
|
||||
|
||||
export type StAnlsDtlListApiResponse = PageableList<StAnlsDtlResponse>;
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
import type { StAnlsDtlResponse } from "./statistics-analysis-detail-interface";
|
||||
|
||||
export interface StAnlsRsltResponse {
|
||||
rsltSn: number; // 결과 일련번호
|
||||
anlsSn: number; // 분석 일련번호
|
||||
vrfcTrgtSn: number; // 검증 대상 일련번호
|
||||
admdstCd: string; // 행정구역 코드
|
||||
admdstNm: string; // 행정구역 명칭
|
||||
sido: string; // 시도
|
||||
gugun: string; // 시군구
|
||||
adstDong: string; // 행정동
|
||||
drngBzmnCd: string; // 하수도 사업자 코드
|
||||
pipeSeCd: string; // 파이프 구분 코드
|
||||
fcltNm: string; // 시설명
|
||||
addr: string; // 주소
|
||||
prvyrVl: number | null; // 전년도 값
|
||||
cryrVl: number | null; // 당해년도 값
|
||||
chgRt: number | null; // 변경율 (r_i)
|
||||
trsfRt: number | null; // 변환 비율 (s_i)
|
||||
otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i)
|
||||
otlrYn: string; // 이상치 여부 (y/n)
|
||||
}
|
||||
|
||||
export interface StAnlsRsltSearchParams extends PageableParams {
|
||||
anlsSn?: number; // 분석 일련번호
|
||||
vrfcTrgtSn?: number; // 검증 대상 일련번호
|
||||
admdstCd?: string; // 행정구역 코드
|
||||
admdstNm?: string; // 행정구역 명칭
|
||||
fcltNm?: string; // 시설명
|
||||
otlrYn?: string; // 이상치 여부 (y/n)
|
||||
}
|
||||
|
||||
export type StAnlsRsltListApiResponse = PageableList<StAnlsRsltResponse>;
|
||||
|
||||
export interface ChartBoundaryPointDto {
|
||||
xPrev: number; // 전년도 값 (x축)
|
||||
yLower: number; // 이상치 하한 당년값 (y축)
|
||||
yUpper: number; // 이상치 상한 당년값 (y축)
|
||||
}
|
||||
|
||||
export interface ChartRetDto {
|
||||
dtl: StAnlsDtlResponse; // 분석 상세 파라미터 (mu, r_mid, T_L, T_U 등)
|
||||
list: ChartBoundaryPointDto[]; // HBR 경계선 곡선 데이터 리스트 (x_prev, y_lower, y_upper)
|
||||
}
|
||||
@@ -76,6 +76,8 @@ export const ENDPOINT = {
|
||||
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",
|
||||
ST_ANLS_DTL: "/statverify-service/api/v1/st-anls-dtl",
|
||||
ST_ANLS_RSLT: "/statverify-service/api/v1/st-anls-rslt",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ import { Button } from "../button";
|
||||
import { Calendar } from "../calendar";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
type DateRangePickerProps = React.HTMLAttributes<HTMLDivElement> &
|
||||
type DateRangePickerProps = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
"defaultValue"
|
||||
> &
|
||||
React.ComponentProps<typeof DayPicker> & {
|
||||
value?: DateRange;
|
||||
defaultValue?: DateRange;
|
||||
|
||||
Generated
+50
-25
@@ -101,10 +101,10 @@ importers:
|
||||
devDependencies:
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
eslint-config-next:
|
||||
specifier: 15.4.0
|
||||
version: 15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
version: 15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
postcss:
|
||||
specifier: ^8.4.49
|
||||
version: 8.5.6
|
||||
@@ -1112,6 +1112,9 @@ importers:
|
||||
chart.js:
|
||||
specifier: ^4.4.8
|
||||
version: 4.5.1
|
||||
chartjs-plugin-zoom:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(chart.js@4.5.1)
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.19
|
||||
@@ -1187,13 +1190,13 @@ importers:
|
||||
version: 5.28.5
|
||||
eslint:
|
||||
specifier: ^9
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
version: 9.39.2(jiti@1.21.7)
|
||||
eslint-config-next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
version: 15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint-plugin-react-hooks:
|
||||
specifier: ^5.0.0
|
||||
version: 5.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
version: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
postcss:
|
||||
specifier: ^8
|
||||
version: 8.5.6
|
||||
@@ -3841,6 +3844,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/hammerjs@2.0.46':
|
||||
resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
|
||||
|
||||
'@types/js-cookie@3.0.6':
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
|
||||
@@ -4444,6 +4450,11 @@ packages:
|
||||
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
|
||||
engines: {pnpm: '>=8'}
|
||||
|
||||
chartjs-plugin-zoom@2.2.0:
|
||||
resolution: {integrity: sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==}
|
||||
peerDependencies:
|
||||
chart.js: '>=3.2.0'
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -5314,6 +5325,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
hammerjs@2.0.8:
|
||||
resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -9305,6 +9320,8 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/hammerjs@2.0.46': {}
|
||||
|
||||
'@types/js-cookie@3.0.6': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
@@ -10134,6 +10151,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@kurkle/color': 0.3.4
|
||||
|
||||
chartjs-plugin-zoom@2.2.0(chart.js@4.5.1):
|
||||
dependencies:
|
||||
'@types/hammerjs': 2.0.46
|
||||
chart.js: 4.5.1
|
||||
hammerjs: 2.0.8
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -10706,6 +10729,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
source-map: 0.7.6
|
||||
|
||||
eslint-config-next@15.1.7(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.7
|
||||
'@rushstack/eslint-patch': 1.15.0
|
||||
'@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.1.7(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.1.7
|
||||
@@ -10726,26 +10769,6 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.4.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.0
|
||||
'@rushstack/eslint-patch': 1.15.0
|
||||
'@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@1.21.7)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@1.21.7))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-config-next@15.4.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.4.0
|
||||
@@ -11344,6 +11367,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
hammerjs@2.0.8: {}
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
Reference in New Issue
Block a user