Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev
This commit is contained in:
@@ -5,7 +5,7 @@ import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
@@ -33,14 +33,14 @@ import { doList } from "../util/column/input/DoList";
|
||||
const getSidoListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList({ instSeCd: key[1] });
|
||||
return statisticsApi.getSidoList({ instSeCd: key[1] });
|
||||
};
|
||||
|
||||
const getSggListFetcher = async (
|
||||
key: [string, string, string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
|
||||
return statisticsAPi.getSggAllList(key[1], {
|
||||
return statisticsApi.getSggAllList(key[1], {
|
||||
instSeCd: key[2],
|
||||
gugunSkipYn,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import CompactDataGrid from "./CompactDataGrid";
|
||||
import SaveButton from "../button/SaveButton";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
@@ -91,13 +91,13 @@ const adminRegionColumnInfo: ColumnInfo = {
|
||||
};
|
||||
|
||||
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
|
||||
return statisticsAPi.getSidoList();
|
||||
return statisticsApi.getSidoList();
|
||||
};
|
||||
|
||||
const getSggAllListFetcher = async (
|
||||
key: [string, string],
|
||||
): Promise<ApiResponse<Sgg[]>> => {
|
||||
return statisticsAPi.getSggAllList(key[1]);
|
||||
return statisticsApi.getSggAllList(key[1]);
|
||||
};
|
||||
|
||||
type SidoSggFacilityNameSearchFormProps = {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
|
||||
export interface VerificationResultDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function VerificationResultDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: VerificationResultDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[640px]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>검증 결과</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="min-h-[120px]" />
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
type VerificationResultGridRow = Record<string, string | number>;
|
||||
|
||||
const PRCS_YN_LABEL: Record<string, string> = {
|
||||
"0": "미통과",
|
||||
"1": "통과",
|
||||
"2": "예외처리",
|
||||
"3": "반려",
|
||||
};
|
||||
|
||||
const verificationResultColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
],
|
||||
columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt"],
|
||||
};
|
||||
|
||||
export interface VerificationResultHistoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
queryContext?: {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
} | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
export function VerificationResultHistoryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
queryContext,
|
||||
title = "검증 결과 내역",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
}: VerificationResultHistoryDialogProps) {
|
||||
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
const fetchVerificationResultPage = useCallback(
|
||||
async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
|
||||
if (!queryContext) return null;
|
||||
const param: VerificationResultListRequest = {
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
ctpvNm: queryContext.ctpvNm,
|
||||
sggNm: queryContext.sggNm,
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
pbcpAprvYn: 'N',
|
||||
vrfcMthdCd,
|
||||
prcsYnList: "0,2,3",
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
return statisticsApi.getVerificationResultList(param);
|
||||
},
|
||||
[queryContext],
|
||||
);
|
||||
|
||||
const loadInitial = useCallback(async () => {
|
||||
if (!queryContext) {
|
||||
setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes] = await Promise.all([
|
||||
fetchVerificationResultPage("NV", 0),
|
||||
fetchVerificationResultPage("IMR", 0),
|
||||
]);
|
||||
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
|
||||
setImrEntries(imrRes?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
}
|
||||
}, [fetchVerificationResultPage, queryContext]);
|
||||
|
||||
const handleNormalPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === normalPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("NV", pageIndex);
|
||||
setNormalEntries(response?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setNormalPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, normalPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleImrPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === imrPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setImrLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("IMR", pageIndex);
|
||||
setImrEntries(response?.data?.verifyResultList ?? []);
|
||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setImrLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, imrPageIndex, queryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
void loadInitial();
|
||||
}, [loadInitial, open, resetState]);
|
||||
|
||||
const mapRows = (entries: VerificationResultReportItem[]): VerificationResultGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn,
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
}));
|
||||
|
||||
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
|
||||
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
|
||||
|
||||
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
|
||||
SetStateAction<Record<string, string | number>[]>
|
||||
>;
|
||||
|
||||
const renderGrid = (
|
||||
rows: VerificationResultGridRow[],
|
||||
pageIndex: number,
|
||||
pageCount: number,
|
||||
onPageIndexChange: (page: number) => void,
|
||||
loading: boolean,
|
||||
) => {
|
||||
if (!loading && rows.length === 0) {
|
||||
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-2">
|
||||
<DataGrid
|
||||
columnInfo={verificationResultColumnInfo}
|
||||
rowData={rows}
|
||||
setRowData={noopSetRows}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
/>
|
||||
<VerificationResultHistoryPagination
|
||||
pageCount={Math.max(pageCount, 1)}
|
||||
pageIndex={Math.max(pageIndex, 0)}
|
||||
onPageIndexChange={onPageIndexChange}
|
||||
className="mx-2 mb-2"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 rounded-md [data-theme='black']:bg-transparent bg-white/40 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
||||
{errorMessage && (
|
||||
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
|
||||
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
|
||||
</section>
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
|
||||
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
|
||||
</section>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DoubleArrowLeftIcon,
|
||||
DoubleArrowRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button, cn } from "@common/components";
|
||||
|
||||
export type VerificationResultHistoryPaginationProps = {
|
||||
pageCount: number;
|
||||
pageIndex: number;
|
||||
onPageIndexChange: (pageIndex: number) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function VerificationResultHistoryPagination({
|
||||
pageCount,
|
||||
pageIndex: currentPage,
|
||||
onPageIndexChange,
|
||||
className,
|
||||
}: VerificationResultHistoryPaginationProps) {
|
||||
const maxVisiblePages = 10;
|
||||
let startPage = Math.max(0, currentPage - maxVisiblePages / 2);
|
||||
const endPage = Math.min(pageCount, startPage + maxVisiblePages);
|
||||
|
||||
if (endPage - startPage < maxVisiblePages) {
|
||||
startPage = Math.max(0, endPage - maxVisiblePages);
|
||||
}
|
||||
|
||||
const pageNumbers = Array(endPage - startPage)
|
||||
.fill(0)
|
||||
.map((_, i) => startPage + i);
|
||||
|
||||
const canPrevious = currentPage > 0;
|
||||
const canNext = pageCount > 0 && currentPage < pageCount - 1;
|
||||
|
||||
return (
|
||||
<div className={cn("mt-2 flex items-center justify-center gap-1 rounded-md border border-border/70 bg-muted/20 px-2 py-2", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-8 p-0 lg:flex"
|
||||
onClick={() => onPageIndexChange(0)}
|
||||
disabled={!canPrevious}
|
||||
>
|
||||
<span className="sr-only">첫 페이지로</span>
|
||||
<DoubleArrowLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-8 p-0"
|
||||
onClick={() => onPageIndexChange(currentPage - 1)}
|
||||
disabled={!canPrevious}
|
||||
>
|
||||
<span className="sr-only">이전 페이지로</span>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
</Button>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{pageNumbers.map((pageNumber) => (
|
||||
<Button
|
||||
key={pageNumber}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"inline-block h-8 min-w-8 border border-transparent !bg-transparent px-2 py-1 text-foreground hover:!bg-accent",
|
||||
currentPage === pageNumber &&
|
||||
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
|
||||
)}
|
||||
onClick={() => onPageIndexChange(pageNumber)}
|
||||
>
|
||||
{pageNumber + 1}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="size-8 p-0"
|
||||
onClick={() => onPageIndexChange(currentPage + 1)}
|
||||
disabled={!canNext}
|
||||
>
|
||||
<span className="sr-only">다음 페이지로</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="white"
|
||||
className="hidden size-8 p-0 lg:flex"
|
||||
onClick={() => onPageIndexChange(Math.max(0, pageCount - 1))}
|
||||
disabled={!canNext}
|
||||
>
|
||||
<span className="sr-only">마지막 페이지로</span>
|
||||
<DoubleArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user