st: feat: 일반검증 관리 기능 작업 중

This commit is contained in:
2026-03-17 10:07:08 +09:00
parent f575e3b677
commit 685ba15baa
11 changed files with 1144 additions and 117 deletions
+2 -2
View File
@@ -229,12 +229,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
<div className="flex items-center p-2">
{mergedProps.fctName &&
<div className="flex items-center gap-2">
<Label htmlFor="liSnSearchInput" className="mr-2 text-label">
<Label htmlFor="fctNmSearchInput" className="mr-2 text-label">
시설명
</Label>
<Input
className="h-[3.2rem] w-[14rem] rounded-sm !text-[1.3rem] mr-4"
id="liSnSearchInput"
id="fctNmSearchInput"
value={values.fctName}
onChange={(e) => setValues({ ...values, fctName: e.target.value })}
autoComplete="off"
@@ -1,125 +1,184 @@
"use client";
import { useMemo, useState } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { CodeSelect } from '@/entities/code';
import { CodeSelectWithAll } from '@/entities/code';
import {
Button,
Label,
// Table,
// TableBody,
// TableCell,
// TableHead,
// TableRow,
Select,
SelectTrigger,
SelectContent,
SelectValue,
SelectItem,
DataTable,
} from "@common/components";
import { LoadingWithAnimation } from '@common/components';
import { NoData } from '@/shared/ui/no-data';
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 { GeneralVerificationColumn } from '@/components/util/column/management/GeneralVerificationColumn';
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';
import { useAlertStore } from '@/shared/lib';
const GeneralVerificationManagement = () => {
const [statsCd, setStatsCd] = useState('');
// const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
// const [pagination, setPagination] = useState<PaginationState>({
// pageIndex: 0,
// pageSize: 100,
// });
const [searchValues, setSearchValues] = useState<{
statsCd: string;
useYn: string;
vrfcClsfCd: string;
}>({
statsCd: '',
useYn: '',
vrfcClsfCd: '',
});
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 [showRegistForm, setShowRegistForm] = useState<boolean>(false);
// const { tableComponent } = useDataTable({
// tableOptions: {
// GeneralVerificationColumn,
// state: {
// pagination,
// rowSelection,
// },
// manualPagination: true,
// enableMultiRowSelection: true,
// getCoreRowModel: getCoreRowModel(),
// onPaginationChange: setPagination,
// onRowSelectionChange: setRowSelection,
// },
// // dataTableOptions: {
// // onRowClick: handleRowClick,
// // viewPagination: true,
// // },
// });
const { setMessage: alert } = useAlertStore((state) => state);
const mockData = useMemo<GeneralverificationList[]>(
() => [
{
vrfSn: 1,
tabNm: '하수도보급률',
vrfNm: '인구수 검증',
vrfClsCd: '시',
statsCd: 'ST000001',
useYn: 'Y',
rgtrId: '관리자',
regDt: '20260102',
},
{
vrfSn: 2,
tabNm: '하수도보급률',
vrfNm: '인구수 검증',
vrfClsCd: '시',
statsCd: 'ST000001',
useYn: 'Y',
rgtrId: '관리자',
regDt: '20260102',
},
{
vrfSn: 3,
tabNm: '하수도보급률',
vrfNm: '인구수 검증',
vrfClsCd: '시',
statsCd: 'ST000001',
useYn: 'Y',
rgtrId: '관리자',
regDt: '20260102',
},
{
vrfSn: 4,
tabNm: '하수도보급률',
vrfNm: '인구수 검증',
vrfClsCd: '시',
statsCd: 'ST000001',
useYn: 'Y',
rgtrId: '관리자',
regDt: '20260102',
},
],
[]
const handleSearch = async () => {
try {
setIsLoading(true);
const params = Object.fromEntries(
Object.entries({
trgtStatsCd: searchValues.statsCd,
vrfcClsfCd: searchValues.vrfcClsfCd,
useYn: searchValues.useYn,
}).filter(([, value]) => value !== '' && value !== 'ALL')
);
const response = await generalverificationApiService.getGeneralverificationList(params);
if (response.status === 'success') {
setVrfcMasterData(response.data);
setRowSelection({});
setSelectedVrfcRow(null);
setShowRegistForm(false);
} else {
alert(response.message || '데이터 조회 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
};
const columns = useMemo(
() => getGeneralVerificationColumns(handleSearch),
[searchValues.statsCd, searchValues.vrfcClsfCd, searchValues.useYn],
);
const table = useReactTable({
data: mockData,
columns: GeneralVerificationColumn,
data: vrfcMasterData,
columns,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => row.vrfcSn,
state: { rowSelection },
onRowSelectionChange: setRowSelection,
});
const handleSearch = () => {
console.log(statsCd);
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('데이터 조회 중 오류가 발생했습니다.');
}
};
const handleReset = () => {
setRowSelection({});
setSelectedVrfcRow(null);
handleSearch();
setShowRegistForm(false);
}
const handleClickRegist = () => {
// setRowSelection({});
// setSelectedVrfcRow(null);
};
useEffect(() => {
handleSearch();
}, []);
return (
<div className='h-full'>
<div className="h-full">
<div className="card card-border">
<div className="flex items-center gap-8 p-2">
<div className="flex items-center gap-2">
<Label className="text-label">
통계항목
</Label>
<CodeSelect
<CodeSelectWithAll
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
value={searchValues.statsCd}
onValueChange={(value) => setSearchValues({ ...searchValues, statsCd: value })}
placeholder="전체"
className="w-[16rem]"
/>
</div>
<div className="flex items-center gap-2">
<Label className="text-label">
검증분류
</Label>
<Select value={searchValues.vrfcClsfCd} onValueChange={(value) => setSearchValues({ ...searchValues, vrfcClsfCd: value })}>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="전체"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="DO_SI">도단위 검증</SelectItem>
<SelectItem value="SI_GU">시단위 검증</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Label className="text-label">
사용여부
</Label>
<Select value={searchValues.useYn} onValueChange={(value) => setSearchValues({ ...searchValues, useYn: value })}>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="전체"></SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
<SelectItem value="Y">Y</SelectItem>
<SelectItem value="N">N</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button color="green" className="h-[3.2rem] w-[96px]" type="button">
<Button color="green" className="h-[3.2rem] w-[96px]" type="button" onClick={handleReset}>
초기화
</Button>
<Button className="h-[3.2rem] w-[96px]" type="button" onClick={handleSearch}>
@@ -128,17 +187,39 @@ const GeneralVerificationManagement = () => {
</div>
</div>
</div>
<div className='card !mb-0 flex gap-5 !p-0 h-[calc(100vh-40rem)]'>
<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">
<DataTable table={table} />
</div>
<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>
</RegistDialog>
</div>
<div className='w-1/2'>
<div className="flex justify-center gap-5 !p-0 h-[65vh]">
<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">
{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>
)}
{isLoading ? null
:
vrfcMasterData.length > 0 ?
<DataTable table={table} onRowClick={(value) => handleVrfcMasterRowClick(value)} />
:
<div className="flex h-full items-center justify-center">
<NoData />
</div>
}
</div>
</div>
<DetailComponent selectedVrfcRow={selectedVrfcRow} form={form} />
</div>
</div>
</div>
@@ -0,0 +1,93 @@
import { NoData } from '@/shared/ui/no-data';
import {
// Button,
// Label,
// Select,
// SelectTrigger,
// SelectContent,
// SelectValue,
// SelectItem,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { useForm, Controller, type UseFormReturn } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import type { GeneralverificationList } from '@/entities/statistics/model/general-verification-interface';
import { type GeneralVerificationMasterSubmitFormData } from '@/entities/statistics/model/general-verification-master-submit-schema';
interface DetailComponentProps {
selectedVrfcRow: GeneralverificationList | null;
form: UseFormReturn<GeneralVerificationMasterSubmitFormData>;
}
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 />
}
</div>
</div>
);
};
export default DetailComponent;
@@ -0,0 +1,613 @@
"use client";
import { useEffect, memo, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
RadioGroup,
RadioGroupItem,
Label,
Select,
SelectTrigger,
SelectContent,
SelectValue,
SelectItem,
Skeleton,
} from "@common/components";
import { Form, FormField, FormItem, FormControl } from "@/shared/ui/form";
import { ENDPOINT } from "@/shared/config";
import { CodeSelect } from "@/entities/code";
import { type GeneralVerificationMasterSubmitFormData, generalVerificationMasterSubmitSchema } from '@/entities/statistics/model/general-verification-master-submit-schema';
import type { UpdateVrfcMasterRequest, RegistVrfcMasterRequest } from "@/entities/statistics/model/general-verification-interface";
import { generalverificationApiService } from '@/entities/statistics/api/general-verification-service';
import { UserApi } from "@/entities/user/api/user-service";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
interface RegistDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
children?: React.ReactNode;
vrfcSn?: string;
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
onSuccess?: () => void;
}
const RegistDialog = memo(
({
children,
open,
onOpenChange,
vrfcSn,
onSuccess,
...props
}: RegistDialogProps) => {
const isVrfcSn = vrfcSn ? true : false;
const [checkVrfcNm, setCheckVrfcNm] = useState<boolean>(false);
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const { data: detailData } = useQuery({
queryKey: [`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`, vrfcSn],
queryFn: () => generalverificationApiService.getGeneralverificationInfo(vrfcSn!),
enabled: open && !!vrfcSn,
});
const form = useForm<GeneralVerificationMasterSubmitFormData>({
resolver: zodResolver(generalVerificationMasterSubmitSchema),
values: {
vrfcSn: detailData?.data.vrfcSn ?? '',
vrfcNm: detailData?.data.vrfcNm ?? '',
vrfcClsfCd: detailData?.data.vrfcClsfCd ?? '',
trgtStatsCd: detailData?.data.trgtStatsCd ?? '',
trgtStatsNm: detailData?.data.trgtStatsNm ?? '',
dcptDigtCnt: detailData?.data.dcptDigtCnt ?? 0,
dcptPrcsMthCd: detailData?.data.dcptPrcsMthCd ?? '',
useYn: detailData?.data.useYn ?? 'Y',
rgtrId: detailData?.data.rgtrId ?? '',
regDt: detailData?.data.regDt ?? '',
mdfrId: detailData?.data.mdfrId ?? '',
mdfcnDt: detailData?.data.mdfcnDt ?? '',
},
});
const dcptDigtCnt = form.watch('dcptDigtCnt');
const rgtrId = form.watch('rgtrId');
const mdfrId = form.watch('mdfrId');
const trgtStatsCd = form.watch('trgtStatsCd');
const vrfcNm = form.watch('vrfcNm');
const vrfcClsfCd = form.watch('vrfcClsfCd');
const { data: registrarUser } = useQuery({
queryKey: ['user', 'rgtr', rgtrId],
queryFn: () => UserApi.findByUserUnqId(rgtrId!),
enabled: open && !!rgtrId,
});
const { data: modifierUser } = useQuery({
queryKey: ['user', 'mdfr', mdfrId],
queryFn: () => UserApi.findByUserUnqId(mdfrId!),
enabled: open && !!mdfrId,
});
const originalVrfcNmRef = useRef<string | null>(null);
useEffect(() => {
if (detailData?.data.vrfcNm && originalVrfcNmRef.current == null) {
originalVrfcNmRef.current = detailData.data.vrfcNm;
}
}, [detailData]);
/** 중복 여부 반환: true = 중복, false = 사용 가능 */
const runVerificationNameCheck = async (trgtStatsCdVal: string, vrfcNmVal: string, vrfcClsfCd: string): Promise<boolean> => {
try {
const response = await generalverificationApiService.checkVerificationName({
trgtStatsCd: trgtStatsCdVal,
vrfcNm: vrfcNmVal,
vrfcClsfCd: vrfcClsfCd,
});
if (response.status === 'success') {
const isDuplicate = response.data;
setCheckVrfcNm(isDuplicate);
return isDuplicate;
}
alert('중복검사 중 오류가 발생했습니다.');
setCheckVrfcNm(true);
return true;
} catch {
alert('중복검사 중 오류가 발생했습니다.');
setCheckVrfcNm(true);
return true;
}
};
const handleUpdateVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => {
try {
const response = await generalverificationApiService.UpdateVrfcMaster(data as UpdateVrfcMasterRequest);
if (response.status === 'success') {
alert('수정되었습니다.');
onOpenChange(false);
onSuccess?.();
} else {
alert(response.message ?? '수정 중 오류가 발생했습니다.');
}
} catch {
alert('수정 중 오류가 발생했습니다.');
}
};
const handleRegistVrfcMaster = async (data: GeneralVerificationMasterSubmitFormData) => {
try {
const response = await generalverificationApiService.RegistVrfcMaster(data as RegistVrfcMasterRequest);
if (response.status === 'success') {
alert('등록되었습니다.');
onOpenChange(false);
onSuccess?.();
} else {
alert(response.message ?? '등록 중 오류가 발생했습니다.');
}
} catch {
alert('등록 중 오류가 발생했습니다.');
}
};
const handleDeleteVrfcMaster = async () => {
const deleteMsg = `${detailData?.data.vrfcNm} 검증룰을
삭제하시겠습니까?`;
if (await confirm(deleteMsg)) {
try {
const response = await generalverificationApiService.DeleteVrfcMaster(vrfcSn || '');
if (response.status === 'success') {
alert('삭제되었습니다.');
onOpenChange(false);
onSuccess?.();
} else {
alert(response.message ?? '삭제 중 오류가 발생했습니다.');
}
} catch {
alert('삭제 중 오류가 발생했습니다.');
}
}
};
useEffect(() => {
if (!open) return;
if (!vrfcNm) return;
const original = originalVrfcNmRef.current;
// 수정 모드: 기준값이 아직 안 세팅됐으면 검사 안 함(다이얼로그 막 열었을 때)
if (isVrfcSn && original === null) return;
// 수정 모드: 사용자가 검증명을 바꾸지 않았으면 검사 안 함
if (original !== null && original === vrfcNm) {
setCheckVrfcNm(false);
return;
}
// const timer = setTimeout(() => {
// runVerificationNameCheck(trgtStatsCd ?? '', vrfcNm, vrfcClsfCd);
// }, 500);
// return () => clearTimeout(timer);
}, [vrfcNm, open, isVrfcSn]);
const onSubmit = async (data: GeneralVerificationMasterSubmitFormData) => {
const currentVrfcNm = (data.vrfcNm ?? '').trim();
const originalVrfcNm = originalVrfcNmRef.current;
// 수정 모드인데 기존값과 동일하면 중복검사 완전 생략
const skipNameCheck =
isVrfcSn &&
(originalVrfcNm === null || currentVrfcNm === originalVrfcNm);
if (!skipNameCheck && currentVrfcNm) {
// 제출 시점에 중복검사 실행
const isDuplicate = await runVerificationNameCheck(
data.trgtStatsCd ?? '',
currentVrfcNm,
data.vrfcClsfCd,
);
if (isDuplicate) {
return;
}
}
const confirmMessage = isVrfcSn
? "수정 하시겠습니까?"
: "등록 하시겠습니까?";
if (await confirm(confirmMessage)) {
const userInfo = await getUserInfo();
if (isVrfcSn) {
const updateData = {
vrfcSn: data.vrfcSn,
vrfcNm: data.vrfcNm,
vrfcClsfCd: data.vrfcClsfCd,
dcptDigtCnt: data.dcptDigtCnt,
...(data.dcptDigtCnt === 0
? {}
: { dcptPrcsMthCd: data.dcptPrcsMthCd }),
trgtStatsCd: data.trgtStatsCd,
mdfrId: userInfo.userUnqId,
};
await handleUpdateVrfcMaster(updateData);
} else {
const registData = {
vrfcNm: data.vrfcNm,
vrfcClsfCd: data.vrfcClsfCd,
dcptDigtCnt: data.dcptDigtCnt,
dcptPrcsMthCd: data.dcptPrcsMthCd,
trgtStatsCd: data.trgtStatsCd,
rgtrId: userInfo.userUnqId
};
await handleRegistVrfcMaster(registData);
}
}
};
useEffect(() => {
if (!open) {
form.reset();
setCheckVrfcNm(false);
originalVrfcNmRef.current = null;
}
}, [open, form]);
return (
<Dialog open={open} onOpenChange={onOpenChange} {...props}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>
{isVrfcSn ?
detailData ? detailData.data.vrfcNm
:
<Skeleton className="h-[14px] rounded-xl w-2/3" />
: "일반 검증 등록"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
className="space-y-4"
onSubmit={form.handleSubmit((data) => {
void onSubmit(data);
})}
>
<Table variant="secondary">
<colgroup>
<col width="25%" />
<col width="75%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>* 통계항목</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="trgtStatsCd"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
upCd="STITEM"
value={field.value ?? ''}
onValueChange={field.onChange}
placeholder="전체"
className="w-[16rem]"
disabled={isVrfcSn}
/>
</FormControl>
{form.formState.errors.trgtStatsCd && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.trgtStatsCd.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 검증명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="vrfcNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="검증명을 입력해주세요"
disabled={(trgtStatsCd ?? '') === ''}
autoComplete="off"
/>
</FormControl>
{checkVrfcNm &&
<p className="text-red-500 text-base pt-2">
이미 등록된 검증명입니다.
</p>
}
{form.formState.errors.vrfcNm && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.vrfcNm.message}
</p>
)}
</FormItem>
)}
/>
{/* <Button color="white" className="mt-2">중복검사</Button> */}
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 검증분류</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="vrfcClsfCd"
render={({ field }) => (
<FormItem>
<FormControl>
<Select
value={field.value ?? ""}
onValueChange={field.onChange}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">공통 검증</SelectItem>
<SelectItem value="DO_SI">도단위 검증</SelectItem>
<SelectItem value="SI_GU">시단위 검증</SelectItem>
</SelectContent>
</Select>
</FormControl>
{form.formState.errors.vrfcClsfCd && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.vrfcClsfCd.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>* 소수점자리수</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="dcptDigtCnt"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? 0}
className="!text-[1.3rem] w-[10rem]"
placeholder="소수점자리수를 입력해주세요"
type="number"
min={0}
/>
</FormControl>
<p className="text-gray-500 text-base !mt-0">
※ 0일시 소수점자리 생략
</p>
{form.formState.errors.dcptDigtCnt && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.dcptDigtCnt.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>소수점처리방식</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="dcptPrcsMthCd"
render={({ field }) => (
<FormItem>
<FormControl>
<Select
value={field.value ?? ""}
onValueChange={field.onChange}
disabled={Number(dcptDigtCnt ?? 0) === 0}
>
<SelectTrigger className="w-[10rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CEIL">올림</SelectItem>
<SelectItem value="ROUND">반올림</SelectItem>
<SelectItem value="FLOOR">내림</SelectItem>
</SelectContent>
</Select>
</FormControl>
<p className="text-gray-500 text-base !mt-0">
※ 소수점자리가 1일시 두 번째 자리에서 올림/반올림/내림 처리<br />
예시 - 14.16 → 14.2
</p>
{form.formState.errors.dcptPrcsMthCd && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.dcptPrcsMthCd.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
{isVrfcSn &&
<>
<TableRow>
<TableHead>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
<Label htmlFor="useY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="useN" />
<Label htmlFor="useN">사용안함</Label>
</div>
</RadioGroup>
</FormControl>
{form.formState.errors.useYn && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.useYn.message}
</p>
)}
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>등록자</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="rgtrId"
render={({ field }) => (
<FormItem>
<FormControl>
{registrarUser?.userName != null ? (
<span className="!text-[1.3rem]">{registrarUser.userName}</span>
) : field.value ? (
<Skeleton className="h-9 w-24 rounded-xl" />
) : (
<span className="!text-[1.3rem]">-</span>
)}
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>등록일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="regDt"
render={({ field }) => (
<FormItem>
<FormControl>
{field.value != null && field.value != '' ?
<span className="!text-[1.3rem]">{field.value}</span>
:
<span className="!text-[1.3rem]">-</span>
}
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정자</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="mdfrId"
render={({ field }) => (
<FormItem>
<FormControl>
{modifierUser?.userName != null ? (
<span className="!text-[1.3rem]">{modifierUser.userName}</span>
) : field.value ? (
<Skeleton className="h-9 w-24 rounded-xl" />
) : (
<span className="!text-[1.3rem]">-</span>
)}
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>수정일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="mdfcnDt"
render={({ field }) => (
<FormItem>
<FormControl>
{field.value != null && field.value != '' ?
<span className="!text-[1.3rem]">{field.value}</span>
:
<span className="!text-[1.3rem]">-</span>
}
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</>
}
</TableBody>
</Table>
<DialogFooter>
{vrfcSn &&
<Button size="lg" type="button" color="red" onClick={handleDeleteVrfcMaster}>
삭제
</Button>
}
<Button size="lg" type="submit">
{vrfcSn ? '수정' : '등록'}
</Button>
<DialogClose asChild>
<Button size="lg" color="white">
취소
</Button>
</DialogClose>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
})
export default RegistDialog;
@@ -1,10 +1,14 @@
import dayjs from "dayjs";
import { useState } from "react";
import type { ColumnDef } from "@tanstack/react-table";
import { Button } from "@common/components";
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
export const GeneralVerificationColumn: ColumnDef<GeneralverificationList>[] = [
import { Button } from "@common/components";
import RegistDialog from "@/components/statistics/verification/generalVerification/RegistDialog";
export const getGeneralVerificationColumns = (
onUpdateSuccess?: () => void,
): ColumnDef<GeneralverificationList>[] => [
{
id: "no",
header: "NO",
@@ -14,21 +18,20 @@ export const GeneralVerificationColumn: ColumnDef<GeneralverificationList>[] = [
return totalRows - (pageIndex * pageSize + row.index);
},
},
// {
// accessorKey: "vrfcSn",
// header: "일련번호",
// },
{
accessorKey: "vrfSn",
header: "일련번호",
enableHiding: true,
},
{
accessorKey: "tabNm",
accessorKey: "trgtStatsNm",
header: "통계항목",
},
{
accessorKey: "vrfNm",
accessorKey: "vrfcNm",
header: "검증명",
},
{
accessorKey: "vrfClsCd",
accessorKey: "vrfcClsfNm",
header: "검증분류",
},
{
@@ -48,15 +51,31 @@ export const GeneralVerificationColumn: ColumnDef<GeneralverificationList>[] = [
header: "관리",
cell: ({ row }) => (
<div onClick={(e) => e.stopPropagation()}>
<GeneralVerificationManageButton vrfSn={row.original.vrfSn} />
<GeneralVerificationManageButton
vrfcSn={row.original.vrfcSn}
onSuccess={onUpdateSuccess}
/>
</div>
),
},
];
const GeneralVerificationManageButton = ({ vrfSn }: { vrfSn: number }) => {
const GeneralVerificationManageButton = ({
vrfcSn,
onSuccess,
}: {
vrfcSn: string;
onSuccess?: () => void;
}) => {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<Button size="sm" >관리</Button>
<RegistDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
vrfcSn={vrfcSn}
onSuccess={onSuccess}
>
<Button size="sm">관리</Button>
</RegistDialog>
);
};
+1
View File
@@ -1,6 +1,7 @@
export { CodeApi } from "./api/code-service";
export type { ICode } from "./model/code-interface";
export { CodeSelect } from "./ui/code-select";
export { CodeSelectWithAll } from "./ui/code-select-with-all";
export { UpCodeApi } from "./api/upcode-service";
export {
@@ -0,0 +1,48 @@
import { useQuery } from "@tanstack/react-query";
import {
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "../api/code-service";
type CodeSelectProps = {
upCd: string;
placeholder?: string;
className?: string;
} & React.ComponentPropsWithoutRef<typeof Select>;
function CodeSelectWithAll({
upCd,
placeholder = "선택",
className,
...props
}: CodeSelectProps) {
const { data: code } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, upCd],
queryFn: () => CodeApi.getCode(upCd),
enabled: !!upCd,
});
return (
<Select {...props}>
<SelectTrigger className={cn("w-[12rem]", className)}>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">전체</SelectItem>
{code?.map((item) => (
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
{item.lwpoCdNm}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export { CodeSelectWithAll };
@@ -0,0 +1,66 @@
import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
GeneralverificationRequest,
GeneralverificationResponse,
GeneralverificationService,
GeneralverificationInfoResponse,
CheckVerificationNameRequest,
CheckVerificationNameResponse,
UpdateVrfcMasterRequest,
UpdateVrfcMasterResponse,
RegistVrfcMasterRequest,
RegistVrfcMasterResponse,
DeleteVrfcMasterResponse,
} from "../model/general-verification-interface";
export class GeneralverificationApiService implements GeneralverificationService {
public async getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse> {
const response = await stClient.get<GeneralverificationResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters`,
{ params: param },
);
return response.data;
}
public async getGeneralverificationInfo(param: string): Promise<GeneralverificationInfoResponse> {
const response = await stClient.get<GeneralverificationInfoResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param}`,
);
return response.data;
}
public async checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse> {
const response = await stClient.get<CheckVerificationNameResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/check-name`,
{ params: param },
);
return response.data;
}
public async UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse> {
const response = await stClient.put<UpdateVrfcMasterResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/masters/${param.vrfcSn}`,
param,
);
return response.data;
}
public async RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse> {
const response = await stClient.post<RegistVrfcMasterResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/master`,
param,
);
return response.data;
}
public async DeleteVrfcMaster(param: string): Promise<DeleteVrfcMasterResponse> {
const response = await stClient.delete<DeleteVrfcMasterResponse> (
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/${param}`,
);
return response.data;
}
}
export const generalverificationApiService = new GeneralverificationApiService();
@@ -1,12 +1,89 @@
export interface GeneralverificationList {
vrfSn: number;
tabNm: string;
vrfNm: string;
vrfClsCd: string;
statsCd: string;
vrfcSn: string;
vrfcNm: string;
vrfcClsfCd: string;
vrfcClsfNm: string;
trgtStatsCd: string;
trgtStatsNm: string;
dcptDigtCnt: number;
dcptPrcsMthCd: string;
dcptPrcsMthNm: string;
useYn: string;
rgtrId?: string;
regDt?: string;
mdfrId?: string;
mdfcnDt?: string;
rgtrId?: string;
mdfrId?: string;
}
export interface GeneralverificationRequest {
trgtStatsCd?: string;
vrfcClsfCd?: string;
useYn?: string;
}
export interface GeneralverificationResponse {
status: string;
message?: string;
data: GeneralverificationList[];
}
export interface GeneralverificationInfoResponse {
status: string;
message?: string;
data: GeneralverificationList;
}
export interface CheckVerificationNameRequest {
trgtStatsCd: string;
vrfcNm: string;
vrfcClsfCd: string;
}
export interface CheckVerificationNameResponse {
status: string;
message?: string;
data: boolean;
}
export interface UpdateVrfcMasterRequest {
vrfcSn?: string | null;
vrfcNm?: string | null;
vrfcClsfCd?: string | null;
dcptDigtCnt?: number | null;
dcptPrcsMthCd?: string | null;
useYn?: string | null;
mdfrId?: string | null;
}
export interface UpdateVrfcMasterResponse {
status: string;
message?: string;
}
export interface RegistVrfcMasterRequest {
vrfcNm?: string | null;
vrfcClsfCd?: string | null;
dcptDigtCnt?: number | null;
dcptPrcsMthCd?: string | null;
rgtrId?: string | null;
trgtStatsCd?: string | null;
}
export interface RegistVrfcMasterResponse {
status: string;
message?: string;
}
export interface DeleteVrfcMasterResponse {
status: string;
message?: string;
}
export interface GeneralverificationService {
getGeneralverificationList(param: GeneralverificationRequest): Promise<GeneralverificationResponse>;
getGeneralverificationInfo(param: string): Promise<GeneralverificationInfoResponse>;
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
DeleteVrfcMaster(param: string): Promise<DeleteVrfcMasterResponse>;
}
@@ -0,0 +1,28 @@
import { z } from "zod";
export const generalVerificationMasterSubmitSchema = z
.object({
vrfcSn: z.string().nullable().optional(),
vrfcNm: z.string().min(1, "검증명을 입력하세요"),
vrfcClsfCd: z.string().min(1, "검증분류를 선택하세요"),
trgtStatsCd: z.string().min(1, "통계항목을 선택하세요"),
trgtStatsNm: z.string().nullable().optional(),
dcptDigtCnt: z.coerce.number(),
dcptPrcsMthCd: z.string().nullable().optional(),
useYn: z.string().nullable().optional(),
rgtrId: z.string().nullable().optional(),
regDt: z.string().nullable().optional(),
mdfrId: z.string().nullable().optional(),
mdfcnDt: z.string().nullable().optional(),
})
.refine(
(data) => data.dcptDigtCnt === 0 || !!data.dcptPrcsMthCd,
{
path: ["dcptPrcsMthCd"],
message: "소수점 처리방식을 선택하세요",
},
);
export type GeneralVerificationMasterSubmitFormData = z.infer<
typeof generalVerificationMasterSubmitSchema
>;
+1
View File
@@ -71,6 +71,7 @@ export const ENDPOINT = {
INPUT: "/st-input-service/api/v1/input",
REVIEW: "/st-management-service/api/v1/review",
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",