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"; "use client";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { usePathname } from "next/navigation";
import { import {
getTabStoreState as getGridTabStoreState, getTabStoreState as getGridTabStoreState,
useTabStore as gridTabStore, useTabStore as gridTabStore,
} from "@rsone/components"; } from "@rsone/components";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; 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 { useStatisticsTabUrlSync } from "@/shared/lib/use-statistics-tab-url-sync";
import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
import { tokenManager } from "@/entities/auth"; import { tokenManager } from "@/entities/auth";
import type { MenuItem } from "@/entities/menu"; import type { MenuItem } from "@/entities/menu";
import { MenuApi } from "@/entities/menu"; import { MenuApi } from "@/entities/menu";
import { BaseLayout } from "@common/components"; import { BaseLayout } from "@common/components";
import { getConfig } from "@common/config"; import { getConfig } from "@common/config";
import { getTabStoreState, useTabStore } from "@/store/tab-store";
import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport"; import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport";
import { getTabStoreState, useTabStore } from "@/store/tab-store";
const appConfig = getConfig(); const appConfig = getConfig();
@@ -58,42 +36,23 @@ const appSiteId = getCookie("site-id-key") ?? "";
const isLoggedIn = !!userId; const isLoggedIn = !!userId;
function WithBaseLayoutInner({ children }: { children: React.ReactNode }) {
function WithBaseLayoutInner({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
/** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */ /** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */
const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] = const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] =
useState(false); useState(false);
const setCurrentMenuInfo = useMenuStore( const setCurrentMenuInfo = useMenuStore(
(state) => state.setCurrentMenuInfo, (state) => state.setCurrentMenuInfo,
); );
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo); const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
const addTab = useTabStore((state) => state.addTab); const addTab = useTabStore((state) => state.addTab);
const setActiveTab = useTabStore((state) => state.setActiveTab); const setActiveTab = useTabStore((state) => state.setActiveTab);
@@ -108,16 +67,11 @@ function WithBaseLayoutInner({
const addTabForGrid = gridTabStore((state) => state.addTab); const addTabForGrid = gridTabStore((state) => state.addTab);
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
const setMenuListForGrid = gridTabStore((state) => state.setMenuList); const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
const alert = useAlertStore((state) => state.setMessage); const alert = useAlertStore((state) => state.setMessage);
const { syncTabToUrl } = useStatisticsTabUrlSync({ const { syncTabToUrl } = useStatisticsTabUrlSync({
pathname, pathname,
menuList, menuList,
@@ -129,41 +83,27 @@ function WithBaseLayoutInner({
addTabForGrid, addTabForGrid,
setCurrentMenuInfo, setCurrentMenuInfo,
}); });
const { data: menuListData } = useQuery({ const { data: menuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES], queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
queryFn: () => MenuApi.getMenuRoles(appSiteId), queryFn: () => MenuApi.getMenuRoles(appSiteId),
enabled: isLoggedIn, enabled: isLoggedIn,
}); });
useEffect(() => { useEffect(() => {
if (menuListData) { if (menuListData) {
setMenuList(menuListData); setMenuList(menuListData);
setMenuListForGrid(menuListData); setMenuListForGrid(menuListData);
} }
}, [menuListData, setMenuList, setMenuListForGrid]); }, [menuListData, setMenuList, setMenuListForGrid]);
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤 // 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
useEffect(() => { useEffect(() => {
if (!menuList) return; if (!menuList) return;
const appTabs = getTabStoreState().tabs; const appTabs = getTabStoreState().tabs;
@@ -172,20 +112,13 @@ function WithBaseLayoutInner({
if (getGridTabStoreState().tabs.length > 0) return; if (getGridTabStoreState().tabs.length > 0) return;
for (const tab of appTabs) { for (const tab of appTabs) {
getGridTabStoreState().addTab(tab.menuItem); getGridTabStoreState().addTab(tab.menuItem);
} }
const activeId = getTabStoreState().activeTabId; const activeId = getTabStoreState().activeTabId;
if (activeId) { if (activeId) {
setActiveTab(activeId); setActiveTab(activeId);
const activeTab = appTabs.find((t) => t.id === activeId); const activeTab = appTabs.find((t) => t.id === activeId);
@@ -193,155 +126,104 @@ function WithBaseLayoutInner({
const gridActiveId = activeTab?.menuItem.menuEngNm; const gridActiveId = activeTab?.menuItem.menuEngNm;
if (gridActiveId) { if (gridActiveId) {
getGridTabStoreState().setActiveTab(gridActiveId); getGridTabStoreState().setActiveTab(gridActiveId);
} }
} }
}, [menuList, tabs, setActiveTab]); }, [menuList, tabs, setActiveTab]);
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거 // 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
const currentPath = useMemo(() => { const currentPath = useMemo(() => {
if (tabs.length === 0) return ""; if (tabs.length === 0) return "";
if (activeTabId && menuList) { if (activeTabId && menuList) {
// menuList에서 활성 탭의 메뉴 찾기 // menuList에서 활성 탭의 메뉴 찾기
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => { const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
for (const item of items) { for (const item of items) {
if (menuMatchesStatisticsTabId(item, activeTabId)) {
if (item.urlPath === activeTabId) {
return item; return item;
} }
if (item.children) { if (item.children) {
const found = findMenuByEngNm(item.children); const found = findMenuByEngNm(item.children);
if (found) return found; if (found) return found;
} }
} }
return null; return null;
}; };
const activeMenu = findMenuByEngNm(menuList); const activeMenu = findMenuByEngNm(menuList);
if (activeMenu?.urlPath) { if (activeMenu?.urlPath) {
return activeMenu.urlPath; return activeMenu.urlPath;
} }
} }
// 탭에서 직접 찾기 // 탭에서 직접 찾기
if (activeTabId) { if (activeTabId) {
const currentTabs = getTabStoreState().tabs; const currentTabs = getTabStoreState().tabs;
const activeTab = currentTabs.find((t) => t.id === activeTabId); const activeTab = currentTabs.find((t) => t.id === activeTabId);
if (activeTab?.menuItem?.urlPath) { if (activeTab?.menuItem?.urlPath) {
return activeTab.menuItem.urlPath; return activeTab.menuItem.urlPath;
} }
} }
// currentMenuInfo에서 가져오기 // currentMenuInfo에서 가져오기
if (currentMenuInfo?.urlPath) { if (currentMenuInfo?.urlPath) {
return currentMenuInfo.urlPath; return currentMenuInfo.urlPath;
} }
// 기본값으로 pathname 사용 // 기본값으로 pathname 사용
return pathname ?? ""; return pathname ?? "";
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]); }, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
const handleMenuClick = useCallback( const handleMenuClick = useCallback(
(item: MenuItem) => { (item: MenuItem) => {
if (item.urlPath.includes("/cms/")) return;
setCurrentMenuInfo(item); setCurrentMenuInfo(item);
if (isVerificationSelectionReportMenu(item)) { if (isVerificationSelectionReportMenu(item)) {
setReportPanelOpen(true); setReportPanelOpen(true);
return; return;
} }
if (isBbsNoticeMenu(item)) {
if (item.urlPath) {
router.push(item.urlPath);
}
return;
}
const addTabResult: boolean = addTab(item); const addTabResult: boolean = addTab(item);
// packages/rsone의 스토어에도 동기화 // packages/rsone의 스토어에도 동기화
if (addTabResult) { if (addTabResult) {
addTabForGrid(item); addTabForGrid(item);
if (item.urlPath) { if (item.urlPath) {
syncTabToUrl(resolveStatisticsTabId(item.urlPath));
syncTabToUrl(item.urlPath);
} }
} }
if (!addTabResult) { if (!addTabResult) {
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다. alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
기존 화면을 닫은 후 이용해주세요.`); 기존 화면을 닫은 후 이용해주세요.`);
} }
}, },
[ [
setCurrentMenuInfo, setCurrentMenuInfo,
addTab, addTab,
@@ -354,70 +236,38 @@ function WithBaseLayoutInner({
syncTabToUrl, syncTabToUrl,
router,
], ],
); );
return ( return (
<BaseLayout <BaseLayout
queryClient={queryClient} queryClient={queryClient}
tokenManager={tokenManager} tokenManager={tokenManager}
currentPath={currentPath} currentPath={currentPath}
handleMenuClick={handleMenuClick} handleMenuClick={handleMenuClick}
> >
<div className="flex h-full flex-1">
<div className="flex flex-1 h-full">
{isVerificationSelectionReportPanelOpen && ( {isVerificationSelectionReportPanelOpen && (
<VerificationSelectionReport <VerificationSelectionReport
onClose={() => setReportPanelOpen(false)} onClose={() => setReportPanelOpen(false)}
/> />
)} )}
<StatisticsPageLayout>{children}</StatisticsPageLayout> <StatisticsPageLayout>{children}</StatisticsPageLayout>
</div> </div>
</BaseLayout> </BaseLayout>
); );
} }
export default function WithBaseLayout({ export default function WithBaseLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<WithBaseLayoutInner>{children}</WithBaseLayoutInner> <WithBaseLayoutInner>{children}</WithBaseLayoutInner>
</Suspense> </Suspense>
); );
} }
@@ -8,11 +8,19 @@ import { statisticsInputSewerageFinanceApi } from "@/entities/statistics/api/inp
import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { import type {
CauserBurdenItem, CauserBurdenItem,
CauserBurdenSaveItem,
CauserBurdenSaveRequest,
SewerageChargeItem, SewerageChargeItem,
SewerageChargeSaveItem,
SewerageChargeSaveRequest,
SewerageFinanceRequestParams, SewerageFinanceRequestParams,
SewerageFinanceSettleItem, SewerageFinanceSettleItem,
SewerageFinanceSettleSaveItem,
SewerageFinanceSettleSaveRequest,
SewerageFinanceVerificationResult, SewerageFinanceVerificationResult,
SewerageUsageFeeItem, SewerageUsageFeeItem,
SewerageUsageFeeSaveItem,
SewerageUsageFeeSaveRequest,
} from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface"; } from "@/entities/statistics/model/input/statistics-input-sewerage-finance-interface";
import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import { isInputPeriodCurrent } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface";
import type { cancelSubmitRequest, SewerageStatusInfo } from "@/entities/statistics/model/statistics-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. 하수도재정 // 하수도 통계 관리 > 09. 하수도재정
const SewerageFinanceInp = () => { const SewerageFinanceInp = () => {
const { setMessage: alert } = useAlertStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state);
@@ -541,7 +706,144 @@ const SewerageFinanceInp = () => {
}; };
const saveFunc = () => { 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 = () => { const openVerificationHistoryDialog = () => {
@@ -58,7 +58,7 @@ export const SewerageChargeInpColumn = {
}, },
// 비고 // 비고
{ name: 'rmrk', fieldName: '비고', header: { text: '비고' } }, { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
// hidden // hidden
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정구역코드' }, visible: false, }, { 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: '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: '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: '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: '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: '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: '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종(대중탕용) // 욕탕1종(대중탕용)
{ name: 'bthhsFrSpcs1LevyAmt', fieldName: 'bthhsFrSpcs1LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { 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: '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종 // 욕탕2종
{ name: 'bthhsFrSpcs2LevyAmt', fieldName: 'bthhsFrSpcs2LevyAmt', header: { text: '(백만원)' }, footer: footerDecimal1, valueType: ValueType.NUMBER, dataFormat: '#,##0.0' }, { 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: '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: '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: '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: '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: '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: '비고' } }, { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
@@ -5,12 +5,20 @@ import { ENDPOINT } from "@/shared/config";
import { import {
type CauserBurdenApiResponse, type CauserBurdenApiResponse,
type CauserBurdenRequestParams, type CauserBurdenRequestParams,
type CauserBurdenSaveRequest,
type CauserBurdenSaveResponse,
type SewerageChargeApiResponse, type SewerageChargeApiResponse,
type SewerageChargeRequestParams, type SewerageChargeRequestParams,
type SewerageChargeSaveRequest,
type SewerageChargeSaveResponse,
type SewerageFinanceSettleApiResponse, type SewerageFinanceSettleApiResponse,
type SewerageFinanceSettleRequestParams, type SewerageFinanceSettleRequestParams,
type SewerageFinanceSettleSaveRequest,
type SewerageFinanceSettleSaveResponse,
type SewerageUsageFeeApiResponse, type SewerageUsageFeeApiResponse,
type SewerageUsageFeeRequestParams, type SewerageUsageFeeRequestParams,
type SewerageUsageFeeSaveRequest,
type SewerageUsageFeeSaveResponse,
type StatisticsInputSewerageFinanceService, type StatisticsInputSewerageFinanceService,
} from "../../model/input/statistics-input-sewerage-finance-interface"; } from "../../model/input/statistics-input-sewerage-finance-interface";
@@ -30,6 +38,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data; 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 */ /** 하수도 요금 조회 — ST00090002 */
public async getSewerageChargeList( public async getSewerageChargeList(
params: SewerageChargeRequestParams, params: SewerageChargeRequestParams,
@@ -41,6 +60,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data; 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 */ /** 하수도 사용료 조회 — ST00090003 */
public async getSewerageUsageFeeList( public async getSewerageUsageFeeList(
params: SewerageUsageFeeRequestParams, params: SewerageUsageFeeRequestParams,
@@ -52,6 +82,17 @@ export class StatisticsInputSewerageFinanceApiService
return response.data; 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 */ /** 원인자 부담금 조회 — ST00090004 */
public async getCauserBurdenList( public async getCauserBurdenList(
params: CauserBurdenRequestParams, params: CauserBurdenRequestParams,
@@ -62,6 +103,17 @@ export class StatisticsInputSewerageFinanceApiService
); );
return response.data; 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 = export const statisticsInputSewerageFinanceApi =
@@ -86,6 +86,63 @@ export interface SewerageFinanceSettleRequestParams {
mode?: string; 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` */ /** 하수도 요금(ST00090002) — `SewerageChargeInpColumn.tabKey` */
export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const; export const SEWERAGE_CHARGE_STATS_CD = "ST00090002" as const;
@@ -135,6 +192,40 @@ export interface SewerageChargeRequestParams {
mode?: string; 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` */ /** 하수도 사용료(ST00090003) — `SewerageUsageFeeInpColumn.tabKey` */
export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const; export const SEWERAGE_USAGE_FEE_STATS_CD = "ST00090003" as const;
@@ -201,6 +292,57 @@ export interface SewerageUsageFeeRequestParams {
mode?: string; 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` */ /** 원인자 부담금(ST00090004) — `CauserBurdenInpColumn.tabKey` */
export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const; export const CAUSER_BURDEN_STATS_CD = "ST00090004" as const;
@@ -252,6 +394,41 @@ export interface CauserBurdenRequestParams {
mode?: string; 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 { export interface SewerageFinanceRequestParams {
ctpvNm?: string; ctpvNm?: string;
@@ -265,13 +442,25 @@ export interface StatisticsInputSewerageFinanceService {
getSewerageFinanceSettleList( getSewerageFinanceSettleList(
params: SewerageFinanceSettleRequestParams, params: SewerageFinanceSettleRequestParams,
): Promise<SewerageFinanceSettleApiResponse>; ): Promise<SewerageFinanceSettleApiResponse>;
saveSewerageFinanceSettle(
request: SewerageFinanceSettleSaveRequest,
): Promise<SewerageFinanceSettleSaveResponse>;
getSewerageChargeList( getSewerageChargeList(
params: SewerageChargeRequestParams, params: SewerageChargeRequestParams,
): Promise<SewerageChargeApiResponse>; ): Promise<SewerageChargeApiResponse>;
saveSewerageCharge(
request: SewerageChargeSaveRequest,
): Promise<SewerageChargeSaveResponse>;
getSewerageUsageFeeList( getSewerageUsageFeeList(
params: SewerageUsageFeeRequestParams, params: SewerageUsageFeeRequestParams,
): Promise<SewerageUsageFeeApiResponse>; ): Promise<SewerageUsageFeeApiResponse>;
saveSewerageUsageFee(
request: SewerageUsageFeeSaveRequest,
): Promise<SewerageUsageFeeSaveResponse>;
getCauserBurdenList( getCauserBurdenList(
params: CauserBurdenRequestParams, params: CauserBurdenRequestParams,
): Promise<CauserBurdenApiResponse>; ): Promise<CauserBurdenApiResponse>;
saveCauserBurden(
request: CauserBurdenSaveRequest,
): Promise<CauserBurdenSaveResponse>;
} }
@@ -8,6 +8,11 @@ import { SkeletonPage } from "@/shared/ui/skeleton-page";
import "@rsone/components/index.css"; import "@rsone/components/index.css";
import Dashboard from "@/components/statistics/Dashboard"; 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 { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report";
import { useAdminStore } from "@/store/admin-store"; import { useAdminStore } from "@/store/admin-store";
import { useTabStore } from "@/store/tab-store"; import { useTabStore } from "@/store/tab-store";
@@ -18,7 +23,9 @@ const StatisticsPage = () => {
const tabsForBody = useMemo( const tabsForBody = useMemo(
() => () =>
tabs.filter( tabs.filter(
(tab) => !isVerificationSelectionReportMenu(tab.menuItem), (tab) =>
!isVerificationSelectionReportMenu(tab.menuItem) &&
!isBbsNoticeMenu(tab.menuItem),
), ),
[tabs], [tabs],
); );
@@ -57,13 +64,17 @@ const StatisticsPage = () => {
{tabsForBody.map((tab) => { {tabsForBody.map((tab) => {
// 렌더링 시점에 컴포넌트가 없으면 바로 로드 // 렌더링 시점에 컴포넌트가 없으면 바로 로드
if (!componentCache.current.has(tab.id)) { if (!componentCache.current.has(tab.id)) {
const componentName = tab.urlPath; const { componentName } = parseStatisticsMenuUrlPath(tab.urlPath);
const parentComponentName = tab.parentEngNm; const category = resolveStatisticsCategory(
tab.urlPath,
tab.parentEngNm,
);
const resolvedComponentName = componentName || tab.urlPath;
const DynamicComponent = dynamic( const DynamicComponent = dynamic(
() => () =>
import( import(
`@/components/statistics${parentComponentName ? `/${parentComponentName}` : ""}/${componentName}` `@/components/statistics/${category}/${resolvedComponentName}`
), ),
{ ssr: false, loading: () => <SkeletonPage /> }, { 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 { MenuItem } from "@/entities/menu";
import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { doNameList } from "@/components/util/column/input/DoList"; import { doNameList } from "@/components/util/column/input/DoList";
import { menuMatchesStatisticsTabId } from "@/shared/lib/statistics-menu-url";
import { import {
SEWAGE_PIPE_INP_TAB_KEYS, SEWAGE_PIPE_INP_TAB_KEYS,
SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS, SEWAGE_PIPE_MAINTAIN_INP_TAB_KEYS,
@@ -97,7 +98,7 @@ export function findMenuItemByUrlPath(
urlPath: string, urlPath: string,
): MenuItem | null { ): MenuItem | null {
for (const item of menus) { for (const item of menus) {
if (item.urlPath === urlPath) return item; if (menuMatchesStatisticsTabId(item, urlPath)) return item;
if (item.children?.length) { if (item.children?.length) {
const found = findMenuItemByUrlPath(item.children, urlPath); const found = findMenuItemByUrlPath(item.children, urlPath);
if (found) return found; if (found) return found;
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import type { MenuItem } from "@/entities/menu"; import type { MenuItem } from "@/entities/menu";
import { isBbsNoticeMenu } from "@/shared/lib/bbs-notice-menu";
import { findMenuItemByUrlPath } from "@/shared/lib/stats-cd-input-navigation"; import { findMenuItemByUrlPath } from "@/shared/lib/stats-cd-input-navigation";
import { getTabStoreState } from "@/store/tab-store"; import { getTabStoreState } from "@/store/tab-store";
@@ -61,7 +62,7 @@ export function useStatisticsTabUrlSync({
if (getTabStoreState().activeTabId === tabParam) return; if (getTabStoreState().activeTabId === tabParam) return;
const menu = findMenuItemByUrlPath(menuList, tabParam); const menu = findMenuItemByUrlPath(menuList, tabParam);
if (!menu) return; if (!menu || isBbsNoticeMenu(menu)) return;
setCurrentMenuInfo(menu); setCurrentMenuInfo(menu);
const ok = addTab(menu); const ok = addTab(menu);
@@ -5,7 +5,7 @@ import { TabBar } from "./tab-bar";
function StatisticsPageLayout({ children }: { children: React.ReactNode }) { function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
const { activeTabId, tabs } = useTabStore(); 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()); 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 type { MenuItem } from "@/entities/menu";
import { MenuAuthorityApi } from "@/entities/menu-authority"; import { MenuAuthorityApi } from "@/entities/menu-authority";
import type { PageMenuAuthority } from "@/entities/menu-authority/model/menu-authority-interface"; 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 { export interface Tab {
id: string; // 고유 ID (urlPath) 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( function sanitizePersistedTabsAndActive(
tabs: Tab[], tabs: Tab[],
activeTabId: string | null, activeTabId: string | null,
): { 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 > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS);
if (nextTabs.length === 0) { if (nextTabs.length === 0) {
@@ -130,8 +139,12 @@ const useTabStoreHook = create<TabStore>()(
activeTabId: null, activeTabId: null,
menuList: null, menuList: null,
addTab: (menuItem: MenuItem) => { addTab: (menuItem: MenuItem) => {
if (isBbsNoticeMenu(menuItem)) {
return false;
}
const { tabs, menuList } = get(); const { tabs, menuList } = get();
const tabId = menuItem.urlPath || ""; const tabId = resolveStatisticsTabId(menuItem.urlPath || "");
const existingTab = tabs.find((tab) => tab.id === tabId); const existingTab = tabs.find((tab) => tab.id === tabId);