[st] - 입력 반려,승인 / 검토 승인
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface ApproveButtonProps {
|
||||
approveFunc: () => void;
|
||||
approveFunc: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ApproveButton = ({ approveFunc }: ApproveButtonProps) => {
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#466995] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
/* 상태바·체크바 헤더 */
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-white;
|
||||
}
|
||||
@@ -36,14 +21,67 @@
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
@apply !font-semibold !text-[#3372cf];
|
||||
}
|
||||
/* 개별 반려: 높이/레이아웃은 RealGrid 행에 그대로 두고, 색·테두리·정렬만 버튼 느낌 */
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
box-sizing: border-box !important;
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
border-radius: 0.25rem !important;
|
||||
border: 1px solid rgb(248 113 113 / 0.55) !important;
|
||||
background-color: rgb(254 242 242) !important;
|
||||
color: rgb(185 28 28) !important;
|
||||
font-size: 1.2rem !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover {
|
||||
background-color: rgb(254 226 226) !important;
|
||||
border-color: rgb(239 68 68 / 0.85) !important;
|
||||
color: rgb(153 27 27) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:active {
|
||||
background-color: rgb(254 202 202) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn {
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
box-sizing: border-box !important;
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
border-radius: 0.25rem !important;
|
||||
border: 1px solid rgb(148 163 184 / 0.7) !important;
|
||||
background-color: rgb(248 250 252) !important;
|
||||
color: rgb(51 65 85) !important;
|
||||
font-size: 1.2rem !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover {
|
||||
background-color: rgb(241 245 249) !important;
|
||||
border-color: rgb(100 116 139 / 0.85) !important;
|
||||
color: rgb(30 41 59) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:active {
|
||||
background-color: rgb(226 232 240) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-action-link {
|
||||
@apply !font-semibold !text-[#2563eb] underline;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-pending {
|
||||
@apply !text-gray-500;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-submitted {
|
||||
@apply !font-semibold
|
||||
@apply !font-semibold;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-approved {
|
||||
@apply !font-semibold
|
||||
@apply !font-semibold;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected {
|
||||
@apply !font-semibold !text-[#ff6c64];
|
||||
@@ -117,60 +155,6 @@
|
||||
.data-grid-status-bubble > span + span {
|
||||
@apply ml-4 border-l border-slate-200/70 pl-4;
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#3C4043] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-[#545556] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
||||
@apply !text-[#f87171];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
@apply !text-[#3372cf];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#646469] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#518585] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rg-cell-stats-status-dot-ping {
|
||||
@@ -203,4 +187,123 @@
|
||||
.rg-grid .rg-head .rg-table tr td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ST 통계 그리드 헤더 색 — @layer 밖(일반 규칙).
|
||||
* packages/rsone/components/grid/css/DataGrid.css 등 @layer 밖 !important와 동일 층에서
|
||||
* .rg-grid … 특이도로 덮어씀.
|
||||
*/
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #466995 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-header .rg-table {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-header .rg-cell,
|
||||
.rg-grid .rg-head .rg-cell {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #3c4043 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
background-color: #545556 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
||||
color: #f87171 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
color: #3372cf !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
|
||||
background-color: rgb(127 29 29 / 0.42) !important;
|
||||
border-color: rgb(248 113 113 / 0.5) !important;
|
||||
color: #fecaca !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover {
|
||||
background-color: rgb(153 27 27 / 0.55) !important;
|
||||
border-color: rgb(252 165 165 / 0.75) !important;
|
||||
color: #fee2e2 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn {
|
||||
background-color: rgb(71 85 105 / 0.35) !important;
|
||||
border-color: rgb(148 163 184 / 0.55) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover {
|
||||
background-color: rgb(71 85 105 / 0.5) !important;
|
||||
border-color: rgb(203 213 225 / 0.65) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #646469 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #518585 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@@ -378,6 +378,7 @@ const DataGrid = ({
|
||||
cellTooltipCallback,
|
||||
onCellEdited,
|
||||
onRowClick,
|
||||
onDataCellClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
onScrollToBottom,
|
||||
@@ -403,6 +404,7 @@ const DataGrid = ({
|
||||
cellTooltipCallback,
|
||||
);
|
||||
const onCellEditedRef = useRef<typeof onCellEdited>(onCellEdited);
|
||||
const onDataCellClickRef = useRef<typeof onDataCellClick>(onDataCellClick);
|
||||
const onScrollToBottomRef = useRef<(() => void) | undefined>(
|
||||
onScrollToBottom,
|
||||
);
|
||||
@@ -417,6 +419,7 @@ const DataGrid = ({
|
||||
onItemCheckedRef.current = onItemChecked;
|
||||
onItemAllCheckedRef.current = onItemAllChecked;
|
||||
onCellEditedRef.current = onCellEdited;
|
||||
onDataCellClickRef.current = onDataCellClick;
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
@@ -1180,7 +1183,11 @@ const DataGrid = ({
|
||||
// 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식)
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || (!onRowClick && !cellTooltipCallback)) return;
|
||||
if (
|
||||
!gridView ||
|
||||
(!onRowClick && !cellTooltipCallback && !onDataCellClick)
|
||||
)
|
||||
return;
|
||||
|
||||
gridView.onCellClicked = (grid, clickData) => {
|
||||
if (
|
||||
@@ -1199,10 +1206,10 @@ const DataGrid = ({
|
||||
};
|
||||
}
|
||||
).getCurrent?.();
|
||||
const fieldName =
|
||||
cellIndex?.fieldName ?? cellIndex?.column ?? "";
|
||||
// cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex)
|
||||
if (cellTooltipCallbackRef.current && row && cellIndex) {
|
||||
const fieldName =
|
||||
cellIndex.fieldName ?? cellIndex.column ?? "";
|
||||
const msg = cellTooltipCallbackRef.current({
|
||||
row: row as Record<string, string | number>,
|
||||
fieldName,
|
||||
@@ -1232,6 +1239,13 @@ const DataGrid = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onDataCellClickRef.current && row !== undefined) {
|
||||
onDataCellClickRef.current({
|
||||
row: (row ?? {}) as Record<string, string | number>,
|
||||
fieldName,
|
||||
itemIndex: clickData.itemIndex,
|
||||
});
|
||||
}
|
||||
if (onRowClick)
|
||||
onRowClick((row ?? {}) as Record<string, string | number>);
|
||||
} catch (error) {
|
||||
@@ -1242,7 +1256,7 @@ const DataGrid = ({
|
||||
return () => {
|
||||
if (gridView.onCellClicked) gridView.onCellClicked = undefined;
|
||||
};
|
||||
}, [onRowClick, cellTooltipCallback, columns]);
|
||||
}, [onRowClick, onDataCellClick, cellTooltipCallback, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
"use client";
|
||||
|
||||
import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { RealGridReact } from "realgrid-react";
|
||||
|
||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ReviewInputSaveRequest,
|
||||
VerificationResultListRequest,
|
||||
VerificationResultReportItem,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
|
||||
export interface ExceptionProcessingHistoryQueryContext {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
}
|
||||
|
||||
export interface ExceptionProcessingHistoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
queryContext?: ExceptionProcessingHistoryQueryContext | null;
|
||||
emptyMessage?: string;
|
||||
/** 반려 API 성공 후 (다이얼로그 내 목록 재조회 이후) 상위 화면 등에서 호출 */
|
||||
onRejectSuccess?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
type ExceptionHistoryGridRow = Record<string, string | number>;
|
||||
type ExceptionHistoryTabKey = "NV" | "IMR" | "HB";
|
||||
|
||||
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
|
||||
{ value: "NV", label: "일반검증 내역" },
|
||||
{ value: "IMR", label: "I-MR 검증내역" },
|
||||
{ value: "HB", label: "HB 검증내역" },
|
||||
];
|
||||
|
||||
const TITLE = "예외처리 내역 조회";
|
||||
/** 예외처리 건만 */
|
||||
const PRCS_YN_LIST = "2";
|
||||
|
||||
export type ExceptionRejectSubmitItem = {
|
||||
vrfcSn: number;
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
mdfrId: string;
|
||||
rjctOpnnCn?: string;
|
||||
};
|
||||
|
||||
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
||||
const s = String(raw).trim();
|
||||
if (s === "") return undefined;
|
||||
const n = Number.parseInt(s, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
|
||||
if (fieldName !== "prcsYnNm") return undefined;
|
||||
const code = prcsYnCodeToNumber(row.prcsYn);
|
||||
if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad";
|
||||
return "rg-cell-vrf-prcs-ok";
|
||||
}
|
||||
|
||||
const exceptionHistoryColumnInfo: 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: "prcsYnNm", fieldName: "prcsYnNm", 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 },
|
||||
{
|
||||
name: "individualReject",
|
||||
fieldName: "individualReject",
|
||||
header: { text: "개별 반려" },
|
||||
editable: false,
|
||||
width: 104,
|
||||
styleName: "rg-cell-reject-mini-btn",
|
||||
},
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
|
||||
{ name: "vrfcSn", fieldName: "vrfcSn", header: { text: "vrfcSn" }, editable: false, visible: false },
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"individualReject",
|
||||
"prcsYn",
|
||||
"vrfcSn",
|
||||
],
|
||||
};
|
||||
|
||||
export function ExceptionProcessingHistoryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
queryContext,
|
||||
emptyMessage = "표시할 예외처리 내역이 없습니다.",
|
||||
onRejectSuccess,
|
||||
}: ExceptionProcessingHistoryDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const nvGridRef = useRef<RealGridReact | null>(null);
|
||||
const imrGridRef = useRef<RealGridReact | null>(null);
|
||||
const hbGridRef = useRef<RealGridReact | null>(null);
|
||||
const [rejectReasonDialogOpen, setRejectReasonDialogOpen] = useState(false);
|
||||
const [pendingRejectVrfcSns, setPendingRejectVrfcSns] = useState<number[]>([]);
|
||||
const [rejectReasonText, setRejectReasonText] = useState("");
|
||||
const [rejectSubmitting, setRejectSubmitting] = useState(false);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ExceptionHistoryTabKey>("NV");
|
||||
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [hbPageIndex, setHbPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [hbPageCount, setHbPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [hbLoading, setHbLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setActiveTab("NV");
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
setErrorMessage(null);
|
||||
setRejectReasonDialogOpen(false);
|
||||
setPendingRejectVrfcSns([]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
}, []);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (vrfcMthdCd: ExceptionHistoryTabKey, 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: PRCS_YN_LIST,
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
return statisticsApi.getVerificationResultList(param);
|
||||
},
|
||||
[queryContext],
|
||||
);
|
||||
|
||||
const loadInitial = useCallback(async () => {
|
||||
if (!queryContext) {
|
||||
setErrorMessage("조회 조건이 없어 예외처리 내역을 불러올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setHbLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes, hbRes] = await Promise.all([
|
||||
fetchPage("NV", 0),
|
||||
fetchPage("IMR", 0),
|
||||
fetchPage("HB", 0),
|
||||
]);
|
||||
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
|
||||
setImrEntries(imrRes?.data?.verifyResultList ?? []);
|
||||
setHbEntries(hbRes?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setErrorMessage("예외처리 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
}
|
||||
}, [fetchPage, queryContext]);
|
||||
|
||||
const handleNormalPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === normalPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("NV", pageIndex);
|
||||
setNormalEntries(response?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setNormalPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, normalPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleImrPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === imrPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setImrLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("IMR", pageIndex);
|
||||
setImrEntries(response?.data?.verifyResultList ?? []);
|
||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setImrLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, imrPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleHbPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === hbPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setHbLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("HB", pageIndex);
|
||||
setHbEntries(response?.data?.verifyResultList ?? []);
|
||||
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setHbLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, hbPageIndex, queryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
void loadInitial();
|
||||
}, [loadInitial, open, resetState]);
|
||||
|
||||
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
individualReject: "개별 반려",
|
||||
vrfcSn: item.vrfcSn,
|
||||
}));
|
||||
|
||||
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
|
||||
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
|
||||
const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
|
||||
|
||||
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
|
||||
SetStateAction<Record<string, string | number>[]>
|
||||
>;
|
||||
|
||||
const handleOpenRejectReasonDialog = useCallback(() => {
|
||||
if (!queryContext) {
|
||||
alert("조회 조건이 없습니다.");
|
||||
return;
|
||||
}
|
||||
const rows =
|
||||
activeTab === "NV" ? normalRows : activeTab === "IMR" ? imrRows : hbRows;
|
||||
const gridRef =
|
||||
activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef;
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) {
|
||||
alert("데이터가 존재하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
const checkedIndices = (gridView.getCheckedRows?.(true, false, true) ?? []) as number[];
|
||||
if (checkedIndices.length === 0) {
|
||||
alert("반려할 항목을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
const vrfcSns: number[] = [];
|
||||
for (const itemIndex of checkedIndices) {
|
||||
const row = rows[itemIndex];
|
||||
if (!row) continue;
|
||||
const vrfcSn = Number(row.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) continue;
|
||||
vrfcSns.push(vrfcSn);
|
||||
}
|
||||
if (vrfcSns.length === 0) {
|
||||
alert("유효한 검증번호가 있는 행을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setPendingRejectVrfcSns(vrfcSns);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
setRejectReasonDialogOpen(true);
|
||||
}, [activeTab, alert, hbRows, imrRows, normalRows, queryContext]);
|
||||
|
||||
const resetRejectReasonDialog = useCallback(() => {
|
||||
setRejectReasonDialogOpen(false);
|
||||
setPendingRejectVrfcSns([]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
}, []);
|
||||
|
||||
const handleRejectReasonDialogOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
if (!nextOpen) resetRejectReasonDialog();
|
||||
},
|
||||
[resetRejectReasonDialog],
|
||||
);
|
||||
|
||||
const handleConfirmReject = useCallback(async () => {
|
||||
if (!queryContext || pendingRejectVrfcSns.length === 0) {
|
||||
alert("반려할 항목이 없습니다. 다시 선택해주세요.");
|
||||
resetRejectReasonDialog();
|
||||
return;
|
||||
}
|
||||
let userInfo;
|
||||
try {
|
||||
userInfo = await getUserInfo();
|
||||
} catch {
|
||||
alert("사용자 정보를 불러오지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
const mdfrId = userInfo.userUnqId;
|
||||
const trimmedReason = rejectReasonText.trim();
|
||||
|
||||
const items: ReviewInputSaveRequest[] = pendingRejectVrfcSns.map((vrfcSn) => ({
|
||||
type: "INPUT",
|
||||
statsYr: queryContext.statsYr,
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
statsCd: queryContext.statsCd,
|
||||
mdfrId,
|
||||
vrfcSn: String(vrfcSn),
|
||||
...(trimmedReason ? { rjctOpnnCn: trimmedReason } : {}),
|
||||
}));
|
||||
|
||||
setRejectSubmitting(true);
|
||||
try {
|
||||
const response = await statisticsApi.saveInputReturn(items);
|
||||
if (response.resultCode.toLowerCase() !== "success") {
|
||||
alert(response.message ?? "반려 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
alert("반려 처리되었습니다.");
|
||||
resetRejectReasonDialog();
|
||||
await Promise.all([
|
||||
loadInitial(),
|
||||
onRejectSuccess?.() ?? Promise.resolve(),
|
||||
]);
|
||||
} catch {
|
||||
alert("반려 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setRejectSubmitting(false);
|
||||
}
|
||||
}, [
|
||||
alert,
|
||||
loadInitial,
|
||||
onRejectSuccess,
|
||||
pendingRejectVrfcSns,
|
||||
queryContext,
|
||||
rejectReasonText,
|
||||
resetRejectReasonDialog,
|
||||
]);
|
||||
|
||||
const handleGridRowClick = useCallback(
|
||||
(rowData: Record<string, string | number>) => {
|
||||
const gridRef =
|
||||
activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef;
|
||||
const current = gridRef.current?.gridView?.getCurrent?.() as
|
||||
| { fieldName?: string; column?: string }
|
||||
| undefined;
|
||||
const fieldName = current?.fieldName ?? current?.column;
|
||||
if (fieldName !== "individualReject") return;
|
||||
const vrfcSn = Number(rowData.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) {
|
||||
alert("유효한 검증번호가 있는 행을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setPendingRejectVrfcSns([vrfcSn]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
setRejectReasonDialogOpen(true);
|
||||
},
|
||||
[activeTab, alert],
|
||||
);
|
||||
|
||||
const renderGrid = (
|
||||
rows: ExceptionHistoryGridRow[],
|
||||
pageIndex: number,
|
||||
pageCount: number,
|
||||
onPageIndexChange: (page: number) => void,
|
||||
loading: boolean,
|
||||
gridRef: MutableRefObject<RealGridReact | null>,
|
||||
) => {
|
||||
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={exceptionHistoryColumnInfo}
|
||||
rowData={rows}
|
||||
setRowData={noopSetRows}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={true}
|
||||
enableSort={false}
|
||||
getGridRef={gridRef}
|
||||
cellStyleCallback={exceptionPrcsYnNmCellStyle}
|
||||
onRowClick={handleGridRowClick}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as ExceptionHistoryTabKey)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{exceptionHistoryTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value="NV" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
normalRows,
|
||||
normalPageIndex,
|
||||
normalPageCount,
|
||||
handleNormalPageChange,
|
||||
normalLoading,
|
||||
nvGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="IMR" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
imrRows,
|
||||
imrPageIndex,
|
||||
imrPageCount,
|
||||
handleImrPageChange,
|
||||
imrLoading,
|
||||
imrGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="HB" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
hbRows,
|
||||
hbPageIndex,
|
||||
hbPageCount,
|
||||
handleHbPageChange,
|
||||
hbLoading,
|
||||
hbGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={handleOpenRejectReasonDialog}
|
||||
disabled={rejectSubmitting}
|
||||
>
|
||||
반려
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={rejectReasonDialogOpen} onOpenChange={handleRejectReasonDialogOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>예외처리 반려</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 py-2">
|
||||
<Label htmlFor="exception-reject-reason" className="text-label">
|
||||
반려사유 (선택, 체크한 모든 행에 동일 적용)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="exception-reject-reason"
|
||||
value={rejectReasonText}
|
||||
onChange={(e) => setRejectReasonText(e.target.value)}
|
||||
rows={4}
|
||||
className="min-h-[8rem] p-2 !text-xl"
|
||||
placeholder='미입력 시 "검증 리포트를 확인하여 주시기 바랍니다." 자동 주입'
|
||||
disabled={rejectSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
disabled={rejectSubmitting}
|
||||
onClick={() => void handleConfirmReject()}
|
||||
>
|
||||
반려 확정
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray" disabled={rejectSubmitting}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function prcsYnCodeToNumber(raw: string | number | undefined): number | undefine
|
||||
function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
|
||||
if (fieldName !== "prcsYnNm") return undefined;
|
||||
const code = prcsYnCodeToNumber(row.prcsYn);
|
||||
if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad";
|
||||
if (code === 0 || code === 3) return "rg-cell-vrf-prcs-bad";
|
||||
return "rg-cell-vrf-prcs-ok";
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const searchDoneRef = useRef(false);
|
||||
/** ST00060001만: RealGrid 편집값이 state에 없을 수 있어 저장/제출 시 그리드에서 직접 읽음 */
|
||||
const st60001GridGetRowsRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
|
||||
|
||||
const submitFunc = () => {
|
||||
void (async () => {
|
||||
if (activeTab !== 'ST00060001') {
|
||||
@@ -891,7 +891,11 @@ const PublicSewageFacilityInp = () => {
|
||||
alert(response.message || '매핑 대상 조회 중 오류가 발생했습니다.');
|
||||
return;
|
||||
}
|
||||
const mappedTargetRows = (response.data ?? []).map((item) => ({
|
||||
const rawList = response.data ?? [];
|
||||
if (rawList.length === 0) {
|
||||
alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.');
|
||||
}
|
||||
const mappedTargetRows = rawList.map((item) => ({
|
||||
...item,
|
||||
__checked: item.mappedYn === 'Y' ? 'Y' : '',
|
||||
}));
|
||||
@@ -961,7 +965,7 @@ const PublicSewageFacilityInp = () => {
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
@@ -969,8 +973,8 @@ const PublicSewageFacilityInp = () => {
|
||||
{showEnterableToolbarButtons && !isMainFacilityTab && (
|
||||
<>
|
||||
{showIsNotApplicable && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />}
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && (
|
||||
@@ -985,7 +989,7 @@ const PublicSewageFacilityInp = () => {
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)}>
|
||||
<TabsList>
|
||||
|
||||
@@ -196,10 +196,10 @@ const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
|
||||
if (!verification) return false;
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
}
|
||||
// function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
|
||||
// if (!verification) return false;
|
||||
// return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
// }
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -841,27 +841,27 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getTabGridRowsForVerification = (tabKey: string): Record<string, string | number>[] => {
|
||||
switch (tabKey) {
|
||||
case "ST00020001":
|
||||
return getGridDataRef.current?.() ?? [];
|
||||
case "ST00020002":
|
||||
return getFactInstallGridDataRef.current?.() ?? [];
|
||||
case "ST00020003":
|
||||
return getPipeTypeGridDataRef.current?.() ?? [];
|
||||
case "ST00020004":
|
||||
return getPipeDiameterGridDataRef.current?.() ?? [];
|
||||
case "ST00020005":
|
||||
return getManholeGridDataRef.current?.() ?? [];
|
||||
case "ST00020006":
|
||||
return [
|
||||
...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
|
||||
...(rowDataMap.get("adminAreaConnectionInp") ?? []),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
// const getTabGridRowsForVerification = (tabKey: string): Record<string, string | number>[] => {
|
||||
// switch (tabKey) {
|
||||
// case "ST00020001":
|
||||
// return getGridDataRef.current?.() ?? [];
|
||||
// case "ST00020002":
|
||||
// return getFactInstallGridDataRef.current?.() ?? [];
|
||||
// case "ST00020003":
|
||||
// return getPipeTypeGridDataRef.current?.() ?? [];
|
||||
// case "ST00020004":
|
||||
// return getPipeDiameterGridDataRef.current?.() ?? [];
|
||||
// case "ST00020005":
|
||||
// return getManholeGridDataRef.current?.() ?? [];
|
||||
// case "ST00020006":
|
||||
// return [
|
||||
// ...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
|
||||
// ...(rowDataMap.get("adminAreaConnectionInp") ?? []),
|
||||
// ];
|
||||
// default:
|
||||
// return [];
|
||||
// }
|
||||
// };
|
||||
|
||||
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const tabKey = activeTabRef.current;
|
||||
@@ -903,7 +903,7 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const submitFunc = async (isException: boolean) => {
|
||||
const tabKey = activeTabRef.current;
|
||||
// const tabKey = activeTabRef.current;
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
|
||||
@@ -211,7 +211,7 @@ const SewagePipeMaintainInp = () => {
|
||||
if (!job) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(job.values, { forcedTabKey: job.tabKey });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회
|
||||
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
|
||||
@@ -60,10 +60,10 @@ type GridRowWithVerification = Record<string, string | number> & {
|
||||
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
|
||||
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
|
||||
|
||||
const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
||||
if (!verification) return false;
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
};
|
||||
// const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
||||
// if (!verification) return false;
|
||||
// return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
// };
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { RealGridReact } from "realgrid-react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
@@ -6,16 +6,26 @@ import useSWR, { useSWRConfig } from "swr";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import {
|
||||
isInputPeriodCurrent,
|
||||
type CheckInputPeriodItem,
|
||||
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
|
||||
import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import type { StatsInputStatusListApiResponse } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import type { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
StatsInputStatusListApiResponse,
|
||||
StatsInputStatusListRequestParams,
|
||||
} from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import {
|
||||
ExceptionProcessingHistoryDialog,
|
||||
type ExceptionProcessingHistoryQueryContext,
|
||||
} from "@/components/module/ExceptionProcessingHistoryDialog";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
@@ -44,6 +54,10 @@ const StatsInputStatusInp = () => {
|
||||
const periodCheckListRef = useRef<CheckInputPeriodItem[]>([]);
|
||||
const inputPeriodCurrentRef = useRef(false);
|
||||
const gridRef = useRef<RealGridReact | null>(null);
|
||||
const [exceptionHistoryOpen, setExceptionHistoryOpen] = useState(false);
|
||||
const [exceptionHistoryContext, setExceptionHistoryContext] = useState<
|
||||
ExceptionProcessingHistoryQueryContext | null
|
||||
>(null);
|
||||
|
||||
const swrKey = searchParams
|
||||
? (["stats-input-status", searchParams] as const)
|
||||
@@ -56,7 +70,12 @@ const StatsInputStatusInp = () => {
|
||||
revalidateOnFocus: true,
|
||||
onSuccess: (data) => {
|
||||
if (data.status === "success" && data.data) {
|
||||
setRowData(toGridRows(data.data));
|
||||
setRowData(
|
||||
toGridRows(data.data).map((r) => ({
|
||||
...r,
|
||||
exceptionSubmitHistory: "예외제출내역",
|
||||
})),
|
||||
);
|
||||
setHasCompletedSearch(true);
|
||||
} else {
|
||||
setHasCompletedSearch(false);
|
||||
@@ -119,29 +138,104 @@ const StatsInputStatusInp = () => {
|
||||
setHasCompletedSearch(false);
|
||||
periodCheckListRef.current = [];
|
||||
inputPeriodCurrentRef.current = false;
|
||||
setExceptionHistoryOpen(false);
|
||||
setExceptionHistoryContext(null);
|
||||
};
|
||||
|
||||
const approveFunc = () => {
|
||||
const checkedRows =
|
||||
gridRef.current?.gridView?.getCheckedRows?.(true, false, true) ?? [];
|
||||
|
||||
if (rowData.length === 0 || checkedRows.length !== rowData.length) {
|
||||
alert('모든 통계 항목을 체크해주세요.');
|
||||
const approveFunc = async () => {
|
||||
if (rowData.length === 0) {
|
||||
alert("조회된 통계 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues?.year?.trim() || !lastSearchedValues?.bsnmCd?.trim()) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInvalidStatus = rowData.some((row) =>
|
||||
["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
|
||||
);
|
||||
// const hasInvalidStatus = rowData.some((row) =>
|
||||
// ["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
|
||||
// );
|
||||
|
||||
if (hasInvalidStatus) {
|
||||
alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
|
||||
return;
|
||||
// if (hasInvalidStatus) {
|
||||
// alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
if (!mdfrId) {
|
||||
alert("수정자 ID를 확인할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const statsYr = lastSearchedValues.year.trim();
|
||||
const sewerBzmnCdDefault = lastSearchedValues.bsnmCd.trim();
|
||||
|
||||
const approveItems: StatsInputStatusApproveItem[] = rowData
|
||||
.map((row) => {
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
|
||||
return { statsYr, sewerBzmnCd, statsCd, mdfrId };
|
||||
})
|
||||
.filter((item) => item.statsCd.length > 0);
|
||||
|
||||
if (approveItems.length === 0) {
|
||||
alert("통계 코드(statsCd)가 있는 행이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "INPUT");
|
||||
if (response.status === "success") {
|
||||
if (searchParams) {
|
||||
await mutate(["stats-input-status", searchParams]);
|
||||
}
|
||||
alert(response.message ?? "승인되었습니다.");
|
||||
} else {
|
||||
alert(response.message ?? "승인에 실패했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
// 승인 요청 로직
|
||||
};
|
||||
|
||||
const openExceptionHistoryForRow = useCallback(
|
||||
(row: Record<string, string | number>) => {
|
||||
const ctx = lastSearchedValues;
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
if (!statsCd) {
|
||||
alert("통계 코드가 없어 예외처리 내역을 조회할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setExceptionHistoryContext({
|
||||
statsYr: ctx.year ?? "",
|
||||
sewerBzmnCd: ctx.bsnmCd ?? "",
|
||||
ctpvNm: ctx.sido ?? "",
|
||||
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
||||
statsCd,
|
||||
});
|
||||
setExceptionHistoryOpen(true);
|
||||
},
|
||||
[alert, lastSearchedValues],
|
||||
);
|
||||
|
||||
const handleExceptionSubmitHistoryCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== "exceptionSubmitHistory") return;
|
||||
openExceptionHistoryForRow(row);
|
||||
},
|
||||
[openExceptionHistoryForRow],
|
||||
);
|
||||
|
||||
const handleExceptionHistoryRejectSuccess = useCallback(async () => {
|
||||
if (!searchParams) return;
|
||||
await mutate(["stats-input-status", searchParams]);
|
||||
}, [mutate, searchParams]);
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
|
||||
: true;
|
||||
@@ -173,8 +267,9 @@ const StatsInputStatusInp = () => {
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
showHeaderCheckbox={false}
|
||||
getGridRef={gridRef}
|
||||
onDataCellClick={handleExceptionSubmitHistoryCellClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName !== "laeSbmsnSttsNm") {
|
||||
return undefined;
|
||||
@@ -197,6 +292,12 @@ const StatsInputStatusInp = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ExceptionProcessingHistoryDialog
|
||||
open={exceptionHistoryOpen}
|
||||
onOpenChange={setExceptionHistoryOpen}
|
||||
queryContext={exceptionHistoryContext}
|
||||
onRejectSuccess={handleExceptionHistoryRejectSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import useSWR from "swr";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { StatisticsLogResponse } from "@/entities/statistics/model/statistics-interface";
|
||||
// import type { StatisticsLogResponse } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
@@ -17,11 +18,17 @@ import {
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { ReviewHistoryResponse } from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import type {
|
||||
ReviewHistoryResponse,
|
||||
StatsInputStatusReviewItem,
|
||||
} from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
|
||||
|
||||
@@ -33,6 +40,17 @@ const checkSubmitStatusCd = (code: string) => {
|
||||
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
|
||||
};
|
||||
|
||||
const isStatsInputStatusRowApprovable = (row: Record<string, string | number>) =>
|
||||
String(row.submitStatusCd ?? "") === "3" && String(row.prvnStatusCd ?? "") === "2";
|
||||
|
||||
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
||||
rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || "0"),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || "0"),
|
||||
reviewHistory: "검토이력",
|
||||
}));
|
||||
|
||||
// 하수도 통계 검토 00. 통계 입력 현황
|
||||
const StatsInputStatusRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
@@ -113,13 +131,7 @@ const StatsInputStatusRvw = () => {
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data.sewerageInputList ?? [];
|
||||
const convertedData = rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || '0'),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || '0')
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(convertedData));
|
||||
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setHasCompletedSearch(false);
|
||||
@@ -136,39 +148,127 @@ const StatsInputStatusRvw = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const approveFunc = () => console.log('approveFunc');
|
||||
const approveFunc = async () => {
|
||||
if (rowData.length === 0) {
|
||||
alert("조회된 통계 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues?.year?.trim() || !lastSearchedValues?.bsnmCd?.trim()) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!rowData.every(isStatsInputStatusRowApprovable)) {
|
||||
// alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
if (!mdfrId) {
|
||||
alert("수정자 ID를 확인할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const statsYr = lastSearchedValues.year.trim();
|
||||
const sewerBzmnCdDefault = lastSearchedValues.bsnmCd.trim();
|
||||
|
||||
const approveItems: StatsInputStatusApproveItem[] = rowData
|
||||
.map((row) => {
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
|
||||
return { statsYr, sewerBzmnCd, statsCd, mdfrId };
|
||||
})
|
||||
.filter((item) => item.statsCd.length > 0);
|
||||
|
||||
if (approveItems.length === 0) {
|
||||
alert("통계 코드(statsCd)가 있는 행이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "REVIEW");
|
||||
if (response.status === "success") {
|
||||
try {
|
||||
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
|
||||
ctpvNm: lastSearchedValues.sido,
|
||||
sggNm: lastSearchedValues.gugun === "ALL" ? "" : lastSearchedValues.gugun,
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd,
|
||||
});
|
||||
if (listResponse.status === "success" && listResponse.data) {
|
||||
const rawList = listResponse.data.sewerageInputList ?? [];
|
||||
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
||||
}
|
||||
} catch {
|
||||
/* 목록 갱신 실패는 별도 안내 없이 생략 */
|
||||
}
|
||||
alert(response.message ?? "승인되었습니다.");
|
||||
} else {
|
||||
alert(response.message ?? "승인에 실패했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
|
||||
: true;
|
||||
|
||||
const allSubmitStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.submitStatusCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
const showActionToolbarCard =
|
||||
hasCompletedSearch && rowData.length > 0 && gugunOkOrNotDoList;
|
||||
hasCompletedSearch &&
|
||||
rowData.length > 0 &&
|
||||
gugunOkOrNotDoList &&
|
||||
!allSubmitStatusApproved;
|
||||
|
||||
const handleRowClick = async (row: Record<string, string | number>) => {
|
||||
setHistorySearchInfo({
|
||||
ctpvNm: String(row.ctpvNm ?? ""),
|
||||
sggNm: String(row.sggNm ?? ""),
|
||||
tabNm: String(row.tabNm ?? ""),
|
||||
});
|
||||
const params = {
|
||||
statsYr: searchedYear,
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsManagementApi.getReviewHistory(params);
|
||||
|
||||
if (response.status === 'success') {
|
||||
setHistoryRowData(response.data);
|
||||
setReviewHistoryOpen(true);
|
||||
} else {
|
||||
alert(response.message || '검토 이력 조회 중 오류가 발생했습니다.');
|
||||
const openReviewHistoryForRow = useCallback(
|
||||
async (row: Record<string, string | number>) => {
|
||||
if (!searchedYear) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
alert('검토 이력 조회 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
setHistorySearchInfo({
|
||||
ctpvNm: String(row.ctpvNm ?? ""),
|
||||
sggNm: String(row.sggNm ?? ""),
|
||||
tabNm: String(row.tabNm ?? ""),
|
||||
});
|
||||
const params = {
|
||||
statsYr: searchedYear,
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsManagementApi.getReviewHistory(params);
|
||||
|
||||
if (response.status === "success") {
|
||||
setHistoryRowData(response.data);
|
||||
setReviewHistoryOpen(true);
|
||||
} else {
|
||||
alert(response.message || "검토 이력 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("검토 이력 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
[alert, searchedYear],
|
||||
);
|
||||
|
||||
const handleReviewHistoryCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== "reviewHistory") return;
|
||||
void openReviewHistoryForRow(row);
|
||||
},
|
||||
[openReviewHistoryForRow],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -193,12 +293,12 @@ const StatsInputStatusRvw = () => {
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
showHeaderCheckbox={false}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
onRowClick={handleRowClick}
|
||||
onDataCellClick={handleReviewHistoryCellClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName === "submitStatusNm") {
|
||||
const statusCd = String(row.submitStatusCd ?? "");
|
||||
|
||||
@@ -279,6 +279,16 @@ export type CellEditedCallback = (
|
||||
params: CellEditedCallbackParams,
|
||||
) => void;
|
||||
|
||||
/** 데이터 셀 클릭 시 컬럼(필드)까지 전달 — 특정 컬럼만 반응할 때 사용 */
|
||||
export type DataCellClickCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
fieldName: string;
|
||||
itemIndex: number;
|
||||
};
|
||||
export type DataCellClickCallback = (
|
||||
params: DataCellClickCallbackParams,
|
||||
) => void;
|
||||
|
||||
export type DataGridProps = {
|
||||
columnInfo: ColumnInfo;
|
||||
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
|
||||
@@ -306,6 +316,8 @@ export type DataGridProps = {
|
||||
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
|
||||
getGridRef?: React.MutableRefObject<RealGridReact | null>;
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
/** 데이터 셀 클릭 시 행 + 클릭한 필드명 전달 (`onRowClick`과 함께 쓸 수 있음) */
|
||||
onDataCellClick?: DataCellClickCallback;
|
||||
/** CheckBar 행 체크 변경 */
|
||||
onItemChecked?: (itemIndex: number, checked: boolean) => void;
|
||||
/** CheckBar 헤더 전체 선택/해제 */
|
||||
|
||||
@@ -10,6 +10,14 @@ export const StatsInputStatusInpColumn = {
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false, editable: false },
|
||||
{ name: 'menuTabName', fieldName: 'menuTabName', header: { text: '구분' }, editable: false, width: 220 },
|
||||
{
|
||||
name: 'exceptionSubmitHistory',
|
||||
fieldName: 'exceptionSubmitHistory',
|
||||
header: { text: '예외제출내역' },
|
||||
editable: false,
|
||||
width: 118,
|
||||
styleName: 'rg-cell-outline-action-btn',
|
||||
},
|
||||
{ name: 'laeSbmsnSttsNm', fieldName: 'laeSbmsnSttsNm', header: { text: '진행상황' }, editable: false },
|
||||
{ name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false },
|
||||
{ name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false },
|
||||
@@ -27,6 +35,7 @@ export const StatsInputStatusInpColumn = {
|
||||
'sggNm',
|
||||
'statsCd',
|
||||
'menuTabName',
|
||||
'exceptionSubmitHistory',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const LogHistoryColumn = {
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsYr', fieldName: 'statsYr', header: { text: '연도' }, editable: false },
|
||||
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '통계항목' }, editable: false },
|
||||
{ name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false },
|
||||
{ name: 'statsDataChgComNm', fieldName: 'statsDataChgComNm', header: { text: '요청 기능명' }, editable: false },
|
||||
{ name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false, visible: false },
|
||||
|
||||
@@ -9,6 +9,13 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
{ name: 'sewerBzmnCd', fieldName: 'sewerBzmnCd', header: { text: '사업자코드' }, visible: false },
|
||||
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false },
|
||||
{ name: 'tabNm', fieldName: 'tabNm', header: { text: '구분' }, width: 220 },
|
||||
{
|
||||
name: 'reviewHistory',
|
||||
fieldName: 'reviewHistory',
|
||||
header: { text: '검토이력' },
|
||||
width: 118,
|
||||
styleName: 'rg-cell-outline-action-btn',
|
||||
},
|
||||
|
||||
{ name: 'chargerNm', fieldName: 'chargerNm', header: { text: '제출자' } },
|
||||
{ name: 'chargerCttpc', fieldName: 'chargerCttpc', header: { text: '연락처' } },
|
||||
@@ -22,6 +29,7 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'tabNm',
|
||||
'reviewHistory',
|
||||
'chargerNm',
|
||||
'chargerCttpc',
|
||||
'submitStatusNm',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ValueType } from "realgrid";
|
||||
*/
|
||||
|
||||
/** ST00020003 관종별: 입력 `SeweragePipeInputPipeTypeStatusItem`·합계 `SewagePipePipeTypeFacilityStatusItem`과 동일 (yy1=1996년 이전 … yy30) */
|
||||
const YY_PIPE_TYPE_FACILITY_MAX = 30;
|
||||
// const YY_PIPE_TYPE_FACILITY_MAX = 30;
|
||||
|
||||
const rawSewagePipeTotList = [
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type ImrStatusResponse,
|
||||
type ReviewerInfoRequest,
|
||||
type ReviewerInfoResponse,
|
||||
type ReviewInputSaveRequest,
|
||||
type ReviewOpinionSaveRequest,
|
||||
type ReviewOpinionSaveResponse,
|
||||
type ReviewReturnSaveRequest,
|
||||
@@ -33,6 +34,9 @@ import {
|
||||
type StatsInputPeriodListRequest,
|
||||
type StatsInputPeriodSaveApiResponse,
|
||||
type StatsInputPeriodSaveRequest,
|
||||
type StatsInputStatusApproveBatchRequest,
|
||||
type StatsInputStatusApproveKind,
|
||||
type StatsInputStatusApproveResponse,
|
||||
type SubmitExceptionRequest,
|
||||
type SubmitExceptionResponse,
|
||||
type SubmitVerifyExceptionRequest,
|
||||
@@ -214,6 +218,56 @@ export class StatisticsApiService implements StatisticsService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 입력 반려 — JSON body는 `ReviewInputSaveRequest`(type 제외) 항목의 배열, `type`은 쿼리 */
|
||||
public async saveInputReturn(
|
||||
items: ReviewInputSaveRequest[],
|
||||
): Promise<ReviewReturnSaveResponse> {
|
||||
if (items.length === 0) {
|
||||
throw new Error("saveInputReturn: 항목이 비어 있습니다.");
|
||||
}
|
||||
const [first] = items;
|
||||
if (!first) {
|
||||
throw new Error("saveInputReturn: 항목이 비어 있습니다.");
|
||||
}
|
||||
const type = first.type;
|
||||
if (!items.every((i) => i.type === type)) {
|
||||
throw new Error("saveInputReturn: 모든 항목의 type이 같아야 합니다.");
|
||||
}
|
||||
const body = items.map(({ type: _t, rjctOpnnCn, ...rest }) => {
|
||||
const row: Record<string, string> = {
|
||||
statsYr: rest.statsYr,
|
||||
sewerBzmnCd: rest.sewerBzmnCd,
|
||||
statsCd: rest.statsCd,
|
||||
mdfrId: rest.mdfrId,
|
||||
};
|
||||
if (rest.vrfcSn != null && String(rest.vrfcSn).trim() !== "") {
|
||||
row.vrfcSn = String(rest.vrfcSn);
|
||||
}
|
||||
const trimmed = rjctOpnnCn?.trim();
|
||||
if (trimmed) row.rjctOpnnCn = trimmed;
|
||||
return row;
|
||||
});
|
||||
const response = await stClient.post<ReviewReturnSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.STATS}/reject`,
|
||||
body,
|
||||
{ params: { type } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 — 쿼리 `type`(기본 `INPUT`), body는 `{ list }` */
|
||||
public async approveStatsInputStatus(
|
||||
param: StatsInputStatusApproveBatchRequest,
|
||||
type: StatsInputStatusApproveKind = "INPUT",
|
||||
): Promise<StatsInputStatusApproveResponse> {
|
||||
const response = await stClient.post<StatsInputStatusApproveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.STATS}/approve`,
|
||||
param.list,
|
||||
{ params: { type } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitReview(
|
||||
param: ReviewSubmitRequest,
|
||||
): Promise<ReviewSubmitResponse> {
|
||||
|
||||
@@ -74,6 +74,12 @@ export {
|
||||
type ImrLogResponse,
|
||||
} from "./model/imr/imr-interface";
|
||||
export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns";
|
||||
export {
|
||||
type StatsInputStatusApproveBatchRequest,
|
||||
type StatsInputStatusApproveItem,
|
||||
type StatsInputStatusApproveKind,
|
||||
type StatsInputStatusApproveResponse,
|
||||
} from "./model/statistics-interface";
|
||||
export { type StatsInputStatusList } from "./model/input/statistics-input-status-interface";
|
||||
export { userListColumns } from "./model/notice-talk/notice-talk-sending-columns";
|
||||
export {
|
||||
|
||||
@@ -340,6 +340,7 @@ export interface ReviewReturnSaveRequest {
|
||||
statsCd: string;
|
||||
rjctOpnnCn: string;
|
||||
mdfrId: string;
|
||||
vrfcSn?: string;
|
||||
}
|
||||
|
||||
export interface ReviewReturnSaveResponse {
|
||||
@@ -349,6 +350,17 @@ export interface ReviewReturnSaveResponse {
|
||||
updatedStts: string;
|
||||
}
|
||||
|
||||
export interface ReviewInputSaveRequest {
|
||||
type: "INPUT";
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
/** 미입력·빈 문자열이면 요청 body에 포함하지 않음 */
|
||||
rjctOpnnCn?: string;
|
||||
mdfrId: string;
|
||||
vrfcSn?: string;
|
||||
}
|
||||
|
||||
export interface ReviewSubmitRequest {
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
@@ -363,6 +375,28 @@ export interface ReviewSubmitResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 통계 일괄 승인 `/stats/approve` 쿼리 파라미터 `type` */
|
||||
export type StatsInputStatusApproveKind = "INPUT" | "REVIEW";
|
||||
|
||||
/** 통계 입력 현황 승인 요청 항목 (행 단위) */
|
||||
export interface StatsInputStatusApproveItem {
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
mdfrId: string;
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 API 요청 본문 */
|
||||
export interface StatsInputStatusApproveBatchRequest {
|
||||
list: StatsInputStatusApproveItem[];
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 API 응답 */
|
||||
export interface StatsInputStatusApproveResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerifyExceptionItem {
|
||||
vrfcSn: number;
|
||||
prcsOpnnExpln: string;
|
||||
@@ -416,6 +450,11 @@ export interface StatisticsService {
|
||||
saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise<ReviewOpinionSaveResponse>;
|
||||
getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse>;
|
||||
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
|
||||
saveInputReturn(items: ReviewInputSaveRequest[]): Promise<ReviewReturnSaveResponse>;
|
||||
approveStatsInputStatus(
|
||||
param: StatsInputStatusApproveBatchRequest,
|
||||
type?: StatsInputStatusApproveKind,
|
||||
): Promise<StatsInputStatusApproveResponse>;
|
||||
submitReview(param: ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
|
||||
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
|
||||
getImrStatus(param: string): Promise<ImrStatusResponse>;
|
||||
|
||||
Reference in New Issue
Block a user