diff --git a/react/src/api/dashboard.api.ts b/react/src/api/dashboard.api.ts index e21d0e0..e7bb078 100644 --- a/react/src/api/dashboard.api.ts +++ b/react/src/api/dashboard.api.ts @@ -1,6 +1,6 @@ import { apiClient } from "./client"; -import type { ApiResponse, PaginatedResponse } from "@/types/api"; -import type { DashboardRequest, DashboardData, ServiceOption } from "@/features/dashboard/types"; +import type { ApiResponse } from "@/types/api"; +import type { DashboardRequest, DashboardData } from "@/features/dashboard/types"; /** 대시보드 통합 조회 */ export function fetchDashboard(data: DashboardRequest, serviceCode?: string) { @@ -10,11 +10,3 @@ export function fetchDashboard(data: DashboardRequest, serviceCode?: string) { serviceCode ? { headers: { "X-Service-Code": serviceCode } } : undefined, ); } - -/** 서비스 목록 조회 (필터 드롭다운용) */ -export function fetchServiceList() { - return apiClient.post>( - "/v1/in/service/list", - { page: 1, pageSize: 100 }, - ); -} diff --git a/react/src/features/dashboard/components/DashboardFilter.tsx b/react/src/features/dashboard/components/DashboardFilter.tsx index b845868..09169b4 100644 --- a/react/src/features/dashboard/components/DashboardFilter.tsx +++ b/react/src/features/dashboard/components/DashboardFilter.tsx @@ -1,13 +1,10 @@ -import { useState, useEffect } from "react"; -import FilterDropdown from "@/components/common/FilterDropdown"; +import { useState } from "react"; import DateRangeInput from "@/components/common/DateRangeInput"; import FilterResetButton from "@/components/common/FilterResetButton"; -import { fetchServiceList } from "@/api/dashboard.api"; export interface DashboardFilterValues { dateStart: string; dateEnd: string; - serviceCode?: string; // 미지정 시 전체 서비스 } interface DashboardFilterProps { @@ -15,8 +12,6 @@ interface DashboardFilterProps { loading?: boolean; } -const ALL_SERVICES_LABEL = "전체 서비스"; - /** 오늘 날짜 YYYY-MM-DD */ function today() { return new Date().toISOString().slice(0, 10); @@ -31,41 +26,16 @@ function thirtyDaysAgo() { export default function DashboardFilter({ onSearch, loading }: DashboardFilterProps) { const [dateStart, setDateStart] = useState(thirtyDaysAgo); const [dateEnd, setDateEnd] = useState(today); - const [selectedService, setSelectedService] = useState(ALL_SERVICES_LABEL); - - // 서비스 목록: label 배열 + code 매핑 - const [serviceOptions, setServiceOptions] = useState([ALL_SERVICES_LABEL]); - const [serviceCodeMap, setServiceCodeMap] = useState>({}); - - // 서비스 목록 로드 - useEffect(() => { - fetchServiceList() - .then((res) => { - const items = res.data.data?.items ?? []; - const labels = [ALL_SERVICES_LABEL, ...items.map((s) => s.service_name)]; - const codeMap: Record = {}; - items.forEach((s) => { - codeMap[s.service_name] = s.service_code; - }); - setServiceOptions(labels); - setServiceCodeMap(codeMap); - }) - .catch(() => { - // 서비스 목록 로드 실패 시 "전체 서비스"만 유지 - }); - }, []); // 초기화 const handleReset = () => { setDateStart(thirtyDaysAgo()); setDateEnd(today()); - setSelectedService(ALL_SERVICES_LABEL); }; // 조회 const handleSearch = () => { - const serviceCode = serviceCodeMap[selectedService]; // 전체 서비스면 undefined - onSearch?.({ dateStart, dateEnd, serviceCode }); + onSearch?.({ dateStart, dateEnd }); }; return ( @@ -80,16 +50,6 @@ export default function DashboardFilter({ onSearch, loading }: DashboardFilterPr disabled={loading} /> - {/* 서비스 드롭다운 */} - - {/* 초기화 */} diff --git a/react/src/features/dashboard/pages/DashboardPage.tsx b/react/src/features/dashboard/pages/DashboardPage.tsx index a97568c..5c7b332 100644 --- a/react/src/features/dashboard/pages/DashboardPage.tsx +++ b/react/src/features/dashboard/pages/DashboardPage.tsx @@ -13,72 +13,78 @@ import type { DashboardData } from "../types"; /** KPI → StatsCards props 변환 */ function mapCards(data: DashboardData) { const { kpi } = data; + const successRate = + kpi.total_send > 0 + ? +(kpi.total_success / kpi.total_send * 100).toFixed(1) + : 0; return [ { label: "오늘 발송 수", - value: formatNumber(kpi.total_sent), - badge: { type: "trend" as const, value: `${Math.abs(kpi.sent_change_rate)}%` }, + value: formatNumber(kpi.total_send), + badge: { type: "trend" as const, value: `${Math.abs(kpi.today_sent_change_rate)}%` }, link: "/statistics", }, { label: "성공률", - value: kpi.success_rate.toFixed(1), + value: successRate.toFixed(1), unit: "%", badge: { type: "icon" as const, icon: "check_circle", color: "bg-green-100 text-green-600" }, link: "/statistics", }, { label: "등록 기기 수", - value: formatNumber(kpi.device_count), + value: formatNumber(kpi.total_devices), badge: { type: "icon" as const, icon: "devices", color: "bg-blue-50 text-primary" }, link: "/devices", }, { label: "활성 서비스", - value: String(kpi.service_count), + value: String(kpi.active_service_count), badge: { type: "icon" as const, icon: "grid_view", color: "bg-purple-50 text-purple-600" }, link: "/services", }, ]; } -/** daily_trend → WeeklyChart props 변환 */ +/** daily → WeeklyChart props 변환 */ function mapChart(data: DashboardData) { - const trends = data.daily_trend; + const trends = data.daily ?? []; if (trends.length === 0) return []; - const maxVal = Math.max(...trends.map((t) => Math.max(t.sent, t.success)), 1); + const maxVal = Math.max(...trends.map((t) => Math.max(t.send_count, t.success_count)), 1); const todayStr = new Date().toISOString().slice(0, 10); return trends.map((t) => { - const isToday = t.date === todayStr; - const mm = t.date.slice(5, 7); - const dd = t.date.slice(8, 10); + const dateStr = t.stat_date ?? ""; + const isToday = dateStr === todayStr; + const mm = dateStr.slice(5, 7); + const dd = dateStr.slice(8, 10); return { label: isToday ? "Today" : `${mm}.${dd}`, - blue: 1 - t.sent / maxVal, // Y축 비율 (0=최상단) - green: 1 - t.success / maxVal, - sent: formatNumber(t.sent), - reach: formatNumber(t.success), + blue: 1 - t.send_count / maxVal, // Y축 비율 (0=최상단) + green: 1 - t.success_count / maxVal, + sent: formatNumber(t.send_count), + reach: formatNumber(t.success_count), }; }); } /** top_messages → RecentMessages props 변환 */ function mapMessages(data: DashboardData) { - return data.top_messages.map((m) => ({ - template: m.message_name, - targetCount: formatNumber(m.target_count), - status: m.status as "완료" | "실패" | "진행" | "예약", - sentAt: m.sent_at, + return (data.top_messages ?? []).map((m) => ({ + template: m.title ?? "", + targetCount: formatNumber(m.total_send_count), + status: (m.status ?? "") as "완료" | "실패" | "진행" | "예약", + sentAt: "", })); } -/** platform_ratio → PlatformDonut props 변환 */ +/** platform_share → PlatformDonut props 변환 */ function mapPlatform(data: DashboardData) { + const shares = data.platform_share ?? []; return { - ios: data.platform_ratio.ios, - android: data.platform_ratio.android, + ios: shares.find((p) => p.platform?.toLowerCase() === "ios")?.ratio ?? 0, + android: shares.find((p) => p.platform?.toLowerCase() === "android")?.ratio ?? 0, }; } @@ -108,15 +114,16 @@ export default function DashboardPage() { try { const res = await fetchDashboard( { start_date: f.dateStart, end_date: f.dateEnd }, - f.serviceCode, ); const d = res.data.data; - // 데이터 비어있는지 판단 + // 데이터 비어있는지 판단 (서비스·기기가 있으면 KPI 표시) const hasData = - d.kpi.total_sent > 0 || - d.daily_trend.length > 0 || - d.top_messages.length > 0; + d.kpi.total_send > 0 || + d.kpi.total_devices > 0 || + d.kpi.active_service_count > 0 || + (d.daily ?? []).length > 0 || + (d.top_messages ?? []).length > 0; if (!hasData) { setEmpty(true); diff --git a/react/src/features/dashboard/types.ts b/react/src/features/dashboard/types.ts index 5e7eb9d..183e54c 100644 --- a/react/src/features/dashboard/types.ts +++ b/react/src/features/dashboard/types.ts @@ -4,49 +4,71 @@ export interface DashboardRequest { end_date: string | null; } -// 대시보드 통합 응답 -export interface DashboardData { - kpi: DashboardKpi; - daily_trend: DailyTrend[]; - platform_ratio: PlatformRatio; - top_messages: TopMessage[]; +// 기간별 통계 +export interface PeriodStat { + send_count: number; + success_count: number; + open_count: number; + ctr: number; } // KPI 지표 export interface DashboardKpi { - total_sent: number; - success_rate: number; - device_count: number; - service_count: number; - sent_change_rate: number; // 전일 대비 증감률 (%) - success_rate_change: number; // 성공률 전일 대비 변화분 (pp) - device_count_change: number; // 등록 기기 수 전일 대비 변화량 - today_sent_change_rate: number; // 오늘 발송 전일 대비 증감률 (%) + total_devices: number; + active_devices: number; + total_messages: number; + total_send: number; + total_success: number; + total_open: number; + avg_ctr: number; + active_service_count: number; + success_rate_change: number; + device_count_change: number; + today_sent_change_rate: number; + today: PeriodStat | null; + this_month: PeriodStat | null; } -// 일별 발송 추이 -export interface DailyTrend { - date: string; - sent: number; - success: number; +// 일별 통계 +export interface DailyStat { + stat_date: string | null; + send_count: number; + success_count: number; + fail_count: number; + open_count: number; + ctr: number; } -// 플랫폼 비율 -export interface PlatformRatio { - ios: number; - android: number; +// 시간대별 통계 +export interface HourlyStat { + hour: number; + send_count: number; + open_count: number; + ctr: number; +} + +// 플랫폼별 통계 +export interface PlatformStat { + platform: string | null; + count: number; + ratio: number; } // 상위 메시지 export interface TopMessage { - message_name: string; - target_count: number; - status: string; - sent_at: string; + message_code: string | null; + title: string | null; + service_name: string | null; + total_send_count: number; + success_count: number; + status: string | null; } -// 서비스 목록 (필터 드롭다운용) -export interface ServiceOption { - service_code: string; - service_name: string; -} +// 대시보드 통합 응답 +export interface DashboardData { + kpi: DashboardKpi; + daily: DailyStat[] | null; + hourly: HourlyStat[] | null; + platform_share: PlatformStat[] | null; + top_messages: TopMessage[] | null; +} \ No newline at end of file diff --git a/react/src/features/service/pages/ServiceDetailPage.tsx b/react/src/features/service/pages/ServiceDetailPage.tsx index de08c87..f817d1b 100644 --- a/react/src/features/service/pages/ServiceDetailPage.tsx +++ b/react/src/features/service/pages/ServiceDetailPage.tsx @@ -154,8 +154,12 @@ export default function ServiceDetailPage() { ); } - // 오늘 발송 = daily_trend의 오늘 데이터 또는 kpi.total_sent (서비스별 조회 시 오늘 기간) - const todaySent = stats?.total_sent ?? 0; + // 오늘 발송 = kpi.total_send (서비스별 조회 시 오늘 기간) + const todaySent = stats?.total_send ?? 0; + const successRate = + stats && stats.total_send > 0 + ? +(stats.total_success / stats.total_send * 100).toFixed(1) + : 0; return (
@@ -167,11 +171,11 @@ export default function ServiceDetailPage() { />