diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index e849c3a1d..287cc4c38 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -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 ( - - -
- +
{isVerificationSelectionReportPanelOpen && ( - setReportPanelOpen(false)} - /> - )} {children} -
- - ); - } - - export default function WithBaseLayout({ - children, - }: { - children: React.ReactNode; - }) { - return ( - - {children} - - ); - } - - diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index 55f1a9998..fb2ebf09b 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -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(); + 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[], +): 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[], +): 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[], +): 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[], +): 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 = () => { diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts index a86546960..b44fad586 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageChargeInpColumn.ts @@ -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, }, diff --git a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts index f3e03d163..2cd90446b 100644 --- a/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts +++ b/apps/st/src/components/util/column/input/sewerageFinance/SewerageUsageFeeInpColumn.ts @@ -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: '비고' } }, diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts index 0b6a919f0..8f1d97e13 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sewerage-finance-service.ts @@ -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 { + const response = await stClient.put( + `${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 { + const response = await stClient.put( + `${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 { + const response = await stClient.put( + `${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 { + const response = await stClient.put( + `${BASE}/causer-burden/save`, + request, + ); + return response.data; + } } export const statisticsInputSewerageFinanceApi = diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts index d2928353f..e56a52858 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -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; + saveSewerageFinanceSettle( + request: SewerageFinanceSettleSaveRequest, + ): Promise; getSewerageChargeList( params: SewerageChargeRequestParams, ): Promise; + saveSewerageCharge( + request: SewerageChargeSaveRequest, + ): Promise; getSewerageUsageFeeList( params: SewerageUsageFeeRequestParams, ): Promise; + saveSewerageUsageFee( + request: SewerageUsageFeeSaveRequest, + ): Promise; getCauserBurdenList( params: CauserBurdenRequestParams, ): Promise; + saveCauserBurden( + request: CauserBurdenSaveRequest, + ): Promise; } diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 721bb3ff3..60db9d6be 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -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: () => }, ); diff --git a/apps/st/src/shared/lib/bbs-notice-menu.ts b/apps/st/src/shared/lib/bbs-notice-menu.ts new file mode 100644 index 000000000..b0f7cef27 --- /dev/null +++ b/apps/st/src/shared/lib/bbs-notice-menu.ts @@ -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) + ); +} diff --git a/apps/st/src/shared/lib/statistics-menu-url.ts b/apps/st/src/shared/lib/statistics-menu-url.ts new file mode 100644 index 000000000..fc9e74356 --- /dev/null +++ b/apps/st/src/shared/lib/statistics-menu-url.ts @@ -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; +} diff --git a/apps/st/src/shared/lib/stats-cd-input-navigation.ts b/apps/st/src/shared/lib/stats-cd-input-navigation.ts index 6a00d33ae..56b8c791d 100644 --- a/apps/st/src/shared/lib/stats-cd-input-navigation.ts +++ b/apps/st/src/shared/lib/stats-cd-input-navigation.ts @@ -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; diff --git a/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts b/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts index 508e6e834..9388438db 100644 --- a/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts +++ b/apps/st/src/shared/lib/use-statistics-tab-url-sync.ts @@ -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); diff --git a/apps/st/src/shared/ui/statistics-page-layout.tsx b/apps/st/src/shared/ui/statistics-page-layout.tsx index 857f8441e..26a9d6ddc 100644 --- a/apps/st/src/shared/ui/statistics-page-layout.tsx +++ b/apps/st/src/shared/ui/statistics-page-layout.tsx @@ -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()); diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 6909ed26b..3a75f37aa 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -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()( 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);