[st] feat: H-B 통계 검증 페이지 API 연결 작업
This commit is contained in:
@@ -1,115 +1,343 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
// import { DataGrid } from "@rsone/components";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Checkbox,
|
||||
} from "@common/components";
|
||||
import { verificationHistoryColumnInfo } from "@/entities/statistics/model/verification-history-column";
|
||||
import { verificationHistoryMockData } from "@/entities/statistics/model/verification-history-mock";
|
||||
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,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "@/entities/statistics/model/analysis-history-interface";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { CodeApi } from "@/entities/code";
|
||||
|
||||
interface VerificationFormValues {
|
||||
type VerifyType = "ALL" | "SELECT";
|
||||
|
||||
interface VerificationFormState {
|
||||
mu: string;
|
||||
percentile: string;
|
||||
a: string;
|
||||
c: string;
|
||||
verificationYear: string;
|
||||
checkboxOptions: {
|
||||
sidoDistribution: boolean;
|
||||
countyDistribution: boolean;
|
||||
countyDistributionUserManagement: boolean;
|
||||
lowerDistribution: boolean;
|
||||
sidoServiceAvailability: boolean;
|
||||
individualService: boolean;
|
||||
legalServiceUsage: boolean;
|
||||
excessUse: boolean;
|
||||
lowerOrgStatistics: boolean;
|
||||
studentStatistics: boolean;
|
||||
developmentStatistics: boolean;
|
||||
lowerManagementEnterprise: boolean;
|
||||
}
|
||||
|
||||
interface OutlierDialogState {
|
||||
open: boolean;
|
||||
anlsSn: number | null;
|
||||
}
|
||||
|
||||
function getVerificationYearOptions(): string[] {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const startYear = 2017;
|
||||
const endYear = currentYear - 1;
|
||||
if (endYear < startYear) return [];
|
||||
return Array.from({ length: endYear - startYear + 1 }, (_, index) =>
|
||||
String(startYear + index),
|
||||
);
|
||||
}
|
||||
|
||||
function toNumber(value: string): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function mapHistoryToRow(
|
||||
item: StAnlsMstResponse,
|
||||
): Record<string, string | number> {
|
||||
const executedAt = item.anlsBgngDt ?? "";
|
||||
const endedAt = item.anlsEndDt ?? "";
|
||||
const verificationYear =
|
||||
executedAt && executedAt.length >= 4 ? executedAt.slice(0, 4) : "";
|
||||
|
||||
return {
|
||||
executedAt,
|
||||
progressStatus: item.anlsSttsNm ?? "",
|
||||
endedAt,
|
||||
verificationYear,
|
||||
mu: 0,
|
||||
percentile: 0,
|
||||
a: 0,
|
||||
c: 0,
|
||||
target: "",
|
||||
normalCount: item.nmlDataNocs ?? 0,
|
||||
excludedCount: item.exclDataNocs ?? 0,
|
||||
abnormalCount: item.otlrDataNocs ?? 0,
|
||||
anlsSn: item.anlsSn,
|
||||
};
|
||||
}
|
||||
|
||||
const HbStatisticsVerification = () => {
|
||||
const [verificationHistoryRowData, setVerificationHistoryRowData] =
|
||||
useState<Record<string, string | number>[]>(
|
||||
verificationHistoryMockData,
|
||||
);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [formValues, setFormValues] = useState<VerificationFormValues>({
|
||||
const yearOptions = useMemo(() => getVerificationYearOptions(), []);
|
||||
const defaultYear: string =
|
||||
yearOptions.length > 0
|
||||
? (yearOptions[yearOptions.length - 1] ?? "")
|
||||
: "";
|
||||
|
||||
const [verifyType, setVerifyType] = useState<VerifyType>("ALL");
|
||||
const [formState, setFormState] = useState<VerificationFormState>({
|
||||
mu: "0",
|
||||
percentile: "0",
|
||||
a: "0",
|
||||
c: "0",
|
||||
verificationYear: "2025",
|
||||
checkboxOptions: {
|
||||
sidoDistribution: false,
|
||||
countyDistribution: false,
|
||||
countyDistributionUserManagement: false,
|
||||
lowerDistribution: false,
|
||||
sidoServiceAvailability: false,
|
||||
individualService: false,
|
||||
legalServiceUsage: false,
|
||||
excessUse: false,
|
||||
lowerOrgStatistics: false,
|
||||
studentStatistics: false,
|
||||
developmentStatistics: false,
|
||||
lowerManagementEnterprise: false,
|
||||
verificationYear: defaultYear,
|
||||
});
|
||||
const [selectedTargets, setSelectedTargets] = useState<string[]>([]);
|
||||
const [outlierDialog, setOutlierDialog] = useState<OutlierDialogState>({
|
||||
open: false,
|
||||
anlsSn: null,
|
||||
});
|
||||
|
||||
const { data: statsItemList } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"],
|
||||
queryFn: async () => {
|
||||
const response = await CodeApi.getCode("STITEM");
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
const handleCheckboxChange = (
|
||||
key: keyof VerificationFormValues["checkboxOptions"],
|
||||
) => {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
checkboxOptions: {
|
||||
...prev.checkboxOptions,
|
||||
[key]: !prev.checkboxOptions[key],
|
||||
},
|
||||
}));
|
||||
};
|
||||
const { data: analysisHistory } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST],
|
||||
queryFn: async () => {
|
||||
const response = await analysisHistoryApi.getAnalysisHistoryList({
|
||||
page: 0,
|
||||
size: 10,
|
||||
});
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
const handleInputChange = (
|
||||
key: keyof Omit<VerificationFormValues, "checkboxOptions">,
|
||||
const historyRowData: Record<string, string | number>[] = useMemo(
|
||||
() =>
|
||||
(analysisHistory?.content ?? []).map((item) =>
|
||||
mapHistoryToRow(item as StAnlsMstResponse),
|
||||
),
|
||||
[analysisHistory],
|
||||
);
|
||||
|
||||
const { data: outlierList, isLoading: isLoadingOutlier } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
|
||||
"outlier",
|
||||
outlierDialog.anlsSn,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!outlierDialog.anlsSn) return [];
|
||||
const response = await analysisHistoryApi.getOutlierList(
|
||||
outlierDialog.anlsSn,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
enabled: outlierDialog.open && !!outlierDialog.anlsSn,
|
||||
});
|
||||
|
||||
const analysisMutation = useMutation<
|
||||
AnalysisResponseDto,
|
||||
Error,
|
||||
AnalysisRequestPayload
|
||||
>({
|
||||
mutationFn: async (payload) => analysisApi.analyze(payload),
|
||||
onSuccess: () => {
|
||||
alert("검증을 요청했습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST],
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
alert("검증 실행 중 오류가 발생했습니다.");
|
||||
},
|
||||
});
|
||||
|
||||
function handleFormChange<Key extends keyof VerificationFormState>(
|
||||
key: Key,
|
||||
value: string,
|
||||
) => {
|
||||
setFormValues((prev) => ({
|
||||
) {
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log("검증 실행:", formValues);
|
||||
};
|
||||
function handleTargetToggle(statsCd: string) {
|
||||
setSelectedTargets((prev) =>
|
||||
prev.includes(statsCd)
|
||||
? prev.filter((cd) => cd !== statsCd)
|
||||
: [...prev, statsCd],
|
||||
);
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
console.log("초기화");
|
||||
};
|
||||
async function handleSubmit() {
|
||||
if (!formState.verificationYear) {
|
||||
alert("검증년도를 선택해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifyType === "SELECT" && selectedTargets.length === 0) {
|
||||
alert("검증 대상을 1개 이상 선택해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const yearNew = toNumber(formState.verificationYear);
|
||||
const payload: AnalysisRequestPayload = {
|
||||
dataset:
|
||||
verifyType === "ALL"
|
||||
? "ALL"
|
||||
: selectedTargets.length > 0
|
||||
? selectedTargets.join(",")
|
||||
: undefined,
|
||||
yearNew,
|
||||
yearOld: yearNew - 1,
|
||||
mu: toNumber(formState.mu),
|
||||
percentile: toNumber(formState.percentile),
|
||||
a: toNumber(formState.a),
|
||||
c: toNumber(formState.c),
|
||||
vrfcMthdCd: "HB",
|
||||
allVerifyYn: verifyType === "ALL" ? "Y" : "N",
|
||||
};
|
||||
|
||||
analysisMutation.mutate(payload);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
setFormState({
|
||||
mu: "0",
|
||||
percentile: "0",
|
||||
a: "0",
|
||||
c: "0",
|
||||
verificationYear: defaultYear,
|
||||
});
|
||||
setSelectedTargets([]);
|
||||
}
|
||||
|
||||
function handleHistoryRowClick(row: Record<string, string | number>) {
|
||||
const abnormalCount = Number(row.abnormalCount ?? 0);
|
||||
const anlsSn = Number(row.anlsSn ?? 0);
|
||||
if (!anlsSn || abnormalCount <= 0) return;
|
||||
setOutlierDialog({ open: true, anlsSn });
|
||||
}
|
||||
|
||||
function closeOutlierDialog() {
|
||||
setOutlierDialog({ open: false, anlsSn: null });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card card-border mt-4">
|
||||
<div className="p-2">
|
||||
{/* 통계 검증 섹션 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="px-2 text-[1.5rem] font-semibold text-title">
|
||||
통계 검증
|
||||
</h3>
|
||||
<>
|
||||
<div className="card card-border flex flex-col gap-6">
|
||||
{/* 검증 종류 선택 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 pl-2">
|
||||
<span className="text-[1.3rem] font-medium text-label">
|
||||
검증 방식
|
||||
</span>
|
||||
<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 className="flex justify-center gap-4">
|
||||
<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>
|
||||
{/* //검증 종류 선택 */}
|
||||
|
||||
<div className="flex gap-2">
|
||||
{/* 왼쪽: 검증 변수 */}
|
||||
<div className="card card-border flex-[0.5] rounded p-4">
|
||||
{verifyType === "SELECT" && (
|
||||
<div className="flex gap-4">
|
||||
{/* 검증 대상 */}
|
||||
<div className="flex-1">
|
||||
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
|
||||
검증 대상
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4 p-2">
|
||||
{verifyType === "SELECT" &&
|
||||
statsItemList &&
|
||||
statsItemList.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{statsItemList.map((item) => (
|
||||
<div
|
||||
key={item.lwpoCd}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={item.lwpoCd}
|
||||
checked={selectedTargets.includes(
|
||||
item.lwpoCd,
|
||||
)}
|
||||
onCheckedChange={() =>
|
||||
handleTargetToggle(
|
||||
item.lwpoCd,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={item.lwpoCd}
|
||||
className="cursor-pointer text-label"
|
||||
>
|
||||
{item.lwpoCdNm}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* //검증 대상 */}
|
||||
|
||||
{/* 검증 변수 */}
|
||||
<div className="flex-[0.5]">
|
||||
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
|
||||
검증 변수
|
||||
</h4>
|
||||
@@ -119,11 +347,11 @@ const HbStatisticsVerification = () => {
|
||||
<Label className="w-32 text-label">μ</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formValues.mu}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
value={formState.mu}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"mu",
|
||||
e.target.value,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
@@ -136,11 +364,11 @@ const HbStatisticsVerification = () => {
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formValues.percentile}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
value={formState.percentile}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"percentile",
|
||||
e.target.value,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
@@ -151,11 +379,11 @@ const HbStatisticsVerification = () => {
|
||||
<Label className="w-32 text-label">A</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formValues.a}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
value={formState.a}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"a",
|
||||
e.target.value,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
@@ -166,11 +394,11 @@ const HbStatisticsVerification = () => {
|
||||
<Label className="w-32 text-label">C</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={formValues.c}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
value={formState.c}
|
||||
onChange={(event) =>
|
||||
handleFormChange(
|
||||
"c",
|
||||
e.target.value,
|
||||
event.target.value,
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
@@ -182,9 +410,9 @@ const HbStatisticsVerification = () => {
|
||||
검증 년도
|
||||
</Label>
|
||||
<Select
|
||||
value={formValues.verificationYear}
|
||||
value={formState.verificationYear}
|
||||
onValueChange={(value) =>
|
||||
handleInputChange(
|
||||
handleFormChange(
|
||||
"verificationYear",
|
||||
value,
|
||||
)
|
||||
@@ -194,289 +422,86 @@ const HbStatisticsVerification = () => {
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="2025">
|
||||
2025년
|
||||
</SelectItem>
|
||||
{yearOptions.map((year) => (
|
||||
<SelectItem
|
||||
key={year}
|
||||
value={year}
|
||||
>
|
||||
{year}년
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 검증 대상 */}
|
||||
<div className="card card-border flex-1 rounded p-4">
|
||||
<h4 className="mb-3 rounded bg-gray-100 p-2 text-[1.4rem] font-medium">
|
||||
검증 대상
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4 p-2">
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.sidoDistribution
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"sidoDistribution",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수도 보급률
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.countyDistribution
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"countyDistribution",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수관로
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.countyDistributionUserManagement
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"countyDistributionUserManagement",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수관로 유지관리
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.lowerDistribution
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"lowerDistribution",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수저류시설 및 빗물펌프장
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.sidoServiceAvailability
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"sidoServiceAvailability",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
펌프장
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.individualService
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"individualService",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
공공하수처리시설
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.sidoServiceAvailability
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"sidoServiceAvailability",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
분뇨처리시설
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.individualService
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"individualService",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
개인하수처리시설
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.lowerOrgStatistics
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"lowerOrgStatistics",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수도재정
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.studentStatistics
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"studentStatistics",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
물 재이용
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.lowerOrgStatistics
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"lowerOrgStatistics",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수 및 분뇨찌꺼기(슬러지) 처리
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.studentStatistics
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"studentStatistics",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수 및 분뇨관련산업
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={
|
||||
formValues.checkboxOptions
|
||||
.lowerManagementEnterprise
|
||||
}
|
||||
onCheckedChange={() =>
|
||||
handleCheckboxChange(
|
||||
"lowerManagementEnterprise",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Label className="cursor-pointer text-label">
|
||||
하수도 서비스 수준
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* //검증 변수 */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 버튼 */}
|
||||
<div className="mt-6 flex justify-center gap-4 pt-4">
|
||||
<Button
|
||||
color="green"
|
||||
className="h-[3.6rem] w-[120px]"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
검증 실행
|
||||
</Button>
|
||||
<Button
|
||||
color="gray"
|
||||
className="h-[3.6rem] w-[120px]"
|
||||
onClick={handleReset}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 검증 이력 섹션 */}
|
||||
<div className="mt-8 border-t border-border pt-6">
|
||||
<h3 className="mb-4 px-2 text-[1.5rem] font-semibold text-title">
|
||||
검증 이력
|
||||
</h3>
|
||||
<DataGrid
|
||||
columnInfo={verificationHistoryColumnInfo}
|
||||
rowData={verificationHistoryRowData}
|
||||
setRowData={setVerificationHistoryRowData}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 px-2">
|
||||
<h3 className="mb-2 px-2 text-[1.5rem] font-semibold text-title">
|
||||
검증 이력
|
||||
</h3>
|
||||
<DataGrid
|
||||
columnInfo={verificationHistoryColumnInfo}
|
||||
rowData={historyRowData}
|
||||
setRowData={() => {}}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
onRowClick={handleHistoryRowClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName === "abnormalCount") {
|
||||
const abnormal = Number(row.abnormalCount ?? 0);
|
||||
if (abnormal > 0) return "rg-cell-verify-error";
|
||||
}
|
||||
return "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Dialog open={outlierDialog.open} onOpenChange={closeOutlierDialog}>
|
||||
<DialogContent className="max-w-[800px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>이상건수 상세</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoadingOutlier && (
|
||||
<p className="px-2 py-4 text-[1.3rem] text-muted-foreground">
|
||||
이상치 데이터를 불러오는 중입니다.
|
||||
</p>
|
||||
)}
|
||||
{!isLoadingOutlier && (outlierList?.length ?? 0) === 0 && (
|
||||
<p className="px-2 py-4 text-[1.3rem] text-muted-foreground">
|
||||
표시할 이상치 데이터가 없습니다.
|
||||
</p>
|
||||
)}
|
||||
{!isLoadingOutlier && (outlierList?.length ?? 0) > 0 && (
|
||||
<div className="max-h-[40rem] space-y-3 overflow-auto px-1 py-2 text-[1.25rem]">
|
||||
{outlierList?.map(
|
||||
(item: StAnlsOutlierListResponse) => (
|
||||
<div
|
||||
key={`${item.anlsSn}-${item.vrfcTrgtSn}`}
|
||||
className="rounded border border-border px-3 py-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.vrfcTrgtNm}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
이상건수:{" "}
|
||||
{item.otlrDataNocs ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
AnalysisRequestPayload,
|
||||
AnalysisResponseDto,
|
||||
JobStatusDto,
|
||||
AnalysisResultDto,
|
||||
} from "../model/analysis-interface";
|
||||
|
||||
export class AnalysisApiService {
|
||||
public async analyze(
|
||||
payload: AnalysisRequestPayload,
|
||||
): Promise<AnalysisResponseDto> {
|
||||
const response = await client.post<AnalysisResponseDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
|
||||
payload,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
|
||||
const response = await client.get<JobStatusDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getResult(jobId: string): Promise<AnalysisResultDto> {
|
||||
const response = await client.get<AnalysisResultDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const analysisApi = new AnalysisApiService();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
StAnlsMstListApiResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "../model/analysis-history-interface";
|
||||
|
||||
export class AnalysisHistoryApiService {
|
||||
public async getAnalysisHistoryList(
|
||||
params: StAnlsMstSearchParams,
|
||||
): Promise<StAnlsMstListApiResponse> {
|
||||
const response = await client.get<StAnlsMstListApiResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getOutlierList(
|
||||
anlsSn: number,
|
||||
): Promise<StAnlsOutlierListResponse[]> {
|
||||
const response = await client.get<StAnlsOutlierListResponse[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/outlierList/${anlsSn}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const analysisHistoryApi = new AnalysisHistoryApiService();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
export interface StAnlsMstResponse {
|
||||
anlsSn: number;
|
||||
anlsBgngDt: string;
|
||||
anlsEndDt: string | null;
|
||||
anlsSttsCd: string;
|
||||
anlsSttsNm: string;
|
||||
nmlDataNocs: number;
|
||||
exclDataNocs: number;
|
||||
otlrDataNocs: number;
|
||||
jobUuid: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
rgtrNm: string;
|
||||
}
|
||||
|
||||
export interface StAnlsMstSearchParams {
|
||||
anlsSttsCd?: string;
|
||||
jobUuid?: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export interface StAnlsOutlierDataResponse {
|
||||
rsltSn: number;
|
||||
vrfcTrgtSn: number;
|
||||
admdstCd: string;
|
||||
sido: string;
|
||||
gugun: string;
|
||||
adstDong: string;
|
||||
addr: string;
|
||||
cryrVl: number;
|
||||
otlrTrsfRt: number;
|
||||
}
|
||||
|
||||
export interface StAnlsOutlierListResponse {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: number;
|
||||
vrfcTrgtNm: string;
|
||||
nmlDataNocs: number;
|
||||
exclDataNocs: number;
|
||||
otlrDataNocs: number;
|
||||
outlierDataList: StAnlsOutlierDataResponse[];
|
||||
}
|
||||
|
||||
export type StAnlsMstListApiResponse = PageableList<StAnlsMstResponse>;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { PageableList } from "@/shared/model";
|
||||
|
||||
export interface AnalysisRequestPayload {
|
||||
dataset?: string;
|
||||
yearOld: number;
|
||||
yearNew: number;
|
||||
mu: number;
|
||||
a: number;
|
||||
c: number;
|
||||
percentile: number;
|
||||
sido?: string;
|
||||
sigungu?: string;
|
||||
vrfcMthdCd?: string;
|
||||
allVerifyYn?: "Y" | "N";
|
||||
}
|
||||
|
||||
export interface AnalysisResponseDto {
|
||||
jobId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
queuePosition: number;
|
||||
createdAt: string;
|
||||
anlsSn: number;
|
||||
}
|
||||
|
||||
export interface JobStatusDto {
|
||||
jobId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
queuePosition: number;
|
||||
progress: number;
|
||||
startedAt: string | null;
|
||||
completedAt: string | null;
|
||||
error: string | null;
|
||||
anlsSn: number | null;
|
||||
processingTimeMs: number | null;
|
||||
}
|
||||
|
||||
export interface AnalysisSummaryDto {
|
||||
// 요약 필드는 Python 분석 서비스 스키마에 따라 확장
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface OutlierRecordDto {
|
||||
// 상세 구조는 Python 분석 서비스 응답에 맞춰 필요 시 확장
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AnalysisResultDto {
|
||||
jobId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
completedAt: string | null;
|
||||
summary: AnalysisSummaryDto | null;
|
||||
multiSummaries: Record<string, AnalysisSummaryDto> | null;
|
||||
outliers: OutlierRecordDto[];
|
||||
results: OutlierRecordDto[];
|
||||
totalCount: number;
|
||||
cached: boolean;
|
||||
processingTimeMs: number;
|
||||
anlsSn: number;
|
||||
}
|
||||
|
||||
export type AnalysisHistoryListApiResponse = PageableList<AnalysisResultDto>;
|
||||
@@ -72,6 +72,8 @@ export const ENDPOINT = {
|
||||
},
|
||||
STATVERIFY_SERVICE: {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user