Merge remote-tracking branch 'origin/iipbs/st/dev'
This commit is contained in:
+4
-1
@@ -39,4 +39,7 @@ next-env.d.ts
|
||||
|
||||
.env*
|
||||
!.env.example
|
||||
*.md
|
||||
*.md
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
@@ -67,7 +67,7 @@ const CompactDataGrid = ({
|
||||
fixedColumns,
|
||||
showHeaderCheckbox = false,
|
||||
rowData,
|
||||
setRowData,
|
||||
// setRowData,
|
||||
getGridDataRef,
|
||||
cellStyleCallback,
|
||||
onRowClick,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -17,9 +17,9 @@ import { useAdminStore } from "@/store/admin-store";
|
||||
const SewagePipeInp = () => {
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [_isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
|
||||
const [isEnterable, setIsEnterable] = useState<boolean>(true);
|
||||
const [isEnterable, _setIsEnterable] = useState<boolean>(true);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
@@ -46,42 +46,42 @@ const SewagePipeInp = () => {
|
||||
});
|
||||
}, [isEnterable, isAdminInfo]);
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
// const handleSearch = async (values: SearchFormValues) => {
|
||||
// if (!values.sido || values.sido === 'ALL') {
|
||||
// alert('시도를 선택해주세요.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
setActiveTab('ST00020001');
|
||||
// setActiveTab('ST00020001');
|
||||
|
||||
setRowDataMap(new Map());
|
||||
// setRowDataMap(new Map());
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// try {
|
||||
// setIsLoading(true);
|
||||
|
||||
// const searchParam: SewagePipeReviewRequestParams = {
|
||||
// statsYr: values.year,
|
||||
// sewerBzmnCd: values.bsnmCd,
|
||||
// ctpvNm: values.sido,
|
||||
// sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
// };
|
||||
// // const searchParam: SewagePipeReviewRequestParams = {
|
||||
// // statsYr: values.year,
|
||||
// // sewerBzmnCd: values.bsnmCd,
|
||||
// // ctpvNm: values.sido,
|
||||
// // sggNm: values.gugun === 'ALL' ? '' : values.gugun
|
||||
// // };
|
||||
|
||||
// searchParamRef.current = searchParam;
|
||||
// const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam);
|
||||
// // searchParamRef.current = searchParam;
|
||||
// // const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam);
|
||||
|
||||
// if (response?.status === 'success' && response.data) {
|
||||
// const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
// newRowDataMap.set('ST00020001', toGridRows(response.data as object[]));
|
||||
// setRowDataMap(newRowDataMap);
|
||||
// } else {
|
||||
// alert(response?.message || "데이터가 없습니다.");
|
||||
// }
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// // if (response?.status === 'success' && response.data) {
|
||||
// // const newRowDataMap = new Map<string, Record<string, string | number>[]>();
|
||||
// // newRowDataMap.set('ST00020001', toGridRows(response.data as object[]));
|
||||
// // setRowDataMap(newRowDataMap);
|
||||
// // } else {
|
||||
// // alert(response?.message || "데이터가 없습니다.");
|
||||
// // }
|
||||
// } catch {
|
||||
// alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
|
||||
@@ -18,7 +18,6 @@ import DataGrid from '@/components/module/DataGrid';
|
||||
import { CodeSelect } from '@/entities/code';
|
||||
|
||||
import { LogHistoryColumn } from '@/components/util/column/management/LogHistoryColumn';
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
// import { LwpoCodeApi } from "@/entities/code";
|
||||
|
||||
// const getStateCdListFetcher = async () => {
|
||||
|
||||
@@ -85,7 +85,7 @@ const PumpingStationRvw = () => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
|
||||
@@ -1,125 +1,157 @@
|
||||
"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 { 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<number | 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) => String(row.vrfcSn),
|
||||
state: { rowSelection },
|
||||
onRowSelectionChange: setRowSelection,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log(statsCd);
|
||||
const handleVrfcMasterRowClick = async (row: GeneralverificationList) => {
|
||||
setRowSelection({ [row.vrfcSn]: true });
|
||||
setSelectedVrfcRow(row.vrfcSn);
|
||||
setShowRegistForm(false);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowSelection({});
|
||||
setSelectedVrfcRow(null);
|
||||
handleSearch();
|
||||
setShowRegistForm(false);
|
||||
}
|
||||
|
||||
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 +160,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">
|
||||
등록
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,19 +15,17 @@ import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import type { ApiErrorResponse } from "@/shared/model";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { analysisApi } from "@/entities/statistics/api/analysis-api";
|
||||
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type {
|
||||
StAnlsMstResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "@/entities/statistics/model/analysis-history-interface";
|
||||
import type {
|
||||
AnalysisRequestPayload,
|
||||
AnalysisResponseDto,
|
||||
} from "@/entities/statistics/model/analysis-interface";
|
||||
import { verificationHistoryColumns } from "@/entities/statistics/model/verification-history-columns";
|
||||
StAnlsMstResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "@/entities/statistics";
|
||||
import { verificationHistoryColumns } from "@/entities/statistics";
|
||||
import { analysisApi } from "@/entities/statistics/api/analysis-api";
|
||||
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
Button,
|
||||
@@ -228,11 +226,7 @@ const HbStatisticsVerification = () => {
|
||||
state: {
|
||||
pagination: historyPagination,
|
||||
},
|
||||
onPaginationChange: (updater) => {
|
||||
setHistoryPagination((prev) =>
|
||||
typeof updater === "function" ? updater(prev) : updater,
|
||||
);
|
||||
},
|
||||
onPaginationChange: setHistoryPagination,
|
||||
meta: {
|
||||
onOutlierCellClick: (row: StAnlsMstResponse) => {
|
||||
setOutlierDialog({ open: true, anlsSn: row.anlsSn });
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import type { ChartOptions } from "chart.js";
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip as ChartTooltip,
|
||||
} from "chart.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi, CodeSelect } from "@/entities/code";
|
||||
import type {
|
||||
StAnlsMstResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsRsltResponse,
|
||||
} from "@/entities/statistics";
|
||||
import {
|
||||
imrDetailGridColumnInfo,
|
||||
imrVerificationHistoryColumns,
|
||||
} from "@/entities/statistics";
|
||||
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
|
||||
import { imrApi } from "@/entities/statistics/api/imr-api";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
DateRangePicker,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
ChartTooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
type DateRangeValue = {
|
||||
from: Date;
|
||||
to?: Date;
|
||||
};
|
||||
|
||||
const EMPTY_DATA: never[] = [];
|
||||
|
||||
const ImrVerificationHistory = () => {
|
||||
// Area A Search States
|
||||
const [searchSido, setSearchSido] = useState<string>("");
|
||||
const [searchSigungu, setSearchSigungu] = useState<string>("");
|
||||
const [searchStatsCd, setSearchStatsCd] = useState<string>("");
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue | undefined>({
|
||||
from: dayjs().subtract(1, "month").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
});
|
||||
const [appliedSearchParams, setAppliedSearchParams] =
|
||||
useState<StAnlsMstSearchParams>({
|
||||
page: 0,
|
||||
size: 10,
|
||||
vrfcMthdCd: "IMR",
|
||||
sort: "anlsSn,desc",
|
||||
});
|
||||
|
||||
// Selected States
|
||||
const [selectedHistory, setSelectedHistory] =
|
||||
useState<StAnlsMstResponse | null>(null);
|
||||
const [selectedDetail, setSelectedDetail] =
|
||||
useState<StAnlsRsltResponse | null>(null);
|
||||
|
||||
// Area B Grid State
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
// Area A Pagination
|
||||
const [historyPagination, setHistoryPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// Codes for Filter
|
||||
const { data: lwpoCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
});
|
||||
|
||||
const sidoOptions = useMemo(() => {
|
||||
if (!lwpoCodes) return [];
|
||||
return lwpoCodes.filter(
|
||||
(c) => c.lwpoCd.startsWith("G3") && c.lwpoCd.length === 4,
|
||||
);
|
||||
}, [lwpoCodes]);
|
||||
|
||||
const sigunguOptions = useMemo(() => {
|
||||
if (!lwpoCodes || searchSido === "") return [];
|
||||
return lwpoCodes.filter(
|
||||
(c) => c.lwpoCd.startsWith(searchSido) && c.lwpoCd.length === 7,
|
||||
);
|
||||
}, [lwpoCodes, searchSido]);
|
||||
|
||||
// Area A Data Fetching
|
||||
const { data: historyData } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST,
|
||||
appliedSearchParams,
|
||||
historyPagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
analysisHistoryApi.getAnalysisHistoryList({
|
||||
...appliedSearchParams,
|
||||
page: historyPagination.pageIndex,
|
||||
size: historyPagination.pageSize,
|
||||
}),
|
||||
});
|
||||
|
||||
const historyTable = useReactTable({
|
||||
data: historyData?.content ?? EMPTY_DATA,
|
||||
columns: imrVerificationHistoryColumns,
|
||||
getRowId: (row) => String(row.anlsSn),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: historyData?.totalPages ?? 0,
|
||||
state: {
|
||||
pagination: historyPagination,
|
||||
rowSelection: selectedHistory
|
||||
? { [selectedHistory.anlsSn]: true }
|
||||
: {},
|
||||
},
|
||||
onPaginationChange: setHistoryPagination,
|
||||
});
|
||||
|
||||
// Area B Data Fetching
|
||||
const { data: detailData, isLoading: isDetailLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
selectedHistory?.anlsSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getImrResultList(selectedHistory!.anlsSn, {
|
||||
page: 0,
|
||||
size: 100,
|
||||
sort: "rsltSn,asc",
|
||||
}),
|
||||
enabled: !!selectedHistory,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailData || !detailData.content) {
|
||||
setGridRowData([]);
|
||||
return;
|
||||
}
|
||||
const mapped = detailData.content.map((item) => ({
|
||||
...item,
|
||||
statsNm: selectedHistory?.vrfcTrgtNm ?? "-",
|
||||
}));
|
||||
setGridRowData(mapped as unknown as Record<string, string | number>[]);
|
||||
}, [detailData, selectedHistory]);
|
||||
|
||||
// Area C Data Fetching
|
||||
const { data: chartData, isLoading: isChartLoading } = useQuery({
|
||||
queryKey: [
|
||||
"imr-log",
|
||||
selectedHistory?.anlsSn,
|
||||
selectedDetail?.vrfcTrgtSn,
|
||||
selectedDetail?.admdstCd,
|
||||
],
|
||||
queryFn: () =>
|
||||
imrApi.getOutliersLog({
|
||||
anlsSn: selectedHistory!.anlsSn,
|
||||
vrfcTrgtSn: selectedDetail!.vrfcTrgtSn,
|
||||
admdstCd: selectedDetail!.admdstCd,
|
||||
}),
|
||||
enabled: !!selectedHistory && !!selectedDetail,
|
||||
});
|
||||
|
||||
const handleSearch = () => {
|
||||
const params: StAnlsMstSearchParams = {
|
||||
vrfcMthdCd: "IMR",
|
||||
sort: "anlsSn,desc",
|
||||
};
|
||||
if (searchSido !== "")
|
||||
params.sewerBzmnCd =
|
||||
searchSigungu !== "" ? searchSigungu : searchSido;
|
||||
if (searchStatsCd) params.statsCd = searchStatsCd;
|
||||
if (dateRange?.from)
|
||||
params.verifyDtStart = dayjs(dateRange.from).format("YYYY-MM-DD");
|
||||
if (dateRange?.to)
|
||||
params.verifyDtEnd = dayjs(dateRange.to).format("YYYY-MM-DD");
|
||||
|
||||
setAppliedSearchParams(params);
|
||||
setHistoryPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
setSelectedHistory(null);
|
||||
setSelectedDetail(null);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchSido("");
|
||||
setSearchSigungu("");
|
||||
setSearchStatsCd("");
|
||||
const defaultRange = {
|
||||
from: dayjs().subtract(1, "month").toDate(),
|
||||
to: dayjs().toDate(),
|
||||
};
|
||||
setDateRange(defaultRange);
|
||||
};
|
||||
|
||||
const chartOptions: ChartOptions<"line"> = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "top" as const,
|
||||
labels: {
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: selectedDetail
|
||||
? `${selectedDetail.fcltNm} - I-MR 분석 차트`
|
||||
: "I-MR 분석 차트",
|
||||
font: {
|
||||
size: 16,
|
||||
weight: "bold",
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "수치",
|
||||
},
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "연도",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[selectedDetail],
|
||||
);
|
||||
|
||||
const lineChartData = useMemo(() => {
|
||||
if (!chartData || !chartData.history) return null;
|
||||
|
||||
const labels = chartData.history.map((h) => String(h.year));
|
||||
const actualData = chartData.history.map((h) => h.x);
|
||||
const uclData = chartData.history.map((h) => h.ucl);
|
||||
const lclData = chartData.history.map((h) => h.lcl);
|
||||
const predictedData = chartData.history.map((h) => h.predicted);
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "실측치 (X)",
|
||||
data: actualData,
|
||||
borderColor: "rgb(75, 192, 192)",
|
||||
backgroundColor: "rgba(75, 192, 192, 0.5)",
|
||||
tension: 0.1,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
},
|
||||
{
|
||||
label: "상한선 (UCL)",
|
||||
data: uclData,
|
||||
borderColor: "rgb(255, 99, 132)",
|
||||
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||
borderDash: [5, 5],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "하한선 (LCL)",
|
||||
data: lclData,
|
||||
borderColor: "rgb(54, 162, 235)",
|
||||
backgroundColor: "rgba(54, 162, 235, 0.2)",
|
||||
borderDash: [5, 5],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: "예측치 (Predicted)",
|
||||
data: predictedData,
|
||||
borderColor: "rgb(153, 102, 255)",
|
||||
backgroundColor: "rgba(153, 102, 255, 0.5)",
|
||||
borderDash: [2, 2],
|
||||
pointStyle: "false",
|
||||
fill: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [chartData]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 영역 A: 검증 이력 그리드 + 검색 필터 */}
|
||||
<div className="card card-border bg-white p-6">
|
||||
<div className="flex flex-wrap items-end gap-6 p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
시도
|
||||
</Label>
|
||||
<Select
|
||||
value={searchSido}
|
||||
onValueChange={(val) => {
|
||||
setSearchSido(val);
|
||||
setSearchSigungu("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue placeholder="시도 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.lwpoCd}
|
||||
value={opt.lwpoCd}
|
||||
>
|
||||
{opt.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
시군구
|
||||
</Label>
|
||||
<Select
|
||||
value={searchSigungu}
|
||||
onValueChange={setSearchSigungu}
|
||||
disabled={searchSido === ""}
|
||||
>
|
||||
<SelectTrigger className="w-[14rem]">
|
||||
<SelectValue placeholder="시군구 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sigunguOptions.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.lwpoCd}
|
||||
value={opt.lwpoCd}
|
||||
>
|
||||
{opt.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
검증일시
|
||||
</Label>
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onValueChange={(val) =>
|
||||
setDateRange(
|
||||
val
|
||||
? { from: val.from!, to: val.to }
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
className="w-[20rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-[1.3rem] font-medium text-label">
|
||||
통계항목
|
||||
</Label>
|
||||
<CodeSelect
|
||||
upCd="STITEM"
|
||||
value={searchStatsCd}
|
||||
onValueChange={setSearchStatsCd}
|
||||
placeholder="통계항목 선택"
|
||||
className="w-[22rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button color="gray" onClick={handleReset}>
|
||||
초기화
|
||||
</Button>
|
||||
<Button onClick={handleSearch}>검색</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pb-4">
|
||||
<DataTable
|
||||
table={historyTable}
|
||||
variant="secondary"
|
||||
onRowClick={(row) => {
|
||||
setSelectedHistory(row);
|
||||
setSelectedDetail(null);
|
||||
}}
|
||||
viewPagination={true}
|
||||
className="[&_td]:!p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{/* 영역 B: 상세 데이터 그리드 */}
|
||||
<div className="card card-border bg-white p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="pl-2 text-[1.6rem] font-bold text-title">
|
||||
이상치 데이터
|
||||
</h2>
|
||||
<Button
|
||||
color="green"
|
||||
disabled={
|
||||
!detailData || detailData.content.length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedHistory) {
|
||||
stAnlsRsltApi.downloadExcel(
|
||||
selectedHistory.anlsSn,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 h-[45rem]">
|
||||
{isDetailLoading ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedHistory || gridRowData.length === 0 ? (
|
||||
<div className="flex h-full w-full items-center justify-center text-[1.4rem] text-muted-foreground">
|
||||
{!selectedHistory
|
||||
? "항목을 선택해주세요."
|
||||
: "표시할 항목이 없습니다."}
|
||||
</div>
|
||||
) : (
|
||||
<DataGrid
|
||||
columnInfo={imrDetailGridColumnInfo}
|
||||
rowData={gridRowData}
|
||||
setRowData={setGridRowData}
|
||||
idFieldName="rsltSn"
|
||||
isExcelExport={false}
|
||||
isExcelImport={false}
|
||||
highlightedRowId={selectedDetail?.rsltSn}
|
||||
onRowClick={(e) => {
|
||||
if (!e.rowId) return;
|
||||
const row = detailData?.content?.find(
|
||||
(item) =>
|
||||
String(item.rsltSn) ===
|
||||
String(e.rowId),
|
||||
);
|
||||
if (row) setSelectedDetail(row);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 영역 C: Line Chart */}
|
||||
<div className="card card-border bg-white p-6">
|
||||
<h2 className="mb-4 text-[1.6rem] font-bold text-title">
|
||||
검증 결과 차트
|
||||
</h2>
|
||||
<div className="flex h-[45rem] items-center justify-center rounded-lg border bg-slate-50 p-4">
|
||||
{isChartLoading ? (
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
) : lineChartData ? (
|
||||
<Line options={chartOptions} data={lineChartData} />
|
||||
) : (
|
||||
<div className="text-[1.4rem] text-muted-foreground">
|
||||
상세 데이터를 선택하면 차트가 표시됩니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImrVerificationHistory;
|
||||
@@ -7,12 +7,12 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeSelect } from "@/entities/code/ui/code-select";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import {
|
||||
hbArticleColumnInfo,
|
||||
imrArticleColumnInfo,
|
||||
validationArticleColumnInfo,
|
||||
} from "@/entities/statistics/model/verification-article-column";
|
||||
} from "@/entities/statistics";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type {
|
||||
VerificationTargetManagementResponse,
|
||||
VerificationTargetManagementSearchParams,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import Indicators from "./Indicators";
|
||||
@@ -26,6 +26,7 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
const [selectedVrfcTrgtSn, setSelectedVrfcTrgtSn] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [checkedSns, setCheckedSns] = useState<string[]>([]);
|
||||
const [appliedFilter, setAppliedFilter] = useState<AppliedFilter>({
|
||||
sido: "",
|
||||
gugun: "",
|
||||
@@ -53,20 +54,23 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={onBack}
|
||||
className=""
|
||||
>
|
||||
<i className="diveicon di-arrow-back" />
|
||||
<span className="sr-only">목록으로 돌아가기</span>
|
||||
</Button>
|
||||
<h2 className="text-[1.8rem] font-bold text-title">
|
||||
검증 상세
|
||||
</h2>
|
||||
<div className="flex items-end justify-between px-4 py-2">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="white"
|
||||
onClick={onBack}
|
||||
className=""
|
||||
>
|
||||
<i className="diveicon di-arrow-back" />
|
||||
<span className="sr-only">목록으로 돌아가기</span>
|
||||
</Button>
|
||||
<h2 className="text-[1.8rem] font-bold text-title">
|
||||
검증 상세
|
||||
</h2>
|
||||
</div>
|
||||
<Button disabled={checkedSns.length === 0}>검증확정</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -75,6 +79,8 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
|
||||
anlsSn={anlsSn}
|
||||
selectedVrfcTrgtSn={selectedVrfcTrgtSn}
|
||||
onSelect={handleTargetSelect}
|
||||
checkedSns={checkedSns}
|
||||
onCheck={setCheckedSns}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics";
|
||||
import { useStAnlsRsltList } from "@/entities/statistics/api/queries";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns";
|
||||
import {
|
||||
Button,
|
||||
LoadingWithAnimation,
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import type { AppliedFilter } from "./HbVerificationDetail";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
interface RegionalGridProps {
|
||||
anlsSn: number;
|
||||
@@ -113,7 +112,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
if (appliedFilter.status) {
|
||||
data = data.filter(
|
||||
(item) =>
|
||||
item.otlrYn.toLowerCase() === appliedFilter.status,
|
||||
item.otlrYnCd.toLowerCase() === appliedFilter.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +132,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
|
||||
const mappedData = data.map((item) => ({
|
||||
...item,
|
||||
otlrYn: mapOtlrToLabel(item.otlrYn),
|
||||
otlrYn: mapOtlrToLabel(item.otlrYnCd),
|
||||
}));
|
||||
|
||||
setGridRowData(
|
||||
@@ -148,89 +147,6 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
gugunOptions,
|
||||
]);
|
||||
|
||||
const columnInfo: ColumnInfo = useMemo(
|
||||
() => ({
|
||||
columns: [
|
||||
{
|
||||
name: "sido",
|
||||
fieldName: "sido",
|
||||
header: { text: "시도" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "gugun",
|
||||
fieldName: "gugun",
|
||||
header: { text: "구군" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "행정구역" },
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
name: "prvyrVl",
|
||||
fieldName: "prvyrVl",
|
||||
header: { text: "이전년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "cryrVl",
|
||||
fieldName: "cryrVl",
|
||||
header: { text: "당해년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "chgRt",
|
||||
fieldName: "chgRt",
|
||||
header: { text: "R" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "trsfRt",
|
||||
fieldName: "trsfRt",
|
||||
header: { text: "S" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrTrsfRt",
|
||||
fieldName: "otlrTrsfRt",
|
||||
header: { text: "E" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrYn",
|
||||
fieldName: "otlrYn",
|
||||
header: { text: "상태" },
|
||||
width: 60,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"sido",
|
||||
"gugun",
|
||||
"admdstNm",
|
||||
"prvyrVl",
|
||||
"cryrVl",
|
||||
"chgRt",
|
||||
"trsfRt",
|
||||
"otlrTrsfRt",
|
||||
"otlrYn",
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSidoChange = (val: string) => {
|
||||
setSelectedSido(val);
|
||||
setSelectedGugun("");
|
||||
@@ -331,6 +247,7 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
<SelectContent>
|
||||
<SelectItem value="0">정상</SelectItem>
|
||||
<SelectItem value="1">이상치</SelectItem>
|
||||
<SelectItem value="2">제외</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,11 @@ import zoomPlugin from "chartjs-plugin-zoom";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics";
|
||||
import {
|
||||
useStAnlsChartData,
|
||||
useStAnlsRsltList,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import type { AppliedFilter } from "./HbVerificationDetail";
|
||||
@@ -124,7 +124,7 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
|
||||
if (
|
||||
appliedFilter.status &&
|
||||
item.otlrYn.toLowerCase() !== appliedFilter.status
|
||||
item.otlrYnCd.toLowerCase() !== appliedFilter.status
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
item,
|
||||
};
|
||||
|
||||
if (item.otlrYn === "0") {
|
||||
if (item.otlrYnCd === "0") {
|
||||
normalPoints.push(pt);
|
||||
} else {
|
||||
outlierPoints.push(pt);
|
||||
|
||||
@@ -1,45 +1,29 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics";
|
||||
import { getVerificationListColumns } from "@/entities/statistics";
|
||||
import { stAnlsDtlApi } from "@/entities/statistics/api/statistics-analysis-detail-api";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type { StAnlsDtlResponse } from "@/entities/statistics/model/statistics-analysis-detail-interface";
|
||||
import { DataTable } from "@common/components";
|
||||
|
||||
interface VerificationListProps {
|
||||
anlsSn: number;
|
||||
selectedVrfcTrgtSn: string | null;
|
||||
onSelect: (row: StAnlsDtlResponse) => void;
|
||||
checkedSns: string[];
|
||||
onCheck: (checkedSns: string[], rows: StAnlsDtlResponse[]) => void;
|
||||
}
|
||||
|
||||
const VerificationList: React.FC<VerificationListProps> = ({
|
||||
anlsSn,
|
||||
selectedVrfcTrgtSn,
|
||||
onSelect,
|
||||
checkedSns,
|
||||
onCheck,
|
||||
}) => {
|
||||
const { data: verificationTargetCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, "codes"],
|
||||
queryFn: () =>
|
||||
verificationTargetManagementApi.getVerificationTargetCodes(),
|
||||
});
|
||||
|
||||
const targetCodeMap = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
if (verificationTargetCodes) {
|
||||
verificationTargetCodes.forEach((item) => {
|
||||
map.set(item.vrfcTrgtSn, item.vrfcTrgtNm);
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [verificationTargetCodes]);
|
||||
|
||||
const { data: detailData, isLoading } = useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_DTL, "list", anlsSn],
|
||||
queryFn: () =>
|
||||
@@ -51,50 +35,9 @@ const VerificationList: React.FC<VerificationListProps> = ({
|
||||
return (detailData?.content ?? []) as StAnlsDtlResponse[];
|
||||
}, [detailData]);
|
||||
|
||||
const columns: ColumnDef<StAnlsDtlResponse>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
header: "통계 년도",
|
||||
accessorKey: "statsYr",
|
||||
},
|
||||
{
|
||||
header: "통계 항목",
|
||||
accessorKey: "statsNm",
|
||||
},
|
||||
{
|
||||
header: "검증 항목",
|
||||
accessorKey: "vrfcTrgtNm",
|
||||
},
|
||||
{
|
||||
header: "μ",
|
||||
accessorKey: "ipcrCntVl",
|
||||
},
|
||||
{
|
||||
header: "Percentile",
|
||||
accessorKey: "pctlCrtrVl",
|
||||
},
|
||||
{
|
||||
header: "A",
|
||||
accessorKey: "scopAjmtCntVl",
|
||||
},
|
||||
{
|
||||
header: "C",
|
||||
accessorKey: "thrsCntVl",
|
||||
},
|
||||
{
|
||||
header: "정상건수",
|
||||
accessorKey: "nmlDataNocs",
|
||||
},
|
||||
{
|
||||
header: "제외건수",
|
||||
accessorKey: "exclDataNocs",
|
||||
},
|
||||
{
|
||||
header: "이상건수",
|
||||
accessorKey: "otlrDataNocs",
|
||||
},
|
||||
],
|
||||
[targetCodeMap],
|
||||
const columns = useMemo(
|
||||
() => getVerificationListColumns({ rowData, checkedSns, onCheck }),
|
||||
[rowData, checkedSns, onCheck],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
@@ -134,15 +77,13 @@ const VerificationList: React.FC<VerificationListProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && rowData.length > 0 && (
|
||||
<div className="rounded-md border">
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination={false}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination={false}
|
||||
className="whitespace-nowrap text-[1.2rem] [&_td]:!px-2 [&_th]:!px-2"
|
||||
/>
|
||||
)}
|
||||
</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;
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { NoData } from '@/shared/ui/no-data';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import FormulaComponents from './FormulaComponents';
|
||||
import { LoadingWithAnimation } from '@common/components';
|
||||
import FomulaDialog from './FomulaDialog';
|
||||
|
||||
import { useAlertStore } from '@/shared/lib';
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
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: number | null;
|
||||
}
|
||||
|
||||
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,
|
||||
}));
|
||||
|
||||
const UNARY_TERMINAL_OPERATORS = new Set<string>(['is_null', '!is_null', 'floor']);
|
||||
const lastOperator = rows.at(-1)?.cmprOpert ?? null;
|
||||
const lastIsUnary = rows.length > 0 && lastOperator != null && UNARY_TERMINAL_OPERATORS.has(lastOperator);
|
||||
const isSingleUnaryFormula = rows.length === 1 && lastIsUnary;
|
||||
|
||||
// 단항 연산자일 때만 row 1개 허용
|
||||
if (rows.length === 1 && !isSingleUnaryFormula) {
|
||||
alert('해당 연산자는 최소 2개 항목이 필요합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 단항 연산자(is_null, !is_null, floor)는 반드시 맨 마지막 항목에만 허용
|
||||
if (rows.length > 1) {
|
||||
const unaryAtWrongIdx = rows.findIndex(
|
||||
(r, idx) => idx < rows.length - 1 && r.cmprOpert != null && UNARY_TERMINAL_OPERATORS.has(r.cmprOpert),
|
||||
);
|
||||
if (unaryAtWrongIdx !== -1) {
|
||||
alert(`단항 연산자(is_null, !is_null, floor)는 맨 마지막 항목에만 사용할 수 있습니다.\n(순번: ${unaryAtWrongIdx + 1})`);
|
||||
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 && !lastIsUnary) {
|
||||
const invalidOpertIdx = rows.slice(0, -1).findIndex((r) => r.cmprOpert == null);
|
||||
if (invalidOpertIdx !== -1) {
|
||||
alert(`연산자가 선택되지 않은 항목이 있습니다.\n(순번: ${invalidOpertIdx + 1})`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 항목일 때 마지막이 단항 연산자면: 이전 항목들 연산자는 무조건 null
|
||||
if (rows.length > 1 && lastIsUnary) {
|
||||
for (let i = 0; i < rows.length - 1; i++) {
|
||||
const prev = rows[i];
|
||||
if (prev) prev.cmprOpert = null;
|
||||
}
|
||||
setFormulaList((prev) => {
|
||||
if (prev.length <= 1) return prev;
|
||||
const next = prev.map((item, idx) =>
|
||||
idx < prev.length - 1 && item != null ? { ...item, cmprOpert: null } : item,
|
||||
);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// 마지막 항목의 연산자는 (단항 연산자 케이스 제외) 무조건 null 처리
|
||||
if (rows.length > 1 && !lastIsUnary) {
|
||||
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>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailComponent;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,614 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, memo, useRef, useState } from "react";
|
||||
import { useForm } 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?: number;
|
||||
/** 수정/등록 성공 시 부모에서 목록 재조회 등 실행 */
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const RegistDialog = memo(
|
||||
({
|
||||
children,
|
||||
open,
|
||||
onOpenChange,
|
||||
vrfcSn,
|
||||
onSuccess,
|
||||
...props
|
||||
}: RegistDialogProps) => {
|
||||
|
||||
const isVrfcSn = vrfcSn != null;
|
||||
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 != null,
|
||||
});
|
||||
|
||||
const form = useForm<GeneralVerificationMasterSubmitFormData>({
|
||||
resolver: zodResolver(generalVerificationMasterSubmitSchema),
|
||||
values: {
|
||||
vrfcSn: detailData?.data.vrfcSn ?? undefined,
|
||||
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 { 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 != null &&
|
||||
<Button size="lg" type="button" color="red" onClick={handleDeleteVrfcMaster}>
|
||||
삭제
|
||||
</Button>
|
||||
}
|
||||
<Button size="lg" type="submit">
|
||||
{vrfcSn != null ? '수정' : '등록'}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
})
|
||||
|
||||
RegistDialog.displayName = 'RegistDialog';
|
||||
|
||||
export default RegistDialog;
|
||||
@@ -1,10 +1,15 @@
|
||||
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";
|
||||
import CopyDialog from "@/components/statistics/verification/generalVerification/CopyDialog";
|
||||
|
||||
export const getGeneralVerificationColumns = (
|
||||
onUpdateSuccess?: () => void,
|
||||
): ColumnDef<GeneralverificationList>[] => [
|
||||
{
|
||||
id: "no",
|
||||
header: "NO",
|
||||
@@ -15,20 +20,15 @@ export const GeneralVerificationColumn: ColumnDef<GeneralverificationList>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfSn",
|
||||
header: "일련번호",
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "tabNm",
|
||||
accessorKey: "trgtStatsNm",
|
||||
header: "통계항목",
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfNm",
|
||||
accessorKey: "vrfcNm",
|
||||
header: "검증명",
|
||||
},
|
||||
{
|
||||
accessorKey: "vrfClsCd",
|
||||
accessorKey: "vrfcClsfNm",
|
||||
header: "검증분류",
|
||||
},
|
||||
{
|
||||
@@ -43,20 +43,68 @@ export const GeneralVerificationColumn: ColumnDef<GeneralverificationList>[] = [
|
||||
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: "관리",
|
||||
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: number;
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,10 +88,10 @@ export const rawPumpingStationInfoList = {
|
||||
]
|
||||
},
|
||||
{ name: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true },
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, // bscFctNm -> swtrmtFcltNm
|
||||
{ name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, // pumpCls -> pumpSeNm
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, // addr -> impdFcltPstnCn
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, // rnDt -> oprtngStrtYmd
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } },
|
||||
{ name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } },
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } },
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } },
|
||||
{
|
||||
name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' },
|
||||
footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000'
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ValueType } from "realgrid";
|
||||
const baseYear = new Date().getFullYear();
|
||||
import dayjs from 'dayjs';
|
||||
import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
|
||||
const currentYear = dayjs().year() - 1;
|
||||
const startYear = 1996;
|
||||
const totalCount = currentYear - startYear + 1 + 1;
|
||||
|
||||
// 하수도 통계 관리 02. 하수관로
|
||||
const rawList = [
|
||||
{
|
||||
@@ -248,7 +253,7 @@ const rawList = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' },valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
@@ -273,19 +278,19 @@ const rawList = [
|
||||
{
|
||||
name: 'addrGroup', header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] },
|
||||
'sggNm', 'admdstNm'
|
||||
]
|
||||
},
|
||||
'swtrmtFcltNm','prcsFcltCpct', 'displayYear',
|
||||
{ name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, // name 변경됨
|
||||
{ name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] },
|
||||
'sprsysVartnRt',
|
||||
{ name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, // name 변경됨
|
||||
{ name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] },
|
||||
{
|
||||
name: 'pipeGroup', header: { text: '하수관로' },
|
||||
items: [
|
||||
'wholFcltPrlg',
|
||||
{ name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, // name 변경됨
|
||||
{ name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] },
|
||||
{
|
||||
name: 'siteGroup', header: { text: '토실/토구(개소)' },
|
||||
items: [
|
||||
@@ -383,18 +388,18 @@ const rawList = [
|
||||
{ name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 동적 연도 컬럼 (1996년이전[yy30] ~ 2025년[yy1]) */
|
||||
...Array.from({ length: 30 }, (_, i) => {
|
||||
const dtoNum = 30 - i; // 30, 29, ..., 1
|
||||
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
const lyngField = `yy${dtoNum}LyngPrlg`;
|
||||
const dmltnField = `yy${dtoNum}DmltnPrlg`;
|
||||
|
||||
|
||||
return [
|
||||
{ name: `${wholField}_col`, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: `${lyngField}_col`, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: `${dmltnField}_col`, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }
|
||||
{ name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }
|
||||
];
|
||||
}).flat(),
|
||||
|
||||
@@ -404,15 +409,17 @@ const rawList = [
|
||||
'displayYear',
|
||||
{ name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum','lyngPrlgSum', 'dmltnPrlgSum'] },
|
||||
|
||||
...Array.from({ length: 30 }, (_, i) => {
|
||||
const dtoNum = 30 - i;
|
||||
const yearLabel = i === 0 ? '1996년이전' : `${1995 + i}년`;
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
|
||||
const dtoNum = totalCount - i;
|
||||
const yearLabel = i === 0
|
||||
? '1996년이전'
|
||||
: `${startYear + (i - 1)}년`;
|
||||
|
||||
return {
|
||||
name: `group_${yearLabel}`,
|
||||
header: { text: yearLabel },
|
||||
items: [`${wholField}_col`, `yy${dtoNum}LyngPrlg_col`, `yy${dtoNum}DmltnPrlg_col`]
|
||||
items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`]
|
||||
};
|
||||
}),
|
||||
'rmrk'
|
||||
@@ -512,7 +519,7 @@ const rawList = [
|
||||
name: 'addrGroup',
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] },
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] },
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
]
|
||||
|
||||
@@ -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 };
|
||||
@@ -2,40 +2,39 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
AnalysisRequestPayload,
|
||||
AnalysisResponseDto,
|
||||
JobStatusDto,
|
||||
AnalysisResultDto,
|
||||
} from "../model/analysis-interface";
|
||||
AnalysisRequestPayload,
|
||||
AnalysisResponseDto,
|
||||
JobStatusDto,
|
||||
AnalysisResultDto,
|
||||
} from "../model/analysis/analysis-interface";
|
||||
|
||||
export class AnalysisApiService {
|
||||
public async analyze(
|
||||
payload: AnalysisRequestPayload,
|
||||
): Promise<AnalysisResponseDto> {
|
||||
const response = await client.post<AnalysisResponseDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
|
||||
payload,
|
||||
);
|
||||
public async analyze(
|
||||
payload: AnalysisRequestPayload,
|
||||
): Promise<AnalysisResponseDto> {
|
||||
const response = await client.post<AnalysisResponseDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/analyze`,
|
||||
payload,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
|
||||
const response = await client.get<JobStatusDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
|
||||
);
|
||||
public async getJobStatus(jobId: string): Promise<JobStatusDto> {
|
||||
const response = await client.get<JobStatusDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/status/${jobId}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getResult(jobId: string): Promise<AnalysisResultDto> {
|
||||
const response = await client.get<AnalysisResultDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
|
||||
);
|
||||
public async getResult(jobId: string): Promise<AnalysisResultDto> {
|
||||
const response = await client.get<AnalysisResultDto>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ANALYSIS}/result/${jobId}`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const analysisApi = new AnalysisApiService();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
StAnlsMstListApiResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "../model/analysis-history-interface";
|
||||
} from "../model/statistics-analysis/statistics-analysis-master-interface";
|
||||
|
||||
export class AnalysisHistoryApiService {
|
||||
public async getAnalysisHistoryList(
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
GeneralverificationRequest,
|
||||
GeneralverificationResponse,
|
||||
GeneralverificationService,
|
||||
GeneralverificationInfoResponse,
|
||||
CheckVerificationNameRequest,
|
||||
CheckVerificationNameResponse,
|
||||
UpdateVrfcMasterRequest,
|
||||
UpdateVrfcMasterResponse,
|
||||
RegistVrfcMasterRequest,
|
||||
RegistVrfcMasterResponse,
|
||||
DeleteVrfcMasterResponse,
|
||||
VrfcOperatorsResponse,
|
||||
GeneralverificationDetailResponse,
|
||||
VerifyColumnListRequest,
|
||||
VerifyColumnListResponse,
|
||||
VerifyDetailRegistRequest,
|
||||
VerifyDetailUpdateRequest,
|
||||
CopyVerifyDataRequest,
|
||||
} 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: number): 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: 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();
|
||||
@@ -0,0 +1,69 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
ImrDetailRequest,
|
||||
ImrDetailResponse,
|
||||
ImrDetectRequest,
|
||||
ImrDetectResponse,
|
||||
ImrListRequest,
|
||||
ImrListResponse,
|
||||
ImrLogRequest,
|
||||
ImrLogResponse,
|
||||
} from "../model/imr/imr-interface";
|
||||
|
||||
export class ImrApiService {
|
||||
/**
|
||||
* 이상치 탐지 알고리즘 실행 요청
|
||||
*/
|
||||
public async detectOutliers(
|
||||
payload: ImrDetectRequest,
|
||||
): Promise<ImrDetectResponse> {
|
||||
const response = await client.post<ImrDetectResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 마스터 목록 조회
|
||||
*/
|
||||
public async getOutliersList(
|
||||
payload: ImrListRequest,
|
||||
): Promise<ImrListResponse[]> {
|
||||
const response = await client.post<ImrListResponse[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}/list`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 상세 조회
|
||||
*/
|
||||
public async getOutliersDetail(
|
||||
payload: ImrDetailRequest,
|
||||
): Promise<ImrDetailResponse> {
|
||||
const response = await client.post<ImrDetailResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}/list/detail`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 산출 로그 조회
|
||||
*/
|
||||
public async getOutliersLog(
|
||||
payload: ImrLogRequest,
|
||||
): Promise<ImrLogResponse> {
|
||||
const response = await client.post<ImrLogResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.IMR}/list/detail/log`,
|
||||
payload,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const imrApi = new ImrApiService();
|
||||
@@ -1,11 +1,12 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableParams } from "@/shared/model";
|
||||
|
||||
import type {
|
||||
ChartRetDto,
|
||||
StAnlsRsltListApiResponse,
|
||||
StAnlsRsltSearchParams,
|
||||
} from "../model/statistics-analysis-result-interface";
|
||||
} from "../model/statistics-analysis/statistics-analysis-result-interface";
|
||||
|
||||
export class StAnlsRsltApiService {
|
||||
public async getResultList(
|
||||
@@ -21,6 +22,20 @@ export class StAnlsRsltApiService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getImrResultList(
|
||||
anlsSn: number,
|
||||
params: PageableParams,
|
||||
): Promise<StAnlsRsltListApiResponse> {
|
||||
const response = await client.get<StAnlsRsltListApiResponse>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/imr/rsltlist/${anlsSn}`,
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getChartData(
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: number,
|
||||
@@ -48,4 +63,3 @@ export class StAnlsRsltApiService {
|
||||
}
|
||||
|
||||
export const stAnlsRsltApi = new StAnlsRsltApiService();
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export {
|
||||
type AnalysisHistoryListApiResponse,
|
||||
type AnalysisRequestPayload,
|
||||
type AnalysisResponseDto,
|
||||
type AnalysisResultDto,
|
||||
type AnalysisSummaryDto,
|
||||
type JobStatusDto,
|
||||
type OutlierRecordDto,
|
||||
} from "./model/analysis/analysis-interface";
|
||||
export { imrDetailGridColumnInfo } from "./model/imr/imr-detail-grid-column-info";
|
||||
export { imrDetailGridColumns } from "./model/imr/imr-detail-grid-columns";
|
||||
export {
|
||||
type ImrDetailRequest,
|
||||
type ImrDetailResponse,
|
||||
type ImrDetailResponseItem,
|
||||
type ImrDetectRequest,
|
||||
type ImrDetectResponse,
|
||||
type ImrListRequest,
|
||||
type ImrListResponse,
|
||||
type ImrLogDataParam,
|
||||
type ImrLogRequest,
|
||||
type ImrLogResponse,
|
||||
} from "./model/imr/imr-interface";
|
||||
export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns";
|
||||
export {
|
||||
type StAnlsDtlListApiResponse,
|
||||
type StAnlsDtlResponse,
|
||||
type StAnlsDtlSearchParams,
|
||||
} from "./model/statistics-analysis/statistics-analysis-detail-interface";
|
||||
export {
|
||||
type StAnlsMstListApiResponse,
|
||||
type StAnlsMstResponse,
|
||||
type StAnlsMstResponseDto,
|
||||
type StAnlsMstSearchDto,
|
||||
type StAnlsMstSearchParams,
|
||||
type StAnlsOutlierDataResponse,
|
||||
type StAnlsOutlierDataResponseDto,
|
||||
type StAnlsOutlierListResponse,
|
||||
type StAnlsOutlierListResponseDto,
|
||||
} from "./model/statistics-analysis/statistics-analysis-master-interface";
|
||||
export {
|
||||
type ChartBoundaryPointDto,
|
||||
type ChartRetDto,
|
||||
type StAnlsRsltListApiResponse,
|
||||
type StAnlsRsltResponse,
|
||||
type StAnlsRsltSearchParams,
|
||||
} from "./model/statistics-analysis/statistics-analysis-result-interface";
|
||||
export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns";
|
||||
export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns";
|
||||
export {
|
||||
hbArticleColumnInfo,
|
||||
imrArticleColumnInfo,
|
||||
validationArticleColumnInfo,
|
||||
} from "./model/verification-article/verification-article-column";
|
||||
export {
|
||||
type HBArticleItem,
|
||||
type IMRArticleItem,
|
||||
type ValidationArticleItem,
|
||||
} from "./model/verification-article/verification-article-interface";
|
||||
@@ -1,12 +1,212 @@
|
||||
export interface GeneralverificationList {
|
||||
vrfSn: number;
|
||||
tabNm: string;
|
||||
vrfNm: string;
|
||||
vrfClsCd: string;
|
||||
statsCd: string;
|
||||
vrfcSn: number;
|
||||
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?: number | 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 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: number): Promise<GeneralverificationInfoResponse>;
|
||||
checkVerificationName(param: CheckVerificationNameRequest): Promise<CheckVerificationNameResponse>;
|
||||
UpdateVrfcMaster(param: UpdateVrfcMasterRequest): Promise<UpdateVrfcMasterResponse>;
|
||||
RegistVrfcMaster(param: RegistVrfcMasterRequest): Promise<RegistVrfcMasterResponse>;
|
||||
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>;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const generalVerificationMasterSubmitSchema = z
|
||||
.object({
|
||||
vrfcSn: z.number().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
|
||||
>;
|
||||
@@ -0,0 +1,84 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
export const imrDetailGridColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "sido",
|
||||
fieldName: "sido",
|
||||
header: { text: "시도" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "gugun",
|
||||
fieldName: "gugun",
|
||||
header: { text: "구군" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "admdstNm",
|
||||
fieldName: "admdstNm",
|
||||
header: { text: "행정구역" },
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
name: "admdstCd",
|
||||
fieldName: "admdstCd",
|
||||
header: { text: "시설코드" },
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
name: "fcltNm",
|
||||
fieldName: "fcltNm",
|
||||
header: { text: "시설명" },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: "addr",
|
||||
fieldName: "addr",
|
||||
header: { text: "주소" },
|
||||
width: 200,
|
||||
styleName: "left-column",
|
||||
},
|
||||
{
|
||||
name: "pipeSeCd",
|
||||
fieldName: "pipeSeCd",
|
||||
header: { text: "관로구분" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "vrfcTrgtSn",
|
||||
fieldName: "vrfcTrgtSn",
|
||||
header: { text: "항목번호" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "statsNm",
|
||||
fieldName: "statsNm",
|
||||
header: { text: "항목명" },
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
name: "cryrVl",
|
||||
fieldName: "cryrVl",
|
||||
header: { text: "당해년도" },
|
||||
width: 100,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
styleName: "right-column",
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"sido",
|
||||
"gugun",
|
||||
"admdstNm",
|
||||
"admdstCd",
|
||||
"fcltNm",
|
||||
"addr",
|
||||
"pipeSeCd",
|
||||
"vrfcTrgtSn",
|
||||
"statsNm",
|
||||
"cryrVl",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import type { StAnlsRsltResponse } from "../statistics-analysis/statistics-analysis-result-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<StAnlsRsltResponse>();
|
||||
|
||||
export const imrDetailGridColumns = [
|
||||
columnHelper.accessor("sido", {
|
||||
header: "시도",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("gugun", {
|
||||
header: "구군",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("admdstNm", {
|
||||
header: "행정구역",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("admdstCd", {
|
||||
header: "시설코드",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("fcltNm", {
|
||||
header: "시설명",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("addr", {
|
||||
header: "주소",
|
||||
cell: (info) => (
|
||||
<div
|
||||
className="max-w-[150px] truncate px-2 py-2"
|
||||
title={info.getValue() ?? ""}
|
||||
>
|
||||
{info.getValue() ?? "-"}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("pipeSeCd", {
|
||||
header: "관로구분",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("vrfcTrgtSn", {
|
||||
header: "항목번호",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "statsNm",
|
||||
header: "항목명",
|
||||
cell: (info) => {
|
||||
const statsNm = (info.table.options.meta as { statsNm?: string })
|
||||
?.statsNm;
|
||||
return <div className="px-2 py-2">{statsNm ?? "-"}</div>;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("cryrVl", {
|
||||
header: "당해년도",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-2 text-right">
|
||||
{info.getValue()?.toLocaleString() ?? "-"}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 이상치(IMR) 탐지 실행 요청 DTO
|
||||
*/
|
||||
export interface ImrDetectRequest {
|
||||
userId?: string; // 사용자 ID
|
||||
sewerBzmnCd: string; // 하수도 사업자 코드
|
||||
statsCd: string; // 통계 탭/항목 코드
|
||||
statsYr: string; // 분석 기준 통계 연도
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치(IMR) 탐지 실행 응답 DTO
|
||||
*/
|
||||
export interface ImrDetectResponse {
|
||||
anlsSn: number; // 분석 테이블 마스터 일련번호
|
||||
jobId: string; // Celery/비동기 처리를 위한 고유 작업 ID
|
||||
totalOtlrCnt: number; // 탐지된 총 이상치 건수
|
||||
statusCode: string; // 처리 상태 코드 (COMPLETED, FAIL, ERROR 등)
|
||||
errorMsg?: string; // 에러 발생 시 상세 메시지
|
||||
processingTimeMs: number; // 총 처리 소요 시간 (밀리초)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 목록(마스터) 조회 요청 DTO
|
||||
*/
|
||||
export interface ImrListRequest {
|
||||
sewerBzmnCd?: string; // 하수도 사업자 코드
|
||||
statsCd?: string; // 통계 항목 코드
|
||||
statsYr?: string; // 통계 연도
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 목록(마스터) 조회 응답 DTO
|
||||
*/
|
||||
export interface ImrListResponse {
|
||||
anlsSn: number; // 분석 마스터 일련번호
|
||||
anlsSttsCd: string; // 분석 상태 코드 (COMPLETED, FAIL, ERROR)
|
||||
nmlDataNocs: number; // 정상 판정 데이터 건수
|
||||
exclDataNocs: number; // 분석 제외 데이터 건수
|
||||
otlrDataNocs: number; // 이상치 판정 데이터 건수
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 상세 조회 요청 DTO
|
||||
*/
|
||||
export interface ImrDetailRequest {
|
||||
anlsSn: number; // 분석 일련번호
|
||||
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 상세 조회 응답 아이템
|
||||
*/
|
||||
export interface ImrDetailResponseItem {
|
||||
anlsSn: number; // 분석 일련번호
|
||||
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
|
||||
admdstCd: string; // 행정동(행정구역) 코드
|
||||
cryrVl: number; // 당해년도 원천 데이터 값
|
||||
otlrTrsfRt: number; // 이상치 판단 기준 (변이 척도)
|
||||
otlrYn: string; // 이상치 여부 플래그 (Y/N)
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 탐지 상세 조회 응답 DTO
|
||||
*/
|
||||
export interface ImrDetailResponse {
|
||||
items: ImrDetailResponseItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 산출 로그 요청 DTO
|
||||
*/
|
||||
export interface ImrLogRequest {
|
||||
jobUuid?: string; // 작업 고유 식별자
|
||||
anlsSn: number; // 분석 일련번호
|
||||
vrfcTrgtSn: number; // 검증 대상 지표 일련번호
|
||||
admdstCd: string; // 행정동(행정구역) 코드
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 로그 기록용 연도/시퀀스별 파라미터 DTO
|
||||
*/
|
||||
export interface ImrLogDataParam {
|
||||
year: number; // 대상 연도
|
||||
t: number; // 시간 시퀀스
|
||||
tDev: number; // 시점 간의 편차
|
||||
x: number; // 원천 데이터 값
|
||||
xDev: number; // 원천 데이터 값 편차
|
||||
prod: number; // t편차와 x편차의 곱
|
||||
tDevSq: number; // t편차의 제곱
|
||||
predicted: number; // 예측 정상치
|
||||
residuals: number; // 잔차
|
||||
mr: number; // 이동 범위(MR)
|
||||
ucl: number; // 관리 상한선
|
||||
lcl: number; // 관리 하한선
|
||||
judgment: string; // 최종 판정
|
||||
}
|
||||
|
||||
/**
|
||||
* 이상치 산출 상세 로그 응답 DTO
|
||||
*/
|
||||
export interface ImrLogResponse {
|
||||
sewerBzmnCd: string; // 하수도 사업자 코드
|
||||
colNm: string; // 영문 컬럼명
|
||||
vrfcTrgtNm: string; // 분석 대상 지표명
|
||||
admdstCd: string; // 행정동 코드
|
||||
regressionA: number; // 회귀식 기울기(a)
|
||||
regressionB: number; // 회귀식 절편(b)
|
||||
tMean: number; // 시퀀스 평균값
|
||||
xMean: number; // 원데이터 평균값
|
||||
prodMean: number; // 편차 곱의 평균
|
||||
tDevSqMean: number; // t편차 제곱의 평균
|
||||
mrMean: number; // 잔차 이동 범위 평균(MR)
|
||||
uc: number; // 상한 허용 폭(UCL)
|
||||
lc: number; // 하한 허용 폭(LCL)
|
||||
controlRange: number; // 관리 전체 폭(CL)
|
||||
confidenceLevel: string; // 신뢰 구간 표기
|
||||
history: ImrLogDataParam[]; // 연도별 상세 추적 기록
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Progress,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
import type { StAnlsMstResponse } from "../statistics-analysis/statistics-analysis-master-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<StAnlsMstResponse>();
|
||||
|
||||
export const imrVerificationHistoryColumns = [
|
||||
columnHelper.accessor("anlsBgngDt", {
|
||||
header: "검증 실행 일시",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor(
|
||||
(row) => ({
|
||||
anlsSttsCd: row.anlsSttsCd,
|
||||
anlsSttsNm: row.anlsSttsNm,
|
||||
anlsPrgrsRt: row.anlsPrgrsRt,
|
||||
errMsgCn: row.errMsgCn,
|
||||
}),
|
||||
{
|
||||
id: "anlsSttsNm",
|
||||
header: "진행 상태",
|
||||
cell: (info) => {
|
||||
const { anlsSttsCd, anlsSttsNm, anlsPrgrsRt, errMsgCn } =
|
||||
info.getValue();
|
||||
if (anlsSttsCd === "PROCESSING") {
|
||||
return (
|
||||
<div className="flex min-w-[8rem] items-center gap-2">
|
||||
<Progress
|
||||
value={anlsPrgrsRt ?? 0}
|
||||
className="h-2 flex-1"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{anlsPrgrsRt ?? 0}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (anlsSttsCd === "FAILED") {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
{anlsSttsNm}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<i className="diveicon di-info-circle text-destructive" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[64rem]">
|
||||
<p className="p-1 text-[1.2rem] leading-6">
|
||||
{errMsgCn}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return anlsSttsNm;
|
||||
},
|
||||
},
|
||||
),
|
||||
columnHelper.accessor("anlsEndDt", {
|
||||
header: "종료 일시",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("statsYr", {
|
||||
header: "검증년도",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4 text-center">
|
||||
{info.getValue() ?? "-"}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("sewerBzmnCd", {
|
||||
header: "하수도사업자",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("vrfcTrgtNm", {
|
||||
header: "검증대상",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("rgtrNm", {
|
||||
header: "검증자",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("nmlDataNocs", {
|
||||
header: "정상건수",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4 text-right">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("exclDataNocs", {
|
||||
header: "제외건수",
|
||||
cell: (info) => (
|
||||
<div className="px-2 py-4 text-right">{info.getValue() ?? "-"}</div>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("otlrDataNocs", {
|
||||
header: "이상건수",
|
||||
cell: (info) => {
|
||||
const value = info.getValue() ?? 0;
|
||||
return <div className="px-2 py-4 text-right">{value}</div>;
|
||||
},
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
export const regionalGridColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{
|
||||
name: "sido",
|
||||
fieldName: "sido",
|
||||
header: { text: "시도" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "gugun",
|
||||
fieldName: "gugun",
|
||||
header: { text: "구군" },
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
name: "adstDong",
|
||||
fieldName: "adstDong",
|
||||
header: { text: "행정구역" },
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
name: "fcltNm",
|
||||
fieldName: "fcltNm",
|
||||
header: { text: "시설명" },
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
name: "prvyrVl",
|
||||
fieldName: "prvyrVl",
|
||||
header: { text: "이전년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "cryrVl",
|
||||
fieldName: "cryrVl",
|
||||
header: { text: "당해년도" },
|
||||
width: 80,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0",
|
||||
},
|
||||
{
|
||||
name: "chgRt",
|
||||
fieldName: "chgRt",
|
||||
header: { text: "R" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "trsfRt",
|
||||
fieldName: "trsfRt",
|
||||
header: { text: "S" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrTrsfRt",
|
||||
fieldName: "otlrTrsfRt",
|
||||
header: { text: "E" },
|
||||
width: 60,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.00",
|
||||
},
|
||||
{
|
||||
name: "otlrYn",
|
||||
fieldName: "otlrYn",
|
||||
header: { text: "상태" },
|
||||
width: 60,
|
||||
},
|
||||
],
|
||||
columnLayout: [
|
||||
"sido",
|
||||
"gugun",
|
||||
"adstDong",
|
||||
"fcltNm",
|
||||
"prvyrVl",
|
||||
"cryrVl",
|
||||
"chgRt",
|
||||
"trsfRt",
|
||||
"otlrTrsfRt",
|
||||
"otlrYn",
|
||||
],
|
||||
};
|
||||
+1
-1
@@ -47,7 +47,7 @@ export interface StAnlsMstResponseDto {
|
||||
/**
|
||||
* 분석 현황 마스터 검색 DTO
|
||||
*/
|
||||
export interface StAnlsMstSearchDto extends PageableParams {
|
||||
export interface StAnlsMstSearchDto extends PageableParams {
|
||||
/** 분석상태코드 (COMPLETED, FAILED, QUEUED, PROCESSING) */
|
||||
anlsSttsCd?: string;
|
||||
/** 작업UUID */
|
||||
+2
-2
@@ -20,7 +20,7 @@ export interface StAnlsRsltResponse {
|
||||
chgRt: number | null; // 변경율 (r_i)
|
||||
trsfRt: number | null; // 변환 비율 (s_i)
|
||||
otlrTrsfRt: number | null; // 이상치 변환 비율 (E_i)
|
||||
otlrYn: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
|
||||
otlrYnCd: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
|
||||
}
|
||||
|
||||
export interface StAnlsRsltSearchParams extends PageableParams {
|
||||
@@ -29,7 +29,7 @@ export interface StAnlsRsltSearchParams extends PageableParams {
|
||||
admdstCd?: string; // 행정구역 코드
|
||||
admdstNm?: string; // 행정구역 명칭
|
||||
fcltNm?: string; // 시설명
|
||||
otlrYn?: string; // 이상치 여부 (y/n)
|
||||
otlrYnCd?: string; // 이상치 여부 (0: 정상, 1: 이상치, 2: 제외)
|
||||
}
|
||||
|
||||
export type StAnlsRsltListApiResponse = PageableList<StAnlsRsltResponse>;
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
import type { StAnlsMstResponse } from "./analysis-history-interface";
|
||||
import type { StAnlsMstResponse } from "./statistics-analysis-master-interface";
|
||||
|
||||
const columnHelper = createColumnHelper<StAnlsMstResponse>();
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Checkbox } from "@common/components";
|
||||
|
||||
import type { StAnlsDtlResponse } from "./statistics-analysis-detail-interface";
|
||||
|
||||
interface GetVerificationListColumnsParams {
|
||||
rowData: StAnlsDtlResponse[];
|
||||
checkedSns: string[];
|
||||
onCheck: (checkedSns: string[], rows: StAnlsDtlResponse[]) => void;
|
||||
}
|
||||
|
||||
export const getVerificationListColumns = ({
|
||||
rowData,
|
||||
checkedSns,
|
||||
onCheck,
|
||||
}: GetVerificationListColumnsParams): ColumnDef<StAnlsDtlResponse>[] => [
|
||||
{
|
||||
id: "checkbox",
|
||||
header: () => {
|
||||
const isAllChecked =
|
||||
rowData.length > 0 && checkedSns.length === rowData.length;
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isAllChecked}
|
||||
onCheckedChange={(value: boolean) => {
|
||||
if (value) {
|
||||
onCheck(
|
||||
rowData.map((r) => String(r.vrfcTrgtSn)),
|
||||
rowData,
|
||||
);
|
||||
} else {
|
||||
onCheck([], []);
|
||||
}
|
||||
}}
|
||||
aria-label="Select all rows"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const rowId = String(row.original.vrfcTrgtSn);
|
||||
const isChecked = checkedSns.includes(rowId);
|
||||
return (
|
||||
<div className="flex items-center justify-center">
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheckedChange={(value: boolean) => {
|
||||
const newCheckedSns = value
|
||||
? [...checkedSns, rowId]
|
||||
: checkedSns.filter((id) => id !== rowId);
|
||||
const newCheckedRows = rowData.filter((r) =>
|
||||
newCheckedSns.includes(String(r.vrfcTrgtSn)),
|
||||
);
|
||||
onCheck(newCheckedSns, newCheckedRows);
|
||||
}}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
aria-label={`Select row ${rowId}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
header: "통계 년도",
|
||||
accessorKey: "statsYr",
|
||||
},
|
||||
{
|
||||
header: "통계 항목",
|
||||
accessorKey: "statsNm",
|
||||
},
|
||||
{
|
||||
header: "검증 항목",
|
||||
accessorKey: "vrfcTrgtNm",
|
||||
},
|
||||
{
|
||||
header: "μ",
|
||||
accessorKey: "ipcrCntVl",
|
||||
},
|
||||
{
|
||||
header: "Percentile",
|
||||
accessorKey: "pctlCrtrVl",
|
||||
},
|
||||
{
|
||||
header: "A",
|
||||
accessorKey: "scopAjmtCntVl",
|
||||
},
|
||||
{
|
||||
header: "C",
|
||||
accessorKey: "thrsCntVl",
|
||||
},
|
||||
{
|
||||
header: "정상건수",
|
||||
accessorKey: "nmlDataNocs",
|
||||
},
|
||||
{
|
||||
header: "제외건수",
|
||||
accessorKey: "exclDataNocs",
|
||||
},
|
||||
{
|
||||
header: "이상건수",
|
||||
accessorKey: "otlrDataNocs",
|
||||
},
|
||||
];
|
||||
@@ -1,150 +0,0 @@
|
||||
import { fakerKO as faker } from "@faker-js/faker";
|
||||
import type {
|
||||
HBArticleItem,
|
||||
IMRArticleItem,
|
||||
ValidationArticleItem,
|
||||
} from "./verification-article-interface";
|
||||
|
||||
const HB_STATS_ITEMS = [
|
||||
"하수도 보급률",
|
||||
"하수관로",
|
||||
"하수처리시설 및 방류량보고",
|
||||
"펌프장",
|
||||
"공공하수처리시설",
|
||||
"분뇨처리시설",
|
||||
"개인하수처리시설",
|
||||
"하수도재정",
|
||||
"물 재이용",
|
||||
];
|
||||
|
||||
const HB_VERIFICATION_ITEMS = [
|
||||
"행정구역 보급률",
|
||||
"유역환경청별 보급률",
|
||||
"관로연장",
|
||||
"처리용량",
|
||||
"방류량",
|
||||
"펌프용량",
|
||||
];
|
||||
|
||||
const IMR_STATS_ITEMS = [
|
||||
"하수도 보급률",
|
||||
"하수관로",
|
||||
"하수처리시설 및 방류량보고",
|
||||
"펌프장",
|
||||
"공공하수처리시설",
|
||||
"분뇨처리시설",
|
||||
"개인하수처리시설",
|
||||
"하수도재정",
|
||||
"물 재이용",
|
||||
];
|
||||
|
||||
const VALIDATION_STATS_ITEMS = [
|
||||
"하수관로 유지관리",
|
||||
"하수저류시설 및 빗물펌프장",
|
||||
"공공하수처리시설",
|
||||
"분뇨처리시설",
|
||||
"개인하수처리시설",
|
||||
"하수도재정",
|
||||
];
|
||||
|
||||
const VERIFICATION_NAMES = [
|
||||
"제정별 사업비",
|
||||
"관로길이",
|
||||
"공란검증",
|
||||
"범위검증",
|
||||
"논리검증",
|
||||
"유지보수",
|
||||
];
|
||||
|
||||
const TABLE_NAMES = [
|
||||
"tb_stats_drng_sys",
|
||||
"tb_stats_ppline",
|
||||
"tb_stats_fclt",
|
||||
"tb_stats_pump_stt",
|
||||
"tb_stats_sewg_trtm",
|
||||
];
|
||||
|
||||
const COLUMN_NAMES = [
|
||||
"drng_sys_rate",
|
||||
"ppline_len",
|
||||
"fclt_cpct",
|
||||
"pump_cpct",
|
||||
"trtm_qty",
|
||||
"popl_cnt",
|
||||
];
|
||||
|
||||
const BASIC_RULES = [
|
||||
"값 > 0",
|
||||
"값 >= 0",
|
||||
"값 != NULL",
|
||||
"범위: 0-100",
|
||||
"A > B",
|
||||
];
|
||||
|
||||
function formatDateTime(date: Date): string {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
const h = String(date.getHours()).padStart(2, "0");
|
||||
const min = String(date.getMinutes()).padStart(2, "0");
|
||||
return `${y}-${m}-${d} ${h}:${min}`;
|
||||
}
|
||||
|
||||
/** H-B 탭 목데이터 생성 */
|
||||
export function createHBArticleMock(count = 10): HBArticleItem[] {
|
||||
faker.seed(100);
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
statsItmNm: faker.helpers.arrayElement(HB_STATS_ITEMS),
|
||||
vrfcItmNm: faker.helpers.arrayElement(HB_VERIFICATION_ITEMS),
|
||||
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
|
||||
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
|
||||
percentileVal: faker.number.float({
|
||||
min: 0,
|
||||
max: 100,
|
||||
fractionDigits: 2,
|
||||
}),
|
||||
aVal: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
|
||||
cVal: faker.number.float({ min: 0, max: 10, fractionDigits: 3 }),
|
||||
regNm: faker.person.fullName(),
|
||||
regDt: formatDateTime(faker.date.recent({ days: 90 })),
|
||||
}));
|
||||
}
|
||||
|
||||
/** I-MR 탭 목데이터 생성 */
|
||||
export function createIMRArticleMock(count = 10): IMRArticleItem[] {
|
||||
faker.seed(200);
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
statsItmNm: faker.helpers.arrayElement(IMR_STATS_ITEMS),
|
||||
vrfcNm: faker.helpers.arrayElement(VERIFICATION_NAMES),
|
||||
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
|
||||
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
|
||||
regNm: faker.person.fullName(),
|
||||
regDt: formatDateTime(faker.date.recent({ days: 90 })),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Validation 탭 목데이터 생성 */
|
||||
export function createValidationArticleMock(
|
||||
count = 6,
|
||||
): ValidationArticleItem[] {
|
||||
faker.seed(300);
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
no: `${index + 1}`,
|
||||
statsItmNm: faker.helpers.arrayElement(VALIDATION_STATS_ITEMS),
|
||||
vrfcNm: faker.helpers.arrayElement(VERIFICATION_NAMES),
|
||||
tblNm: faker.helpers.arrayElement(TABLE_NAMES),
|
||||
colNm: faker.helpers.arrayElement(COLUMN_NAMES),
|
||||
cmpTblNm: faker.helpers.arrayElement(TABLE_NAMES),
|
||||
cmpColNm: faker.helpers.arrayElement(COLUMN_NAMES),
|
||||
bscRul: faker.helpers.arrayElement(BASIC_RULES),
|
||||
}));
|
||||
}
|
||||
|
||||
/** H-B 탭 기본 목데이터 */
|
||||
export const hbArticleMockData = createHBArticleMock(10);
|
||||
|
||||
/** I-MR 탭 기본 목데이터 */
|
||||
export const imrArticleMockData = createIMRArticleMock(10);
|
||||
|
||||
/** Validation 탭 기본 목데이터 */
|
||||
export const validationArticleMockData = createValidationArticleMock(6);
|
||||
@@ -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",
|
||||
@@ -78,6 +79,7 @@ export const ENDPOINT = {
|
||||
ST_ANLS_MST: "/statverify-service/api/v1/st-anls-mst",
|
||||
ST_ANLS_DTL: "/statverify-service/api/v1/st-anls-dtl",
|
||||
ST_ANLS_RSLT: "/statverify-service/api/v1/st-anls-rslt",
|
||||
IMR: "/statverify-service/api/v1/imr/outlier/detect",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user