Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-03-06 13:04:21 +09:00
4 changed files with 206 additions and 259 deletions
+2 -127
View File
@@ -1,129 +1,4 @@
.card {
position: relative;
margin: 0.6rem;
box-sizing: border-box;
min-height: 2rem;
border-radius: 0.5rem;
padding: 0.6rem;
font-size: 1.3rem;
color: #666;
}
.card_width10 {
width: calc(10% - 12px);
}
.card_width20 {
width: calc(20% - 12px);
}
.card_width25 {
width: calc(25% - 12px);
}
.card_width30 {
width: calc(30% - 12px);
}
.card_width33 {
width: calc(33.333% - 12px);
}
.card_width40 {
width: calc(40% - 12px);
}
.card_width45 {
width: calc(45% - 12px);
}
.card_width50 {
width: calc(50% - 12px);
}
.card_width60 {
width: calc(60% - 12px);
}
.card_width66 {
width: calc(66.666% - 12px);
}
.card_width70 {
width: calc(70% - 12px);
}
.card_width75 {
width: calc(75% - 12px);
}
.card_width80 {
width: calc(80% - 12px);
}
.card_width90 {
width: calc(90% - 12px);
}
.card_width100 {
width: calc(100% - 12px);
}
.card_nopadding {
padding: 0 !important;
}
.card_nomargin {
width: 100%;
margin: 0 !important;
padding: 0 !important;
}
.topline {
border-top: 1px solid #ccc;
}
.bottomline {
border-bottom: 1px solid #e6eef3;
background: #fff;
}
.card-border {
border: 1px solid #dbe2e6;
background-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.card-header {
margin-top: -0.6rem;
margin-left: -0.6rem;
margin-right: -0.6rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e1e7eb;
padding: 1rem;
}
.card-footer {
margin-left: -0.6rem;
margin-right: -0.6rem;
margin-top: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-top: 1px solid #e1e7eb;
padding-top: 1rem;
padding-bottom: 1rem;
}
/* 블랙 테마 */
[data-theme="black"] {
.card-border {
background-color: rgba(255, 255, 255, 0.065);
border-color: #38383a;
}
.card-header {
border-bottom-color: #3f4042;
}
margin: 0.5rem !important;
padding: 0.5rem !important;
}
@@ -1,7 +1,7 @@
"use client";
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
@@ -49,7 +49,9 @@ import type {
} from "@/entities/statistics/model/analysis-history-interface";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib";
import { CodeApi } from "@/entities/code";
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";
type DateRangeValue = {
from?: Date;
@@ -78,7 +80,7 @@ function getDefaultHistoryDateRange(): DateRangeValue {
}
function getVerificationYearOptions(): string[] {
const currentYear = new Date().getFullYear();
const currentYear = dayjs().year();
const startYear = 2017;
const endYear = currentYear - 1;
if (endYear < startYear) return [];
@@ -116,6 +118,7 @@ const HbStatisticsVerification = () => {
open: false,
anlsSn: null,
});
const [selectedStatsCd, setSelectedStatsCd] = useState<string>("");
const defaultHistoryDateRange = useMemo(
() => getDefaultHistoryDateRange(),
@@ -133,14 +136,35 @@ const HbStatisticsVerification = () => {
pageSize: 10,
});
const { data: statsItemList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STITEM"],
queryFn: async () => {
const response = await CodeApi.getCode("STITEM");
return response;
},
const { data: verificationTargetCodes } = useQuery<
VerificationTargetCodeResponse[]
>({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"],
queryFn: () =>
verificationTargetManagementApi.getVerificationTargetCodes(),
});
const filteredVerificationTargetCodes = useMemo(() => {
return verificationTargetCodes?.filter((item) =>
selectedStatsCd
? item.vrfcTrgtSn.startsWith(selectedStatsCd)
: true,
);
}, [verificationTargetCodes, selectedStatsCd]);
const allVerificationTargetIds = useMemo(
() =>
(filteredVerificationTargetCodes ?? []).map(
(item) => item.vrfcTrgtSn,
),
[filteredVerificationTargetCodes],
);
useEffect(() => {
if (!allVerificationTargetIds.length) return;
setSelectedTargets(allVerificationTargetIds);
}, [allVerificationTargetIds]);
const { data: analysisHistory } = useQuery({
queryKey: [
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
@@ -268,7 +292,9 @@ const HbStatisticsVerification = () => {
const payload: AnalysisRequestPayload = {
dataset:
verifyType === "ALL"
? "ALL"
? allVerificationTargetIds.length > 0
? allVerificationTargetIds.join(",")
: undefined
: selectedTargets.length > 0
? selectedTargets.join(",")
: undefined,
@@ -329,28 +355,52 @@ const HbStatisticsVerification = () => {
<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 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 justify-center gap-4">
<div className="flex gap-2">
<Button
color="green"
className="h-[3.6rem] w-[120px]"
@@ -378,38 +428,65 @@ const HbStatisticsVerification = () => {
<h4 className="mb-3 rounded bg-gray-100 px-4 py-2 text-[1.4rem] font-medium">
검증 대상
</h4>
<div className="space-y-4 p-2">
<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" &&
statsItemList &&
statsItemList.length > 0 && (
<div className="grid grid-cols-3 gap-4">
{statsItemList.map((item) => (
filteredVerificationTargetCodes &&
filteredVerificationTargetCodes.length > 0 ? (
<div className="grid grid-cols-3 gap-4">
{filteredVerificationTargetCodes.map(
(item) => (
<div
key={item.lwpoCd}
key={item.vrfcTrgtSn}
className="flex items-center gap-2"
>
<Checkbox
id={item.lwpoCd}
id={item.vrfcTrgtSn}
checked={selectedTargets.includes(
item.lwpoCd,
item.vrfcTrgtSn,
)}
onCheckedChange={() =>
handleTargetToggle(
item.lwpoCd,
item.vrfcTrgtSn,
)
}
/>
<Label
htmlFor={item.lwpoCd}
htmlFor={
item.vrfcTrgtSn
}
className="cursor-pointer text-label"
>
{item.lwpoCdNm}
{item.vrfcTrgtNm}
</Label>
</div>
))}
</div>
)}
),
)}
</div>
) : (
<div className="flex items-center justify-center py-4">
<p className="text-[1.3rem] text-muted-foreground">
검증 대상이 없습니다.
</p>
</div>
)}
</div>
</div>
{/* //검증 대상 */}
@@ -482,35 +559,6 @@ const HbStatisticsVerification = () => {
className="flex-1"
/>
</div>
<div className="flex items-center gap-4">
<Label className="w-32 text-label">
검증 년도
</Label>
<Select
value={formState.verificationYear}
onValueChange={(value) =>
handleFormChange(
"verificationYear",
value,
)
}
>
<SelectTrigger className="flex-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
{yearOptions.map((year) => (
<SelectItem
key={year}
value={year}
>
{year}년
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* //검증 변수 */}
@@ -518,22 +566,24 @@ const HbStatisticsVerification = () => {
)}
</div>
<div className="mt-10 px-4 pb-8">
<h3 className="mb-2 px-2 text-[1.5rem] font-semibold text-title">
<div className="mt-10 px-2 pb-8">
<h3 className="mb-4 text-[1.6rem] font-semibold text-title">
검증 이력
</h3>
<div className="mb-4 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="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>
<div className="flex gap-2">
<Button
type="button"
@@ -590,57 +640,58 @@ const HbStatisticsVerification = () => {
<TableHeader>
<TableRow>
<TableHead
colSpan={4}
colSpan={5}
className="bg-muted text-left text-[1.3rem] font-semibold"
>
{item.vrfcTrgtNm}
</TableHead>
</TableRow>
<TableRow>
<TableHead className="bg-muted font-semibold">
시도
</TableHead>
<TableHead className="bg-muted font-semibold">
시군구
</TableHead>
<TableHead className="bg-muted font-semibold">
행정구역
</TableHead>
<TableHead className="bg-muted font-semibold">
시설명
</TableHead>
<TableHead className="bg-muted text-center font-semibold">
입력값
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({
length: Math.ceil(
item.outlierDataList
.length / 4,
{item.outlierDataList.map(
(data) => (
<TableRow key={data.rsltSn}>
<TableCell className="text-center">
{data.sido ?? "-"}
</TableCell>
<TableCell className="text-center">
{data.gugun ?? "-"}
</TableCell>
<TableCell className="text-center">
{data.adstDong ??
"-"}
</TableCell>
<TableCell className="text-center">
{/* 시설명: DTO에 해당 필드 없음 */}
-
</TableCell>
<TableCell className="text-right">
{data.cryrVl != null
? String(
data.cryrVl,
)
: "-"}
</TableCell>
</TableRow>
),
}).map((_, rowIdx) => (
<TableRow key={rowIdx}>
{item.outlierDataList
.slice(
rowIdx * 4,
rowIdx * 4 + 4,
)
.map((data) => (
<TableCell
key={
data.rsltSn
}
className="text-[1.25rem]"
>
{`${data.sido ?? ""}${data.adstDong ? ` ${data.adstDong}` : ""}`.trim() ||
"-"}
</TableCell>
))}
{/* 빈 셀 추가 */}
{item.outlierDataList.slice(
rowIdx * 4,
rowIdx * 4 + 4,
).length < 4 &&
Array.from({
length:
4 -
item.outlierDataList.slice(
rowIdx * 4,
rowIdx * 4 +
4,
).length,
}).map((_, idx) => (
<TableCell
key={`empty-${idx}`}
/>
))}
</TableRow>
))}
)}
</TableBody>
</Table>
),
@@ -2,6 +2,7 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type {
VerificationTargetCodeResponse,
VerificationTargetManagementService,
VerificationTargetManagementSearchParams,
VerificationTargetManagementListApiResponse,
@@ -43,6 +44,18 @@ export class VerificationTargetManagementApiService implements VerificationTarge
return response.data;
}
/**
* 검증 대상 코드 목록 조회
*/
public async getVerificationTargetCodes(): Promise<
VerificationTargetCodeResponse[]
> {
const response = await client.get<VerificationTargetCodeResponse[]>(
`${ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG}/code`,
);
return response.data;
}
/**
* 검증 대상 등록
* @param request 등록 요청 데이터
@@ -1,6 +1,12 @@
import type { PageableList } from "@/shared/model";
import type { ApiResponse } from "./statistics-interface";
// 검증 대상 코드 응답 DTO
export interface VerificationTargetCodeResponse {
vrfcTrgtSn: string;
vrfcTrgtNm: string;
}
// 검증 대상 관리 응답 DTO
export interface VerificationTargetManagementResponse {
vrfcTrgtSn: number; // 검증 대상 일련번호
@@ -90,6 +96,8 @@ export interface VerificationTargetManagementService {
vrfcTrgtSn: number,
): Promise<VerificationTargetManagementDetailApiResponse>;
getVerificationTargetCodes(): Promise<VerificationTargetCodeResponse[]>;
saveVerificationTargetManagement(
request: VerificationTargetManagementSaveRequest,
): Promise<VerificationTargetManagementSaveApiResponse>;