[st] feat: 검증 미통과 내역 메뉴 작업
This commit is contained in:
@@ -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}
|
||||
>
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
<div className="flex flex-1 h-full">
|
||||
{isVerificationSelectionReportPanelOpen && (
|
||||
<VerificationSelectionReport
|
||||
onClose={() => setReportPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<StatisticsPageLayout>{children}</StatisticsPageLayout>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, BadgeProps["color"]> = {
|
||||
NV: "gray",
|
||||
IMR: "red",
|
||||
};
|
||||
|
||||
const TYPE_CODE_NAME_MAP: Record<string, string> = {
|
||||
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) => (
|
||||
<section className="flex min-h-0 flex-1 flex-col rounded-sm border border-border h-[50rem]">
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||
<Badge color={TYPE_CODE_COLOR_MAP[typeCode]} className="rounded-full px-2 py-1 text-lg">
|
||||
{TYPE_CODE_NAME_MAP[typeCode]} {items.length}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
{items.length === 0 ? (
|
||||
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
|
||||
항목이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 tracking-tight">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={`${typeCode}-${index}`}
|
||||
className="flex flex-col rounded-sm border border-border p-3 cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="font-medium">
|
||||
{item.statsNm}
|
||||
</span>
|
||||
<Badge
|
||||
color={TYPE_CODE_COLOR_MAP[item.vrfcCd]}
|
||||
className="text-sm"
|
||||
>
|
||||
{TYPE_CODE_NAME_MAP[item.vrfcCd]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
{item.rsnExpln}
|
||||
</p>
|
||||
<div className="flex justify-between text-lg text-muted-foreground">
|
||||
<span>{item.admdstNm}</span>
|
||||
<span>
|
||||
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
|
||||
{item.compVlCn ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
|
||||
<div className="card card-border flex-1 bg-background !px-0">
|
||||
<div className="border-b border-border flex justify-between">
|
||||
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">검증 미통과 내역</h2>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="닫기"
|
||||
className="mt-2 mr-4 flex flex-shrink-0 cursor-pointer items-start justify-center text-3xl leading-none hover:text-[#2094fa]"
|
||||
onClick={() => onClose?.()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onClose?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="ri-close-line" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-body max-h-[calc(100%-3.25rem)] p-2.5">
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="relative size-[5rem]">
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
데이터를 불러올 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<div className="grid grid-rows-2 gap-6">
|
||||
{renderSection("IMR", imrList)}
|
||||
{renderSection("NV", nvList)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerificationSelectionReport;
|
||||
@@ -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 (
|
||||
<div className="card card-border !mt-6 border-border bg-background">
|
||||
{tabs.map((tab) => {
|
||||
{tabsForBody.map((tab) => {
|
||||
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
|
||||
if (!componentCache.current.has(tab.id)) {
|
||||
const componentName = tab.urlPath;
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ function StatisticsPageLayout({ children }: { children: React.ReactNode }) {
|
||||
const menuPaths = pageTitle?.path?.split(">").map((path) => path.trim());
|
||||
|
||||
return (
|
||||
<div className="h-auto min-h-full bg-container pb-2">
|
||||
<div className="h-auto min-h-full bg-container pb-2 flex-1">
|
||||
<TabBar />
|
||||
<PageTitle
|
||||
title={pageTitle?.label ?? ""}
|
||||
|
||||
Reference in New Issue
Block a user