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 (
-