Merge remote-tracking branch 'origin/iipbs/st/dev'
Deploy Frontend / deploy (push) Successful in 4m23s

This commit is contained in:
2026-05-27 17:20:24 +09:00
30 changed files with 3973 additions and 652 deletions
+249 -20
View File
@@ -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>
+50
View File
@@ -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;
}
+210 -11
View File
@@ -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,
})}
+62 -28
View File
@@ -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;
@@ -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,
@@ -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,
@@ -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: '빗물펌프장',
};
@@ -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();
}
@@ -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();
@@ -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;
}
@@ -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>;
}
@@ -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>;
}
@@ -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;
};
}