[st] feat: 검증 항목 상세 다이얼로그 작업

This commit is contained in:
2026-02-13 14:57:20 +09:00
parent 93d4cb57e6
commit b644281348
5 changed files with 497 additions and 15 deletions
+2 -2
View File
@@ -1087,10 +1087,10 @@ const DataGrid = ({
) {
try {
// 행 전체 데이터 가져오기
const rowData = grid.getValues(clickData.itemIndex);
const row = rowData[clickData.itemIndex];
// 부모 컴포넌트로 데이터 전달
onRowClick(rowData as Record<string, string | number>);
onRowClick(row ?? {});
} catch (error) {
console.error("행 데이터 가져오기 오류:", error);
}
@@ -17,10 +17,15 @@ import {
} from "@common/components";
import { CodeSelect } from "@/entities/code/ui/code-select";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { ENDPOINT } from "@/shared/config";
import type { VerificationTargetManagementSearchParams } from "@/entities/statistics/model/verification-target-management-interface";
import type {
VerificationTargetManagementSearchParams,
VerificationTargetManagementResponse,
} from "@/entities/statistics/model/verification-target-management-interface";
import { VerificationTargetDialog } from "./VerificationTargetDialog";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
const TAB_CONFIG = {
HB: { value: "HB", label: "H-B", columnInfo: hbArticleColumnInfo },
@@ -35,6 +40,8 @@ const TAB_CONFIG = {
type TabValue = keyof typeof TAB_CONFIG;
function VerificationArticleManagement() {
const queryClient = useQueryClient();
const { confirm } = useConfirm();
const [activeTab, setActiveTab] = useState<TabValue>("HB");
const [statsCd, setStatsCd] = useState("");
const [vrfcTrgtNm, setVrfcTrgtNm] = useState("");
@@ -44,6 +51,9 @@ function VerificationArticleManagement() {
statsCd,
vrfcTrgtNm,
});
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedData, setSelectedData] =
useState<VerificationTargetManagementResponse | null>(null);
const { data: verificationData } = useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, searchParams],
@@ -94,8 +104,51 @@ function VerificationArticleManagement() {
});
}
const updateMutation = useMutation({
mutationFn: (data: VerificationTargetManagementResponse) =>
verificationTargetManagementApi.updateVerificationTargetManagement(
data.vrfcTrgtSn,
{
vrfcMthdCd: data.vrfcMthdCd,
statsCd: data.statsCd,
schmNm: data.schmNm,
tblNm: data.tblNm,
colNm: data.colNm,
pkColNm: data.pkColNm,
ipcrCntVl: data.ipcrCntVl,
pctlCrtrVl: data.pctlCrtrVl,
scopCntCntVl: data.scopCntCntVl,
thrsCntVl: data.thrsCntVl,
useYn: data.useYn,
vrfcTrgtNm: data.vrfcTrgtNm,
},
),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG],
});
setDialogOpen(false);
},
});
const deleteMutation = useMutation({
mutationFn: (vrfcTrgtSn: number) =>
verificationTargetManagementApi.deleteVerificationTargetManagement(
vrfcTrgtSn,
),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG],
});
setDialogOpen(false);
},
});
function handleRowClick(rowData: Record<string, string | number>) {
console.log("클릭한 행 데이터:", rowData);
setSelectedData(
rowData as unknown as VerificationTargetManagementResponse,
);
setDialogOpen(true);
}
function handleSubmit(e: React.FormEvent) {
@@ -103,6 +156,18 @@ function VerificationArticleManagement() {
handleSearch();
}
async function handleSave(data: VerificationTargetManagementResponse) {
if (await confirm("저장하시겠습니까?")) {
updateMutation.mutate(data);
}
}
async function handleDelete(vrfcTrgtSn: number) {
if (await confirm("정말 삭제하시겠습니까?")) {
deleteMutation.mutate(vrfcTrgtSn);
}
}
return (
<Tabs
defaultValue="HB"
@@ -132,7 +197,7 @@ function VerificationArticleManagement() {
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
placeholder="통계항목"
placeholder="선택"
className="w-[16rem]"
/>
</div>
@@ -181,6 +246,14 @@ function VerificationArticleManagement() {
/>
</TabsContent>
))}
<VerificationTargetDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
data={selectedData}
onSave={handleSave}
onDelete={handleDelete}
/>
</Tabs>
);
}
@@ -0,0 +1,380 @@
"use client";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import { CodeSelect } from "@/entities/code/ui/code-select";
import type { VerificationTargetManagementResponse } from "@/entities/statistics/model/verification-target-management-interface";
import {
verificationTargetManagementSchema,
type VerificationTargetManagementFormData,
} from "@/entities/statistics/model/verification-target-management-schema";
import { useEffect } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
interface VerificationTargetDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
data: VerificationTargetManagementResponse | null;
onSave: (data: VerificationTargetManagementResponse) => void;
onDelete: (vrfcTrgtSn: number) => void;
}
export function VerificationTargetDialog({
open,
onOpenChange,
data,
onSave,
onDelete,
}: VerificationTargetDialogProps) {
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<VerificationTargetManagementFormData>({
resolver: zodResolver(verificationTargetManagementSchema),
defaultValues: data || undefined,
});
useEffect(() => {
if (data && open) reset(data);
}, [data, open]);
function onSubmit(formData: VerificationTargetManagementFormData) {
if (!data) return;
onSave({
...data,
...formData,
} as VerificationTargetManagementResponse);
}
function handleDelete() {
if (data?.vrfcTrgtSn) {
onDelete(data.vrfcTrgtSn);
}
}
if (!data) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>검증 항목 상세</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<Table variant="secondary">
<TableBody>
<TableRow>
<TableHead
className="w-[150px] bg-muted font-medium"
data-required
>
통계항목
</TableHead>
<TableCell>
<Controller
name="statsCd"
control={control}
render={({ field }) => (
<div className="space-y-1">
<CodeSelect
upCd="STITEM"
value={field.value}
onValueChange={
field.onChange
}
placeholder="선택"
/>
{errors.statsCd && (
<p className="text-xs text-red-500">
{errors.statsCd.message}
</p>
)}
</div>
)}
/>
</TableCell>
<TableHead
className="w-[150px] bg-muted font-medium"
data-required
>
검증명
</TableHead>
<TableCell>
<Controller
name="vrfcTrgtNm"
control={control}
render={({ field }) => (
<Input
{...field}
value={field.value || ""}
placeholder="검증명"
/>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead
className="bg-muted font-medium"
data-required
>
스키마명
</TableHead>
<TableCell>
<Controller
name="schmNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
{...field}
placeholder="스키마명"
/>
{errors.schmNm && (
<p className="text-xs text-red-500">
{errors.schmNm.message}
</p>
)}
</div>
)}
/>
</TableCell>
<TableHead
className="bg-muted font-medium"
data-required
>
테이블명
</TableHead>
<TableCell>
<Controller
name="tblNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
{...field}
placeholder="테이블명"
/>
{errors.tblNm && (
<p className="text-xs text-red-500">
{errors.tblNm.message}
</p>
)}
</div>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead
className="bg-muted font-medium"
data-required
>
컬럼명
</TableHead>
<TableCell>
<Controller
name="colNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
{...field}
placeholder="컬럼명"
/>
{errors.colNm && (
<p className="text-xs text-red-500">
{errors.colNm.message}
</p>
)}
</div>
)}
/>
</TableCell>
<TableHead
className="bg-muted font-medium"
data-required
>
PK 키
</TableHead>
<TableCell>
<Controller
name="pkColNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
{...field}
placeholder="PK 키"
/>
{errors.pkColNm && (
<p className="text-xs text-red-500">
{errors.pkColNm.message}
</p>
)}
</div>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead className="bg-muted font-medium">
중요도 계수(μ)
</TableHead>
<TableCell>
<Controller
name="ipcrCntVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
step="0.1"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e.target
.value,
)
: null,
)
}
placeholder="(0 ~ 1)"
/>
</div>
)}
/>
</TableCell>
<TableHead className="bg-muted font-medium">
백분위수 기준값(pct)
</TableHead>
<TableCell>
<Controller
name="pctlCrtrVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
step="0.1"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e.target
.value,
)
: null,
)
}
placeholder="(0 ~ 0.5)"
/>
</div>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead className="bg-muted font-medium">
스케일 조정 계수(A)
</TableHead>
<TableCell>
<Controller
name="scopCntCntVl"
control={control}
render={({ field }) => (
<Input
type="number"
step="0.05"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e.target
.value,
)
: null,
)
}
/>
)}
/>
</TableCell>
<TableHead className="bg-muted font-medium">
임계 계수(c)
</TableHead>
<TableCell>
<Controller
name="thrsCntVl"
control={control}
render={({ field }) => (
<Input
type="number"
step="1"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e.target
.value,
)
: null,
)
}
/>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
<DialogFooter className="mt-6">
<Button
type="button"
color="gray"
onClick={() => onOpenChange(false)}
>
취소
</Button>
<Button
type="button"
color="red"
onClick={handleDelete}
>
삭제
</Button>
<Button type="submit" color="green">
저장
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -12,10 +12,10 @@ export interface VerificationTargetManagementResponse {
tblNm: string; // 테이블 명
colNm: string; // 컬럼 명
pkColNm: string; // pk 컬럼 명
ipcrCntVl: number | null; // 중요도 개수 값 (mu)
ipcrCntVl: number | null; // 중요도 계수 값 (mu)
pctlCrtrVl: number | null; // 백분위 기준 값 (percentile)
scopCntCntVl: number | null; // 범위 조정 개수 값 (A)
thrsCntVl: number | null; // 임계 개수 값 (C)
scopCntCntVl: number | null; // 범위 조정 계수 값 (A)
thrsCntVl: number | null; // 임계 계수 값 (C)
useYn: string; // 사용 여부
vrfcTrgtNm: string; // 검증대상명
rgtr: string | null; // 등록자 ID
@@ -47,10 +47,10 @@ export interface VerificationTargetManagementSaveRequest {
tblNm: string; // 테이블 명 (필수, 최대 150자)
colNm: string; // 컬럼 명 (필수, 최대 150자)
pkColNm: string; // pk 컬럼 명 (필수, 최대 150자)
ipcrCntVl?: number | null; // 중요도 개수 값 (mu)
ipcrCntVl?: number | null; // 중요도 계수 값 (mu)
pctlCrtrVl?: number | null; // 백분위 기준 값 (percentile)
scopCntCntVl?: number | null; // 범위 조정 개수 값 (A)
thrsCntVl?: number | null; // 임계 개수 값 (C)
scopCntCntVl?: number | null; // 범위 조정 계수 값 (A)
thrsCntVl?: number | null; // 임계 계수 값 (C)
useYn?: string; // 사용 여부 (Y/N, 최대 1자)
vrfcTrgtNm?: string; // 검증대상명 (최대 150자)
}
@@ -63,10 +63,10 @@ export interface VerificationTargetManagementUpdateRequest {
tblNm: string; // 테이블 명 (필수, 최대 150자)
colNm: string; // 컬럼 명 (필수, 최대 150자)
pkColNm: string; // pk 컬럼 명 (필수, 최대 150자)
ipcrCntVl?: number | null; // 중요도 개수 값 (mu)
ipcrCntVl?: number | null; // 중요도 계수 값 (mu)
pctlCrtrVl?: number | null; // 백분위 기준 값 (percentile)
scopCntCntVl?: number | null; // 범위 조정 개수 값 (A)
thrsCntVl?: number | null; // 임계 개수 값 (C)
scopCntCntVl?: number | null; // 범위 조정 계수 값 (A)
thrsCntVl?: number | null; // 임계 계수 값 (C)
useYn?: string; // 사용 여부 (Y/N, 최대 1자)
vrfcTrgtNm?: string; // 검증대상명 (최대 150자)
}
@@ -0,0 +1,29 @@
import { z } from "zod";
export const verificationTargetManagementSchema = z.object({
vrfcTrgtSn: z.number().optional(),
vrfcMthdCd: z.string().optional(),
vrfcMthdNm: z.string().optional().nullable(),
statsCd: z.string().min(1, "통계항목을 선택해주세요"),
statsNm: z.string().optional().nullable(),
schmNm: z.string().min(1, "스키마명을 입력해주세요"),
tblNm: z.string().min(1, "테이블명을 입력해주세요"),
colNm: z.string().min(1, "컬럼명을 입력해주세요"),
pkColNm: z.string().min(1, "PK 키를 입력해주세요"),
ipcrCntVl: z.number().optional().nullable(),
pctlCrtrVl: z.number().optional().nullable(),
scopCntCntVl: z.number().optional().nullable(),
thrsCntVl: z.number().optional().nullable(),
useYn: z.string().optional(),
vrfcTrgtNm: z.string().optional().nullable(),
rgtr: z.string().optional().nullable(),
regDt: z.string().optional().nullable(),
mdfr: z.string().optional().nullable(),
rgtrNm: z.string().optional().nullable(),
mdfrNm: z.string().optional().nullable(),
mdfcn: z.string().optional().nullable(),
});
export type VerificationTargetManagementFormData = z.infer<
typeof verificationTargetManagementSchema
>;