diff --git a/react/src/components/common/PlatformBadge.tsx b/react/src/components/common/PlatformBadge.tsx index 0606264..be2c9fb 100644 --- a/react/src/components/common/PlatformBadge.tsx +++ b/react/src/components/common/PlatformBadge.tsx @@ -16,7 +16,7 @@ export default function PlatformBadge({ platform }: PlatformBadgeProps) { return ( - android + android ); } diff --git a/react/src/components/common/StatusBadge.tsx b/react/src/components/common/StatusBadge.tsx index d429520..2dadc14 100644 --- a/react/src/components/common/StatusBadge.tsx +++ b/react/src/components/common/StatusBadge.tsx @@ -30,7 +30,8 @@ const VARIANT_STYLES: Record = { /** 상태 dot 뱃지 (완료/실패/진행/예약/활성/비활성) */ export default function StatusBadge({ variant, label }: StatusBadgeProps) { - const style = VARIANT_STYLES[variant]; + // variant가 유효하지 않으면 default로 폴백 + const style = VARIANT_STYLES[variant] ?? VARIANT_STYLES.default; return ( {msg.targetCount} diff --git a/react/src/features/dashboard/pages/DashboardPage.tsx b/react/src/features/dashboard/pages/DashboardPage.tsx index 5c7b332..da16bdd 100644 --- a/react/src/features/dashboard/pages/DashboardPage.tsx +++ b/react/src/features/dashboard/pages/DashboardPage.tsx @@ -46,13 +46,38 @@ function mapCards(data: DashboardData) { ]; } -/** daily → WeeklyChart props 변환 */ +/** daily → WeeklyChart props 변환 (최근 7일 기준, 없는 날짜는 0으로 채움) */ function mapChart(data: DashboardData) { - const trends = data.daily ?? []; - if (trends.length === 0) return []; + const allTrends = data.daily ?? []; - const maxVal = Math.max(...trends.map((t) => Math.max(t.send_count, t.success_count)), 1); - const todayStr = new Date().toISOString().slice(0, 10); + // 최근 7일 날짜 목록 생성 (오늘 포함) + const today = new Date(); + const todayStr = today.toISOString().slice(0, 10); + const last7Days = Array.from({ length: 7 }, (_, i) => { + const d = new Date(today); + d.setDate(today.getDate() - 6 + i); + return d.toISOString().slice(0, 10); + }); + + const dataMap = new Map(allTrends.map((t) => [t.stat_date, t])); + + // 없는 날짜는 send_count 0으로 채움 + const trends = last7Days.map( + (date) => + dataMap.get(date) ?? { + stat_date: date, + send_count: 0, + success_count: 0, + fail_count: 0, + open_count: 0, + ctr: 0, + }, + ); + + const maxVal = Math.max( + ...trends.map((t) => Math.max(t.send_count, t.success_count)), + 1, + ); return trends.map((t) => { const dateStr = t.stat_date ?? ""; diff --git a/react/src/features/device/components/DeviceSlidePanel.tsx b/react/src/features/device/components/DeviceSlidePanel.tsx index 7fc5a45..a42e5da 100644 --- a/react/src/features/device/components/DeviceSlidePanel.tsx +++ b/react/src/features/device/components/DeviceSlidePanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import CopyButton from "@/components/common/CopyButton"; import { deleteDevice } from "@/api/device.api"; +import { formatDate } from "@/utils/format"; import type { DeviceListItem } from "../types"; interface DeviceSlidePanelProps { @@ -68,7 +69,7 @@ export default function DeviceSlidePanel({ // 플랫폼 아이콘 렌더링 const renderPlatformIcon = () => { if (!device) return null; - if (device.platform === "iOS") { + if (device.platform?.toLowerCase() === "ios") { return (
@@ -261,7 +262,7 @@ export default function DeviceSlidePanel({

등록일

- {device.created_at} + {formatDate(device.created_at ?? "")}

@@ -269,7 +270,7 @@ export default function DeviceSlidePanel({ 마지막 활동

- {device.last_active_at} + {formatDate(device.last_active_at ?? "")}

@@ -281,7 +282,7 @@ export default function DeviceSlidePanel({

플랫폼

- {device.platform} + {device.platform?.toLowerCase() === "ios" ? "iOS" : "Android"}

diff --git a/react/src/features/device/components/SecretToggleCell.tsx b/react/src/features/device/components/SecretToggleCell.tsx index a7ce7d6..2d86d24 100644 --- a/react/src/features/device/components/SecretToggleCell.tsx +++ b/react/src/features/device/components/SecretToggleCell.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState } from "react"; import CopyButton from "@/components/common/CopyButton"; interface SecretToggleCellProps { @@ -18,7 +18,10 @@ export default function SecretToggleCell({ onToggle, }: SecretToggleCellProps) { const containerRef = useRef(null); + const buttonRef = useRef(null); + const popoverRef = useRef(null); const isOpen = openKey === dropdownKey; + const [pos, setPos] = useState<{ top: number; left: number } | null>(null); // 외부 클릭 시 닫힘 useEffect(() => { @@ -26,7 +29,9 @@ export default function SecretToggleCell({ const handleClick = (e: MouseEvent) => { if ( containerRef.current && - !containerRef.current.contains(e.target as Node) + !containerRef.current.contains(e.target as Node) && + popoverRef.current && + !popoverRef.current.contains(e.target as Node) ) { onToggle(null); } @@ -35,6 +40,26 @@ export default function SecretToggleCell({ return () => document.removeEventListener("mousedown", handleClick); }, [isOpen, onToggle]); + // 스크롤 시 닫기 + useEffect(() => { + if (!isOpen) return; + const close = () => onToggle(null); + window.addEventListener("scroll", close, true); + return () => window.removeEventListener("scroll", close, true); + }, [isOpen, onToggle]); + + const handleToggle = () => { + if (isOpen) { + onToggle(null); + } else { + const rect = buttonRef.current?.getBoundingClientRect(); + if (rect) { + setPos({ top: rect.bottom + 4, left: rect.left + rect.width / 2 }); + } + onToggle(dropdownKey); + } + }; + return (
e.stopPropagation()} > - {isOpen && ( -
+ {/* fixed로 렌더링해서 테이블 overflow 밖으로 벗어남 */} + {isOpen && pos && ( +
{value} diff --git a/react/src/features/device/pages/DeviceListPage.tsx b/react/src/features/device/pages/DeviceListPage.tsx index a887488..03bcbe9 100644 --- a/react/src/features/device/pages/DeviceListPage.tsx +++ b/react/src/features/device/pages/DeviceListPage.tsx @@ -396,7 +396,7 @@ export default function DeviceListPage() { diff --git a/react/src/utils/format.ts b/react/src/utils/format.ts index 09bf683..f523f10 100644 --- a/react/src/utils/format.ts +++ b/react/src/utils/format.ts @@ -1,9 +1,12 @@ import { format, formatDistanceToNow } from "date-fns"; import { ko } from "date-fns/locale"; -/** 날짜 포맷 (YYYY-MM-DD) */ +/** 날짜 포맷 (YYYY-MM-DD). 서버 기본값(0001-01-01)이면 "-" 반환 */ export function formatDate(date: string | Date): string { - return format(new Date(date), "yyyy-MM-dd"); + if (!date) return "-"; + const d = new Date(date); + if (d.getFullYear() <= 1) return "-"; + return format(d, "yyyy-MM-dd"); } /** 날짜+시간 포맷 (YYYY-MM-DD HH:mm) */