Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useQueryClient, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore, useAlertStore } from "@/shared/lib";
|
||||
import { useTabStore, getTabStoreState } from "@/store/tab-store";
|
||||
import { useTabStore as gridTabStore } from "@rsone/components";
|
||||
import { getConfig } from "@common/config";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { getMenuList } from "../../../../packages/common/components/src/api";
|
||||
import {
|
||||
getTabStoreState as getGridTabStoreState,
|
||||
useTabStore as gridTabStore,
|
||||
} from "@rsone/components";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib";
|
||||
import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import { getConfig } from "@common/config";
|
||||
|
||||
import { getMenuList } from "../../../../packages/common/components/src/api";
|
||||
|
||||
import { getTabStoreState, useTabStore } from "@/store/tab-store";
|
||||
|
||||
const appConfig = getConfig();
|
||||
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
|
||||
@@ -22,118 +26,147 @@ const appSiteId = getCookie("site-id-key") ?? "";
|
||||
const isLoggedIn = !!userId;
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
const setCurrentMenuInfo = useMenuStore(
|
||||
(state) => state.setCurrentMenuInfo,
|
||||
);
|
||||
const currentMenuInfo = useMenuStore((state) => state.currentMenuInfo);
|
||||
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
const setMenuList = useTabStore((state) => state.setMenuList);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const menuList = useTabStore((state) => state.menuList);
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const addTab = useTabStore((state) => state.addTab);
|
||||
const setActiveTab = useTabStore((state) => state.setActiveTab);
|
||||
const setMenuList = useTabStore((state) => state.setMenuList);
|
||||
const activeTabId = useTabStore((state) => state.activeTabId);
|
||||
const menuList = useTabStore((state) => state.menuList);
|
||||
const tabs = useTabStore((state) => state.tabs);
|
||||
const addTabForGrid = gridTabStore((state) => state.addTab);
|
||||
const setActiveTabForGrid = gridTabStore((state) => state.setActiveTab);
|
||||
const setMenuListForGrid = gridTabStore((state) => state.setMenuList);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: menuListData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => getMenuList(appSiteId),
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
const { data: menuListData } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
|
||||
queryFn: () => getMenuList(appSiteId),
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (menuListData) {
|
||||
setMenuList(menuListData);
|
||||
setMenuListForGrid(menuListData);
|
||||
}
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
// pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const tab = currentTabs.find((t) => t.path === pathname);
|
||||
if (tab) {
|
||||
setActiveTab(tab.id);
|
||||
setActiveTabForGrid(tab.id);
|
||||
}
|
||||
}, [pathname, setActiveTab, setActiveTabForGrid]);
|
||||
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
const currentPath = useMemo(() => {
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
if (activeTabId && menuList) {
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.urlPath === activeTabId) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findMenuByEngNm(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (menuListData) {
|
||||
setMenuList(menuListData);
|
||||
setMenuListForGrid(menuListData);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}, [menuListData, setMenuList, setMenuListForGrid]);
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
// 새로고침 후 persist로 앱 탭만 복원된 경우 rsone 그리드 스토어에 탭을 맞춤
|
||||
useEffect(() => {
|
||||
if (!menuList) return;
|
||||
const appTabs = getTabStoreState().tabs;
|
||||
if (appTabs.length === 0) return;
|
||||
if (getGridTabStoreState().tabs.length > 0) return;
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
return activeMenu.urlPath;
|
||||
}
|
||||
}
|
||||
for (const tab of appTabs) {
|
||||
getGridTabStoreState().addTab(tab.menuItem);
|
||||
}
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
if (activeTabId) {
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
return activeTab.menuItem.urlPath;
|
||||
}
|
||||
}
|
||||
const activeId = getTabStoreState().activeTabId;
|
||||
if (activeId) {
|
||||
const activeTab = appTabs.find((t) => t.id === activeId);
|
||||
const gridActiveId = activeTab?.menuItem.menuEngNm;
|
||||
if (gridActiveId) {
|
||||
getGridTabStoreState().setActiveTab(gridActiveId);
|
||||
}
|
||||
}
|
||||
}, [menuList, tabs]);
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
return currentMenuInfo.urlPath;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
return pathname ?? "";
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
// pathname이 변경될 때만 실행되도록 tabs를 직접 가져옴
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const tab = currentTabs.find((t) => t.path === pathname);
|
||||
if (tab) {
|
||||
setActiveTab(tab.id);
|
||||
const gridActiveId = tab.menuItem.menuEngNm;
|
||||
if (gridActiveId) {
|
||||
setActiveTabForGrid(gridActiveId);
|
||||
}
|
||||
}
|
||||
}, [pathname, setActiveTab, setActiveTabForGrid]);
|
||||
|
||||
const handleMenuClick = useCallback((item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
if (addTabResult) {
|
||||
addTabForGrid(item);
|
||||
}
|
||||
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
|
||||
const currentPath = useMemo(() => {
|
||||
if (tabs.length === 0) return "";
|
||||
|
||||
if (!addTabResult) {
|
||||
alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다.
|
||||
if (activeTabId && menuList) {
|
||||
// menuList에서 활성 탭의 메뉴 찾기
|
||||
const findMenuByEngNm = (items: MenuItem[]): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.urlPath === activeTabId) {
|
||||
return item;
|
||||
}
|
||||
if (item.children) {
|
||||
const found = findMenuByEngNm(item.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const activeMenu = findMenuByEngNm(menuList);
|
||||
|
||||
if (activeMenu?.urlPath) {
|
||||
return activeMenu.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 탭에서 직접 찾기
|
||||
if (activeTabId) {
|
||||
const currentTabs = getTabStoreState().tabs;
|
||||
const activeTab = currentTabs.find((t) => t.id === activeTabId);
|
||||
if (activeTab?.menuItem?.urlPath) {
|
||||
return activeTab.menuItem.urlPath;
|
||||
}
|
||||
}
|
||||
|
||||
// currentMenuInfo에서 가져오기
|
||||
if (currentMenuInfo?.urlPath) {
|
||||
return currentMenuInfo.urlPath;
|
||||
}
|
||||
|
||||
// 기본값으로 pathname 사용
|
||||
return pathname ?? "";
|
||||
}, [tabs.length, activeTabId, menuList, currentMenuInfo, pathname]);
|
||||
|
||||
const handleMenuClick = useCallback(
|
||||
(item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
const addTabResult: boolean = addTab(item);
|
||||
|
||||
// packages/rsone의 스토어에도 동기화
|
||||
if (addTabResult) {
|
||||
addTabForGrid(item);
|
||||
}
|
||||
|
||||
if (!addTabResult) {
|
||||
alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다.
|
||||
기존 화면을 닫은 후 이용해주세요.`);
|
||||
}
|
||||
}, [setCurrentMenuInfo, addTab, addTabForGrid, alert]);
|
||||
}
|
||||
},
|
||||
[setCurrentMenuInfo, addTab, addTabForGrid, alert],
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={currentPath}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { SeweragePentrationRateTotColumn } from "@/components/util/column/total/SeweragePentrationRateTotColumn";
|
||||
|
||||
import SearchForm from "@/components/module/SearchForm";
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
import SubmitButton from "@/components/button/SumitButton";
|
||||
import SaveButton from "@/components/button/SaveButton";
|
||||
import LastYearDataButton from "@/components/button/LastYearDataButton";
|
||||
import SubmitExceptionButton from "@/components/button/SubmitExceptionButton";
|
||||
import { useState } from "react";
|
||||
|
||||
const SeweragePenetrationRateTot = () => {
|
||||
const columns = [
|
||||
@@ -103,38 +102,12 @@ const SeweragePenetrationRateTot = () => {
|
||||
'rmk',
|
||||
];
|
||||
|
||||
const submitFunc = () => {
|
||||
console.log('submit');
|
||||
// 제출 로직
|
||||
};
|
||||
|
||||
const saveFunc = () => {
|
||||
console.log('submit');
|
||||
// 저장 로직
|
||||
};
|
||||
|
||||
const getLastYearDataFunc = () => {
|
||||
console.log('get last year data');
|
||||
// 지난연도 데이터 가져오기 로직
|
||||
};
|
||||
|
||||
const SubmitExceptionFunc = () => {
|
||||
console.log('submit exception');
|
||||
// 예외 제출 로직
|
||||
};
|
||||
|
||||
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchForm />
|
||||
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
||||
<SubmitButton submitFunc={submitFunc} />
|
||||
<SaveButton saveFunc={saveFunc} />
|
||||
<LastYearDataButton getLastYearDataFunc={getLastYearDataFunc} />
|
||||
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
|
||||
</div>
|
||||
<DataGrid columnInfo={{columns, columnLayout}} rowStartNum={2} isExcelImport={true} fixedColumns={1} rowData={rowData} setRowData={setRowData} />
|
||||
<DataGrid columnInfo={SeweragePentrationRateTotColumn} rowStartNum={2} isExcelImport={false} fixedColumns={1} rowData={rowData} setRowData={setRowData} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
} from "@/entities/statistics";
|
||||
import { verificationHistoryColumns } from "@/entities/statistics";
|
||||
import { analysisApi } from "@/entities/statistics/api/analysis-api";
|
||||
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
|
||||
import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api";
|
||||
import { verificationTargetManagementApi } from "@/entities/statistics/api/verification-target-management-service";
|
||||
import type { VerificationTargetCodeResponse } from "@/entities/statistics/model/verification-target-management-interface";
|
||||
import {
|
||||
@@ -202,7 +202,7 @@ const HbStatisticsVerification = () => {
|
||||
params.verifyDtEnd = dayjs(to).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
return analysisHistoryApi.getAnalysisHistoryList(params);
|
||||
return stAnlsMstApi.getAnalysisHistoryList(params);
|
||||
},
|
||||
refetchInterval: (query) => {
|
||||
const data = query.state.data;
|
||||
@@ -242,7 +242,7 @@ const HbStatisticsVerification = () => {
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!outlierDialog.anlsSn) return [];
|
||||
const response = await analysisHistoryApi.getOutlierList(
|
||||
const response = await stAnlsMstApi.getOutlierList(
|
||||
outlierDialog.anlsSn,
|
||||
);
|
||||
return response;
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
imrDetailGridColumnInfo,
|
||||
imrVerificationHistoryColumns,
|
||||
} from "@/entities/statistics";
|
||||
import { analysisHistoryApi } from "@/entities/statistics/api/analysis-history-api";
|
||||
import { stAnlsMstApi } from "@/entities/statistics/api/statistics-analysis-master-api";
|
||||
import { imrApi } from "@/entities/statistics/api/imr-api";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import {
|
||||
@@ -90,6 +90,7 @@ const ImrVerificationHistory = () => {
|
||||
useState<StAnlsMstResponse | null>(null);
|
||||
const [selectedDetail, setSelectedDetail] =
|
||||
useState<StAnlsRsltResponse | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
|
||||
// Area B Grid State
|
||||
const [gridRowData, setGridRowData] = useState<
|
||||
@@ -130,7 +131,7 @@ const ImrVerificationHistory = () => {
|
||||
historyPagination,
|
||||
],
|
||||
queryFn: () =>
|
||||
analysisHistoryApi.getAnalysisHistoryList({
|
||||
stAnlsMstApi.getAnalysisHistoryList({
|
||||
...appliedSearchParams,
|
||||
page: historyPagination.pageIndex,
|
||||
size: historyPagination.pageSize,
|
||||
@@ -230,6 +231,29 @@ const ImrVerificationHistory = () => {
|
||||
setDateRange(defaultRange);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!selectedHistory || isDownloading) return;
|
||||
setIsDownloading(true);
|
||||
|
||||
try {
|
||||
const blob = await stAnlsRsltApi.downloadExcelImr(
|
||||
selectedHistory.anlsSn,
|
||||
);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `IMR_분석결과.xlsx`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.error("Failed to download IMR excel", error);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const chartOptions: ChartOptions<"line"> = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
@@ -442,16 +466,11 @@ const ImrVerificationHistory = () => {
|
||||
<Button
|
||||
color="green"
|
||||
disabled={
|
||||
!detailData || detailData.content.length === 0
|
||||
!detailData ||
|
||||
detailData.content.length === 0 ||
|
||||
isDownloading
|
||||
}
|
||||
onClick={() => {
|
||||
if (selectedHistory) {
|
||||
stAnlsRsltApi.downloadExcel(
|
||||
selectedHistory.anlsSn,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics";
|
||||
import { useStAnlsRsltList } from "@/entities/statistics/api/queries";
|
||||
import type {
|
||||
AdstZoneResponseDto,
|
||||
StAnlsRsltResponse,
|
||||
} from "@/entities/statistics";
|
||||
import {
|
||||
useStAnlsRsltList,
|
||||
useStAnlsSidoCombo,
|
||||
useStAnlsSigunguCombo,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import { regionalGridColumnInfo as columnInfo } from "@/entities/statistics/model/regional-grid-columns";
|
||||
import {
|
||||
@@ -22,6 +25,8 @@ import type { AppliedFilter } from "./HbVerificationDetail";
|
||||
|
||||
import DataGrid from "@/components/module/DataGrid";
|
||||
|
||||
const EMPTY_ADST_LIST: AdstZoneResponseDto[] = [];
|
||||
|
||||
interface RegionalGridProps {
|
||||
anlsSn: number;
|
||||
vrfcTrgtSn: string | null;
|
||||
@@ -52,27 +57,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
Record<string, string | number>[]
|
||||
>([]);
|
||||
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
});
|
||||
|
||||
const sidoOptions = useMemo(() => {
|
||||
if (!regionCodes) return [];
|
||||
return regionCodes.filter(
|
||||
(code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4,
|
||||
);
|
||||
}, [regionCodes]);
|
||||
|
||||
const gugunOptions = useMemo(() => {
|
||||
if (!regionCodes || !selectedSido) return [];
|
||||
|
||||
const sidoPrefix = selectedSido;
|
||||
return regionCodes.filter(
|
||||
(code) =>
|
||||
code.lwpoCd.startsWith(sidoPrefix) && code.lwpoCd.length === 7,
|
||||
);
|
||||
}, [regionCodes, selectedSido]);
|
||||
const { data: sidoData } = useStAnlsSidoCombo();
|
||||
const { data: gugunData } = useStAnlsSigunguCombo(selectedSido);
|
||||
const sidoOptions = sidoData ?? EMPTY_ADST_LIST;
|
||||
const gugunOptions = gugunData ?? EMPTY_ADST_LIST;
|
||||
|
||||
const { data: resultData, isLoading } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
@@ -90,22 +78,22 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
data = showAllOnEmptyFilter ? data : [];
|
||||
} else {
|
||||
if (appliedFilter.sido) {
|
||||
const sidoName = sidoOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.sido,
|
||||
)?.lwpoCdNm;
|
||||
if (sidoName) {
|
||||
data = data.filter((item) => item.sido === sidoName);
|
||||
const sidoCd = sidoOptions.find(
|
||||
(opt) => opt.adstCd === appliedFilter.sido,
|
||||
)?.adstCd;
|
||||
if (sidoCd) {
|
||||
data = data.filter((item) =>
|
||||
item.admdstCd.startsWith(sidoCd.slice(0, 2)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (appliedFilter.gugun) {
|
||||
const gugunName = gugunOptions.find(
|
||||
(opt) => opt.lwpoCd === appliedFilter.gugun,
|
||||
)?.lwpoCdNm;
|
||||
if (gugunName) {
|
||||
data = data.filter((item) =>
|
||||
gugunName.includes(item.gugun),
|
||||
);
|
||||
const gugunCd = gugunOptions.find(
|
||||
(opt) => opt.adstCd === appliedFilter.gugun,
|
||||
)?.adstCd;
|
||||
if (gugunCd) {
|
||||
data = data.filter((item) => item.admdstCd === gugunCd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +123,6 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
otlrYn: mapOtlrToLabel(item.otlrYnCd),
|
||||
}));
|
||||
|
||||
setGridRowData(
|
||||
mappedData as unknown as Record<string, string | number>[],
|
||||
);
|
||||
return mappedData;
|
||||
}, [
|
||||
resultData,
|
||||
@@ -198,6 +183,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setGridRowData(rowData as Record<string, string | number>[]);
|
||||
}, [rowData]);
|
||||
|
||||
useEffect(() => {
|
||||
handleReset();
|
||||
}, [vrfcTrgtSn]);
|
||||
@@ -215,8 +204,8 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sidoOptions.map((opt) => (
|
||||
<SelectItem key={opt.lwpoCd} value={opt.lwpoCd}>
|
||||
{opt.lwpoCdNm}
|
||||
<SelectItem key={opt.adstCd} value={opt.adstCd}>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -231,8 +220,8 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gugunOptions.map((opt) => (
|
||||
<SelectItem key={opt.lwpoCd} value={opt.lwpoCd}>
|
||||
{opt.lwpoCdNm}
|
||||
<SelectItem key={opt.adstCd} value={opt.adstCd}>
|
||||
{opt.adstNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Scatter } from "react-chartjs-2";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
type ChartData,
|
||||
@@ -17,8 +16,6 @@ import {
|
||||
} from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics";
|
||||
import {
|
||||
useStAnlsChartData,
|
||||
@@ -55,29 +52,28 @@ interface DataPoint extends LinePoint {
|
||||
item: StAnlsRsltResponse;
|
||||
}
|
||||
|
||||
function matchesAdmdstFilter(
|
||||
admdstCd: string,
|
||||
sidoCd: string | null | undefined,
|
||||
gugunCd: string | undefined,
|
||||
): boolean {
|
||||
const gugun = (gugunCd ?? "").trim();
|
||||
if (gugun) {
|
||||
return admdstCd === gugun;
|
||||
}
|
||||
const sido = (sidoCd ?? "").trim();
|
||||
if (!sido) {
|
||||
return true;
|
||||
}
|
||||
return admdstCd.startsWith(sido.slice(0, 2));
|
||||
}
|
||||
|
||||
const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
appliedFilter,
|
||||
onPointClick,
|
||||
}) => {
|
||||
const { data: regionCodes } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
||||
queryFn: () => CodeApi.getCode("LAESECD"),
|
||||
});
|
||||
|
||||
const filterNames = useMemo(() => {
|
||||
if (!regionCodes) return { sido: null, gugun: null };
|
||||
return {
|
||||
sido:
|
||||
regionCodes.find((c) => c.lwpoCd === appliedFilter.sido)
|
||||
?.lwpoCdNm || null,
|
||||
gugun:
|
||||
regionCodes.find((c) => c.lwpoCd === appliedFilter.gugun)
|
||||
?.lwpoCdNm || null,
|
||||
};
|
||||
}, [regionCodes, appliedFilter.sido, appliedFilter.gugun]);
|
||||
|
||||
const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
@@ -113,12 +109,13 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
const outlierPoints: DataPoint[] = [];
|
||||
|
||||
rawPoints.forEach((item) => {
|
||||
// Apply Filters (Sido, Gugun, Status)
|
||||
if (filterNames.sido && item.sido !== filterNames.sido) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterNames.gugun && !filterNames.gugun.includes(item.gugun)) {
|
||||
if (
|
||||
!matchesAdmdstFilter(
|
||||
item.admdstCd,
|
||||
appliedFilter.sido,
|
||||
appliedFilter.gugun,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -249,7 +246,13 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
};
|
||||
|
||||
return { data, options };
|
||||
}, [resultData, chartData, filterNames, appliedFilter.status]);
|
||||
}, [
|
||||
resultData,
|
||||
chartData,
|
||||
appliedFilter.sido,
|
||||
appliedFilter.gugun,
|
||||
appliedFilter.status,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 p-4">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
import { ValueType } from "realgrid";
|
||||
/**
|
||||
* 하수도 통계 관리 > 01. 하수도 보급률
|
||||
* 단일 Tab
|
||||
*/
|
||||
export const SeweragePentrationRateTotColumn = {
|
||||
columns: [
|
||||
// 행정구역
|
||||
{ name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, editable: false, },
|
||||
{ name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, },
|
||||
{ name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, },
|
||||
|
||||
// 수계 및 권역
|
||||
{ name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, },
|
||||
{ name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } },
|
||||
|
||||
// 수계영향권별분류
|
||||
{ name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, },
|
||||
{ name: 'lgbsn', fieldName: 'lgbsn', header: { text: '중권역' }, editable: false, },
|
||||
{ name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, },
|
||||
|
||||
// 특별대책지역
|
||||
{ name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } },
|
||||
|
||||
// 행정참고자료인구
|
||||
{ name: 'pbadmsWholPpltnCnt', fieldName: 'pbadmsWholPpltnCnt', header: { text: '총인구' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
|
||||
{ name: 'domplWholPpltnCnt', fieldName: 'domplWholPpltnCnt', header: { text: '내국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
|
||||
{ name: 'frgnrWholPpltnCnt', fieldName: 'frgnrWholPpltnCnt', header: { text: '외국인' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', },
|
||||
|
||||
// 행정참고자료면적
|
||||
{ name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', },
|
||||
|
||||
// 총인구
|
||||
{
|
||||
name: 'wholPpltnCnt',
|
||||
fieldName: 'wholPpltnCnt',
|
||||
header: { text: '총인구(명)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
|
||||
// 총면적
|
||||
{
|
||||
name: 'wholArea',
|
||||
fieldName: 'wholArea',
|
||||
header: { text: '총면적(㎢)' },
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.000',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 합계
|
||||
{
|
||||
name: 'wholSwtrmtPpltnCnt',
|
||||
fieldName: 'wholSwtrmtPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 공공하수처리인구
|
||||
{
|
||||
name: 'wholPblSwtrmtPpltnCnt',
|
||||
fieldName: 'wholPblSwtrmtPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 폐수처리인구
|
||||
{
|
||||
name: 'wholWwtPrcsPpltnCnt',
|
||||
fieldName: 'wholWwtPrcsPpltnCnt',
|
||||
header: { text: '소계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 미접속인구
|
||||
{ name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 하수처리구역 내 > 면적
|
||||
{ name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 하수처리구역 외
|
||||
{
|
||||
name: 'wholStaaExcpPpltnCnt',
|
||||
fieldName: 'wholStaaExcpPpltnCnt',
|
||||
header: { text: '합계' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0',
|
||||
},
|
||||
{ name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
{ name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
|
||||
// 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'staaPsplyRt',
|
||||
fieldName: 'staaPsplyRt',
|
||||
header: { text: '공공하수처리구역인구보급률(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100)
|
||||
{
|
||||
name: 'altdPrcsPsplyRt',
|
||||
fieldName: 'altdPrcsPsplyRt',
|
||||
header: { text: '고도처리인구보급률(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 하수도 설치율
|
||||
{
|
||||
name: 'sewerInstlRt',
|
||||
fieldName: 'sewerInstlRt',
|
||||
header: { text: '하수도설치율(%)' },
|
||||
editable: false,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.0',
|
||||
},
|
||||
|
||||
// 비고
|
||||
{ name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } },
|
||||
|
||||
{ name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false },
|
||||
{ name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false },
|
||||
],
|
||||
|
||||
columnLayout: [
|
||||
{
|
||||
name: 'groupA',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정구역' },
|
||||
items: [
|
||||
'ctpvNm',
|
||||
'sggNm',
|
||||
'admdstNm',
|
||||
],
|
||||
},
|
||||
'rvbsn',
|
||||
'trbtrNm',
|
||||
{
|
||||
name: 'groupB',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '수계영향권별분류' },
|
||||
items: ['mdbsn', 'lgbsn', 'mdmbsn'],
|
||||
},
|
||||
'spcCtpnRgnCn',
|
||||
{
|
||||
name: 'groupC',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '행정참고자료인구' },
|
||||
items: ['pbadmsWholPpltnCnt', 'domplWholPpltnCnt', 'frgnrWholPpltnCnt'],
|
||||
},
|
||||
'refArea',
|
||||
'wholPpltnCnt',
|
||||
'wholArea',
|
||||
{
|
||||
name: 'groupD',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역내' },
|
||||
items: [
|
||||
'wholSwtrmtPpltnCnt',
|
||||
{
|
||||
name: 'pblswrgGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '공공하수처리인구(명)' },
|
||||
items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'],
|
||||
},
|
||||
{
|
||||
name: 'wstrGroup',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '폐수처리인구(명)' },
|
||||
items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'],
|
||||
},
|
||||
'yetCntnPpltnCnt',
|
||||
'staaArea'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupE',
|
||||
hideChildHeaders: false,
|
||||
header: { text: '하수처리구역외' },
|
||||
items: [
|
||||
'wholStaaExcpPpltnCnt',
|
||||
'unprcsPpltnCnt',
|
||||
'drtwPrcsPpltnCnt',
|
||||
'spctnkPpltnCnt',
|
||||
'staaExcpArea'
|
||||
],
|
||||
},
|
||||
'staaPsplyRt',
|
||||
'altdPrcsPsplyRt',
|
||||
'sewerInstlRt',
|
||||
'rmrk',
|
||||
'swagePplPsplyRt',
|
||||
'admdstCd',
|
||||
]
|
||||
};
|
||||
@@ -1,13 +1,18 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import {
|
||||
ENDPOINT,
|
||||
JWT_EXPIRATION_TIME,
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
ENDPOINT,
|
||||
JWT_EXPIRATION_TIME,
|
||||
} from "@/shared/config";
|
||||
import type { LoginService, ILogin } from "../model/login-interface";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
import type { ILogin,LoginService } from "../model/login-interface";
|
||||
|
||||
import { clearTabStorePersist } from "@/store/tab-store";
|
||||
|
||||
export class LoginApiService implements LoginService {
|
||||
private static instance: LoginApiService;
|
||||
@@ -39,6 +44,11 @@ export class LoginApiService implements LoginService {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
}
|
||||
|
||||
const prevTokenId = getCookie(AUTH_USER_ID);
|
||||
if (prevTokenId !== undefined && prevTokenId !== tokenId) {
|
||||
clearTabStorePersist();
|
||||
}
|
||||
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken);
|
||||
tokenManager.setToken(AUTH_USER_ID, tokenId);
|
||||
tokenManager.setupRefreshTimer(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";
|
||||
import { getCookie, removeCookie,setCookie } from "@/shared/lib";
|
||||
|
||||
import { clearTabStorePersist } from "@/store/tab-store";
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
@@ -74,6 +76,7 @@ export class TokenManager {
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
|
||||
clearTabStorePersist();
|
||||
this.removeToken(CLAIM_NAME);
|
||||
this.removeToken(AUTH_USER_ID);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,24 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { stAnlsMstApi } from "./statistics-analysis-master-api";
|
||||
import { stAnlsRsltApi } from "./statistics-analysis-result-api";
|
||||
|
||||
export const useStAnlsSidoCombo = () => {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sido"],
|
||||
queryFn: () => stAnlsMstApi.getSidoCombo(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useStAnlsSigunguCombo = (sidoCd: string) => {
|
||||
return useQuery({
|
||||
queryKey: [ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST, "sigungu", sidoCd],
|
||||
queryFn: () => stAnlsMstApi.getSigunguCombo(sidoCd),
|
||||
enabled: Boolean(sidoCd),
|
||||
});
|
||||
};
|
||||
|
||||
export const useStAnlsRsltList = (
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: string | null,
|
||||
@@ -20,7 +36,8 @@ export const useStAnlsRsltList = (
|
||||
anlsSn,
|
||||
vrfcTrgtSn: Number(vrfcTrgtSn),
|
||||
page: 0,
|
||||
size: 1000,
|
||||
size: 15000,
|
||||
sort: "admdstCd,asc",
|
||||
}),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
|
||||
+20
-2
@@ -2,12 +2,13 @@ import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import type {
|
||||
AdstZoneResponseDto,
|
||||
StAnlsMstListApiResponse,
|
||||
StAnlsMstSearchParams,
|
||||
StAnlsOutlierListResponse,
|
||||
} from "../model/statistics-analysis/statistics-analysis-master-interface";
|
||||
|
||||
export class AnalysisHistoryApiService {
|
||||
export class StAnlsMstApiService {
|
||||
public async getAnalysisHistoryList(
|
||||
params: StAnlsMstSearchParams,
|
||||
): Promise<StAnlsMstListApiResponse> {
|
||||
@@ -30,6 +31,23 @@ export class AnalysisHistoryApiService {
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSidoCombo(): Promise<AdstZoneResponseDto[]> {
|
||||
const response = await client.get<AdstZoneResponseDto[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sido`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getSigunguCombo(sidoCd: string): Promise<AdstZoneResponseDto[]> {
|
||||
const response = await client.get<AdstZoneResponseDto[]>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_MST}/sigungu`,
|
||||
{ params: { sidoCd } },
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const analysisHistoryApi = new AnalysisHistoryApiService();
|
||||
export const stAnlsMstApi = new StAnlsMstApiService();
|
||||
@@ -60,6 +60,17 @@ export class StAnlsRsltApiService {
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async downloadExcelImr(anlsSn: number): Promise<Blob> {
|
||||
const response = await client.get<Blob>(
|
||||
`${ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT}/excel-down/imr/${anlsSn}`,
|
||||
{
|
||||
responseType: "blob",
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const stAnlsRsltApi = new StAnlsRsltApiService();
|
||||
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
type StAnlsDtlSearchParams,
|
||||
} from "./model/statistics-analysis/statistics-analysis-detail-interface";
|
||||
export {
|
||||
type AdstZoneResponseDto,
|
||||
type StAnlsMstListApiResponse,
|
||||
type StAnlsMstResponse,
|
||||
type StAnlsMstResponseDto,
|
||||
|
||||
+10
@@ -1,5 +1,15 @@
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
/**
|
||||
* 행정구역(시도·시군구) 콤보 응답 DTO
|
||||
*/
|
||||
export interface AdstZoneResponseDto {
|
||||
/** 행정구역 코드 */
|
||||
adstCd: string;
|
||||
/** 행정구역 명 */
|
||||
adstNm: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 분석 현황 마스터 응답 DTO
|
||||
*/
|
||||
|
||||
+153
-83
@@ -1,8 +1,10 @@
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
|
||||
export interface Tab {
|
||||
id: string; // 고유 ID (menuEngNm)
|
||||
id: string; // 고유 ID (urlPath)
|
||||
label: string; // 탭에 표시될 이름
|
||||
path: string; // 메뉴 경로
|
||||
urlPath: string; // url 경로
|
||||
@@ -22,96 +24,164 @@ interface TabStore {
|
||||
closeOtherTabs: (tabId: string) => void;
|
||||
}
|
||||
|
||||
const useTabStoreHook = create<TabStore>((set, get) => ({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
menuList: null,
|
||||
addTab: (menuItem: MenuItem) => {
|
||||
const { tabs, menuList } = get();
|
||||
const tabId = menuItem.urlPath || '';
|
||||
export const TAB_STORE_PERSIST_KEY = "st-tab-store";
|
||||
const TAB_STORE_VERSION = 1;
|
||||
const MAX_TABS = 10;
|
||||
|
||||
// 이미 존재하는 탭인지 확인
|
||||
const existingTab = tabs.find((tab) => tab.id === tabId);
|
||||
function tabForPersist(tab: Tab): Tab {
|
||||
return {
|
||||
...tab,
|
||||
menuItem: { ...tab.menuItem, children: null },
|
||||
};
|
||||
}
|
||||
|
||||
if (existingTab) {
|
||||
// 이미 존재하면 활성화만
|
||||
set({ activeTabId: tabId });
|
||||
return true;
|
||||
}
|
||||
function sanitizePersistedTabsAndActive(
|
||||
tabs: Tab[],
|
||||
activeTabId: string | null,
|
||||
): { tabs: Tab[]; activeTabId: string | null } {
|
||||
let nextTabs = Array.isArray(tabs) ? tabs : [];
|
||||
if (nextTabs.length > MAX_TABS) nextTabs = nextTabs.slice(0, MAX_TABS);
|
||||
|
||||
if (tabs.length < 10) {
|
||||
const newTab: Tab = {
|
||||
id: tabId,
|
||||
label: menuItem.menuKornNm,
|
||||
path: menuItem.menuPath,
|
||||
urlPath: menuItem.urlPath,
|
||||
parentEngNm: menuList?.find((m) => m.menuCd === menuItem.parentId)?.menuEngNm || '',
|
||||
menuItem,
|
||||
};
|
||||
|
||||
set({
|
||||
tabs: [...tabs, newTab],
|
||||
activeTabId: tabId,
|
||||
});
|
||||
if (nextTabs.length === 0) {
|
||||
return { tabs: [], activeTabId: null };
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
let nextActive = activeTabId;
|
||||
if (nextActive && !nextTabs.some((t) => t.id === nextActive)) {
|
||||
nextActive = nextTabs[0]?.id ?? null;
|
||||
}
|
||||
if (nextActive == null && nextTabs.length > 0) {
|
||||
nextActive = nextTabs[0]?.id ?? null;
|
||||
}
|
||||
|
||||
// 새 탭 추가
|
||||
},
|
||||
removeTab: (tabId: string): void => {
|
||||
const { tabs, activeTabId } = get();
|
||||
const newTabs = tabs.filter((tab) => tab.id !== tabId);
|
||||
return { tabs: nextTabs, activeTabId: nextActive };
|
||||
}
|
||||
|
||||
// 닫는 탭이 현재 활성 탭이면 다른 탭으로 전환
|
||||
let newActiveTabId = activeTabId;
|
||||
if (activeTabId === tabId) {
|
||||
const closedTabIndex = tabs.findIndex((tab) => tab.id === tabId);
|
||||
if (newTabs.length > 0) {
|
||||
// 닫힌 탭의 이전 탭을 활성화, 없으면 다음 탭
|
||||
if (closedTabIndex > 0) {
|
||||
const prevTab = tabs[closedTabIndex - 1];
|
||||
newActiveTabId = prevTab ? prevTab.id : null;
|
||||
} else {
|
||||
const firstTab = newTabs[0];
|
||||
newActiveTabId = firstTab ? firstTab.id : null;
|
||||
}
|
||||
} else {
|
||||
newActiveTabId = null;
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
tabs: newTabs,
|
||||
activeTabId: newActiveTabId,
|
||||
});
|
||||
},
|
||||
setActiveTab: (tabId: string) => {
|
||||
set({ activeTabId: tabId });
|
||||
},
|
||||
setMenuList: (menuList: MenuItem[] | null) => {
|
||||
set({ menuList });
|
||||
},
|
||||
closeAllTabs: () => {
|
||||
set({
|
||||
const useTabStoreHook = create<TabStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
});
|
||||
},
|
||||
closeOtherTabs: (tabId: string) => {
|
||||
const { tabs } = get();
|
||||
const tabToKeep = tabs.find((tab) => tab.id === tabId);
|
||||
menuList: null,
|
||||
addTab: (menuItem: MenuItem) => {
|
||||
const { tabs, menuList } = get();
|
||||
const tabId = menuItem.urlPath || "";
|
||||
|
||||
if (tabToKeep) {
|
||||
set({
|
||||
tabs: [tabToKeep],
|
||||
activeTabId: tabId,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
const existingTab = tabs.find((tab) => tab.id === tabId);
|
||||
|
||||
if (existingTab) {
|
||||
set({ activeTabId: tabId });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tabs.length < MAX_TABS) {
|
||||
const newTab: Tab = {
|
||||
id: tabId,
|
||||
label: menuItem.menuKornNm,
|
||||
path: menuItem.menuPath,
|
||||
urlPath: menuItem.urlPath,
|
||||
parentEngNm:
|
||||
menuList?.find(
|
||||
(m) => m.menuCd === menuItem.parentId,
|
||||
)?.menuEngNm || "",
|
||||
menuItem,
|
||||
};
|
||||
|
||||
set({
|
||||
tabs: [...tabs, newTab],
|
||||
activeTabId: tabId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
removeTab: (tabId: string): void => {
|
||||
const { tabs, activeTabId } = get();
|
||||
const newTabs = tabs.filter((tab) => tab.id !== tabId);
|
||||
|
||||
let newActiveTabId = activeTabId;
|
||||
if (activeTabId === tabId) {
|
||||
const closedTabIndex = tabs.findIndex(
|
||||
(tab) => tab.id === tabId,
|
||||
);
|
||||
if (newTabs.length > 0) {
|
||||
if (closedTabIndex > 0) {
|
||||
const prevTab = tabs[closedTabIndex - 1];
|
||||
newActiveTabId = prevTab ? prevTab.id : null;
|
||||
} else {
|
||||
const firstTab = newTabs[0];
|
||||
newActiveTabId = firstTab ? firstTab.id : null;
|
||||
}
|
||||
} else {
|
||||
newActiveTabId = null;
|
||||
}
|
||||
}
|
||||
|
||||
set({
|
||||
tabs: newTabs,
|
||||
activeTabId: newActiveTabId,
|
||||
});
|
||||
},
|
||||
setActiveTab: (tabId: string) => {
|
||||
set({ activeTabId: tabId });
|
||||
},
|
||||
setMenuList: (menuList: MenuItem[] | null) => {
|
||||
set({ menuList });
|
||||
},
|
||||
closeAllTabs: () => {
|
||||
set({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
});
|
||||
},
|
||||
closeOtherTabs: (tabId: string) => {
|
||||
const { tabs } = get();
|
||||
const tabToKeep = tabs.find((tab) => tab.id === tabId);
|
||||
|
||||
if (tabToKeep) {
|
||||
set({
|
||||
tabs: [tabToKeep],
|
||||
activeTabId: tabId,
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: TAB_STORE_PERSIST_KEY,
|
||||
version: TAB_STORE_VERSION,
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
tabs: state.tabs.map(tabForPersist),
|
||||
activeTabId: state.activeTabId,
|
||||
}),
|
||||
merge: (persistedState, currentState) => {
|
||||
const p = persistedState as Partial<
|
||||
Pick<TabStore, "tabs" | "activeTabId">
|
||||
> | null;
|
||||
const rawTabs = p?.tabs;
|
||||
const rawActive = p?.activeTabId;
|
||||
const { tabs, activeTabId } = sanitizePersistedTabsAndActive(
|
||||
(rawTabs as Tab[]) ?? [],
|
||||
typeof rawActive === "string" || rawActive === null
|
||||
? rawActive
|
||||
: null,
|
||||
);
|
||||
return {
|
||||
...currentState,
|
||||
tabs,
|
||||
activeTabId,
|
||||
};
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useTabStore = useTabStoreHook;
|
||||
export const getTabStoreState = () => useTabStoreHook.getState();
|
||||
export const getTabStoreState = () => useTabStoreHook.getState();
|
||||
|
||||
// 로그아웃 또는 사용자 전환 시 로컬 탭 스냅샷 제거 및 메모리 상태 초기화
|
||||
export function clearTabStorePersist(): void {
|
||||
useTabStoreHook.getState().closeAllTabs();
|
||||
useTabStoreHook.persist.clearStorage();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user