diff --git a/apps/st/src/components/statistics/Dashboard.tsx b/apps/st/src/components/statistics/Dashboard.tsx index b3734e386..423294d2f 100644 --- a/apps/st/src/components/statistics/Dashboard.tsx +++ b/apps/st/src/components/statistics/Dashboard.tsx @@ -63,9 +63,6 @@ function Dashboard() { items={panels.map((p) => p.id)} strategy={rectSortingStrategy} > - {/* TBD: 그리드 레이아웃 비율 정의 필요 - 현재: 3열 그리드, 반응형 브레이크포인트 적용 - SPEC 레이아웃에 따라 조정 필요 */}
{panels.map((panel) => ( ) : (
- - - - - - + + + + + +
)} diff --git a/apps/st/src/pages/statistics/model/panel-config.tsx b/apps/st/src/pages/statistics/model/panel-config.tsx index 75f678a7f..8cc4f6271 100644 --- a/apps/st/src/pages/statistics/model/panel-config.tsx +++ b/apps/st/src/pages/statistics/model/panel-config.tsx @@ -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: , className: "", }, + // { + // id: "panel-d", + // component: , + // className: "", + // }, { id: "panel-d", - component: , - className: "", - }, - { - id: "panel-e", component: , className: "", }, { - id: "panel-f", + id: "panel-e", component: , className: "", }, { - id: "panel-g", + id: "panel-f", component: , className: "", }, diff --git a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx index 8580ce2f0..2d182b741 100644 --- a/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/notice-panel.tsx @@ -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 ( -
+
-

공지사항

-
@@ -67,17 +71,16 @@ const noticeColumns = [ columnHelper.accessor("pstTtl", { header: "제목", cell: ({ row }) => ( -
-
- {row.original.pstTtl} -
-
- {row.original.wrtrNm} - {formatDate(row.original.regDt)} -
-
+
{row.original.pstTtl}
), }), + columnHelper.accessor("wrtrNm", { + header: "작성자", + }), + columnHelper.accessor("regDt", { + header: "등록일", + cell: ({ row }) => formatDate(row.original.regDt), + }), ]; function formatDate(value: string) { diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx index 4bc29a441..8f85b1351 100644 --- a/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/schedule-calendar-panel.tsx @@ -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 ( -
+
-

일정관리

+

일정관리

-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {data && ( -
- {/* TBD: FullCalendar 상세 설정 (locale, plugins, 이벤트 색상 등) */} +
diff --git a/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx index 7d2ab0eb6..8c162a436 100644 --- a/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/schedule-info-panel.tsx @@ -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 ( -
+
-

일정안내

+

일정안내

+ {data && ( +
+ + {data.ddayLabel} + + + {data.dday} + +
+ )}
-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {error && ( -
+
데이터를 불러올 수 없습니다.
)} {data && ( -
- {/* D-day 표시 */} -
-
- {data.ddayLabel} -
-
- {data.dday} -
-
- {data.currentSe} -
-
- - {/* TBD: 일정 목록 표시 형식 정의 필요 */} -
-
-
- 입력기간 -
-
- {data.inptBgngYmd} ~ {data.inptEndYmd} + <> +
+
+

+ 이번 주 주요 일정 +

+
+ + 현재 진행 상태 + + + { + CURRENT_STATUS_LABEL_MAP[ + data.currentSe as keyof typeof CURRENT_STATUS_LABEL_MAP + ] + } +
-
-
- 검증기간 +
+
+ + {data.inptSeCd}차 입력기간 + +

+ {dayjs(data.inptBgngYmd).format( + "YYYY.MM.DD", + )}{" "} + ~{" "} + {dayjs(data.inptEndYmd).format( + "YYYY.MM.DD", + )} +

-
- {data.vrfcBgngYmd} ~ {data.vrfcEndYmd} +
+ + {data.vrfcSeCd}차 검증기간 + +

+ {dayjs(data.vrfcBgngYmd).format( + "YYYY.MM.DD", + )}{" "} + ~{" "} + {dayjs(data.vrfcEndYmd).format( + "YYYY.MM.DD", + )} +

-
+ )}
diff --git a/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx index 23fe0c963..f973f4e9e 100644 --- a/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/stats-submission-panel.tsx @@ -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: , + GOOD: , + WARNING: , + 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 ( -
-
+ {/*
-

통계입력현황

-
-
- {isLoading && ( -
-
- -
-
- )} - {error && ( -
- 데이터를 불러올 수 없습니다. -
- )} - {data && ( -
- {/* TBD: statusLevel별 이모지 매핑 규칙 필요 */} -
-
- {/* TBD: statusLevel 값에 따른 이모지 표시 */} - {data.statusLevel === "GOOD" - ? "😊" - : data.statusLevel === "WARNING" - ? "⚠️" - : "📊"} +

통계입력현황

+
*/} +
+
+
+

통계입력현황

+ {isLoading && ( +
+
+ +
-
- {data.statusLevel} + )} + {error && ( +
+ 데이터를 불러올 수 없습니다.
-
+ )} +
+ {data && ( +
+ {/* 통계제출률 상태 이모지 */} +
+ { + STATUS_LEVEL_EMOJI[ + data.statusLevel as keyof typeof STATUS_LEVEL_EMOJI + ] + } +
- {/* 통계제출률 프로그레스바 */} -
-
- 통계제출률 - - {data.submissionRate?.toFixed(1)}% - + {/* 통계제출률 프로그레스바 */} +
+
+ + 통계제출률 + + + {data.submissionRate?.toFixed( + 1, + )} + % + + ({data.submittedCount}/ + {data.totalCount}) + + +
+ +
+
+ )} +
+
+
+

+ 검증오류 및 예외 +

+
+ {/* 검증오류 수 카드 */} +
+
+ + 검증오류 수 +
+ {vrfcErrQuery.isLoading && ( +
-
+ )} + {vrfcErrQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {vrfcErrQuery.data && ( +
+ {vrfcErrQuery.data.vrfcErrCount.toLocaleString()} +
+ )}
-
-
-
-
- 제출: {data.submittedCount} - 전체: {data.totalCount} + + {/* 예외신청 수 카드 */} +
+
+ + 예외신청 수 +
+ {ecptReqQuery.isLoading && ( +
-
+ )} + {ecptReqQuery.error && ( +
+ 데이터를 불러올 수 없습니다. +
+ )} + {ecptReqQuery.data && ( +
+ {ecptReqQuery.data.ecptReqCount.toLocaleString()} +
+ )}
- )} +
); diff --git a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx index d8c1fc832..fae349694 100644 --- a/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/unsubmit-panel.tsx @@ -15,23 +15,20 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) { const { data, isLoading, error } = useDashboardUnSubmitCount(params); return ( -
+
-

미제출 지표

+

미제출 지표

{data && ( - + {data.unSubmitCount}건 )}
-
+
{isLoading && (
@@ -51,11 +48,11 @@ export function UnSubmitPanel({ params }: UnSubmitPanelProps) { 미제출 지표가 없습니다.
) : ( -
+
{data.unSubmitList.map((item) => (

{item.statsNm} diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx index 2a753196c..58fb11477 100644 --- a/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-err-panel.tsx @@ -20,15 +20,15 @@ export function VrfcErrPanel({ params }: VrfcErrPanelProps) { const ecptReqQuery = useDashboardEcptReqCount(params); return ( -

+
-

검증오류 및 예외

+

검증오류 및 예외

-
+
{/* 검증오류 수 카드 */}
diff --git a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx index 193e71a37..51834d857 100644 --- a/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx +++ b/apps/st/src/pages/statistics/ui/panels/vrfc-report-panel.tsx @@ -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 = { + NV: "일반", + IMR: "I-MR", +}; + +const TYPE_CODE_COLOR_MAP: Record = { + NV: "gray", + IMR: "red", +}; + export function VrfcReportPanel({ params }: VrfcReportPanelProps) { const { dragHandleProps, isDragging } = useDragHandle(); const { data, isLoading, error } = useDashboardVrfcReport(params); return ( -
+
-

이상치 선별보고서

+

이상치 선별보고서

-
+
{isLoading && ( -
로딩 중...
+
+
+ +
+
)} {error && ( -
+
데이터를 불러올 수 없습니다.
)} {data && ( -
+
{/* TBD: typeCounts 표시 방법 (탭? 필터?) */} {data.typeCounts && Object.keys(data.typeCounts).length > 0 && (
{Object.entries(data.typeCounts).map( ([type, count]) => ( - - {type}: {count} - + {TYPE_CODE_NAME_MAP[type]}:{" "} + {count} + ), )}
)} {/* TBD: 테이블 컬럼 정의, 증감 표시 방법 */} -
+
{data.list.length === 0 ? ( -
+
이상치 항목이 없습니다.
) : ( -
+
{data.list.map((item, index) => (
-
-
+
+ {item.statsNm} -
- {/* TBD: vrfcCd 값별 표시 레이블 */} - - {item.vrfcCd} + + { + TYPE_CODE_NAME_MAP[ + item.vrfcCd + ] + } +
-
+

{item.rsnExpln} -

-
+

+
{item.admdstNm} {/* TBD: 증감 표시 방법 (nowVlCn - compVlCn 계산?) */} - 현재: {item.nowVlCn} / 비교:{" "} - {item.compVlCn} + 현재: {item.nowVlCn ?? "-"}{" "} + / 비교:{" "} + {item.compVlCn ?? "-"}
{/* TBD: 등급/점수/단계/업데이트 시각 필드 미제공 */} diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index e50f90a0d..131ab95dd 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -37,14 +37,14 @@ const StatisticsPage = () => { // 탭이 없으면 Dashboard 표시 if (tabs.length === 0) { return ( -
+
); } return ( -
+
{tabs.map((tab) => { // 렌더링 시점에 컴포넌트가 없으면 바로 로드 if (!componentCache.current.has(tab.id)) {