st: feat: 일반검증 관리 기능
This commit is contained in:
@@ -19,10 +19,6 @@ import DetailComponent from './generalVerification/DetailComponent';
|
||||
import RegistDialog from './generalVerification/RegistDialog';
|
||||
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { generalVerificationMasterSubmitSchema, type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema';
|
||||
// import { useDataTable } from "@/shared/lib";
|
||||
import { getGeneralVerificationColumns } from '@/components/util/column/management/GeneralVerificationColumn';
|
||||
import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface';
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
@@ -43,7 +39,7 @@ const GeneralVerificationManagement = () => {
|
||||
const [vrfcMasterData, setVrfcMasterData] = useState<GeneralverificationList[]>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [selectedVrfcRow, setSelectedVrfcRow] = useState<GeneralverificationList | null>(null);
|
||||
const [selectedVrfcRow, setSelectedVrfcRow] = useState<number | null>(null);
|
||||
const [showRegistForm, setShowRegistForm] = useState<boolean>(false);
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
@@ -86,33 +82,15 @@ const GeneralVerificationManagement = () => {
|
||||
data: vrfcMasterData,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (row) => row.vrfcSn,
|
||||
getRowId: (row) => String(row.vrfcSn),
|
||||
state: { rowSelection },
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const form = useForm<GeneralVerificationMasterSubmitFormData>({
|
||||
resolver: zodResolver(generalVerificationMasterSubmitSchema),
|
||||
defaultValues: {},
|
||||
});
|
||||
|
||||
const handleVrfcMasterRowClick = async (row: GeneralverificationList) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.getGeneralverificationInfo(row.vrfcSn);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setRowSelection({ [row.vrfcSn]: true });
|
||||
setSelectedVrfcRow(row);
|
||||
setShowRegistForm(false);
|
||||
|
||||
form.reset(response.data);
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
form.reset({});
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
setRowSelection({ [row.vrfcSn]: true });
|
||||
setSelectedVrfcRow(row.vrfcSn);
|
||||
setShowRegistForm(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
@@ -122,11 +100,6 @@ const GeneralVerificationManagement = () => {
|
||||
setShowRegistForm(false);
|
||||
}
|
||||
|
||||
const handleClickRegist = () => {
|
||||
// setRowSelection({});
|
||||
// setSelectedVrfcRow(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleSearch();
|
||||
}, []);
|
||||
@@ -190,7 +163,7 @@ const GeneralVerificationManagement = () => {
|
||||
<div className="card !my-0 flex flex-col !p-0">
|
||||
<div className='flex items-center justify-end gap-[0.6rem] px-[1.2rem]'>
|
||||
<RegistDialog open={showRegistForm} onOpenChange={setShowRegistForm} onSuccess={handleSearch}>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="button" onClick={handleClickRegist}>
|
||||
<Button className="h-[3.2rem] w-[96px]" type="button">
|
||||
등록
|
||||
</Button>
|
||||
</RegistDialog>
|
||||
@@ -219,7 +192,7 @@ const GeneralVerificationManagement = () => {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<DetailComponent selectedVrfcRow={selectedVrfcRow} form={form} />
|
||||
<DetailComponent selectedVrfcRow={selectedVrfcRow} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Label,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
|
||||
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
interface CopyDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
row?: GeneralverificationList;
|
||||
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const CopyDialog = ({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
row,
|
||||
onSuccess,
|
||||
...props
|
||||
}: CopyDialogProps) => {
|
||||
|
||||
const [selectedCode, setSelectedCode] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const handleChangeCode = (value: string) => {
|
||||
if (row?.trgtStatsCd === value) {
|
||||
alert('원본과 동일한 통계항목으로 복사할 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedCode(value);
|
||||
};
|
||||
|
||||
const handleCopyItem = async () => {
|
||||
if (!selectedCode) {
|
||||
alert('복사 대상 통계항목을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm('복사하시겠습니까?')) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await generalverificationApiService.CopyVrfcData({
|
||||
vrfcSn: row?.vrfcSn ?? null,
|
||||
targetStatsCd: selectedCode ?? '',
|
||||
rgtrId: userInfo.userUnqId
|
||||
});
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('복사되었습니다.');
|
||||
onOpenChange(false);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
alert(response.message || '데이터 복사 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 복사 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setSelectedCode('');
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{row?.vrfcNm}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading &&
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="flex flex-col gap-4 m-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
원본 통계항목
|
||||
</Label>
|
||||
<Input
|
||||
value={row?.trgtStatsNm}
|
||||
disabled
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">
|
||||
대상 통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={selectedCode}
|
||||
onValueChange={handleChangeCode}
|
||||
placeholder="선택"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button size="lg" type="button" onClick={handleCopyItem}>
|
||||
복사
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyDialog;
|
||||
+322
-72
@@ -1,12 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { NoData } from '@/shared/ui/no-data';
|
||||
import {
|
||||
// Button,
|
||||
// Label,
|
||||
// Select,
|
||||
// SelectTrigger,
|
||||
// SelectContent,
|
||||
// SelectValue,
|
||||
// SelectItem,
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -14,79 +10,333 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
|
||||
import FormulaComponents from './FormulaComponents';
|
||||
import { LoadingWithAnimation } from '@common/components';
|
||||
import FomulaDialog from './FomulaDialog';
|
||||
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { useAlertStore } from '@/shared/lib';
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface';
|
||||
import { type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema';
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
import type { VerifyDetailData, VerifyDetailResponseDto } from '@/entities/statistics/model/general-verification-interface';
|
||||
import { getUserInfo } from '../../../../../../../packages/common/components/src/api';
|
||||
|
||||
interface DetailComponentProps {
|
||||
selectedVrfcRow: GeneralverificationList | null;
|
||||
form: UseFormReturn<GeneralVerificationMasterSubmitFormData>;
|
||||
selectedVrfcRow: number | null;
|
||||
}
|
||||
|
||||
const DetailComponent = ({ selectedVrfcRow, form }: DetailComponentProps) => {
|
||||
return (
|
||||
<div className='card card-border w-1/2 !mx-0 flex flex-col'>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">검증 상세 정보</h3>
|
||||
</div>
|
||||
<div className="card !m-4 h-full overflow-auto !p-0">
|
||||
{selectedVrfcRow ?
|
||||
<>
|
||||
<div className='flex flex-col'>
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<div>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>* 검증명</TableHead>
|
||||
<TableCell>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="vrfcNm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="!text-[1.3rem] w-full min-w-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
{form.formState.errors.vrfcNm && (
|
||||
<p className="text-red-500 text-base pt-2">
|
||||
{form.formState.errors.vrfcNm.message}
|
||||
</p>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableHead>* 통계항목</TableHead>
|
||||
<TableCell className='text-center'>{(selectedVrfcRow as GeneralverificationList)?.trgtStatsNm ?? "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
:
|
||||
<NoData />
|
||||
const DetailComponent = ({ selectedVrfcRow }: DetailComponentProps) => {
|
||||
|
||||
const [detailInfo, setDetailInfo] = useState<VerifyDetailData | null>(null);
|
||||
const [formulaList, setFormulaList] = useState<(VerifyDetailResponseDto | null)[]>([]);
|
||||
const [hasPersistedDetails, setHasPersistedDetails] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [showFomulaDialog, setShowFomulaDialog] = useState<boolean>(false);
|
||||
const [selectedFormula, setSelectedFormula] = useState<VerifyDetailResponseDto | null>(null);
|
||||
const [selectedFormulaIndex, setSelectedFormulaIndex] = useState<number>(0);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
const persistedFormulaSignatureRef = useRef<string>('');
|
||||
|
||||
const normalizeFormulaListForCompare = (list: (VerifyDetailResponseDto | null)[]) => {
|
||||
return list.map((row) =>
|
||||
row
|
||||
? {
|
||||
vrfcDtlSn: row.vrfcDtlSn ?? null,
|
||||
refStatsCd: row.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row.engArtopnEqtnCn ?? '',
|
||||
cmprOpert: row.cmprOpert ?? null,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
const trgtStatsCd = detailInfo?.commonInfo.trgtStatsCd;
|
||||
if (!trgtStatsCd) {
|
||||
alert('대상 통계항목(trgtStatsCd) 정보가 없습니다.');
|
||||
return;
|
||||
}
|
||||
const rows = formulaList.map((row, idx) => ({
|
||||
eqtnCombSeq: idx + 1,
|
||||
trgtStatsCd,
|
||||
refStatsCd: row?.refStatsCd ?? null,
|
||||
kornArtopnEqtnCn: row?.kornArtopnEqtnCn ?? '',
|
||||
engArtopnEqtnCn: row?.engArtopnEqtnCn ?? '',
|
||||
cmprOpert: row?.cmprOpert ?? null,
|
||||
}));
|
||||
|
||||
if (rows.length < 2) {
|
||||
alert('수식 항목은 최소 2개 이상 등록해야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidIdx = rows.findIndex(
|
||||
(r) => r.kornArtopnEqtnCn.trim().length === 0 || r.engArtopnEqtnCn.trim().length === 0,
|
||||
);
|
||||
if (invalidIdx !== -1) {
|
||||
alert(`수식이 비어있는 항목이 있습니다.\n(순번: ${invalidIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 마지막을 제외한 항목들은 연산자(cmprOpert)가 반드시 있어야 함
|
||||
if (rows.length > 1) {
|
||||
const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null);
|
||||
if (invalidOpertIdx !== -1) {
|
||||
alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 항목의 연산자는 무조건 null 처리
|
||||
if (rows.length > 0) {
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (lastRow) lastRow.cmprOpert = null;
|
||||
setFormulaList((prev) => {
|
||||
if (prev.length === 0) return prev;
|
||||
const last = prev[prev.length - 1];
|
||||
if (!last) return prev;
|
||||
if (last.cmprOpert == null) return prev;
|
||||
const next = [...prev];
|
||||
next[next.length - 1] = { ...last, cmprOpert: null };
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
if (await confirm('등록하시겠습니까?')) {
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
let response;
|
||||
|
||||
if (!hasPersistedDetails) {
|
||||
response = await generalverificationApiService.RegistVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, rgtrId: userInfo.userUnqId });
|
||||
} else {
|
||||
response = await generalverificationApiService.UpdateVrfcDetail({ vrfcSn: detailInfo?.commonInfo.vrfcSn ?? null, detailList: rows, mdfrId: userInfo.userUnqId });
|
||||
}
|
||||
|
||||
if (response?.status === 'success') {
|
||||
alert('등록되었습니다.');
|
||||
getVrfcDetail();
|
||||
} else {
|
||||
alert(response?.message || '데이터 등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getVrfcDetail = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await generalverificationApiService.getVrfcDetail(selectedVrfcRow!);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setDetailInfo(response.data);
|
||||
const list = response.data.verifyDetailResponseDtoList;
|
||||
setFormulaList(list?.length ? [...list] : [null]);
|
||||
setHasPersistedDetails((list?.length ?? 0) > 0);
|
||||
// "기존 데이터" 스냅샷(서버 응답 기준)을 시그니처로 저장
|
||||
persistedFormulaSignatureRef.current = JSON.stringify(
|
||||
normalizeFormulaListForCompare(list?.length ? [...list] : [null]),
|
||||
);
|
||||
} else {
|
||||
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickVrfcDetail = (formula?: VerifyDetailResponseDto | null, index?: number) => {
|
||||
setSelectedFormula(formula ?? null);
|
||||
setSelectedFormulaIndex(index ?? 0);
|
||||
setShowFomulaDialog(true);
|
||||
};
|
||||
|
||||
const handleFormulaConfirm = (payload: { kornArtopnEqtnCn: string; engArtopnEqtnCn: string; refStatsCd: string | null }) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
const i = selectedFormulaIndex;
|
||||
if (i < 0 || i >= next.length) return prev;
|
||||
const item = next[i];
|
||||
next[i] = item
|
||||
? {
|
||||
...item,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
}
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: payload.refStatsCd,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: payload.kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn: payload.engArtopnEqtnCn,
|
||||
cmprOpert: null,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
setShowFomulaDialog(false);
|
||||
setSelectedFormula(null);
|
||||
};
|
||||
|
||||
const handleResetClick = async () => {
|
||||
const currentSignature = JSON.stringify(normalizeFormulaListForCompare(formulaList));
|
||||
if (currentSignature === persistedFormulaSignatureRef.current) {
|
||||
alert('변경된 내용이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm(`변경 전 데이터로 리셋하시겠습니까?\n등록 완료된 데이터는 등록 전 데이터를 불러올 수 없습니다.`)) {
|
||||
getVrfcDetail();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVrfcRow != null) {
|
||||
getVrfcDetail();
|
||||
}
|
||||
}, [selectedVrfcRow]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='card card-border w-1/2 !mx-0 flex flex-col'>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">검증 상세 정보</h3>
|
||||
</div>
|
||||
<div className="card !m-4 h-full !p-0">
|
||||
{selectedVrfcRow ?
|
||||
isLoading ?
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className='flex flex-col gap-12 m-2 h-full'>
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
<col width="25%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>검증명</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcNm ?? "-"}</TableCell>
|
||||
<TableHead>통계항목</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.trgtStatsNm ?? "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>검증분류</TableHead>
|
||||
<TableCell className='text-center'>{detailInfo?.commonInfo.vrfcClsfNm ?? "-"}</TableCell>
|
||||
<TableHead>소수점처리방식</TableHead>
|
||||
<TableCell className='text-center'>
|
||||
{detailInfo?.commonInfo.dcptDigtCnt === 0 ?
|
||||
'소수점 생략'
|
||||
:
|
||||
`${detailInfo?.commonInfo.dcptDigtCnt}자리에서 ${detailInfo?.commonInfo.dcptPrcsMthNm}`
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className='flex flex-col gap-4 overflow-auto'>
|
||||
<h3 className="font-bold text-title">수식 정보</h3>
|
||||
{formulaList.map((data, index) => (
|
||||
<FormulaComponents
|
||||
key={data?.vrfcDtlSn ?? `empty-${index}`}
|
||||
data={data}
|
||||
index={index}
|
||||
onOperatorChange={(v) => {
|
||||
setFormulaList((prev) => {
|
||||
const next = [...prev];
|
||||
if (index < 0 || index >= next.length) return prev;
|
||||
const current = next[index];
|
||||
const cmprOpert = v === 'none' ? null : v;
|
||||
next[index] = current
|
||||
? { ...current, cmprOpert }
|
||||
: {
|
||||
vrfcDtlSn: null,
|
||||
refStatsCd: null,
|
||||
refStatsNm: null,
|
||||
kornArtopnEqtnCn: '',
|
||||
engArtopnEqtnCn: '',
|
||||
cmprOpert,
|
||||
};
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onDelete={() => {
|
||||
setFormulaList((prev) => {
|
||||
const next = prev.filter((_, i) => i !== index);
|
||||
return next.length > 0 ? next : [null];
|
||||
});
|
||||
}}
|
||||
handleFomulaDialog={(show, formula, idx) => {
|
||||
if (show) handleClickVrfcDetail(formula, idx ?? index);
|
||||
else setShowFomulaDialog(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[4rem] rounded-full"
|
||||
onClick={() => {
|
||||
setFormulaList((prev) => [...prev, null]);
|
||||
}}
|
||||
>
|
||||
<i className="ri-play-list-add-fill text-[16px]"></i>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 flex gap-2 justify-end items-end">
|
||||
<Button
|
||||
type="button"
|
||||
color='red'
|
||||
title='리셋'
|
||||
onClick={handleResetClick}
|
||||
>
|
||||
<i className="diveicon di-arrow-reset" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSaveClick}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<NoData />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FomulaDialog
|
||||
key={showFomulaDialog ? (selectedFormula?.vrfcDtlSn ?? 'new') : 'closed'}
|
||||
open={showFomulaDialog}
|
||||
onOpenChange={(open) => {
|
||||
setShowFomulaDialog(open);
|
||||
if (!open) setSelectedFormula(null);
|
||||
}}
|
||||
data={selectedFormula}
|
||||
trgtStatsCd={detailInfo?.commonInfo.trgtStatsCd}
|
||||
trgtStatsNm={detailInfo?.commonInfo.trgtStatsNm}
|
||||
onConfirm={handleFormulaConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,593 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogClose,
|
||||
Textarea,
|
||||
Skeleton,
|
||||
} from "@common/components";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
|
||||
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
|
||||
import type { VerifyDetailResponseDto, VerifyColumnListResponseData, SelectedVerifyColumnListResponseData } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export interface FomulaDialogConfirmPayload {
|
||||
kornArtopnEqtnCn: string;
|
||||
engArtopnEqtnCn: string;
|
||||
/** 참조 통계코드 (선택 안 했으면 null) */
|
||||
refStatsCd: string | null;
|
||||
}
|
||||
|
||||
interface FomulaDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
data?: VerifyDetailResponseDto | null;
|
||||
trgtStatsCd?: string;
|
||||
trgtStatsNm?: string;
|
||||
/** 확인 클릭 시 검사 통과 후 호출. 해당 행에 반영 후 다이얼로그 닫기는 부모에서 처리 */
|
||||
onConfirm?: (payload: FomulaDialogConfirmPayload) => void;
|
||||
}
|
||||
|
||||
const FomulaDialog = ({ open, onOpenChange, data, trgtStatsCd, trgtStatsNm, onConfirm }: FomulaDialogProps) => {
|
||||
const [columnInfo, setColumnInfo] = useState<VerifyColumnListResponseData | null>(null);
|
||||
const [selectedColumnInfo, setSelectedColumnInfo] = useState<SelectedVerifyColumnListResponseData | null>(null);
|
||||
const [showCommonColumns, setShowCommonColumns] = useState<boolean>(true);
|
||||
const [showTargetColumns, setShowTargetColumns] = useState<boolean>(true);
|
||||
const [showRefColumns, setShowRefColumns] = useState<boolean>(true);
|
||||
/** 참조 통계 - 다이얼로그 내부에서만 사용. 열 때마다 해당 행(data)의 저장값으로 초기화, 저장 시에만 부모에 반영 예정 */
|
||||
const [refStatsCdLocal, setRefStatsCdLocal] = useState<string>('');
|
||||
/** 수식 = 토큰 배열. 한글명 중복 시 출처(공통/대상/참조)별 engColNm 보존 */
|
||||
const [formulaDisplaySegments, setFormulaDisplaySegments] = useState<string[]>([]);
|
||||
const [formulaValueSegments, setFormulaValueSegments] = useState<string[]>([]);
|
||||
/** 확인 클릭 시 검사 실패 메시지 (Textarea 아래 빨간 글씨) */
|
||||
const [formulaSaveError, setFormulaSaveError] = useState<string | null>(null);
|
||||
/** 참조 통계 변경 시 안내 메시지 */
|
||||
const [refStatsChangeNotice, setRefStatsChangeNotice] = useState<string | null>(null);
|
||||
|
||||
const formulaTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
|
||||
/** 다이얼로그에서 쓰는 기호 → 제출용 value. 백엔드 규격에 맞게 수정 가능 */
|
||||
const OPERATOR_SYMBOL_TO_VALUE: Record<string, string> = {
|
||||
'+': '+',
|
||||
'-': '-',
|
||||
'×': '*',
|
||||
'%': '/',
|
||||
'(': '(',
|
||||
')': ')',
|
||||
};
|
||||
|
||||
/** 공통 → 대상 → 참조 순, 첫 등장 kornColNm만 사용 (로드/직접입력 시 파싱용) */
|
||||
const getKornToEngMapForParse = (): Map<string, string> => {
|
||||
const map = new Map<string, string>();
|
||||
const refCols = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? [];
|
||||
[...(columnInfo?.commonColumnInfo ?? []), ...(columnInfo?.targetColumnInfo?.colList ?? []), ...refCols].forEach(
|
||||
(item) => {
|
||||
if (!map.has(item.kornColNm)) map.set(item.kornColNm, item.engColNm);
|
||||
}
|
||||
);
|
||||
return map;
|
||||
};
|
||||
|
||||
/** 한글 수식 문자열을 토큰(표시/값) 배열로 파싱. 컬럼명은 longest match */
|
||||
const parseDisplayToSegments = (displayStr: string): { display: string[]; value: string[] } => {
|
||||
if (displayStr.length === 0) return { display: [], value: [] };
|
||||
const kornToEng = getKornToEngMapForParse();
|
||||
const kornList = [...kornToEng.keys()].filter(Boolean).sort((a, b) => b.length - a.length);
|
||||
const display: string[] = [];
|
||||
const value: string[] = [];
|
||||
let i = 0;
|
||||
while (i < displayStr.length) {
|
||||
let matched = false;
|
||||
for (const korn of kornList) {
|
||||
if (displayStr.slice(i, i + korn.length) === korn) {
|
||||
display.push(korn);
|
||||
value.push(kornToEng.get(korn) ?? korn);
|
||||
i += korn.length;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
const ch = displayStr[i];
|
||||
if (ch != null) {
|
||||
display.push(ch);
|
||||
value.push(ch);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return { display, value };
|
||||
};
|
||||
|
||||
const handleFormulaKey = (val: string) => {
|
||||
setFormulaSaveError(null);
|
||||
if (val === 'backspace') {
|
||||
setFormulaDisplaySegments((prev) => prev.slice(0, -1));
|
||||
setFormulaValueSegments((prev) => prev.slice(0, -1));
|
||||
} else if (val === 'ac') {
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
} else {
|
||||
setFormulaDisplaySegments((prev) => [...prev, val]);
|
||||
setFormulaValueSegments((prev) => [...prev, val]);
|
||||
}
|
||||
};
|
||||
|
||||
/** 컬럼 버튼: 한글 표시 + 해당 출처의 영문명 저장 (한글 중복 시에도 구분) */
|
||||
const handleFormulaColumn = (kornColNm: string, engColNm: string) => {
|
||||
setFormulaSaveError(null);
|
||||
setFormulaDisplaySegments((prev) => [...prev, kornColNm]);
|
||||
setFormulaValueSegments((prev) => [...prev, engColNm]);
|
||||
};
|
||||
|
||||
/** 제출/저장 시 사용할 영문 수식 (컬럼 engColNm + 연산자 기호 → value 치환) */
|
||||
const getFormulaValueForSubmit = (): string =>
|
||||
formulaValueSegments.map((seg) => OPERATOR_SYMBOL_TO_VALUE[seg] ?? seg).join('');
|
||||
|
||||
const OPERATORS = ['+', '-', '×', '/', '%'] as const;
|
||||
const isOperator = (s: string) => s.length === 1 && (OPERATORS as readonly string[]).includes(s);
|
||||
const isParen = (s: string) => s === '(' || s === ')';
|
||||
const isValueToken = (s: string) =>
|
||||
s.length > 1 || (s.length === 1 && !isOperator(s) && !isParen(s));
|
||||
const isDigit = (s: string) => s.length === 1 && /[0-9]/.test(s);
|
||||
|
||||
/** 저장 시 수식 규칙 검사. 오류 시 메시지 반환, 통과 시 null */
|
||||
const validateFormulaForSave = (segments: string[]): string | null => {
|
||||
if (segments.length === 0) return null;
|
||||
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
const a = segments[i];
|
||||
const b = segments[i + 1];
|
||||
if (a != null && b != null && isValueToken(a) && isValueToken(b) && !(isDigit(a) && isDigit(b))) {
|
||||
return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.';
|
||||
}
|
||||
}
|
||||
|
||||
// "(값)"도 하나의 값으로 취급: 값/닫는괄호 뒤에는 '(' 또는 값이 올 수 없고, 사이에 연산자가 있어야 함.
|
||||
// 예: 컬럼(100), 값)(100) 같은 형태는 연산자 누락으로 간주
|
||||
for (let i = 0; i < segments.length - 1; i++) {
|
||||
const a = segments[i];
|
||||
const b = segments[i + 1];
|
||||
if (a == null || b == null) continue;
|
||||
// 연속 숫자(예: 50)는 예외 허용
|
||||
if (isDigit(a) && isDigit(b)) continue;
|
||||
|
||||
const aIsValueBoundary = isValueToken(a) || a === ')';
|
||||
const bIsValueBoundary = b === '(' || isValueToken(b);
|
||||
if (aIsValueBoundary && bIsValueBoundary) {
|
||||
return '컬럼(또는 숫자) 사이에 더하기, 빼기, 나누기, 곱하기 기호가 있어야 합니다.';
|
||||
}
|
||||
}
|
||||
|
||||
const last = segments[segments.length - 1];
|
||||
if (last && isOperator(last)) {
|
||||
return '수식은 기호로 끝날 수 없습니다. (닫는 괄호 제외)';
|
||||
}
|
||||
|
||||
const stack: boolean[] = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const s = segments[i];
|
||||
if (s == null) continue;
|
||||
if (s === '(') {
|
||||
stack.push(false);
|
||||
} else if (s === ')') {
|
||||
if (stack.length === 0) return '닫는 괄호가 여는 괄호 없이 사용되었습니다.';
|
||||
const hadValue = stack.pop();
|
||||
if (!hadValue) return '괄호 안에 값이 있어야 합니다.';
|
||||
if (stack.length > 0) stack[stack.length - 1] = true;
|
||||
} else if (stack.length > 0 && isValueToken(s)) {
|
||||
stack[stack.length - 1] = true;
|
||||
}
|
||||
}
|
||||
if (stack.length > 0) return '여는 괄호에 대응하는 닫는 괄호가 없습니다.';
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getVerifyColumnList = async () => {
|
||||
try {
|
||||
if (!trgtStatsCd) return;
|
||||
let response;
|
||||
|
||||
if (data?.vrfcDtlSn == null || data.vrfcDtlSn <= 0) {
|
||||
response = await generalverificationApiService.getSelectedVerifyColumnList(trgtStatsCd)
|
||||
} else {
|
||||
response = await generalverificationApiService.getVerifyColumnList({
|
||||
trgtStatsCd,
|
||||
vrfcDtlSn: data.vrfcDtlSn,
|
||||
refStatsCd: data.refStatsCd,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (response?.status === 'success') {
|
||||
setColumnInfo(response.data);
|
||||
} else {
|
||||
alert(response?.message || '데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedVerifyColumnList = async (statsCd: string) => {
|
||||
try {
|
||||
const response = await generalverificationApiService.getSelectedVerifyColumnList(statsCd);
|
||||
if (response?.status === 'success' && response.data) {
|
||||
setSelectedColumnInfo({
|
||||
targetColumnInfo: response.data.targetColumnInfo ?? { statsCd: statsCd, colList: [] },
|
||||
commonColumnInfo: response.data.commonColumnInfo ?? [],
|
||||
});
|
||||
} else {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setColumnInfo(null);
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
if (open) void getVerifyColumnList();
|
||||
}, [open, trgtStatsCd, data?.vrfcDtlSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setRefStatsCdLocal(data?.refStatsCd ?? '');
|
||||
}, [open, data?.refStatsCd, data?.vrfcDtlSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setSelectedColumnInfo(null);
|
||||
setRefStatsCdLocal('');
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
setFormulaSaveError(null);
|
||||
setRefStatsChangeNotice(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const str = data?.kornArtopnEqtnCn ?? '';
|
||||
const { display, value } = parseDisplayToSegments(str);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}
|
||||
}, [open, data?.kornArtopnEqtnCn, data?.vrfcDtlSn]);
|
||||
|
||||
/**
|
||||
* 재오픈 직후에는 columnInfo가 아직 없어서 parseDisplayToSegments가 컬럼명을 매칭 못하고
|
||||
* 한글을 1글자씩 토큰화하는 경우가 있음.
|
||||
* 컬럼 목록 로딩 후(= map이 채워진 후) "표시 문자열이 원본과 동일 + 값 토큰이 표시 토큰과 동일"한 케이스만 재파싱해서
|
||||
* engColNm 치환이 반영되도록 보정한다.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const src = data?.kornArtopnEqtnCn ?? '';
|
||||
if (!src) return;
|
||||
|
||||
const displayJoined = formulaDisplaySegments.join('');
|
||||
const valueJoined = formulaValueSegments.join('');
|
||||
// 아직 컬럼 치환이 안 된 상태(= value가 display와 동일)에서만 재파싱
|
||||
if (displayJoined !== src) return;
|
||||
if (valueJoined !== displayJoined) return;
|
||||
|
||||
// 컬럼 목록이 준비된 이후에만 의미가 있음
|
||||
const hasColumns =
|
||||
(columnInfo?.commonColumnInfo?.length ?? 0) > 0 ||
|
||||
(columnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0 ||
|
||||
(columnInfo?.refColumnInfo?.colList?.length ?? 0) > 0 ||
|
||||
(selectedColumnInfo?.targetColumnInfo?.colList?.length ?? 0) > 0;
|
||||
if (!hasColumns) return;
|
||||
|
||||
const { display, value } = parseDisplayToSegments(src);
|
||||
const nextDisplayJoined = display.join('');
|
||||
const nextValueJoined = value.join('');
|
||||
// 파싱 결과가 현재와 완전히 같으면 setState 반복으로 무한 렌더가 날 수 있으니 중단
|
||||
if (nextDisplayJoined === displayJoined && nextValueJoined === valueJoined) return;
|
||||
// 컬럼 치환이 전혀 안 됐다면(= value가 display와 동일) 보정의 의미가 없으니 중단
|
||||
if (nextValueJoined === nextDisplayJoined) return;
|
||||
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}, [
|
||||
open,
|
||||
data?.kornArtopnEqtnCn,
|
||||
columnInfo?.commonColumnInfo,
|
||||
columnInfo?.targetColumnInfo?.colList,
|
||||
columnInfo?.refColumnInfo?.colList,
|
||||
selectedColumnInfo?.targetColumnInfo?.colList,
|
||||
formulaDisplaySegments,
|
||||
formulaValueSegments,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || columnInfo?.kornArtopnEqtnCn == null) return;
|
||||
if (data?.kornArtopnEqtnCn !== columnInfo.kornArtopnEqtnCn) return;
|
||||
const { display, value } = parseDisplayToSegments(columnInfo.kornArtopnEqtnCn);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}, [open, data?.kornArtopnEqtnCn, columnInfo?.kornArtopnEqtnCn]);
|
||||
|
||||
const handleChangeRefStatsCd = (next: string) => {
|
||||
if (!open) return;
|
||||
|
||||
// 현재 refStatsCdLocal에 해당하는 참조 컬럼이 수식에 포함돼 있으면 변경을 막고 안내만 표시
|
||||
const currentRefCols = refColList ?? [];
|
||||
const currentRefEngSet = new Set(currentRefCols.map((c) => c.engColNm).filter(Boolean));
|
||||
const hasCurrentRefToken =
|
||||
currentRefEngSet.size > 0 && formulaValueSegments.some((seg) => currentRefEngSet.has(seg));
|
||||
|
||||
if (hasCurrentRefToken) {
|
||||
setRefStatsChangeNotice('수식에 기존 참조 컬럼이 포함되어 있어 참조 통계를 변경할 수 없습니다. 먼저 해당 참조 컬럼을 제거해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setRefStatsChangeNotice(null);
|
||||
setRefStatsCdLocal(next);
|
||||
};
|
||||
|
||||
/** 참조 통계 선택 시 해당 통계의 컬럼 목록으로 교체 */
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (refStatsCdLocal) {
|
||||
void getSelectedVerifyColumnList(refStatsCdLocal);
|
||||
} else {
|
||||
setSelectedColumnInfo(null);
|
||||
}
|
||||
}, [open, refStatsCdLocal]);
|
||||
|
||||
/** 참조 컬럼: 선택한 참조 통계로 조회한 목록이 있으면 사용, 없으면 getVerifyColumnList 결과 사용 */
|
||||
const refColList = selectedColumnInfo?.targetColumnInfo?.colList ?? columnInfo?.refColumnInfo?.colList ?? [];
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) {
|
||||
setFormulaDisplaySegments([]);
|
||||
setFormulaValueSegments([]);
|
||||
setFormulaSaveError(null);
|
||||
setSelectedColumnInfo(null);
|
||||
setRefStatsCdLocal('');
|
||||
}
|
||||
onOpenChange?.(nextOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent aria-describedby={undefined} className="max-w-[100rem] w-full max-h-[90%] overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
수식 상세 정보
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-start gap-20 p-4 border rounded-xl">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-bold text-title text-[14px]">기호 및 숫자</h3>
|
||||
<div className="grid grid-cols-3 gap-2 w-max">
|
||||
<Button type="button" value="+" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('+')}>
|
||||
<span className="text-[18px]">+</span>
|
||||
</Button>
|
||||
<Button type="button" value="-" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('-')}>
|
||||
<span className="text-[18px]">-</span>
|
||||
</Button>
|
||||
<Button type="button" value="*" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('×')}>
|
||||
<span className="text-[18px]">×</span>
|
||||
</Button>
|
||||
<Button type="button" value="/" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('%')}>
|
||||
<span className="text-[18px]">%</span>
|
||||
</Button>
|
||||
<Button type="button" value="(" color="gray" className="w-[65px]" onClick={() => handleFormulaKey('(')}>
|
||||
<span className="text-[18px]">(</span>
|
||||
</Button>
|
||||
<Button type="button" value=")" color="gray" className="w-[65px]" onClick={() => handleFormulaKey(')')}>
|
||||
<span className="text-[18px]">)</span>
|
||||
</Button>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 0, '⌫', 'AC'].map((n) => {
|
||||
const keyVal = n !== 'AC' && n !== '⌫' ? String(n) : n === '⌫' ? 'backspace' : 'ac';
|
||||
return (
|
||||
<Button
|
||||
key={n}
|
||||
type="button"
|
||||
value={keyVal}
|
||||
color={n !== 'AC' && n !== '⌫' ? 'white' : 'primary'}
|
||||
className="w-[65px]"
|
||||
onClick={() => handleFormulaKey(keyVal)}
|
||||
>
|
||||
<span className="text-[18px]">{n}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<h3 className="font-bold text-title text-[14px]">수식 미리보기</h3>
|
||||
<Textarea
|
||||
className="w-full flex-1 bg-gray-100 resize-none !text-3xl p-4 !leading-loose text-gray-600 font-bold"
|
||||
id="formulaDisplayInput"
|
||||
ref={formulaTextareaRef}
|
||||
value={formulaDisplaySegments.join('')}
|
||||
onChange={(e) => {
|
||||
setFormulaSaveError(null);
|
||||
const { display, value } = parseDisplayToSegments(e.target.value);
|
||||
setFormulaDisplaySegments(display);
|
||||
setFormulaValueSegments(value);
|
||||
}}
|
||||
rows={4}
|
||||
readOnly
|
||||
/>
|
||||
{formulaSaveError && (
|
||||
<p className="text-sm text-red-600 mt-1" role="alert">
|
||||
{formulaSaveError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-title text-[14px]">공통 컬럼</h3>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowCommonColumns((prev) => !prev)}
|
||||
>
|
||||
{showCommonColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
</div>
|
||||
{showCommonColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{columnInfo?.commonColumnInfo?.length && columnInfo.commonColumnInfo.length > 0 ?
|
||||
columnInfo.commonColumnInfo.map((c) => (
|
||||
<Button
|
||||
key={c.engColNm}
|
||||
type="button"
|
||||
value={c.engColNm}
|
||||
size="sm"
|
||||
color="lightGray"
|
||||
onClick={() => handleFormulaColumn(c.kornColNm, c.engColNm)}
|
||||
>
|
||||
{c.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
<Skeleton className="h-[12rem] w-full rounded-xl" />
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-title text-[14px]">{trgtStatsNm} 컬럼</h3>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowTargetColumns((prev) => !prev)}
|
||||
>
|
||||
{showTargetColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
</div>
|
||||
{showTargetColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{columnInfo?.targetColumnInfo?.colList.length && columnInfo.targetColumnInfo.colList.length > 0 ?
|
||||
columnInfo.targetColumnInfo.colList.map((t) => (
|
||||
<Button
|
||||
key={t.engColNm}
|
||||
type="button"
|
||||
value={t.engColNm}
|
||||
size="sm"
|
||||
onClick={() => handleFormulaColumn(t.kornColNm, t.engColNm)}
|
||||
>
|
||||
{t.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
<Skeleton className="h-[12rem] w-full rounded-xl" />
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 p-4 border rounded-xl">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
<h3 className="font-bold text-title text-[14px]">참조 컬럼</h3>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={refStatsCdLocal}
|
||||
onValueChange={handleChangeRefStatsCd}
|
||||
placeholder="선택"
|
||||
className="w-[16rem]"
|
||||
/>
|
||||
</div>
|
||||
{(refColList?.length ?? 0) > 0 ?
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="white"
|
||||
onClick={() => setShowRefColumns((prev) => !prev)}
|
||||
>
|
||||
{showRefColumns ? "접기" : "펼치기"}
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
{refStatsChangeNotice && (
|
||||
<p className="text-[12px] text-red-600" role="alert">
|
||||
{refStatsChangeNotice}
|
||||
</p>
|
||||
)}
|
||||
{showRefColumns ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(refColList?.length ?? 0) > 0 ?
|
||||
refColList.map((r) => (
|
||||
<Button
|
||||
key={r.engColNm}
|
||||
type="button"
|
||||
value={r.engColNm}
|
||||
size="sm"
|
||||
color="sky"
|
||||
onClick={() => handleFormulaColumn(r.kornColNm, r.engColNm)}
|
||||
>
|
||||
{r.kornColNm}
|
||||
</Button>
|
||||
))
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
size="lg"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const err = validateFormulaForSave(formulaValueSegments);
|
||||
if (err) {
|
||||
setFormulaSaveError(err);
|
||||
requestAnimationFrame(() => {
|
||||
formulaTextareaRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
formulaTextareaRef.current?.focus?.();
|
||||
});
|
||||
return;
|
||||
}
|
||||
setFormulaSaveError(null);
|
||||
const kornArtopnEqtnCn = formulaDisplaySegments.join('');
|
||||
const engArtopnEqtnCn = getFormulaValueForSubmit();
|
||||
const isEmptyFormula = kornArtopnEqtnCn.trim().length === 0;
|
||||
onConfirm?.({
|
||||
kornArtopnEqtnCn,
|
||||
engArtopnEqtnCn,
|
||||
refStatsCd: isEmptyFormula ? null : (refStatsCdLocal ? refStatsCdLocal : null),
|
||||
});
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default FomulaDialog;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Button, Input } from "@common/components";;
|
||||
import OperatorsComponents from "./OperatorsComponents";
|
||||
import type { VerifyDetailResponseDto } from "@/entities/statistics/model/general-verification-interface";
|
||||
|
||||
interface FormulaComponentsProps {
|
||||
data?: VerifyDetailResponseDto | null;
|
||||
index?: number;
|
||||
onDelete?: () => void;
|
||||
onOperatorChange?: (value: string) => void;
|
||||
handleFomulaDialog?: (show: boolean, data?: VerifyDetailResponseDto | null, index?: number) => void;
|
||||
}
|
||||
|
||||
const FormulaComponents = ({ data, index, onDelete, onOperatorChange, handleFomulaDialog }: FormulaComponentsProps) => {
|
||||
const cmprOpert = data?.cmprOpert;
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-4 mb-2 p-2 justify-around rounded has-[.formulaDeleteBtn:hover]:bg-gray-100'>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
className="size-[3rem] shrink-0 rounded-full formulaDeleteBtn"
|
||||
color='red'
|
||||
onClick={onDelete}
|
||||
>
|
||||
<i className="ri-subtract-fill text-[16px]"></i>
|
||||
</Button>
|
||||
<Input
|
||||
className='h-[4rem] !cursor-pointer'
|
||||
placeholder='클릭하여 수식을 등록해주세요.'
|
||||
id='formulaInput'
|
||||
value={data ? data.kornArtopnEqtnCn : ''}
|
||||
title={data ? data.kornArtopnEqtnCn : ''}
|
||||
onClick={() => handleFomulaDialog?.(true, data, index)}
|
||||
readOnly
|
||||
/>
|
||||
<OperatorsComponents cmprOpert={cmprOpert} onValueChange={onOperatorChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormulaComponents;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectValue,
|
||||
SelectItem,
|
||||
} from "@common/components";
|
||||
|
||||
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
|
||||
|
||||
type OperatorItem = {
|
||||
opertSn: string;
|
||||
opertSymbl: string;
|
||||
opertKornNm: string;
|
||||
};
|
||||
|
||||
interface OperatorsComponentsProps {
|
||||
cmprOpert?: string | null;
|
||||
onValueChange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const OperatorsComponents = ({
|
||||
cmprOpert,
|
||||
onValueChange,
|
||||
placeholder = "연산자 없음",
|
||||
}: OperatorsComponentsProps) => {
|
||||
const [operators, setOperators] = useState<OperatorItem[]>([]);
|
||||
const [localValue, setLocalValue] = useState<string>(cmprOpert ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(cmprOpert ?? "");
|
||||
}, [cmprOpert]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOperators = async () => {
|
||||
const response = await generalverificationApiService.getVrfcOperators();
|
||||
if (response.status === 'success' && response.data) {
|
||||
setOperators(response.data);
|
||||
}
|
||||
};
|
||||
fetchOperators();
|
||||
}, []);
|
||||
|
||||
// onValueChange가 있으면 부모가 제어(controlled), 없으면 내부 state로 표시
|
||||
const isControlled = onValueChange !== undefined;
|
||||
const value = (isControlled ? (cmprOpert ?? "") : localValue) || "";
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(v) => {
|
||||
setLocalValue(v);
|
||||
onValueChange?.(v);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="!w-[150px] min-w-[150px] max-w-[150px] h-[4rem] overflow-hidden focus:outline-none focus:ring-0">
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-[150px]">
|
||||
<SelectItem value='none'>
|
||||
연산자 없음
|
||||
</SelectItem>
|
||||
{operators.map((op) => (
|
||||
<SelectItem key={op.opertSn} value={op.opertSymbl}>
|
||||
{op.opertKornNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperatorsComponents;
|
||||
@@ -46,7 +46,7 @@ interface RegistDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
children?: React.ReactNode;
|
||||
vrfcSn?: string;
|
||||
vrfcSn?: number;
|
||||
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ const RegistDialog = memo(
|
||||
...props
|
||||
}: RegistDialogProps) => {
|
||||
|
||||
const isVrfcSn = vrfcSn ? true : false;
|
||||
const isVrfcSn = vrfcSn != null;
|
||||
const [checkVrfcNm, setCheckVrfcNm] = useState<boolean>(false);
|
||||
const { confirm } = useConfirm();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
@@ -69,13 +69,13 @@ const RegistDialog = memo(
|
||||
const { data: detailData } = useQuery({
|
||||
queryKey: [`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, vrfcSn],
|
||||
queryFn: () => generalverificationApiService.getGeneralverificationInfo(vrfcSn!),
|
||||
enabled: open && !!vrfcSn,
|
||||
enabled: open && vrfcSn != null,
|
||||
});
|
||||
|
||||
const form = useForm<GeneralVerificationMasterSubmitFormData>({
|
||||
resolver: zodResolver(generalVerificationMasterSubmitSchema),
|
||||
values: {
|
||||
vrfcSn: detailData?.data.vrfcSn ?? '',
|
||||
vrfcSn: detailData?.data.vrfcSn ?? undefined,
|
||||
vrfcNm: detailData?.data.vrfcNm ?? '',
|
||||
vrfcClsfCd: detailData?.data.vrfcClsfCd ?? '',
|
||||
trgtStatsCd: detailData?.data.trgtStatsCd ?? '',
|
||||
@@ -177,8 +177,8 @@ const RegistDialog = memo(
|
||||
|
||||
if (await confirm(deleteMsg)) {
|
||||
try {
|
||||
const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn || '');
|
||||
|
||||
const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn!);
|
||||
|
||||
if (response.status === 'success') {
|
||||
alert('삭제되었습니다.');
|
||||
onOpenChange(false);
|
||||
@@ -588,13 +588,13 @@ const RegistDialog = memo(
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DialogFooter>
|
||||
{vrfcSn &&
|
||||
{vrfcSn != null &&
|
||||
<Button size="lg" type="button" color="red" onClick={handleDeleteVrfcMaster}>
|
||||
삭제
|
||||
</Button>
|
||||
}
|
||||
<Button size="lg" type="submit">
|
||||
{vrfcSn ? '수정' : '등록'}
|
||||
{vrfcSn != null ? '수정' : '등록'}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { GeneralverificationList } from "@/entities/statistics/model/genera
|
||||
|
||||
import { Button } from "@common/components";
|
||||
import RegistDialog from "@/components/statistics/verification/generalVerification/RegistDialog";
|
||||
import CopyDialog from "@/components/statistics/verification/generalVerification/CopyDialog";
|
||||
|
||||
export const getGeneralVerificationColumns = (
|
||||
onUpdateSuccess?: () => void,
|
||||
@@ -18,10 +19,6 @@ export const getGeneralVerificationColumns = (
|
||||
return totalRows - (pageIndex * pageSize + row.index);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// accessorKey: "vrfcSn",
|
||||
// header: "일련번호",
|
||||
// },
|
||||
{
|
||||
accessorKey: "trgtStatsNm",
|
||||
header: "통계항목",
|
||||
@@ -46,6 +43,18 @@ export const getGeneralVerificationColumns = (
|
||||
return date.format("YYYY-MM-DD");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "copy",
|
||||
header: "복사",
|
||||
cell: ({ row }) => (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<GeneralVerificationCopyButton
|
||||
row={row.original}
|
||||
onSuccess={onUpdateSuccess}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "관리",
|
||||
@@ -64,7 +73,7 @@ const GeneralVerificationManageButton = ({
|
||||
vrfcSn,
|
||||
onSuccess,
|
||||
}: {
|
||||
vrfcSn: string;
|
||||
vrfcSn: number;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
@@ -79,3 +88,23 @@ const GeneralVerificationManageButton = ({
|
||||
</RegistDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralVerificationCopyButton = ({
|
||||
row,
|
||||
onSuccess,
|
||||
}: {
|
||||
row: GeneralverificationList;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
return (
|
||||
<CopyDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
row={row}
|
||||
onSuccess={onSuccess}
|
||||
>
|
||||
<Button size="sm" color="white">복사</Button>
|
||||
</CopyDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,13 @@ import type {
|
||||
RegistVrfcMasterRequest,
|
||||
RegistVrfcMasterResponse,
|
||||
DeleteVrfcMasterResponse,
|
||||
VrfcOperatorsResponse,
|
||||
GeneralverificationDetailResponse,
|
||||
VerifyColumnListRequest,
|
||||
VerifyColumnListResponse,
|
||||
VerifyDetailRegistRequest,
|
||||
VerifyDetailUpdateRequest,
|
||||
CopyVerifyDataRequest,
|
||||
} from "../model/general-verification-interface";
|
||||
|
||||
export class GeneralverificationApiService implements GeneralverificationService {
|
||||
@@ -24,7 +31,7 @@ export class GeneralverificationApiService implements GeneralverificationService
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getGeneralverificationInfo(param: string): Promise<GeneralverificationInfoResponse> {
|
||||
public async getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse> {
|
||||
const response = await stClient.get<GeneralverificationInfoResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`,
|
||||
);
|
||||
@@ -55,12 +62,66 @@ export class GeneralverificationApiService implements GeneralverificationService
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async DeleteVrfcMaster(param: string): Promise<DeleteVrfcMasterResponse> {
|
||||
public async DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse> {
|
||||
const response = await stClient.delete<DeleteVrfcMasterResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVrfcOperators(): Promise<VrfcOperatorsResponse> {
|
||||
const response = await stClient.get<VrfcOperatorsResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/operators`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse> {
|
||||
const response = await stClient.get<GeneralverificationDetailResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse> (
|
||||
param.refStatsCd ?
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}/?refStatsCd=${param.refStatsCd}`
|
||||
:
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param.trgtStatsCd}/${param.vrfcDtlSn}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse> {
|
||||
const response = await stClient.get<VerifyColumnListResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/columns/${param}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
param
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.put<RegistVrfcMasterResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/details`,
|
||||
param
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse> {
|
||||
const response = await stClient.post<RegistVrfcMasterResponse> (
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param.vrfcSn}/copy?targetStatsCd=${param.targetStatsCd}&rgtrId=${param.rgtrId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const generalverificationApiService = new GeneralverificationApiService();
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface GeneralverificationList {
|
||||
vrfcSn: string;
|
||||
vrfcSn: number;
|
||||
vrfcNm: string;
|
||||
vrfcClsfCd: string;
|
||||
vrfcClsfNm: string;
|
||||
@@ -46,7 +46,7 @@ export interface CheckVerificationNameResponse {
|
||||
}
|
||||
|
||||
export interface UpdateVrfcMasterRequest {
|
||||
vrfcSn?: string | null;
|
||||
vrfcSn?: number | null;
|
||||
vrfcNm?: string | null;
|
||||
vrfcClsfCd?: string | null;
|
||||
dcptDigtCnt?: number | null;
|
||||
@@ -79,11 +79,134 @@ export interface DeleteVrfcMasterResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface VrfcOperatorsResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data?: {
|
||||
opertSn: string;
|
||||
opertSymbl: string;
|
||||
opertKornNm: string;
|
||||
}[]
|
||||
}
|
||||
|
||||
/** 검증 상세 - 공통 정보 */
|
||||
export interface GeneralverificationDetailCommonInfo {
|
||||
vrfcSn: number;
|
||||
vrfcNm: string;
|
||||
vrfcClsfCd: string;
|
||||
vrfcClsfNm: string;
|
||||
trgtStatsCd: string;
|
||||
trgtStatsNm: string;
|
||||
dcptDigtCnt: number;
|
||||
dcptPrcsMthCd: string | null;
|
||||
dcptPrcsMthNm: string | null;
|
||||
useYn: string;
|
||||
}
|
||||
|
||||
/** 검증 상세 - 수식/조건 항목 */
|
||||
export interface VerifyDetailResponseDto {
|
||||
/** DB에서 내려온 경우에만 존재(신규 작성 중이면 null) */
|
||||
vrfcDtlSn: number | null;
|
||||
refStatsCd: string | null;
|
||||
refStatsNm: string | null;
|
||||
kornArtopnEqtnCn: string;
|
||||
engArtopnEqtnCn: string;
|
||||
cmprOpert: string | null;
|
||||
}
|
||||
|
||||
export interface VerifyDetailData {
|
||||
commonInfo: GeneralverificationDetailCommonInfo;
|
||||
verifyDetailResponseDtoList: VerifyDetailResponseDto[];
|
||||
}
|
||||
|
||||
export interface GeneralverificationDetailResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: VerifyDetailData;
|
||||
}
|
||||
|
||||
export interface VerifyColumnListRequest {
|
||||
trgtStatsCd: string;
|
||||
vrfcDtlSn: number;
|
||||
refStatsCd?: string | null;
|
||||
}
|
||||
|
||||
export interface VerifyColumnItem {
|
||||
kornColNm: string;
|
||||
engColNm: string;
|
||||
}
|
||||
|
||||
/** 통계코드 + 컬럼 목록 */
|
||||
export interface VerifyColumnInfo {
|
||||
statsCd: string;
|
||||
colList: VerifyColumnItem[];
|
||||
}
|
||||
|
||||
export interface VerifyColumnListResponseData {
|
||||
vrfcDtlSn?: number;
|
||||
kornArtopnEqtnCn?: string;
|
||||
engArtopnEqtnCn?: string;
|
||||
targetColumnInfo?: VerifyColumnInfo;
|
||||
commonColumnInfo?: VerifyColumnItem[];
|
||||
refColumnInfo?: VerifyColumnInfo;
|
||||
}
|
||||
|
||||
export interface VerifyColumnListResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: VerifyColumnListResponseData;
|
||||
}
|
||||
|
||||
export interface SelectedVerifyColumnListResponseData {
|
||||
targetColumnInfo: VerifyColumnInfo;
|
||||
commonColumnInfo: VerifyColumnItem[];
|
||||
}
|
||||
|
||||
export interface SelectedVerifyColumnListResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: VerifyColumnListResponseData
|
||||
}
|
||||
|
||||
export interface VerifyDetailRegistData {
|
||||
trgtStatsCd: string;
|
||||
refStatsCd: string | null;
|
||||
kornArtopnEqtnCn: string,
|
||||
engArtopnEqtnCn: string,
|
||||
cmprOpert: string | null;
|
||||
eqtnCombSeq: number
|
||||
}
|
||||
|
||||
export interface VerifyDetailRegistRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[]
|
||||
rgtrId: string;
|
||||
}
|
||||
|
||||
export interface VerifyDetailUpdateRequest {
|
||||
vrfcSn: number | null;
|
||||
detailList: VerifyDetailRegistData[]
|
||||
mdfrId: string;
|
||||
}
|
||||
|
||||
export interface CopyVerifyDataRequest {
|
||||
vrfcSn?: number | null;
|
||||
targetStatsCd : string;
|
||||
rgtrId: string;
|
||||
}
|
||||
|
||||
export interface GeneralverificationService {
|
||||
getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse>;
|
||||
getGeneralverificationInfo(param: string): Promise<GeneralverificationInfoResponse>;
|
||||
getGeneralverificationInfo(param: number): Promise<GeneralverificationInfoResponse>;
|
||||
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
|
||||
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
|
||||
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
|
||||
DeleteVrfcMaster(param: string): Promise<DeleteVrfcMasterResponse>;
|
||||
DeleteVrfcMaster(param: number): Promise<DeleteVrfcMasterResponse>;
|
||||
getVrfcOperators(): Promise<VrfcOperatorsResponse>;
|
||||
getVrfcDetail(param: number): Promise<GeneralverificationDetailResponse>;
|
||||
getVerifyColumnList(param: VerifyColumnListRequest): Promise<VerifyColumnListResponse>;
|
||||
getSelectedVerifyColumnList(param: string): Promise<VerifyColumnListResponse>;
|
||||
RegistVrfcDetail(param: VerifyDetailRegistRequest): Promise<RegistVrfcMasterResponse>;
|
||||
UpdateVrfcDetail(param: VerifyDetailUpdateRequest): Promise<RegistVrfcMasterResponse>;
|
||||
CopyVrfcData(param: CopyVerifyDataRequest): Promise<RegistVrfcMasterResponse>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
export const generalVerificationMasterSubmitSchema = z
|
||||
.object({
|
||||
vrfcSn: z.string().nullable().optional(),
|
||||
vrfcSn: z.number().nullable().optional(),
|
||||
vrfcNm: z.string().min(1, "검증명을 입력하세요"),
|
||||
vrfcClsfCd: z.string().min(1, "검증분류를 선택하세요"),
|
||||
trgtStatsCd: z.string().min(1, "통계항목을 선택하세요"),
|
||||
|
||||
Reference in New Issue
Block a user