diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index 654a08534..1ba6133ea 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { usePathname } from "next/navigation"; import { @@ -11,6 +11,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; import { getCookie, useAlertStore, useMenuStore } from "@/shared/lib"; +import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { StatisticsPageLayout } from "@/shared/ui/statistics-page-layout"; import { tokenManager } from "@/entities/auth"; import type { MenuItem } from "@/entities/menu"; @@ -20,6 +21,8 @@ import { getConfig } from "@common/config"; import { getTabStoreState, useTabStore } from "@/store/tab-store"; +import VerificationSelectionReport from "@/components/statistics/VerificationSelectionReport"; + const appConfig = getConfig(); const userId = getCookie(appConfig.AUTH_USER_ID ?? ""); const appSiteId = getCookie("site-id-key") ?? ""; @@ -33,6 +36,10 @@ export default function WithBaseLayout({ const pathname = usePathname(); const queryClient = useQueryClient(); + /** 탭 추가 없이 메뉴만 눌렀을 때 패널 유지 (다른 메뉴 클릭해도 유지) */ + const [isVerificationSelectionReportPanelOpen, setReportPanelOpen] = + useState(false); + const setCurrentMenuInfo = useMenuStore( (state) => state.setCurrentMenuInfo, ); @@ -145,6 +152,12 @@ export default function WithBaseLayout({ const handleMenuClick = useCallback( (item: MenuItem) => { setCurrentMenuInfo(item); + + if (isVerificationSelectionReportMenu(item)) { + setReportPanelOpen(true); + return; + } + const addTabResult: boolean = addTab(item); // packages/rsone의 스토어에도 동기화 @@ -153,11 +166,11 @@ export default function WithBaseLayout({ } if (!addTabResult) { - alert(`최대 추가 할 수 있는 화면 수를 초과 하였습니다. + alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다. 기존 화면을 닫은 후 이용해주세요.`); } }, - [setCurrentMenuInfo, addTab, addTabForGrid, alert], + [setCurrentMenuInfo, addTab, addTabForGrid, alert, setReportPanelOpen], ); return ( @@ -167,7 +180,14 @@ export default function WithBaseLayout({ currentPath={currentPath} handleMenuClick={handleMenuClick} > - {children} +
+ {isVerificationSelectionReportPanelOpen && ( + setReportPanelOpen(false)} + /> + )} + {children} +
); } diff --git a/apps/st/src/components/statistics/VerificationSelectionReport.tsx b/apps/st/src/components/statistics/VerificationSelectionReport.tsx new file mode 100644 index 000000000..65b4a8a20 --- /dev/null +++ b/apps/st/src/components/statistics/VerificationSelectionReport.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useDashboardVrfcReport } from "@/entities/statistics"; +import dayjs from "dayjs"; + +import { useUserStore } from "@/shared/lib"; + +import { + Badge, + type BadgeProps, + LoadingWithAnimation, +} from "@common/components"; + +const TYPE_CODE_COLOR_MAP: Record = { + NV: "gray", + IMR: "red", +}; + +const TYPE_CODE_NAME_MAP: Record = { + NV: "일반", + IMR: "I-MR", +}; + +type VerificationSelectionReportProps = { + onClose?: () => void; +}; + +const VerificationSelectionReport = ({ + onClose, +}: VerificationSelectionReportProps) => { + const userInfo = useUserStore((state) => state.userInfo); + + const param = { + statsYr: String(dayjs().year() - 1), + dtlInstCd: userInfo?.dtlInstCd || '', + enoCd: userInfo?.enoCd || '', + laeSeCd: userInfo?.laeSeCd || '', + }; + + const { data, isLoading, error } = useDashboardVrfcReport(param); + const imrList = data?.list.filter((item) => item.vrfcCd === "IMR") ?? []; + const nvList = data?.list.filter((item) => item.vrfcCd === "NV") ?? []; + + const renderSection = (typeCode: "IMR" | "NV", items: typeof imrList) => ( +
+
+ + {TYPE_CODE_NAME_MAP[typeCode]} {items.length} + +
+
+ {items.length === 0 ? ( +
+ 항목이 없습니다. +
+ ) : ( +
+ {items.map((item, index) => ( +
+
+ + {item.statsNm} + + + {TYPE_CODE_NAME_MAP[item.vrfcCd]} + +
+

+ {item.rsnExpln} +

+
+ {item.admdstNm} + + 현재: {item.nowVlCn ?? "-"} / 비교:{" "} + {item.compVlCn ?? "-"} + +
+
+ ))} +
+ )} +
+
+ ); + + return ( +
+
+
+

검증 미통과 내역

+ onClose?.()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClose?.(); + } + }} + > + + +
+
+ {isLoading && ( +
+
+ +
+
+ )} + {error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {data && ( +
+ {renderSection("IMR", imrList)} + {renderSection("NV", nvList)} +
+ )} +
+
+
+ ); +}; + +export default VerificationSelectionReport; \ No newline at end of file diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 131ab95dd..721bb3ff3 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Suspense, useEffect, useRef } from "react"; +import { Suspense, useEffect, useMemo, useRef } from "react"; import dynamic from "next/dynamic"; import { SkeletonPage } from "@/shared/ui/skeleton-page"; @@ -8,11 +8,20 @@ import { SkeletonPage } from "@/shared/ui/skeleton-page"; import "@rsone/components/index.css"; import Dashboard from "@/components/statistics/Dashboard"; +import { isVerificationSelectionReportMenu } from "@/shared/lib/verification-selection-report"; import { useAdminStore } from "@/store/admin-store"; import { useTabStore } from "@/store/tab-store"; const StatisticsPage = () => { const { tabs, activeTabId } = useTabStore(); + + const tabsForBody = useMemo( + () => + tabs.filter( + (tab) => !isVerificationSelectionReportMenu(tab.menuItem), + ), + [tabs], + ); const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo); // 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨) @@ -45,7 +54,7 @@ const StatisticsPage = () => { return (
- {tabs.map((tab) => { + {tabsForBody.map((tab) => { // 렌더링 시점에 컴포넌트가 없으면 바로 로드 if (!componentCache.current.has(tab.id)) { const componentName = tab.urlPath; diff --git a/apps/st/src/shared/lib/verification-selection-report.ts b/apps/st/src/shared/lib/verification-selection-report.ts new file mode 100644 index 000000000..47424f88d --- /dev/null +++ b/apps/st/src/shared/lib/verification-selection-report.ts @@ -0,0 +1,11 @@ +import type { MenuItem } from "@/entities/menu"; + +export const VERIFICATION_SELECTION_REPORT_MARKER = + "VerificationSelectionReport"; + +export function isVerificationSelectionReportMenu(item: MenuItem): boolean { + return ( + item.menuEngNm === VERIFICATION_SELECTION_REPORT_MARKER || + !!item.urlPath?.includes(VERIFICATION_SELECTION_REPORT_MARKER) + ); +} diff --git a/apps/st/src/shared/ui/statistics-page-layout.tsx b/apps/st/src/shared/ui/statistics-page-layout.tsx index d7d5b98b4..857f8441e 100644 --- a/apps/st/src/shared/ui/statistics-page-layout.tsx +++ b/apps/st/src/shared/ui/statistics-page-layout.tsx @@ -10,7 +10,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) { const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim()); return ( -
+