Merge remote-tracking branch 'origin/iipbs/st/dev'

This commit is contained in:
2026-03-20 17:30:40 +09:00
8 changed files with 219 additions and 130 deletions
@@ -99,7 +99,7 @@ const ImrVerificationHistory = () => {
// Area A Pagination
const [historyPagination, setHistoryPagination] = useState({
pageIndex: 0,
pageSize: 10,
pageSize: 5,
});
// Codes for Filter
@@ -178,20 +178,21 @@ const ImrVerificationHistory = () => {
...item,
statsNm: selectedHistory?.vrfcTrgtNm ?? "-",
}));
setGridRowData(mapped as unknown as Record<string, string | number>[]);
setGridRowData(mapped as Record<string, string | number>[]);
}, [detailData, selectedHistory]);
// Area C Data Fetching
const { data: chartData, isLoading: isChartLoading } = useQuery({
queryKey: [
"imr-log",
ENDPOINT.STATVERIFY_SERVICE.IMR,
selectedHistory?.anlsSn,
selectedDetail?.vrfcTrgtSn,
selectedDetail?.admdstCd,
],
queryFn: () =>
imrApi.getOutliersLog({
anlsSn: selectedHistory!.anlsSn,
jobUuid: selectedHistory!.jobUuid,
anlsSn: selectedDetail!.anlsSn,
vrfcTrgtSn: selectedDetail!.vrfcTrgtSn,
admdstCd: selectedDetail!.admdstCd,
}),
@@ -244,9 +245,7 @@ const ImrVerificationHistory = () => {
},
title: {
display: true,
text: selectedDetail
? `${selectedDetail.fcltNm} - I-MR 분석 차트`
: "I-MR 분석 차트",
text: "I-MR 잔차 관리도",
font: {
size: 16,
weight: "bold",
@@ -435,7 +434,7 @@ const ImrVerificationHistory = () => {
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{/* 영역 B: 상세 데이터 그리드 */}
<div className="card card-border bg-white p-6">
<div className="card card-border bg-white p-4">
<div className="flex items-center justify-between">
<h2 className="pl-2 text-[1.6rem] font-bold text-title">
이상치 데이터
@@ -457,7 +456,7 @@ const ImrVerificationHistory = () => {
다운로드
</Button>
</div>
<div className="mt-2 h-[45rem]">
<div className="mt-2">
{isDetailLoading ? (
<div className="flex h-full w-full items-center justify-center">
<div className="relative size-[5rem]">
@@ -480,11 +479,11 @@ const ImrVerificationHistory = () => {
isExcelImport={false}
highlightedRowId={selectedDetail?.rsltSn}
onRowClick={(e) => {
if (!e.rowId) return;
if (!e.anlsSn) return;
const row = detailData?.content?.find(
(item) =>
String(item.rsltSn) ===
String(e.rowId),
String(e.rsltSn),
);
if (row) setSelectedDetail(row);
}}
@@ -494,23 +493,18 @@ const ImrVerificationHistory = () => {
</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 className="m-2 flex h-[45rem] items-center justify-center rounded-sm border bg-white p-4">
{isChartLoading ? (
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
) : lineChartData ? (
<Line options={chartOptions} data={lineChartData} />
) : (
<div className="text-[1.4rem]">
이상치 데이터를 선택하면 차트가 표시됩니다.
</div>
)}
</div>
</div>
</div>
@@ -19,6 +19,11 @@ import type {
} from "@/entities/statistics/model/verification-target-management-interface";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Label,
Tabs,
@@ -58,6 +63,8 @@ function VerificationArticleManagement() {
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedData, setSelectedData] =
useState<VerificationTargetManagementResponse | null>(null);
const [resultDialogOpen, setResultDialogOpen] = useState(false);
const [resultDialogMessage, setResultDialogMessage] = useState("");
const { data: verificationData } = useQuery({
queryKey: [ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG, searchParams],
@@ -68,6 +75,7 @@ function VerificationArticleManagement() {
vrfcMthdCd: searchParams.vrfcMthdCd,
statsCd: searchParams.statsCd || undefined,
vrfcTrgtNm: searchParams.vrfcTrgtNm || undefined,
sort: ["statsCd,asc", "vrfcTrgtNm,asc"],
},
);
return response;
@@ -172,6 +180,23 @@ function VerificationArticleManagement() {
},
});
const checkColumnsMutation = useMutation({
mutationFn: () =>
verificationTargetManagementApi.checkColumnsExist({
vrfcMthdCd: activeTab,
}),
onSuccess: (response) => {
setResultDialogMessage(
response.message || "컬럼 검사가 완료되었습니다.",
);
setResultDialogOpen(true);
},
onError: () => {
setResultDialogMessage("컬럼 검사 중 오류가 발생했습니다.");
setResultDialogOpen(true);
},
});
function handleRowClick(rowData: Record<string, string | number>) {
setSelectedData(
rowData as unknown as VerificationTargetManagementResponse,
@@ -205,101 +230,137 @@ function VerificationArticleManagement() {
}
}
async function handleCheckColumns() {
if (!(await confirm("컬럼 검사를 실행하시겠습니까?"))) return;
checkColumnsMutation.mutate();
}
return (
<Tabs
defaultValue="HB"
value={activeTab}
onValueChange={handleTabChange}
>
<TabsList>
<>
<Tabs
defaultValue="HB"
value={activeTab}
onValueChange={handleTabChange}
>
<TabsList>
{Object.entries(TAB_CONFIG).map(([key, config]) => (
<TabsTrigger
key={key}
value={config.value}
className="!px-[2rem] !py-[0.8rem] !text-[1.2rem]"
>
{config.label}
</TabsTrigger>
))}
</TabsList>
<div className="card card-border !mx-0">
<form onSubmit={handleSubmit}>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap px-4 text-label">
통계항목
</Label>
<CodeSelect
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
placeholder="선택"
className="w-[16rem]"
/>
</div>
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap px-4 text-label">
검증명
</Label>
<Input
value={vrfcTrgtNm}
onChange={(e) =>
setVrfcTrgtNm(e.target.value)
}
placeholder="검증명"
className="w-[20rem]"
/>
</div>
<div className="ml-auto flex gap-2">
<Button
type="button"
color="white"
className="h-[3.6rem]"
onClick={handleCheckColumns}
disabled={checkColumnsMutation.isPending}
>
컬럼 검사
</Button>
<Button
type="button"
className="h-[3.6rem]"
onClick={handleRegister}
>
등록
</Button>
<Button
type="submit"
color="green"
className="h-[3.6rem]"
>
조회
</Button>
<Button
type="button"
color="gray"
className="h-[3.6rem]"
onClick={handleInitialize}
>
초기화
</Button>
</div>
</div>
</form>
</div>
{Object.entries(TAB_CONFIG).map(([key, config]) => (
<TabsTrigger
key={key}
value={config.value}
className="!px-[2rem] !py-[0.8rem] !text-[1.2rem]"
>
{config.label}
</TabsTrigger>
<TabsContent key={key} value={config.value}>
<DataGrid
columnInfo={columnInfo}
rowData={rowData}
setRowData={() => {}}
onRowClick={handleRowClick}
isExcelImport={false}
/>
</TabsContent>
))}
</TabsList>
<div className="card card-border !mx-0">
<form onSubmit={handleSubmit}>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap px-4 text-label">
통계항목
</Label>
<CodeSelect
upCd="STITEM"
value={statsCd}
onValueChange={setStatsCd}
placeholder="선택"
className="w-[16rem]"
/>
</div>
<div className="flex items-center gap-4">
<Label className="whitespace-nowrap px-4 text-label">
검증명
</Label>
<Input
value={vrfcTrgtNm}
onChange={(e) => setVrfcTrgtNm(e.target.value)}
placeholder="검증명"
className="w-[20rem]"
/>
</div>
<div className="ml-auto flex gap-2">
<Button
type="button"
className="h-[3.6rem] w-[80px]"
onClick={handleRegister}
>
등록
</Button>
<Button
type="submit"
color="green"
className="h-[3.6rem] w-[80px]"
>
조회
</Button>
<Button
type="button"
color="gray"
className="h-[3.6rem] w-[80px]"
onClick={handleInitialize}
>
초기화
</Button>
</div>
<VerificationTargetDialog
tabValue={activeTab}
open={dialogOpen}
onOpenChange={setDialogOpen}
data={selectedData}
onSave={handleSave}
onDelete={handleDelete}
/>
</Tabs>
<Dialog open={resultDialogOpen} onOpenChange={setResultDialogOpen}>
<DialogContent
className="max-w-[60rem]"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>컬럼 검사 결과</DialogTitle>
</DialogHeader>
<div className="max-h-[50rem] overflow-y-auto whitespace-pre-wrap break-words text-[1.4rem]">
{resultDialogMessage}
</div>
</form>
</div>
{Object.entries(TAB_CONFIG).map(([key, config]) => (
<TabsContent key={key} value={config.value}>
<DataGrid
columnInfo={columnInfo}
rowData={rowData}
setRowData={() => {}}
onRowClick={handleRowClick}
isExcelImport={false}
/>
</TabsContent>
))}
<VerificationTargetDialog
tabValue={activeTab}
open={dialogOpen}
onOpenChange={setDialogOpen}
data={selectedData}
onSave={handleSave}
onDelete={handleDelete}
/>
</Tabs>
<DialogFooter>
<Button onClick={() => setResultDialogOpen(false)}>
확인
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
@@ -3,6 +3,8 @@ import { ENDPOINT } from "@/shared/config";
import type {
VerificationTargetCodeResponse,
VerificationTargetManagementCheckColumnsParams,
VerificationTargetManagementCheckColumnsResponse,
VerificationTargetManagementService,
VerificationTargetManagementSearchParams,
VerificationTargetManagementListApiResponse,
@@ -26,7 +28,12 @@ export class VerificationTargetManagementApiService implements VerificationTarge
const response =
await client.get<VerificationTargetManagementListApiResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG}`,
{ params },
{
params,
paramsSerializer: {
indexes: null,
},
},
);
return response.data;
}
@@ -45,6 +52,21 @@ export class VerificationTargetManagementApiService implements VerificationTarge
return response.data;
}
/**
* 검증 대상 컬럼 검사
* @param params 검사 조건 (vrfcMthdCd)
*/
public async checkColumnsExist(
params: VerificationTargetManagementCheckColumnsParams,
): Promise<VerificationTargetManagementCheckColumnsResponse> {
const response =
await client.get<VerificationTargetManagementCheckColumnsResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.VRFCTGT_MNG}/check-columns`,
{ params },
);
return response.data;
}
/**
* 테이블 메타데이터 조회
* @param statsCd 통계항목 코드
@@ -54,8 +54,8 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
width: 80,
},
{
name: "statsNm",
fieldName: "statsNm",
name: "vrfcTrgtNm",
fieldName: "vrfcTrgtNm",
header: { text: "항목명" },
width: 150,
},
@@ -78,7 +78,7 @@ export const imrDetailGridColumnInfo: ColumnInfo = {
"addr",
"pipeSeCd",
"vrfcTrgtSn",
"statsNm",
"vrfcTrgtNm",
"cryrVl",
],
};
@@ -6,8 +6,8 @@ export interface StAnlsRsltResponse {
rsltSn: number; // 결과 일련번호
anlsSn: number; // 분석 일련번호
vrfcTrgtSn: number; // 검증 대상 일련번호
vrfcTrgtNm: string; // 검증 대상명(항목명)
admdstCd: string; // 행정구역 코드
admdstNm: string; // 행정구역 명칭
sido: string; // 시도
gugun: string; // 시군구
adstDong: string; // 행정동
@@ -1,4 +1,4 @@
import type { PageableList } from "@/shared/model";
import type { PageableList, PageableParams } from "@/shared/model";
export interface NumericColumnDto {
columnName: string;
@@ -46,16 +46,24 @@ export interface VerificationTargetManagementResponse {
}
// 검증 대상 관리 검색 DTO
export interface VerificationTargetManagementSearchParams {
export interface VerificationTargetManagementSearchParams extends PageableParams {
vrfcMthdCd?: string; // 검증 방법 코드
statsCd?: string; // 통계 코드
tblNm?: string; // 테이블 명
colNm?: string; // 컬럼 명
vrfcTrgtNm?: string; // 검증대상명
useYn?: string; // 사용 여부
page?: number; // 페이지 번호 (0부터 시작)
size?: number; // 페이지 크기
sort?: string; // 정렬 조건 (예: "vrfcTrgtSn,desc")
}
// 검증 대상 컬럼 검사 검색 DTO
export interface VerificationTargetManagementCheckColumnsParams {
vrfcMthdCd: string; // 검증 방법 코드 (HB, IMR, VAL 등)
}
// 검증 대상 컬럼 검사 응답 DTO
export interface VerificationTargetManagementCheckColumnsResponse {
code: number; // 응답 코드 (정상: 0, 에러: -1)
message: string; // 응답 메시지
}
// 검증 대상 관리 저장 요청 DTO
@@ -109,6 +117,10 @@ export interface VerificationTargetManagementService {
vrfcTrgtSn: number,
): Promise<VerificationTargetManagementDetailApiResponse>;
checkColumnsExist(
params: VerificationTargetManagementCheckColumnsParams,
): Promise<VerificationTargetManagementCheckColumnsResponse>;
getVerificationTargetCodes(): Promise<VerificationTargetCodeResponse[]>;
saveVerificationTargetManagement(
+1 -1
View File
@@ -30,7 +30,7 @@ export interface PageableList<T> {
export interface PageableParams {
page?: number;
size?: number;
sort?: string;
sort?: string | string[];
}
export interface ApiErrorResponse {
+1 -1
View File
@@ -25,7 +25,7 @@
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@common/components": ["../../../packages/common/components/src/"],
"@common/components": ["../../../packages/common/components/dist/"],
"@common/config": ["../../../packages/common/config/dist/"],
"@common/network": ["../../../packages/common/network/"],
"@rsone/components": ["../../../packages/rsone/dist/"]