Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 4m23s
Deploy Frontend / deploy (push) Successful in 4m23s
This commit is contained in:
@@ -1,194 +1,423 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
|
||||
|
||||
import {
|
||||
|
||||
getTabStoreState as getGridTabStoreState,
|
||||
|
||||
useTabStore as gridTabStore,
|
||||
|
||||
} from "@rsone/components";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
|
||||
import { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync";
|
||||
|
||||
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
|
||||
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
|
||||
import { tokenManager } from "@/entities/auth";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
import { MenuApi } from "@/entities/menu";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
|
||||
|
||||
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
|
||||
|
||||
|
||||
|
||||
const appConfig = getConfig();
|
||||
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
|
||||
const appSiteId = getCookie("site-id-key") ?? "";
|
||||
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
export default function WithBaseLayout({
|
||||
|
||||
|
||||
function WithBaseLayoutInner({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
|
||||
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
|
||||
|
||||
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
|
||||
|
||||
useState(false);
|
||||
|
||||
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
|
||||
);
|
||||
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
|
||||
const setMenuList = useTabStore((state) => state.setMenuList);
|
||||
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
|
||||
const menuList = useTabStore((state) => state.menuList);
|
||||
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => MenuApi.getMenuRoles(appSiteId),
|
||||
enabled: isLoggedIn,
|
||||
|
||||
|
||||
const { syncTabToUrl } = useStatisticsTabUrlSync({
|
||||
|
||||
pathname,
|
||||
|
||||
menuList,
|
||||
|
||||
activeTabId,
|
||||
|
||||
addTab,
|
||||
|
||||
addTabForGrid,
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
|
||||
queryFn: () => MenuApi.getMenuRoles(appSiteId),
|
||||
|
||||
enabled: isLoggedIn,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (menuListData) {
|
||||
|
||||
setMenuList(menuListData);
|
||||
|
||||
setMenuListForGrid(menuListData);
|
||||
|
||||
}
|
||||
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
|
||||
|
||||
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (!menuList) return;
|
||||
|
||||
const appTabs = getTabStoreState().tabs;
|
||||
|
||||
if (appTabs.length === 0) return;
|
||||
|
||||
if (getGridTabStoreState().tabs.length > 0) return;
|
||||
|
||||
|
||||
|
||||
for (const tab of appTabs) {
|
||||
|
||||
getGridTabStoreState().addTab(tab.menuItem);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const activeId = getTabStoreState().activeTabId;
|
||||
|
||||
if (activeId) {
|
||||
|
||||
setActiveTab(activeId);
|
||||
|
||||
const activeTab = appTabs.find((t) => t.id === activeId);
|
||||
|
||||
const gridActiveId = activeTab?.menuItem.menuEngNm;
|
||||
|
||||
if (gridActiveId) {
|
||||
|
||||
getGridTabStoreState().setActiveTab(gridActiveId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, [menuList, tabs, setActiveTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
// pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const tab = currentTabs.find((t) => t.path === pathname);
|
||||
if (tab) {
|
||||
setActiveTab(tab.id);
|
||||
const gridActiveId = tab.menuItem.menuEngNm;
|
||||
if (gridActiveId) {
|
||||
setActiveTabForGrid(gridActiveId);
|
||||
}
|
||||
}
|
||||
}, [pathname, setActiveTab, setActiveTabForGrid, tabs.length, activeTabId]);
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
|
||||
const currentPath = useMemo(() => {
|
||||
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
|
||||
for (const item of items) {
|
||||
|
||||
if (item.urlPath === activeTabId) {
|
||||
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
|
||||
const found = findMenuByEngNm(item.children);
|
||||
|
||||
if (found) return found;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
|
||||
return activeMenu.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
|
||||
if (activeTabId) {
|
||||
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
|
||||
return activeTab.menuItem.urlPath;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
|
||||
return currentMenuInfo.urlPath;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
|
||||
return pathname ?? "";
|
||||
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
|
||||
(item: MenuItem) => {
|
||||
|
||||
setCurrentMenuInfo(item);
|
||||
|
||||
|
||||
|
||||
if (isVerificationSelectionReportMenu(item)) {
|
||||
|
||||
setReportPanelOpen(true);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
|
||||
if (addTabResult) {
|
||||
|
||||
addTabForGrid(item);
|
||||
|
||||
if (item.urlPath) {
|
||||
|
||||
syncTabToUrl(item.urlPath);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!addTabResult) {
|
||||
|
||||
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
|
||||
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
[setCurrentMenuInfo, addTab, addTabForGrid, alert, setReportPanelOpen],
|
||||
|
||||
[
|
||||
|
||||
setCurrentMenuInfo,
|
||||
|
||||
addTab,
|
||||
|
||||
addTabForGrid,
|
||||
|
||||
alert,
|
||||
|
||||
setReportPanelOpen,
|
||||
|
||||
syncTabToUrl,
|
||||
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<BaseLayout
|
||||
|
||||
queryClient={queryClient}
|
||||
|
||||
tokenManager={tokenManager}
|
||||
|
||||
currentPath={currentPath}
|
||||
|
||||
handleMenuClick={handleMenuClick}
|
||||
|
||||
>
|
||||
|
||||
<div className="flex flex-1 h-full">
|
||||
|
||||
{isVerificationSelectionReportPanelOpen && (
|
||||
|
||||
<VerificationSelectionReport
|
||||
|
||||
onClose={() => setReportPanelOpen(false)}
|
||||
|
||||
/>
|
||||
|
||||
)}
|
||||
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
|
||||
</div>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function WithBaseLayout({
|
||||
|
||||
children,
|
||||
|
||||
}: {
|
||||
|
||||
children: React.ReactNode;
|
||||
|
||||
}) {
|
||||
|
||||
return (
|
||||
|
||||
<Suspense fallback={null}>
|
||||
|
||||
<WithBaseLayoutInner>{children}</WithBaseLayoutInner>
|
||||
|
||||
</Suspense>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ interface AddArticleButtonProps {
|
||||
|
||||
const AddArticleButton = ({ addArticleFunc }: AddArticleButtonProps) => {
|
||||
return (
|
||||
<Button className="h-[3.2rem] w-[96px]" onClick={addArticleFunc}>
|
||||
<Button className="h-[3.2rem] w-[96px]" color="white" onClick={addArticleFunc}>
|
||||
<i className="ri-add-fill" />
|
||||
<span>항목 추가</span>
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,7 @@ interface DeleteArticleButtonProps {
|
||||
|
||||
const DeleteArticleButton = ({ deleteArticleFunc }: DeleteArticleButtonProps) => {
|
||||
return (
|
||||
<Button className="h-[3.2rem] w-[96px]" color="red" onClick={deleteArticleFunc}>
|
||||
<Button className="h-[3.2rem] w-[96px]" color="white" onClick={deleteArticleFunc}>
|
||||
<i className="ri-subtract-fill"></i>
|
||||
<span>항목 제거</span>
|
||||
</Button>
|
||||
|
||||
@@ -154,6 +154,24 @@
|
||||
@apply !bg-[#fff7bf];
|
||||
}
|
||||
|
||||
/*
|
||||
* 검증 오류 셀(rg-cell-verify-error / -imr) 최우선 적용.
|
||||
* 클래스 이중화로 specificity를 한 단계 올려 동일 specificity의 다른 셀 클래스
|
||||
* (rg-cell-rowtype-*, rg-cell-total, rg-cell-readonly 등)보다 항상 이김.
|
||||
*/
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error {
|
||||
@apply !bg-[#ffe6e6] !text-[#c00];
|
||||
}
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
|
||||
@apply !bg-[#fff7bf] !text-[#b45309];
|
||||
}
|
||||
|
||||
/* 진행상황: 소프트 인라인 요약 (툴바와 자연스럽게 blend) */
|
||||
.data-grid-status-bubble {
|
||||
@apply rounded-lg border border-slate-200/80 bg-white px-8 py-2.5;
|
||||
@@ -336,3 +354,35 @@
|
||||
background-color: #6bafae !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error {
|
||||
background-color: #ffe6e6 !important;
|
||||
color: #c00 !important;
|
||||
}
|
||||
.rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
.rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
|
||||
background-color: #fff7bf !important;
|
||||
color: #b45309 !important;
|
||||
}
|
||||
|
||||
/* dark theme에서도 검증 오류 색을 유지하고 싶다면 동일 색 유지(가독성 우선) */
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
[data-theme="black"] .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error,
|
||||
[data-theme="black"] .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error.rg-cell-verify-error {
|
||||
background-color: #ffe6e6 !important;
|
||||
color: #c00 !important;
|
||||
}
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
[data-theme="black"] .rg-grid .rg-fixed-body .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr,
|
||||
[data-theme="black"] .rg-grid .rg-fixed-body .rg-data-row .rg-data-cell.rg-cell-verify-error-imr.rg-cell-verify-error-imr {
|
||||
background-color: #fff7bf !important;
|
||||
color: #b45309 !important;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,142 @@ if (typeof window !== "undefined") {
|
||||
|
||||
RealGrid.setLicenseKey(process.env.NEXT_PUBLIC_ST_REALGRID_KEY || "");
|
||||
|
||||
function isDropDownEditorColumn(column: {
|
||||
editor?: { type?: string };
|
||||
values?: string[];
|
||||
}): boolean {
|
||||
const type = column.editor?.type;
|
||||
return (
|
||||
type === "dropdown" ||
|
||||
type === "list" ||
|
||||
type === "dropDown" ||
|
||||
(column.values != null && column.values.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
type GridViewWithSetColumnProperty = {
|
||||
setColumnProperty?: (col: string, key: string, value: unknown) => void;
|
||||
};
|
||||
|
||||
type GridViewWithEditOptions = {
|
||||
setEditOptions?: (options: { commitWhenLeave?: boolean }) => void;
|
||||
};
|
||||
|
||||
/** 포커스가 그리드 밖으로 나갈 때 편집 중인 값을 자동 commit */
|
||||
function applyDefaultGridEditOptions(
|
||||
gridView: GridViewWithEditOptions | null | undefined,
|
||||
) {
|
||||
gridView?.setEditOptions?.({ commitWhenLeave: true });
|
||||
}
|
||||
|
||||
function isDateEditorColumn(column: {
|
||||
editor?: { type?: string };
|
||||
}): boolean {
|
||||
return column.editor?.type === "date";
|
||||
}
|
||||
|
||||
/** RealGrid 날짜(date) 편집기 컬럼 속성 적용 */
|
||||
function applyColumnDateEditorProperties(
|
||||
gridView: GridViewWithSetColumnProperty,
|
||||
column: import("../type/DataGrid.type").ColumnConfig,
|
||||
) {
|
||||
if (!gridView.setColumnProperty) return;
|
||||
const fmt =
|
||||
column.datetimeFormat ??
|
||||
column.editor?.datetimeFormat ??
|
||||
"yyyy-MM-dd";
|
||||
gridView.setColumnProperty(column.name, "datetimeFormat", fmt);
|
||||
gridView.setColumnProperty(column.name, "editor", {
|
||||
type: "date",
|
||||
datetimeFormat: fmt,
|
||||
dropDownWhenClick: column.editor?.dropDownWhenClick ?? true,
|
||||
showToday: column.editor?.showToday ?? true,
|
||||
});
|
||||
gridView.setColumnProperty(column.name, "editButtonVisibility", "always");
|
||||
}
|
||||
|
||||
/** RealGrid 드롭다운·날짜 등 컬럼 편집기 속성 적용 */
|
||||
function applyColumnListEditorProperties(
|
||||
gridView: GridViewWithSetColumnProperty,
|
||||
column: import("../type/DataGrid.type").ColumnConfig,
|
||||
) {
|
||||
if (!gridView.setColumnProperty) return;
|
||||
|
||||
if (isDateEditorColumn(column)) {
|
||||
applyColumnDateEditorProperties(gridView, column);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = column.values ?? column.editor?.values;
|
||||
const labels = column.labels ?? column.editor?.labels ?? values;
|
||||
const isDropDown = isDropDownEditorColumn(column);
|
||||
|
||||
if (!isDropDown) {
|
||||
if (column.editor) {
|
||||
gridView.setColumnProperty(column.name, "editor", column.editor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (values?.length) {
|
||||
gridView.setColumnProperty(column.name, "values", values);
|
||||
gridView.setColumnProperty(column.name, "labels", labels ?? values);
|
||||
}
|
||||
if (column.lookupDisplay) {
|
||||
gridView.setColumnProperty(column.name, "lookupDisplay", true);
|
||||
}
|
||||
gridView.setColumnProperty(column.name, "editor", {
|
||||
type: "dropdown",
|
||||
domainOnly: column.editor?.domainOnly ?? true,
|
||||
textReadOnly: column.editor?.textReadOnly ?? true,
|
||||
commitOnSelect: column.editor?.commitOnSelect ?? true,
|
||||
dropDownWhenClick: column.editor?.dropDownWhenClick ?? true,
|
||||
values,
|
||||
labels: labels ?? values,
|
||||
});
|
||||
gridView.setColumnProperty(column.name, "editButtonVisibility", "always");
|
||||
}
|
||||
|
||||
/** 그리드 마운트·데이터 로드 후 드롭다운 컬럼 재적용용 */
|
||||
export function applyDropDownColumnToGrid(
|
||||
gridView: GridViewWithSetColumnProperty | null | undefined,
|
||||
columnName: string,
|
||||
options: string[],
|
||||
) {
|
||||
if (!gridView?.setColumnProperty || options.length === 0) return;
|
||||
(
|
||||
gridView as {
|
||||
setEditOptions?: (opts: { editable?: boolean }) => void;
|
||||
}
|
||||
).setEditOptions?.({ editable: true });
|
||||
gridView.setColumnProperty(columnName, "editable", true);
|
||||
gridView.setColumnProperty(columnName, "values", options);
|
||||
gridView.setColumnProperty(columnName, "labels", options);
|
||||
gridView.setColumnProperty(columnName, "lookupDisplay", true);
|
||||
gridView.setColumnProperty(columnName, "editor", {
|
||||
type: "dropdown",
|
||||
domainOnly: true,
|
||||
textReadOnly: true,
|
||||
commitOnSelect: true,
|
||||
dropDownWhenClick: true,
|
||||
values: options,
|
||||
labels: options,
|
||||
});
|
||||
gridView.setColumnProperty(columnName, "editButtonVisibility", "always");
|
||||
(gridView as { refresh?: () => void }).refresh?.();
|
||||
}
|
||||
|
||||
function reapplyListEditorsOnGrid(
|
||||
gridView: GridViewWithSetColumnProperty | null | undefined,
|
||||
cols: import("../type/DataGrid.type").ColumnConfig[],
|
||||
) {
|
||||
if (!gridView) return;
|
||||
cols.forEach((col) => applyColumnListEditorProperties(gridView, col));
|
||||
if (cols.some((c) => isDropDownEditorColumn(c) || isDateEditorColumn(c))) {
|
||||
(gridView as { refresh?: () => void }).refresh?.();
|
||||
}
|
||||
}
|
||||
|
||||
type GridViewWithCheckApi = {
|
||||
checkItem?: (itemIndex: number, checked: boolean, exclusive?: boolean) => void;
|
||||
checkRow?: (dataRow: number, checked: boolean, exclusive?: boolean) => void;
|
||||
@@ -325,6 +461,8 @@ function buildFieldsFromColumns(
|
||||
columns: {
|
||||
fieldName: string;
|
||||
valueType?: ValueType;
|
||||
datetimeFormat?: string;
|
||||
editor?: { type?: string; datetimeFormat?: string };
|
||||
computed?: import("../type/DataGrid.type").ComputedFieldConfig;
|
||||
}[],
|
||||
) {
|
||||
@@ -332,6 +470,7 @@ function buildFieldsFromColumns(
|
||||
const field: {
|
||||
fieldName: string;
|
||||
dataType: string;
|
||||
datetimeFormat?: string;
|
||||
valueCallback?: (
|
||||
prod: unknown,
|
||||
dataRow: number,
|
||||
@@ -341,8 +480,19 @@ function buildFieldsFromColumns(
|
||||
) => number;
|
||||
} = {
|
||||
fieldName: col.fieldName,
|
||||
dataType: col.valueType === ValueType.NUMBER ? "number" : "text",
|
||||
dataType:
|
||||
col.editor?.type === "date"
|
||||
? "datetime"
|
||||
: col.valueType === ValueType.NUMBER
|
||||
? "number"
|
||||
: "text",
|
||||
};
|
||||
if (col.editor?.type === "date") {
|
||||
field.datetimeFormat =
|
||||
col.datetimeFormat ??
|
||||
col.editor.datetimeFormat ??
|
||||
"yyyy-MM-dd";
|
||||
}
|
||||
if (col.computed) {
|
||||
const hasFields =
|
||||
"fields" in col.computed && col.computed.fields.length > 0;
|
||||
@@ -844,6 +994,7 @@ const DataGrid = ({
|
||||
|
||||
const gridView = gridRef.current.gridView;
|
||||
const dataProvider = gridRef.current.dataProvider;
|
||||
applyDefaultGridEditOptions(gridView as GridViewWithEditOptions);
|
||||
|
||||
// mergeCells: true인 컬럼이 있으면 onSorting에서 클라이언트 정렬로 처리
|
||||
const hasMergeCells = columns.some((col) => col.mergeCells === true);
|
||||
@@ -894,15 +1045,8 @@ const DataGrid = ({
|
||||
(gridView.header as { showTooltip?: boolean }).showTooltip = true;
|
||||
}
|
||||
columns.forEach((column) => {
|
||||
(
|
||||
gridView as {
|
||||
setColumnProperty?: (
|
||||
col: string,
|
||||
key: string,
|
||||
value: unknown,
|
||||
) => void;
|
||||
}
|
||||
).setColumnProperty?.(
|
||||
const gv = gridView as GridViewWithSetColumnProperty;
|
||||
gv.setColumnProperty?.(
|
||||
column.name,
|
||||
"header",
|
||||
realGridHeaderWithTooltip({
|
||||
@@ -910,7 +1054,11 @@ const DataGrid = ({
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
applyColumnListEditorProperties(gv, column);
|
||||
});
|
||||
if (columns.some((c) => isDropDownEditorColumn(c))) {
|
||||
(gridView as { refresh?: () => void }).refresh?.();
|
||||
}
|
||||
(
|
||||
gridView as {
|
||||
onShowHeaderTooltip?: (
|
||||
@@ -1095,6 +1243,12 @@ const DataGrid = ({
|
||||
(currentGridView as GridViewWithSetColumns).setColumns?.(
|
||||
columnsWithFooter,
|
||||
);
|
||||
columns.forEach((col) =>
|
||||
applyColumnListEditorProperties(
|
||||
currentGridView as GridViewWithSetColumnProperty,
|
||||
col,
|
||||
),
|
||||
);
|
||||
applyFooterSettings(currentGridView);
|
||||
if (hasFooterUserSpans) {
|
||||
currentGridView.setColumnLayout?.(columnLayout);
|
||||
@@ -1499,6 +1653,12 @@ const DataGrid = ({
|
||||
autoGenerateField={true}
|
||||
layout={columnLayout}
|
||||
rows={rowData}
|
||||
onRGDataColumnsChanged={(grid) => {
|
||||
reapplyListEditorsOnGrid(
|
||||
grid as GridViewWithSetColumnProperty,
|
||||
columnsRef.current,
|
||||
);
|
||||
}}
|
||||
onInitialized={(grid) => {
|
||||
if (!grid) return;
|
||||
(
|
||||
@@ -1538,6 +1698,9 @@ const DataGrid = ({
|
||||
dataProvider.setRows(rowData);
|
||||
if (getGridRef) getGridRef.current = gridRef.current;
|
||||
const gridView = gridRef.current?.gridView ?? grid;
|
||||
applyDefaultGridEditOptions(
|
||||
gridView as GridViewWithEditOptions,
|
||||
);
|
||||
if (!gridView?.columnByName) return;
|
||||
setTimeout(() => {
|
||||
const gv = gridRef.current?.gridView;
|
||||
@@ -1653,6 +1816,10 @@ const DataGrid = ({
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
}),
|
||||
);
|
||||
applyColumnListEditorProperties(
|
||||
gv as GridViewWithSetColumnProperty,
|
||||
column,
|
||||
);
|
||||
if (cellTooltipCallbackRef.current) {
|
||||
(
|
||||
gv as {
|
||||
@@ -1707,9 +1874,15 @@ const DataGrid = ({
|
||||
{columns.map((column) => {
|
||||
const baseStyleName = column.styleName;
|
||||
|
||||
const columnReactKey =
|
||||
column.values?.length &&
|
||||
isDropDownEditorColumn(column)
|
||||
? `${column.name}-dropdown-${column.values.length}`
|
||||
: column.name;
|
||||
|
||||
return (
|
||||
<RGDataColumn
|
||||
key={column.name}
|
||||
key={columnReactKey}
|
||||
name={column.name}
|
||||
fieldName={column.fieldName}
|
||||
valueType={
|
||||
@@ -1727,6 +1900,32 @@ const DataGrid = ({
|
||||
tooltip: resolveColumnTooltip(column),
|
||||
})}
|
||||
editable={column.editable}
|
||||
{...(column.editor != null && {
|
||||
editor: column.editor,
|
||||
})}
|
||||
{...(column.values != null && {
|
||||
values: column.values,
|
||||
})}
|
||||
{...(column.labels != null && {
|
||||
labels: column.labels,
|
||||
})}
|
||||
{...(column.lookupDisplay === true && {
|
||||
lookupDisplay: true,
|
||||
})}
|
||||
{...(isDateEditorColumn(column) && {
|
||||
field: {
|
||||
fieldName: column.fieldName,
|
||||
dataType: ValueType.DATETIME,
|
||||
datetimeFormat:
|
||||
column.datetimeFormat ??
|
||||
column.editor?.datetimeFormat ??
|
||||
"yyyy-MM-dd",
|
||||
},
|
||||
datetimeFormat:
|
||||
column.datetimeFormat ??
|
||||
column.editor?.datetimeFormat ??
|
||||
"yyyy-MM-dd",
|
||||
})}
|
||||
{...(column.width != null && {
|
||||
width: column.width,
|
||||
})}
|
||||
|
||||
@@ -47,6 +47,47 @@ const getSggListFetcher = async (
|
||||
});
|
||||
};
|
||||
|
||||
function buildSearchFormValues(
|
||||
props: SearchFormProps,
|
||||
sidoList: ApiResponse<SearchCondition> | undefined,
|
||||
defaultYear: number,
|
||||
): SearchFormValues {
|
||||
const d = props.defaultValues;
|
||||
const initialClsfCd: SearchFormClsfCd =
|
||||
d?.clsfCd === "ALL" || d?.clsfCd === "INPT" || d?.clsfCd === "VRFC"
|
||||
? d.clsfCd
|
||||
: "ALL";
|
||||
|
||||
if (d?.sido) {
|
||||
return {
|
||||
sido: d.sido,
|
||||
gugun: d.gugun || "",
|
||||
year: d.year || defaultYear.toString(),
|
||||
order: d.order || "",
|
||||
fctName: d.fctName || "",
|
||||
bsnmCd: d.bsnmCd || "",
|
||||
isDetail: false,
|
||||
isSummary: d.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
};
|
||||
}
|
||||
|
||||
const first = sidoList?.data?.sidoList?.[0];
|
||||
const useFirstSido = (props.sido ?? defaultSearchFormProps.sido) && first;
|
||||
|
||||
return {
|
||||
sido: useFirstSido ? first.sido : "",
|
||||
gugun: "",
|
||||
year: d?.year || defaultYear.toString(),
|
||||
order: d?.order || "",
|
||||
fctName: d?.fctName || "",
|
||||
bsnmCd: useFirstSido ? first.bsnmCd : "",
|
||||
isDetail: false,
|
||||
isSummary: d?.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
};
|
||||
}
|
||||
|
||||
const SearchForm = (props: SearchFormProps = {}) => {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const userInfo = useUserStore((state) => state.userInfo);
|
||||
@@ -82,23 +123,27 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
},
|
||||
);
|
||||
|
||||
const initialClsfCd: SearchFormClsfCd = (() => {
|
||||
const d = props.defaultValues?.clsfCd;
|
||||
if (d === "ALL" || d === "INPT" || d === "VRFC") return d;
|
||||
return "ALL";
|
||||
})();
|
||||
// 마운트 시 SWR 캐시의 시도 목록으로 초기값 설정 (탭 닫았다 열 때 API 재호출 없이 동작)
|
||||
const [values, setValues] = useState<SearchFormValues>(() =>
|
||||
buildSearchFormValues(props, sidoList, defaultYear),
|
||||
);
|
||||
|
||||
const [values, setValues] = useState<SearchFormValues>({
|
||||
sido: props.defaultValues?.sido || "",
|
||||
gugun: props.defaultValues?.gugun || "",
|
||||
year: props.defaultValues?.year || defaultYear.toString(),
|
||||
order: props.defaultValues?.order || "",
|
||||
fctName: props.defaultValues?.fctName || "",
|
||||
bsnmCd: props.defaultValues?.bsnmCd || "",
|
||||
isDetail: false,
|
||||
isSummary: props.defaultValues?.isSummary || "",
|
||||
clsfCd: initialClsfCd,
|
||||
});
|
||||
// 최초 진입만 시도 API가 늦게 오는 경우
|
||||
useEffect(() => {
|
||||
if (props.defaultValues?.sido) return;
|
||||
const first = sidoList?.data?.sidoList?.[0];
|
||||
if (!first || !mergedProps.sido) return;
|
||||
setValues((prev) =>
|
||||
prev.sido
|
||||
? prev
|
||||
: {
|
||||
...prev,
|
||||
sido: first.sido,
|
||||
gugun: "",
|
||||
bsnmCd: first.bsnmCd,
|
||||
},
|
||||
);
|
||||
}, [sidoList, props.defaultValues?.sido, mergedProps.sido]);
|
||||
|
||||
const sggKey: [string, string, string, string] | null =
|
||||
values.sido && values.sido !== "ALL"
|
||||
@@ -133,18 +178,7 @@ const SearchForm = (props: SearchFormProps = {}) => {
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const resetValues: SearchFormValues = {
|
||||
sido: "",
|
||||
gugun: "",
|
||||
year: String(defaultYear),
|
||||
order: "",
|
||||
fctName: "",
|
||||
bsnmCd: "",
|
||||
isDetail: false,
|
||||
isSummary: "",
|
||||
clsfCd: "ALL",
|
||||
};
|
||||
setValues(resetValues);
|
||||
setValues(buildSearchFormValues(props, sidoList, defaultYear));
|
||||
props.onReset?.();
|
||||
};
|
||||
|
||||
|
||||
@@ -143,7 +143,6 @@ const verificationResultColumnInfo: ColumnInfo = {
|
||||
{ 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: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false },
|
||||
@@ -157,7 +156,6 @@ const verificationResultColumnInfo: ColumnInfo = {
|
||||
"nowVlCn",
|
||||
"compVlCn",
|
||||
"prcsYnNm",
|
||||
"pbcpAprvYn",
|
||||
"regDt",
|
||||
"mdfcnDt",
|
||||
"prcsYn",
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import {
|
||||
type InfiniteData,
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useTabStore as useGridTabStore } from "@rsone/components";
|
||||
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
|
||||
import type {
|
||||
VerificationResultListRequest,
|
||||
VerificationResultListResponse,
|
||||
VerificationResultReportItem,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import {
|
||||
findMenuItemByUrlPath,
|
||||
@@ -23,7 +32,9 @@ import {
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
Label,
|
||||
} from "@common/components";
|
||||
import { CodeSelectWithAll } from "@/entities/code";
|
||||
|
||||
type VerificationReportTabKey = "NV" | "IMR" | "HB";
|
||||
|
||||
@@ -45,6 +56,64 @@ const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
HB: "HB",
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
const SCROLL_LOAD_THRESHOLD_PX = 120;
|
||||
|
||||
function getNextVerificationResultPage(
|
||||
lastPage: VerificationResultListResponse,
|
||||
): number | undefined {
|
||||
const pageInfo = lastPage?.data?.pageInfo;
|
||||
if (!pageInfo) return undefined;
|
||||
const { currentPage, totalPages } = pageInfo;
|
||||
if (currentPage + 1 <= totalPages) return currentPage + 1;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function flattenVerificationResults(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
): VerificationResultReportItem[] {
|
||||
return (
|
||||
data?.pages.flatMap((page) => page?.data?.verifyResultList ?? []) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function getVerificationResultTotalCount(
|
||||
data: InfiniteData<VerificationResultListResponse> | undefined,
|
||||
loadedCount: number,
|
||||
): number {
|
||||
return data?.pages[0]?.data?.pageInfo?.totalElements ?? loadedCount;
|
||||
}
|
||||
|
||||
function useVerificationReportList(
|
||||
statsYr: string,
|
||||
statsCdQueryKey: string,
|
||||
vrfcMthdCd: VerificationReportTabKey,
|
||||
statsCd: string,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
vrfcMthdCd,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
initialPageParam: 1,
|
||||
queryFn: ({ pageParam }) =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
statsYr,
|
||||
pbcpAprvYn: "N",
|
||||
page: pageParam,
|
||||
size: PAGE_SIZE,
|
||||
prcsYnList: "0",
|
||||
vrfcMthdCd,
|
||||
...(!(statsCd === "" || statsCd === "ALL") && { statsCd }),
|
||||
} satisfies VerificationResultListRequest),
|
||||
getNextPageParam: getNextVerificationResultPage,
|
||||
placeholderData: keepPreviousData,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
|
||||
type VerificationSelectionReportProps = {
|
||||
onClose?: () => void;
|
||||
};
|
||||
@@ -52,6 +121,7 @@ type VerificationSelectionReportProps = {
|
||||
const VerificationSelectionReport = ({
|
||||
onClose,
|
||||
}: VerificationSelectionReportProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const menuList = useTabStore((s) => s.menuList);
|
||||
const addTab = useTabStore((s) => s.addTab);
|
||||
const setCurrentMenuInfo = useMenuStore((s) => s.setCurrentMenuInfo);
|
||||
@@ -61,78 +131,71 @@ const VerificationSelectionReport = ({
|
||||
);
|
||||
const alert = useAlertStore((s) => s.setMessage);
|
||||
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
|
||||
const [statsCd, setStatsCd] = useState<string>("ALL");
|
||||
|
||||
const statsYr = String(dayjs().year() - 1);
|
||||
const statsCdQueryKey =
|
||||
statsCd === "" || statsCd === "ALL" ? "" : statsCd;
|
||||
|
||||
const baseRequest = {
|
||||
const nvQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
pbcpAprvYn: "N" as const,
|
||||
page: 1,
|
||||
size: 10000,
|
||||
prcsYnList: "0",
|
||||
};
|
||||
statsCdQueryKey,
|
||||
"NV",
|
||||
statsCd,
|
||||
);
|
||||
const imrQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"IMR",
|
||||
statsCd,
|
||||
);
|
||||
const hbQuery = useVerificationReportList(
|
||||
statsYr,
|
||||
statsCdQueryKey,
|
||||
"HB",
|
||||
statsCd,
|
||||
);
|
||||
|
||||
const [nvQuery, imrQuery, hbQuery] = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
"NV",
|
||||
baseRequest.pbcpAprvYn,
|
||||
baseRequest.page,
|
||||
baseRequest.size,
|
||||
baseRequest.prcsYnList,
|
||||
],
|
||||
queryFn: () =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
...baseRequest,
|
||||
vrfcMthdCd: "NV",
|
||||
}),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
"IMR",
|
||||
baseRequest.pbcpAprvYn,
|
||||
baseRequest.page,
|
||||
baseRequest.size,
|
||||
baseRequest.prcsYnList,
|
||||
],
|
||||
queryFn: () =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
...baseRequest,
|
||||
vrfcMthdCd: "IMR",
|
||||
}),
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
"HB",
|
||||
baseRequest.pbcpAprvYn,
|
||||
baseRequest.page,
|
||||
baseRequest.size,
|
||||
baseRequest.prcsYnList,
|
||||
],
|
||||
queryFn: () =>
|
||||
statisticsApi.getVerificationResultList({
|
||||
...baseRequest,
|
||||
vrfcMthdCd: "HB",
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
const queryByTab = {
|
||||
NV: nvQuery,
|
||||
IMR: imrQuery,
|
||||
HB: hbQuery,
|
||||
} as const;
|
||||
|
||||
const isLoading =
|
||||
nvQuery.isLoading || imrQuery.isLoading || hbQuery.isLoading;
|
||||
const isInitialLoading =
|
||||
nvQuery.isPending || imrQuery.isPending || hbQuery.isPending;
|
||||
const isRefreshing =
|
||||
(nvQuery.isFetching && !nvQuery.isPending) ||
|
||||
(imrQuery.isFetching && !imrQuery.isPending) ||
|
||||
(hbQuery.isFetching && !hbQuery.isPending);
|
||||
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
const tabCodes: VerificationReportTabKey[] = ["NV", "IMR", "HB"];
|
||||
void Promise.all(
|
||||
tabCodes.map((code) =>
|
||||
queryClient.resetQueries({
|
||||
queryKey: [
|
||||
"verificationResultList",
|
||||
statsYr,
|
||||
code,
|
||||
statsCdQueryKey,
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
}, [queryClient, statsYr, statsCdQueryKey]);
|
||||
|
||||
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
|
||||
NV: nvQuery.data?.data?.verifyResultList ?? [],
|
||||
IMR: imrQuery.data?.data?.verifyResultList ?? [],
|
||||
HB: hbQuery.data?.data?.verifyResultList ?? [],
|
||||
NV: flattenVerificationResults(nvQuery.data),
|
||||
IMR: flattenVerificationResults(imrQuery.data),
|
||||
HB: flattenVerificationResults(hbQuery.data),
|
||||
};
|
||||
|
||||
const totalCountByTab: Record<VerificationReportTabKey, number> = {
|
||||
NV: getVerificationResultTotalCount(nvQuery.data, listsByTab.NV.length),
|
||||
IMR: getVerificationResultTotalCount(imrQuery.data, listsByTab.IMR.length),
|
||||
HB: getVerificationResultTotalCount(hbQuery.data, listsByTab.HB.length),
|
||||
};
|
||||
|
||||
const openInputFromStatsCd = useCallback(
|
||||
@@ -182,12 +245,43 @@ const VerificationSelectionReport = ({
|
||||
],
|
||||
);
|
||||
|
||||
const handleListScroll = useCallback(
|
||||
(
|
||||
e: React.UIEvent<HTMLDivElement>,
|
||||
hasNextPage: boolean,
|
||||
isFetchingNextPage: boolean,
|
||||
fetchNextPage: () => void,
|
||||
) => {
|
||||
const el = e.currentTarget;
|
||||
const remaining =
|
||||
el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
if (remaining > SCROLL_LOAD_THRESHOLD_PX) return;
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const renderResultList = (
|
||||
typeCode: VerificationReportTabKey,
|
||||
items: VerificationResultReportItem[],
|
||||
hasNextPage: boolean,
|
||||
isFetchingNextPage: boolean,
|
||||
fetchNextPage: () => void,
|
||||
) => (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
{items.length === 0 ? (
|
||||
<div
|
||||
className="min-h-0 flex-1 overflow-y-auto p-2"
|
||||
onScroll={(e) =>
|
||||
handleListScroll(
|
||||
e,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
)
|
||||
}
|
||||
>
|
||||
{items.length === 0 && !isFetchingNextPage ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
|
||||
항목이 없습니다.
|
||||
</div>
|
||||
@@ -234,6 +328,13 @@ const VerificationSelectionReport = ({
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isFetchingNextPage && (
|
||||
<div className="flex justify-center py-3">
|
||||
<div className="relative size-[3rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -241,9 +342,26 @@ const VerificationSelectionReport = ({
|
||||
|
||||
return (
|
||||
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
|
||||
<div className="card card-border flex-1 bg-background !px-0">
|
||||
<div className="card card-border flex-1 bg-background !px-0 max-h-[1000px] flex flex-col">
|
||||
<div className="border-b border-border flex justify-between">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">이상치 선별 보고서</h2>
|
||||
<div className="flex justify-center items-center text-[2.4rem] ">
|
||||
<h2 className="font-semibold text-title p-4 pr-1">이상치 선별 보고서</h2>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="새로고침"
|
||||
className={`text-gray-400 cursor-pointer hover:text-[#2094fa] ${isRefreshing ? "pointer-events-none opacity-50" : ""}`}
|
||||
onClick={handleRefresh}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleRefresh();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="diveicon di-arrow-reflash" />
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -260,8 +378,18 @@ const VerificationSelectionReport = ({
|
||||
<i className="ri-close-line" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] p-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 pt-2.5 px-4">
|
||||
<Label className="text-label shrink-0">통계항목</Label>
|
||||
<CodeSelectWithAll
|
||||
upCd="STITEM"
|
||||
value={statsCd}
|
||||
onValueChange={setStatsCd}
|
||||
placeholder="전체"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body p-2.5 flex-1 flex flex-col overflow-hidden relative min-h-0">
|
||||
{isInitialLoading && (
|
||||
<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" />
|
||||
@@ -273,24 +401,24 @@ const VerificationSelectionReport = ({
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && (
|
||||
<section className="flex min-h-0 flex-1 flex-col h-[85rem]">
|
||||
{!isInitialLoading && !error && (
|
||||
<section className="flex min-h-0 flex-1 flex-col">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) =>
|
||||
setActiveTab(value as VerificationReportTabKey)
|
||||
}
|
||||
className="flex min-h-0 flex-1 flex-col h-full"
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
variant="default"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3 p-1">
|
||||
<TabsList className="grid w-full grid-cols-3 p-1 shrink-0">
|
||||
{VERIFICATION_REPORT_TABS.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="!px-2 !py-2 text-[1.3rem] whitespace-normal"
|
||||
>
|
||||
{tab.label} ({listsByTab[tab.value].length})
|
||||
{tab.label} ({totalCountByTab[tab.value]})
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
@@ -304,6 +432,12 @@ const VerificationSelectionReport = ({
|
||||
? renderResultList(
|
||||
tab.value,
|
||||
listsByTab[tab.value],
|
||||
queryByTab[tab.value]
|
||||
.hasNextPage ?? false,
|
||||
queryByTab[tab.value]
|
||||
.isFetchingNextPage,
|
||||
queryByTab[tab.value]
|
||||
.fetchNextPage,
|
||||
)
|
||||
: null}
|
||||
</TabsContent>
|
||||
|
||||
@@ -165,7 +165,7 @@ const PublicSewageFacilityInp = () => {
|
||||
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 [_toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<Map<string, TabToolbarSnapshot>>(() => new Map());
|
||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||
const [periodCheckList, setPeriodCheckList] = useState<CheckInputPeriodItem[]>([]);
|
||||
const [showDailog, setShowDailog] = useState<boolean>(false);
|
||||
@@ -180,7 +180,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') {
|
||||
@@ -191,9 +191,10 @@ const PublicSewageFacilityInp = () => {
|
||||
alert('먼저 조회를 진행해주세요.');
|
||||
return;
|
||||
}
|
||||
const treatAsNoApplicable = getNoApplicableForRequest() || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!currentData || currentData.length === 0) {
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
@@ -270,9 +271,12 @@ const PublicSewageFacilityInp = () => {
|
||||
alert('먼저 조회를 진행해주세요.');
|
||||
return;
|
||||
}
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!currentData || currentData.length === 0) {
|
||||
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('제출할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +284,7 @@ const PublicSewageFacilityInp = () => {
|
||||
try {
|
||||
showGlobalProgress({ label: '서버에 요청 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const submitList = buildFacilitySaveList(currentData);
|
||||
const submitList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const statsCdForTab = tabStatsCdMap.get(activeTab) ?? activeTab;
|
||||
const requestData = {
|
||||
@@ -289,7 +293,7 @@ const PublicSewageFacilityInp = () => {
|
||||
sggNm: lastSearchedValues.gugun === 'ALL' ? '' : lastSearchedValues.gugun ?? '',
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
noApplicable: getNoApplicableForRequest(),
|
||||
noApplicable,
|
||||
laePicNm: submitInfo.laePicNm,
|
||||
laeKbrdrJbgdNm: submitInfo.laeKbrdrJbgdNm,
|
||||
laeKbrdrDeptNm: submitInfo.laeKbrdrDeptNm,
|
||||
@@ -408,6 +412,12 @@ const PublicSewageFacilityInp = () => {
|
||||
|
||||
const saveFunc = () => {
|
||||
void (async () => {
|
||||
const verificationBlocked = getVerificationPeriodActionBlockedMessage(periodCheckList);
|
||||
if (verificationBlocked) {
|
||||
alert(verificationBlocked);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lastSearchedValues || !searchParamRef.current) {
|
||||
alert('먼저 조회를 진행해주세요.');
|
||||
return;
|
||||
@@ -419,13 +429,13 @@ const PublicSewageFacilityInp = () => {
|
||||
statsCd: 'ST00060001',
|
||||
statsYr: lastSearchedValues.year ?? ''
|
||||
});
|
||||
|
||||
|
||||
if (statusCodeResponse.status === 'success') {
|
||||
if (!['0', '1', '4'].includes(statusCodeResponse.data)) {
|
||||
const codeName = statusCodeResponse.data === '2' ? '제출' : (statusCodeResponse.data === '3' ? '승인' : '검증중');
|
||||
alert(`공공 하수처리 시설 데이터가 ${codeName} 상태일 때는 중심 처리 시설 데이터를 변경할 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert('오류가 발생했습니다.\n관리자에게 문의하세요')
|
||||
}
|
||||
@@ -508,14 +518,16 @@ const PublicSewageFacilityInp = () => {
|
||||
try {
|
||||
showGlobalProgress({ label: '저장 진행 중...', progress: 0 });
|
||||
const userInfo = await getUserInfo();
|
||||
const noApplicable = getNoApplicableForRequest();
|
||||
const treatAsNoApplicable = noApplicable || isNotApplicable;
|
||||
const currentData =
|
||||
st60001GridGetRowsRef.current?.() ?? rowDataMap.get('ST00060001') ?? [];
|
||||
if (!currentData || currentData.length === 0) {
|
||||
if (!treatAsNoApplicable && (!currentData || currentData.length === 0)) {
|
||||
alert('저장할 데이터가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const saveList = buildFacilitySaveList(currentData);
|
||||
const saveList = treatAsNoApplicable ? [] : buildFacilitySaveList(currentData);
|
||||
const param: PublicSewageFacilitySaveRequest = {
|
||||
statsYr: lastSearchedValues.year ?? '',
|
||||
ctpvNm: lastSearchedValues.sido ?? '',
|
||||
@@ -523,7 +535,7 @@ const PublicSewageFacilityInp = () => {
|
||||
sewerBzmnCd: lastSearchedValues.bsnmCd ?? '',
|
||||
userId: userInfo.userUnqId,
|
||||
// superAdmin: isAdminInfo ? 'superAdmin' : '',
|
||||
noApplicable: getNoApplicableForRequest(),
|
||||
noApplicable,
|
||||
saveList,
|
||||
};
|
||||
|
||||
@@ -643,6 +655,8 @@ const PublicSewageFacilityInp = () => {
|
||||
param: PublicSewageFacilityRequestParams,
|
||||
searchContext: SearchFormValues | null,
|
||||
): Promise<boolean> => {
|
||||
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
|
||||
setIsNotApplicable(false);
|
||||
try {
|
||||
if (tabKey === 'ST00060001') {
|
||||
const response = await statisticsInputPublicSewageFacilityApiService.getPublicSewageFacility(param);
|
||||
@@ -1062,7 +1076,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]">
|
||||
{!isMainFacilityTab && (
|
||||
<VerificationResultButton
|
||||
@@ -1072,8 +1086,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' && (
|
||||
@@ -1088,7 +1102,7 @@ const PublicSewageFacilityInp = () => {
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs value={activeTab} onValueChange={(v) => void handleActiveTabChange(v)}>
|
||||
<TabsList>
|
||||
@@ -1099,165 +1113,165 @@ const PublicSewageFacilityInp = () => {
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{PublicSewageFactInpColumnList.map((tab) => {
|
||||
if (tab.isSplitGrid && tab.left && tab.right) {
|
||||
const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`;
|
||||
const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`;
|
||||
{PublicSewageFactInpColumnList.map((tab) => {
|
||||
if (tab.isSplitGrid && tab.left && tab.right) {
|
||||
const leftKey = tab.left.tabKey ?? `${tab.tabKey}_left`;
|
||||
const rightKey = tab.right.tabKey ?? `${tab.tabKey}_right`;
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)}
|
||||
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)}
|
||||
leftFixedColumns={tab.left.fixedColumns}
|
||||
rightFixedColumns={tab.right.fixedColumns}
|
||||
showHeaderCheckbox={true}
|
||||
enableSort={false}
|
||||
leftRowData={rowDataMap.get(leftKey) ?? []}
|
||||
rightRowData={rowDataMap.get(rightKey) ?? []}
|
||||
leftOnRowClick={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (row) => {
|
||||
void handleMainFacilityRowClick(row);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
leftCellStyleCallback={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? leftMainFacilityCellStyleCallback
|
||||
: undefined
|
||||
}
|
||||
rightOnItemChecked={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (itemIndex, checked) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const rows = [...(next.get('MainFacilityTarget') ?? [])];
|
||||
if (!rows[itemIndex]) return prev;
|
||||
rows[itemIndex] = {
|
||||
...rows[itemIndex],
|
||||
__checked: checked ? 'Y' : '',
|
||||
};
|
||||
next.set('MainFacilityTarget', rows);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rightOnItemAllChecked={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (checked) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({
|
||||
...r,
|
||||
__checked: checked ? 'Y' : '',
|
||||
}));
|
||||
next.set('MainFacilityTarget', rows);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
setLeftRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(leftKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(leftKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
setRightRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(rightKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(rightKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
}
|
||||
|
||||
const gridColumnInfo = getGridColumnInfo(tab);
|
||||
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null;
|
||||
const tabEnterableForGrid = computeTabEnterable(
|
||||
tabStatus,
|
||||
inputPeriodEnterable,
|
||||
);
|
||||
const excelToolbarEnabledForTab =
|
||||
tabRowDataExists(tab.tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOkOrNotDoList;
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<SplitDataGrid
|
||||
leftColumnInfo={getGridColumnInfo(tab.left, tab.tabKey)}
|
||||
rightColumnInfo={getGridColumnInfo(tab.right, tab.tabKey)}
|
||||
leftFixedColumns={tab.left.fixedColumns}
|
||||
rightFixedColumns={tab.right.fixedColumns}
|
||||
showHeaderCheckbox={true}
|
||||
enableSort={false}
|
||||
leftRowData={rowDataMap.get(leftKey) ?? []}
|
||||
rightRowData={rowDataMap.get(rightKey) ?? []}
|
||||
leftOnRowClick={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (row) => {
|
||||
void handleMainFacilityRowClick(row);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
leftCellStyleCallback={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? leftMainFacilityCellStyleCallback
|
||||
: undefined
|
||||
}
|
||||
rightOnItemChecked={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (itemIndex, checked) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const rows = [...(next.get('MainFacilityTarget') ?? [])];
|
||||
if (!rows[itemIndex]) return prev;
|
||||
rows[itemIndex] = {
|
||||
...rows[itemIndex],
|
||||
__checked: checked ? 'Y' : '',
|
||||
};
|
||||
next.set('MainFacilityTarget', rows);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rightOnItemAllChecked={
|
||||
tab.tabKey === 'ST00060002'
|
||||
? (checked) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const rows = [...(next.get('MainFacilityTarget') ?? [])].map((r) => ({
|
||||
...r,
|
||||
__checked: checked ? 'Y' : '',
|
||||
}));
|
||||
next.set('MainFacilityTarget', rows);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
setLeftRowData={(updater) => {
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
|
||||
fixedColumns={tab.fixedColumns}
|
||||
statusInfo={tabStatus}
|
||||
rowData={rowDataMap.get(tab.tabKey) ?? []}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(leftKey) ?? [];
|
||||
const cur = next.get(tab.tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(leftKey, n);
|
||||
next.set(tab.tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
setRightRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(rightKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(rightKey, n);
|
||||
return next;
|
||||
});
|
||||
getGridDataRef={
|
||||
tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined
|
||||
}
|
||||
isExcelImport={excelToolbarEnabledForTab}
|
||||
isExcelExport
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const gridColumnInfo = getGridColumnInfo(tab);
|
||||
const tabStatus = tabStatusInfoMap.get(tab.tabKey) ?? null;
|
||||
const tabEnterableForGrid = computeTabEnterable(
|
||||
tabStatus,
|
||||
inputPeriodEnterable,
|
||||
);
|
||||
const excelToolbarEnabledForTab =
|
||||
tabRowDataExists(tab.tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOkOrNotDoList;
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.tabKey} value={tab.tabKey}>
|
||||
<DataGrid
|
||||
key={(tabEnterableForGrid || isAdminInfo) ? 'enterable' : 'readonly'}
|
||||
columnInfo={gridColumnInfo}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
excelExportFileSuffix={tab.tabLabel || tab.tabKey}
|
||||
fixedColumns={tab.fixedColumns}
|
||||
statusInfo={tabStatus}
|
||||
rowData={rowDataMap.get(tab.tabKey) ?? []}
|
||||
setRowData={(updater) => {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const cur = next.get(tab.tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === 'function'
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
: updater;
|
||||
next.set(tab.tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
getGridDataRef={
|
||||
tab.tabKey === 'ST00060001' ? st60001GridGetRowsRef : undefined
|
||||
}
|
||||
isExcelImport={excelToolbarEnabledForTab}
|
||||
isExcelExport
|
||||
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>
|
||||
);
|
||||
})}
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
<VerificationResultHistoryDialog
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,58 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
import { ValueType } from "realgrid";
|
||||
import type { RealGridReact } from "realgrid-react";
|
||||
import { LoadingWithAnimation, Tabs, TabsContent, TabsList, TabsTrigger } from "@common/components";
|
||||
|
||||
import AddArticleButton from "@/components/button/AddArticleButton";
|
||||
import CancelSubmitButton from "@/components/button/CancelSubmitButton";
|
||||
import DeleteArticleButton from "@/components/button/DeleteArticleButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import NotApplicableCheckButton from "@/components/button/NotApplicableCheckButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import VerificationResultButton from "@/components/button/VerificationResultButton";
|
||||
import DataGrid, { applyDropDownColumnToGrid } from "@/components/module/DataGrid";
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import type { ColumnInfo, ColumnInfoList } from "@/components/type/DataGrid.type";
|
||||
import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog";
|
||||
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
||||
import type {
|
||||
CellEditedCallbackParams,
|
||||
CellStyleCallbackParams,
|
||||
CellTooltipCallbackParams,
|
||||
ColumnConfig,
|
||||
ColumnInfo,
|
||||
ColumnInfoList,
|
||||
} from "@/components/type/DataGrid.type";
|
||||
import { doList } from "@/components/util/column/input/DoList";
|
||||
import { RainfallPumpStationInpColumn } from "@/components/util/column/input/sewageReservoirPump/RainfallPumpStationInpColumn";
|
||||
import { SewageReservoirInpColumn } from "@/components/util/column/input/sewageReservoirPump/SewageReservoirInpColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { formatCompactYmdForGrid, normalizeOxYnValue } from "@/components/util/grid/ymdFormat";
|
||||
import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import {
|
||||
SEWAGE_RESERVOIR_STATS_CD,
|
||||
type SewageReservoirItem,
|
||||
type SewageReservoirRequestParams,
|
||||
type RainfallPumpStationItem,
|
||||
type SewageReservoirVerificationResult,
|
||||
} from "@/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface";
|
||||
import type {
|
||||
ApiResponse,
|
||||
Sgg,
|
||||
SewerageStatusInfo,
|
||||
} from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore, useStatisticsInputToolbarGroupGate } from "@/shared/lib";
|
||||
import { useAdminStore } from "@/store/admin-store";
|
||||
|
||||
const SewageReservoirPumpInpTabList = [
|
||||
SewageReservoirInpColumn,
|
||||
@@ -25,64 +64,781 @@ const toGridColumnInfo = (tab: ColumnInfoList): ColumnInfo => ({
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
|
||||
const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? "sewageReservoirInp";
|
||||
const firstTabKey = SewageReservoirPumpInpTabList[0]?.tabKey ?? SEWAGE_RESERVOIR_STATS_CD;
|
||||
const rainfallPumpTabKey =
|
||||
RainfallPumpStationInpColumn.tabKey ?? "rainfallPumpStationInp";
|
||||
|
||||
type GridRowWithVerification = Record<string, string | number> & {
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
};
|
||||
|
||||
type TabToolbarSnapshot = {
|
||||
showSaveInToolbar: boolean;
|
||||
showEnterableToolbarButtons: boolean;
|
||||
showActionToolbarCard: boolean;
|
||||
};
|
||||
|
||||
const computeTabEnterable = (
|
||||
statusInfo: SewerageStatusInfo | null | undefined,
|
||||
periodEnterable: boolean,
|
||||
): boolean => {
|
||||
if (statusInfo?.laeSbmsnSttsCd) {
|
||||
if (["2", "3"].includes(statusInfo.laeSbmsnSttsCd)) {
|
||||
return false;
|
||||
}
|
||||
return periodEnterable;
|
||||
}
|
||||
return periodEnterable;
|
||||
};
|
||||
|
||||
function buildGugunOkForToolbar(values: SearchFormValues | null): boolean {
|
||||
return doList.includes(values?.bsnmCd || "")
|
||||
? Boolean(values?.gugun && values.gugun !== "ALL")
|
||||
: true;
|
||||
}
|
||||
|
||||
function computeTabToolbarSnapshot(
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
hasRowData: boolean,
|
||||
treatAsSearched: boolean,
|
||||
searchContext: SearchFormValues | null,
|
||||
periodEnterable: boolean,
|
||||
isAdminInfo: boolean,
|
||||
): TabToolbarSnapshot {
|
||||
const gugunOk = buildGugunOkForToolbar(searchContext);
|
||||
const enterable = computeTabEnterable(statusInfo, periodEnterable);
|
||||
const isSubmitted = statusInfo?.laeSbmsnSttsCd === "2";
|
||||
const showSaveInToolbar = (enterable || isAdminInfo) && gugunOk;
|
||||
const showEnterableToolbarButtons = enterable && gugunOk;
|
||||
const showSubmittedInInputPeriodToolbar =
|
||||
isSubmitted && periodEnterable && gugunOk;
|
||||
const showActionToolbarCard =
|
||||
(hasRowData || treatAsSearched) &&
|
||||
(showSaveInToolbar ||
|
||||
showEnterableToolbarButtons ||
|
||||
showSubmittedInInputPeriodToolbar ||
|
||||
(isSubmitted && isAdminInfo));
|
||||
return { showSaveInToolbar, showEnterableToolbarButtons, showActionToolbarCard };
|
||||
}
|
||||
|
||||
function tabRowDataExists(
|
||||
m: Map<string, Record<string, string | number>[]>,
|
||||
tabKey: string,
|
||||
): boolean {
|
||||
const rows = m.get(tabKey);
|
||||
return Array.isArray(rows) && rows.length > 0;
|
||||
}
|
||||
|
||||
function buildSewageReservoirSearchParam(
|
||||
values: SearchFormValues,
|
||||
overrides?: { mode?: string },
|
||||
): SewageReservoirRequestParams {
|
||||
const fcltNm = values.fctName?.trim();
|
||||
return {
|
||||
statsYr: values.year,
|
||||
sewerBzmnCd: values.bsnmCd,
|
||||
ctpvNm: values.sido,
|
||||
sggNm: values.gugun === "ALL" ? "" : values.gugun,
|
||||
...(fcltNm ? { fcltNm } : {}),
|
||||
...(overrides?.mode ? { mode: overrides.mode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** 항목 추가 시 빈 행 (시도는 조회 시 선택한 시도명) */
|
||||
function createEmptySewageReservoirRow(
|
||||
ctpvNm: string,
|
||||
sggNm = "",
|
||||
): GridRowWithVerification {
|
||||
const row: GridRowWithVerification = { ctpvNm, sggNm };
|
||||
for (const col of SewageReservoirInpColumn.columns) {
|
||||
const fieldName = col.fieldName;
|
||||
if (fieldName === "ctpvNm" || fieldName === "sggNm") continue;
|
||||
row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : "";
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 빗물펌프장 탭 항목 추가 시 빈 행 */
|
||||
function createEmptyRainfallPumpStationRow(
|
||||
ctpvNm: string,
|
||||
sggNm = "",
|
||||
): GridRowWithVerification {
|
||||
const row: GridRowWithVerification = { ctpvNm, sggNm };
|
||||
for (const col of RainfallPumpStationInpColumn.columns) {
|
||||
const fieldName = col.fieldName;
|
||||
if (!fieldName) continue;
|
||||
if (fieldName === "ctpvNm" || fieldName === "sggNm") continue;
|
||||
row[fieldName] = col.valueType === ValueType.NUMBER ? 0 : "";
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function applySggNmDropDownToReservoirGrid(
|
||||
gridRef: RefObject<RealGridReact | null>,
|
||||
labels: string[],
|
||||
) {
|
||||
if (labels.length === 0) return;
|
||||
applyDropDownColumnToGrid(
|
||||
gridRef.current?.gridView as {
|
||||
setColumnProperty?: (col: string, key: string, value: unknown) => void;
|
||||
},
|
||||
"sggNm",
|
||||
labels,
|
||||
);
|
||||
}
|
||||
|
||||
function parseSggListFromResponse(res: ApiResponse<Sgg[]>): Sgg[] {
|
||||
if (res.status !== "success" || res.data == null) return [];
|
||||
return Array.isArray(res.data) ? res.data : [];
|
||||
}
|
||||
|
||||
function buildSggNmColumn(
|
||||
col: ColumnConfig,
|
||||
canEdit: boolean,
|
||||
sggOptions: Sgg[],
|
||||
): ColumnConfig {
|
||||
if (!canEdit) {
|
||||
return {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
};
|
||||
}
|
||||
if (sggOptions.length === 0) {
|
||||
return { ...col, editable: true };
|
||||
}
|
||||
const labels = sggOptions.map((s) => s.gugun);
|
||||
return {
|
||||
...col,
|
||||
editable: true,
|
||||
editor: {
|
||||
type: "dropdown",
|
||||
domainOnly: true,
|
||||
textReadOnly: true,
|
||||
commitOnSelect: true,
|
||||
dropDownWhenClick: true,
|
||||
values: labels,
|
||||
labels,
|
||||
},
|
||||
values: labels,
|
||||
labels,
|
||||
lookupDisplay: true,
|
||||
};
|
||||
}
|
||||
|
||||
function mapSewageReservoirListToGridRows(
|
||||
list: SewageReservoirItem[],
|
||||
ctpvNm: string,
|
||||
): GridRowWithVerification[] {
|
||||
return list.map((item) => {
|
||||
const row = toGridRows([
|
||||
{
|
||||
...item,
|
||||
ctpvNm,
|
||||
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
|
||||
fldngPrvtnYn: normalizeOxYnValue(item.fldngPrvtnYn),
|
||||
wqPlltnMtgtYn: normalizeOxYnValue(item.wqPlltnMtgtYn),
|
||||
rtlztnUsgYn: normalizeOxYnValue(item.rtlztnUsgYn),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
function mapRainfallPumpStationListToGridRows(
|
||||
list: RainfallPumpStationItem[],
|
||||
ctpvNm: string,
|
||||
): GridRowWithVerification[] {
|
||||
return list.map((item) => {
|
||||
const row = toGridRows([
|
||||
{
|
||||
...item,
|
||||
ctpvNm,
|
||||
oprtngStrtYmd: formatCompactYmdForGrid(item.oprtngStrtYmd),
|
||||
rsvrYn: normalizeOxYnValue(item.rsvrYn),
|
||||
prcsgFcltYn: normalizeOxYnValue(item.prcsgFcltYn),
|
||||
ddrztnEqpmYn: normalizeOxYnValue(item.ddrztnEqpmYn),
|
||||
oslfEtgtYn: normalizeOxYnValue(item.oslfEtgtYn),
|
||||
},
|
||||
])[0] as GridRowWithVerification;
|
||||
|
||||
if (item.verificationResult) {
|
||||
row.verificationResult = item.verificationResult;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 04. 하수저류 시설 및 빗물 펌프장
|
||||
const SewageReservoirPumpInp = () => {
|
||||
const submitExceptionFunc = () => {
|
||||
console.log("submit exception");
|
||||
// 예외 제출 로직
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = () => {
|
||||
console.log("get last year data");
|
||||
// 지난연도 데이터 가져오기 로직
|
||||
};
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const { isAdminInfo } = useAdminStore();
|
||||
const toolbarGroupAllowed = useStatisticsInputToolbarGroupGate();
|
||||
|
||||
const submitFunc = () => {
|
||||
console.log("submit");
|
||||
// 제출 로직
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log("save");
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
const addArticleFunc = () => {
|
||||
console.log("save");
|
||||
// 항목 추가 로직
|
||||
const cnacelSubmitFunc = () => {
|
||||
console.log("cancel submit");
|
||||
};
|
||||
|
||||
const deleteArticleFunc = () => {
|
||||
console.log("save");
|
||||
// 항목 제외 로직
|
||||
console.log("delete article");
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = useState(firstTabKey);
|
||||
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
|
||||
const [rowDataMap, setRowDataMap] = useState<
|
||||
Map<string, Record<string, string | number>[]>
|
||||
>(new Map());
|
||||
const [isNotApplicable, setIsNotApplicable] = useState(false);
|
||||
const [showIsNotApplicable, setShowIsNotApplicable] = useState(false);
|
||||
const [searchValues, setSearchValues] = useState<SearchFormValues | null>(null);
|
||||
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(
|
||||
null,
|
||||
);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true);
|
||||
const [tabStatusInfoMap, setTabStatusInfoMap] = useState<
|
||||
Map<string, SewerageStatusInfo | null>
|
||||
>(new Map());
|
||||
const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState<
|
||||
Map<string, TabToolbarSnapshot>
|
||||
>(new Map());
|
||||
const [tabStatsCdMap, setTabStatsCdMap] = useState<Map<string, string>>(new Map());
|
||||
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
|
||||
const [sggOptions, setSggOptions] = useState<Sgg[]>([]);
|
||||
const reservoirGridRef = useRef<RealGridReact | null>(null);
|
||||
const sggOptionsSidoRef = useRef<string | null>(null);
|
||||
|
||||
const searchDoneRef = useRef(false);
|
||||
const periodEnterableRef = useRef(true);
|
||||
const activeTabRef = useRef(activeTab);
|
||||
activeTabRef.current = activeTab;
|
||||
|
||||
const commitToolbarSnapshotForLoadedTab = (
|
||||
tabKey: string,
|
||||
statusInfo: SewerageStatusInfo | null,
|
||||
hasRowData: boolean,
|
||||
searchContext: SearchFormValues,
|
||||
) => {
|
||||
setToolbarSnapshotByTab((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(
|
||||
tabKey,
|
||||
computeTabToolbarSnapshot(
|
||||
statusInfo,
|
||||
hasRowData,
|
||||
searchDoneRef.current,
|
||||
searchContext,
|
||||
periodEnterableRef.current,
|
||||
isAdminInfo,
|
||||
),
|
||||
);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const applyRowMap = (
|
||||
mergeIntoExistingMap: boolean,
|
||||
apply: (m: Map<string, Record<string, string | number>[]>) => void,
|
||||
) => {
|
||||
if (mergeIntoExistingMap) {
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
apply(next);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
const next = new Map<string, Record<string, string | number>[]>();
|
||||
apply(next);
|
||||
setRowDataMap(next);
|
||||
}
|
||||
};
|
||||
|
||||
const loadActiveTabData = async (
|
||||
tabKey: string,
|
||||
values: SearchFormValues,
|
||||
mergeIntoExistingMap: boolean,
|
||||
updateLastSearchedValues: boolean,
|
||||
overrides?: { mode?: string },
|
||||
): Promise<boolean> => {
|
||||
// 서버 데이터로 새로 불러오는 시점이므로 "해당없음" 체크는 항상 초기화
|
||||
setIsNotApplicable(false);
|
||||
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
|
||||
const response =
|
||||
await statisticsInputSewageReservoirPumpApi.getSewageReservoirList(
|
||||
buildSewageReservoirSearchParam(values, overrides),
|
||||
);
|
||||
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message ?? "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const { commonInfo, statusInfo, sewageReservoirList } = response.data;
|
||||
const list = sewageReservoirList ?? [];
|
||||
const gridRows = mapSewageReservoirListToGridRows(
|
||||
list,
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
if (activeTabRef.current === tabKey) {
|
||||
setShowIsNotApplicable(list.length === 0);
|
||||
}
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) {
|
||||
setLastSearchedValues(values);
|
||||
}
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
statusInfo,
|
||||
gridRows.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabKey === rainfallPumpTabKey) {
|
||||
const response =
|
||||
await statisticsInputSewageReservoirPumpApi.getRainfallPumpStationList(
|
||||
buildSewageReservoirSearchParam(values, overrides),
|
||||
);
|
||||
|
||||
if (response.status !== "success" || !response.data) {
|
||||
alert(response.message ?? "데이터 조회 중 오류가 발생했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const { commonInfo, statusInfo, rainfallPumpStationList } = response.data;
|
||||
const list = rainfallPumpStationList ?? [];
|
||||
const gridRows = mapRainfallPumpStationListToGridRows(
|
||||
list,
|
||||
commonInfo.ctpvNm,
|
||||
);
|
||||
|
||||
applyRowMap(mergeIntoExistingMap, (m) => {
|
||||
m.set(tabKey, gridRows);
|
||||
});
|
||||
setTabStatusInfoMap((prev) => new Map(prev).set(tabKey, statusInfo));
|
||||
setTabStatsCdMap((prev) => new Map(prev).set(tabKey, commonInfo.statsCd));
|
||||
if (updateLastSearchedValues) {
|
||||
setLastSearchedValues(values);
|
||||
}
|
||||
commitToolbarSnapshotForLoadedTab(
|
||||
tabKey,
|
||||
statusInfo,
|
||||
gridRows.length > 0,
|
||||
values,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
alert("해당 탭 조회 API가 없습니다.");
|
||||
return false;
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = async () => {
|
||||
const values = lastSearchedValues ?? searchValues;
|
||||
if (!values?.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!hasSearched) {
|
||||
alert("먼저 데이터를 조회해주세요.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadActiveTabData(activeTabRef.current, values, true, false, {
|
||||
mode: "PRE",
|
||||
});
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSggOptions = useCallback(async (sido: string): Promise<Sgg[]> => {
|
||||
if (!sido || sido === "ALL") {
|
||||
setSggOptions([]);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const res = await statisticsApi.getSggAllList(sido);
|
||||
const list = parseSggListFromResponse(res);
|
||||
setSggOptions(list);
|
||||
return list;
|
||||
} catch {
|
||||
setSggOptions([]);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleReset = () => {
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
setLastSearchedValues(null);
|
||||
setSggOptions([]);
|
||||
sggOptionsSidoRef.current = null;
|
||||
setHasSearched(false);
|
||||
setInputPeriodEnterable(true);
|
||||
setIsNotApplicable(false);
|
||||
setShowIsNotApplicable(false);
|
||||
searchDoneRef.current = false;
|
||||
periodEnterableRef.current = true;
|
||||
setActiveTab(firstTabKey);
|
||||
};
|
||||
|
||||
const handleSearch = async (values: SearchFormValues) => {
|
||||
if (!values.sido || values.sido === "ALL") {
|
||||
alert("시도를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const yearChangedFromLastSearch =
|
||||
lastSearchedValues != null &&
|
||||
(lastSearchedValues.year ?? "") !== (values.year ?? "");
|
||||
|
||||
if (yearChangedFromLastSearch) {
|
||||
setRowDataMap(new Map());
|
||||
setTabStatusInfoMap(new Map());
|
||||
setTabStatsCdMap(new Map());
|
||||
setToolbarSnapshotByTab(new Map());
|
||||
setActiveTab(firstTabKey);
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
searchDoneRef.current = true;
|
||||
setHasSearched(true);
|
||||
setLastSearchedValues(values);
|
||||
|
||||
const tabKeyToLoad = yearChangedFromLastSearch ? firstTabKey : activeTabRef.current;
|
||||
if (yearChangedFromLastSearch) {
|
||||
activeTabRef.current = firstTabKey;
|
||||
setActiveTab(firstTabKey);
|
||||
}
|
||||
|
||||
const ok = await loadActiveTabData(
|
||||
tabKeyToLoad,
|
||||
values,
|
||||
!yearChangedFromLastSearch,
|
||||
true,
|
||||
);
|
||||
if (!ok) return;
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveTabChange = async (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
if (tabKey === SEWAGE_RESERVOIR_STATS_CD) {
|
||||
setShowIsNotApplicable(
|
||||
hasSearched && !tabRowDataExists(rowDataMap, tabKey),
|
||||
);
|
||||
} else {
|
||||
setShowIsNotApplicable(false);
|
||||
}
|
||||
if (!hasSearched || !lastSearchedValues) return;
|
||||
if (tabRowDataExists(rowDataMap, tabKey)) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await loadActiveTabData(tabKey, lastSearchedValues, true, false);
|
||||
} catch {
|
||||
alert("데이터 조회 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const searchContextForToolbar = lastSearchedValues ?? searchValues;
|
||||
const activeTabStatus = tabStatusInfoMap.get(activeTab) ?? null;
|
||||
const activeTabToolbar =
|
||||
toolbarSnapshotByTab.get(activeTab) ??
|
||||
computeTabToolbarSnapshot(
|
||||
activeTabStatus,
|
||||
tabRowDataExists(rowDataMap, activeTab),
|
||||
hasSearched,
|
||||
searchContextForToolbar,
|
||||
inputPeriodEnterable,
|
||||
isAdminInfo,
|
||||
);
|
||||
|
||||
const {
|
||||
showSaveInToolbar: baseShowSaveInToolbar,
|
||||
showEnterableToolbarButtons: baseShowEnterableToolbarButtons,
|
||||
showActionToolbarCard: baseShowActionToolbarCard,
|
||||
} = activeTabToolbar;
|
||||
|
||||
const showSaveInToolbar = baseShowSaveInToolbar && toolbarGroupAllowed;
|
||||
const showEnterableToolbarButtons =
|
||||
baseShowEnterableToolbarButtons && toolbarGroupAllowed;
|
||||
const showActionToolbarCard =
|
||||
baseShowActionToolbarCard && toolbarGroupAllowed;
|
||||
// 항목추가/제거 버튼은 "데이터가 있든 없든", "과거 데이터(입력 불가 상태)여도"
|
||||
// 조회를 하고나면 항상 보여야 합니다.
|
||||
const isReservoirOrRainfallTab =
|
||||
activeTab === SEWAGE_RESERVOIR_STATS_CD || activeTab === rainfallPumpTabKey;
|
||||
const showArticleToolbarCard =
|
||||
hasSearched && isReservoirOrRainfallTab && toolbarGroupAllowed;
|
||||
|
||||
const openVerificationHistoryDialog = () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
const activeTabStatsCd = tabStatsCdMap.get(activeTab);
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !activeTabStatsCd) {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
setVerificationHistoryOpen(true);
|
||||
};
|
||||
|
||||
const addArticleFunc = async () => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
if (!hasSearched || !ctx?.sido || ctx.sido === "ALL") {
|
||||
alert("먼저 조회를 진행해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTabKey = activeTabRef.current;
|
||||
const isReservoirTab = activeTabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
const isRainfallPumpTab = activeTabKey === rainfallPumpTabKey;
|
||||
if (!isReservoirTab && !isRainfallPumpTab) {
|
||||
alert("항목을 추가할 수 없는 탭입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sggNm =
|
||||
ctx.gugun && ctx.gugun !== "ALL" ? ctx.gugun : "";
|
||||
|
||||
if (isRainfallPumpTab) {
|
||||
const newRow = createEmptyRainfallPumpStationRow(ctx.sido, sggNm);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const current = next.get(rainfallPumpTabKey) ?? [];
|
||||
next.set(rainfallPumpTabKey, [...current, newRow]);
|
||||
return next;
|
||||
});
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(rainfallPumpTabKey) ?? null;
|
||||
commitToolbarSnapshotForLoadedTab(rainfallPumpTabKey, statusInfo, true, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
let list = sggOptions;
|
||||
if (sggOptionsSidoRef.current !== ctx.sido || list.length === 0) {
|
||||
list = await loadSggOptions(ctx.sido);
|
||||
sggOptionsSidoRef.current = ctx.sido;
|
||||
}
|
||||
if (list.length === 0) {
|
||||
alert("시군구 목록을 불러오지 못했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const tabKey = SEWAGE_RESERVOIR_STATS_CD;
|
||||
const labels = list.map((s) => s.gugun).filter(Boolean);
|
||||
const newRow = createEmptySewageReservoirRow(ctx.sido, sggNm);
|
||||
|
||||
flushSync(() => {
|
||||
setSggOptions(list);
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const current = next.get(tabKey) ?? [];
|
||||
next.set(tabKey, [...current, newRow]);
|
||||
return next;
|
||||
});
|
||||
});
|
||||
|
||||
// 드롭다운 컬럼은 columnInfoByTab(buildSggNmColumn) 렌더링으로 반영한다.
|
||||
// 행 추가 직후 setColumnProperty를 강제 재적용하면 타이밍 이슈로
|
||||
// `fieldIndex is out of bounds`가 발생할 수 있어 제거한다.
|
||||
|
||||
setShowIsNotApplicable(false);
|
||||
|
||||
const statusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
commitToolbarSnapshotForLoadedTab(tabKey, statusInfo, true, ctx);
|
||||
};
|
||||
|
||||
const columnInfoByTab = useMemo(() => {
|
||||
const map = new Map<string, ColumnInfo>();
|
||||
for (const tab of SewageReservoirPumpInpTabList) {
|
||||
const tabKey = tab.tabKey ?? "";
|
||||
const canEdit =
|
||||
computeTabEnterable(
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
const columns = (tab.columns ?? []).map((col: ColumnConfig) => {
|
||||
if (isReservoirTab && col.fieldName === "sggNm") {
|
||||
return buildSggNmColumn(col, canEdit, sggOptions);
|
||||
}
|
||||
return canEdit
|
||||
? col
|
||||
: {
|
||||
...col,
|
||||
editable: false as const,
|
||||
styleName: col.styleName ?? "rg-cell-readonly",
|
||||
};
|
||||
});
|
||||
map.set(tabKey, {
|
||||
columns,
|
||||
columnLayout: tab.columnLayout ?? [],
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [tabStatusInfoMap, inputPeriodEnterable, isAdminInfo, sggOptions]);
|
||||
|
||||
const handleSewageReservoirCellEdited = useCallback(
|
||||
(params: CellEditedCallbackParams) => {
|
||||
if (params.fieldName !== "sggNm") return;
|
||||
const selected = sggOptions.find((s) => s.gugun === params.newValue);
|
||||
if (!selected) return;
|
||||
|
||||
const tabKey = SEWAGE_RESERVOIR_STATS_CD;
|
||||
setRowDataMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
const rows = [...(next.get(tabKey) ?? [])];
|
||||
const row = rows[params.itemIndex];
|
||||
if (!row) return prev;
|
||||
rows[params.itemIndex] = {
|
||||
...row,
|
||||
sggNm: selected.gugun,
|
||||
admdstCd: selected.adstDstrCd,
|
||||
admdstNm: selected.gugun,
|
||||
};
|
||||
next.set(tabKey, rows);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[sggOptions],
|
||||
);
|
||||
|
||||
const verificationCellStyle = ({
|
||||
fieldName,
|
||||
row,
|
||||
}: CellStyleCallbackParams): string | undefined => {
|
||||
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";
|
||||
}
|
||||
};
|
||||
|
||||
const verificationCellTooltip = ({
|
||||
fieldName,
|
||||
row,
|
||||
}: CellTooltipCallbackParams): string | undefined => {
|
||||
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검증 -";
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
};
|
||||
|
||||
const verificationQueryContext = (() => {
|
||||
const ctx = lastSearchedValues ?? searchValues;
|
||||
const statsCd = tabStatsCdMap.get(activeTab);
|
||||
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd || !statsCd) return null;
|
||||
return {
|
||||
sewerBzmnCd: ctx.bsnmCd,
|
||||
ctpvNm: ctx.sido,
|
||||
sggNm: ctx.gugun === "ALL" ? "" : (ctx.gugun ?? ""),
|
||||
statsYr: ctx.year,
|
||||
statsCd,
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<SearchForm fctName={true} />
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<SubmitExceptionButton SubmitExceptionFunc={submitExceptionFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<SearchForm
|
||||
fctName={true}
|
||||
onSubmit={handleSearch}
|
||||
onReset={handleReset}
|
||||
onValuesChange={setSearchValues}
|
||||
/>
|
||||
{showActionToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<VerificationResultButton
|
||||
VerificationResultButtonFunc={openVerificationHistoryDialog}
|
||||
/>
|
||||
{showEnterableToolbarButtons && (
|
||||
<>
|
||||
<LastYearDataButton
|
||||
getLastYearDataFunc={() => void getLastYearDataFunc()}
|
||||
/>
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
</>
|
||||
)}
|
||||
{activeTabStatus?.laeSbmsnSttsCd === "2" && (
|
||||
<CancelSubmitButton cnacelSubmitFunc={cnacelSubmitFunc} />
|
||||
)}
|
||||
{showSaveInToolbar && <SaveButton saveFunc={saveFunc} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
<NotApplicableCheckButton isCheck={isNotApplicable} setIsCheck={setIsNotApplicable} />
|
||||
<AddArticleButton addArticleFunc={addArticleFunc} />
|
||||
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
|
||||
)}
|
||||
{showArticleToolbarCard && (
|
||||
<div className="card !py-0">
|
||||
<div className="m-[0.6rem] flex items-center justify-end gap-[0.6rem]">
|
||||
{showIsNotApplicable && (
|
||||
<NotApplicableCheckButton
|
||||
isCheck={isNotApplicable}
|
||||
setIsCheck={setIsNotApplicable}
|
||||
/>
|
||||
)}
|
||||
<AddArticleButton addArticleFunc={addArticleFunc} />
|
||||
<DeleteArticleButton deleteArticleFunc={deleteArticleFunc} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
)}
|
||||
{isLoading && (
|
||||
<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>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => void handleActiveTabChange(v)}
|
||||
>
|
||||
<TabsList>
|
||||
{SewageReservoirPumpInpTabList.map((tab) => (
|
||||
<TabsTrigger key={tab.tabKey} value={tab.tabKey ?? ""}>
|
||||
@@ -92,10 +848,29 @@ const SewageReservoirPumpInp = () => {
|
||||
</TabsList>
|
||||
{SewageReservoirPumpInpTabList.map((tab) => {
|
||||
const tabKey = tab.tabKey ?? "";
|
||||
const tabEnterableForGrid =
|
||||
computeTabEnterable(
|
||||
tabStatusInfoMap.get(tabKey) ?? null,
|
||||
inputPeriodEnterable,
|
||||
) || isAdminInfo;
|
||||
const gugunOk = buildGugunOkForToolbar(lastSearchedValues);
|
||||
const tabStatusInfo = tabStatusInfoMap.get(tabKey) ?? null;
|
||||
const isReservoirTab = tabKey === SEWAGE_RESERVOIR_STATS_CD;
|
||||
return (
|
||||
<TabsContent key={tabKey} value={tabKey}>
|
||||
<DataGrid
|
||||
columnInfo={toGridColumnInfo(tab)}
|
||||
key={
|
||||
tabEnterableForGrid || isAdminInfo
|
||||
? `${tabKey}-enterable`
|
||||
: `${tabKey}-readonly`
|
||||
}
|
||||
getGridRef={
|
||||
isReservoirTab ? reservoirGridRef : undefined
|
||||
}
|
||||
columnInfo={
|
||||
columnInfoByTab.get(tabKey) ??
|
||||
toGridColumnInfo(tab)
|
||||
}
|
||||
rowStartNum={tab.rowStartNum}
|
||||
fixedColumns={tab.fixedColumns}
|
||||
rowData={rowDataMap.get(tabKey) ?? []}
|
||||
@@ -105,19 +880,47 @@ const SewageReservoirPumpInp = () => {
|
||||
const cur = next.get(tabKey) ?? [];
|
||||
const n =
|
||||
typeof updater === "function"
|
||||
? (updater as (p: Record<string, string | number>[]) => Record<string, string | number>[])(cur)
|
||||
? (updater as (
|
||||
p: Record<
|
||||
string,
|
||||
string | number
|
||||
>[],
|
||||
) => Record<
|
||||
string,
|
||||
string | number
|
||||
>[])(cur)
|
||||
: updater;
|
||||
next.set(tabKey, n);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
showHeaderCheckbox={false}
|
||||
showHeaderCheckbox
|
||||
statusInfo={tabStatusInfo}
|
||||
isExcelImport={
|
||||
tabRowDataExists(rowDataMap, tabKey) &&
|
||||
tabEnterableForGrid &&
|
||||
gugunOk
|
||||
}
|
||||
cellStyleCallback={verificationCellStyle}
|
||||
cellTooltipCallback={verificationCellTooltip}
|
||||
onCellEdited={
|
||||
isReservoirTab &&
|
||||
tabEnterableForGrid &&
|
||||
sggOptions.length > 0
|
||||
? handleSewageReservoirCellEdited
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
<VerificationResultHistoryDialog
|
||||
open={verificationHistoryOpen}
|
||||
onOpenChange={setVerificationHistoryOpen}
|
||||
queryContext={verificationQueryContext}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -254,7 +254,6 @@ const SeweragePenetrationRateInp = () => {
|
||||
...d,
|
||||
ctpvNm: commonInfo.ctpvNm,
|
||||
spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn,
|
||||
swagePplPsplyRt: commonInfo.swagePplPsplyRt,
|
||||
}));
|
||||
setRowData(
|
||||
convertedData.map((d) => {
|
||||
@@ -823,7 +822,7 @@ const SeweragePenetrationRateInp = () => {
|
||||
if (Array.isArray(errors) && errors.length > 0) {
|
||||
return errors
|
||||
.map((e) => {
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NM' ? '일반검증 - ' : 'HB검증 -');
|
||||
const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : (e.vrfcCd === 'NV' ? '일반검증 - ' : 'HB검증 -');
|
||||
return `${prefix}${e.rsnExpln}
|
||||
(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`;
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { GridFitStyle } from "realgrid";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -10,9 +11,20 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
LoadingWithAnimation,
|
||||
} from "@common/components";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import type { ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import {
|
||||
alignLogDetailRowsToColumns,
|
||||
buildLogHistoryDetailColumnInfo,
|
||||
emptyLogHistoryDetailColumnInfo,
|
||||
} from "@/components/util/column/management/LogHistoryDetailColumn";
|
||||
import { toGridRows } from "@/components/util/grid/gridFunction";
|
||||
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
||||
import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface";
|
||||
import { useAlertStore } from "@/shared/lib";
|
||||
|
||||
export interface LogHistoryDetailDialogProps {
|
||||
open: boolean;
|
||||
@@ -25,24 +37,115 @@ export function LogHistoryDetailDialog({
|
||||
onOpenChange,
|
||||
row,
|
||||
}: LogHistoryDetailDialogProps) {
|
||||
const { setMessage: alert } = useAlertStore((state) => state);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>(
|
||||
[],
|
||||
);
|
||||
const [columnInfo, setColumnInfo] = useState<ColumnInfo>(
|
||||
emptyLogHistoryDetailColumnInfo,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !row) return;
|
||||
if (!open) {
|
||||
setRowData([]);
|
||||
setColumnInfo(emptyLogHistoryDetailColumnInfo);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!row) return;
|
||||
|
||||
const sn = row.statsLogSn;
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
const response = await statisticsApi.getLogDetail(
|
||||
sn !== undefined && sn !== null ? String(sn) : "",
|
||||
);
|
||||
console.log(response.data);
|
||||
setIsLoading(true);
|
||||
setRowData([]);
|
||||
|
||||
try {
|
||||
const response = await statisticsApi.getLogDetail(
|
||||
sn !== undefined && sn !== null ? String(sn) : "",
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
if (response.status !== "success") {
|
||||
alert(
|
||||
response.message ??
|
||||
"로그 상세 정보를 불러오는데 실패했습니다.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Array.isArray(response.data)
|
||||
? response.data
|
||||
: [];
|
||||
const nextColumnInfo = buildLogHistoryDetailColumnInfo(list);
|
||||
const nextRows = alignLogDetailRowsToColumns(
|
||||
toGridRows(list as StatisticsLogDetailRow[]),
|
||||
nextColumnInfo,
|
||||
);
|
||||
setColumnInfo(nextColumnInfo);
|
||||
setRowData(nextRows);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
alert("로그 상세 정보를 불러오는데 실패했습니다.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false);
|
||||
}
|
||||
})();
|
||||
}, [open, row]);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, row, alert]);
|
||||
|
||||
const gridRemountKey = useMemo(
|
||||
() => columnInfo.columns.map((c) => c.fieldName).join("\u0001"),
|
||||
[columnInfo],
|
||||
);
|
||||
|
||||
const showGrid = !isLoading && rowData.length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogContent
|
||||
className="max-w-[80%]"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle />
|
||||
<DialogTitle>로그 상세 정보</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="relative min-h-[32rem]">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showGrid ? (
|
||||
<DataGrid
|
||||
key={gridRemountKey}
|
||||
columnInfo={columnInfo}
|
||||
rowData={rowData}
|
||||
setRowData={setRowData}
|
||||
isExcelImport={false}
|
||||
isExcelExport={false}
|
||||
enableSort={false}
|
||||
gridHeight={55}
|
||||
fitStyle={GridFitStyle.NONE}
|
||||
/>
|
||||
) : (
|
||||
!isLoading && (
|
||||
<p className="flex h-full items-center justify-center text-[1.3rem] text-muted-foreground">
|
||||
표시할 상세 데이터가 없습니다.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white" type="button">
|
||||
|
||||
@@ -175,6 +175,26 @@ export type ColumnConfig = {
|
||||
sortPreserveGroupOrder?: SortPreserveGroupOrder;
|
||||
valueType?: ValueType;
|
||||
dataFormat?: string;
|
||||
/** RealGrid 드롭다운 목록 값 (예: 시군구 `gugun`) */
|
||||
values?: string[];
|
||||
/** RealGrid 드롭다운 표시 라벨 (`values`와 동일 길이 권장) */
|
||||
labels?: string[];
|
||||
/** true이면 셀에 라벨 표시 */
|
||||
lookupDisplay?: boolean;
|
||||
/** RealGrid 날짜 표시 형식 (예: `yyyy-MM-dd`) */
|
||||
datetimeFormat?: string;
|
||||
/** RealGrid 셀 편집기 */
|
||||
editor?: {
|
||||
type?: "dropdown" | "list" | "date";
|
||||
domainOnly?: boolean;
|
||||
textReadOnly?: boolean;
|
||||
commitOnSelect?: boolean;
|
||||
dropDownWhenClick?: boolean;
|
||||
showToday?: boolean;
|
||||
datetimeFormat?: string;
|
||||
values?: string[];
|
||||
labels?: string[];
|
||||
};
|
||||
footer?:
|
||||
| {
|
||||
text?: string;
|
||||
|
||||
+79
-16
@@ -1,26 +1,38 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import { footerDecimal1, footerDecimal2,footerInt } from "@/components/util/grid/footerSummary";
|
||||
import { footerDecimal1, footerDecimal2, footerInt } from "@/components/util/grid/footerSummary";
|
||||
|
||||
// 하수도 통계 관리 > 03.하수관로 유지관리 > tab 2. 관로준설
|
||||
export const PipeDredgingStatusInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false, footer: { text: '합계' } },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false },
|
||||
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 하수관로 총 시설연장
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로총시설연장(m)' }, editable: false, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
{ name: 'wholFcltPrlg', fieldName: 'wholFcltPrlg', header: { text: '하수관로총시설연장(m)' }, editable: false, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00', styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 준설연장 및 개소수 > 관로(m)
|
||||
{ name: 'wholSwagePpl', fieldName: 'wholSwagePpl', header: { text: '합계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{
|
||||
name: 'wholSwagePpl',
|
||||
fieldName: 'wholSwagePpl',
|
||||
header: { text: '합계' },
|
||||
editable: false,
|
||||
footer: footerDecimal1,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
fields: ['cbsysPplPrlg', 'sprsysExlnPrlg', 'sprsysDrtwPrlg'],
|
||||
type: 'sum' as const,
|
||||
},
|
||||
},
|
||||
{ name: 'cbsysPplPrlg', fieldName: 'cbsysPplPrlg', header: { text: '합류식' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
|
||||
// 준설연장 및 개소수 > 관로(m) > 분류식
|
||||
{ name: 'sprsysExlnPrlg', fieldName: 'sprsysExlnPrlg', header: { text: '우수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
|
||||
// 준설연장 및 개소수 > 맨홀, 오·우수받이, 토실·토구
|
||||
{ name: 'mnhlCnt', fieldName: 'mnhlCnt', header: { text: '맨홀(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'exlnDrtwRcvprpCnt', fieldName: 'exlnDrtwRcvprpCnt', header: { text: '오·우수받이(개소)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
@@ -28,8 +40,36 @@ export const PipeDredgingStatusInpColumn = {
|
||||
|
||||
// 계획 & 비율
|
||||
{ name: 'drdgnPlanPrlg', fieldName: 'drdgnPlanPrlg', header: { text: '준설 계획 연장(m)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'drdgnTrgtCtrsDrdgnRt', fieldName: 'drdgnTrgtCtrsDrdgnRt', header: { text: '계획대비 준설율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'wholPrlgCtrsDrdgnRt', fieldName: 'wholPrlgCtrsDrdgnRt', header: { text: '총시설연장대비 준설율(%)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{
|
||||
name: 'drdgnTrgtCtrsDrdgnRt',
|
||||
fieldName: 'drdgnTrgtCtrsDrdgnRt',
|
||||
header: { text: '계획대비 준설율(%)' },
|
||||
editable: false,
|
||||
footer: footerDecimal1,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
numeratorFields: ['cbsysPplPrlg', 'sprsysExlnPrlg', 'sprsysDrtwPrlg'],
|
||||
denominatorFields: ['drdgnPlanPrlg'],
|
||||
type: 'pblswrgPopltnSuprtRate' as const,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'wholPrlgCtrsDrdgnRt',
|
||||
fieldName: 'wholPrlgCtrsDrdgnRt',
|
||||
header: { text: '총시설연장대비 준설율(%)' },
|
||||
editable: false,
|
||||
footer: footerDecimal1,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
numeratorFields: ['drdgnPlanPrlg'],
|
||||
denominatorFields: ['wholFcltPrlg'],
|
||||
type: 'pblswrgPopltnSuprtRate' as const,
|
||||
},
|
||||
},
|
||||
|
||||
// 준설실적
|
||||
{ name: 'drdgnAmt', fieldName: 'drdgnAmt', header: { text: '금액(백만원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
@@ -37,17 +77,38 @@ export const PipeDredgingStatusInpColumn = {
|
||||
|
||||
// 준설토사처리
|
||||
{ name: 'drdgnUntprcAmt', fieldName: 'drdgnUntprcAmt', header: { text: '준설토처리비(백만원)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'wholDrdgnPrcsQnt', fieldName: 'wholDrdgnPrcsQnt', header: { text: '처리량 계(톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{
|
||||
name: 'wholDrdgnPrcsQnt',
|
||||
fieldName: 'wholDrdgnPrcsQnt',
|
||||
header: { text: '처리량 계(톤)' },
|
||||
editable: false,
|
||||
footer: footerDecimal1,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
fields: ['rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt'],
|
||||
type: 'sum' as const,
|
||||
},
|
||||
},
|
||||
{ name: 'rclmQnt', fieldName: 'rclmQnt', header: { text: '매립' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'oceanEmsnQnt', fieldName: 'oceanEmsnQnt', header: { text: '해양투기' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'rcycQnt', fieldName: 'rcycQnt', header: { text: '재활용' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'etcQnt', fieldName: 'etcQnt', header: { text: '기타(소각, 고형화, 시멘트원료 등)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
|
||||
// 비고
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, editable: false, visible: false },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, visible: false },
|
||||
],
|
||||
columnLayout: [
|
||||
{ name: 'group_admin', header: { text: '행정구역' }, items: ['admdstCd', 'sggNm', 'admdstNm'] },
|
||||
{
|
||||
name: 'group_admin', header: { text: '행정구역' },
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] },
|
||||
'sggNm',
|
||||
],
|
||||
},
|
||||
'wholFcltPrlg',
|
||||
{
|
||||
name: 'group_dredge_info',
|
||||
@@ -75,7 +136,9 @@ export const PipeDredgingStatusInpColumn = {
|
||||
header: { text: '준설토사처리' },
|
||||
items: ['drdgnUntprcAmt', 'wholDrdgnPrcsQnt', 'rclmQnt', 'oceanEmsnQnt', 'rcycQnt', 'etcQnt']
|
||||
},
|
||||
'rmrk'
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'admdstNm',
|
||||
],
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 1,
|
||||
|
||||
+39
-7
@@ -5,12 +5,24 @@ import { ValueType } from "realgrid";
|
||||
export const RepairStatusInpColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, editable: false },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false, footer: { text: '합계' } },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구명' }, editable: false, footer: { text: '합계' }, styleName: 'rg-cell-readonly' },
|
||||
|
||||
// 개·보수관로 > 전체보수(m)
|
||||
{ name: 'mendngWhol', fieldName: 'mendngWhol', header: { text: '소계' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{
|
||||
name: 'mendngWhol',
|
||||
fieldName: 'mendngWhol',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
footer: footerDecimal1,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
fields: ['cbsysMendngPrlg', 'sprsysExlnPrlg', 'sprsysDrtwPrlg'],
|
||||
type: 'sum' as const,
|
||||
},
|
||||
},
|
||||
{ name: 'cbsysMendngPrlg', fieldName: 'cbsysMendngPrlg', header: { text: '합류식' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 개·보수관로 > 전체보수(m) > 분류식
|
||||
@@ -18,7 +30,20 @@ export const RepairStatusInpColumn = {
|
||||
{ name: 'sprsysDrtwPrlg', fieldName: 'sprsysDrtwPrlg', header: { text: '오수' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 개·보수관로 > 부분보수(개소)
|
||||
{ name: 'partMendngWhol', fieldName: 'partMendngWhol', header: { text: '소계' }, editable: false, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{
|
||||
name: 'partMendngWhol',
|
||||
fieldName: 'partMendngWhol',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
footer: footerInt,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
styleName: 'rg-cell-total',
|
||||
computed: {
|
||||
fields: ['cbsysPartMendngPrlg', 'sprsysExlnMendngPrlg', 'sprsysDrtwMendngPrlg'],
|
||||
type: 'sum' as const,
|
||||
},
|
||||
},
|
||||
{ name: 'cbsysPartMendngPrlg', fieldName: 'cbsysPartMendngPrlg', header: { text: '합류식' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 개·보수관로 > 부분보수(개소) > 분류식
|
||||
@@ -45,12 +70,17 @@ export const RepairStatusInpColumn = {
|
||||
|
||||
// 비고
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, editable: false, visible: false },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, visible: false },
|
||||
],
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'group_admin',
|
||||
header: { text: '행정구역' },
|
||||
items: ['admdstCd', 'sggNm', 'admdstNm']
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 2 }] },
|
||||
'sggNm',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'group_repair_main',
|
||||
@@ -107,7 +137,9 @@ export const RepairStatusInpColumn = {
|
||||
items: ['stofcbrCnt', 'otflCnt']
|
||||
},
|
||||
'cctvExmnPrlg',
|
||||
'rmrk'
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'admdstNm',
|
||||
],
|
||||
rowStartNum: 4,
|
||||
fixedColumns: 1,
|
||||
|
||||
+91
-92
@@ -4,90 +4,85 @@ import { ValueType } from "realgrid";
|
||||
// 하수도 통계 관리 > 04.하수저류시설 및 빗물펌프장 > tab 2: 빗물펌프장
|
||||
export const RainfallPumpStationInpColumn = {
|
||||
columns: [
|
||||
// Hidden Columns : 시스템 관리를 위한 값
|
||||
{ name: 'sidoCd', fieldName: '시도코드', header: { text: '시도코드' }, hidden: true },
|
||||
{ name: 'adstDstrCd', fieldName: '행정구역코드', header: { text: '행정구역코드' }, hidden: true },
|
||||
{ name: 'adstCd', fieldName: '행정코드', header: { text: '행정코드' }, hidden: true },
|
||||
{ name: 'swgeStrgeFctSeq', fieldName: '시설SEQ', header: { text: '시설SEQ' }, hidden: true },
|
||||
{ name: 'yr', fieldName: '년도', header: { text: '년도' }, hidden: true },
|
||||
{ name: 'odr', fieldName: '차수', header: { text: '차수' }, hidden: true },
|
||||
{ name: 'bsnmCd', fieldName: '사업자코드', header: { text: '사업자코드' }, hidden: true },
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
|
||||
|
||||
// Visible Columns
|
||||
// 행정구역
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false },
|
||||
{ name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } },
|
||||
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } },
|
||||
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } },
|
||||
|
||||
// 시설명
|
||||
{ name: 'swgeStrgeNm', fieldName: '시설명', header: { text: '시설명' } },
|
||||
{ name: 'fcltyKndCn', fieldName: 'fcltyKndCn', header: { text: '시설물종류' } },
|
||||
|
||||
// 위치
|
||||
{ name: 'addr', fieldName: '위치', header: { text: '위치' } },
|
||||
|
||||
// 시설물종류
|
||||
{ name: 'facRes', fieldName: '시설물종류', header: { text: '시설물종류' } },
|
||||
|
||||
// 가동개시 년월일 yyyy-mm-dd
|
||||
{ name: 'rnDt', fieldName: '가동개시 년월일 yyyy-mm-dd', header: { text: '가동개시 년월일 yyyy-mm-dd' } },
|
||||
|
||||
// 계획설계빈도
|
||||
{ name: 'plDsgnFrqnc', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } },
|
||||
{
|
||||
name: 'oprtngStrtYmd',
|
||||
fieldName: 'oprtngStrtYmd',
|
||||
header: { text: '가동개시 년월일 yyyy-mm-dd' },
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
editor: {
|
||||
type: 'date',
|
||||
dropDownWhenClick: true,
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
},
|
||||
{ name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } },
|
||||
|
||||
// 집수구역면적(㎢)
|
||||
{ name: 'dgrAr', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 유수지
|
||||
{ name: 'revFnnc', fieldName: '유수지유무(유,무)', header: { text: '유수지유무(유,무)' } },
|
||||
{ name: 'revCpcty', fieldName: '저류랑(m³)', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'rsvrYn', fieldName: 'rsvrYn', header: { text: '유수지유무(유,무)' } },
|
||||
{ name: 'rsvrImpndQnt', fieldName: 'rsvrImpndQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 흡수정
|
||||
{ name: 'absCpcty', fieldName: '저류랑(m³)', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'abstnRgnImpdQnt', fieldName: 'abstnRgnImpdQnt', header: { text: '저류랑(m³)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 펌프시설 > 총양수량(m³/min)
|
||||
{ name: 'wpCpcty', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 펌프시설1
|
||||
{ name: 'pumpCalbr', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpHpCo', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpCo', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpPreparCo', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 펌프시설2
|
||||
{ name: 'pumpCalbr2', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpHpCo2', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpCo2', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpPreparCo2', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 펌프시설3
|
||||
{ name: 'pumpCalbr3', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpHpCo3', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpCo3', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpPreparCo3', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 부대시설
|
||||
{ name: 'dorZeqpFnnc', fieldName: '전처리시설유무(유/무)', header: { text: '전처리시설유무(유/무)' } },
|
||||
{ name: 'selfEtrqFnnc', fieldName: '탈취설비유무(유/무)', header: { text: '탈취설비유무(유/무)' } },
|
||||
{ name: 'freFnnc', fieldName: '자가발전유무(유/무)', header: { text: '자가발전유무(유/무)' } },
|
||||
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } },
|
||||
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } },
|
||||
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } },
|
||||
|
||||
// 방류수역
|
||||
{ name: 'disWat', fieldName: '방류수역', header: { text: '방류수역' } },
|
||||
{ name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역' } },
|
||||
|
||||
// 최종처분량(ton/년)
|
||||
{ name: 'sdmtTreCpcty', fieldName: '침사', header: { text: '침사' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'iprtTreCpcty', fieldName: '협잡물', header: { text: '협잡물' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 최종처분방법
|
||||
{ name: 'sdmtTreMth', fieldName: '침사', header: { text: '침사' } },
|
||||
{ name: 'iprtTreMth', fieldName: '협잡물', header: { text: '협잡물' } },
|
||||
{ name: 'prcptnLastDspsMthdExpln', fieldName: 'prcptnLastDspsMthdExpln', header: { text: '침사' } },
|
||||
{ name: 'ctmnLastDspsMthdExpln', fieldName: 'ctmnLastDspsMthdExpln', header: { text: '협잡물' } },
|
||||
|
||||
// 운영실적
|
||||
{ name: 'yrstpRnDyn', fieldName: '가동일수(일/년)', header: { text: '가동일수(일/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 입력값 3자리만 가능
|
||||
{ name: 'dsplcmntAmnt', fieldName: '총배수량(m³/년)', header: { text: '총배수량(m³/년)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
{ name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수(일/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 입력값 3자리만 가능
|
||||
{ name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(m³/년)' }, footer: footerDecimal2, valueType: ValueType.NUMBER, dataFormat: '#,##0.00' },
|
||||
|
||||
// 비고
|
||||
{ name: 'rmk', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'chckerOpinion', fieldName: '검토자의견', header: { text: '검토자의견' } },
|
||||
{ name: 'wrterOpinion', fieldName: '사용자의견', header: { text: '사용자의견' } },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
// Hidden Columns : 시스템 관리를 위한 값
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설일련번호' }, visible: false, },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -95,21 +90,24 @@ export const RainfallPumpStationInpColumn = {
|
||||
name: 'adminArea',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sido', 'gugun'],
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] },
|
||||
'sggNm',
|
||||
],
|
||||
},
|
||||
'swgeStrgeNm',
|
||||
'addr',
|
||||
'facRes',
|
||||
'rnDt',
|
||||
'plDsgnFrqnc',
|
||||
'dgrAr',
|
||||
'fcltNm',
|
||||
'fcltPstnCn',
|
||||
'fcltyKndCn',
|
||||
'oprtngStrtYmd',
|
||||
'planDesignFreqCnt',
|
||||
'wtrStrgZoneArea',
|
||||
{
|
||||
name: 'reservoir',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '유수지' },
|
||||
items: [
|
||||
'revFnnc',
|
||||
'revCpcty',
|
||||
'rsvrYn',
|
||||
'rsvrImpndQnt',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -117,7 +115,7 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '흡수정' },
|
||||
items: [
|
||||
'absCpcty',
|
||||
'abstnRgnImpdQnt',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -125,16 +123,16 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설' },
|
||||
items: [
|
||||
'wpCpcty',
|
||||
'grssPpqty',
|
||||
{
|
||||
name: 'pumpSystem1',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설1' },
|
||||
items: [
|
||||
'pumpCalbr',
|
||||
'pumpHpCo',
|
||||
'pumpCo',
|
||||
'pumpPreparCo',
|
||||
'pumpFcltN1DmtrExpln',
|
||||
'pumpFcltN1PerPpqty',
|
||||
'pumpFcltN1PumpCnt',
|
||||
'pumpFcltN1RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -142,10 +140,10 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설2' },
|
||||
items: [
|
||||
'pumpCalbr2',
|
||||
'pumpHpCo2',
|
||||
'pumpCo2',
|
||||
'pumpPreparCo2',
|
||||
'pumpFcltN2DmtrExpln',
|
||||
'pumpFcltN2PerPpqty',
|
||||
'pumpFcltN2PumpCnt',
|
||||
'pumpFcltN2RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -153,10 +151,10 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설3' },
|
||||
items: [
|
||||
'pumpCalbr3',
|
||||
'pumpHpCo3',
|
||||
'pumpCo3',
|
||||
'pumpPreparCo3',
|
||||
'pumpFcltN3DmtrExpln',
|
||||
'pumpFcltN3PerPpqty',
|
||||
'pumpFcltN3PumpCnt',
|
||||
'pumpFcltN3RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -166,19 +164,19 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '부대시설' },
|
||||
items: [
|
||||
'dorZeqpFnnc',
|
||||
'selfEtrqFnnc',
|
||||
'freFnnc',
|
||||
'prcsgFcltYn',
|
||||
'ddrztnEqpmYn',
|
||||
'oslfEtgtYn',
|
||||
],
|
||||
},
|
||||
'disWat',
|
||||
'dschgWtrsExpln',
|
||||
{
|
||||
name: 'finalDisposalAmount',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '최종처분량(ton/년)' },
|
||||
items: [
|
||||
'sdmtTreCpcty',
|
||||
'iprtTreCpcty',
|
||||
'prcptnLastDspsQnt',
|
||||
'ctmnLastDspsQnt',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -186,8 +184,8 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '최종처분방법' },
|
||||
items: [
|
||||
'sdmtTreMth',
|
||||
'iprtTreMth',
|
||||
'prcptnLastDspsMthdExpln',
|
||||
'ctmnLastDspsMthdExpln',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -195,17 +193,18 @@ export const RainfallPumpStationInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '운영실적' },
|
||||
items: [
|
||||
'yrstpRnDyn',
|
||||
'dsplcmntAmnt',
|
||||
'oprtngDayCnt',
|
||||
'grssMltvQnt',
|
||||
],
|
||||
},
|
||||
'rmk',
|
||||
'chckerOpinion',
|
||||
'wrterOpinion',
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'fcltSn',
|
||||
'admdstNm'
|
||||
],
|
||||
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
tabKey: 'rainfallPumpStationInp',
|
||||
tabKey: 'ST00040002',
|
||||
tabLabel: '빗물펌프장',
|
||||
};
|
||||
+162
-88
@@ -1,92 +1,160 @@
|
||||
import type { ColumnConfig } from "@/components/type/DataGrid.type";
|
||||
import { footerInt, footerDecimal1, footerDecimal3 } from "@/components/util/grid/footerSummary";
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
/** 설치목적 O/X 표기 */
|
||||
export const SEWAGE_RESERVOIR_OX_YN_VALUES = ["O", "X"] as const;
|
||||
|
||||
/** 시설구조(ST00040001 `fcltStrctCn`) */
|
||||
export const SEWAGE_RESERVOIR_FCLT_STRCT_VALUES = [
|
||||
"일반지하식",
|
||||
"지하터널식",
|
||||
"기타",
|
||||
] as const;
|
||||
|
||||
function dropdownColumn(
|
||||
col: Pick<ColumnConfig, "name" | "fieldName" | "header"> &
|
||||
Partial<ColumnConfig>,
|
||||
options: readonly string[],
|
||||
): ColumnConfig {
|
||||
const values = [...options];
|
||||
return {
|
||||
...col,
|
||||
values,
|
||||
labels: values,
|
||||
lookupDisplay: true,
|
||||
editor: {
|
||||
type: "dropdown",
|
||||
domainOnly: true,
|
||||
textReadOnly: true,
|
||||
commitOnSelect: true,
|
||||
dropDownWhenClick: true,
|
||||
values,
|
||||
labels: values,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function oxYnDropdownColumn(
|
||||
col: Pick<ColumnConfig, "name" | "fieldName" | "header"> &
|
||||
Partial<ColumnConfig>,
|
||||
): ColumnConfig {
|
||||
return dropdownColumn(col, SEWAGE_RESERVOIR_OX_YN_VALUES);
|
||||
}
|
||||
|
||||
// 하수도 통계 관리 > 04.하수저류시설 및 빗물펌프장 > tab 1: 하수저류시설
|
||||
export const SewageReservoirInpColumn = {
|
||||
columns: [
|
||||
// Hidden Columns : 시스템 관리를 위한 값
|
||||
{ name: 'sidoCd', fieldName: '시도코드', header: { text: '시도코드' }, hidden: true },
|
||||
{ name: 'adstDstrCd', fieldName: '행정구역코드', header: { text: '행정구역코드' }, hidden: true },
|
||||
{ name: 'adstCd', fieldName: '행정코드', header: { text: '행정코드' }, hidden: true },
|
||||
{ name: 'swgeStrgeFctSeq', fieldName: '시설SEQ', header: { text: '시설SEQ' }, hidden: true },
|
||||
|
||||
// Visible Columns
|
||||
// 행정구역
|
||||
{ name: 'sido', fieldName: '시도', header: { text: '시도' }, editable: false },
|
||||
{ name: 'gugun', fieldName: '시군구', header: { text: '시군구' }, footer: { text: '합계' } },
|
||||
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' } },
|
||||
|
||||
// 기본 정보
|
||||
{ name: 'swgeStrgeNm', fieldName: '시설명', header: { text: '시설명' } },
|
||||
{ name: 'addr', fieldName: '위치', header: { text: '위치' } },
|
||||
{ name: 'rnDt', fieldName: '가동개시 년월일 yyyy-mm-dd', header: { text: '가동개시 년월일 yyyy-mm-dd' } },
|
||||
{ name: 'designFrequency', fieldName: '계획설계빈도', header: { text: '계획설계빈도' } },
|
||||
{ name: 'fcltNm', fieldName: 'fcltNm', header: { text: '시설명' } },
|
||||
{ name: 'fcltPstnCn', fieldName: 'fcltPstnCn', header: { text: '위치' } },
|
||||
{
|
||||
name: 'oprtngStrtYmd',
|
||||
fieldName: 'oprtngStrtYmd',
|
||||
header: { text: '가동개시 년월일 yyyy-mm-dd' },
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
editor: {
|
||||
type: 'date',
|
||||
dropDownWhenClick: true,
|
||||
datetimeFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
},
|
||||
{ name: 'planDesignFreqCnt', fieldName: 'planDesignFreqCnt', header: { text: '계획설계빈도' } },
|
||||
|
||||
// 설치목적
|
||||
{ name: 'floodingPrevention', fieldName: '침수예방', header: { text: '침수예방' } },
|
||||
{ name: 'waterQualityReduction', fieldName: '수질오염저감', header: { text: '수질오염저감' } },
|
||||
{ name: 'sewerageReuseUse', fieldName: '하수재이용용도', header: { text: '하수재이용용도' } },
|
||||
// 설치목적 (O/X)
|
||||
oxYnDropdownColumn({
|
||||
name: "fldngPrvtnYn",
|
||||
fieldName: "fldngPrvtnYn",
|
||||
header: { text: "침수예방" },
|
||||
}),
|
||||
oxYnDropdownColumn({
|
||||
name: "wqPlltnMtgtYn",
|
||||
fieldName: "wqPlltnMtgtYn",
|
||||
header: { text: "수질오염저감" },
|
||||
}),
|
||||
oxYnDropdownColumn({
|
||||
name: "rtlztnUsgYn",
|
||||
fieldName: "rtlztnUsgYn",
|
||||
header: { text: "하수재이용용도" },
|
||||
}),
|
||||
|
||||
// 시설 구조
|
||||
{ name: 'facilityStructure', fieldName: '시설구조(일반지하식,지하터널식,기타)', header: { text: '시설구조(일반지하식,지하터널식,기타)' } },
|
||||
dropdownColumn(
|
||||
{
|
||||
name: "fcltStrctCn",
|
||||
fieldName: "fcltStrctCn",
|
||||
header: { text: "시설구조" },
|
||||
},
|
||||
SEWAGE_RESERVOIR_FCLT_STRCT_VALUES,
|
||||
),
|
||||
|
||||
// 집수구역면적
|
||||
{ name: 'catchmentAreaSize', fieldName: '집수구역면적(㎢)', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{ name: 'wtrStrgZoneArea', fieldName: 'wtrStrgZoneArea', header: { text: '집수구역면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 시설용량
|
||||
{ name: 'facilityCapacity', fieldName: '시설용량(m³)', header: { text: '시설용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'impdFcltCpct', fieldName: 'impdFcltCpct', header: { text: '시설용량(m³)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 펌프시설 > 총양수량
|
||||
{ name: 'totalPumpingCapacity', fieldName: '총양수량(m³/min)', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'grssPpqty', fieldName: 'grssPpqty', header: { text: '총양수량(m³/min)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
|
||||
// 펌프시설 1
|
||||
{ name: 'pump1Diameter', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump1PumpingCapacity', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pump1Count', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump1StandbyCount', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN1DmtrExpln', fieldName: 'pumpFcltN1DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN1PerPpqty', fieldName: 'pumpFcltN1PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN1PumpCnt', fieldName: 'pumpFcltN1PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN1RsrvPumpCnt', fieldName: 'pumpFcltN1RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 펌프시설 2
|
||||
{ name: 'pump2Diameter', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump2PumpingCapacity', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pump2Count', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump2StandbyCount', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN2DmtrExpln', fieldName: 'pumpFcltN2DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN2PerPpqty', fieldName: 'pumpFcltN2PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN2PumpCnt', fieldName: 'pumpFcltN2PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN2RsrvPumpCnt', fieldName: 'pumpFcltN2RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 펌프시설 3
|
||||
{ name: 'pump3Diameter', fieldName: '구경(mm)', header: { text: '구경(mm)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump3PumpingCapacity', fieldName: '대당 양수량(m³/min·대)', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pump3Count', fieldName: '펌프대수', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pump3StandbyCount', fieldName: '예비펌프대수', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN3DmtrExpln', fieldName: 'pumpFcltN3DmtrExpln', header: { text: '구경(mm)' } },
|
||||
{ name: 'pumpFcltN3PerPpqty', fieldName: 'pumpFcltN3PerPpqty', header: { text: '대당 양수량(m³/min·대)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
|
||||
{ name: 'pumpFcltN3PumpCnt', fieldName: 'pumpFcltN3PumpCnt', header: { text: '펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'pumpFcltN3RsrvPumpCnt', fieldName: 'pumpFcltN3RsrvPumpCnt', header: { text: '예비펌프대수' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 부대시설
|
||||
{ name: 'preprocessingFacility', fieldName: '전처리시설유무(유/무)', header: { text: '전처리시설유무(유/무)' } },
|
||||
{ name: 'odorRemovalEquipment', fieldName: '탈취설비유무(유/무)', header: { text: '탈취설비유무(유/무)' } },
|
||||
{ name: 'selfGenerationEquipment', fieldName: '자가발전유무(유/무)', header: { text: '자가발전유무(유/무)' } },
|
||||
{ name: 'prcsgFcltYn', fieldName: 'prcsgFcltYn', header: { text: '전처리시설유무(유/무)' } },
|
||||
{ name: 'ddrztnEqpmYn', fieldName: 'ddrztnEqpmYn', header: { text: '탈취설비유무(유/무)' } },
|
||||
{ name: 'oslfEtgtYn', fieldName: 'oslfEtgtYn', header: { text: '자가발전유무(유/무)' } },
|
||||
|
||||
// 배수 방식
|
||||
{ name: 'drainageMethod', fieldName: '배수방식(자연배수,강제배수)', header: { text: '배수방식(자연배수,강제배수)' } },
|
||||
{ name: 'mltvMthCn', fieldName: 'mltvMthCn', header: { text: '배수방식(자연배수,강제배수)' } },
|
||||
|
||||
// 저류 방식
|
||||
{ name: 'storageMethod', fieldName: '저류방식(직렬,병렬)', header: { text: '저류방식(직렬,병렬)' } },
|
||||
{ name: 'impdMthCn', fieldName: 'impdMthCn', header: { text: '저류방식(직렬,병렬)' } },
|
||||
|
||||
// 저류수 처리방법
|
||||
{ name: 'storageWaterTreatmentMethod', fieldName: '저류수처리방법(자체처리,연계처리)', header: { text: '저류수처리방법(자체처리,연계처리)' } },
|
||||
{ name: 'impdPrcsMthdCn', fieldName: 'impdPrcsMthdCn', header: { text: '저류수처리방법(자체처리,연계처리)' } },
|
||||
|
||||
// 방류 수역
|
||||
{ name: 'dischargeDestination', fieldName: '방류수역(또는 연계처리장명)', header: { text: '방류수역(또는 연계처리장명)' } },
|
||||
{ name: 'dschgWtrsExpln', fieldName: 'dschgWtrsExpln', header: { text: '방류수역(또는 연계처리장명)' } },
|
||||
|
||||
// 최종처분량
|
||||
{ name: 'sedimentDisposalAmount', fieldName: '침사', header: { text: '침사' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'debrisDisposalAmount', fieldName: '협잡물', header: { text: '협잡물' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'prcptnLastDspsQnt', fieldName: 'prcptnLastDspsQnt', header: { text: '침사' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ctmnLastDspsQnt', fieldName: 'ctmnLastDspsQnt', header: { text: '협잡물' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 최종처분방법
|
||||
{ name: 'sedimentDisposalMethod', fieldName: '침사', header: { text: '침사' } },
|
||||
{ name: 'debrisDisposalMethod', fieldName: '협잡물', header: { text: '협잡물' } },
|
||||
{ name: 'prcptnLastDspsMthdExpln', fieldName: 'prcptnLastDspsMthdExpln', header: { text: '침사' } },
|
||||
{ name: 'ctmnLastDspsMthdExpln', fieldName: 'ctmnLastDspsMthdExpln', header: { text: '협잡물' } },
|
||||
|
||||
// 운영실적
|
||||
{ name: 'operationDaysPerYear', fieldName: '가동일수(일/년)', header: { text: '가동일수(일/년)' }, valueType: ValueType.NUMBER }, // 3자리 수만 입력 가능
|
||||
{ name: 'totalDischargePerYear', fieldName: '총배수량(m³/년)', header: { text: '총배수량(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'oprtngDayCnt', fieldName: 'oprtngDayCnt', header: { text: '가동일수(일/년)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 3자리 수만 입력 가능
|
||||
{ name: 'grssMltvQnt', fieldName: 'grssMltvQnt', header: { text: '총배수량(m³/년)' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 비고
|
||||
{ name: 'remarks', fieldName: '비고', header: { text: '비고' } },
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
// Hidden Columns : 시스템 관리를 위한 값
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, },
|
||||
{ name: 'fcltSn', fieldName: 'fcltSn', header: { text: '시설일련번호' }, visible: false, },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
@@ -94,42 +162,45 @@ export const SewageReservoirInpColumn = {
|
||||
name: 'adminArea',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: ['sido', 'gugun'],
|
||||
items: [
|
||||
{ column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] },
|
||||
'sggNm',
|
||||
],
|
||||
},
|
||||
'swgeStrgeNm',
|
||||
'addr',
|
||||
'rnDt',
|
||||
'designFrequency',
|
||||
'fcltNm',
|
||||
'fcltPstnCn',
|
||||
'oprtngStrtYmd',
|
||||
'planDesignFreqCnt',
|
||||
|
||||
{
|
||||
name: 'installationPurpose',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '설치목적(O,X로 표기, 중복체크가능)' },
|
||||
items: [
|
||||
'floodingPrevention',
|
||||
'waterQualityReduction',
|
||||
'sewerageReuseUse',
|
||||
'fldngPrvtnYn',
|
||||
'wqPlltnMtgtYn',
|
||||
'rtlztnUsgYn',
|
||||
],
|
||||
},
|
||||
'facilityStructure',
|
||||
'catchmentAreaSize',
|
||||
'facilityCapacity',
|
||||
'fcltStrctCn',
|
||||
'wtrStrgZoneArea',
|
||||
'impdFcltCpct',
|
||||
|
||||
{
|
||||
name: 'pumpSystem',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설' },
|
||||
items: [
|
||||
'totalPumpingCapacity',
|
||||
'grssPpqty',
|
||||
{
|
||||
name: 'pumpSystem1',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설1' },
|
||||
items: [
|
||||
'pump1Diameter',
|
||||
'pump1PumpingCapacity',
|
||||
'pump1Count',
|
||||
'pump1StandbyCount',
|
||||
'pumpFcltN1DmtrExpln',
|
||||
'pumpFcltN1PerPpqty',
|
||||
'pumpFcltN1PumpCnt',
|
||||
'pumpFcltN1RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -138,10 +209,10 @@ export const SewageReservoirInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설2' },
|
||||
items: [
|
||||
'pump2Diameter',
|
||||
'pump2PumpingCapacity',
|
||||
'pump2Count',
|
||||
'pump2StandbyCount',
|
||||
'pumpFcltN2DmtrExpln',
|
||||
'pumpFcltN2PerPpqty',
|
||||
'pumpFcltN2PumpCnt',
|
||||
'pumpFcltN2RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -150,10 +221,10 @@ export const SewageReservoirInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '펌프시설3' },
|
||||
items: [
|
||||
'pump3Diameter',
|
||||
'pump3PumpingCapacity',
|
||||
'pump3Count',
|
||||
'pump3StandbyCount',
|
||||
'pumpFcltN3DmtrExpln',
|
||||
'pumpFcltN3PerPpqty',
|
||||
'pumpFcltN3PumpCnt',
|
||||
'pumpFcltN3RsrvPumpCnt',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -164,23 +235,23 @@ export const SewageReservoirInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '부대시설' },
|
||||
items: [
|
||||
'preprocessingFacility',
|
||||
'odorRemovalEquipment',
|
||||
'selfGenerationEquipment',
|
||||
'prcsgFcltYn',
|
||||
'ddrztnEqpmYn',
|
||||
'oslfEtgtYn',
|
||||
],
|
||||
},
|
||||
'drainageMethod',
|
||||
'storageMethod',
|
||||
'storageWaterTreatmentMethod',
|
||||
'dischargeDestination',
|
||||
'mltvMthCn',
|
||||
'impdMthCn',
|
||||
'impdPrcsMthdCn',
|
||||
'dschgWtrsExpln',
|
||||
|
||||
{
|
||||
name: 'finalDisposalAmount',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '최종처분량(ton/년)' },
|
||||
items: [
|
||||
'sedimentDisposalAmount',
|
||||
'debrisDisposalAmount',
|
||||
'prcptnLastDspsQnt',
|
||||
'ctmnLastDspsQnt',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -189,8 +260,8 @@ export const SewageReservoirInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '최종처분방법' },
|
||||
items: [
|
||||
'sedimentDisposalMethod',
|
||||
'debrisDisposalMethod',
|
||||
'prcptnLastDspsMthdExpln',
|
||||
'ctmnLastDspsMthdExpln',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -199,15 +270,18 @@ export const SewageReservoirInpColumn = {
|
||||
hideChildHeaders: false,
|
||||
header: { text: '운영실적' },
|
||||
items: [
|
||||
'operationDaysPerYear',
|
||||
'totalDischargePerYear',
|
||||
'oprtngDayCnt',
|
||||
'grssMltvQnt',
|
||||
],
|
||||
},
|
||||
'remarks',
|
||||
'rmrk',
|
||||
'admdstCd',
|
||||
'fcltSn',
|
||||
'admdstNm'
|
||||
],
|
||||
|
||||
rowStartNum: 3,
|
||||
fixedColumns: 2,
|
||||
tabKey: 'sewageReservoirInp',
|
||||
tabKey: 'ST00040001',
|
||||
tabLabel: '하수저류시설',
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ValueType } from "realgrid";
|
||||
|
||||
import type { ColumnConfig, ColumnInfo } from "@/components/type/DataGrid.type";
|
||||
import type { StatisticsLogDetailRow } from "@/entities/statistics/model/statistics-interface";
|
||||
|
||||
const COMMON_FIELD_ORDER = ["rmrk", "sggNm", "admdstCd", "admdstNm"] as const;
|
||||
|
||||
const FIELD_HEADER: Record<string, string> = {
|
||||
rmrk: "비고",
|
||||
sggNm: "시군구",
|
||||
admdstCd: "행정구역코드",
|
||||
admdstNm: "행정구역명",
|
||||
};
|
||||
|
||||
function collectFieldNames(rows: StatisticsLogDetailRow[]): string[] {
|
||||
const keys = new Set<string>();
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
const common = COMMON_FIELD_ORDER.filter((k) => keys.has(k));
|
||||
const rest = [...keys]
|
||||
.filter((k) => !COMMON_FIELD_ORDER.includes(k as (typeof COMMON_FIELD_ORDER)[number]))
|
||||
.sort((a, b) => a.localeCompare(b, "ko"));
|
||||
return [...common, ...rest];
|
||||
}
|
||||
|
||||
function isNumericField(rows: StatisticsLogDetailRow[], fieldName: string): boolean {
|
||||
return rows.some((row) => {
|
||||
const v = row[fieldName];
|
||||
return typeof v === "number" && !Number.isNaN(v);
|
||||
});
|
||||
}
|
||||
|
||||
function buildColumns(rows: StatisticsLogDetailRow[]): ColumnConfig[] {
|
||||
const fieldNames = collectFieldNames(rows);
|
||||
if (fieldNames.length === 0) {
|
||||
return COMMON_FIELD_ORDER.map((fieldName) => ({
|
||||
name: fieldName,
|
||||
fieldName,
|
||||
header: { text: FIELD_HEADER[fieldName] ?? fieldName },
|
||||
editable: false,
|
||||
width: Math.min(340, Math.max(90, fieldName.length * 9 + 28))
|
||||
}));
|
||||
}
|
||||
|
||||
return fieldNames.map((fieldName) => {
|
||||
const numeric = isNumericField(rows, fieldName);
|
||||
return {
|
||||
name: fieldName,
|
||||
fieldName,
|
||||
header: { text: FIELD_HEADER[fieldName] ?? fieldName },
|
||||
editable: false,
|
||||
valueType: numeric ? ValueType.NUMBER : undefined,
|
||||
dataFormat: numeric ? "#,##0.###" : undefined,
|
||||
width: Math.min(340, Math.max(90, fieldName.length * 9 + 28)),
|
||||
visible: fieldName !== "admdstCd",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildLogHistoryDetailColumnInfo(
|
||||
rows: StatisticsLogDetailRow[],
|
||||
): ColumnInfo {
|
||||
const columns = buildColumns(rows);
|
||||
return {
|
||||
columns,
|
||||
columnLayout: columns.map((c) => c.fieldName),
|
||||
};
|
||||
}
|
||||
|
||||
/** 그리드 field 정의와 행 키를 맞춤 (누락 필드 → "") */
|
||||
export function alignLogDetailRowsToColumns(
|
||||
rows: Record<string, string | number>[],
|
||||
columnInfo: ColumnInfo,
|
||||
): Record<string, string | number>[] {
|
||||
const fieldNames = columnInfo.columns.map((c) => c.fieldName);
|
||||
return rows.map((row) => {
|
||||
const aligned: Record<string, string | number> = {};
|
||||
for (const fieldName of fieldNames) {
|
||||
const value = row[fieldName];
|
||||
aligned[fieldName] =
|
||||
value === null || value === undefined ? "" : value;
|
||||
}
|
||||
return aligned;
|
||||
});
|
||||
}
|
||||
|
||||
export const emptyLogHistoryDetailColumnInfo =
|
||||
buildLogHistoryDetailColumnInfo([]);
|
||||
@@ -8,6 +8,12 @@ function toNum(v: unknown): number {
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
/** 소수점 첫째 자리 이후 버림 (반올림 없음) */
|
||||
function truncateToFirstDecimal(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.floor(value * 10) / 10;
|
||||
}
|
||||
|
||||
/** 합계용 valueCallback 생성 */
|
||||
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
|
||||
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
|
||||
@@ -141,7 +147,7 @@ export function createCoverageRateValueCallback(
|
||||
};
|
||||
}
|
||||
|
||||
// 하수도 설치율(%) valueCallback
|
||||
// 하수도 설치율(%) valueCallback — 항목·중간·결과 모두 소수 첫째 자리 이후 버림(반올림 없음)
|
||||
export function createSwrgInstlrtRateValueCallback(
|
||||
numeratorFields: string[],
|
||||
denominatorFields: string[],
|
||||
@@ -152,18 +158,24 @@ export function createSwrgInstlrtRateValueCallback(
|
||||
let sum = 0;
|
||||
for (const f of fields) {
|
||||
const idx = fieldNamesArr.indexOf(f);
|
||||
if (idx >= 0) sum += toNum(values[idx]);
|
||||
if (idx >= 0) {
|
||||
sum += truncateToFirstDecimal(toNum(values[idx]));
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
return truncateToFirstDecimal(sum);
|
||||
};
|
||||
const sum1 = getSum(numeratorFields);
|
||||
const sum2 = getSum(denominatorFields);
|
||||
|
||||
const piplSupprt = toNum(values[fieldNamesArr.indexOf(thirdFields[0] || 'piplSupprt')]);
|
||||
const piplSupprt = truncateToFirstDecimal(
|
||||
toNum(values[fieldNamesArr.indexOf(thirdFields[0] || "piplSupprt")]),
|
||||
);
|
||||
|
||||
const total1 = (sum1 === 0 ? 0 : Math.round(sum2 / sum1 * 100));
|
||||
const ratioPercent =
|
||||
sum1 === 0 ? 0 : truncateToFirstDecimal((sum2 / sum1) * 100);
|
||||
const result = truncateToFirstDecimal((ratioPercent * piplSupprt) / 100);
|
||||
|
||||
return total1 * piplSupprt / 100;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** API·DB 8자리 일자(yyyyMMdd) → 그리드·date 편집기용 yyyy-MM-dd */
|
||||
export function formatCompactYmdForGrid(ymd: string | number | null | undefined): string {
|
||||
const s = String(ymd ?? "").replace(/\D/g, "");
|
||||
if (s.length !== 8) return String(ymd ?? "").trim();
|
||||
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
/** 그리드 값 → API 8자리 일자(yyyyMMdd) */
|
||||
export function toCompactYmd(value: string | number | null | undefined): string {
|
||||
const s = String(value ?? "").replace(/\D/g, "");
|
||||
if (s.length !== 8) return "";
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 설치목적 등 O/X 표기 → 대문자 O 또는 X (그 외는 원문 유지) */
|
||||
export function normalizeOxYnValue(
|
||||
value: string | number | null | undefined,
|
||||
): string {
|
||||
const v = String(value ?? "").trim().toUpperCase();
|
||||
if (v === "O" || v === "X") return v;
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
+48
@@ -3,7 +3,15 @@ import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
SewagePipeMaintainDredgingResponse,
|
||||
SewagePipeMaintainDredgingSaveRequest,
|
||||
SewagePipeMaintainDredgingSaveResponse,
|
||||
SewagePipeMaintainDredgingSubmitRequest,
|
||||
SewagePipeMaintainDredgingSubmitResponse,
|
||||
SewagePipeMaintainRepairResponse,
|
||||
SewagePipeMaintainRepairSaveRequest,
|
||||
SewagePipeMaintainRepairSaveResponse,
|
||||
SewagePipeMaintainRepairSubmitRequest,
|
||||
SewagePipeMaintainRepairSubmitResponse,
|
||||
SewagePipeMaintainRequestParams,
|
||||
SewagePipeMaintainService,
|
||||
} from "../../model/input/statistics-input-sewage-pipe-maintain-interface";
|
||||
@@ -24,6 +32,46 @@ export class SewagePipeMaintainApiService implements SewagePipeMaintainService {
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
public async saveSewagePipeMaintainRepair(
|
||||
param: SewagePipeMaintainRepairSaveRequest,
|
||||
): Promise<SewagePipeMaintainRepairSaveResponse> {
|
||||
const response = await stClient.put<SewagePipeMaintainRepairSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/repair/save`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitSewagePipeMaintainRepair(
|
||||
param: SewagePipeMaintainRepairSubmitRequest,
|
||||
): Promise<SewagePipeMaintainRepairSubmitResponse> {
|
||||
const response = await stClient.put<SewagePipeMaintainRepairSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/repair/submit`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async saveSewagePipeMaintainDredging(
|
||||
param: SewagePipeMaintainDredgingSaveRequest,
|
||||
): Promise<SewagePipeMaintainDredgingSaveResponse> {
|
||||
const response = await stClient.put<SewagePipeMaintainDredgingSaveResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/dredging/save`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async submitSewagePipeMaintainDredging(
|
||||
param: SewagePipeMaintainDredgingSubmitRequest,
|
||||
): Promise<SewagePipeMaintainDredgingSubmitResponse> {
|
||||
const response = await stClient.put<SewagePipeMaintainDredgingSubmitResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe-maintain/dredging/submit`,
|
||||
param,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const sewagePipeMaintainApiService = new SewagePipeMaintainApiService();
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { stClient } from "@/shared/api/client";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import {
|
||||
type StatisticsInputSewageReservoirPumpService,
|
||||
type SewageReservoirApiResponse,
|
||||
type SewageReservoirRequestParams,
|
||||
type RainfallPumpStationApiResponse,
|
||||
} from "../../model/input/statistics-input-sewage-reservoir-pump-interface";
|
||||
|
||||
export class StatisticsInputSewageReservoirPumpApiService implements StatisticsInputSewageReservoirPumpService {
|
||||
/** 하수저류시설 조회 — ST00040001 */
|
||||
public async getSewageReservoirList(params: SewageReservoirRequestParams): Promise<SewageReservoirApiResponse> {
|
||||
const response = await stClient.get<SewageReservoirApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/sewage-reservoir`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** 빗물펌프장 조회 — ST00040002 */
|
||||
public async getRainfallPumpStationList(
|
||||
params: SewageReservoirRequestParams,
|
||||
): Promise<RainfallPumpStationApiResponse> {
|
||||
const response = await stClient.get<RainfallPumpStationApiResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUT}/sewage-reservoir-pump/rainfall-pump`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const statisticsInputSewageReservoirPumpApi = new StatisticsInputSewageReservoirPumpApiService();
|
||||
@@ -287,9 +287,14 @@ export class StatisticsApiService implements StatisticsService {
|
||||
}
|
||||
|
||||
public async getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse> {
|
||||
const { statsCd, ...rest } = param;
|
||||
const params: VerificationResultListRequest = {
|
||||
...rest,
|
||||
...(statsCd && statsCd !== "ALL" ? { statsCd } : {}),
|
||||
};
|
||||
const response = await stClient.get<VerificationResultListResponse>(
|
||||
`${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`,
|
||||
{ params: param },
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
+152
@@ -3,6 +3,8 @@ import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
|
||||
export interface SewagePipeMaintainRepairVerificationResultItem {
|
||||
vrfcCd: string;
|
||||
rsnExpln: string;
|
||||
nowVlCn?: string;
|
||||
compVlCn?: string;
|
||||
}
|
||||
|
||||
/** 필드명 -> 검증 결과 배열 */
|
||||
@@ -93,7 +95,157 @@ export interface SewagePipeMaintainRequestParams {
|
||||
export type SewagePipeMaintainRepairResponse = ApiResponse<SewagePipeMaintainRepairData>;
|
||||
export type SewagePipeMaintainDredgingResponse = ApiResponse<SewagePipeMaintainDredgingData>;
|
||||
|
||||
/** ST00030001 하수관로 유지관리 > 개보수 저장·제출 API 행 */
|
||||
export interface SewagePipeMaintainRepairSaveItem {
|
||||
admdstCd: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
mendngWhol: number;
|
||||
cbsysMendngPrlg: number;
|
||||
sprsysExlnPrlg: number;
|
||||
sprsysDrtwPrlg: number;
|
||||
cbsysPartMendngPrlg: number;
|
||||
sprsysExlnMendngPrlg: number;
|
||||
sprsysDrtwMendngPrlg: number;
|
||||
cbsysMnhlCnt: number;
|
||||
sprsysDrtwMnhlCnt: number;
|
||||
sprsysExlnMnhlCnt: number;
|
||||
drtwRcvprpCnt: number;
|
||||
exlnRcvprpCnt: number;
|
||||
stofcbrCnt: number;
|
||||
otflCnt: number;
|
||||
cctvExmnPrlg: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
/** ST00030001 하수관로 유지관리 > 개보수 저장 API 요청 */
|
||||
export interface SewagePipeMaintainRepairSaveRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
superAdmin?: string;
|
||||
saveList: SewagePipeMaintainRepairSaveItem[];
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairSaveResponse {
|
||||
data?: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** ST00030001 하수관로 유지관리 > 개보수 제출 API 요청 */
|
||||
export interface SewagePipeMaintainRepairSubmitRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
laePicNm: string;
|
||||
laePicTelNo: string;
|
||||
laeKbrdrDeptNm: string;
|
||||
laeKbrdrJbgdNm: string;
|
||||
laeIdfrNm: string;
|
||||
laeIdfrTelNo: string;
|
||||
laeIdfrDeptNm: string;
|
||||
laeIdfrJbgdNm: string;
|
||||
submitList: SewagePipeMaintainRepairSaveItem[];
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainRepairSubmitResponse {
|
||||
data: {
|
||||
finalStatus: string;
|
||||
nvpassed: boolean;
|
||||
};
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */
|
||||
export interface SewagePipeMaintainDredgingSaveItem {
|
||||
admdstCd: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
wholFcltPrlg: number;
|
||||
wholSwagePpl: number;
|
||||
cbsysPplPrlg: number;
|
||||
sprsysExlnPrlg: number;
|
||||
sprsysDrtwPrlg: number;
|
||||
mnhlCnt: number;
|
||||
exlnDrtwRcvprpCnt: number;
|
||||
stofcbrOtflCnt: number;
|
||||
drdgnPlanPrlg: number;
|
||||
drdgnTrgtCtrsDrdgnRt: number;
|
||||
wholPrlgCtrsDrdgnRt: number;
|
||||
drdgnAmt: number;
|
||||
drdgnQnt: number;
|
||||
drdgnUntprcAmt: number;
|
||||
wholDrdgnPrcsQnt: number;
|
||||
rclmQnt: number;
|
||||
oceanEmsnQnt: number;
|
||||
rcycQnt: number;
|
||||
etcQnt: number;
|
||||
rmrk: string | null;
|
||||
}
|
||||
|
||||
/** ST00030002 하수관로 유지관리 > 관로준설 저장 API 요청 */
|
||||
export interface SewagePipeMaintainDredgingSaveRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
superAdmin?: string;
|
||||
saveList: SewagePipeMaintainDredgingSaveItem[];
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainDredgingSaveResponse {
|
||||
data?: string;
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** ST00030002 하수관로 유지관리 > 관로준설 제출 API 요청 */
|
||||
export interface SewagePipeMaintainDredgingSubmitRequest {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
sewerBzmnCd: string;
|
||||
userId: string;
|
||||
laePicNm: string;
|
||||
laePicTelNo: string;
|
||||
laeKbrdrDeptNm: string;
|
||||
laeKbrdrJbgdNm: string;
|
||||
laeIdfrNm: string;
|
||||
laeIdfrTelNo: string;
|
||||
laeIdfrDeptNm: string;
|
||||
laeIdfrJbgdNm: string;
|
||||
submitList: SewagePipeMaintainDredgingSaveItem[];
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainDredgingSubmitResponse {
|
||||
data: {
|
||||
finalStatus: string;
|
||||
nvpassed: boolean;
|
||||
};
|
||||
message?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SewagePipeMaintainService {
|
||||
getSewagePipeMaintainRepair(param: SewagePipeMaintainRequestParams): Promise<SewagePipeMaintainRepairResponse>;
|
||||
getSewagePipeMaintainDredging(param: SewagePipeMaintainRequestParams): Promise<SewagePipeMaintainDredgingResponse>;
|
||||
saveSewagePipeMaintainRepair(
|
||||
param: SewagePipeMaintainRepairSaveRequest,
|
||||
): Promise<SewagePipeMaintainRepairSaveResponse>;
|
||||
submitSewagePipeMaintainRepair(
|
||||
param: SewagePipeMaintainRepairSubmitRequest,
|
||||
): Promise<SewagePipeMaintainRepairSubmitResponse>;
|
||||
saveSewagePipeMaintainDredging(
|
||||
param: SewagePipeMaintainDredgingSaveRequest,
|
||||
): Promise<SewagePipeMaintainDredgingSaveResponse>;
|
||||
submitSewagePipeMaintainDredging(
|
||||
param: SewagePipeMaintainDredgingSubmitRequest,
|
||||
): Promise<SewagePipeMaintainDredgingSubmitResponse>;
|
||||
}
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import type { ApiResponse, SewerageStatusInfo } from "../statistics-interface";
|
||||
|
||||
/** 하수저류시설 Tab1 — `SewageReservoirInpColumn.tabKey` */
|
||||
export const SEWAGE_RESERVOIR_STATS_CD = "ST00040001" as const;
|
||||
|
||||
/** 검증 결과 한 건 */
|
||||
export interface SewageReservoirVerificationResultItem {
|
||||
vrfcCd: string;
|
||||
rsnExpln: string;
|
||||
nowVlCn: string;
|
||||
compVlCn: string;
|
||||
}
|
||||
|
||||
/** 필드명 → 검증 결과 배열 */
|
||||
export type SewageReservoirVerificationResult = Record<
|
||||
string,
|
||||
SewageReservoirVerificationResultItem[]
|
||||
>;
|
||||
|
||||
/** 하수저류시설 조회 `data.commonInfo` */
|
||||
export interface SewageReservoirCommonInfo {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 하수저류시설 조회 `data.sewageReservoirList[]` 한 행.
|
||||
* `SewageReservoirInpColumn` fieldName과 동일 (ctpvNm은 commonInfo에서 그리드에 합침).
|
||||
*/
|
||||
export interface SewageReservoirItem {
|
||||
admdstCd: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
fcltSn: string;
|
||||
fcltNm: string;
|
||||
fcltPstnCn: string;
|
||||
oprtngStrtYmd: string;
|
||||
planDesignFreqCnt: number;
|
||||
fldngPrvtnYn: string;
|
||||
wqPlltnMtgtYn: string;
|
||||
rtlztnUsgYn: string;
|
||||
fcltStrctCn: string | null;
|
||||
wtrStrgZoneArea: number;
|
||||
impdFcltCpct: number;
|
||||
grssPpqty: number;
|
||||
pumpFcltN1DmtrExpln: string;
|
||||
pumpFcltN1PerPpqty: number;
|
||||
pumpFcltN1PumpCnt: number;
|
||||
pumpFcltN1RsrvPumpCnt: number;
|
||||
pumpFcltN2DmtrExpln: string;
|
||||
pumpFcltN2PerPpqty: number;
|
||||
pumpFcltN2PumpCnt: number;
|
||||
pumpFcltN2RsrvPumpCnt: number;
|
||||
pumpFcltN3DmtrExpln: string;
|
||||
pumpFcltN3PerPpqty: number;
|
||||
pumpFcltN3PumpCnt: number;
|
||||
pumpFcltN3RsrvPumpCnt: number;
|
||||
prcsgFcltYn: string;
|
||||
ddrztnEqpmYn: string;
|
||||
oslfEtgtYn: string;
|
||||
mltvMthCn: string | null;
|
||||
impdMthCn: string | null;
|
||||
impdPrcsMthdCn: string | null;
|
||||
dschgWtrsExpln: string;
|
||||
prcptnLastDspsQnt: number;
|
||||
ctmnLastDspsQnt: number;
|
||||
prcptnLastDspsMthdExpln: string;
|
||||
ctmnLastDspsMthdExpln: string;
|
||||
oprtngDayCnt: number;
|
||||
grssMltvQnt: number;
|
||||
rmrk: string | null;
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
}
|
||||
|
||||
export interface SewageReservoirResponseData {
|
||||
statusInfo: SewerageStatusInfo;
|
||||
commonInfo: SewageReservoirCommonInfo;
|
||||
sewageReservoirList: SewageReservoirItem[];
|
||||
}
|
||||
|
||||
export type SewageReservoirApiResponse = ApiResponse<SewageReservoirResponseData> & {
|
||||
message: string;
|
||||
};
|
||||
|
||||
/** 빗물펌프장 Tab2 `data.commonInfo` */
|
||||
export interface RainfallPumpStationCommonInfo {
|
||||
statsYr: string;
|
||||
ctpvNm: string;
|
||||
sewerBzmnCd: string;
|
||||
statsCd: string;
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 빗물펌프장 조회 `data.rainfallPumpStationList[]` 한 행.
|
||||
* `RainfallPumpStationInpColumn` fieldName과 동일 (ctpvNm은 commonInfo에서 그리드에 합침).
|
||||
*/
|
||||
export interface RainfallPumpStationItem {
|
||||
admdstCd: string;
|
||||
sggNm: string;
|
||||
admdstNm: string;
|
||||
fcltSn: string;
|
||||
fcltNm: string;
|
||||
fcltPstnCn: string;
|
||||
fcltyKndCn: string;
|
||||
oprtngStrtYmd: string;
|
||||
planDesignFreqCnt: number;
|
||||
wtrStrgZoneArea: number;
|
||||
rsvrYn: string;
|
||||
rsvrImpndQnt: number;
|
||||
abstnRgnImpdQnt: number;
|
||||
grssPpqty: number;
|
||||
|
||||
pumpFcltN1DmtrExpln: string;
|
||||
pumpFcltN1PerPpqty: number;
|
||||
pumpFcltN1PumpCnt: number;
|
||||
pumpFcltN1RsrvPumpCnt: number;
|
||||
|
||||
pumpFcltN2DmtrExpln: string;
|
||||
pumpFcltN2PerPpqty: number;
|
||||
pumpFcltN2PumpCnt: number;
|
||||
pumpFcltN2RsrvPumpCnt: number;
|
||||
|
||||
pumpFcltN3DmtrExpln: string;
|
||||
pumpFcltN3PerPpqty: number;
|
||||
pumpFcltN3PumpCnt: number;
|
||||
pumpFcltN3RsrvPumpCnt: number;
|
||||
|
||||
prcsgFcltYn: string;
|
||||
ddrztnEqpmYn: string;
|
||||
oslfEtgtYn: string;
|
||||
dschgWtrsExpln: string;
|
||||
|
||||
prcptnLastDspsQnt: number;
|
||||
ctmnLastDspsQnt: number;
|
||||
prcptnLastDspsMthdExpln: string;
|
||||
ctmnLastDspsMthdExpln: string;
|
||||
|
||||
oprtngDayCnt: number;
|
||||
grssMltvQnt: number;
|
||||
|
||||
rmrk: string | null;
|
||||
verificationResult?: SewageReservoirVerificationResult;
|
||||
}
|
||||
|
||||
export interface RainfallPumpStationResponseData {
|
||||
statusInfo: SewerageStatusInfo;
|
||||
commonInfo: RainfallPumpStationCommonInfo;
|
||||
rainfallPumpStationList: RainfallPumpStationItem[];
|
||||
}
|
||||
|
||||
export type RainfallPumpStationApiResponse = ApiResponse<RainfallPumpStationResponseData> & {
|
||||
message: string;
|
||||
};
|
||||
|
||||
/** 하수저류시설(ST00040001) 조회 요청 파라미터 */
|
||||
export interface SewageReservoirRequestParams {
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
sewerBzmnCd?: string;
|
||||
statsYr?: string;
|
||||
mode?: string;
|
||||
fcltNm?: string;
|
||||
}
|
||||
|
||||
export interface StatisticsInputSewageReservoirPumpService {
|
||||
getSewageReservoirList(params: SewageReservoirRequestParams): Promise<SewageReservoirApiResponse>;
|
||||
getRainfallPumpStationList(
|
||||
params: SewageReservoirRequestParams,
|
||||
): Promise<RainfallPumpStationApiResponse>;
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,6 @@ export interface SewerageSupplyRateCommonInfo {
|
||||
sewerBzmnWholPpltnCnt: number;
|
||||
piplFcltyExtn: number;
|
||||
piplPlanExtn: number;
|
||||
swagePplPsplyRt: number;
|
||||
ctpvNm: string;
|
||||
statsYr: string;
|
||||
statsCd: string;
|
||||
@@ -62,6 +61,7 @@ export interface SewerageSupplyRateItem {
|
||||
staaPsplyRt: number;
|
||||
altdPrcsPsplyRt: number;
|
||||
sewerInstlRt: number;
|
||||
swagePplPsplyRt: number;
|
||||
rmrk: string | null;
|
||||
verificationResult?: SewerageSupplyRateVerificationResult | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const STATISTICS_ROUTE = "/statistics";
|
||||
export const STATISTICS_TAB_QUERY_KEY = "tab";
|
||||
|
||||
export function buildStatisticsTabUrl(tabId: string): string {
|
||||
return `${STATISTICS_ROUTE}?${STATISTICS_TAB_QUERY_KEY}=${encodeURIComponent(tabId)}`;
|
||||
}
|
||||
|
||||
export function isStatisticsPathname(
|
||||
pathname: string | null,
|
||||
): pathname is typeof STATISTICS_ROUTE {
|
||||
return pathname === STATISTICS_ROUTE;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { findMenuItemByUrlPath } from "@/shared/lib/stats-cd-input-navigation";
|
||||
import { getTabStoreState } from "@/store/tab-store";
|
||||
|
||||
import {
|
||||
STATISTICS_ROUTE,
|
||||
STATISTICS_TAB_QUERY_KEY,
|
||||
buildStatisticsTabUrl,
|
||||
isStatisticsPathname,
|
||||
} from "./statistics-tab-url";
|
||||
|
||||
type UseStatisticsTabUrlSyncParams = {
|
||||
pathname: string | null;
|
||||
menuList: MenuItem[] | null;
|
||||
activeTabId: string | null;
|
||||
addTab: (menuItem: MenuItem) => boolean;
|
||||
addTabForGrid: (menuItem: MenuItem) => void;
|
||||
setCurrentMenuInfo: (item: MenuItem) => void;
|
||||
};
|
||||
|
||||
/** `/statistics?tab=` ↔ Zustand 활성 탭 동기화 */
|
||||
export function useStatisticsTabUrlSync({
|
||||
pathname,
|
||||
menuList,
|
||||
activeTabId,
|
||||
addTab,
|
||||
addTabForGrid,
|
||||
setCurrentMenuInfo,
|
||||
}: UseStatisticsTabUrlSyncParams): {
|
||||
syncTabToUrl: (tabId: string) => void;
|
||||
} {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams?.get(STATISTICS_TAB_QUERY_KEY) ?? null;
|
||||
const skipUrlToStoreRef = useRef(false);
|
||||
|
||||
const syncTabToUrl = (tabId: string) => {
|
||||
if (!isStatisticsPathname(pathname)) {
|
||||
router.push(buildStatisticsTabUrl(tabId));
|
||||
return;
|
||||
}
|
||||
if (tabParam === tabId) return;
|
||||
skipUrlToStoreRef.current = true;
|
||||
router.replace(buildStatisticsTabUrl(tabId), { scroll: false });
|
||||
};
|
||||
|
||||
// URL ?tab → 탭 스토어
|
||||
useEffect(() => {
|
||||
if (!isStatisticsPathname(pathname) || !menuList) return;
|
||||
if (skipUrlToStoreRef.current) {
|
||||
skipUrlToStoreRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tabParam) return;
|
||||
if (getTabStoreState().activeTabId === tabParam) return;
|
||||
|
||||
const menu = findMenuItemByUrlPath(menuList, tabParam);
|
||||
if (!menu) return;
|
||||
|
||||
setCurrentMenuInfo(menu);
|
||||
const ok = addTab(menu);
|
||||
if (ok) {
|
||||
addTabForGrid(menu);
|
||||
}
|
||||
}, [
|
||||
pathname,
|
||||
menuList,
|
||||
tabParam,
|
||||
addTab,
|
||||
addTabForGrid,
|
||||
setCurrentMenuInfo,
|
||||
]);
|
||||
|
||||
// 활성 탭 → URL (탭 바 전환·persist 복원 포함)
|
||||
useEffect(() => {
|
||||
if (!isStatisticsPathname(pathname)) return;
|
||||
|
||||
if (!activeTabId) {
|
||||
if (tabParam) {
|
||||
skipUrlToStoreRef.current = true;
|
||||
router.replace(STATISTICS_ROUTE, { scroll: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tabParam === activeTabId) return;
|
||||
|
||||
skipUrlToStoreRef.current = true;
|
||||
router.replace(buildStatisticsTabUrl(activeTabId), { scroll: false });
|
||||
}, [pathname, activeTabId, tabParam, router]);
|
||||
|
||||
return { syncTabToUrl };
|
||||
}
|
||||
@@ -8,6 +8,12 @@ function toNum(v: unknown): number {
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
/** 소수점 첫째 자리 이후 버림 (반올림 없음) */
|
||||
function truncateToFirstDecimal(value: number): number {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.floor(value * 10) / 10;
|
||||
}
|
||||
|
||||
/** 합계용 valueCallback 생성 */
|
||||
export function createSumValueCallback(fieldNames: string[]): DataFieldValueCallback {
|
||||
return (_prod, _dataRow, _fieldName, fieldNamesArr, values) => {
|
||||
@@ -57,7 +63,7 @@ export function createCoverageRateValueCallback(
|
||||
};
|
||||
}
|
||||
|
||||
// 하수도 설치율(%) valueCallback
|
||||
// 하수도 설치율(%) valueCallback — 항목·중간·결과 모두 소수 첫째 자리 이후 버림(반올림 없음)
|
||||
export function createSwrgInstlrtRateValueCallback(
|
||||
numeratorFields: string[],
|
||||
denominatorFields: string[],
|
||||
@@ -68,18 +74,24 @@ export function createSwrgInstlrtRateValueCallback(
|
||||
let sum = 0;
|
||||
for (const f of fields) {
|
||||
const idx = fieldNamesArr.indexOf(f);
|
||||
if (idx >= 0) sum += toNum(values[idx]);
|
||||
if (idx >= 0) {
|
||||
sum += truncateToFirstDecimal(toNum(values[idx]));
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
return truncateToFirstDecimal(sum);
|
||||
};
|
||||
const sum1 = getSum(numeratorFields);
|
||||
const sum2 = getSum(denominatorFields);
|
||||
|
||||
const piplSupprt = toNum(values[fieldNamesArr.indexOf(thirdFields[0] || 'piplSupprt')]);
|
||||
const piplSupprt = truncateToFirstDecimal(
|
||||
toNum(values[fieldNamesArr.indexOf(thirdFields[0] || "piplSupprt")]),
|
||||
);
|
||||
|
||||
const total1 = (sum1 === 0 ? 0 : Math.round(sum2 / sum1 * 100));
|
||||
const ratioPercent =
|
||||
sum1 === 0 ? 0 : truncateToFirstDecimal((sum2 / sum1) * 100);
|
||||
const result = truncateToFirstDecimal((ratioPercent * piplSupprt) / 100);
|
||||
|
||||
return total1 * piplSupprt / 100;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user