From b644281348bfc4f5bd9f9e9d4b1c1cdb02e6b1c1 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 13 Feb 2026 14:57:20 +0900 Subject: [PATCH] =?UTF-8?q?[st]=20feat:=20=EA=B2=80=EC=A6=9D=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EC=83=81=EC=84=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/DataGrid.tsx | 4 +- .../VerificationArticleManagement.tsx | 81 +++- .../verification/VerificationTargetDialog.tsx | 380 ++++++++++++++++++ ...erification-target-management-interface.ts | 18 +- .../verification-target-management-schema.ts | 29 ++ 5 files changed, 497 insertions(+), 15 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx create mode 100644 apps/st/src/entities/statistics/model/verification-target-management-schema.ts diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 1ee114f97..6327b0fba 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -1087,10 +1087,10 @@ const DataGrid = ({ ) { try { // 행 전체 데이터 가져오기 - const rowData = grid.getValues(clickData.itemIndex); + const row = rowData[clickData.itemIndex]; // 부모 컴포넌트로 데이터 전달 - onRowClick(rowData as Record); + onRowClick(row ?? {}); } catch (error) { console.error("행 데이터 가져오기 오류:", error); } diff --git a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx index 824afd0f1..321d2dca2 100644 --- a/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx +++ b/apps/st/src/components/statistics/verification/VerificationArticleManagement.tsx @@ -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("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(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) { - 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 ( @@ -181,6 +246,14 @@ function VerificationArticleManagement() { /> ))} + + ); } diff --git a/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx new file mode 100644 index 000000000..29e1607d3 --- /dev/null +++ b/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx @@ -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({ + 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 ( + + + + 검증 항목 상세 + + +
+ + + + + 통계항목 + + + ( +
+ + {errors.statsCd && ( +

+ {errors.statsCd.message} +

+ )} +
+ )} + /> +
+ + 검증명 + + + ( + + )} + /> + +
+ + + + 스키마명 + + + ( +
+ + {errors.schmNm && ( +

+ {errors.schmNm.message} +

+ )} +
+ )} + /> +
+ + 테이블명 + + + ( +
+ + {errors.tblNm && ( +

+ {errors.tblNm.message} +

+ )} +
+ )} + /> +
+
+ + + + 컬럼명 + + + ( +
+ + {errors.colNm && ( +

+ {errors.colNm.message} +

+ )} +
+ )} + /> +
+ + PK 키 + + + ( +
+ + {errors.pkColNm && ( +

+ {errors.pkColNm.message} +

+ )} +
+ )} + /> +
+
+ + + + 중요도 계수(μ) + + + ( +
+ + field.onChange( + e.target.value + ? Number( + e.target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 1)" + /> +
+ )} + /> +
+ + 백분위수 기준값(pct) + + + ( +
+ + field.onChange( + e.target.value + ? Number( + e.target + .value, + ) + : null, + ) + } + placeholder="(0 ~ 0.5)" + /> +
+ )} + /> +
+
+ + + + 스케일 조정 계수(A) + + + ( + + field.onChange( + e.target.value + ? Number( + e.target + .value, + ) + : null, + ) + } + /> + )} + /> + + + 임계 계수(c) + + + ( + + field.onChange( + e.target.value + ? Number( + e.target + .value, + ) + : null, + ) + } + /> + )} + /> + + +
+
+ + + + + + +
+
+
+ ); +} diff --git a/apps/st/src/entities/statistics/model/verification-target-management-interface.ts b/apps/st/src/entities/statistics/model/verification-target-management-interface.ts index 6f2d94246..08d771256 100644 --- a/apps/st/src/entities/statistics/model/verification-target-management-interface.ts +++ b/apps/st/src/entities/statistics/model/verification-target-management-interface.ts @@ -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자) } diff --git a/apps/st/src/entities/statistics/model/verification-target-management-schema.ts b/apps/st/src/entities/statistics/model/verification-target-management-schema.ts new file mode 100644 index 000000000..821e860f8 --- /dev/null +++ b/apps/st/src/entities/statistics/model/verification-target-management-schema.ts @@ -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 +>;