[st] feat: 대시보드 페이지 작업
This commit is contained in:
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user