[st] feat: 대시보드 페이지 작업

This commit is contained in:
2026-04-29 11:37:17 +09:00
parent d98003b3ec
commit 9fc2fc8942
10 changed files with 348 additions and 167 deletions
@@ -63,9 +63,6 @@ function Dashboard() {
items={panels.map((p) => p.id)}
strategy={rectSortingStrategy}
>
{/* TBD: 그리드 레이아웃 비율 정의 필요
현재: 3열 그리드, 반응형 브레이크포인트 적용
SPEC 레이아웃에 따라 조정 필요 */}
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
{panels.map((panel) => (
<SortablePanel
@@ -81,12 +78,12 @@ function Dashboard() {
</DndContext>
) : (
<div className="grid grid-cols-1 gap-4 p-2 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
<Skeleton className="h-[20rem] rounded-xl bg-muted" />
</div>
)}
</>
@@ -6,7 +6,7 @@ import { ScheduleCalendarPanel } from "../ui/panels/schedule-calendar-panel";
import { ScheduleInfoPanel } from "../ui/panels/schedule-info-panel";
import { StatsSubmissionPanel } from "../ui/panels/stats-submission-panel";
import { UnSubmitPanel } from "../ui/panels/unsubmit-panel";
import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
// import { VrfcErrPanel } from "../ui/panels/vrfc-err-panel";
import { VrfcReportPanel } from "../ui/panels/vrfc-report-panel";
import type { PanelItem } from "./types";
@@ -36,23 +36,23 @@ export function getInitialPanels({
component: <NoticePanel />,
className: "",
},
// {
// id: "panel-d",
// component: <VrfcErrPanel params={dashboardParams} />,
// className: "",
// },
{
id: "panel-d",
component: <VrfcErrPanel params={dashboardParams} />,
className: "",
},
{
id: "panel-e",
component: <ScheduleCalendarPanel />,
className: "",
},
{
id: "panel-f",
id: "panel-e",
component: <VrfcReportPanel params={dashboardParams} />,
className: "",
},
{
id: "panel-g",
id: "panel-f",
component: <UnSubmitPanel params={dashboardParams} />,
className: "",
},
@@ -17,7 +17,7 @@ export function NoticePanel() {
const recentPostListQuery = useRecentPostList({
page: 0,
size: 3,
size: 4,
keywordType: "bbsTypeCd",
keyword: "BBSNOTICE",
});
@@ -45,14 +45,18 @@ export function NoticePanel() {
});
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-lg font-semibold">공지사항</h3>
<Button onClick={handleMoreClick} className="h-8">
<h3 className="text-2xl font-semibold">공지사항</h3>
<Button
size="sm"
className="!text-xl"
onClick={handleMoreClick}
>
더보기
</Button>
</div>
@@ -67,17 +71,16 @@ const noticeColumns = [
columnHelper.accessor("pstTtl", {
header: "제목",
cell: ({ row }) => (
<div className="py-2">
<div className="truncate font-medium">
{row.original.pstTtl}
</div>
<div className="mt-1 flex justify-between text-sm text-muted-foreground">
<span>{row.original.wrtrNm}</span>
<span>{formatDate(row.original.regDt)}</span>
</div>
</div>
<div className="min-w-[20rem] truncate">{row.original.pstTtl}</div>
),
}),
columnHelper.accessor("wrtrNm", {
header: "작성자",
}),
columnHelper.accessor("regDt", {
header: "등록일",
cell: ({ row }) => formatDate(row.original.regDt),
}),
];
function formatDate(value: string) {
@@ -8,6 +8,7 @@ import dayjs from "dayjs";
import { SITE_ID } from "@/shared/config";
import { useSchedules } from "@/entities/schedule";
import { LoadingWithAnimation } from "@common/components";
import { useDragHandle } from "../sortable-panel";
@@ -31,21 +32,24 @@ export function ScheduleCalendarPanel() {
});
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-lg font-semibold">일정관리</h3>
<h3 className="text-2xl font-semibold">일정관리</h3>
</div>
<div className="card-body">
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="py-8 text-center">로딩 중...</div>
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{data && (
<div className="h-[400px]">
{/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */}
<div className="h-[48rem]">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin]}
initialView="dayGridMonth"
@@ -67,7 +71,6 @@ export function ScheduleCalendarPanel() {
// TBD: allDay 판단 기준 (1439분 이상?)
allDay: true,
}))}
// TBD: 이벤트 클릭 핸들러 (현재 SPEC에서 제거됨)
height="100%"
/>
</div>
@@ -1,7 +1,14 @@
"use client";
import dayjs from "dayjs";
import type { ScheduleInfoSearchParams } from "@/entities/statistics";
import { useScheduleInfo } from "@/entities/statistics";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
@@ -9,63 +16,121 @@ interface ScheduleInfoPanelProps {
params: ScheduleInfoSearchParams | null;
}
const CURRENT_STATUS_LABEL_MAP = {
INPT: "입력중",
VRFC: "검증중",
NONE: "기간외",
};
const CURRENT_STATUS_COLOR_MAP = {
INPT: "",
VRFC: "gray",
NONE: "lightGray",
};
export function ScheduleInfoPanel({ params }: ScheduleInfoPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useScheduleInfo(params);
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-lg font-semibold">일정안내</h3>
<h3 className="text-2xl font-semibold">일정안내</h3>
{data && (
<div className="flex items-center gap-2">
<span className="text-xl tracking-tight">
{data.ddayLabel}
</span>
<Badge
size="lg"
color="orange"
className="rounded-full !px-3 text-xl font-semibold"
>
{data.dday}
</Badge>
</div>
)}
</div>
<div className="card-body">
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="py-8 text-center">로딩 중...</div>
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="py-8 text-center text-red-500">
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-4">
{/* D-day 표시 */}
<div className="rounded-lg bg-blue-50 p-4 text-center">
<div className="mb-1 text-sm text-muted-foreground">
{data.ddayLabel}
</div>
<div className="text-3xl font-bold text-blue-600">
{data.dday}
</div>
<div className="mt-1 text-sm text-muted-foreground">
{data.currentSe}
</div>
</div>
{/* TBD: 일정 목록 표시 형식 정의 필요 */}
<div className="space-y-2">
<div className="rounded border p-3">
<div className="mb-1 text-sm font-medium">
입력기간
</div>
<div className="text-sm text-muted-foreground">
{data.inptBgngYmd} ~ {data.inptEndYmd}
<>
<div className="space-y-4">
<div className="mt-2 flex items-center justify-between">
<h4 className="px-2 text-2xl font-semibold tracking-tight text-label">
이번 주 주요 일정
</h4>
<div className="flex items-center gap-2">
<span className="tracking-tight text-label">
현재 진행 상태
</span>
<Badge
color={
CURRENT_STATUS_COLOR_MAP[
data.currentSe as keyof typeof CURRENT_STATUS_COLOR_MAP
] as BadgeProps["color"]
}
className="rounded-full !px-3"
>
{
CURRENT_STATUS_LABEL_MAP[
data.currentSe as keyof typeof CURRENT_STATUS_LABEL_MAP
]
}
</Badge>
</div>
</div>
<div className="rounded border p-3">
<div className="mb-1 text-sm font-medium">
검증기간
<div className="space-y-2">
<div className="flex items-center justify-between rounded-sm border border-border p-3">
<Badge size="lg" className="font-semibold">
{data.inptSeCd}차 입력기간
</Badge>
<p className="text-2xl">
{dayjs(data.inptBgngYmd).format(
"YYYY.MM.DD",
)}{" "}
~{" "}
{dayjs(data.inptEndYmd).format(
"YYYY.MM.DD",
)}
</p>
</div>
<div className="text-sm text-muted-foreground">
{data.vrfcBgngYmd} ~ {data.vrfcEndYmd}
<div className="flex items-center justify-between rounded-sm border border-border p-3">
<Badge
size="lg"
color="gray"
className="font-semibold"
>
{data.vrfcSeCd}차 검증기간
</Badge>
<p className="text-2xl">
{dayjs(data.vrfcBgngYmd).format(
"YYYY.MM.DD",
)}{" "}
~{" "}
{dayjs(data.vrfcEndYmd).format(
"YYYY.MM.DD",
)}
</p>
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
@@ -1,8 +1,14 @@
"use client";
import { useDashboardStatsSubmissionRate } from "@/entities/statistics";
import { Frown, Laugh, Meh, OctagonAlert, OctagonX, Smile } from "lucide-react";
import {
useDashboardEcptReqCount,
useDashboardStatsSubmissionRate,
useDashboardVrfcErrCount,
} from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import { LoadingWithAnimation } from "@common/components";
import { LoadingWithAnimation, Progress } from "@common/components";
import { useDragHandle } from "../sortable-panel";
@@ -10,70 +16,147 @@ interface StatsSubmissionPanelProps {
params: DashboardSearchParams | null;
}
const STATUS_LEVEL_TEXT_COLOR = {
GREAT: "text-blue-500",
GOOD: "text-green-500",
WARNING: "text-yellow-500",
BAD: "text-red-500",
};
const STATUS_LEVEL_EMOJI = {
GREAT: <Laugh size={160} className={STATUS_LEVEL_TEXT_COLOR.GREAT} />,
GOOD: <Smile size={160} className={STATUS_LEVEL_TEXT_COLOR.GOOD} />,
WARNING: <Meh size={160} className={STATUS_LEVEL_TEXT_COLOR.WARNING} />,
BAD: <Frown size={160} className={STATUS_LEVEL_TEXT_COLOR.BAD} />,
};
export function StatsSubmissionPanel({ params }: StatsSubmissionPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardStatsSubmissionRate(params);
const vrfcErrQuery = useDashboardVrfcErrCount(params);
const ecptReqQuery = useDashboardEcptReqCount(params);
return (
<div className="card card-border !m-0 h-full">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
{/* <div
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-xl font-semibold">통계입력현황</h3>
</div>
<div className="card-body h-[calc(100%-3.25rem)] py-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 py-8 text-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-4">
{/* TBD: statusLevel별 이모지 매핑 규칙 필요 */}
<div className="text-center">
<div className="mb-2 text-4xl">
{/* TBD: statusLevel 값에 따른 이모지 표시 */}
{data.statusLevel === "GOOD"
? "😊"
: data.statusLevel === "WARNING"
? "⚠️"
: "📊"}
<h3 className="text-2xl font-semibold">통계입력현황</h3>
</div> */}
<div
className={`card-body cursor-grab py-2 active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
>
<div className="flex h-full gap-6 px-2">
<div className="h-full flex-1 flex-col">
<h3 className="text-2xl font-semibold">통계입력현황</h3>
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
<div className="text-sm text-muted-foreground">
{data.statusLevel}
)}
{error && (
<div className="flex h-full items-center justify-center py-8 text-center">
데이터를 불러올 수 없습니다.
</div>
</div>
)}
<div className="h-full">
{data && (
<div className="flex h-full flex-col items-center gap-4">
{/* 통계제출률 상태 이모지 */}
<div className="flex justify-center">
{
STATUS_LEVEL_EMOJI[
data.statusLevel as keyof typeof STATUS_LEVEL_EMOJI
]
}
</div>
{/* 통계제출률 프로그레스바 */}
<div>
<div className="mb-2 flex justify-between text-sm">
<span>통계제출률</span>
<span className="font-semibold">
{data.submissionRate?.toFixed(1)}%
</span>
{/* 통계제출률 프로그레스바 */}
<div className="w-full space-y-3">
<div className="flex w-full justify-between">
<span className="text-2xl">
통계제출률
</span>
<span className="text-2xl font-semibold">
{data.submissionRate?.toFixed(
1,
)}
%
<span className="ml-1">
({data.submittedCount}/
{data.totalCount})
</span>
</span>
</div>
<Progress
value={data.submissionRate}
className="h-6 bg-muted"
/>
</div>
</div>
)}
</div>
</div>
<div className="h-full flex-[0.5]">
<h3 className="text-2xl font-semibold">
검증오류 및 예외
</h3>
<div className="mt-4 space-y-4">
{/* 검증오류 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonX
className="text-red-600"
size={20}
/>
<span className="text-lg">검증오류 수</span>
</div>
{vrfcErrQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{vrfcErrQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{vrfcErrQuery.data && (
<div className="text-5xl font-bold text-red-600">
{vrfcErrQuery.data.vrfcErrCount.toLocaleString()}
</div>
)}
</div>
<div className="h-2.5 w-full rounded-full bg-gray-200">
<div
className="h-2.5 rounded-full bg-blue-600 transition-all"
style={{ width: `${data.submissionRate}%` }}
/>
</div>
<div className="mt-1 flex justify-between text-xs text-muted-foreground">
<span>제출: {data.submittedCount}</span>
<span>전체: {data.totalCount}</span>
{/* 예외신청 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-orange-200 bg-orange-100 p-4 text-center">
<div className="flex flex-col items-center gap-2">
<OctagonAlert
className="text-orange-600"
size={20}
/>
<span className="text-lg">예외신청 수</span>
</div>
{ecptReqQuery.isLoading && (
<div className="text-2xl font-bold">-</div>
)}
{ecptReqQuery.error && (
<div className="text-sm">
데이터를 불러올 수 없습니다.
</div>
)}
{ecptReqQuery.data && (
<div className="text-5xl font-bold text-orange-600">
{ecptReqQuery.data.ecptReqCount.toLocaleString()}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
@@ -15,23 +15,20 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
const { data, isLoading, error } = useDashboardUnSubmitCount(params);
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-xl font-semibold">미제출 지표</h3>
<h3 className="text-2xl font-semibold">미제출 지표</h3>
{data && (
<Badge
color="red"
className="text-md rounded-full px-2 py-1 text-destructive-foreground"
>
<Badge size="lg" className="rounded-full !px-3">
{data.unSubmitCount}건
</Badge>
)}
</div>
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
@@ -51,11 +48,11 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) {
미제출 지표가 없습니다.
</div>
) : (
<div className="max-h-[32rem] space-y-2 overflow-y-auto">
<div className="max-h-[48rem] space-y-2 overflow-y-auto">
{data.unSubmitList.map((item) => (
<div
key={item.statsCd}
className="rounded-sm border p-3"
className="rounded-sm border border-border p-3"
>
<p className="font-medium">
{item.statsNm}
@@ -20,15 +20,15 @@ export function VrfcErrPanel({ params }: VrfcErrPanelProps) {
const ecptReqQuery = useDashboardEcptReqCount(params);
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-xl font-semibold">검증오류 및 예외</h3>
<h3 className="text-2xl font-semibold">검증오류 및 예외</h3>
</div>
<div className="card-body h-[calc(100%-3.25rem)] py-2.5">
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
<div className="grid h-full grid-cols-2 gap-4">
{/* 검증오류 수 카드 */}
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-red-200 bg-red-100 p-4 text-center">
@@ -2,6 +2,11 @@
import { useDashboardVrfcReport } from "@/entities/statistics";
import type { DashboardSearchParams } from "@/entities/statistics/model/dashboard/dashboard-interface";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
} from "@common/components";
import { useDragHandle } from "../sortable-panel";
@@ -9,79 +14,107 @@ interface VrfcReportPanelProps {
params: DashboardSearchParams | null;
}
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
};
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
};
export function VrfcReportPanel({ params }: VrfcReportPanelProps) {
const { dragHandleProps, isDragging } = useDragHandle();
const { data, isLoading, error } = useDashboardVrfcReport(params);
return (
<div className="card card-border !m-0 h-full">
<div className="card card-border !m-0 h-full border-border bg-background text-foreground">
<div
className={`card-header cursor-grab active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
className={`card-header cursor-grab border-border active:cursor-grabbing ${isDragging ? "opacity-50" : ""}`}
{...dragHandleProps}
title="헤더를 드래그하여 패널 순서 변경"
>
<h3 className="text-lg font-semibold">이상치 선별보고서</h3>
<h3 className="text-2xl font-semibold">이상치 선별보고서</h3>
</div>
<div className="card-body">
<div className="card-body max-h-[calc(100%-3.25rem)] py-2.5">
{isLoading && (
<div className="py-8 text-center">로딩 중...</div>
<div className="flex h-full items-center justify-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
{error && (
<div className="py-8 text-center text-red-500">
<div className="flex h-full items-center justify-center">
데이터를 불러올 수 없습니다.
</div>
)}
{data && (
<div className="space-y-4">
<div className="space-y-2">
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */}
{data.typeCounts &&
Object.keys(data.typeCounts).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(data.typeCounts).map(
([type, count]) => (
<span
<Badge
key={type}
className="rounded-full bg-gray-100 px-3 py-1 text-sm"
color={
TYPE_CODE_COLOR_MAP[type]
}
className="rounded-full px-2 py-1 text-lg"
>
{type}: {count}
</span>
{TYPE_CODE_NAME_MAP[type]}:{" "}
{count}
</Badge>
),
)}
</div>
)}
{/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */}
<div className="max-h-96 overflow-y-auto">
<div className="max-h-[44rem] overflow-y-auto">
{data.list.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">
<div className="flex h-full items-center justify-center">
이상치 항목이 없습니다.
</div>
) : (
<div className="space-y-2">
<div className="space-y-2 tracking-tight">
{data.list.map((item, index) => (
<div
key={index}
className="cursor-pointer rounded border p-3 transition-colors hover:bg-accent"
/* TBD: 행 클릭 시 이동 라우트 */
className="flex flex-col rounded-sm border border-border p-3"
>
<div className="mb-2 flex items-start justify-between">
<div className="font-medium">
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</div>
{/* TBD: vrfcCd 값별 표시 레이블 */}
<span className="rounded bg-red-100 px-2 py-0.5 text-xs text-red-700">
{item.vrfcCd}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[
item.vrfcCd
]
}
className="text-sm"
>
{
TYPE_CODE_NAME_MAP[
item.vrfcCd
]
}
</Badge>
</div>
<div className="mb-1 text-sm text-muted-foreground">
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</div>
<div className="flex justify-between text-xs text-muted-foreground">
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
{/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */}
현재: {item.nowVlCn} / 비교:{" "}
{item.compVlCn}
현재: {item.nowVlCn ?? "-"}{" "}
/ 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */}
@@ -37,14 +37,14 @@ const StatisticsPage = () => {
// 탭이 없으면 Dashboard 표시
if (tabs.length === 0) {
return (
<div className="card card-border !mt-6">
<div className="card card-border !mt-6 border-border bg-background">
<Dashboard />
</div>
);
}
return (
<div className="card card-border !mt-6">
<div className="card card-border !mt-6 border-border bg-background">
{tabs.map((tab) => {
// 렌더링 시점에 컴포넌트가 없으면 바로 로드
if (!componentCache.current.has(tab.id)) {