Compare commits

...
Author SHA1 Message Date
yangsein 7ffc02e887 refactor: 대시보드 통계처리현황 표준화 수정 2026-10-14 15:17:25 +09:00
yangsein cbacb27bb1 refactor: 행정동 갱신 표준화 수정 2026-10-14 15:16:40 +09:00
yangsein 5331e10c96 [refactor] 대시보드 - 통계처리현황 반려되어 제출중인 상태 구분 처리 포함 2026-09-30 16:59:05 +09:00
rsone001 de8971bea6 [ST] fix: 대시보드 통계 처리 현황 panel css 수정 2026-09-08 13:43:26 +09:00
rsone001 a7fd6a8e16 [ST] - fix: 취약점 점검 관련 조치 2026-09-07 14:26:00 +09:00
rsone001 8039ab8281 [ST] - refactor: 2-5 맨홀 지표 변경된 테이블에 맞게 수정, 일반검증 항목 입력기간일 시 등록/수정 불가 2026-09-04 16:05:26 +09:00
rsone001 794691f05e [ST] - fix: 제출결과(검증)화면에 시설명 컬럼 추가 2026-09-03 16:29:40 +09:00
rsone001 e58b84a01b [ST] - fix: input 제출 - 시계열 검증 요청 response & request 수정, 대시보드 구성 수정 2026-09-03 15:27:19 +09:00
rsone001 3d51f003c0 Merge remote-tracking branch 'origin/feature/sein' into iipbs/st/dev 2026-09-03 13:52:34 +09:00
yangsein b5a99e5f59 [feat] 대쉬 보드 일정관리 제거 , 통계 현황상태 추가 2026-09-03 13:44:09 +09:00
rsone001 afca685fd4 [ST] - fix: 2-3 매설연장 관리자일시 수정 가능하도록 적용, SearchFrom 로직 수정 2026-09-03 11:26:23 +09:00
rsone001 261941452a [ST] fix - 2-2 지표 컬럼 설정 및 저장 제출 로직 수정 2026-09-02 16:41:12 +09:00
rsone001 8576ef83b5 [ST] - refactor: 6-1 중심처리시설 row는 위탁계약기간 직접 수정 못하도록 적용 2026-09-01 16:25:55 +09:00
rsone001 e80b8953a0 [ST] - fix: 일반검증 수식 사용자 직접입력 등록시 공백제거 2026-09-01 14:41:49 +09:00
rsone001 e8b309460e [ST] input 특광역시 로직 수정, 일반검증 엑셀 다운로드 추가, 모든 number 타입 컬럼에 footer 추가 2026-08-28 17:08:20 +09:00
chanii8 37ac927c45 [st] feat: 시계열 검토 이력 > 지자체 진입 시 시도/시군구 자동 선택 2026-08-28 17:05:40 +09:00
chanii8 bf9a34cbe8 [st] feat: 하수도 통계 검증 > 시도/시군구 셀렉트에 '전체' 옵션 추가 2026-08-28 15:55:05 +09:00
rsone001 18a11ab5fa [ST] - refactor: 입력화면 전용 시도 조회 api 추가 2026-08-27 17:22:34 +09:00
rsone001 214ff1a435 [ST] - fix: 일반 검증 타입 총계 검증일 시 group by 항목 렌더링 되지 않게 수정 2026-08-27 15:44:45 +09:00
rsone001 d7cc579335 [ST] - refactor: 지난검증결과 불러오기 필드 추가(시설명) 2026-08-27 15:32:12 +09:00
rsone001 9b53c1ae08 [ST] - fix: 일반검증 항목 관리, 참조 지표(컬럼) 오류 로직 수정 2026-08-27 13:07:33 +09:00
rsone001 64ed53cf4d [ST] refactor: input - 자동 계산 cell 부동소수점 문제 해결을 위해 decimal.js 라이브러리 추가 및 계산 로직 수정, 6-2 지표 로직 수정 2026-08-25 16:04:31 +09:00
rsone001 043fd3d4a8 [ST] - 입력 6-1 공공하수처리시설 컬럼 설정 변경 2026-08-24 17:19:33 +09:00
rsone001 6dd09971f1 [ST] - 컬럼 자동 계산식 오류 수정 2026-08-24 16:39:24 +09:00
chanii8 23424b5eee [st] chore: 통계 검증 리포트 초기 조회 파라미터 수정 2026-08-24 14:09:12 +09:00
chanii8 3f190dfff6 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-21 17:27:37 +09:00
chanii8 6cc02da7a4 [st] feat: 통계 검증 리포트 목록 조회 API 수정 2026-08-21 17:27:30 +09:00
rsone001 c87ccf8788 [ST] - fix: 입력 저장&제출시 빈값 null처리 2026-08-21 14:56:34 +09:00
rsone001 8e757ab9db Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-20 16:31:13 +09:00
rsone001 de7e66afb4 [ST] - fix: 11-4 데이터 체크 수정 2026-08-20 16:31:00 +09:00
chanii8 c981b83284 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-20 13:07:50 +09:00
chanii8 927bb7cb3a [st] chore: eslint 해결 2026-08-20 11:48:18 +09:00
chanii8 725976b5c0 [st] feat: 시도/시군구 조회 쿼리 변경 2026-08-20 11:47:50 +09:00
rsone001 cdbe6a855f [ST] - fix: 검토 하수관로 컬럼 수정 및 엑셀 내보내기 셀 스타일 수정 2026-08-19 12:31:37 +09:00
chanii8 a1c96e9e98 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-19 11:58:06 +09:00
chanii8 1488805242 [as] feat: 통계 검증 리포트 새로고침 버튼 추가 2026-08-19 11:57:58 +09:00
rsone001 5b12f81b8f [ST] - fix: 통계 집계표 조회 기본값 수정, 엑셀 다운로드 스타일 적용, 요약표 마우스 툴팁 추가 / 입력단 오류 수정 및 검토단 컬럼 설정 수정 2026-08-18 17:44:22 +09:00
rsone001 df24e7c2a8 [ST] - fix: 일반 검증 수식 안내 문구 수정 2026-08-13 16:20:54 +09:00
rsone001 3b5fee047b [ST] - fix: qa 조치, 일반 검증 개발 수정 2026-08-13 15:30:00 +09:00
rsone001 181730bcde [ST] - fix: 입력: 새로고침 시 열려있는 모든 텝의 공통 api가 실행되는 현상 수정, 하수도보급률 저장&제출 request 수정(외국인총인구 제거, 총면적 추가), 검증 툴팁 max-width 추가, 지난 검증 결과 및 제출 결과 데이터 마우스 툴팁 추가 2026-08-10 17:58:05 +09:00
chanii8 678af2f06e [st] chore: 승인 정보 입력 폼 수정 2026-08-10 15:48:19 +09:00
rsone001 82d67bb4c6 [ST] - fix: 검증 항목 명칭 변경 2026-08-07 17:22:08 +09:00
rsone001 85ef1805e5 [ST] - 공단 오류 및 개선 요청사항 처리 2026-08-07 16:24:39 +09:00
chanii8 26f64988ef [st] typo: 404 페이지 문구 수정 2026-08-06 17:06:00 +09:00
chanii8 29e39f98af Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-06 15:15:09 +09:00
chanii8 5192f3e3c0 [st] feat: 404 페이지 구현 2026-08-06 15:15:04 +09:00
rsone001 83de317308 Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev 2026-08-05 10:09:58 +09:00
rsone001 84b1fd07ae [ST] - management service code refresh 로직 추가 2026-08-05 10:08:26 +09:00
chanii8 a88f8ab33e [st] feat: 로그아웃 라우트 핸들러 적용 2026-08-03 15:56:48 +09:00
chanii8 c67b5844b6 [st] chore: metadata 수정 2026-08-03 11:14:48 +09:00
259 changed files with 8567 additions and 4812 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import "@/app/styles/base.css"; import "@/app/styles/base.css";
export const metadata = { export const metadata = {
title: "유역하수도 통합 정보플랫폼", title: "하수도통계 관리시스템",
}; };
export default function AuthLayout({ export default function AuthLayout({
@@ -97,7 +97,7 @@ function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
if (!getToken(CLAIM_NAME)) return; if (!getToken(CLAIM_NAME)) return;
void statisticsApi.refreshCommonCodes().catch(() => { void statisticsApi.refreshAllCommonCodes().catch(() => {
// 캐시 동기화 실패는 화면 진입을 막지 않음 // 캐시 동기화 실패는 화면 진입을 막지 않음
}); });
}, []); }, []);
+2 -1
View File
@@ -1,8 +1,9 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { App } from "@/app"; import { App } from "@/app";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "유역하수도 통합 정보플랫폼", title: "하수도통계 관리시스템",
}; };
export default App; export default App;
+10
View File
@@ -0,0 +1,10 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { COOKIE_DOMAIN, REFRESH_TOKEN } from "@/shared/config";
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete({ name: REFRESH_TOKEN, domain: COOKIE_DOMAIN });
return NextResponse.json({ message: "Logged out" }, { status: 200 });
}
+47
View File
@@ -0,0 +1,47 @@
import type { Metadata } from "next";
import Image from "next/image";
import "@/app/styles/base.css";
export const metadata: Metadata = {
title: "하수도통계 관리시스템",
description: "페이지가 존재하지 않습니다. 관리자에게 문의하여 주십시오.",
};
export default function GlobalNotFound() {
return (
<html lang="ko">
<body>
<main className="flex min-h-screen flex-col items-center justify-center bg-[#f0f4f8] px-4 py-16">
<div className="mb-16 text-center">
<h1 className="text-[3.2rem] font-bold leading-snug tracking-tight">
<span className="text-primary">
하수도통계 관리시스템
</span>
에 오신 것을 환영합니다.
</h1>
<p className="mt-6 text-[2rem] text-[#666]">
오류가 발생하였습니다.
</p>
</div>
<section className="card card-border flex w-full max-w-[72rem] flex-col items-center !rounded-2xl !py-16">
<Image
src="/images/magnifying-glass-with-warning.png"
alt=""
width={280}
height={280}
className="size-[28rem] object-contain"
/>
<p className="mt-4 text-[2.4rem] font-bold text-[#444]">
페이지가 존재하지 않습니다.
</p>
<p className="mt-2 text-[1.6rem] text-[#666]">
관리자에게 문의하여 주십시오
</p>
</section>
</main>
</body>
</html>
);
}
+3
View File
@@ -5,6 +5,9 @@ const subDomain =
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
experimental: {
globalNotFound: true,
},
async headers() { async headers() {
return [ return [
{ {
+1
View File
@@ -41,6 +41,7 @@
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"chartjs-plugin-zoom": "^2.2.0", "chartjs-plugin-zoom": "^2.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"decimal.js": "^10.6.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.474.0", "lucide-react": "^0.474.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

+7 -5
View File
@@ -1,12 +1,14 @@
import { cookies } from "next/headers";
import Script from "next/script";
import "@fontsource-variable/noto-sans-kr";
import { AppProvider } from "./providers/app-provider";
import "./styles/base.css"; import "./styles/base.css";
import "./styles/components.css"; import "./styles/components.css";
import "@fontsource-variable/noto-sans-kr";
import "realgrid/dist/realgrid-style.css"; import "realgrid/dist/realgrid-style.css";
import "../components/css/DataGrid.css"; import "../components/css/DataGrid.css";
import { cookies } from "next/headers";
import { AppProvider } from "./providers/app-provider";
import Script from "next/script";
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => { const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
const theme = (await cookies()).get("site-theme-key"); const theme = (await cookies()).get("site-theme-key");
+12 -10
View File
@@ -1,20 +1,22 @@
"use client"; "use client";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { MultiBackend, getBackendOptions } from "@minoru/react-dnd-treeview";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { getBackendOptions,MultiBackend } from "@minoru/react-dnd-treeview";
import { AlertDialog as RsoneAlertDialog } from "@rsone/components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { PromptProvider, StoreProvider } from "@/shared/providers";
import { AlertDialog } from "@/shared/ui/alert-dialog";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay";
import { PromptDialog } from "@/shared/ui/prompt-dialog";
import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog";
import { Toaster } from "@/shared/ui/sonner";
import { AuthWrapper } from "@/entities/auth"; import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu"; import { MenuWrapper } from "@/entities/menu";
import { Toaster } from "@/shared/ui/sonner";
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";
const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -1,6 +1,7 @@
import { Button } from "@common/components";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
interface LastYearDataButtonProps { interface LastYearDataButtonProps {
getLastYearDataFunc: () => void; getLastYearDataFunc: () => void;
} }
@@ -1,6 +1,7 @@
import { Checkbox, buttonVariants, cn } from "@common/components";
import type React from "react"; import type React from "react";
import { buttonVariants, Checkbox, cn } from "@common/components";
interface NotApplicableCheckButtonProp { interface NotApplicableCheckButtonProp {
isCheck: boolean; isCheck: boolean;
setIsCheck: React.Dispatch<React.SetStateAction<boolean>>; setIsCheck: React.Dispatch<React.SetStateAction<boolean>>;
@@ -2,15 +2,17 @@ import { Button } from "@common/components";
interface SubmitButtonProps { interface SubmitButtonProps {
submitFunc: () => void; submitFunc: () => void;
/** 버튼 문구 (기본: 제출) */
text?: string;
} }
const SubmitButton = ({ submitFunc }: SubmitButtonProps) => { const SubmitButton = ({ submitFunc, text = "제출" }: SubmitButtonProps) => {
return ( return (
<Button className="h-[3.2rem] w-[96px]" onClick={submitFunc}> <Button className="h-[3.2rem] min-w-[96px] px-4" onClick={submitFunc}>
<i className="diveicon di-upload" /> <i className="diveicon di-upload" />
<span>제출</span> <span>{text}</span>
</Button> </Button>
); );
}; };
export default SubmitButton; export default SubmitButton;
+49
View File
@@ -1,5 +1,22 @@
@tailwind components; @tailwind components;
/* RealGrid 기본은 white-space: nowrap → max-width만 주면 잘림. 너비 초과 시에만 줄바꿈 */
.rg-tooltip {
max-width: 60rem;
white-space: pre-line;
}
/* RealGrid Excel export — applyDynamicStyles가 class background-color를 읽을 수 있도록 */
.rg-cell-rowtype-biz-sum {
background-color: #ffe9cf !important;
}
.rg-cell-rowtype-summary {
background-color: #feee85 !important;
}
.rg-cell-rowtype-other {
background-color: #fff7bf !important;
}
@layer components { @layer components {
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
@apply !bg-white; @apply !bg-white;
@@ -8,6 +25,15 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f0f0f0] !text-[#666]; @apply !bg-[#f0f0f0] !text-[#666];
} }
/* 고정 컬럼 — readonly와 동일한 화면 스타일 (엑셀 export 시 exportStyleName에서 제외) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly,
.rg-grid .rg-body .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-fixed-body .rg-table .rg-data-row td.rg-cell-readonly,
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-readonly {
@apply !bg-[#f0f0f0] !text-[#666];
}
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-readonly.rg-cell-rowtype-curr {
@apply !bg-white !text-[#333]; @apply !bg-white !text-[#333];
} }
@@ -17,6 +43,15 @@
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr {
@apply !bg-[#fff7bf] !text-[#b45309]; @apply !bg-[#fff7bf] !text-[#b45309];
} }
/* 총계 footer 검증 오류 (commonInfo.verificationResult) */
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error {
@apply !bg-[#ffe6e6] !text-[#c00];
}
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr {
@apply !bg-[#fff7bf] !text-[#b45309];
}
/* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */ /* 검증결과 내역: 처리여부(prcsYn 0·4 미처리 등) */
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
@apply !font-semibold !text-[#dc2626]; @apply !font-semibold !text-[#dc2626];
@@ -399,6 +434,13 @@
background-color: #ffe6e6 !important; background-color: #ffe6e6 !important;
color: #c00 !important; color: #c00 !important;
} }
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error.rg-cell-verify-error {
background-color: #ffe6e6 !important;
color: #c00 !important;
}
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr, .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
@@ -406,6 +448,13 @@
background-color: #fff7bf !important; background-color: #fff7bf !important;
color: #b45309 !important; color: #b45309 !important;
} }
.rg-grid .rg-footer .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footer .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
.rg-grid .rg-footers .rg-table .rg-footer-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
background-color: #fff7bf !important;
color: #b45309 !important;
}
/* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */ /* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error, [data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
@@ -1,10 +1,13 @@
import { useRef, useEffect, useLayoutEffect } from "react" import { useEffect, useLayoutEffect,useRef } from "react"
import { ValueType } from "realgrid"
import { ValueType } from "realgrid"
import RealGrid, { GridFitStyle } from "realgrid" import RealGrid, { GridFitStyle } from "realgrid"
import { RealGridReact, RGDataColumn } from "realgrid-react" import { RealGridReact, RGDataColumn } from "realgrid-react"
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, CellStyleCallback, CellStyleCallbackResult, CellStyleCallbackParams } from "../type/DataGrid.type"
import type { CellStyleCallback, CellStyleCallbackParams,CellStyleCallbackResult, DataGridProps, FooterItemWithCallback, GridViewWithSetColumns, RealGridColumnInfo } from "../type/DataGrid.type"
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
function mergeCellStyleCallbackResult( function mergeCellStyleCallbackResult(
baseStyleName: string | undefined, baseStyleName: string | undefined,
@@ -30,7 +33,7 @@ declare module "realgrid" {
setOptions(options: { setOptions(options: {
footer?: { visible?: boolean }; footer?: { visible?: boolean };
summaryMode?: 'aggregate' | 'statistical' | 'none'; summaryMode?: 'aggregate' | 'statistical' | 'none';
checkBar?: { visible?: boolean }; checkBar?: { visible?: boolean; checkableOnly?: boolean };
}): void; }): void;
} }
} }
@@ -47,10 +50,11 @@ type GridViewWithCheckApi = GridViewWithSetColumns & {
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void; checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void; checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
checkAll?: (checked: boolean) => void; checkAll?: (checked: boolean) => void;
setCheckable?: (itemIndex: number, checkable: boolean) => void;
}; };
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */ /** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) { function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
return columns.map((col) => { return columns.map((col) => {
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = { const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
fieldName: col.fieldName, fieldName: col.fieldName,
@@ -66,7 +70,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
} }
} }
return field; return field;
@@ -136,6 +140,7 @@ const CompactDataGrid = ({
onRowClick, onRowClick,
onItemChecked, onItemChecked,
onItemAllChecked, onItemAllChecked,
checkableCallback,
compactFullWidth = false, compactFullWidth = false,
compactGridHeightClass = "h-[55vh]", compactGridHeightClass = "h-[55vh]",
headerHeight = 60, headerHeight = 60,
@@ -145,9 +150,11 @@ const CompactDataGrid = ({
const columnsRef = useRef(columns); const columnsRef = useRef(columns);
const rowDataRef = useRef<Record<string, string | number>[]>(rowData); const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback); const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
const checkableCallbackRef = useRef<DataGridProps["checkableCallback"]>(checkableCallback);
columnsRef.current = columns; columnsRef.current = columns;
rowDataRef.current = rowData; rowDataRef.current = rowData;
cellStyleCallbackRef.current = cellStyleCallback; cellStyleCallbackRef.current = cellStyleCallback;
checkableCallbackRef.current = checkableCallback;
useLayoutEffect(() => { useLayoutEffect(() => {
if (!cellStyleCallback || !gridRef.current?.gridView) return; if (!cellStyleCallback || !gridRef.current?.gridView) return;
@@ -176,8 +183,10 @@ const CompactDataGrid = ({
try { try {
checkGrid.checkAll?.(false); checkGrid.checkAll?.(false);
rowData.forEach((row, itemIndex) => { rowData.forEach((row, itemIndex) => {
const checkable = checkableCallbackRef.current?.(row, itemIndex) ?? true;
checkGrid.setCheckable?.(itemIndex, checkable);
const checked = checkByRowFlag((row as Record<string, unknown>).__checked); const checked = checkByRowFlag((row as Record<string, unknown>).__checked);
if (!checked) return; if (!checked || !checkable) return;
if (checkGrid.checkItem) { if (checkGrid.checkItem) {
checkGrid.checkItem(itemIndex, true, false); checkGrid.checkItem(itemIndex, true, false);
return; return;
@@ -274,7 +283,7 @@ const CompactDataGrid = ({
// 헤더 체크박스 표시 여부 설정 // 헤더 체크박스 표시 여부 설정
gridView.setOptions({ gridView.setOptions({
checkBar: { visible: showHeaderCheckbox }, checkBar: { visible: showHeaderCheckbox, checkableOnly: true },
rowIndicator: { visible: false }, rowIndicator: { visible: false },
}); });
@@ -306,6 +315,7 @@ const CompactDataGrid = ({
if (fixedColumns !== undefined && fixedColumns > 0) { if (fixedColumns !== undefined && fixedColumns > 0) {
gridView.setFixedOptions({ gridView.setFixedOptions({
colCount: fixedColumns, colCount: fixedColumns,
resizable: true,
}); });
} }
@@ -496,6 +506,7 @@ const CompactDataGrid = ({
compactFullWidth, compactFullWidth,
compactGridHeightClass, compactGridHeightClass,
headerHeight, headerHeight,
checkableCallback,
]); ]);
// 행 클릭 이벤트 // 행 클릭 이벤트
@@ -524,6 +535,17 @@ const CompactDataGrid = ({
if (onItemChecked) { if (onItemChecked) {
gridView.onItemChecked = (_grid, itemIndex, checked) => { gridView.onItemChecked = (_grid, itemIndex, checked) => {
const row = rowDataRef.current[itemIndex];
if (
row &&
checkableCallbackRef.current &&
!checkableCallbackRef.current(row, itemIndex)
) {
if (checked) {
(_grid as GridViewWithCheckApi).checkItem?.(itemIndex, false, false);
}
return;
}
onItemChecked(itemIndex, checked); onItemChecked(itemIndex, checked);
}; };
} else { } else {
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,6 @@ import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback,
import { RealGridReact } from "realgrid-react"; import { RealGridReact } from "realgrid-react";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
@@ -29,6 +28,8 @@ import {
Textarea, Textarea,
} from "@common/components"; } from "@common/components";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
@@ -56,8 +57,8 @@ type ExceptionHistoryTabKey = "NV" | "IMR" | "HB";
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [ const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" }, { value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" }, { value: "IMR", label: "시계열 검증내역" },
{ value: "HB", label: "HB 검증내역" }, { value: "HB", label: "모집단 검증내역" },
]; ];
const TITLE = "예외처리 내역 조회"; const TITLE = "예외처리 내역 조회";
@@ -274,7 +275,7 @@ export function ExceptionProcessingHistoryDialog({
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setImrPageIndex(pageIndex); setImrPageIndex(pageIndex);
} catch { } catch {
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); setErrorMessage("시계열 검증내역 조회 중 오류가 발생했습니다.");
} finally { } finally {
setImrLoading(false); setImrLoading(false);
} }
@@ -293,7 +294,7 @@ export function ExceptionProcessingHistoryDialog({
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1); setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
setHbPageIndex(pageIndex); setHbPageIndex(pageIndex);
} catch { } catch {
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다."); setErrorMessage("모집단 검증내역 조회 중 오류가 발생했습니다.");
} finally { } finally {
setHbLoading(false); setHbLoading(false);
} }
@@ -1,11 +1,13 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components"; import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from "@common/components";
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
import type { SearchFormValues } from "../type/SearchForm.type"; import type { SearchFormValues } from "../type/SearchForm.type";
import SidoSggFacilityNameSearchForm from "./SidoSggFacilityNameSearchForm";
type GridTopActionButtonProps = { type GridTopActionButtonProps = {
text: string; text: string;
onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void; onClick?: (helpers: { openDialog: (title?: string) => void; closeDialog: () => void }) => void;
+158 -52
View File
@@ -5,6 +5,11 @@ import { useEffect, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import {
fetchSearchCondition,
fetchSearchConditionInput,
SEARCH_CONDITION_SWR_KEY,
} from "@/shared/lib/use-search-condition-sido-list";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
ApiResponse, ApiResponse,
@@ -23,28 +28,43 @@ import {
SelectValue, SelectValue,
} from "@common/components"; } from "@common/components";
import { doList, doNameList } from "@/components/util/column/input/DoList";
import { import {
defaultSearchFormProps, defaultSearchFormProps,
type SearchFormClsfCd, type SearchFormClsfCd,
type SearchFormProps, type SearchFormProps,
type SearchFormValues, type SearchFormValues,
} from "../type/SearchForm.type"; } from "../type/SearchForm.type";
import { doList } from "../util/column/input/DoList";
const getSidoListFetcher = async ( function isDoListBsnm(bsnmCd: string | undefined): boolean {
key: [string, string], return Boolean(bsnmCd && doList.includes(bsnmCd));
): Promise<ApiResponse<SearchCondition>> => { }
return statisticsApi.getSidoList({ instSeCd: key[1] });
}; function isDoSido(sido?: { bsnmCd?: string; sido?: string }): boolean {
if (!sido) return false;
if (isDoListBsnm(sido.bsnmCd)) return true;
return Boolean(sido.sido && doNameList.includes(sido.sido));
}
function isAllGugunValue(gugun: string | undefined): boolean {
const value = (gugun ?? "").trim();
return !value || value === "ALL" || value === "전체";
}
function firstConcreteSgg(list: Sgg[] | undefined): Sgg | undefined {
return (list ?? []).find((sgg) => !isAllGugunValue(sgg.gugun));
}
const getSggListFetcher = async ( const getSggListFetcher = async (
key: [string, string, string, string], key: [string, string, string, "Y" | "N"],
): Promise<ApiResponse<Sgg[]>> => { ): Promise<ApiResponse<Sgg[]>> => {
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y"; const [kind, sidoName, instSeCd, gugunSkipYn] = key;
return statisticsApi.getSggList(key[1], { const params = { instSeCd, gugunSkipYn };
instSeCd: key[2], if (kind === "sgg-all") {
gugunSkipYn, return statisticsApi.getSggAllList(sidoName, params);
}); }
return statisticsApi.getSggList(sidoName, params);
}; };
function buildSearchFormValues( function buildSearchFormValues(
@@ -59,9 +79,15 @@ function buildSearchFormValues(
: "ALL"; : "ALL";
if (d?.sido) { if (d?.sido) {
const sidoItem = sidoList?.data?.sidoList?.find(
(item) => item.sido === d.sido,
);
const gugun =
d.gugun ||
(sidoItem && !isDoSido(sidoItem) ? "ALL" : "");
return { return {
sido: d.sido, sido: d.sido,
gugun: d.gugun || "", gugun,
year: d.year || defaultYear.toString(), year: d.year || defaultYear.toString(),
order: d.order || "", order: d.order || "",
fctName: d.fctName || "", fctName: d.fctName || "",
@@ -73,12 +99,28 @@ function buildSearchFormValues(
}; };
} }
if (props.defaultRegionAll) {
return {
sido: "ALL",
gugun: "ALL",
year: d?.year || defaultYear.toString(),
order: d?.order || "",
fctName: d?.fctName || "",
bsnmCd: "",
isDetail: false,
isSummary: d?.isSummary || "",
clsfCd: initialClsfCd,
capacityGbn: d?.capacityGbn || "ALL",
};
}
const first = sidoList?.data?.sidoList?.[0]; const first = sidoList?.data?.sidoList?.[0];
const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first; const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first;
return { return {
sido: useFirstSido ? first.sido : "", sido: useFirstSido ? first.sido : "",
gugun: "", gugun:
useFirstSido && first && !isDoSido(first) ? "ALL" : "",
year: d?.year || defaultYear.toString(), year: d?.year || defaultYear.toString(),
order: d?.order || "", order: d?.order || "",
fctName: d?.fctName || "", fctName: d?.fctName || "",
@@ -102,10 +144,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const defaultYear = currentYear - 1; const defaultYear = currentYear - 1;
const isInput = Boolean(props.isInput);
const sidoListKind = isInput ? "input" : "default";
const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>( const { data: sidoList } = useSWR<ApiResponse<SearchCondition>>(
["sido-list", instSeCd], SEARCH_CONDITION_SWR_KEY(instSeCd, sidoListKind),
getSidoListFetcher, isInput ? fetchSearchConditionInput : fetchSearchCondition,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
@@ -132,6 +176,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
// 최초 진입만 시도 API가 늦게 오는 경우 // 최초 진입만 시도 API가 늦게 오는 경우
useEffect(() => { useEffect(() => {
if (props.defaultRegionAll) return;
if (props.defaultValues?.sido) return; if (props.defaultValues?.sido) return;
const first = sidoList?.data?.sidoList?.[0]; const first = sidoList?.data?.sidoList?.[0];
if (!first || !mergedProps.sido) return; if (!first || !mergedProps.sido) return;
@@ -139,17 +184,33 @@ const SearchForm = (props: SearchFormProps = {}) => {
prev.sido prev.sido
? prev ? prev
: { : {
...prev, ...prev,
sido: first.sido, sido: first.sido,
gugun: "", gugun: isDoSido(first) ? "" : "ALL",
bsnmCd: first.bsnmCd, bsnmCd: first.bsnmCd,
}, },
); );
}, [sidoList, props.defaultValues?.sido, mergedProps.sido]); }, [
sidoList,
props.defaultValues?.sido,
props.defaultRegionAll,
mergedProps.sido,
]);
const sggKey: [string, string, string, string] | null = const selectedSido = sidoList?.data?.sidoList?.find(
values.sido && values.sido !== "ALL" (item) => item.sido === values.sido,
? ["sgg-list", values.sido, instSeCd, values.bsnmCd || ""] );
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
const useSggAllList =
gugunSkipYn === "N" && !isDoListBsnm(selectedSido?.bsnmCd);
const sggKey: [string, string, string, "Y" | "N"] | null =
values.sido && values.sido !== "ALL" && sidoList?.data
? [
useSggAllList ? "sgg-all" : "sgg-list",
values.sido,
instSeCd,
gugunSkipYn,
]
: null; : null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>( const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey, sggKey,
@@ -163,39 +224,60 @@ const SearchForm = (props: SearchFormProps = {}) => {
onSuccess: (data) => { onSuccess: (data) => {
if (data.status !== "success") { if (data.status !== "success") {
alert(`시군구 목록을 불러오는데 실패했습니다. alert(`시군구 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`); 새로고침 후 다시 시도해주세요.`);
} }
}, },
onError: () => { onError: () => {
alert(`시군구 목록을 불러오는데 실패했습니다. alert(`시군구 목록을 불러오는데 실패했습니다.
새로고침 후 다시 시도해주세요.`); 새로고침 후 다시 시도해주세요.`);
}, },
}, },
); );
const sggOptions = sggList?.data ?? []; const sggOptions = sggList?.data ?? [];
const isDoSelected = isDoSido(selectedSido);
const firstConcreteSggOption = firstConcreteSgg(sggOptions);
// 도: 빈 값일 때만 첫 시군구로 보이게. ALL은 사용자가 고른 전체 유지
const gugunSelectValue =
isDoSelected && !values.gugun
? (firstConcreteSggOption?.gugun ?? undefined)
: values.gugun || undefined;
// 시도 설정 후 시군구 목록이 오면 첫 번째 항목 자동 선택 // 도: 초기/시도 변경(빈 값)만 첫 시군구. 전체(ALL)는 유지. 그 외: 전체
useEffect(() => { useEffect(() => {
if (!mergedProps.gugun) return; if (!mergedProps.gugun) return;
if (props.defaultValues?.gugun) return; if (props.defaultRegionAll) return;
if (!values.sido || values.sido === "ALL") return; if (!values.sido || values.sido === "ALL") return;
if (!selectedSido) return;
const firstSgg = sggList?.data?.[0]; if (!isDoSelected) {
if (!firstSgg) return; setValues((prev) => {
if (prev.gugun) return prev;
return {
...prev,
gugun: "ALL",
bsnmCd: selectedSido.bsnmCd || prev.bsnmCd,
};
});
return;
}
if (!firstConcreteSggOption) return;
setValues((prev) => { setValues((prev) => {
if (prev.gugun) return prev; if (prev.gugun) return prev;
return { return {
...prev, ...prev,
gugun: firstSgg.gugun, gugun: firstConcreteSggOption.gugun,
bsnmCd: firstSgg.bsnmCd, bsnmCd: firstConcreteSggOption.bsnmCd,
}; };
}); });
}, [ }, [
sggList, firstConcreteSggOption,
values.sido, isDoSelected,
mergedProps.gugun, mergedProps.gugun,
props.defaultValues?.gugun, props.defaultRegionAll,
selectedSido,
values.sido,
]); ]);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
@@ -209,12 +291,17 @@ const SearchForm = (props: SearchFormProps = {}) => {
}; };
const handleSidoChange = (value: string) => { const handleSidoChange = (value: string) => {
const selectedBsnm = const selected =
sidoList?.data?.sidoList?.find((v) => v.sido === value)?.bsnmCd || ""; value === "ALL"
? undefined
: sidoList?.data?.sidoList?.find((v) => v.sido === value);
const selectedBsnm = selected?.bsnmCd || "";
const gugunAll =
props.defaultRegionAll || value === "ALL" || !isDoSido(selected);
setValues((prev) => ({ setValues((prev) => ({
...prev, ...prev,
sido: value, sido: value,
gugun: "", gugun: gugunAll ? "ALL" : "",
bsnmCd: selectedBsnm, bsnmCd: selectedBsnm,
})); }));
}; };
@@ -224,7 +311,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
const selectedBsnm = const selectedBsnm =
value === "ALL" value === "ALL"
? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido) ? sidoList?.data?.sidoList?.find((v) => v.sido === values.sido)
?.bsnmCd || "" ?.bsnmCd || ""
: listForBsnm.find((v) => v.gugun === value)?.bsnmCd || ""; : listForBsnm.find((v) => v.gugun === value)?.bsnmCd || "";
setValues((prev) => ({ setValues((prev) => ({
@@ -274,7 +361,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="text-label">시군구</Label> <Label className="text-label">시군구</Label>
<Select <Select
value={values.gugun} value={gugunSelectValue}
onValueChange={handleSggChange} onValueChange={handleSggChange}
> >
<SelectTrigger className="w-[12rem]"> <SelectTrigger className="w-[12rem]">
@@ -377,16 +464,23 @@ const SearchForm = (props: SearchFormProps = {}) => {
<SelectValue placeholder="전체"></SelectValue> <SelectValue placeholder="전체"></SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value='ALL'>전체</SelectItem> <SelectItem value="ALL">전체</SelectItem>
<SelectItem value='500톤이상'>500톤이상</SelectItem> <SelectItem value="500톤이상">
<SelectItem value='500톤미만'>500톤미만</SelectItem> 500톤이상
</SelectItem>
<SelectItem value="500톤미만">
500톤미만
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)} )}
{mergedProps.isDetail && ( {mergedProps.isDetail && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="text-label cursor-pointer" htmlFor="isDetail"> <Label
className="cursor-pointer text-label"
htmlFor="isDetail"
>
세부 데이터 포함 세부 데이터 포함
</Label> </Label>
<Checkbox <Checkbox
@@ -403,7 +497,11 @@ const SearchForm = (props: SearchFormProps = {}) => {
)} )}
{mergedProps.isSummary && ( {mergedProps.isSummary && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="text-label cursor-pointer"> <Label
className="cursor-pointer text-label"
title="시·군·구 및 읍·면·동 변경에 따라 인구 및 관로 등 공표된 통계수치와 일부 상이할 수 있으니 유의 바랍니다."
>
<i className="diveicon di-exclamation-circle mr-1" />
요약표 포함 요약표 포함
</Label> </Label>
<Select <Select
@@ -416,11 +514,17 @@ const SearchForm = (props: SearchFormProps = {}) => {
<SelectValue placeholder="선택 안함"></SelectValue> <SelectValue placeholder="선택 안함"></SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="ALL">선택 안함</SelectItem> <SelectItem value="ALL">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => ( 선택 안함
<SelectItem value={i.toString()} key={i.toString()}>{`${i}개년`}</SelectItem> </SelectItem>
)) {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(
} (i) => (
<SelectItem
value={i.toString()}
key={i.toString()}
>{`${i}개년`}</SelectItem>
),
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -455,7 +559,9 @@ const SearchForm = (props: SearchFormProps = {}) => {
</Button> </Button>
)} )}
<Button className="h-[3.2rem] w-[96px]" type="submit"> <Button className="h-[3.2rem] w-[96px]" type="submit">
{props.searchButtonText ? props.searchButtonText : '조회'} {props.searchButtonText
? props.searchButtonText
: "조회"}
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,41 +1,42 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import useSWR from "swr";
import { ValueType } from "realgrid";
import { import { ValueType } from "realgrid";
Button, import useSWR from "swr";
Input,
Label,
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
LoadingWithAnimation,
} from "@common/components";
import CompactDataGrid from "./CompactDataGrid";
import SaveButton from "../button/SaveButton";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { isGugunRequiredSido } from "@/entities/statistics";
import type {
SeweragePipeFacilitiesListRequest,
SeweragePipeFacilityAdminAreaListRequest,
SeweragePipeFactConnectionSaveItem,
SeweragePipeFactConnectionSaveRequest,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface";
import type { import type {
ApiResponse, ApiResponse,
SearchCondition, SearchCondition,
Sgg, Sgg,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type { ColumnInfo } from "../type/DataGrid.type"; import {
import type { Button,
SeweragePipeFacilitiesListRequest, Input,
SeweragePipeFacilityAdminAreaListRequest, Label,
SeweragePipeFactConnectionSaveRequest, LoadingWithAnimation,
SeweragePipeFactConnectionSaveItem, Select,
} from "@/entities/statistics/model/input/statistics-sewage-pipe-input-interface"; SelectContent,
import { statisticsInputSewPipeApiService } from "@/entities/statistics/api/input/statistics-sewage-pipe-input-service"; SelectItem,
import type { SearchFormValues } from "../type/SearchForm.type"; SelectTrigger,
import { doNameList } from "../util/column/input/DoList"; SelectValue,
} from "@common/components";
import { getUserInfo } from "../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../packages/common/components/src/api";
import type { ColumnInfo } from "../type/DataGrid.type";
import type { SearchFormValues } from "../type/SearchForm.type";
import CompactDataGrid from "./CompactDataGrid";
/** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */ /** 다이얼로그 좌측: 처리시설 정보 (순번 없음) */
const treatmentFacilityColumnInfo: ColumnInfo = { const treatmentFacilityColumnInfo: ColumnInfo = {
@@ -114,9 +115,9 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
}; };
const getSggAllListFetcher = async ( const getSggAllListFetcher = async (
key: [string, string], key: [string, string, "Y" | "N"],
): Promise<ApiResponse<Sgg[]>> => { ): Promise<ApiResponse<Sgg[]>> => {
return statisticsApi.getSggList(key[1]); return statisticsApi.getSggList(key[1], { gugunSkipYn: key[2] });
}; };
type SidoSggFacilityNameSearchFormProps = { type SidoSggFacilityNameSearchFormProps = {
@@ -182,9 +183,16 @@ const SidoSggFacilityNameSearchForm = ({
Record<string, string | number> | null Record<string, string | number> | null
>(null); >(null);
const didAutoFetchRef = useRef(false); const didAutoFetchRef = useRef(false);
const isSavingConnectionRef = useRef(false);
const sggKey: [string, string] | null = const selectedSido = sidoList?.data?.sidoList?.find(
sido && sido !== "ALL" ? ["sgg-all", sido] : null; (item) => item.sido === sido,
);
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
const sggKey: [string, string, "Y" | "N"] | null =
sido && sido !== "ALL" && sidoList?.data
? ["sgg-all", sido, gugunSkipYn]
: null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>( const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey, sggKey,
getSggAllListFetcher, getSggAllListFetcher,
@@ -297,7 +305,11 @@ const SidoSggFacilityNameSearchForm = ({
swtrmtFcltCd, swtrmtFcltCd,
sewerBzmnCd: initialSearchValues?.bsnmCd || '', sewerBzmnCd: initialSearchValues?.bsnmCd || '',
ctpvNm: initialSidoValue || '', ctpvNm: initialSidoValue || '',
...(doNameList.includes(initialSidoValue) ? { sggNm: initialGugunValue } : {}), ...(isGugunRequiredSido(sidoList?.data?.sidoList, {
sido: initialSidoValue,
})
? { sggNm: initialGugunValue }
: {}),
}; };
try { try {
@@ -329,119 +341,213 @@ const SidoSggFacilityNameSearchForm = ({
} }
}; };
const handleSaveAdminRegions = async () => { const canEditFactConnection = async (): Promise<boolean> => {
const statusCodeResponse = await statisticsApi.CheckStatusCode({ const statusCodeResponse = await statisticsApi.CheckStatusCode({
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "", sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
statsCd: 'ST00020002', statsCd: "ST00020002",
statsYr: statsYr?.trim() ?? '' statsYr: statsYr?.trim() ?? "",
}); });
if (statusCodeResponse.status === 'success') { if (statusCodeResponse.status === "success") {
if (!['0', '1', '4'].includes(statusCodeResponse.data)) { if (!["0", "1", "4"].includes(statusCodeResponse.data)) {
const codeName = statusCodeResponse.data === '2' ? '제출' : (statusCodeResponse.data === '3' ? '승인' : '검증중'); const codeName =
alert(`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`); statusCodeResponse.data === "2"
return; ? "제출"
} : statusCodeResponse.data === "3"
} else { ? "승인"
alert('오류가 발생했습니다.\n관리자에게 문의하세요') : "검증중";
alert(
`시설별 설치 현황 데이터가 ${codeName} 상태일 때는 처리 시설 정보를 변경할 수 없습니다.`,
);
return false;
}
return true;
} }
alert("오류가 발생했습니다.\n관리자에게 문의하세요");
return false;
};
const persistFactConnection = async (
saveList: SeweragePipeFactConnectionSaveItem[],
deleteList: string[],
): Promise<boolean> => {
if (!statsYr?.trim()) { if (!statsYr?.trim()) {
alert("상단에서 통계연도로 조회한 뒤 이용해 주세요."); alert("상단에서 통계연도로 조회한 뒤 이용해 주세요.");
return; return false;
} }
if (!selectedFacilityRow) { if (!selectedFacilityRow) {
alert("처리시설을 먼저 선택해주세요."); alert("처리시설을 먼저 선택해주세요.");
return; return false;
}
if (saveList.length === 0 && deleteList.length === 0) {
return true;
}
if (!(await canEditFactConnection())) {
return false;
} }
const hasChanged = adminRegionRows.some( try {
(row) => setIsLoading(true);
(row.__checked === "1" && row.isConnection !== "1") || const userInfo = await getUserInfo();
(row.__checked !== "1" && row.isConnection === "1"), const request: SeweragePipeFactConnectionSaveRequest = {
); ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
sggNm: String(selectedFacilityRow.sggNm ?? ""),
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
statsYr: statsYr.trim(),
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const saveList: SeweragePipeFactConnectionSaveItem[] = adminRegionRows const response =
.filter((row) => row.__checked === "1") await statisticsInputSewPipeApiService.saveFactConnection(
.map((row) => ({ request,
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.emdNm ?? ""),
}))
.filter((row) => row.admdstCd && row.admdstNm);
const deleteList = adminRegionRows
.filter((row) => row.__checked !== "1" && row.isConnection === "1")
.map((row) => String(row.admdstCd ?? ""))
.filter(Boolean);
if (!hasChanged) {
alert("변경된 연결 정보가 없습니다.");
return;
}
const run = async () => {
try {
setIsLoading(true);
const userInfo = await getUserInfo();
const request: SeweragePipeFactConnectionSaveRequest = {
ctpvNm: String(selectedFacilityRow.ctpvNm ?? ""),
sggNm: String(selectedFacilityRow.sggNm ?? ""),
fcltSn: String(selectedFacilityRow.fcltSn ?? ""),
statsYr: statsYr.trim(),
swtrmtFcltCd: String(selectedFacilityRow.fcltCd ?? ""),
swtrmtFcltNm: String(selectedFacilityRow.fcltNm ?? ""),
prcsFcltCpct: Number(selectedFacilityRow.fcltCpct ?? 0),
sewerBzmnCd: initialSearchValues?.bsnmCd?.trim() ?? "",
userId: userInfo.userUnqId,
saveList,
deleteList,
};
const response =
await statisticsInputSewPipeApiService.saveFactConnection(
request,
);
if (response.status !== "success") {
alert(response.message ?? "연결관계 저장에 실패했습니다.");
return;
}
setAdminRegionRows((prev) =>
prev.map((row) => ({
...row,
isConnection: row.__checked === "1" ? "1" : "0",
})),
); );
await onSaveSuccess?.();
alert("저장되었습니다.");
} catch {
alert("연결관계 저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
void run(); if (response.status !== "success") {
alert(response.message ?? "연결관계 저장에 실패했습니다.");
return false;
}
await onSaveSuccess?.();
return true;
} catch {
alert("연결관계 저장 중 오류가 발생했습니다.");
return false;
} finally {
setIsLoading(false);
}
}; };
const handleAdminRegionItemChecked = ( const syncConnectionFlags = (
rows: Record<string, string | number>[],
): Record<string, string | number>[] =>
rows.map((row) => ({
...row,
isConnection: row.__checked === "1" ? "1" : "0",
}));
const revertCheckedToConnection = (
rows: Record<string, string | number>[],
): Record<string, string | number>[] =>
rows.map((row) => ({
...row,
__checked: row.isConnection === "1" ? "1" : "0",
}));
const handleAdminRegionItemChecked = async (
itemIndex: number, itemIndex: number,
checked: boolean, checked: boolean,
) => { ) => {
if (isSavingConnectionRef.current) {
// RealGrid가 이미 토글했을 수 있어 __checked 기준으로 재동기화
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
return;
}
const target = adminRegionRows[itemIndex];
if (!target) return;
const nextChecked = checked ? "1" : "0";
const prevChecked = target.isConnection === "1" ? "1" : "0";
setAdminRegionRows((prev) => setAdminRegionRows((prev) =>
prev.map((row, idx) => prev.map((row, idx) =>
idx === itemIndex idx === itemIndex
? { ...row, __checked: checked ? "1" : "0" } ? { ...row, __checked: nextChecked }
: row, : row,
), ),
); );
// 서버 상태와 동일하면 API 호출 없이 UI만 반영
if (nextChecked === prevChecked) {
return;
}
const saveList: SeweragePipeFactConnectionSaveItem[] =
checked
? [
{
admdstCd: String(target.admdstCd ?? ""),
admdstNm: String(target.emdNm ?? ""),
},
].filter((row) => row.admdstCd && row.admdstNm)
: [];
const deleteList =
!checked && String(target.admdstCd ?? "")
? [String(target.admdstCd)]
: [];
if (saveList.length === 0 && deleteList.length === 0) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
alert("행정구역 코드가 없어 저장할 수 없습니다.");
return;
}
isSavingConnectionRef.current = true;
const ok = await persistFactConnection(saveList, deleteList);
isSavingConnectionRef.current = false;
if (!ok) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
return;
}
setAdminRegionRows((prev) => syncConnectionFlags(prev));
}; };
const handleAdminRegionItemAllChecked = (checked: boolean) => { const handleAdminRegionItemAllChecked = async (checked: boolean) => {
if (isSavingConnectionRef.current) {
setAdminRegionRows((prev) => prev.map((row) => ({ ...row })));
return;
}
const snapshot = adminRegionRows;
if (snapshot.length === 0) return;
setAdminRegionRows((prev) => setAdminRegionRows((prev) =>
prev.map((row) => ({ ...row, __checked: checked ? "1" : "0" })), prev.map((row) => ({
...row,
__checked: checked ? "1" : "0",
})),
); );
const saveList: SeweragePipeFactConnectionSaveItem[] = checked
? snapshot
.filter((row) => row.isConnection !== "1")
.map((row) => ({
admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.emdNm ?? ""),
}))
.filter((row) => row.admdstCd && row.admdstNm)
: [];
const deleteList = !checked
? snapshot
.filter((row) => row.isConnection === "1")
.map((row) => String(row.admdstCd ?? ""))
.filter(Boolean)
: [];
if (saveList.length === 0 && deleteList.length === 0) {
setAdminRegionRows((prev) => syncConnectionFlags(prev));
return;
}
isSavingConnectionRef.current = true;
const ok = await persistFactConnection(saveList, deleteList);
isSavingConnectionRef.current = false;
if (!ok) {
setAdminRegionRows((prev) => revertCheckedToConnection(prev));
return;
}
setAdminRegionRows((prev) => syncConnectionFlags(prev));
}; };
useEffect(() => { useEffect(() => {
@@ -575,8 +681,10 @@ const SidoSggFacilityNameSearchForm = ({
조회 조회
</Button> </Button>
</div> </div>
<div className="ml-auto"> <div>
<SaveButton saveFunc={handleSaveAdminRegions} /> <span className="text-lg text-gray-500 float-end">
* 체크 박스를 선택 / 해제 시 반영됩니다.
</span>
</div> </div>
</form> </form>
{isLoading && ( {isLoading && (
@@ -16,6 +16,7 @@ const SplitDataGrid = ({
leftCellStyleCallback, leftCellStyleCallback,
rightOnItemChecked, rightOnItemChecked,
rightOnItemAllChecked, rightOnItemAllChecked,
rightCheckableCallback,
enableSort = false, enableSort = false,
}: SplitDataGridProps) => { }: SplitDataGridProps) => {
return ( return (
@@ -39,6 +40,7 @@ const SplitDataGrid = ({
setRowData={setRightRowData} setRowData={setRightRowData}
onItemChecked={rightOnItemChecked} onItemChecked={rightOnItemChecked}
onItemAllChecked={rightOnItemAllChecked} onItemAllChecked={rightOnItemAllChecked}
checkableCallback={rightCheckableCallback}
/> />
</div> </div>
); );
+109 -31
View File
@@ -18,32 +18,48 @@ function mergeCellStyleCallbackResult(
baseStyleName: string | undefined, baseStyleName: string | undefined,
extra: CellStyleCallbackResult | undefined, extra: CellStyleCallbackResult | undefined,
): CellStyleCallbackResult { ): CellStyleCallbackResult {
let styleName: string | undefined;
let extraObj: Extract<CellStyleCallbackResult, object> | undefined;
if (!extra) { if (!extra) {
return baseStyleName ? { styleName: baseStyleName } : {}; styleName = baseStyleName || undefined;
} else if (typeof extra === "string") {
styleName = [extra, baseStyleName].filter(Boolean).join(" ") || undefined;
} else {
extraObj = extra;
styleName =
[extra.styleName, baseStyleName].filter(Boolean).join(" ") ||
undefined;
} }
if (typeof extra === "string") {
const styleName = [extra, baseStyleName].filter(Boolean).join(" "); if (!styleName && !extraObj) return {};
return styleName ? { styleName } : {};
} const result: CellStyleCallbackResult = {
const styleName = [extra.styleName, baseStyleName].filter(Boolean).join(" "); ...extraObj,
return {
...extra,
...(styleName ? { styleName } : {}), ...(styleName ? { styleName } : {}),
}; };
return result;
} }
import SplitDataGrid from "./SplitDataGrid"; import SplitDataGrid from "./SplitDataGrid";
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks" import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
import { wrapWithFormatTruncate } from "@/components/util/grid/decimalNumber"
import { import {
buildExcelExportFileBaseName, buildExcelExportFileBaseName,
buildFixedColumnNameSet,
getExcelImportColumnFieldNames, getExcelImportColumnFieldNames,
getNonEditableColumnFieldNames, getNonEditableColumnFieldNames,
hasAdmdstCdColumn, hasAdmdstCdColumn,
isExcelImportAdmdstCdInvalid, isExcelImportAdmdstCdInvalid,
mergeExcelImportWithExistingRows, mergeExcelImportWithExistingRows,
prepareGridColumnsForExcelExport, prepareGridColumnsForExcelExport,
releaseFixedOptionsForExcelExport,
resolveColumnStyleName,
restoreFixedOptionsAfterExcelExport,
restoreGridColumnsAfterExcelExport, restoreGridColumnsAfterExcelExport,
shouldPrepareColumnsForExcelExport, shouldPrepareColumnsForExcelExport,
disableAlternateRowStyle,
} from "@/components/util/gridExcelExport" } from "@/components/util/gridExcelExport"
import { useTabStore } from "@/store/tab-store" import { useTabStore } from "@/store/tab-store"
@@ -88,7 +104,7 @@ function safeGridViewRefresh(gv: { refresh?: () => void } | null | undefined) {
try { try {
gv.refresh(); gv.refresh();
} catch { } catch {
/* noop */ console.error('refresh error');
} }
} }
@@ -237,7 +253,7 @@ function sortRowsGroupPreserving(
} }
/** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */ /** columnInfo → RealGrid DataField 배열 (computed → valueCallback 포함, setFields용) */
function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) { function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueType; dataFormat?: string; computed?: import("../type/DataGrid.type").ComputedFieldConfig }[]) {
return columns.map((col) => { return columns.map((col) => {
const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = { const field: { fieldName: string; dataType: string; valueCallback?: (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number } = {
fieldName: col.fieldName, fieldName: col.fieldName,
@@ -253,7 +269,7 @@ function buildFieldsFromColumns(columns: { fieldName: string; valueType?: ValueT
const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0; const hasSumOfMaxGroups = "groups" in col.computed && Array.isArray(col.computed.groups) && col.computed.groups.length > 0;
const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField); const hasSumPerTenThousand = col.computed.type === "sumPerTenThousand" && "numeratorFields" in col.computed && col.computed.numeratorFields.length > 0 && "denominatorField" in col.computed && Boolean(col.computed.denominatorField);
if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) { if (hasFields || hasNumeratorDenominator || hasMaxOfFieldAndSum || hasDifferenceOfSums || hasHalfSumOfSums || hasSumOfTwoHalfSumOfSums || hasSumOfMaxGroups || hasSumPerTenThousand) {
field.valueCallback = getComputedValueCallback(col.computed) as (prod: unknown, dataRow: number, fieldName: string, fieldNames: string[], values: unknown[]) => number; field.valueCallback = wrapWithFormatTruncate(getComputedValueCallback(col.computed), col.dataFormat);
} }
} }
return field; return field;
@@ -282,7 +298,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
const TabDataGrid = ({ const TabDataGrid = ({
columnInfo, columnInfo,
isExcelImport = true, isExcelImport = true,
excelImportSkipNonEditable = true, excelImportSkipNonEditable = false,
excelImportSewerBzmnCd, excelImportSewerBzmnCd,
excelExportSearchContext, excelExportSearchContext,
isExcelExport = true, isExcelExport = true,
@@ -313,17 +329,23 @@ const TabDataGrid = ({
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(undefined); const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(undefined);
cellStyleCallbackRef.current = cellStyleCallback; cellStyleCallbackRef.current = cellStyleCallback;
const attachCellStyleCallbacksForTab = useCallback((tabKey: string, columns: ColumnConfig[], attempt = 0) => { const attachCellStyleCallbacksForTab = useCallback((tabKey: string, columns: ColumnConfig[], columnLayout: ColumnLayoutItem[] | undefined, fixedCols: number | undefined, attempt = 0) => {
if (!cellStyleCallbackRef.current) return;
const el = gridRefs.current.get(tabKey); const el = gridRefs.current.get(tabKey);
const gv = el?.gridView; const gv = el?.gridView;
if (!gv) return; if (!gv) return;
const fixedColumnNameSet = buildFixedColumnNameSet(columns, columnLayout, fixedCols);
const hasUserCellStyleCallback = Boolean(cellStyleCallbackRef.current);
if (fixedColumnNameSet.size === 0 && !hasUserCellStyleCallback) return;
let attachedCount = 0; let attachedCount = 0;
for (const column of columns) { for (const column of columns) {
const isFixedColumn = fixedColumnNameSet.has(column.name);
if (!isFixedColumn && !hasUserCellStyleCallback) continue;
const gridCol = safeColumnByName(gv, column.name); const gridCol = safeColumnByName(gv, column.name);
if (!gridCol) continue; if (!gridCol) continue;
const baseStyleName = column.styleName; const baseStyleName = resolveColumnStyleName(column, fixedColumnNameSet);
gridCol.styleCallback = (grid, dataCell) => { gridCol.styleCallback = (grid, dataCell) => {
const dc = dataCell as { const dc = dataCell as {
itemIndex?: number; itemIndex?: number;
@@ -338,19 +360,21 @@ const TabDataGrid = ({
return mergeCellStyleCallbackResult(baseStyleName, undefined); return mergeCellStyleCallbackResult(baseStyleName, undefined);
} }
const row = data[itemIndex] as Record<string, string | number>; const row = data[itemIndex] as Record<string, string | number>;
const extra = cellStyleCallbackRef.current?.({ const extra = hasUserCellStyleCallback
rowIndex: itemIndex, ? cellStyleCallbackRef.current?.({
fieldName, rowIndex: itemIndex,
row, fieldName,
grid: grid as CellStyleCallbackParams["grid"], row,
}); grid: grid as CellStyleCallbackParams["grid"],
})
: undefined;
return mergeCellStyleCallbackResult(baseStyleName, extra); return mergeCellStyleCallbackResult(baseStyleName, extra);
}; };
attachedCount += 1; attachedCount += 1;
} }
if (attachedCount === 0 && attempt < 20) { if (attachedCount === 0 && attempt < 20) {
setTimeout(() => attachCellStyleCallbacksForTab(tabKey, columns, attempt + 1), 100); setTimeout(() => attachCellStyleCallbacksForTab(tabKey, columns, columnLayout, fixedCols, attempt + 1), 100);
return; return;
} }
@@ -459,6 +483,14 @@ const TabDataGrid = ({
const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep const excelExportVisibilitySnapshot = useExcelColumnVisibilityPrep
? prepareGridColumnsForExcelExport(gridView, columns) ? prepareGridColumnsForExcelExport(gridView, columns)
: undefined; : undefined;
const savedFixedColCount = columnInfoItem.fixedColumns
? releaseFixedOptionsForExcelExport(
gridView,
columnInfoItem.fixedColumns,
)
: undefined;
disableAlternateRowStyle(gridView, true);
const exportBaseName = buildExcelExportFileBaseName( const exportBaseName = buildExcelExportFileBaseName(
menuName, menuName,
@@ -473,6 +505,12 @@ const TabDataGrid = ({
footer: 'hidden', footer: 'hidden',
allColumns: !useExcelColumnVisibilityPrep, allColumns: !useExcelColumnVisibilityPrep,
done: () => { done: () => {
if (savedFixedColCount !== undefined) {
restoreFixedOptionsAfterExcelExport(
gridView,
savedFixedColCount,
);
}
if (excelExportVisibilitySnapshot) { if (excelExportVisibilitySnapshot) {
restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot); restoreGridColumnsAfterExcelExport(gridView, excelExportVisibilitySnapshot);
} }
@@ -968,6 +1006,11 @@ const TabDataGrid = ({
return null; return null;
} }
const { columns, columnLayout } = info; const { columns, columnLayout } = info;
const fixedColumnNameSet = buildFixedColumnNameSet(
columns,
columnLayout,
info.fixedColumns,
);
const currentRowData = rowDataMap.get(tabKey) || []; const currentRowData = rowDataMap.get(tabKey) || [];
return ( return (
@@ -1074,6 +1117,7 @@ const TabDataGrid = ({
gridView.setOptions({ gridView.setOptions({
summaryMode: "aggregate" summaryMode: "aggregate"
}); });
disableAlternateRowStyle(gridView);
// footerUserSpans가 있는 경우 setColumnLayout으로 설정 // footerUserSpans가 있는 경우 setColumnLayout으로 설정
const checkFooterUserSpans = (layout: unknown[]): boolean => { const checkFooterUserSpans = (layout: unknown[]): boolean => {
@@ -1189,9 +1233,27 @@ const TabDataGrid = ({
if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) { if (currentInfo.fixedColumns !== undefined && currentInfo.fixedColumns > 0) {
gridView.setFixedOptions({ gridView.setFixedOptions({
colCount: currentInfo.fixedColumns, colCount: currentInfo.fixedColumns,
resizable: true,
}); });
} }
const tabFixedSet = buildFixedColumnNameSet(
columns,
currentInfo.columnLayout,
currentInfo.fixedColumns,
);
columns.forEach((col) => {
const styleName = resolveColumnStyleName(
col,
tabFixedSet,
);
gridView.setColumnProperty?.(
col.name,
"styleName",
styleName,
);
});
// 각 탭별 ref 초기화 // 각 탭별 ref 초기화
if (!sortDirRefs.current.has(tabKey)) { if (!sortDirRefs.current.has(tabKey)) {
sortDirRefs.current.set(tabKey, { current: {} }); sortDirRefs.current.set(tabKey, { current: {} });
@@ -1247,9 +1309,17 @@ const TabDataGrid = ({
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement); const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false; (gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = enableSort ? onSortingHandler : () => false;
if (cellStyleCallbackRef.current) { if (
cellStyleCallbackRef.current ||
tabFixedSet.size > 0
) {
setTimeout(() => { setTimeout(() => {
attachCellStyleCallbacksForTab(tabKey, columns); attachCellStyleCallbacksForTab(
tabKey,
columns,
currentInfo.columnLayout,
currentInfo.fixedColumns,
);
}, 100); }, 100);
} }
}, 150); }, 150);
@@ -1262,7 +1332,16 @@ const TabDataGrid = ({
layout={columnLayout} layout={columnLayout}
rows={currentRowData} rows={currentRowData}
> >
{columns.map((column) => ( {columns.map((column) => {
const resolvedStyleName =
resolveColumnStyleName(
column,
fixedColumnNameSet,
) ||
(column.editable === false
? "rg-cell-readonly"
: undefined);
return (
<RGDataColumn <RGDataColumn
key={column.name} key={column.name}
name={column.name} name={column.name}
@@ -1271,11 +1350,9 @@ const TabDataGrid = ({
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })} {...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.00' })}
header={column.header} header={column.header}
editable={column.editable} editable={column.editable}
{...(column.styleName != null {...(resolvedStyleName != null
? { styleName: column.styleName } ? { styleName: resolvedStyleName }
: column.editable === false : {})}
? { styleName: 'rg-cell-readonly' as const }
: {})}
{...(column.visible === false && { visible: false })} {...(column.visible === false && { visible: false })}
footer={column.footer && !Array.isArray(column.footer) ? { footer={column.footer && !Array.isArray(column.footer) ? {
text: (column.footer as { text?: string }).text, text: (column.footer as { text?: string }).text,
@@ -1285,7 +1362,8 @@ const TabDataGrid = ({
mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined} mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined}
sortable={enableSort && !column.sortdisable} sortable={enableSort && !column.sortdisable}
/> />
))} );
})}
</RealGridReact> </RealGridReact>
) : null} ) : null}
</div> </div>
@@ -10,7 +10,12 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
import { import {
Button, Button,
Dialog, Dialog,
@@ -21,17 +26,17 @@ import {
DialogTitle, DialogTitle,
LoadingWithAnimation, LoadingWithAnimation,
} from "@common/components"; } from "@common/components";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { import type {
CellEditedCallbackParams, CellEditedCallbackParams,
CellStyleCallbackParams, CellStyleCallbackParams,
ColumnInfo, ColumnInfo,
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
import { useAlertStore } from "@/shared/lib";
type VerificationResultGridRow = Record<string, string | number>; type VerificationResultGridRow = Record<string, string | number>;
@@ -41,8 +46,8 @@ const VERIFICATION_METHOD_CODES: VerificationMethodCd[] = ["NV", "IMR", "HB"];
const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = { const VRFC_MTHD_NAME_MAP: Record<VerificationMethodCd, string> = {
NV: "일반", NV: "일반",
IMR: "I-MR", IMR: "시계열",
HB: "HB", HB: "모집단",
}; };
export type VerificationExceptionSubmitItem = { export type VerificationExceptionSubmitItem = {
@@ -63,7 +68,7 @@ function setRowCheckByItemIndex(
try { try {
gv?.checkItem?.(itemIndex, checked, false); gv?.checkItem?.(itemIndex, checked, false);
} catch { } catch {
/* RealGrid 체크 API 미지원 등 */ console.error('RealGrid 체크 API error');
} }
} }
@@ -93,6 +98,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
{ name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false }, { name: "vrfcMthdCd", fieldName: "vrfcMthdCd", header: { text: "검증유형코드" }, editable: false, visible: false },
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false }, { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false }, { name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false }, { name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false }, { name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
@@ -111,6 +117,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
"vrfcMthdNm", "vrfcMthdNm",
"ctpvNm", "ctpvNm",
"admdstNm", "admdstNm",
"fcltNm",
"vrfcTrgtNm", "vrfcTrgtNm",
"rsnExpln", "rsnExpln",
"nowVlCn", "nowVlCn",
@@ -125,7 +132,7 @@ const verificationSubmitResultColumnInfo: ColumnInfo = {
export interface VerificationResultDialogQueryContext { export interface VerificationResultDialogQueryContext {
sewerBzmnCd: string; sewerBzmnCd: string;
ctpvNm: string; ctpvNm: string;
sggNm: string; sggNm?: string;
statsYr: string; statsYr: string;
statsCd: string; statsCd: string;
} }
@@ -201,6 +208,7 @@ export function VerificationResultDialog({
vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm, vrfcMthdNm: VRFC_MTHD_NAME_MAP[vrfcMthdCd] ?? item.vrfcMthdNm,
ctpvNm: item.ctpvNm, ctpvNm: item.ctpvNm,
admdstNm: item.admdstNm ?? item.sggNm ?? "-", admdstNm: item.admdstNm ?? item.sggNm ?? "-",
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm, vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln, rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-", nowVlCn: item.nowVlCn ?? "-",
@@ -413,6 +421,7 @@ export function VerificationResultDialog({
getGridDataRef={getGridDataRef} getGridDataRef={getGridDataRef}
onCellEdited={onPrcsOpnnExplnEdited} onCellEdited={onPrcsOpnnExplnEdited}
cellStyleCallback={verificationMthdCellStyle} cellStyleCallback={verificationMthdCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => { onItemChecked={(itemIndex, checked) => {
setCheckedIndices((prev) => { setCheckedIndices((prev) => {
const next = new Set(prev); const next = new Set(prev);
@@ -10,9 +10,10 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import type { RealGridReact } from "realgrid-react"; import type { 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 { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
import { import {
@@ -30,18 +31,20 @@ import {
TabsTrigger, TabsTrigger,
} from "@common/components"; } from "@common/components";
import { getUserInfo } from "../../../../../packages/common/components/src/api";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { cellContentTooltip } from "@/components/module/verificationResultCellTooltip";
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
import { useAlertStore } from "@/shared/lib";
type VerificationResultGridRow = Record<string, string | number>; type VerificationResultGridRow = Record<string, string | number>;
type VerificationHistoryTabKey = "NV" | "IMR" | "HB"; type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [ const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
{ value: "NV", label: "일반검증 내역" }, { value: "NV", label: "일반검증 내역" },
{ value: "IMR", label: "I-MR 검증내역" }, { value: "IMR", label: "시계열 검증내역" },
{ value: "HB", label: "HB 검증내역" }, { value: "HB", label: "모집단 검증내역" },
]; ];
const PAGE_SIZE = 30; const PAGE_SIZE = 30;
@@ -88,7 +91,7 @@ function uncheckGridRow(gridRef: MutableRefObject<RealGridReact | null>, itemInd
try { try {
(gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false); (gridRef.current?.gridView as GridViewWithCheckApi | undefined)?.checkItem?.(itemIndex, false, false);
} catch { } catch {
/* ignore */ console.error('RealGrid 체크 API error');
} }
} }
@@ -138,6 +141,7 @@ const verificationResultColumnInfo: ColumnInfo = {
columns: [ columns: [
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false }, { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역" }, editable: false },
{ name: "fcltNm", fieldName: "fcltNm", header: { text: "시설명" }, editable: false },
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false }, { name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false }, { name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false }, { name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
@@ -154,6 +158,7 @@ const verificationResultColumnInfo: ColumnInfo = {
columnLayout: [ columnLayout: [
"ctpvNm", "ctpvNm",
"admdstNm", "admdstNm",
"fcltNm",
"vrfcTrgtNm", "vrfcTrgtNm",
"rsnExpln", "rsnExpln",
"nowVlCn", "nowVlCn",
@@ -175,7 +180,7 @@ export interface VerificationResultHistoryDialogProps {
queryContext?: { queryContext?: {
sewerBzmnCd: string; sewerBzmnCd: string;
ctpvNm: string; ctpvNm: string;
sggNm: string; sggNm?: string;
statsYr: string; statsYr: string;
statsCd: string; statsCd: string;
} | null; } | null;
@@ -375,8 +380,9 @@ export function VerificationResultHistoryDialog({
return { return {
vrfcSn: item.vrfcSn, vrfcSn: item.vrfcSn,
__checked: isChecked ? 1 : 0, __checked: isChecked ? 1 : 0,
ctpvNm: item.ctpvNm, ctpvNm: item.ctpvNm ?? "-",
admdstNm: item.admdstNm ?? item.sggNm ?? "-", admdstNm: item.admdstNm ?? item.sggNm ?? "-",
fcltNm: item.fcltNm ?? "-",
vrfcTrgtNm: item.vrfcTrgtNm, vrfcTrgtNm: item.vrfcTrgtNm,
rsnExpln: item.rsnExpln, rsnExpln: item.rsnExpln,
nowVlCn: item.nowVlCn ?? "-", nowVlCn: item.nowVlCn ?? "-",
@@ -500,6 +506,7 @@ export function VerificationResultHistoryDialog({
enableSort={false} enableSort={false}
getGridRef={gridRef} getGridRef={gridRef}
cellStyleCallback={verificationPrcsYnNmCellStyle} cellStyleCallback={verificationPrcsYnNmCellStyle}
cellTooltipCallback={cellContentTooltip}
onItemChecked={(itemIndex, checked) => { onItemChecked={(itemIndex, checked) => {
const entry = entries[itemIndex]; const entry = entries[itemIndex];
if (!entry) return; if (!entry) return;
@@ -558,7 +565,7 @@ export function VerificationResultHistoryDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[80%]" aria-describedby={undefined}> <DialogContent className="max-w-[80%]" aria-describedby={undefined}>
<DialogHeader> <DialogHeader>
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle> <DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm ? queryContext?.sggNm : ''}`}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="max-h-[85vh] space-y-5 overflow-auto"> <div className="max-h-[85vh] space-y-5 overflow-auto">
{errorMessage && ( {errorMessage && (
@@ -6,6 +6,7 @@ import {
DoubleArrowLeftIcon, DoubleArrowLeftIcon,
DoubleArrowRightIcon, DoubleArrowRightIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { Button, cn } from "@common/components"; import { Button, cn } from "@common/components";
export type VerificationResultHistoryPaginationProps = { export type VerificationResultHistoryPaginationProps = {
@@ -0,0 +1,11 @@
import type { CellTooltipCallbackParams } from "@/components/type/DataGrid.type";
/** RealGrid 셀 호버 시 title처럼 전체 내용 표시 */
export function cellContentTooltip({
row,
fieldName,
}: CellTooltipCallbackParams): string | undefined {
const value = row[fieldName];
if (value == null || value === "") return undefined;
return String(value);
}
@@ -1,47 +1,48 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useTabStore as useGridTabStore } from "@rsone/components";
import { import {
type InfiniteData, type InfiniteData,
keepPreviousData, keepPreviousData,
useInfiniteQuery, useInfiniteQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { useTabStore as useGridTabStore } from "@rsone/components"; import dayjs from "dayjs";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import {
findMenuItemByUrlPath,
resolveInputPageNavigationFromStatsCd,
} from "@/shared/lib/stats-cd-input-navigation";
import { CodeSelectWithAll } from "@/entities/code";
import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service"; import { verificationInputReportApi } from "@/entities/statistics/api/verification-input-report-service";
import type { import type {
VerificationInputReportItem, VerificationInputReportItem,
VerificationInputReportResponse, VerificationInputReportResponse,
VerificationInputReportTypeCountingInfo, VerificationInputReportTypeCountingInfo,
} from "@/entities/statistics/model/verification-input-report-interface"; } from "@/entities/statistics/model/verification-input-report-interface";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import {
findMenuItemByUrlPath,
resolveInputPageNavigationFromStatsCd,
} from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
import dayjs from "dayjs";
import { import {
Badge, Badge,
type BadgeProps, type BadgeProps,
Label,
LoadingWithAnimation, LoadingWithAnimation,
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
Label,
} from "@common/components"; } from "@common/components";
import { CodeSelectWithAll } from "@/entities/code";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
type VerificationReportTabKey = "NV" | "IMR" | "HB"; type VerificationReportTabKey = "NV" | "IMR" | "HB";
const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [ const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
{ value: "NV", label: "일반" }, { value: "NV", label: "일반" },
{ value: "IMR", label: "I-MR" }, { value: "IMR", label: "시계열" },
{ value: "HB", label: "HB" }, { value: "HB", label: "모집단" },
]; ];
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = { const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
@@ -52,8 +53,8 @@ const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
const TYPE_CODE_NAME_MAP: Record<string, string> = { const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반", NV: "일반",
IMR: "I-MR", IMR: "시계열",
HB: "HB", HB: "모집단",
}; };
const PAGE_SIZE = 100; const PAGE_SIZE = 100;
@@ -1,20 +1,56 @@
import { import {
type RefObject,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type RefObject,
} from "react"; } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid"; import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
EXCRETA_TREATMENT_FACILITY_STATS_CD,
type ExcretaTreatmentFacilityItem,
type ExcretaTreatmentFacilityRequestParams,
type ExcretaTreatmentFacilitySaveItem,
type ExcretaTreatmentFacilityVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
SewerageStatusInfo,
Sgg,
Sido,
} from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
resolveNoApplicableForRequest,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation } from "@common/components"; import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import AddArticleButton from "@/components/button/AddArticleButton"; import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import DeleteArticleButton from "@/components/button/DeleteArticleButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton";
@@ -24,49 +60,24 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import SubmitDailog from "./SubmitDailog";
import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn";
import type { import type {
CellStyleCallbackParams, CellStyleCallbackParams,
CellTooltipCallbackParams, CellTooltipCallbackParams,
ColumnConfig, ColumnConfig,
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList"; import { ExcretaTreatFactInpColumn } from "@/components/util/column/input/ExcretaTreatFactInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import {
EXCRETA_TREATMENT_FACILITY_STATS_CD,
type ExcretaTreatmentFacilityItem,
type ExcretaTreatmentFacilityRequestParams,
type ExcretaTreatmentFacilitySaveItem,
type ExcretaTreatmentFacilityVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
Sgg,
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: ExcretaTreatmentFacilityVerificationResult; verificationResult?: ExcretaTreatmentFacilityVerificationResult;
@@ -91,10 +102,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -108,7 +131,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk; isSubmitted && periodEnterable && gugunOk;
@@ -128,9 +151,8 @@ function buildExcretaTreatmentFacilitySearchParam(
const fcltNm = values.fctName?.trim(); const fcltNm = values.fctName?.trim();
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(fcltNm ? { fcltNm } : {}), ...(fcltNm ? { fcltNm } : {}),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
@@ -191,13 +213,6 @@ function mapExcretaTreatmentFacilityListToGridRows(
}); });
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -210,9 +225,9 @@ function toApiYmd(value: string | number | Date | null | undefined): string | nu
function resolveExcretaTreatmentFacilityAdmdst( function resolveExcretaTreatmentFacilityAdmdst(
row: Record<string, string | number>, row: Record<string, string | number>,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const existingCd = String(row.admdstCd ?? "").trim(); const existingCd = nullableTrimmedString(row.admdstCd);
const existingNm = String(row.admdstNm ?? "").trim(); const existingNm = nullableTrimmedString(row.admdstNm);
if (existingCd && existingNm) { if (existingCd && existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm }; return { admdstCd: existingCd, admdstNm: existingNm };
} }
@@ -221,7 +236,10 @@ function resolveExcretaTreatmentFacilityAdmdst(
if (matched) { if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; return {
admdstCd: existingCd,
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
};
} }
/** 그리드 행 → ST00070001 저장/삭제 API 항목 */ /** 그리드 행 → ST00070001 저장/삭제 API 항목 */
@@ -236,18 +254,18 @@ function buildExcretaTreatmentFacilitySaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? ""), fcltNm: nullableTrimmedString(row.fcltNm),
fcltPstnCn: String(row.fcltPstnCn ?? ""), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
telNo: String(row.telNo ?? ""), telNo: nullableTrimmedString(row.telNo),
cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd), cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd),
oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd), oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd),
wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt), wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt),
prcsFcltCpct: toGridNumber(row.prcsFcltCpct), prcsFcltCpct: toGridNumber(row.prcsFcltCpct),
cpct: toGridNumber(row.cpct), cpct: toGridNumber(row.cpct),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt), oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
prcsMthdExpln: String(row.prcsMthdExpln ?? ""), prcsMthdExpln: nullableTrimmedString(row.prcsMthdExpln),
linkTmplntCn: String(row.linkTmplntCn ?? ""), linkTmplntCn: nullableTrimmedString(row.linkTmplntCn),
linkPrcsQnt: toGridNumber(row.linkPrcsQnt), linkPrcsQnt: toGridNumber(row.linkPrcsQnt),
inflwBodWqVl: toGridNumber(row.inflwBodWqVl), inflwBodWqVl: toGridNumber(row.inflwBodWqVl),
inflwTocWqVl: toGridNumber(row.inflwTocWqVl), inflwTocWqVl: toGridNumber(row.inflwTocWqVl),
@@ -261,11 +279,11 @@ function buildExcretaTreatmentFacilitySaveItem(
dschgTnWqVl: toGridNumber(row.dschgTnWqVl), dschgTnWqVl: toGridNumber(row.dschgTnWqVl),
dschgTpWqVl: toGridNumber(row.dschgTpWqVl), dschgTpWqVl: toGridNumber(row.dschgTpWqVl),
dschgClfgCnt: toGridNumber(row.dschgClfgCnt), dschgClfgCnt: toGridNumber(row.dschgClfgCnt),
wtsysNm: String(row.wtsysNm ?? ""), wtsysNm: nullableTrimmedString(row.wtsysNm),
trbtrNm: String(row.trbtrNm ?? ""), trbtrNm: nullableTrimmedString(row.trbtrNm),
msirbCn: String(row.msirbCn ?? ""), msirbCn: nullableTrimmedString(row.msirbCn),
ssirbCn: String(row.ssirbCn ?? ""), ssirbCn: nullableTrimmedString(row.ssirbCn),
operMthdCdCn: String(row.operMthdCdCn ?? ""), operMthdCdCn: nullableTrimmedString(row.operMthdCdCn),
wholNope: toGridNumber(row.wholNope), wholNope: toGridNumber(row.wholNope),
sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt), sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
@@ -290,11 +308,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function createEmptyExcretaTreatmentFacilityRow( function createEmptyExcretaTreatmentFacilityRow(
@@ -316,6 +334,8 @@ const EXCRETA_TREATMENT_FACILITY_NAME_GUIDE_MESSAGE =
// 하수도 통계 관리 > 07. 분뇨처리시설 // 하수도 통계 관리 > 07. 분뇨처리시설
const ExcretaTreatmentFacilityInp = () => { const ExcretaTreatmentFacilityInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -338,6 +358,8 @@ const ExcretaTreatmentFacilityInp = () => {
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null); const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState(""); const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false); const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]); const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -356,7 +378,10 @@ const ExcretaTreatmentFacilityInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null); const sggOptionsSidoRef = useRef<string | null>(null);
const [searchLoaded, setSearchLoaded] = useState(false); const [searchLoaded, setSearchLoaded] = useState(false);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.EXCRETA_TREATMENT_FACILITY) return;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
try { try {
@@ -382,7 +407,7 @@ const ExcretaTreatmentFacilityInp = () => {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => { const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") { if (!sido || sido === "ALL") {
@@ -402,7 +427,7 @@ const ExcretaTreatmentFacilityInp = () => {
const columnInfo = useMemo(() => { const columnInfo = useMemo(() => {
const enterable = const enterable =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; (computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = ExcretaTreatFactInpColumn.columns ?? []; const rawColumns = ExcretaTreatFactInpColumn.columns ?? [];
const base = enterable const base = enterable
? rawColumns ? rawColumns
@@ -430,7 +455,7 @@ const ExcretaTreatmentFacilityInp = () => {
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
const canEditGrid = const canEditGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; (computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const getCurrentRows = (): Record<string, string | number>[] => const getCurrentRows = (): Record<string, string | number>[] =>
gridGetRowsRef.current?.() ?? rowData; gridGetRowsRef.current?.() ?? rowData;
@@ -466,13 +491,18 @@ const ExcretaTreatmentFacilityInp = () => {
statusInfo: nextStatusInfo, statusInfo: nextStatusInfo,
excretaTreatmentFacilityList, excretaTreatmentFacilityList,
} = response.data; } = response.data;
const list = excretaTreatmentFacilityList ?? []; const list = filterBySelectedAdmdstCd(
excretaTreatmentFacilityList ?? [],
sidoListRef.current,
values,
);
const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm); const gridRows = mapExcretaTreatmentFacilityListToGridRows(list, commonInfo.ctpvNm);
setRowData(gridRows); setRowData(gridRows);
setStatusInfo(nextStatusInfo); setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd); setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
setFooterVerificationResult(commonInfo.verificationResult ?? null);
if (overrides?.mode !== "PRE") { if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0); setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) { } else if (gridRows.length > 0) {
@@ -532,6 +562,7 @@ const ExcretaTreatmentFacilityInp = () => {
setStatusInfo(null); setStatusInfo(null);
setStatsCd(""); setStatsCd("");
setIsVerificationFailed(false); setIsVerificationFailed(false);
setFooterVerificationResult(null);
setIsNotApplicable(false); setIsNotApplicable(false);
setInputPeriodEnterable(true); setInputPeriodEnterable(true);
setSggOptions([]); setSggOptions([]);
@@ -581,12 +612,8 @@ const ExcretaTreatmentFacilityInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD, statsCd: statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD,
@@ -622,7 +649,7 @@ const ExcretaTreatmentFacilityInp = () => {
if (!(await confirm("저장 하시겠습니까?"))) return; if (!(await confirm("저장 하시겠습니까?"))) return;
commitGrid(); commitGrid();
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
const saveList = noApplicable const saveList = noApplicable
? [] ? []
@@ -633,7 +660,7 @@ const ExcretaTreatmentFacilityInp = () => {
.filter((row) => String(row.fcltSn ?? "").trim() !== "") .filter((row) => String(row.fcltSn ?? "").trim() !== "")
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -646,11 +673,7 @@ const ExcretaTreatmentFacilityInp = () => {
{ {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable, noApplicable,
@@ -686,9 +709,9 @@ const ExcretaTreatmentFacilityInp = () => {
return; return;
} }
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -713,7 +736,7 @@ const ExcretaTreatmentFacilityInp = () => {
} }
commitGrid(); commitGrid();
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
const submitList = noApplicable const submitList = noApplicable
? [] ? []
@@ -724,19 +747,15 @@ const ExcretaTreatmentFacilityInp = () => {
.filter((row) => String(row.fcltSn ?? "").trim() !== "") .filter((row) => String(row.fcltSn ?? "").trim() !== "")
.map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions)); .map((row) => buildExcretaTreatmentFacilitySaveItem(row, sggOptions));
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD; const statsCdForSubmit = statsCd || EXCRETA_TREATMENT_FACILITY_STATS_CD;
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForSubmit, statsCd: statsCdForSubmit,
}; };
@@ -755,11 +774,7 @@ const ExcretaTreatmentFacilityInp = () => {
{ {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
@@ -786,15 +801,16 @@ const ExcretaTreatmentFacilityInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForSubmit, stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -941,10 +957,10 @@ const ExcretaTreatmentFacilityInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -953,7 +969,7 @@ const ExcretaTreatmentFacilityInp = () => {
} }
}; };
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = lastSearchedValues;
const toolbarSnapshot = computeTabToolbarSnapshot( const toolbarSnapshot = computeTabToolbarSnapshot(
statusInfo, statusInfo,
rowData.length > 0, rowData.length > 0,
@@ -973,23 +989,23 @@ const ExcretaTreatmentFacilityInp = () => {
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
(showSaveInToolbar || showEnterableToolbarButtons); (showSaveInToolbar || showEnterableToolbarButtons);
const tabEnterableForGrid =
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showNotApplicable = const showNotApplicable =
searchLoaded && searchLoaded &&
hasSearched && hasSearched &&
rowData.length === 0 && rowData.length === 0 &&
computeTabEnterable(statusInfo, inputPeriodEnterable); computeTabEnterable(statusInfo, inputPeriodEnterable) &&
submitOk;
const tabEnterableForGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const verificationQueryContext = (() => { const verificationQueryContext = (() => {
const ctx = lastSearchedValues ?? searchValues; const ctx = lastSearchedValues ?? searchValues;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1005,6 +1021,7 @@ const ExcretaTreatmentFacilityInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
fctName={true} fctName={true}
@@ -1013,7 +1030,7 @@ const ExcretaTreatmentFacilityInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]"> <div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
@@ -1024,18 +1041,18 @@ const ExcretaTreatmentFacilityInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{statusInfo?.laeSbmsnSttsCd === "2" && ( {statusInfo?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div> </div>
</div> </div>
)} )}
{showArticleToolbarCard && ( {showArticleToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]"> <div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
{showNotApplicable && ( {showNotApplicable && (
<NotApplicableCheckButton <NotApplicableCheckButton
@@ -1043,10 +1060,10 @@ const ExcretaTreatmentFacilityInp = () => {
setIsCheck={setIsNotApplicable} setIsCheck={setIsNotApplicable}
/> />
)} )}
<AddArticleButton addArticleFunc={addArticleFunc} /> <AddArticleButton addArticleFunc={addArticleFunc} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} /> <DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</div> </div>
</div> </div>
)} )}
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent"> <div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
@@ -1055,14 +1072,14 @@ const ExcretaTreatmentFacilityInp = () => {
</div> </div>
</div> </div>
)} )}
<DataGrid <DataGrid
key={canEditGrid ? "enterable" : "readonly"} key={canEditGrid ? "enterable" : "readonly"}
getGridRef={gridRef as RefObject<RealGridReact | null>} getGridRef={gridRef as RefObject<RealGridReact | null>}
columnInfo={columnInfo} columnInfo={columnInfo}
rowStartNum={ExcretaTreatFactInpColumn.rowStartNum} rowStartNum={ExcretaTreatFactInpColumn.rowStartNum}
fixedColumns={ExcretaTreatFactInpColumn.fixedColumns} fixedColumns={ExcretaTreatFactInpColumn.fixedColumns}
rowData={rowData} rowData={rowData}
setRowData={setRowData} setRowData={setRowData}
getGridDataRef={gridGetRowsRef} getGridDataRef={gridGetRowsRef}
showHeaderCheckbox showHeaderCheckbox
statusInfo={statusInfo} statusInfo={statusInfo}
@@ -1072,6 +1089,7 @@ const ExcretaTreatmentFacilityInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
/> />
</div> </div>
@@ -1,8 +1,9 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { CalendarIcon } from "lucide-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { CalendarIcon } from "lucide-react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { import {
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { useAlertStore, useGlobalProgressStore, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
@@ -10,20 +10,20 @@ import { statisticsInputPrivateSewageFacilityApi } from "@/entities/statistics/a
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; 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 type { import type {
PrivateSewageDesigStatusSaveItem,
PrivateSewageFacilityVerificationResult,
PrivateSewageDesigStatusItem, PrivateSewageDesigStatusItem,
PrivateSewageDesigStatusSaveItem,
PrivateSewageDesigStatusSaveRequest, PrivateSewageDesigStatusSaveRequest,
PrivateSewageDesigStatusSubmitRequest, PrivateSewageDesigStatusSubmitRequest,
PrivateSewageFacilityVerificationResult,
SepticTankStatusItem,
SepticTankStatusSaveItem, SepticTankStatusSaveItem,
SepticTankStatusSaveRequest, SepticTankStatusSaveRequest,
SepticTankStatusSubmitRequest, SepticTankStatusSubmitRequest,
SewageTreatFactStatusItem, SewageTreatFactStatusItem,
SewageTreatFactStatusRequestParams,
SewageTreatFactStatusSaveItem, SewageTreatFactStatusSaveItem,
SewageTreatFactStatusSaveRequest, SewageTreatFactStatusSaveRequest,
SewageTreatFactStatusSubmitRequest, SewageTreatFactStatusSubmitRequest,
SepticTankStatusItem,
SewageTreatFactStatusRequestParams,
} from "@/entities/statistics/model/input/statistics-input-private-sewage-facility-interface"; } from "@/entities/statistics/model/input/statistics-input-private-sewage-facility-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
@@ -31,7 +31,15 @@ import type {
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -39,25 +47,31 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap
import SubmitDailog from "./SubmitDailog"; import SubmitDailog from "./SubmitDailog";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import SubmitButton from "@/components/button/SumitButton";
import SaveButton from "@/components/button/SaveButton";
import LastYearDataButton from "@/components/button/LastYearDataButton"; import LastYearDataButton from "@/components/button/LastYearDataButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import SearchForm from "@/components/module/SearchForm";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn"; import { PrivateSewageDesigStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/PrivateSewageDesigStatusInpColumn";
import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn"; import { SepticTankStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SepticTankStatusInpColumn";
import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn"; import { SewageTreatFactStatusInpColumn } from "@/components/util/column/input/privateSewageFacility/SewageTreatFactStatusInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
const PrivateSewageFacilityInpTabList = [ const PrivateSewageFacilityInpTabList = [
SewageTreatFactStatusInpColumn, SewageTreatFactStatusInpColumn,
@@ -74,10 +88,6 @@ const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, s
[PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003", [PrivateSewageDesigStatusInpColumn.tabKey]: "ST00080003",
}; };
const PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS = Array.from(
new Set(Object.values(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY)),
);
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: PrivateSewageFacilityVerificationResult; verificationResult?: PrivateSewageFacilityVerificationResult;
}; };
@@ -99,9 +109,8 @@ function buildPrivateSewageFacilitySearchParam(
): SewageTreatFactStatusRequestParams { ): SewageTreatFactStatusRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -119,10 +128,14 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -136,7 +149,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -158,8 +171,8 @@ function tabRowDataExists(
function mapRegionalListToGridRows< function mapRegionalListToGridRows<
T extends { verificationResult?: PrivateSewageFacilityVerificationResult }, T extends { verificationResult?: PrivateSewageFacilityVerificationResult },
>(list: T[], ctpvNm: string): GridRowWithVerification[] { >(list: T[], ctpvNm: string, values?: SearchFormValues | null): GridRowWithVerification[] {
return list.map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const { verificationResult, ...rest } = item; const { verificationResult, ...rest } = item;
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult; if (verificationResult) row.verificationResult = verificationResult;
@@ -167,17 +180,6 @@ function mapRegionalListToGridRows<
}); });
} }
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
function buildColumnFormatNumber( function buildColumnFormatNumber(
columns: { fieldName?: string; dataFormat?: string }[], columns: { fieldName?: string; dataFormat?: string }[],
) { ) {
@@ -206,8 +208,8 @@ function buildSewageTreatFactStatusSaveList(
const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns); const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"), step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"),
step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"), step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"),
step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"), step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"),
@@ -222,7 +224,7 @@ function buildSewageTreatFactStatusSaveList(
step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"), step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"),
step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"), step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"),
step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"), step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -232,8 +234,8 @@ function buildSepticTankStatusSaveList(
const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns); const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"), staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"),
step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"), step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"),
staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"), staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"),
@@ -262,7 +264,7 @@ function buildSepticTankStatusSaveList(
step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"), step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"),
step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"), step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"),
step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"), step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -272,8 +274,8 @@ function buildPrivateSewageDesigStatusSaveList(
const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns); const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"), indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"),
indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"), indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"),
spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"), spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"),
@@ -283,7 +285,7 @@ function buildPrivateSewageDesigStatusSaveList(
"drtwPrcsFcltGrssFcltCnt", "drtwPrcsFcltGrssFcltCnt",
), ),
drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"), drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -305,11 +307,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function resolvePrivateSewageFacilityTab(tabKey: string) { function resolvePrivateSewageFacilityTab(tabKey: string) {
@@ -322,6 +324,8 @@ function resolvePrivateSewageFacilityTab(tabKey: string) {
// 하수도 통계 관리 > 08. 개인하수처리시설 // 하수도 통계 관리 > 08. 개인하수처리시설
const PrivateSewageFacilityInp = () => { const PrivateSewageFacilityInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -345,9 +349,15 @@ const PrivateSewageFacilityInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [showSubmitDialog, setShowSubmitDialog] = useState(false);
@@ -362,42 +372,30 @@ const PrivateSewageFacilityInp = () => {
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null); const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PRIVATE_SEWAGE_FACILITY) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd =
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries); setColumnGuidesByTab((prev) =>
setColumnGuidesByTab( Object.prototype.hasOwnProperty.call(prev, tabKey)
Object.fromEntries( ? prev
Object.entries(PRIVATE_SEWAGE_FACILITY_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( : { ...prev, [tabKey]: next },
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
); );
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = ( const commitToolbarSnapshotForLoadedTab = (
tabKey: string, tabKey: string,
@@ -420,6 +418,13 @@ const PrivateSewageFacilityInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -462,6 +467,7 @@ const PrivateSewageFacilityInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(sewageTreatFactStatusList ?? []) as SewageTreatFactStatusItem[], (sewageTreatFactStatusList ?? []) as SewageTreatFactStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -472,6 +478,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -490,6 +499,7 @@ const PrivateSewageFacilityInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(septicTankStatusList ?? []) as SepticTankStatusItem[], (septicTankStatusList ?? []) as SepticTankStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -500,6 +510,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -518,6 +531,7 @@ const PrivateSewageFacilityInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(privateSewageDesigStatusList ?? []) as PrivateSewageDesigStatusItem[], (privateSewageDesigStatusList ?? []) as PrivateSewageDesigStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -528,6 +542,9 @@ const PrivateSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -545,7 +562,9 @@ const PrivateSewageFacilityInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -587,7 +606,9 @@ const PrivateSewageFacilityInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
} }
@@ -697,12 +718,8 @@ const PrivateSewageFacilityInp = () => {
} }
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -785,11 +802,7 @@ const PrivateSewageFacilityInp = () => {
const submitInfoBase = { const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -801,12 +814,8 @@ const PrivateSewageFacilityInp = () => {
}; };
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -826,7 +835,7 @@ const PrivateSewageFacilityInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
setGlobalProgressLabel("일반 검증 진행 중..."); setGlobalProgressLabel("일반 검증 진행 중...");
let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean } }; let response: { status: string; message?: string; data?: { finalStatus?: string; nvpassed?: boolean; isHbPassed?: string } };
if (isSewageTreatTab) { if (isSewageTreatTab) {
const param: SewageTreatFactStatusSubmitRequest = { const param: SewageTreatFactStatusSubmitRequest = {
@@ -871,14 +880,15 @@ const PrivateSewageFacilityInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -962,11 +972,7 @@ const PrivateSewageFacilityInp = () => {
const baseRequest = { const baseRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
}; };
try { try {
@@ -1068,9 +1074,8 @@ const PrivateSewageFacilityInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1081,10 +1086,10 @@ const PrivateSewageFacilityInp = () => {
for (const tab of PrivateSewageFacilityInpTabList) { for (const tab of PrivateSewageFacilityInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1101,19 +1106,20 @@ const PrivateSewageFacilityInp = () => {
return map; return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
hasSearched, hasSearched,
lastSearchedValues, searchContextForToolbar,
inputPeriodEnterable, inputPeriodEnterable,
isAdminInfo, isAdminInfo,
); );
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } =
activeTabToolbar; activeTabToolbar;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const verificationCellStyle = ({ const verificationCellStyle = ({
fieldName, fieldName,
@@ -1146,10 +1152,10 @@ const PrivateSewageFacilityInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -1172,6 +1178,7 @@ const PrivateSewageFacilityInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -1192,10 +1199,10 @@ const PrivateSewageFacilityInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void lastYearDataFunc()} getLastYearDataFunc={() => void lastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1227,11 +1234,11 @@ const PrivateSewageFacilityInp = () => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return ( return (
@@ -1269,6 +1276,9 @@ const PrivateSewageFacilityInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport isExcelExport
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
getGridDataRef={ getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined activeTab === tabKey ? getGridDataRef : undefined
@@ -1,8 +1,8 @@
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react'; import { type Dispatch, type SetStateAction,useEffect, useRef, useState } from 'react';
import { useConfirm } from '@frontend-opensource/use-react-hooks'; import { useConfirm } from '@frontend-opensource/use-react-hooks';
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib'; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from '@/shared/lib';
import { STATISTICS_INPUT_URL_PATH } from '@/shared/lib/stats-cd-input-navigation'; import { STATISTICS_INPUT_URL_PATH } from '@/shared/lib/stats-cd-input-navigation';
import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics-input-pending-navigation'; import { useStatisticsInputPendingNavigation } from '@/shared/lib/use-statistics-input-pending-navigation';
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service'; import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
@@ -23,7 +23,9 @@ import {
type SewerageSupplyRateVerificationResult, type SewerageSupplyRateVerificationResult,
} from '@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface'; } from '@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface';
import type { StatisticsInputSubmitInfoFormData } from '@/entities/statistics/model/statistics-input-submit-info-schema'; import type { StatisticsInputSubmitInfoFormData } from '@/entities/statistics/model/statistics-input-submit-info-schema';
import type { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo } from '@/entities/statistics/model/statistics-interface'; import type { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, Sido } from '@/entities/statistics/model/statistics-interface';
import { filterBySelectedAdmdstCd, isGugunOkForToolbar, isSubmitOkForToolbar, resolveNoApplicableForRequest, toInputSearchRegion } from '@/entities/statistics';
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components'; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '@common/components';
import { getUserInfo } from '../../../../../../packages/common/components/src/api'; import { getUserInfo } from '../../../../../../packages/common/components/src/api';
@@ -38,19 +40,22 @@ import SubmitButton from '@/components/button/SumitButton';
import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton'; import UpdateOperateDataButton from '@/components/button/UpdateOperateDataButton';
import VerificationResultButton from '@/components/button/VerificationResultButton'; import VerificationResultButton from '@/components/button/VerificationResultButton';
import DataGrid from '@/components/module/DataGrid'; import DataGrid from '@/components/module/DataGrid';
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { openVerificationResultDialogIfNeeded } from '@/components/module/post-submit-verification-results';
import SearchForm from '@/components/module/SearchForm'; import SearchForm from '@/components/module/SearchForm';
import SplitDataGrid from '@/components/module/SplitDataGrid'; import SplitDataGrid from '@/components/module/SplitDataGrid';
import { openVerificationResultDialogIfNeeded } from '@/components/module/post-submit-verification-results';
import { VerificationResultDialog } from '@/components/module/VerificationResultDialog'; import { VerificationResultDialog } from '@/components/module/VerificationResultDialog';
import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog'; import { VerificationResultHistoryDialog } from '@/components/module/VerificationResultHistoryDialog';
import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type'; import type { CellStyleCallbackParams, ColumnInfo, ColumnInfoList } from '@/components/type/DataGrid.type';
import type { SearchFormValues } from '@/components/type/SearchForm.type'; import type { SearchFormValues } from '@/components/type/SearchForm.type';
import { doList } from '@/components/util/column/input/DoList';
import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn'; import { PublicSewageFactInpColumnList } from '@/components/util/column/input/PublicSewageFactInpColumn/PublicSewageFactInpColumn';
import { fetchColumnGuidesRecord } from '@/components/util/grid/columnGuides';
import { toGridRows } from '@/components/util/grid/gridFunction'; import { toGridRows } from '@/components/util/grid/gridFunction';
import { patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag'; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { patchFooterVerificationByTab,patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag';
import type { VerificationResultLikeMap } from '@/components/util/input/verification-result-display';
import { nullableTrimmedString } from '@/components/util/input/nullableString';
import { useAdminStore } from '@/store/admin-store'; import { useAdminStore } from '@/store/admin-store';
import { useTabStore } from '@/store/tab-store';
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult; verificationResult?: SewerageSupplyRateVerificationResult;
@@ -62,10 +67,22 @@ type TabToolbarSnapshot = {
showActionToolbarCard: boolean; showActionToolbarCard: boolean;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || '') return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== 'ALL') }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
const computeTabEnterable = ( const computeTabEnterable = (
@@ -92,7 +109,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2'; const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -122,11 +139,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === 'COMPLETED' && Number(res.job_prog) === 100) return;
if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') { if (res.status === 'FAILED' || res.status === 'ERROR' || res.status === 'CANCELLED') {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error('IMR 검증 완료 대기 시간이 초과되었습니다.'); throw new Error('시계열 검증 완료 대기 시간이 초과되었습니다.');
} }
type MainFacilityMappingSaveDraft = { type MainFacilityMappingSaveDraft = {
@@ -144,10 +161,6 @@ type MainFacilityMappingSaveDraft = {
initialPrxyCtrtCn: string; initialPrxyCtrtCn: string;
}; };
const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
);
/** 위탁계약기간: `yyyymmdd~yyyymmdd` (빈 값 허용). 하이픈 형식은 정규화 */ /** 위탁계약기간: `yyyymmdd~yyyymmdd` (빈 값 허용). 하이픈 형식은 정규화 */
function normalizePrxyCtrtCn(raw: unknown): string | null { function normalizePrxyCtrtCn(raw: unknown): string | null {
if (raw === null || raw === undefined) return null; if (raw === null || raw === undefined) return null;
@@ -160,6 +173,10 @@ function normalizePrxyCtrtCn(raw: unknown): string | null {
return s; return s;
} }
function isMainFacilityRow(row: Record<string, string | number>): boolean {
return String(row.isMainYn ?? '').toUpperCase() === 'Y';
}
function isValidPrxyCtrtCn(value: string | null): boolean { function isValidPrxyCtrtCn(value: string | null): boolean {
if (value === null) return true; if (value === null) return true;
if (!/^\d{8}~\d{8}$/.test(value)) return false; if (!/^\d{8}~\d{8}$/.test(value)) return false;
@@ -181,6 +198,8 @@ function validatePrxyCtrtCnInRows(rows: Record<string, string | number>[]): stri
// 하수도 통계 관리 > 06. 공공하수처리시설 // 하수도 통계 관리 > 06. 공공하수처리시설
const PublicSewageFacilityInp = () => { const PublicSewageFacilityInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -203,9 +222,15 @@ const PublicSewageFacilityInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
/** 탭키별 `engColNm -> columnGuide` 캐시 */ /** 탭키별 `engColNm -> columnGuide` 캐시 */
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({}); const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map()); const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]); const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
const [showDailog, setShowDailog] = useState<boolean>(false); const [showDailog, setShowDailog] = useState<boolean>(false);
@@ -244,10 +269,10 @@ const PublicSewageFacilityInp = () => {
alert('먼저 조회를 진행해주세요.'); alert('먼저 조회를 진행해주세요.');
return; return;
} }
const treatAsNoApplicable = getNoApplicableForRequest() || isNotApplicable; const treatAsNoApplicable = getNoApplicableForRequest();
const currentData = const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) { if (!treatAsNoApplicable && (!currentData || currentData.length === 0) && buildSubmitOkForToolbar(searchContextByTab.get(activeTab) ?? lastSearchedValues)) {
alert('해당사항 없음을 체크해주세요.'); alert('해당사항 없음을 체크해주세요.');
return; return;
} }
@@ -275,19 +300,13 @@ const PublicSewageFacilityInp = () => {
return Number.isFinite(num) ? num : 0; return Number.isFinite(num) ? num : 0;
}; };
const toNullableString = (v: unknown): string | null => {
if (v === null || v === undefined) return null;
const s = typeof v === 'string' ? v : String(v);
return s.trim() === '' ? null : s;
};
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ''), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: String(d.admdstNm ?? ''), admdstNm: nullableTrimmedString(d.admdstNm),
swtrmtFcltCd: String(d.swtrmtFcltCd ?? ''), swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: String(d.swtrmtFcltNm ?? ''), swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
fcltSn: String(d.fcltSn ?? ''), fcltSn: nullableTrimmedString(d.fcltSn),
fcltPstnCn: d.fcltPstnCn === '' ? null : toNullableString(d.fcltPstnCn), fcltPstnCn: nullableTrimmedString(d.fcltPstnCn),
inflwSwagePhysQnt: toNumber(d.inflwSwagePhysQnt), inflwSwagePhysQnt: toNumber(d.inflwSwagePhysQnt),
inflwSwageBoilQnt: toNumber(d.inflwSwageBoilQnt), inflwSwageBoilQnt: toNumber(d.inflwSwageBoilQnt),
inflwSwageAltdQnt: toNumber(d.inflwSwageAltdQnt), inflwSwageAltdQnt: toNumber(d.inflwSwageAltdQnt),
@@ -296,18 +315,19 @@ const PublicSewageFacilityInp = () => {
prcsAltdCpct: toNumber(d.prcsAltdCpct), prcsAltdCpct: toNumber(d.prcsAltdCpct),
prxyCtrtCn: normalizePrxyCtrtCn(d.prxyCtrtCn), prxyCtrtCn: normalizePrxyCtrtCn(d.prxyCtrtCn),
sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt), sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt),
rmrk: toNullableString(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
}; };
const getNoApplicableForRequest = (): boolean => { const getNoApplicableForRequest = (): boolean => {
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const searchContextForToolbar = lastSearchedValues ?? searchValues; const currentData =
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar); st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
const enterable = computeTabEnterable(activeTabStatus, inputPeriodEnterable); return buildNoApplicableForRequest(
const showEnterableToolbarButtons = enterable && gugunOk; searchContextForToolbar,
const noApplicableVisible = showEnterableToolbarButtons && showIsNotApplicable; Boolean(isNotApplicable),
return noApplicableVisible ? Boolean(isNotApplicable) : false; buildFacilitySaveList(currentData).length === 0,
);
}; };
const isCheckedRow = (row: Record<string, string | number>): boolean => { const isCheckedRow = (row: Record<string, string | number>): boolean => {
@@ -315,6 +335,28 @@ const PublicSewageFacilityInp = () => {
return v === 1 || v === '1' || v === 'Y' || v === 'y' || v === 'true'; return v === 1 || v === '1' || v === 'Y' || v === 'y' || v === 'true';
}; };
const isSelfMappingTarget = (
row: Record<string, string | number>,
mainFcltSn: string | null,
): boolean => Boolean(mainFcltSn) && String(row.fcltSn ?? '') === mainFcltSn;
const hasCheckedSelfMapping = (
rows: Record<string, string | number>[],
mainFcltSn: string | null,
): boolean => rows.some((r) => isSelfMappingTarget(r, mainFcltSn) && isCheckedRow(r));
/** 왼쪽에서 선택한 시설을 오른쪽에서 본인 체크하면 다른 시설은 해제 */
const applyExclusiveSelfMappingChecks = (
rows: Record<string, string | number>[],
mainFcltSn: string | null,
): Record<string, string | number>[] => {
if (!hasCheckedSelfMapping(rows, mainFcltSn)) return rows;
return rows.map((r) => ({
...r,
__checked: isSelfMappingTarget(r, mainFcltSn) ? 'Y' : '',
}));
};
const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList); const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
if (verificationBlocked) { if (verificationBlocked) {
@@ -330,7 +372,7 @@ const PublicSewageFacilityInp = () => {
return; return;
} }
const noApplicable = getNoApplicableForRequest(); const noApplicable = getNoApplicableForRequest();
const treatAsNoApplicable = noApplicable || isNotApplicable; const treatAsNoApplicable = noApplicable;
const currentData = const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
@@ -341,7 +383,7 @@ const PublicSewageFacilityInp = () => {
} }
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
if (!treatAsNoApplicable && submitList.length === 0) { if (!treatAsNoApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(activeTab) ?? lastSearchedValues)) {
alert('해당사항 없음을 체크해주세요.'); alert('해당사항 없음을 체크해주세요.');
return; return;
} }
@@ -354,8 +396,7 @@ const PublicSewageFacilityInp = () => {
const requestData = { const requestData = {
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
noApplicable, noApplicable,
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
@@ -385,14 +426,15 @@ const PublicSewageFacilityInp = () => {
if (finalStatus === '5') { if (finalStatus === '5') {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel('IMR 검증 진행 중...'); setGlobalProgressLabel('시계열 검증 진행 중...');
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? '', sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? '',
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? '', stats_yr: lastSearchedValues.year ?? '',
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2', lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === '4' ? '4' : '2',
gen_verify_stts_cd: nvpassed ? '1' : '2', gen_verify_stts_cd: nvpassed ? '1' : '2',
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -416,9 +458,8 @@ const PublicSewageFacilityInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -433,9 +474,8 @@ const PublicSewageFacilityInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -457,9 +497,8 @@ const PublicSewageFacilityInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : (lastSearchedValues.gugun ?? ''),
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -489,7 +528,7 @@ const PublicSewageFacilityInp = () => {
if (activeTab === 'ST00060002') { if (activeTab === 'ST00060002') {
const statusCodeResponse = await statisticsApi.CheckStatusCode({ const statusCodeResponse = await statisticsApi.CheckStatusCode({
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', sewerBzmnCd: toInputSearchRegion(sidoListRef.current, lastSearchedValues).sewerBzmnCd,
statsCd: 'ST00060001', statsCd: 'ST00060001',
statsYr: lastSearchedValues.year ?? '' statsYr: lastSearchedValues.year ?? ''
}); });
@@ -523,19 +562,36 @@ const PublicSewageFacilityInp = () => {
return; return;
} }
const saveList: PublicSewageMainFacilityMappingSaveItem[] = rightRows const checkedRows = rightRows.filter((r) => isCheckedRow(r));
.filter((r) => isCheckedRow(r)) const exclusiveSelf = hasCheckedSelfMapping(rightRows, selectedMainFacilitySn);
.map((r) => ({ const saveList: PublicSewageMainFacilityMappingSaveItem[] = (
fcltSn: String(r.fcltSn ?? ''), exclusiveSelf
admdstCd: String(r.admdstCd ?? ''), ? checkedRows.filter((r) => isSelfMappingTarget(r, selectedMainFacilitySn))
admdstNm: String(r.admdstNm ?? ''), : checkedRows
swtrmtFcltCd: String(r.swtrmtFcltCd ?? ''), ).map((r) => ({
swtrmtFcltNm: String(r.swtrmtFcltNm ?? ''), fcltSn: nullableTrimmedString(r.fcltSn),
isMainYn: String(r.isMainYn ?? ''), admdstCd: nullableTrimmedString(r.admdstCd),
})); admdstNm: nullableTrimmedString(r.admdstNm),
swtrmtFcltCd: nullableTrimmedString(r.swtrmtFcltCd),
swtrmtFcltNm: nullableTrimmedString(r.swtrmtFcltNm),
isMainYn: nullableTrimmedString(r.isMainYn),
}));
const exclusiveDroppedSns = exclusiveSelf
? new Set(
checkedRows
.filter((r) => !isSelfMappingTarget(r, selectedMainFacilitySn))
.map((r) => String(r.fcltSn ?? ''))
.filter((sn) => sn !== ''),
)
: new Set<string>();
const deleteList = rightRows const deleteList = rightRows
.filter((r) => String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r)) .filter((r) => {
const sn = String(r.fcltSn ?? '');
if (String(r.mappedYn ?? '') === 'Y' && !isCheckedRow(r)) return true;
return exclusiveDroppedSns.has(sn) && String(r.mappedYn ?? '') === 'Y';
})
.map((r) => String(r.fcltSn ?? '')) .map((r) => String(r.fcltSn ?? ''))
.filter((sn) => sn !== ''); .filter((sn) => sn !== '');
@@ -549,7 +605,7 @@ const PublicSewageFacilityInp = () => {
mainFacilityMappingSaveDraftRef.current = { mainFacilityMappingSaveDraftRef.current = {
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '', sewerBzmnCd: toInputSearchRegion(sidoListRef.current, lastSearchedValues).sewerBzmnCd,
admdstCd: String(selectedMain.admdstCd ?? ''), admdstCd: String(selectedMain.admdstCd ?? ''),
admdstNm: String(selectedMain.admdstNm ?? ''), admdstNm: String(selectedMain.admdstNm ?? ''),
mainFcltSn: String(selectedMain.fcltSn ?? ''), mainFcltSn: String(selectedMain.fcltSn ?? ''),
@@ -580,7 +636,7 @@ const PublicSewageFacilityInp = () => {
if (activeTab === 'ST00060001') { if (activeTab === 'ST00060001') {
const noApplicable = getNoApplicableForRequest(); const noApplicable = getNoApplicableForRequest();
const treatAsNoApplicable = noApplicable || isNotApplicable; const treatAsNoApplicable = noApplicable;
const currentData = const currentData =
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? []; st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
const prxyCtrtCnError = validatePrxyCtrtCnInRows(currentData); const prxyCtrtCnError = validatePrxyCtrtCnInRows(currentData);
@@ -589,7 +645,7 @@ const PublicSewageFacilityInp = () => {
return; return;
} }
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData); const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
if (!treatAsNoApplicable && saveList.length === 0) { if (!treatAsNoApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(activeTab) ?? lastSearchedValues)) {
alert('해당사항 없음을 체크해주세요.'); alert('해당사항 없음을 체크해주세요.');
return; return;
} }
@@ -600,8 +656,7 @@ const PublicSewageFacilityInp = () => {
const param: PublicSewageFacilitySaveRequest = { const param: PublicSewageFacilitySaveRequest = {
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: 'superAdmin' } : {}), ...(isAdminInfo ? { superAdmin: 'superAdmin' } : {}),
noApplicable, noApplicable,
@@ -644,35 +699,28 @@ const PublicSewageFacilityInp = () => {
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001'; const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null); const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUBLIC_SEWAGE_FACILITY) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(tabKey);
PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== 'success' || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = ( const commitToolbarSnapshotForLoadedTab = (
tabKey: string, tabKey: string,
@@ -695,12 +743,18 @@ const PublicSewageFacilityInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const buildPublicSewageFacilityParams = (values: SearchFormValues): PublicSewageFacilityRequestParams => ({ const buildPublicSewageFacilityParams = (values: SearchFormValues): PublicSewageFacilityRequestParams => ({
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd,
fcltNm: values.fctName, fcltNm: values.fctName,
statsYr: values.year, statsYr: values.year,
}); });
@@ -730,7 +784,11 @@ const PublicSewageFacilityInp = () => {
const statusInfo = response.data.statusInfo ?? null; const statusInfo = response.data.statusInfo ?? null;
const statsCd = response.data.commonInfo.statsCd || tabKey; const statsCd = response.data.commonInfo.statsCd || tabKey;
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const list = response.data.publicSewageFacilityList ?? []; const list = filterBySelectedAdmdstCd(
response.data.publicSewageFacilityList ?? [],
sidoListRef.current,
searchContext,
);
const convertedData = list.map((item) => ({ const convertedData = list.map((item) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...item, ...item,
@@ -759,6 +817,9 @@ const PublicSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
setRowDataMap((prev) => { setRowDataMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
next.set('ST00060001', gridRows); next.set('ST00060001', gridRows);
@@ -806,6 +867,9 @@ const PublicSewageFacilityInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo), patchVerificationFailedByTab(prev, tabKey, response.data.commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, response.data.commonInfo),
);
setRowDataMap((prev) => { setRowDataMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
next.set('MainFacility', leftGridRows); next.set('MainFacility', leftGridRows);
@@ -837,8 +901,7 @@ const PublicSewageFacilityInp = () => {
const request = { const request = {
statsYr: lastSearchedValues.year ?? '', statsYr: lastSearchedValues.year ?? '',
ctpvNm: lastSearchedValues.sido ?? '', ctpvNm: lastSearchedValues.sido ?? '',
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
}; };
@@ -968,7 +1031,9 @@ const PublicSewageFacilityInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
} }
@@ -1031,7 +1096,9 @@ const PublicSewageFacilityInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setInputPeriodEnterable(true); setInputPeriodEnterable(true);
periodEnterableRef.current = true; periodEnterableRef.current = true;
@@ -1058,9 +1125,8 @@ const PublicSewageFacilityInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const statsCdForCancel = tabStatsCdMap.get(activeTab) ?? activeTab; const statsCdForCancel = tabStatsCdMap.get(activeTab) ?? activeTab;
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues?.sido ?? '', ctpvNm: lastSearchedValues?.sido ?? '',
sggNm: lastSearchedValues?.gugun === 'ALL' ? '' : lastSearchedValues?.gugun ?? '',
statsYr: lastSearchedValues?.year ?? '', statsYr: lastSearchedValues?.year ?? '',
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForCancel, statsCd: statsCdForCancel,
@@ -1089,13 +1155,16 @@ const PublicSewageFacilityInp = () => {
const getGridColumnInfo = ( const getGridColumnInfo = (
tab: ColumnInfoList, tab: ColumnInfoList,
guideTabKey?: string, guideTabKey?: string,
canEdit = true,
): ColumnInfo => ({ ): ColumnInfo => ({
columns: (tab.columns ?? []).map((col) => { columns: (tab.columns ?? []).map((col) => {
const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim(); const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim();
return { const withGuide = {
...col, ...col,
tooltip: guide && guide.length > 0 ? guide : undefined, tooltip: guide && guide.length > 0 ? guide : undefined,
}; };
if (canEdit) return withGuide;
return { ...withGuide, editable: false as const };
}), }),
columnLayout: tab.columnLayout ?? [], columnLayout: tab.columnLayout ?? [],
}); });
@@ -1115,7 +1184,9 @@ const PublicSewageFacilityInp = () => {
const param: PublicSewageMappingTargetsRequestParams = { const param: PublicSewageMappingTargetsRequestParams = {
sewerBzmnCd: searchParamRef.current.sewerBzmnCd ?? '', sewerBzmnCd: searchParamRef.current.sewerBzmnCd ?? '',
ctpvNm: searchParamRef.current.ctpvNm ?? '', ctpvNm: searchParamRef.current.ctpvNm ?? '',
sggNm: searchParamRef.current.sggNm ?? '', ...(searchParamRef.current.sggNm != null
? { sggNm: searchParamRef.current.sggNm }
: {}),
statsYr: searchParamRef.current.statsYr ?? '', statsYr: searchParamRef.current.statsYr ?? '',
fcltSn, fcltSn,
}; };
@@ -1131,10 +1202,13 @@ const PublicSewageFacilityInp = () => {
if (rawList.length === 0) { if (rawList.length === 0) {
alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.'); alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.');
} }
const mappedTargetRows = rawList.map((item) => ({ const mappedTargetRows = applyExclusiveSelfMappingChecks(
...item, rawList.map((item) => ({
__checked: item.mappedYn === 'Y' ? 'Y' : '', ...item,
})); __checked: item.mappedYn === 'Y' ? 'Y' : '',
})),
fcltSn,
);
setRowDataMap((prev) => { setRowDataMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
next.set('MainFacilityTarget', toGridRows(mappedTargetRows)); next.set('MainFacilityTarget', toGridRows(mappedTargetRows));
@@ -1152,8 +1226,9 @@ const PublicSewageFacilityInp = () => {
return String(row.fcltSn ?? '') === selectedMainFacilitySn ? 'rg-cell-selected-row' : undefined; return String(row.fcltSn ?? '') === selectedMainFacilitySn ? 'rg-cell-selected-row' : undefined;
}; };
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const gugunOkOrNotDoList = buildGugunOkForToolbar(searchContextForToolbar); const gugunOkOrNotDoList = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = computeTabToolbarSnapshot( const activeTabToolbar = computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
@@ -1173,7 +1248,7 @@ const PublicSewageFacilityInp = () => {
isMainFacilityTab && isMainFacilityTab &&
tabRowDataExists('ST00060002') && tabRowDataExists('ST00060002') &&
gugunOkOrNotDoList && gugunOkOrNotDoList &&
(isAdminInfo || inputPeriodEnterable); inputPeriodEnterable;
const showSaveInToolbar = const showSaveInToolbar =
toolbarGroupAllowed && (baseShowSaveInToolbar || showMainFacilitySaveInToolbar); toolbarGroupAllowed && (baseShowSaveInToolbar || showMainFacilitySaveInToolbar);
const showEnterableToolbarButtons = const showEnterableToolbarButtons =
@@ -1198,9 +1273,8 @@ const PublicSewageFacilityInp = () => {
const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab; const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === 'ALL' ? '' : (ctx.gugun ?? ''),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1221,6 +1295,7 @@ const PublicSewageFacilityInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
fctName={true} fctName={true}
@@ -1241,12 +1316,12 @@ const PublicSewageFacilityInp = () => {
)} )}
{showEnterableToolbarButtons && !isMainFacilityTab && ( {showEnterableToolbarButtons && !isMainFacilityTab && (
<> <>
{showIsNotApplicable && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />} {showIsNotApplicable && submitOk && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />}
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} /> <UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && ( {!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1270,14 +1345,28 @@ const PublicSewageFacilityInp = () => {
</TabsList> </TabsList>
{PublicSewageFactInpColumnList.map((tab) => { {PublicSewageFactInpColumnList.map((tab) => {
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null;
const tabEnterableForGrid =
(computeTabEnterable(tabStatus, inputPeriodEnterable) ||
isAdminInfo) &&
inputPeriodEnterable;
if (tab.isSplitGrid && tab.left && tab.right) { if (tab.isSplitGrid && tab.left && tab.right) {
const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`; const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`;
const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`; const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`;
return ( return (
<TabsContent key={tab.tabKey} value={tab.tabKey}> <TabsContent key={tab.tabKey} value={tab.tabKey}>
<SplitDataGrid <SplitDataGrid
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)} leftColumnInfo={getGridColumnInfo(
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)} tab.left,
tab.tabKey,
tabEnterableForGrid,
)}
rightColumnInfo={getGridColumnInfo(
tab.right,
tab.tabKey,
tabEnterableForGrid,
)}
leftFixedColumns={tab.left.fixedColumns} leftFixedColumns={tab.left.fixedColumns}
rightFixedColumns={tab.right.fixedColumns} rightFixedColumns={tab.right.fixedColumns}
showHeaderCheckbox={true} showHeaderCheckbox={true}
@@ -1303,11 +1392,25 @@ const PublicSewageFacilityInp = () => {
const next = new Map(prev); const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])]; const rows = [...(next.get('MainFacilityTarget') ?? [])];
if (!rows[itemIndex]) return prev; if (!rows[itemIndex]) return prev;
const clicked = rows[itemIndex];
if (
checked &&
hasCheckedSelfMapping(rows, selectedMainFacilitySn) &&
!isSelfMappingTarget(clicked, selectedMainFacilitySn)
) {
return prev;
}
rows[itemIndex] = { rows[itemIndex] = {
...rows[itemIndex], ...clicked,
__checked: checked ? 'Y' : '', __checked: checked ? 'Y' : '',
}; };
next.set('MainFacilityTarget', rows); next.set(
'MainFacilityTarget',
applyExclusiveSelfMappingChecks(
rows,
selectedMainFacilitySn,
),
);
return next; return next;
}); });
} }
@@ -1318,16 +1421,59 @@ const PublicSewageFacilityInp = () => {
? (checked) => { ? (checked) => {
setRowDataMap((prev) => { setRowDataMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({ const rows = [...(next.get('MainFacilityTarget') ?? [])];
...r, if (
__checked: checked ? 'Y' : '', checked &&
})); hasCheckedSelfMapping(rows, selectedMainFacilitySn)
next.set('MainFacilityTarget', rows); ) {
next.set(
'MainFacilityTarget',
applyExclusiveSelfMappingChecks(
rows,
selectedMainFacilitySn,
),
);
return next;
}
next.set(
'MainFacilityTarget',
rows.map((r) => ({
...r,
__checked:
checked &&
!isSelfMappingTarget(
r,
selectedMainFacilitySn,
)
? 'Y'
: '',
})),
);
return next; return next;
}); });
} }
: undefined : undefined
} }
rightCheckableCallback={
tab.tabKey === 'ST00060002'
? (row) => {
const rows =
rowDataMap.get('MainFacilityTarget') ?? [];
if (
!hasCheckedSelfMapping(
rows,
selectedMainFacilitySn,
)
) {
return true;
}
return isSelfMappingTarget(
row,
selectedMainFacilitySn,
);
}
: undefined
}
setLeftRowData={(updater) => { setLeftRowData={(updater) => {
setRowDataMap((prev) => { setRowDataMap((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -1357,11 +1503,10 @@ const PublicSewageFacilityInp = () => {
); );
} }
const gridColumnInfo = getGridColumnInfo(tab); const gridColumnInfo = getGridColumnInfo(
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null; tab,
const tabEnterableForGrid = computeTabEnterable( undefined,
tabStatus, tabEnterableForGrid,
inputPeriodEnterable,
); );
const excelToolbarEnabledForTab = const excelToolbarEnabledForTab =
tabRowDataExists(tab.tabKey) && tabRowDataExists(tab.tabKey) &&
@@ -1371,7 +1516,7 @@ const PublicSewageFacilityInp = () => {
return ( return (
<TabsContent key={tab.tabKey} value={tab.tabKey}> <TabsContent key={tab.tabKey} value={tab.tabKey}>
<DataGrid <DataGrid
key={tab.tabKey} key={`${tab.tabKey}-${tabEnterableForGrid ? 'enterable' : 'readonly'}`}
columnInfo={gridColumnInfo} columnInfo={gridColumnInfo}
rowStartNum={tab.rowStartNum} rowStartNum={tab.rowStartNum}
excelExportFileSuffix={tab.tabLabel || tab.tabKey} excelExportFileSuffix={tab.tabLabel || tab.tabKey}
@@ -1400,14 +1545,29 @@ const PublicSewageFacilityInp = () => {
showHeaderCheckbox={false} showHeaderCheckbox={false}
enableSort={false} enableSort={false}
cellStyleCallback={({ row, fieldName }) => { cellStyleCallback={({ row, fieldName }) => {
const lockPrxyCtrtCn =
fieldName === 'prxyCtrtCn' && isMainFacilityRow(row);
const verification = (row as GridRowWithVerification).verificationResult; const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return; let verifyStyle: string | undefined;
const errors = verification[fieldName]; if (verification) {
if (Array.isArray(errors) && errors.length > 0) { const errors = verification[fieldName];
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB"); if (Array.isArray(errors) && errors.length > 0) {
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
verifyStyle = hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
}
} }
if (lockPrxyCtrtCn) {
return {
editable: false,
readOnly: true,
styleName: [verifyStyle, 'rg-cell-readonly'].filter(Boolean).join(' ') || 'rg-cell-readonly',
};
}
return verifyStyle;
}} }}
footerVerificationResult={
footerVerificationByTab.get(tab.tabKey) ?? null
}
cellTooltipCallback={({ row, fieldName }) => { cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult; const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return; if (!verification) return;
@@ -1416,7 +1576,7 @@ const PublicSewageFacilityInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "); e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? ""; const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? ""; const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
@@ -1,20 +1,56 @@
import { import {
type RefObject,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type RefObject,
} from "react"; } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid"; import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
PUMPING_STATION_STATS_CD,
type PumpingStationItem,
type PumpingStationRequestParams,
type PumpingStationSaveItem,
type PumpingStationVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-pumping-station-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
SewerageStatusInfo,
Sgg,
Sido,
} from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
resolveNoApplicableForRequest,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation } from "@common/components"; import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import AddArticleButton from "@/components/button/AddArticleButton"; import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import DeleteArticleButton from "@/components/button/DeleteArticleButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton";
@@ -24,49 +60,24 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import { PumpingStationInpColumn } from "@/components/util/column/input/PumpingStationInpColumn";
import SubmitDailog from "./SubmitDailog";
import type { import type {
CellStyleCallbackParams, CellStyleCallbackParams,
CellTooltipCallbackParams, CellTooltipCallbackParams,
ColumnConfig, ColumnConfig,
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList"; import { PumpingStationInpColumn } from "@/components/util/column/input/PumpingStationInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import {
PUMPING_STATION_STATS_CD,
type PumpingStationItem,
type PumpingStationRequestParams,
type PumpingStationSaveItem,
type PumpingStationVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-pumping-station-interface";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
Sgg,
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: PumpingStationVerificationResult; verificationResult?: PumpingStationVerificationResult;
@@ -91,10 +102,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -108,7 +131,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk; isSubmitted && periodEnterable && gugunOk;
@@ -128,9 +151,8 @@ function buildPumpingStationSearchParam(
const fcltNm = values.fctName?.trim(); const fcltNm = values.fctName?.trim();
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(fcltNm ? { fcltNm } : {}), ...(fcltNm ? { fcltNm } : {}),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
@@ -190,13 +212,6 @@ function mapPumpingStationListToGridRows(
}); });
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -205,9 +220,9 @@ function toGridNumber(value: string | number | null | undefined): number {
function resolvePumpingStationAdmdst( function resolvePumpingStationAdmdst(
row: Record<string, string | number>, row: Record<string, string | number>,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const existingCd = String(row.admdstCd ?? "").trim(); const existingCd = nullableTrimmedString(row.admdstCd);
const existingNm = String(row.admdstNm ?? "").trim(); const existingNm = nullableTrimmedString(row.admdstNm);
if (existingCd && existingNm) { if (existingCd && existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm }; return { admdstCd: existingCd, admdstNm: existingNm };
} }
@@ -216,7 +231,10 @@ function resolvePumpingStationAdmdst(
if (matched) { if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; return {
admdstCd: existingCd,
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
};
} }
/** 그리드 행 → ST00050001 저장/삭제 API 항목 */ /** 그리드 행 → ST00050001 저장/삭제 API 항목 */
@@ -225,15 +243,14 @@ function buildPumpingStationSaveItem(
sggOptions: Sgg[] = [], sggOptions: Sgg[] = [],
): PumpingStationSaveItem { ): PumpingStationSaveItem {
const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions); const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions);
const fcltSn = String(row.fcltSn ?? "").trim();
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: fcltSn, fcltSn: nullableTrimmedString(row.fcltSn),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""), swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
fcltNm: String(row.fcltNm ?? ""), fcltNm: nullableTrimmedString(row.fcltNm),
pumpSeNm: String(row.pumpSeNm ?? ""), pumpSeNm: nullableTrimmedString(row.pumpSeNm),
fcltPstnCn: String(row.fcltPstnCn ?? ""), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planMltvArea: toGridNumber(row.planMltvArea), planMltvArea: toGridNumber(row.planMltvArea),
sndyArea: toGridNumber(row.sndyArea), sndyArea: toGridNumber(row.sndyArea),
@@ -285,11 +302,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function createEmptyPumpingStationRow( function createEmptyPumpingStationRow(
@@ -311,6 +328,8 @@ const PUMPING_STATION_FACILITY_NAME_GUIDE_MESSAGE =
// 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴) // 하수도 통계 관리 > 05. 펌프장 (SewageReservoirPumpInp 단일 탭 패턴)
const PumpingStationInp = () => { const PumpingStationInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -333,6 +352,8 @@ const PumpingStationInp = () => {
const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null); const [statusInfo, setStatusInfo] = useState<SewerageStatusInfo | null>(null);
const [statsCd, setStatsCd] = useState(""); const [statsCd, setStatsCd] = useState("");
const [isVerificationFailed, setIsVerificationFailed] = useState(false); const [isVerificationFailed, setIsVerificationFailed] = useState(false);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]); const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
@@ -353,7 +374,10 @@ const PumpingStationInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null); const sggOptionsSidoRef = useRef<string | null>(null);
const [searchLoaded, setSearchLoaded] = useState(false); const [searchLoaded, setSearchLoaded] = useState(false);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.PUMPING_STATION) return;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
try { try {
@@ -377,7 +401,7 @@ const PumpingStationInp = () => {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => { const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") { if (!sido || sido === "ALL") {
@@ -397,7 +421,7 @@ const PumpingStationInp = () => {
const columnInfo = useMemo(() => { const columnInfo = useMemo(() => {
const enterable = const enterable =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; (computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = PumpingStationInpColumn.columns ?? []; const rawColumns = PumpingStationInpColumn.columns ?? [];
const base = enterable const base = enterable
? rawColumns ? rawColumns
@@ -425,7 +449,7 @@ const PumpingStationInp = () => {
}, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]); }, [statusInfo, inputPeriodEnterable, isAdminInfo, sggOptions, columnGuidesByEngCol]);
const canEditGrid = const canEditGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo; (computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const getCurrentRows = (): Record<string, string | number>[] => const getCurrentRows = (): Record<string, string | number>[] =>
gridGetRowsRef.current?.() ?? rowData; gridGetRowsRef.current?.() ?? rowData;
@@ -457,7 +481,11 @@ const PumpingStationInp = () => {
const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } = const { commonInfo, statusInfo: nextStatusInfo, pumpingStationList } =
response.data; response.data;
const list = pumpingStationList ?? []; const list = filterBySelectedAdmdstCd(
pumpingStationList ?? [],
sidoListRef.current,
values,
);
const gridRows = mapPumpingStationListToGridRows(list, commonInfo.ctpvNm); const gridRows = mapPumpingStationListToGridRows(list, commonInfo.ctpvNm);
setPumpingStationDeleteList([]); setPumpingStationDeleteList([]);
@@ -465,6 +493,7 @@ const PumpingStationInp = () => {
setStatusInfo(nextStatusInfo); setStatusInfo(nextStatusInfo);
setStatsCd(commonInfo.statsCd); setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
setFooterVerificationResult(commonInfo.verificationResult ?? null);
if (overrides?.mode !== "PRE") { if (overrides?.mode !== "PRE") {
setIsNotApplicable(gridRows.length === 0); setIsNotApplicable(gridRows.length === 0);
} else if (gridRows.length > 0) { } else if (gridRows.length > 0) {
@@ -524,6 +553,7 @@ const PumpingStationInp = () => {
setStatusInfo(null); setStatusInfo(null);
setStatsCd(""); setStatsCd("");
setIsVerificationFailed(false); setIsVerificationFailed(false);
setFooterVerificationResult(null);
setIsNotApplicable(false); setIsNotApplicable(false);
setInputPeriodEnterable(true); setInputPeriodEnterable(true);
setSggOptions([]); setSggOptions([]);
@@ -573,12 +603,8 @@ const PumpingStationInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCd || PUMPING_STATION_STATS_CD, statsCd: statsCd || PUMPING_STATION_STATS_CD,
@@ -613,9 +639,9 @@ const PumpingStationInp = () => {
return; return;
} }
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -642,7 +668,7 @@ const PumpingStationInp = () => {
if (!(await confirm("저장 하시겠습니까?"))) return; if (!(await confirm("저장 하시겠습니까?"))) return;
commitGrid(); commitGrid();
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
const saveList = noApplicable const saveList = noApplicable
? [] ? []
@@ -653,7 +679,7 @@ const PumpingStationInp = () => {
.filter((row) => String(row.fcltSn ?? "").trim() !== "") .filter((row) => String(row.fcltSn ?? "").trim() !== "")
.map((row) => buildPumpingStationSaveItem(row, sggOptions)); .map((row) => buildPumpingStationSaveItem(row, sggOptions));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -664,11 +690,7 @@ const PumpingStationInp = () => {
const response = await statisticsInputPumpingStationApi.savePumpingStationList({ const response = await statisticsInputPumpingStationApi.savePumpingStationList({
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
noApplicable, noApplicable,
@@ -702,7 +724,7 @@ const PumpingStationInp = () => {
} }
commitGrid(); commitGrid();
const noApplicable = isNotApplicable; const noApplicable = buildNoApplicableForRequest(lastSearchedValues, isNotApplicable, getCurrentRows().length === 0);
const currentData = getCurrentRows(); const currentData = getCurrentRows();
const submitList = noApplicable const submitList = noApplicable
? [] ? []
@@ -711,19 +733,15 @@ const PumpingStationInp = () => {
.filter((row) => String(row.fcltSn ?? "").trim() !== "") .filter((row) => String(row.fcltSn ?? "").trim() !== "")
.map((row) => buildPumpingStationSaveItem(row, sggOptions)); .map((row) => buildPumpingStationSaveItem(row, sggOptions));
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
const statsCdForSubmit = statsCd || PUMPING_STATION_STATS_CD; const statsCdForSubmit = statsCd || PUMPING_STATION_STATS_CD;
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForSubmit, statsCd: statsCdForSubmit,
}; };
@@ -740,11 +758,7 @@ const PumpingStationInp = () => {
const response = await statisticsInputPumpingStationApi.submitPumpingStationList({ const response = await statisticsInputPumpingStationApi.submitPumpingStationList({
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
@@ -770,15 +784,16 @@ const PumpingStationInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForSubmit, stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2", statusInfo?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -925,10 +940,10 @@ const PumpingStationInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -937,7 +952,7 @@ const PumpingStationInp = () => {
} }
}; };
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = lastSearchedValues;
const toolbarSnapshot = computeTabToolbarSnapshot( const toolbarSnapshot = computeTabToolbarSnapshot(
statusInfo, statusInfo,
rowData.length > 0, rowData.length > 0,
@@ -957,23 +972,23 @@ const PumpingStationInp = () => {
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
(showSaveInToolbar || showEnterableToolbarButtons); (showSaveInToolbar || showEnterableToolbarButtons);
const tabEnterableForGrid =
(computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showNotApplicable = const showNotApplicable =
searchLoaded && searchLoaded &&
hasSearched && hasSearched &&
rowData.length === 0 && rowData.length === 0 &&
computeTabEnterable(statusInfo, inputPeriodEnterable); computeTabEnterable(statusInfo, inputPeriodEnterable) &&
submitOk;
const tabEnterableForGrid =
computeTabEnterable(statusInfo, inputPeriodEnterable) || isAdminInfo;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const verificationQueryContext = (() => { const verificationQueryContext = (() => {
const ctx = lastSearchedValues ?? searchValues; const ctx = lastSearchedValues ?? searchValues;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -989,6 +1004,7 @@ const PumpingStationInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
fctName={true} fctName={true}
@@ -1008,10 +1024,10 @@ const PumpingStationInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{statusInfo?.laeSbmsnSttsCd === "2" && ( {statusInfo?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1056,6 +1072,7 @@ const PumpingStationInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
/> />
</div> </div>
@@ -1,44 +1,17 @@
import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react"; import { type Dispatch, type SetStateAction,useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import AddArticleButton from "@/components/button/AddArticleButton"; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import DeleteArticleButton from "@/components/button/DeleteArticleButton"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import LastYearDataButton from "@/components/button/LastYearDataButton"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton"; import { nullableTrimmedString } from "@/components/util/input/nullableString";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type {
CellStyleCallback,
CellStyleCallbackParams,
CellStyleCallbackResult,
CellTooltipCallbackParams,
ColumnConfig,
ColumnInfo,
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { ExcretaRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn";
import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn";
import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn";
import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
ExcretaRelatedIndustrySaveItem, ExcretaRelatedIndustrySaveItem,
@@ -54,27 +27,70 @@ import {
SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD, SEWAGE_EXCRETA_PUBLIC_INSTITUTION_SEWER_STATS_CD,
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface"; } from "@/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type { import type {
ApiResponse, ApiResponse,
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
Sgg, Sgg,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import {
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; filterBySelectedAdmdstCd,
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; isGugunOkForToolbar,
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; isSubmitOkForToolbar,
import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; resolveNoApplicableForRequest,
import { useAdminStore } from "@/store/admin-store"; toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog"; import SubmitDailog from "./SubmitDailog";
const SewageExcretaRelatedInpTabList = [ import AddArticleButton from "@/components/button/AddArticleButton";
PublicInstitutionSewerIndustryInpColumn, import CancelSubmitButton from "@/components/button/CancelSubmitButton";
PublicInstitutionExcretaIndustryInpColumn, import DeleteArticleButton from "@/components/button/DeleteArticleButton";
ExcretaRelatedIndustryInpColumn, import LastYearDataButton from "@/components/button/LastYearDataButton";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type {
CellStyleCallback,
CellStyleCallbackParams,
CellStyleCallbackResult,
CellTooltipCallbackParams,
ColumnConfig,
ColumnInfo,
} from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ExcretaRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/ExcretaRelatedIndustryInpColumn";
import { PrivateSewageTreatRelatedIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PrivateSewageTreatRelatedIndustryInpColumn";
import { PublicInstitutionExcretaIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionExcretaIndustryInpColumn";
import { PublicInstitutionSewerIndustryInpColumn } from "@/components/util/column/input/sewageExcretaRelated/PublicInstitutionSewerIndustryInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
const SewageExcretaRelatedInpTabList = [
PublicInstitutionSewerIndustryInpColumn,
PublicInstitutionExcretaIndustryInpColumn,
ExcretaRelatedIndustryInpColumn,
PrivateSewageTreatRelatedIndustryInpColumn, PrivateSewageTreatRelatedIndustryInpColumn,
] as const; ] as const;
@@ -150,10 +166,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -167,7 +195,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -312,7 +340,7 @@ function buildSggNmColumn(
function resolvePublicInstitutionAdmdst( function resolvePublicInstitutionAdmdst(
row: Record<string, string | number>, row: Record<string, string | number>,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const sggNm = String(row.sggNm ?? "").trim(); const sggNm = String(row.sggNm ?? "").trim();
if (sggNm) { if (sggNm) {
const matched = sggOptions.find((s) => s.gugun === sggNm); const matched = sggOptions.find((s) => s.gugun === sggNm);
@@ -320,9 +348,12 @@ function resolvePublicInstitutionAdmdst(
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
} }
const existingCd = String(row.admdstCd ?? "").trim(); const existingCd = nullableTrimmedString(row.admdstCd);
const existingNm = String(row.admdstNm ?? "").trim(); const existingNm = nullableTrimmedString(row.admdstNm);
return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; return {
admdstCd: existingCd,
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
};
} }
function mergeGridRowsWithMapRows( function mergeGridRowsWithMapRows(
@@ -362,13 +393,6 @@ function mergeGridRowsWithArticleMeta(
}); });
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -383,8 +407,8 @@ function buildPublicInstitutionIndustrySaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
ognzSn: String(row.ognzSn ?? "").trim(), ognzSn: nullableTrimmedString(row.ognzSn),
ognzNm: String(row.ognzNm ?? "").trim(), ognzNm: nullableTrimmedString(row.ognzNm),
pbadmsNope: toGridNumber(row.pbadmsNope), pbadmsNope: toGridNumber(row.pbadmsNope),
engrkCnt: toGridNumber(row.engrkCnt), engrkCnt: toGridNumber(row.engrkCnt),
mchnCnt: toGridNumber(row.mchnCnt), mchnCnt: toGridNumber(row.mchnCnt),
@@ -402,8 +426,8 @@ function buildExcretaRelatedIndustrySaveItem(
row: Record<string, string | number>, row: Record<string, string | number>,
): ExcretaRelatedIndustrySaveItem { ): ExcretaRelatedIndustrySaveItem {
return { return {
admdstCd: String(row.admdstCd ?? "").trim(), admdstCd: nullableTrimmedString(row.admdstCd),
admdstNm: String(row.admdstNm ?? "").trim(), admdstNm: nullableTrimmedString(row.admdstNm),
bzentyCnt: toGridNumber(row.bzentyCnt), bzentyCnt: toGridNumber(row.bzentyCnt),
empNope: toGridNumber(row.empNope), empNope: toGridNumber(row.empNope),
n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt), n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt),
@@ -420,8 +444,8 @@ function buildPrivateSewageTreatRelatedIndustrySaveItem(
row: Record<string, string | number>, row: Record<string, string | number>,
): PrivateSewageTreatRelatedIndustrySaveItem { ): PrivateSewageTreatRelatedIndustrySaveItem {
return { return {
admdstCd: String(row.admdstCd ?? "").trim(), admdstCd: nullableTrimmedString(row.admdstCd),
admdstNm: String(row.admdstNm ?? "").trim(), admdstNm: nullableTrimmedString(row.admdstNm),
designMdsrCnt: toGridNumber(row.designMdsrCnt), designMdsrCnt: toGridNumber(row.designMdsrCnt),
designMdsrNope: toGridNumber(row.designMdsrNope), designMdsrNope: toGridNumber(row.designMdsrNope),
mfbizCnt: toGridNumber(row.mfbizCnt), mfbizCnt: toGridNumber(row.mfbizCnt),
@@ -521,10 +545,10 @@ function verificationCellTooltip({
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -539,9 +563,8 @@ function buildSewageExcretaRelatedSearchParam(
): SewageExcretaRelatedRequestParams { ): SewageExcretaRelatedRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -549,8 +572,9 @@ function buildSewageExcretaRelatedSearchParam(
function mapListToGridRows<T extends { verificationResult?: SewageExcretaRelatedVerificationResult }>( function mapListToGridRows<T extends { verificationResult?: SewageExcretaRelatedVerificationResult }>(
list: T[] | undefined, list: T[] | undefined,
ctpvNm: string, ctpvNm: string,
values?: SearchFormValues | null,
): GridRowWithVerification[] { ): GridRowWithVerification[] {
return (list ?? []).map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification; const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification;
if (item.verificationResult) { if (item.verificationResult) {
row.verificationResult = item.verificationResult; row.verificationResult = item.verificationResult;
@@ -577,15 +601,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 12. 하수 및 분뇨관련산업 // 하수도 통계 관리 > 12. 하수 및 분뇨관련산업
const SewageExcretaRelatedInp = () => { const SewageExcretaRelatedInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const isAdminInfo = useAdminStore((state) => state.isAdminInfo); const isAdminInfo = useAdminStore((state) => state.isAdminInfo);
@@ -602,7 +628,7 @@ const SewageExcretaRelatedInp = () => {
const [tabStatusInfoMap, setTabStatusInfoMap] = useState< const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
Map<string, SewerageStatusInfo | null> Map<string, SewerageStatusInfo | null>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [columnGuidesByTab, setColumnGuidesByTab] = useState< const [columnGuidesByTab, setColumnGuidesByTab] = useState<
@@ -612,6 +638,9 @@ const SewageExcretaRelatedInp = () => {
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>( const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(
null, null,
); );
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [hasSearched, setHasSearched] = useState(false); const [hasSearched, setHasSearched] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
@@ -620,6 +649,9 @@ const SewageExcretaRelatedInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>( const [isNotApplicableByTab, setIsNotApplicableByTab] = useState<Map<string, boolean>>(
() => new Map(), () => new Map(),
); );
@@ -645,36 +677,29 @@ const SewageExcretaRelatedInp = () => {
const sggOptionsSidoRef = useRef<string | null>(null); const sggOptionsSidoRef = useRef<string | null>(null);
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_RELATED) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SewageExcretaRelatedInpTabList.map(async (tab) => {
const tabKey = tab.tabKey;
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const shouldShowNotApplicable = ( const shouldShowNotApplicable = (
tabKey: string, tabKey: string,
@@ -739,6 +764,13 @@ const SewageExcretaRelatedInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const markTabLoaded = (tabKey: string) => { const markTabLoaded = (tabKey: string) => {
@@ -854,6 +886,7 @@ const SewageExcretaRelatedInp = () => {
const gridRows = mapListToGridRows( const gridRows = mapListToGridRows(
publicInstitutionSewerIndustryList, publicInstitutionSewerIndustryList,
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
syncPublicInstitutionArticleTracking( syncPublicInstitutionArticleTracking(
tabKey, tabKey,
@@ -871,6 +904,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -897,6 +933,7 @@ const SewageExcretaRelatedInp = () => {
const gridRows = mapListToGridRows( const gridRows = mapListToGridRows(
publicInstitutionExcretaIndustryList, publicInstitutionExcretaIndustryList,
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
syncPublicInstitutionArticleTracking( syncPublicInstitutionArticleTracking(
tabKey, tabKey,
@@ -914,6 +951,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -936,7 +976,7 @@ const SewageExcretaRelatedInp = () => {
return false; return false;
} }
const { commonInfo, statusInfo, excretaRelatedIndustryList } = response.data; const { commonInfo, statusInfo, excretaRelatedIndustryList } = response.data;
const gridRows = mapListToGridRows(excretaRelatedIndustryList, commonInfo.ctpvNm); const gridRows = mapListToGridRows(excretaRelatedIndustryList, commonInfo.ctpvNm, searchContext);
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
m.set(ExcretaRelatedIndustryInpColumn.tabKey, gridRows); m.set(ExcretaRelatedIndustryInpColumn.tabKey, gridRows);
}); });
@@ -945,6 +985,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -970,6 +1013,7 @@ const SewageExcretaRelatedInp = () => {
const gridRows = mapListToGridRows( const gridRows = mapListToGridRows(
privateSewageTreatRelatedIndustryList, privateSewageTreatRelatedIndustryList,
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
m.set(PrivateSewageTreatRelatedIndustryInpColumn.tabKey, gridRows); m.set(PrivateSewageTreatRelatedIndustryInpColumn.tabKey, gridRows);
@@ -979,6 +1023,9 @@ const SewageExcretaRelatedInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -998,6 +1045,7 @@ const SewageExcretaRelatedInp = () => {
setRowDataMap(new Map()); setRowDataMap(new Map());
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -1010,6 +1058,7 @@ const SewageExcretaRelatedInp = () => {
setLoadedTabKeys(new Set()); setLoadedTabKeys(new Set());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setIsNotApplicableByTab(new Map()); setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map()); setDeleteListByTab(new Map());
clearArticleOgnzSnTracking( clearArticleOgnzSnTracking(
@@ -1060,7 +1109,9 @@ const SewageExcretaRelatedInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setIsNotApplicableByTab(new Map()); setIsNotApplicableByTab(new Map());
setDeleteListByTab(new Map()); setDeleteListByTab(new Map());
clearArticleOgnzSnTracking( clearArticleOgnzSnTracking(
@@ -1125,9 +1176,8 @@ const SewageExcretaRelatedInp = () => {
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: ctx.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido ?? "", ctpvNm: ctx.sido ?? "",
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year ?? "", statsYr: ctx.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -1218,10 +1268,10 @@ const SewageExcretaRelatedInp = () => {
} }
commitActiveGrid(); commitActiveGrid();
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
if (NOT_APPLICABLE_TAB_KEYS.has(tabKey)) { if (NOT_APPLICABLE_TAB_KEYS.has(tabKey)) {
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1255,18 +1305,14 @@ const SewageExcretaRelatedInp = () => {
const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey); const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey);
const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const mapRows = rowDataMap.get(tabKey) ?? []; const mapRows = rowDataMap.get(tabKey) ?? [];
const searchContextBase = { const searchContextBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
}; };
let submitList: let submitList:
@@ -1327,7 +1373,7 @@ const SewageExcretaRelatedInp = () => {
) )
.map((row) => buildPublicInstitutionIndustrySaveItem(row, list)); .map((row) => buildPublicInstitutionIndustrySaveItem(row, list));
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1431,15 +1477,16 @@ const SewageExcretaRelatedInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -1513,18 +1560,14 @@ const SewageExcretaRelatedInp = () => {
const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey); const isDistrictRowTab = DISTRICT_ROW_TAB_KEYS.has(tabKey);
const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey); const isPublicInstitutionTab = ARTICLE_EDIT_TAB_KEYS.has(tabKey);
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const mapRows = rowDataMap.get(tabKey) ?? []; const mapRows = rowDataMap.get(tabKey) ?? [];
const baseSaveRequest = { const baseSaveRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(isPublicInstitutionTab && noApplicable ...(isPublicInstitutionTab && noApplicable
? { noApplicable: true as const } ? { noApplicable: true as const }
: {}), : {}),
@@ -1607,7 +1650,7 @@ const SewageExcretaRelatedInp = () => {
) )
.map((row) => buildPublicInstitutionIndustrySaveItem(row, list)); .map((row) => buildPublicInstitutionIndustrySaveItem(row, list));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1698,8 +1741,8 @@ const SewageExcretaRelatedInp = () => {
newRow.__isNewArticle = 1; newRow.__isNewArticle = 1;
if (sggNm) { if (sggNm) {
const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(newRow, list); const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(newRow, list);
newRow.admdstCd = admdstCd; newRow.admdstCd = admdstCd ?? "";
newRow.admdstNm = admdstNm; newRow.admdstNm = admdstNm ?? "";
} }
flushSync(() => { flushSync(() => {
@@ -1816,10 +1859,10 @@ const SewageExcretaRelatedInp = () => {
for (const tab of SewageExcretaRelatedInpTabList) { for (const tab of SewageExcretaRelatedInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1871,11 +1914,10 @@ const SewageExcretaRelatedInp = () => {
[publicInstitutionArticleCellStyleCallback], [publicInstitutionArticleCellStyleCallback],
); );
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab); const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab);
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
activeTabHasRowData, activeTabHasRowData,
@@ -1893,10 +1935,13 @@ const SewageExcretaRelatedInp = () => {
(activeTabToolbar.showActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab))); (activeTabToolbar.showActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab)));
const showArticleEditButtons = const showArticleEditButtons =
ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons; ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons;
const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showNotApplicableForActiveTab = const showNotApplicableForActiveTab =
NOT_APPLICABLE_TAB_KEYS.has(activeTab) && NOT_APPLICABLE_TAB_KEYS.has(activeTab) &&
loadedTabKeys.has(activeTab) && loadedTabKeys.has(activeTab) &&
shouldShowNotApplicable(activeTab, activeTabHasRowData, activeTabStatus); shouldShowNotApplicable(activeTab, activeTabHasRowData, activeTabStatus) &&
submitOk;
const showSecondaryToolbarCard = const showSecondaryToolbarCard =
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
@@ -1914,16 +1959,14 @@ const SewageExcretaRelatedInp = () => {
}, },
[isNotApplicableByTab, setIsNotApplicableForTab], [isNotApplicableByTab, setIsNotApplicableForTab],
); );
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
const verificationQueryContext = (() => { const verificationQueryContext = (() => {
const ctx = lastSearchedValues ?? searchValues; const ctx = lastSearchedValues ?? searchValues;
const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab; const statsCd = tabStatsCdMap.get(activeTab) ?? activeTab;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1944,6 +1987,7 @@ const SewageExcretaRelatedInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -1951,8 +1995,8 @@ const SewageExcretaRelatedInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={ isVerificationFailed={
@@ -1964,19 +2008,19 @@ const SewageExcretaRelatedInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void lastYearDataFunc()} getLastYearDataFunc={() => void lastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div>
</div> </div>
</div>
)} )}
{showSecondaryToolbarCard && ( {showSecondaryToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showNotApplicableForActiveTab && ( {showNotApplicableForActiveTab && (
<NotApplicableCheckButton <NotApplicableCheckButton
isCheck={isNotApplicableForActiveTab} isCheck={isNotApplicableForActiveTab}
@@ -1986,11 +2030,11 @@ const SewageExcretaRelatedInp = () => {
{showArticleEditButtons && ( {showArticleEditButtons && (
<> <>
<AddArticleButton addArticleFunc={() => void addArticleFunc()} /> <AddArticleButton addArticleFunc={() => void addArticleFunc()} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} /> <DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</> </>
)} )}
</div>
</div> </div>
</div>
)} )}
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -2018,8 +2062,8 @@ const SewageExcretaRelatedInp = () => {
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable(tabStatusInfo, inputPeriodEnterable) || (computeTabEnterable(tabStatusInfo, inputPeriodEnterable) ||
isAdminInfo; isAdminInfo) && inputPeriodEnterable;
return ( return (
<TabsContent key={tabKey} value={tabKey}> <TabsContent key={tabKey} value={tabKey}>
@@ -2060,6 +2104,9 @@ const SewageExcretaRelatedInp = () => {
} }
getGridRef={activeTab === tabKey ? getGridRef : undefined} getGridRef={activeTab === tabKey ? getGridRef : undefined}
cellStyleCallback={resolveGridCellStyleCallback(tabKey)} cellStyleCallback={resolveGridCellStyleCallback(tabKey)}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
/> />
</TabsContent> </TabsContent>
@@ -2107,7 +2154,7 @@ const SewageExcretaRelatedInp = () => {
false, false,
); );
}} }}
/> />
</> </>
); );
}; };
@@ -1,19 +1,56 @@
import { import {
type Dispatch,
type SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type Dispatch,
type SetStateAction,
} from "react"; } from "react";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
DigestorOperatingStatusSaveItem,
ExcretaSlurgeProcessingStatusSaveItem,
SewageExcretaSlurgeRequestParams,
SewageExcretaSlurgeVerificationResult,
SewageSlurgeProcessingFacStatusSaveItem,
SlurgeProcessingFacStatusSaveItem,
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface";
import {
SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD,
SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD,
SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD,
SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD,
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type { ApiResponse, cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, Sgg, Sido } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
resolveNoApplicableForRequest,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import AddArticleButton from "@/components/button/AddArticleButton"; import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
@@ -24,12 +61,10 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import SubmitDailog from "./SubmitDailog";
import type { import type {
CellEditedCallback, CellEditedCallback,
CellStyleCallback, CellStyleCallback,
@@ -41,42 +76,23 @@ import type {
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { import {
DigestorOperatingStatusInpColumn,
DIGESTOR_OPERATING_STATUS_EN_YN_FIELD, DIGESTOR_OPERATING_STATUS_EN_YN_FIELD,
DigestorOperatingStatusInpColumn,
isDigestorOperatingRowDisabled, isDigestorOperatingRowDisabled,
} from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn"; } from "@/components/util/column/input/sewageExcretaSlurge/DigestorOperatingStatusInpColumn";
import { ExcretaSlurgeProcessingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn"; import { ExcretaSlurgeProcessingStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/ExcretaSlurgeProcessingStatusInpColumn";
import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn"; import { SewageSlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SewageSlurgeProcessingFacStatusInpColumn";
import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn"; import { SlurgeProcessingFacStatusInpColumn } from "@/components/util/column/input/sewageExcretaSlurge/SlurgeProcessingFacStatusInpColumn";
import { doList } from "@/components/util/column/input/DoList"; import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { import {
SEWAGE_EXCRETA_DIGESTOR_OPERATING_STATS_CD, patchFooterVerificationByTab,
SEWAGE_EXCRETA_EXCRETA_SLURGE_PROCESSING_STATS_CD, patchVerificationFailedByTab,
SEWAGE_EXCRETA_SEWAGE_SLURGE_FAC_STATUS_STATS_CD, } from "@/components/util/input/verification-failed-flag";
SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD, import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface";
import type {
SewageExcretaSlurgeRequestParams,
DigestorOperatingStatusSaveItem,
ExcretaSlurgeProcessingStatusSaveItem,
SewageExcretaSlurgeVerificationResult,
SewageSlurgeProcessingFacStatusSaveItem,
SlurgeProcessingFacStatusSaveItem,
} from "@/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { ApiResponse, cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo, Sgg } from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { useTabStore } from "@/store/tab-store";
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewageExcretaSlurgeVerificationResult; verificationResult?: SewageExcretaSlurgeVerificationResult;
@@ -136,10 +152,10 @@ function verificationCellTooltip({
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -154,9 +170,8 @@ function buildSewageExcretaSlurgeSearchParam(
): SewageExcretaSlurgeRequestParams { ): SewageExcretaSlurgeRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
fcltNm: values.fctName?.trim() || undefined, fcltNm: values.fctName?.trim() || undefined,
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
@@ -165,8 +180,9 @@ function buildSewageExcretaSlurgeSearchParam(
function mapListToGridRows<T extends { verificationResult?: SewageExcretaSlurgeVerificationResult }>( function mapListToGridRows<T extends { verificationResult?: SewageExcretaSlurgeVerificationResult }>(
list: T[] | undefined, list: T[] | undefined,
ctpvNm: string, ctpvNm: string,
values?: SearchFormValues | null,
): GridRowWithVerification[] { ): GridRowWithVerification[] {
return (list ?? []).map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification; const row = toGridRows([{ ctpvNm, ...item }])[0] as GridRowWithVerification;
if (item.verificationResult) { if (item.verificationResult) {
row.verificationResult = item.verificationResult; row.verificationResult = item.verificationResult;
@@ -175,10 +191,10 @@ function mapListToGridRows<T extends { verificationResult?: SewageExcretaSlurgeV
}); });
} }
const SewageExcretaSlurgeInpTabList = [ const SewageExcretaSlurgeInpTabList = [
SlurgeProcessingFacStatusInpColumn, SlurgeProcessingFacStatusInpColumn,
DigestorOperatingStatusInpColumn, DigestorOperatingStatusInpColumn,
SewageSlurgeProcessingFacStatusInpColumn, SewageSlurgeProcessingFacStatusInpColumn,
ExcretaSlurgeProcessingStatusInpColumn, ExcretaSlurgeProcessingStatusInpColumn,
] as const; ] as const;
@@ -336,10 +352,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -353,7 +381,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk; isSubmitted && periodEnterable && gugunOk;
@@ -433,13 +461,6 @@ function mergeGridRowsWithMapRows(
}); });
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -448,7 +469,7 @@ function toGridNumber(value: string | number | null | undefined): number {
function resolveSlurgeFacStatusAdmdst( function resolveSlurgeFacStatusAdmdst(
row: Record<string, string | number>, row: Record<string, string | number>,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const sggNm = String(row.sggNm ?? "").trim(); const sggNm = String(row.sggNm ?? "").trim();
if (sggNm) { if (sggNm) {
const matched = sggOptions.find((s) => s.gugun === sggNm); const matched = sggOptions.find((s) => s.gugun === sggNm);
@@ -456,12 +477,15 @@ function resolveSlurgeFacStatusAdmdst(
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
} }
const existingCd = String(row.admdstCd ?? "").trim(); const existingCd = nullableTrimmedString(row.admdstCd);
const existingNm = String(row.admdstNm ?? "").trim(); const existingNm = nullableTrimmedString(row.admdstNm);
if (existingCd || existingNm) { if (existingCd || existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; return {
admdstCd: existingCd,
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
};
} }
return { admdstCd: "", admdstNm: sggNm }; return { admdstCd: null, admdstNm: nullableTrimmedString(sggNm) };
} }
/** 그리드 행 → ST00110001 저장/삭제 API 항목 */ /** 그리드 행 → ST00110001 저장/삭제 API 항목 */
@@ -473,12 +497,12 @@ function buildSlurgeProcessingFacStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
itslfQnt: toGridNumber(row.itslfQnt), itslfQnt: toGridNumber(row.itslfQnt),
otsdInflwQnt: toGridNumber(row.otsdInflwQnt), otsdInflwQnt: toGridNumber(row.otsdInflwQnt),
transfTmplntNm: String(row.transfTmplntNm ?? "").trim(), transfTmplntNm: nullableTrimmedString(row.transfTmplntNm),
transfPrcsQnt: toGridNumber(row.transfPrcsQnt), transfPrcsQnt: toGridNumber(row.transfPrcsQnt),
srpFncRt: toGridNumber(row.srpFncRt), srpFncRt: toGridNumber(row.srpFncRt),
itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt), itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt),
@@ -486,32 +510,32 @@ function buildSlurgeProcessingFacStatusSaveItem(
itslfTs1AltdQnt: toGridNumber(row.itslfTs1AltdQnt), itslfTs1AltdQnt: toGridNumber(row.itslfTs1AltdQnt),
itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt), itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt),
itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt), itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt),
itslfTs2CmpstgQnt: toGridNumber(row.itslfTs2CmpstgQnt), itslfTs1CmpstgQnt: toGridNumber(row.itslfTs1CmpstgQnt),
cmpsPrcsFcltBzentyNm: String(row.cmpsPrcsFcltBzentyNm ?? "").trim(), cmpsPrcsFcltBzentyNm: nullableTrimmedString(row.cmpsPrcsFcltBzentyNm),
incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm), incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm),
incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt), incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt),
incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt), incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt),
incnrPrcsFcltNm: String(row.incnrPrcsFcltNm ?? "").trim(), incnrPrcsFcltNm: nullableTrimmedString(row.incnrPrcsFcltNm),
stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm), stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm),
stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt), stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt),
stbznPrcsFcltNm: String(row.stbznPrcsFcltNm ?? "").trim(), stbznPrcsFcltNm: nullableTrimmedString(row.stbznPrcsFcltNm),
dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm), dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm),
dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt), dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt),
dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt), dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt),
dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt), dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt),
dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt), dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt),
dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt), dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt),
dryPrcsFcltNm: String(row.dryPrcsFcltNm ?? "").trim(), dryPrcsFcltNm: nullableTrimmedString(row.dryPrcsFcltNm),
fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt), fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt),
fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt), fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt),
fuelPrcsFcltNm: String(row.fuelPrcsFcltNm ?? "").trim(), fuelPrcsFcltNm: nullableTrimmedString(row.fuelPrcsFcltNm),
otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt), otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt),
otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt), otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt),
otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt), otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt),
otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt), otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt),
otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt), otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt),
otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt), otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt),
otsdCnsgnFcltNm: String(row.otsdCnsgnFcltNm ?? "").trim(), otsdCnsgnFcltNm: nullableTrimmedString(row.otsdCnsgnFcltNm),
cparTs1Whol: toGridNumber(row.cparTs1Whol), cparTs1Whol: toGridNumber(row.cparTs1Whol),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
@@ -526,10 +550,10 @@ function buildDigestorOperatingStatusSaveItem(
const base = { const base = {
admdstCd, admdstCd,
admdstNm, admdstNm,
swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
digstnEnYn: String(row.digstnEnYn ?? "").trim(), digstnEnYn: nullableTrimmedString(row.digstnEnYn),
}; };
if (isDigestorOperatingRowDisabled(row)) { if (isDigestorOperatingRowDisabled(row)) {
@@ -577,9 +601,9 @@ function buildSewageSlurgeProcessingFacStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? "").trim(), fcltNm: nullableTrimmedString(row.fcltNm),
prcsMthExpln: String(row.prcsMthExpln ?? "").trim(), prcsMthExpln: nullableTrimmedString(row.prcsMthExpln),
fcltCpct: toGridNumber(row.fcltCpct), fcltCpct: toGridNumber(row.fcltCpct),
anlPrcsQnt: toGridNumber(row.anlPrcsQnt), anlPrcsQnt: toGridNumber(row.anlPrcsQnt),
dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr), dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr),
@@ -597,9 +621,9 @@ function buildExcretaSlurgeProcessingStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? "").trim(), fcltNm: nullableTrimmedString(row.fcltNm),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
rcycQnt: toGridNumber(row.rcycQnt), rcycQnt: toGridNumber(row.rcycQnt),
olandLfvlm: toGridNumber(row.olandLfvlm), olandLfvlm: toGridNumber(row.olandLfvlm),
incnrQnt: toGridNumber(row.incnrQnt), incnrQnt: toGridNumber(row.incnrQnt),
@@ -607,7 +631,7 @@ function buildExcretaSlurgeProcessingStatusSaveItem(
fuelQnt: toGridNumber(row.fuelQnt), fuelQnt: toGridNumber(row.fuelQnt),
etcQnt: toGridNumber(row.etcQnt), etcQnt: toGridNumber(row.etcQnt),
nondspsQnt: toGridNumber(row.nondspsQnt), nondspsQnt: toGridNumber(row.nondspsQnt),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
} }
@@ -630,15 +654,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리 // 하수도 통계 관리 > 11. 하수 및 분뇨찌꺼기(슬러지)처리
const SewageExcretaSlurgeInp = () => { const SewageExcretaSlurgeInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -667,9 +693,15 @@ const SewageExcretaSlurgeInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [sggOptions, setSggOptions] = useState<Sgg[]>([]); const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
const [loadedTabKeys, setLoadedTabKeys] = useState<Set<string>>(() => new Set()); const [loadedTabKeys, setLoadedTabKeys] = useState<Set<string>>(() => new Set());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
@@ -700,36 +732,29 @@ const SewageExcretaSlurgeInp = () => {
const preYearArticleFcltSnRef = useRef<ArticleFcltSnSet>(new Set()); const preYearArticleFcltSnRef = useRef<ArticleFcltSnSet>(new Set());
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_EXCRETA_SLURGE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SewageExcretaSlurgeInpTabList.map(async (tab) => {
const tabKey = tab.tabKey;
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const shouldShowNotApplicable = ( const shouldShowNotApplicable = (
hasRows: boolean, hasRows: boolean,
@@ -789,6 +814,13 @@ const SewageExcretaSlurgeInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const getTabGridRows = (tabKey: string): Record<string, string | number>[] => const getTabGridRows = (tabKey: string): Record<string, string | number>[] =>
@@ -922,7 +954,7 @@ const SewageExcretaSlurgeInp = () => {
return false; return false;
} }
const { commonInfo, statusInfo, slurgeProcessingFacStatusList } = response.data; const { commonInfo, statusInfo, slurgeProcessingFacStatusList } = response.data;
const gridRows = mapListToGridRows(slurgeProcessingFacStatusList, commonInfo.ctpvNm); const gridRows = mapListToGridRows(slurgeProcessingFacStatusList, commonInfo.ctpvNm, searchContext);
syncSlurgeFacStatusArticleFcltSnTracking(gridRows, fromPreYear); syncSlurgeFacStatusArticleFcltSnTracking(gridRows, fromPreYear);
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
m.set(SlurgeProcessingFacStatusInpColumn.tabKey, gridRows); m.set(SlurgeProcessingFacStatusInpColumn.tabKey, gridRows);
@@ -932,6 +964,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -954,7 +989,7 @@ const SewageExcretaSlurgeInp = () => {
return false; return false;
} }
const { commonInfo, statusInfo, digestorOperatingStatusList } = response.data; const { commonInfo, statusInfo, digestorOperatingStatusList } = response.data;
const gridRows = mapListToGridRows(digestorOperatingStatusList, commonInfo.ctpvNm); const gridRows = mapListToGridRows(digestorOperatingStatusList, commonInfo.ctpvNm, searchContext);
if (!fromPreYear) { if (!fromPreYear) {
publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; publicSewageFacilitiesAvailableRef.current = gridRows.length > 0;
if (gridRows.length === 0 && overrides?.alertIfDigestorEmpty) { if (gridRows.length === 0 && overrides?.alertIfDigestorEmpty) {
@@ -969,6 +1004,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -995,6 +1033,7 @@ const SewageExcretaSlurgeInp = () => {
const gridRows = mapListToGridRows( const gridRows = mapListToGridRows(
sewageSlurgeProcessingFacStatusList, sewageSlurgeProcessingFacStatusList,
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
m.set(SewageSlurgeProcessingFacStatusInpColumn.tabKey, gridRows); m.set(SewageSlurgeProcessingFacStatusInpColumn.tabKey, gridRows);
@@ -1004,6 +1043,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -1029,6 +1071,7 @@ const SewageExcretaSlurgeInp = () => {
const gridRows = mapListToGridRows( const gridRows = mapListToGridRows(
excretaSlurgeProcessingStatusList, excretaSlurgeProcessingStatusList,
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
if (!fromPreYear) { if (!fromPreYear) {
excretaTreatmentFacilitiesAvailableRef.current = gridRows.length > 0; excretaTreatmentFacilitiesAvailableRef.current = gridRows.length > 0;
@@ -1044,6 +1087,9 @@ const SewageExcretaSlurgeInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab( commitToolbarSnapshotForLoadedTab(
tabKey, tabKey,
@@ -1065,7 +1111,9 @@ const SewageExcretaSlurgeInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -1123,7 +1171,9 @@ const SewageExcretaSlurgeInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
loadedTabKeysRef.current.clear(); loadedTabKeysRef.current.clear();
setLoadedTabKeys(new Set()); setLoadedTabKeys(new Set());
setDeleteListByTab(new Map()); setDeleteListByTab(new Map());
@@ -1232,9 +1282,9 @@ const SewageExcretaSlurgeInp = () => {
} }
commitActiveGrid(); commitActiveGrid();
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1262,7 +1312,7 @@ const SewageExcretaSlurgeInp = () => {
commitActiveGrid(); commitActiveGrid();
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const mapRows = rowDataMap.get(tabKey) ?? []; const mapRows = rowDataMap.get(tabKey) ?? [];
@@ -1320,6 +1370,7 @@ const SewageExcretaSlurgeInp = () => {
if ( if (
!noApplicable && !noApplicable &&
buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues) &&
((isSlurgeFacTab && slurgeSubmitList.length === 0) || ((isSlurgeFacTab && slurgeSubmitList.length === 0) ||
(isDigestorTab && digestorSubmitList.length === 0) || (isDigestorTab && digestorSubmitList.length === 0) ||
(isSewageSlurgeFacTab && sewageSlurgeFacSubmitList.length === 0) || (isSewageSlurgeFacTab && sewageSlurgeFacSubmitList.length === 0) ||
@@ -1341,12 +1392,8 @@ const SewageExcretaSlurgeInp = () => {
: SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD); : SEWAGE_EXCRETA_SLURGE_PROCESSING_FAC_STATUS_STATS_CD);
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForSubmit, statsCd: statsCdForSubmit,
}; };
@@ -1375,11 +1422,7 @@ const SewageExcretaSlurgeInp = () => {
const submitRequestBase = { const submitRequestBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: "", userId: "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
@@ -1437,15 +1480,16 @@ const SewageExcretaSlurgeInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForSubmit, stats_cd: statsCdForSubmit,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -1517,12 +1561,8 @@ const SewageExcretaSlurgeInp = () => {
} }
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -1570,7 +1610,7 @@ const SewageExcretaSlurgeInp = () => {
commitActiveGrid(); commitActiveGrid();
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const mapRows = rowDataMap.get(tabKey) ?? []; const mapRows = rowDataMap.get(tabKey) ?? [];
@@ -1588,11 +1628,7 @@ const SewageExcretaSlurgeInp = () => {
const baseSaveRequest = { const baseSaveRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(noApplicable ? { noApplicable: true } : {}), ...(noApplicable ? { noApplicable: true } : {}),
}; };
@@ -1623,7 +1659,7 @@ const SewageExcretaSlurgeInp = () => {
.filter((row) => isPersistedArticleRowForTab(tabKey, row)) .filter((row) => isPersistedArticleRowForTab(tabKey, row))
.map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list)); .map((row) => buildSlurgeProcessingFacStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1650,7 +1686,7 @@ const SewageExcretaSlurgeInp = () => {
buildDigestorOperatingStatusSaveItem(row, list), buildDigestorOperatingStatusSaveItem(row, list),
); );
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1676,7 +1712,7 @@ const SewageExcretaSlurgeInp = () => {
.filter((row) => isPersistedArticleRowForTab(tabKey, row)) .filter((row) => isPersistedArticleRowForTab(tabKey, row))
.map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list)); .map((row) => buildSewageSlurgeProcessingFacStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1703,7 +1739,7 @@ const SewageExcretaSlurgeInp = () => {
buildExcretaSlurgeProcessingStatusSaveItem(row, list), buildExcretaSlurgeProcessingStatusSaveItem(row, list),
); );
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1881,10 +1917,10 @@ const SewageExcretaSlurgeInp = () => {
for (const tab of SewageExcretaSlurgeInpTabList) { for (const tab of SewageExcretaSlurgeInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1898,7 +1934,7 @@ const SewageExcretaSlurgeInp = () => {
col.fieldName === "sggNm" col.fieldName === "sggNm"
) { ) {
next = buildSggNmColumn(col, canEdit, sggOptions); next = buildSggNmColumn(col, canEdit, sggOptions);
} else if (col.fieldName === "sggNm" || col.fieldName === "swtrmtFcltNm") { } else if (col.fieldName === "sggNm") {
next = { ...col, editable: false as const, styleName: "rg-cell-readonly" }; next = { ...col, editable: false as const, styleName: "rg-cell-readonly" };
} }
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
@@ -1962,7 +1998,7 @@ const SewageExcretaSlurgeInp = () => {
[digestorOperatingCellStyleCallback], [digestorOperatingCellStyleCallback],
); );
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab); const activeTabHasRowData = tabRowDataExists(rowDataMap, activeTab);
const activeTabToolbar = computeTabToolbarSnapshot( const activeTabToolbar = computeTabToolbarSnapshot(
@@ -1983,6 +2019,7 @@ const SewageExcretaSlurgeInp = () => {
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed; const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
const showEnterableToolbarButtons = const showEnterableToolbarButtons =
baseShowEnterableToolbarButtons && toolbarGroupAllowed; baseShowEnterableToolbarButtons && toolbarGroupAllowed;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showActionToolbarCard = const showActionToolbarCard =
toolbarGroupAllowed && toolbarGroupAllowed &&
(baseShowActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab))); (baseShowActionToolbarCard || (hasSearched && loadedTabKeys.has(activeTab)));
@@ -1990,7 +2027,8 @@ const SewageExcretaSlurgeInp = () => {
ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons; ARTICLE_EDIT_TAB_KEYS.has(activeTab) && showEnterableToolbarButtons;
const showNotApplicableForActiveTab = const showNotApplicableForActiveTab =
loadedTabKeys.has(activeTab) && loadedTabKeys.has(activeTab) &&
shouldShowNotApplicable(activeTabHasRowData, activeTabStatus); shouldShowNotApplicable(activeTabHasRowData, activeTabStatus) &&
submitOk;
const showSecondaryToolbarCard = const showSecondaryToolbarCard =
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
@@ -2024,9 +2062,8 @@ const SewageExcretaSlurgeInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -2047,6 +2084,7 @@ const SewageExcretaSlurgeInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
fctName fctName
@@ -2055,8 +2093,8 @@ const SewageExcretaSlurgeInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={ isVerificationFailed={
@@ -2068,19 +2106,19 @@ const SewageExcretaSlurgeInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void lastYearDataFunc()} getLastYearDataFunc={() => void lastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div> </div>
</div> </div>
)} )}
{showSecondaryToolbarCard && ( {showSecondaryToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showNotApplicableForActiveTab && ( {showNotApplicableForActiveTab && (
<NotApplicableCheckButton <NotApplicableCheckButton
isCheck={isNotApplicableForActiveTab} isCheck={isNotApplicableForActiveTab}
@@ -2089,12 +2127,12 @@ const SewageExcretaSlurgeInp = () => {
)} )}
{showArticleEditButtons && ( {showArticleEditButtons && (
<> <>
<AddArticleButton addArticleFunc={addArticleFunc} /> <AddArticleButton addArticleFunc={addArticleFunc} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} /> <DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</> </>
)} )}
</div> </div>
</div> </div>
)} )}
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -2159,6 +2197,9 @@ const SewageExcretaSlurgeInp = () => {
activeTab === tabKey ? getGridRef : undefined activeTab === tabKey ? getGridRef : undefined
} }
cellStyleCallback={resolveGridCellStyleCallback(tabKey)} cellStyleCallback={resolveGridCellStyleCallback(tabKey)}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
onCellEdited={ onCellEdited={
tabKey === DigestorOperatingStatusInpColumn.tabKey tabKey === DigestorOperatingStatusInpColumn.tabKey
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { import {
STATISTICS_INPUT_URL_PATH, STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues, verificationSearchToFormValues,
@@ -13,9 +13,7 @@ import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
SeweragePipeInputFactInstallStatusSaveRequest, SeweragePipeInputFactInstallStatusSaveRequest,
SeweragePipeInputFactInstallStatusSubmitRequest, SeweragePipeInputFactInstallStatusSubmitRequest,
SeweragePipeInputManholeStatusItem,
SeweragePipeInputManholeStatusSaveRequest, SeweragePipeInputManholeStatusSaveRequest,
SeweragePipeInputManholeStatusSaveYearlyStatItem,
SeweragePipeInputManholeStatusSubmitRequest, SeweragePipeInputManholeStatusSubmitRequest,
SeweragePipeInputPipeDiameterStatusSaveRequest, SeweragePipeInputPipeDiameterStatusSaveRequest,
SeweragePipeInputPipeDiameterStatusSubmitRequest, SeweragePipeInputPipeDiameterStatusSubmitRequest,
@@ -40,7 +38,15 @@ import type {
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
SubmitExceptionRequest, SubmitExceptionRequest,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -53,26 +59,29 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import GridTopActionButton from "@/components/module/GridTopActionButton"; import GridTopActionButton from "@/components/module/GridTopActionButton";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import SplitDataGrid from "@/components/module/SplitDataGrid"; import SplitDataGrid from "@/components/module/SplitDataGrid";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList, ColumnLayoutItem } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList, ColumnLayoutItem } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn"; import { FactInstallStatusInpColumn } from "@/components/util/column/input/sewagePipe/FactInstallStatusInpColumn";
import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn"; import { ManholeStatusInpColumn } from "@/components/util/column/input/sewagePipe/ManholeStatusInpColumn";
import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn"; import { PipeDiameterStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeDiameterStatusInpColumn";
import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn"; import { PipeTypeStatusInpColumn } from "@/components/util/column/input/sewagePipe/PipeTypeStatusInpColumn";
import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn"; import { RegionalFactStatusInpColumn } from "@/components/util/column/input/sewagePipe/RegionalFactStatusInpColumn";
import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn"; import { SewagePipeInpColumnInfoList } from "@/components/util/column/input/sewagePipe/SewagePipeInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { import {
patchFooterVerificationByTab,
patchVerificationFailedByTab, patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag"; } from "@/components/util/input/verification-failed-flag";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store"; import { useTabStore } from "@/store/tab-store";
@@ -97,9 +106,8 @@ function buildSewagePipeSearchParam(
): SeweragePipeRequestParams { ): SeweragePipeRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -165,12 +173,13 @@ const applyPipeTypeStatusColumnLayout = (
return walk(layout ?? []); return walk(layout ?? []);
}; };
/** ST00020003: 입력기간 통과 시 조회 연도의 매설연장(y{year}LyngPrlg)만 편집 가능, 해당 연도 철거연장 컬럼 숨김 */ /** ST00020003: 일반 사용자는 조회 연도 매설연장만, 관리자는 매설연장 컬럼 전체 편집 가능. 해당 연도 철거연장 컬럼 숨김 */
const applyPipeTypeStatusEditableColumns = ( const applyPipeTypeStatusEditableColumns = (
cols: ColumnConfig[] | undefined, cols: ColumnConfig[] | undefined,
canEdit: boolean, canEdit: boolean,
periodEnterable: boolean, periodEnterable: boolean,
statsYr: string | null, statsYr: string | null,
isAdmin: boolean,
): ColumnConfig[] => { ): ColumnConfig[] => {
const hideDmltnYear = shouldHidePipeTypeEditableYearDmltn(canEdit, periodEnterable, statsYr); const hideDmltnYear = shouldHidePipeTypeEditableYearDmltn(canEdit, periodEnterable, statsYr);
@@ -195,11 +204,12 @@ const applyPipeTypeStatusEditableColumns = (
if (fieldName === "lyngPrlg" || isYearLyngField) { if (fieldName === "lyngPrlg" || isYearLyngField) {
const editableLyngField = periodEnterable ? resolvePipeTypeEditableLyngField(statsYr) : null; const editableLyngField = periodEnterable ? resolvePipeTypeEditableLyngField(statsYr) : null;
const isTargetYear = editableLyngField !== null && fieldName === editableLyngField; const isTargetYear = editableLyngField !== null && fieldName === editableLyngField;
if (isTargetYear) { if (isAdmin || isTargetYear) {
const { styleName: _readonlyStyle, ...rest } = col; const { styleName, ...rest } = col;
return { return {
...rest, ...rest,
editable: true as const, editable: true as const,
...(styleName && styleName !== "rg-cell-readonly" ? { styleName } : {}),
}; };
} }
return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" }; return { ...col, editable: false as const, styleName: col.styleName ?? "rg-cell-readonly" };
@@ -209,10 +219,17 @@ const applyPipeTypeStatusEditableColumns = (
}); });
}; };
const getManholeLastDataYear = () => { /** 2-3 관종별: verificationResult.prlgWhol → 총계 계(pipeByPrstWhol) */
const lastDataYear = new Date().getFullYear() - 1; function remapPipeTypePrlgWholVerification(
return Math.max(2014, lastDataYear); result?: SeweragePipeInputVerificationResult | null,
}; ): SeweragePipeInputVerificationResult | null | undefined {
if (!result) return result;
const fromAlias = result.prlgWhol;
if (!Array.isArray(fromAlias) || fromAlias.length === 0) return result;
const existing = result.pipeByPrstWhol;
if (Array.isArray(existing) && existing.length > 0) return result;
return { ...result, pipeByPrstWhol: fromAlias };
}
/** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */ /** 관종별 신 응답(yearlyStatList) -> 그리드 y{year}* 필드로 정규화 */
function normalizePipeTypeStatusRow( function normalizePipeTypeStatusRow(
@@ -224,6 +241,7 @@ function normalizePipeTypeStatusRow(
yearlyStatList, yearlyStatList,
lyngPrlgWhol, lyngPrlgWhol,
dmltnPrlgWhol, dmltnPrlgWhol,
verificationResult,
...rest ...rest
} = row; } = row;
const yearFields: Record<string, number> = {}; const yearFields: Record<string, number> = {};
@@ -251,43 +269,7 @@ function normalizePipeTypeStatusRow(
lyngPrlg: Number(row.lyngPrlg ?? lyngPrlgWhol ?? 0), lyngPrlg: Number(row.lyngPrlg ?? lyngPrlgWhol ?? 0),
dmltnPrlg: Number(row.dmltnPrlg ?? dmltnPrlgWhol ?? 0), dmltnPrlg: Number(row.dmltnPrlg ?? dmltnPrlgWhol ?? 0),
...yearFields, ...yearFields,
}; verificationResult: remapPipeTypePrlgWholVerification(verificationResult) ?? undefined,
}
/** 맨홀 신 응답(yearlyStats) -> 그리드 yy{14..25}* 필드로 정규화 */
function normalizeManholeStatusRow(
row: SeweragePipeInputManholeStatusItem,
ctpvNm: string,
): Record<string, string | number | SeweragePipeInputVerificationResult | undefined> {
const maxYy = getManholeLastDataYear() % 100;
const { yearlyStats, ...rest } = row;
const yearFields: Record<string, number> = {};
for (let yy = 14; yy <= maxYy; yy++) {
yearFields[`yy${yy}WholMnhlCnt`] = 0;
yearFields[`yy${yy}Step1MnhlCnt`] = 0;
yearFields[`yy${yy}Step2MnhlCnt`] = 0;
yearFields[`yy${yy}Step3MnhlCnt`] = 0;
yearFields[`yy${yy}Step4MnhlCnt`] = 0;
yearFields[`yy${yy}Step5MnhlCnt`] = 0;
}
for (const y of yearlyStats ?? []) {
const year = Number(y.manhYr);
if (!Number.isFinite(year)) continue;
const yy = year % 100;
if (yy < 14 || yy > maxYy) continue;
yearFields[`yy${yy}WholMnhlCnt`] = Number(y.wholMnhlCnt ?? 0);
yearFields[`yy${yy}Step1MnhlCnt`] = Number(y.step1MnhlCnt ?? 0);
yearFields[`yy${yy}Step2MnhlCnt`] = Number(y.step2MnhlCnt ?? 0);
yearFields[`yy${yy}Step3MnhlCnt`] = Number(y.step3MnhlCnt ?? 0);
yearFields[`yy${yy}Step4MnhlCnt`] = Number(y.step4MnhlCnt ?? 0);
yearFields[`yy${yy}Step5MnhlCnt`] = Number(y.step5MnhlCnt ?? 0);
}
return {
ctpvNm,
...rest,
...yearFields,
}; };
} }
@@ -297,9 +279,6 @@ type GridRowWithVerification = Record<string, string | number> & {
}; };
const REGIONAL_FACT_TAB_KEY = "ST00020001"; const REGIONAL_FACT_TAB_KEY = "ST00020001";
const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)),
);
// function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean { // function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
// if (!verification) return false; // if (!verification) return false;
@@ -324,11 +303,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function buildRegionalFactSubmitListFromGrid( function buildRegionalFactSubmitListFromGrid(
@@ -350,10 +329,10 @@ function buildRegionalFactSubmitListFromGrid(
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
}; };
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: String(d.sggNm ?? ""), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: String(d.goalYr ?? ""), goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
@@ -389,7 +368,7 @@ function buildRegionalFactSubmitListFromGrid(
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
rmrk: (d.rmrk as string | null) ?? null, rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -416,10 +395,14 @@ type TabToolbarSnapshot = {
showActionToolbarCard: boolean; showActionToolbarCard: boolean;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
} }
/** /**
@@ -437,7 +420,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -451,6 +434,8 @@ function computeTabToolbarSnapshot(
// 하수도 통계 관리 > 02. 하수관로 // 하수도 통계 관리 > 02. 하수관로
const SewagePipeInp = () => { const SewagePipeInp = () => {
const { sidoList, isReady } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map()); const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001"); const [activeTab, setActiveTab] = useState<string>(SewagePipeInpColumnInfoList[0]?.tabKey ?? "ST00020001");
@@ -467,6 +452,9 @@ const SewagePipeInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]); const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
/** 탭키별 `engColNm -> columnGuide` 캐시 */ /** 탭키별 `engColNm -> columnGuide` 캐시 */
@@ -476,7 +464,10 @@ const SewagePipeInp = () => {
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */ /** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
/** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */ /** 탭별 조회 시점의 상단 버튼 노출 조건(해당 탭을 다시 조회할 때만 그 탭 항목 갱신) */
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>( const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(
() => new Map(),
);
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(), () => new Map(),
); );
const { confirm } = useConfirm(); const { confirm } = useConfirm();
@@ -521,6 +512,7 @@ const SewagePipeInp = () => {
const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce); const inputFocusNonce = useStatisticsInputFocusStore((s) => s.nonce);
useLayoutEffect(() => { useLayoutEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return; if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
if (!isReady) return;
const pending = useStatisticsInputFocusStore const pending = useStatisticsInputFocusStore
.getState() .getState()
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE); .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE);
@@ -529,43 +521,33 @@ const SewagePipeInp = () => {
activeTabRef.current = tabKey; activeTabRef.current = tabKey;
setActiveTab(tabKey); setActiveTab(tabKey);
if (search) { if (search) {
const values = verificationSearchToFormValues(search); const values = verificationSearchToFormValues(search, sidoList);
setVerificationSearchDefaults(values); setVerificationSearchDefaults(values);
postVerificationSearchRef.current = { values, tabKey }; postVerificationSearchRef.current = { values, tabKey };
setSearchFormRemountKey((k) => k + 1); setSearchFormRemountKey((k) => k + 1);
} }
}, [activeTabId, inputFocusNonce]); }, [activeTabId, inputFocusNonce, isReady, sidoList]);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return; if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(tabKey);
SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [activeTabId]); }, [activeTabId, activeTab, columnGuidesByTab]);
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => { const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
if (loadedTabKey === activeTabRef.current) { if (loadedTabKey === activeTabRef.current) {
@@ -578,11 +560,13 @@ const SewagePipeInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setLastSearchedYear(null); setLastSearchedYear(null);
setLastSearchedValues(null); setLastSearchedValues(null);
setHasSearched(false); setHasSearched(false);
setInputPeriodEnterable(true); setInputPeriodEnterable(true);
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
searchParamRef.current = null; searchParamRef.current = null;
searchDoneRef.current = false; searchDoneRef.current = false;
lazyTabLoggedRef.current.clear(); lazyTabLoggedRef.current.clear();
@@ -606,7 +590,7 @@ const SewagePipeInp = () => {
}); });
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => { return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
const canEdit = const canEdit =
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo; (computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo) && inputPeriodEnterable;
const applyEditable = (cols: ColumnConfig[] | undefined) => const applyEditable = (cols: ColumnConfig[] | undefined) =>
tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY tab.tabKey === PIPE_TYPE_STATUS_TAB_KEY
? applyPipeTypeStatusEditableColumns( ? applyPipeTypeStatusEditableColumns(
@@ -614,6 +598,7 @@ const SewagePipeInp = () => {
canEdit, canEdit,
inputPeriodEnterable, inputPeriodEnterable,
lastSearchedYear, lastSearchedYear,
isAdminInfo,
) )
: (cols ?? []).map((col: ColumnConfig) => : (cols ?? []).map((col: ColumnConfig) =>
canEdit canEdit
@@ -672,6 +657,13 @@ const SewagePipeInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
/** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */ /** mergeIntoExistingMap: true면 기존 맵에 병합(다른 탭 rowData·검색 후 다른 탭 캐시 유지). false면 맵 전체 교체 */
@@ -685,7 +677,7 @@ const SewagePipeInp = () => {
const searchConditionKey = JSON.stringify({ const searchConditionKey = JSON.stringify({
statsYr: searchParam.statsYr ?? "", statsYr: searchParam.statsYr ?? "",
ctpvNm: searchParam.ctpvNm ?? "", ctpvNm: searchParam.ctpvNm ?? "",
sggNm: searchParam.sggNm ?? "", sggNm: values?.gugun === "ALL" ? "" : (values?.gugun ?? searchParam.sggNm ?? ""),
sewerBzmnCd: searchParam.sewerBzmnCd ?? "", sewerBzmnCd: searchParam.sewerBzmnCd ?? "",
}); });
const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => { const applyRowMap = (apply: (m: Map<string, Record<string, string | number>[]>) => void) => {
@@ -701,12 +693,14 @@ const SewagePipeInp = () => {
setRowDataMap(next); setRowDataMap(next);
} }
}; };
const filterFetched = <T,>(list: readonly T[] | null | undefined) =>
filterBySelectedAdmdstCd(list, sidoListRef.current, values);
if (tabKey === 'ST00020001') { if (tabKey === 'ST00020001') {
const response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam); const response = await statisticsInputSewPipeApiService.getRegionalFactStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const convertedData = (response.data.regionalFactStatusList ?? []).map((d) => ({ const convertedData = filterFetched(response.data.regionalFactStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...d, ...d,
})); }));
@@ -715,6 +709,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -733,7 +730,7 @@ const SewagePipeInp = () => {
const response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam); const response = await statisticsInputSewPipeApiService.getFactInstallStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const convertedData = (response.data.factInstallStatusList ?? []).map((d) => ({ const convertedData = filterFetched(response.data.factInstallStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...d, ...d,
})); }));
@@ -759,6 +756,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -777,7 +777,7 @@ const SewagePipeInp = () => {
const response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam); const response = await statisticsInputSewPipeApiService.getPipeTypeStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const convertedData = (response.data.pipeTypeStatusList ?? []).map((d) => const convertedData = filterFetched(response.data.pipeTypeStatusList).map((d) =>
normalizePipeTypeStatusRow(d, commonInfo.ctpvNm), normalizePipeTypeStatusRow(d, commonInfo.ctpvNm),
); );
applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData))); applyRowMap((m) => m.set('ST00020003', toGridRows(convertedData)));
@@ -785,6 +785,13 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, {
verificationResult:
remapPipeTypePrlgWholVerification(commonInfo.verificationResult) ??
null,
}),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -803,7 +810,7 @@ const SewagePipeInp = () => {
const response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam); const response = await statisticsInputSewPipeApiService.getPipeDiameterStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const convertedData = (response.data.pipeDiameterStatusList ?? []).map((d) => ({ const convertedData = filterFetched(response.data.pipeDiameterStatusList).map((d) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...d, ...d,
})); }));
@@ -812,6 +819,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -830,14 +840,18 @@ const SewagePipeInp = () => {
const response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam); const response = await statisticsInputSewPipeApiService.getManholeStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const convertedData = (response.data.manholeStatusList ?? []).map((d) => const convertedData = filterFetched(response.data.manholeStatusList).map((d) => ({
normalizeManholeStatusRow(d, commonInfo.ctpvNm), ctpvNm: commonInfo.ctpvNm,
); ...d,
}));
applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData))); applyRowMap((m) => m.set('ST00020005', toGridRows(convertedData)));
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo)); setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, response.data.statusInfo));
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -856,10 +870,10 @@ const SewagePipeInp = () => {
const response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam); const response = await statisticsInputSewPipeApiService.getConnectionStatus(searchParam);
if (response.status === 'success') { if (response.status === 'success') {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const facilityConvertedData = (response.data.facilityConnectionList ?? []).map((d) => ({ const facilityConvertedData = filterFetched(response.data.facilityConnectionList).map((d) => ({
...d, ...d,
})); }));
const adminConvertedData = (response.data.adminAreaConnectionList ?? []).map((d) => ({ const adminConvertedData = filterFetched(response.data.adminAreaConnectionList).map((d) => ({
...d, ...d,
ctpvNm: d.ctpvNm || commonInfo.ctpvNm, ctpvNm: d.ctpvNm || commonInfo.ctpvNm,
})); }));
@@ -871,6 +885,9 @@ const SewagePipeInp = () => {
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd)); setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
);
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
); );
syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd); syncTopStatusIfStillOnTab(tabKey, commonInfo.statsCd);
if (updateLastSearchedValues) setLastSearchedValues(values); if (updateLastSearchedValues) setLastSearchedValues(values);
@@ -924,8 +941,10 @@ const SewagePipeInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setStatsCd(""); setStatsCd("");
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
} }
lazyTabLoggedRef.current.clear(); lazyTabLoggedRef.current.clear();
@@ -1087,9 +1106,8 @@ const SewagePipeInp = () => {
const params: SubmitExceptionRequest = { const params: SubmitExceptionRequest = {
...submitInfo, ...submitInfo,
sewerBzmnCd: searchValues?.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? "", ctpvNm: searchValues?.sido ?? "",
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
statsYr: searchValues?.year ?? "", statsYr: searchValues?.year ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -1172,8 +1190,7 @@ const SewagePipeInp = () => {
const submitBase = { const submitBase = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
@@ -1226,13 +1243,12 @@ const SewagePipeInp = () => {
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
}; };
const submitList = currentData.map((d) => ({ const submitList = currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: String(d.sggNm ?? ""), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: String(d.fcltSn ?? ""), fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"), sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
@@ -1253,7 +1269,7 @@ const SewagePipeInp = () => {
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
})); }));
const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = { const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = {
...submitBase, ...submitBase,
@@ -1297,11 +1313,11 @@ const SewagePipeInp = () => {
}); });
} }
return { return {
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
pipeSeCd: String(d.pipeSeCd ?? ""), pipeSeCd: nullableTrimmedString(d.pipeSeCd),
yearlyStatList, yearlyStatList,
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"),
dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"),
pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"),
@@ -1335,7 +1351,7 @@ const SewagePipeInp = () => {
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
}; };
const submitList = currentData.map((d) => ({ const submitList = currentData.map((d) => ({
pipeSeCd: String(d.pipeSeCd ?? ""), pipeSeCd: nullableTrimmedString(d.pipeSeCd),
step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"),
step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"),
step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"),
@@ -1343,10 +1359,10 @@ const SewagePipeInp = () => {
step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"),
sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"),
etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"),
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
})); }));
const requestData: SeweragePipeInputPipeDiameterStatusSubmitRequest = { const requestData: SeweragePipeInputPipeDiameterStatusSubmitRequest = {
...submitBase, ...submitBase,
@@ -1375,36 +1391,17 @@ const SewagePipeInp = () => {
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
}; };
const minYy = 14; const submitList = currentData.map((d) => ({
const maxYy = getManholeLastDataYear() % 100; admdstCd: nullableTrimmedString(d.admdstCd),
const submitList = currentData.map((d) => { admdstNm: nullableTrimmedString(d.admdstNm),
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = []; emphsMngSeCd: nullableTrimmedString(d.emphsMngSeCd),
for (let yy = minYy; yy <= maxYy; yy++) { frlvlMnhlCnt: formatNumber(d.frlvlMnhlCnt, "frlvlMnhlCnt"),
const year = 2000 + yy; sclvlMnhlCnt: formatNumber(d.sclvlMnhlCnt, "sclvlMnhlCnt"),
const wholKey = `yy${yy}WholMnhlCnt`; thlvlMnhlCnt: formatNumber(d.thlvlMnhlCnt, "thlvlMnhlCnt"),
const step1Key = `yy${yy}Step1MnhlCnt`; folvlMnhlCnt: formatNumber(d.folvlMnhlCnt, "folvlMnhlCnt"),
const step2Key = `yy${yy}Step2MnhlCnt`; filvlMnhlCnt: formatNumber(d.filvlMnhlCnt, "filvlMnhlCnt"),
const step3Key = `yy${yy}Step3MnhlCnt`; rmrk: nullableTrimmedString(d.rmrk),
const step4Key = `yy${yy}Step4MnhlCnt`; }));
const step5Key = `yy${yy}Step5MnhlCnt`;
yearlyStats.push({
manhYr: String(year),
wholMnhlCnt: formatNumber(d[wholKey] as string | number | undefined, wholKey),
step1MnhlCnt: formatNumber(d[step1Key] as string | number | undefined, step1Key),
step2MnhlCnt: formatNumber(d[step2Key] as string | number | undefined, step2Key),
step3MnhlCnt: formatNumber(d[step3Key] as string | number | undefined, step3Key),
step4MnhlCnt: formatNumber(d[step4Key] as string | number | undefined, step4Key),
step5MnhlCnt: formatNumber(d[step5Key] as string | number | undefined, step5Key),
});
}
return {
emphsMngSeCd: String(d.emphsMngSeCd ?? ""),
admdstCd: String(d.admdstCd ?? ""),
admdstNm: String(d.admdstNm ?? ""),
yearlyStats,
rmrk: String(d.rmrk ?? ""),
};
});
const requestData: SeweragePipeInputManholeStatusSubmitRequest = { const requestData: SeweragePipeInputManholeStatusSubmitRequest = {
...submitBase, ...submitBase,
submitList, submitList,
@@ -1428,14 +1425,15 @@ const SewagePipeInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: values.bsnmCd ?? "", sewer_bzmn_cd: values?.bsnmCd ?? '',
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: values.year ?? "", stats_yr: values.year ?? "",
lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2", lae_sbmsn_stts_cd: statusForTab?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -1461,9 +1459,8 @@ const SewagePipeInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: values.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
statsYr: values.year ?? "", statsYr: values.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -1480,9 +1477,8 @@ const SewagePipeInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: values.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
statsYr: values.year ?? "", statsYr: values.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -1506,9 +1502,8 @@ const SewagePipeInp = () => {
); );
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
{ {
sewerBzmnCd: values.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, values),
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""),
statsYr: values.year ?? "", statsYr: values.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}, },
@@ -1535,9 +1530,8 @@ const SewagePipeInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
statsYr: searchValues?.year ?? '', statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd, statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
@@ -1622,10 +1616,10 @@ const SewagePipeInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const saveList = currentData.map((d) => ({ const saveList = currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: String(d.sggNm ?? ""), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
goalYr: String(d.goalYr ?? ""), goalYr: nullableTrimmedString(d.goalYr),
wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"),
wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
@@ -1661,14 +1655,13 @@ const SewagePipeInp = () => {
exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"),
sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"),
sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"),
rmrk: (d.rmrk as string | null) ?? null, rmrk: nullableTrimmedString(d.rmrk),
})); }));
const param: SeweragePipeInputRegionalFactStatusSaveRequest = { const param: SeweragePipeInputRegionalFactStatusSaveRequest = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList, saveList,
@@ -1714,13 +1707,12 @@ const SewagePipeInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const saveList = currentData.map((d) => ({ const saveList = currentData.map((d) => ({
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: String(d.sggNm ?? ""), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
fcltSn: String(d.fcltSn ?? ""), fcltSn: nullableTrimmedString(d.fcltSn),
swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"),
prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"),
cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"),
sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"), sprsysPrcsZoneArea: formatNumber(d.sprsysPrcsZoneArea, "sprsysPrcsZoneArea"),
@@ -1741,14 +1733,13 @@ const SewagePipeInp = () => {
cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"),
exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"),
drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"),
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
})); }));
const param: SeweragePipeInputFactInstallStatusSaveRequest = { const param: SeweragePipeInputFactInstallStatusSaveRequest = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList, saveList,
@@ -1816,11 +1807,11 @@ const SewagePipeInp = () => {
}); });
} }
return { return {
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
pipeSeCd: String(d.pipeSeCd ?? ""), pipeSeCd: nullableTrimmedString(d.pipeSeCd),
yearlyStatList, yearlyStatList,
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"),
dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"),
pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"),
@@ -1832,8 +1823,7 @@ const SewagePipeInp = () => {
const param: SeweragePipeInputPipeTypeStatusSaveRequest = { const param: SeweragePipeInputPipeTypeStatusSaveRequest = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList, saveList,
@@ -1876,7 +1866,7 @@ const SewagePipeInp = () => {
}; };
const saveList = currentData.map((d) => ({ const saveList = currentData.map((d) => ({
pipeSeCd: String(d.pipeSeCd ?? ""), pipeSeCd: nullableTrimmedString(d.pipeSeCd),
step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"),
step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"),
step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"),
@@ -1884,10 +1874,10 @@ const SewagePipeInp = () => {
step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"),
sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"),
etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"),
rmrk: String(d.rmrk ?? ""), rmrk: nullableTrimmedString(d.rmrk),
pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"),
admdstCd: String(d.admdstCd ?? ""), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: String(d.admdstNm ?? ""), admdstNm: nullableTrimmedString(d.admdstNm),
})); }));
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
@@ -1895,8 +1885,7 @@ const SewagePipeInp = () => {
const param: SeweragePipeInputPipeDiameterStatusSaveRequest = { const param: SeweragePipeInputPipeDiameterStatusSaveRequest = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList, saveList,
@@ -1938,44 +1927,23 @@ const SewagePipeInp = () => {
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
}; };
const minYy = 14; const saveList = currentData.map((d) => ({
const maxYy = getManholeLastDataYear() % 100; admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: nullableTrimmedString(d.admdstNm),
const saveList = currentData.map((d) => { emphsMngSeCd: nullableTrimmedString(d.emphsMngSeCd),
const yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[] = []; frlvlMnhlCnt: formatNumber(d.frlvlMnhlCnt, "frlvlMnhlCnt"),
for (let yy = minYy; yy <= maxYy; yy++) { sclvlMnhlCnt: formatNumber(d.sclvlMnhlCnt, "sclvlMnhlCnt"),
const year = 2000 + yy; thlvlMnhlCnt: formatNumber(d.thlvlMnhlCnt, "thlvlMnhlCnt"),
const wholKey = `yy${yy}WholMnhlCnt`; folvlMnhlCnt: formatNumber(d.folvlMnhlCnt, "folvlMnhlCnt"),
const step1Key = `yy${yy}Step1MnhlCnt`; filvlMnhlCnt: formatNumber(d.filvlMnhlCnt, "filvlMnhlCnt"),
const step2Key = `yy${yy}Step2MnhlCnt`; rmrk: nullableTrimmedString(d.rmrk),
const step3Key = `yy${yy}Step3MnhlCnt`; }));
const step4Key = `yy${yy}Step4MnhlCnt`;
const step5Key = `yy${yy}Step5MnhlCnt`;
yearlyStats.push({
manhYr: String(year),
wholMnhlCnt: formatNumber(d[wholKey] as string | number | undefined, wholKey),
step1MnhlCnt: formatNumber(d[step1Key] as string | number | undefined, step1Key),
step2MnhlCnt: formatNumber(d[step2Key] as string | number | undefined, step2Key),
step3MnhlCnt: formatNumber(d[step3Key] as string | number | undefined, step3Key),
step4MnhlCnt: formatNumber(d[step4Key] as string | number | undefined, step4Key),
step5MnhlCnt: formatNumber(d[step5Key] as string | number | undefined, step5Key),
});
}
return {
emphsMngSeCd: String(d.emphsMngSeCd ?? ""),
admdstCd: String(d.admdstCd ?? ""),
admdstNm: String(d.admdstNm ?? ""),
yearlyStats,
rmrk: String(d.rmrk ?? ""),
};
});
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const param: SeweragePipeInputManholeStatusSaveRequest = { const param: SeweragePipeInputManholeStatusSaveRequest = {
statsYr: values.year ?? "", statsYr: values.year ?? "",
ctpvNm: values.sido ?? "", ctpvNm: values.sido ?? "",
sggNm: values.gugun === "ALL" ? "" : (values.gugun ?? ""), ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
saveList, saveList,
@@ -2004,12 +1972,12 @@ const SewagePipeInp = () => {
} }
}; };
const gugunOkOrNotDoList = buildGugunOkForToolbar(lastSearchedValues); const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const gugunOkOrNotDoList = buildGugunOkForToolbar(searchContextForToolbar);
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const searchContextForToolbar = lastSearchedValues ?? searchValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
@@ -2035,6 +2003,7 @@ const SewagePipeInp = () => {
return ( return (
<> <>
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -2053,10 +2022,10 @@ const SewagePipeInp = () => {
{showEnterableToolbarButtons && ( {showEnterableToolbarButtons && (
<> <>
{activeTab !== 'ST00020002' && <LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />} {activeTab !== 'ST00020002' && <LastYearDataButton getLastYearDataFunc={() => void getLastYearDataFunc()} />}
<SubmitButton submitFunc={() => void submitFunc(false)} /> {submitOk && <SubmitButton submitFunc={() => void submitFunc(false)} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -2205,7 +2174,9 @@ const SewagePipeInp = () => {
}} }}
isExcelImport={excelToolbarEnabledForTab} isExcelImport={excelToolbarEnabledForTab}
excelImportSkipNonEditable={ excelImportSkipNonEditable={
tabKey === PIPE_TYPE_STATUS_TAB_KEY ? !isAdminInfo : true tabKey === PIPE_TYPE_STATUS_TAB_KEY
? !isAdminInfo
: false
} }
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
@@ -2236,6 +2207,9 @@ const SewagePipeInp = () => {
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error"; return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
} }
}} }}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={({ row, fieldName }) => { cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult; const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return; if (!verification) return;
@@ -2244,7 +2218,7 @@ const SewagePipeInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "); e.vrfcCd === "IMR" ? "시계열 검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ");
const comp = e.compVlCn ?? ""; const comp = e.compVlCn ?? "";
const now = e.nowVlCn ?? ""; const now = e.nowVlCn ?? "";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
@@ -2269,9 +2243,8 @@ const SewagePipeInp = () => {
searchValues?.bsnmCd && searchValues?.bsnmCd &&
(tabStatsCdMap.get(activeTab) ?? statsCd) (tabStatsCdMap.get(activeTab) ?? statsCd)
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd, statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
} }
@@ -2305,9 +2278,8 @@ const SewagePipeInp = () => {
searchValues?.bsnmCd && searchValues?.bsnmCd &&
(tabStatsCdMap.get(activeTab) ?? statsCd) (tabStatsCdMap.get(activeTab) ?? statsCd)
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd, statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
} }
@@ -2,12 +2,12 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { import {
STATISTICS_INPUT_URL_PATH, STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues, verificationSearchToFormValues,
} from "@/shared/lib/stats-cd-input-navigation"; } from "@/shared/lib/stats-cd-input-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service"; import { sewagePipeMaintainApiService } from "@/entities/statistics/api/input/statistics-input-sewage-pipe-maintain-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -27,7 +27,15 @@ import type {
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { import {
LoadingWithAnimation, LoadingWithAnimation,
Tabs, Tabs,
@@ -46,18 +54,23 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import SearchForm from "@/components/module/SearchForm";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn"; import { PipeDredgingStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/PipeDredgingStatusInpColumn";
import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn"; import { RepairStatusInpColumn } from "@/components/util/column/input/sewagePipeMaintain/RepairStatusInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store"; import { useTabStore } from "@/store/tab-store";
@@ -68,9 +81,8 @@ function buildSewagePipeMaintainSearchParam(
): SewagePipeMaintainRequestParams { ): SewagePipeMaintainRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -82,12 +94,6 @@ const SewagePipeMaintainInpTabList = [
const firstTabKey = RepairStatusInpColumn.tabKey; const firstTabKey = RepairStatusInpColumn.tabKey;
const SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(
SewagePipeMaintainInpTabList.map((tab) => tab.tabKey).filter(Boolean),
),
);
const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({ const toGridColumnInfo = (tab: (typeof SewagePipeMaintainInpTabList)[number]): ColumnInfo => ({
columns: tab.columns ?? [], columns: tab.columns ?? [],
columnLayout: tab.columnLayout ?? [], columnLayout: tab.columnLayout ?? [],
@@ -116,10 +122,14 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -133,7 +143,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk; isSubmitted && periodEnterable && gugunOk;
@@ -154,17 +164,6 @@ function tabRowDataExists(
return Array.isArray(rows) && rows.length > 0; return Array.isArray(rows) && rows.length > 0;
} }
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
async function waitForImrJobCompleted( async function waitForImrJobCompleted(
@@ -183,11 +182,11 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
function buildColumnFormatNumber( function buildColumnFormatNumber(
@@ -218,9 +217,9 @@ function buildRepairRowList(
const formatNumber = buildColumnFormatNumber(RepairStatusInpColumn.columns); const formatNumber = buildColumnFormatNumber(RepairStatusInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
mendngWhol: formatNumber(d.mendngWhol, "mendngWhol"), mendngWhol: formatNumber(d.mendngWhol, "mendngWhol"),
cbsysMendngPrlg: formatNumber(d.cbsysMendngPrlg, "cbsysMendngPrlg"), cbsysMendngPrlg: formatNumber(d.cbsysMendngPrlg, "cbsysMendngPrlg"),
sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"), sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"),
@@ -236,7 +235,7 @@ function buildRepairRowList(
stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"), stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"),
otflCnt: formatNumber(d.otflCnt, "otflCnt"), otflCnt: formatNumber(d.otflCnt, "otflCnt"),
cctvExmnPrlg: formatNumber(d.cctvExmnPrlg, "cctvExmnPrlg"), cctvExmnPrlg: formatNumber(d.cctvExmnPrlg, "cctvExmnPrlg"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -246,9 +245,9 @@ function buildDredgingRowList(
const formatNumber = buildColumnFormatNumber(PipeDredgingStatusInpColumn.columns); const formatNumber = buildColumnFormatNumber(PipeDredgingStatusInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"),
wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"), wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"),
cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"), cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"),
@@ -268,12 +267,14 @@ function buildDredgingRowList(
oceanEmsnQnt: formatNumber(d.oceanEmsnQnt, "oceanEmsnQnt"), oceanEmsnQnt: formatNumber(d.oceanEmsnQnt, "oceanEmsnQnt"),
rcycQnt: formatNumber(d.rcycQnt, "rcycQnt"), rcycQnt: formatNumber(d.rcycQnt, "rcycQnt"),
etcQnt: formatNumber(d.etcQnt, "etcQnt"), etcQnt: formatNumber(d.etcQnt, "etcQnt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
// 하수도 통계 관리 > 03. 하수관로 유지관리 // 하수도 통계 관리 > 03. 하수관로 유지관리
const SewagePipeMaintainInp = () => { const SewagePipeMaintainInp = () => {
const { sidoList, isReady } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate(); const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
@@ -296,9 +297,15 @@ const SewagePipeMaintainInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
const [showSubmitDialog, setShowSubmitDialog] = useState(false); const [showSubmitDialog, setShowSubmitDialog] = useState(false);
@@ -328,6 +335,7 @@ const SewagePipeMaintainInp = () => {
useLayoutEffect(() => { useLayoutEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return; if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
if (!isReady) return;
const pending = useStatisticsInputFocusStore const pending = useStatisticsInputFocusStore
.getState() .getState()
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN); .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN);
@@ -335,43 +343,34 @@ const SewagePipeMaintainInp = () => {
const { tabKey, search } = pending; const { tabKey, search } = pending;
setActiveTab(tabKey); setActiveTab(tabKey);
if (search) { if (search) {
const values = verificationSearchToFormValues(search); const values = verificationSearchToFormValues(search, sidoList);
setVerificationSearchDefaults(values); setVerificationSearchDefaults(values);
postVerificationSearchRef.current = { values, tabKey }; postVerificationSearchRef.current = { values, tabKey };
setSearchFormRemountKey((k) => k + 1); setSearchFormRemountKey((k) => k + 1);
} }
}, [activeTabId, inputFocusNonce]); }, [activeTabId, inputFocusNonce, isReady, sidoList]);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return; if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE_MAINTAIN) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SEWAGE_PIPE_MAINTAIN_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [activeTabId]); }, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = ( const commitToolbarSnapshotForLoadedTab = (
tabKey: string, tabKey: string,
@@ -394,6 +393,13 @@ const SewagePipeMaintainInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -435,7 +441,11 @@ const SewagePipeMaintainInp = () => {
return false; return false;
} }
const { commonInfo, statusInfo, repairStatusList } = response.data; const { commonInfo, statusInfo, repairStatusList } = response.data;
const convertedData = (repairStatusList ?? []).map((d) => ({ const convertedData = filterBySelectedAdmdstCd(
repairStatusList ?? [],
sidoListRef.current,
searchContext,
).map((d) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...d, ...d,
})); }));
@@ -449,6 +459,9 @@ const SewagePipeMaintainInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) { if (updateLastSearchedValues) {
setLastSearchedValues(searchContext); setLastSearchedValues(searchContext);
} }
@@ -471,7 +484,11 @@ const SewagePipeMaintainInp = () => {
return false; return false;
} }
const { commonInfo, statusInfo, pipeDredgingStatusList } = response.data; const { commonInfo, statusInfo, pipeDredgingStatusList } = response.data;
const convertedData = (pipeDredgingStatusList ?? []).map((d) => ({ const convertedData = filterBySelectedAdmdstCd(
pipeDredgingStatusList ?? [],
sidoListRef.current,
searchContext,
).map((d) => ({
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
...d, ...d,
})); }));
@@ -485,6 +502,9 @@ const SewagePipeMaintainInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) { if (updateLastSearchedValues) {
setLastSearchedValues(searchContext); setLastSearchedValues(searchContext);
} }
@@ -592,11 +612,7 @@ const SewagePipeMaintainInp = () => {
const submitInfoBase = { const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -608,12 +624,8 @@ const SewagePipeMaintainInp = () => {
}; };
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -655,14 +667,15 @@ const SewagePipeMaintainInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -738,11 +751,7 @@ const SewagePipeMaintainInp = () => {
const baseRequest = { const baseRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
}; };
try { try {
@@ -844,12 +853,8 @@ const SewagePipeMaintainInp = () => {
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -880,7 +885,9 @@ const SewagePipeMaintainInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
setLastSearchedValues(null); setLastSearchedValues(null);
setHasSearched(false); setHasSearched(false);
@@ -925,7 +932,9 @@ const SewagePipeMaintainInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
} }
@@ -1000,10 +1009,10 @@ const SewagePipeMaintainInp = () => {
for (const tab of SewagePipeMaintainInpTabList) { for (const tab of SewagePipeMaintainInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = tab.columns ?? []; const rawColumns = tab.columns ?? [];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1021,10 +1030,9 @@ const SewagePipeMaintainInp = () => {
return map; return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
@@ -1045,6 +1053,7 @@ const SewagePipeMaintainInp = () => {
baseShowEnterableToolbarButtons && toolbarGroupAllowed; baseShowEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard = const showActionToolbarCard =
baseShowActionToolbarCard && toolbarGroupAllowed; baseShowActionToolbarCard && toolbarGroupAllowed;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const openVerificationHistoryDialog = () => { const openVerificationHistoryDialog = () => {
const ctx = lastSearchedValues ?? searchValues; const ctx = lastSearchedValues ?? searchValues;
@@ -1061,9 +1070,8 @@ const SewagePipeMaintainInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1099,7 +1107,7 @@ const SewagePipeMaintainInp = () => {
return errors return errors
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" ? "IMR검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - "; e.vrfcCd === "IMR" ? "시계열 검증 - " : e.vrfcCd === "NV" ? "일반검증 - " : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -1111,6 +1119,7 @@ const SewagePipeMaintainInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -1131,10 +1140,10 @@ const SewagePipeMaintainInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1166,11 +1175,11 @@ const SewagePipeMaintainInp = () => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return ( return (
@@ -1182,6 +1191,7 @@ const SewagePipeMaintainInp = () => {
toGridColumnInfo(info) toGridColumnInfo(info)
} }
excelExportFileSuffix={info.tabLabel || tabKey} excelExportFileSuffix={info.tabLabel || tabKey}
fixedColumns={info.fixedColumns}
rowData={rowData} rowData={rowData}
setRowData={(updater) => { setRowData={(updater) => {
setRowDataMap((prev) => { setRowDataMap((prev) => {
@@ -1206,6 +1216,9 @@ const SewagePipeMaintainInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
getGridDataRef={ getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined activeTab === tabKey ? getGridDataRef : undefined
@@ -1,21 +1,59 @@
import { import {
type Dispatch,
type SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type Dispatch,
type SetStateAction,
} from "react"; } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { ValueType } from "realgrid"; import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
type RainfallPumpStationItem,
type RainfallPumpStationSaveItem,
SEWAGE_RESERVOIR_STATS_CD,
type SewageReservoirItem,
type SewageReservoirRequestParams,
type SewageReservoirSaveItem,
type SewageReservoirVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
SewerageStatusInfo,
Sgg,
Sido,
} from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
resolveNoApplicableForRequest,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import AddArticleButton from "@/components/button/AddArticleButton"; import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import DeleteArticleButton from "@/components/button/DeleteArticleButton"; import DeleteArticleButton from "@/components/button/DeleteArticleButton";
@@ -25,13 +63,10 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import SubmitDailog from "./SubmitDailog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import type { import type {
CellStyleCallbackParams, CellStyleCallbackParams,
CellTooltipCallbackParams, CellTooltipCallbackParams,
@@ -39,44 +74,24 @@ import type {
ColumnInfo, ColumnInfo,
ColumnInfoList, ColumnInfoList,
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import { doList } from "@/components/util/column/input/DoList"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn"; import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn"; import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { import {
formatCompactYmdForGrid, formatCompactYmdForGrid,
normalizeOxYnValue, normalizeOxYnValue,
toCompactYmdOrNull, toCompactYmdOrNull,
} from "@/components/util/grid/ymdFormat"; } from "@/components/util/grid/ymdFormat";
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import { import {
SEWAGE_RESERVOIR_STATS_CD, patchFooterVerificationByTab,
type SewageReservoirItem, patchVerificationFailedByTab,
type SewageReservoirRequestParams, } from "@/components/util/input/verification-failed-flag";
type SewageReservoirSaveItem, import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
type RainfallPumpStationItem,
type RainfallPumpStationSaveItem,
type SewageReservoirVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
Sgg,
SewerageStatusInfo,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
const SewageReservoirPumpInpTabList = [ const SewageReservoirPumpInpTabList = [
SewageReservoirInpColumn, SewageReservoirInpColumn,
@@ -92,12 +107,6 @@ const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR
const rainfallPumpTabKey = const rainfallPumpTabKey =
RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp"; RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp";
const SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(
SewageReservoirPumpInpTabList.map((tab) => tab.tabKey).filter(Boolean),
),
);
const SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE = const SEWAGE_RESERVOIR_FACILITY_NAME_GUIDE_MESSAGE =
"하수저류시설 등록시 하수저류시설명이 동일할 경우 같은 데이터로 분류됩니다.\n\n" + "하수저류시설 등록시 하수저류시설명이 동일할 경우 같은 데이터로 분류됩니다.\n\n" +
"하수저류시설명이 동일 할 경우 구분을 위해 하수저류시설 1, 하수저류시설 2 형태로 구분하여 주시길 바랍니다."; "하수저류시설명이 동일 할 경우 구분을 위해 하수저류시설 1, 하수저류시설 2 형태로 구분하여 주시길 바랍니다.";
@@ -125,10 +134,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -142,7 +163,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && periodEnterable && gugunOk; isSubmitted && periodEnterable && gugunOk;
@@ -170,9 +191,8 @@ function buildSewageReservoirSearchParam(
const fcltNm = values.fctName?.trim(); const fcltNm = values.fctName?.trim();
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(fcltNm ? { fcltNm } : {}), ...(fcltNm ? { fcltNm } : {}),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
@@ -245,8 +265,9 @@ function buildSggNmColumn(
function mapSewageReservoirListToGridRows( function mapSewageReservoirListToGridRows(
list: SewageReservoirItem[], list: SewageReservoirItem[],
ctpvNm: string, ctpvNm: string,
values?: SearchFormValues | null,
): GridRowWithVerification[] { ): GridRowWithVerification[] {
return list.map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const row = toGridRows([ const row = toGridRows([
{ {
...item, ...item,
@@ -264,13 +285,6 @@ function mapSewageReservoirListToGridRows(
}); });
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -290,13 +304,13 @@ function toPumpDmtrExpln(value: string | number | null | undefined): number {
function resolveSewageReservoirAdmdst( function resolveSewageReservoirAdmdst(
sggNm: string, sggNm: string,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const trimmed = sggNm.trim(); const trimmed = sggNm.trim();
const matched = sggOptions.find((s) => s.gugun === trimmed); const matched = sggOptions.find((s) => s.gugun === trimmed);
if (matched) { if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
return { admdstCd: "", admdstNm: trimmed }; return { admdstCd: null, admdstNm: nullableTrimmedString(trimmed) };
} }
/** 그리드 행 → ST00040001 저장/삭제 API 항목 */ /** 그리드 행 → ST00040001 저장/삭제 API 항목 */
@@ -308,19 +322,18 @@ function buildSewageReservoirSaveItem(
String(row.sggNm ?? ""), String(row.sggNm ?? ""),
sggOptions, sggOptions,
); );
const fcltSn = String(row.fcltSn ?? "").trim();
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
// 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1" // 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1"
fcltSn: fcltSn, fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? ""), fcltNm: nullableTrimmedString(row.fcltNm),
fcltPstnCn: String(row.fcltPstnCn ?? ""), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
fldngPrvtnYn: normalizeOxYnValue(row.fldngPrvtnYn), fldngPrvtnYn: nullableTrimmedString(normalizeOxYnValue(row.fldngPrvtnYn)),
wqPlltnMtgtYn: normalizeOxYnValue(row.wqPlltnMtgtYn), wqPlltnMtgtYn: nullableTrimmedString(normalizeOxYnValue(row.wqPlltnMtgtYn)),
rtlztnUsgYn: normalizeOxYnValue(row.rtlztnUsgYn), rtlztnUsgYn: nullableTrimmedString(normalizeOxYnValue(row.rtlztnUsgYn)),
fcltStrctCn: nullableTrimmedString(row.fcltStrctCn), fcltStrctCn: nullableTrimmedString(row.fcltStrctCn),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
impdFcltCpct: toGridNumber(row.impdFcltCpct), impdFcltCpct: toGridNumber(row.impdFcltCpct),
@@ -337,17 +350,17 @@ function buildSewageReservoirSaveItem(
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
mltvMthCn: nullableTrimmedString(row.mltvMthCn), mltvMthCn: nullableTrimmedString(row.mltvMthCn),
impdMthCn: nullableTrimmedString(row.impdMthCn), impdMthCn: nullableTrimmedString(row.impdMthCn),
impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn), impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn),
dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln),
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln),
ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt), oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
grssMltvQnt: toGridNumber(row.grssMltvQnt), grssMltvQnt: toGridNumber(row.grssMltvQnt),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
@@ -363,18 +376,17 @@ function buildRainfallPumpStationSaveItem(
String(row.sggNm ?? ""), String(row.sggNm ?? ""),
sggOptions, sggOptions,
); );
const fcltSn = String(row.fcltSn ?? "").trim();
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: fcltSn, fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? ""), fcltNm: nullableTrimmedString(row.fcltNm),
fcltPstnCn: String(row.fcltPstnCn ?? ""), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
fcltyKndCn: String(row.fcltyKndCn ?? ""), fcltyKndCn: nullableTrimmedString(row.fcltyKndCn),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt),
wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea),
rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""), rsvrYn: row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : null,
rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt), rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt),
abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt), abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt),
grssPpqty: toGridNumber(row.grssPpqty), grssPpqty: toGridNumber(row.grssPpqty),
@@ -390,14 +402,14 @@ function buildRainfallPumpStationSaveItem(
pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty),
pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt),
pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt),
prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null,
dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln),
prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt),
ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt),
prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln),
ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln),
oprtngDayCnt: toGridNumber(row.oprtngDayCnt), oprtngDayCnt: toGridNumber(row.oprtngDayCnt),
grssMltvQnt: toGridNumber(row.grssMltvQnt), grssMltvQnt: toGridNumber(row.grssMltvQnt),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
@@ -407,8 +419,9 @@ function buildRainfallPumpStationSaveItem(
function mapRainfallPumpStationListToGridRows( function mapRainfallPumpStationListToGridRows(
list: RainfallPumpStationItem[], list: RainfallPumpStationItem[],
ctpvNm: string, ctpvNm: string,
values?: SearchFormValues | null,
): GridRowWithVerification[] { ): GridRowWithVerification[] {
return list.map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const row = toGridRows([ const row = toGridRows([
{ {
...item, ...item,
@@ -446,15 +459,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장 // 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장
const SewageReservoirPumpInp = () => { const SewageReservoirPumpInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -486,9 +501,9 @@ const SewageReservoirPumpInp = () => {
alert("시군구를 선택해주세요."); alert("시군구를 선택해주세요.");
return; return;
} }
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getCurrentRowsForTab(tabKey).length === 0);
const currentData = getCurrentRowsForTab(tabKey); const currentData = getCurrentRowsForTab(tabKey);
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -513,12 +528,8 @@ const SewageReservoirPumpInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -671,7 +682,7 @@ const SewageReservoirPumpInp = () => {
if (!(await confirm("저장 하시겠습니까?"))) return; if (!(await confirm("저장 하시겠습니까?"))) return;
commitGridForTab(tabKey); commitGridForTab(tabKey);
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getCurrentRowsForTab(tabKey).length === 0);
const currentData = getCurrentRowsForTab(tabKey); const currentData = getCurrentRowsForTab(tabKey);
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
const saveList = noApplicable const saveList = noApplicable
@@ -690,7 +701,7 @@ const SewageReservoirPumpInp = () => {
: rainfallPumpStationDeleteList : rainfallPumpStationDeleteList
.filter((row) => String(row.fcltSn ?? "").trim() !== "") .filter((row) => String(row.fcltSn ?? "").trim() !== "")
.map((row) => buildRainfallPumpStationSaveItem(row, sggOptions)); .map((row) => buildRainfallPumpStationSaveItem(row, sggOptions));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -698,11 +709,7 @@ const SewageReservoirPumpInp = () => {
const requestBase = { const requestBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
...(noApplicable ? { noApplicable: true } : {}), ...(noApplicable ? { noApplicable: true } : {}),
}; };
@@ -762,7 +769,7 @@ const SewageReservoirPumpInp = () => {
} }
commitGridForTab(tabKey); commitGridForTab(tabKey);
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getCurrentRowsForTab(tabKey).length === 0);
const currentData = getCurrentRowsForTab(tabKey); const currentData = getCurrentRowsForTab(tabKey);
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
const submitList = noApplicable const submitList = noApplicable
@@ -781,12 +788,8 @@ const SewageReservoirPumpInp = () => {
const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null; const activeTabStatus = tabStatusInfoMap.get(tabKey) ?? null;
const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey; const statsCdForTab = tabStatsCdMap.get(tabKey) ?? tabKey;
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -800,7 +803,7 @@ const SewageReservoirPumpInp = () => {
await loadActiveTabData(tabKey, lastSearchedValues, true, false); await loadActiveTabData(tabKey, lastSearchedValues, true, false);
}; };
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -808,11 +811,7 @@ const SewageReservoirPumpInp = () => {
const submitRequestBase = { const submitRequestBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(noApplicable ? { noApplicable: true } : {}), ...(noApplicable ? { noApplicable: true } : {}),
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
@@ -852,15 +851,16 @@ const SewageReservoirPumpInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -934,13 +934,19 @@ const SewageReservoirPumpInp = () => {
const [tabStatusInfoMap, setTabStatusInfoMap] = useState< const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
Map<string, SewerageStatusInfo | null> Map<string, SewerageStatusInfo | null>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = const [verificationResultDialogOpen, setVerificationResultDialogOpen] =
useState(false); useState(false);
@@ -957,35 +963,29 @@ const SewageReservoirPumpInp = () => {
useRef<(() => Record<string, string | number>[]) | null>(null); useRef<(() => Record<string, string | number>[]) | null>(null);
const sggOptionsSidoRef = useRef<string | null>(null); const sggOptionsSidoRef = useRef<string | null>(null);
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_RESERVOIR_PUMP) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SEWAGE_RESERVOIR_PUMP_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const searchDoneRef = useRef(false); const searchDoneRef = useRef(false);
const periodEnterableRef = useRef(true); const periodEnterableRef = useRef(true);
@@ -1035,6 +1035,13 @@ const SewageReservoirPumpInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -1079,6 +1086,7 @@ const SewageReservoirPumpInp = () => {
const gridRows = mapSewageReservoirListToGridRows( const gridRows = mapSewageReservoirListToGridRows(
list, list,
commonInfo.ctpvNm, commonInfo.ctpvNm,
values,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -1089,6 +1097,9 @@ const SewageReservoirPumpInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) { if (updateLastSearchedValues) {
setLastSearchedValues(values); setLastSearchedValues(values);
} }
@@ -1124,6 +1135,7 @@ const SewageReservoirPumpInp = () => {
const gridRows = mapRainfallPumpStationListToGridRows( const gridRows = mapRainfallPumpStationListToGridRows(
list, list,
commonInfo.ctpvNm, commonInfo.ctpvNm,
values,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -1134,6 +1146,9 @@ const SewageReservoirPumpInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) { if (updateLastSearchedValues) {
setLastSearchedValues(values); setLastSearchedValues(values);
} }
@@ -1201,7 +1216,9 @@ const SewageReservoirPumpInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setLastSearchedValues(null); setLastSearchedValues(null);
setSggOptions([]); setSggOptions([]);
sggOptionsSidoRef.current = null; sggOptionsSidoRef.current = null;
@@ -1251,7 +1268,9 @@ const SewageReservoirPumpInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
loadedTabKeysRef.current.clear(); loadedTabKeysRef.current.clear();
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
} }
@@ -1321,10 +1340,9 @@ const SewageReservoirPumpInp = () => {
} }
}; };
const searchContextForToolbar = lastSearchedValues ?? searchValues; const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
@@ -1345,6 +1363,7 @@ const SewageReservoirPumpInp = () => {
baseShowEnterableToolbarButtons && toolbarGroupAllowed; baseShowEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard = const showActionToolbarCard =
baseShowActionToolbarCard && toolbarGroupAllowed; baseShowActionToolbarCard && toolbarGroupAllowed;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const isReservoirOrRainfallTab = const isReservoirOrRainfallTab =
activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey; activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey;
const showArticleToolbarCard = const showArticleToolbarCard =
@@ -1359,7 +1378,8 @@ const SewageReservoirPumpInp = () => {
activeTab, activeTab,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
tabStatusInfoMap.get(activeTab), tabStatusInfoMap.get(activeTab),
); ) &&
submitOk;
const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback( const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback(
(next) => { (next) => {
const tabKey = activeTabRef.current; const tabKey = activeTabRef.current;
@@ -1438,10 +1458,10 @@ const SewageReservoirPumpInp = () => {
for (const tab of SewageReservoirPumpInpTabList) { for (const tab of SewageReservoirPumpInpTabList) {
const tabKey = tab.tabKey ?? ""; const tabKey = tab.tabKey ?? "";
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
const isRainfallPumpTab = tabKey === rainfallPumpTabKey; const isRainfallPumpTab = tabKey === rainfallPumpTabKey;
const rawColumns = tab.columns ?? []; const rawColumns = tab.columns ?? [];
@@ -1500,10 +1520,10 @@ const SewageReservoirPumpInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 -"; : "모집단 검증 -";
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -1517,9 +1537,8 @@ const SewageReservoirPumpInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1540,6 +1559,7 @@ const SewageReservoirPumpInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
fctName={true} fctName={true}
@@ -1561,10 +1581,10 @@ const SewageReservoirPumpInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1606,11 +1626,11 @@ const SewageReservoirPumpInp = () => {
{SewageReservoirPumpInpTabList.map((tab) => { {SewageReservoirPumpInpTabList.map((tab) => {
const tabKey = tab.tabKey ?? ""; const tabKey = tab.tabKey ?? "";
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD; const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
return ( return (
@@ -1668,6 +1688,9 @@ const SewageReservoirPumpInp = () => {
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
/> />
</TabsContent> </TabsContent>
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { useAlertStore, useGlobalProgressStore, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
@@ -35,7 +35,15 @@ import type {
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -48,21 +56,27 @@ import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton"; import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn"; import { CauserBurdenInpColumn } from "@/components/util/column/input/sewerageFinance/CauserBurdenInpColumn";
import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn"; import { SewerageChargeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageChargeInpColumn";
import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn"; import { SewerageFinanceSettleInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageFinanceSettleInpColumn";
import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn"; import { SewerageUsageFeeInpColumn } from "@/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
const SewerageFinanceInpTabList = [ const SewerageFinanceInpTabList = [
SewerageFinanceSettleInpColumn, SewerageFinanceSettleInpColumn,
@@ -73,10 +87,6 @@ const SewerageFinanceInpTabList = [
const firstTabKey = SewerageFinanceSettleInpColumn.tabKey; const firstTabKey = SewerageFinanceSettleInpColumn.tabKey;
const SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS = Array.from(
new Set(SewerageFinanceInpTabList.map((tab) => tab.tabKey).filter(Boolean)),
);
type GridRowWithVerification = Record<string, string | number> & { type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageFinanceVerificationResult; verificationResult?: SewerageFinanceVerificationResult;
}; };
@@ -98,9 +108,8 @@ function buildSewerageFinanceSearchParam(
): SewerageFinanceRequestParams { ): SewerageFinanceRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -118,10 +127,14 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -135,7 +148,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -157,8 +170,8 @@ function tabRowDataExists(
function mapRegionalListToGridRows< function mapRegionalListToGridRows<
T extends { verificationResult?: SewerageFinanceVerificationResult }, T extends { verificationResult?: SewerageFinanceVerificationResult },
>(list: T[], ctpvNm: string): GridRowWithVerification[] { >(list: T[], ctpvNm: string, values?: SearchFormValues | null): GridRowWithVerification[] {
return list.map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const { verificationResult, ...rest } = item; const { verificationResult, ...rest } = item;
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult; if (verificationResult) row.verificationResult = verificationResult;
@@ -166,17 +179,6 @@ function mapRegionalListToGridRows<
}); });
} }
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) { function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) {
const columnFormatMap = new Map<string, string>(); const columnFormatMap = new Map<string, string>();
columns.forEach((col) => { columns.forEach((col) => {
@@ -212,9 +214,9 @@ function buildSewerageFinanceSettleSaveList(
const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns); const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"), cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"),
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"), mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"), wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"),
@@ -238,7 +240,7 @@ function buildSewerageFinanceSettleSaveList(
etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"), etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"),
etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"), etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"),
etcAmt: formatNumber(d.etcAmt, "etcAmt"), etcAmt: formatNumber(d.etcAmt, "etcAmt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"), fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"),
usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"), usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"),
tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"), tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"),
@@ -254,13 +256,13 @@ function buildSewerageChargeSaveList(
const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns); const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"), anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"), levyAmt: formatNumber(d.levyAmt, "levyAmt"),
grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"), grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"), avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"),
grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"), grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"),
rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"), rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"),
@@ -273,9 +275,9 @@ function buildSewerageUsageFeeSaveList(
const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns); const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"), hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"), hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"), hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"),
@@ -297,7 +299,7 @@ function buildSewerageUsageFeeSaveList(
etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"), etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"),
etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"), etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"),
etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"), etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
levyAmt: formatNumber(d.levyAmt, "levyAmt"), levyAmt: formatNumber(d.levyAmt, "levyAmt"),
levyQnt: formatNumber(d.levyQnt, "levyQnt"), levyQnt: formatNumber(d.levyQnt, "levyQnt"),
})); }));
@@ -309,16 +311,16 @@ function buildCauserBurdenSaveList(
const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns); const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"), levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"), levyAmt: formatNumber(d.levyAmt, "levyAmt"),
mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"), mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"),
mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"), mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"),
othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"), othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"),
othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"), othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"), trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"),
})); }));
} }
@@ -341,15 +343,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 09. 하수도재정 // 하수도 통계 관리 > 09. 하수도재정
const SewerageFinanceInp = () => { const SewerageFinanceInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -374,9 +378,15 @@ const SewerageFinanceInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [columnGuidesByTab, setColumnGuidesByTab] = useState< const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>> Record<string, Record<string, string>>
>({}); >({});
@@ -391,35 +401,29 @@ const SewerageFinanceInp = () => {
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null); const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_FINANCE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SEWERAGE_FINANCE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => {
try {
const res = await statisticsApi.getColumnGuides(tabKey);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [tabKey, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [tabKey, next] as const;
} catch {
return [tabKey, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
setColumnGuidesByTab(Object.fromEntries(entries)); setColumnGuidesByTab((prev) =>
Object.prototype.hasOwnProperty.call(prev, tabKey)
? prev
: { ...prev, [tabKey]: next },
);
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = ( const commitToolbarSnapshotForLoadedTab = (
tabKey: string, tabKey: string,
@@ -442,6 +446,13 @@ const SewerageFinanceInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -482,6 +493,7 @@ const SewerageFinanceInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(sewerageFinanceSettleList ?? []) as SewerageFinanceSettleItem[], (sewerageFinanceSettleList ?? []) as SewerageFinanceSettleItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -492,6 +504,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -508,6 +523,7 @@ const SewerageFinanceInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(sewerageChargeList ?? []) as SewerageChargeItem[], (sewerageChargeList ?? []) as SewerageChargeItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -518,6 +534,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -534,6 +553,7 @@ const SewerageFinanceInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(sewerageUsageFeeList ?? []) as SewerageUsageFeeItem[], (sewerageUsageFeeList ?? []) as SewerageUsageFeeItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -544,6 +564,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -560,6 +583,7 @@ const SewerageFinanceInp = () => {
const gridRows = mapRegionalListToGridRows( const gridRows = mapRegionalListToGridRows(
(causerBurdenList ?? []) as CauserBurdenItem[], (causerBurdenList ?? []) as CauserBurdenItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -570,6 +594,9 @@ const SewerageFinanceInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
return true; return true;
@@ -587,7 +614,9 @@ const SewerageFinanceInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -629,7 +658,9 @@ const SewerageFinanceInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
} }
@@ -739,12 +770,8 @@ const SewerageFinanceInp = () => {
} }
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -827,11 +854,7 @@ const SewerageFinanceInp = () => {
const submitInfoBase = { const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -843,12 +866,8 @@ const SewerageFinanceInp = () => {
}; };
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -918,14 +937,15 @@ const SewerageFinanceInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -1009,11 +1029,7 @@ const SewerageFinanceInp = () => {
const baseRequest = { const baseRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
}; };
try { try {
@@ -1134,9 +1150,8 @@ const SewerageFinanceInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1147,10 +1162,10 @@ const SewerageFinanceInp = () => {
for (const tab of SewerageFinanceInpTabList) { for (const tab of SewerageFinanceInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1167,19 +1182,20 @@ const SewerageFinanceInp = () => {
return map; return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
hasSearched, hasSearched,
lastSearchedValues, searchContextForToolbar,
inputPeriodEnterable, inputPeriodEnterable,
isAdminInfo, isAdminInfo,
); );
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } =
activeTabToolbar; activeTabToolbar;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const verificationCellStyle = ({ const verificationCellStyle = ({
fieldName, fieldName,
@@ -1212,10 +1228,10 @@ const SewerageFinanceInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -1238,6 +1254,7 @@ const SewerageFinanceInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -1245,8 +1262,8 @@ const SewerageFinanceInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={ isVerificationFailed={
@@ -1258,10 +1275,10 @@ const SewerageFinanceInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void lastYearDataFunc()} getLastYearDataFunc={() => void lastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
@@ -1293,11 +1310,11 @@ const SewerageFinanceInp = () => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return ( return (
@@ -1335,6 +1352,9 @@ const SewerageFinanceInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport isExcelExport
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
getGridDataRef={ getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined activeTab === tabKey ? getGridDataRef : undefined
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; import { useAlertStore, useGlobalProgressStore, useSearchConditionSidoList, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { import {
STATISTICS_INPUT_URL_PATH, STATISTICS_INPUT_URL_PATH,
verificationSearchToFormValues, verificationSearchToFormValues,
@@ -25,8 +25,15 @@ import type {
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo, SewerageStatusInfo,
Sido,
SubmitExceptionRequest, SubmitExceptionRequest,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation } from "@common/components"; import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
@@ -40,17 +47,18 @@ import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
// import { DataGrid } from "@rsone/components"; // import { DataGrid } from "@rsone/components";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import SearchForm from "@/components/module/SearchForm";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn";
import { getFooterSummary } from "@/components/util/grid/footerSummary"; import { getFooterSummary } from "@/components/util/grid/footerSummary";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag"; import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store"; import { useTabStore } from "@/store/tab-store";
@@ -60,6 +68,28 @@ type GridRowWithVerification = Record<string, string | number> & {
verificationResult?: SewerageSupplyRateVerificationResult; verificationResult?: SewerageSupplyRateVerificationResult;
}; };
/** commonInfo 푸터 필드명 → 그리드 fieldName */
const FOOTER_VERIFY_FIELD_ALIASES: Record<string, string> = {
sewerBzmnWholPpltnCnt: "pbadmsWholPpltnCnt",
sewerBzmnDomplWholPpltnCnt: "domplWholPpltnCnt",
sewerBzmnFrgnrWholPpltnCnt: "frgnrWholPpltnCnt",
sewerBzmnArea: "refArea",
};
const remapCommonInfoVerificationToGridFields = (
result: SewerageSupplyRateVerificationResult | null | undefined,
): VerificationResultLikeMap | null => {
if (!result) return null;
const remapped: VerificationResultLikeMap = {};
for (const [key, errors] of Object.entries(result)) {
const gridField = FOOTER_VERIFY_FIELD_ALIASES[key] ?? key;
remapped[gridField] = errors;
// API 키로도 조회 가능하도록 유지
if (gridField !== key) remapped[key] = errors;
}
return remapped;
};
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */ /** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001"; const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
@@ -87,36 +117,31 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
const sidoListRef: { current: Sido[] } = { current: [] };
function getSewerageSupplyRateParams( function getSewerageSupplyRateParams(
values: SearchFormValues, values: SearchFormValues,
overrides?: { yr?: string; demoYr?: string; mode?: string } overrides?: { yr?: string; demoYr?: string; mode?: string }
): SewerageSupplyRateRequestParams { ): SewerageSupplyRateRequestParams {
return { return {
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === 'ALL' ? '' : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
sewerBzmnCd: values.bsnmCd,
statsYr: overrides?.yr ?? values.year, statsYr: overrides?.yr ?? values.year,
...(overrides?.mode && { mode: overrides.mode }), ...(overrides?.mode && { mode: overrides.mode }),
}; };
} }
function normalizeGridString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
return value.trim() === "" ? null : value;
}
// 하수도 통계 관리 > 01. 하수도 보급률 // 하수도 통계 관리 > 01. 하수도 보급률
const SeweragePenetrationRateInp = () => { const SeweragePenetrationRateInp = () => {
const { sidoList, isReady } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]); const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -137,6 +162,8 @@ const SeweragePenetrationRateInp = () => {
/** `engColNm` → `columnGuide` (헤더 hover) */ /** `engColNm` → `columnGuide` (헤더 hover) */
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({}); const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
const [footerVerificationResult, setFooterVerificationResult] =
useState<VerificationResultLikeMap | null>(null);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const showGlobalProgress = useGlobalProgressStore((state) => state.show); const showGlobalProgress = useGlobalProgressStore((state) => state.show);
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
@@ -156,15 +183,16 @@ const SeweragePenetrationRateInp = () => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) { if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
return; return;
} }
if (!isReady) return;
const pending = useStatisticsInputFocusStore const pending = useStatisticsInputFocusStore
.getState() .getState()
.takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE); .takePendingForUrl(STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE);
if (!pending?.search) return; if (!pending?.search) return;
const values = verificationSearchToFormValues(pending.search); const values = verificationSearchToFormValues(pending.search, sidoList);
setVerificationSearchDefaults(values); setVerificationSearchDefaults(values);
postVerificationSearchRef.current = values; postVerificationSearchRef.current = values;
setSearchFormRemountKey((k) => k + 1); setSearchFormRemountKey((k) => k + 1);
}, [activeTabId, inputFocusNonce]); }, [activeTabId, inputFocusNonce, isReady, sidoList]);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) { if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
@@ -208,11 +236,14 @@ const SeweragePenetrationRateInp = () => {
(footerFromApi && footerFromApi[fieldName] != null ? footerFromApi[fieldName] : null) ?? getFooterSummary(grid, column), (footerFromApi && footerFromApi[fieldName] != null ? footerFromApi[fieldName] : null) ?? getFooterSummary(grid, column),
}); });
// 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요. // 관리자는 제출/승인 상태에서도 편집 가능. 단, 입력기간 외에는 불가.
const columnInfo = useMemo(() => { const columnInfo = useMemo(() => {
const { columns, columnLayout } = SeweragePenetrationRateInpColumn; const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const })); const canEdit =
(isEnterable || isAdminInfo) && isInputPeriodCurrent(periodCheckList);
const base = canEdit
? columns
: columns.map((col) => ({ ...col, editable: false as const }));
const withHeaderTooltips = base.map((col) => { const withHeaderTooltips = base.map((col) => {
const guide = columnGuidesByEngCol[col.fieldName]?.trim(); const guide = columnGuidesByEngCol[col.fieldName]?.trim();
@@ -233,7 +264,7 @@ const SeweragePenetrationRateInp = () => {
return col; return col;
}), }),
}; };
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]); }, [isEnterable, isAdminInfo, periodCheckList, footerFromApi, columnGuidesByEngCol]);
const handleReset = () => { const handleReset = () => {
setRowData([]); setRowData([]);
@@ -242,19 +273,28 @@ const SeweragePenetrationRateInp = () => {
setLastSearchedYear(null); setLastSearchedYear(null);
setLastSearchedValues(null); setLastSearchedValues(null);
setFooterFromApi(null); setFooterFromApi(null);
setFooterVerificationResult(null);
setVerificationSearchDefaults(null); setVerificationSearchDefaults(null);
setIsVerificationFailed(false); setIsVerificationFailed(false);
setSearchFormRemountKey((k) => k + 1); setSearchFormRemountKey((k) => k + 1);
}; };
const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => { const fetchSewerageSupplyRate = async (
params: SewerageSupplyRateRequestParams,
isEnterable: boolean,
values?: SearchFormValues | null,
) => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await statisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params); const response = await statisticsInputSewSupplyRateApi.getSewerageSupplyRateList(params);
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
const commonInfo = response.data.commonInfo; const commonInfo = response.data.commonInfo;
const statusInfo = response.data.statusInfo; const statusInfo = response.data.statusInfo;
const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d: SewerageSupplyRateItem) => ({ const convertedData: SewerageSupplyRateItem[] = filterBySelectedAdmdstCd(
response.data.sewerageSupplyRateList,
sidoListRef.current,
values,
).map((d: SewerageSupplyRateItem) => ({
...d, ...d,
ctpvNm: commonInfo.ctpvNm, ctpvNm: commonInfo.ctpvNm,
spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn, spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn,
@@ -278,6 +318,11 @@ const SeweragePenetrationRateInp = () => {
} }
: null : null
); );
setFooterVerificationResult(
remapCommonInfoVerificationToGridFields(
commonInfo.verificationResult,
),
);
setStatsCd(commonInfo.statsCd); setStatsCd(commonInfo.statsCd);
setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo)); setIsVerificationFailed(isVerificationFailedFromCommonInfo(commonInfo));
if (statusInfo && statusInfo.laeSbmsnSttsCd) { if (statusInfo && statusInfo.laeSbmsnSttsCd) {
@@ -299,6 +344,7 @@ const SeweragePenetrationRateInp = () => {
setRowData([]); setRowData([]);
setStatusInfo(null); setStatusInfo(null);
setFooterFromApi(null); setFooterFromApi(null);
setFooterVerificationResult(null);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -318,7 +364,7 @@ const SeweragePenetrationRateInp = () => {
setPeriodCheckList(periods); setPeriodCheckList(periods);
const checkEnterable = isInputPeriodCurrent(periods); const checkEnterable = isInputPeriodCurrent(periods);
setLastSearchedYear(values.year || null); setLastSearchedYear(values.year || null);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable, values);
setLastSearchedValues(values); setLastSearchedValues(values);
} else { } else {
alert(response.message); alert(response.message);
@@ -346,9 +392,8 @@ const SeweragePenetrationRateInp = () => {
const params: SubmitExceptionRequest = { const params: SubmitExceptionRequest = {
...submitInfo, ...submitInfo,
sewerBzmnCd: searchValues?.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '', statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
statsCd: statsCd, statsCd: statsCd,
@@ -364,7 +409,7 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsApi.submitException(params); const response = await statisticsApi.submitException(params);
if (response.status === 'success') { if (response.status === 'success') {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false, searchValues ?? {});
alert('제출되었습니다.'); alert('제출되었습니다.');
} else { } else {
alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); alert(response.message || '데이터 제출 중 오류가 발생했습니다.');
@@ -388,7 +433,7 @@ const SeweragePenetrationRateInp = () => {
return; return;
} }
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true, values);
}; };
const submitFunc = async (isException: boolean) => { const submitFunc = async (isException: boolean) => {
@@ -453,12 +498,12 @@ const SeweragePenetrationRateInp = () => {
}; };
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
sggNm: normalizeGridString(d.sggNm), sggNm: nullableTrimmedString(d.sggNm),
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
trbtrNm: normalizeGridString(d.trbtrNm), trbtrNm: nullableTrimmedString(d.trbtrNm),
spcCtpnRgnCn: spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음' ? null : normalizeGridString(d.spcCtpnRgnCn), d.spcCtpnRgnCn === '해당없음' ? null : nullableTrimmedString(d.spcCtpnRgnCn),
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
@@ -471,7 +516,7 @@ const SeweragePenetrationRateInp = () => {
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
rmrk: normalizeGridString(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
wholArea: formatNumber(d.wholArea, 'wholArea'), wholArea: formatNumber(d.wholArea, 'wholArea'),
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
@@ -484,13 +529,12 @@ const SeweragePenetrationRateInp = () => {
})); }));
const requestData: SewerageSupplyRateSaveRequest = { const requestData: SewerageSupplyRateSaveRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '', statsYr: searchValues?.year ?? '',
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
submitList: convertedData, submitList: convertedData,
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt, sewerBzmnArea: footerFromApi?.sewerBzmnArea,
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm, laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -507,7 +551,7 @@ const SeweragePenetrationRateInp = () => {
setGlobalProgress(50); setGlobalProgress(50);
if (response.data.finalStatus === '5') { if (response.data.finalStatus === '5') {
setGlobalProgressLabel('IMR 검증 진행 중...'); setGlobalProgressLabel('시계열 검증 진행 중...');
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: searchValues?.bsnmCd ?? '', sewer_bzmn_cd: searchValues?.bsnmCd ?? '',
@@ -515,6 +559,7 @@ const SeweragePenetrationRateInp = () => {
stats_yr: searchValues?.year ?? '', stats_yr: searchValues?.year ?? '',
lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'), lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'),
gen_verify_stts_cd: response.data.nvpassed ? '1' : '2', gen_verify_stts_cd: response.data.nvpassed ? '1' : '2',
hb_pass_yn: response.data.isHbPassed,
}; };
try { try {
@@ -530,13 +575,12 @@ const SeweragePenetrationRateInp = () => {
); );
} }
setGlobalProgress(100); setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true, searchValues ?? {});
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd, statsCd,
} }
@@ -545,14 +589,13 @@ const SeweragePenetrationRateInp = () => {
{ notify: alert }, { notify: alert },
); );
} catch (e) { } catch (e) {
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true, searchValues ?? {});
setIsLoading(false); setIsLoading(false);
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd, statsCd,
} }
@@ -569,13 +612,12 @@ const SeweragePenetrationRateInp = () => {
} }
} else { } else {
setGlobalProgress(100); setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false, searchValues ?? {});
await openVerificationResultDialogIfNeeded( await openVerificationResultDialogIfNeeded(
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd, statsCd,
} }
@@ -636,14 +678,14 @@ const SeweragePenetrationRateInp = () => {
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
// 행정구역 // 행정구역
sggNm: normalizeGridString(d.sggNm), // 시군구 sggNm: nullableTrimmedString(d.sggNm), // 시군구
admdstCd: normalizeGridString(d.admdstCd), // 행정코드 admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드
admdstNm: normalizeGridString(d.admdstNm), // 행정구역명 admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명
trbtrNm: normalizeGridString(d.trbtrNm), // 지류 trbtrNm: nullableTrimmedString(d.trbtrNm), // 지류
spcCtpnRgnCn: spcCtpnRgnCn:
d.spcCtpnRgnCn === '해당없음' d.spcCtpnRgnCn === '해당없음'
? null ? null
: normalizeGridString(d.spcCtpnRgnCn), // 특별대책지역 : nullableTrimmedString(d.spcCtpnRgnCn), // 특별대책지역
// 공공하수처리인구(명) - 물리적/생물학적/고도처리 // 공공하수처리인구(명) - 물리적/생물학적/고도처리
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리 ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리 ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
@@ -659,7 +701,7 @@ const SeweragePenetrationRateInp = () => {
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구 drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구 spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢) staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
rmrk: normalizeGridString(d.rmrk), // 비고 rmrk: nullableTrimmedString(d.rmrk), // 비고
// 합계·소계(계산필드 저장용) // 합계·소계(계산필드 저장용)
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명) wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢) wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
@@ -674,13 +716,12 @@ const SeweragePenetrationRateInp = () => {
})); }));
const requestData: SewerageSupplyRateSaveRequest = { const requestData: SewerageSupplyRateSaveRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드 ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', // 시도 ctpvNm: searchValues?.sido ?? '', // 시도
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
statsYr: searchValues?.year ?? '', // 통계 연도 statsYr: searchValues?.year ?? '', // 통계 연도
userId: userInfo.userUnqId, // 사용자 userId: userInfo.userUnqId, // 사용자
saveList: convertedData, // 저장 데이터 saveList: convertedData, // 저장 데이터
sewerBzmnFrgnrWholPpltnCnt: footerFromApi?.sewerBzmnFrgnrWholPpltnCnt // 행정참고자료 외국인 합계 sewerBzmnArea: footerFromApi?.sewerBzmnArea, // 행정참고자료면적
}; };
if (isAdminInfo) { if (isAdminInfo) {
@@ -692,7 +733,7 @@ const SeweragePenetrationRateInp = () => {
if (response.status === 'success') { if (response.status === 'success') {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgress(100); setGlobalProgress(100);
await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true, searchValues ?? {});
alert('저장되었습니다.'); alert('저장되었습니다.');
} else { } else {
alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
@@ -719,9 +760,8 @@ const SeweragePenetrationRateInp = () => {
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: searchValues?.bsnmCd ?? '', ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues?.sido ?? '', ctpvNm: searchValues?.sido ?? '',
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
statsYr: searchValues?.year ?? '', statsYr: searchValues?.year ?? '',
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCd, statsCd: statsCd,
@@ -730,7 +770,7 @@ const SeweragePenetrationRateInp = () => {
const response = await statisticsApi.cancelSubmit(requestData); const response = await statisticsApi.cancelSubmit(requestData);
if (response.status === 'success') { if (response.status === 'success') {
fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true, searchValues ?? {});
alert('제출 취소되었습니다.'); alert('제출 취소되었습니다.');
} else { } else {
alert(response.message ?? '제출 취소 중 오류가 발생했습니다.'); alert(response.message ?? '제출 취소 중 오류가 발생했습니다.');
@@ -749,13 +789,14 @@ const SeweragePenetrationRateInp = () => {
setVerificationHistoryOpen(true); setVerificationHistoryOpen(true);
}; };
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '') const toolbarSearchContext = lastSearchedValues;
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') const gugunOkOrNotDoList = isGugunOkForToolbar(sidoList, toolbarSearchContext);
: true; const submitOk = isSubmitOkForToolbar(sidoList, toolbarSearchContext);
const inputPeriodCurrent = isInputPeriodCurrent(periodCheckList); const inputPeriodCurrent = isInputPeriodCurrent(periodCheckList);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2'; const isSubmitted = statusInfo?.laeSbmsnSttsCd === '2';
const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList; const baseShowSaveInToolbar =
(isEnterable || isAdminInfo) && inputPeriodCurrent && gugunOkOrNotDoList;
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList; const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
const showSubmittedInInputPeriodToolbar = const showSubmittedInInputPeriodToolbar =
isSubmitted && inputPeriodCurrent && gugunOkOrNotDoList; isSubmitted && inputPeriodCurrent && gugunOkOrNotDoList;
@@ -773,6 +814,7 @@ const SeweragePenetrationRateInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -789,10 +831,10 @@ const SeweragePenetrationRateInp = () => {
{showEnterableToolbarButtons && ( {showEnterableToolbarButtons && (
<> <>
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} /> <LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={() => submitFunc(false)} /> {submitOk && <SubmitButton submitFunc={() => submitFunc(false)} />}
</> </>
)} )}
{statusInfo?.laeSbmsnSttsCd === '2' && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />} {statusInfo?.laeSbmsnSttsCd === '2' && submitOk && <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div> </div>
</div> </div>
@@ -805,9 +847,9 @@ const SeweragePenetrationRateInp = () => {
</div> </div>
} }
<DataGrid <DataGrid
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'} key={((isEnterable || isAdminInfo) && inputPeriodCurrent) ? 'enterable' : 'readonly'}
columnInfo={columnInfo} columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} isExcelImport={(rowData.length > 0) && isEnterable && gugunOkOrNotDoList}
excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd} excelImportSewerBzmnCd={(lastSearchedValues ?? searchValues)?.bsnmCd}
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
rowStartNum={3} rowStartNum={3}
@@ -826,6 +868,7 @@ const SeweragePenetrationRateInp = () => {
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error'; return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
} }
}} }}
footerVerificationResult={footerVerificationResult}
cellTooltipCallback={({ row, fieldName }) => { cellTooltipCallback={({ row, fieldName }) => {
const verification = (row as GridRowWithVerification).verificationResult; const verification = (row as GridRowWithVerification).verificationResult;
if (!verification) return; if (!verification) return;
@@ -833,7 +876,7 @@ const SeweragePenetrationRateInp = () => {
if (Array.isArray(errors) && errors.length > 0) { if (Array.isArray(errors) && errors.length > 0) {
return errors return errors
.map((e) => { .map((e) => {
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : 'HB검증 -'); const prefix = e.vrfcCd === 'IMR' ? '시계열 검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : '모집단 검증 -');
return `${prefix}${e.rsnExpln} return `${prefix}${e.rsnExpln}
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; (검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
@@ -862,9 +905,8 @@ const SeweragePenetrationRateInp = () => {
queryContext={ queryContext={
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm: searchValues.gugun === "ALL" ? "" : (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd, statsCd,
} }
@@ -882,12 +924,8 @@ const SeweragePenetrationRateInp = () => {
queryContext={ queryContext={
searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd searchValues?.year && searchValues?.sido && searchValues?.bsnmCd && statsCd
? { ? {
sewerBzmnCd: searchValues.bsnmCd, ...toInputSearchRegion(sidoListRef.current, searchValues),
ctpvNm: searchValues.sido, ctpvNm: searchValues.sido,
sggNm:
searchValues.gugun === "ALL"
? ""
: (searchValues.gugun ?? ""),
statsYr: searchValues.year, statsYr: searchValues.year,
statsCd, statsCd,
} }
@@ -2,33 +2,13 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn";
import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { patchVerificationFailedByTab } from "@/components/util/input/verification-failed-flag";
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputSewerageServiceLevelApi } from "@/entities/statistics/api/input/statistics-input-sewerage-service-level-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import { import {
SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
@@ -39,20 +19,55 @@ import {
type SewerageServiceLevelResidentFriendlyFactSaveItem, type SewerageServiceLevelResidentFriendlyFactSaveItem,
type SewerageServiceLevelVerificationResult, type SewerageServiceLevelVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-sewerage-service-level-interface"; } from "@/entities/statistics/model/input/statistics-input-sewerage-service-level-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type { import type {
SewerageStatusInfo,
cancelSubmitRequest, cancelSubmitRequest,
ImrStatusResponse, ImrStatusResponse,
SewerageStatusInfo,
Sido,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import {
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib"; filterBySelectedAdmdstCd,
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; isGugunOkForToolbar,
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; isSubmitOkForToolbar,
import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; resolveNoApplicableForRequest,
import { useAdminStore } from "@/store/admin-store"; toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog"; import SubmitDailog from "./SubmitDailog";
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
import LastYearDataButton from "@/components/button/LastYearDataButton";
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
import SaveButton from "@/components/button/SaveButton";
import SubmitButton from "@/components/button/SumitButton";
import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ComplaintInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ComplaintInpColumn";
import { ResidentFriendlyFactInpColumn } from "@/components/util/column/input/sewerageServiceLevel/ResidentFriendlyFactInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import {
patchFooterVerificationByTab,
patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag";
import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
const SewerageServiceLevelInpTabList = [ const SewerageServiceLevelInpTabList = [
ComplaintInpColumn, ComplaintInpColumn,
ResidentFriendlyFactInpColumn, ResidentFriendlyFactInpColumn,
@@ -60,11 +75,6 @@ const SewerageServiceLevelInpTabList = [
const firstTabKey = ComplaintInpColumn.tabKey; const firstTabKey = ComplaintInpColumn.tabKey;
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS = [
SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
] as const;
const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = { const SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
[ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD, [ComplaintInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_COMPLAINT_STATS_CD,
[ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD, [ResidentFriendlyFactInpColumn.tabKey]: SEWERAGE_SERVICE_LEVEL_RESIDENT_FRIENDLY_FACT_STATS_CD,
@@ -118,10 +128,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -135,7 +157,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -153,17 +175,16 @@ function buildSewerageServiceLevelSearchParam(
): SewerageServiceLevelComplaintRequestParams { ): SewerageServiceLevelComplaintRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
function mapServiceLevelListToGridRows< function mapServiceLevelListToGridRows<
T extends { verificationResult?: SewerageServiceLevelVerificationResult }, T extends { verificationResult?: SewerageServiceLevelVerificationResult },
>(list: T[], ctpvNm: string): GridRowWithVerification[] { >(list: T[], ctpvNm: string, values?: SearchFormValues | null): GridRowWithVerification[] {
return list.map((item) => { return filterBySelectedAdmdstCd(list, sidoListRef.current, values).map((item) => {
const { verificationResult, ...rest } = item; const { verificationResult, ...rest } = item;
const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification; const row = toGridRows([{ ctpvNm, ...rest }])[0] as GridRowWithVerification;
if (verificationResult) row.verificationResult = verificationResult; if (verificationResult) row.verificationResult = verificationResult;
@@ -171,17 +192,6 @@ function mapServiceLevelListToGridRows<
}); });
} }
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
function buildColumnFormatNumber( function buildColumnFormatNumber(
columns: { fieldName?: string; dataFormat?: string }[], columns: { fieldName?: string; dataFormat?: string }[],
) { ) {
@@ -210,8 +220,8 @@ function buildComplaintSaveList(
const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns); const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"), swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"),
dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"), dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"),
odorCnt: formatNumber(d.odorCnt, "odorCnt"), odorCnt: formatNumber(d.odorCnt, "odorCnt"),
@@ -220,7 +230,7 @@ function buildComplaintSaveList(
swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"), swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"),
sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"), sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"),
etcCnt: formatNumber(d.etcCnt, "etcCnt"), etcCnt: formatNumber(d.etcCnt, "etcCnt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
whol: formatNumber(d.whol, "whol"), whol: formatNumber(d.whol, "whol"),
crtrPpltnCvlcptEvntcsCnt: formatNumber( crtrPpltnCvlcptEvntcsCnt: formatNumber(
d.crtrPpltnCvlcptEvntcsCnt, d.crtrPpltnCvlcptEvntcsCnt,
@@ -235,11 +245,11 @@ function buildResidentFriendlyFactSaveList(
const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns); const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns);
return currentData.map((d) => ({ return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd), admdstCd: nullableTrimmedString(d.admdstCd),
admdstNm: normalizeGridString(d.admdstNm), admdstNm: nullableTrimmedString(d.admdstNm),
swtrmtFcltCd: normalizeGridString(d.swtrmtFcltCd), swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd),
swtrmtFcltNm: normalizeGridString(d.swtrmtFcltNm), swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm),
fcltSn: normalizeGridString(d.fcltSn), fcltSn: nullableTrimmedString(d.fcltSn),
tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"), tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"),
brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"), brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"),
childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"), childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"),
@@ -249,7 +259,7 @@ function buildResidentFriendlyFactSaveList(
eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"), eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"),
wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"), wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"),
etcCnt: formatNumber(d.etcCnt, "etcCnt"), etcCnt: formatNumber(d.etcCnt, "etcCnt"),
rmrk: normalizeGridRmrk(d.rmrk), rmrk: nullableTrimmedString(d.rmrk),
})); }));
} }
@@ -271,15 +281,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 13. 하수도 서비스 수준 // 하수도 통계 관리 > 13. 하수도 서비스 수준
const SewerageServiceLevelInp = () => { const SewerageServiceLevelInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -307,9 +319,15 @@ const SewerageServiceLevelInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [columnGuidesByTab, setColumnGuidesByTab] = useState< const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>> Record<string, Record<string, string>>
>({}); >({});
@@ -326,42 +344,30 @@ const SewerageServiceLevelInp = () => {
const publicSewageFacilitiesAvailableRef = useRef(false); const publicSewageFacilitiesAvailableRef = useRef(false);
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_SERVICE_LEVEL) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd =
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries); setColumnGuidesByTab((prev) =>
setColumnGuidesByTab( Object.prototype.hasOwnProperty.call(prev, tabKey)
Object.fromEntries( ? prev
Object.entries(SEWERAGE_SERVICE_LEVEL_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( : { ...prev, [tabKey]: next },
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
); );
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const commitToolbarSnapshotForLoadedTab = ( const commitToolbarSnapshotForLoadedTab = (
tabKey: string, tabKey: string,
@@ -384,6 +390,13 @@ const SewerageServiceLevelInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -425,6 +438,7 @@ const SewerageServiceLevelInp = () => {
const gridRows = mapServiceLevelListToGridRows( const gridRows = mapServiceLevelListToGridRows(
(complaintList ?? []) as SewerageServiceLevelComplaintItem[], (complaintList ?? []) as SewerageServiceLevelComplaintItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -435,6 +449,9 @@ const SewerageServiceLevelInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true); setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
@@ -454,6 +471,7 @@ const SewerageServiceLevelInp = () => {
const gridRows = mapServiceLevelListToGridRows( const gridRows = mapServiceLevelListToGridRows(
(residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[], (residentFriendlyFactList ?? []) as SewerageServiceLevelResidentFriendlyFactItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
searchContext,
); );
if (!fromPreYear) { if (!fromPreYear) {
publicSewageFacilitiesAvailableRef.current = gridRows.length > 0; publicSewageFacilitiesAvailableRef.current = gridRows.length > 0;
@@ -470,6 +488,9 @@ const SewerageServiceLevelInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
setSearchLoaded(true); setSearchLoaded(true);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
@@ -493,7 +514,9 @@ const SewerageServiceLevelInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -538,7 +561,9 @@ const SewerageServiceLevelInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setIsNotApplicable(false); setIsNotApplicable(false);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
@@ -657,9 +682,19 @@ const SewerageServiceLevelInp = () => {
const tabKey = activeTabRef.current; const tabKey = activeTabRef.current;
const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey; const isResidentTab = tabKey === ResidentFriendlyFactInpColumn.tabKey;
const latestRows = getTabGridRows(tabKey); const latestRows = getTabGridRows(tabKey);
const noApplicable = isResidentTab && isNotApplicable; const noApplicable =
isResidentTab &&
buildNoApplicableForRequest(
searchContextByTab.get(tabKey) ?? lastSearchedValues,
isNotApplicable,
latestRows.length === 0,
);
if (latestRows.length === 0) { if (latestRows.length === 0) {
if (isResidentTab && !noApplicable) { if (
isResidentTab &&
!noApplicable &&
buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)
) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -700,7 +735,13 @@ const SewerageServiceLevelInp = () => {
return next; return next;
}); });
const noApplicable = isResidentTab && isNotApplicable; const noApplicable =
isResidentTab &&
buildNoApplicableForRequest(
searchContextByTab.get(tabKey) ?? lastSearchedValues,
isNotApplicable,
latestRows.length === 0,
);
const complaintSubmitList = buildComplaintSaveList(latestRows); const complaintSubmitList = buildComplaintSaveList(latestRows);
const residentSubmitList = noApplicable const residentSubmitList = noApplicable
? [] ? []
@@ -710,7 +751,12 @@ const SewerageServiceLevelInp = () => {
alert("제출할 데이터가 없습니다."); alert("제출할 데이터가 없습니다.");
return; return;
} }
if (isResidentTab && !noApplicable && residentSubmitList.length === 0) { if (
isResidentTab &&
!noApplicable &&
residentSubmitList.length === 0 &&
buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)
) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -725,11 +771,7 @@ const SewerageServiceLevelInp = () => {
const submitInfoBase = { const submitInfoBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -741,12 +783,8 @@ const SewerageServiceLevelInp = () => {
}; };
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -791,14 +829,15 @@ const SewerageServiceLevelInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", lae_sbmsn_stts_cd: activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -875,15 +914,17 @@ const SewerageServiceLevelInp = () => {
return next; return next;
}); });
const noApplicable = isResidentTab && isNotApplicable; const noApplicable =
isResidentTab &&
buildNoApplicableForRequest(
searchContextByTab.get(tabKey) ?? lastSearchedValues,
isNotApplicable,
latestRows.length === 0,
);
const baseRequest = { const baseRequest = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
}; };
try { try {
@@ -913,7 +954,11 @@ const SewerageServiceLevelInp = () => {
const saveList = noApplicable const saveList = noApplicable
? [] ? []
: buildResidentFriendlyFactSaveList(latestRows); : buildResidentFriendlyFactSaveList(latestRows);
if (!noApplicable && saveList.length === 0) { if (
!noApplicable &&
saveList.length === 0 &&
buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)
) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -973,12 +1018,8 @@ const SewerageServiceLevelInp = () => {
} }
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -1016,10 +1057,10 @@ const SewerageServiceLevelInp = () => {
for (const tab of SewerageServiceLevelInpTabList) { for (const tab of SewerageServiceLevelInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1036,15 +1077,15 @@ const SewerageServiceLevelInp = () => {
return map; return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]); }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab]);
const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabRowData = rowDataMap.get(activeTab) ?? []; const activeTabRowData = rowDataMap.get(activeTab) ?? [];
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
hasSearched, hasSearched,
lastSearchedValues, searchContextForToolbar,
inputPeriodEnterable, inputPeriodEnterable,
isAdminInfo, isAdminInfo,
); );
@@ -1055,12 +1096,14 @@ const SewerageServiceLevelInp = () => {
activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard = const showActionToolbarCard =
activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showNotApplicableForResidentTab = const showNotApplicableForResidentTab =
activeTab === ResidentFriendlyFactInpColumn.tabKey && activeTab === ResidentFriendlyFactInpColumn.tabKey &&
searchLoaded && searchLoaded &&
hasSearched && hasSearched &&
activeTabRowData.length === 0 && activeTabRowData.length === 0 &&
computeTabEnterable(activeTabStatus, inputPeriodEnterable); computeTabEnterable(activeTabStatus, inputPeriodEnterable) &&
submitOk;
const showSecondaryToolbarCard = const showSecondaryToolbarCard =
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
@@ -1071,9 +1114,8 @@ const SewerageServiceLevelInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -1110,10 +1152,10 @@ const SewerageServiceLevelInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -1136,6 +1178,7 @@ const SewerageServiceLevelInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -1143,8 +1186,8 @@ const SewerageServiceLevelInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={ isVerificationFailed={
@@ -1156,19 +1199,19 @@ const SewerageServiceLevelInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div> </div>
</div> </div>
)} )}
{showSecondaryToolbarCard && ( {showSecondaryToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showNotApplicableForResidentTab && ( {showNotApplicableForResidentTab && (
<NotApplicableCheckButton <NotApplicableCheckButton
isCheck={isNotApplicable} isCheck={isNotApplicable}
@@ -1203,11 +1246,11 @@ const SewerageServiceLevelInp = () => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return ( return (
@@ -1245,6 +1288,9 @@ const SewerageServiceLevelInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport isExcelExport
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
getGridDataRef={ getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined activeTab === tabKey ? getGridDataRef : undefined
@@ -1,13 +1,14 @@
import { useState } from "react"; import { useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { LoadingWithAnimation } from "@common/components";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import type { ColumnConfig } from "@/components/type/DataGrid.type"; import type { ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation } from "@common/components";
const StatisticsReviewHistory = () => { const StatisticsReviewHistory = () => {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -69,7 +70,7 @@ const StatisticsReviewHistory = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} /> <SearchForm isInput onSubmit={handleSearch} onReset={handleReset} />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <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]"> <div className="relative size-[5rem]">
@@ -4,32 +4,33 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { RealGridReact } from "realgrid-react"; import { RealGridReact } from "realgrid-react";
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useActiveTabRegAuthorityAllowed, useAlertStore } from "@/shared/lib"; import { useActiveTabRegAuthorityAllowed, useAlertStore, useSearchConditionSidoList } from "@/shared/lib";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; 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 { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-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 { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
import type { import type {
StatsInputStatusListApiResponse, StatsInputStatusListApiResponse,
StatsInputStatusListRequestParams, StatsInputStatusListRequestParams,
} from "@/entities/statistics/model/input/statistics-input-status-interface"; } from "@/entities/statistics/model/input/statistics-input-status-interface";
import {
type CheckInputPeriodItem,
isInputPeriodCurrent,
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
import { isGugunOkForToolbar } from "@/entities/statistics";
import { LoadingWithAnimation } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { LoadingWithAnimation } from "@common/components";
import ApproveButton from "@/components/button/ApproveButton"; import ApproveButton from "@/components/button/ApproveButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import { import {
ExceptionProcessingHistoryDialog, ExceptionProcessingHistoryDialog,
type ExceptionProcessingHistoryQueryContext, type ExceptionProcessingHistoryQueryContext,
} from "@/components/module/ExceptionProcessingHistoryDialog"; } from "@/components/module/ExceptionProcessingHistoryDialog";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn"; import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { useTabStore } from "@/store/tab-store"; import { useTabStore } from "@/store/tab-store";
@@ -43,6 +44,7 @@ const getStatsInputStatusFetcher = async (
// 하수도 통계 관리 > 00. 통계 입력 현황 // 하수도 통계 관리 > 00. 통계 입력 현황
const StatsInputStatusInp = () => { const StatsInputStatusInp = () => {
const { sidoList } = useSearchConditionSidoList();
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const activeTabId = useTabStore((state) => state.activeTabId); const activeTabId = useTabStore((state) => state.activeTabId);
@@ -253,9 +255,7 @@ const StatsInputStatusInp = () => {
await mutate(["stats-input-status", searchParams]); await mutate(["stats-input-status", searchParams]);
}, [mutate, searchParams]); }, [mutate, searchParams]);
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "") const gugunOkOrNotDoList = isGugunOkForToolbar(sidoList, lastSearchedValues);
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
: true;
const allRowsLaeSbmsnApproved = useMemo( const allRowsLaeSbmsnApproved = useMemo(
() => () =>
@@ -1,11 +1,12 @@
"use client"; "use client";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm, Controller } from "react-hook-form"; import { Controller,useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type { SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface";
import { zodResolver } from "@hookform/resolvers/zod";
import { type StatisticsInputSubmitInfoFormData,statisticsInputSubmitInfoSchema } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type { SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface";
import { import {
Button, Button,
Dialog, Dialog,
@@ -14,12 +15,12 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
Input, Input,
LoadingWithAnimation,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
LoadingWithAnimation,
} from "@common/components"; } from "@common/components";
/** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */ /** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */
@@ -1,19 +1,68 @@
import { import {
type Dispatch,
type SetStateAction,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
type Dispatch,
type SetStateAction,
} from "react"; } from "react";
import { flushSync } from "react-dom";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { ValueType } from "realgrid"; import { ValueType } from "realgrid";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { flushSync } from "react-dom";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate, useSearchConditionSidoList } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { nullableTrimmedString } from "@/components/util/input/nullableString";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import {
type PublicSewageTreatWaterReuseItem,
type PublicSewageTreatWaterReuseSaveItem,
type RainFallUtilFactStatusItem,
type RainFallUtilFactStatusSaveItem,
type RecycledWaterStatusItem,
type RecycledWaterStatusSaveItem,
type TreatWaterReuseBusinessApprStatusItem,
type TreatWaterReuseBusinessApprStatusSaveItem,
type TreatWaterReuseDesignConstStatusItem,
type TreatWaterReuseDesignConstStatusSaveItem,
WATER_REUSE_BUSINESS_APPR_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
type WaterReuseRequestParams,
type WaterReuseVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-water-reuse-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import type {
ApiResponse,
cancelSubmitRequest,
ImrStatusResponse,
SewerageStatusInfo,
Sgg,
Sido,
} from "@/entities/statistics/model/statistics-interface";
import {
filterBySelectedAdmdstCd,
isGugunOkForToolbar,
isSubmitOkForToolbar,
resolveNoApplicableForRequest,
toInputSearchRegion,
} from "@/entities/statistics";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import SubmitDailog from "./SubmitDailog";
import AddArticleButton from "@/components/button/AddArticleButton"; import AddArticleButton from "@/components/button/AddArticleButton";
import CancelSubmitButton from "@/components/button/CancelSubmitButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton";
@@ -25,83 +74,39 @@ import SubmitButton from "@/components/button/SumitButton";
import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton"; import UpdateOperateDataButton from "@/components/button/UpdateOperateDataButton";
import VerificationResultButton from "@/components/button/VerificationResultButton"; import VerificationResultButton from "@/components/button/VerificationResultButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results"; import { openVerificationResultDialogIfNeeded } from "@/components/module/post-submit-verification-results";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { VerificationResultDialog } from "@/components/module/VerificationResultDialog";
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doList } from "@/components/util/column/input/DoList";
import { PublicSewageTreatWaterReuseInpColumn } from "@/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn"; import { PublicSewageTreatWaterReuseInpColumn } from "@/components/util/column/input/waterReuse/PublicSewageTreatWaterReuseInpColumn";
import { RainFallUtilFactStatusInpColumn } from "@/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn"; import { RainFallUtilFactStatusInpColumn } from "@/components/util/column/input/waterReuse/RainFallUtilFactStatusInpColumn";
import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn"; import { RecycledWaterStatusInpColumn } from "@/components/util/column/input/waterReuse/RecycledWaterStatusInpColumn";
import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn"; import { TreatWaterReuseBusinessApprStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseBusinessApprStatusInpColumn";
import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn"; import { TreatWaterReuseDesignConstStatusInpColumn } from "@/components/util/column/input/waterReuse/TreatWaterReuseDesignConstStatusInpColumn";
import { fetchColumnGuidesRecord } from "@/components/util/grid/columnGuides";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat";
import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport";
import { import {
patchFooterVerificationByTab,
patchVerificationFailedByTab, patchVerificationFailedByTab,
} from "@/components/util/input/verification-failed-flag"; } from "@/components/util/input/verification-failed-flag";
import { formatCompactYmdForGrid, toCompactYmdOrNull } from "@/components/util/grid/ymdFormat"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display";
import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import {
WATER_REUSE_BUSINESS_APPR_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
type PublicSewageTreatWaterReuseItem,
type PublicSewageTreatWaterReuseSaveItem,
type RainFallUtilFactStatusItem,
type RainFallUtilFactStatusSaveItem,
type RecycledWaterStatusItem,
type RecycledWaterStatusSaveItem,
type TreatWaterReuseBusinessApprStatusItem,
type TreatWaterReuseBusinessApprStatusSaveItem,
type TreatWaterReuseDesignConstStatusItem,
type TreatWaterReuseDesignConstStatusSaveItem,
type WaterReuseRequestParams,
type WaterReuseVerificationResult,
} from "@/entities/statistics/model/input/statistics-input-water-reuse-interface";
import type {
ApiResponse,
ImrStatusResponse,
Sgg,
SewerageStatusInfo,
cancelSubmitRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema";
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
import { getUserInfo } from "../../../../../../packages/common/components/src/api"; const WaterReuseInpTabList = [
PublicSewageTreatWaterReuseInpColumn,
import SubmitDailog from "./SubmitDailog"; RecycledWaterStatusInpColumn,
RainFallUtilFactStatusInpColumn,
const WaterReuseInpTabList = [ TreatWaterReuseDesignConstStatusInpColumn,
PublicSewageTreatWaterReuseInpColumn,
RecycledWaterStatusInpColumn,
RainFallUtilFactStatusInpColumn,
TreatWaterReuseDesignConstStatusInpColumn,
TreatWaterReuseBusinessApprStatusInpColumn, TreatWaterReuseBusinessApprStatusInpColumn,
] as const; ] as const;
const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey; const firstTabKey = PublicSewageTreatWaterReuseInpColumn.tabKey;
const WATER_REUSE_COLUMN_GUIDE_STATS_CDS = [
WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
WATER_REUSE_RECYCLED_WATER_STATS_CD,
WATER_REUSE_RAINFALL_UTIL_FACT_STATS_CD,
WATER_REUSE_DESIGN_CONST_STATS_CD,
WATER_REUSE_BUSINESS_APPR_STATS_CD,
] as const;
const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = { const WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY: Record<string, string> = {
[PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD, [PublicSewageTreatWaterReuseInpColumn.tabKey]: WATER_REUSE_PUBLIC_SEWAGE_TREAT_STATS_CD,
[RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD, [RecycledWaterStatusInpColumn.tabKey]: WATER_REUSE_RECYCLED_WATER_STATS_CD,
@@ -162,10 +167,22 @@ const computeTabEnterable = (
return periodEnterable; return periodEnterable;
}; };
const sidoListRef: { current: Sido[] } = { current: [] };
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean { function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
return doList.includes(values?.bsnmCd || "") return isGugunOkForToolbar(sidoListRef.current, values);
? Boolean(values?.gugun && values.gugun !== "ALL") }
: true;
function buildSubmitOkForToolbar(values: SearchFormValues | null): boolean {
return isSubmitOkForToolbar(sidoListRef.current, values);
}
function buildNoApplicableForRequest(
values: SearchFormValues | null,
checked: boolean,
isSaveListEmpty: boolean,
): boolean {
return resolveNoApplicableForRequest(sidoListRef.current, values, checked, isSaveListEmpty);
} }
function computeTabToolbarSnapshot( function computeTabToolbarSnapshot(
@@ -179,7 +196,7 @@ function computeTabToolbarSnapshot(
const gugunOk = buildGugunOkForToolbar(searchContext); const gugunOk = buildGugunOkForToolbar(searchContext);
const enterable = computeTabEnterable(statusInfo, periodEnterable); const enterable = computeTabEnterable(statusInfo, periodEnterable);
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2"; const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk; const showSaveInToolbar = (enterable || isAdminInfo) && periodEnterable && gugunOk;
const showEnterableToolbarButtons = enterable && gugunOk; const showEnterableToolbarButtons = enterable && gugunOk;
const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk; const showSubmittedInInputPeriodToolbar = isSubmitted && periodEnterable && gugunOk;
const showActionToolbarCard = const showActionToolbarCard =
@@ -197,9 +214,8 @@ function buildWaterReuseSearchParam(
): WaterReuseRequestParams { ): WaterReuseRequestParams {
return { return {
statsYr: values.year, statsYr: values.year,
sewerBzmnCd: values.bsnmCd,
ctpvNm: values.sido, ctpvNm: values.sido,
sggNm: values.gugun === "ALL" ? "" : values.gugun, ...toInputSearchRegion(sidoListRef.current, values),
...(overrides?.mode ? { mode: overrides.mode } : {}), ...(overrides?.mode ? { mode: overrides.mode } : {}),
}; };
} }
@@ -239,13 +255,6 @@ function buildSggNmColumn(
}; };
} }
function nullableTrimmedString(
value: string | number | null | undefined,
): string | null {
const s = String(value ?? "").trim();
return s === "" ? null : s;
}
function toGridNumber(value: string | number | null | undefined): number { function toGridNumber(value: string | number | null | undefined): number {
const n = Number(value); const n = Number(value);
return Number.isFinite(n) ? n : 0; return Number.isFinite(n) ? n : 0;
@@ -254,9 +263,9 @@ function toGridNumber(value: string | number | null | undefined): number {
function resolveArticleAdmdst( function resolveArticleAdmdst(
row: Record<string, string | number>, row: Record<string, string | number>,
sggOptions: Sgg[], sggOptions: Sgg[],
): { admdstCd: string; admdstNm: string } { ): { admdstCd: string | null; admdstNm: string | null } {
const existingCd = String(row.admdstCd ?? "").trim(); const existingCd = nullableTrimmedString(row.admdstCd);
const existingNm = String(row.admdstNm ?? "").trim(); const existingNm = nullableTrimmedString(row.admdstNm);
if (existingCd && existingNm) { if (existingCd && existingNm) {
return { admdstCd: existingCd, admdstNm: existingNm }; return { admdstCd: existingCd, admdstNm: existingNm };
} }
@@ -265,7 +274,10 @@ function resolveArticleAdmdst(
if (matched) { if (matched) {
return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun };
} }
return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; return {
admdstCd: existingCd,
admdstNm: existingNm ?? nullableTrimmedString(sggNm),
};
} }
function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null { function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null {
@@ -301,11 +313,11 @@ function buildRecycledWaterStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? "").trim(), fcltNm: nullableTrimmedString(row.fcltNm),
bldgSeCn: String(row.bldgSeCn ?? "").trim(), bldgSeCn: nullableTrimmedString(row.bldgSeCn),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), lglTrgtYn: nullableTrimmedString(row.lglTrgtYn),
cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd), cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
archWholArea: toGridNumber(row.archWholArea), archWholArea: toGridNumber(row.archWholArea),
@@ -313,7 +325,7 @@ function buildRecycledWaterStatusSaveItem(
wtrUseQnt: toGridNumber(row.wtrUseQnt), wtrUseQnt: toGridNumber(row.wtrUseQnt),
ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt), ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt),
sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt), sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt),
mnUsgCn: String(row.mnUsgCn ?? "").trim(), mnUsgCn: nullableTrimmedString(row.mnUsgCn),
ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt), ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
@@ -327,18 +339,18 @@ function buildRainFallUtilFactStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
fcltNm: String(row.fcltNm ?? "").trim(), fcltNm: nullableTrimmedString(row.fcltNm),
bdstTypCn: String(row.bdstTypCn ?? "").trim(), bdstTypCn: nullableTrimmedString(row.bdstTypCn),
fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), fcltPstnCn: nullableTrimmedString(row.fcltPstnCn),
lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), lglTrgtYn: nullableTrimmedString(row.lglTrgtYn),
cmptYmd: toCompactYmdOrNull(row.cmptYmd), cmptYmd: toCompactYmdOrNull(row.cmptYmd),
instlAmt: toGridNumber(row.instlAmt), instlAmt: toGridNumber(row.instlAmt),
rcptMynCn: String(row.rcptMynCn ?? "").trim(), rcptMynCn: nullableTrimmedString(row.rcptMynCn),
filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(), filtrPrcsFcltYnExpln: nullableTrimmedString(row.filtrPrcsFcltYnExpln),
impdCpct: toGridNumber(row.impdCpct), impdCpct: toGridNumber(row.impdCpct),
anlUseQnt: toGridNumber(row.anlUseQnt), anlUseQnt: toGridNumber(row.anlUseQnt),
exlnUtlzUsgExpln: String(row.exlnUtlzUsgExpln ?? "").trim(), exlnUtlzUsgExpln: nullableTrimmedString(row.exlnUtlzUsgExpln),
anlOperAmt: toGridNumber(row.anlOperAmt), anlOperAmt: toGridNumber(row.anlOperAmt),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
@@ -352,10 +364,10 @@ function buildTreatWaterReuseDesignConstStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
bplcSn: String(row.bplcSn ?? "").trim(), bplcSn: nullableTrimmedString(row.bplcSn),
bplcNm: String(row.bplcNm ?? "").trim(), bplcNm: nullableTrimmedString(row.bplcNm),
rprsvNm: String(row.rprsvNm ?? "").trim(), rprsvNm: nullableTrimmedString(row.rprsvNm),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), bplcPstnCn: nullableTrimmedString(row.bplcPstnCn),
coRegYmd: toCompactYmdOrNull(row.coRegYmd), coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
@@ -369,10 +381,10 @@ function buildTreatWaterReuseBusinessApprStatusSaveItem(
return { return {
admdstCd, admdstCd,
admdstNm, admdstNm,
bplcSn: String(row.bplcSn ?? "").trim(), bplcSn: nullableTrimmedString(row.bplcSn),
bplcNm: String(row.bplcNm ?? "").trim(), bplcNm: nullableTrimmedString(row.bplcNm),
rprsvNm: String(row.rprsvNm ?? "").trim(), rprsvNm: nullableTrimmedString(row.rprsvNm),
bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), bplcPstnCn: nullableTrimmedString(row.bplcPstnCn),
coRegYmd: toCompactYmdOrNull(row.coRegYmd), coRegYmd: toCompactYmdOrNull(row.coRegYmd),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
}; };
@@ -382,18 +394,18 @@ function buildPublicSewageTreatWaterReuseSaveItem(
row: Record<string, string | number>, row: Record<string, string | number>,
): PublicSewageTreatWaterReuseSaveItem { ): PublicSewageTreatWaterReuseSaveItem {
return { return {
admdstCd: String(row.admdstCd ?? "").trim(), admdstCd: nullableTrimmedString(row.admdstCd),
admdstNm: String(row.admdstNm ?? row.sggNm ?? "").trim(), admdstNm: nullableTrimmedString(row.admdstNm ?? row.sggNm),
fcltSn: String(row.fcltSn ?? "").trim(), fcltSn: nullableTrimmedString(row.fcltSn),
swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd),
swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm),
wholPrcsQnt: toGridNumber(row.wholPrcsQnt), wholPrcsQnt: toGridNumber(row.wholPrcsQnt),
lglYn: String(row.lglYn ?? "").trim(), lglYn: nullableTrimmedString(row.lglYn),
instlYn: String(row.instlYn ?? "").trim(), instlYn: nullableTrimmedString(row.instlYn),
oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd),
instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd), instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd),
rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct), rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct),
mtcnsNm: String(row.mtcnsNm ?? "").trim(), mtcnsNm: nullableTrimmedString(row.mtcnsNm),
ruswtPplLen: toGridNumber(row.ruswtPplLen), ruswtPplLen: toGridNumber(row.ruswtPplLen),
ruswtPumpLen: toGridNumber(row.ruswtPumpLen), ruswtPumpLen: toGridNumber(row.ruswtPumpLen),
rmrk: nullableTrimmedString(row.rmrk), rmrk: nullableTrimmedString(row.rmrk),
@@ -508,15 +520,17 @@ async function waitForImrJobCompleted(
options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); 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 === "COMPLETED" && Number(res.job_prog) === 100) return;
if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") {
throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); throw new Error(res.error_msg || `시계열 검증이 ${res.status} 상태로 종료되었습니다.`);
} }
await sleep(pollIntervalMs); await sleep(pollIntervalMs);
} }
throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); throw new Error("시계열 검증 완료 대기 시간이 초과되었습니다.");
} }
// 하수도 통계 관리 > 10. 물 재이용 // 하수도 통계 관리 > 10. 물 재이용
const WaterReuseInp = () => { const WaterReuseInp = () => {
const { sidoList } = useSearchConditionSidoList({ isInput: true });
sidoListRef.current = sidoList;
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { isAdminInfo } = useAdminStore(); const { isAdminInfo } = useAdminStore();
@@ -545,9 +559,15 @@ const WaterReuseInp = () => {
const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState< const [isVerificationFailedByTab, setIsVerificationFailedByTab] = useState<
Map<string, boolean> Map<string, boolean>
>(new Map()); >(new Map());
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState< const [footerVerificationByTab, setFooterVerificationByTab] = useState<
Map<string, VerificationResultLikeMap | null>
>(new Map());
const [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
Map<string, TabToolbarSnapshot> Map<string, TabToolbarSnapshot>
>(new Map()); >(new Map());
const [searchContextByTab, setSearchContextByTab] = useState<Map<string, SearchFormValues>>(
() => new Map(),
);
const [columnGuidesByTab, setColumnGuidesByTab] = useState< const [columnGuidesByTab, setColumnGuidesByTab] = useState<
Record<string, Record<string, string>> Record<string, Record<string, string>>
>({}); >({});
@@ -569,42 +589,29 @@ const WaterReuseInp = () => {
const loadedTabKeysRef = useRef<Set<string>>(new Set()); const loadedTabKeysRef = useRef<Set<string>>(new Set());
activeTabRef.current = activeTab; activeTabRef.current = activeTab;
const activeTabId = useTabStore((s) => s.activeTabId);
useEffect(() => { useEffect(() => {
if (activeTabId !== STATISTICS_INPUT_URL_PATH.WATER_REUSE) return;
const tabKey = activeTab;
if (!tabKey) return;
if (Object.prototype.hasOwnProperty.call(columnGuidesByTab, tabKey)) return;
const statsCd = WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? tabKey;
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
const entries = await Promise.all( const next = await fetchColumnGuidesRecord(statsCd);
WATER_REUSE_COLUMN_GUIDE_STATS_CDS.map(async (statsCd) => {
try {
const res = await statisticsApi.getColumnGuides(statsCd);
if (res.status !== "success" || !Array.isArray(res.data)) {
return [statsCd, {}] as const;
}
const next: Record<string, string> = {};
for (const item of res.data) {
const key = item.engColNm?.trim();
const guide = item.columnGuide?.trim();
if (key && guide) next[key] = guide;
}
return [statsCd, next] as const;
} catch {
return [statsCd, {}] as const;
}
}),
);
if (cancelled) return; if (cancelled) return;
const guidesByStatsCd = Object.fromEntries(entries); setColumnGuidesByTab((prev) =>
setColumnGuidesByTab( Object.prototype.hasOwnProperty.call(prev, tabKey)
Object.fromEntries( ? prev
Object.entries(WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY).map( : { ...prev, [tabKey]: next },
([tabKey, statsCd]) => [tabKey, guidesByStatsCd[statsCd] ?? {}],
),
),
); );
})(); })();
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, []); }, [activeTabId, activeTab, columnGuidesByTab]);
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => { const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
if (!sido || sido === "ALL") { if (!sido || sido === "ALL") {
@@ -661,6 +668,13 @@ const WaterReuseInp = () => {
); );
return next; return next;
}); });
if (searchContext) {
setSearchContextByTab((prev) => {
const next = new Map(prev);
next.set(tabKey, searchContext);
return next;
});
}
}; };
const applyRowMap = ( const applyRowMap = (
@@ -690,6 +704,8 @@ const WaterReuseInp = () => {
): Promise<boolean> => { ): Promise<boolean> => {
const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param; const requestParam = overrides?.mode ? { ...param, mode: overrides.mode } : param;
const fromPreYear = overrides?.mode === "PRE"; const fromPreYear = overrides?.mode === "PRE";
const filterFetched = <T,>(list: T[] | undefined) =>
filterBySelectedAdmdstCd(list, sidoListRef.current, searchContext);
const applyNotApplicableForLoadedTab = ( const applyNotApplicableForLoadedTab = (
gridRows: readonly Record<string, string | number>[], gridRows: readonly Record<string, string | number>[],
@@ -721,7 +737,7 @@ const WaterReuseInp = () => {
} }
const { commonInfo, statusInfo, publicSewageTreatWaterReuseList } = response.data; const { commonInfo, statusInfo, publicSewageTreatWaterReuseList } = response.data;
const gridRows = mapPublicSewageTreatWaterReuseListToGridRows( const gridRows = mapPublicSewageTreatWaterReuseListToGridRows(
(publicSewageTreatWaterReuseList ?? []) as PublicSewageTreatWaterReuseItem[], filterFetched(publicSewageTreatWaterReuseList) as PublicSewageTreatWaterReuseItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -732,6 +748,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows); applyNotApplicableForLoadedTab(gridRows);
@@ -749,7 +768,7 @@ const WaterReuseInp = () => {
} }
const { commonInfo, statusInfo, recycledWaterList } = response.data; const { commonInfo, statusInfo, recycledWaterList } = response.data;
const gridRows = mapRecycledWaterStatusListToGridRows( const gridRows = mapRecycledWaterStatusListToGridRows(
(recycledWaterList ?? []) as RecycledWaterStatusItem[], filterFetched(recycledWaterList) as RecycledWaterStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -760,6 +779,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows); applyNotApplicableForLoadedTab(gridRows);
@@ -777,7 +799,7 @@ const WaterReuseInp = () => {
} }
const { commonInfo, statusInfo, rainFallUtilFactStatusList } = response.data; const { commonInfo, statusInfo, rainFallUtilFactStatusList } = response.data;
const gridRows = mapRainFallUtilFactStatusListToGridRows( const gridRows = mapRainFallUtilFactStatusListToGridRows(
(rainFallUtilFactStatusList ?? []) as RainFallUtilFactStatusItem[], filterFetched(rainFallUtilFactStatusList) as RainFallUtilFactStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -788,6 +810,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows); applyNotApplicableForLoadedTab(gridRows);
@@ -806,8 +831,7 @@ const WaterReuseInp = () => {
} }
const { commonInfo, statusInfo, treatWaterReuseDesignConstStatusList } = response.data; const { commonInfo, statusInfo, treatWaterReuseDesignConstStatusList } = response.data;
const gridRows = mapTreatWaterReuseStatusListToGridRows( const gridRows = mapTreatWaterReuseStatusListToGridRows(
(treatWaterReuseDesignConstStatusList ?? filterFetched(treatWaterReuseDesignConstStatusList) as TreatWaterReuseDesignConstStatusItem[],
[]) as TreatWaterReuseDesignConstStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -818,6 +842,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows); applyNotApplicableForLoadedTab(gridRows);
@@ -837,8 +864,7 @@ const WaterReuseInp = () => {
const { commonInfo, statusInfo, treatWaterReuseBusinessApprStatusList } = const { commonInfo, statusInfo, treatWaterReuseBusinessApprStatusList } =
response.data; response.data;
const gridRows = mapTreatWaterReuseStatusListToGridRows( const gridRows = mapTreatWaterReuseStatusListToGridRows(
(treatWaterReuseBusinessApprStatusList ?? filterFetched(treatWaterReuseBusinessApprStatusList) as TreatWaterReuseBusinessApprStatusItem[],
[]) as TreatWaterReuseBusinessApprStatusItem[],
commonInfo.ctpvNm, commonInfo.ctpvNm,
); );
applyRowMap(mergeIntoExistingMap, (m) => { applyRowMap(mergeIntoExistingMap, (m) => {
@@ -849,6 +875,9 @@ const WaterReuseInp = () => {
setIsVerificationFailedByTab((prev) => setIsVerificationFailedByTab((prev) =>
patchVerificationFailedByTab(prev, tabKey, commonInfo), patchVerificationFailedByTab(prev, tabKey, commonInfo),
); );
setFooterVerificationByTab((prev) =>
patchFooterVerificationByTab(prev, tabKey, commonInfo),
);
if (updateLastSearchedValues) setLastSearchedValues(searchContext); if (updateLastSearchedValues) setLastSearchedValues(searchContext);
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext); commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, gridRows.length > 0, searchContext);
applyNotApplicableForLoadedTab(gridRows); applyNotApplicableForLoadedTab(gridRows);
@@ -906,7 +935,9 @@ const WaterReuseInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setActiveTab(firstTabKey); setActiveTab(firstTabKey);
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
setLastSearchedValues(null); setLastSearchedValues(null);
@@ -953,7 +984,9 @@ const WaterReuseInp = () => {
setTabStatusInfoMap(new Map()); setTabStatusInfoMap(new Map());
setTabStatsCdMap(new Map()); setTabStatsCdMap(new Map());
setIsVerificationFailedByTab(new Map()); setIsVerificationFailedByTab(new Map());
setFooterVerificationByTab(new Map());
setToolbarSnapshotByTab(new Map()); setToolbarSnapshotByTab(new Map());
setSearchContextByTab(new Map());
setDeleteListByTab(new Map()); setDeleteListByTab(new Map());
loadedTabKeysRef.current.clear(); loadedTabKeysRef.current.clear();
activeTabRef.current = firstTabKey; activeTabRef.current = firstTabKey;
@@ -1076,9 +1109,9 @@ const WaterReuseInp = () => {
return; return;
} }
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
if (!noApplicable && currentData.length === 0) { if (!noApplicable && currentData.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1111,11 +1144,7 @@ const WaterReuseInp = () => {
const submitRequestBase = { const submitRequestBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
laePicNm: submitInfo.laePicNm, laePicNm: submitInfo.laePicNm,
laePicTelNo: submitInfo.laePicTelNo, laePicTelNo: submitInfo.laePicTelNo,
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm, laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
@@ -1135,12 +1164,8 @@ const WaterReuseInp = () => {
WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ?? WATER_REUSE_COLUMN_GUIDE_STATS_CD_BY_TAB_KEY[tabKey] ??
tabKey; tabKey;
const verificationQuery = { const verificationQuery = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
statsCd: statsCdForTab, statsCd: statsCdForTab,
}; };
@@ -1170,16 +1195,16 @@ const WaterReuseInp = () => {
let response: { let response: {
status: string; status: string;
message?: string; message?: string;
data?: { finalStatus: string; nvpassed: boolean }; data?: { finalStatus: string; nvpassed: boolean; isHbPassed?: string };
}; };
if (isPublicSewageTab) { if (isPublicSewageTab) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const submitList = noApplicable const submitList = noApplicable
? [] ? []
: currentData.map(buildPublicSewageTreatWaterReuseSaveItem); : currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1191,7 +1216,7 @@ const WaterReuseInp = () => {
submitList, submitList,
}); });
} else { } else {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? ""; const sido = lastSearchedValues.sido ?? "";
@@ -1222,7 +1247,7 @@ const WaterReuseInp = () => {
.filter((row) => isPersistedArticleRow(tabKey, row)) .filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list)); .map((row) => buildRecycledWaterStatusSaveItem(row, list));
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1242,7 +1267,7 @@ const WaterReuseInp = () => {
.filter((row) => isPersistedArticleRow(tabKey, row)) .filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); .map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1264,7 +1289,7 @@ const WaterReuseInp = () => {
buildTreatWaterReuseDesignConstStatusSaveItem(row, list), buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
); );
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1289,7 +1314,7 @@ const WaterReuseInp = () => {
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
); );
if (!noApplicable && submitList.length === 0) { if (!noApplicable && submitList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1318,15 +1343,16 @@ const WaterReuseInp = () => {
if (finalStatus === "5") { if (finalStatus === "5") {
setGlobalProgress(50); setGlobalProgress(50);
setGlobalProgressLabel("IMR 검증 진행 중..."); setGlobalProgressLabel("시계열 검증 진행 중...");
const imrParam = { const imrParam = {
user_id: userInfo.userUnqId, user_id: userInfo.userUnqId,
sewer_bzmn_cd: lastSearchedValues.bsnmCd ?? "", sewer_bzmn_cd: lastSearchedValues?.bsnmCd ?? "",
stats_cd: statsCdForTab, stats_cd: statsCdForTab,
stats_yr: lastSearchedValues.year ?? "", stats_yr: lastSearchedValues.year ?? "",
lae_sbmsn_stts_cd: lae_sbmsn_stts_cd:
activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2", activeTabStatus?.laeSbmsnSttsCd === "4" ? "4" : "2",
gen_verify_stts_cd: nvpassed ? "1" : "2", gen_verify_stts_cd: nvpassed ? "1" : "2",
hb_pass_yn: response.data?.isHbPassed,
}; };
try { try {
@@ -1396,11 +1422,7 @@ const WaterReuseInp = () => {
const requestBase = { const requestBase = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}), ...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
}; };
@@ -1418,12 +1440,12 @@ const WaterReuseInp = () => {
}; };
if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) { if (tabKey === PublicSewageTreatWaterReuseInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const saveList = noApplicable const saveList = noApplicable
? [] ? []
: currentData.map(buildPublicSewageTreatWaterReuseSaveItem); : currentData.map(buildPublicSewageTreatWaterReuseSaveItem);
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1454,7 +1476,7 @@ const WaterReuseInp = () => {
} }
if (tabKey === RecycledWaterStatusInpColumn.tabKey) { if (tabKey === RecycledWaterStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? ""; const sido = lastSearchedValues.sido ?? "";
@@ -1477,7 +1499,7 @@ const WaterReuseInp = () => {
.filter((row) => isPersistedArticleRow(tabKey, row)) .filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRecycledWaterStatusSaveItem(row, list)); .map((row) => buildRecycledWaterStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1514,7 +1536,7 @@ const WaterReuseInp = () => {
} }
if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) { if (tabKey === RainFallUtilFactStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? ""; const sido = lastSearchedValues.sido ?? "";
@@ -1537,7 +1559,7 @@ const WaterReuseInp = () => {
.filter((row) => isPersistedArticleRow(tabKey, row)) .filter((row) => isPersistedArticleRow(tabKey, row))
.map((row) => buildRainFallUtilFactStatusSaveItem(row, list)); .map((row) => buildRainFallUtilFactStatusSaveItem(row, list));
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1575,7 +1597,7 @@ const WaterReuseInp = () => {
} }
if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) { if (tabKey === TreatWaterReuseDesignConstStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? ""; const sido = lastSearchedValues.sido ?? "";
@@ -1600,7 +1622,7 @@ const WaterReuseInp = () => {
buildTreatWaterReuseDesignConstStatusSaveItem(row, list), buildTreatWaterReuseDesignConstStatusSaveItem(row, list),
); );
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1640,7 +1662,7 @@ const WaterReuseInp = () => {
} }
if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) { if (tabKey === TreatWaterReuseBusinessApprStatusInpColumn.tabKey) {
const noApplicable = isNotApplicableByTab.get(tabKey) ?? false; const noApplicable = buildNoApplicableForRequest(searchContextByTab.get(tabKey) ?? lastSearchedValues, isNotApplicableByTab.get(tabKey) ?? false, getTabGridRows(tabKey).length === 0);
const currentData = getTabGridRows(tabKey); const currentData = getTabGridRows(tabKey);
const sido = lastSearchedValues.sido ?? ""; const sido = lastSearchedValues.sido ?? "";
@@ -1665,7 +1687,7 @@ const WaterReuseInp = () => {
buildTreatWaterReuseBusinessApprStatusSaveItem(row, list), buildTreatWaterReuseBusinessApprStatusSaveItem(row, list),
); );
if (!noApplicable && saveList.length === 0) { if (!noApplicable && saveList.length === 0 && buildSubmitOkForToolbar(searchContextByTab.get(tabKey) ?? lastSearchedValues)) {
alert("해당사항 없음을 체크해주세요."); alert("해당사항 없음을 체크해주세요.");
return; return;
} }
@@ -1719,11 +1741,7 @@ const WaterReuseInp = () => {
const request = { const request = {
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm: ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
userId: userInfo.userUnqId, userId: userInfo.userUnqId,
}; };
@@ -1881,12 +1899,8 @@ const WaterReuseInp = () => {
} }
const requestData: cancelSubmitRequest = { const requestData: cancelSubmitRequest = {
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "", ...toInputSearchRegion(sidoListRef.current, lastSearchedValues),
ctpvNm: lastSearchedValues.sido ?? "", ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
statsYr: lastSearchedValues.year ?? "", statsYr: lastSearchedValues.year ?? "",
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
statsCd: statsCdForTab, statsCd: statsCdForTab,
@@ -1924,10 +1938,10 @@ const WaterReuseInp = () => {
for (const tab of WaterReuseInpTabList) { for (const tab of WaterReuseInpTabList) {
const tabKey = tab.tabKey; const tabKey = tab.tabKey;
const canEdit = const canEdit =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const rawColumns = (tab.columns ?? []) as ColumnConfig[]; const rawColumns = (tab.columns ?? []) as ColumnConfig[];
const base = canEdit const base = canEdit
? rawColumns ? rawColumns
@@ -1963,14 +1977,14 @@ const WaterReuseInp = () => {
return map; return map;
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]); }, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, columnGuidesByTab, sggOptions]);
const searchContextForToolbar = searchContextByTab.get(activeTab) ?? lastSearchedValues;
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null; const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
const activeTabToolbar = const activeTabToolbar =
toolbarSnapshotByTab.get(activeTab) ??
computeTabToolbarSnapshot( computeTabToolbarSnapshot(
activeTabStatus, activeTabStatus,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
hasSearched, hasSearched,
lastSearchedValues, searchContextForToolbar,
inputPeriodEnterable, inputPeriodEnterable,
isAdminInfo, isAdminInfo,
); );
@@ -1981,6 +1995,7 @@ const WaterReuseInp = () => {
activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed; activeTabToolbar.showEnterableToolbarButtons && toolbarGroupAllowed;
const showActionToolbarCard = const showActionToolbarCard =
activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed; activeTabToolbar.showActionToolbarCard && toolbarGroupAllowed;
const submitOk = buildSubmitOkForToolbar(searchContextForToolbar);
const showSecondaryToolbarCard = const showSecondaryToolbarCard =
hasSearched && hasSearched &&
toolbarGroupAllowed && toolbarGroupAllowed &&
@@ -1995,7 +2010,8 @@ const WaterReuseInp = () => {
activeTab, activeTab,
tabRowDataExists(rowDataMap, activeTab), tabRowDataExists(rowDataMap, activeTab),
activeTabStatus, activeTabStatus,
); ) &&
submitOk;
const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback( const setIsNotApplicableForActiveTab: Dispatch<SetStateAction<boolean>> = useCallback(
(next) => { (next) => {
const tabKey = activeTabRef.current; const tabKey = activeTabRef.current;
@@ -2014,9 +2030,8 @@ const WaterReuseInp = () => {
const statsCd = tabStatsCdMap.get(activeTab); const statsCd = tabStatsCdMap.get(activeTab);
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null; if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
return { return {
sewerBzmnCd: ctx.bsnmCd, ...toInputSearchRegion(sidoListRef.current, ctx),
ctpvNm: ctx.sido, ctpvNm: ctx.sido,
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
statsYr: ctx.year, statsYr: ctx.year,
statsCd, statsCd,
}; };
@@ -2053,10 +2068,10 @@ const WaterReuseInp = () => {
.map((e) => { .map((e) => {
const prefix = const prefix =
e.vrfcCd === "IMR" e.vrfcCd === "IMR"
? "IMR검증 - " ? "시계열 검증 - "
: e.vrfcCd === "NV" : e.vrfcCd === "NV"
? "일반검증 - " ? "일반검증 - "
: "HB검증 - "; : "모집단 검증 - ";
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
}) })
.filter(Boolean) .filter(Boolean)
@@ -2079,6 +2094,7 @@ const WaterReuseInp = () => {
<> <>
<div className="relative"> <div className="relative">
<SearchForm <SearchForm
isInput
key={searchFormRemountKey} key={searchFormRemountKey}
defaultValues={verificationSearchDefaults ?? undefined} defaultValues={verificationSearchDefaults ?? undefined}
onSubmit={handleSearch} onSubmit={handleSearch}
@@ -2086,8 +2102,8 @@ const WaterReuseInp = () => {
onValuesChange={setSearchValues} onValuesChange={setSearchValues}
/> />
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<VerificationResultButton <VerificationResultButton
VerificationResultButtonFunc={openVerificationHistoryDialog} VerificationResultButtonFunc={openVerificationHistoryDialog}
isVerificationFailed={ isVerificationFailed={
@@ -2099,19 +2115,19 @@ const WaterReuseInp = () => {
<LastYearDataButton <LastYearDataButton
getLastYearDataFunc={() => void getLastYearDataFunc()} getLastYearDataFunc={() => void getLastYearDataFunc()}
/> />
<SubmitButton submitFunc={submitFunc} /> {submitOk && <SubmitButton submitFunc={submitFunc} />}
</> </>
)} )}
{activeTabStatus?.laeSbmsnSttsCd === "2" && ( {activeTabStatus?.laeSbmsnSttsCd === "2" && submitOk && (
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} /> <CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
)} )}
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />} {showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
</div> </div>
</div> </div>
)} )}
{showSecondaryToolbarCard && ( {showSecondaryToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
{showNotApplicableForActiveTab && ( {showNotApplicableForActiveTab && (
<NotApplicableCheckButton <NotApplicableCheckButton
isCheck={isNotApplicableForActiveTab} isCheck={isNotApplicableForActiveTab}
@@ -2125,12 +2141,12 @@ const WaterReuseInp = () => {
)} )}
{isArticleEditTab && showEnterableToolbarButtons && ( {isArticleEditTab && showEnterableToolbarButtons && (
<> <>
<AddArticleButton addArticleFunc={addArticleFunc} /> <AddArticleButton addArticleFunc={addArticleFunc} />
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} /> <DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
</> </>
)} )}
</div> </div>
</div> </div>
)} )}
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -2157,11 +2173,11 @@ const WaterReuseInp = () => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
const rowData = rowDataMap.get(tabKey) ?? []; const rowData = rowDataMap.get(tabKey) ?? [];
const tabEnterableForGrid = const tabEnterableForGrid =
computeTabEnterable( (computeTabEnterable(
tabStatusInfoMap.get(tabKey) ?? null, tabStatusInfoMap.get(tabKey) ?? null,
inputPeriodEnterable, inputPeriodEnterable,
) || isAdminInfo; ) || isAdminInfo) && inputPeriodEnterable;
const gugunOk = buildGugunOkForToolbar(lastSearchedValues); const gugunOk = buildGugunOkForToolbar(searchContextForToolbar);
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null; const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
return ( return (
@@ -2202,6 +2218,9 @@ const WaterReuseInp = () => {
excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)} excelExportSearchContext={toExcelExportSearchContextFromSearchForm(lastSearchedValues ?? searchValues)}
isExcelExport isExcelExport
cellStyleCallback={verificationCellStyle} cellStyleCallback={verificationCellStyle}
footerVerificationResult={
footerVerificationByTab.get(tabKey) ?? null
}
cellTooltipCallback={verificationCellTooltip} cellTooltipCallback={verificationCellTooltip}
getGridDataRef={ getGridDataRef={
activeTab === tabKey ? getGridDataRef : undefined activeTab === tabKey ? getGridDataRef : undefined
@@ -1,15 +1,15 @@
import { Button } from "@common/components"; import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import {
AdmdstRenewalReducedColumnConfig,
AdmdstRenewalAddedColumnConfig,
} from "@/components/util/column/management/AdmdstRenewalColumn";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog"; import DataGrid from "@/components/module/DataGrid";
import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab"; import type { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast"; import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types"; import type { AdmdstRenewalAddedItem } from "@/components/statistics/management/types/population-tab-types";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import {
AdmdstRenewalAddedColumnConfig,
AdmdstRenewalReducedColumnConfig,
} from "@/components/util/column/management/AdmdstRenewalColumn";
const reducedColumnInfo: ColumnInfo = { const reducedColumnInfo: ColumnInfo = {
columns: AdmdstRenewalReducedColumnConfig.columns ?? [], columns: AdmdstRenewalReducedColumnConfig.columns ?? [],
@@ -1,17 +1,19 @@
import { Button } from "@common/components"; import { useRef } from "react";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type"; import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type";
import { import {
DomesticPopulationLeftColumnConfig, DomesticPopulationLeftColumnConfig,
DomesticPopulationRightColumnConfig, DomesticPopulationRightColumnConfig,
} from "@/components/util/column/management/DomesticPopulationColumn"; } from "@/components/util/column/management/DomesticPopulationColumn";
import { useRef } from "react";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
const leftColumnInfo: ColumnInfo = { const leftColumnInfo: ColumnInfo = {
@@ -96,7 +98,7 @@ const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
enableSort={false} enableSort={false}
getGridRef={rightGridRef} // ← 추가 getGridRef={rightGridRef} // ← 추가
onDataCellClick={({ row, fieldName }) => { onDataCellClick={({ row, fieldName }) => {
if (fieldName === "mpinCd") return; if (fieldName === "sbstAdmdstCd") return;
if (fieldName === "admdstCd") { if (fieldName === "admdstCd") {
const gridView = rightGridRef.current?.gridView as const gridView = rightGridRef.current?.gridView as
| GridViewWithEditCommit | GridViewWithEditCommit
@@ -106,7 +108,7 @@ const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
tab.setRightRowData((prev) => tab.setRightRowData((prev) =>
prev.map((r) => prev.map((r) =>
r.admdstCd === row.admdstCd r.admdstCd === row.admdstCd
? { ...r, mpinCd: String(row.admdstCd ?? "") } ? { ...r, sbstAdmdstCd: String(row.admdstCd ?? "") }
: r, : r,
), ),
); );
@@ -115,7 +117,7 @@ const DomesticPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
} }
}} }}
onCellDblClick={({ row, fieldName }) => { onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "mpinCd") return; if (fieldName !== "sbstAdmdstCd") return;
void tab.handleRightRowClick(fromGridRow<DomesticPopulationItem>(row), statsYr); void tab.handleRightRowClick(fromGridRow<DomesticPopulationItem>(row), statsYr);
}} }}
/> />
@@ -1,18 +1,20 @@
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type";
import { useRef } from "react"; import { useRef } from "react";
import type { RealGridReact } from "realgrid-react"; import type { RealGridReact } from "realgrid-react";
import { Button } from "@common/components";
import DataGrid from "@/components/module/DataGrid";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { ColumnInfo, GridViewWithEditCommit } from "@/components/type/DataGrid.type";
import { import {
ForeignPopulationLeftColumnConfig, ForeignPopulationLeftColumnConfig,
ForeignPopulationRightColumnConfig, ForeignPopulationRightColumnConfig,
} from "@/components/util/column/management/ForeignPopulationColumn"; } from "@/components/util/column/management/ForeignPopulationColumn";
import { UpperCodeSelectDialog } from "@/components/statistics/management/UpperCodeSelectDialog";
import type { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { asGridRowData, asGridRowSetter, fromGridRow } from "@/components/statistics/management/lib/gridCast";
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
const leftColumnInfo: ColumnInfo = { const leftColumnInfo: ColumnInfo = {
columns: ForeignPopulationLeftColumnConfig.columns ?? [], columns: ForeignPopulationLeftColumnConfig.columns ?? [],
columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [], columnLayout: ForeignPopulationLeftColumnConfig.columnLayout ?? [],
@@ -95,7 +97,7 @@ const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
enableSort={false} enableSort={false}
getGridRef={rightGridRef} getGridRef={rightGridRef}
onDataCellClick={({ row, fieldName }) => { onDataCellClick={({ row, fieldName }) => {
if (fieldName === "mpinCd") return; if (fieldName === "sbstAdmdstCd") return;
if (fieldName === "admdstCd") { if (fieldName === "admdstCd") {
const gridView = rightGridRef.current?.gridView as const gridView = rightGridRef.current?.gridView as
| GridViewWithEditCommit | GridViewWithEditCommit
@@ -105,7 +107,7 @@ const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
tab.setRightRowData((prev) => tab.setRightRowData((prev) =>
prev.map((r) => prev.map((r) =>
r.admdstCd === row.admdstCd r.admdstCd === row.admdstCd
? { ...r, mpinCd: String(row.admdstCd ?? "") } ? { ...r, sbstAdmdstCd: String(row.admdstCd ?? "") }
: r, : r,
), ),
); );
@@ -114,7 +116,7 @@ const ForeignPopulationTab = ({ tab, statsYr, onFileButtonClick }: Props) => {
} }
}} }}
onCellDblClick={({ row, fieldName }) => { onCellDblClick={({ row, fieldName }) => {
if (fieldName !== "mpinCd") return; if (fieldName !== "sbstAdmdstCd") return;
void tab.handleRightRowClick(fromGridRow<ForeignPopulationItem>(row), statsYr); void tab.handleRightRowClick(fromGridRow<ForeignPopulationItem>(row), statsYr);
}} }}
/> />
@@ -130,9 +130,9 @@ const getSidoListFetcher = async (): Promise<ApiResponse<SearchCondition>> => {
}; };
const getSggListFetcher = async ( const getSggListFetcher = async (
key: [string, string], key: [string, string, "Y" | "N"],
): Promise<ApiResponse<Sgg[]>> => { ): Promise<ApiResponse<Sgg[]>> => {
return statisticsApi.getSggList(key[1]); return statisticsApi.getSggList(key[1], { gugunSkipYn: key[2] });
}; };
const LogHistory = () => { const LogHistory = () => {
@@ -214,8 +214,14 @@ const LogHistory = () => {
}, },
); );
const sggKey: [string, string] | null = const selectedSido = sidoList?.data?.sidoList?.find(
sido && sido !== "ALL" ? ["log-history-sgg-list", sido] : null; (item) => item.sido === sido,
);
const gugunSkipYn = selectedSido?.gugunSkipYn ?? "Y";
const sggKey: [string, string, "Y" | "N"] | null =
sido && sido !== "ALL" && sidoList?.data
? ["log-history-sgg-list", sido, gugunSkipYn]
: null;
const { data: sggList } = useSWR<ApiResponse<Sgg[]>>( const { data: sggList } = useSWR<ApiResponse<Sgg[]>>(
sggKey, sggKey,
getSggListFetcher, getSggListFetcher,
@@ -1,8 +1,12 @@
"use client"; "use client";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { GridFitStyle } from "realgrid"; import { GridFitStyle } from "realgrid";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { UserApi } from "@/entities/user";
import { import {
Button, Button,
Dialog, Dialog,
@@ -14,6 +18,8 @@ import {
LoadingWithAnimation, LoadingWithAnimation,
} from "@common/components"; } from "@common/components";
import { LogHistoryDetailsNestedDialog } from "./LogHistoryDetailsNestedDialog";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import type { import type {
ColumnInfo, ColumnInfo,
@@ -25,15 +31,10 @@ import {
emptyLogHistoryDetailColumnInfo, emptyLogHistoryDetailColumnInfo,
LOG_DETAIL_DETAILS_FIELD, LOG_DETAIL_DETAILS_FIELD,
LOG_DETAIL_DETAILS_RAW_FIELD, LOG_DETAIL_DETAILS_RAW_FIELD,
normalizeLogDetailList,
type LogDetailVerificationItem, type LogDetailVerificationItem,
normalizeLogDetailList,
prepareLogDetailRowsForGrid, prepareLogDetailRowsForGrid,
} from "@/components/util/column/management/LogHistoryDetailColumn"; } from "@/components/util/column/management/LogHistoryDetailColumn";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { UserApi } from "@/entities/user";
import { useAlertStore } from "@/shared/lib";
import { LogHistoryDetailsNestedDialog } from "./LogHistoryDetailsNestedDialog";
export interface LogHistoryDetailDialogProps { export interface LogHistoryDetailDialogProps {
open: boolean; open: boolean;
@@ -5,8 +5,8 @@ import { GridFitStyle } from "realgrid";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
UnSubmitDataStatusItem,
unSubmitDataRequest, unSubmitDataRequest,
UnSubmitDataStatusItem,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation } from "@common/components"; import { LoadingWithAnimation } from "@common/components";
@@ -279,7 +279,6 @@ const NotEnteredStatus = () => {
return; return;
} }
const params = buildUnSubmitDataRequest(values, userInfo); const params = buildUnSubmitDataRequest(values, userInfo);
console.log(params);
if (!params.dtlInstCd) { if (!params.dtlInstCd) {
alert("기관 정보를 확인할 수 없습니다."); alert("기관 정보를 확인할 수 없습니다.");
return; return;
@@ -1,18 +1,19 @@
import { Chart } from "react-chartjs-2"; import { Chart } from "react-chartjs-2";
import type { ChartOptions } from "chart.js";
import { import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarController, BarController,
BarElement, BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineController, LineController,
LineElement, LineElement,
PointElement, PointElement,
Title, Title,
Tooltip, Tooltip,
Legend,
} from "chart.js"; } from "chart.js";
import type { ChartOptions } from "chart.js";
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
@@ -1,9 +1,8 @@
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import { PopulationStatusColumnConfig } from "@/components/util/column/management/PopulationStatusColumn";
import type { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab"; import type { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
import { asGridRowData, asGridRowSetter } from "@/components/statistics/management/lib/gridCast"; import { asGridRowData, asGridRowSetter } from "@/components/statistics/management/lib/gridCast";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import { PopulationStatusColumnConfig } from "@/components/util/column/management/PopulationStatusColumn";
const columnInfo: ColumnInfo = { const columnInfo: ColumnInfo = {
columns: PopulationStatusColumnConfig.columns ?? [], columns: PopulationStatusColumnConfig.columns ?? [],
@@ -1,4 +1,5 @@
import PopulationStatusChart from "./PopulationStatusChart"; import PopulationStatusChart from "./PopulationStatusChart";
import type { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab"; import type { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
type Props = { type Props = {
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -115,6 +115,14 @@ const StatisticsInputManagement = () => {
setRowData([]); setRowData([]);
}; };
useEffect(() => {
const defaultYear = String(new Date().getFullYear() - 1);
void handleSearch({
year: defaultYear,
clsfCd: "ALL",
});
}, []);
const handleRegisterClick = () => { const handleRegisterClick = () => {
const yr = const yr =
searchFormValues?.year ?? lastSearchRef.current?.year ?? ""; searchFormValues?.year ?? lastSearchRef.current?.year ?? "";
@@ -1,23 +1,22 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; import type { DomesticPopulationItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import { Button, LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab"; import AdmdstRenewalTab from "./AdmdstRenewalTab";
import { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
import { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
import DomesticPopulationTab from "./DomesticPopulationTab"; import DomesticPopulationTab from "./DomesticPopulationTab";
import ForeignPopulationTab from "./ForeignPopulationTab"; import ForeignPopulationTab from "./ForeignPopulationTab";
import AdmdstRenewalTab from "./AdmdstRenewalTab";
import PopulationStatusTab from "./PopulationStatusTab"; import PopulationStatusTab from "./PopulationStatusTab";
import PopulationVizTab from "./PopulationVizTab"; import PopulationVizTab from "./PopulationVizTab";
import SearchForm from "@/components/module/SearchForm";
import { useAdmdstRenewalTab } from "@/components/statistics/management/hooks/useAdmdstRenewalTab";
import { usePopulationMappingTab } from "@/components/statistics/management/hooks/usePopulationMappingTab";
import { usePopulationStatusTab } from "@/components/statistics/management/hooks/usePopulationStatusTab";
import { usePopulationVizTab } from "@/components/statistics/management/hooks/usePopulationVizTab";
import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types"; import type { ForeignPopulationItem } from "@/components/statistics/management/types/population-tab-types";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
const TAB_LIST = [ const TAB_LIST = [
{ key: "domestic", label: "행정인구등록" }, { key: "domestic", label: "행정인구등록" },
@@ -1,4 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Button, Button,
Dialog, Dialog,
@@ -2,7 +2,8 @@ import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { toGridRows } from "@/components/util/grid/gridFunction"; import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
// TODO: 원본 파일은 `../../../../../../packages/common/components/src/api` (6단계 상위) 였음. // TODO: 원본 파일은 `../../../../../../packages/common/components/src/api` (6단계 상위) 였음.
// 이 훅 파일은 원본 컴포넌트보다 한 단계 더 깊은 폴더(hooks/)에 위치하므로 // 이 훅 파일은 원본 컴포넌트보다 한 단계 더 깊은 폴더(hooks/)에 위치하므로
// 실제 프로젝트에 옮길 때 상대경로 깊이를 다시 맞춰줘야 함 (혹은 @common/components에 getUserInfo가 // 실제 프로젝트에 옮길 때 상대경로 깊이를 다시 맞춰줘야 함 (혹은 @common/components에 getUserInfo가
@@ -10,13 +11,14 @@ import { toGridRows } from "@/components/util/grid/gridFunction";
import { getUserInfo } from "../../../../../../../packages/common/components/src/api"; import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
import { useAsyncAction } from "./useAsyncAction"; import { useAsyncAction } from "./useAsyncAction";
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import type { import type {
AdmdstRenewalAddedItem, AdmdstRenewalAddedItem,
AdmdstRenewalAddedSaveItem, AdmdstRenewalAddedSaveItem,
AdmdstRenewalReducedItem, AdmdstRenewalReducedItem,
AdmdstRenewalReducedSaveItem, AdmdstRenewalReducedSaveItem,
} from "@/components/statistics/management/types/population-tab-types"; } from "@/components/statistics/management/types/population-tab-types";
import { toGridRows } from "@/components/util/grid/gridFunction";
export function useAdmdstRenewalTab() { export function useAdmdstRenewalTab() {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -1,15 +1,17 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import * as XLSX from "xlsx"; import * as XLSX from "xlsx";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAsyncAction } from "./useAsyncAction"; import { useAsyncAction } from "./useAsyncAction";
import type { import type {
PopulationMappingApiConfig, PopulationMappingApiConfig,
PopulationMappingRow, PopulationMappingRow,
UpperCodeCandidateItem, UpperCodeCandidateItem,
} from "@/components/statistics/management/types/population-tab-types"; } from "@/components/statistics/management/types/population-tab-types";
import { toGridRows } from "@/components/util/grid/gridFunction";
/* ── 엑셀 헤더 검증 (원본 컴포넌트의 validateExcelHeader를 그대로 이동) ── */ /* ── 엑셀 헤더 검증 (원본 컴포넌트의 validateExcelHeader를 그대로 이동) ── */
const validateExcelHeader = (file: File, requiredKeywords: string[]): Promise<boolean> => { const validateExcelHeader = (file: File, requiredKeywords: string[]): Promise<boolean> => {
@@ -124,12 +126,12 @@ export function usePopulationMappingTab<TRow extends PopulationMappingRow>(
await run(async () => { await run(async () => {
const regList = rightRowData const regList = rightRowData
.filter((row) => String(row.mpinCd ?? "").trim() !== "") .filter((row) => String(row.sbstAdmdstCd ?? "").trim() !== "")
.map((row) => ({ .map((row) => ({
statsYr, statsYr,
admdstCd: String(row.admdstCd ?? ""), admdstCd: String(row.admdstCd ?? ""),
admdstNm: String(row.admdstNm ?? ""), admdstNm: String(row.admdstNm ?? ""),
subAdmdstCd: String(row.mpinCd ?? ""), sbstAdmdstCd: String(row.sbstAdmdstCd ?? ""),
})); }));
const res = await config.saveFinal(statsYr, regList); const res = await config.saveFinal(statsYr, regList);
@@ -165,7 +167,7 @@ const handleRightRowClick = async (rowData: TRow, statsYr: string) => {
const rowIndex = selectedRowIndexRef.current; const rowIndex = selectedRowIndexRef.current;
if (rowIndex === null) return; if (rowIndex === null) return;
setRightRowData((prev) => setRightRowData((prev) =>
prev.map((row, idx) => (idx === rowIndex ? { ...row, mpinCd: item["admdstCd"] ?? "" } : row)), prev.map((row, idx) => (idx === rowIndex ? { ...row, sbstAdmdstCd: item["admdstCd"] ?? "" } : row)),
); );
selectedRowIndexRef.current = null; selectedRowIndexRef.current = null;
}; };
@@ -2,10 +2,12 @@ import { useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { toGridRows } from "@/components/util/grid/gridFunction";
import type { PopulationStatusItem } from "../types/population-tab-types";
import { useAsyncAction } from "./useAsyncAction"; import { useAsyncAction } from "./useAsyncAction";
import type { PopulationStatusItem } from "../types/population-tab-types";
import { toGridRows } from "@/components/util/grid/gridFunction";
export function usePopulationStatusTab() { export function usePopulationStatusTab() {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -3,9 +3,10 @@ import { useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service"; import { statisticsPpltnApi } from "@/entities/statistics/api/statistics-ppltn-service";
import { useAsyncAction } from "./useAsyncAction";
import type { PopulationChartData } from "../PopulationStatusChart"; import type { PopulationChartData } from "../PopulationStatusChart";
import { useAsyncAction } from "./useAsyncAction";
export function usePopulationVizTab() { export function usePopulationVizTab() {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const { isLoading, run } = useAsyncAction(alert); const { isLoading, run } = useAsyncAction(alert);
@@ -1,5 +1,5 @@
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface"; import type { UpperCodeCandidateItem } from "@/entities/statistics/model/ppltn/statistics-domestic-population-interface";
import type { ApiResponse } from "@/entities/statistics/model/statistics-interface";
/* ══════════════════════════════════════════════════ /* ══════════════════════════════════════════════════
외국인 인구 관련 타입 외국인 인구 관련 타입
@@ -14,7 +14,7 @@ export interface ForeignPopulationItem {
frgnrMlPpltnCnt: number; frgnrMlPpltnCnt: number;
frgnrFmlPpltnCnt: number; frgnrFmlPpltnCnt: number;
/** 미매핑 리스트(우측) 전용 - 매핑 대상 행정구역코드(상위코드) */ /** 미매핑 리스트(우측) 전용 - 매핑 대상 행정구역코드(상위코드) */
mpinCd: string; sbstAdmdstCd: string;
} }
export type ForeignPopulationListResponse = ApiResponse<ForeignPopulationItem[]>; export type ForeignPopulationListResponse = ApiResponse<ForeignPopulationItem[]>;
@@ -88,7 +88,7 @@ export interface PopulationMappingRegItem {
statsYr: string; statsYr: string;
admdstCd: string; admdstCd: string;
admdstNm: string; admdstNm: string;
subAdmdstCd: string; sbstAdmdstCd: string;
} }
/** 좌/우 그리드 행이 공통으로 가져야 하는 최소 필드 */ /** 좌/우 그리드 행이 공통으로 가져야 하는 최소 필드 */
@@ -96,7 +96,7 @@ export interface PopulationMappingRow {
admdstCd: string; admdstCd: string;
admdstNm: string; admdstNm: string;
/** 우측(미매핑 리스트) 전용 필드. 좌측 데이터에는 존재하지 않을 수 있음 */ /** 우측(미매핑 리스트) 전용 필드. 좌측 데이터에는 존재하지 않을 수 있음 */
mpinCd?: string; sbstAdmdstCd?: string;
} }
/** usePopulationMappingTab에 주입하는 API/검증 설정 - 내국인/외국인 탭이 이 설정만 다르게 넘김 */ /** usePopulationMappingTab에 주입하는 API/검증 설정 - 내국인/외국인 탭이 이 설정만 다르게 넘김 */
@@ -1,9 +1,8 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
ExcretaTreatmentFacilityReviewItem, ExcretaTreatmentFacilityReviewItem,
} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; } from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface";
@@ -11,28 +10,29 @@ import {
extractExcretaTreatmentFacilityReviewList, extractExcretaTreatmentFacilityReviewList,
extractExcretaTreatmentFacilityReviewStatsStatusInfo, extractExcretaTreatmentFacilityReviewStatsStatusInfo,
} from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface"; } from "@/entities/statistics/model/statistics-excreta-treatment-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { import type {
ReviewOpinionSaveRequest,
ReviewerInfoRequest, ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { LoadingWithAnimation } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn"; import { ExcretaTreatmentFacilityInfoList } from "@/components/util/column/review/ExcretaTreatmentFacilityRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0]; const excretaTreatmentFacilityTab = ExcretaTreatmentFacilityInfoList[0];
@@ -3,6 +3,10 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { import {
extractPrivateSewageFacilityReviewList, extractPrivateSewageFacilityReviewList,
extractPrivateSewageFacilityReviewStatsStatusInfo, extractPrivateSewageFacilityReviewStatsStatusInfo,
@@ -10,29 +14,25 @@ import {
type PrivateSewageFacilityReviewRequestParams, type PrivateSewageFacilityReviewRequestParams,
} from "@/entities/statistics/model/statistics-private-sewage-facility-review-interface"; } from "@/entities/statistics/model/statistics-private-sewage-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn"; import { PrivateSewageFacilityInfoListRvw } from "@/components/util/column/review/PrivateSewageFacilityColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001"; const firstTabKey = PrivateSewageFacilityInfoListRvw[0]?.tabKey ?? "ST00080001";
@@ -1,32 +1,32 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { PublicSewageFacilityReviewItem } from "@/entities/statistics/model/statistics-public-sewage-facility-review-interface"; import type { PublicSewageFacilityReviewItem } from "@/entities/statistics/model/statistics-public-sewage-facility-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { import { LoadingWithAnimation } from "@common/components";
ReviewOpinionSaveRequest,
ReviewerInfoRequest, import ReturnButton from "@/components/button/ReturnButton";
} from "@/entities/statistics/model/statistics-interface"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import SubmitButton from "@/components/button/SumitButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
// 하수도 통계 관리 06.공공하수처리시설 // 하수도 통계 관리 06.공공하수처리시설
const PublicSewageFacilityRvw = () => { const PublicSewageFacilityRvw = () => {
@@ -1,36 +1,36 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { LoadingWithAnimation } from "@common/components";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { import {
extractMediationPumpStationList, extractMediationPumpStationList,
extractMediationPumpStationStatsStatusInfo, extractMediationPumpStationStatsStatusInfo,
type MediationPumpStationItem, type MediationPumpStationItem,
} from "@/entities/statistics/model/statistics-pumping-station-review-interface"; } from "@/entities/statistics/model/statistics-pumping-station-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { import { LoadingWithAnimation } from "@common/components";
ReviewOpinionSaveRequest,
ReviewerInfoRequest,
} from "@/entities/statistics/model/statistics-interface";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn"; import { PumpingStationInfoList } from "@/components/util/column/review/PumpingStationRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
/** /**
* 하수도 통계 검토 05.중계펌프장 * 하수도 통계 검토 05.중계펌프장
@@ -1,9 +1,12 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { ReviewReturnSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { import {
Button, Button,
Dialog, Dialog,
@@ -16,9 +19,7 @@ import {
Textarea, Textarea,
} from "@common/components"; } from "@common/components";
import type { ReviewReturnSaveRequest } from "@/entities/statistics/model/statistics-interface"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { useAlertStore } from "@/shared/lib";
export type ReturnReasonDialogContext = Pick< export type ReturnReasonDialogContext = Pick<
ReviewReturnSaveRequest, ReviewReturnSaveRequest,
@@ -2,6 +2,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface";
import { import {
Button, Button,
Dialog, Dialog,
@@ -13,10 +15,8 @@ import {
Label, Label,
Textarea, Textarea,
} from "@common/components"; } from "@common/components";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useAlertStore } from "@/shared/lib";
export type ReviewOpinionDialogContext = Pick< export type ReviewOpinionDialogContext = Pick<
ReviewOpinionSaveRequest, ReviewOpinionSaveRequest,
@@ -103,7 +103,7 @@ export function ReviewOpinionDialog({
resetForm(); resetForm();
onOpenChange(false); onOpenChange(false);
} catch { } catch {
/* 저장 실패 시 부모에서 alert 후 throw — 다이얼로그는 유지 */ console.error('검토 의견 저장 error');
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@@ -1,8 +1,14 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import { useAlertStore } from "@/shared/lib";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewerInfoResponse,
ReviewSubmitRequest,
} from "@/entities/statistics/model/statistics-interface";
import { import {
Button, Button,
Dialog, Dialog,
@@ -16,19 +22,18 @@ import {
Textarea, Textarea,
} from "@common/components"; } from "@common/components";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import type {
ReviewerInfoRequest,
ReviewerInfoResponse,
ReviewSubmitRequest,
} from "@/entities/statistics/model/statistics-interface";
import { useAlertStore } from "@/shared/lib";
export interface ReviewSubmitDialogProps { export interface ReviewSubmitDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
context: ReviewerInfoRequest | null; context: ReviewerInfoRequest | null;
onSubmitted?: () => void | Promise<void>; onSubmitted?: () => void | Promise<void>;
/**
* true면 검토의견·처리내역 UI를 숨기고
* submit body에 statsCd / splmntOpnnCn / wrterOpnnCn을 넣지 않음
*/
hideOpinionFields?: boolean;
} }
export function ReviewSubmitDialog({ export function ReviewSubmitDialog({
@@ -36,6 +41,7 @@ export function ReviewSubmitDialog({
onOpenChange, onOpenChange,
context, context,
onSubmitted, onSubmitted,
hideOpinionFields = false,
}: ReviewSubmitDialogProps) { }: ReviewSubmitDialogProps) {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const [reviewerInfo, setReviewerInfo] = const [reviewerInfo, setReviewerInfo] =
@@ -98,14 +104,20 @@ export function ReviewSubmitDialog({
setIsSubmitting(true); setIsSubmitting(true);
const userInfo = await getUserInfo(); const userInfo = await getUserInfo();
const submitParam: ReviewSubmitRequest = { const submitParam: ReviewSubmitRequest = hideOpinionFields
statsYr: context.statsYr, ? {
sewerBzmnCd: context.sewerBzmnCd, statsYr: context.statsYr,
statsCd: context.statsCd, sewerBzmnCd: [context.sewerBzmnCd],
mdfrId: userInfo.userUnqId, mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "", }
wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "", : {
}; statsYr: context.statsYr,
sewerBzmnCd: context.sewerBzmnCd,
statsCd: context.statsCd,
mdfrId: userInfo.userUnqId,
splmntOpnnCn: reviewerInfo?.splmntOpnnCn ?? "",
wrterOpnnCn: reviewerInfo?.wrterOpnnCn ?? "",
};
const response = await statisticsApi.submitReview(submitParam); const response = await statisticsApi.submitReview(submitParam);
if (response.status !== "success") { if (response.status !== "success") {
@@ -153,22 +165,26 @@ export function ReviewSubmitDialog({
<Label className="text-label">직책</Label> <Label className="text-label">직책</Label>
<Input value={reviewerInfo?.cps ?? "-"} readOnly /> <Input value={reviewerInfo?.cps ?? "-"} readOnly />
</div> </div>
<div className="col-span-2 flex flex-col gap-2"> {!hideOpinionFields && (
<Label className="text-label">검토의견</Label> <>
<Textarea <div className="col-span-2 flex flex-col gap-2">
value={reviewerInfo?.splmntOpnnCn ?? "-"} <Label className="text-label">검토의견</Label>
readOnly <Textarea
className="min-h-[10rem] p-2 resize-none !text-xl" value={reviewerInfo?.splmntOpnnCn ?? "-"}
/> readOnly
</div> className="min-h-[10rem] p-2 resize-none !text-xl"
<div className="col-span-2 flex flex-col gap-2"> />
<Label className="text-label">처리내역</Label> </div>
<Textarea <div className="col-span-2 flex flex-col gap-2">
value={reviewerInfo?.wrterOpnnCn ?? "-"} <Label className="text-label">처리내역</Label>
readOnly <Textarea
className="min-h-[10rem] p-2 resize-none !text-xl" value={reviewerInfo?.wrterOpnnCn ?? "-"}
/> readOnly
</div> className="min-h-[10rem] p-2 resize-none !text-xl"
/>
</div>
</>
)}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
@@ -3,6 +3,10 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { import {
extractSewageExcretaRelatedReviewList, extractSewageExcretaRelatedReviewList,
extractSewageExcretaRelatedReviewStatsStatusInfo, extractSewageExcretaRelatedReviewStatsStatusInfo,
@@ -10,29 +14,25 @@ import {
type SewageExcretaRelatedReviewRequestParams, type SewageExcretaRelatedReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewage-excreta-related-review-interface"; } from "@/entities/statistics/model/statistics-sewage-excreta-related-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn"; import { SewageExcretaRelatedInfoListRvw } from "@/components/util/column/review/SewageExcretaRelatedRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001"; const firstTabKey = SewageExcretaRelatedInfoListRvw[0]?.tabKey ?? "ST00120001";
@@ -3,6 +3,10 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { import {
extractSewageExcretaSlurgeReviewList, extractSewageExcretaSlurgeReviewList,
extractSewageExcretaSlurgeReviewStatsStatusInfo, extractSewageExcretaSlurgeReviewStatsStatusInfo,
@@ -10,29 +14,25 @@ import {
type SewageExcretaSlurgeReviewRequestParams, type SewageExcretaSlurgeReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface"; } from "@/entities/statistics/model/statistics-sewage-excreta-slurge-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn"; import { SewageExcretaSludgeInfoListRvw } from "@/components/util/column/review/SewageExcretaSlurgeRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001"; const firstTabKey = SewageExcretaSludgeInfoListRvw[0]?.tabKey ?? "ST00110001";
@@ -3,33 +3,33 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewerPipeMtcRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
import type { import type {
ReviewerInfoRequest, ReviewerInfoRequest,
ReviewOpinionSaveRequest, ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type {
SewerPipeMtcRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-mtc-review-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn"; import { SewagePipeMaintainColumnInfoList } from "@/components/util/column/review/SewagePipeMaintainRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001"; const firstTabKey = SewagePipeMaintainColumnInfoList[0]?.tabKey ?? "ST00030001";
@@ -3,27 +3,28 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
SewagePipeReviewRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { import type {
ReviewerInfoRequest, ReviewerInfoRequest,
ReviewOpinionSaveRequest, ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type {
SewagePipeReviewRequestParams,
StatsStatusInfo,
} from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { import type {
ColumnConfig, ColumnConfig,
ColumnInfo, ColumnInfo,
@@ -31,10 +32,9 @@ import type {
ColumnLayoutItem, ColumnLayoutItem,
} from "@/components/type/DataGrid.type"; } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn"; import { getST00020003RvwConfig,SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001"; const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001";
@@ -148,15 +148,23 @@ const SewagePipeRvw = () => {
[tabStatsStatusInfoMap, activeTab], [tabStatsStatusInfoMap, activeTab],
); );
const columnInfo = useMemo<ColumnInfoList[]>( const columnInfo = useMemo<ColumnInfoList[]>(
() => () => {
SewagePipeColumnInfoListRvw.map((tab) => ({ const statsYr = lastSearchValues?.year ?? new Date().getFullYear();
...tab, const st00020003Config = getST00020003RvwConfig(statsYr);
columns: (tab.columns ?? []).map((col) => cloneColumnConfig(col)), return SewagePipeColumnInfoListRvw.map((tab) => {
columnLayout: (tab.columnLayout ?? []).map((item) => const source =
cloneColumnLayoutItem(item), tab.tabKey === "ST00020003" ? st00020003Config
), : tab;
})), return {
[], ...source,
columns: (source.columns ?? []).map((col) => cloneColumnConfig(col as Parameters<typeof cloneColumnConfig>[0])),
columnLayout: (source.columnLayout ?? []).map((item) =>
cloneColumnLayoutItem(item as ColumnLayoutItem),
),
};
});
},
[lastSearchValues?.year],
); );
const reviewCellStyleCallbackByTab = useMemo( const reviewCellStyleCallbackByTab = useMemo(
@@ -3,31 +3,31 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface"; import type { SewerReservoirRequestParams } from "@/entities/statistics/model/sewage-reservoir-pump-review-interface";
import type { import type {
ReviewerInfoRequest, ReviewerInfoRequest,
ReviewOpinionSaveRequest, ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface"; } from "@/entities/statistics/model/statistics-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn"; import { SewageReservoirPumpInfoListRvw } from "@/components/util/column/review/SewageReservoirPumpRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001"; const firstTabKey = SewageReservoirPumpInfoListRvw[0]?.tabKey ?? "ST00040001";
@@ -3,36 +3,36 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { import {
extractSewerageFinanceReviewList, extractSewerageFinanceReviewList,
extractSewerageFinanceReviewStatsStatusInfo, extractSewerageFinanceReviewStatsStatusInfo,
mapSewerageFinanceReviewRows, mapSewerageFinanceReviewRows,
type SewerageFinanceReviewRequestParams, type SewerageFinanceReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewerage-finance-review-interface"; } from "@/entities/statistics/model/statistics-sewerage-finance-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn"; import { SewerageFinanceInfoListRvw } from "@/components/util/column/review/SewerageFinanceRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001"; const firstTabKey = SewerageFinanceInfoListRvw[0]?.tabKey ?? "ST00090001";
@@ -1,30 +1,31 @@
import { useState, useMemo } from "react"; import { useMemo,useState } from "react";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import ReturnButton from "@/components/button/ReturnButton";
import SubmitButton from "@/components/button/SumitButton";
import { LoadingWithAnimation } from "@common/components";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface";
import type {
ReviewOpinionSaveRequest,
ReviewerInfoRequest,
} from "@/entities/statistics/model/statistics-interface";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import { toGridRows } from "@/components/util/grid/gridFunction"; import type {
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle"; ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface";
import { LoadingWithAnimation } from "@common/components";
import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallback } from "@/components/util/grid/reviewEmptyCurrCellStyle";
// 하수도 통계 검토 01.하수도 보급률 // 하수도 통계 검토 01.하수도 보급률
const SeweragePenetrationRateRvw = () => { const SeweragePenetrationRateRvw = () => {
@@ -3,30 +3,31 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { import {
extractSewerageServiceLevelReviewList, extractSewerageServiceLevelReviewList,
extractSewerageServiceLevelReviewStatsStatusInfo, extractSewerageServiceLevelReviewStatsStatusInfo,
mapSewerageServiceLevelReviewRows, mapSewerageServiceLevelReviewRows,
type SewerageServiceLevelReviewRequestParams, type SewerageServiceLevelReviewRequestParams,
} from "@/entities/statistics/model/statistics-sewerage-service-level-review-interface"; } from "@/entities/statistics/model/statistics-sewerage-service-level-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { import type {
ColumnConfig, ColumnConfig,
ColumnInfo, ColumnInfo,
@@ -36,7 +37,6 @@ import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageServiceLevelInfoListRvw } from "@/components/util/column/review/SewerageServiceLevelRvwColumn"; import { SewerageServiceLevelInfoListRvw } from "@/components/util/column/review/SewerageServiceLevelRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = SewerageServiceLevelInfoListRvw[0]?.tabKey ?? "ST00130001"; const firstTabKey = SewerageServiceLevelInfoListRvw[0]?.tabKey ?? "ST00130001";
@@ -1,36 +1,42 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import DataGrid from "@/components/module/DataGrid"; import { useAlertStore, useSearchConditionSidoList, useUserStore } from "@/shared/lib";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import SearchForm from "@/components/module/SearchForm"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import ApproveButton from "@/components/button/ApproveButton"; import type {
import { doList } from "@/components/util/column/input/DoList"; ReviewHistoryResponse,
StatsInputStatusReviewItem,
} from "@/entities/statistics/model/statistics-input-review-interface";
import type {
ReviewerInfoRequest,
StatsInputStatusApproveItem,
} from "@/entities/statistics/model/statistics-interface";
import { isGugunOkForToolbar } from "@/entities/statistics";
import { import {
Button, Button,
DataTable,
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
LoadingWithAnimation, LoadingWithAnimation} from "@common/components";
DataTable
} 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,
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"; import { getUserInfo } from "../../../../../../packages/common/components/src/api";
import ApproveButton from "@/components/button/ApproveButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
type ReviewHistoryRow = ReviewHistoryResponse["data"][number]; type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
const checkPrvnStatusCd = (code: string) => { const checkPrvnStatusCd = (code: string) => {
@@ -54,7 +60,9 @@ const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
// 하수도 통계 검토 00. 통계 입력 현황 // 하수도 통계 검토 00. 통계 입력 현황
const StatsInputStatusRvw = () => { const StatsInputStatusRvw = () => {
const { sidoList } = useSearchConditionSidoList();
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const userInfo = useUserStore((state) => state.userInfo);
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]); const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]); const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
@@ -63,6 +71,7 @@ const StatsInputStatusRvw = () => {
const [searchedYear, setSearchedYear] = useState<string>(""); const [searchedYear, setSearchedYear] = useState<string>("");
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null); const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
const [hasCompletedSearch, setHasCompletedSearch] = useState(false); const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false); const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
const [historySearchInfo, setHistorySearchInfo] = useState<{ const [historySearchInfo, setHistorySearchInfo] = useState<{
ctpvNm: string; ctpvNm: string;
@@ -112,6 +121,7 @@ const StatsInputStatusRvw = () => {
setLastSearchedValues(null); setLastSearchedValues(null);
setHasCompletedSearch(false); setHasCompletedSearch(false);
setIsApproveLoading(false); setIsApproveLoading(false);
setSubmitDialogOpen(false);
}; };
const handleSearch = async (values: SearchFormValues) => { const handleSearch = async (values: SearchFormValues) => {
@@ -223,9 +233,7 @@ const StatsInputStatusRvw = () => {
} }
}; };
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "") const gugunOkOrNotDoList = isGugunOkForToolbar(sidoList, lastSearchedValues);
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
: true;
const allSubmitStatusApproved = useMemo( const allSubmitStatusApproved = useMemo(
() => () =>
@@ -242,13 +250,74 @@ const StatsInputStatusRvw = () => {
[rowData], [rowData],
); );
const showActionToolbarCard = /** 진행상태(prvn) 코드가 모두 제출완료(2) */
const allPrvnStatusSubmitted = useMemo(
() =>
rowData.length > 0 &&
rowData.every((row) => String(row.prvnStatusCd ?? "") === "2"),
[rowData],
);
const showSubmitButton =
hasCompletedSearch &&
rowData.length > 0 &&
gugunOkOrNotDoList &&
!allPrvnStatusApproved &&
!allPrvnStatusSubmitted;
const showApproveButton =
hasCompletedSearch && hasCompletedSearch &&
rowData.length > 0 && rowData.length > 0 &&
gugunOkOrNotDoList && gugunOkOrNotDoList &&
allSubmitStatusApproved && allSubmitStatusApproved &&
!allPrvnStatusApproved; !allPrvnStatusApproved;
const showActionToolbarCard = showSubmitButton || showApproveButton;
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
if (lastSearchedValues == null) return null;
const statsYr = lastSearchedValues.year ?? "";
const sewerBzmnCd = lastSearchedValues.bsnmCd ?? "";
const instSeCd = userInfo?.instSeCd ?? "";
if (!statsYr || !sewerBzmnCd || !instSeCd) {
return null;
}
return {
statsYr,
sewerBzmnCd,
instSeCd,
};
}, [lastSearchedValues, userInfo?.instSeCd]);
const submitFunc = () => {
if (submitDialogContext == null) {
alert("조회 후 제출할 수 있습니다.");
return;
}
setSubmitDialogOpen(true);
};
const refreshListAfterSubmit = async () => {
if (!lastSearchedValues) return;
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 {
/* 목록 갱신 실패는 별도 안내 없이 생략 */
}
};
const openReviewHistoryForRow = useCallback( const openReviewHistoryForRow = useCallback(
async (row: Record<string, string | number>) => { async (row: Record<string, string | number>) => {
if (!searchedYear) { if (!searchedYear) {
@@ -297,7 +366,10 @@ const StatsInputStatusRvw = () => {
{showActionToolbarCard && ( {showActionToolbarCard && (
<div className="card !py-0"> <div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]"> <div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
<ApproveButton approveFunc={approveFunc} /> {showSubmitButton && (
<SubmitButton submitFunc={submitFunc} text="일괄 제출" />
)}
{showApproveButton && <ApproveButton approveFunc={approveFunc} />}
</div> </div>
</div> </div>
)} )}
@@ -357,6 +429,13 @@ const StatsInputStatusRvw = () => {
} }
}} }}
/> />
<ReviewSubmitDialog
open={submitDialogOpen}
onOpenChange={setSubmitDialogOpen}
context={submitDialogContext}
hideOpinionFields
onSubmitted={() => void refreshListAfterSubmit()}
/>
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}> <Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
<DialogContent <DialogContent
className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4" className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4"
@@ -3,36 +3,36 @@ import { useMemo, useRef, useState } from "react";
import { useAlertStore, useUserStore } from "@/shared/lib"; import { useAlertStore, useUserStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import { import {
extractWaterReuseReviewList, extractWaterReuseReviewList,
extractWaterReuseReviewStatsStatusInfo, extractWaterReuseReviewStatsStatusInfo,
mapWaterReuseReviewRows, mapWaterReuseReviewRows,
type WaterReuseReviewRequestParams, type WaterReuseReviewRequestParams,
} from "@/entities/statistics/model/statistics-water-reuse-review-interface"; } from "@/entities/statistics/model/statistics-water-reuse-review-interface";
import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface";
import type {
ReviewerInfoRequest,
ReviewOpinionSaveRequest,
} from "@/entities/statistics/model/statistics-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SubmitButton from "@/components/button/SumitButton";
import ReturnButton from "@/components/button/ReturnButton"; import ReturnButton from "@/components/button/ReturnButton";
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
import SubmitButton from "@/components/button/SumitButton";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm"; import SearchForm from "@/components/module/SearchForm";
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog"; import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
import { import {
ReviewOpinionDialog, ReviewOpinionDialog,
type ReviewOpinionDialogContext, type ReviewOpinionDialogContext,
} from "@/components/statistics/review/ReviewOpinionDialog"; } from "@/components/statistics/review/ReviewOpinionDialog";
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type"; import type { ColumnConfig, ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn"; import { WaterReuseInfoListRvw } from "@/components/util/column/review/WaterReuseRvwColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle"; import { buildReviewCellStyleCallbackByTab } from "@/components/util/grid/reviewEmptyCurrCellStyle";
import ReviewEmptyCurrLegend from "@/components/statistics/review/ReviewEmptyCurrLegend";
const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001"; const firstTabKey = WaterReuseInfoListRvw[0]?.tabKey ?? "ST00100001";
@@ -1,24 +1,23 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 분리된 분뇨처리시설용 컬럼 구성 임포트
import { ExcretaTreatmentStatusColumnConfig } from "@/components/util/column/total/SewerExcretaTreatmentStatusColumn";
// 집계표(TOTAL) API 및 인터페이스 명세 호출 // 집계표(TOTAL) API 및 인터페이스 명세 호출
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
ExcretaTreatmentMaintainTotalRequest ExcretaTreatmentMaintainTotalRequest
} from "@/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-excreta-treatment-total-interface";
import { LoadingWithAnimation } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
// 분리된 분뇨처리시설용 컬럼 구성 임포트
import { ExcretaTreatmentStatusColumnConfig } from "@/components/util/column/total/SewerExcretaTreatmentStatusColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
const ExcretaTreatmentFacilityTot = () => { const ExcretaTreatmentFacilityTot = () => {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -84,7 +83,7 @@ const ExcretaTreatmentFacilityTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -105,6 +104,7 @@ const ExcretaTreatmentFacilityTot = () => {
setRowData={setRowData} // 필수 프로퍼티 에러 해결 setRowData={setRowData} // 필수 프로퍼티 에러 해결
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={config.fixedColumns} fixedColumns={config.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,26 +1,25 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 분리된 개별 3개 컬럼 스펙 파일 로드
import { PrivateSewageTab1ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column";
import { PrivateSewageTab2ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column";
import { PrivateSewageTab3ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column";
// 통계 API 서비스 및 통합 DTO 명세 임포트 // 통계 API 서비스 및 통합 DTO 명세 임포트
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
PrivateSewageFacilitySummaryRequest PrivateSewageFacilitySummaryRequest
} from "@/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-private-sewage-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
// 분리된 개별 3개 컬럼 스펙 파일 로드
import { PrivateSewageTab1ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab1Column";
import { PrivateSewageTab2ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab2Column";
import { PrivateSewageTab3ColumnConfig } from "@/components/util/column/total/sewerPrivateSewageColumn/SewerPrivateSewageTab3Column";
import { toGridRows } from "@/components/util/grid/gridFunction";
const PrivateSewageColumnListTot = [ const PrivateSewageColumnListTot = [
PrivateSewageTab1ColumnConfig, PrivateSewageTab1ColumnConfig,
PrivateSewageTab2ColumnConfig, PrivateSewageTab2ColumnConfig,
@@ -132,7 +131,7 @@ const PrivateSewageFacilityTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{/* 하수관로 규칙과 100% 싱크 맞춘 중앙 정렬 소형 팝업 로딩 바 고정 */} {/* 하수관로 규칙과 100% 싱크 맞춘 중앙 정렬 소형 팝업 로딩 바 고정 */}
{isLoading && ( {isLoading && (
@@ -184,6 +183,7 @@ const PrivateSewageFacilityTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,15 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { LoadingWithAnimation } from "@common/components";
import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PublicSewageFacilityColumn } from "@/components/util/column/total/PublicSewageFacilityColumn"; import { PublicSewageFacilityColumn } from "@/components/util/column/total/PublicSewageFacilityColumn";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAlertStore } from "@/shared/lib";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import CellColorInfo from "./CellColorInfo";
import { LoadingWithAnimation } from "@common/components";
const PublicSewageFacilityTot = () => { const PublicSewageFacilityTot = () => {
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]); const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
@@ -52,7 +53,7 @@ const PublicSewageFacilityTot = () => {
return ( return (
<> <>
<SearchForm isDetail={true} capacityGbn={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" /> <SearchForm isDetail={true} capacityGbn={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <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]"> <div className="relative size-[5rem]">
@@ -65,6 +66,7 @@ const PublicSewageFacilityTot = () => {
columnInfo={PublicSewageFacilityColumn} columnInfo={PublicSewageFacilityColumn}
rowStartNum={2} rowStartNum={2}
isExcelImport={false} isExcelImport={false}
excelExportApplyStyles
fixedColumns={2} fixedColumns={2}
rowData={rowData} rowData={rowData}
setRowData={setRowData} setRowData={setRowData}
@@ -1,24 +1,23 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 분리된 중계펌프장용 컬럼 구성 임포트
import { MediationPumpStatusColumnConfig } from "@/components/util/column/total/SewerMediationPumpStatusColumn";
// 집계표(TOTAL) API 및 인터페이스 명세 호출 // 집계표(TOTAL) API 및 인터페이스 명세 호출
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
MediationPumpMaintainTotalRequest MediationPumpMaintainTotalRequest
} from "@/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-mediation-pump-total-interface";
import { LoadingWithAnimation } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
// 분리된 중계펌프장용 컬럼 구성 임포트
import { MediationPumpStatusColumnConfig } from "@/components/util/column/total/SewerMediationPumpStatusColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
const PumpingStationTot = () => { const PumpingStationTot = () => {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -84,7 +83,7 @@ const PumpingStationTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -105,6 +104,7 @@ const PumpingStationTot = () => {
setRowData={setRowData} setRowData={setRowData}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={config.fixedColumns} fixedColumns={config.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,25 +1,26 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import CellColorInfo from "./CellColorInfo";
// 12. 하수 및 분뇨 관리 산업 탭 설정 (경로 확인 필수) import { useAlertStore } from "@/shared/lib";
import { PublicSewageStaffTab1ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column";
import { PublicHumanWasteStaffTab2ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column";
import { HumanWasteCollectionTab3ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column";
import { IndividualSewageTab4ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
SewerageIndustry,
PublicSewageStaffTab1Response,
PublicHumanWasteStaffTab2Response,
HumanWasteCollectionTab3Response, HumanWasteCollectionTab3Response,
IndividualSewageTab4Response IndividualSewageTab4Response,
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface"; PublicHumanWasteStaffTab2Response,
PublicSewageStaffTab1Response,
SewerageIndustry} from "@/entities/statistics/model/statistics-total/statistics-sewerage-industry-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { HumanWasteCollectionTab3ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/HumanWasteCollectionTab3Column";
import { IndividualSewageTab4ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/IndividualSewageTab4Column";
import { PublicHumanWasteStaffTab2ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicHumanWasteStaffTab2Column";
// 12. 하수 및 분뇨 관리 산업 탭 설정 (경로 확인 필수)
import { PublicSewageStaffTab1ColumnConfig } from "@/components/util/column/total/sewageExcretaIndustry/PublicSewageStaffTab1Column";
import { toGridRows } from "@/components/util/grid/gridFunction";
const SewageExcretaIndustryColumnListTot = [PublicSewageStaffTab1ColumnConfig,PublicHumanWasteStaffTab2ColumnConfig,HumanWasteCollectionTab3ColumnConfig,IndividualSewageTab4ColumnConfig].filter(Boolean); const SewageExcretaIndustryColumnListTot = [PublicSewageStaffTab1ColumnConfig,PublicHumanWasteStaffTab2ColumnConfig,HumanWasteCollectionTab3ColumnConfig,IndividualSewageTab4ColumnConfig].filter(Boolean);
const firstTabKey = SewageExcretaIndustryColumnListTot[0]?.tabKey ?? "ST00120001"; const firstTabKey = SewageExcretaIndustryColumnListTot[0]?.tabKey ?? "ST00120001";
@@ -102,7 +103,7 @@ const SewageExcretaRelatedTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -140,6 +141,7 @@ const SewageExcretaRelatedTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,30 +1,28 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 4개 탭 컬럼 설정 임포트
import { SludgeProcessingTab1ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column";
import { DigestionTankTab2ColumnConfig } from "@/components/util/column/total/sludgeProcessing/DigestionTankTab2Column";
import { SewageSludgeOperTab3ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column";
import { HumanWasteSludgeTab4ColumnConfig } from "@/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column";
// API 인스턴스 및 인터페이스 임포트 (타입 안전성 확보) // API 인스턴스 및 인터페이스 임포트 (타입 안전성 확보)
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
SludgeSummaryRequest,
SludgeProcessingTab1Response,
DigestionTankOperTab2Response, DigestionTankOperTab2Response,
HumanWasteSludgeTab4Response,
SewageSludgeOperTab3Response, SewageSludgeOperTab3Response,
HumanWasteSludgeTab4Response SludgeProcessingTab1Response,
} from "@/entities/statistics/model/statistics-total/statistics-sludge-total-interface"; SludgeSummaryRequest} from "@/entities/statistics/model/statistics-total/statistics-sludge-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { DigestionTankTab2ColumnConfig } from "@/components/util/column/total/sludgeProcessing/DigestionTankTab2Column";
import { HumanWasteSludgeTab4ColumnConfig } from "@/components/util/column/total/sludgeProcessing/HumanWasteSludgeTab4Column";
import { SewageSludgeOperTab3ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SewageSludgeOperTab3Column";
// 4개 탭 컬럼 설정 임포트
import { SludgeProcessingTab1ColumnConfig } from "@/components/util/column/total/sludgeProcessing/SludgeProcessingTab1Column";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 탭 순회 설정 // 탭 순회 설정
const SludgeColumnListTot = [ const SludgeColumnListTot = [
SludgeProcessingTab1ColumnConfig, SludgeProcessingTab1ColumnConfig,
@@ -146,7 +144,7 @@ const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<b
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -188,6 +186,7 @@ const loadGridData = async (tabKey: string, values: SearchFormValues): Promise<b
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -2,15 +2,15 @@ import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeMaintainColumnListTot } from "@/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn"; import { SewagePipeMaintainColumnListTot } from "@/components/util/column/total/sewagePipeMaintain/sewagePipeMaintainColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import type { SewagePipeMaintainTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-maintain-total-interface";
const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001"; const firstTabKey = SewagePipeMaintainColumnListTot[0]?.tabKey ?? "ST00030001";
@@ -133,7 +133,7 @@ const SewagePipeMaintainTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <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]"> <div className="relative size-[5rem]">
@@ -186,6 +186,7 @@ const SewagePipeMaintainTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport isExcelExport
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -2,16 +2,17 @@ import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewagePipeColumnInfoListTot, getST00020003Config } from "@/components/util/column/total/SewagePipeTotColumn"; import { getST00020003Config, SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface";
import CellColorInfo from "./CellColorInfo";
const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001"; const firstTabKey = SewagePipeColumnInfoListTot[0]?.tabKey ?? "ST00020001";
@@ -178,7 +179,7 @@ const SewagePipeTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <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]"> <div className="relative size-[5rem]">
@@ -204,11 +205,10 @@ const SewagePipeTot = () => {
{columnInfo.map((info) => { {columnInfo.map((info) => {
const tabKey = info.tabKey; const tabKey = info.tabKey;
// ★ [동적 처리] ST00020003인 경우 함수를 통해 컬럼 정보를 실시간으로 생성 // ★ [동적 처리] ST00020003은 함수를 통해 컬럼 정보를 실시간으로 생성
const currentYear = lastSearchedValuesRef.current?.year || new Date().getFullYear(); const currentYear = lastSearchedValuesRef.current?.year || new Date().getFullYear();
const finalTabConfig = tabKey === "ST00020003" const finalTabConfig =
? getST00020003Config(currentYear) tabKey === "ST00020003" ? getST00020003Config(currentYear) : info;
: info;
if (!finalTabConfig.columns || !finalTabConfig.columnLayout) { if (!finalTabConfig.columns || !finalTabConfig.columnLayout) {
return null; return null;
@@ -239,6 +239,7 @@ const SewagePipeTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport isExcelExport
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={finalTabConfig.fixedColumns} fixedColumns={finalTabConfig.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,25 +1,24 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 분리된 개별 컬럼 구성 임포트 경로 유지
import { ReservoirStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn";
import { PumpStationStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn";
// 수정 적용한 API 및 인터페이스 명세 호출 유지 // 수정 적용한 API 및 인터페이스 명세 호출 유지
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
SewerReservoirMaintainTotalRequest SewerReservoirMaintainTotalRequest
} from "@/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-sewage-reservoir-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { PumpStationStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerPumpStationStatusColumn";
// 분리된 개별 컬럼 구성 임포트 경로 유지
import { ReservoirStatusColumnConfig } from "@/components/util/column/total/sewerReservoir/SewerReservoirStatusColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
const SewerReservoirColumnListTot = [ const SewerReservoirColumnListTot = [
ReservoirStatusColumnConfig, ReservoirStatusColumnConfig,
PumpStationStatusColumnConfig PumpStationStatusColumnConfig
@@ -118,7 +117,7 @@ const SewageReservoirPumpTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -168,6 +167,7 @@ const SewageReservoirPumpTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -199,6 +199,7 @@ const SewerageAggregateSummary = () => {
<SearchForm <SearchForm
isSummary={true} isSummary={true}
sidoSelectText="전체" sidoSelectText="전체"
defaultRegionAll
searchButtonText="내려받기" searchButtonText="내려받기"
onSubmit={(values) => void handleDownload(values)} onSubmit={(values) => void handleDownload(values)}
isReset={false} isReset={false}
@@ -1,27 +1,26 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 분리된 4개 개별 탭 컬럼 파일 바인딩
import { SewerageFinancialTab1ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column";
import { SewerageFeeTab2ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column";
import { SewerageUsageFeeTab3ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column";
import { SewerageOrgCauseTab4ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column";
// API 인스턴스 및 인터페이스 호출 // API 인스턴스 및 인터페이스 호출
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
SewerageFinanceSummaryRequest SewerageFinanceSummaryRequest
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-sewerage-finance-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageFeeTab2ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFeeTab2Column";
// 분리된 4개 개별 탭 컬럼 파일 바인딩
import { SewerageFinancialTab1ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageFinancialTab1Column";
import { SewerageOrgCauseTab4ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageOrgCauseTab4Column";
import { SewerageUsageFeeTab3ColumnConfig } from "@/components/util/column/total/sewerageFinancial/SewerageUsageFeeTab3Column";
import { toGridRows } from "@/components/util/grid/gridFunction";
const SewerageFinanceColumnListTot = [ const SewerageFinanceColumnListTot = [
SewerageFinancialTab1ColumnConfig, SewerageFinancialTab1ColumnConfig,
SewerageFeeTab2ColumnConfig, SewerageFeeTab2ColumnConfig,
@@ -144,7 +143,7 @@ const SewerageFinanceTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -195,6 +194,7 @@ const SewerageFinanceTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,15 +1,16 @@
import { useState } from "react"; import { useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { LoadingWithAnimation } from "@common/components";
import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn"; import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
import { useAlertStore } from "@/shared/lib";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import CellColorInfo from "./CellColorInfo";
import { LoadingWithAnimation } from "@common/components";
const SeweragePenetrationRateTot = () => { const SeweragePenetrationRateTot = () => {
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]); const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
@@ -51,7 +52,7 @@ const SeweragePenetrationRateTot = () => {
return ( return (
<> <>
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} sidoSelectText="전체" defaultRegionAll />
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> {isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]"> <div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" /> <LoadingWithAnimation color="primary" />
@@ -62,6 +63,7 @@ const SeweragePenetrationRateTot = () => {
columnInfo={SeweragePentrationRateTotColumn} columnInfo={SeweragePentrationRateTotColumn}
rowStartNum={2} rowStartNum={2}
isExcelImport={false} isExcelImport={false}
excelExportApplyStyles
fixedColumns={1} fixedColumns={1}
rowData={rowData} rowData={rowData}
setRowData={setRowData} setRowData={setRowData}
@@ -1,22 +1,23 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type {
SewerageServiceLevelComplaintResponse,
SewerageServiceLevelRequest,
SewerageServiceLevelResidentFriendlyFactResponse
} from "@/entities/statistics/model/statistics-total/statistics-service-level-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components"; import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "@/components/module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import SearchForm from "@/components/module/SearchForm";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
// 13. 하수도서비스수준 컬럼 설정 // 13. 하수도서비스수준 컬럼 설정
import { SewageComplaintTab1ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn"; import { SewageComplaintTab1ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ComplaintTotColumn";
import { ResidentFriendlyFacilityTab2ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn"; import { ResidentFriendlyFacilityTab2ColumnConfig } from "@/components/util/column/total/sewerageServiceLevel/ResidentFriendlyFactTotColumn";
import { toGridRows } from "@/components/util/grid/gridFunction";
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type {
SewerageServiceLevelRequest,
SewerageServiceLevelComplaintResponse,
SewerageServiceLevelResidentFriendlyFactResponse
} from "@/entities/statistics/model/statistics-total/statistics-service-level-total-interface";
const SewageServiceLevelColumnListTot = [ const SewageServiceLevelColumnListTot = [
SewageComplaintTab1ColumnConfig, SewageComplaintTab1ColumnConfig,
@@ -91,7 +92,7 @@ const SewerageServiceLevelTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{isLoading && ( {isLoading && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center"> <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
@@ -129,6 +130,7 @@ const SewerageServiceLevelTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -1,28 +1,28 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "../../module/SearchForm";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// [폴더 정밀 정비 완료] 프로젝트 트리 구조 5개 컬럼 스펙 패키지 호출
import { SewerageReuseTab1ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageReuseTab1Column";
import { SewerageGrayWaterTab2ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column";
import { SewerageRainwaterTab3ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column";
import { SewerageWwtRtlztnRegTab4ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column";
import { SewerageWwtRtlztnSncnTab5ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column";
// 집계표(TOTAL) API 서비스 및 인터페이스 명세 임포트 // 집계표(TOTAL) API 서비스 및 인터페이스 명세 임포트
import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service"; import { statisticsTotalStatusApi } from "@/entities/statistics/api/statistics-total-service";
import type { import type {
WaterReuseSummaryRequest WaterReuseSummaryRequest
} from "@/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface"; } from "@/entities/statistics/model/statistics-total/statistics-sewerage-reuse-total-interface";
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
import SearchForm from "../../module/SearchForm";
import CellColorInfo from "./CellColorInfo"; import CellColorInfo from "./CellColorInfo";
import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { SewerageGrayWaterTab2ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageGrayWaterTab2Column";
import { SewerageRainwaterTab3ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageRainwaterTab3Column";
// [폴더 정밀 정비 완료] 프로젝트 트리 구조 5개 컬럼 스펙 패키지 호출
import { SewerageReuseTab1ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageReuseTab1Column";
import { SewerageWwtRtlztnRegTab4ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnRegTab4Column";
import { SewerageWwtRtlztnSncnTab5ColumnConfig } from "@/components/util/column/total/sewerageReuse/SewerageWwtRtlztnSncnTab5Column";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 5개 멀티 탭 매핑을 위한 설정 취합 배열 // 5개 멀티 탭 매핑을 위한 설정 취합 배열
const WaterReuseColumnListTot = [ const WaterReuseColumnListTot = [
SewerageReuseTab1ColumnConfig, SewerageReuseTab1ColumnConfig,
@@ -157,7 +157,7 @@ const WaterReuseTot = () => {
return ( return (
<div className="relative"> <div className="relative">
<SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" /> <SearchForm isDetail={true} isSummary={true} onSubmit={handleSearch} onReset={handleReset} sidoSelectText="전체" defaultRegionAll />
{/* 정중앙 5rem 크기 고정 격리형 로딩 애니메이션 */} {/* 정중앙 5rem 크기 고정 격리형 로딩 애니메이션 */}
{isLoading && ( {isLoading && (
@@ -207,6 +207,7 @@ const WaterReuseTot = () => {
}} }}
isExcelImport={false} isExcelImport={false}
isExcelExport={true} isExcelExport={true}
excelExportApplyStyles
showHeaderCheckbox={false} showHeaderCheckbox={false}
fixedColumns={info.fixedColumns} fixedColumns={info.fixedColumns}
enableSort={false} enableSort={false}
@@ -12,11 +12,9 @@ import {
DialogTitle, DialogTitle,
Input, Input,
Label, Label,
Textarea,
} from "@common/components"; } from "@common/components";
export interface ApprovalFormData { export interface ApprovalFormData {
rjctOpnnCn: string;
lastCfmtnPicNm: string; lastCfmtnPicNm: string;
lastCfmtnPicTelNo: string; lastCfmtnPicTelNo: string;
lastCfmtnPicDeptNm: string; lastCfmtnPicDeptNm: string;
@@ -34,7 +32,6 @@ export function ApprovalInputDialog({
onOpenChange, onOpenChange,
onConfirm, onConfirm,
}: ApprovalInputDialogProps) { }: ApprovalInputDialogProps) {
const [rjctOpnnCn, setRjctOpnnCn] = useState("");
const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState(""); const [lastCfmtnPicNm, setLastCfmtnPicNm] = useState("");
const [lastCfmtnPicTelNo, setLastCfmtnPicTelNo] = useState(""); const [lastCfmtnPicTelNo, setLastCfmtnPicTelNo] = useState("");
const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState(""); const [lastCfmtnPicDeptNm, setLastCfmtnPicDeptNm] = useState("");
@@ -43,7 +40,6 @@ export function ApprovalInputDialog({
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
setRjctOpnnCn("");
setLastCfmtnPicNm(""); setLastCfmtnPicNm("");
setLastCfmtnPicTelNo(""); setLastCfmtnPicTelNo("");
setLastCfmtnPicDeptNm(""); setLastCfmtnPicDeptNm("");
@@ -65,7 +61,6 @@ export function ApprovalInputDialog({
try { try {
setIsSubmitting(true); setIsSubmitting(true);
const formData: ApprovalFormData = { const formData: ApprovalFormData = {
rjctOpnnCn: rjctOpnnCn.trim(),
lastCfmtnPicNm: lastCfmtnPicNm.trim(), lastCfmtnPicNm: lastCfmtnPicNm.trim(),
lastCfmtnPicTelNo: lastCfmtnPicTelNo.trim(), lastCfmtnPicTelNo: lastCfmtnPicTelNo.trim(),
lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(), lastCfmtnPicDeptNm: lastCfmtnPicDeptNm.trim(),
@@ -3,12 +3,18 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import * as XLSX from "xlsx";
import { useAlertStore } from "@/shared/lib"; import { useAlertStore } from "@/shared/lib";
import { NoData } from "@/shared/ui/no-data"; import { NoData } from "@/shared/ui/no-data";
import { CodeSelectWithAll } from "@/entities/code"; import { CodeSelectWithAll } from "@/entities/code";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface"; import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
import {
getInputPeriodVerificationManageBlockedMessage,
isInputPeriodCurrent,
} from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import { import {
Button, Button,
DataTable, DataTable,
@@ -43,20 +49,31 @@ const GeneralVerificationManagement = () => {
{}, {},
); );
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [isExporting, setIsExporting] = useState<boolean>(false);
const [selectedVrfcRow, setSelectedVrfcRow] = useState<number | null>(null); const [selectedVrfcRow, setSelectedVrfcRow] = useState<number | null>(null);
const [showRegistForm, setShowRegistForm] = useState<boolean>(false); const [showRegistForm, setShowRegistForm] = useState<boolean>(false);
const [inputPeriodBlocked, setInputPeriodBlocked] = useState(false);
const [inputPeriodBlockedMessage, setInputPeriodBlockedMessage] = useState<
string | null
>(null);
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
const handleSearch = async () => { const handleSearch = async (
values: {
statsCd: string;
useYn: string;
vrfcClsfCd: string;
} = searchValues,
) => {
try { try {
setIsLoading(true); setIsLoading(true);
const params = Object.fromEntries( const params = Object.fromEntries(
Object.entries({ Object.entries({
trgtStatsCd: searchValues.statsCd, trgtStatsCd: values.statsCd,
vrfcClsfCd: searchValues.vrfcClsfCd, vrfcClsfCd: values.vrfcClsfCd,
useYn: searchValues.useYn, useYn: values.useYn,
}).filter(([, value]) => value !== "" && value !== "ALL"), }).filter(([, value]) => value !== "" && value !== "ALL"),
); );
@@ -83,8 +100,18 @@ const GeneralVerificationManagement = () => {
}; };
const columns = useMemo( const columns = useMemo(
() => getGeneralVerificationColumns(handleSearch), () =>
[searchValues.statsCd, searchValues.vrfcClsfCd, searchValues.useYn], getGeneralVerificationColumns(() => handleSearch(), {
mutationBlocked: inputPeriodBlocked,
blockedMessage: inputPeriodBlockedMessage,
}),
[
searchValues.statsCd,
searchValues.vrfcClsfCd,
searchValues.useYn,
inputPeriodBlocked,
inputPeriodBlockedMessage,
],
); );
const table = useReactTable({ const table = useReactTable({
@@ -102,15 +129,76 @@ const GeneralVerificationManagement = () => {
setShowRegistForm(false); setShowRegistForm(false);
}; };
const handleExportExcel = async () => {
try {
setIsExporting(true);
const response = await generalverificationApiService.getVerifyExcelList();
if (response.status !== "success") {
alert(response.message || "엑셀 데이터 조회 중 오류가 발생했습니다.");
return;
}
const list = response.data ?? [];
if (list.length === 0) {
alert("내보낼 데이터가 없습니다.");
return;
}
const header = [
"지표명",
"검증명",
"검증타입",
"소수점 자리수",
"소수점 자리 처리 방식",
"검증식",
];
const rows = list.map((item) => [
item.trgtStatsNm,
item.vrfcNm,
item.vrfcClsfNm,
item.dcptDigtCnt,
item.dcptPrcsMthNm,
item.vrfcEqtnCn,
]);
const worksheet = XLSX.utils.aoa_to_sheet([header, ...rows]);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "일반검증목록");
XLSX.writeFile(workbook, "일반검증목록.xlsx");
} catch {
alert("엑셀 내보내기 중 오류가 발생했습니다.");
} finally {
setIsExporting(false);
}
};
const handleReset = () => { const handleReset = () => {
const resetValues = { statsCd: "", useYn: "", vrfcClsfCd: "" };
setSearchValues(resetValues);
setRowSelection({}); setRowSelection({});
setSelectedVrfcRow(null); setSelectedVrfcRow(null);
handleSearch();
setShowRegistForm(false); setShowRegistForm(false);
void handleSearch(resetValues);
}; };
useEffect(() => { useEffect(() => {
handleSearch(); void handleSearch();
void (async () => {
try {
const statsYr = String(new Date().getFullYear() - 1);
const response =
await statisticsInputSewSupplyRateApi.checkInputPeriod(statsYr);
if (response.status !== "success") return;
const blocked = isInputPeriodCurrent(response.data);
setInputPeriodBlocked(blocked);
setInputPeriodBlockedMessage(
blocked
? getInputPeriodVerificationManageBlockedMessage(response.data)
: null,
);
} catch {
setInputPeriodBlocked(false);
setInputPeriodBlockedMessage(null);
}
})();
}, []); }, []);
return ( return (
@@ -148,11 +236,11 @@ const GeneralVerificationManagement = () => {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="ALL">전체</SelectItem> <SelectItem value="ALL">전체</SelectItem>
<SelectItem value="DO_SI"> <SelectItem value="TOTAL">
도단위 검증 총계 검증
</SelectItem> </SelectItem>
<SelectItem value="SI_GU"> <SelectItem value="ROW">
시단위 검증 행 검증
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -182,7 +270,7 @@ const GeneralVerificationManagement = () => {
<Button <Button
color="green" color="green"
className="h-[3.2rem]" className="h-[3.2rem]"
onClick={handleSearch} onClick={() => void handleSearch()}
> >
조회 조회
</Button> </Button>
@@ -202,16 +290,39 @@ const GeneralVerificationManagement = () => {
<h3 className="text-2xl font-medium text-label"> <h3 className="text-2xl font-medium text-label">
일반 검증 목록 일반 검증 목록
</h3> </h3>
<RegistDialog <div className="flex items-center gap-2">
open={showRegistForm} <Button
onOpenChange={setShowRegistForm} className="h-[3.2rem] w-[96px] bg-gradient-green px-6 hover:bg-gradient-green-hover"
onSuccess={handleSearch} type="button"
> disabled={isExporting}
<Button>등록</Button> onClick={() => void handleExportExcel()}
</RegistDialog> >
<i className="diveicon di-file-excel" />
<span>내보내기</span>
</Button>
{inputPeriodBlocked ? (
<span
title={
inputPeriodBlockedMessage ?? undefined
}
>
<Button disabled type="button">
등록
</Button>
</span>
) : (
<RegistDialog
open={showRegistForm}
onOpenChange={setShowRegistForm}
onSuccess={() => void handleSearch()}
>
<Button>등록</Button>
</RegistDialog>
)}
</div>
</div> </div>
<div className="card !mx-2 !my-4 h-full overflow-auto !p-0"> <div className="card !mx-2 !my-4 h-full overflow-auto !p-0">
{isLoading && ( {(isLoading || isExporting) && (
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10"> <div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
<div className="relative size-[5rem]"> <div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" /> <LoadingWithAnimation color="primary" />
@@ -232,7 +343,11 @@ const GeneralVerificationManagement = () => {
)} )}
</div> </div>
</div> </div>
<DetailComponent selectedVrfcRow={selectedVrfcRow} /> <DetailComponent
selectedVrfcRow={selectedVrfcRow}
mutationBlocked={inputPeriodBlocked}
blockedMessage={inputPeriodBlockedMessage}
/>
</div> </div>
</> </>
); );
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import type { Dispatch, SetStateAction } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
@@ -23,9 +24,9 @@ import {
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { useUserStore } from "@/shared/lib";
import { CodeSelect } from "@/entities/code"; import { CodeSelect } from "@/entities/code";
import { import {
type AdstZoneResponseDto,
IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD, IMR_DETAIL_VISIBLE_COLUMNS_BY_STATS_CD,
IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT, IMR_DETAIL_VISIBLE_COLUMNS_DEFAULT,
imrApi, imrApi,
@@ -36,8 +37,8 @@ import {
stAnlsRsltApi, stAnlsRsltApi,
type StAnlsRsltResponse, type StAnlsRsltResponse,
statisticsAnalysisMasterService, statisticsAnalysisMasterService,
useStAnlsSidoCombo, useSggList,
useStAnlsSigunguCombo, useSidoList,
} from "@/entities/statistics"; } from "@/entities/statistics";
import { import {
Button, Button,
@@ -54,6 +55,7 @@ import {
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { ColumnInfo } from "@/components/type/DataGrid.type";
import { doList } from "@/components/util/column/input/DoList";
ChartJS.register( ChartJS.register(
CategoryScale, CategoryScale,
@@ -71,7 +73,7 @@ type DateRangeValue = {
}; };
const EMPTY_DATA: never[] = []; const EMPTY_DATA: never[] = [];
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = []; const REGION_ALL_VALUE = "ALL";
function getImrDetailGridColumnInfo(statsCd: string): ColumnInfo { function getImrDetailGridColumnInfo(statsCd: string): ColumnInfo {
const visible = const visible =
@@ -96,6 +98,21 @@ function getVerificationYearOptions(): string[] {
); );
} }
function applySingleOptionRegionDefault<T>(
list: T[] | undefined,
setValue: Dispatch<SetStateAction<string>>,
resolveValue: (item: T) => string,
) {
if (!list || list.length !== 1) return;
const onlyItem = list[0];
if (!onlyItem) return;
setValue((prev) =>
prev === REGION_ALL_VALUE ? resolveValue(onlyItem) : prev,
);
}
const ImrVerificationHistory = () => { const ImrVerificationHistory = () => {
const yearOptions = useMemo(() => getVerificationYearOptions(), []); const yearOptions = useMemo(() => getVerificationYearOptions(), []);
const defaultYear: string = const defaultYear: string =
@@ -104,8 +121,9 @@ const ImrVerificationHistory = () => {
: ""; : "";
// Area A Search States // Area A Search States
const [searchSido, setSearchSido] = useState<string>(""); const [searchSido, setSearchSido] = useState<string>(REGION_ALL_VALUE);
const [searchSigungu, setSearchSigungu] = useState<string>(""); const [searchSigungu, setSearchSigungu] =
useState<string>(REGION_ALL_VALUE);
const [searchStatsYr, setSearchStatsYr] = useState<string>(defaultYear); const [searchStatsYr, setSearchStatsYr] = useState<string>(defaultYear);
const [searchStatsCd, setSearchStatsCd] = useState<string>(""); const [searchStatsCd, setSearchStatsCd] = useState<string>("");
const [dateRange, setDateRange] = useState<DateRangeValue | undefined>({ const [dateRange, setDateRange] = useState<DateRangeValue | undefined>({
@@ -139,10 +157,39 @@ const ImrVerificationHistory = () => {
pageSize: 5, pageSize: 5,
}); });
const { data: sidoData } = useStAnlsSidoCombo(); const userInfo = useUserStore((state) => state.userInfo);
const { data: sigunguData } = useStAnlsSigunguCombo(searchSido); const userInstSeCd = userInfo?.instSeCd;
const sidoOptions = sidoData ?? EMPTY_ADST_LIST; const { data: sidoData } = useSidoList({ instSeCd: userInstSeCd });
const sigunguOptions = sigunguData ?? EMPTY_ADST_LIST; const sidoOptions = sidoData?.sidoList ?? [];
const selectedSidoBsnmCd =
sidoOptions.find((item) => item.sido === searchSido)?.bsnmCd ?? "";
const { data: sigunguData } = useSggList(searchSido, {
instSeCd: userInstSeCd,
gugunSkipYn: doList.includes(selectedSidoBsnmCd) ? "N" : "Y",
});
const sigunguOptions = sigunguData ?? [];
const allowRegionAutoDefaultRef = useRef(true);
useEffect(() => {
if (!allowRegionAutoDefaultRef.current) return;
applySingleOptionRegionDefault(
sidoData?.sidoList,
setSearchSido,
(item) => item.sido,
);
}, [sidoData?.sidoList]);
useEffect(() => {
if (!allowRegionAutoDefaultRef.current) return;
if (searchSido === REGION_ALL_VALUE) return;
applySingleOptionRegionDefault(
sigunguData,
setSearchSigungu,
(item) => item.gugun,
);
}, [sigunguData, searchSido]);
const imrDetailColumnInfo = useMemo( const imrDetailColumnInfo = useMemo(
() => getImrDetailGridColumnInfo(selectedHistory?.statsCd ?? ""), () => getImrDetailGridColumnInfo(selectedHistory?.statsCd ?? ""),
@@ -241,9 +288,15 @@ const ImrVerificationHistory = () => {
vrfcMthdCd: "IMR", vrfcMthdCd: "IMR",
sort: "anlsSn,desc", sort: "anlsSn,desc",
}; };
if (searchSido !== "") if (searchSido !== "" && searchSido !== REGION_ALL_VALUE) {
const selectedSggBsnmCd = sigunguOptions.find(
(item) => item.gugun === searchSigungu,
)?.bsnmCd;
params.sewerBzmnCd = params.sewerBzmnCd =
searchSigungu !== "" ? searchSigungu : searchSido; searchSigungu !== "" && searchSigungu !== REGION_ALL_VALUE
? (selectedSggBsnmCd ?? "")
: selectedSidoBsnmCd;
}
if (searchStatsYr) params.statsYr = searchStatsYr; if (searchStatsYr) params.statsYr = searchStatsYr;
if (searchStatsCd) params.statsCd = searchStatsCd; if (searchStatsCd) params.statsCd = searchStatsCd;
if (dateRange?.from) if (dateRange?.from)
@@ -258,8 +311,9 @@ const ImrVerificationHistory = () => {
}; };
const handleReset = () => { const handleReset = () => {
setSearchSido(""); allowRegionAutoDefaultRef.current = false;
setSearchSigungu(""); setSearchSido(REGION_ALL_VALUE);
setSearchSigungu(REGION_ALL_VALUE);
setSearchStatsCd(""); setSearchStatsCd("");
const defaultRange = { const defaultRange = {
from: dayjs().subtract(1, "month").toDate(), from: dayjs().subtract(1, "month").toDate(),
@@ -397,19 +451,22 @@ const ImrVerificationHistory = () => {
value={searchSido} value={searchSido}
onValueChange={(val) => { onValueChange={(val) => {
setSearchSido(val); setSearchSido(val);
setSearchSigungu(""); setSearchSigungu(REGION_ALL_VALUE);
}} }}
> >
<SelectTrigger className="w-[14rem]"> <SelectTrigger className="w-[14rem]">
<SelectValue placeholder="시도 선택" /> <SelectValue placeholder="전체" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={REGION_ALL_VALUE}>
전체
</SelectItem>
{sidoOptions.map((opt) => ( {sidoOptions.map((opt) => (
<SelectItem <SelectItem
key={opt.adstCd} key={opt.adstDstrCd}
value={opt.adstCd} value={opt.sido}
> >
{opt.adstNm} {opt.sido}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -423,18 +480,24 @@ const ImrVerificationHistory = () => {
<Select <Select
value={searchSigungu} value={searchSigungu}
onValueChange={setSearchSigungu} onValueChange={setSearchSigungu}
disabled={searchSido === ""} disabled={
searchSido === "" ||
searchSido === REGION_ALL_VALUE
}
> >
<SelectTrigger className="w-[14rem]"> <SelectTrigger className="w-[14rem]">
<SelectValue placeholder="시군구 선택" /> <SelectValue placeholder="전체" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={REGION_ALL_VALUE}>
전체
</SelectItem>
{sigunguOptions.map((opt) => ( {sigunguOptions.map((opt) => (
<SelectItem <SelectItem
key={opt.adstCd} key={opt.adstDstrCd}
value={opt.adstCd} value={opt.gugun}
> >
{opt.adstNm} {opt.gugun}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@@ -14,14 +14,14 @@ import { CodeApi, CodeSelect } from "@/entities/code";
import { import {
type ApproveRequest, type ApproveRequest,
type RejectRequest, type RejectRequest,
type ReportStatusResponseDto,
statisticsApprovalApi, statisticsApprovalApi,
type StatisticsVerificationFilterState, type StatisticsVerificationFilterState,
statisticsVerificationReportColumnInfo, statisticsVerificationReportColumnInfo,
statisticsVerificationResultColumnInfo, statisticsVerificationResultColumnInfo,
type StatisticsVerificationSubFilterState, type StatisticsVerificationSubFilterState,
type StatsInputStatusList, useSggList,
useStAnlsSidoCombo, useSidoList,
useStAnlsSigunguCombo,
useStatsVrfcRptData, useStatsVrfcRptData,
useStatsVrfcRsltData, useStatsVrfcRsltData,
type VerifyResultItem, type VerifyResultItem,
@@ -53,6 +53,7 @@ import {
ApprovalInputDialog, ApprovalInputDialog,
} from "@/components/statistics/verification/ApprovalInputDialog"; } from "@/components/statistics/verification/ApprovalInputDialog";
import type { CellEditedCallbackParams } from "@/components/type/DataGrid.type"; import type { CellEditedCallbackParams } from "@/components/type/DataGrid.type";
import { doList } from "@/components/util/column/input/DoList";
import { toGridRows } from "@/components/util/grid/gridFunction"; import { toGridRows } from "@/components/util/grid/gridFunction";
const getYearOptions = () => { const getYearOptions = () => {
@@ -71,8 +72,8 @@ const getYearOptions = () => {
}; };
function isSameStatsInputStatusRow( function isSameStatsInputStatusRow(
a: StatsInputStatusList, a: ReportStatusResponseDto,
b: StatsInputStatusList, b: ReportStatusResponseDto,
): boolean { ): boolean {
return ( return (
a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd
@@ -125,17 +126,22 @@ function useGridCheckboxSelection<T>(
return { handleItemChecked, handleAllChecked }; return { handleItemChecked, handleAllChecked };
} }
const REGION_ALL_OPTION = { label: "전체", value: "ALL" };
const DEFAULT_FILTERS: StatisticsVerificationFilterState = { const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
ctpvNm: null, ctpvNm: null,
sggNm: null, sggNm: null,
statsYr: String(dayjs().year() - 1), statsYr: String(dayjs().year() - 1),
sewerBzmnCd: null, sewerBzmnCd: null,
statsCd: null, statsCd: null,
laeSbmsnSttsCd: "3", pbcpSbmsnSttsCd: null,
ctpvSewerBzmnCd: "ALL",
sggSewerBzmnCd: "ALL",
}; };
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = { const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
...DEFAULT_FILTERS, ...DEFAULT_FILTERS,
laeSbmsnSttsCd: "3",
prcsYnList: "1,2", prcsYnList: "1,2",
rejectCode: "0", rejectCode: "0",
pbcpAprvYn: "N", pbcpAprvYn: "N",
@@ -152,6 +158,13 @@ const REJECT_STATUS_OPTIONS = [
{ label: "공단반려", value: "2" }, { label: "공단반려", value: "2" },
]; ];
const PBCP_SBMSN_STTS_OPTIONS = [
{ label: "미작성", value: "0" },
{ label: "작성중", value: "1" },
{ label: "승인", value: "3" },
{ label: "반려", value: "4" },
];
function StatisticsVerificationReport() { function StatisticsVerificationReport() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const userInfo = useUserStore((state) => state.userInfo); const userInfo = useUserStore((state) => state.userInfo);
@@ -162,7 +175,7 @@ function StatisticsVerificationReport() {
const [subFilters, setSubFilters] = const [subFilters, setSubFilters] =
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS); useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
const [selectedReportRows, setSelectedReportRows] = useState< const [selectedReportRows, setSelectedReportRows] = useState<
StatsInputStatusList[] ReportStatusResponseDto[]
>([]); >([]);
const [selectedResultRows, setSelectedResultRows] = useState< const [selectedResultRows, setSelectedResultRows] = useState<
VerifyResultItem[] VerifyResultItem[]
@@ -175,10 +188,15 @@ function StatisticsVerificationReport() {
const prevLengthRef = useRef(0); const prevLengthRef = useRef(0);
const rejectedVrfcSnRef = useRef<Set<number>>(new Set()); const rejectedVrfcSnRef = useRef<Set<number>>(new Set());
const { data: sidoData } = useStAnlsSidoCombo(); const userInstSeCd = userInfo?.instSeCd;
const { data: sigunguData } = useStAnlsSigunguCombo( const { data: sidoData } = useSidoList({ instSeCd: userInstSeCd });
filters.sewerBzmnCd ?? "", const sidoList = sidoData?.sidoList ?? [];
); const selectedSidoBsnmCd = filters.ctpvSewerBzmnCd ?? "";
const { data: sigunguData } = useSggList(filters.ctpvNm ?? "", {
instSeCd: userInstSeCd,
gugunSkipYn: doList.includes(selectedSidoBsnmCd) ? "N" : "Y",
});
const detailParams = useMemo( const detailParams = useMemo(
() => ({ () => ({
statsYr: filters.statsYr, statsYr: filters.statsYr,
@@ -221,9 +239,6 @@ function StatisticsVerificationReport() {
const statisticsVerificationReportQuery = useStatsVrfcRptData( const statisticsVerificationReportQuery = useStatsVrfcRptData(
statisticsVerificationReportFilters, statisticsVerificationReportFilters,
); );
const statisticsVerificationResultQuery =
useStatsVrfcRsltData(detailParams);
const statisticsVerificationReportData = const statisticsVerificationReportData =
statisticsVerificationReportQuery.data ?? []; statisticsVerificationReportQuery.data ?? [];
const statisticsVerificationReportGridRows = useMemo( const statisticsVerificationReportGridRows = useMemo(
@@ -231,6 +246,8 @@ function StatisticsVerificationReport() {
[statisticsVerificationReportData], [statisticsVerificationReportData],
); );
const statisticsVerificationResultQuery =
useStatsVrfcRsltData(detailParams);
const statisticsVerificationResultData = useMemo( const statisticsVerificationResultData = useMemo(
() => () =>
statisticsVerificationResultQuery.data?.pages.flatMap( statisticsVerificationResultQuery.data?.pages.flatMap(
@@ -409,14 +426,25 @@ function StatisticsVerificationReport() {
// 시도 변경 시 // 시도 변경 시
if (key === "ctpvNm") { if (key === "ctpvNm") {
setFilters((prev) => ({ if (value === "ALL") {
...prev, setFilters((prev) => ({
sewerBzmnCd: value, ...prev,
ctpvNm: optionData?.label, ctpvNm: null,
ctpvSewerBzmnCd: value, sggNm: null,
sggNm: "", sewerBzmnCd: null,
sggSewerBzmnCd: "", ctpvSewerBzmnCd: "ALL",
})); sggSewerBzmnCd: "ALL",
}));
} else {
setFilters((prev) => ({
...prev,
sewerBzmnCd: value,
ctpvNm: optionData?.label,
ctpvSewerBzmnCd: value,
sggNm: null,
sggSewerBzmnCd: "ALL",
}));
}
setSubFilters((prev) => ({ setSubFilters((prev) => ({
...prev, ...prev,
ctpvNm: "", ctpvNm: "",
@@ -428,12 +456,25 @@ function StatisticsVerificationReport() {
// 시군구 변경 시 // 시군구 변경 시
if (key === "sggNm") { if (key === "sggNm") {
setFilters((prev) => ({ if (value === "ALL") {
...prev, setFilters((prev) => ({
sewerBzmnCd: value, ...prev,
sggNm: optionData?.label, sggNm: null,
sggSewerBzmnCd: value, sggSewerBzmnCd: "ALL",
})); sewerBzmnCd:
prev.ctpvSewerBzmnCd &&
prev.ctpvSewerBzmnCd !== "ALL"
? prev.ctpvSewerBzmnCd
: null,
}));
} else {
setFilters((prev) => ({
...prev,
sewerBzmnCd: value,
sggNm: optionData?.label,
sggSewerBzmnCd: value,
}));
}
return; return;
} }
@@ -552,6 +593,18 @@ function StatisticsVerificationReport() {
} }
} }
function handleReportRefresh() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.ST_SERVICE.INPUT, "validation-report"],
});
}
function handleResultRefresh() {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.ST_SERVICE.INPUTVERIFY, "verify-results"],
});
}
return ( return (
<div className="relative"> <div className="relative">
<ApprovalInputDialog <ApprovalInputDialog
@@ -563,25 +616,31 @@ function StatisticsVerificationReport() {
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<FilterSelect <FilterSelect
label="시도" label="시도"
value={filters.ctpvSewerBzmnCd ?? ""} value={filters.ctpvSewerBzmnCd ?? "ALL"}
onChange={handleFilterChange("ctpvNm")} onChange={handleFilterChange("ctpvNm")}
options={ options={[
sidoData?.map((item) => ({ REGION_ALL_OPTION,
label: item.adstNm, ...sidoList.map((item) => ({
value: item.adstCd, label: item.sido,
})) ?? [] value: item.bsnmCd,
} })),
]}
/> />
<FilterSelect <FilterSelect
label="시군구" label="시군구"
value={filters.sggSewerBzmnCd ?? ""} value={filters.sggSewerBzmnCd ?? "ALL"}
onChange={handleFilterChange("sggNm")} onChange={handleFilterChange("sggNm")}
options={ disabled={
sigunguData?.map((item) => ({ !filters.ctpvSewerBzmnCd ||
label: item.adstNm, filters.ctpvSewerBzmnCd === "ALL"
value: item.adstCd,
})) ?? []
} }
options={[
REGION_ALL_OPTION,
...(sigunguData?.map((item) => ({
label: item.gugun,
value: item.bsnmCd,
})) ?? []),
]}
/> />
<FilterSelect <FilterSelect
label="년도" label="년도"
@@ -589,6 +648,12 @@ function StatisticsVerificationReport() {
onChange={handleFilterChange("statsYr")} onChange={handleFilterChange("statsYr")}
options={getYearOptions()} options={getYearOptions()}
/> />
<FilterSelect
label="공단진행상태"
value={filters.pbcpSbmsnSttsCd ?? ""}
onChange={handleFilterChange("pbcpSbmsnSttsCd")}
options={PBCP_SBMSN_STTS_OPTIONS}
/>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label>통계지표</Label> <Label>통계지표</Label>
<CodeSelect <CodeSelect
@@ -601,6 +666,9 @@ function StatisticsVerificationReport() {
</div> </div>
</div> </div>
<div className="flex items-end justify-end gap-2"> <div className="flex items-end justify-end gap-2">
<Button color="gray" onClick={handleReportRefresh}>
새로고침
</Button>
<Button color="white" onClick={handleReset}> <Button color="white" onClick={handleReset}>
초기화 초기화
</Button> </Button>
@@ -626,7 +694,7 @@ function StatisticsVerificationReport() {
setRowData={() => {}} setRowData={() => {}}
showHeaderCheckbox={true} showHeaderCheckbox={true}
gridHeight={30} gridHeight={30}
onRowClick={(row: StatsInputStatusList) => { onRowClick={(row: ReportStatusResponseDto) => {
setSubFilters((prev) => ({ setSubFilters((prev) => ({
...prev, ...prev,
ctpvNm: row?.ctpvNm ?? "", ctpvNm: row?.ctpvNm ?? "",
@@ -662,6 +730,9 @@ function StatisticsVerificationReport() {
/> />
</div> </div>
<div className="flex items-end justify-end gap-2"> <div className="flex items-end justify-end gap-2">
<Button color="gray" onClick={handleResultRefresh}>
새로고침
</Button>
<Button color="white" onClick={handleSubFilterReset}> <Button color="white" onClick={handleSubFilterReset}>
초기화 초기화
</Button> </Button>
@@ -717,6 +788,7 @@ interface FilterSelectProps {
) => void; ) => void;
options: Array<{ label: string; value: string }>; options: Array<{ label: string; value: string }>;
value?: string; value?: string;
disabled?: boolean;
} }
function FilterCombobox({ function FilterCombobox({
@@ -783,12 +855,19 @@ function FilterCombobox({
); );
} }
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) { function FilterSelect({
label,
onChange,
options,
value,
disabled,
}: FilterSelectProps) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="whitespace-nowrap">{label}</Label> <Label className="whitespace-nowrap">{label}</Label>
<Select <Select
value={value} value={value}
disabled={disabled}
onValueChange={(value) => onValueChange={(value) =>
onChange( onChange(
value, value,
@@ -39,8 +39,8 @@ import { VerificationTargetDialog } from "./VerificationTargetDialog";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
const TAB_CONFIG = { const TAB_CONFIG = {
HB: { value: "HB", label: "H-B", columnInfo: hbArticleColumnInfo }, HB: { value: "HB", label: "모집단", columnInfo: hbArticleColumnInfo },
IMR: { value: "IMR", label: "I-MR", columnInfo: imrArticleColumnInfo }, IMR: { value: "IMR", label: "시계열", columnInfo: imrArticleColumnInfo },
GENERAL: { GENERAL: {
value: "GENERAL", value: "GENERAL",
label: "일반", label: "일반",
@@ -40,8 +40,8 @@ const HbVerificationDetail: React.FC<HbVerificationDetailProps> = ({
); );
const [checkedSns, setCheckedSns] = useState<string[]>([]); const [checkedSns, setCheckedSns] = useState<string[]>([]);
const [appliedFilter, setAppliedFilter] = useState<AppliedFilter>({ const [appliedFilter, setAppliedFilter] = useState<AppliedFilter>({
sido: "", sido: "ALL",
gugun: "", gugun: "ALL",
status: "", status: "",
}); });
const [selectedRowId, setSelectedRowId] = useState<number | null>(null); const [selectedRowId, setSelectedRowId] = useState<number | null>(null);
@@ -26,6 +26,7 @@ import type { AppliedFilter } from "./HbVerificationDetail";
import DataGrid from "@/components/module/DataGrid"; import DataGrid from "@/components/module/DataGrid";
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = []; const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
const REGION_ALL_VALUE = "ALL";
interface RegionalGridProps { interface RegionalGridProps {
anlsSn: number; anlsSn: number;
@@ -53,7 +54,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
>([]); >([]);
const { data: sidoData } = useStAnlsSidoCombo(); const { data: sidoData } = useStAnlsSidoCombo();
const { data: gugunData } = useStAnlsSigunguCombo(appliedFilter.sido ?? ""); const sidoCd = appliedFilter.sido ?? "";
const { data: gugunData } = useStAnlsSigunguCombo(
sidoCd !== REGION_ALL_VALUE ? sidoCd : "",
);
const sidoOptions = sidoData ?? EMPTY_ADST_LIST; const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
const gugunOptions = gugunData ?? EMPTY_ADST_LIST; const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
@@ -66,14 +70,14 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
let data = (resultData?.content ?? []) as StAnlsRsltResponse[]; let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
{ {
const sidoCd = appliedFilter.sido; const sidoCd = appliedFilter.sido;
if (sidoCd) { if (sidoCd && sidoCd !== REGION_ALL_VALUE) {
data = data.filter((item) => data = data.filter((item) =>
item.admdstCd.startsWith(sidoCd.slice(0, 2)), item.admdstCd.startsWith(sidoCd.slice(0, 2)),
); );
} }
const gugunCd = appliedFilter.gugun; const gugunCd = appliedFilter.gugun;
if (gugunCd) { if (gugunCd && gugunCd !== REGION_ALL_VALUE) {
data = data.filter((item) => item.admdstCd === gugunCd); data = data.filter((item) => item.admdstCd === gugunCd);
} }
@@ -107,13 +111,17 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
}, [resultData, appliedFilter]); }, [resultData, appliedFilter]);
const handleSidoChange = (val: string) => { const handleSidoChange = (val: string) => {
onFilterChange({ ...appliedFilter, sido: val, gugun: "" }); onFilterChange({
...appliedFilter,
sido: val,
gugun: REGION_ALL_VALUE,
});
}; };
const handleReset = () => { const handleReset = () => {
onFilterChange({ onFilterChange({
sido: "", sido: REGION_ALL_VALUE,
gugun: "", gugun: REGION_ALL_VALUE,
status: "", status: "",
}); });
}; };
@@ -156,13 +164,16 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select
value={appliedFilter.sido || ""} value={appliedFilter.sido || REGION_ALL_VALUE}
onValueChange={handleSidoChange} onValueChange={handleSidoChange}
> >
<SelectTrigger className="w-[12rem] bg-white"> <SelectTrigger className="w-[12rem] bg-white">
<SelectValue placeholder="시도 선택" /> <SelectValue placeholder="전체" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={REGION_ALL_VALUE}>
전체
</SelectItem>
{sidoOptions.map((opt) => ( {sidoOptions.map((opt) => (
<SelectItem key={opt.adstCd} value={opt.adstCd}> <SelectItem key={opt.adstCd} value={opt.adstCd}>
{opt.adstNm} {opt.adstNm}
@@ -171,16 +182,22 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
</SelectContent> </SelectContent>
</Select> </Select>
<Select <Select
value={appliedFilter.gugun || ""} value={appliedFilter.gugun || REGION_ALL_VALUE}
onValueChange={(val) => onValueChange={(val) =>
onFilterChange({ ...appliedFilter, gugun: val }) onFilterChange({ ...appliedFilter, gugun: val })
} }
disabled={!appliedFilter.sido} disabled={
!appliedFilter.sido ||
appliedFilter.sido === REGION_ALL_VALUE
}
> >
<SelectTrigger className="w-[12rem] bg-white"> <SelectTrigger className="w-[12rem] bg-white">
<SelectValue placeholder="시군구 선택" /> <SelectValue placeholder="전체" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={REGION_ALL_VALUE}>
전체
</SelectItem>
{gugunOptions.map((opt) => ( {gugunOptions.map((opt) => (
<SelectItem key={opt.adstCd} value={opt.adstCd}> <SelectItem key={opt.adstCd} value={opt.adstCd}>
{opt.adstNm} {opt.adstNm}
@@ -1,26 +1,26 @@
import { useState, useEffect } from "react"; import { useEffect,useState } from "react";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
import { CodeSelect } from "@/entities/code";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service";
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
import { import {
Button, Button,
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
DialogClose,
Label,
Input, Input,
Label,
} from "@common/components"; } from "@common/components";
import { CodeSelect } from "@/entities/code";
import { useAlertStore } from "@/shared/lib";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
import { LoadingWithAnimation } from "@common/components"; import { LoadingWithAnimation } from "@common/components";
import { generalverificationApiService } from "@/entities/statistics/api/general-verification-service"; import { getUserInfo } from "../../../../../../../packages/common/components/src/api";
import type { GeneralverificationList } from "@/entities/statistics/model/general-verification-interface";
interface CopyDialogProps { interface CopyDialogProps {
open: boolean; open: boolean;

Some files were not shown because too many files have changed in this diff Show More