Merge branch 'iipbs/as/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/as/dev
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env.*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -38,13 +38,28 @@ async function handler(
|
||||
body = await request.json();
|
||||
}
|
||||
|
||||
const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, {
|
||||
const targetUrl = `${SERVER_API_URL}/${params.url.join("/")}`;
|
||||
console.log(`[Proxy API] ${method} ${params.url.join("/")} -> ${targetUrl}`);
|
||||
console.log(`[Proxy API] Headers:`, headers);
|
||||
if (!isRefreshHandler) {
|
||||
console.log(`[Proxy API] Body:`, body);
|
||||
}
|
||||
|
||||
const result = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
console.log(`[Proxy API] Response Status: ${result.status}`);
|
||||
console.log(`[Proxy API] Response Headers:`, {
|
||||
[ACCESS_TOKEN]: result.headers.get(ACCESS_TOKEN),
|
||||
[REFRESH_TOKEN]: result.headers.get(REFRESH_TOKEN) ? '***hidden***' : undefined,
|
||||
[AUTH_USER_ID]: result.headers.get(AUTH_USER_ID),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
console.error(`[Proxy API] Request failed with status ${result.status}`);
|
||||
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -53,6 +68,7 @@ async function handler(
|
||||
const userId = result.headers.get(AUTH_USER_ID);
|
||||
|
||||
if (accessToken && userId) {
|
||||
console.log(`[Proxy API] Login successful for user: ${userId}`);
|
||||
const response = NextResponse.json({
|
||||
[ACCESS_TOKEN]: accessToken,
|
||||
[AUTH_USER_ID]: userId,
|
||||
@@ -79,6 +95,7 @@ async function handler(
|
||||
|
||||
return NextResponse.json({ message: "Invalid Credentials" }, { status: 401 });
|
||||
} catch (error) {
|
||||
console.error(`[Proxy API] Error caught in handler:`, error);
|
||||
return NextResponse.json({ message: "Server Error", error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const ENDPOINT = {
|
||||
AS_INSTL_SERVICE: {
|
||||
INSTL_SNCN: "/as-instl-service/api/v1/instl-sncn",
|
||||
},
|
||||
USER_SERVICE: {
|
||||
LOGIN: "/user-service/login",
|
||||
SILENT_REFRESH: "/user-service/api/v1/users/token/refresh",
|
||||
|
||||
@@ -33,8 +33,8 @@ type CaseItem = {
|
||||
};
|
||||
|
||||
const TABS: { key: TabKey; label: string }[] = [
|
||||
{ key: "rain", label: "빗물 재이용 사례" },
|
||||
{ key: "reuse", label: "중수 재이용 사례" },
|
||||
{ key: "rain", label: "빗물이용시설 사례" },
|
||||
{ key: "reuse", label: "중수도 사례" },
|
||||
{ key: "sewage", label: "하수 재이용 사례" },
|
||||
{ key: "global", label: "해외 재이용 사례" },
|
||||
];
|
||||
@@ -59,7 +59,7 @@ const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
],
|
||||
reuse: Array.from({ length: 6 }).map((_, i) => ({
|
||||
id: `reuse-${i}`,
|
||||
title: `중수 재이용 사례 ${i + 1}`,
|
||||
title: `중수도 사례 ${i + 1}`,
|
||||
desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
|
||||
imageSrc: "/images/landing/sample_case.png",
|
||||
href: "#",
|
||||
|
||||
@@ -23,7 +23,7 @@ export const LNB_CASES = [
|
||||
// 물 재이용 사례처럼 2depth가 있는 메뉴
|
||||
export const LNB_BUSINESS: LnbItem[] = [
|
||||
{
|
||||
label: "빗물 재이용 사례",
|
||||
label: "빗물이용시설 사례",
|
||||
children: [
|
||||
{ label: "경기도 수원시", href: "/reuse/business/rain/suwon" },
|
||||
// { label: "충청남도 보령시", href: "/reuse/business/rain/bolyeong" },
|
||||
@@ -31,7 +31,7 @@ export const LNB_BUSINESS: LnbItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "중수 재이용 사례",
|
||||
label: "중수도 사례",
|
||||
children: [{ label: "사례 목록", href: "/reuse/business/graywater" }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ function InfoList({
|
||||
}
|
||||
|
||||
export default function GreywaterExampleSection() {
|
||||
const title = "중수 재이용 사례";
|
||||
const title = "중수도 사례";
|
||||
return (
|
||||
<div className="bg-white">
|
||||
{/* 상단 타이틀 */}
|
||||
|
||||
@@ -17,7 +17,7 @@ function Hr({ className = "" }: { className?: string }) {
|
||||
}
|
||||
|
||||
export default function RainExampleSection() {
|
||||
const title = "빗물 재이용 사례";
|
||||
const title = "빗물이용시설 사례";
|
||||
return (
|
||||
<div className="bg-white">
|
||||
{/* 상단 타이틀 */}
|
||||
|
||||
@@ -9,7 +9,7 @@ import GlobalCaseSection from "./_components/GlobalCaseSection";
|
||||
const menus: BusinessSubNavMenu[] = [
|
||||
{
|
||||
key: "example-rain",
|
||||
label: "빗물 재이용 사례",
|
||||
label: "빗물이용시설 사례",
|
||||
sections: [
|
||||
{ id: "example-rain-suwon", label: "경기도 수원시" },
|
||||
{ id: "example-rain-jeju", label: "제주특별자치도" },
|
||||
@@ -17,7 +17,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
key: "example-greywater",
|
||||
label: "중수 재이용 사례",
|
||||
label: "중수도 사례",
|
||||
defaultOpen: false,
|
||||
sections: [
|
||||
{ id: "example-greywater-ansan", label: "경기도 안산시" },
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function GreywaterPage() {
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "빗물 재이용 사례" },
|
||||
{ label: "빗물이용시설 사례" },
|
||||
{ label: "수원시", href: "/reuse/business/rain/suwon" },
|
||||
]}
|
||||
hideFooterLine
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
import { ContentsPage } from "@/components/ui/contents";
|
||||
|
||||
export default async function Contents() {
|
||||
const contsSn = 28;
|
||||
const menuCd = "24";
|
||||
|
||||
return <ContentsPage contsSn={contsSn} menuCd={menuCd} />;
|
||||
}
|
||||
@@ -650,8 +650,7 @@ const globalCases: GlobalCase[] = [
|
||||
depth: 1,
|
||||
},
|
||||
|
||||
{ text: "안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능" },
|
||||
{ text: "공급단가는 수질과 운영조건에 따라 다르게 책정" },
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ function InfoList({
|
||||
}
|
||||
|
||||
export default function GreywaterExampleSection() {
|
||||
const title = "중수 재이용 사례";
|
||||
const title = "중수도 사례";
|
||||
return (
|
||||
<div className="bg-white">
|
||||
{/* 상단 타이틀 */}
|
||||
|
||||
@@ -123,7 +123,7 @@ function RainTable({ rows }: { rows: RainTableRow[] }) {
|
||||
}
|
||||
|
||||
export default function RainExampleSection() {
|
||||
const title = "빗물 재이용 사례";
|
||||
const title = "빗물이용시설 사례";
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
|
||||
@@ -334,7 +334,7 @@ const sewageCases: SewageCase[] = [
|
||||
];
|
||||
|
||||
export default function SewageExampleSection() {
|
||||
const title = "하수처리 재이용 사례";
|
||||
const title = "하수처리수 재이용 사례";
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
|
||||
@@ -115,11 +115,11 @@ export default function GreywaterPage() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<SubPageHeader
|
||||
title="중수 재이용 사례"
|
||||
title="중수도 사례"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "중수 재이용 사례" },
|
||||
{ label: "중수도 사례" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 물 재이용 사례
|
||||
*/
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import BusinessSubNav, { type BusinessSubNavMenu } from "@/components/BusinessSubNav";
|
||||
|
||||
@@ -9,7 +12,7 @@ import GlobalCaseSection from "./_components/GlobalCaseSection";
|
||||
const menus: BusinessSubNavMenu[] = [
|
||||
{
|
||||
key: "example-rain",
|
||||
label: "빗물 재이용 사례",
|
||||
label: "빗물이용시설 사례",
|
||||
sections: [
|
||||
{ id: "example-rain-suwon", label: "경기도 수원시" },
|
||||
{ id: "example-rain-jeju", label: "제주특별자치도" },
|
||||
@@ -17,7 +20,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
key: "example-greywater",
|
||||
label: "중수 재이용 사례",
|
||||
label: "중수도 사례",
|
||||
defaultOpen: false,
|
||||
sections: [
|
||||
{ id: "example-greywater-ansan", label: "경기도 안산시" },
|
||||
@@ -27,7 +30,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
key: "example-sewage",
|
||||
label: "하수처리 재이용 사례",
|
||||
label: "하수처리수 재이용 사례",
|
||||
defaultOpen: false,
|
||||
sections: [
|
||||
{ id: "example-sewage-guri", label: "경기도 구리시" },
|
||||
|
||||
@@ -24,11 +24,11 @@ export default function RainPage() {
|
||||
<BusinessSubNav />
|
||||
|
||||
<SubPageHeader
|
||||
title="빗물 재이용 사례"
|
||||
title="빗물이용시설 사례"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/main" },
|
||||
{ label: "물 재이용 사례", href: "/example/rain" },
|
||||
{ label: "빗물 재이용 사례" },
|
||||
{ label: "빗물이용시설 사례" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
|
||||
@@ -56,7 +56,7 @@ function SectionShell({
|
||||
}
|
||||
|
||||
export default function LawIntroSection() {
|
||||
const title = "물 재이용 법";
|
||||
const title = "물재이용법";
|
||||
|
||||
return (
|
||||
<div className="bg-white">
|
||||
|
||||
@@ -54,7 +54,7 @@ function SectionShell({
|
||||
}
|
||||
|
||||
export default function RainIntroSection() {
|
||||
const title = "빗물 이용시설";
|
||||
const title = "빗물이용시설";
|
||||
|
||||
return (
|
||||
<div className="mt-[120px] bg-white">
|
||||
|
||||
@@ -52,7 +52,7 @@ function SectionShell({
|
||||
}
|
||||
|
||||
export default function TreatedIntroSection() {
|
||||
const title = "하수처리수";
|
||||
const title = "하수처리수 재이용";
|
||||
|
||||
return (
|
||||
<div className="mt-[120px] bg-white">
|
||||
|
||||
@@ -9,7 +9,7 @@ import TreatedIntroSection from "./_components/TreatedIntroSection";
|
||||
const menus: BusinessSubNavMenu[] = [
|
||||
{
|
||||
key: "intro-law",
|
||||
label: "물 재이용 법",
|
||||
label: "물재이용법",
|
||||
sections: [
|
||||
{ id: "law-plan", label: "법률 제·개정 및 기본계획 수립" },
|
||||
{ id: "plan-performance", label: "제2차 계획 대비 실적 현황" },
|
||||
@@ -21,7 +21,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
key: "intro-rain",
|
||||
label: "빗물 이용시설",
|
||||
label: "빗물이용시설",
|
||||
defaultOpen: false,
|
||||
sections: [
|
||||
{ id: "rain-intro", label: "빗물 재이용 소개" },
|
||||
@@ -48,7 +48,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
key: "intro-treated",
|
||||
label: "하수처리수",
|
||||
label: "하수처리수 재이용",
|
||||
defaultOpen: false,
|
||||
sections: [
|
||||
{ id: "treatedwater-intro", label: "하수처리수 소개" },
|
||||
|
||||
@@ -287,7 +287,7 @@ export default function SewageStatusSection() {
|
||||
const isSubItem = type === "red-item" || type === "blue-item";
|
||||
|
||||
if (index === 0) {
|
||||
console.log(header.id, value);
|
||||
// console.log(header.id, value);
|
||||
return (
|
||||
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} relative`}>
|
||||
{isSubItem ? (
|
||||
|
||||
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
|
||||
},
|
||||
{
|
||||
id: "status-treated-usage",
|
||||
label: "연도별 하수처리수 재이용 현황",
|
||||
label: "용도별 하수처리수 재이용 현황",
|
||||
},
|
||||
{
|
||||
id: "status-treated-facility",
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function RootLayout({
|
||||
<title>물재이용 대민</title>
|
||||
</head>
|
||||
<body>
|
||||
<div className="min-w-[1400px] overflow-x-auto">
|
||||
<div className="min-w-[1400px]">
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// app/main/layout.tsx
|
||||
import "../reuse.module.css";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import { StoreProvider } from "../store";
|
||||
|
||||
export default function ReuseLandingLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<StoreProvider>
|
||||
<TopNav />
|
||||
<div className="pt-[83px]">{children}</div> {/* TopNav 높이만큼 */}
|
||||
</StoreProvider>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export const StoreZus = (
|
||||
return createStore<DmStore>()((set) => {
|
||||
if (typeof window !== "undefined") {
|
||||
tokenManager.setToken("site-id-key", process.env.NEXT_PUBLIC_SITE_ID||"DM");
|
||||
tokenManager.setToken("site-theme-key", "green");
|
||||
tokenManager.setToken("site-theme-key", "white");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 우측 메뉴
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
@@ -47,44 +50,67 @@ export default function BusinessSubNav({
|
||||
setOpenMenus(initialOpenState);
|
||||
}, [menus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!allSections.length) return;
|
||||
useEffect(() => {
|
||||
if (!allSections.length) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
|
||||
const targets = allSections
|
||||
.map((section) => document.getElementById(section.id))
|
||||
.filter(Boolean) as HTMLElement[];
|
||||
|
||||
if (visible[0]) {
|
||||
setActiveId(visible[0].target.id);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: `-${offsetTop}px 0px -55% 0px`,
|
||||
threshold: [0.2, 0.35, 0.5, 0.7],
|
||||
if (!targets.length) return;
|
||||
|
||||
let rafId = 0;
|
||||
|
||||
const updateActiveSection = () => {
|
||||
const baseLine = offsetTop + 500;
|
||||
let nextActiveId = targets[0]?.id ?? "";
|
||||
|
||||
for (const el of targets) {
|
||||
const { top } = el.getBoundingClientRect();
|
||||
if (top <= baseLine) {
|
||||
nextActiveId = el.id;
|
||||
continue;
|
||||
}
|
||||
);
|
||||
|
||||
const targets = allSections
|
||||
.map((section) => document.getElementById(section.id))
|
||||
.filter(Boolean) as HTMLElement[];
|
||||
break;
|
||||
}
|
||||
|
||||
targets.forEach((el) => observer.observe(el));
|
||||
setActiveId((prev) => (prev === nextActiveId ? prev : nextActiveId));
|
||||
};
|
||||
|
||||
const hash = window.location.hash.replace("#", "");
|
||||
const initialActive =
|
||||
allSections.find((section) => section.id === hash)?.id ??
|
||||
allSections[0]?.id ??
|
||||
"";
|
||||
const requestUpdate = () => {
|
||||
if (rafId) return;
|
||||
|
||||
setActiveId(initialActive);
|
||||
rafId = window.requestAnimationFrame(() => { // 예약번호 반환
|
||||
updateActiveSection();
|
||||
rafId = 0;
|
||||
});
|
||||
};
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [allSections, offsetTop]);
|
||||
const hash = window.location.hash.replace("#", "");
|
||||
const initialActive =
|
||||
allSections.find((section) => section.id === hash)?.id ??
|
||||
allSections[0]?.id ??
|
||||
"";
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId) return;
|
||||
setActiveId(initialActive);
|
||||
updateActiveSection();
|
||||
|
||||
window.addEventListener("scroll", requestUpdate, { passive: true });
|
||||
window.addEventListener("resize", requestUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", requestUpdate);
|
||||
window.removeEventListener("resize", requestUpdate);
|
||||
|
||||
if (rafId) {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
}
|
||||
};
|
||||
}, [allSections, offsetTop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId) return;
|
||||
|
||||
const parentMenu = menus.find((menu) =>
|
||||
(menu.sections ?? []).some((section) => section.id === activeId)
|
||||
@@ -93,13 +119,13 @@ export default function BusinessSubNav({
|
||||
if (!parentMenu) return;
|
||||
|
||||
setOpenMenus(() => {
|
||||
const nextOpenState: Record<string, boolean> = {};
|
||||
const nextOpenState: Record<string, boolean> = {};
|
||||
|
||||
menus.forEach((menu) => {
|
||||
menus.forEach((menu) => {
|
||||
nextOpenState[menu.key] = menu.key === parentMenu.key;
|
||||
});
|
||||
});
|
||||
|
||||
return nextOpenState;
|
||||
return nextOpenState;
|
||||
});
|
||||
}, [activeId, menus]);
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ type CaseItem = {
|
||||
};
|
||||
|
||||
const TABS: { key: TabKey; label: string }[] = [
|
||||
{ key: "rain", label: "빗물 재이용 사례" },
|
||||
{ key: "greywater", label: "중수 재이용 사례" },
|
||||
{ key: "rain", label: "빗물이용시설 사례" },
|
||||
{ key: "greywater", label: "중수도 사례" },
|
||||
{ key: "sewage", label: "하수 재이용 사례" },
|
||||
{ key: "global", label: "해외 재이용 사례" },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// apps/dm/src/entities/content/
|
||||
// contents-api.ts
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ENDPOINT } from "@/src/shared/config";
|
||||
|
||||
|
||||
|
||||
export const getCachedContentDetail = unstable_cache(
|
||||
async (contsSn: number, menuCd: string) => {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_PROXY_HOST || "http://localhost:3015";
|
||||
const res = await fetch(
|
||||
`${baseUrl}${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}?menuCd=${menuCd}`,
|
||||
{
|
||||
next: { revalidate: 3600, tags: [`content:${contsSn}`] },
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.log(res);
|
||||
throw new Error("Failed to fetch content");
|
||||
}
|
||||
|
||||
return res.json();
|
||||
},
|
||||
["dm-content-detail"],
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 컨텐츠 서버 페이지
|
||||
*/
|
||||
import { getCachedContentDetail } from "./contents-api";
|
||||
import { ContentsPageClient } from "./contentsPageClient";
|
||||
|
||||
type ContentsPageProps = {
|
||||
contsSn: number;
|
||||
menuCd: string;
|
||||
};
|
||||
|
||||
async function ContentsPage({ contsSn, menuCd }: ContentsPageProps) {
|
||||
const initialContent = await getCachedContentDetail(contsSn, menuCd);
|
||||
|
||||
return <ContentsPageClient initialContent={initialContent} />;
|
||||
}
|
||||
|
||||
export { ContentsPage };
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ContentsDetail } from "@/src/entities/content/model/contents-interface";
|
||||
|
||||
type ContentsPageClientProps = {
|
||||
initialContent: ContentsDetail;
|
||||
};
|
||||
|
||||
function ContentsPageClient({ initialContent }: ContentsPageClientProps) {
|
||||
const shadowRootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shadowRootRef.current || !initialContent?.contsCn) return;
|
||||
|
||||
const shadowRoot =
|
||||
shadowRootRef.current.shadowRoot ??
|
||||
shadowRootRef.current.attachShadow({ mode: "open" });
|
||||
shadowRoot.innerHTML = "";
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.textContent = `div { font-size: 16px; line-height: 1.5; }`;
|
||||
shadowRoot.appendChild(styleElement);
|
||||
|
||||
const contentElement = document.createElement("div");
|
||||
contentElement.innerHTML = initialContent.contsCn;
|
||||
shadowRoot.appendChild(contentElement);
|
||||
}, [initialContent]);
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6 !p-6">
|
||||
<div ref={shadowRootRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { ContentsPageClient };
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentsPage } from "./contentsPage";
|
||||
@@ -60,7 +60,7 @@ export function middleware(req: NextRequest) {
|
||||
|
||||
if (pathname.startsWith("/user-service")
|
||||
|| pathname.startsWith("/cms-service")) {
|
||||
let queryParams = searchParams.toString();
|
||||
const queryParams = searchParams.toString();
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const newUrl = `${serverApiUrl}${pathname}${queryParams ? `?${queryParams}` : ""}`;
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ const nextConfig: NextConfig = {
|
||||
source: "/operation-service/:path*",
|
||||
destination: `${subDomain}/operation-service/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/cms-service/:path*",
|
||||
destination: `${subDomain}/cms-service/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 284 KiB |
@@ -0,0 +1,101 @@
|
||||
import { client } from "@/src/shared/api";
|
||||
import { ENDPOINT } from "@/src/shared/config";
|
||||
import type {
|
||||
ContentsService,
|
||||
ContentsParams,
|
||||
ContentsList,
|
||||
ContentsFormData,
|
||||
ContentsVersionParams,
|
||||
ContentsVersionList,
|
||||
ContentsDetail,
|
||||
} from "../model/contents-interface";
|
||||
|
||||
export class ContentsApiService implements ContentsService {
|
||||
public async getContentsList(params: ContentsParams): Promise<ContentsList> {
|
||||
const response = await client.get<ContentsList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLatestApprovedContentsInfo(
|
||||
contsSn: number,
|
||||
menuCd: string,
|
||||
): Promise<ContentsDetail> {
|
||||
const response = await client.get<ContentsDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
|
||||
{ params: { menuCd } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getContentsVersionList(
|
||||
params: ContentsVersionParams,
|
||||
): Promise<ContentsVersionList> {
|
||||
const response = await client.get<ContentsVersionList>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getLatestContentsInfo(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<ContentsDetail> {
|
||||
const response = await client.get<ContentsDetail>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createContents(data: ContentsFormData): Promise<void> {
|
||||
const response = await client.post<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
data: ContentsFormData,
|
||||
): Promise<void> {
|
||||
const response = await client.put<void>(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.delete(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
|
||||
);
|
||||
}
|
||||
|
||||
public async requestApprove(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approveRequest`,
|
||||
);
|
||||
}
|
||||
|
||||
public async approveContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<void> {
|
||||
await client.put(
|
||||
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}/approve`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ContentsApi = new ContentsApiService();
|
||||
@@ -0,0 +1,8 @@
|
||||
export { ContentsApi } from "./api/contents-service";
|
||||
export {
|
||||
CONTENTS_APPROVE_STATUS,
|
||||
contentsFormSchema,
|
||||
type Contents,
|
||||
type ContentsParams,
|
||||
type ContentsFormData,
|
||||
} from "./model/contents-interface";
|
||||
@@ -0,0 +1,34 @@
|
||||
interface SortConfig {
|
||||
empty: boolean;
|
||||
sorted: boolean;
|
||||
unsorted: boolean;
|
||||
}
|
||||
|
||||
interface PageableConfig {
|
||||
sort: SortConfig;
|
||||
offset: number;
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
paged: boolean;
|
||||
unpaged: boolean;
|
||||
}
|
||||
|
||||
export interface PageableList<T> {
|
||||
content: T[];
|
||||
pageable: PageableConfig;
|
||||
sort: SortConfig;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
number: number;
|
||||
numberOfElements: number;
|
||||
size: number;
|
||||
first: boolean;
|
||||
last: boolean;
|
||||
empty: boolean;
|
||||
}
|
||||
|
||||
export interface PageableParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "./api-interface";
|
||||
|
||||
export const CONTENTS_APPROVE_STATUS = {
|
||||
DRAFT: "01",
|
||||
REQUESTED: "02",
|
||||
APPROVED: "99",
|
||||
} as const;
|
||||
|
||||
export type ContentsApproveStatus =
|
||||
(typeof CONTENTS_APPROVE_STATUS)[keyof typeof CONTENTS_APPROVE_STATUS];
|
||||
|
||||
export interface ContentsParams extends PageableParams {
|
||||
contsTtl?: string;
|
||||
aprvContsVer?: number;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ContentsVersionParams extends PageableParams {
|
||||
contsSn?: number;
|
||||
contsCn?: string;
|
||||
aprvCd?: ContentsApproveStatus;
|
||||
cclCd?: string;
|
||||
koglCd?: string;
|
||||
tag?: string;
|
||||
useYn?: string;
|
||||
}
|
||||
|
||||
export interface ContentsVersion {
|
||||
contsSn: number;
|
||||
contsVer: number;
|
||||
contsCn: string;
|
||||
useYn: string;
|
||||
}
|
||||
|
||||
export const contentsSchema = z.object({
|
||||
contsSn: z.number(),
|
||||
contsTtl: z.string(),
|
||||
aprvContsVer: z.number(),
|
||||
useYn: z.string(),
|
||||
mdfcnDt: z.string(),
|
||||
mdfr: z.string(),
|
||||
mdfrNm: z.string(),
|
||||
aprvDt: z.string(),
|
||||
autzrNm: z.string(),
|
||||
workAprvCd: z.string(),
|
||||
workAprvNm: z.string(),
|
||||
workContsVer: z.number(),
|
||||
});
|
||||
|
||||
export const contentsDetailSchema = z.object({
|
||||
contsSn: z.number(),
|
||||
contsVer: z.number(),
|
||||
contsTtl: z.string(),
|
||||
contsCn: z.string(),
|
||||
contsRmrk: z.string().nullish(),
|
||||
aprvCd: z.enum([
|
||||
CONTENTS_APPROVE_STATUS.DRAFT,
|
||||
CONTENTS_APPROVE_STATUS.REQUESTED,
|
||||
CONTENTS_APPROVE_STATUS.APPROVED,
|
||||
]),
|
||||
cclCd: z.string().nullish(),
|
||||
koglCd: z.string().nullish(),
|
||||
tag: z.string().nullish(),
|
||||
aprvDt: z.string(),
|
||||
autzr: z.string(),
|
||||
useYn: z.string(),
|
||||
menuPicUserUnqIdNm: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const contentsFormSchema = contentsDetailSchema.omit({
|
||||
contsVer: true,
|
||||
aprvDt: true,
|
||||
autzr: true,
|
||||
});
|
||||
|
||||
export type Contents = z.infer<typeof contentsSchema>;
|
||||
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
|
||||
export type ContentsFormData = z.infer<typeof contentsFormSchema>;
|
||||
export type ContentsList = PageableList<Contents>;
|
||||
export type ContentsVersionList = PageableList<ContentsVersion>;
|
||||
|
||||
export interface ContentsService {
|
||||
getContentsList(params: ContentsParams): Promise<ContentsList>;
|
||||
getLatestApprovedContentsInfo(
|
||||
contsSn: number,
|
||||
menuCd: string,
|
||||
): Promise<ContentsDetail>;
|
||||
getContentsVersionList(
|
||||
params: ContentsVersionParams,
|
||||
): Promise<ContentsVersionList>;
|
||||
getLatestContentsInfo(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
): Promise<ContentsDetail>;
|
||||
createContents(data: ContentsFormData): Promise<void>;
|
||||
updateContents(
|
||||
contsSn: number,
|
||||
contsVer: number,
|
||||
data: ContentsFormData,
|
||||
): Promise<void>;
|
||||
deleteContents(contsSn: number, contsVer: number): Promise<void>;
|
||||
requestApprove(contsSn: number, contsVer: number): Promise<void>;
|
||||
approveContents(contsSn: number, contsVer: number): Promise<void>;
|
||||
}
|
||||
@@ -3,13 +3,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import { UiTable } from "@common/business_components/ui";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_LIST_HEADLIST,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
} from "./agedPipeUtils";
|
||||
import { downloadAgedPipeCsv } from "./agedPipeExport";
|
||||
|
||||
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
@@ -40,6 +40,7 @@ export default function AgedPipePopupPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [downloadProgressText, setDownloadProgressText] = useState("");
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
@@ -122,7 +123,7 @@ export default function AgedPipePopupPage() {
|
||||
return () => window.removeEventListener("message", handleFilterSyncMessage);
|
||||
}, [loadRows]);
|
||||
|
||||
const handleSearch = (page = 1) => {
|
||||
const handleSearch = async (page = 1) => {
|
||||
if (!validateElapsedYears(Number(filters.elapsedYears))) {
|
||||
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||||
return;
|
||||
@@ -130,7 +131,7 @@ export default function AgedPipePopupPage() {
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
setFilters(normalized);
|
||||
loadRows(page, normalized);
|
||||
await loadRows(page, normalized);
|
||||
};
|
||||
|
||||
const handleRowClick = (index?: number) => {
|
||||
@@ -156,17 +157,28 @@ export default function AgedPipePopupPage() {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchRows(1, filters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
setDownloadProgressText("CSV export 작업을 준비하고 있습니다.");
|
||||
await downloadAgedPipeCsv(filters, (job) => {
|
||||
if (job.status === "QUEUED") {
|
||||
setDownloadProgressText("CSV export 작업이 대기 중입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.status === "RUNNING") {
|
||||
setDownloadProgressText(
|
||||
`CSV export 생성 중입니다. (${job.progressPercent ?? 0}%)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
setDownloadProgressText("");
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
setDownloadProgressText("");
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "노후관로 CSV 다운로드 중 오류가 발생했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -202,6 +214,12 @@ export default function AgedPipePopupPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{downloadProgressText ? (
|
||||
<div className="border-b border-slate-200 bg-emerald-50 px-5 py-2 text-[1rem] text-emerald-700">
|
||||
{downloadProgressText}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section className="overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[160px_320px_320px_auto] md:items-end md:justify-between">
|
||||
<label className="block md:max-w-[160px]">
|
||||
@@ -221,7 +239,7 @@ export default function AgedPipePopupPage() {
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") handleSearch(1);
|
||||
if (event.key === "Enter") void handleSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
@@ -270,7 +288,7 @@ export default function AgedPipePopupPage() {
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={() => handleSearch(1)}
|
||||
onClick={() => void handleSearch(1)}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
@@ -293,7 +311,7 @@ export default function AgedPipePopupPage() {
|
||||
pageSize={10}
|
||||
total={total}
|
||||
cellClick={(index) => handleRowClick(index)}
|
||||
onPageChange={(page) => handleSearch(page)}
|
||||
onPageChange={(page) => void handleSearch(page)}
|
||||
curPage={currentPage}
|
||||
index={selectedIndex}
|
||||
widthFixed={true}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { postBlobData } from "@/components/lib/api";
|
||||
import { fileDownload } from "@/components/lib/utils/fileDownload";
|
||||
import {
|
||||
AGED_PIPE_EXCEL_FIELDS,
|
||||
normalizeAgedPipeFilters,
|
||||
type AgedPipeFilters,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
type AgedPipeExportProgress = {
|
||||
fileName?: string;
|
||||
progressPercent?: number;
|
||||
status: "QUEUED" | "RUNNING" | "COMPLETED";
|
||||
};
|
||||
|
||||
/** 노후관로 CSV 다운로드를 요청한다. */
|
||||
export const downloadAgedPipeCsv = async (
|
||||
filters: AgedPipeFilters,
|
||||
onProgress?: (progress: AgedPipeExportProgress) => void,
|
||||
) => {
|
||||
const normalizedFilters = normalizeAgedPipeFilters(filters);
|
||||
|
||||
onProgress?.({ status: "RUNNING", progressPercent: 0 });
|
||||
|
||||
const { blob, fileName } = await postBlobData("v1/fclts/agedPipes/exportJobs/download", {
|
||||
elapsedYears: normalizedFilters.elapsedYears,
|
||||
ctpvCd: normalizedFilters.ctpvCd,
|
||||
sggCd: normalizedFilters.sggCd,
|
||||
format: "csv",
|
||||
columns: [...AGED_PIPE_EXCEL_FIELDS],
|
||||
});
|
||||
|
||||
const resolvedFileName = fileName || "노후관로_현황.csv";
|
||||
fileDownload(blob, resolvedFileName);
|
||||
onProgress?.({ status: "COMPLETED", fileName: resolvedFileName, progressPercent: 100 });
|
||||
|
||||
return {
|
||||
status: "COMPLETED" as const,
|
||||
fileName: resolvedFileName,
|
||||
};
|
||||
};
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
export const AGED_PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||
export const AGED_PIPE_DEFAULT_ELAPSED_YEARS = 30;
|
||||
export const AGED_PIPE_PAGE_SIZE = 10;
|
||||
export const AGED_PIPE_MIN_ZOOM = 14;
|
||||
export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101", "19990101"];
|
||||
export const AGED_PIPE_MIN_ZOOM = 13;
|
||||
export const AGED_PIPE_GUIDE_MIN_ZOOM = 13.1;
|
||||
export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101"];
|
||||
|
||||
export type AgedPipeFilters = {
|
||||
elapsedYears: number;
|
||||
@@ -46,6 +47,12 @@ export const AGED_PIPE_DEFAULT_FILTERS: AgedPipeFilters = {
|
||||
sggCd: "0",
|
||||
};
|
||||
|
||||
const normalizeRegionFilterCode = (value: unknown, prefixLength: number) => {
|
||||
const digits = String(value ?? "").replace(/[^0-9]/g, "");
|
||||
if (digits === "" || digits === "0") return "0";
|
||||
return digits.length > prefixLength ? digits.slice(0, prefixLength) : digits;
|
||||
};
|
||||
|
||||
export const AGED_PIPE_LIST_HEADLIST = [
|
||||
{ id: "rowNumber", title: "NO", widthPercent: 6 },
|
||||
{ id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 },
|
||||
@@ -57,8 +64,61 @@ export const AGED_PIPE_LIST_HEADLIST = [
|
||||
{ id: "elapsedYears", title: "경과년도", widthPercent: 12, align: "right" },
|
||||
];
|
||||
|
||||
export const AGED_PIPE_EXCEL_FIELDS = [
|
||||
"ftreSmbolCd",
|
||||
"fcltMngNo",
|
||||
"pbadmsEmdCd",
|
||||
"mapshtNo",
|
||||
"mngInstCd",
|
||||
"instlYmd",
|
||||
"usgCd",
|
||||
"mqltCd",
|
||||
"fcltyShpCd",
|
||||
"pdmtVl",
|
||||
"wdthLen",
|
||||
"vrtcLen",
|
||||
"prlgVl",
|
||||
"tmptDpth",
|
||||
"scendDpth",
|
||||
"avgGrdn",
|
||||
"trflnCnt",
|
||||
"exclnMltvArea",
|
||||
"drtwMltvArea",
|
||||
"rnyssnFvlcVl",
|
||||
"clarFvlcVl",
|
||||
"tmptMhlMngNo",
|
||||
"scendMhlMngNo",
|
||||
"instMngNo",
|
||||
"fid",
|
||||
"lyrTypeNm",
|
||||
"swtrmtFcltCd",
|
||||
"elapsedYears",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
] as const;
|
||||
|
||||
export const AGED_PIPE_EXCEL_HEADER = {
|
||||
rowNumber: "NO",
|
||||
ftreSmbolCd: "지형지물부호코드",
|
||||
pbadmsEmdCd: "행정읍면동코드",
|
||||
mapshtNo: "도엽번호",
|
||||
mngInstCd: "관리기관코드",
|
||||
wdthLen: "가로길이",
|
||||
vrtcLen: "세로길이",
|
||||
tmptDpth: "시점깊이",
|
||||
scendDpth: "종점깊이",
|
||||
avgGrdn: "평균구배",
|
||||
trflnCnt: "차선수",
|
||||
exclnMltvArea: "우수배수면적",
|
||||
drtwMltvArea: "오수배수면적",
|
||||
rnyssnFvlcVl: "우기유속값",
|
||||
clarFvlcVl: "맑음유속값",
|
||||
tmptMhlMngNo: "시점맨홀관리번호",
|
||||
scendMhlMngNo: "종점맨홀관리번호",
|
||||
instMngNo: "기관관리번호",
|
||||
fid: "FID",
|
||||
lyrTypeNm: "레이어타입명",
|
||||
swtrmtFcltCd: "시설코드",
|
||||
fcltyShpCd: "시설물형태코드",
|
||||
fcltMngNo: "시설관리번호",
|
||||
instlYmd: "설치일자",
|
||||
pdmtVl: "관경(mm)",
|
||||
@@ -70,6 +130,21 @@ export const AGED_PIPE_EXCEL_HEADER = {
|
||||
sggNm: "시군구",
|
||||
};
|
||||
|
||||
const getExcelColumnLetter = (columnIndex: number) => {
|
||||
let current = columnIndex;
|
||||
let result = "";
|
||||
|
||||
while (current > 0) {
|
||||
const remainder = (current - 1) % 26;
|
||||
result = String.fromCharCode(65 + remainder) + result;
|
||||
current = Math.floor((current - 1) / 26);
|
||||
}
|
||||
|
||||
return result || "A";
|
||||
};
|
||||
|
||||
export const AGED_PIPE_EXCEL_HEADER_RANGE = `A1:${getExcelColumnLetter(AGED_PIPE_EXCEL_FIELDS.length)}1`;
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const normalizeYmd = (value: unknown) =>
|
||||
@@ -82,6 +157,8 @@ export const normalizeAgedPipeFilters = (filters: AgedPipeFilters): AgedPipeFilt
|
||||
|
||||
return {
|
||||
...filters,
|
||||
ctpvCd: normalizeRegionFilterCode(filters.ctpvCd, 2),
|
||||
sggCd: normalizeRegionFilterCode(filters.sggCd, 5),
|
||||
elapsedYears:
|
||||
Number.isFinite(elapsedYears) && elapsedYears >= 1 && elapsedYears <= 100
|
||||
? elapsedYears
|
||||
@@ -224,6 +301,16 @@ export const toDisplayPipeRow = (
|
||||
};
|
||||
};
|
||||
|
||||
export const toAgedPipeExcelRow = (row: AgedPipeRow) => {
|
||||
const exportRow: Record<string, unknown> = {};
|
||||
|
||||
AGED_PIPE_EXCEL_FIELDS.forEach((field) => {
|
||||
exportRow[field] = row[field] ?? "-";
|
||||
});
|
||||
|
||||
return exportRow;
|
||||
};
|
||||
|
||||
export const isAgedPipeLayerNode = (node: any) => {
|
||||
const data = node?.data ?? node;
|
||||
const layerName = String(data?.lyrNm ?? node?.text ?? "");
|
||||
|
||||
@@ -1,50 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ChartColumnBig,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
History,
|
||||
PanelsTopLeft,
|
||||
PencilRuler,
|
||||
Search,
|
||||
Settings2,
|
||||
TriangleAlert,
|
||||
Waves,
|
||||
} from "lucide-react";
|
||||
import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
|
||||
type Props = {
|
||||
onOpenPopup: (popupKey: WindowPopupKey) => void;
|
||||
onOpenInfoSearch: () => void;
|
||||
};
|
||||
|
||||
const ANALYSIS_POPUP_ITEMS: Array<{ key: WindowPopupKey; label: string }> = [
|
||||
{ key: "pipeAnalysisPopup", label: "관로분석" },
|
||||
{ key: "floodPredictionMapPopup", label: "침수예측지도" },
|
||||
{ key: "floodHistoryPopup", label: "침수발생이력" },
|
||||
{ key: "gisEditPopup", label: "GIS 편집" },
|
||||
{ key: "gisManagementPopup", label: "GIS 관리" },
|
||||
{ key: "flowRiskPopup", label: "통수 위험" },
|
||||
const ANALYSIS_POPUP_ITEMS: Array<{
|
||||
key: WindowPopupKey | "infoSearch";
|
||||
label: string;
|
||||
icon: typeof ChartColumnBig;
|
||||
}> = [
|
||||
{ key: "infoSearch", label: "정보검색", icon: Search },
|
||||
{ key: "pipeAnalysisPopup", label: "관로분석", icon: ChartColumnBig },
|
||||
{ key: "floodPredictionMapPopup", label: "침수예측지도", icon: Waves },
|
||||
{ key: "floodHistoryPopup", label: "침수발생이력", icon: History },
|
||||
{ key: "gisEditPopup", label: "GIS 편집", icon: PencilRuler },
|
||||
{ key: "gisManagementPopup", label: "GIS 관리", icon: Settings2 },
|
||||
{ key: "flowRiskPopup", label: "통수 위험", icon: TriangleAlert },
|
||||
];
|
||||
|
||||
export default function AnalysisPopupMenu({ onOpenPopup }: Props) {
|
||||
export default function AnalysisPopupMenu({ onOpenPopup, onOpenInfoSearch }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [activeKey, setActiveKey] = useState<WindowPopupKey | "infoSearch" | null>(null);
|
||||
|
||||
return (
|
||||
<div className="absolute right-[0.35rem] top-[53%] z-[70]">
|
||||
<div className="absolute right-[0.55rem] top-[20px] z-[45]">
|
||||
<div className="relative">
|
||||
{isOpen ? (
|
||||
<div className="absolute right-[21px] top-1/2 flex w-[86px] -translate-y-1/2 flex-col gap-[3px]">
|
||||
{ANALYSIS_POPUP_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className="min-h-[40px] rounded-md border border-primary bg-primary px-2 py-1 text-center text-[1rem] font-semibold leading-[1.2] text-white shadow transition hover:brightness-95"
|
||||
onClick={() => onOpenPopup(item.key)}
|
||||
>
|
||||
<span className="break-keep whitespace-normal">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="absolute right-[28px] w-[132px] overflow-hidden rounded-[6px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
|
||||
<div className="flex h-[36px] items-center gap-2 border-b border-[#e5eaf1] bg-[#f8fafc] px-[10px] text-[13px] font-bold text-[#1f2937]">
|
||||
<div className="flex items-center gap-2">
|
||||
<PanelsTopLeft className="h-4 w-4 text-[#1e88e5]" />
|
||||
<span>분석도구</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 p-[6px]">
|
||||
{ANALYSIS_POPUP_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeKey === item.key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`relative flex h-[34px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition ${
|
||||
isActive
|
||||
? "border-[#b9dcff] bg-[#eaf4ff] font-bold text-[#0f5fa8] before:absolute before:left-0 before:top-[7px] before:bottom-[7px] before:w-[3px] before:rounded-r-[2px] before:bg-[#1e88e5]"
|
||||
: "border-transparent bg-white text-[#374151] hover:border-[#cfe5ff] hover:bg-[#eef6ff] hover:text-[#1e3a5f]"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setActiveKey(item.key);
|
||||
|
||||
if (item.key === "infoSearch") {
|
||||
onOpenInfoSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
onOpenPopup(item.key);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className={`h-[15px] w-[15px] shrink-0 ${
|
||||
isActive ? "text-[#1e88e5]" : "text-[#64748b]"
|
||||
}`}
|
||||
/>
|
||||
<span className="break-keep whitespace-normal">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-[36px] w-[18px] items-center justify-center rounded-sm border border-[#2f5c9b] bg-white text-[0.9rem] font-bold text-[#2f5c9b] shadow transition hover:bg-slate-50"
|
||||
className="absolute right-0 top-0 flex h-[36px] w-[20px] items-center justify-center rounded-[4px] border border-[#d8dee8] bg-[rgba(255,255,255,0.96)] text-[#64748b] shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-[#eaf4ff] hover:text-[#1e88e5]"
|
||||
onClick={() => setIsOpen((value) => !value)}
|
||||
title={isOpen ? "분석 메뉴 접기" : "분석 메뉴 펼치기"}
|
||||
aria-label={isOpen ? "분석 메뉴 접기" : "분석 메뉴 펼치기"}
|
||||
>
|
||||
{isOpen ? ">" : "<"}
|
||||
{isOpen ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+30
-2
@@ -18,6 +18,7 @@ import {
|
||||
buildPipeAnalysisScope,
|
||||
normalizeNumberInput,
|
||||
resolveElapsedYearBucketLabel,
|
||||
resolveMaterialKoreanName,
|
||||
resolveMaterialName,
|
||||
resolvePipeAnalysisTypeLabel,
|
||||
resolveRegionName,
|
||||
@@ -27,6 +28,19 @@ import {
|
||||
} from "./pipeAnalysisUtils";
|
||||
|
||||
type DetailViewRow = ReturnType<typeof toPipeAnalysisDetailViewRows>[number];
|
||||
const PIPE_ANALYSIS_DETAIL_EXPORT_KEYS = [
|
||||
"rowNo",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"fcltMngNo",
|
||||
"instMngNo",
|
||||
"swtrmtZoneNm",
|
||||
"instlYmd",
|
||||
"useNm",
|
||||
"pdmtVl",
|
||||
"prlgVl",
|
||||
"materialName",
|
||||
] as const;
|
||||
|
||||
export default function PipeAnalysisDetailPopupPage() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -151,11 +165,25 @@ export default function PipeAnalysisDetailPopupPage() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchRows(1, filters, Math.max(total, 1000));
|
||||
const exportRows = response.rows.map((row) => ({
|
||||
rowNo: row.rowNo,
|
||||
ctpvNm: row.ctpvNm,
|
||||
sggNm: row.sggNm,
|
||||
fcltMngNo: row.fcltMngNo,
|
||||
instMngNo: row.instMngNo,
|
||||
swtrmtZoneNm: row.swtrmtZoneNm,
|
||||
instlYmd: row.instlYmd,
|
||||
useNm: row.useNm,
|
||||
pdmtVl: row.pdmtVl,
|
||||
prlgVl: row.prlgVl,
|
||||
materialName: resolveMaterialKoreanName(row.materialCode) || row.materialName,
|
||||
}));
|
||||
const detailHeaderRange = `A1:${String.fromCharCode(64 + PIPE_ANALYSIS_DETAIL_EXPORT_KEYS.length)}1`;
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as unknown as JSON[],
|
||||
data: exportRows as unknown as JSON[],
|
||||
fileName: `관로분석_관로현황_${buildPipeAnalysisFileDate()}`,
|
||||
sheetNameArray: ["관로현황"],
|
||||
headerRange: "A1:K1",
|
||||
headerRange: detailHeaderRange,
|
||||
headerMapping: PIPE_ANALYSIS_DETAIL_EXCEL_HEADERS,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -519,17 +519,69 @@ export const resolveMaterialName = (materialCode?: string) => {
|
||||
return materialMap.get(code) ?? code;
|
||||
};
|
||||
|
||||
/**
|
||||
* 관재질 라벨에서 영문 약어/코드를 걷어내고 엑셀 헤더용 한글명을 우선 추출한다.
|
||||
* - `염화비닐관(PVC)` -> `염화비닐관`
|
||||
* - `PCF관(강판보강 PE 나선형관)` -> `강판보강 나선형관`
|
||||
*/
|
||||
const toKoreanMaterialLabel = (value?: string) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
if (!normalized) return "";
|
||||
|
||||
const withoutParenthesis = normalized
|
||||
.replace(/\s*\([^)]*\)\s*/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const withoutAsciiToken = withoutParenthesis
|
||||
.replace(/[A-Za-z0-9-]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
if (/[가-힣]/.test(withoutAsciiToken)) {
|
||||
return withoutAsciiToken;
|
||||
}
|
||||
|
||||
if (/[가-힣]/.test(withoutParenthesis)) {
|
||||
return withoutParenthesis;
|
||||
}
|
||||
|
||||
const parenthesisMatches = Array.from(normalized.matchAll(/\(([^)]*)\)/g))
|
||||
.map((match) => String(match[1] ?? "").trim())
|
||||
.filter(Boolean);
|
||||
const koreanParenthesisText = parenthesisMatches.find((text) => /[가-힣]/.test(text));
|
||||
|
||||
if (koreanParenthesisText) {
|
||||
const simplifiedParenthesisText = koreanParenthesisText
|
||||
.replace(/[A-Za-z0-9-]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return simplifiedParenthesisText || koreanParenthesisText;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export const resolveMaterialColumnName = (column: PipeAnalysisMaterialColumn) => {
|
||||
const rawName = String(column.materialName ?? "").trim();
|
||||
const rawCode = String(column.materialCode ?? "").trim();
|
||||
const rawKey = String(column.key ?? "").trim();
|
||||
if (!rawName) {
|
||||
return resolveMaterialName(rawCode || rawKey);
|
||||
}
|
||||
if (rawName === rawCode || rawName === rawKey) {
|
||||
return resolveMaterialName(rawCode || rawKey);
|
||||
}
|
||||
return rawName;
|
||||
const mappedName = resolveMaterialName(rawCode || rawKey);
|
||||
const candidates = [
|
||||
toKoreanMaterialLabel(mappedName),
|
||||
toKoreanMaterialLabel(rawName),
|
||||
mappedName,
|
||||
rawName,
|
||||
rawCode,
|
||||
rawKey,
|
||||
].filter(Boolean);
|
||||
|
||||
return candidates[0] ?? "-";
|
||||
};
|
||||
|
||||
export const resolveMaterialKoreanName = (materialCode?: string) => {
|
||||
const resolved = resolveMaterialName(materialCode);
|
||||
const koreanLabel = toKoreanMaterialLabel(resolved);
|
||||
return koreanLabel || resolved;
|
||||
};
|
||||
|
||||
export const resolveElapsedYearBucketLabel = (bucket?: string) => {
|
||||
|
||||
@@ -36,11 +36,9 @@ import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
import { Button } from "@common/components";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useStore from "@/store/useStore";
|
||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
|
||||
import type { PipeAnalysisDetailRow } from "./components/pipeAnalysis/pipeAnalysisUtils";
|
||||
import {
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_MIN_ZOOM,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
@@ -53,7 +51,9 @@ import {
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_GUIDE_MIN_ZOOM,
|
||||
} from "./components/agedPipe/agedPipeUtils";
|
||||
import { downloadAgedPipeCsv } from "./components/agedPipe/agedPipeExport";
|
||||
import { PIPE_ANALYSIS_DETAIL_ROW_SELECT_MESSAGE } from "./components/pipeAnalysis/pipeAnalysisUtils";
|
||||
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
|
||||
import type { WindowPopupKey } from "@/features/windowPopup/model/popupDefinitions";
|
||||
@@ -528,6 +528,7 @@ const Map = () => {
|
||||
const [agedPipeTotalCount, setAgedPipeTotalCount] = useState(0);
|
||||
const [agedPipeCurrentPage, setAgedPipeCurrentPage] = useState(1);
|
||||
const [isAgedPipeListLoading, setIsAgedPipeListLoading] = useState(false);
|
||||
const [agedPipeDownloadProgressText, setAgedPipeDownloadProgressText] = useState("");
|
||||
const { ctpv, sgg } = useStore();
|
||||
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||
@@ -1018,21 +1019,32 @@ const Map = () => {
|
||||
|
||||
try {
|
||||
setIsAgedPipeListLoading(true);
|
||||
const response = await fetchAgedPipeRows(1, appliedAgedPipeFilters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
setAgedPipeDownloadProgressText("CSV export 작업을 준비하고 있습니다.");
|
||||
await downloadAgedPipeCsv(appliedAgedPipeFilters, (job) => {
|
||||
if (job.status === "QUEUED") {
|
||||
setAgedPipeDownloadProgressText("CSV export 작업이 대기 중입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.status === "RUNNING") {
|
||||
setAgedPipeDownloadProgressText(
|
||||
`CSV export 생성 중입니다. (${job.progressPercent ?? 0}%)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
setAgedPipeDownloadProgressText("");
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
setAgedPipeDownloadProgressText("");
|
||||
alert(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "노후관로 CSV 다운로드 중 오류가 발생했습니다.",
|
||||
);
|
||||
} finally {
|
||||
setIsAgedPipeListLoading(false);
|
||||
}
|
||||
}, [agedPipeTotalCount, appliedAgedPipeFilters, fetchAgedPipeRows]);
|
||||
}, [agedPipeTotalCount, appliedAgedPipeFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAgedPipePopupMessage = (event: MessageEvent) => {
|
||||
@@ -1224,13 +1236,21 @@ const Map = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
|
||||
<div className="sn-map-control-layout bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
|
||||
{layerLoadError ? (
|
||||
<div className="absolute left-4 top-4 z-[60] max-w-[520px] rounded border border-red-200 bg-white px-4 py-3 text-sm text-red-700 shadow">
|
||||
레이어 설정 조회 실패: {layerLoadError}
|
||||
</div>
|
||||
) : null}
|
||||
<AnalysisPopupMenu onOpenPopup={openAnalysisPopup} />
|
||||
{agedPipeDownloadProgressText ? (
|
||||
<div className="absolute left-4 top-20 z-[60] max-w-[520px] rounded border border-emerald-200 bg-white px-4 py-3 text-sm text-emerald-700 shadow">
|
||||
{agedPipeDownloadProgressText}
|
||||
</div>
|
||||
) : null}
|
||||
<AnalysisPopupMenu
|
||||
onOpenPopup={openAnalysisPopup}
|
||||
onOpenInfoSearch={() => setIsSearchModalOpen(true)}
|
||||
/>
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
@@ -1247,7 +1267,13 @@ const Map = () => {
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
onOpenPopup={handleOpenLayerPopup}
|
||||
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
|
||||
searchBar={
|
||||
<ProcSearchBar
|
||||
gisMaps={gisMaps}
|
||||
facilityTypes={facilityTypes}
|
||||
sidoSearchZoom={isAgedPipeChecked ? AGED_PIPE_GUIDE_MIN_ZOOM : undefined}
|
||||
/>
|
||||
}
|
||||
waterDepth={showWaterDepth ? WaterDepth : undefined}
|
||||
inlineLayerSettingView={inlineLayerSettingView}
|
||||
layerIconVisible={isAgedPipeLayerNode}
|
||||
@@ -1257,6 +1283,17 @@ const Map = () => {
|
||||
checkInterval={2500}
|
||||
checkedLayerChangeDelay={0}
|
||||
showZoomLevel={true}
|
||||
showZoomScaleInfo={true}
|
||||
showFloatingSearchButton={false}
|
||||
treeTogglePosition="top"
|
||||
zoomGuide={
|
||||
isAgedPipeChecked
|
||||
? {
|
||||
label: "노후관로 기준",
|
||||
zoom: AGED_PIPE_GUIDE_MIN_ZOOM,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
@@ -1293,6 +1330,37 @@ const Map = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<style jsx global>{`
|
||||
.sn-map-control-layout .mapbtn {
|
||||
top: auto;
|
||||
right: 15px;
|
||||
bottom: 451px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .custom-toolbar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar {
|
||||
top: auto;
|
||||
right: 15px;
|
||||
bottom: 248px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-control {
|
||||
right: 16px;
|
||||
bottom: 56px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-summary {
|
||||
right: 56px;
|
||||
bottom: -15px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .ol-scale-line {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -101,6 +101,46 @@ export const postData = async (url: string, data: object): Promise<any> => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseDownloadFileName = (contentDisposition: string | null) => {
|
||||
if (!contentDisposition) return undefined;
|
||||
|
||||
const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
|
||||
if (utf8Match?.[1]) {
|
||||
return decodeURIComponent(utf8Match[1].trim());
|
||||
}
|
||||
|
||||
const plainMatch = contentDisposition.match(/filename\s*=\s*"?([^\";]+)"?/i);
|
||||
return plainMatch?.[1]?.trim();
|
||||
};
|
||||
|
||||
export const postBlobData = async (
|
||||
url: string,
|
||||
data: object,
|
||||
): Promise<{ blob: Blob; fileName?: string }> => {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "POST",
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text().catch(() => "");
|
||||
throw new Error(
|
||||
`Failed to fetch blob: ${res.status} ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ""} (${url})`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
blob: await res.blob(),
|
||||
fileName: parseDownloadFileName(res.headers.get("content-disposition")),
|
||||
};
|
||||
};
|
||||
|
||||
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
|
||||
const res = await axios.post(`${BASE_URL}/${url}`, data, {
|
||||
headers: DEFAULT_HEADERS,
|
||||
|
||||
@@ -33,8 +33,8 @@ export const fetchCtpv = async (set: (state: any) => void) => {
|
||||
|
||||
const list = res.features
|
||||
.map((f: any) => ({
|
||||
text: f.properties.mctpv_korn_nm,
|
||||
val: f.properties.mctpv_cd,
|
||||
text: String(f.properties.mctpv_korn_nm ?? "").trim(),
|
||||
val: String(f.properties.mctpv_cd ?? "").trim(),
|
||||
}))
|
||||
.sort((a:any, b:any) => {
|
||||
// 커스텀 순서 정의
|
||||
@@ -83,8 +83,8 @@ export const fetchSgg = async (set: (state: any) => void) => {
|
||||
|
||||
const list = res.features
|
||||
.map((f: any) => ({
|
||||
text: f.properties.sgg_korn_nm,
|
||||
val: f.properties.sgg_cd,
|
||||
text: String(f.properties.sgg_korn_nm ?? "").trim(),
|
||||
val: String(f.properties.sgg_cd ?? "").trim(),
|
||||
}))
|
||||
.sort((a:any, b:any) => a.text.localeCompare(b.text, 'ko'));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@common/components";
|
||||
|
||||
interface ApproveButtonProps {
|
||||
approveFunc: () => void;
|
||||
approveFunc: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
const ApproveButton = ({ approveFunc }: ApproveButtonProps) => {
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#466995] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
/* 상태바·체크바 헤더 */
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#5987bf] !text-[#fff];
|
||||
}
|
||||
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-white;
|
||||
}
|
||||
@@ -36,6 +21,91 @@
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
@apply !font-semibold !text-[#3372cf];
|
||||
}
|
||||
/* 개별 반려: 높이/레이아웃은 RealGrid 행에 그대로 두고, 색·테두리·정렬만 버튼 느낌 */
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
box-sizing: border-box !important;
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
border-radius: 0.25rem !important;
|
||||
border: 1px solid rgb(248 113 113 / 0.55) !important;
|
||||
background-color: rgb(254 242 242) !important;
|
||||
color: rgb(185 28 28) !important;
|
||||
font-size: 1.2rem !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover {
|
||||
background-color: rgb(254 226 226) !important;
|
||||
border-color: rgb(239 68 68 / 0.85) !important;
|
||||
color: rgb(153 27 27) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:active {
|
||||
background-color: rgb(254 202 202) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn {
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
box-sizing: border-box !important;
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
border-radius: 0.25rem !important;
|
||||
border: 1px solid rgb(148 163 184 / 0.7) !important;
|
||||
background-color: rgb(248 250 252) !important;
|
||||
color: rgb(51 65 85) !important;
|
||||
font-size: 1.2rem !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap !important;
|
||||
cursor: pointer !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover {
|
||||
background-color: rgb(241 245 249) !important;
|
||||
border-color: rgb(100 116 139 / 0.85) !important;
|
||||
color: rgb(30 41 59) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:active {
|
||||
background-color: rgb(226 232 240) !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-action-link {
|
||||
@apply !font-semibold !text-[#2563eb] underline;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-pending {
|
||||
@apply !text-gray-500;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-submitted {
|
||||
@apply !font-semibold;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-approved {
|
||||
@apply !font-semibold;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected {
|
||||
@apply !font-semibold !text-[#ff6c64];
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected > * {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center;
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected > *::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
margin-left: 0.25rem;
|
||||
vertical-align: middle;
|
||||
width: 0.6125rem;
|
||||
height: 0.6125rem;
|
||||
border-radius: 9999px;
|
||||
background: #ff6c64;
|
||||
box-shadow: 0 0 0 0 rgba(255, 108, 100, 0.75);
|
||||
animation: rg-cell-stats-status-dot-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-total {
|
||||
@apply !bg-[#e3eaf2];
|
||||
}
|
||||
@@ -85,59 +155,12 @@
|
||||
.data-grid-status-bubble > span + span {
|
||||
@apply ml-4 border-l border-slate-200/70 pl-4;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="black"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#3C4043] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#62686d] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
@apply !bg-[#545556] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
||||
@apply !text-[#f87171];
|
||||
}
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
@apply !text-[#3372cf];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#646469] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#8d8d94] !text-[#fff];
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
@apply !bg-[#518585] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
@apply !bg-[#6bafae] !text-[#fff];
|
||||
}
|
||||
@keyframes rg-cell-stats-status-dot-ping {
|
||||
75%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.5rem rgba(255, 108, 100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,4 +187,130 @@
|
||||
.rg-grid .rg-head .rg-table tr td {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ST 통계 그리드 헤더 색 — @layer 밖(일반 규칙).
|
||||
* packages/rsone/components/grid/css/DataGrid.css 등 @layer 밖 !important와 동일 층에서
|
||||
* .rg-grid … 특이도로 덮어씀.
|
||||
*/
|
||||
.rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-header .rg-table {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.rg-grid .rg-header .rg-cell,
|
||||
.rg-grid .rg-head .rg-cell {
|
||||
background-color: #5987bf !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.rg-header .rg-table tr td:hover { background: #5987bf; }
|
||||
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #3c4043 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="black"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-header .rg-table tr td:hover {
|
||||
background-color: #62686d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell {
|
||||
background-color: #545556 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad {
|
||||
color: #f87171 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok {
|
||||
color: #3372cf !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn {
|
||||
background-color: rgb(127 29 29 / 0.42) !important;
|
||||
border-color: rgb(248 113 113 / 0.5) !important;
|
||||
color: #fecaca !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover {
|
||||
background-color: rgb(153 27 27 / 0.55) !important;
|
||||
border-color: rgb(252 165 165 / 0.75) !important;
|
||||
color: #fee2e2 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn {
|
||||
background-color: rgb(71 85 105 / 0.35) !important;
|
||||
border-color: rgb(148 163 184 / 0.55) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover {
|
||||
background-color: rgb(71 85 105 / 0.5) !important;
|
||||
border-color: rgb(203 213 225 / 0.65) !important;
|
||||
color: #f8fafc !important;
|
||||
}
|
||||
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #646469 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="red"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="red"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #8d8d94 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-group-cell {
|
||||
background-color: #518585 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-cell {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:first-child,
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2),
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:first-child,
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
[data-theme="green"] .rg-grid .rg-header .rg-cell,
|
||||
[data-theme="green"] .rg-grid .rg-head .rg-cell {
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,38 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function realGridHeaderWithTooltip<T extends { text: string; tooltip?: string; showTooltip?: boolean; template?: string }>(
|
||||
header: T,
|
||||
): T {
|
||||
const tip = header.tooltip?.trim();
|
||||
if (!tip) return header;
|
||||
return {
|
||||
...header,
|
||||
tooltip: tip,
|
||||
showTooltip: header.showTooltip ?? true,
|
||||
template:
|
||||
header.template ??
|
||||
`<span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;width:100%;"><span>${escapeHtml(header.text)}</span><i class="diveicon di-info-circle" aria-hidden="true"></i></span>`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveColumnTooltip(column: {
|
||||
tooltip?: string;
|
||||
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
|
||||
}): string | undefined {
|
||||
const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
|
||||
return tip && tip.length > 0 ? tip : undefined;
|
||||
}
|
||||
|
||||
const CompactDataGrid = ({
|
||||
columnInfo,
|
||||
fixedColumns,
|
||||
@@ -88,8 +120,10 @@ const CompactDataGrid = ({
|
||||
}: DataGridProps) => {
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
const columnsRef = useRef(columns);
|
||||
const rowDataRef = useRef<Record<string, string | number>[]>(rowData);
|
||||
const cellStyleCallbackRef = useRef<CellStyleCallback | undefined>(cellStyleCallback);
|
||||
columnsRef.current = columns;
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
|
||||
@@ -155,6 +189,66 @@ const CompactDataGrid = ({
|
||||
});
|
||||
|
||||
gridView.header.height = 60;
|
||||
if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
|
||||
(gridView.header as { showTooltip?: boolean }).showTooltip = true;
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
(
|
||||
gridView as {
|
||||
setColumnProperty?: (
|
||||
col: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) => void;
|
||||
}
|
||||
).setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
realGridHeaderWithTooltip({
|
||||
...column.header,
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
});
|
||||
(
|
||||
gridView as {
|
||||
onShowHeaderTooltip?: (
|
||||
grid: unknown,
|
||||
column: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
header?: { tooltip?: string };
|
||||
},
|
||||
value: string,
|
||||
layout?: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
column?: string | { name?: string; fieldName?: string };
|
||||
},
|
||||
isEllipsis?: boolean,
|
||||
) => string | undefined;
|
||||
}
|
||||
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
|
||||
const currentColumns = columnsRef.current;
|
||||
const directTip = headerColumn?.header?.tooltip?.trim();
|
||||
if (directTip) return directTip;
|
||||
|
||||
const layoutColumn =
|
||||
typeof layout?.column === "string"
|
||||
? layout.column
|
||||
: layout?.column?.name ?? layout?.column?.fieldName;
|
||||
|
||||
const col = currentColumns.find(
|
||||
(c) =>
|
||||
c.name === headerColumn?.name ||
|
||||
c.fieldName === headerColumn?.fieldName ||
|
||||
c.name === layout?.name ||
|
||||
c.fieldName === layout?.fieldName ||
|
||||
c.name === layoutColumn ||
|
||||
c.fieldName === layoutColumn,
|
||||
);
|
||||
return col ? resolveColumnTooltip(col) : undefined;
|
||||
};
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
@@ -235,7 +329,10 @@ const CompactDataGrid = ({
|
||||
const colInfo: RealGridColumnInfo = {
|
||||
name: col.name,
|
||||
fieldName: col.fieldName,
|
||||
header: col.header,
|
||||
header: realGridHeaderWithTooltip({
|
||||
...col.header,
|
||||
tooltip: resolveColumnTooltip(col),
|
||||
}),
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
@@ -427,7 +524,10 @@ const CompactDataGrid = ({
|
||||
fieldName={column.fieldName}
|
||||
valueType={column.valueType ? column.valueType : ValueType.TEXT}
|
||||
{...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })}
|
||||
header={column.header}
|
||||
header={realGridHeaderWithTooltip({
|
||||
...column.header,
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
})}
|
||||
editable={column.editable}
|
||||
{...(column.styleName != null
|
||||
? { styleName: column.styleName }
|
||||
|
||||
@@ -81,6 +81,42 @@ function createNumberComparer(
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* RealGrid 헤더 툴팁: `grid.header.showTooltip`이 비어 있으면 `column.header.showTooltip === true`인 컬럼만 표시됨.
|
||||
* `tooltip`이 있는 헤더는 info-circle 아이콘이 함께 보이도록 template도 자동 구성한다.
|
||||
*/
|
||||
function realGridHeaderWithTooltip<T extends { text: string; tooltip?: string; showTooltip?: boolean; template?: string }>(
|
||||
header: T,
|
||||
): T {
|
||||
const tip = header.tooltip?.trim();
|
||||
if (!tip) return header;
|
||||
return {
|
||||
...header,
|
||||
tooltip: tip,
|
||||
showTooltip: header.showTooltip ?? true,
|
||||
template:
|
||||
header.template ??
|
||||
`<span style="display:inline-flex;align-items:center;justify-content:center;gap:4px;width:100%;"><span>${escapeHtml(header.text)}</span><i class="diveicon di-info-circle" aria-hidden="true"></i></span>`,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveColumnTooltip(column: {
|
||||
tooltip?: string;
|
||||
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
|
||||
}): string | undefined {
|
||||
const tip = column.tooltip?.trim() || column.header.tooltip?.trim();
|
||||
return tip && tip.length > 0 ? tip : undefined;
|
||||
}
|
||||
|
||||
/** 정렬 필드 값을 숫자로 파싱 (쉼표 제거) */
|
||||
function toNum(v: unknown): number {
|
||||
if (v == null) return NaN;
|
||||
@@ -342,6 +378,7 @@ const DataGrid = ({
|
||||
cellTooltipCallback,
|
||||
onCellEdited,
|
||||
onRowClick,
|
||||
onDataCellClick,
|
||||
onItemChecked,
|
||||
onItemAllChecked,
|
||||
onScrollToBottom,
|
||||
@@ -354,6 +391,7 @@ const DataGrid = ({
|
||||
const { columns, columnLayout } = columnInfo;
|
||||
const gridRef = useRef<RealGridReact>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const columnsRef = useRef(columns);
|
||||
const sortDirRef = useRef<Record<string, "asc" | "desc" | "none">>({});
|
||||
const originalRowsRef = useRef<
|
||||
Record<string, Record<string, string | number>[]>
|
||||
@@ -366,12 +404,14 @@ const DataGrid = ({
|
||||
cellTooltipCallback,
|
||||
);
|
||||
const onCellEditedRef = useRef<typeof onCellEdited>(onCellEdited);
|
||||
const onDataCellClickRef = useRef<typeof onDataCellClick>(onDataCellClick);
|
||||
const onScrollToBottomRef = useRef<(() => void) | undefined>(
|
||||
onScrollToBottom,
|
||||
);
|
||||
const onItemCheckedRef = useRef<typeof onItemChecked>(onItemChecked);
|
||||
const onItemAllCheckedRef =
|
||||
useRef<typeof onItemAllChecked>(onItemAllChecked);
|
||||
columnsRef.current = columns;
|
||||
rowDataRef.current = rowData;
|
||||
cellStyleCallbackRef.current = cellStyleCallback;
|
||||
cellTooltipCallbackRef.current = cellTooltipCallback;
|
||||
@@ -379,6 +419,7 @@ const DataGrid = ({
|
||||
onItemCheckedRef.current = onItemChecked;
|
||||
onItemAllCheckedRef.current = onItemAllChecked;
|
||||
onCellEditedRef.current = onCellEdited;
|
||||
onDataCellClickRef.current = onDataCellClick;
|
||||
|
||||
// highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택
|
||||
useEffect(() => {
|
||||
@@ -813,6 +854,66 @@ const DataGrid = ({
|
||||
GridFitStyle.EVEN;
|
||||
gridView.displayOptions.rowHeight = 30;
|
||||
gridView.header.height = 60;
|
||||
if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) {
|
||||
(gridView.header as { showTooltip?: boolean }).showTooltip = true;
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
(
|
||||
gridView as {
|
||||
setColumnProperty?: (
|
||||
col: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) => void;
|
||||
}
|
||||
).setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
realGridHeaderWithTooltip({
|
||||
...column.header,
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
});
|
||||
(
|
||||
gridView as {
|
||||
onShowHeaderTooltip?: (
|
||||
grid: unknown,
|
||||
column: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
header?: { tooltip?: string };
|
||||
},
|
||||
value: string,
|
||||
layout?: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
column?: string | { name?: string; fieldName?: string };
|
||||
},
|
||||
isEllipsis?: boolean,
|
||||
) => string | undefined;
|
||||
}
|
||||
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
|
||||
const currentColumns = columnsRef.current;
|
||||
const directTip = headerColumn?.header?.tooltip?.trim();
|
||||
if (directTip) return directTip;
|
||||
|
||||
const layoutColumn =
|
||||
typeof layout?.column === "string"
|
||||
? layout.column
|
||||
: layout?.column?.name ?? layout?.column?.fieldName;
|
||||
|
||||
const col = currentColumns.find(
|
||||
(c) =>
|
||||
c.name === headerColumn?.name ||
|
||||
c.fieldName === headerColumn?.fieldName ||
|
||||
c.name === layout?.name ||
|
||||
c.fieldName === layout?.fieldName ||
|
||||
c.name === layoutColumn ||
|
||||
c.fieldName === layoutColumn,
|
||||
);
|
||||
return col ? resolveColumnTooltip(col) : undefined;
|
||||
};
|
||||
|
||||
// 헤더 체크박스 표시 여부 설정
|
||||
gridView.setOptions({
|
||||
@@ -820,9 +921,17 @@ const DataGrid = ({
|
||||
rowIndicator: { visible: false },
|
||||
});
|
||||
|
||||
// footer가 있는 컬럼이 있으면 footer 높이 설정
|
||||
const hasFooter = columns.some((col) => col.footer);
|
||||
if (hasFooter) {
|
||||
const applyFooterSettings = (
|
||||
targetGridView: typeof gridView,
|
||||
) => {
|
||||
const hasFooter = columns.some((col) => col.footer);
|
||||
if (!hasFooter) return;
|
||||
|
||||
targetGridView.setOptions({
|
||||
summaryMode: "aggregate",
|
||||
footer: { visible: true },
|
||||
});
|
||||
|
||||
// footer가 배열이거나 multiRow가 true인 footer가 있으면 멀티푸터 설정 (여러 행)
|
||||
const hasArrayFooter = columns.some(
|
||||
(col) => col.footer && Array.isArray(col.footer),
|
||||
@@ -837,23 +946,24 @@ const DataGrid = ({
|
||||
);
|
||||
});
|
||||
if (hasArrayFooter || hasMultiRowFooter) {
|
||||
// footer 배열의 길이를 확인하여 행 수 결정
|
||||
const arrayFooterLengths = columns
|
||||
.filter((col) => col.footer && Array.isArray(col.footer))
|
||||
.map((col) => (col.footer as unknown[]).length);
|
||||
const maxFooterLength =
|
||||
arrayFooterLengths.length > 0
|
||||
? Math.max(...arrayFooterLengths)
|
||||
: 2; // 기본값 2
|
||||
: 2;
|
||||
const footerHeights = Array.from(
|
||||
{ length: maxFooterLength },
|
||||
() => ({ height: 40 }),
|
||||
);
|
||||
gridView.setFooters?.(footerHeights);
|
||||
targetGridView.setFooters?.(footerHeights);
|
||||
} else {
|
||||
gridView.footer.height = 40;
|
||||
targetGridView.footer.height = 40;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
applyFooterSettings(gridView);
|
||||
|
||||
// 고정 컬럼 설정
|
||||
if (fixedColumns !== undefined && fixedColumns > 0) {
|
||||
@@ -862,10 +972,6 @@ const DataGrid = ({
|
||||
});
|
||||
}
|
||||
|
||||
gridView.setOptions({
|
||||
summaryMode: "aggregate",
|
||||
});
|
||||
|
||||
// footerUserSpans가 있는 경우 setColumnLayout으로 설정
|
||||
const checkFooterUserSpans = (layout: unknown[]): boolean => {
|
||||
return layout.some((item) => {
|
||||
@@ -888,7 +994,9 @@ const DataGrid = ({
|
||||
});
|
||||
};
|
||||
|
||||
if (checkFooterUserSpans(columnLayout)) {
|
||||
const hasFooterUserSpans = checkFooterUserSpans(columnLayout);
|
||||
|
||||
if (hasFooterUserSpans) {
|
||||
// setColumnLayout은 그리드가 완전히 렌더링된 후 호출해야 함
|
||||
setTimeout(() => {
|
||||
if (gridRef.current?.gridView && columnLayout) {
|
||||
@@ -917,7 +1025,7 @@ const DataGrid = ({
|
||||
const colInfo: RealGridColumnInfo = {
|
||||
name: col.name,
|
||||
fieldName: col.fieldName,
|
||||
header: col.header,
|
||||
header: realGridHeaderWithTooltip(col.header),
|
||||
editable: col.editable,
|
||||
valueType: col.valueType,
|
||||
dataFormat: col.dataFormat,
|
||||
@@ -951,6 +1059,10 @@ const DataGrid = ({
|
||||
(currentGridView as GridViewWithSetColumns).setColumns?.(
|
||||
columnsWithFooter,
|
||||
);
|
||||
applyFooterSettings(currentGridView);
|
||||
if (hasFooterUserSpans) {
|
||||
currentGridView.setColumnLayout?.(columnLayout);
|
||||
}
|
||||
|
||||
// setColumns 호출 후 footer 배열의 valueCallback을 다시 설정
|
||||
setTimeout(() => {
|
||||
@@ -991,6 +1103,11 @@ const DataGrid = ({
|
||||
);
|
||||
}
|
||||
});
|
||||
applyFooterSettings(currentGridView);
|
||||
if (hasFooterUserSpans) {
|
||||
currentGridView.setColumnLayout?.(columnLayout);
|
||||
}
|
||||
currentGridView.refresh?.();
|
||||
}, 50);
|
||||
} catch (e) {
|
||||
console.warn("Failed to set columns with footer array:", e);
|
||||
@@ -1066,7 +1183,11 @@ const DataGrid = ({
|
||||
// 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식)
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView || (!onRowClick && !cellTooltipCallback)) return;
|
||||
if (
|
||||
!gridView ||
|
||||
(!onRowClick && !cellTooltipCallback && !onDataCellClick)
|
||||
)
|
||||
return;
|
||||
|
||||
gridView.onCellClicked = (grid, clickData) => {
|
||||
if (
|
||||
@@ -1085,10 +1206,10 @@ const DataGrid = ({
|
||||
};
|
||||
}
|
||||
).getCurrent?.();
|
||||
const fieldName =
|
||||
cellIndex?.fieldName ?? cellIndex?.column ?? "";
|
||||
// cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex)
|
||||
if (cellTooltipCallbackRef.current && row && cellIndex) {
|
||||
const fieldName =
|
||||
cellIndex.fieldName ?? cellIndex.column ?? "";
|
||||
const msg = cellTooltipCallbackRef.current({
|
||||
row: row as Record<string, string | number>,
|
||||
fieldName,
|
||||
@@ -1118,6 +1239,13 @@ const DataGrid = ({
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onDataCellClickRef.current && row !== undefined) {
|
||||
onDataCellClickRef.current({
|
||||
row: (row ?? {}) as Record<string, string | number>,
|
||||
fieldName,
|
||||
itemIndex: clickData.itemIndex,
|
||||
});
|
||||
}
|
||||
if (onRowClick)
|
||||
onRowClick((row ?? {}) as Record<string, string | number>);
|
||||
} catch (error) {
|
||||
@@ -1128,7 +1256,7 @@ const DataGrid = ({
|
||||
return () => {
|
||||
if (gridView.onCellClicked) gridView.onCellClicked = undefined;
|
||||
};
|
||||
}, [onRowClick, cellTooltipCallback, columns]);
|
||||
}, [onRowClick, onDataCellClick, cellTooltipCallback, columns]);
|
||||
|
||||
useEffect(() => {
|
||||
const gridView = gridRef.current?.gridView;
|
||||
@@ -1334,6 +1462,48 @@ const DataGrid = ({
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
if (!gv?.columnByName) return;
|
||||
// 공식 문서: 헤더 툴팁 = header.showTooltip + onShowHeaderTooltip
|
||||
(
|
||||
gv as {
|
||||
onShowHeaderTooltip?: (
|
||||
grid: unknown,
|
||||
column: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
header?: {
|
||||
tooltip?: string;
|
||||
};
|
||||
},
|
||||
value: string,
|
||||
layout?: {
|
||||
name?: string;
|
||||
fieldName?: string;
|
||||
column?: string | { name?: string; fieldName?: string };
|
||||
},
|
||||
isEllipsis?: boolean,
|
||||
) => string | undefined;
|
||||
}
|
||||
).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => {
|
||||
const currentColumns = columnsRef.current;
|
||||
const directTip = headerColumn?.header?.tooltip?.trim();
|
||||
if (directTip) return directTip;
|
||||
|
||||
const layoutColumn =
|
||||
typeof layout?.column === "string"
|
||||
? layout.column
|
||||
: layout?.column?.name ?? layout?.column?.fieldName;
|
||||
|
||||
const col = currentColumns.find(
|
||||
(c) =>
|
||||
c.name === headerColumn?.name ||
|
||||
c.fieldName === headerColumn?.fieldName ||
|
||||
c.name === layout?.name ||
|
||||
c.fieldName === layout?.fieldName ||
|
||||
c.name === layoutColumn ||
|
||||
c.fieldName === layoutColumn,
|
||||
);
|
||||
return col ? resolveColumnTooltip(col) : undefined;
|
||||
};
|
||||
// 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함)
|
||||
if (cellTooltipCallbackRef.current) {
|
||||
(
|
||||
@@ -1387,6 +1557,22 @@ const DataGrid = ({
|
||||
renderer?: { showTooltip?: boolean };
|
||||
} | null;
|
||||
if (gridCol) {
|
||||
(
|
||||
gv as {
|
||||
setColumnProperty?: (
|
||||
col: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) => void;
|
||||
}
|
||||
).setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
realGridHeaderWithTooltip({
|
||||
...column.header,
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
if (cellTooltipCallbackRef.current) {
|
||||
(
|
||||
gv as {
|
||||
@@ -1456,7 +1642,10 @@ const DataGrid = ({
|
||||
? column.dataFormat
|
||||
: "#,##0.000",
|
||||
})}
|
||||
header={column.header}
|
||||
header={realGridHeaderWithTooltip({
|
||||
...column.header,
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
})}
|
||||
editable={column.editable}
|
||||
{...(column.width != null && {
|
||||
width: column.width,
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
"use client";
|
||||
|
||||
import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { RealGridReact } from "realgrid-react";
|
||||
|
||||
import { getUserInfo } from "../../../../../packages/common/components/src/api";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ReviewInputSaveRequest,
|
||||
VerificationResultListRequest,
|
||||
VerificationResultReportItem,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination";
|
||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */
|
||||
export interface ExceptionProcessingHistoryQueryContext {
|
||||
sewerBzmnCd: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
}
|
||||
|
||||
export interface ExceptionProcessingHistoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
queryContext?: ExceptionProcessingHistoryQueryContext | null;
|
||||
emptyMessage?: string;
|
||||
/** 반려 API 성공 후 (다이얼로그 내 목록 재조회 이후) 상위 화면 등에서 호출 */
|
||||
onRejectSuccess?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
type ExceptionHistoryGridRow = Record<string, string | number>;
|
||||
type ExceptionHistoryTabKey = "NV" | "IMR" | "HB";
|
||||
|
||||
const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [
|
||||
{ value: "NV", label: "일반검증 내역" },
|
||||
{ value: "IMR", label: "I-MR 검증내역" },
|
||||
{ value: "HB", label: "HB 검증내역" },
|
||||
];
|
||||
|
||||
const TITLE = "예외처리 내역 조회";
|
||||
/** 예외처리 건만 */
|
||||
const PRCS_YN_LIST = "2";
|
||||
|
||||
export type ExceptionRejectSubmitItem = {
|
||||
vrfcSn: number;
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
mdfrId: string;
|
||||
rjctOpnnCn?: string;
|
||||
};
|
||||
|
||||
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
||||
const s = String(raw).trim();
|
||||
if (s === "") return undefined;
|
||||
const n = Number.parseInt(s, 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
|
||||
if (fieldName !== "prcsYnNm") return undefined;
|
||||
const code = prcsYnCodeToNumber(row.prcsYn);
|
||||
if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad";
|
||||
return "rg-cell-vrf-prcs-ok";
|
||||
}
|
||||
|
||||
const exceptionHistoryColumnInfo: ColumnInfo = {
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false },
|
||||
{ name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false },
|
||||
{ name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false },
|
||||
{ name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false },
|
||||
{ name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false },
|
||||
{ name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false },
|
||||
{ name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false },
|
||||
{ name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false },
|
||||
{
|
||||
name: "individualReject",
|
||||
fieldName: "individualReject",
|
||||
header: { text: "개별 반려" },
|
||||
editable: false,
|
||||
width: 104,
|
||||
styleName: "rg-cell-reject-mini-btn",
|
||||
},
|
||||
{ name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
|
||||
{ name: "vrfcSn", fieldName: "vrfcSn", header: { text: "vrfcSn" }, editable: false, visible: false },
|
||||
],
|
||||
columnLayout: [
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"vrfcTrgtNm",
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"individualReject",
|
||||
"prcsYn",
|
||||
"vrfcSn",
|
||||
],
|
||||
};
|
||||
|
||||
export function ExceptionProcessingHistoryDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
queryContext,
|
||||
emptyMessage = "표시할 예외처리 내역이 없습니다.",
|
||||
onRejectSuccess,
|
||||
}: ExceptionProcessingHistoryDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const nvGridRef = useRef<RealGridReact | null>(null);
|
||||
const imrGridRef = useRef<RealGridReact | null>(null);
|
||||
const hbGridRef = useRef<RealGridReact | null>(null);
|
||||
const [rejectReasonDialogOpen, setRejectReasonDialogOpen] = useState(false);
|
||||
const [pendingRejectVrfcSns, setPendingRejectVrfcSns] = useState<number[]>([]);
|
||||
const [rejectReasonText, setRejectReasonText] = useState("");
|
||||
const [rejectSubmitting, setRejectSubmitting] = useState(false);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ExceptionHistoryTabKey>("NV");
|
||||
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [hbPageIndex, setHbPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [hbPageCount, setHbPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [hbLoading, setHbLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setActiveTab("NV");
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
setErrorMessage(null);
|
||||
setRejectReasonDialogOpen(false);
|
||||
setPendingRejectVrfcSns([]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
}, []);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (vrfcMthdCd: ExceptionHistoryTabKey, pageIndex: number) => {
|
||||
if (!queryContext) return null;
|
||||
const param: VerificationResultListRequest = {
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
ctpvNm: queryContext.ctpvNm,
|
||||
sggNm: queryContext.sggNm,
|
||||
statsYr: queryContext.statsYr,
|
||||
statsCd: queryContext.statsCd,
|
||||
pbcpAprvYn: "N",
|
||||
vrfcMthdCd,
|
||||
prcsYnList: PRCS_YN_LIST,
|
||||
page: pageIndex + 1,
|
||||
size: 30,
|
||||
};
|
||||
return statisticsApi.getVerificationResultList(param);
|
||||
},
|
||||
[queryContext],
|
||||
);
|
||||
|
||||
const loadInitial = useCallback(async () => {
|
||||
if (!queryContext) {
|
||||
setErrorMessage("조회 조건이 없어 예외처리 내역을 불러올 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setHbLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes, hbRes] = await Promise.all([
|
||||
fetchPage("NV", 0),
|
||||
fetchPage("IMR", 0),
|
||||
fetchPage("HB", 0),
|
||||
]);
|
||||
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
|
||||
setImrEntries(imrRes?.data?.verifyResultList ?? []);
|
||||
setHbEntries(hbRes?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setErrorMessage("예외처리 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
}
|
||||
}, [fetchPage, queryContext]);
|
||||
|
||||
const handleNormalPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === normalPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("NV", pageIndex);
|
||||
setNormalEntries(response?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setNormalPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, normalPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleImrPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === imrPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setImrLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("IMR", pageIndex);
|
||||
setImrEntries(response?.data?.verifyResultList ?? []);
|
||||
setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setImrLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, imrPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleHbPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === hbPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setHbLoading(true);
|
||||
try {
|
||||
const response = await fetchPage("HB", pageIndex);
|
||||
setHbEntries(response?.data?.verifyResultList ?? []);
|
||||
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setHbLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchPage, hbPageIndex, queryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
void loadInitial();
|
||||
}, [loadInitial, open, resetState]);
|
||||
|
||||
const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] =>
|
||||
entries.map((item) => ({
|
||||
ctpvNm: item.ctpvNm,
|
||||
sggNm: item.sggNm,
|
||||
vrfcTrgtNm: item.vrfcTrgtNm,
|
||||
rsnExpln: item.rsnExpln,
|
||||
nowVlCn: item.nowVlCn ?? "-",
|
||||
compVlCn: item.compVlCn ?? "-",
|
||||
prcsYnNm: item.prcsYnNm ?? "-",
|
||||
prcsYn: item.prcsYn ?? "",
|
||||
pbcpAprvYn: item.pbcpAprvYn,
|
||||
regDt: (item.regDt ?? "").slice(0, 16),
|
||||
mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16),
|
||||
individualReject: "개별 반려",
|
||||
vrfcSn: item.vrfcSn,
|
||||
}));
|
||||
|
||||
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
|
||||
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
|
||||
const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
|
||||
|
||||
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
|
||||
SetStateAction<Record<string, string | number>[]>
|
||||
>;
|
||||
|
||||
const handleOpenRejectReasonDialog = useCallback(() => {
|
||||
if (!queryContext) {
|
||||
alert("조회 조건이 없습니다.");
|
||||
return;
|
||||
}
|
||||
const rows =
|
||||
activeTab === "NV" ? normalRows : activeTab === "IMR" ? imrRows : hbRows;
|
||||
const gridRef =
|
||||
activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef;
|
||||
const gridView = gridRef.current?.gridView;
|
||||
if (!gridView) {
|
||||
alert("데이터가 존재하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
const checkedIndices = (gridView.getCheckedRows?.(true, false, true) ?? []) as number[];
|
||||
if (checkedIndices.length === 0) {
|
||||
alert("반려할 항목을 체크해주세요.");
|
||||
return;
|
||||
}
|
||||
const vrfcSns: number[] = [];
|
||||
for (const itemIndex of checkedIndices) {
|
||||
const row = rows[itemIndex];
|
||||
if (!row) continue;
|
||||
const vrfcSn = Number(row.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) continue;
|
||||
vrfcSns.push(vrfcSn);
|
||||
}
|
||||
if (vrfcSns.length === 0) {
|
||||
alert("유효한 검증번호가 있는 행을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setPendingRejectVrfcSns(vrfcSns);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
setRejectReasonDialogOpen(true);
|
||||
}, [activeTab, alert, hbRows, imrRows, normalRows, queryContext]);
|
||||
|
||||
const resetRejectReasonDialog = useCallback(() => {
|
||||
setRejectReasonDialogOpen(false);
|
||||
setPendingRejectVrfcSns([]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
}, []);
|
||||
|
||||
const handleRejectReasonDialogOpenChange = useCallback(
|
||||
(nextOpen: boolean) => {
|
||||
if (!nextOpen) resetRejectReasonDialog();
|
||||
},
|
||||
[resetRejectReasonDialog],
|
||||
);
|
||||
|
||||
const handleConfirmReject = useCallback(async () => {
|
||||
if (!queryContext || pendingRejectVrfcSns.length === 0) {
|
||||
alert("반려할 항목이 없습니다. 다시 선택해주세요.");
|
||||
resetRejectReasonDialog();
|
||||
return;
|
||||
}
|
||||
let userInfo;
|
||||
try {
|
||||
userInfo = await getUserInfo();
|
||||
} catch {
|
||||
alert("사용자 정보를 불러오지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
const mdfrId = userInfo.userUnqId;
|
||||
const trimmedReason = rejectReasonText.trim();
|
||||
|
||||
const items: ReviewInputSaveRequest[] = pendingRejectVrfcSns.map((vrfcSn) => ({
|
||||
type: "INPUT",
|
||||
statsYr: queryContext.statsYr,
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
statsCd: queryContext.statsCd,
|
||||
mdfrId,
|
||||
vrfcSn: String(vrfcSn),
|
||||
...(trimmedReason ? { rjctOpnnCn: trimmedReason } : {}),
|
||||
}));
|
||||
|
||||
setRejectSubmitting(true);
|
||||
try {
|
||||
const response = await statisticsApi.saveInputReturn(items);
|
||||
if (response.resultCode.toLowerCase() !== "success") {
|
||||
alert(response.message ?? "반려 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
alert("반려 처리되었습니다.");
|
||||
resetRejectReasonDialog();
|
||||
await Promise.all([
|
||||
loadInitial(),
|
||||
onRejectSuccess?.() ?? Promise.resolve(),
|
||||
]);
|
||||
} catch {
|
||||
alert("반려 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setRejectSubmitting(false);
|
||||
}
|
||||
}, [
|
||||
alert,
|
||||
loadInitial,
|
||||
onRejectSuccess,
|
||||
pendingRejectVrfcSns,
|
||||
queryContext,
|
||||
rejectReasonText,
|
||||
resetRejectReasonDialog,
|
||||
]);
|
||||
|
||||
const handleGridRowClick = useCallback(
|
||||
(rowData: Record<string, string | number>) => {
|
||||
const gridRef =
|
||||
activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef;
|
||||
const current = gridRef.current?.gridView?.getCurrent?.() as
|
||||
| { fieldName?: string; column?: string }
|
||||
| undefined;
|
||||
const fieldName = current?.fieldName ?? current?.column;
|
||||
if (fieldName !== "individualReject") return;
|
||||
const vrfcSn = Number(rowData.vrfcSn);
|
||||
if (!Number.isFinite(vrfcSn)) {
|
||||
alert("유효한 검증번호가 있는 행을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setPendingRejectVrfcSns([vrfcSn]);
|
||||
setRejectReasonText("");
|
||||
setRejectSubmitting(false);
|
||||
setRejectReasonDialogOpen(true);
|
||||
},
|
||||
[activeTab, alert],
|
||||
);
|
||||
|
||||
const renderGrid = (
|
||||
rows: ExceptionHistoryGridRow[],
|
||||
pageIndex: number,
|
||||
pageCount: number,
|
||||
onPageIndexChange: (page: number) => void,
|
||||
loading: boolean,
|
||||
gridRef: MutableRefObject<RealGridReact | null>,
|
||||
) => {
|
||||
if (!loading && rows.length === 0) {
|
||||
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-2">
|
||||
<DataGrid
|
||||
columnInfo={exceptionHistoryColumnInfo}
|
||||
rowData={rows}
|
||||
setRowData={noopSetRows}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
showHeaderCheckbox={true}
|
||||
enableSort={false}
|
||||
getGridRef={gridRef}
|
||||
cellStyleCallback={exceptionPrcsYnNmCellStyle}
|
||||
onRowClick={handleGridRowClick}
|
||||
/>
|
||||
<VerificationResultHistoryPagination
|
||||
pageCount={Math.max(pageCount, 1)}
|
||||
pageIndex={Math.max(pageIndex, 0)}
|
||||
onPageIndexChange={onPageIndexChange}
|
||||
className="mx-2 mb-2"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="absolute inset-0 z-10 rounded-md [data-theme='black']:bg-transparent bg-white/40 flex items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`${TITLE} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[85vh] space-y-5 overflow-auto">
|
||||
{errorMessage && (
|
||||
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as ExceptionHistoryTabKey)}
|
||||
className="w-full"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{exceptionHistoryTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value="NV" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
normalRows,
|
||||
normalPageIndex,
|
||||
normalPageCount,
|
||||
handleNormalPageChange,
|
||||
normalLoading,
|
||||
nvGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="IMR" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
imrRows,
|
||||
imrPageIndex,
|
||||
imrPageCount,
|
||||
handleImrPageChange,
|
||||
imrLoading,
|
||||
imrGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="HB" className="rounded-md border border-border">
|
||||
{renderGrid(
|
||||
hbRows,
|
||||
hbPageIndex,
|
||||
hbPageCount,
|
||||
handleHbPageChange,
|
||||
hbLoading,
|
||||
hbGridRef,
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
onClick={handleOpenRejectReasonDialog}
|
||||
disabled={rejectSubmitting}
|
||||
>
|
||||
일괄 반려
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray">
|
||||
닫기
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={rejectReasonDialogOpen} onOpenChange={handleRejectReasonDialogOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>예외처리 반려</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 py-2">
|
||||
<Label htmlFor="exception-reject-reason" className="text-label">
|
||||
반려사유 (선택, 체크한 모든 행에 동일 적용)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="exception-reject-reason"
|
||||
value={rejectReasonText}
|
||||
onChange={(e) => setRejectReasonText(e.target.value)}
|
||||
rows={4}
|
||||
className="min-h-[8rem] p-2 !text-xl"
|
||||
placeholder='미입력 시 "검증 리포트를 확인하여 주시기 바랍니다." 자동 주입'
|
||||
disabled={rejectSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="red"
|
||||
disabled={rejectSubmitting}
|
||||
onClick={() => void handleConfirmReject()}
|
||||
>
|
||||
반려 확정
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" color="gray" disabled={rejectSubmitting}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -109,6 +109,8 @@ export interface VerificationResultDialogProps {
|
||||
queryContext?: VerificationResultDialogQueryContext | null;
|
||||
title?: string;
|
||||
emptyMessage?: string;
|
||||
/** 예외 제출 API 성공 후 부모(입력 화면)에서 메인 데이터를 다시 조회할 때 사용 */
|
||||
onExceptionSubmitted?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function VerificationResultDialog({
|
||||
@@ -117,6 +119,7 @@ export function VerificationResultDialog({
|
||||
queryContext,
|
||||
title = "검증 결과",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
onExceptionSubmitted,
|
||||
}: VerificationResultDialogProps) {
|
||||
/** 부모가 매 렌더마다 새 객체를 넘기면 queryContext 참조만 바뀌어 loadInitial이 재실행되므로 필드 값 기준으로 안정화 */
|
||||
const stableQueryContext = useMemo((): VerificationResultDialogQueryContext | null => {
|
||||
@@ -385,6 +388,11 @@ export function VerificationResultDialog({
|
||||
});
|
||||
if (response.status === "success") {
|
||||
alert("예외 제출 처리되었습니다.");
|
||||
try {
|
||||
await onExceptionSubmitted?.();
|
||||
} catch {
|
||||
alert("예외 제출은 완료되었으나 화면 데이터를 다시 불러오지 못했습니다. 조회로 새로고침 해주세요.");
|
||||
}
|
||||
onOpenChange(false);
|
||||
} else {
|
||||
alert(response.message ?? "예외 제출 처리 중 오류가 발생했습니다.");
|
||||
@@ -396,9 +404,10 @@ export function VerificationResultDialog({
|
||||
alert,
|
||||
imrCheckedIndices,
|
||||
imrRows,
|
||||
loadInitial,
|
||||
normalCheckedIndices,
|
||||
normalRows,
|
||||
onExceptionSubmitted,
|
||||
onOpenChange,
|
||||
stableQueryContext,
|
||||
]);
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
@@ -20,6 +24,13 @@ import { VerificationResultHistoryPagination } from "@/components/module/Verific
|
||||
import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
|
||||
type VerificationResultGridRow = Record<string, string | number>;
|
||||
type VerificationHistoryTabKey = "NV" | "IMR" | "HB";
|
||||
|
||||
const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [
|
||||
{ value: "NV", label: "일반검증 내역" },
|
||||
{ value: "IMR", label: "I-MR 검증내역" },
|
||||
{ value: "HB", label: "HB 검증내역" },
|
||||
];
|
||||
|
||||
function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined {
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
@@ -34,7 +45,7 @@ function prcsYnCodeToNumber(raw: string | number | undefined): number | undefine
|
||||
function verificationPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined {
|
||||
if (fieldName !== "prcsYnNm") return undefined;
|
||||
const code = prcsYnCodeToNumber(row.prcsYn);
|
||||
if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad";
|
||||
if (code === 0 || code === 3) return "rg-cell-vrf-prcs-bad";
|
||||
return "rg-cell-vrf-prcs-ok";
|
||||
}
|
||||
|
||||
@@ -76,30 +87,40 @@ export function VerificationResultHistoryDialog({
|
||||
title = "검증 결과 내역",
|
||||
emptyMessage = "표시할 검증 결과가 없습니다.",
|
||||
}: VerificationResultHistoryDialogProps) {
|
||||
const [activeTab, setActiveTab] = useState<VerificationHistoryTabKey>("NV");
|
||||
const [normalEntries, setNormalEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [imrEntries, setImrEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [hbEntries, setHbEntries] = useState<VerificationResultReportItem[]>([]);
|
||||
const [normalPageIndex, setNormalPageIndex] = useState(0);
|
||||
const [imrPageIndex, setImrPageIndex] = useState(0);
|
||||
const [hbPageIndex, setHbPageIndex] = useState(0);
|
||||
const [normalPageCount, setNormalPageCount] = useState(1);
|
||||
const [imrPageCount, setImrPageCount] = useState(1);
|
||||
const [hbPageCount, setHbPageCount] = useState(1);
|
||||
const [normalLoading, setNormalLoading] = useState(false);
|
||||
const [imrLoading, setImrLoading] = useState(false);
|
||||
const [hbLoading, setHbLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setActiveTab("NV");
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
setErrorMessage(null);
|
||||
}, []);
|
||||
|
||||
const fetchVerificationResultPage = useCallback(
|
||||
async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => {
|
||||
async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => {
|
||||
if (!queryContext) return null;
|
||||
const param: VerificationResultListRequest = {
|
||||
sewerBzmnCd: queryContext.sewerBzmnCd,
|
||||
@@ -126,26 +147,34 @@ export function VerificationResultHistoryDialog({
|
||||
setErrorMessage(null);
|
||||
setNormalLoading(true);
|
||||
setImrLoading(true);
|
||||
setHbLoading(true);
|
||||
setNormalPageIndex(0);
|
||||
setImrPageIndex(0);
|
||||
setHbPageIndex(0);
|
||||
try {
|
||||
const [nvRes, imrRes] = await Promise.all([
|
||||
const [nvRes, imrRes, hbRes] = await Promise.all([
|
||||
fetchVerificationResultPage("NV", 0),
|
||||
fetchVerificationResultPage("IMR", 0),
|
||||
fetchVerificationResultPage("HB", 0),
|
||||
]);
|
||||
setNormalEntries(nvRes?.data?.verifyResultList ?? []);
|
||||
setImrEntries(imrRes?.data?.verifyResultList ?? []);
|
||||
setHbEntries(hbRes?.data?.verifyResultList ?? []);
|
||||
setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1);
|
||||
} catch {
|
||||
setNormalEntries([]);
|
||||
setImrEntries([]);
|
||||
setHbEntries([]);
|
||||
setNormalPageCount(1);
|
||||
setImrPageCount(1);
|
||||
setHbPageCount(1);
|
||||
setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setNormalLoading(false);
|
||||
setImrLoading(false);
|
||||
setHbLoading(false);
|
||||
}
|
||||
}, [fetchVerificationResultPage, queryContext]);
|
||||
|
||||
@@ -187,6 +216,25 @@ export function VerificationResultHistoryDialog({
|
||||
[fetchVerificationResultPage, imrPageIndex, queryContext],
|
||||
);
|
||||
|
||||
const handleHbPageChange = useCallback(
|
||||
async (pageIndex: number) => {
|
||||
if (pageIndex === hbPageIndex || !queryContext) return;
|
||||
setErrorMessage(null);
|
||||
setHbLoading(true);
|
||||
try {
|
||||
const response = await fetchVerificationResultPage("HB", pageIndex);
|
||||
setHbEntries(response?.data?.verifyResultList ?? []);
|
||||
setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1);
|
||||
setHbPageIndex(pageIndex);
|
||||
} catch {
|
||||
setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setHbLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchVerificationResultPage, hbPageIndex, queryContext],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
resetState();
|
||||
@@ -212,6 +260,7 @@ export function VerificationResultHistoryDialog({
|
||||
|
||||
const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]);
|
||||
const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]);
|
||||
const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]);
|
||||
|
||||
const noopSetRows = ((_: SetStateAction<Record<string, string | number>[]>) => {}) as Dispatch<
|
||||
SetStateAction<Record<string, string | number>[]>
|
||||
@@ -269,14 +318,24 @@ export function VerificationResultHistoryDialog({
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">일반검증 내역</h3>
|
||||
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
|
||||
</section>
|
||||
<section className="rounded-md border border-border">
|
||||
<h3 className="border-b border-border px-4 py-2 text-[1.35rem] font-semibold">I-MR 검증내역</h3>
|
||||
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
|
||||
</section>
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default">
|
||||
<TabsList>
|
||||
{verificationHistoryTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value="NV" className="rounded-md border border-border">
|
||||
{renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)}
|
||||
</TabsContent>
|
||||
<TabsContent value="IMR" className="rounded-md border border-border">
|
||||
{renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)}
|
||||
</TabsContent>
|
||||
<TabsContent value="HB" className="rounded-md border border-border">
|
||||
{renderGrid(hbRows, hbPageIndex, hbPageCount, handleHbPageChange, hbLoading)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputDatePicker,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
|
||||
@@ -36,27 +37,43 @@ export interface MainFacilityMappingSaveDialogProps {
|
||||
onConfirm?: (values: MainFacilityMappingSaveDialogValues) => void | Promise<void>;
|
||||
}
|
||||
|
||||
function isValidYmd(ymd: string): boolean {
|
||||
if (!/^\d{8}$/.test(ymd)) return false;
|
||||
const y = Number(ymd.slice(0, 4));
|
||||
const m = Number(ymd.slice(4, 6));
|
||||
const d = Number(ymd.slice(6, 8));
|
||||
const dt = new Date(y, m - 1, d);
|
||||
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
|
||||
function formatDateToYmd(d: Date | undefined): string | null {
|
||||
if (!d || Number.isNaN(d.getTime())) return null;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(d.getDate()).padStart(2, "0");
|
||||
return `${y}${m}${day}`;
|
||||
}
|
||||
|
||||
function validatePrxyCtrtCn(value: string): string | null {
|
||||
const t = value.trim();
|
||||
if (!t) return "위탁계약기간을 입력해 주세요.";
|
||||
/** `YYYYMMDD` → `Date` (로컬 자정) */
|
||||
function parseYmdToDate(ymd: string): Date | undefined {
|
||||
const s = ymd.replace(/\D/g, "");
|
||||
if (s.length !== 8) return undefined;
|
||||
const y = Number(s.slice(0, 4));
|
||||
const mo = Number(s.slice(4, 6)) - 1;
|
||||
const day = Number(s.slice(6, 8));
|
||||
const d = new Date(y, mo, day);
|
||||
if (d.getFullYear() !== y || d.getMonth() !== mo || d.getDate() !== day) return undefined;
|
||||
return d;
|
||||
}
|
||||
|
||||
function parseInitialPrxyCtrtCnToDates(initial: string): { start: Date | undefined; end: Date | undefined } {
|
||||
const t = initial.trim();
|
||||
if (!/^\d{8}~\d{8}$/.test(t)) {
|
||||
return "위탁계약기간은 YYYYMMDD~YYYYMMDD 형식으로 입력해 주세요. (예: 20240101~20281231)";
|
||||
return { start: undefined, end: undefined };
|
||||
}
|
||||
const start = t.slice(0, 8);
|
||||
const end = t.slice(9, 17);
|
||||
if (!isValidYmd(start) || !isValidYmd(end)) {
|
||||
return "시작일 또는 종료일이 유효한 날짜가 아닙니다.";
|
||||
const start = parseYmdToDate(t.slice(0, 8));
|
||||
const end = parseYmdToDate(t.slice(9, 17));
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function validateContractRange(start: Date | undefined, end: Date | undefined): string | null {
|
||||
const startYmd = formatDateToYmd(start);
|
||||
const endYmd = formatDateToYmd(end);
|
||||
if (!startYmd || !endYmd) {
|
||||
return "시작일과 종료일을 선택해 주세요.";
|
||||
}
|
||||
if (start > end) {
|
||||
if (startYmd > endYmd) {
|
||||
return "시작일이 종료일보다 늦을 수 없습니다.";
|
||||
}
|
||||
return null;
|
||||
@@ -75,7 +92,8 @@ export function MainFacilityMappingSaveDialog({
|
||||
const [prxyBzentyNmCn, setPrxyBzentyNmCn] = useState("");
|
||||
const [cnsgnCstInput, setCnsgnCstInput] = useState("");
|
||||
const [wholNopeInput, setWholNopeInput] = useState("");
|
||||
const [prxyCtrtCn, setPrxyCtrtCn] = useState("");
|
||||
const [prxyCtrtStart, setPrxyCtrtStart] = useState<Date | undefined>(undefined);
|
||||
const [prxyCtrtEnd, setPrxyCtrtEnd] = useState<Date | undefined>(undefined);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,7 +112,9 @@ export function MainFacilityMappingSaveDialog({
|
||||
? ""
|
||||
: String(initialWholNope),
|
||||
);
|
||||
setPrxyCtrtCn((initialPrxyCtrtCn ?? "").trim());
|
||||
const { start, end } = parseInitialPrxyCtrtCnToDates(initialPrxyCtrtCn ?? "");
|
||||
setPrxyCtrtStart(start);
|
||||
setPrxyCtrtEnd(end);
|
||||
}
|
||||
}, [open, initialPrxyBzentyNmCn, initialCnsgnCstVl, initialWholNope, initialPrxyCtrtCn]);
|
||||
|
||||
@@ -103,7 +123,8 @@ export function MainFacilityMappingSaveDialog({
|
||||
setPrxyBzentyNmCn("");
|
||||
setCnsgnCstInput("");
|
||||
setWholNopeInput("");
|
||||
setPrxyCtrtCn("");
|
||||
setPrxyCtrtStart(undefined);
|
||||
setPrxyCtrtEnd(undefined);
|
||||
}
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
@@ -121,18 +142,21 @@ export function MainFacilityMappingSaveDialog({
|
||||
alert("전체 인원은 0 이상의 정수로 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
const prxyCtrtErr = validatePrxyCtrtCn(prxyCtrtCn);
|
||||
const prxyCtrtErr = validateContractRange(prxyCtrtStart, prxyCtrtEnd);
|
||||
if (prxyCtrtErr) {
|
||||
alert(prxyCtrtErr);
|
||||
return;
|
||||
}
|
||||
const startYmd = formatDateToYmd(prxyCtrtStart)!;
|
||||
const endYmd = formatDateToYmd(prxyCtrtEnd)!;
|
||||
const prxyCtrtCnParam = `${startYmd}~${endYmd}`;
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onConfirm?.({
|
||||
prxyBzentyNmCn: prxyBzentyNmCn.trim(),
|
||||
cnsgnCstVl,
|
||||
wholNope,
|
||||
prxyCtrtCn: prxyCtrtCn.trim(),
|
||||
prxyCtrtCn: prxyCtrtCnParam,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@@ -193,14 +217,26 @@ export function MainFacilityMappingSaveDialog({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">위탁계약기간</Label>
|
||||
<Input
|
||||
value={prxyCtrtCn}
|
||||
onChange={(e) => setPrxyCtrtCn(e.target.value)}
|
||||
className="!text-[1.3rem]"
|
||||
placeholder="YYYYMMDD~YYYYMMDD (예: 20240101~20281231)"
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">시작일</Label>
|
||||
<InputDatePicker
|
||||
value={prxyCtrtStart}
|
||||
onChange={setPrxyCtrtStart}
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<Label className="text-label text-[1.2rem]">종료일</Label>
|
||||
<InputDatePicker
|
||||
value={prxyCtrtEnd}
|
||||
onChange={setPrxyCtrtEnd}
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useConfirm } from '@frontend-opensource/use-react-hooks';
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from '@/shared/lib';
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from '@/shared/lib';
|
||||
import { statisticsInputSewSupplyRateApi } from '@/entities/statistics/api/input/statistics-input-sew-supply-rate-service';
|
||||
import { statisticsInputPublicSewageFacilityApiService } from '@/entities/statistics/api/input/statistics-public-sewage-facility-input-service';
|
||||
import { statisticsApi } from '@/entities/statistics/api/statistics-service';
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type CheckInputPeriodItem,
|
||||
getVerificationPeriodActionBlockedMessage,
|
||||
isInputPeriodCurrent,
|
||||
type SewerageSupplyRateVerificationResult,
|
||||
} 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 { cancelSubmitRequest, ImrStatusResponse, SewerageStatusInfo } from '@/entities/statistics/model/statistics-interface';
|
||||
@@ -25,7 +26,7 @@ import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from '
|
||||
|
||||
import { getUserInfo } from '../../../../../../packages/common/components/src/api';
|
||||
|
||||
import { MainFacilityMappingSaveDialog } from './MainFacilityMappingSaveDialog';
|
||||
import { MainFacilityMappingSaveDialog, type MainFacilityMappingSaveDialogValues } from './MainFacilityMappingSaveDialog';
|
||||
import SubmitDailog from './SubmitDailog';
|
||||
|
||||
import CancelSubmitButton from '@/components/button/CancelSubmitButton';
|
||||
@@ -46,6 +47,10 @@ import { PublicSewageFactInpColumnList } from '@/components/util/column/input/Pu
|
||||
import { toGridRows } from '@/components/util/grid/gridFunction';
|
||||
import { useAdminStore } from '@/store/admin-store';
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageSupplyRateVerificationResult;
|
||||
};
|
||||
|
||||
type TabToolbarSnapshot = {
|
||||
showSaveInToolbar: boolean;
|
||||
showEnterableToolbarButtons: boolean;
|
||||
@@ -131,11 +136,16 @@ type MainFacilityMappingSaveDraft = {
|
||||
initialPrxyCtrtCn: string;
|
||||
};
|
||||
|
||||
const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from(
|
||||
new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)),
|
||||
);
|
||||
|
||||
// 하수도 통계 관리 > 06. 공공하수처리시설
|
||||
const PublicSewageFacilityInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress);
|
||||
const setGlobalProgressLabel = useGlobalProgressStore((state) => state.setLabel);
|
||||
@@ -149,6 +159,8 @@ const PublicSewageFacilityInp = () => {
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<Map<string, SewerageStatusInfo | null>>(new Map());
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
/** 탭키별 `engColNm -> columnGuide` 캐시 */
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
|
||||
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
|
||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
||||
@@ -164,7 +176,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const searchDoneRef = useRef(false);
|
||||
/** ST00060001만: RealGrid 편집값이 state에 없을 수 있어 저장/제출 시 그리드에서 직접 읽음 */
|
||||
const st60001GridGetRowsRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
|
||||
|
||||
const submitFunc = () => {
|
||||
void (async () => {
|
||||
if (activeTab !== 'ST00060001') {
|
||||
@@ -404,6 +416,13 @@ const PublicSewageFacilityInp = () => {
|
||||
.filter((sn) => sn !== '');
|
||||
|
||||
const wholNopeFromRow = Number(selectedMain.wholNope ?? 0);
|
||||
const deleteOnly = saveList.length === 0 && deleteList.length > 0;
|
||||
const nothingToPersist = saveList.length === 0 && deleteList.length === 0;
|
||||
if (nothingToPersist) {
|
||||
alert('저장할 변경 사항이 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
mainFacilityMappingSaveDraftRef.current = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
|
||||
@@ -418,6 +437,16 @@ const PublicSewageFacilityInp = () => {
|
||||
initialWholNope: Number.isFinite(wholNopeFromRow) ? wholNopeFromRow : 0,
|
||||
initialPrxyCtrtCn: String(selectedMain.prxyCtrtCn ?? ''),
|
||||
};
|
||||
|
||||
if (deleteOnly) {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
const ok = await runMainFacilityMappingSave(draft, null);
|
||||
if (ok) {
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMainFacilityMappingSaveDialogKey((k) => k + 1);
|
||||
setMainFacilityMappingSaveDialogOpen(true);
|
||||
return;
|
||||
@@ -489,6 +518,36 @@ const PublicSewageFacilityInp = () => {
|
||||
const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001';
|
||||
const searchParamRef = useRef<PublicSewageFacilityRequestParams | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
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;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
@@ -630,42 +689,40 @@ const PublicSewageFacilityInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainFacilityMappingSaveConfirm = async (input: {
|
||||
prxyBzentyNmCn: string;
|
||||
cnsgnCstVl: number;
|
||||
wholNope: number;
|
||||
prxyCtrtCn: string;
|
||||
}) => {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
if (!draft || !searchParamRef.current || !lastSearchedValues) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
return;
|
||||
const runMainFacilityMappingSave = async (
|
||||
draft: MainFacilityMappingSaveDraft,
|
||||
dialogInput: MainFacilityMappingSaveDialogValues | null,
|
||||
): Promise<boolean> => {
|
||||
if (!searchParamRef.current || !lastSearchedValues) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const param: PublicSewageMainFacilityMappingSaveRequest = {
|
||||
const base = {
|
||||
statsYr: draft.statsYr,
|
||||
sewerBzmnCd: draft.sewerBzmnCd,
|
||||
admdstCd: draft.admdstCd,
|
||||
admdstNm: draft.admdstNm,
|
||||
mainFcltSn: draft.mainFcltSn,
|
||||
mainFcltNm: draft.mainFcltNm,
|
||||
prxyBzentyNmCn: input.prxyBzentyNmCn,
|
||||
cnsgnCstVl: input.cnsgnCstVl,
|
||||
wholNope: input.wholNope,
|
||||
prxyCtrtCn: input.prxyCtrtCn,
|
||||
userId: userInfo.userUnqId,
|
||||
saveList: draft.saveList,
|
||||
deleteList: draft.deleteList,
|
||||
};
|
||||
const param: PublicSewageMainFacilityMappingSaveRequest = dialogInput
|
||||
? {
|
||||
...base,
|
||||
prxyBzentyNmCn: dialogInput.prxyBzentyNmCn,
|
||||
cnsgnCstVl: dialogInput.cnsgnCstVl,
|
||||
wholNope: dialogInput.wholNope,
|
||||
prxyCtrtCn: dialogInput.prxyCtrtCn,
|
||||
}
|
||||
: { ...base };
|
||||
const response =
|
||||
await statisticsInputPublicSewageFacilityApiService.saveMainFacilityMapping(param);
|
||||
if (response.status === 'success') {
|
||||
setGlobalProgress(50);
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
await loadPublicSewageFacilityActiveTabData(
|
||||
'ST00060002',
|
||||
searchParamRef.current,
|
||||
@@ -673,16 +730,32 @@ const PublicSewageFacilityInp = () => {
|
||||
);
|
||||
setGlobalProgress(100);
|
||||
alert('저장되었습니다.');
|
||||
} else {
|
||||
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
||||
return true;
|
||||
}
|
||||
alert(response.message || '데이터 저장 중 오류가 발생했습니다.');
|
||||
return false;
|
||||
} catch {
|
||||
alert('데이터 저장 중 오류가 발생했습니다.');
|
||||
return false;
|
||||
} finally {
|
||||
hideGlobalProgress();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainFacilityMappingSaveConfirm = async (input: MainFacilityMappingSaveDialogValues) => {
|
||||
const draft = mainFacilityMappingSaveDraftRef.current;
|
||||
if (!draft || !searchParamRef.current || !lastSearchedValues) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
return;
|
||||
}
|
||||
const ok = await runMainFacilityMappingSave(draft, input);
|
||||
if (ok) {
|
||||
setMainFacilityMappingSaveDialogOpen(false);
|
||||
mainFacilityMappingSaveDraftRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === 'ALL') {
|
||||
alert('시도를 선택해주세요.');
|
||||
@@ -783,7 +856,7 @@ const PublicSewageFacilityInp = () => {
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '',
|
||||
ctpvNm: lastSearchedValues?.sido ?? '',
|
||||
sggNm: lastSearchedValues?.gugun ?? '',
|
||||
sggNm: lastSearchedValues?.gugun === 'ALL' ? '' : lastSearchedValues?.gugun ?? '',
|
||||
statsYr: lastSearchedValues?.year ?? '',
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: statsCdForCancel,
|
||||
@@ -811,8 +884,15 @@ const PublicSewageFacilityInp = () => {
|
||||
|
||||
const getGridColumnInfo = (
|
||||
tab: ColumnInfoList,
|
||||
guideTabKey?: string,
|
||||
): ColumnInfo => ({
|
||||
columns: tab.columns ?? [],
|
||||
columns: (tab.columns ?? []).map((col) => {
|
||||
const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim();
|
||||
return {
|
||||
...col,
|
||||
tooltip: guide && guide.length > 0 ? guide : undefined,
|
||||
};
|
||||
}),
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
|
||||
@@ -843,7 +923,11 @@ const PublicSewageFacilityInp = () => {
|
||||
alert(response.message || '매핑 대상 조회 중 오류가 발생했습니다.');
|
||||
return;
|
||||
}
|
||||
const mappedTargetRows = (response.data ?? []).map((item) => ({
|
||||
const rawList = response.data ?? [];
|
||||
if (rawList.length === 0) {
|
||||
alert('해당 시설은 중심처리시설의 위탁시설로 연결되어있습니다.');
|
||||
}
|
||||
const mappedTargetRows = rawList.map((item) => ({
|
||||
...item,
|
||||
__checked: item.mappedYn === 'Y' ? 'Y' : '',
|
||||
}));
|
||||
@@ -876,7 +960,14 @@ const PublicSewageFacilityInp = () => {
|
||||
inputPeriodEnterable,
|
||||
isAdminInfo,
|
||||
);
|
||||
const { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard } = activeTabToolbar;
|
||||
const {
|
||||
showSaveInToolbar: baseShowSaveInToolbar,
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard = baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
const isMainFacilityTab = activeTab === 'ST00060002';
|
||||
|
||||
const openVerificationHistoryDialog = () => {
|
||||
@@ -913,7 +1004,7 @@ const PublicSewageFacilityInp = () => {
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
@@ -921,8 +1012,8 @@ const PublicSewageFacilityInp = () => {
|
||||
{showEnterableToolbarButtons && !isMainFacilityTab && (
|
||||
<>
|
||||
{showIsNotApplicable && <NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />}
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<UpdateOperateDataButton updateOperateDataButtonFunc={updateOperateDataButtonFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{!isMainFacilityTab && activeTabStatus?.laeSbmsnSttsCd === '2' && (
|
||||
@@ -937,7 +1028,7 @@ const PublicSewageFacilityInp = () => {
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)}>
|
||||
<TabsList>
|
||||
@@ -955,8 +1046,8 @@ const PublicSewageFacilityInp = () => {
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={getGridColumnInfo(tab.left)}
|
||||
rightColumnInfo={getGridColumnInfo(tab.right)}
|
||||
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)}
|
||||
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)}
|
||||
leftFixedColumns={tab.left.fixedColumns}
|
||||
rightFixedColumns={tab.right.fixedColumns}
|
||||
showHeaderCheckbox={true}
|
||||
@@ -1062,6 +1153,33 @@ const PublicSewageFacilityInp = () => {
|
||||
}
|
||||
showHeaderCheckbox={false}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
|
||||
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
||||
}
|
||||
}}
|
||||
cellTooltipCallback={({ row, fieldName }) => {
|
||||
const verification = (row as GridRowWithVerification).verificationResult;
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - ");
|
||||
const comp = e.compVlCn ?? "";
|
||||
const now = e.nowVlCn ?? "";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
@@ -1085,6 +1203,11 @@ const PublicSewageFacilityInp = () => {
|
||||
open={verificationResultDialogOpen}
|
||||
onOpenChange={setVerificationResultDialogOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
onExceptionSubmitted={async () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
if (!ctx || !searchParamRef.current) return;
|
||||
await loadPublicSewageFacilityActiveTabData(activeTab, searchParamRef.current, ctx);
|
||||
}}
|
||||
/>
|
||||
<MainFacilityMappingSaveDialog
|
||||
key={`main-facility-mapping-save-${mainFacilityMappingSaveDialogKey}`}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect,useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
@@ -192,11 +192,14 @@ type GridRowWithVerification = Record<string, string | number> & {
|
||||
};
|
||||
|
||||
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 {
|
||||
if (!verification) return false;
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
}
|
||||
// function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean {
|
||||
// if (!verification) return false;
|
||||
// return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
// }
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -355,6 +358,8 @@ const SewagePipeInp = () => {
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
||||
/** 탭키별 `engColNm -> columnGuide` 캐시 */
|
||||
const [columnGuidesByTab, setColumnGuidesByTab] = useState<Record<string, Record<string, string>>>({});
|
||||
/** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
/** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */
|
||||
@@ -370,6 +375,7 @@ const SewagePipeInp = () => {
|
||||
const hideGlobalProgress = useGlobalProgressStore((state) => state.hide);
|
||||
const globalProgressVisible = useGlobalProgressStore((state) => state.visible);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
const getFactInstallGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
@@ -417,6 +423,37 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const entries = await Promise.all(
|
||||
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;
|
||||
setColumnGuidesByTab(Object.fromEntries(entries));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
|
||||
const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => {
|
||||
if (loadedTabKey === activeTabRef.current) {
|
||||
setStatsCd(nextStatsCd);
|
||||
@@ -445,6 +482,14 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const applyColumnGuides = (cols: ColumnConfig[] | undefined, tabKey: string) =>
|
||||
(cols ?? []).map((col: ColumnConfig) => {
|
||||
const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim();
|
||||
return {
|
||||
...col,
|
||||
tooltip: guide && guide.length > 0 ? guide : undefined,
|
||||
};
|
||||
});
|
||||
return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => {
|
||||
const canEdit =
|
||||
computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo;
|
||||
@@ -458,16 +503,16 @@ const SewagePipeInp = () => {
|
||||
if (tab.left && tab.right) {
|
||||
return {
|
||||
...tab,
|
||||
left: { ...tab.left, columns: applyEditable(tab.left.columns) },
|
||||
right: { ...tab.right, columns: applyEditable(tab.right.columns) },
|
||||
left: { ...tab.left, columns: applyColumnGuides(applyEditable(tab.left.columns), tab.tabKey) },
|
||||
right: { ...tab.right, columns: applyColumnGuides(applyEditable(tab.right.columns), tab.tabKey) },
|
||||
};
|
||||
}
|
||||
return {
|
||||
...tab,
|
||||
columns: applyEditable(tab.columns),
|
||||
columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey),
|
||||
};
|
||||
});
|
||||
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]);
|
||||
}, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab]);
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
@@ -797,27 +842,27 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getTabGridRowsForVerification = (tabKey: string): Record<string, string | number>[] => {
|
||||
switch (tabKey) {
|
||||
case "ST00020001":
|
||||
return getGridDataRef.current?.() ?? [];
|
||||
case "ST00020002":
|
||||
return getFactInstallGridDataRef.current?.() ?? [];
|
||||
case "ST00020003":
|
||||
return getPipeTypeGridDataRef.current?.() ?? [];
|
||||
case "ST00020004":
|
||||
return getPipeDiameterGridDataRef.current?.() ?? [];
|
||||
case "ST00020005":
|
||||
return getManholeGridDataRef.current?.() ?? [];
|
||||
case "ST00020006":
|
||||
return [
|
||||
...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
|
||||
...(rowDataMap.get("adminAreaConnectionInp") ?? []),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
// const getTabGridRowsForVerification = (tabKey: string): Record<string, string | number>[] => {
|
||||
// switch (tabKey) {
|
||||
// case "ST00020001":
|
||||
// return getGridDataRef.current?.() ?? [];
|
||||
// case "ST00020002":
|
||||
// return getFactInstallGridDataRef.current?.() ?? [];
|
||||
// case "ST00020003":
|
||||
// return getPipeTypeGridDataRef.current?.() ?? [];
|
||||
// case "ST00020004":
|
||||
// return getPipeDiameterGridDataRef.current?.() ?? [];
|
||||
// case "ST00020005":
|
||||
// return getManholeGridDataRef.current?.() ?? [];
|
||||
// case "ST00020006":
|
||||
// return [
|
||||
// ...(rowDataMap.get("treatmentPlantConnectionInp") ?? []),
|
||||
// ...(rowDataMap.get("adminAreaConnectionInp") ?? []),
|
||||
// ];
|
||||
// default:
|
||||
// return [];
|
||||
// }
|
||||
// };
|
||||
|
||||
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
const tabKey = activeTabRef.current;
|
||||
@@ -859,21 +904,21 @@ const SewagePipeInp = () => {
|
||||
};
|
||||
|
||||
const submitFunc = async (isException: boolean) => {
|
||||
const tabKey = activeTabRef.current;
|
||||
// const tabKey = activeTabRef.current;
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
if (!isException) {
|
||||
const currentData = getTabGridRowsForVerification(tabKey);
|
||||
const rowsWithVerificationErrors = currentData.filter((d) =>
|
||||
hasRegionalFactVerificationErrors((d as GridRowWithVerification).verificationResult),
|
||||
);
|
||||
if (rowsWithVerificationErrors.length > 0) {
|
||||
alert("데이터에 오류가 있습니다.\n수정 후 제출해주세요.");
|
||||
return;
|
||||
}
|
||||
// const currentData = getTabGridRowsForVerification(tabKey);
|
||||
// const rowsWithVerificationErrors = currentData.filter((d) =>
|
||||
// hasRegionalFactVerificationErrors((d as GridRowWithVerification).verificationResult),
|
||||
// );
|
||||
// if (rowsWithVerificationErrors.length > 0) {
|
||||
// alert("데이터에 오류가 있습니다.\n수정 후 제출해주세요.");
|
||||
// return;
|
||||
// }
|
||||
if (await confirm("제출 하시겠습니까?")) {
|
||||
setIsExceptionSubmitFlow(false);
|
||||
setShowDailog(true);
|
||||
@@ -1251,7 +1296,7 @@ const SewagePipeInp = () => {
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
|
||||
statsYr: searchValues?.year ?? '',
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: tabStatsCdMap.get(activeTab) ?? statsCd,
|
||||
@@ -1739,10 +1784,12 @@ const SewagePipeInp = () => {
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
const showSaveInToolbar = !hideToolbarForConnectionTab && baseShowSaveInToolbar;
|
||||
const showSaveInToolbar =
|
||||
!hideToolbarForConnectionTab && baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons =
|
||||
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons;
|
||||
const showActionToolbarCard = !hideToolbarForConnectionTab && baseShowActionToolbarCard;
|
||||
!hideToolbarForConnectionTab && baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard =
|
||||
!hideToolbarForConnectionTab && baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1934,7 +1981,7 @@ const SewagePipeInp = () => {
|
||||
if (!verification) return;
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR");
|
||||
const hasImr = errors.some((e) => e.vrfcCd === "IMR" || e.vrfcCd === "HB");
|
||||
return hasImr ? "rg-cell-verify-error-imr" : "rg-cell-verify-error";
|
||||
}
|
||||
}}
|
||||
@@ -1946,10 +1993,11 @@ const SewagePipeInp = () => {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix =
|
||||
e.vrfcCd === "IMR" ? "IMR검증 - " : "일반검증 - ";
|
||||
e.vrfcCd === "IMR" ? "IMR검증 - " : (e.vrfcCd === "NV" ? "일반검증 - " : "HB검증 - ");
|
||||
const comp = e.compVlCn ?? "";
|
||||
const now = e.nowVlCn ?? "";
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${comp} / 현재 입력값: ${now})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@@ -2008,6 +2056,12 @@ const SewagePipeInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.year || !values?.sido || !values?.bsnmCd) return;
|
||||
const refreshedParam = buildSewagePipeSearchParam(values);
|
||||
await loadSewagePipeActiveTabData(activeTab, refreshedParam, values, true, false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -211,7 +211,7 @@ const SewagePipeMaintainInp = () => {
|
||||
if (!job) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(job.values, { forcedTabKey: job.tabKey });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- 검색폼 remount 직후 1회 자동 조회
|
||||
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect,useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import { useAlertStore, useGlobalProgressStore } from "@/shared/lib";
|
||||
import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import {
|
||||
STATISTICS_INPUT_URL_PATH,
|
||||
verificationSearchToFormValues,
|
||||
@@ -57,10 +57,13 @@ type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewerageSupplyRateVerificationResult;
|
||||
};
|
||||
|
||||
const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
||||
if (!verification) return false;
|
||||
return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
};
|
||||
/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */
|
||||
const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001";
|
||||
|
||||
// const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => {
|
||||
// if (!verification) return false;
|
||||
// return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0);
|
||||
// };
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -101,6 +104,14 @@ function getSewerageSupplyRateParams(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGridString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.trim() === "" ? null : value;
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 01. 하수도 보급률
|
||||
const SeweragePenetrationRateInp = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
@@ -119,6 +130,8 @@ const SeweragePenetrationRateInp = () => {
|
||||
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
|
||||
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
|
||||
const [statsCd, setStatsCd] = useState<string>('');
|
||||
/** `engColNm` → `columnGuide` (헤더 hover) */
|
||||
const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState<Record<string, string>>({});
|
||||
const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
const showGlobalProgress = useGlobalProgressStore((state) => state.show);
|
||||
@@ -149,7 +162,37 @@ const SeweragePenetrationRateInp = () => {
|
||||
setSearchFormRemountKey((k) => k + 1);
|
||||
}, [activeTabId, inputFocusNonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await statisticsApi.getColumnGuides(SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD);
|
||||
if (cancelled) return;
|
||||
if (res.status !== "success" || !Array.isArray(res.data)) {
|
||||
setColumnGuidesByEngCol({});
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
setColumnGuidesByEngCol(next);
|
||||
} catch {
|
||||
if (!cancelled) setColumnGuidesByEngCol({});
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTabId]);
|
||||
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
|
||||
const getGridDataRef = useRef<(() => Record<string, string | number>[]) | null>(null);
|
||||
|
||||
@@ -167,9 +210,17 @@ const SeweragePenetrationRateInp = () => {
|
||||
const { columns, columnLayout } = SeweragePenetrationRateInpColumn;
|
||||
const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const }));
|
||||
|
||||
const withHeaderTooltips = base.map((col) => {
|
||||
const guide = columnGuidesByEngCol[col.fieldName]?.trim();
|
||||
return {
|
||||
...col,
|
||||
tooltip: guide && guide.length > 0 ? guide : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
columnLayout,
|
||||
columns: base.map((col) => {
|
||||
columns: withHeaderTooltips.map((col) => {
|
||||
if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') };
|
||||
if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') };
|
||||
if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') };
|
||||
@@ -178,7 +229,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
return col;
|
||||
}),
|
||||
};
|
||||
}, [isEnterable, isAdminInfo, footerFromApi]);
|
||||
}, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
@@ -276,7 +327,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (!values) return;
|
||||
postVerificationSearchRef.current = null;
|
||||
void handleSearch(values);
|
||||
|
||||
|
||||
}, [searchFormRemountKey]);
|
||||
|
||||
const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => {
|
||||
@@ -291,7 +342,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
...submitInfo,
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
||||
statsYr: searchValues?.year ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
statsCd: statsCd,
|
||||
@@ -336,15 +387,15 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
const submitFunc = async (isException: boolean) => {
|
||||
if (!isException) {
|
||||
const isError = rowData.filter((d) =>
|
||||
hasVerificationErrors((d as GridRowWithVerification).verificationResult)
|
||||
);
|
||||
// const isError = rowData.filter((d) =>
|
||||
// hasVerificationErrors((d as GridRowWithVerification).verificationResult)
|
||||
// );
|
||||
|
||||
if (isError.length > 0) {
|
||||
alert(`데이터에 오류가 있습니다.
|
||||
수정 후 제출해주세요.`);
|
||||
return;
|
||||
}
|
||||
// if (isError.length > 0) {
|
||||
// alert(`데이터에 오류가 있습니다.
|
||||
// 수정 후 제출해주세요.`);
|
||||
// return;
|
||||
// }
|
||||
if (await confirm('제출 하시겠습니까?')) {
|
||||
setIsExceptionSubmitFlow(false);
|
||||
setShowDailog(true);
|
||||
@@ -396,11 +447,12 @@ const SeweragePenetrationRateInp = () => {
|
||||
};
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
sggNm: (d.sggNm as string) || null,
|
||||
admdstCd: d.admdstCd as string,
|
||||
admdstNm: d.admdstNm as string,
|
||||
trbtrNm: d.trbtrNm as string,
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null),
|
||||
sggNm: normalizeGridString(d.sggNm),
|
||||
admdstCd: normalizeGridString(d.admdstCd),
|
||||
admdstNm: normalizeGridString(d.admdstNm),
|
||||
trbtrNm: normalizeGridString(d.trbtrNm),
|
||||
spcCtpnRgnCn:
|
||||
d.spcCtpnRgnCn === '해당없음' ? null : normalizeGridString(d.spcCtpnRgnCn),
|
||||
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'),
|
||||
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'),
|
||||
ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'),
|
||||
@@ -413,7 +465,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'),
|
||||
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'),
|
||||
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'),
|
||||
rmrk: (d.rmrk as string) ?? '',
|
||||
rmrk: normalizeGridString(d.rmrk),
|
||||
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'),
|
||||
wholArea: formatNumber(d.wholArea, 'wholArea'),
|
||||
wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'),
|
||||
@@ -428,7 +480,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const requestData: SewerageSupplyRateSaveRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
||||
statsYr: searchValues?.year ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
submitList: convertedData,
|
||||
@@ -542,11 +594,14 @@ const SeweragePenetrationRateInp = () => {
|
||||
|
||||
const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({
|
||||
// 행정구역
|
||||
sggNm: (d.sggNm as string) || null, // 시군구
|
||||
admdstCd: d.admdstCd as string, // 행정코드
|
||||
admdstNm: d.admdstNm as string, // 행정구역명
|
||||
trbtrNm: d.trbtrNm as string, // 지류
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), // 특별대책지역
|
||||
sggNm: normalizeGridString(d.sggNm), // 시군구
|
||||
admdstCd: normalizeGridString(d.admdstCd), // 행정코드
|
||||
admdstNm: normalizeGridString(d.admdstNm), // 행정구역명
|
||||
trbtrNm: normalizeGridString(d.trbtrNm), // 지류
|
||||
spcCtpnRgnCn:
|
||||
d.spcCtpnRgnCn === '해당없음'
|
||||
? null
|
||||
: normalizeGridString(d.spcCtpnRgnCn), // 특별대책지역
|
||||
// 공공하수처리인구(명) - 물리적/생물학적/고도처리
|
||||
ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리
|
||||
ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리
|
||||
@@ -562,7 +617,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구
|
||||
spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구
|
||||
staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢)
|
||||
rmrk: (d.rmrk as string) ?? '', // 비고
|
||||
rmrk: normalizeGridString(d.rmrk), // 비고
|
||||
// 합계·소계(계산필드 저장용)
|
||||
wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명)
|
||||
wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢)
|
||||
@@ -579,7 +634,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const requestData: SewerageSupplyRateSaveRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드
|
||||
ctpvNm: searchValues?.sido ?? '', // 시도
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''),
|
||||
statsYr: searchValues?.year ?? '', // 통계 연도
|
||||
userId: userInfo.userUnqId, // 사용자
|
||||
saveList: convertedData, // 저장 데이터
|
||||
@@ -624,7 +679,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const requestData: cancelSubmitRequest = {
|
||||
sewerBzmnCd: searchValues?.bsnmCd ?? '',
|
||||
ctpvNm: searchValues?.sido ?? '',
|
||||
sggNm: searchValues?.gugun ?? '',
|
||||
sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""),
|
||||
statsYr: searchValues?.year ?? '',
|
||||
mdfrId: userInfo.userUnqId,
|
||||
statsCd: statsCd,
|
||||
@@ -656,12 +711,15 @@ const SeweragePenetrationRateInp = () => {
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL')
|
||||
: true;
|
||||
|
||||
const showSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const showEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
const baseShowSaveInToolbar = (isEnterable || isAdminInfo) && gugunOkOrNotDoList;
|
||||
const baseShowEnterableToolbarButtons = isEnterable && gugunOkOrNotDoList;
|
||||
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons = baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
/** 카드는 저장/제출줄/제출취소 중 하나라도 보일 때만 (관리자만으로 빈 카드 방지) */
|
||||
const showActionToolbarCard =
|
||||
rowData.length > 0 &&
|
||||
(showSaveInToolbar ||
|
||||
toolbarGroupAllowed &&
|
||||
(baseShowSaveInToolbar ||
|
||||
(statusInfo?.laeSbmsnSttsCd === '2' && (isAdminInfo || (isEnterable && gugunOkOrNotDoList))));
|
||||
|
||||
return (
|
||||
@@ -712,7 +770,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
const errors = verification[fieldName];
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
// IMR 검증은 노란색으로 구분
|
||||
const hasImr = errors.some((e) => e.vrfcCd === 'IMR');
|
||||
const hasImr = errors.some((e) => e.vrfcCd === 'IMR' || e.vrfcCd === 'HB');
|
||||
return hasImr ? 'rg-cell-verify-error-imr' : 'rg-cell-verify-error';
|
||||
}
|
||||
}}
|
||||
@@ -723,8 +781,9 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - ';
|
||||
return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NM' ? '일반검증 - ' : 'HB검증 -');
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
@@ -777,6 +836,11 @@ const SeweragePenetrationRateInp = () => {
|
||||
}
|
||||
: null
|
||||
}
|
||||
onExceptionSubmitted={async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values) return;
|
||||
await handleSearch(values);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { RealGridReact } from "realgrid-react";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAlertStore, useStatisticsInputStatusConfirmGroupGate } from "@/shared/lib";
|
||||
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
|
||||
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
|
||||
import type { StatsInputStatusListRequestParams } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import type { StatsInputStatusListApiResponse } from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
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 {
|
||||
StatsInputStatusListApiResponse,
|
||||
StatsInputStatusListRequestParams,
|
||||
} from "@/entities/statistics/model/input/statistics-input-status-interface";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import RejectButton from "@/components/button/RejectButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import {
|
||||
ExceptionProcessingHistoryDialog,
|
||||
type ExceptionProcessingHistoryQueryContext,
|
||||
} from "@/components/module/ExceptionProcessingHistoryDialog";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { StatsInputStatusInpColumn } from "@/components/util/column/input/StatsInputStatusInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { useTabStore } from "@/store/tab-store";
|
||||
@@ -25,13 +42,27 @@ const getStatsInputStatusFetcher = async (
|
||||
|
||||
// 하수도 통계 관리 > 00. 통계 입력 현황
|
||||
const StatsInputStatusInp = () => {
|
||||
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
);
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<StatsInputStatusListRequestParams | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] =
|
||||
useState<SearchFormValues | null>(null);
|
||||
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
|
||||
const periodCheckListRef = useRef<CheckInputPeriodItem[]>([]);
|
||||
const inputPeriodCurrentRef = useRef(false);
|
||||
const gridRef = useRef<RealGridReact | null>(null);
|
||||
const [exceptionHistoryOpen, setExceptionHistoryOpen] = useState(false);
|
||||
const [exceptionHistoryContext, setExceptionHistoryContext] = useState<
|
||||
ExceptionProcessingHistoryQueryContext | null
|
||||
>(null);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const confirmGroupToolbarAllowed = useStatisticsInputStatusConfirmGroupGate();
|
||||
|
||||
const swrKey = searchParams
|
||||
? (["stats-input-status", searchParams] as const)
|
||||
@@ -44,13 +75,22 @@ const StatsInputStatusInp = () => {
|
||||
revalidateOnFocus: true,
|
||||
onSuccess: (data) => {
|
||||
if (data.status === "success" && data.data) {
|
||||
setRowData(toGridRows(data.data));
|
||||
setRowData(
|
||||
toGridRows(data.data).map((r) => ({
|
||||
...r,
|
||||
exceptionSubmitHistory: "예외제출내역",
|
||||
})),
|
||||
);
|
||||
setHasCompletedSearch(true);
|
||||
} else {
|
||||
setHasCompletedSearch(false);
|
||||
setRowData([]);
|
||||
alert(data.message ?? "조회에 실패했습니다.");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
setHasCompletedSearch(false);
|
||||
setRowData([]);
|
||||
},
|
||||
},
|
||||
@@ -62,45 +102,184 @@ const StatsInputStatusInp = () => {
|
||||
}
|
||||
}, [activeTabId]);
|
||||
|
||||
const handleSearch = (values: SearchFormValues) => {
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (values.sido === "" || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
const params: StatsInputStatusListRequestParams = {
|
||||
ctpvNm: values.sido,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
statsYr: values.year ?? "",
|
||||
};
|
||||
setSearchParams(params);
|
||||
mutate(["stats-input-status", params]);
|
||||
|
||||
try {
|
||||
const response = await statisticsInputSewSupplyRateApi.checkInputPeriod(values.year || "");
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message ?? "입력기간 확인에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const periods = Array.isArray(response.data) ? response.data : [];
|
||||
periodCheckListRef.current = periods;
|
||||
const checkEnterable = isInputPeriodCurrent(periods);
|
||||
inputPeriodCurrentRef.current = checkEnterable;
|
||||
|
||||
const params: StatsInputStatusListRequestParams = {
|
||||
ctpvNm: values.sido,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
statsYr: values.year ?? "",
|
||||
};
|
||||
setHasCompletedSearch(false);
|
||||
setLastSearchedValues(values);
|
||||
setSearchParams(params);
|
||||
mutate(["stats-input-status", params]);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSearchParams(null);
|
||||
setRowData([]);
|
||||
setLastSearchedValues(null);
|
||||
setHasCompletedSearch(false);
|
||||
periodCheckListRef.current = [];
|
||||
inputPeriodCurrentRef.current = false;
|
||||
setExceptionHistoryOpen(false);
|
||||
setExceptionHistoryContext(null);
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const rejectFunc = () => {
|
||||
// 보완 요청 로직
|
||||
const approveFunc = async () => {
|
||||
if (rowData.length === 0) {
|
||||
alert("조회된 통계 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues?.year?.trim() || !lastSearchedValues?.bsnmCd?.trim()) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const hasInvalidStatus = rowData.some((row) =>
|
||||
["0", "1", "4"].includes(String(row.laeSbmsnSttsCd ?? "")),
|
||||
);
|
||||
|
||||
if (hasInvalidStatus) {
|
||||
alert("미작성이나 미제출, 혹은 보완요청(반려) 상태인 항목이 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproveLoading(true);
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
if (!mdfrId) {
|
||||
alert("수정자 ID를 확인할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const statsYr = lastSearchedValues.year.trim();
|
||||
const sewerBzmnCdDefault = lastSearchedValues.bsnmCd.trim();
|
||||
|
||||
const approveItems: StatsInputStatusApproveItem[] = rowData
|
||||
.map((row) => {
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
|
||||
return { statsYr, sewerBzmnCd, statsCd, mdfrId };
|
||||
})
|
||||
.filter((item) => item.statsCd.length > 0);
|
||||
|
||||
if (approveItems.length === 0) {
|
||||
alert("통계 코드(statsCd)가 있는 행이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "INPUT");
|
||||
if (response.resultCode.toLocaleLowerCase() === "success") {
|
||||
if (searchParams) {
|
||||
await mutate(["stats-input-status", searchParams]);
|
||||
}
|
||||
alert(response.message ?? "승인되었습니다.");
|
||||
} else {
|
||||
alert(response.message ?? "승인에 실패했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsApproveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const approveFunc = () => {
|
||||
// 승인 요청 로직
|
||||
};
|
||||
const openExceptionHistoryForRow = useCallback(
|
||||
(row: Record<string, string | number>) => {
|
||||
const ctx = lastSearchedValues;
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
if (!statsCd) {
|
||||
alert("통계 코드가 없어 예외처리 내역을 조회할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
setExceptionHistoryContext({
|
||||
statsYr: ctx.year ?? "",
|
||||
sewerBzmnCd: ctx.bsnmCd ?? "",
|
||||
ctpvNm: ctx.sido ?? "",
|
||||
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
||||
statsCd,
|
||||
});
|
||||
setExceptionHistoryOpen(true);
|
||||
},
|
||||
[alert, lastSearchedValues],
|
||||
);
|
||||
|
||||
const handleExceptionSubmitHistoryCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== "exceptionSubmitHistory") return;
|
||||
openExceptionHistoryForRow(row);
|
||||
},
|
||||
[openExceptionHistoryForRow],
|
||||
);
|
||||
|
||||
const handleExceptionHistoryRejectSuccess = useCallback(async () => {
|
||||
if (!searchParams) return;
|
||||
await mutate(["stats-input-status", searchParams]);
|
||||
}, [mutate, searchParams]);
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
|
||||
: true;
|
||||
|
||||
const allRowsLaeSbmsnApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.laeSbmsnSttsCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
const showActionToolbarCard =
|
||||
hasCompletedSearch &&
|
||||
rowData.length > 0 &&
|
||||
gugunOkOrNotDoList &&
|
||||
inputPeriodCurrentRef.current &&
|
||||
!allRowsLaeSbmsnApproved &&
|
||||
confirmGroupToolbarAllowed;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<RejectButton rejectFunc={rejectFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
)}
|
||||
{(isLoading || isApproveLoading) && (
|
||||
<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]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
@@ -114,9 +293,37 @@ const StatsInputStatusInp = () => {
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
showHeaderCheckbox={false}
|
||||
getGridRef={gridRef}
|
||||
onDataCellClick={handleExceptionSubmitHistoryCellClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName !== "laeSbmsnSttsNm") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const statusCd = String(row.laeSbmsnSttsCd ?? "");
|
||||
if (statusCd === "0" || statusCd === "1") {
|
||||
return "rg-cell-stats-status-pending";
|
||||
}
|
||||
if (statusCd === "2") {
|
||||
return "rg-cell-stats-status-submitted";
|
||||
}
|
||||
if (statusCd === "3") {
|
||||
return "rg-cell-stats-status-approved";
|
||||
}
|
||||
if (statusCd === "4") {
|
||||
return "rg-cell-stats-status-rejected";
|
||||
}
|
||||
return undefined;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ExceptionProcessingHistoryDialog
|
||||
open={exceptionHistoryOpen}
|
||||
onOpenChange={setExceptionHistoryOpen}
|
||||
queryContext={exceptionHistoryContext}
|
||||
onRejectSuccess={handleExceptionHistoryRejectSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import useSWR from "swr";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
// import type { StatisticsLogResponse } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
ApiResponse,
|
||||
SearchCondition,
|
||||
@@ -129,7 +130,7 @@ const LogHistory = () => {
|
||||
const selectedBsnm =
|
||||
value === "ALL"
|
||||
? (sidoList?.data.sidoList.find((v) => v.sido === sido)
|
||||
?.bsnmCd ?? "")
|
||||
?.bsnmCd ?? "")
|
||||
: (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? "");
|
||||
setGugun(value);
|
||||
setBsnmCd(selectedBsnm);
|
||||
@@ -150,8 +151,8 @@ const LogHistory = () => {
|
||||
...(bsnmCd && { sewerBzmnCd: bsnmCd }),
|
||||
...(statsDataChgComCd &&
|
||||
statsDataChgComCd !== "ALL" && {
|
||||
statsDataChgComCd,
|
||||
}),
|
||||
statsDataChgComCd,
|
||||
}),
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -160,15 +161,19 @@ const LogHistory = () => {
|
||||
const response = await statisticsApi.getLogList(param);
|
||||
|
||||
if (response.status === "success") {
|
||||
const lostList = response.data?.statsLogList ?? [];
|
||||
const logListRaw = response.data?.statsLogList ?? [];
|
||||
const logList = logListRaw.map((l) => ({
|
||||
...l,
|
||||
sggNm: String(l.sggNm ?? "").trim() || "-",
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(lostList));
|
||||
setRowData(toGridRows(logList));
|
||||
setTotalPages(response.data?.pageInfo?.totalPages ?? 0);
|
||||
setPageIndex(Math.max(0, page1Based - 1));
|
||||
} else {
|
||||
alert(
|
||||
response.message ||
|
||||
"데이터 조회 중 오류가 발생했습니다.",
|
||||
"데이터 조회 중 오류가 발생했습니다.",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
@@ -331,7 +336,7 @@ const LogHistory = () => {
|
||||
columnInfo={LogHistoryColumn}
|
||||
isExcelImport={false}
|
||||
rowData={rowData}
|
||||
setRowData={() => {}}
|
||||
setRowData={() => { }}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
<LogHistoryDetailDialog
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { ColumnConfig, DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
/** API `YYYYMMDD` 등 → Date */
|
||||
function seTypeToDisplayLabel(seType: StatsInputPeriodItem["seType"]): string {
|
||||
if (seType === "INPT") return "입력";
|
||||
if (seType === "VRFC") return "검증";
|
||||
@@ -32,8 +33,7 @@ function rowSeTypeToCode(row: Record<string, string | number>): "INPT" | "VRFC"
|
||||
return "INPT";
|
||||
}
|
||||
|
||||
/** API `YYYYMMDD` 등 → Date */
|
||||
function parseYmdToDate(ymd: unknown): Date | undefined {
|
||||
function parseYmdToDate(ymd: string): Date | undefined {
|
||||
const s = String(ymd ?? "").replace(/\D/g, "");
|
||||
if (s.length !== 8) return undefined;
|
||||
const y = Number(s.slice(0, 4));
|
||||
@@ -47,6 +47,14 @@ function parseYmdToDate(ymd: unknown): Date | undefined {
|
||||
return dt;
|
||||
}
|
||||
|
||||
function formatYmdForDisplay(ymd: string): string {
|
||||
const s = String(ymd ?? "").replace(/\D/g, "");
|
||||
if (s.length !== 8) return String(ymd ?? "");
|
||||
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
const PERIOD_DETAIL_FIELD = "detailInfo";
|
||||
|
||||
const StatisticsInputManagement = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -71,15 +79,18 @@ const StatisticsInputManagement = () => {
|
||||
setIsLoading(true);
|
||||
const params: StatsInputPeriodListRequest = {
|
||||
statsYr: values.year,
|
||||
...(values.clsfCd && values.clsfCd !== "ALL"
|
||||
? { seType: values.clsfCd }
|
||||
: {}),
|
||||
};
|
||||
if (values.clsfCd && values.clsfCd !== "ALL") {
|
||||
params.seType = values.clsfCd;
|
||||
}
|
||||
const res = await statisticsApi.getStatsInputPeriodList(params);
|
||||
if (res.status === "success" && Array.isArray(res.data)) {
|
||||
const rowsForGrid = res.data.map((row) => ({
|
||||
...row,
|
||||
seType: seTypeToDisplayLabel(row.seType),
|
||||
inptBgngYmd: formatYmdForDisplay(row.inptBgngYmd),
|
||||
inptEndYmd: formatYmdForDisplay(row.inptEndYmd),
|
||||
[PERIOD_DETAIL_FIELD]: "상세정보",
|
||||
}));
|
||||
setRowData(toGridRows(rowsForGrid));
|
||||
} else {
|
||||
@@ -111,22 +122,59 @@ const StatisticsInputManagement = () => {
|
||||
setPeriodDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Record<string, string | number>) => {
|
||||
const seCdRaw = row.seCd;
|
||||
const seCdNum =
|
||||
typeof seCdRaw === "number"
|
||||
? seCdRaw
|
||||
: parseInt(String(seCdRaw ?? ""), 10);
|
||||
setPeriodDialogMode("edit");
|
||||
setPeriodDialogInitial({
|
||||
statsYr: String(row.statsYr ?? ""),
|
||||
seType: rowSeTypeToCode(row),
|
||||
seCd: Number.isFinite(seCdNum) ? seCdNum : 0,
|
||||
inptBgngYmd: parseYmdToDate(row.inptBgngYmd),
|
||||
inptEndYmd: parseYmdToDate(row.inptEndYmd),
|
||||
});
|
||||
setPeriodDialogOpen(true);
|
||||
};
|
||||
const openPeriodDetailForRow = useCallback(
|
||||
async (row: Record<string, string | number>) => {
|
||||
const seCdRaw = row.seCd;
|
||||
const seCdNum =
|
||||
typeof seCdRaw === "number"
|
||||
? seCdRaw
|
||||
: parseInt(String(seCdRaw ?? ""), 10);
|
||||
const seCd = Number.isFinite(seCdNum) ? seCdNum : 0;
|
||||
const statsYr = String(row.statsYr ?? "");
|
||||
const seType = rowSeTypeToCode(row);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsApi.getStatsInputPeriodDetail({
|
||||
statsYr,
|
||||
seType,
|
||||
seCd,
|
||||
});
|
||||
|
||||
if (res.status !== "success" || !res.data) {
|
||||
alert(res.message || "상세 정보를 조회하지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = res.data;
|
||||
setPeriodDialogMode("edit");
|
||||
setPeriodDialogInitial({
|
||||
statsYr: String(detail.statsYr ?? statsYr),
|
||||
seType: detail.seType === "VRFC" ? "VRFC" : "INPT",
|
||||
seCd:
|
||||
typeof detail.seCd === "number" && Number.isFinite(detail.seCd)
|
||||
? detail.seCd
|
||||
: seCd,
|
||||
inptBgngYmd: parseYmdToDate(detail.inptBgngYmd),
|
||||
inptEndYmd: parseYmdToDate(detail.inptEndYmd),
|
||||
});
|
||||
setPeriodDialogOpen(true);
|
||||
} catch {
|
||||
alert("상세 정보 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[alert],
|
||||
);
|
||||
|
||||
const handlePeriodDetailCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== PERIOD_DETAIL_FIELD) return;
|
||||
void openPeriodDetailForRow(row);
|
||||
},
|
||||
[openPeriodDetailForRow],
|
||||
);
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{
|
||||
@@ -159,6 +207,14 @@ const StatisticsInputManagement = () => {
|
||||
header: { text: "종료일" },
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: PERIOD_DETAIL_FIELD,
|
||||
fieldName: PERIOD_DETAIL_FIELD,
|
||||
header: { text: "상세정보" },
|
||||
editable: false,
|
||||
width: 118,
|
||||
styleName: "rg-cell-outline-action-btn",
|
||||
},
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
@@ -167,6 +223,7 @@ const StatisticsInputManagement = () => {
|
||||
"seCd",
|
||||
"inptBgngYmd",
|
||||
"inptEndYmd",
|
||||
PERIOD_DETAIL_FIELD,
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -198,7 +255,7 @@ const StatisticsInputManagement = () => {
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
onRowClick={handleRowClick}
|
||||
onDataCellClick={handlePeriodDetailCellClick}
|
||||
/>
|
||||
<StatisticsInputPeriodDialog
|
||||
open={periodDialogOpen}
|
||||
|
||||
@@ -140,11 +140,13 @@ export function StatisticsInputPeriodDialog({
|
||||
const param: StatsInputPeriodSaveRequest = {
|
||||
statsYr: yr,
|
||||
seType,
|
||||
seCd: seCdParsed,
|
||||
inptBgngYmd: bgng,
|
||||
inptEndYmd: end,
|
||||
rgtrId: userInfo.userUnqId,
|
||||
};
|
||||
if (!isCreate) {
|
||||
param.seCd = seCdParsed;
|
||||
}
|
||||
const res = await statisticsApi.saveStatsInputPeriod(param);
|
||||
if (res.status === "success") {
|
||||
alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`);
|
||||
@@ -243,7 +245,7 @@ export function StatisticsInputPeriodDialog({
|
||||
<Select
|
||||
value={seType ?? undefined}
|
||||
onValueChange={(v) => setSeType(v as "INPT" | "VRFC")}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || mode === "edit"}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="선택" />
|
||||
|
||||
@@ -3,37 +3,92 @@ import { useState } from "react";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
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 { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const columns: ColumnConfig[] = [
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' }, mergeCells: true, editable: false, sortdisable: true },
|
||||
{ name: '구분', fieldName: '구분', header: { text: '구분' }, editable: false, sortdisable: true },
|
||||
{ name: '광역자치단체', fieldName: '광역자치단체', header: { text: '광역자치단체' }, editable: false, sortdisable: true },
|
||||
{ name: '공단', fieldName: '공단', header: { text: '공단' }, editable: false, sortdisable: true },
|
||||
{ name: '처리내역', fieldName: '처리내역', header: { text: '처리내역' }, editable: false, sortdisable: true },
|
||||
{ name: "statsYr", fieldName: "statsYr", header: { text: "연도" }, editable: false },
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "sewerBzmnCd", fieldName: "sewerBzmnCd", header: { text: "하수사업자코드" }, editable: false },
|
||||
{ name: "statsCd", fieldName: "statsCd", header: { text: "통계코드" }, editable: false },
|
||||
{ name: "menuNm", fieldName: "menuNm", header: { text: "통계항목" }, editable: false },
|
||||
{ name: "chckerOpinionSm", fieldName: "chckerOpinionSm", header: { text: "검토자의견" }, editable: false },
|
||||
{ name: "wrterOpinionSm", fieldName: "wrterOpinionSm", header: { text: "처리내역" }, editable: false },
|
||||
];
|
||||
|
||||
const columnLayout = [
|
||||
'sido',
|
||||
'sgg',
|
||||
'구분',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '검토내역' },
|
||||
items: ['광역자치단체', '공단', '처리내역'],
|
||||
},
|
||||
"statsYr",
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"sewerBzmnCd",
|
||||
"statsCd",
|
||||
"menuNm",
|
||||
"chckerOpinionSm",
|
||||
"wrterOpinionSm",
|
||||
];
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.year?.trim()) {
|
||||
alert("년도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await statisticsApi.getStatiscicsManagementReviewHistory({
|
||||
statsYr: values.year.trim(),
|
||||
...(values.bsnmCd?.trim() ? { sewerBzmnCd: values.bsnmCd.trim() } : {}),
|
||||
...(values.sido && values.sido !== "ALL" ? { ctpvNm: values.sido } : {}),
|
||||
});
|
||||
|
||||
if (res.status === "success" && Array.isArray(res.data)) {
|
||||
setRowData(toGridRows(res.data));
|
||||
} else {
|
||||
setRowData([]);
|
||||
alert(res.message || "조회된 데이터가 없습니다.");
|
||||
}
|
||||
} catch {
|
||||
setRowData([]);
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<DataGrid columnInfo={{ columns, columnLayout }} rowStartNum={2} fixedColumns={3} rowData={rowData} setRowData={setRowData} isExcelImport={false} />
|
||||
</>
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
{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]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DataGrid
|
||||
columnInfo={{ columns, columnLayout }}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
enableSort={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatisticsReviewHistory;
|
||||
export default StatisticsReviewHistory;
|
||||
|
||||
@@ -1,47 +1,281 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { DataGrid } from "@rsone/components";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
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 {
|
||||
ReviewOpinionSaveRequest,
|
||||
ReviewerInfoRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
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 { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
|
||||
import {
|
||||
ReviewOpinionDialog,
|
||||
type ReviewOpinionDialogContext,
|
||||
} from "@/components/statistics/review/ReviewOpinionDialog";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn";
|
||||
|
||||
// 하수도 통계 관리 06.공공하수처리시설
|
||||
const PublicSewageFacilityRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [statsStatusInfo, setStatsStatusInfo] = useState<StatsStatusInfo>({});
|
||||
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
|
||||
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
|
||||
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
|
||||
const approveFunc = () => {
|
||||
console.log('approveFunc');
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout, fixedColumns, rowStartNum } = PublicSewageFacilityInfoList;
|
||||
return {
|
||||
columnLayout,
|
||||
fixedColumns,
|
||||
rowStartNum,
|
||||
columns: columns.map((col) => ({
|
||||
...col,
|
||||
editable: false as const,
|
||||
})),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
|
||||
const submit = statsStatusInfo.submitStatusCd != null ? String(statsStatusInfo.submitStatusCd) : "";
|
||||
const prvn = statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : "";
|
||||
const submitIs3 = submit === "3";
|
||||
const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1");
|
||||
return {
|
||||
showReviewOpinionButton: reviewSubmitVisible,
|
||||
showSubmitButton: reviewSubmitVisible,
|
||||
showReturnButton: submitIs3 && prvn !== "3",
|
||||
};
|
||||
}, [statsStatusInfo]);
|
||||
|
||||
const showActionBar =
|
||||
showReviewOpinionButton || showReturnButton || showSubmitButton;
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (reviewOpinionContext == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const instSeCd = userInfo?.instSeCd ?? "";
|
||||
if (!instSeCd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
instSeCd,
|
||||
};
|
||||
}, [reviewOpinionContext, userInfo?.instSeCd]);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
setLastSearchValues(null);
|
||||
setSubmitDialogOpen(false);
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log('submit');
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
setLastSearchValues(values);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await statisticsManagementApi.getPublicSewageFacilityReviewList({
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
statsYr: values.year,
|
||||
swtrmtFcltNm: values.fctName,
|
||||
});
|
||||
|
||||
if (response.status === "success" && response.data) {
|
||||
const rawList = response.data.publicSewageList;
|
||||
const convertedData: PublicSewageFacilityReviewItem[] = rawList.map((item) => ({
|
||||
...item,
|
||||
impdFcltPstnCn: item.fcltPstnCn ?? "",
|
||||
prxyBzentyNmCn: item.prxyBzentyNmCn ?? "",
|
||||
prxyCtrtCn: item.prxyCtrtCn ?? "",
|
||||
intgPrcsFcltCn: item.intgPrcsFcltCn ?? "",
|
||||
rmrk: item.rmrk ?? "",
|
||||
})) as PublicSewageFacilityReviewItem[];
|
||||
|
||||
setStatsStatusInfo(response.data.statsStatusInfo);
|
||||
setReviewOpinionContext({
|
||||
statsYr: values.year ?? "",
|
||||
sewerBzmnCd: values.bsnmCd ?? "",
|
||||
statsCd: response.data.statsStatusInfo.statsCd ?? "",
|
||||
});
|
||||
setLatestReviewOpinion(
|
||||
response.data.statsStatusInfo.splmntOpnnCn ?? "",
|
||||
);
|
||||
setLatestProcessHistory(
|
||||
response.data.statsStatusInfo.wrterOpnnCn ?? "",
|
||||
);
|
||||
setRowData(toGridRows(convertedData as unknown as object[]));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
setReviewOpinionContext(null);
|
||||
setLatestReviewOpinion("");
|
||||
setLatestProcessHistory("");
|
||||
alert(response.message || "데이터가 없습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("공공하수처리시설 조회 중 에러 발생:", error);
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = () => {
|
||||
console.log('reviewOpinionFunc');
|
||||
const submitFunc = () => {
|
||||
if (submitDialogContext == null) {
|
||||
alert("조회 후 제출할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitDialogOpen(true);
|
||||
};
|
||||
|
||||
const returnFunc = () => {
|
||||
console.log('returnFunc');
|
||||
const reviewOpinionFunc = async () => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 검토의견을 작성할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const detailRes = await statisticsApi.getReviewOpinionDetail({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
});
|
||||
|
||||
if (detailRes.status !== "success") {
|
||||
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setLatestReviewOpinion(row?.splmntOpnnCn ?? "");
|
||||
setLatestProcessHistory(row?.wrterOpnnCn ?? "");
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
|
||||
try {
|
||||
const response = await statisticsApi.saveReviewOpinion(param);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert(response.message || "저장되었습니다.");
|
||||
setStatsStatusInfo((prev) => ({
|
||||
...prev,
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
wrterOpnnCn: param.wrterOpnnCn,
|
||||
}));
|
||||
setLatestReviewOpinion(param.splmntOpnnCn);
|
||||
setLatestProcessHistory(param.wrterOpnnCn);
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const returnFunc = () => setReturnDialogOpen(true);
|
||||
|
||||
const handleSaveReturnReason = async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm fctName={true} />
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
<ReturnButton returnFunc={returnFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<div className="relative">
|
||||
<SearchForm fctName={true} onSubmit={handleSearch} onReset={handleReset} />
|
||||
{showActionBar && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
{showReviewOpinionButton && (
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
)}
|
||||
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
|
||||
{showSubmitButton && <SubmitButton submitFunc={submitFunc} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataGrid columnInfo={PublicSewageFacilityInfoList} isExcelImport={false} rowStartNum={3} fixedColumns={3} rowData={rowData} setRowData={setRowData} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{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]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
fixedColumns={columnInfo.fixedColumns}
|
||||
rowStartNum={columnInfo.rowStartNum}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
/>
|
||||
<ReviewOpinionDialog
|
||||
open={reviewOpinionOpen}
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
open={submitDialogOpen}
|
||||
onOpenChange={setSubmitDialogOpen}
|
||||
context={submitDialogContext}
|
||||
onSubmitted={async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ReturnReasonDialog
|
||||
open={returnDialogOpen}
|
||||
onOpenChange={setReturnDialogOpen}
|
||||
context={reviewOpinionContext}
|
||||
onSave={handleSaveReturnReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -14,25 +16,47 @@ import {
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import type { ReviewReturnSaveRequest } from "@/entities/statistics/model/statistics-interface";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export type ReturnReasonDialogContext = Pick<
|
||||
ReviewReturnSaveRequest,
|
||||
"statsYr" | "sewerBzmnCd" | "statsCd"
|
||||
>;
|
||||
|
||||
export interface ReturnReasonDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave?: (reason: string) => void | Promise<void>;
|
||||
context: ReturnReasonDialogContext | null;
|
||||
onSave?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ReturnReasonDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
context,
|
||||
onSave,
|
||||
}: ReturnReasonDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const [reason, setReason] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [reasonError, setReasonError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const resetForm = () => {
|
||||
setReason("");
|
||||
setReasonError(false);
|
||||
};
|
||||
|
||||
const handleDialogOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) resetForm();
|
||||
onOpenChange(nextOpen);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
resetForm();
|
||||
}, [open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -41,24 +65,65 @@ export function ReturnReasonDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
context == null ||
|
||||
context.statsYr === "" ||
|
||||
context.sewerBzmnCd === "" ||
|
||||
context.statsCd === ""
|
||||
) {
|
||||
alert("시도·시군구·년도로 조회한 뒤 반려 처리할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("반려 처리 하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await onSave?.(reason);
|
||||
let userInfo: Awaited<ReturnType<typeof getUserInfo>>;
|
||||
try {
|
||||
userInfo = await getUserInfo();
|
||||
} catch {
|
||||
alert("사용자 정보를 불러오지 못했습니다. 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const param: ReviewReturnSaveRequest = {
|
||||
type: "REVIEW",
|
||||
statsYr: context.statsYr,
|
||||
sewerBzmnCd: context.sewerBzmnCd,
|
||||
statsCd: context.statsCd,
|
||||
rjctOpnnCn: reason,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
};
|
||||
|
||||
const response = await statisticsApi.saveReviewReturn(param);
|
||||
if (response.resultCode.toLowerCase() !== "success") {
|
||||
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert("반려 처리 되었습니다.");
|
||||
await onSave?.();
|
||||
resetForm();
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
alert("반려 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleDialogOpenChange}>
|
||||
<DialogContent className="max-w-[56rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>반려 사유</DialogTitle>
|
||||
<DialogTitle>반려</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">반려 사유</Label>
|
||||
<Label className="text-label mb-2">반려 사유</Label>
|
||||
<Textarea
|
||||
value={reason}
|
||||
onChange={(e) => {
|
||||
@@ -86,11 +151,11 @@ export function ReturnReasonDialog({
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
color="red"
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
저장
|
||||
반려 제출
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -115,9 +115,9 @@ export function ReviewOpinionDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토의견작성</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 py-2">
|
||||
<div className="flex flex-col gap-8 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">검토의견</Label>
|
||||
<Label className="text-label mb-2">검토의견</Label>
|
||||
<Textarea
|
||||
value={reviewOpinion}
|
||||
onChange={(e) => {
|
||||
@@ -137,7 +137,7 @@ export function ReviewOpinionDialog({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">처리내역</Label>
|
||||
<Label className="text-label mb-2">처리내역</Label>
|
||||
<Textarea
|
||||
value={processHistory}
|
||||
onChange={(e) => setProcessHistory(e.target.value)}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
LoadingWithAnimation,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
ReviewerInfoRequest,
|
||||
ReviewerInfoResponse,
|
||||
ReviewSubmitRequest,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export interface ReviewSubmitDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
context: ReviewerInfoRequest | null;
|
||||
onSubmitted?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ReviewSubmitDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
context,
|
||||
onSubmitted,
|
||||
}: ReviewSubmitDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [reviewerInfo, setReviewerInfo] =
|
||||
useState<ReviewerInfoResponse["data"] | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setReviewerInfo(null);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
setReviewerInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadReviewerInfo = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await statisticsApi.getReviewerInfo(context);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert("제출 정보를 불러오지 못했습니다.");
|
||||
setReviewerInfo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setReviewerInfo(response.data);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
alert("제출 정보를 불러오지 못했습니다.");
|
||||
setReviewerInfo(null);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadReviewerInfo();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, context, alert]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (context == null || reviewerInfo == null) {
|
||||
alert("제출 정보를 확인한 뒤 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const userInfo = await getUserInfo();
|
||||
const submitParam: ReviewSubmitRequest = {
|
||||
statsYr: context.statsYr,
|
||||
sewerBzmnCd: context.sewerBzmnCd,
|
||||
statsCd: context.statsCd,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
splmntOpnnCn: reviewerInfo.splmntOpnnCn ?? "",
|
||||
wrterOpnnCn: reviewerInfo.wrterOpnnCn ?? "",
|
||||
};
|
||||
|
||||
const response = await statisticsApi.submitReview(submitParam);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "제출에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert(response.message || "제출되었습니다.");
|
||||
onOpenChange(false);
|
||||
await onSubmitted?.();
|
||||
} catch {
|
||||
alert("제출 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[64rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>제출 정보</DialogTitle>
|
||||
</DialogHeader>
|
||||
{(isLoading || isSubmitting) && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10 [data-theme='black']:bg-transparent">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 py-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">검토자명</Label>
|
||||
<Input value={reviewerInfo?.userNm ?? "-"} readOnly />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">연락처</Label>
|
||||
<Input value={reviewerInfo?.telOffrm ?? "-"} readOnly />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">부서</Label>
|
||||
<Input value={reviewerInfo?.dpmNm ?? "-"} readOnly />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-label">직책</Label>
|
||||
<Input value={reviewerInfo?.cps ?? "-"} readOnly />
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-2">
|
||||
<Label className="text-label">검토의견</Label>
|
||||
<Textarea
|
||||
value={reviewerInfo?.splmntOpnnCn ?? "-"}
|
||||
readOnly
|
||||
className="min-h-[10rem] p-2 resize-none !text-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-col gap-2">
|
||||
<Label className="text-label">처리내역</Label>
|
||||
<Textarea
|
||||
value={reviewerInfo?.wrterOpnnCn ?? "-"}
|
||||
readOnly
|
||||
className="min-h-[10rem] p-2 resize-none !text-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
onClick={() => void handleConfirm()}
|
||||
disabled={isLoading || isSubmitting || reviewerInfo == null}
|
||||
>
|
||||
제출
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="gray"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isLoading || isSubmitting}
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useAlertStore, useUserStore } from "@/shared/lib";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type {
|
||||
SewagePipeReviewRequestParams,
|
||||
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 SubmitButton from "@/components/button/SumitButton";
|
||||
@@ -13,13 +18,58 @@ import ReturnButton from "@/components/button/ReturnButton";
|
||||
import ReviewOpinionButton from "@/components/button/ReviewOpinionButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog";
|
||||
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
|
||||
import {
|
||||
ReviewOpinionDialog,
|
||||
type ReviewOpinionDialogContext,
|
||||
} from "@/components/statistics/review/ReviewOpinionDialog";
|
||||
import type {
|
||||
ColumnConfig,
|
||||
ColumnInfo,
|
||||
ColumnInfoList,
|
||||
ColumnLayoutItem,
|
||||
} from "@/components/type/DataGrid.type";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
|
||||
const firstTabKey = SewagePipeColumnInfoListRvw[0]?.tabKey ?? "ST00020001";
|
||||
|
||||
function cloneColumnLayoutItem(item: ColumnLayoutItem): ColumnLayoutItem {
|
||||
if (typeof item === "string") return item;
|
||||
if ("column" in item) {
|
||||
return {
|
||||
column: item.column,
|
||||
...(item.footerUserSpans && {
|
||||
footerUserSpans: item.footerUserSpans.map((span) => ({ ...span })),
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
header: { ...item.header },
|
||||
items: item.items.map((child) =>
|
||||
typeof child === "string" ? child : cloneColumnLayoutItem(child),
|
||||
),
|
||||
...(item.footerUserSpans && {
|
||||
footerUserSpans: item.footerUserSpans.map((span) => ({ ...span })),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function cloneColumnConfig(col: ColumnConfig): ColumnConfig {
|
||||
return {
|
||||
...col,
|
||||
header: { ...col.header },
|
||||
...(col.footer && {
|
||||
footer: Array.isArray(col.footer)
|
||||
? col.footer.map((item) => ({ ...item }))
|
||||
: { ...col.footer },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchSewagePipeReviewTab(
|
||||
tabKey: string,
|
||||
searchParam: SewagePipeReviewRequestParams,
|
||||
@@ -36,6 +86,9 @@ async function fetchSewagePipeReviewTab(
|
||||
if (tabKey === "ST00020004") {
|
||||
return statisticsManagementApi.getSewagePipeDiamList(searchParam);
|
||||
}
|
||||
if (tabKey === "ST00020005") {
|
||||
return await statisticsManagementApi.getSewagePipeManholeList(searchParam);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,6 +109,8 @@ function extractReviewRowsFromResponseData(
|
||||
return Array.isArray(d.pipeTypeList) ? (d.pipeTypeList as object[]) : [];
|
||||
case "ST00020004":
|
||||
return Array.isArray(d.pipeDiamList) ? (d.pipeDiamList as object[]) : [];
|
||||
case "ST00020005":
|
||||
return Array.isArray(d.manholeList) ? (d.manholeList as object[]) : [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -70,9 +125,14 @@ function extractStatsStatusInfo(data: unknown): StatsStatusInfo | null {
|
||||
// 하수도 통계 검토 02.하수관로
|
||||
const SewagePipeRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>(firstTabKey);
|
||||
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
|
||||
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
|
||||
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
/** 탭별 조회 응답의 statsStatusInfo — 활성 탭에 맞춰 버튼 노출 (SewagePipeInp의 tabStatusInfoMap과 동일 패턴) */
|
||||
const [tabStatsStatusInfoMap, setTabStatsStatusInfoMap] = useState<
|
||||
Map<string, StatsStatusInfo>
|
||||
@@ -81,9 +141,53 @@ const SewagePipeRvw = () => {
|
||||
const searchParamRef = useRef<SewagePipeReviewRequestParams | null>(null);
|
||||
const searchDoneRef = useRef(false);
|
||||
const lazyTabLoggedRef = useRef<Set<string>>(new Set());
|
||||
const activeStatsStatusInfo = useMemo(
|
||||
() => tabStatsStatusInfoMap.get(activeTab) ?? {},
|
||||
[tabStatsStatusInfoMap, activeTab],
|
||||
);
|
||||
const columnInfo = useMemo<ColumnInfoList[]>(
|
||||
() =>
|
||||
SewagePipeColumnInfoListRvw.map((tab) => ({
|
||||
...tab,
|
||||
columns: (tab.columns ?? []).map((col) => cloneColumnConfig(col)),
|
||||
columnLayout: (tab.columnLayout ?? []).map((item) =>
|
||||
cloneColumnLayoutItem(item),
|
||||
),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
const reviewOpinionContext = useMemo<ReviewOpinionDialogContext | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
return {
|
||||
statsYr: lastSearchValues.year ?? "",
|
||||
sewerBzmnCd: lastSearchValues.bsnmCd ?? "",
|
||||
statsCd: activeStatsStatusInfo.statsCd ?? "",
|
||||
};
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues]);
|
||||
const latestReviewOpinion = activeStatsStatusInfo.splmntOpnnCn ?? "";
|
||||
const latestProcessHistory = activeStatsStatusInfo.wrterOpnnCn ?? "";
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (lastSearchValues == null) return null;
|
||||
|
||||
const statsYr = lastSearchValues.year ?? "";
|
||||
const sewerBzmnCd = lastSearchValues.bsnmCd ?? "";
|
||||
const statsCd = activeStatsStatusInfo.statsCd ?? "";
|
||||
const instSeCd = userInfo?.instSeCd ?? "";
|
||||
|
||||
if (!statsYr || !sewerBzmnCd || !statsCd || !instSeCd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
statsYr,
|
||||
sewerBzmnCd,
|
||||
statsCd,
|
||||
instSeCd,
|
||||
};
|
||||
}, [activeStatsStatusInfo.statsCd, lastSearchValues, userInfo?.instSeCd]);
|
||||
|
||||
const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => {
|
||||
const statsStatusInfo = tabStatsStatusInfoMap.get(activeTab) ?? {};
|
||||
const statsStatusInfo = activeStatsStatusInfo;
|
||||
const submit =
|
||||
statsStatusInfo.submitStatusCd != null
|
||||
? String(statsStatusInfo.submitStatusCd)
|
||||
@@ -97,7 +201,7 @@ const SewagePipeRvw = () => {
|
||||
showSubmitButton: reviewSubmitVisible,
|
||||
showReturnButton: submitIs3 && prvn !== "3",
|
||||
};
|
||||
}, [tabStatsStatusInfoMap, activeTab]);
|
||||
}, [activeStatsStatusInfo]);
|
||||
|
||||
const showActionBar =
|
||||
showReviewOpinionButton || showReturnButton || showSubmitButton;
|
||||
@@ -106,6 +210,10 @@ const SewagePipeRvw = () => {
|
||||
setRowDataMap(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
setTabStatsStatusInfoMap(new Map());
|
||||
setReviewOpinionOpen(false);
|
||||
setReturnDialogOpen(false);
|
||||
setSubmitDialogOpen(false);
|
||||
setLastSearchValues(null);
|
||||
searchParamRef.current = null;
|
||||
searchDoneRef.current = false;
|
||||
lazyTabLoggedRef.current.clear();
|
||||
@@ -119,6 +227,7 @@ const SewagePipeRvw = () => {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLastSearchValues(values);
|
||||
|
||||
const searchParam: SewagePipeReviewRequestParams = {
|
||||
statsYr: values.year,
|
||||
@@ -204,6 +313,79 @@ const SewagePipeRvw = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitFunc = () => {
|
||||
if (submitDialogContext == null) {
|
||||
alert("조회 후 제출할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitDialogOpen(true);
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = async () => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 검토의견을 작성할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const detailRes = await statisticsApi.getReviewOpinionDetail({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
});
|
||||
|
||||
if (detailRes.status !== "success") {
|
||||
alert(detailRes.message || "검토의견 조회에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const row = detailRes.data?.[0];
|
||||
setTabStatsStatusInfoMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: row?.splmntOpnnCn ?? "",
|
||||
wrterOpnnCn: row?.wrterOpnnCn ?? "",
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setReviewOpinionOpen(true);
|
||||
} catch {
|
||||
alert("검토의견 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => {
|
||||
try {
|
||||
const response = await statisticsApi.saveReviewOpinion(param);
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "저장에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
alert(response.message || "저장되었습니다.");
|
||||
setTabStatsStatusInfoMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(activeTab, {
|
||||
...(next.get(activeTab) ?? {}),
|
||||
splmntOpnnCn: param.splmntOpnnCn,
|
||||
wrterOpnnCn: param.wrterOpnnCn,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
alert("저장 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const returnFunc = () => setReturnDialogOpen(true);
|
||||
|
||||
const handleSaveReturnReason = async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
@@ -212,11 +394,11 @@ const SewagePipeRvw = () => {
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
{showReviewOpinionButton && (
|
||||
<ReviewOpinionButton reviewOpinionFunc={() => {}} />
|
||||
<ReviewOpinionButton reviewOpinionFunc={reviewOpinionFunc} />
|
||||
)}
|
||||
{showReturnButton && <ReturnButton returnFunc={() => {}} />}
|
||||
{showReturnButton && <ReturnButton returnFunc={returnFunc} />}
|
||||
{showSubmitButton && (
|
||||
<SubmitButton submitFunc={() => {}} />
|
||||
<SubmitButton submitFunc={() => submitFunc()} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,13 +419,13 @@ const SewagePipeRvw = () => {
|
||||
variant="default"
|
||||
>
|
||||
<TabsList>
|
||||
{SewagePipeColumnInfoListRvw.map((info) => (
|
||||
{columnInfo.map((info) => (
|
||||
<TabsTrigger key={info.tabKey} value={info.tabKey}>
|
||||
{info.tabLabel || info.tabKey}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{SewagePipeColumnInfoListRvw.map((info) => {
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
return null;
|
||||
@@ -283,6 +465,30 @@ const SewagePipeRvw = () => {
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
<ReviewOpinionDialog
|
||||
open={reviewOpinionOpen}
|
||||
onOpenChange={setReviewOpinionOpen}
|
||||
context={reviewOpinionContext}
|
||||
initialReviewOpinion={latestReviewOpinion}
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
open={submitDialogOpen}
|
||||
onOpenChange={setSubmitDialogOpen}
|
||||
context={submitDialogContext}
|
||||
onSubmitted={async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ReturnReasonDialog
|
||||
open={returnDialogOpen}
|
||||
onOpenChange={setReturnDialogOpen}
|
||||
context={reviewOpinionContext}
|
||||
onSave={handleSaveReturnReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,13 +6,15 @@ 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 { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
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 } from "@/entities/statistics/model/statistics-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 type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
@@ -25,17 +27,18 @@ import {
|
||||
// 하수도 통계 검토 01.하수도 보급률
|
||||
const SeweragePenetrationRateRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [statsStatusInfo, setStatsStatusInfo] = useState<statsStatusInfo>({});
|
||||
const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false);
|
||||
const [returnDialogOpen, setReturnDialogOpen] = useState(false);
|
||||
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
||||
const [reviewOpinionContext, setReviewOpinionContext] =
|
||||
useState<ReviewOpinionDialogContext | null>(null);
|
||||
const [latestReviewOpinion, setLatestReviewOpinion] = useState("");
|
||||
const [latestProcessHistory, setLatestProcessHistory] = useState("");
|
||||
const [lastSearchValues, setLastSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const { confirm } = useConfirm();
|
||||
|
||||
const columnInfo = useMemo(() => {
|
||||
const { columns, columnLayout, fixedColumns, rowStartNum } = SeweragePenetrationRateInfoList;
|
||||
@@ -66,6 +69,24 @@ const SeweragePenetrationRateRvw = () => {
|
||||
const showActionBar =
|
||||
showReviewOpinionButton || showReturnButton || showSubmitButton;
|
||||
|
||||
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
||||
if (reviewOpinionContext == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const instSeCd = userInfo?.instSeCd ?? "";
|
||||
if (!instSeCd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
instSeCd,
|
||||
};
|
||||
}, [reviewOpinionContext, userInfo?.instSeCd]);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setStatsStatusInfo({});
|
||||
@@ -129,37 +150,13 @@ const SeweragePenetrationRateRvw = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const submitFunc = async () => {
|
||||
if (reviewOpinionContext == null) {
|
||||
const submitFunc = () => {
|
||||
if (submitDialogContext == null) {
|
||||
alert("조회 후 제출할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("제출 하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await statisticsApi.submitReview({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
});
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "제출에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert(response.message || "제출되었습니다.");
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
} catch {
|
||||
alert("제출 중 오류가 발생했습니다.");
|
||||
}
|
||||
setSubmitDialogOpen(true);
|
||||
};
|
||||
|
||||
const reviewOpinionFunc = async () => {
|
||||
@@ -211,36 +208,9 @@ const SeweragePenetrationRateRvw = () => {
|
||||
|
||||
const returnFunc = () => setReturnDialogOpen(true);
|
||||
|
||||
const handleSaveReturnReason = async (reason: string) => {
|
||||
if (reviewOpinionContext == null) {
|
||||
alert("조회 후 반려할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (await confirm('반려 처리 하시겠습니까?')) {
|
||||
try {
|
||||
const userInfo = await getUserInfo();
|
||||
const response = await statisticsApi.saveReviewReturn({
|
||||
statsYr: reviewOpinionContext.statsYr,
|
||||
sewerBzmnCd: reviewOpinionContext.sewerBzmnCd,
|
||||
statsCd: reviewOpinionContext.statsCd,
|
||||
rjctOpnnCn: reason,
|
||||
mdfrId: userInfo.userUnqId,
|
||||
});
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(response.message || "반려 처리 중 오류가 발생했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
alert(response.message || "반려 처리 되었습니다.");
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
|
||||
} catch {
|
||||
alert("반려 처리 중 오류가 발생했습니다.");
|
||||
}
|
||||
const handleSaveReturnReason = async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -285,9 +255,20 @@ const SeweragePenetrationRateRvw = () => {
|
||||
initialProcessHistory={latestProcessHistory}
|
||||
onSave={saveReviewOpinion}
|
||||
/>
|
||||
<ReviewSubmitDialog
|
||||
open={submitDialogOpen}
|
||||
onOpenChange={setSubmitDialogOpen}
|
||||
context={submitDialogContext}
|
||||
onSubmitted={async () => {
|
||||
if (lastSearchValues != null) {
|
||||
await handleSearch(lastSearchValues);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ReturnReasonDialog
|
||||
open={returnDialogOpen}
|
||||
onOpenChange={setReturnDialogOpen}
|
||||
context={reviewOpinionContext}
|
||||
onSave={handleSaveReturnReason}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import ApproveButton from "@/components/button/ApproveButton";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -16,11 +19,17 @@ import {
|
||||
} from "@common/components";
|
||||
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { StatsInputStatusApproveItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
|
||||
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
||||
import type { ReviewHistoryResponse } from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import type {
|
||||
ReviewHistoryResponse,
|
||||
StatsInputStatusReviewItem,
|
||||
} from "@/entities/statistics/model/statistics-input-review-interface";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
||||
|
||||
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
|
||||
|
||||
@@ -32,13 +41,28 @@ const checkSubmitStatusCd = (code: string) => {
|
||||
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
|
||||
};
|
||||
|
||||
const isStatsInputStatusRowApprovable = (row: Record<string, string | number>) =>
|
||||
String(row.submitStatusCd ?? "") === "3" && (String(row.prvnStatusCd ?? "") === "2" || String(row.prvnStatusCd ?? "") === "3");
|
||||
|
||||
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
||||
rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || "0"),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || "0"),
|
||||
reviewHistory: "검토이력",
|
||||
}));
|
||||
|
||||
// 하수도 통계 검토 00. 통계 입력 현황
|
||||
const StatsInputStatusRvw = () => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { confirm } = useConfirm();
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [searchedYear, setSearchedYear] = useState<string>("");
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
||||
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
|
||||
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
|
||||
const [historySearchInfo, setHistorySearchInfo] = useState<{
|
||||
ctpvNm: string;
|
||||
@@ -85,6 +109,9 @@ const StatsInputStatusRvw = () => {
|
||||
const handleReset = () => {
|
||||
setRowData([]);
|
||||
setSearchedYear("");
|
||||
setLastSearchedValues(null);
|
||||
setHasCompletedSearch(false);
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
@@ -96,6 +123,8 @@ const StatsInputStatusRvw = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSearchedYear(values.year ?? "");
|
||||
setLastSearchedValues(values);
|
||||
setHasCompletedSearch(false);
|
||||
|
||||
const response = await statisticsManagementApi.getStatsInputStatusList({
|
||||
ctpvNm: values.sido,
|
||||
@@ -106,64 +135,174 @@ const StatsInputStatusRvw = () => {
|
||||
|
||||
if (response.status === 'success' && response.data) {
|
||||
const rawList = response.data.sewerageInputList ?? [];
|
||||
const convertedData = rawList.map((r) => ({
|
||||
...r,
|
||||
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || '0'),
|
||||
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || '0')
|
||||
}));
|
||||
|
||||
setRowData(toGridRows(convertedData));
|
||||
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
||||
} else {
|
||||
setRowData([]);
|
||||
setHasCompletedSearch(false);
|
||||
alert(response.message || "조회된 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
setHasCompletedSearch(true);
|
||||
} catch {
|
||||
alert('데이터 조회 중 오류가 발생했습니다.');
|
||||
setRowData([]);
|
||||
setHasCompletedSearch(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const approveFunc = () => console.log('approveFunc');
|
||||
const approveFunc = async () => {
|
||||
if (rowData.length === 0) {
|
||||
alert("조회된 통계 항목이 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (!lastSearchedValues?.year?.trim() || !lastSearchedValues?.bsnmCd?.trim()) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleRowClick = async (row: Record<string, string | number>) => {
|
||||
setHistorySearchInfo({
|
||||
ctpvNm: String(row.ctpvNm ?? ""),
|
||||
sggNm: String(row.sggNm ?? ""),
|
||||
tabNm: String(row.tabNm ?? ""),
|
||||
});
|
||||
const params = {
|
||||
statsYr: searchedYear,
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
if (!rowData.every(isStatsInputStatusRowApprovable)) {
|
||||
alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsApproveLoading(true);
|
||||
try {
|
||||
const response = await statisticsManagementApi.getReviewHistory(params);
|
||||
const userInfo = await getUserInfo();
|
||||
const mdfrId = (userInfo.userUnqId ?? "").trim();
|
||||
if (!mdfrId) {
|
||||
alert("수정자 ID를 확인할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 'success') {
|
||||
setHistoryRowData(response.data);
|
||||
setReviewHistoryOpen(true);
|
||||
const statsYr = lastSearchedValues.year.trim();
|
||||
const sewerBzmnCdDefault = lastSearchedValues.bsnmCd.trim();
|
||||
|
||||
const approveItems: StatsInputStatusApproveItem[] = rowData
|
||||
.map((row) => {
|
||||
const statsCd = String(row.statsCd ?? "").trim();
|
||||
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
|
||||
return { statsYr, sewerBzmnCd, statsCd, mdfrId };
|
||||
})
|
||||
.filter((item) => item.statsCd.length > 0);
|
||||
|
||||
if (approveItems.length === 0) {
|
||||
alert("통계 코드(statsCd)가 있는 행이 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "REVIEW");
|
||||
if (response.resultCode.toLocaleLowerCase() === "success") {
|
||||
try {
|
||||
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
|
||||
ctpvNm: lastSearchedValues.sido,
|
||||
sggNm: lastSearchedValues.gugun === "ALL" ? "" : lastSearchedValues.gugun,
|
||||
statsYr: lastSearchedValues.year ?? "",
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd,
|
||||
});
|
||||
if (listResponse.status === "success" && listResponse.data) {
|
||||
const rawList = listResponse.data.sewerageInputList ?? [];
|
||||
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
||||
}
|
||||
} catch {
|
||||
/* 목록 갱신 실패는 별도 안내 없이 생략 */
|
||||
}
|
||||
alert(response.message ?? "승인되었습니다.");
|
||||
} else {
|
||||
alert(response.message || '검토 이력 조회 중 오류가 발생했습니다.');
|
||||
alert(response.message ?? "승인에 실패했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert('검토 이력 조회 중 오류가 발생했습니다.');
|
||||
alert("승인 처리 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsApproveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || "")
|
||||
? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== "ALL")
|
||||
: true;
|
||||
|
||||
const allSubmitStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.submitStatusCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
/** 진행상태(prvn) 코드가 모두 승인완료(3) */
|
||||
const allPrvnStatusApproved = useMemo(
|
||||
() =>
|
||||
rowData.length > 0 &&
|
||||
rowData.every((row) => String(row.prvnStatusCd ?? "") === "3"),
|
||||
[rowData],
|
||||
);
|
||||
|
||||
const showActionToolbarCard =
|
||||
hasCompletedSearch &&
|
||||
rowData.length > 0 &&
|
||||
gugunOkOrNotDoList &&
|
||||
allSubmitStatusApproved &&
|
||||
!allPrvnStatusApproved;
|
||||
|
||||
const openReviewHistoryForRow = useCallback(
|
||||
async (row: Record<string, string | number>) => {
|
||||
if (!searchedYear) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
setHistorySearchInfo({
|
||||
ctpvNm: String(row.ctpvNm ?? ""),
|
||||
sggNm: String(row.sggNm ?? ""),
|
||||
tabNm: String(row.tabNm ?? ""),
|
||||
});
|
||||
const params = {
|
||||
statsYr: searchedYear,
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await statisticsManagementApi.getReviewHistory(params);
|
||||
|
||||
if (response.status === "success") {
|
||||
setHistoryRowData(response.data);
|
||||
setReviewHistoryOpen(true);
|
||||
} else {
|
||||
alert(response.message || "검토 이력 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
} catch {
|
||||
alert("검토 이력 조회 중 오류가 발생했습니다.");
|
||||
}
|
||||
},
|
||||
[alert, searchedYear],
|
||||
);
|
||||
|
||||
const handleReviewHistoryCellClick = useCallback(
|
||||
({ fieldName, row }: DataCellClickCallbackParams) => {
|
||||
if (fieldName !== "reviewHistory") return;
|
||||
void openReviewHistoryForRow(row);
|
||||
},
|
||||
[openReviewHistoryForRow],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
||||
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<ApproveButton approveFunc={approveFunc} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
{(isLoading || isApproveLoading) && (
|
||||
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
@@ -174,12 +313,49 @@ const StatsInputStatusRvw = () => {
|
||||
<DataGrid
|
||||
columnInfo={columnInfo}
|
||||
isExcelImport={false}
|
||||
showHeaderCheckbox={true}
|
||||
showHeaderCheckbox={false}
|
||||
rowStartNum={2}
|
||||
fixedColumns={3}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
onRowClick={handleRowClick}
|
||||
onDataCellClick={handleReviewHistoryCellClick}
|
||||
cellStyleCallback={({ row, fieldName }) => {
|
||||
if (fieldName === "submitStatusNm") {
|
||||
const statusCd = String(row.submitStatusCd ?? "");
|
||||
|
||||
if (statusCd === "0" || statusCd === "1") {
|
||||
return "rg-cell-stats-status-pending";
|
||||
}
|
||||
if (statusCd === "2") {
|
||||
return "rg-cell-stats-status-submitted";
|
||||
}
|
||||
if (statusCd === "3") {
|
||||
return "rg-cell-stats-status-approved";
|
||||
}
|
||||
if (statusCd === "4") {
|
||||
return "rg-cell-stats-status-rejected";
|
||||
}
|
||||
return undefined;
|
||||
} else if (fieldName === "prvnStatusNm") {
|
||||
const statusCd = String(row.prvnStatusCd ?? "");
|
||||
|
||||
if (statusCd === "0" || statusCd === "1") {
|
||||
return "rg-cell-stats-status-pending";
|
||||
}
|
||||
if (statusCd === "2") {
|
||||
return "rg-cell-stats-status-submitted";
|
||||
}
|
||||
if (statusCd === "3") {
|
||||
return "rg-cell-stats-status-approved";
|
||||
}
|
||||
if (statusCd === "4") {
|
||||
return "rg-cell-stats-status-rejected";
|
||||
}
|
||||
return undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
|
||||
<DialogContent
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn";
|
||||
import { SewagePipeColumnInfoListTot, getST00020003Config } from "@/components/util/column/total/SewagePipeTotColumn";
|
||||
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";
|
||||
@@ -46,7 +46,6 @@ const SewagePipeTot = () => {
|
||||
lastSearchedValuesRef.current = null;
|
||||
};
|
||||
|
||||
/** SewagePipeInp의 `loadSewagePipeActiveTabData`와 동일 역할: 탭키별 API 호출 후 `rowDataMap` 반영 */
|
||||
const loadSewagePipeTotTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
@@ -107,7 +106,8 @@ const SewagePipeTot = () => {
|
||||
alert(response.message || "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
if (tabKey === "ST00020005") {
|
||||
|
||||
if (tabKey === "ST00020005") {
|
||||
const response = await statisticsTotalStatusApi.getSewagePipePipeMnhleStatus(param);
|
||||
if (response.status === "success" && response.data) {
|
||||
applyRows(toGridRows(response.data as object[]));
|
||||
@@ -124,7 +124,6 @@ const SewagePipeTot = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// SewagePipeInp과 동일: 검색 시 활성 탭 데이터만 조회·맵 병합
|
||||
let ok = false;
|
||||
if (
|
||||
activeTab === "ST00020001" ||
|
||||
@@ -175,8 +174,6 @@ const SewagePipeTot = () => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[SewagePipeTot] tabKey (API 미연결):", tabKey);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -206,22 +203,29 @@ const SewagePipeTot = () => {
|
||||
</TabsList>
|
||||
{columnInfo.map((info) => {
|
||||
const tabKey = info.tabKey;
|
||||
if (!info.columns || !info.columnLayout) {
|
||||
|
||||
// ★ [동적 처리] ST00020003인 경우 함수를 통해 컬럼 정보를 실시간으로 생성
|
||||
const currentYear = lastSearchedValuesRef.current?.year || new Date().getFullYear();
|
||||
const finalTabConfig = tabKey === "ST00020003"
|
||||
? getST00020003Config(currentYear)
|
||||
: info;
|
||||
|
||||
if (!finalTabConfig.columns || !finalTabConfig.columnLayout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowData = rowDataMap.get(tabKey) ?? [];
|
||||
const gridColumnInfo: ColumnInfo = {
|
||||
columns: info.columns,
|
||||
columnLayout: info.columnLayout,
|
||||
columns: finalTabConfig.columns,
|
||||
columnLayout: finalTabConfig.columnLayout,
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={info.rowStartNum}
|
||||
excelExportFileSuffix={info.tabLabel || tabKey}
|
||||
rowStartNum={finalTabConfig.rowStartNum}
|
||||
excelExportFileSuffix={finalTabConfig.tabLabel || tabKey}
|
||||
rowData={rowData}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
@@ -236,7 +240,7 @@ const SewagePipeTot = () => {
|
||||
isExcelImport={false}
|
||||
isExcelExport
|
||||
showHeaderCheckbox={false}
|
||||
fixedColumns={info.fixedColumns}
|
||||
fixedColumns={finalTabConfig.fixedColumns}
|
||||
enableSort={false}
|
||||
cellStyleCallback={({ row }) => {
|
||||
const rowType = String(row.rowType ?? '');
|
||||
@@ -255,4 +259,4 @@ const SewagePipeTot = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SewagePipeTot;
|
||||
export default SewagePipeTot;
|
||||
@@ -4,11 +4,13 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dayjs from "dayjs";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib";
|
||||
import { CodeSelect } from "@/entities/code";
|
||||
import { CodeApi, CodeSelect } from "@/entities/code";
|
||||
import {
|
||||
type ApproveRequest,
|
||||
type RejectRequest,
|
||||
@@ -129,21 +131,15 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
||||
statsYr: String(dayjs().year() - 1),
|
||||
sewerBzmnCd: null,
|
||||
statsCd: null,
|
||||
laeSbmsnSttsCd: "2",
|
||||
laeSbmsnSttsCd: "3",
|
||||
};
|
||||
|
||||
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
||||
...DEFAULT_FILTERS,
|
||||
prcsYnList: "1,2,3",
|
||||
prcsYnList: "1,2",
|
||||
pbcpAprvYn: "N",
|
||||
};
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: "수정완료", value: "1" },
|
||||
{ label: "예외통과", value: "2" },
|
||||
{ label: "반려", value: "3" },
|
||||
];
|
||||
|
||||
const APPROVAL_STATUS_OPTIONS = [
|
||||
{ label: "승인", value: "Y" },
|
||||
{ label: "미승인", value: "N" },
|
||||
@@ -195,6 +191,18 @@ function StatisticsVerificationReport() {
|
||||
[filters, subFilters],
|
||||
);
|
||||
|
||||
const { data: statusOptions } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STINPTEXCT"],
|
||||
queryFn: () => CodeApi.getCode("STINPTEXCT"),
|
||||
select: (data) =>
|
||||
data
|
||||
.filter(({ lwpoCd }) => !["0", "4"].includes(lwpoCd))
|
||||
.map(({ lwpoCdNm: label, lwpoCd: value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
|
||||
const statisticsVerificationReportQuery = useStatsVrfcRptData(filters);
|
||||
const statisticsVerificationResultQuery =
|
||||
useStatsVrfcRsltData(detailParams);
|
||||
@@ -460,8 +468,6 @@ function StatisticsVerificationReport() {
|
||||
async function handleReject() {
|
||||
const commonParam = {
|
||||
statsYr: filters.statsYr,
|
||||
sewerBzmnCd: filters.sewerBzmnCd ?? "",
|
||||
statsCd: filters.statsCd ?? "",
|
||||
mdfrId: userInfo?.userUnqId ?? "",
|
||||
};
|
||||
|
||||
@@ -488,6 +494,8 @@ function StatisticsVerificationReport() {
|
||||
...commonParam,
|
||||
vrfcSn: Number.isNaN(vrfcSn) ? 0 : vrfcSn,
|
||||
rjctOpnnCn: String(row.rjctOpnnCn ?? ""),
|
||||
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
||||
statsCd: String(row.statsCd ?? ""),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -594,7 +602,7 @@ function StatisticsVerificationReport() {
|
||||
label="상태"
|
||||
value={subFilters.prcsYnList}
|
||||
onChange={handleSubFilterChange("prcsYnList")}
|
||||
options={STATUS_OPTIONS}
|
||||
options={statusOptions ?? []}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="승인상태"
|
||||
@@ -691,7 +699,7 @@ function FilterCombobox({
|
||||
onChange(list.length > 0 ? list.join(",") : undefined);
|
||||
}}
|
||||
>
|
||||
<ComboboxChips ref={anchor} className="max-w-[26rem]">
|
||||
<ComboboxChips ref={anchor} className="max-w-[32rem]">
|
||||
<ComboboxValue>
|
||||
{(values: string[]) => (
|
||||
<>
|
||||
|
||||
@@ -17,7 +17,7 @@ export type DataGridStatusBannerInfo = {
|
||||
export type RealGridColumnInfo = {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
header: { text: string };
|
||||
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
|
||||
editable?: boolean;
|
||||
valueType?: import("realgrid").ValueType;
|
||||
dataFormat?: string;
|
||||
@@ -154,7 +154,9 @@ export type ComputedFieldConfig =
|
||||
export type ColumnConfig = {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
header: { text: string };
|
||||
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
|
||||
/** 헤더 hover 시 보여줄 컬럼 가이드 문구 */
|
||||
tooltip?: string;
|
||||
/** 계산 필드: fields 합계/평균 등. valueCallback으로 dataProvider.setFields 시 전달 */
|
||||
computed?: ComputedFieldConfig;
|
||||
editable?: boolean; // 셀 편집 가능 여부
|
||||
@@ -210,7 +212,7 @@ export type ColumnLayoutItem =
|
||||
| {
|
||||
name: string;
|
||||
hideChildHeaders?: boolean;
|
||||
header: { text: string };
|
||||
header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string };
|
||||
items: (string | ColumnLayoutItem)[];
|
||||
footerUserSpans?: FooterUserSpan[]; // footer 병합 설정
|
||||
}
|
||||
@@ -277,6 +279,16 @@ export type CellEditedCallback = (
|
||||
params: CellEditedCallbackParams,
|
||||
) => void;
|
||||
|
||||
/** 데이터 셀 클릭 시 컬럼(필드)까지 전달 — 특정 컬럼만 반응할 때 사용 */
|
||||
export type DataCellClickCallbackParams = {
|
||||
row: Record<string, string | number>;
|
||||
fieldName: string;
|
||||
itemIndex: number;
|
||||
};
|
||||
export type DataCellClickCallback = (
|
||||
params: DataCellClickCallbackParams,
|
||||
) => void;
|
||||
|
||||
export type DataGridProps = {
|
||||
columnInfo: ColumnInfo;
|
||||
rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주)
|
||||
@@ -304,6 +316,8 @@ export type DataGridProps = {
|
||||
/** 외부에서 gridRef에 직접 접근하기 위한 ref (setRows/addRows/setValue 직접 호출용) */
|
||||
getGridRef?: React.MutableRefObject<RealGridReact | null>;
|
||||
onRowClick?: (rowData: Record<string, string | number>) => void; // 행 클릭 이벤트
|
||||
/** 데이터 셀 클릭 시 행 + 클릭한 필드명 전달 (`onRowClick`과 함께 쓸 수 있음) */
|
||||
onDataCellClick?: DataCellClickCallback;
|
||||
/** CheckBar 행 체크 변경 */
|
||||
onItemChecked?: (itemIndex: number, checked: boolean) => void;
|
||||
/** CheckBar 헤더 전체 선택/해제 */
|
||||
|
||||
@@ -10,8 +10,15 @@ export const StatsInputStatusInpColumn = {
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false, editable: false },
|
||||
{ name: 'menuTabName', fieldName: 'menuTabName', header: { text: '구분' }, editable: false, width: 220 },
|
||||
{ name: 'locgStatusNm', fieldName: 'locgStatusNm', header: { text: '진행상황' }, editable: false },
|
||||
// { name: 'inputRate', fieldName: 'inputRate', header: { text: '작성률' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{
|
||||
name: 'exceptionSubmitHistory',
|
||||
fieldName: 'exceptionSubmitHistory',
|
||||
header: { text: '예외제출내역' },
|
||||
editable: false,
|
||||
width: 118,
|
||||
styleName: 'rg-cell-outline-action-btn',
|
||||
},
|
||||
{ name: 'laeSbmsnSttsNm', fieldName: 'laeSbmsnSttsNm', header: { text: '진행상황' }, editable: false },
|
||||
{ name: 'laePicNm', fieldName: 'laePicNm', header: { text: '작성자' }, editable: false },
|
||||
{ name: 'laePicTelNo', fieldName: 'laePicTelNo', header: { text: '작성자연락처' }, editable: false },
|
||||
{ name: 'laeKbrdrDeptNm', fieldName: 'laeKbrdrDeptNm', header: { text: '부서' }, editable: false },
|
||||
@@ -20,6 +27,7 @@ export const StatsInputStatusInpColumn = {
|
||||
{ name: 'laeIdfrDeptNm', fieldName: 'laeIdfrDeptNm', header: { text: '부서' }, editable: false },
|
||||
{ name: 'laeIdfrJbgdNm', fieldName: 'laeIdfrJbgdNm', header: { text: '직제' }, editable: false },
|
||||
{ name: 'rjctOpnnCn', fieldName: 'rjctOpnnCn', header: { text: '반려사유' }, editable: false },
|
||||
{ name: 'laeSbmsnSttsCd', fieldName: 'laeSbmsnSttsCd', header: { text: '진행상황' }, editable: false, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -27,13 +35,13 @@ export const StatsInputStatusInpColumn = {
|
||||
'sggNm',
|
||||
'statsCd',
|
||||
'menuTabName',
|
||||
'exceptionSubmitHistory',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수도사업자' },
|
||||
items: [
|
||||
'locgStatusNm',
|
||||
// 'inputRate',
|
||||
'laeSbmsnSttsNm',
|
||||
'laePicNm',
|
||||
'laePicTelNo',
|
||||
'laeKbrdrDeptNm',
|
||||
@@ -41,7 +49,8 @@ export const StatsInputStatusInpColumn = {
|
||||
'laeIdfrNm',
|
||||
'laeIdfrDeptNm',
|
||||
'laeIdfrJbgdNm',
|
||||
'rjctOpnnCn'
|
||||
'rjctOpnnCn',
|
||||
'laeSbmsnSttsCd'
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ export const LogHistoryColumn = {
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false },
|
||||
{ name: 'statsYr', fieldName: 'statsYr', header: { text: '연도' }, editable: false },
|
||||
{ name: 'statsLogSn', fieldName: 'statsLogSn', header: { text: '일련번호' }, editable: false, visible: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '메뉴명' }, editable: false },
|
||||
{ name: 'menuNm', fieldName: 'menuNm', header: { text: '통계항목' }, editable: false },
|
||||
{ name: 'tblNm', fieldName: 'tblNm', header: { text: '테이블명' }, visible: false, editable: false },
|
||||
{ name: 'statsDataChgComNm', fieldName: 'statsDataChgComNm', header: { text: '요청 기능명' }, editable: false },
|
||||
{ name: 'rgtrId', fieldName: 'rgtrId', header: { text: '등록자' }, editable: false, visible: false },
|
||||
|
||||
@@ -1,142 +1,190 @@
|
||||
import { ValueType } from "realgrid";
|
||||
import {
|
||||
footerAvgDecimalArrayByDivision,
|
||||
footerDecimal1ArrayByDivision,
|
||||
footerDecimal3ArrayByDivision,
|
||||
footerIntArrayByDivision,
|
||||
} from "@/components/util/grid/footerSummary";
|
||||
import type { GridView } from "@/components/type/DataGrid.type";
|
||||
|
||||
const getDivisionFooterLabel = (grid: GridView, divisionIndex: number, withSuffix: boolean) => {
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, "displayYear");
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
if (divisionValues.length <= divisionIndex) {
|
||||
return withSuffix ? "합계" : "";
|
||||
}
|
||||
|
||||
const label = divisionValues[divisionIndex] ?? "";
|
||||
return withSuffix ? `${label}합계` : label;
|
||||
};
|
||||
|
||||
// 하수도 통계 관리 06.공공하수처리시설
|
||||
export const rawPublicSewageFacilityInfoList = {
|
||||
columns: [
|
||||
/* 1. 기본 정보 */
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' } },
|
||||
{ name: 'sgg', fieldName: '시군구', header: { text: '시군구' } },
|
||||
{ name: 'GBN', fieldName: '구분', header: { text: '구분' } },
|
||||
{ name: 'ADDR', fieldName: '소재지', header: { text: '소재지' } },
|
||||
{ name: 'FCT_NM_GROUP', fieldName: '시설명(500m3/일이상/미만)', header: { text: '시설명(500m3/일이상/미만)' } },
|
||||
{
|
||||
name: 'ctpvNm',
|
||||
fieldName: 'ctpvNm',
|
||||
header: { text: '시도' },
|
||||
footer: [
|
||||
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, true) },
|
||||
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, true) },
|
||||
],
|
||||
},
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
footer: [
|
||||
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 0, false) },
|
||||
{ valueCallback: (grid: GridView) => getDivisionFooterLabel(grid, 1, false) },
|
||||
],
|
||||
},
|
||||
{ name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '시설명(500m³/일 이상/미만)' } },
|
||||
{ name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '소재지' } },
|
||||
|
||||
/* 2. 시설용량 (㎥/일) */
|
||||
{ name: 'FCT_CPC_GROUP', fieldName: '시설용량(500m3/일이상/미만)_용량', header: { text: '시설용량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'FCT_CPC_PHY', fieldName: '물리적_용량', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'FCT_CPC_BIO', fieldName: '생물학적_용량', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'FCT_CPC_ADV', fieldName: '고도_용량', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '시설용량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltPhysCpct', fieldName: 'prcsFcltPhysCpct', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltBoilCpct', fieldName: 'prcsFcltBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsFcltAltdCpct', fieldName: 'prcsFcltAltdCpct', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 3. 유입하수량 (㎥/일) */
|
||||
{ name: 'IN_QTY_GROUP', fieldName: '유입하수량(500m3/일이상/미만)', header: { text: '유입하수량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_QTY_PHY', fieldName: '물리적_유입', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_QTY_BIO', fieldName: '생물학적_유입', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_QTY_ADV', fieldName: '고도_유입', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwageQnt', fieldName: 'inflwSwageQnt', header: { text: '유입하수량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwagePhysQnt', fieldName: 'inflwSwagePhysQnt', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwageBoilQnt', fieldName: 'inflwSwageBoilQnt', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwSwageAltdQnt', fieldName: 'inflwSwageAltdQnt', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 4. 방류량 (㎥/일) */
|
||||
{ name: 'OUT_QTY_GROUP', fieldName: '방류량(500m3/일이상/미만)', header: { text: '방류량(500m3/일이상/미만)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_QTY_PHY', fieldName: '물리적_방류', header: { text: '물리적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_QTY_BIO', fieldName: '생물학적_방류', header: { text: '생물학적' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_QTY_ADV', fieldName: '고도_방류', header: { text: '고도' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsQnt', fieldName: 'prcsQnt', header: { text: '방류량(500m³/일 이상/미만)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsPhysCpct', fieldName: 'prcsPhysCpct', header: { text: '물리적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsBoilCpct', fieldName: 'prcsBoilCpct', header: { text: '생물학적' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsAltdCpct', fieldName: 'prcsAltdCpct', header: { text: '고도' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 5. 수질 및 처리효율 */
|
||||
{ name: 'IN_BOD', fieldName: '유입BOD', header: { text: '유입BOD' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_BOD', fieldName: '방류BOD', header: { text: '방류BOD' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_TOC', fieldName: '유입TOC', header: { text: '유입TOC' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_TOC', fieldName: '방류TOC', header: { text: '방류TOC' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_SS', fieldName: '유입SS', header: { text: '유입SS' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_SS', fieldName: '방류SS', header: { text: '방류SS' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'IN_TN', fieldName: '유입T-N', header: { text: '유입T-N' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'OUT_TN', fieldName: '방류T-N', header: { text: '방류T-N' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'IN_TP', fieldName: '유입T-P', header: { text: '유입T-P' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'OUT_TP', fieldName: '방류T-P', header: { text: '방류T-P' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'IN_COLI', fieldName: '유입총대장균군수', header: { text: '유입총대장균군수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'OUT_COLI', fieldName: '방대총대장균군수', header: { text: '방대총대장균군수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'TRT_EFF', fieldName: '처리효율(%)', header: { text: '처리효율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'TRT_LOD', fieldName: '처리부하량(kgBOD/D)', header: { text: '처리부하량(kgBOD/D)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'TRT_MTHD', fieldName: '처리방법', header: { text: '처리방법' } },
|
||||
{ name: 'inflwBodVl', fieldName: 'inflwBodVl', header: { text: '유입BOD' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dschgBodVl', fieldName: 'dschgBodVl', header: { text: '방류BOD' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwTocQnt', fieldName: 'inflwTocQnt', header: { text: '유입TOC' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dschgTocQnt', fieldName: 'dschgTocQnt', header: { text: '방류TOC' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwSsQnt', fieldName: 'inflwSsQnt', header: { text: '유입SS' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dschgSsQnt', fieldName: 'dschgSsQnt', header: { text: '방류SS' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'inflwTnVl', fieldName: 'inflwTnVl', header: { text: '유입T-N' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'dschgTnVl', fieldName: 'dschgTnVl', header: { text: '방류T-N' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'inflwTpVl', fieldName: 'inflwTpVl', header: { text: '유입T-P' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'dschgTpVl', fieldName: 'dschgTpVl', header: { text: '방류T-P' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'inflwGrssClfgVl', fieldName: 'inflwGrssClfgVl', header: { text: '유입총대장균군수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'dschgGrssClfgVl', fieldName: 'dschgGrssClfgVl', header: { text: '방류총대장균군수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsRt', fieldName: 'prcsRt', header: { text: '처리효율(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsLodQnt', fieldName: 'prcsLodQnt', header: { text: '처리부하량(kgBOD/D)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prcsMthdExpln', fieldName: 'prcsMthdExpln', header: { text: '처리방법' } },
|
||||
|
||||
/* 6. 연계처리량 */
|
||||
{ name: 'LNK_EXC', fieldName: '분뇨', header: { text: '분뇨' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'LNK_LVS', fieldName: '축산', header: { text: '축산' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'LNK_LCH', fieldName: '침출수', header: { text: '침출수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'LNK_ETC', fieldName: '기타_연계', header: { text: '기타' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'wholHmnQnt', fieldName: 'wholHmnQnt', header: { text: '분뇨' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'wholLvstQnt', fieldName: 'wholLvstQnt', header: { text: '축산' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'wholEfntQnt', fieldName: 'wholEfntQnt', header: { text: '침출수' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'wholEtcQnt', fieldName: 'wholEtcQnt', header: { text: '기타' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
|
||||
/* 7. 상세 시설 정보 */
|
||||
{ name: 'TEL_NO', fieldName: '전화번호', header: { text: '전화번호' } },
|
||||
{ name: 'COMP_DT', fieldName: '준공일', header: { text: '준공일' } },
|
||||
{ name: 'RN_DT', fieldName: '가동개시일', header: { text: '가동개시일' } },
|
||||
{ name: 'BSNS_CT', fieldName: '사업비(백만원)', header: { text: '사업비(백만원)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'DSCH_DSF_MTHD', fieldName: '방류수소독방법', header: { text: '방류수소독방법' } },
|
||||
{ name: 'telNo', fieldName: 'telNo', header: { text: '전화번호' } },
|
||||
{ name: 'cmptnStrtYmd', fieldName: 'cmptnStrtYmd', header: { text: '준공일' } },
|
||||
{ name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동개시일' } },
|
||||
{ name: 'wholBizVl', fieldName: 'wholBizVl', header: { text: '사업비' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'eflntDsnfMthdExpln', fieldName: 'eflntDsnfMthdExpln', header: { text: '방류수 소독방법' } },
|
||||
|
||||
/* 8. 방류수역 */
|
||||
{ name: 'WTR_SYS', fieldName: '수계', header: { text: '수계' } },
|
||||
{ name: 'STRM', fieldName: '지류', header: { text: '지류' } },
|
||||
{ name: 'DET_UNIT_ZONE', fieldName: '세부단위구역', header: { text: '세부단위구역' } },
|
||||
{ name: 'RGN_GBN', fieldName: '지역구분', header: { text: '지역구분' } },
|
||||
{ name: 'MID_ZONE_NM', fieldName: '명칭', header: { text: '명칭' } },
|
||||
{ name: 'MID_ZONE_BOD', fieldName: '목표수질(BOD)', header: { text: '목표수질(BOD)' } },
|
||||
{ name: 'wtsysNm', fieldName: 'wtsysNm', header: { text: '수계' } },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
|
||||
{ name: 'dtlUnitZoneCn', fieldName: 'dtlUnitZoneCn', header: { text: '세부단위구역' } },
|
||||
{ name: 'rgnSeNm', fieldName: 'rgnSeNm', header: { text: '지역구분' } },
|
||||
{ name: 'msirbCn', fieldName: 'msirbCn', header: { text: '명칭' } },
|
||||
{ name: 'goalWqNo', fieldName: 'goalWqNo', header: { text: '목표수질(BOD)' } },
|
||||
|
||||
/* 9. 에너지 및 환경 */
|
||||
{ name: 'ENG_SLF_RT', fieldName: '에너지자립율(%)', header: { text: '에너지자립율(%)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ENG_SLF_QTY', fieldName: '에너지자체생산량(TOE)', header: { text: '에너지자체생산량(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ENG_TOT_SUM', fieldName: '계(TOE)', header: { text: '계(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ENG_TOT_PWR', fieldName: '총전력사용량(KWh)', header: { text: '총전력사용량(KWh)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ENG_TOT_SLF', fieldName: '자체에너지사용량(TOE)', header: { text: '자체에너지사용량(TOE)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'CO2_EMISS', fieldName: '하수처리량당CO2배출량(KgCO2/m3)', header: { text: '하수처리량당CO2배출량(KgCO2/m3)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
{ name: 'engySrlneRt', fieldName: 'engySrlneRt', header: { text: '에너지자립률(%)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'engyItslfPrdctnQnt', fieldName: 'engyItslfPrdctnQnt', header: { text: '에너지자체생산량(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'anlEngyWholUseQnt', fieldName: 'anlEngyWholUseQnt', header: { text: '계(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'anlElpwrUseQnt', fieldName: 'anlElpwrUseQnt', header: { text: '총 전력사용량(kWh)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'itslfEngyUseQnt', fieldName: 'itslfEngyUseQnt', header: { text: '자체 에너지사용량(TOE)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'swtrmtEmsnQnt', fieldName: 'swtrmtEmsnQnt', header: { text: '하수처리량당 CO2배출량(kgCO2/m³)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' },
|
||||
|
||||
/* 10. 운영 및 위탁 */
|
||||
{ name: 'OPR_MBY', fieldName: '운영주체', header: { text: '운영주체' } },
|
||||
{ name: 'ENTRPS_NM', fieldName: '위탁업체명', header: { text: '위탁업체명' } },
|
||||
{ name: 'ENTRPS_CT', fieldName: '위탁비용(백만원/년)', header: { text: '위탁비용(백만원/년)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'ENTRPS_TERM', fieldName: '위탁계약기간', header: { text: '위탁계약기간' } },
|
||||
{ name: 'CTR_TRT_FCT', fieldName: '중심처리시설', header: { text: '중심처리시설' } },
|
||||
{ name: 'EMP_CNT', fieldName: '직원총수(명)', header: { text: '직원총수(명)' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'ACC_CNT', fieldName: '안전사고건수', header: { text: '안전사고건수' }, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'RMK', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'operMnbdCn', fieldName: 'operMnbdCn', header: { text: '운영주체(자체/공기업/민간대행)' } },
|
||||
{ name: 'prxyBzentyNmCn', fieldName: 'prxyBzentyNmCn', header: { text: '위탁업체명' } },
|
||||
{ name: 'cnsgnCstVl', fieldName: 'cnsgnCstVl', header: { text: '위탁비용(백만원/년)' }, footer: footerDecimal1ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' },
|
||||
{ name: 'prxyCtrtCn', fieldName: 'prxyCtrtCn', header: { text: '위탁계약기간' } },
|
||||
{ name: 'intgPrcsFcltCn', fieldName: 'intgPrcsFcltCn', header: { text: '중심처리시설' } },
|
||||
{ name: 'wholNope', fieldName: 'wholNope', header: { text: '직원총수(명)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'sftyAcdntEvntcsCnt', fieldName: 'sftyAcdntEvntcsCnt', header: { text: '안전사고건수' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0' },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'groupA', header: { text: '행정구역' }, items: ['sido', 'sgg'] },
|
||||
'FCT_NM_GROUP', 'GBN', 'ADDR', 'FCT_CPC_GROUP',
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: [{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, 'sggNm'],
|
||||
},
|
||||
'displayYear', 'swtrmtFcltNm', 'impdFcltPstnCn', 'prcsFcltCpct',
|
||||
{
|
||||
name: 'groupB',
|
||||
header: { text: '시설용량(㎥/일)' },
|
||||
items: ['FCT_CPC_PHY', 'FCT_CPC_BIO', 'FCT_CPC_ADV'],
|
||||
items: ['prcsFcltPhysCpct', 'prcsFcltBoilCpct', 'prcsFcltAltdCpct'],
|
||||
},
|
||||
'IN_QTY_GROUP',
|
||||
'inflwSwageQnt',
|
||||
{
|
||||
name: 'groupC',
|
||||
header: { text: '유입하수량(㎥/일)' },
|
||||
items: ['IN_QTY_PHY', 'IN_QTY_BIO', 'IN_QTY_ADV'],
|
||||
items: ['inflwSwagePhysQnt', 'inflwSwageBoilQnt', 'inflwSwageAltdQnt'],
|
||||
},
|
||||
'OUT_QTY_GROUP',
|
||||
'prcsQnt',
|
||||
{
|
||||
name: 'groupD',
|
||||
header: { text: '방류량(㎥/일)' },
|
||||
items: ['OUT_QTY_PHY', 'OUT_QTY_BIO', 'OUT_QTY_ADV'],
|
||||
items: ['prcsPhysCpct', 'prcsBoilCpct', 'prcsAltdCpct'],
|
||||
},
|
||||
'IN_BOD', 'OUT_BOD', 'IN_TOC', 'OUT_TOC', 'IN_SS', 'OUT_SS', 'IN_TN', 'OUT_TN', 'IN_TP', 'OUT_TP', 'IN_COLI', 'OUT_COLI',
|
||||
'TRT_EFF', 'TRT_LOD', 'TRT_MTHD',
|
||||
'inflwBodVl', 'dschgBodVl', 'inflwTocQnt', 'dschgTocQnt', 'inflwSsQnt', 'dschgSsQnt', 'inflwTnVl', 'dschgTnVl', 'inflwTpVl', 'dschgTpVl', 'inflwGrssClfgVl', 'dschgGrssClfgVl',
|
||||
'prcsRt', 'prcsLodQnt', 'prcsMthdExpln',
|
||||
{
|
||||
name: 'groupE',
|
||||
header: { text: '연계처리량(㎥/일)' },
|
||||
items: ['LNK_EXC', 'LNK_LVS', 'LNK_LCH', 'LNK_ETC'],
|
||||
items: ['wholHmnQnt', 'wholLvstQnt', 'wholEfntQnt', 'wholEtcQnt'],
|
||||
},
|
||||
'TEL_NO', 'COMP_DT', 'RN_DT', 'BSNS_CT', 'DSCH_DSF_MTHD',
|
||||
'telNo', 'cmptnStrtYmd', 'oprtngStrtYmd', 'wholBizVl', 'eflntDsnfMthdExpln',
|
||||
{
|
||||
name: 'groupF',
|
||||
header: { text: '방류수역' },
|
||||
items: ['WTR_SYS', 'STRM', 'DET_UNIT_ZONE', 'RGN_GBN',
|
||||
items: ['wtsysNm', 'trbtrNm', 'dtlUnitZoneCn', 'rgnSeNm',
|
||||
{
|
||||
name: 'groupG',
|
||||
header: { text: '중권역' },
|
||||
items: ['MID_ZONE_NM', 'MID_ZONE_BOD'],
|
||||
items: ['msirbCn', 'goalWqNo'],
|
||||
},
|
||||
],
|
||||
},
|
||||
'ENG_SLF_RT', 'ENG_SLF_QTY',
|
||||
'engySrlneRt', 'engyItslfPrdctnQnt',
|
||||
{
|
||||
name: 'groupH',
|
||||
header: { text: '연간 총 에너지 사용량(TOE)' },
|
||||
items: ['ENG_TOT_SUM', 'ENG_TOT_PWR', 'ENG_TOT_SLF'],
|
||||
items: ['anlEngyWholUseQnt', 'anlElpwrUseQnt', 'itslfEngyUseQnt'],
|
||||
},
|
||||
'CO2_EMISS', 'OPR_MBY',
|
||||
'swtrmtEmsnQnt', 'operMnbdCn',
|
||||
{
|
||||
name: 'groupI',
|
||||
header: { text: '위탁현황' },
|
||||
items: ['ENTRPS_NM', 'ENTRPS_CT', 'ENTRPS_TERM'],
|
||||
items: ['prxyBzentyNmCn', 'cnsgnCstVl', 'prxyCtrtCn'],
|
||||
},
|
||||
'CTR_TRT_FCT', 'EMP_CNT', 'ACC_CNT', 'RMK'
|
||||
]
|
||||
'intgPrcsFcltCn', 'wholNope', 'sftyAcdntEvntcsCnt', 'rmrk'
|
||||
],
|
||||
fixedColumns: 3,
|
||||
rowStartNum: 3
|
||||
};
|
||||
|
||||
// 최종 Export 객체 (일괄 editable: false 적용)
|
||||
@@ -144,6 +192,7 @@ export const PublicSewageFacilityInfoList = {
|
||||
...rawPublicSewageFacilityInfoList,
|
||||
columns: rawPublicSewageFacilityInfoList.columns.map(col => ({
|
||||
...col,
|
||||
editable: false
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
const currentYear = dayjs().year() - 1;
|
||||
const startYear = 1996;
|
||||
const totalCount = currentYear - startYear + 1 + 1;
|
||||
const totalCount = currentYear - startYear + 1;
|
||||
|
||||
// 하수도 통계 관리 02. 하수관로
|
||||
const rawList = [
|
||||
@@ -398,7 +398,7 @@ const rawList = [
|
||||
|
||||
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
const dtoNum = i + 1;
|
||||
|
||||
const wholField = `yy${dtoNum}PrlgWhol`;
|
||||
const lyngField = `yy${dtoNum}LyngPrlg`;
|
||||
@@ -426,7 +426,8 @@ const rawList = [
|
||||
{ name: "summaryGroup", header: { text: "총계" }, items: ["pipeByPrstWhol", "lyngPrlg", "dmltnPrlg"] },
|
||||
|
||||
...Array.from({ length: totalCount }, (_, i) => {
|
||||
const dtoNum = totalCount - i;
|
||||
// const dtoNum = totalCount - i;
|
||||
const dtoNum = i + 1;
|
||||
const yearLabel = i === 0 ? "1996년이전" : `${startYear + (i - 1)}년`;
|
||||
|
||||
return {
|
||||
@@ -550,7 +551,311 @@ const rawList = [
|
||||
'etcPdmtPrlg',
|
||||
'rmrk'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
tabKey: "ST00020005",
|
||||
tabLabel: "맨홀 현황",
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 3,
|
||||
columns: [
|
||||
{
|
||||
name: "ctpvNm",
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
mergeCells: true,
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, "displayYear");
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + "합계" : "합계";
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, "displayYear");
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + "합계" : "합계";
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, mergeCells: true, sortdisable: true },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, mergeCells: true, sortdisable: true },
|
||||
{ name: "emphsMngSeNm", fieldName: "emphsMngSeNm", header: { text: "집중강우\n중점관리\n구역구분" }, mergeCells: true },
|
||||
{
|
||||
name: 'displayYear',
|
||||
fieldName: 'displayYear',
|
||||
header: { text: '구분' },
|
||||
sortdisable: true,
|
||||
footer: [
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 0;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
valueCallback: (grid: GridView) => {
|
||||
const divisionIndex = 1;
|
||||
const dataSource = grid.getDataSource();
|
||||
const uniqueValues = new Set<string>();
|
||||
for (let i = 0; i < dataSource.getRowCount(); i++) {
|
||||
const val = dataSource.getValue(i, 'displayYear');
|
||||
if (val) uniqueValues.add(String(val));
|
||||
}
|
||||
const divisionValues = Array.from(uniqueValues);
|
||||
return divisionValues.length > divisionIndex ? divisionValues[divisionIndex] + '합계' : '';
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{ name: "totalWholMnhlCnt", fieldName: "totalWholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "totalStep1MnhlCnt", fieldName: "totalStep1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "totalStep2MnhlCnt", fieldName: "totalStep2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "totalStep3MnhlCnt", fieldName: "totalStep3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "totalStep4MnhlCnt", fieldName: "totalStep4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "totalStep5MnhlCnt", fieldName: "totalStep5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2015년 이전 (yy14) ──────────────────────────────────
|
||||
{ name: "yy14WholMnhlCnt", fieldName: "yy14WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy14Step1MnhlCnt", fieldName: "yy14Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy14Step2MnhlCnt", fieldName: "yy14Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy14Step3MnhlCnt", fieldName: "yy14Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy14Step4MnhlCnt", fieldName: "yy14Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy14Step5MnhlCnt", fieldName: "yy14Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2015년 (yy15) ───────────────────────────────────────
|
||||
{ name: "yy15WholMnhlCnt", fieldName: "yy15WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy15Step1MnhlCnt", fieldName: "yy15Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy15Step2MnhlCnt", fieldName: "yy15Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy15Step3MnhlCnt", fieldName: "yy15Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy15Step4MnhlCnt", fieldName: "yy15Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy15Step5MnhlCnt", fieldName: "yy15Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2016년 (yy16) ───────────────────────────────────────
|
||||
{ name: "yy16WholMnhlCnt", fieldName: "yy16WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy16Step1MnhlCnt", fieldName: "yy16Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy16Step2MnhlCnt", fieldName: "yy16Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy16Step3MnhlCnt", fieldName: "yy16Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy16Step4MnhlCnt", fieldName: "yy16Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy16Step5MnhlCnt", fieldName: "yy16Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2017년 (yy17) ───────────────────────────────────────
|
||||
{ name: "yy17WholMnhlCnt", fieldName: "yy17WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy17Step1MnhlCnt", fieldName: "yy17Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy17Step2MnhlCnt", fieldName: "yy17Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy17Step3MnhlCnt", fieldName: "yy17Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy17Step4MnhlCnt", fieldName: "yy17Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy17Step5MnhlCnt", fieldName: "yy17Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2018년 (yy18) ───────────────────────────────────────
|
||||
{ name: "yy18WholMnhlCnt", fieldName: "yy18WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy18Step1MnhlCnt", fieldName: "yy18Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy18Step2MnhlCnt", fieldName: "yy18Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy18Step3MnhlCnt", fieldName: "yy18Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy18Step4MnhlCnt", fieldName: "yy18Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy18Step5MnhlCnt", fieldName: "yy18Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2019년 (yy19) ───────────────────────────────────────
|
||||
{ name: "yy19WholMnhlCnt", fieldName: "yy19WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy19Step1MnhlCnt", fieldName: "yy19Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy19Step2MnhlCnt", fieldName: "yy19Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy19Step3MnhlCnt", fieldName: "yy19Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy19Step4MnhlCnt", fieldName: "yy19Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy19Step5MnhlCnt", fieldName: "yy19Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2020년 (yy20) ───────────────────────────────────────
|
||||
{ name: "yy20WholMnhlCnt", fieldName: "yy20WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy20Step1MnhlCnt", fieldName: "yy20Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy20Step2MnhlCnt", fieldName: "yy20Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy20Step3MnhlCnt", fieldName: "yy20Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy20Step4MnhlCnt", fieldName: "yy20Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy20Step5MnhlCnt", fieldName: "yy20Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2021년 (yy21) ───────────────────────────────────────
|
||||
{ name: "yy21WholMnhlCnt", fieldName: "yy21WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy21Step1MnhlCnt", fieldName: "yy21Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy21Step2MnhlCnt", fieldName: "yy21Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy21Step3MnhlCnt", fieldName: "yy21Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy21Step4MnhlCnt", fieldName: "yy21Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy21Step5MnhlCnt", fieldName: "yy21Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2022년 (yy22) ───────────────────────────────────────
|
||||
{ name: "yy22WholMnhlCnt", fieldName: "yy22WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy22Step1MnhlCnt", fieldName: "yy22Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy22Step2MnhlCnt", fieldName: "yy22Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy22Step3MnhlCnt", fieldName: "yy22Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy22Step4MnhlCnt", fieldName: "yy22Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy22Step5MnhlCnt", fieldName: "yy22Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2023년 (yy23) ───────────────────────────────────────
|
||||
{ name: "yy23WholMnhlCnt", fieldName: "yy23WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy23Step1MnhlCnt", fieldName: "yy23Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy23Step2MnhlCnt", fieldName: "yy23Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy23Step3MnhlCnt", fieldName: "yy23Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy23Step4MnhlCnt", fieldName: "yy23Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy23Step5MnhlCnt", fieldName: "yy23Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2024년 (yy24) ───────────────────────────────────────
|
||||
{ name: "yy24WholMnhlCnt", fieldName: "yy24WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy24Step1MnhlCnt", fieldName: "yy24Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy24Step2MnhlCnt", fieldName: "yy24Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy24Step3MnhlCnt", fieldName: "yy24Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy24Step4MnhlCnt", fieldName: "yy24Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy24Step5MnhlCnt", fieldName: "yy24Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
// ── 2025년 (yy25) ───────────────────────────────────────
|
||||
{ name: "yy25WholMnhlCnt", fieldName: "yy25WholMnhlCnt", header: { text: "계" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy25Step1MnhlCnt", fieldName: "yy25Step1MnhlCnt", header: { text: "주철" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy25Step2MnhlCnt", fieldName: "yy25Step2MnhlCnt", header: { text: "주철(조화)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy25Step3MnhlCnt", fieldName: "yy25Step3MnhlCnt", header: { text: "콘크리트" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy25Step4MnhlCnt", fieldName: "yy25Step4MnhlCnt", header: { text: "기타" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
{ name: "yy25Step5MnhlCnt", fieldName: "yy25Step5MnhlCnt", header: { text: "추락방지시설\n설치맨홀(개)" }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, dataFormat: "#,##0", editable: false },
|
||||
|
||||
{ name: "rmrk", fieldName: "rmrk", header: { text: "비고" }, editable: false },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: "addrGroup",
|
||||
header: { text: "행정구역" },
|
||||
items: [
|
||||
{ column: "ctpvNm", footerUserSpans: [{ colspan: 5 }, { colspan: 5 }] },
|
||||
"sggNm",
|
||||
"admdstNm",
|
||||
],
|
||||
},
|
||||
"emphsMngSeNm",
|
||||
"displayYear",
|
||||
{
|
||||
name: "totGroup",
|
||||
header: { text: "총계" },
|
||||
items: [
|
||||
{ name: "totMtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["totalWholMnhlCnt", "totalStep1MnhlCnt", "totalStep2MnhlCnt", "totalStep3MnhlCnt", "totalStep4MnhlCnt"] },
|
||||
"totalStep5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy14Group",
|
||||
header: { text: "2015년 이전" }, // ← _14Y
|
||||
items: [
|
||||
{ name: "yy14MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy14WholMnhlCnt", "yy14Step1MnhlCnt", "yy14Step2MnhlCnt", "yy14Step3MnhlCnt", "yy14Step4MnhlCnt"] },
|
||||
"yy14Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy15Group",
|
||||
header: { text: "2015년" }, // ← _15Y
|
||||
items: [
|
||||
{ name: "yy15MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy15WholMnhlCnt", "yy15Step1MnhlCnt", "yy15Step2MnhlCnt", "yy15Step3MnhlCnt", "yy15Step4MnhlCnt"] },
|
||||
"yy15Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy16Group",
|
||||
header: { text: "2016년" }, // ← _16Y
|
||||
items: [
|
||||
{ name: "yy16MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy16WholMnhlCnt", "yy16Step1MnhlCnt", "yy16Step2MnhlCnt", "yy16Step3MnhlCnt", "yy16Step4MnhlCnt"] },
|
||||
"yy16Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy17Group",
|
||||
header: { text: "2017년" },
|
||||
items: [
|
||||
{ name: "yy17MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy17WholMnhlCnt", "yy17Step1MnhlCnt", "yy17Step2MnhlCnt", "yy17Step3MnhlCnt", "yy17Step4MnhlCnt"] },
|
||||
"yy17Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy18Group",
|
||||
header: { text: "2018년" },
|
||||
items: [
|
||||
{ name: "yy18MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy18WholMnhlCnt", "yy18Step1MnhlCnt", "yy18Step2MnhlCnt", "yy18Step3MnhlCnt", "yy18Step4MnhlCnt"] },
|
||||
"yy18Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy19Group",
|
||||
header: { text: "2019년" },
|
||||
items: [
|
||||
{ name: "yy19MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy19WholMnhlCnt", "yy19Step1MnhlCnt", "yy19Step2MnhlCnt", "yy19Step3MnhlCnt", "yy19Step4MnhlCnt"] },
|
||||
"yy19Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy20Group",
|
||||
header: { text: "2020년" },
|
||||
items: [
|
||||
{ name: "yy20MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy20WholMnhlCnt", "yy20Step1MnhlCnt", "yy20Step2MnhlCnt", "yy20Step3MnhlCnt", "yy20Step4MnhlCnt"] },
|
||||
"yy20Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy21Group",
|
||||
header: { text: "2021년" },
|
||||
items: [
|
||||
{ name: "yy21MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy21WholMnhlCnt", "yy21Step1MnhlCnt", "yy21Step2MnhlCnt", "yy21Step3MnhlCnt", "yy21Step4MnhlCnt"] },
|
||||
"yy21Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy22Group",
|
||||
header: { text: "2022년" },
|
||||
items: [
|
||||
{ name: "yy22MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy22WholMnhlCnt", "yy22Step1MnhlCnt", "yy22Step2MnhlCnt", "yy22Step3MnhlCnt", "yy22Step4MnhlCnt"] },
|
||||
"yy22Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy23Group",
|
||||
header: { text: "2023년" },
|
||||
items: [
|
||||
{ name: "yy23MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy23WholMnhlCnt", "yy23Step1MnhlCnt", "yy23Step2MnhlCnt", "yy23Step3MnhlCnt", "yy23Step4MnhlCnt"] },
|
||||
"yy23Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy24Group",
|
||||
header: { text: "2024년" },
|
||||
items: [
|
||||
{ name: "yy24MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy24WholMnhlCnt", "yy24Step1MnhlCnt", "yy24Step2MnhlCnt", "yy24Step3MnhlCnt", "yy24Step4MnhlCnt"] },
|
||||
"yy24Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "yy25Group",
|
||||
header: { text: "2025년" },
|
||||
items: [
|
||||
{ name: "yy25MtrlGroup", header: { text: "맨홀뚜껑 재질(개)" }, items: ["yy25WholMnhlCnt", "yy25Step1MnhlCnt", "yy25Step2MnhlCnt", "yy25Step3MnhlCnt", "yy25Step4MnhlCnt"] },
|
||||
"yy25Step5MnhlCnt",
|
||||
],
|
||||
},
|
||||
"rmrk",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const SewagePipeColumnInfoListRvw = rawList.map(tab => ({
|
||||
|
||||
@@ -9,6 +9,13 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
{ name: 'sewerBzmnCd', fieldName: 'sewerBzmnCd', header: { text: '사업자코드' }, visible: false },
|
||||
{ name: 'statsCd', fieldName: 'statsCd', header: { text: '메뉴코드' }, visible: false },
|
||||
{ name: 'tabNm', fieldName: 'tabNm', header: { text: '구분' }, width: 220 },
|
||||
{
|
||||
name: 'reviewHistory',
|
||||
fieldName: 'reviewHistory',
|
||||
header: { text: '검토이력' },
|
||||
width: 118,
|
||||
styleName: 'rg-cell-outline-action-btn',
|
||||
},
|
||||
|
||||
{ name: 'chargerNm', fieldName: 'chargerNm', header: { text: '제출자' } },
|
||||
{ name: 'chargerCttpc', fieldName: 'chargerCttpc', header: { text: '연락처' } },
|
||||
@@ -22,6 +29,7 @@ const StatsInputStatusInfoColumnRaw = {
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'tabNm',
|
||||
'reviewHistory',
|
||||
'chargerNm',
|
||||
'chargerCttpc',
|
||||
'submitStatusNm',
|
||||
@@ -38,6 +46,5 @@ export const StatsInputStatusInfoList = {
|
||||
columns: StatsInputStatusInfoColumnRaw.columns.map(col => ({
|
||||
...col,
|
||||
editable: false,
|
||||
styleName: "rg-cell-readonly"
|
||||
}))
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import type { ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
|
||||
/**
|
||||
* 하수도 통계 관리 > 02. 하수관로 (합계)
|
||||
* 항목은 검토(SewagePipeRvwColumn)와 동일하나 footer·mergeCells·구분(displayYear)·비고(rmrk) 제외.
|
||||
@@ -7,7 +9,7 @@ import { ValueType } from "realgrid";
|
||||
*/
|
||||
|
||||
/** ST00020003 관종별: 입력 `SeweragePipeInputPipeTypeStatusItem`·합계 `SewagePipePipeTypeFacilityStatusItem`과 동일 (yy1=1996년 이전 … yy30) */
|
||||
const YY_PIPE_TYPE_FACILITY_MAX = 30;
|
||||
// const YY_PIPE_TYPE_FACILITY_MAX = 30;
|
||||
|
||||
const rawSewagePipeTotList = [
|
||||
{
|
||||
@@ -605,103 +607,6 @@ const rawSewagePipeTotList = [
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
tabKey: "ST00020003",
|
||||
tabLabel: "관종별 현황",
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false },
|
||||
{ name: "swrgPipeNm", fieldName: "swrgPipeNm", header: { text: "관종" }, editable: false },
|
||||
{
|
||||
name: "pipeByPrstWholSum",
|
||||
fieldName: "pipeByPrstWhol",
|
||||
header: { text: "계" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "lyngPrlgSum",
|
||||
fieldName: "lyngPrlg",
|
||||
header: { text: "매설연장(m)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "dmltnPrlgSum",
|
||||
fieldName: "dmltnPrlg",
|
||||
header: { text: "철거연장(m)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX }, (_, i) => {
|
||||
const yyIdx = i + 1;
|
||||
const wholField = `yy${yyIdx}PrlgWhol`;
|
||||
const lyngField = `yy${yyIdx}LyngPrlg`;
|
||||
const dmltnField = `yy${yyIdx}DmltnPrlg`;
|
||||
return [
|
||||
{
|
||||
name: wholField,
|
||||
fieldName: wholField,
|
||||
header: { text: "계" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: lyngField,
|
||||
fieldName: lyngField,
|
||||
header: { text: "매설연장(m)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: dmltnField,
|
||||
fieldName: dmltnField,
|
||||
header: { text: "철거연장(m)" },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: "#,##0.0",
|
||||
editable: false,
|
||||
},
|
||||
];
|
||||
}).flat(),
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: "areaGroup",
|
||||
header: { text: "행정구역" },
|
||||
items: ["ctpvNm", "sggNm", "admdstNm"],
|
||||
},
|
||||
"swrgPipeNm",
|
||||
{
|
||||
name: "summaryGroup",
|
||||
header: { text: "총계" },
|
||||
items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"],
|
||||
},
|
||||
{
|
||||
name: "year1996Before",
|
||||
header: { text: "1996년이전" },
|
||||
items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"],
|
||||
},
|
||||
...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX - 1 }, (_, i) => {
|
||||
const yyIdx = i + 2;
|
||||
const year = 1994 + yyIdx;
|
||||
return {
|
||||
name: `group_yy${yyIdx}`,
|
||||
header: { text: `${year}년` },
|
||||
items: [`yy${yyIdx}PrlgWhol`, `yy${yyIdx}LyngPrlg`, `yy${yyIdx}DmltnPrlg`],
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
tabKey: "ST00020004",
|
||||
tabLabel: "관경별 현황",
|
||||
@@ -1019,13 +924,72 @@ const rawSewagePipeTotList = [
|
||||
],
|
||||
},
|
||||
"rmrk",
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
export const SewagePipeColumnInfoListTot = rawSewagePipeTotList.map((tab) => ({
|
||||
// 2. 동적 컬럼(ST00020003) 생성 함수
|
||||
export const getST00020003Config = (statsYr: string | number) => {
|
||||
const startYear = 1995;
|
||||
const endYear = Number(statsYr) || new Date().getFullYear();
|
||||
|
||||
return {
|
||||
tabKey: "ST00020003",
|
||||
tabLabel: "관종별 현황",
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 2,
|
||||
columns: [
|
||||
{ name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false },
|
||||
{ name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false },
|
||||
{ name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false },
|
||||
{ name: "swrgPipeNm", fieldName: "swrgPipeNm", header: { text: "관종" }, editable: false },
|
||||
{ name: "pipeByPrstWholSum", fieldName: "pipeByPrstWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "lyngPrlgSum", fieldName: "lyngPrlg", header: { text: "매설연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: "dmltnPrlgSum", fieldName: "dmltnPrlg", header: { text: "철거연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
...(() => {
|
||||
const yearColumns = [];
|
||||
for (let yr = startYear; yr <= endYear; yr++) {
|
||||
yearColumns.push(
|
||||
{ name: `prlg_whol_${yr}`, fieldName: `prlg_whol_${yr}`, header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: `lyng_prlg_${yr}`, fieldName: `lyng_prlg_${yr}`, header: { text: "매설연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false },
|
||||
{ name: `dmltn_prlg_${yr}`, fieldName: `dmltn_prlg_${yr}`, header: { text: "철거연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }
|
||||
);
|
||||
}
|
||||
return yearColumns;
|
||||
})(),
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] },
|
||||
"swrgPipeNm",
|
||||
{ name: "summaryGroup", header: { text: "총계" }, items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"] },
|
||||
...(() => {
|
||||
const layouts = [];
|
||||
for (let yr = startYear; yr <= endYear; yr++) {
|
||||
layouts.push({
|
||||
name: `group_${yr}`,
|
||||
header: { text: yr === 1995 ? "1996년이전" : `${yr}년` },
|
||||
items: [`prlg_whol_${yr}`, `lyng_prlg_${yr}`, `dmltn_prlg_${yr}`],
|
||||
});
|
||||
}
|
||||
return layouts;
|
||||
})(),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const SewagePipeColumnInfoListTot: ColumnInfoList[] = rawSewagePipeTotList.map((tab) => ({
|
||||
...tab,
|
||||
columns: tab.columns.map((col) => ({ ...col, editable: false as const })),
|
||||
}));
|
||||
|
||||
const st00020003PlaceholderTab: ColumnInfoList = {
|
||||
tabKey: "ST00020003",
|
||||
tabLabel: "관종별 현황",
|
||||
rowStartNum: 2,
|
||||
fixedColumns: 2,
|
||||
columns: [],
|
||||
columnLayout: [],
|
||||
};
|
||||
|
||||
SewagePipeColumnInfoListTot.splice(2, 0, st00020003PlaceholderTab);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type {
|
||||
GroupUserService,
|
||||
GroupUserSearchParams,
|
||||
GroupUserListResponseDto,
|
||||
GroupUserDetailResponse,
|
||||
PageGroupUserList,
|
||||
GroupUserSaveData,
|
||||
GroupUserUpdateData,
|
||||
} from "../model/group-user-interface";
|
||||
|
||||
export class GroupUsersApiService implements GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
public async findPageGroupUser(
|
||||
params: GroupUserSearchParams,
|
||||
): Promise<PageGroupUserList> {
|
||||
const response = await client.get<PageGroupUserList>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
public async findByGroupAndUserId(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.get<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장
|
||||
public async saveGroupUser(
|
||||
data: GroupUserSaveData[],
|
||||
): Promise<GroupUserListResponseDto[]> {
|
||||
const response = await client.post<GroupUserListResponseDto[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정
|
||||
public async updateGroupUser(
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
): Promise<GroupUserDetailResponse> {
|
||||
const response = await client.put<GroupUserDetailResponse>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${grpSn}/${userUnqId}`,
|
||||
data,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
public async deleteGroupUser(
|
||||
groupId: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
const response = await client.delete<boolean>(
|
||||
`${ENDPOINT.CMS_SERVICE.GROUP_USERS}/${groupId}/${userUnqId}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const GroupUsersApi = new GroupUsersApiService();
|
||||
@@ -0,0 +1,5 @@
|
||||
export { GroupUsersApi } from "./api/group-user-service";
|
||||
export {
|
||||
type GroupUserListResponseDto,
|
||||
type GroupUserSaveData,
|
||||
} from "./model/group-user-interface";
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { GroupUsersApi } from "../api/group-user-service";
|
||||
|
||||
export const GRP_ST_INPUT = "GRP_ST_INPUT";
|
||||
export const GRP_ST_CONFIRM = "GRP_ST_CONFIRM";
|
||||
export const GRP_ADMIN = "GRP_ADMIN";
|
||||
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
/**
|
||||
* 지정한 그룹(`keyword`)마다 `findPageGroupUser`를 호출하고,
|
||||
* 응답 목록이 비어 있지 않으며 로그인 사용자 `userUnqId`와 일치하는 행이 있으면 true.
|
||||
*/
|
||||
export async function checkPageGroupUserMembershipForUser(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
groupKeywords: readonly string[],
|
||||
): Promise<boolean> {
|
||||
const uid = userUnqId.trim();
|
||||
const nm = userName.trim();
|
||||
if (!uid || !nm) return false;
|
||||
|
||||
const paramsBase = { userNm: nm, page: 0, size: PAGE_SIZE };
|
||||
|
||||
for (const keyword of groupKeywords) {
|
||||
const page = await GroupUsersApi.findPageGroupUser({
|
||||
...paramsBase,
|
||||
keyword,
|
||||
});
|
||||
const rows = page.content ?? [];
|
||||
if (rows.length > 0 && rows.some((r) => String(r.userUnqId ?? "").trim() === uid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 입력 화면 상단 액션 — `GRP_ST_INPUT` / `GRP_ADMIN`.
|
||||
*/
|
||||
export async function checkStatisticsInputToolbarGroupMembership(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_INPUT, GRP_ADMIN]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 입력 현황 승인 등 — `GRP_ST_CONFIRM` / `GRP_ADMIN`.
|
||||
*/
|
||||
export async function checkStatisticsInputStatusConfirmGroupMembership(
|
||||
userName: string,
|
||||
userUnqId: string,
|
||||
): Promise<boolean> {
|
||||
return checkPageGroupUserMembershipForUser(userName, userUnqId, [GRP_ST_CONFIRM, GRP_ADMIN]);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
// 그룹 사용자 검색 파라미터 인터페이스
|
||||
export interface GroupUserSearchParams extends PageableParams {
|
||||
keyword?: string;
|
||||
userNm?: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 목록 응답 인터페이스
|
||||
export interface GroupUserListResponseDto {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
ogdpSeCd: string;
|
||||
ogdpSeNm: string;
|
||||
mphn: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 페이지 응답 인터페이스
|
||||
export type PageGroupUserList = PageableList<GroupUserListResponseDto>;
|
||||
|
||||
// 그룹 사용자 상세 응답 인터페이스
|
||||
export interface GroupUserDetailResponse {
|
||||
grpSn: number;
|
||||
userUnqId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
email: string;
|
||||
mphn: string;
|
||||
regDt: string;
|
||||
rgtr: string;
|
||||
mdfcnDt: string;
|
||||
mdfr: string;
|
||||
rgtrNm: string;
|
||||
mdfrNm: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 저장 데이터 인터페이스
|
||||
export interface GroupUserSaveData {
|
||||
mode: string;
|
||||
groupId: string;
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 수정 데이터 인터페이스
|
||||
export interface GroupUserUpdateData {
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
// 그룹 사용자 서비스 인터페이스
|
||||
export interface GroupUserService {
|
||||
// 그룹 사용자 목록 조회
|
||||
findPageGroupUser: (
|
||||
params: GroupUserSearchParams,
|
||||
) => Promise<PageGroupUserList>;
|
||||
|
||||
// 그룹 사용자 단건 조회
|
||||
findByGroupAndUserId: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 저장
|
||||
saveGroupUser: (
|
||||
data: GroupUserSaveData[],
|
||||
) => Promise<GroupUserListResponseDto[]>;
|
||||
|
||||
// 그룹 사용자 수정
|
||||
updateGroupUser: (
|
||||
grpSn: number,
|
||||
userUnqId: string,
|
||||
data: GroupUserUpdateData,
|
||||
) => Promise<GroupUserDetailResponse>;
|
||||
|
||||
// 그룹 사용자 삭제
|
||||
deleteGroupUser: (groupId: string, userUnqId: string) => Promise<boolean>;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type SewagePipeFacilityApiResponse,
|
||||
type SewerPipeTypeApiResponse,
|
||||
type SewerPipeDiamApiResponse,
|
||||
type SewagePipeManholeApiResponse,
|
||||
} from "../model/statistics-sewage-pipe-review-interface";
|
||||
import {
|
||||
type SewerPipeMtcApiResponse,
|
||||
@@ -30,6 +31,10 @@ import {
|
||||
import {
|
||||
type MediationPumpStationApiResponse,
|
||||
} from "../model/statistics-pumping-station-review-interface";
|
||||
import {
|
||||
type PublicSewageFacilityReviewApiResponse,
|
||||
type PublicSewageFacilityReviewRequestParams,
|
||||
} from "../model/statistics-public-sewage-facility-review-interface";
|
||||
|
||||
|
||||
export class StatisticsManagementApiService {
|
||||
@@ -61,6 +66,20 @@ export class StatisticsManagementApiService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 06. 공공하수처리시설 검토 목록 조회
|
||||
* @param params (sido, gugun, yr, bsnmCd, fctName)
|
||||
*/
|
||||
public async getPublicSewageFacilityReviewList(
|
||||
params: PublicSewageFacilityReviewRequestParams
|
||||
): Promise<PublicSewageFacilityReviewApiResponse> {
|
||||
const response = await stClient.get<PublicSewageFacilityReviewApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/public-sewage`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 02. 하수관로 지역별 시설현황 목록 조회 (Tab1)
|
||||
* @param params (sido, gugun, yr, bsnmCd)
|
||||
@@ -117,6 +136,20 @@ export class StatisticsManagementApiService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 02. 하수관로 맨홀 현황 조회 (Tab 5)
|
||||
* @param params (sido, gugun, yr, bsnmCd)
|
||||
*/
|
||||
public async getSewagePipeManholeList(
|
||||
params: SewagePipeReviewRequestParams
|
||||
): Promise<SewagePipeManholeApiResponse> {
|
||||
const response = await stClient.get<SewagePipeManholeApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/sewer-pipe/manhole`,
|
||||
{ params }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 03. 하수관로 유지관리 개보수 현황 조회
|
||||
* @param params (yr, bsnmCd, sido, gugun)
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
type ImrRequest,
|
||||
type ImrResponse,
|
||||
type ImrStatusResponse,
|
||||
type ReviewerInfoRequest,
|
||||
type ReviewerInfoResponse,
|
||||
type ReviewInputSaveRequest,
|
||||
type ReviewOpinionSaveRequest,
|
||||
type ReviewOpinionSaveResponse,
|
||||
type ReviewReturnSaveRequest,
|
||||
@@ -18,16 +21,24 @@ import {
|
||||
type ReviewSubmitResponse,
|
||||
type SearchCondition,
|
||||
type Sgg,
|
||||
type StatiscicsManagementReviewHistoryRequest,
|
||||
type StatiscicsManagementReviewHistoryResponse,
|
||||
type StatisticsColumnGuideListResponse,
|
||||
type StatisticsLogDetailResponse,
|
||||
type StatisticsLogRequest,
|
||||
type StatisticsLogResponse,
|
||||
type StatisticsService,
|
||||
type StatsInputPeriodDeleteApiResponse,
|
||||
type StatsInputPeriodDeleteRequest,
|
||||
type StatsInputPeriodDetailRequest,
|
||||
type StatsInputPeriodDetailResponse,
|
||||
type StatsInputPeriodListApiResponse,
|
||||
type StatsInputPeriodListRequest,
|
||||
type StatsInputPeriodSaveApiResponse,
|
||||
type StatsInputPeriodSaveRequest,
|
||||
type StatsInputStatusApproveBatchRequest,
|
||||
type StatsInputStatusApproveKind,
|
||||
type StatsInputStatusApproveResponse,
|
||||
type SubmitExceptionRequest,
|
||||
type SubmitExceptionResponse,
|
||||
type SubmitVerifyExceptionRequest,
|
||||
@@ -80,6 +91,14 @@ export class StatisticsApiService implements StatisticsService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getStatsInputPeriodDetail(params: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse> {
|
||||
const response = await stClient.get<StatsInputPeriodDetailResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.DASHBOARD}/schedule-info/detail`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 통계 입력 기간 저장 — 경로는 백엔드 스펙에 맞게 수정 */
|
||||
public async saveStatsInputPeriod(
|
||||
param: StatsInputPeriodSaveRequest,
|
||||
@@ -158,6 +177,17 @@ export class StatisticsApiService implements StatisticsService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 컬럼 가이드 목록 조회 — GET `.../common/column-guides?statsCd=` */
|
||||
public async getColumnGuides(
|
||||
param: string,
|
||||
): Promise<StatisticsColumnGuideListResponse> {
|
||||
const response = await stClient.get<StatisticsColumnGuideListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTCOMMON}/column-guides`,
|
||||
{ params: { statsCd: param } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveReviewOpinion(
|
||||
param: ReviewOpinionSaveRequest,
|
||||
): Promise<ReviewOpinionSaveResponse> {
|
||||
@@ -181,9 +211,61 @@ export class StatisticsApiService implements StatisticsService {
|
||||
public async saveReviewReturn(
|
||||
param: ReviewReturnSaveRequest,
|
||||
): Promise<ReviewReturnSaveResponse> {
|
||||
const { type, ...body } = param;
|
||||
const response = await stClient.post<ReviewReturnSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/return`,
|
||||
param,
|
||||
`${ENDPOINT.ST_SERVICE.STATS}/reject`,
|
||||
body,
|
||||
{ params: { type } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 입력 반려 — JSON body는 `ReviewInputSaveRequest`(type 제외) 항목의 배열, `type`은 쿼리 */
|
||||
public async saveInputReturn(
|
||||
items: ReviewInputSaveRequest[],
|
||||
): Promise<ReviewReturnSaveResponse> {
|
||||
if (items.length === 0) {
|
||||
throw new Error("saveInputReturn: 항목이 비어 있습니다.");
|
||||
}
|
||||
const [first] = items;
|
||||
if (!first) {
|
||||
throw new Error("saveInputReturn: 항목이 비어 있습니다.");
|
||||
}
|
||||
const type = first.type;
|
||||
if (!items.every((i) => i.type === type)) {
|
||||
throw new Error("saveInputReturn: 모든 항목의 type이 같아야 합니다.");
|
||||
}
|
||||
const body = items.map(({ type: _t, rjctOpnnCn, ...rest }) => {
|
||||
const row: Record<string, string> = {
|
||||
statsYr: rest.statsYr,
|
||||
sewerBzmnCd: rest.sewerBzmnCd,
|
||||
statsCd: rest.statsCd,
|
||||
mdfrId: rest.mdfrId,
|
||||
};
|
||||
if (rest.vrfcSn != null && String(rest.vrfcSn).trim() !== "") {
|
||||
row.vrfcSn = String(rest.vrfcSn);
|
||||
}
|
||||
const trimmed = rjctOpnnCn?.trim();
|
||||
if (trimmed) row.rjctOpnnCn = trimmed;
|
||||
return row;
|
||||
});
|
||||
const response = await stClient.post<ReviewReturnSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.STATS}/reject`,
|
||||
body,
|
||||
{ params: { type } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 — 쿼리 `type`(기본 `INPUT`), body는 `{ list }` */
|
||||
public async approveStatsInputStatus(
|
||||
param: StatsInputStatusApproveBatchRequest,
|
||||
type: StatsInputStatusApproveKind = "INPUT",
|
||||
): Promise<StatsInputStatusApproveResponse> {
|
||||
const response = await stClient.post<StatsInputStatusApproveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.STATS}/approve`,
|
||||
param.list,
|
||||
{ params: { type } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -213,6 +295,22 @@ export class StatisticsApiService implements StatisticsService {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse> {
|
||||
const response = await stClient.get<ReviewerInfoResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/reviewer-info`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise<StatiscicsManagementReviewHistoryResponse> {
|
||||
const response = await stClient.get<StatiscicsManagementReviewHistoryResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.REVIEW}/stats-review`,
|
||||
{ params: param },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsApi = new StatisticsApiService();
|
||||
|
||||
@@ -74,6 +74,12 @@ export {
|
||||
type ImrLogResponse,
|
||||
} from "./model/imr/imr-interface";
|
||||
export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns";
|
||||
export {
|
||||
type StatsInputStatusApproveBatchRequest,
|
||||
type StatsInputStatusApproveItem,
|
||||
type StatsInputStatusApproveKind,
|
||||
type StatsInputStatusApproveResponse,
|
||||
} from "./model/statistics-interface";
|
||||
export { type StatsInputStatusList } from "./model/input/statistics-input-status-interface";
|
||||
export { userListColumns } from "./model/notice-talk/notice-talk-sending-columns";
|
||||
export {
|
||||
|
||||
@@ -230,6 +230,7 @@ export interface VerifyResultItem {
|
||||
nowVlCn?: string;
|
||||
compVlCn?: string;
|
||||
prcsYn?: string;
|
||||
prcsYnNm?: string;
|
||||
prcsOpnnExpln?: string;
|
||||
prcrId?: string;
|
||||
rjctOpnnCn?: string;
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface StatsInputStatusList {
|
||||
statsCd?: string;
|
||||
sewerBzmnCd?: string;
|
||||
menuTabName?: string;
|
||||
locgStatusNm?: string;
|
||||
inputRate?: number;
|
||||
laeSbmsnSttsNm?: string;
|
||||
laeSbmsnSttsCd?: string;
|
||||
laePicNm?: string;
|
||||
laePicTelNo?: string;
|
||||
laeKbrdrDeptNm?: string;
|
||||
|
||||
+5
-4
@@ -187,10 +187,11 @@ export interface PublicSewageMainFacilityMappingSaveRequest {
|
||||
admdstNm: string;
|
||||
mainFcltSn: string;
|
||||
mainFcltNm: string;
|
||||
prxyBzentyNmCn: string;
|
||||
cnsgnCstVl: number;
|
||||
wholNope: number;
|
||||
prxyCtrtCn: string;
|
||||
/** 매핑 추가(saveList 있음) 시 입력. 매핑 삭제만 할 때는 요청에서 생략 가능 */
|
||||
prxyBzentyNmCn?: string;
|
||||
cnsgnCstVl?: number;
|
||||
wholNope?: number;
|
||||
prxyCtrtCn?: string;
|
||||
userId: string;
|
||||
saveList: PublicSewageMainFacilityMappingSaveItem[];
|
||||
deleteList: string[];
|
||||
|
||||
+4
-4
@@ -87,9 +87,9 @@ export interface SewerageSupplyRateRequestParams {
|
||||
/** 하수도 보급률 저장 API 요청 body (한 행) */
|
||||
export interface SewerageSupplyRateSaveItem {
|
||||
sggNm: string | null;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
trbtrNm: string;
|
||||
admdstCd: string | null;
|
||||
admdstNm: string | null;
|
||||
trbtrNm: string | null;
|
||||
spcCtpnRgnCn: string | null;
|
||||
ts1SwtrmtPpltnCnt: number;
|
||||
ts2SwtrmtPpltnCnt: number;
|
||||
@@ -103,7 +103,7 @@ export interface SewerageSupplyRateSaveItem {
|
||||
drtwPrcsPpltnCnt: number;
|
||||
spctnkPpltnCnt: number;
|
||||
staaExcpArea: number;
|
||||
rmrk: string;
|
||||
rmrk: string | null;
|
||||
wholPpltnCnt: number;
|
||||
wholArea: number;
|
||||
wholSwtrmtPpltnCnt: number;
|
||||
|
||||
@@ -35,11 +35,23 @@ export interface ApiResponse<T> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 통계 컬럼 가이드(영문 컬럼명·한글명·유형·계산 여부·가이드 문구) 한 행 */
|
||||
export interface StatisticsColumnGuideItem {
|
||||
engColNm: string;
|
||||
korColNm: string;
|
||||
colType: string;
|
||||
isCalcCol: string;
|
||||
columnGuide: string;
|
||||
}
|
||||
|
||||
/** 컬럼 가이드 정보 조회 응답 (`data`: 컬럼 가이드 배열) */
|
||||
export type StatisticsColumnGuideListResponse = ApiResponse<StatisticsColumnGuideItem[]>;
|
||||
|
||||
/** 통계 입력 관리 등 — 연도·구분·분류(입력/검증)별 입력 기간 한 행 */
|
||||
export interface StatsInputPeriodItem {
|
||||
statsYr: string;
|
||||
seCd: number;
|
||||
seType: "INPT" | "VRFC";
|
||||
seType: string;
|
||||
inptBgngYmd: string;
|
||||
inptEndYmd: string;
|
||||
}
|
||||
@@ -50,15 +62,26 @@ export type StatsInputPeriodListApiResponse = ApiResponse<StatsInputPeriodItem[]
|
||||
/** 통계 입력 기간 목록 조회 요청 쿼리 */
|
||||
export interface StatsInputPeriodListRequest {
|
||||
statsYr?: string;
|
||||
/** 전체 / 입력 / 검증 (SearchForm `clsfCd`와 동일) */
|
||||
seType?: "ALL" | "INPT" | "VRFC";
|
||||
seType?: string;
|
||||
}
|
||||
|
||||
export interface StatsInputPeriodDetailRequest {
|
||||
statsYr: string;
|
||||
seType: string;
|
||||
seCd: number;
|
||||
}
|
||||
|
||||
export interface StatsInputPeriodDetailResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: StatsInputPeriodItem;
|
||||
}
|
||||
|
||||
/** 통계 입력 기간 등록·수정 등 요청 본문 */
|
||||
export interface StatsInputPeriodSaveRequest {
|
||||
statsYr: string;
|
||||
seType: string;
|
||||
seCd: number;
|
||||
seCd?: number;
|
||||
/** 시작일 (백엔드 형식 문자열, 예: YYYYMMDD) */
|
||||
inptBgngYmd: string;
|
||||
/** 종료일 (백엔드 형식 문자열, 예: YYYYMMDD) */
|
||||
@@ -69,8 +92,15 @@ export interface StatsInputPeriodSaveRequest {
|
||||
/** 통계 입력 기간 저장 응답 */
|
||||
export type StatsInputPeriodSaveApiResponse = ApiResponse<unknown>;
|
||||
|
||||
/** 통계 입력 기간 삭제 요청 (필드 구성은 저장과 동일) */
|
||||
export type StatsInputPeriodDeleteRequest = StatsInputPeriodSaveRequest;
|
||||
/** 통계 입력 기간 삭제 요청 */
|
||||
export interface StatsInputPeriodDeleteRequest {
|
||||
statsYr: string;
|
||||
seType: string;
|
||||
seCd: number;
|
||||
inptBgngYmd: string;
|
||||
inptEndYmd: string;
|
||||
rgtrId: string;
|
||||
}
|
||||
|
||||
/** 통계 입력 기간 삭제 응답 */
|
||||
export type StatsInputPeriodDeleteApiResponse = ApiResponse<unknown>;
|
||||
@@ -215,6 +245,10 @@ export interface StatisticsLog {
|
||||
statsDataChgComCd?: string;
|
||||
rgtrId?: string;
|
||||
regDt?: string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
statsDataChgComNm?: string;
|
||||
statsYr?: string;
|
||||
}
|
||||
|
||||
export interface StatisticsLogRequest {
|
||||
@@ -300,16 +334,31 @@ export interface ReviewOpinionSaveResponse {
|
||||
}
|
||||
|
||||
export interface ReviewReturnSaveRequest {
|
||||
type: "REVIEW";
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
rjctOpnnCn: string;
|
||||
mdfrId: string;
|
||||
vrfcSn?: string;
|
||||
}
|
||||
|
||||
export interface ReviewReturnSaveResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
exceptionCnt: number;
|
||||
message: string;
|
||||
resultCode: string;
|
||||
updatedStts: string;
|
||||
}
|
||||
|
||||
export interface ReviewInputSaveRequest {
|
||||
type: "INPUT";
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
/** 미입력·빈 문자열이면 요청 body에 포함하지 않음 */
|
||||
rjctOpnnCn?: string;
|
||||
mdfrId: string;
|
||||
vrfcSn?: string;
|
||||
}
|
||||
|
||||
export interface ReviewSubmitRequest {
|
||||
@@ -317,6 +366,8 @@ export interface ReviewSubmitRequest {
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
mdfrId: string;
|
||||
splmntOpnnCn: string;
|
||||
wrterOpnnCn: string;
|
||||
}
|
||||
|
||||
export interface ReviewSubmitResponse {
|
||||
@@ -324,6 +375,30 @@ export interface ReviewSubmitResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 통계 일괄 승인 `/stats/approve` 쿼리 파라미터 `type` */
|
||||
export type StatsInputStatusApproveKind = "INPUT" | "REVIEW";
|
||||
|
||||
/** 통계 입력 현황 승인 요청 항목 (행 단위) */
|
||||
export interface StatsInputStatusApproveItem {
|
||||
statsYr: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
mdfrId: string;
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 API 요청 본문 */
|
||||
export interface StatsInputStatusApproveBatchRequest {
|
||||
list: StatsInputStatusApproveItem[];
|
||||
}
|
||||
|
||||
/** 통계 입력 현황 일괄 승인 API 응답 */
|
||||
export interface StatsInputStatusApproveResponse {
|
||||
exceptionCnt: number;
|
||||
message: string;
|
||||
resultCode: string;
|
||||
updatedStts: string;
|
||||
}
|
||||
|
||||
export interface SubmitVerifyExceptionItem {
|
||||
vrfcSn: number;
|
||||
prcsOpnnExpln: string;
|
||||
@@ -342,13 +417,57 @@ export interface SubmitVerifyExceptionResponse {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ReviewerInfoRequest {
|
||||
sewerBzmnCd: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
instSeCd : string;
|
||||
}
|
||||
|
||||
export interface ReviewerInfoResponse {
|
||||
status: string;
|
||||
data: {
|
||||
userNm: string;
|
||||
telOffrm: string;
|
||||
dpmNm: string;
|
||||
cps: string;
|
||||
splmntOpnnCn: string;
|
||||
wrterOpnnCn: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface StatiscicsManagementReviewHistoryRequest {
|
||||
statsYr: string;
|
||||
sewerBzmnCd?: string;
|
||||
ctpvNm?: string;
|
||||
statsCd?: string;
|
||||
}
|
||||
|
||||
/** 통계관리 검토 이력 조회 API `data` 배열 요소 */
|
||||
export interface StatiscicsManagementReviewHistoryItem {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
menuNm: string;
|
||||
wrterOpinionSm: string | null;
|
||||
chckerOpinionSm: string | null;
|
||||
returnOpinionSm: string | null;
|
||||
}
|
||||
|
||||
export interface StatiscicsManagementReviewHistoryResponse {
|
||||
status: string;
|
||||
message?: string;
|
||||
data: StatiscicsManagementReviewHistoryItem[];
|
||||
}
|
||||
|
||||
export interface StatisticsService {
|
||||
getSidoList(params?: { instSeCd?: string }): Promise<ApiResponse<SearchCondition>>;
|
||||
getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
|
||||
getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
|
||||
getStatsInputPeriodList(
|
||||
params?: StatsInputPeriodListRequest,
|
||||
): Promise<StatsInputPeriodListApiResponse>;
|
||||
getStatsInputPeriodList(params?: StatsInputPeriodListRequest): Promise<StatsInputPeriodListApiResponse>;
|
||||
getStatsInputPeriodDetail(param: StatsInputPeriodDetailRequest): Promise<StatsInputPeriodDetailResponse>;
|
||||
saveStatsInputPeriod(param: StatsInputPeriodSaveRequest): Promise<StatsInputPeriodSaveApiResponse>;
|
||||
deleteStatsInputPeriod(param: StatsInputPeriodDeleteRequest): Promise<StatsInputPeriodDeleteApiResponse>;
|
||||
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
|
||||
@@ -359,8 +478,16 @@ export interface StatisticsService {
|
||||
saveReviewOpinion(param: ReviewOpinionSaveRequest): Promise<ReviewOpinionSaveResponse>;
|
||||
getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise<getReviewOpinionDetailResponse>;
|
||||
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
|
||||
saveInputReturn(items: ReviewInputSaveRequest[]): Promise<ReviewReturnSaveResponse>;
|
||||
approveStatsInputStatus(
|
||||
param: StatsInputStatusApproveBatchRequest,
|
||||
type?: StatsInputStatusApproveKind,
|
||||
): Promise<StatsInputStatusApproveResponse>;
|
||||
submitReview(param: ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
|
||||
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
|
||||
getImrStatus(param: string): Promise<ImrStatusResponse>;
|
||||
SubmitVerifyException(param: SubmitVerifyExceptionRequest): Promise<SubmitVerifyExceptionResponse>;
|
||||
getColumnGuides(param: string): Promise<StatisticsColumnGuideListResponse>;
|
||||
getReviewerInfo(param: ReviewerInfoRequest): Promise<ReviewerInfoResponse>;
|
||||
getStatiscicsManagementReviewHistory(param: StatiscicsManagementReviewHistoryRequest): Promise<StatiscicsManagementReviewHistoryResponse>;
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import type { ApiResponse } from "./statistics-interface";
|
||||
import type { StatsStatusInfo } from "./statistics-sewage-pipe-review-interface";
|
||||
|
||||
export interface PublicSewageFacilityReviewItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
statsYr: string;
|
||||
admdstCd: string;
|
||||
admdstNm: string;
|
||||
swtrmtFcltNm: string;
|
||||
displayYear: string;
|
||||
rowType: "OLD" | "CURR";
|
||||
fcltPstnCn: string | null;
|
||||
prcsFcltCpct: number;
|
||||
prcsFcltPhysCpct: number;
|
||||
prcsFcltBoilCpct: number;
|
||||
prcsFcltAltdCpct: number;
|
||||
inflwSwageQnt: number;
|
||||
inflwSwagePhysQnt: number;
|
||||
inflwSwageBoilQnt: number;
|
||||
inflwSwageAltdQnt: number;
|
||||
prcsQnt: number;
|
||||
prcsPhysCpct: number;
|
||||
prcsBoilCpct: number;
|
||||
prcsAltdCpct: number;
|
||||
inflwBodVl: number;
|
||||
dschgBodVl: number;
|
||||
inflwTocQnt: number;
|
||||
dschgTocQnt: number;
|
||||
inflwSsQnt: number;
|
||||
dschgSsQnt: number;
|
||||
inflwTnVl: number;
|
||||
dschgTnVl: number;
|
||||
inflwTpVl: number;
|
||||
dschgTpVl: number;
|
||||
inflwGrssClfgVl: number;
|
||||
dschgGrssClfgVl: number;
|
||||
prcsRt: number;
|
||||
prcsLodQnt: number;
|
||||
prcsMthdExpln: string | null;
|
||||
wholHmnQnt: number;
|
||||
wholLvstQnt: number;
|
||||
wholEfntQnt: number;
|
||||
wholEtcQnt: number;
|
||||
telNo: string | null;
|
||||
cmptnStrtYmd: string | null;
|
||||
oprtngStrtYmd: string | null;
|
||||
wholBizVl: number;
|
||||
eflntDsnfMthdExpln: string | null;
|
||||
wtsysNm: string | null;
|
||||
trbtrNm: string | null;
|
||||
dtlUnitZoneCn: string | null;
|
||||
rgnSeNm: string | null;
|
||||
msirbCn: string | null;
|
||||
goalWqNo: string | null;
|
||||
engySrlneRt: number;
|
||||
engyItslfPrdctnQnt: number;
|
||||
anlEngyWholUseQnt: number;
|
||||
anlElpwrUseQnt: number;
|
||||
itslfEngyUseQnt: number;
|
||||
swtrmtEmsnQnt: number;
|
||||
operMnbdCn: string | null;
|
||||
prxyBzentyNmCn: string | null;
|
||||
cnsgnCstVl: number;
|
||||
prxyCtrtCn: string | null;
|
||||
intgPrcsFcltCn: string | null;
|
||||
wholNope: number;
|
||||
sftyAcdntEvntcsCnt: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
export interface PublicSewageFacilityReviewResponseData {
|
||||
publicSewageList: PublicSewageFacilityReviewItem[];
|
||||
statsStatusInfo: StatsStatusInfo;
|
||||
}
|
||||
|
||||
export interface PublicSewageFacilityReviewRequestParams {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
statsYr?: string;
|
||||
sewerBzmnCd?: string;
|
||||
swtrmtFcltNm?: string;
|
||||
}
|
||||
|
||||
export type PublicSewageFacilityReviewApiResponse =
|
||||
ApiResponse<PublicSewageFacilityReviewResponseData>;
|
||||
|
||||
export interface PublicSewageFacilityReviewService {
|
||||
getPublicSewageFacilityReviewList(
|
||||
params: PublicSewageFacilityReviewRequestParams,
|
||||
): Promise<PublicSewageFacilityReviewApiResponse>;
|
||||
}
|
||||
@@ -244,6 +244,118 @@ export interface SewagePipeDiamService {
|
||||
getSewagePipeDiamList(params: SewagePipeReviewRequestParams): Promise<SewerPipeDiamApiResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 02. 하수관로 맨홀 현황 아이템 (Tab 5)
|
||||
*/
|
||||
export interface SewagePipeReviewManholeItem {
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
admdstCd: string;
|
||||
statsYr: string;
|
||||
displayYear: string;
|
||||
rowType: 'OLD' | 'CURR';
|
||||
emphsMngSeCd: string;
|
||||
emphsMngSeNm?: string;
|
||||
|
||||
yy14WholMnhlCnt: number;
|
||||
yy14Step1MnhlCnt: number;
|
||||
yy14Step2MnhlCnt: number;
|
||||
yy14Step3MnhlCnt: number;
|
||||
yy14Step4MnhlCnt: number;
|
||||
yy14Step5MnhlCnt: number;
|
||||
|
||||
yy15WholMnhlCnt: number;
|
||||
yy15Step1MnhlCnt: number;
|
||||
yy15Step2MnhlCnt: number;
|
||||
yy15Step3MnhlCnt: number;
|
||||
yy15Step4MnhlCnt: number;
|
||||
yy15Step5MnhlCnt: number;
|
||||
|
||||
yy16WholMnhlCnt: number;
|
||||
yy16Step1MnhlCnt: number;
|
||||
yy16Step2MnhlCnt: number;
|
||||
yy16Step3MnhlCnt: number;
|
||||
yy16Step4MnhlCnt: number;
|
||||
yy16Step5MnhlCnt: number;
|
||||
|
||||
yy17WholMnhlCnt: number;
|
||||
yy17Step1MnhlCnt: number;
|
||||
yy17Step2MnhlCnt: number;
|
||||
yy17Step3MnhlCnt: number;
|
||||
yy17Step4MnhlCnt: number;
|
||||
yy17Step5MnhlCnt: number;
|
||||
|
||||
yy18WholMnhlCnt: number;
|
||||
yy18Step1MnhlCnt: number;
|
||||
yy18Step2MnhlCnt: number;
|
||||
yy18Step3MnhlCnt: number;
|
||||
yy18Step4MnhlCnt: number;
|
||||
yy18Step5MnhlCnt: number;
|
||||
|
||||
yy19WholMnhlCnt: number;
|
||||
yy19Step1MnhlCnt: number;
|
||||
yy19Step2MnhlCnt: number;
|
||||
yy19Step3MnhlCnt: number;
|
||||
yy19Step4MnhlCnt: number;
|
||||
yy19Step5MnhlCnt: number;
|
||||
|
||||
yy20WholMnhlCnt: number;
|
||||
yy20Step1MnhlCnt: number;
|
||||
yy20Step2MnhlCnt: number;
|
||||
yy20Step3MnhlCnt: number;
|
||||
yy20Step4MnhlCnt: number;
|
||||
yy20Step5MnhlCnt: number;
|
||||
|
||||
yy21WholMnhlCnt: number;
|
||||
yy21Step1MnhlCnt: number;
|
||||
yy21Step2MnhlCnt: number;
|
||||
yy21Step3MnhlCnt: number;
|
||||
yy21Step4MnhlCnt: number;
|
||||
yy21Step5MnhlCnt: number;
|
||||
|
||||
yy22WholMnhlCnt: number;
|
||||
yy22Step1MnhlCnt: number;
|
||||
yy22Step2MnhlCnt: number;
|
||||
yy22Step3MnhlCnt: number;
|
||||
yy22Step4MnhlCnt: number;
|
||||
yy22Step5MnhlCnt: number;
|
||||
|
||||
yy23WholMnhlCnt: number;
|
||||
yy23Step1MnhlCnt: number;
|
||||
yy23Step2MnhlCnt: number;
|
||||
yy23Step3MnhlCnt: number;
|
||||
yy23Step4MnhlCnt: number;
|
||||
yy23Step5MnhlCnt: number;
|
||||
|
||||
yy24WholMnhlCnt: number;
|
||||
yy24Step1MnhlCnt: number;
|
||||
yy24Step2MnhlCnt: number;
|
||||
yy24Step3MnhlCnt: number;
|
||||
yy24Step4MnhlCnt: number;
|
||||
yy24Step5MnhlCnt: number;
|
||||
|
||||
yy25WholMnhlCnt: number;
|
||||
yy25Step1MnhlCnt: number;
|
||||
yy25Step2MnhlCnt: number;
|
||||
yy25Step3MnhlCnt: number;
|
||||
yy25Step4MnhlCnt: number;
|
||||
yy25Step5MnhlCnt: number;
|
||||
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
export interface SewagePipeManholeResponseData {
|
||||
manholeList: SewagePipeReviewManholeItem[];
|
||||
statsStatusInfo: StatsStatusInfo;
|
||||
}
|
||||
|
||||
export type SewagePipeManholeApiResponse = ApiResponse<SewagePipeManholeResponseData>;
|
||||
|
||||
export interface SewagePipeManholeService {
|
||||
getSewagePipeManholeList(params: SewagePipeReviewRequestParams): Promise<SewagePipeManholeApiResponse>;
|
||||
}
|
||||
|
||||
export interface StatsStatusInfo {
|
||||
statsCd?: string;
|
||||
submitStatusCd?: string;
|
||||
|
||||
+14
-94
@@ -102,99 +102,7 @@ export interface SewagePipeFactInstallStatusResponse {
|
||||
|
||||
/** ST00020003 합계 — `GET .../sewer-pipe/pipe-type-facility-status` */
|
||||
export interface SewagePipePipeTypeFacilityStatusItem {
|
||||
pipeByPrstWhol: number;
|
||||
lyngPrlg: number;
|
||||
dmltnPrlg: number;
|
||||
yy1PrlgWhol: number;
|
||||
yy1LyngPrlg: number;
|
||||
yy1DmltnPrlg: number;
|
||||
yy2PrlgWhol: number;
|
||||
yy2LyngPrlg: number;
|
||||
yy2DmltnPrlg: number;
|
||||
yy3PrlgWhol: number;
|
||||
yy3LyngPrlg: number;
|
||||
yy3DmltnPrlg: number;
|
||||
yy4PrlgWhol: number;
|
||||
yy4LyngPrlg: number;
|
||||
yy4DmltnPrlg: number;
|
||||
yy5PrlgWhol: number;
|
||||
yy5LyngPrlg: number;
|
||||
yy5DmltnPrlg: number;
|
||||
yy6PrlgWhol: number;
|
||||
yy6LyngPrlg: number;
|
||||
yy6DmltnPrlg: number;
|
||||
yy7PrlgWhol: number;
|
||||
yy7LyngPrlg: number;
|
||||
yy7DmltnPrlg: number;
|
||||
yy8PrlgWhol: number;
|
||||
yy8LyngPrlg: number;
|
||||
yy8DmltnPrlg: number;
|
||||
yy9PrlgWhol: number;
|
||||
yy9LyngPrlg: number;
|
||||
yy9DmltnPrlg: number;
|
||||
yy10PrlgWhol: number;
|
||||
yy10LyngPrlg: number;
|
||||
yy10DmltnPrlg: number;
|
||||
yy11PrlgWhol: number;
|
||||
yy11LyngPrlg: number;
|
||||
yy11DmltnPrlg: number;
|
||||
yy12PrlgWhol: number;
|
||||
yy12LyngPrlg: number;
|
||||
yy12DmltnPrlg: number;
|
||||
yy13PrlgWhol: number;
|
||||
yy13LyngPrlg: number;
|
||||
yy13DmltnPrlg: number;
|
||||
yy14PrlgWhol: number;
|
||||
yy14LyngPrlg: number;
|
||||
yy14DmltnPrlg: number;
|
||||
yy15PrlgWhol: number;
|
||||
yy15LyngPrlg: number;
|
||||
yy15DmltnPrlg: number;
|
||||
yy16PrlgWhol: number;
|
||||
yy16LyngPrlg: number;
|
||||
yy16DmltnPrlg: number;
|
||||
yy17PrlgWhol: number;
|
||||
yy17LyngPrlg: number;
|
||||
yy17DmltnPrlg: number;
|
||||
yy18PrlgWhol: number;
|
||||
yy18LyngPrlg: number;
|
||||
yy18DmltnPrlg: number;
|
||||
yy19PrlgWhol: number;
|
||||
yy19LyngPrlg: number;
|
||||
yy19DmltnPrlg: number;
|
||||
yy20PrlgWhol: number;
|
||||
yy20LyngPrlg: number;
|
||||
yy20DmltnPrlg: number;
|
||||
yy21PrlgWhol: number;
|
||||
yy21LyngPrlg: number;
|
||||
yy21DmltnPrlg: number;
|
||||
yy22PrlgWhol: number;
|
||||
yy22LyngPrlg: number;
|
||||
yy22DmltnPrlg: number;
|
||||
yy23PrlgWhol: number;
|
||||
yy23LyngPrlg: number;
|
||||
yy23DmltnPrlg: number;
|
||||
yy24PrlgWhol: number;
|
||||
yy24LyngPrlg: number;
|
||||
yy24DmltnPrlg: number;
|
||||
yy25PrlgWhol: number;
|
||||
yy25LyngPrlg: number;
|
||||
yy25DmltnPrlg: number;
|
||||
yy26PrlgWhol: number;
|
||||
yy26LyngPrlg: number;
|
||||
yy26DmltnPrlg: number;
|
||||
yy27PrlgWhol: number;
|
||||
yy27LyngPrlg: number;
|
||||
yy27DmltnPrlg: number;
|
||||
yy28PrlgWhol: number;
|
||||
yy28LyngPrlg: number;
|
||||
yy28DmltnPrlg: number;
|
||||
yy29PrlgWhol: number;
|
||||
yy29LyngPrlg: number;
|
||||
yy29DmltnPrlg: number;
|
||||
yy30PrlgWhol: number;
|
||||
yy30LyngPrlg: number;
|
||||
yy30DmltnPrlg: number;
|
||||
// 고정 필드
|
||||
ctpvNm: string;
|
||||
sggNm: string | null;
|
||||
admdstCd: string;
|
||||
@@ -203,7 +111,19 @@ export interface SewagePipePipeTypeFacilityStatusItem {
|
||||
statsYr: string;
|
||||
rowType: string;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
// 총계 필드
|
||||
pipeByPrstWhol: number;
|
||||
lyngPrlg: number;
|
||||
dmltnPrlg: number;
|
||||
|
||||
/**
|
||||
* 연도별 동적 데이터 필드
|
||||
"prlg_whol_1995", "lyng_prlg_1995", "dmltn_prlg_1995",
|
||||
|
||||
*/
|
||||
[key: string]: string | number | null;
|
||||
}
|
||||
|
||||
export interface SewagePipePipeTypeFacilityStatusResponse {
|
||||
status: string;
|
||||
|
||||
+14
-11
@@ -9,12 +9,14 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
fieldName: "ctpvNm",
|
||||
header: { text: "시도" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sggNm",
|
||||
fieldName: "sggNm",
|
||||
header: { text: "시군구" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "menuTabName",
|
||||
@@ -22,19 +24,14 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
header: { text: "구분" },
|
||||
width: 150,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
},
|
||||
{
|
||||
name: "locgStatusNm",
|
||||
fieldName: "locgStatusNm",
|
||||
header: { text: "진행상태" },
|
||||
width: 120,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laePicNm",
|
||||
fieldName: "laePicNm",
|
||||
header: { text: "작성자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laePicTelNo",
|
||||
@@ -42,36 +39,42 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
header: { text: "작성자연락처" },
|
||||
width: 130,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeKbrdrDeptNm",
|
||||
fieldName: "laeKbrdrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeKbrdrJbgdNm",
|
||||
fieldName: "laeKbrdrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrNm",
|
||||
fieldName: "laeIdfrNm",
|
||||
header: { text: "승인자" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrDeptNm",
|
||||
fieldName: "laeIdfrDeptNm",
|
||||
header: { text: "부서" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "laeIdfrJbgdNm",
|
||||
fieldName: "laeIdfrJbgdNm",
|
||||
header: { text: "직급" },
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "rjctOpnnCn",
|
||||
@@ -79,6 +82,7 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
header: { text: "반려사유" },
|
||||
width: 220,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "sewerBzmnCd",
|
||||
@@ -97,7 +101,6 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = {
|
||||
"ctpvNm",
|
||||
"sggNm",
|
||||
"menuTabName",
|
||||
"locgStatusNm",
|
||||
"laePicNm",
|
||||
"laePicTelNo",
|
||||
"laeKbrdrDeptNm",
|
||||
@@ -176,8 +179,8 @@ export const statisticsVerificationResultColumnInfo: ColumnInfo = {
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "prcsYn",
|
||||
fieldName: "prcsYn",
|
||||
name: "prcsYnNm",
|
||||
fieldName: "prcsYnNm",
|
||||
header: { text: "상태" },
|
||||
width: 90,
|
||||
styleName: READ_ONLY_STYLE,
|
||||
@@ -223,7 +226,7 @@ export const statisticsVerificationResultColumnInfo: ColumnInfo = {
|
||||
"rsnExpln",
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYn",
|
||||
"prcsYnNm",
|
||||
"prcsOpnnExpln",
|
||||
"rjctOpnnCn",
|
||||
"pbcpAprvYn",
|
||||
|
||||
@@ -17,11 +17,13 @@ interface VrfcReportPanelProps {
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
NV: "일반",
|
||||
IMR: "I-MR",
|
||||
HB: "HB",
|
||||
};
|
||||
|
||||
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
HB: "blue",
|
||||
};
|
||||
|
||||
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
|
||||
|
||||
@@ -70,6 +70,7 @@ export const ENDPOINT = {
|
||||
COMMON: "/st-management-service/api/v1/common",
|
||||
INPUT: "/st-input-service/api/v1/input",
|
||||
REVIEW: "/st-management-service/api/v1/review",
|
||||
STATS: "/st-management-service/api/stats",
|
||||
INPUTCOMMON: "/st-input-service/api/v1/common",
|
||||
INPUTVERIFY: "/st-input-service/api/v1/verify",
|
||||
TOTAL: "/st-management-service/api/v1/stats",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user