Skip to content

[Feature/#74] 통합 대시보드 UI 구현 - #85

Merged
Seojegyeong merged 7 commits into
developfrom
feature/#74
Mar 8, 2026
Merged

[Feature/#74] 통합 대시보드 UI 구현#85
Seojegyeong merged 7 commits into
developfrom
feature/#74

Conversation

@Seojegyeong

@Seojegyeong Seojegyeong commented Mar 8, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

Closes #74

✨ 변경사항

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

✏️ 작업 내용

통합 대시보드 UI 구현

1. TrafficChart — 실시간 트래픽 차트

  • [ApexCharts] area 차트
  • X축: 00:00~24:00, 6시간 간격 레이블만 표시
  • Y축: K 단위 변환, 0 숨김
  1. BudgetGaugeChart — 예산 소진 게이지
  • 안정(초록) / 주의(노랑) / 위험(빨강) 3단계 상태
  • 진행 바 + 잔액 카드로 구성
  • 피그마 상 디자인과 달라져서 리뷰 및 피드백 부탁드립니다.
  1. PlatformComparison — 플랫폼별 비교
    (1) 왼쪽: PlatformComparisonChart — Google/NAVER/kakao 바 차트
    (2) 오른쪽: PlatformRoasTable — ROAS 순위 테이블
  • 각각의 행 클릭 시 모두 플랫폼 별 대시보드로 이동

스크린샷

CaptureX_2026-03-08_194918_localhost
CaptureX_2026-03-08_194955_localhost
CaptureX_2026-03-08_195436_localhost
CaptureX_2026-03-08_200745_localhost

📂 폴더 구조

src/
├── pages/
│   └── dashboard/
│       ├── overview/
│       │   ├── OverviewDashboard.tsx        # 메인 통합 대시보드 페이지
│       │   ├── OverviewAiReportPanel.tsx    # AI 리포트 드로어 콘텐츠
│       │   ├── overview.mock.ts             # KPI 목업 데이터
│       │   └── aiReport.mock.ts             # AI 리포트 목업 데이터
│       ├── platform/
│       │   └── PlatformDashboard.tsx        # 플랫폼 대시보드 페이지 (미구현)
│       └── timeline/
│           └── Timeline.tsx
│
├── components/
│   ├── dashboard/
│   │   ├── charts/
│   │   │   ├── TrafficChart.tsx             # 실시간 트래픽 변화 차트
│   │   │   ├── trafficChart.mock.ts
│   │   │   ├── BudgetGaugeChart.tsx         # 예산 소진 현황 게이지
│   │   │   └── budgetGaugeChart.mock.ts
│   │   └── platform/
│   │       ├── PlatformComparison.tsx       # 플랫폼별 비교 섹션 (래퍼)
│   │       ├── PlatformComparisonChart.tsx  # 플랫폼 순위 바 차트
│   │       ├── PlatformRoasTable.tsx        # ROAS 성과 순위 테이블
│   │       └── platformComparison.mock.ts
│   │
│   └── common/
│       ├── card/
│       │   ├── Card.tsx                     # 공용 카드 컴포넌트
│       │   └── StatCard.tsx                 # KPI 통계 카드
│       ├── chart/
│       │   └── ChartLegend.tsx              # 차트 범례 컴포넌트
│       └── drawer/
│           └── Drawer.tsx                   # AI 패널 드로어
│
├── types/
│   └── dashboard/
│       └── overview.ts                      # IKpiMetric, IOverviewData 타입
│
└── assets/
    ├── icon/
    │   ├── dashboard/
    │   │   ├── trend-up.svg
    │   │   └── trend-down.svg
    │   └── ai-report/
    │       ├── sparkle.svg
    │       ├── sparkle-circle.svg
    │       ├── warning.svg
    │       ├── link.svg
    │       ├── download.svg
    │       ├── close.svg
    │       └── more.svg
    └── logo/
        └── ai-요약버튼.svg

😅 미완성 작업

  • 통합 대시보드 API 연동 예정
  • 이상 클릭 탐지 기능 구현 예정

📢 논의 사항 및 참고 사항

  1. 미팅 자료 제출 마감이 임박해 PR 범위가 넓어졌습니다..죄송합니다.
  2. UI 구현 과정에서 예산 소진 현황 카드 및 플랫폼 비교 카드 디자인 일부 수정했습니다. 함께 피드백 남겨주시면 감사하겠습니다!!!
  3. 아이콘 의미에 맞지 않는 네이밍 및 중복 사용되는 구간이 있는 것 같아 전체적으로 정리 필요할 것 같습니다.
  4. KPI 카드의 trend up, down 에 따라 다른 의미를 가질 수 있으니 색상에 대해서 의미 정리 필요할 것 같습니다.
  5. *-logo.svg → *-circle.svg 로 이름이 바뀐 아이콘 파일을 참조하는 PlatformCard.tsx, CampaignRow.tsx, AdRow.tsx 세 파일 모두 수정해서 develop에 반영했습니다.

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 대시보드 재설계: 실시간 트래픽 차트, 예산 현황, 플랫폼 비교 분석 추가
    • AI 인사이트 패널 추가 - URL 공유 및 PDF 내보내기 기능 포함
    • 알림 시스템(Toast) 및 드로어(Drawer) 슬라이드 패널 도입
  • 스타일

    • 카드 및 통계 카드 시각 개선 (그림자, 투명도, 애니메이션)
    • 드롭다운 메뉴 스타일 업데이트
    • 새로운 페이드인 애니메이션 추가

@Seojegyeong
Seojegyeong requested review from YermIm and jjjsun March 8, 2026 11:18
@Seojegyeong Seojegyeong self-assigned this Mar 8, 2026
@Seojegyeong Seojegyeong added 🎨 Html&css 마크업 & 스타일링 ✨ Feature 기능 개발 labels Mar 8, 2026
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

대시보드의 실시간 트래픽과 예산 소진 차트 UI를 ApexCharts로 구현하고, 이를 표시할 새로운 공통 컴포넌트들(Drawer, Toast, ChartLegend)과 AI 보고서 패널을 추가했습니다. 기존 카드 컴포넌트들도 스타일 업그레이드와 타입 개선을 진행했습니다.

Changes

Cohort / File(s) Summary
로고 및 레이아웃 마이너 수정
src/components/ads/CampaignRow.tsx, src/components/common/dropdownmenu/DropdownMenu.tsx
플랫폼 로고 SVG 파일 변경(circle 버전으로 교체), 드롭다운 메뉴의 위치 지정과 폰트 크기 조정
카드 컴포넌트 개선
src/components/common/card/Card.tsx, src/components/common/card/StatCard.tsx
Card의 description prop을 ReactNode로 확장, 카드 스타일을 translucent/backdrop blur로 업그레이드, StatCard의 타이포그래피와 트렌드 배지 스타일 정비
새로운 기본 UI 컴포넌트
src/components/common/chart/ChartLegend.tsx, src/components/common/drawer/Drawer.tsx, src/components/common/toast/Toast.tsx
차트 범례, 슬라이드-인 드로우 패널, 자동 해제 토스트 알림 컴포넌트 신규 추가
차트 및 모형 데이터
src/components/dashboard/charts/BudgetGaugeChart.tsx, src/components/dashboard/charts/TrafficChart.tsx, src/components/dashboard/charts/budgetGaugeChart.mock.ts, src/components/dashboard/charts/trafficChart.mock.ts
예산 소진 게이지와 실시간 트래픽 선형 차트 컴포넌트 구현, 해당 모형 데이터 추가
플랫폼 비교 섹션
src/components/dashboard/platform/PlatformComparison.tsx, src/components/dashboard/platform/PlatformComparisonChart.tsx, src/components/dashboard/platform/PlatformRoasTable.tsx, src/components/dashboard/platform/platformComparison.mock.ts
플랫폼별 성과 비교 차트와 ROAS 순위 테이블, 해당 모형 데이터 추가
대시보드 통합 및 기반 설정
src/pages/dashboard/overview/OverviewDashboard.tsx, src/pages/dashboard/overview/OverviewAiReportPanel.tsx, src/pages/dashboard/overview/aiReport.mock.ts, src/index.css, src/vite-env.d.ts
대시보드 개요 페이지 재구성(차트 통합, AI 패널 추가, 토스트 상태 관리), AI 보고서 패널 컴포넌트, 인쇄 스타일과 fade-in-up 애니메이션 추가, SVG React 컴포넌트 타입 선언

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Dashboard as OverviewDashboard
    participant Drawer
    participant Toast
    participant Panel as OverviewAiReportPanel

    User->>Dashboard: AI 버튼 클릭
    Dashboard->>Dashboard: isAiPanelOpen = true
    Dashboard->>Drawer: isOpen = true
    Drawer->>Drawer: 슬라이드-인 애니메이션
    Drawer->>Panel: OverviewAiReportPanel 렌더링

    User->>Drawer: 드롭다운 메뉴 선택<br/>(링크 공유 또는 PDF 저장)
    alt 링크 공유
        Drawer->>Drawer: 클립보드 복사
        Drawer->>Toast: showToast('success')
        Toast->>User: 성공 토스트 표시
    else PDF 저장
        Drawer->>Drawer: body print 클래스 토글
        Drawer->>Window: window.print()
        Window->>Drawer: 인쇄 완료 후 cleanup
    end

    User->>Drawer: 패널 닫기
    Dashboard->>Dashboard: isAiPanelOpen = false
    Drawer->>Drawer: 슬라이드-아웃 애니메이션
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

검토 포인트:

  • 새로운 컴포넌트가 9개 추가되어 코드 분량과 다양성이 높음
  • 기존 컴포넌트(Card, StatCard, OverviewDashboard)의 타입과 스타일 변경으로 호환성 검증 필요
  • OverviewDashboard의 상태 관리(isAiPanelOpen, toast) 로직 검증
  • ApexCharts 설정의 정확성과 성능(특히 trafficChart의 대량 데이터 처리)
  • Drawer의 포탈 기반 렌더링과 스크롤 비활성화 동작 확인
  • 인쇄 기능의 CSS 동작과 cleanup 로직

Possibly related PRs

Suggested reviewers

  • YermIm
  • jjjsun
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항인 통합 대시보드 UI 구현을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed PR이 #74 이슈의 주요 목표들을 충족합니다: ApexCharts 통합, 트래픽 차트 구현(실시간 라인/에어리어), 예산 게이지 차트 구현, 색상 구간 처리. 클릭 이상 징후 마커는 미완료 항목입니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 #74 이슈의 대시보드 UI 구현 범위 내입니다. 추가된 공용 컴포넌트(Card, Drawer, ChartLegend, Toast)도 대시보드 기능을 지원하는 필수 요소입니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 대부분 따르고 있으며, 관련 이슈, 변경사항 체크박스, 작업 내용, 폴더 구조, 미완성 작업, 논의 사항을 포함하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#74

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Mar 8, 2026

Copy link
Copy Markdown

📚 Storybook 배포 완료

항목 링크
📖 Storybook https://69a147b60a56365d9e2185ef-utvpledifb.chromatic.com/
🔍 Chromatic https://www.chromatic.com/build?appId=69a147b60a56365d9e2185ef&number=38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (15)
src/pages/dashboard/overview/aiReport.mock.ts (1)

1-31: 타입 정의 추가를 권장합니다

Mock 데이터의 구조를 명시하는 인터페이스를 추가하면 타입 안정성이 향상됩니다. 소비하는 컴포넌트(OverviewAiReportPanel)에서 타입 추론이 명확해지고, 구조 변경 시 타입 체크가 가능해집니다.

♻️ 타입 정의 추가 제안
+interface IAiReportSection {
+  title: string;
+  content: string;
+}
+
+export interface IAiReportData {
+  label: string;
+  title: string;
+  strategySuggestion: IAiReportSection;
+  sections: IAiReportSection[];
+  warning: IAiReportSection;
+}
+
-export const aiReportMockData = {
+export const aiReportMockData: IAiReportData = {
   label: "오늘의 데이터 분석 중",
   // ... rest of the data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/dashboard/overview/aiReport.mock.ts` around lines 1 - 31, Add
explicit TypeScript interfaces for the mock shape (e.g., AiReport with nested
StrategySuggestion, Section, Warning types), export them and annotate the mock:
declare interfaces matching the properties used in aiReportMockData (label,
title, strategySuggestion, sections[], warning), then change the declaration to
const aiReportMockData: AiReport = { ... } so OverviewAiReportPanel and other
consumers can import the AiReport type and get compile-time validation when the
structure changes.
src/components/dashboard/platform/PlatformComparisonChart.tsx (1)

87-91: API 연동 시 고려사항

PR 설명에 API 연동이 예정되어 있다고 하셨는데, 현재 series가 모듈 레벨 상수로 정의되어 있어 mock 데이터가 동적으로 변경되어도 반영되지 않습니다. API 연동 시에는 series를 컴포넌트 내부에서 useMemo로 계산하거나 props로 받도록 변경이 필요합니다.

♻️ API 연동을 위한 구조 제안 (추후 작업용)
// API 연동 시 아래와 같이 변경 고려
interface IPlatformComparisonChartProps {
  data: IPlatformStats[];
}

export default function PlatformComparisonChart({ data }: IPlatformComparisonChartProps) {
  const series = useMemo(() => [
    { name: "클릭률", data: data.map((p) => p.clickRate) },
    { name: "전환률", data: data.map((p) => p.conversionRate) },
    { name: "노출수", data: data.map((p) => p.impressionRate) },
  ], [data]);
  
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/platform/PlatformComparisonChart.tsx` around lines
87 - 91, The series array is defined at module scope using
platformComparisonMock so updates from API or props won't re-render; move
creation of series into the PlatformComparisonChart component and derive it from
incoming data (or state) using useMemo to avoid recomputation. Specifically,
replace the module-level const series with a memoized value inside
PlatformComparisonChart (depend on the prop or API data array) and build objects
named "클릭률"/"전환률"/"노출수" from that data (reference symbols: series,
PlatformComparisonChart, platformComparisonMock, useMemo, data prop).
src/components/dashboard/platform/PlatformRoasTable.tsx (1)

9-13: 타입 안정성 강화 권장

platformLogoMap의 키 타입을 IPlatformStats["name"]으로 명시하면 mock 데이터와의 타입 일관성이 보장됩니다.

♻️ 타입 명시 제안
+import type { IPlatformStats } from "./platformComparison.mock";
+
-const platformLogoMap = {
+const platformLogoMap: Record<IPlatformStats["name"], JSX.Element> = {
   Google: <GoogleLogo className="h-7 w-auto" />,
   NAVER: <NaverLogo className="h-7 w-auto" />,
   kakao: <KakaoLogo className="h-7 w-auto" />,
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx` around lines 9 - 13,
The platformLogoMap object uses platform name keys but lacks an explicit key
type; update its declaration to type the keys as IPlatformStats["name"] to
enforce consistency with mock data and ensure type safety—locate the
platformLogoMap constant in PlatformRoasTable.tsx and declare it with an index
signature or mapped type using IPlatformStats["name"] so keys like "Google",
"NAVER", and "kakao" are validated by TypeScript.
src/pages/dashboard/overview/OverviewDashboard.tsx (3)

127-163: dropdownItems 배열이 매 렌더링마다 재생성됩니다.

현재 규모에서는 성능 이슈가 없지만, Drawer가 자주 열리거나 복잡해질 경우 useMemo로 메모이제이션하면 불필요한 재계산을 방지할 수 있습니다.

♻️ useMemo 적용 예시
const dropdownItems = useMemo(() => [
  {
    label: "링크 공유하기",
    icon: <LinkIcon ... />,
    onClick: () => { ... },
  },
  {
    label: "PDF로 저장하기",
    icon: <DownloadIcon ... />,
    onClick: () => { ... },
  },
], [showToast]); // showToast가 의존성이면 useCallback으로 안정화 필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/dashboard/overview/OverviewDashboard.tsx` around lines 127 - 163,
The dropdownItems array is recreated on every render; wrap its creation in
React's useMemo to memoize it (e.g., create a memoized dropdownItems via
useMemo(() => [...], [showToast])) and stabilize any callbacks it depends on
(e.g., wrap showToast usage with useCallback or include only stable deps) so the
items (with LinkIcon/DownloadIcon and their onClick handlers) are not rebuilt
unnecessarily when OverviewDashboard rerenders.

46-54: 날짜/시간이 컴포넌트 렌더링 시점에 갱신됩니다.

new Date()가 렌더링마다 호출되어 시간이 변경될 수 있습니다. "실시간" 업데이트가 의도라면 interval로 주기적 갱신이 필요하고, 초기 로드 시점 고정이 의도라면 상태나 ref로 캡처하는 것이 좋습니다.

💡 시간 고정 방식 예시
// 컴포넌트 마운트 시점 고정
const [timestamp] = useState(() => new Date());

// 또는 실시간 갱신
const [timestamp, setTimestamp] = useState(() => new Date());
useEffect(() => {
  const interval = setInterval(() => setTimestamp(new Date()), 60000);
  return () => clearInterval(interval);
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/dashboard/overview/OverviewDashboard.tsx` around lines 46 - 54, The
JSX in OverviewDashboard currently calls new Date() during render causing the
displayed timestamp to change on every render; capture the timestamp at mount or
set up a timer instead: in the OverviewDashboard component initialize state or a
ref (e.g., timestamp via useState(() => new Date()) or useRef(new Date())) to
freeze the value for initial load, or if real-time updates are intended add a
useEffect that sets an interval to update the timestamp state (and clears it on
unmount) and replace the direct new Date() call in the JSX with that state/ref.

23-36: 컴포넌트 로직이 다소 복잡해지고 있습니다.

AI 패널 관련 상태(isAiPanelOpen, 토스트, 드롭다운 액션 등)를 커스텀 훅(useAiReportPanel)으로 분리하면 컴포넌트 가독성이 향상됩니다. 현재 규모에서는 필수는 아니지만, 향후 기능 추가 시 고려해 주세요. As per coding guidelines, 페이지에 비즈니스 로직이 과도하지 않은지 확인하고 커스텀 훅으로의 분리 여부를 검토합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/dashboard/overview/OverviewDashboard.tsx` around lines 23 - 36,
OverviewDashboard contains AI panel state and toast logic (isAiPanelOpen,
setIsAiPanelOpen, toast, setToast, showToast) which bloats the component;
extract these into a custom hook named useAiReportPanel that encapsulates panel
open/close, toast state and showToast behavior and any dropdown/action handlers,
then replace the inline state in OverviewDashboard with calls to
useAiReportPanel to return {isAiPanelOpen, openAiPanel, closeAiPanel, toast,
showToast, ...actions} so the component simply consumes the hook and renders UI.
src/components/common/chart/ChartLegend.tsx (1)

17-24: 색상 표시 요소에 대한 접근성 개선을 고려해 주세요.

색상 점(dot)은 시각적 정보만 제공하므로, 스크린 리더에서 불필요하게 읽히지 않도록 aria-hidden="true"를 추가하면 좋습니다.

♿ 접근성 개선 제안
         <div key={index} className="flex items-center gap-1.5">
           <div
+            aria-hidden="true"
             className={twMerge("w-1.5 h-1.5 rounded-full", item.colorClass)}
           />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/chart/ChartLegend.tsx` around lines 17 - 24, The color
dot div in the ChartLegend component is purely decorative and should not be
announced by assistive tech; update the element rendered for the color marker
(the div using twMerge("w-1.5 h-1.5 rounded-full", item.colorClass) inside the
map in ChartLegend.tsx) to include aria-hidden="true" (or role="presentation")
so screen readers ignore it, leaving the accessible label span ({item.label}) as
the only announced content.
src/pages/dashboard/overview/OverviewAiReportPanel.tsx (1)

35-47: 섹션 렌더링에서 key로 index를 사용하고 있습니다.

현재 목업 데이터가 정적이라 문제가 없지만, 향후 API 연동 시 섹션 데이터에 고유 ID가 있다면 해당 값을 key로 사용하는 것이 좋습니다. 데이터 재정렬이나 필터링 시 React 재조정(reconciliation) 최적화에 도움이 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/dashboard/overview/OverviewAiReportPanel.tsx` around lines 35 - 47,
The map in data.sections.map is using the array index (key={idx}) which can
break reconciliation; change the key to a stable unique identifier from each
section (e.g., use section.id or section.uuid) inside the OverviewAiReportPanel
rendering, falling back to the index only if no unique id exists. Update the key
prop on the mapped <div> (currently key={idx}) to use that unique field
(section.id) and ensure the source data mapping or type reflects the id field so
future API-backed sections use stable keys.
src/components/dashboard/charts/BudgetGaugeChart.tsx (3)

64-71: 상태별 색상 로직이 중복됨

statusPointClasses(라인 16-20)에서 이미 상태별 색상을 정의했는데, 라인 66-70에서 동일한 조건문으로 색상을 다시 결정하고 있습니다. statusPointClasses[status]를 재사용하면 유지보수성이 향상됩니다.

♻️ 중복 제거 제안
           <div
             className={twMerge(
               "absolute top-0 left-0 h-full rounded-full transition-all duration-1000 ease-out",
-              status === "안정"
-                ? "bg-status-green"
-                : status === "주의"
-                  ? "bg-status-yellow"
-                  : "bg-status-red",
+              statusPointClasses[status],
             )}
             style={{ width: `${Math.min(percentage, 100)}%` }}
           />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/BudgetGaugeChart.tsx` around lines 64 - 71,
The status color decision is duplicated: instead of re-evaluating status in the
className block inside the BudgetGaugeChart JSX, reuse the existing mapping
statusPointClasses by replacing the ternary chain with
statusPointClasses[status]; update the className expression (the element with
className={twMerge(...)} that currently uses status === "안정" ? ... : ...) to
reference statusPointClasses[status] so the component uses the single source of
truth for status colors and eliminates the duplicated logic.

10-20: Record 타입을 더 명확한 유니온 타입으로 개선 가능

Record<string, string> 대신 명시적인 상태 타입을 사용하면 타입 안정성이 향상됩니다. 현재는 존재하지 않는 키로 접근해도 컴파일 에러가 발생하지 않습니다.

🔧 타입 개선 제안
+type TBudgetStatus = "안정" | "주의" | "위험";
+
-const statusBadgeClasses: Record<string, string> = {
+const statusBadgeClasses: Record<TBudgetStatus, string> = {
   안정: "bg-status-green/[0.08] text-status-green",
   주의: "bg-status-yellow/[0.08] text-status-yellow",
   위험: "bg-status-red/[0.08] text-status-red",
 };

-const statusPointClasses: Record<string, string> = {
+const statusPointClasses: Record<TBudgetStatus, string> = {
   안정: "bg-status-green",
   주의: "bg-status-yellow",
   위험: "bg-status-red",
 };

getStatus 함수의 반환 타입도 TBudgetStatus로 명시하면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/BudgetGaugeChart.tsx` around lines 10 - 20,
Replace the loose Record<string,string> typings with a concrete union-keyed
mapping using the TBudgetStatus union: change statusBadgeClasses and
statusPointClasses to use Record<TBudgetStatus, string> (or a mapped type like {
[K in TBudgetStatus]: string }) and update the getStatus function signature to
return TBudgetStatus so lookups are type-safe; ensure any code that derives or
uses status values is adjusted to produce TBudgetStatus to avoid indexing with
unknown keys.

62-74: Progress bar에 접근성 속성 추가 필요

시각적으로만 진행률을 표시하고 있어 스크린 리더 사용자가 정보를 인식하기 어렵습니다. 코딩 가이드라인에 따라 시맨틱 HTML과 ARIA 속성 사용을 확인해 주세요.

♿ 접근성 개선 제안
-        <div className="relative h-3 w-full bg-bg-disabled/50 rounded-full overflow-hidden">
+        <div
+          role="progressbar"
+          aria-valuenow={percentage}
+          aria-valuemin={0}
+          aria-valuemax={100}
+          aria-label={`예산 소진율 ${percentage}%`}
+          className="relative h-3 w-full bg-bg-disabled/50 rounded-full overflow-hidden"
+        >
           <div
             className={twMerge(
               "absolute top-0 left-0 h-full rounded-full transition-all duration-1000 ease-out",
               status === "안정"
                 ? "bg-status-green"
                 : status === "주의"
                   ? "bg-status-yellow"
                   : "bg-status-red",
             )}
             style={{ width: `${Math.min(percentage, 100)}%` }}
           />
         </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/BudgetGaugeChart.tsx` around lines 62 - 74,
The progress bar in BudgetGaugeChart.tsx is purely visual; update the rendered
element (the inner div that uses twMerge and style width) to be an accessible
progressbar by adding role="progressbar" plus
aria-valuenow={Math.min(percentage,100)}, aria-valuemin={0}, aria-valuemax={100}
and an informative aria-label or aria-labelledby that includes the current
status and percentage (use existing status and percentage props/variables);
ensure any decorative outer container remains non-interactive and consider
adding a visually hidden text node for screen readers if more context is needed.
src/components/common/drawer/Drawer.tsx (3)

11-20: dropdownItems 타입과 TMenuItem 간의 일관성

DropdownMenuitems prop은 TMenuItem[] 타입을 기대하는데(관련 코드 스니펫 참조), IDrawerProps.dropdownItems에는 active 필드가 없습니다. 현재는 구조적 타이핑으로 호환되지만, TMenuItem 타입을 import해서 재사용하면 타입 일관성이 향상됩니다.

🔧 타입 일관성 개선 제안
+import { type TMenuItem, DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu";
-import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu";

 export interface IDrawerProps {
   isOpen: boolean;
   onClose: () => void;
   title?: ReactNode;
   children: ReactNode;
   className?: string;
   hideHeader?: boolean;
-  dropdownItems?: { label: string; icon?: ReactNode; onClick: () => void }[];
+  dropdownItems?: TMenuItem[];
   disableScroll?: boolean;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/drawer/Drawer.tsx` around lines 11 - 20,
IDrawerProps.dropdownItems should use the same menu item type as DropdownMenu to
ensure type consistency: import TMenuItem and change the dropdownItems
declaration on the IDrawerProps interface from "{ label: string; icon?:
ReactNode; onClick: () => void }[]" to "TMenuItem[]", keeping it optional;
update any code that constructs dropdownItems to satisfy TMenuItem (e.g., add an
optional active field if required) so DropdownMenu(items={...}) no longer relies
on structural typing mismatches.

67-73: Drawer 패널에 포커스 트랩 고려

현재 구현에서는 Drawer가 열렸을 때 포커스가 Drawer 내부에 갇히지 않아, Tab 키로 Drawer 외부 요소에 접근할 수 있습니다. 접근성 관점에서 포커스 트랩 구현을 고려해 주세요. focus-trap-react 같은 라이브러리를 사용하거나 직접 구현할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/drawer/Drawer.tsx` around lines 67 - 73, The Drawer
component currently allows focus to escape when open; update the Drawer
(component named Drawer, the root panel div that uses isOpen and className) to
implement a focus trap so keyboard Tab/Shift+Tab stay within the drawer while
open. Integrate a focus-trap solution (e.g., wrap the panel contents with
FocusTrap from focus-trap-react and enable options like escapeDeactivates and
clickOutsideDeactivates as appropriate) or add an internal focus-trapping
mechanism that mounts when isOpen is true, sets initial focus to a logical
element inside, cycles Tab/Shift+Tab, and restores focus on close; ensure Escape
handling and cleanup are preserved.

38-47: Escape 키로 Drawer 닫기 기능 추가 필요

접근성 가이드라인에 따라 모달/드로어는 Escape 키로 닫을 수 있어야 합니다. 현재는 오버레이 클릭이나 닫기 버튼으로만 닫을 수 있습니다.

♿ Escape 키 핸들러 추가 제안
   useEffect(() => {
     if (isOpen) {
       document.body.style.overflow = "hidden";
+
+      const handleKeyDown = (e: KeyboardEvent) => {
+        if (e.key === "Escape") {
+          onClose();
+        }
+      };
+      document.addEventListener("keydown", handleKeyDown);
+      return () => {
+        document.body.style.overflow = "";
+        document.removeEventListener("keydown", handleKeyDown);
+      };
     } else {
       document.body.style.overflow = "";
     }
-    return () => {
-      document.body.style.overflow = "";
-    };
   }, [isOpen]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/drawer/Drawer.tsx` around lines 38 - 47, The Drawer
component's useEffect should also register a keydown listener for the Escape key
when isOpen is true and call the component's close handler (e.g., onClose or
closeDrawer) to close the drawer; add an event listener inside the existing
useEffect that checks e.key === "Escape" (or e.keyCode === 27 for broader
support) and invokes the same close function used by the overlay/close button,
and ensure you remove the listener in the effect's cleanup and only attach it
while isOpen is true so it doesn't run when the drawer is closed; keep the
existing body overflow toggling logic and ensure both the overflow reset and the
removed keydown listener happen in the cleanup.
src/components/dashboard/charts/TrafficChart.tsx (1)

6-11: 모듈 레벨에서 데이터 바인딩으로 인해 재사용 불가

labels, clicks, yAxisMax, options, series가 모듈 로드 시점에 고정되어 다른 데이터로 차트를 렌더링할 수 없습니다. 현재 목업 데이터 연동 단계라면 괜찮지만, API 연동 시 컴포넌트가 props를 통해 데이터를 받도록 리팩토링이 필요합니다.

또한 clicks 배열이 빈 경우 Math.max(...clicks)-Infinity를 반환하여 차트가 깨질 수 있습니다.

♻️ Props 기반 구조로 개선 제안
+interface ITrafficChartProps {
+  labels: string[];
+  clicks: number[];
+}

-const { labels, clicks } = trafficChartMock;
-
-// x축 시간대
-const LABEL_HOURS = new Set(["00:00", "06:00", "12:00", "18:00", "24:00"]);
-
-const yAxisMax = Math.ceil(Math.max(...clicks) / 10000) * 10000;
+const LABEL_HOURS = new Set(["00:00", "06:00", "12:00", "18:00", "24:00"]);

-export default function TrafficChart() {
+export default function TrafficChart({ labels, clicks }: ITrafficChartProps) {
+  const yAxisMax = clicks.length > 0
+    ? Math.ceil(Math.max(...clicks) / 10000) * 10000
+    : 10000;
+
+  const options: ApexOptions = useMemo(() => ({
+    // ... 기존 options 내용
+  }), [yAxisMax]);
+
+  const series = useMemo(() => [
+    {
+      name: "클릭수",
+      data: labels.map((label, i) => ({ x: label, y: clicks[i] })),
+    },
+  ], [labels, clicks]);
+
   return (
     <ReactApexChart
       type="area"
       options={options}
       series={series}
       height={360}
     />
   );
 }

PR 목표에 API 연동이 남은 작업으로 명시되어 있으니, 해당 작업 시 참고해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/TrafficChart.tsx` around lines 6 - 11, Move
the module-level bindings into the TrafficChart component and accept chart data
via props: stop using the top-level labels, clicks, yAxisMax, options, and
series constants and instead add props (e.g., labels: string[], clicks:
number[]) to the TrafficChart component, compute yAxisMax inside the component
(e.g., guard against empty clicks by using Math.max(0, ...clicks) or a fallback
like 0), and build options and series from those props (use useMemo keyed on
labels/clicks to avoid unnecessary recompute). Ensure references to LABEL_HOURS
remain constant if needed but compute any values that depend on data (yAxisMax,
series, options) inside the component so the chart can be reused with different
datasets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/common/toast/Toast.tsx`:
- Around line 25-28: 현재 Toast 컴포넌트의 useEffect는 duration과 onClose를 의존성으로 하여 부모가
onClose를 매 렌더마다 새로 생성하면 타이머가 재설정됩니다; 이를 고치려면 Toast.tsx 내부에서 onClose를 useRef (예:
const onCloseRef = useRef(onClose))에 저장하고 useEffect에서 onCloseRef.current를 호출하도록
변경해 duration만 의존성 배열에 남기거나, useEffect에서 onClose를 의존성으로 유지하되 부모에 useCallback으로
핸들러를 전달하도록 문서화하세요; 참고 심볼: useEffect (타이머 생성), duration, onClose, onCloseRef,
clearTimeout.
- Around line 30-45: The Toast component currently calls createPortal with
document.body on initial render which can cause SSR hydration issues; introduce
an isMounted boolean state in the Toast component (useState(false)) and flip it
to true inside useEffect(() => setIsMounted(true), []) so the portal is only
created on the client; then guard the render so that if !isMounted you return
null (or a non-portal fallback) and only call createPortal(...) to document.body
when isMounted is true, keeping the existing props/variantClasses and aria
attributes intact.

In `@src/components/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 28-29: Guard against totalBudget being zero or negative in
BudgetGaugeChart by changing the percentage and remaining calculations: compute
percentage as totalBudget > 0 ? Math.round((spent / totalBudget) * 100) : 0, and
compute remaining as Math.max(0, totalBudget - spent) so the UI never shows NaN
or negative remaining; update the variables named percentage and remaining in
the BudgetGaugeChart component (where spent and totalBudget are used)
accordingly.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 39-66: The clickable row in PlatformRoasTable.tsx uses a plain
<div> with onClick (the block keyed by platform.name that calls
navigate("/platform")), which is not keyboard-accessible; change it to a
semantic interactive element (preferably a <button>) or add role="button",
tabIndex={0}, and an onKeyDown handler that triggers the same navigation on
Enter/Space, and ensure the element has an accessible name (e.g., include
platform.name in an aria-label) while preserving existing classes and
platformLogoMap usage so style and visuals remain unchanged.

---

Nitpick comments:
In `@src/components/common/chart/ChartLegend.tsx`:
- Around line 17-24: The color dot div in the ChartLegend component is purely
decorative and should not be announced by assistive tech; update the element
rendered for the color marker (the div using twMerge("w-1.5 h-1.5 rounded-full",
item.colorClass) inside the map in ChartLegend.tsx) to include
aria-hidden="true" (or role="presentation") so screen readers ignore it, leaving
the accessible label span ({item.label}) as the only announced content.

In `@src/components/common/drawer/Drawer.tsx`:
- Around line 11-20: IDrawerProps.dropdownItems should use the same menu item
type as DropdownMenu to ensure type consistency: import TMenuItem and change the
dropdownItems declaration on the IDrawerProps interface from "{ label: string;
icon?: ReactNode; onClick: () => void }[]" to "TMenuItem[]", keeping it
optional; update any code that constructs dropdownItems to satisfy TMenuItem
(e.g., add an optional active field if required) so DropdownMenu(items={...}) no
longer relies on structural typing mismatches.
- Around line 67-73: The Drawer component currently allows focus to escape when
open; update the Drawer (component named Drawer, the root panel div that uses
isOpen and className) to implement a focus trap so keyboard Tab/Shift+Tab stay
within the drawer while open. Integrate a focus-trap solution (e.g., wrap the
panel contents with FocusTrap from focus-trap-react and enable options like
escapeDeactivates and clickOutsideDeactivates as appropriate) or add an internal
focus-trapping mechanism that mounts when isOpen is true, sets initial focus to
a logical element inside, cycles Tab/Shift+Tab, and restores focus on close;
ensure Escape handling and cleanup are preserved.
- Around line 38-47: The Drawer component's useEffect should also register a
keydown listener for the Escape key when isOpen is true and call the component's
close handler (e.g., onClose or closeDrawer) to close the drawer; add an event
listener inside the existing useEffect that checks e.key === "Escape" (or
e.keyCode === 27 for broader support) and invokes the same close function used
by the overlay/close button, and ensure you remove the listener in the effect's
cleanup and only attach it while isOpen is true so it doesn't run when the
drawer is closed; keep the existing body overflow toggling logic and ensure both
the overflow reset and the removed keydown listener happen in the cleanup.

In `@src/components/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 64-71: The status color decision is duplicated: instead of
re-evaluating status in the className block inside the BudgetGaugeChart JSX,
reuse the existing mapping statusPointClasses by replacing the ternary chain
with statusPointClasses[status]; update the className expression (the element
with className={twMerge(...)} that currently uses status === "안정" ? ... : ...)
to reference statusPointClasses[status] so the component uses the single source
of truth for status colors and eliminates the duplicated logic.
- Around line 10-20: Replace the loose Record<string,string> typings with a
concrete union-keyed mapping using the TBudgetStatus union: change
statusBadgeClasses and statusPointClasses to use Record<TBudgetStatus, string>
(or a mapped type like { [K in TBudgetStatus]: string }) and update the
getStatus function signature to return TBudgetStatus so lookups are type-safe;
ensure any code that derives or uses status values is adjusted to produce
TBudgetStatus to avoid indexing with unknown keys.
- Around line 62-74: The progress bar in BudgetGaugeChart.tsx is purely visual;
update the rendered element (the inner div that uses twMerge and style width) to
be an accessible progressbar by adding role="progressbar" plus
aria-valuenow={Math.min(percentage,100)}, aria-valuemin={0}, aria-valuemax={100}
and an informative aria-label or aria-labelledby that includes the current
status and percentage (use existing status and percentage props/variables);
ensure any decorative outer container remains non-interactive and consider
adding a visually hidden text node for screen readers if more context is needed.

In `@src/components/dashboard/charts/TrafficChart.tsx`:
- Around line 6-11: Move the module-level bindings into the TrafficChart
component and accept chart data via props: stop using the top-level labels,
clicks, yAxisMax, options, and series constants and instead add props (e.g.,
labels: string[], clicks: number[]) to the TrafficChart component, compute
yAxisMax inside the component (e.g., guard against empty clicks by using
Math.max(0, ...clicks) or a fallback like 0), and build options and series from
those props (use useMemo keyed on labels/clicks to avoid unnecessary recompute).
Ensure references to LABEL_HOURS remain constant if needed but compute any
values that depend on data (yAxisMax, series, options) inside the component so
the chart can be reused with different datasets.

In `@src/components/dashboard/platform/PlatformComparisonChart.tsx`:
- Around line 87-91: The series array is defined at module scope using
platformComparisonMock so updates from API or props won't re-render; move
creation of series into the PlatformComparisonChart component and derive it from
incoming data (or state) using useMemo to avoid recomputation. Specifically,
replace the module-level const series with a memoized value inside
PlatformComparisonChart (depend on the prop or API data array) and build objects
named "클릭률"/"전환률"/"노출수" from that data (reference symbols: series,
PlatformComparisonChart, platformComparisonMock, useMemo, data prop).

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 9-13: The platformLogoMap object uses platform name keys but lacks
an explicit key type; update its declaration to type the keys as
IPlatformStats["name"] to enforce consistency with mock data and ensure type
safety—locate the platformLogoMap constant in PlatformRoasTable.tsx and declare
it with an index signature or mapped type using IPlatformStats["name"] so keys
like "Google", "NAVER", and "kakao" are validated by TypeScript.

In `@src/pages/dashboard/overview/aiReport.mock.ts`:
- Around line 1-31: Add explicit TypeScript interfaces for the mock shape (e.g.,
AiReport with nested StrategySuggestion, Section, Warning types), export them
and annotate the mock: declare interfaces matching the properties used in
aiReportMockData (label, title, strategySuggestion, sections[], warning), then
change the declaration to const aiReportMockData: AiReport = { ... } so
OverviewAiReportPanel and other consumers can import the AiReport type and get
compile-time validation when the structure changes.

In `@src/pages/dashboard/overview/OverviewAiReportPanel.tsx`:
- Around line 35-47: The map in data.sections.map is using the array index
(key={idx}) which can break reconciliation; change the key to a stable unique
identifier from each section (e.g., use section.id or section.uuid) inside the
OverviewAiReportPanel rendering, falling back to the index only if no unique id
exists. Update the key prop on the mapped <div> (currently key={idx}) to use
that unique field (section.id) and ensure the source data mapping or type
reflects the id field so future API-backed sections use stable keys.

In `@src/pages/dashboard/overview/OverviewDashboard.tsx`:
- Around line 127-163: The dropdownItems array is recreated on every render;
wrap its creation in React's useMemo to memoize it (e.g., create a memoized
dropdownItems via useMemo(() => [...], [showToast])) and stabilize any callbacks
it depends on (e.g., wrap showToast usage with useCallback or include only
stable deps) so the items (with LinkIcon/DownloadIcon and their onClick
handlers) are not rebuilt unnecessarily when OverviewDashboard rerenders.
- Around line 46-54: The JSX in OverviewDashboard currently calls new Date()
during render causing the displayed timestamp to change on every render; capture
the timestamp at mount or set up a timer instead: in the OverviewDashboard
component initialize state or a ref (e.g., timestamp via useState(() => new
Date()) or useRef(new Date())) to freeze the value for initial load, or if
real-time updates are intended add a useEffect that sets an interval to update
the timestamp state (and clears it on unmount) and replace the direct new Date()
call in the JSX with that state/ref.
- Around line 23-36: OverviewDashboard contains AI panel state and toast logic
(isAiPanelOpen, setIsAiPanelOpen, toast, setToast, showToast) which bloats the
component; extract these into a custom hook named useAiReportPanel that
encapsulates panel open/close, toast state and showToast behavior and any
dropdown/action handlers, then replace the inline state in OverviewDashboard
with calls to useAiReportPanel to return {isAiPanelOpen, openAiPanel,
closeAiPanel, toast, showToast, ...actions} so the component simply consumes the
hook and renders UI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5273931-c1b8-470f-a7e3-9e3dcb236e24

📥 Commits

Reviewing files that changed from the base of the PR and between 81335b5 and 5c5b1cf.

⛔ Files ignored due to path filters (19)
  • package-lock.json is excluded by !**/package-lock.json and included by none
  • package.json is excluded by none and included by none
  • src/assets/icon/ads/google-circle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/google-wordmark.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/kakao-circle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/kakao-wordmark.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/naver-circle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/naver-logo.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ads/naver-wordmark.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/close.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/download.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/link.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/more.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/sparkle-circle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/sparkle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai-report/warning.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/chevron-double-right.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/ai-요약버튼.svg is excluded by !**/*.svg and included by src/**
  • vite.config.ts is excluded by none and included by none
📒 Files selected for processing (20)
  • src/components/ads/CampaignRow.tsx
  • src/components/common/card/Card.tsx
  • src/components/common/card/StatCard.tsx
  • src/components/common/chart/ChartLegend.tsx
  • src/components/common/drawer/Drawer.tsx
  • src/components/common/dropdownmenu/DropdownMenu.tsx
  • src/components/common/toast/Toast.tsx
  • src/components/dashboard/charts/BudgetGaugeChart.tsx
  • src/components/dashboard/charts/TrafficChart.tsx
  • src/components/dashboard/charts/budgetGaugeChart.mock.ts
  • src/components/dashboard/charts/trafficChart.mock.ts
  • src/components/dashboard/platform/PlatformComparison.tsx
  • src/components/dashboard/platform/PlatformComparisonChart.tsx
  • src/components/dashboard/platform/PlatformRoasTable.tsx
  • src/components/dashboard/platform/platformComparison.mock.ts
  • src/index.css
  • src/pages/dashboard/overview/OverviewAiReportPanel.tsx
  • src/pages/dashboard/overview/OverviewDashboard.tsx
  • src/pages/dashboard/overview/aiReport.mock.ts
  • src/vite-env.d.ts

Comment thread src/components/common/toast/Toast.tsx
Comment thread src/components/common/toast/Toast.tsx
Comment thread src/components/dashboard/charts/BudgetGaugeChart.tsx
Comment on lines +39 to +66
<div
key={platform.name}
onClick={() => navigate("/platform")}
className="group grid grid-cols-[2.5rem_9rem_5rem_1fr] gap-4 items-center px-3 py-4 rounded-component-lg transition-all duration-300 hover:bg-bg-surface hover:shadow-Medium border border-transparent hover:border-white active:scale-[0.98] cursor-pointer"
>
<span className="text-center font-extrabold text-text-placeholder font-body1 group-hover:text-status-blue transition-colors">
{index + 1}
</span>
<div className="flex items-center gap-3">
<div className="shrink-0 transform group-hover:scale-110 transition-transform duration-300">
{platformLogoMap[platform.name]}
</div>
<span className="font-bold text-text-main font-body1 tracking-tight">
{platform.name}
</span>
</div>
<span className="text-right font-black text-text-main font-body1 tracking-tighter">
{platform.roas}%
</span>
<div className="flex flex-col items-end text-right">
<span className="font-body2 font-extrabold text-text-main leading-tight">
₩{platform.revenue.toLocaleString()}
</span>
<span className="font-caption font-medium text-text-sub opacity-70 tracking-tight">
₩{platform.adCost.toLocaleString()}
</span>
</div>
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

접근성 개선 필요: 키보드 네비게이션 지원

클릭 가능한 행이 <div>로 구현되어 있어 키보드 사용자가 접근할 수 없습니다. tabIndex, role, onKeyDown 핸들러를 추가하거나 <button>으로 변경해 주세요.

♿ 접근성 개선 제안
 <div
   key={platform.name}
   onClick={() => navigate("/platform")}
+  onKeyDown={(e) => {
+    if (e.key === "Enter" || e.key === " ") {
+      e.preventDefault();
+      navigate("/platform");
+    }
+  }}
+  role="button"
+  tabIndex={0}
   className="group grid grid-cols-[2.5rem_9rem_5rem_1fr] gap-4 items-center px-3 py-4 rounded-component-lg transition-all duration-300 hover:bg-bg-surface hover:shadow-Medium border border-transparent hover:border-white active:scale-[0.98] cursor-pointer"
 >

코딩 가이드라인에 따라 시맨틱 HTML과 ARIA 속성 사용을 확인합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
key={platform.name}
onClick={() => navigate("/platform")}
className="group grid grid-cols-[2.5rem_9rem_5rem_1fr] gap-4 items-center px-3 py-4 rounded-component-lg transition-all duration-300 hover:bg-bg-surface hover:shadow-Medium border border-transparent hover:border-white active:scale-[0.98] cursor-pointer"
>
<span className="text-center font-extrabold text-text-placeholder font-body1 group-hover:text-status-blue transition-colors">
{index + 1}
</span>
<div className="flex items-center gap-3">
<div className="shrink-0 transform group-hover:scale-110 transition-transform duration-300">
{platformLogoMap[platform.name]}
</div>
<span className="font-bold text-text-main font-body1 tracking-tight">
{platform.name}
</span>
</div>
<span className="text-right font-black text-text-main font-body1 tracking-tighter">
{platform.roas}%
</span>
<div className="flex flex-col items-end text-right">
<span className="font-body2 font-extrabold text-text-main leading-tight">
{platform.revenue.toLocaleString()}
</span>
<span className="font-caption font-medium text-text-sub opacity-70 tracking-tight">
{platform.adCost.toLocaleString()}
</span>
</div>
</div>
<div
key={platform.name}
onClick={() => navigate("/platform")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
navigate("/platform");
}
}}
role="button"
tabIndex={0}
className="group grid grid-cols-[2.5rem_9rem_5rem_1fr] gap-4 items-center px-3 py-4 rounded-component-lg transition-all duration-300 hover:bg-bg-surface hover:shadow-Medium border border-transparent hover:border-white active:scale-[0.98] cursor-pointer"
>
<span className="text-center font-extrabold text-text-placeholder font-body1 group-hover:text-status-blue transition-colors">
{index + 1}
</span>
<div className="flex items-center gap-3">
<div className="shrink-0 transform group-hover:scale-110 transition-transform duration-300">
{platformLogoMap[platform.name]}
</div>
<span className="font-bold text-text-main font-body1 tracking-tight">
{platform.name}
</span>
</div>
<span className="text-right font-black text-text-main font-body1 tracking-tighter">
{platform.roas}%
</span>
<div className="flex flex-col items-end text-right">
<span className="font-body2 font-extrabold text-text-main leading-tight">
{platform.revenue.toLocaleString()}
</span>
<span className="font-caption font-medium text-text-sub opacity-70 tracking-tight">
{platform.adCost.toLocaleString()}
</span>
</div>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx` around lines 39 -
66, The clickable row in PlatformRoasTable.tsx uses a plain <div> with onClick
(the block keyed by platform.name that calls navigate("/platform")), which is
not keyboard-accessible; change it to a semantic interactive element (preferably
a <button>) or add role="button", tabIndex={0}, and an onKeyDown handler that
triggers the same navigation on Enter/Space, and ensure the element has an
accessible name (e.g., include platform.name in an aria-label) while preserving
existing classes and platformLogoMap usage so style and visuals remain
unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] ApexCharts 연동 및 실시간 트래픽/예산 소진 차트 UI 구현

1 participant