Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/sein

This commit is contained in:
yangsein
2026-06-16 16:44:28 +09:00
13 changed files with 692 additions and 188 deletions
+20 -170
View File
@@ -1,54 +1,32 @@
"use client";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } 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 { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import {
menuMatchesStatisticsTabId,
resolveStatisticsTabId,
} from "@/shared/lib/statistics-menu-url";
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";
import { getTabStoreState, useTabStore } from "@/store/tab-store";
const appConfig = getConfig();
@@ -58,42 +36,23 @@ const appSiteId = getCookie("site-id-key") ?? "";
const isLoggedIn = !!userId;
function WithBaseLayoutInner({
children,
}: {
children: React.ReactNode;
}) {
function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const router = useRouter();
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);
@@ -108,16 +67,11 @@ function WithBaseLayoutInner({
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 { syncTabToUrl } = useStatisticsTabUrlSync({
pathname,
menuList,
@@ -129,41 +83,27 @@ function WithBaseLayoutInner({
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;
@@ -172,20 +112,13 @@ function WithBaseLayoutInner({
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);
@@ -193,155 +126,104 @@ function WithBaseLayoutInner({
const gridActiveId = activeTab?.menuItem.menuEngNm;
if (gridActiveId) {
getGridTabStoreState().setActiveTab(gridActiveId);
}
}
}, [menuList, tabs, setActiveTab]);
// 활성 탭의 메뉴를 찾아서 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) {
if (menuMatchesStatisticsTabId(item, 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) => {
if (item.urlPath.includes("/cms/")) return;
setCurrentMenuInfo(item);
if (isVerificationSelectionReportMenu(item)) {
setReportPanelOpen(true);
return;
}
if (isBbsNoticeMenu(item)) {
if (item.urlPath) {
router.push(item.urlPath);
}
return;
}
const addTabResult: boolean = addTab(item);
// packages/rsone의 스토어에도 동기화
if (addTabResult) {
addTabForGrid(item);
if (item.urlPath) {
syncTabToUrl(item.urlPath);
syncTabToUrl(resolveStatisticsTabId(item.urlPath));
}
}
if (!addTabResult) {
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
기존 화면을 닫은 후 이용해주세요.`);
}
},
[
setCurrentMenuInfo,
addTab,
@@ -354,70 +236,38 @@ function WithBaseLayoutInner({
syncTabToUrl,
router,
],
);
return (
<BaseLayout
queryClient={queryClient}
tokenManager={tokenManager}
currentPath={currentPath}
handleMenuClick={handleMenuClick}
>
<div className="flex flex-1 h-full">
<div className="flex h-full flex-1">
{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>
);
}
@@ -8,11 +8,19 @@ import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/inp
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type {
CauserBurdenItem,
CauserBurdenSaveItem,
CauserBurdenSaveRequest,
SewerageChargeItem,
SewerageChargeSaveItem,
SewerageChargeSaveRequest,
SewerageFinanceRequestParams,
SewerageFinanceSettleItem,
SewerageFinanceSettleSaveItem,
SewerageFinanceSettleSaveRequest,
SewerageFinanceVerificationResult,
SewerageUsageFeeItem,
SewerageUsageFeeSaveItem,
SewerageUsageFeeSaveRequest,
} from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-interface";
@@ -141,6 +149,163 @@ function mapRegionalListToGridRows<
});
}
function normalizeGridString(value: unknown): string {
if (value === null || value === undefined) return "";
return typeof value === "string" ? value : String(value);
}
function normalizeGridRmrk(value: unknown): string | null {
if (value === null || value === undefined) return null;
const s = typeof value === "string" ? value : String(value);
return s.trim() === "" ? null : s;
}
function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) {
const columnFormatMap = new Map<string, string>();
columns.forEach((col) => {
if (col.fieldName && col.dataFormat) {
columnFormatMap.set(col.fieldName, col.dataFormat);
}
});
return (value: string | number | null | undefined, fieldName: string): number => {
if (value === null || value === undefined || value === "") return 0;
const num =
typeof value === "string" ? parseFloat(value.replace(/,/g, "")) : value;
if (isNaN(num)) return 0;
const format = columnFormatMap.get(fieldName);
if (!format) return num;
const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0;
return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num);
};
}
function resolveSewerageFinanceTab(tabKey: string) {
return {
isSettleTab: tabKey === SewerageFinanceSettleInpColumn.tabKey,
isChargeTab: tabKey === SewerageChargeInpColumn.tabKey,
isUsageFeeTab: tabKey === SewerageUsageFeeInpColumn.tabKey,
isCauserBurdenTab: tabKey === CauserBurdenInpColumn.tabKey,
};
}
function buildSewerageFinanceSettleSaveList(
currentData: Record<string, string | number>[],
): SewerageFinanceSettleSaveItem[] {
const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"),
mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"),
wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"),
sewerUseAmt: formatNumber(d.sewerUseAmt, "sewerUseAmt"),
orgprBrdnAmt: formatNumber(d.orgprBrdnAmt, "orgprBrdnAmt"),
etcIncmAmt: formatNumber(d.etcIncmAmt, "etcIncmAmt"),
crfwAmt: formatNumber(d.crfwAmt, "crfwAmt"),
lbltAmt: formatNumber(d.lbltAmt, "lbltAmt"),
tmplntFcltAmt: formatNumber(d.tmplntFcltAmt, "tmplntFcltAmt"),
tmplntMendngAmt: formatNumber(d.tmplntMendngAmt, "tmplntMendngAmt"),
tmplntOperKeepAmt: formatNumber(d.tmplntOperKeepAmt, "tmplntOperKeepAmt"),
tmplntEtcAmt: formatNumber(d.tmplntEtcAmt, "tmplntEtcAmt"),
swagePplFcltAmt: formatNumber(d.swagePplFcltAmt, "swagePplFcltAmt"),
swagePplMendngAmt: formatNumber(d.swagePplMendngAmt, "swagePplMendngAmt"),
swagePplOperKeepAmt: formatNumber(d.swagePplOperKeepAmt, "swagePplOperKeepAmt"),
swagePplEtcAmt: formatNumber(d.swagePplEtcAmt, "swagePplEtcAmt"),
swageImpdFcltAmt: formatNumber(d.swageImpdFcltAmt, "swageImpdFcltAmt"),
swageImpdMendngAmt: formatNumber(d.swageImpdMendngAmt, "swageImpdMendngAmt"),
swageImpdOperKeepAmt: formatNumber(d.swageImpdOperKeepAmt, "swageImpdOperKeepAmt"),
swageImpdFcltEtcAmt: formatNumber(d.swageImpdFcltEtcAmt, "swageImpdFcltEtcAmt"),
etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"),
etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"),
etcAmt: formatNumber(d.etcAmt, "etcAmt"),
rmrk: normalizeGridRmrk(d.rmrk),
fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"),
usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"),
tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"),
wholSwagePplAmt: formatNumber(d.wholSwagePplAmt, "wholSwagePplAmt"),
wholSwageImpdFcltAmt: formatNumber(d.wholSwageImpdFcltAmt, "wholSwageImpdFcltAmt"),
etcWholAmt: formatNumber(d.etcWholAmt, "etcWholAmt"),
}));
}
function buildSewerageChargeSaveList(
currentData: Record<string, string | number>[],
): SewerageChargeSaveItem[] {
const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"),
rmrk: normalizeGridRmrk(d.rmrk),
avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"),
grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"),
rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"),
}));
}
function buildSewerageUsageFeeSaveList(
currentData: Record<string, string | number>[],
): SewerageUsageFeeSaveItem[] {
const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"),
hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"),
hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"),
taskFrLevyAmt: formatNumber(d.taskFrLevyAmt, "taskFrLevyAmt"),
taskFrLevyQnt: formatNumber(d.taskFrLevyQnt, "taskFrLevyQnt"),
taskFrUntprcAmt: formatNumber(d.taskFrUntprcAmt, "taskFrUntprcAmt"),
fbuLevyAmt: formatNumber(d.fbuLevyAmt, "fbuLevyAmt"),
fbuLevyQnt: formatNumber(d.fbuLevyQnt, "fbuLevyQnt"),
fbuUntprcAmt: formatNumber(d.fbuUntprcAmt, "fbuUntprcAmt"),
bthhsFrSpcs1LevyAmt: formatNumber(d.bthhsFrSpcs1LevyAmt, "bthhsFrSpcs1LevyAmt"),
bthhsFrSpcs1LevyQnt: formatNumber(d.bthhsFrSpcs1LevyQnt, "bthhsFrSpcs1LevyQnt"),
bthhsFrSpcs1UntprcAmt: formatNumber(d.bthhsFrSpcs1UntprcAmt, "bthhsFrSpcs1UntprcAmt"),
bthhsFrSpcs2LevyAmt: formatNumber(d.bthhsFrSpcs2LevyAmt, "bthhsFrSpcs2LevyAmt"),
bthhsFrSpcs2LevyQnt: formatNumber(d.bthhsFrSpcs2LevyQnt, "bthhsFrSpcs2LevyQnt"),
bthhsFrSpcs2UntprcAmt: formatNumber(d.bthhsFrSpcs2UntprcAmt, "bthhsFrSpcs2UntprcAmt"),
induLevyAmt: formatNumber(d.induLevyAmt, "induLevyAmt"),
induLevyQnt: formatNumber(d.induLevyQnt, "induLevyQnt"),
induUntprcAmt: formatNumber(d.induUntprcAmt, "induUntprcAmt"),
etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"),
etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"),
etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"),
rmrk: normalizeGridRmrk(d.rmrk),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
levyQnt: formatNumber(d.levyQnt, "levyQnt"),
}));
}
function buildCauserBurdenSaveList(
currentData: Record<string, string | number>[],
): CauserBurdenSaveItem[] {
const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns);
return currentData.map((d) => ({
admdstCd: normalizeGridString(d.admdstCd),
sggNm: normalizeGridString(d.sggNm),
admdstNm: normalizeGridString(d.admdstNm),
levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"),
levyAmt: formatNumber(d.levyAmt, "levyAmt"),
mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"),
mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"),
othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"),
othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"),
rmrk: normalizeGridRmrk(d.rmrk),
trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"),
}));
}
// 하수도 통계 관리 > 09. 하수도재정
const SewerageFinanceInp = () => {
const { setMessage: alert } = useAlertStore((state) => state);
@@ -541,7 +706,144 @@ const SewerageFinanceInp = () => {
};
const saveFunc = () => {
// 저장 로직
void (async () => {
const tabKey = activeTabRef.current;
const { isSettleTab, isChargeTab, isUsageFeeTab, isCauserBurdenTab } =
resolveSewerageFinanceTab(tabKey);
if (!isSettleTab && !isChargeTab && !isUsageFeeTab && !isCauserBurdenTab) {
alert("저장할 수 없는 탭입니다.");
return;
}
if (!lastSearchedValues || !searchParamRef.current) {
alert("먼저 조회를 진행해주세요.");
return;
}
if (!buildGugunOkForToolbar(lastSearchedValues)) {
alert("시군구를 선택해주세요.");
return;
}
if (!(await confirm("저장 하시겠습니까?"))) return;
const currentData = getTabGridRows(tabKey);
if (!currentData.length) {
alert("저장할 데이터가 없습니다.");
return;
}
const baseRequest = {
statsYr: lastSearchedValues.year ?? "",
ctpvNm: lastSearchedValues.sido ?? "",
sggNm:
lastSearchedValues.gugun === "ALL"
? ""
: (lastSearchedValues.gugun ?? ""),
sewerBzmnCd: lastSearchedValues.bsnmCd ?? "",
};
try {
setIsLoading(true);
const userInfo = await getUserInfo();
if (isSettleTab) {
const param: SewerageFinanceSettleSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildSewerageFinanceSettleSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
const response =
await statisticsInputSewerageFinanceApi.saveSewerageFinanceSettle(param);
if (response.status === "success") {
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
false,
false,
);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
return;
}
if (isChargeTab) {
const param: SewerageChargeSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildSewerageChargeSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
const response =
await statisticsInputSewerageFinanceApi.saveSewerageCharge(param);
if (response.status === "success") {
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
false,
false,
);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
return;
}
if (isUsageFeeTab) {
const param: SewerageUsageFeeSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildSewerageUsageFeeSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
const response =
await statisticsInputSewerageFinanceApi.saveSewerageUsageFee(param);
if (response.status === "success") {
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
false,
false,
);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
return;
}
const param: CauserBurdenSaveRequest = {
...baseRequest,
userId: userInfo.userUnqId,
saveList: buildCauserBurdenSaveList(currentData),
...(isAdminInfo ? { superAdmin: "superAdmin" } : {}),
};
const response = await statisticsInputSewerageFinanceApi.saveCauserBurden(param);
if (response.status === "success") {
await loadActiveTabData(
tabKey,
searchParamRef.current,
lastSearchedValues,
false,
false,
);
alert("저장되었습니다.");
} else {
alert(response.message || "데이터 저장 중 오류가 발생했습니다.");
}
} catch {
alert("데이터 저장 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
})();
};
const openVerificationHistoryDialog = () => {
@@ -58,7 +58,7 @@ export const SewerageChargeInpColumn = {
},
// 비고
{ name: 'rmrk', fieldName: '비고', header: { text: '비고' } },
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
// hidden
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, },
@@ -59,37 +59,37 @@ export const SewerageUsageFeeInpColumn = {
// 가정용
{ name: 'hshusLevyAmt', fieldName: 'hshusLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'hshusLevyQnt', fieldName: 'hshusLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'hshusUntprcAmt', fieldName: 'hshusUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 업무용
{ name: 'taskFrLevyAmt', fieldName: 'taskFrLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'taskFrLevyQnt', fieldName: 'taskFrLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'taskFrUntprcAmt', fieldName: 'taskFrUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 영업용(일반용)
{ name: 'fbuLevyAmt', fieldName: 'fbuLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'fbuLevyQnt', fieldName: 'fbuLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'fbuUntprcAmt', fieldName: 'fbuUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 욕탕1종(대중탕용)
{ name: 'bthhsFrSpcs1LevyAmt', fieldName: 'bthhsFrSpcs1LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs1LevyQnt', fieldName: 'bthhsFrSpcs1LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs1UntprcAmt', fieldName: 'bthhsFrSpcs1UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 욕탕2종
{ name: 'bthhsFrSpcs2LevyAmt', fieldName: 'bthhsFrSpcs2LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs2LevyQnt', fieldName: 'bthhsFrSpcs2LevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'bthhsFrSpcs2UntprcAmt', fieldName: 'bthhsFrSpcs2UntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 산업 및 공업용
{ name: 'induLevyAmt', fieldName: 'induLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'induLevyQnt', fieldName: 'induLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'induUntprcAmt', fieldName: 'induUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 기타
{ name: 'etcLevyAmt', fieldName: 'etcLevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'etcLevyQnt', fieldName: 'etcLevyQnt', header: { text: '(천톤)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' },
{ name: 'etcLevyUntprcAmt', fieldName: 'etcLevyUntprcAmt', header: { text: '(원/톤)' }, editable: false, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', styleName: 'rg-cell-readonly' },
// 비고
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
@@ -5,12 +5,20 @@ import { ENDPOINT } from "@/shared/config";
import {
type CauserBurdenApiResponse,
type CauserBurdenRequestParams,
type CauserBurdenSaveRequest,
type CauserBurdenSaveResponse,
type SewerageChargeApiResponse,
type SewerageChargeRequestParams,
type SewerageChargeSaveRequest,
type SewerageChargeSaveResponse,
type SewerageFinanceSettleApiResponse,
type SewerageFinanceSettleRequestParams,
type SewerageFinanceSettleSaveRequest,
type SewerageFinanceSettleSaveResponse,
type SewerageUsageFeeApiResponse,
type SewerageUsageFeeRequestParams,
type SewerageUsageFeeSaveRequest,
type SewerageUsageFeeSaveResponse,
type StatisticsInputSewerageFinanceService,
} from "../../model/input/statistics-input-sewerage-finance-interface";
@@ -30,6 +38,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data;
}
/** 하수도재정 결산 저장 — ST00090001 */
public async saveSewerageFinanceSettle(
request: SewerageFinanceSettleSaveRequest,
): Promise<SewerageFinanceSettleSaveResponse> {
const response = await stClient.put<SewerageFinanceSettleSaveResponse>(
`${BASE}/settle/save`,
request,
);
return response.data;
}
/** 하수도 요금 조회 — ST00090002 */
public async getSewerageChargeList(
params: SewerageChargeRequestParams,
@@ -41,6 +60,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data;
}
/** 하수도 요금 저장 — ST00090002 */
public async saveSewerageCharge(
request: SewerageChargeSaveRequest,
): Promise<SewerageChargeSaveResponse> {
const response = await stClient.put<SewerageChargeSaveResponse>(
`${BASE}/charge/save`,
request,
);
return response.data;
}
/** 하수도 사용료 조회 — ST00090003 */
public async getSewerageUsageFeeList(
params: SewerageUsageFeeRequestParams,
@@ -52,6 +82,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data;
}
/** 하수도 사용료 저장 — ST00090003 */
public async saveSewerageUsageFee(
request: SewerageUsageFeeSaveRequest,
): Promise<SewerageUsageFeeSaveResponse> {
const response = await stClient.put<SewerageUsageFeeSaveResponse>(
`${BASE}/usage-fee/save`,
request,
);
return response.data;
}
/** 원인자 부담금 조회 — ST00090004 */
public async getCauserBurdenList(
params: CauserBurdenRequestParams,
@@ -62,6 +103,17 @@ export class StatisticsInputSewerageFinanceApiService
);
return response.data;
}
/** 원인자 부담금 저장 — ST00090004 */
public async saveCauserBurden(
request: CauserBurdenSaveRequest,
): Promise<CauserBurdenSaveResponse> {
const response = await stClient.put<CauserBurdenSaveResponse>(
`${BASE}/causer-burden/save`,
request,
);
return response.data;
}
}
export const statisticsInputSewerageFinanceApi =
@@ -86,6 +86,63 @@ export interface SewerageFinanceSettleRequestParams {
mode?: string;
}
/**
* ST00090001 저장 API `saveList[]` 한 행.
* `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명).
*/
export interface SewerageFinanceSettleSaveItem {
admdstCd: string;
sggNm: string;
admdstNm: string;
cnterGovSprtAmt: number;
mctpvLcexpAmt: number;
wareaSggLclAmt: number;
sewerUseAmt: number;
orgprBrdnAmt: number;
etcIncmAmt: number;
crfwAmt: number;
lbltAmt: number;
tmplntFcltAmt: number;
tmplntMendngAmt: number;
tmplntOperKeepAmt: number;
tmplntEtcAmt: number;
swagePplFcltAmt: number;
swagePplMendngAmt: number;
swagePplOperKeepAmt: number;
swagePplEtcAmt: number;
swageImpdFcltAmt: number;
swageImpdMendngAmt: number;
swageImpdOperKeepAmt: number;
swageImpdFcltEtcAmt: number;
etcAcdntCrfwAmt: number;
etcNrcpAmt: number;
etcAmt: number;
rmrk: string | null;
fnnByWholPjtcoAmt: number;
usgByWholPjtcoAmt: number;
tmplntWholAmt: number;
wholSwagePplAmt: number;
wholSwageImpdFcltAmt: number;
etcWholAmt: number;
}
/** ST00090001 저장 API 요청 */
export interface SewerageFinanceSettleSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
saveList: SewerageFinanceSettleSaveItem[];
}
export interface SewerageFinanceSettleSaveResponse {
status: string;
message?: string;
data?: string;
}
/** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */
export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const;
@@ -135,6 +192,40 @@ export interface SewerageChargeRequestParams {
mode?: string;
}
/**
* ST00090002 저장 API `saveList[]` 한 행.
* `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명).
*/
export interface SewerageChargeSaveItem {
admdstCd: string;
sggNm: string;
admdstNm: string;
anlLevyQnt: number;
levyAmt: number;
grssPrmcoAmt: number;
rmrk: string | null;
avgUntprcAmt: number;
grssUnitPrmcoAmt: number;
rlztnRt: number;
}
/** ST00090002 저장 API 요청 */
export interface SewerageChargeSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
saveList: SewerageChargeSaveItem[];
}
export interface SewerageChargeSaveResponse {
status: string;
message?: string;
data?: string;
}
/** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */
export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const;
@@ -201,6 +292,57 @@ export interface SewerageUsageFeeRequestParams {
mode?: string;
}
/**
* ST00090003 저장 API `saveList[]` 한 행.
* `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명).
*/
export interface SewerageUsageFeeSaveItem {
admdstCd: string;
sggNm: string;
admdstNm: string;
hshusLevyAmt: number;
hshusLevyQnt: number;
hshusUntprcAmt: number;
taskFrLevyAmt: number;
taskFrLevyQnt: number;
taskFrUntprcAmt: number;
fbuLevyAmt: number;
fbuLevyQnt: number;
fbuUntprcAmt: number;
bthhsFrSpcs1LevyAmt: number;
bthhsFrSpcs1LevyQnt: number;
bthhsFrSpcs1UntprcAmt: number;
bthhsFrSpcs2LevyAmt: number;
bthhsFrSpcs2LevyQnt: number;
bthhsFrSpcs2UntprcAmt: number;
induLevyAmt: number;
induLevyQnt: number;
induUntprcAmt: number;
etcLevyAmt: number;
etcLevyQnt: number;
etcLevyUntprcAmt: number;
rmrk: string | null;
levyAmt: number;
levyQnt: number;
}
/** ST00090003 저장 API 요청 */
export interface SewerageUsageFeeSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
saveList: SewerageUsageFeeSaveItem[];
}
export interface SewerageUsageFeeSaveResponse {
status: string;
message?: string;
data?: string;
}
/** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */
export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const;
@@ -252,6 +394,41 @@ export interface CauserBurdenRequestParams {
mode?: string;
}
/**
* ST00090004 저장 API `saveList[]` 한 행.
* `CauserBurdenInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명).
*/
export interface CauserBurdenSaveItem {
admdstCd: string;
sggNm: string;
admdstNm: string;
levyEvntcsCnt: number;
levyAmt: number;
mltvEqpmEvntcsCnt: number;
mltvEqpmAmt: number;
othrCstrnOthrActrCnt: number;
othrCstrnOthrActrAmt: number;
rmrk: string | null;
trgtByWholClctnAmt: number;
}
/** ST00090004 저장 API 요청 */
export interface CauserBurdenSaveRequest {
statsYr: string;
ctpvNm: string;
sggNm: string;
sewerBzmnCd: string;
userId: string;
superAdmin?: string;
saveList: CauserBurdenSaveItem[];
}
export interface CauserBurdenSaveResponse {
status: string;
message?: string;
data?: string;
}
/** 하수도재정 조회 공통 요청 파라미터 */
export interface SewerageFinanceRequestParams {
ctpvNm?: string;
@@ -265,13 +442,25 @@ export interface StatisticsInputSewerageFinanceService {
getSewerageFinanceSettleList(
params: SewerageFinanceSettleRequestParams,
): Promise<SewerageFinanceSettleApiResponse>;
saveSewerageFinanceSettle(
request: SewerageFinanceSettleSaveRequest,
): Promise<SewerageFinanceSettleSaveResponse>;
getSewerageChargeList(
params: SewerageChargeRequestParams,
): Promise<SewerageChargeApiResponse>;
saveSewerageCharge(
request: SewerageChargeSaveRequest,
): Promise<SewerageChargeSaveResponse>;
getSewerageUsageFeeList(
params: SewerageUsageFeeRequestParams,
): Promise<SewerageUsageFeeApiResponse>;
saveSewerageUsageFee(
request: SewerageUsageFeeSaveRequest,
): Promise<SewerageUsageFeeSaveResponse>;
getCauserBurdenList(
params: CauserBurdenRequestParams,
): Promise<CauserBurdenApiResponse>;
saveCauserBurden(
request: CauserBurdenSaveRequest,
): Promise<CauserBurdenSaveResponse>;
}
@@ -8,6 +8,11 @@ import { SkeletonPage } from "@/shared/ui/skeleton-page";
import "@rsone/components/index.css";
import Dashboard from "@/components/statistics/Dashboard";
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import {
parseStatisticsMenuUrlPath,
resolveStatisticsCategory,
} from "@/shared/lib/statistics-menu-url";
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store";
@@ -18,7 +23,9 @@ const StatisticsPage = () => {
const tabsForBody = useMemo(
() =>
tabs.filter(
(tab) => !isVerificationSelectionReportMenu(tab.menuItem),
(tab) =>
!isVerificationSelectionReportMenu(tab.menuItem) &&
!isBbsNoticeMenu(tab.menuItem),
),
[tabs],
);
@@ -57,13 +64,17 @@ const StatisticsPage = () => {
{tabsForBody.map((tab) => {
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
if (!componentCache.current.has(tab.id)) {
const componentName = tab.urlPath;
const parentComponentName = tab.parentEngNm;
const { componentName } = parseStatisticsMenuUrlPath(tab.urlPath);
const category = resolveStatisticsCategory(
tab.urlPath,
tab.parentEngNm,
);
const resolvedComponentName = componentName || tab.urlPath;
const DynamicComponent = dynamic(
() =>
import(
`@/components/statistics${parentComponentName ? `/${parentComponentName}` : ""}/${componentName}`
`@/components/statistics/${category}/${resolvedComponentName}`
),
{ ssr: false, loading: () => <SkeletonPage /> },
);
+12
View File
@@ -0,0 +1,12 @@
import type { MenuItem } from "@/entities/menu";
export const BBS_NOTICE_MENU_MARKER = "bbsnotice";
export function isBbsNoticeMenu(item: MenuItem): boolean {
const marker = BBS_NOTICE_MENU_MARKER;
return (
item.urlPath?.toLowerCase().includes(marker) ||
item.menuEngNm?.toLowerCase().includes(marker) ||
item.menuPath?.toLowerCase().includes(marker)
);
}
@@ -0,0 +1,73 @@
import type { MenuItem } from "@/entities/menu";
export type ParsedStatisticsMenuUrl = {
categoryFromPath: string | null;
componentName: string | null;
rawUrlPath: string;
};
/** 메뉴 `urlPath`에서 통계 화면 카테고리·컴포넌트명을 파싱합니다. */
export function parseStatisticsMenuUrlPath(urlPath: string): ParsedStatisticsMenuUrl {
const trimmed = (urlPath ?? "").trim();
if (!trimmed) {
return { categoryFromPath: null, componentName: null, rawUrlPath: trimmed };
}
let pathPart = trimmed;
let queryString = "";
const queryIndex = trimmed.indexOf("?");
if (queryIndex >= 0) {
pathPart = trimmed.slice(0, queryIndex);
queryString = trimmed.slice(queryIndex + 1);
}
const tabParam = new URLSearchParams(queryString).get("tab")?.trim() || null;
const segments = pathPart.replace(/^\/+/, "").split("/").filter(Boolean);
let categoryFromPath: string | null = null;
if (segments[0] === "statistics" && segments.length >= 2) {
categoryFromPath = segments[1] ?? null;
}
let componentName = tabParam;
if (!componentName) {
if (!pathPart.includes("/") && !queryString) {
componentName = trimmed;
} else if (segments.length === 1 && segments[0] !== "statistics") {
componentName = segments[0] ?? null;
} else if (segments[0] === "statistics" && segments.length >= 3) {
componentName = segments[segments.length - 1] ?? null;
}
}
return { categoryFromPath, componentName, rawUrlPath: trimmed };
}
/** 동적 import용 카테고리 폴더명 (`input`, `review` 등) */
export function resolveStatisticsCategory(urlPath: string, parentEngNm: string): string {
const { categoryFromPath } = parseStatisticsMenuUrlPath(urlPath);
return categoryFromPath || parentEngNm || "";
}
/** 탭 ID·URL `?tab=` 값 — 컴포넌트 파일명(확장자 제외) */
export function resolveStatisticsTabId(urlPath: string): string {
const { componentName, rawUrlPath } = parseStatisticsMenuUrlPath(urlPath);
return componentName || rawUrlPath;
}
/** `@/components/statistics/{category}/{Component}` 경로 */
export function buildStatisticsComponentImportPath(
urlPath: string,
parentEngNm: string,
): string {
const { componentName } = parseStatisticsMenuUrlPath(urlPath);
const category = resolveStatisticsCategory(urlPath, parentEngNm);
const name = componentName || urlPath;
return `@/components/statistics/${category}/${name}`;
}
export function menuMatchesStatisticsTabId(menu: MenuItem, tabId: string): boolean {
if (!tabId) return false;
if (menu.urlPath === tabId) return true;
return resolveStatisticsTabId(menu.urlPath) === tabId;
}
@@ -1,6 +1,7 @@
import type { MenuItem } from "@/entities/menu";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doNameList } from "@/components/util/column/input/DoList";
import { menuMatchesStatisticsTabId } from "@/shared/lib/statistics-menu-url";
import {
SEWAGE_PIPE_INP_TAB_KEYS,
SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
@@ -97,7 +98,7 @@ export function findMenuItemByUrlPath(
urlPath: string,
): MenuItem | null {
for (const item of menus) {
if (item.urlPath === urlPath) return item;
if (menuMatchesStatisticsTabId(item, urlPath)) return item;
if (item.children?.length) {
const found = findMenuItemByUrlPath(item.children, urlPath);
if (found) return found;
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import type { MenuItem } from "@/entities/menu";
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import { findMenuItemByUrlPath } from "@/shared/lib/stats-cd-input-navigation";
import { getTabStoreState } from "@/store/tab-store";
@@ -61,7 +62,7 @@ export function useStatisticsTabUrlSync({
if (getTabStoreState().activeTabId === tabParam) return;
const menu = findMenuItemByUrlPath(menuList, tabParam);
if (!menu) return;
if (!menu || isBbsNoticeMenu(menu)) return;
setCurrentMenuInfo(menu);
const ok = addTab(menu);
@@ -5,7 +5,7 @@ import { TabBar } from "./tab-bar";
function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
const { activeTabId, tabs } = useTabStore();
const pageTitle = tabs.find((t) => t.menuItem.urlPath === activeTabId);
const pageTitle = tabs.find((t) => t.id === activeTabId);
const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim());
+15 -2
View File
@@ -4,6 +4,8 @@ import { createJSONStorage, persist } from "zustand/middleware";
import type { MenuItem } from "@/entities/menu";
import { MenuAuthorityApi } from "@/entities/menu-authority";
import type { PageMenuAuthority } from "@/entities/menu-authority/model/menu-authority-interface";
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import { resolveStatisticsTabId } from "@/shared/lib/statistics-menu-url";
export interface Tab {
id: string; // 고유 ID (urlPath)
@@ -101,11 +103,18 @@ function tabForPersist(tab: Tab): Tab {
};
}
function normalizePersistedTab(tab: Tab): Tab {
const tabId = resolveStatisticsTabId(tab.urlPath || tab.id);
return tabId === tab.id ? tab : { ...tab, id: tabId };
}
function sanitizePersistedTabsAndActive(
tabs: Tab[],
activeTabId: string | null,
): { tabs: Tab[]; activeTabId: string | null } {
let nextTabs = Array.isArray(tabs) ? tabs : [];
let nextTabs = Array.isArray(tabs)
? tabs.filter((tab) => !isBbsNoticeMenu(tab.menuItem)).map(normalizePersistedTab)
: [];
if (nextTabs.length > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS);
if (nextTabs.length === 0) {
@@ -130,8 +139,12 @@ const useTabStoreHook = create<TabStore>()(
activeTabId: null,
menuList: null,
addTab: (menuItem: MenuItem) => {
if (isBbsNoticeMenu(menuItem)) {
return false;
}
const { tabs, menuList } = get();
const tabId = menuItem.urlPath || "";
const tabId = resolveStatisticsTabId(menuItem.urlPath || "");
const existingTab = tabs.find((tab) => tab.id === tabId);