This commit is contained in:
2026-04-21 18:09:43 +09:00
32 changed files with 1622 additions and 684 deletions
@@ -12,6 +12,7 @@ import { AlertDialog } from "@/shared/ui/alert-dialog";
import { PromptDialog } from "@/shared/ui/prompt-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
import { PromptProvider, StoreProvider } from "@/shared/providers";
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
@@ -36,6 +37,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
<PromptDialog />
<ConfirmDialog />
<SelectPromptDialog />
<GlobalProgressOverlay />
<Toaster position="top-center" />
</AuthWrapper>
</ConfirmProvider>
@@ -0,0 +1,16 @@
import { Button } from "@common/components";
interface VerificationResultButtonProps {
VerificationResultButtonFunc: () => void;
}
const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => {
return (
<Button color="white" className="h-[3.2rem]" onClick={VerificationResultButtonFunc}>
<i className="diveicon di-list" />
<span>지난 검증 결과</span>
</Button>
);
};
export default VerificationResultButton;
+6 -6
View File
@@ -16,12 +16,6 @@
@apply !bg-[#5987bf] !text-[#fff];
}
.rg-grid .rg-header .rg-table tr td,
.rg-grid .rg-head .rg-table tr td {
border: none;
border-bottom: 1px solid #e5e7eb;
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
@apply !bg-white;
}
@@ -138,4 +132,10 @@
.rg-grid .rg-body .rg-table tr.rg-alternate-row td,
.rg-grid .rg-body .rg-table tr.rg-alternate-row .rg-data-cell {
background-color: #ffffff !important;
}
.rg-grid .rg-header .rg-table tr td,
.rg-grid .rg-head .rg-table tr td {
border: none;
border-bottom: 1px solid #e5e7eb;
}
+33 -3
View File
@@ -319,8 +319,10 @@ const DataGrid = ({
rowData,
setRowData,
getGridDataRef,
getGridRef,
cellStyleCallback,
cellTooltipCallback,
onCellEdited,
onRowClick,
onItemChecked,
onItemAllChecked,
@@ -345,6 +347,7 @@ const DataGrid = ({
const cellTooltipCallbackRef = useRef<CellTooltipCallback | undefined>(
cellTooltipCallback,
);
const onCellEditedRef = useRef<typeof onCellEdited>(onCellEdited);
const onScrollToBottomRef = useRef<(() => void) | undefined>(
onScrollToBottom,
);
@@ -357,6 +360,7 @@ const DataGrid = ({
onScrollToBottomRef.current = onScrollToBottom;
onItemCheckedRef.current = onItemChecked;
onItemAllCheckedRef.current = onItemAllChecked;
onCellEditedRef.current = onCellEdited;
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
useEffect(() => {
@@ -1149,6 +1153,33 @@ const DataGrid = ({
};
}, [onItemChecked, onItemAllChecked]);
useEffect(() => {
const gridView = gridRef.current?.gridView;
if (!gridView) return;
gridView.onCellEdited = (grid, itemIndex, _row, field) => {
const fieldName = columns[field]?.fieldName;
if (!fieldName) return;
const newValue = grid.getValue(itemIndex, fieldName);
const prevRow = rowDataRef.current[itemIndex];
const oldValue = prevRow?.[fieldName] ?? "";
onCellEditedRef.current?.({
itemIndex,
fieldName,
oldValue,
newValue,
});
};
return () => {
if (gridView.onCellEdited) {
gridView.onCellEdited = undefined;
}
};
}, [columns, setRowData]);
return (
<div className="mx-[0.6rem] flex flex-col gap-[0.6rem]">
<input
@@ -1223,7 +1254,6 @@ const DataGrid = ({
ref={gridRef}
autoGenerateField={true}
layout={columnLayout}
rows={rowData}
onInitialized={(grid) => {
if (!grid) return;
(
@@ -1259,9 +1289,9 @@ const DataGrid = ({
buildFieldsFromColumns(columns),
);
}
if (dataProvider?.setRows && rowData?.length) {
if (dataProvider?.setRows)
dataProvider.setRows(rowData);
}
if (getGridRef) getGridRef.current = gridRef.current;
const gridView = gridRef.current?.gridView ?? grid;
if (!gridView?.columnByName) return;
setTimeout(() => {
+3 -3
View File
@@ -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>
);
}
@@ -6,16 +6,18 @@ import DataGrid from "@/components/module/DataGrid";
import SubmitButton from "@/components/button/SumitButton";
import SaveButton from "@/components/button/SaveButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { LoadingWithAnimation } from "@common/components";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import SubmitDailog from "./SubmitDailog";
import { useAlertStore } from "@/shared/lib";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
import { useAdminStore } from "@/store/admin-store";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -30,7 +32,11 @@ import type {
SewerageSupplyRateSaveRequest,
SewerageSupplyRateStatusInfo,
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { cancelSubmitRequest, SubmitExceptionRequest } from "@/entities/statistics/model/statistics-interface";
import type {
cancelSubmitRequest,
ImrStatusResponse,
SubmitExceptionRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { doList } from "@/components/util/column/input/DoList";
@@ -45,6 +51,32 @@ const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResu
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
};
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
/** IMR 상태가 COMPLETED 이고 job_prog 가 100일 때까지 폴링 */
async function waitForImrJobCompleted(
jobId: string,
getStatus: (id: string) => Promise<ImrStatusResponse>,
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<void> {
const pollIntervalMs = options?.pollIntervalMs ?? 2000;
const maxAttempts = options?.maxAttempts ?? 150;
for (let i = 0; i < maxAttempts; i++) {
const res = await getStatus(jobId);
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0)));
if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`);
}
await sleep(pollIntervalMs);
}
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다.");
}
function getSewerageSupplyRateParams(
values: SearchFormValues,
overrides?: { yr?: string; demoYr?: string; mode?: string }
@@ -69,11 +101,17 @@ const SeweragePenetrationRateInp = () => {
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [statusInfo, setStatusInfo] = useState<SewerageSupplyRateStatusInfo | null>(null);
const [showDailog, setShowDailog] = useState<boolean>(false);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
const { confirm } = useConfirm();
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
const { isAdminInfo } = useAdminStore();
@@ -88,6 +126,7 @@ const SeweragePenetrationRateInp = () => {
});
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요.
const columnInfo = useMemo(() => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
@@ -212,7 +251,7 @@ const SeweragePenetrationRateInp = () => {
try {
setIsLoading(true);
const response = await statisticsAPi.submitException(params);
const response = await statisticsApi.submitException(params);
if (response.status === 'success') {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
@@ -271,7 +310,7 @@ const SeweragePenetrationRateInp = () => {
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
try {
setIsLoading(true);
showGlobalProgress({ label: "제출 진행 중...", progress: 0 });
const userInfo = await getUserInfo();
const currentData = getGridDataRef.current?.();
@@ -348,39 +387,62 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData);
if (response.status === 'success') {
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
stats_cd: statsCd,
stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
};
setGlobalProgress(50);
if (response.data.finalStatus === '5') {
const imrParam = {
user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
stats_cd: statsCd,
stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
gen_verify_stts_cd: response.data.nvpassed ? '1' : '2',
};
try {
await statisticsAPi.callImrService(imrParam);
} catch {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
setIsLoading(false);
alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
return;
try {
const imrResponse = await statisticsApi.callImrService(imrParam);
if (imrResponse.job_id) {
await waitForImrJobCompleted(imrResponse.job_id, (id) =>
statisticsApi.getImrStatus(id),
{
onProgress: (jobProgress) => {
setGlobalProgress(50 + Math.round(jobProgress / 2));
},
},
);
}
setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
setVerificationResultDialogOpen(true);
} catch (e) {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
setIsLoading(false);
const msg =
e instanceof Error && e.message
? e.message
: 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.';
alert(msg);
return;
}
} else {
setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
alert('제출되었습니다.');
}
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false);
alert('제출되었습니다.');
} else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
}
} catch {
alert('데이터 제출 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
hideGlobalProgress();
}
};
const saveFunc = async () => {
try {
if (await confirm('저장 하시겠습니까?')) {
setIsLoading(true);
showGlobalProgress({ label: "저장 진행 중...", progress: 0 });
const userInfo = await getUserInfo();
const currentData = getGridDataRef.current?.();
@@ -465,23 +527,8 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData);
if (response.status === 'success') {
// const imrParam = {
// user_id: userInfo.userUnqId,
// sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
// stats_cd: statsCd,
// stats_yr: searchValues?.year ?? '',
// lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'),
// };
// try {
// await statisticsAPi.callImrService(imrParam);
// } catch {
// await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
// setIsLoading(false);
// alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.');
// return;
// }
setGlobalProgress(50);
setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
alert('저장되었습니다.');
} else {
@@ -493,7 +540,7 @@ const SeweragePenetrationRateInp = () => {
} catch {
alert('데이터 저장 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
hideGlobalProgress();
}
};
@@ -512,7 +559,7 @@ const SeweragePenetrationRateInp = () => {
statsCd: statsCd,
};
const response = await statisticsAPi.cancelSubmit(requestData);
const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === 'success') {
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true);
@@ -526,6 +573,14 @@ const SeweragePenetrationRateInp = () => {
}
};
const openVerificationHistoryDialog = () => {
if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !statsCd) {
alert("먼저 조회를 진행해주세요.");
return;
}
setVerificationHistoryOpen(true);
};
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '')
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
: true;
@@ -547,12 +602,11 @@ const SeweragePenetrationRateInp = () => {
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
{showEnterableToolbarButtons && (
<>
{isAdminInfo && statusInfo && statusInfo.laeSbmsnSttsCd !== '0' && (
<SubmitExceptionButton SubmitExceptionFunc={() => submitFunc(true)} />
)}
<VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog}
/>
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={() => submitFunc(false)} />
</>
)}
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
@@ -560,7 +614,7 @@ const SeweragePenetrationRateInp = () => {
</div>
</div>
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
{isLoading && !globalProgressVisible && <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>
@@ -593,7 +647,7 @@ const SeweragePenetrationRateInp = () => {
return errors
.map((e) => {
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - ';
return `${prefix}${e.rsnExpln}`;
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
})
.filter(Boolean)
.join('\n');
@@ -614,6 +668,25 @@ const SeweragePenetrationRateInp = () => {
submitMode={isExceptionSubmitFlow ? 'exception' : 'normal'}
isLoading={isLoading}
/>
<VerificationResultHistoryDialog
open={verificationHistoryOpen}
onOpenChange={setVerificationHistoryOpen}
queryContext={
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? {
sewerBzmnCd: searchValues.bsnmCd,
ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year,
statsCd,
}
: null
}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
/>
</>
);
};
@@ -7,7 +7,7 @@ import useSWR from "swr";
import { useAlertStore } from "@/shared/lib";
import { CodeSelectWithAll } from "@/entities/code";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ApiResponse,
SearchCondition,
@@ -36,13 +36,13 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
const PAGE_SIZE = 100;
const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
return statisticsApi.getSidoList();
};
const getSggListFetcher = async (
key: [string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggList(key[1]);
return statisticsApi.getSggList(key[1]);
};
const LogHistory = () => {
@@ -158,7 +158,7 @@ const LogHistory = () => {
try {
setIsLoading(true);
const response = await statisticsAPi.getLogList(param);
const response = await statisticsApi.getLogList(param);
if (response.status === "success") {
const lostList = response.data?.statsLogList ?? [];
@@ -12,7 +12,7 @@ import {
DialogTitle,
} from "@common/components";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
export interface LogHistoryDetailDialogProps {
open: boolean;
@@ -30,7 +30,7 @@ export function LogHistoryDetailDialog({
const sn = row.statsLogSn;
void (async () => {
const response = await statisticsAPi.getLogDetail(
const response = await statisticsApi.getLogDetail(
sn !== undefined && sn !== null ? String(sn) : "",
);
console.log(response.data);
@@ -13,7 +13,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { statisticsAPi } from "@/entities/statistics/api/statistics-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
@@ -141,7 +141,7 @@ const SeweragePenetrationRateRvw = () => {
try {
const userInfo = await getUserInfo();
const response = await statisticsAPi.submitReview({
const response = await statisticsApi.submitReview({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
@@ -169,7 +169,7 @@ const SeweragePenetrationRateRvw = () => {
}
try {
const detailRes = await statisticsAPi.getReviewOpinionDetail({
const detailRes = await statisticsApi.getReviewOpinionDetail({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
@@ -191,7 +191,7 @@ const SeweragePenetrationRateRvw = () => {
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
try {
const response = await statisticsAPi.saveReviewOpinion(param);
const response = await statisticsApi.saveReviewOpinion(param);
if (response.status !== "success") {
alert(response.message || "저장에 실패했습니다.");
return;
@@ -220,7 +220,7 @@ const SeweragePenetrationRateRvw = () => {
if (await confirm('반려 처리 하시겠습니까?')) {
try {
const userInfo = await getUserInfo();
const response = await statisticsAPi.saveReviewReturn({
const response = await statisticsApi.saveReviewReturn({
statsYr: reviewOpinionContext.statsYr,
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
statsCd: reviewOpinionContext.statsCd,
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import React from "react";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
@@ -241,6 +242,17 @@ export type CellTooltipCallback = (
params: CellTooltipCallbackParams,
) => string | undefined;
/** 셀 편집 완료 시 호출되는 콜백 (RealGrid onEditRowChanged) */
export type CellEditedCallbackParams = {
itemIndex: number;
fieldName: string;
oldValue: string | number;
newValue: string | number;
};
export type CellEditedCallback = (
params: CellEditedCallbackParams,
) => void;
export type DataGridProps = {
columnInfo: ColumnInfo;
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
@@ -259,10 +271,14 @@ export type DataGridProps = {
cellStyleCallback?: CellStyleCallback;
/** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */
cellTooltipCallback?: CellTooltipCallback;
/** 셀 편집 완료 시 호출 (RealGrid onEditRowChanged) */
onCellEdited?: CellEditedCallback;
/** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */
getGridDataRef?: {
current: (() => Record<string, string | number>[]) | null;
};
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
getGridRef?: React.MutableRefObject<RealGridReact | null>;
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
/** CheckBar 행 체크 변경 */
onItemChecked?: (itemIndex: number, checked: boolean) => void;
@@ -7,7 +7,8 @@ import {
type SewerageSupplyRateApiResponse,
type CheckInputPeriodResponse,
type SewerageSupplyRateSaveRequest,
type SewerageSupplyRateSaveApiResponse,
type SewerageSupplyRateSubmitResponse,
type SewerageSupplyRateSaveResponse,
} from "../../model/input/statistics-sewage-supply-rate-input-interface";
export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSewSupplyRateService {
@@ -26,16 +27,16 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe
return response.data;
}
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
const response = await stClient.post<SewerageSupplyRateSaveApiResponse>(
public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveResponse> {
const response = await stClient.post<SewerageSupplyRateSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`,
params,
);
return response.data;
}
public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse> {
const response = await stClient.post<SewerageSupplyRateSaveApiResponse>(
public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSubmitResponse> {
const response = await stClient.post<SewerageSupplyRateSubmitResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`,
params,
);
@@ -11,7 +11,7 @@ import type {
SeweragePipeInputPipeDiameterResponse,
SeweragePipeInputPipeTypeResponse,
SeweragePipeInputRegionalFactStatusSaveRequest,
SeweragePipeInputRegionalFactStatusSaveApiResponse,
SeweragePipeInputRegionalFactStatusSaveResponse,
SeweragePipeInputRegionalResponse,
SeweragePipeRequestParams,
StatisticsInputSewPipeService,
@@ -82,8 +82,8 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS
return response.data;
}
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveApiResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveApiResponse>(
public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise<SeweragePipeInputRegionalFactStatusSaveResponse> {
const response = await stClient.post<SeweragePipeInputRegionalFactStatusSaveResponse>(
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`,
param,
);
@@ -2,17 +2,28 @@ import { stClient } from "@/shared/api/client";
import { ENDPOINT } from "@/shared/config";
import type {
ApprovalRequest,
ApprovalResponse,
ApproveRequest,
RejectRequest,
StatisticsApprovalService,
} from "../model/statistics-approval-interface";
class StatisticsApprovalApiService implements StatisticsApprovalService {
public async approveStats(
params: ApprovalRequest[],
params: ApproveRequest[],
): Promise<ApprovalResponse> {
const response = await stClient.post<ApprovalResponse>(
`${ENDPOINT.ST_SERVICE.APPROVAL}?type=VRFC&action=APPROVE`,
`${ENDPOINT.ST_SERVICE.APPROVAL}/approve?type=VRFC`,
params,
);
return response.data;
}
public async rejectStats(
params: RejectRequest[],
): Promise<ApprovalResponse> {
const response = await stClient.post<ApprovalResponse>(
`${ENDPOINT.ST_SERVICE.APPROVAL}/reject?type=VRFC`,
params,
);
return response.data;
@@ -14,6 +14,8 @@ import {
type ReviewReturnSaveResponse,
type ReviewSubmitRequest,
type ReviewSubmitResponse,
type VerificationResultListRequest,
type VerificationResultListResponse,
type SearchCondition,
type Sgg,
type StatisticsLogDetailResponse,
@@ -22,9 +24,11 @@ import {
type StatisticsService,
type SubmitExceptionRequest,
type SubmitExceptionResponse,
type ImrResponse,
type ImrStatusResponse,
} from "../model/statistics-interface";
export class StatisticsAPiService implements StatisticsService {
export class StatisticsApiService implements StatisticsService {
public async getSidoList(params?: {
instSeCd?: string;
}): Promise<ApiResponse<SearchCondition>> {
@@ -68,14 +72,21 @@ export class StatisticsAPiService implements StatisticsService {
return response.data;
}
public async callImrService(param: ImrRequest): Promise<null> {
const response = await client.post<null>(
public async callImrService(param: ImrRequest): Promise<ImrResponse> {
const response = await client.post<ImrResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
param,
);
return response.data;
}
public async getImrStatus(param: string): Promise<ImrStatusResponse> {
const response = await client.get<ImrStatusResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`,
);
return response.data;
}
// 예외 제출
public async submitException(
param: SubmitExceptionRequest,
@@ -145,6 +156,14 @@ export class StatisticsAPiService implements StatisticsService {
);
return response.data;
}
public async getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse> {
const response = await stClient.get<VerificationResultListResponse>(
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
{ params: param },
);
return response.data;
}
}
export const statisticsAPi = new StatisticsAPiService();
export const statisticsApi = new StatisticsApiService();
+4 -3
View File
@@ -64,12 +64,13 @@ export {
export { verificationHistoryColumns } from "./model/statistics-analysis/verification-history-columns";
export { getVerificationListColumns } from "./model/statistics-analysis/verification-list-columns";
export {
type ApprovalRequest,
type ApprovalResponse,
type ApproveRequest,
type RejectRequest,
} from "./model/statistics-approval-interface";
export {
statisticsVerificationDetailColumnInfo,
statisticsVerificationSummaryColumnInfo,
statisticsVerificationReportColumnInfo,
statisticsVerificationResultColumnInfo,
} from "./model/statistics-verification-report/statistics-verification-report-columns";
export {
type StatisticsVerificationFilterState,
@@ -1,5 +1,3 @@
import type { SewerageSupplyRateSaveApiResponse } from "./statistics-sewage-supply-rate-input-interface";
export interface SeweragePipeRequestParams {
ctpvNm?: string;
sggNm?: string;
@@ -119,8 +117,11 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest {
saveList: SeweragePipeInputRegionalFactStatusSaveItem[];
}
/** 지역별 시설현황 저장 API 응답 — 보급률 저장과 동일 래핑 */
export type SeweragePipeInputRegionalFactStatusSaveApiResponse = SewerageSupplyRateSaveApiResponse;
export interface SeweragePipeInputRegionalFactStatusSaveResponse {
data: string;
message?: string;
status: string;
}
/** 시설별 설치 현황 — 목록 항목 */
export interface SeweragePipeInputFactInstallStatusItem {
@@ -519,5 +520,5 @@ export interface StatisticsInputSewPipeService {
getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise<SeweragePipeFacilityAdminAreaListResponse>;
saveRegionalFactStatus(
param: SeweragePipeInputRegionalFactStatusSaveRequest,
): Promise<SeweragePipeInputRegionalFactStatusSaveApiResponse>;
): Promise<SeweragePipeInputRegionalFactStatusSaveResponse>;
}
@@ -36,6 +36,8 @@ export interface SewerageSupplyRateCommonInfo {
export interface VerificationResultItem {
vrfcCd: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string;
}
/** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */
@@ -150,12 +152,20 @@ export interface SewerageSupplyRateSaveRequest {
superAdmin?: string;
}
export interface SewerageSupplyRateSaveData {
httpStatus?: number;
code?: string;
export interface SewerageSupplyRateSaveResponse {
data: string;
message?: string;
status: string;
}
export type SewerageSupplyRateSaveApiResponse = ApiResponse<SewerageSupplyRateSaveData>;
export interface SewerageSupplyRateSubmitResponse {
data: {
finalStatus: string;
nvpassed: boolean;
}
message?: string;
status: string;
}
// 하수도 통계 입력 기간 조회
export interface CheckInputPeriodResponse {
@@ -167,6 +177,6 @@ export interface CheckInputPeriodResponse {
export interface StatisticsInputSewSupplyRateService {
getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise<SewerageSupplyRateApiResponse>;
checkInputPeriod(params: string): Promise<CheckInputPeriodResponse>;
saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse>;
submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveApiResponse>;
saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSaveResponse>;
submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise<SewerageSupplyRateSubmitResponse>;
}
@@ -1,13 +1,21 @@
export interface ApprovalRequest {
export interface ApproveRequest {
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
rjctOpnnCn: string;
lastCfmtnPicNm: string;
lastCfmtnPicTelNo: string;
lastCfmtnPicDeptNm: string;
lastCfmtnPicJbgdNm: string;
lastCfmtnPicNm?: string;
lastCfmtnPicTelNo?: string;
lastCfmtnPicDeptNm?: string;
lastCfmtnPicJbgdNm?: string;
}
export interface RejectRequest {
vrfcSn: number;
statsYr: string;
sewerBzmnCd: string;
statsCd: string;
mdfrId: string;
rjctOpnnCn?: string;
}
export interface ApprovalResponse {
@@ -18,5 +26,6 @@ export interface ApprovalResponse {
}
export interface StatisticsApprovalService {
approveStats(params: ApprovalRequest[]): Promise<ApprovalResponse>;
approveStats(params: ApproveRequest[]): Promise<ApprovalResponse>;
rejectStats(params: RejectRequest[]): Promise<ApprovalResponse>;
}
@@ -41,6 +41,91 @@ export interface ImrRequest {
stats_cd: string;
stats_yr: string;
lae_sbmsn_stts_cd: string;
gen_verify_stts_cd?: string;
}
export interface ImrResponse {
anls_sn: number;
job_id: string;
total_otlr_cnt: number;
status_code: string;
error_msg: string;
processing_time_ms: number;
job_prog: number;
queue_position: number;
}
export interface ImrStatusResponse {
job_id: string;
anls_sn: number;
status: string;
job_prog: number;
total_otlr_cnt: number;
total_nml_cnt: number;
total_excl_cnt: number;
processing_time_ms: number;
error_msg: string;
queue_position: string;
}
export interface VerificationResultListRequest {
statsYr: string;
sewerBzmnCd?: string;
ctpvNm?: string;
sggNm?: string;
statsCd?: string;
/** 검증 방법 코드 (예: IMR) */
vrfcMthdCd?: string;
/** 처리여부 코드 목록, 콤마 구분 */
prcsYnList?: string;
pbcpAprvYn?: string;
page?: number;
size?: number;
}
/** 검증 결과 리포트 목록 한 행 (`verifyResultList` 원소) */
export interface VerificationResultReportItem {
statsYr: string;
statsCd: string;
statsNm: string;
sewerBzmnCd: string;
ctpvNm: string;
sggNm: string;
admdstNm: string;
vrfcSn: number;
vrfcMthdCd: string;
vrfcMthdNm: string;
vrfcTrgtNm: string;
regDt: string;
rsnExpln: string;
nowVlCn: string;
compVlCn: string | null;
prcsYn: string;
prcsOpnnExpln: string | null;
prcrId: string | null;
rjctOpnnCn: string | null;
pbcpAprvYn: string;
mdfrId: string | null;
mdfcnDt: string;
}
export interface VerificationResultListPageInfo {
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface VerificationResultListData {
verifyResultList: VerificationResultReportItem[];
pageInfo: VerificationResultListPageInfo;
}
/** 검증 결과 리포트 조회 API 응답 */
export interface VerificationResultListResponse {
status: string;
message?: string;
data?: VerificationResultListData;
}
export interface SubmitExceptionRequest {
@@ -188,7 +273,7 @@ export interface StatisticsService {
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<null>;
callImrService(param: ImrRequest): Promise<ImrResponse>;
submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse>;
getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse>;
getLogDetail(param: string): Promise<StatisticsLogDetailResponse>;
@@ -196,4 +281,6 @@ export interface StatisticsService {
getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse>;
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
submitReview(param :ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
getImrStatus(param: string): Promise<ImrStatusResponse>;
}
@@ -2,7 +2,7 @@ import type { ColumnInfo } from "@/components/type/DataGrid.type";
const READ_ONLY_STYLE = "rg-cell-readonly";
export const statisticsVerificationSummaryColumnInfo: ColumnInfo = {
export const statisticsVerificationReportColumnInfo: ColumnInfo = {
columns: [
{
name: "ctpvNm",
@@ -111,19 +111,21 @@ export const statisticsVerificationSummaryColumnInfo: ColumnInfo = {
],
};
export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
export const statisticsVerificationResultColumnInfo: ColumnInfo = {
columns: [
{
name: "ctpvNm",
fieldName: "ctpvNm",
header: { text: "시도" },
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "sggNm",
fieldName: "sggNm",
header: { text: "시군구" },
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "statsNm",
@@ -131,6 +133,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "지표" },
width: 140,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "vrfcTrgtNm",
@@ -138,6 +141,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "세부항목" },
width: 180,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "regDt",
@@ -145,6 +149,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "검증날짜" },
width: 150,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "rsnExpln",
@@ -152,6 +157,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "오류내역" },
width: 200,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "nowVlCn",
@@ -159,6 +165,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "입력값" },
width: 90,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "compVlCn",
@@ -166,6 +173,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "비교값" },
width: 90,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "prcsYn",
@@ -173,6 +181,7 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "상태" },
width: 90,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "prcsOpnnExpln",
@@ -180,13 +189,14 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "예외사유" },
width: 170,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "rjctOpnnCn",
fieldName: "rjctOpnnCn",
header: { text: "반려의견" },
width: 170,
styleName: READ_ONLY_STYLE,
editable: true,
},
{
name: "pbcpAprvYn",
@@ -194,12 +204,14 @@ export const statisticsVerificationDetailColumnInfo: ColumnInfo = {
header: { text: "승인여부" },
width: 90,
styleName: READ_ONLY_STYLE,
editable: false,
},
{
name: "vrfcSn",
fieldName: "vrfcSn",
header: { text: "vrfcSn" },
visible: false,
editable: false,
},
],
columnLayout: [
+1 -1
View File
@@ -73,7 +73,7 @@ export const ENDPOINT = {
INPUTCOMMON: "/st-input-service/api/v1/common",
INPUTVERIFY: "/st-input-service/api/v1/verify",
TOTAL: "/st-management-service/api/v1/stats",
APPROVAL: "/st-management-service/api/stats/approval",
APPROVAL: "/st-management-service/api/stats",
},
STATVERIFY_SERVICE: {
VRFCTGT_MNG: "/statverify-service/api/v1/vrfc-trgt-mng",
+1
View File
@@ -2,6 +2,7 @@ export { getCookie, setCookie, removeCookie } from "./cookie";
export { useMutationWrapper } from "./use-mutation-wrapper";
export { useAlertStore } from "./use-alert-store";
export { useDataTable } from "./use-data-table";
export { useGlobalProgressStore } from "./use-global-progress-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export { usePrompt } from "./use-prompt";
@@ -0,0 +1,16 @@
import { useContext } from "react";
import { useStore } from "zustand";
import type { GlobalProgressStore } from "../store/global-progress-store";
import { StoreContext } from "../providers/store-provider";
export const useGlobalProgressStore = <T>(
selector: (store: GlobalProgressStore) => T,
): T => {
const storeContext = useContext(StoreContext);
if (!storeContext) {
throw new Error("useGlobalProgressStore must be used within StoreProvider");
}
return useStore(storeContext.globalProgressStore, selector);
};
@@ -4,6 +4,7 @@ import { createContext, useRef } from "react";
import {
createAlertStore,
createAttachmentStore,
createGlobalProgressStore,
createMenuStore,
createUserStore,
} from "../store";
@@ -11,6 +12,7 @@ import {
type StoreContextType = {
alertStore: ReturnType<typeof createAlertStore>;
attachmentStore: ReturnType<typeof createAttachmentStore>;
globalProgressStore: ReturnType<typeof createGlobalProgressStore>;
menuStore: ReturnType<typeof createMenuStore>;
userStore: ReturnType<typeof createUserStore>;
};
@@ -25,6 +27,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
storeRef.current = {
alertStore: createAlertStore(),
attachmentStore: createAttachmentStore(),
globalProgressStore: createGlobalProgressStore(),
menuStore: createMenuStore(),
userStore: createUserStore(),
};
@@ -0,0 +1,45 @@
import { createStore } from "zustand/vanilla";
export type GlobalProgressState = {
visible: boolean;
label: string;
progress: number;
};
export type GlobalProgressActions = {
show: (payload?: { label?: string; progress?: number }) => void;
setLabel: (label: string) => void;
setProgress: (progress: number) => void;
hide: () => void;
};
export type GlobalProgressStore = GlobalProgressState & GlobalProgressActions;
export const defaultGlobalProgressState: GlobalProgressState = {
visible: false,
label: "진행 중...",
progress: 0,
};
export const createGlobalProgressStore = (
initState: GlobalProgressState = defaultGlobalProgressState,
) => {
return createStore<GlobalProgressStore>()((set) => ({
...initState,
show: (payload) =>
set(() => ({
visible: true,
label: payload?.label ?? defaultGlobalProgressState.label,
progress: Math.max(0, Math.min(100, payload?.progress ?? 0)),
})),
setLabel: (label) => set(() => ({ label })),
setProgress: (progress) =>
set(() => ({ progress: Math.max(0, Math.min(100, progress)) })),
hide: () =>
set(() => ({
visible: false,
progress: 0,
label: defaultGlobalProgressState.label,
})),
}));
};
+1
View File
@@ -1,4 +1,5 @@
export { createAlertStore } from "./alert-store";
export { createAttachmentStore } from "./attachment-store";
export { createGlobalProgressStore } from "./global-progress-store";
export { createMenuStore, type BbsType } from "./menu-store";
export { createUserStore, type IUserInfo } from "./user-store";
@@ -0,0 +1,23 @@
"use client";
import { Progress } from "@common/components";
import { useGlobalProgressStore } from "@/shared/lib";
export const GlobalProgressOverlay = () => {
const visible = useGlobalProgressStore((state) => state.visible);
const label = useGlobalProgressStore((state) => state.label);
const progress = useGlobalProgressStore((state) => state.progress);
if (!visible) return null;
return (
<div className="fixed inset-0 z-[999] bg-black/35 [data-theme='black']:bg-black/55 backdrop-blur-[1px] rounded-lg p-10 flex justify-center items-center">
<div className="w-[34rem] rounded-md border border-border bg-background p-6 shadow-md">
<p className="mb-3 text-center text-[1.3rem] text-foreground">
{label} {progress}%
</p>
<Progress value={progress} />
</div>
</div>
);
};