Files
frontend/apps/st/src/components/statistics/verification/VerificationTargetDialog.tsx
T

571 lines
28 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { CodeSelect } from "@/entities/code/ui/code-select";
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
import type {
NumericColumnDto,
VerificationTargetManagementResponse,
} from "@/entities/statistics/model/verification-target-management-interface";
import {
type VerificationTargetManagementFormData,
verificationTargetManagementSchema,
} from "@/entities/statistics/model/verification-target-management-schema";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import type { TabValue } from "./VerificationArticleManagement";
interface VerificationTargetDialogProps {
tabValue: TabValue;
open: boolean;
onOpenChange: (open: boolean) => void;
data: VerificationTargetManagementResponse | null;
onSave: (data: VerificationTargetManagementResponse) => void;
onDelete: (vrfcTrgtSn: number) => void;
}
export function VerificationTargetDialog({
tabValue,
open,
onOpenChange,
data,
onSave,
onDelete,
}: VerificationTargetDialogProps) {
const { control, handleSubmit, reset, watch, getValues, setValue } =
useForm<VerificationTargetManagementFormData>({
resolver: zodResolver(verificationTargetManagementSchema),
defaultValues: data || {
vrfcMthdCd: tabValue,
statsCd: "",
vrfcTrgtNm: "",
schmNm: "",
tblNm: "",
colNm: "",
pkColNm: "",
useYn: "Y",
},
});
const watchStatsCd = watch("statsCd");
const [isColumnDialogOpen, setIsColumnDialogOpen] = useState(false);
const [numericColumns, setNumericColumns] = useState<NumericColumnDto[]>(
[],
);
useEffect(() => {
if (open) {
reset(
data || {
vrfcMthdCd: tabValue,
statsCd: "",
vrfcTrgtNm: "",
schmNm: "",
tblNm: "",
colNm: "",
pkColNm: "",
useYn: "Y",
},
);
} else {
setNumericColumns([]);
}
}, [data, open, reset, tabValue]);
function onSubmit(formData: VerificationTargetManagementFormData) {
onSave({
...(data || {}),
...formData,
} as VerificationTargetManagementResponse);
}
function handleDelete() {
if (data?.vrfcTrgtSn) {
onDelete(data.vrfcTrgtSn);
}
}
async function handleSearchTableMetadata(e?: React.MouseEvent) {
e?.preventDefault();
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 handleOpenColumnDialog(e?: React.MouseEvent) {
e?.preventDefault();
if (numericColumns.length === 0) {
handleSearchTableMetadata();
} else {
setIsColumnDialogOpen(true);
}
}
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>
<form onSubmit={handleSubmit(onSubmit)}>
<Table variant="secondary">
<colgroup>
<col style={{ width: "20%" }} />
<col style={{ width: "30%" }} />
<col style={{ width: "20%" }} />
<col style={{ width: "30%" }} />
</colgroup>
<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"
/>
)}
/>
<Button
className="px-4"
onClick={
handleSearchTableMetadata
}
disabled={!watchStatsCd}
>
조회
</Button>
</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="스키마명"
/>
</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>
<div className="flex items-center gap-2">
<Controller
name="colNm"
control={control}
render={({ field }) => (
<Input
{...field}
placeholder="컬럼명"
className="flex-1"
/>
)}
/>
<Button
color="white"
className="whitespace-nowrap px-4"
onClick={handleOpenColumnDialog}
disabled={!watchStatsCd}
>
목록보기
</Button>
</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">
중요도 계수(μ)
</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.01"
value={
field.value ??
""
}
onChange={(e) =>
field.onChange(
e.target
.value
? Number(
e
.target
.value,
)
: null,
)
}
placeholder="(0 ~ 0.5, 0.01 단위)"
/>
</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="gray"
onClick={() => onOpenChange(false)}
>
취소
</Button>
{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
size="sm"
onClick={() =>
handleSelectColumn(
col.columnName,
)
}
>
입력하기
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</DialogContent>
</Dialog>
</>
);
}