[통계 프론트엔드] 검증 항목등록 수정에 컬럼명 선택 팝업 추가

This commit is contained in:
2026-03-13 16:58:19 +09:00
parent 5446a8a656
commit 8269b31b42
3 changed files with 441 additions and 266 deletions
@@ -1,12 +1,17 @@
"use client";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Search } from "lucide-react";
import { CodeSelect } from "@/entities/code/ui/code-select";
import type { VerificationTargetManagementResponse } from "@/entities/statistics/model/verification-target-management-interface";
import type {
NumericColumnDto,
VerificationTargetManagementResponse,
} from "@/entities/statistics/model/verification-target-management-interface";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import {
type VerificationTargetManagementFormData,
verificationTargetManagementSchema,
@@ -23,6 +28,7 @@ import {
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
@@ -45,7 +51,7 @@ export function VerificationTargetDialog({
onSave,
onDelete,
}: VerificationTargetDialogProps) {
const { control, handleSubmit, reset } =
const { control, handleSubmit, reset, getValues, setValue } =
useForm<VerificationTargetManagementFormData>({
resolver: zodResolver(verificationTargetManagementSchema),
defaultValues: data || {
@@ -60,6 +66,11 @@ export function VerificationTargetDialog({
},
});
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
const [numericColumns, setNumericColumns] = useState<NumericColumnDto[]>(
[],
);
useEffect(() => {
if (open) {
reset(
@@ -90,321 +101,459 @@ export function VerificationTargetDialog({
}
}
async function handleSearchTableMetadata() {
const statsCd = getValues("statsCd");
if (!statsCd) {
alert("통계항목을 선택해주세요.");
return;
}
try {
const metadata =
await verificationTargetManagementApi.getTableInfo(statsCd);
setValue("schmNm", metadata.schmNm, { shouldValidate: true });
setValue("tblNm", metadata.tblNm, { shouldValidate: true });
if (metadata.pkColumns && metadata.pkColumns.length > 0) {
setValue("pkColNm", metadata.pkColumns.join(", "), {
shouldValidate: true,
});
}
setNumericColumns(metadata.numericColumns || []);
setIsColumnDialogOpen(true);
} catch (error) {
alert("테이블 정보를 조회하는 중 오류가 발생했습니다.");
console.error(error);
}
}
function handleSelectColumn(colNm: string) {
setValue("colNm", colNm, { shouldValidate: true });
setIsColumnDialogOpen(false);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>
{data ? "검증 항목 상세" : "검증 항목 등록"}
</DialogTitle>
</DialogHeader>
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>
{data ? "검증 항목 상세" : "검증 항목 등록"}
</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="선택"
/>
</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="검증명"
<form onSubmit={handleSubmit(onSubmit)}>
<Table variant="secondary">
<TableBody>
<TableRow>
<TableHead
className="w-[150px] bg-muted font-medium"
data-required
>
통계항목
</TableHead>
<TableCell>
<div className="flex items-center gap-2">
<Controller
name="statsCd"
control={control}
render={({ field }) => (
<CodeSelect
upCd="STITEM"
value={field.value}
onValueChange={
field.onChange
}
placeholder="선택"
className="flex-1"
/>
)}
/>
)}
/>
</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="스키마명"
/>
</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="테이블명"
/>
</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="컬럼명"
/>
</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 키"
/>
</div>
)}
/>
</TableCell>
</TableRow>
{tabValue !== "IMR" && (
<TableRow>
<TableHead className="bg-muted font-medium">
중요도 계수(μ)
<Button
type="button"
color="gray"
variant="outline"
onClick={
handleSearchTableMetadata
}
className="px-3"
>
<Search className="mr-1 h-4 w-4" />
조회
</Button>
</div>
</TableCell>
<TableHead
className="w-[150px] bg-muted font-medium"
data-required
>
검증명
</TableHead>
<TableCell>
<Controller
name="ipcrCntVl"
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
type="number"
min="0"
max="1"
step="0.1"
value={
field.value ?? ""
}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 1, 0.1 단위)"
{...field}
placeholder="스키마명"
/>
</div>
)}
/>
</TableCell>
<TableHead className="bg-muted font-medium">
백분위수 기준값(pct)
<TableHead
className="bg-muted font-medium"
data-required
>
테이블명
</TableHead>
<TableCell>
<Controller
name="pctlCrtrVl"
name="tblNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
min="0"
max="0.5"
step="0.1"
value={
field.value ?? ""
}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 0.5, 0.1 단위)"
{...field}
placeholder="테이블명"
/>
</div>
)}
/>
</TableCell>
</TableRow>
)}
{tabValue !== "IMR" && (
<TableRow>
<TableHead className="bg-muted font-medium">
스케일 조정 계수(A)
<TableHead
className="bg-muted font-medium"
data-required
>
컬럼명
</TableHead>
<TableCell>
<Controller
name="scopCntCntVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<div className="flex items-center gap-2">
<Controller
name="colNm"
control={control}
render={({ field }) => (
<Input
type="number"
min="0"
max="1"
step="0.01"
value={
field.value ?? ""
}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 1, 0.01 단위)"
{...field}
placeholder="컬럼명"
className="flex-1"
/>
</div>
)}
/>
)}
/>
<Button
type="button"
variant="outline"
className="whitespace-nowrap px-3"
onClick={() =>
setIsColumnDialogOpen(true)
}
>
목록보기
</Button>
</div>
</TableCell>
<TableHead className="bg-muted font-medium">
임계 계수(c)
<TableHead
className="bg-muted font-medium"
data-required
>
PK 키
</TableHead>
<TableCell>
<Controller
name="thrsCntVl"
name="pkColNm"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
min="0"
max="100"
step="1"
value={
field.value ?? ""
}
onChange={(e) =>
field.onChange(
e.target.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 100, 1 단위)"
{...field}
placeholder="PK 키"
/>
</div>
)}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<DialogFooter className="mt-6">
<Button
type="button"
color="gray"
onClick={() => onOpenChange(false)}
>
취소
</Button>
{data && (
{tabValue !== "IMR" && (
<TableRow>
<TableHead className="bg-muted font-medium">
중요도 계수(μ)
</TableHead>
<TableCell>
<Controller
name="ipcrCntVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
min="0"
max="1"
step="0.1"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 1, 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"
min="0"
max="0.5"
step="0.1"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 0.5, 0.1 단위)"
/>
</div>
)}
/>
</TableCell>
</TableRow>
)}
{tabValue !== "IMR" && (
<TableRow>
<TableHead className="bg-muted font-medium">
스케일 조정 계수(A)
</TableHead>
<TableCell>
<Controller
name="scopCntCntVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
min="0"
max="1"
step="0.01"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 1, 0.01 단위)"
/>
</div>
)}
/>
</TableCell>
<TableHead className="bg-muted font-medium">
임계 계수(c)
</TableHead>
<TableCell>
<Controller
name="thrsCntVl"
control={control}
render={({ field }) => (
<div className="space-y-1">
<Input
type="number"
min="0"
max="100"
step="1"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 100, 1 단위)"
/>
</div>
)}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<DialogFooter className="mt-6">
<Button
type="button"
color="red"
onClick={handleDelete}
color="gray"
onClick={() => onOpenChange(false)}
>
삭제
취소
</Button>
)}
<Button type="submit" color="green">
저장
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{data && (
<Button
type="button"
color="red"
onClick={handleDelete}
>
삭제
</Button>
)}
<Button type="submit" color="green">
저장
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog
open={isColumnDialogOpen}
onOpenChange={setIsColumnDialogOpen}
>
<DialogContent className="max-w-[600px]">
<DialogHeader>
<DialogTitle>숫자형 컬럼 목록</DialogTitle>
</DialogHeader>
<div className="max-h-[400px] overflow-y-auto">
<Table variant="default">
<TableHeader>
<TableRow>
<TableHead className="bg-muted text-center font-bold">
컬럼명
</TableHead>
<TableHead className="bg-muted text-center font-bold">
데이터 타입
</TableHead>
<TableHead className="bg-muted text-center font-bold">
코멘트
</TableHead>
<TableHead className="bg-muted text-center font-bold">
선택
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{numericColumns.length === 0 ? (
<TableRow>
<TableCell
colSpan={4}
className="py-8 text-center text-gray-500"
>
조회된 숫자형 컬럼이 없습니다.
</TableCell>
</TableRow>
) : (
numericColumns.map((col, idx) => (
<TableRow key={idx}>
<TableCell className="text-center">
{col.columnName}
</TableCell>
<TableCell className="text-center">
{col.dataType}
</TableCell>
<TableCell className="text-center">
{col.columnComment}
</TableCell>
<TableCell className="text-center">
<Button
type="button"
size="sm"
variant="outline"
onClick={() =>
handleSelectColumn(
col.columnName,
)
}
>
입력
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</DialogContent>
</Dialog>
</>
);
}
@@ -12,6 +12,7 @@ import type {
VerificationTargetManagementUpdateRequest,
VerificationTargetManagementUpdateApiResponse,
VerificationTargetManagementDeleteApiResponse,
TableMetadataResponse,
} from "../model/verification-target-management-interface";
export class VerificationTargetManagementApiService implements VerificationTargetManagementService {
@@ -44,6 +45,18 @@ export class VerificationTargetManagementApiService implements VerificationTarge
return response.data;
}
/**
* 테이블 메타데이터 조회
* @param statsCd 통계항목 코드
*/
public async getTableInfo(statsCd: string): Promise<TableMetadataResponse> {
const response = await client.get<TableMetadataResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG}/table-info`,
{ params: { statsCd } },
);
return response.data;
}
/**
* 검증 대상 코드 목록 조회
*/
@@ -1,4 +1,17 @@
import type { PageableList } from "@/shared/model";
export interface NumericColumnDto {
columnName: string;
dataType: string;
columnComment: string;
}
export interface TableMetadataResponse {
schmNm: string;
tblNm: string;
pkColumns: string[];
numericColumns: NumericColumnDto[];
}
import type { ApiResponse } from "./statistics-interface";
// 검증 대상 코드 응답 DTO