Skip to content

[Feature/#88] 통합 대시보드 디자인 개선 및 Custom Tooltip 적용 - #96

Merged
Seojegyeong merged 25 commits into
developfrom
feature/#88
Mar 16, 2026
Merged

[Feature/#88] 통합 대시보드 디자인 개선 및 Custom Tooltip 적용#96
Seojegyeong merged 25 commits into
developfrom
feature/#88

Conversation

@Seojegyeong

@Seojegyeong Seojegyeong commented Mar 13, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

#88

✨ 변경사항

  • 🐞 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.)

✏️ 작업 내용

  1. UI/기능
  • 트래픽 차트 이상 클릭 탐지 마커 + 커스텀 말풍선 툴팁 구현
  • AI 요약하기 버튼 shimmer 애니메이션
  • 예산 소진 카드 progress bar + 인사이트 메시지
  • 플랫폼 비교 테이블 ROAS 바 시각화 + 증감 배지
  1. 반응형
  • 트래픽 차트 + 예산 카드: 2xl(1536px) 이상에서만 나란히 배치, 이하 세로 스택
  • KPI 카드: xl(1280px) 이상 4열, 이하 2열
  • 예산 카드 하단 정보: sm 이하 세로 스택
  • 플랫폼 테이블: 가로 스크롤 없이 비율 기반 그리드
  1. 코드 품질
  • useIsMounted 공통 훅 추출
  • useAnomalyMarkerPos 훅 분리
  • getBudgetStatus 유틸 추출 (OverviewDashboard -> BudgetGaugeChart 중복 제거)

스크린샷

default.mov
2026-03-13.3.59.05.mov

CaptureX_2026-03-13_153752_localhost
CaptureX_2026-03-13_153826_localhost

폴더구조

src/
├── pages/dashboard/overview/
│   ├── OverviewDashboard.tsx       # 대시보드 페이지 (레이아웃 조합)
│   ├── OverviewAiReportPanel.tsx   # AI 요약 드로어 패널
│   ├── overview.mock.ts            # KPI 목업 데이터
│   └── aiReport.mock.ts            # AI 리포트 목업 데이터
│
├── components/dashboard/
│   ├── charts/
│   │   ├── TrafficChart.tsx            # 실시간 트래픽 차트 컴포넌트
│   │   ├── trafficChart.config.ts      # ApexCharts 옵션 설정
│   │   ├── trafficChart.mock.ts        # 시간대별 클릭수 목업 데이터
│   │   ├── useAnomalyMarkerPos.ts      # 이상 징후 마커 좌표 추적 훅
│   │   ├── BudgetGaugeChart.tsx        # 예산 소진 게이지 컴포넌트
│   │   └── budgetGaugeChart.mock.ts    # 예산 목업 데이터 및 인터페이스
│   │
│   └── platform/
│       ├── PlatformRoasTable.tsx       # 플랫폼별 ROAS 비교 테이블
│       └── platformComparison.mock.ts  # 플랫폼 성과 목업 데이터
│
└── hooks/common/
    └── useIsMounted.ts             # 클라이언트 마운트 감지 공통 훅

😅 미완성 작업

API 연동 예정

📢 논의 사항 및 참고 사항

디자인 변경 내용 백엔드한테 전달해야해서 디자인 피드백도 함께 부탁드립니다!

💬 리뷰어 가이드 (P-Rules)

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

Summary by CodeRabbit

  • 새로운 기능

    • 통계 카드에 트렌드 배지 추가 및 트래픽 차트에 이상점 호버 툴팁 및 다운로드 항목 추가
    • 클라이언트 마운트 표시 훅과 이상점 위치 훅 추가
  • 개선

    • 예산 게이지에 상태별 색상 진행률, 임계값 마커 및 인사이트 메시지 추가
    • 플랫폼 ROAS 표에 진행률 바, CTR/CVR 변화 지표 및 수익/비용 표시 강화
    • 카드 헤더 간격·레이아웃·버튼 스타일 조정
  • 리팩터

    • 플랫폼 비교 관련 컴포넌트 정리 및 차트 구성 모듈화(구성 이동/삭제)
    • 플랫폼 데이터: "Kakao" 명칭 및 델타 값 추가 업데이트

@Seojegyeong Seojegyeong self-assigned this Mar 13, 2026
@Seojegyeong Seojegyeong added 🎨 Html&css 마크업 & 스타일링 ✨ Feature 기능 개발 labels Mar 13, 2026
@Seojegyeong Seojegyeong linked an issue Mar 13, 2026 that may be closed by this pull request
5 tasks
@Seojegyeong Seojegyeong added the 🔨 Refactor 코드 리팩토링 label Mar 13, 2026
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

카드 컴포넌트 스타일 조정, StatCard에 TrendBadge 분리, BudgetGaugeChart에 상태 타입화·인사이트 메시지 추가, TrafficChart에 이상 마커 호버/툴팁과 config·위치 훅 도입, 플랫폼 ROAS 테이블 재구성 및 관련 mock/훅 추가/삭제가 적용되었습니다.

Changes

Cohort / File(s) Summary
공통 카드 컴포넌트
src/components/common/card/Card.tsx, src/components/common/card/StatCard.tsx
Card 루트에 relative 클래스 추가 및 헤더 간격 조정. StatCard에서 추세 렌더링을 TrendBadge라는 별도(공개) 컴포넌트로 추출.
예산 게이지 차트
src/components/dashboard/charts/BudgetGaugeChart.tsx
TBudgetStatus 타입과 getBudgetStatus 도입, 상태-배지/포인트 매핑 타입화, useIsMounted 적용으로 마운트 로직 정리, 임계값 기반 인사이트 메시지 및 하단 Alert 블록 추가. (새로운 export: TBudgetStatus, getBudgetStatus, statusBadgeVariant, statusPointClasses)
트래픽 차트 및 설정
src/components/dashboard/charts/TrafficChart.tsx, src/components/dashboard/charts/trafficChart.config.ts, src/components/dashboard/charts/useAnomalyMarkerPos.ts
차트 옵션·다운로드 로직을 별도 config로 분리(BASE_OPTIONS, CHART_ID, DOWNLOAD_ITEMS 등). 이상 마커 호버 UX 추가: 컨테이너 ref, useAnomalyMarkerPos 훅으로 SVG 마커 좌표 계산, AnomalyBubble 툴팁 표시/숨김 제어, 차트 높이 및 마운트 안전성 보강.
플랫폼 비교 영역
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
PlatformComparisonPlatformComparisonChart 제거. PlatformRoasTable로 UI/레이아웃 재구성(로고, 순위, ROAS 바, CTR/CVR 델타, 수익/비용 표시 등). mock 타입·데이터 변경: ctrDelta, conversionDelta 추가, impressionRate 제거, kakaoKakao로 이름 변경.
유틸리티 훅
src/hooks/common/useIsMounted.ts
SSR 안전한 client-mount 감지 훅(useIsMounted) 추가 (requestAnimationFrame 사용).
대시보드 페이지
src/pages/dashboard/overview/OverviewDashboard.tsx
getBudgetStatus/statusBadgeVariant를 사용해 예산 상태 배지 추가. PlatformComparisonPlatformRoasTable로 교체, 날짜/레이아웃/AI 버튼 마크업·스타일 조정.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Container as TrafficChart Container
    participant Hook as useAnomalyMarkerPos
    participant Chart as ApexCharts SVG
    participant Bubble as AnomalyBubble

    User->>Container: mouse move / focus
    Container->>Hook: 요청 (containerRef)
    Hook->>Chart: SVG에서 annotation marker 쿼리
    Chart-->>Hook: marker 화면 좌표 반환
    Hook-->>Container: container-local markerPos
    Container->>Container: 거리 계산 → HOVER_RADIUS 비교
    alt within HOVER_RADIUS
        Container->>Bubble: render / show (rgba(255,165,0,0.5))
        Bubble-->>User: 툴팁 표시
    else outside HOVER_RADIUS
        Container->>Bubble: hide
    end
    User->>Container: mouse leave
    Container->>Bubble: hide
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

Possibly related PRs

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 나타내고 있으며, 이슈 #88과 대시보드 디자인 개선이라는 핵심 내용을 잘 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션을 충실하게 작성했으나, 작업 내용이 매우 상세하고 폴더 구조 등 추가 정보는 포함되어 있으나 미완성 작업은 'API 연동 예정'으로만 간략히 표시되어 있습니다.

✏️ 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/#88
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@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: 1

🧹 Nitpick comments (8)
src/components/common/card/StatCard.tsx (1)

25-27: 접근성 레이블 방식은 aria-label보다 구조적 텍스트 제공이 더 안전합니다.

Line 26처럼 일반 spanaria-label만 두기보다, sr-only로 “상승/하락” 텍스트를 같이 렌더링하는 쪽이 스크린리더 호환성이 더 좋습니다.

접근성 개선 예시
 export function TrendBadge({ direction, value }: ITrend) {
+  const trendText = direction === "up" ? "상승" : "하락";
   return (
     <span
-      aria-label={`${value} ${direction === "up" ? "상승" : "하락"}`}
       className={twMerge(
         "inline-flex items-center gap-1 px-2 py-1 rounded-full font-caption w-fit",
         trendClasses[direction],
       )}
     >
       {direction === "up" ? (
         <TrendUpIcon aria-hidden className="w-4 h-4" />
       ) : (
         <TrendDownIcon aria-hidden className="w-4 h-4" />
       )}
+      <span className="sr-only">{trendText}</span>
       {value}
     </span>
   );
 }

As per coding guidelines, "7. 접근성: 시맨틱 HTML, ARIA 속성 사용 확인."

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

In `@src/components/common/card/StatCard.tsx` around lines 25 - 27, The span in
the StatCard component currently relies only on an aria-label for the direction;
instead render the direction text as visually-hidden content so screen readers
get real DOM text (e.g., add a sibling or inside the same element a span with
the existing "sr-only" / visually-hidden class containing direction === "up" ?
"상승" : "하락") while keeping the visible value unchanged; update the span that
currently has aria-label to include the sr-only element (or remove aria-label if
redundant) so screen readers read the structured text rather than only an
aria-label.
src/components/dashboard/charts/TrafficChart.tsx (2)

42-67: AnomalyBubble 내용이 하드코딩되어 있습니다.

현재 "구글-캠페인 A-광고 1", "부정 클릭 의심" 등의 텍스트가 하드코딩되어 있는데, API 연동 시 동적 데이터로 교체가 필요해 보입니다.

♻️ Props로 데이터 전달하는 구조 제안
-function AnomalyBubble({ x, y }: { x: number; y: number }) {
+interface AnomalyBubbleProps {
+  x: number;
+  y: number;
+  title?: string;
+  description?: string;
+}
+
+function AnomalyBubble({ 
+  x, 
+  y, 
+  title = "클릭 이상 징후 감지",
+  description = "구글-캠페인 A-광고 1\n부정 클릭 의심"
+}: AnomalyBubbleProps) {
🤖 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 42 - 67,
AnomalyBubble currently renders hardcoded strings; change its signature to
accept props (e.g., anomaly: { title: string; detail: string } or separate props
like title and detail) and use those props instead of the fixed texts ("구글-캠페인
A-광고 1", "부정 클릭 의심"); update any callers (the component that renders
AnomalyBubble in TrafficChart) to pass the API-provided anomaly data so the
bubble displays dynamic content and remains positioned using the existing x/y
props.

102-104: 조건부 클래스 처리에 twMerge 또는 clsx 사용을 권장합니다.

현재 문자열 연결로 조건부 클래스를 처리하고 있는데, 다른 파일에서는 twMerge를 사용하고 있어 일관성을 위해 동일한 방식을 사용하면 좋겠습니다.

♻️ twMerge 사용 제안
+import { twMerge } from "tailwind-merge";

 <div
   id={CHART_CONTAINER_ID}
   ref={containerRef}
   role="img"
   aria-label="실시간 트래픽 변화 차트: 시간대별 클릭수 추이"
-  className={`relative [&_.apexcharts-toolbar]:hidden${isAnomalyHovered ? " [&_.apexcharts-tooltip]:hidden!" : ""}`}
+  className={twMerge(
+    "relative [&_.apexcharts-toolbar]:hidden",
+    isAnomalyHovered && "[&_.apexcharts-tooltip]:hidden!"
+  )}
   onMouseMove={handleMouseMove}
   onMouseLeave={handleMouseLeave}
 >
🤖 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 102 - 104,
Replace the inline string concatenation used in the className prop of the
TrafficChart component with a call to the project's class merging utility
(twMerge) to match other files: import/use twMerge and build the class name by
merging the static string "relative [&_.apexcharts-toolbar]:hidden" with the
conditional part that depends on isAnomalyHovered so the conditional classes are
applied cleanly; update the JSX element that currently uses
className={`...${isAnomalyHovered ? " ...": ""}`} (the same element with
onMouseMove={handleMouseMove} and onMouseLeave={handleMouseLeave}) to call
twMerge instead.
src/components/dashboard/charts/trafficChart.config.ts (2)

17-18: 모듈 로드 시점에 날짜가 고정되는 점 참고해 주세요.

TODAY 상수가 모듈 로드 시 한 번만 평가되므로, 장기 실행 시 날짜가 업데이트되지 않습니다. 파일명 용도이므로 큰 문제는 아니지만, 필요시 함수로 래핑하여 호출 시점에 날짜를 생성할 수 있습니다.

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

In `@src/components/dashboard/charts/trafficChart.config.ts` around lines 17 - 18,
TODAY is computed once at module load (const TODAY = new
Date().toISOString().slice(0, 10)) so it will not update during long-running
processes; change to a function (e.g., getToday()) or replace usages of TODAY
with a call that computes new Date().toISOString().slice(0,10) at call time so
the generated filename uses the current date; update all references to TODAY in
this module (trafficChart.config related exports/functions) to call the new
function.

83-97: 하드코딩된 annotation 값이 mock 데이터와 동기화되지 않을 수 있습니다.

현재 x: 11, y: 53000 값이 직접 하드코딩되어 있는데, trafficChartMock.clicks[ANOMALY_INDEX] 값이 변경되면 annotation이 잘못된 위치에 표시될 수 있습니다.

♻️ mock 데이터와 연동하는 방식 제안
+import { ANOMALY_INDEX } from "./trafficChart.config";
+
 // 이상 징후 위치에 빨간 점 표시
   annotations: {
     points: [
       {
-        x: 11,
-        y: 53000,
+        x: ANOMALY_INDEX,
+        y: trafficChartMock.clicks[ANOMALY_INDEX],
         marker: {
           size: 5,
           fillColor: "#ff4560",
           strokeColor: "#ff4560",
           strokeWidth: 1,
         },
       },
     ],
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/trafficChart.config.ts` around lines 83 - 97,
The annotations block currently hardcodes x: 11 and y: 53000 which can desync
from the mock; replace those literals inside the annotations.points entry in
trafficChart.config.ts with values derived from
trafficChartMock.clicks[ANOMALY_INDEX] (use ANOMALY_INDEX for the x position or
pull the corresponding x/timestamp if your mock stores x/y pairs) and the click
value for y, and add a safe fallback if the mock entry is undefined so the chart
doesn't break; update the annotations object (the points array) to compute x and
y from trafficChartMock and ANOMALY_INDEX rather than using hardcoded numbers.
src/components/dashboard/charts/useAnomalyMarkerPos.ts (1)

35-44: 300ms 딜레이가 차트 렌더링 시간에 의존합니다.

현재 하드코딩된 300ms 딜레이는 ApexCharts가 렌더링을 완료하기를 기다리는 것으로 보입니다. 네트워크 지연이나 저사양 기기에서는 이 시간이 충분하지 않을 수 있습니다.

더 안정적인 방법으로는 MutationObserver를 사용하여 .apexcharts-point-annotation-marker 요소가 DOM에 추가될 때 위치를 계산하거나, BASE_OPTIONS.chart.events.mounted 콜백을 활용하는 방안이 있습니다.

♻️ MutationObserver 활용 예시
 useEffect(() => {
-  const timer = setTimeout(updateMarkerPos, 300);
+  // 마커 요소가 DOM에 추가될 때까지 관찰
+  const mutationObserver = new MutationObserver(() => {
+    if (containerRef.current?.querySelector(".apexcharts-point-annotation-marker")) {
+      updateMarkerPos();
+      mutationObserver.disconnect();
+    }
+  });
+  if (containerRef.current) {
+    mutationObserver.observe(containerRef.current, { childList: true, subtree: true });
+  }
+
   const observer = new ResizeObserver(updateMarkerPos);
   if (containerRef.current) observer.observe(containerRef.current);
+
   return () => {
-    clearTimeout(timer);
+    mutationObserver.disconnect();
     observer.disconnect();
   };
 }, [updateMarkerPos, containerRef]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/charts/useAnomalyMarkerPos.ts` around lines 35 - 44,
The useEffect in useAnomalyMarkerPos currently waits a hardcoded 300ms before
calling updateMarkerPos which is brittle; replace the timeout approach by
observing DOM mutations or ApexCharts mount event: use a MutationObserver on
containerRef.current that watches for additions of elements matching
".apexcharts-point-annotation-marker" and call updateMarkerPos when they appear
(and disconnect the observer in the cleanup), or alternatively invoke
updateMarkerPos from BASE_OPTIONS.chart.events.mounted (or chart instance
mounted callback) so the position is calculated exactly when charts finish
rendering; keep the existing ResizeObserver
(observer.observe(containerRef.current)) but remove the setTimeout and ensure
both observers are properly disconnected in the return cleanup.
src/components/dashboard/platform/PlatformRoasTable.tsx (1)

40-54: 테이블 구조의 접근성 개선을 고려해 주세요.

현재 div 기반의 그리드 레이아웃으로 테이블을 구현하고 있는데, 스크린 리더 사용자를 위해 테이블 관련 ARIA 속성을 추가하면 좋겠습니다.

♻️ ARIA 테이블 역할 추가 제안
-    <div className="flex flex-col h-full font-pretendard w-full">
-      <div className="flex flex-col flex-1 min-w-0">
+    <div className="flex flex-col h-full font-pretendard w-full" role="table" aria-label="플랫폼별 ROAS 비교">
+      <div className="flex flex-col flex-1 min-w-0" role="rowgroup">
         {/* 헤더 */}
         <div
-          className={`grid ${COL} gap-x-4 px-4 pt-2 pb-4 font-caption text-[`#8B95A1`] font-medium tracking-wider uppercase border-b border-[`#F2F4F6`]`}
+          role="row"
+          className={`grid ${COL} gap-x-4 px-4 pt-2 pb-4 font-caption text-[`#8B95A1`] font-medium tracking-wider uppercase border-b border-[`#F2F4F6`]`}
         >
-          <span className="text-center">#</span>
+          <span role="columnheader" className="text-center">#</span>

As per coding guidelines: "접근성: 시맨틱 HTML, ARIA 속성 사용 확인"

🤖 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 40 -
54, PlatformRoasTable currently renders a visual grid with divs (using the COL
constant) but lacks semantic table/ARIA roles; update the component to expose
proper table semantics by adding role="table" and an accessible name (aria-label
or aria-labelledby) to the outer container, wrap header group with
role="rowgroup" and the header row div with role="row", and mark each header
cell spans as role="columnheader" (or add scope="col" if you convert to actual
<th> later); similarly ensure data rows use role="row" and cells use role="cell"
so screen readers can interpret the structure (make these changes in
PlatformRoasTable around the header div and the corresponding row/cell elements
that use COL).
src/pages/dashboard/overview/OverviewDashboard.tsx (1)

45-55: useMemo 의존성 배열에 대한 lint 경고 가능성이 있습니다.

budgetGaugeChartMock이 상수이므로 현재 로직은 정확하지만, ESLint react-hooks/exhaustive-deps 규칙에서 경고가 발생할 수 있습니다. 의도적인 최적화임을 명시하거나, 상수 참조를 의존성에 추가하는 것을 고려해 주세요.

♻️ 의도 명시 또는 의존성 추가
 const budgetStatusBadge = useMemo(() => {
   const { totalBudget, spent, warningThreshold, dangerThreshold } =
     budgetGaugeChartMock;
   const pct = totalBudget > 0 ? Math.round((spent / totalBudget) * 100) : 0;
   const status = getBudgetStatus(pct, warningThreshold, dangerThreshold);
   return (
     <Badge variant={statusBadgeVariant[status]} size="sm" className="px-2">
       {status}
     </Badge>
   );
-}, []);
+}, []); // budgetGaugeChartMock is a module-level constant

또는 API 연동 시 실제 데이터를 사용하게 되면 적절한 의존성을 추가:

}, [budgetData]); // 실제 API 데이터로 교체 시
🤖 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 45 - 55, The
useMemo for budgetStatusBadge currently has an empty dependency array which may
trigger react-hooks/exhaustive-deps lint warnings; update the dependency array
to include the values used (e.g., budgetGaugeChartMock or its fields) or
explicitly document intent by adding an eslint comment. Concretely, modify the
useMemo call (budgetStatusBadge) to use dependencies like [budgetGaugeChartMock]
or [budgetGaugeChartMock.totalBudget, budgetGaugeChartMock.spent,
budgetGaugeChartMock.warningThreshold, budgetGaugeChartMock.dangerThreshold], or
if this is intentionally constant, add a single-line // eslint-disable-next-line
react-hooks/exhaustive-deps above the useMemo to suppress the warning and keep
getBudgetStatus and statusBadgeVariant usage unchanged.
🤖 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/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 16-17: The calculation for maxRoas in PlatformRoasTable.tsx can
produce 0 or -Infinity (if platformRoasRanking is empty) leading to division by
zero/NaN; update the logic that computes maxRoas (the
Math.max(...platformRoasRanking.map((p) => p.roas)) expression) to safely handle
empty arrays and zero values by providing a sensible fallback (e.g., treat empty
or all-zero results as 1) before using it for ratio/division operations; ensure
any downstream usage of maxRoas (in bar width/percentage calculations) uses this
guarded value.

---

Nitpick comments:
In `@src/components/common/card/StatCard.tsx`:
- Around line 25-27: The span in the StatCard component currently relies only on
an aria-label for the direction; instead render the direction text as
visually-hidden content so screen readers get real DOM text (e.g., add a sibling
or inside the same element a span with the existing "sr-only" / visually-hidden
class containing direction === "up" ? "상승" : "하락") while keeping the visible
value unchanged; update the span that currently has aria-label to include the
sr-only element (or remove aria-label if redundant) so screen readers read the
structured text rather than only an aria-label.

In `@src/components/dashboard/charts/trafficChart.config.ts`:
- Around line 17-18: TODAY is computed once at module load (const TODAY = new
Date().toISOString().slice(0, 10)) so it will not update during long-running
processes; change to a function (e.g., getToday()) or replace usages of TODAY
with a call that computes new Date().toISOString().slice(0,10) at call time so
the generated filename uses the current date; update all references to TODAY in
this module (trafficChart.config related exports/functions) to call the new
function.
- Around line 83-97: The annotations block currently hardcodes x: 11 and y:
53000 which can desync from the mock; replace those literals inside the
annotations.points entry in trafficChart.config.ts with values derived from
trafficChartMock.clicks[ANOMALY_INDEX] (use ANOMALY_INDEX for the x position or
pull the corresponding x/timestamp if your mock stores x/y pairs) and the click
value for y, and add a safe fallback if the mock entry is undefined so the chart
doesn't break; update the annotations object (the points array) to compute x and
y from trafficChartMock and ANOMALY_INDEX rather than using hardcoded numbers.

In `@src/components/dashboard/charts/TrafficChart.tsx`:
- Around line 42-67: AnomalyBubble currently renders hardcoded strings; change
its signature to accept props (e.g., anomaly: { title: string; detail: string }
or separate props like title and detail) and use those props instead of the
fixed texts ("구글-캠페인 A-광고 1", "부정 클릭 의심"); update any callers (the component
that renders AnomalyBubble in TrafficChart) to pass the API-provided anomaly
data so the bubble displays dynamic content and remains positioned using the
existing x/y props.
- Around line 102-104: Replace the inline string concatenation used in the
className prop of the TrafficChart component with a call to the project's class
merging utility (twMerge) to match other files: import/use twMerge and build the
class name by merging the static string "relative
[&_.apexcharts-toolbar]:hidden" with the conditional part that depends on
isAnomalyHovered so the conditional classes are applied cleanly; update the JSX
element that currently uses className={`...${isAnomalyHovered ? " ...": ""}`}
(the same element with onMouseMove={handleMouseMove} and
onMouseLeave={handleMouseLeave}) to call twMerge instead.

In `@src/components/dashboard/charts/useAnomalyMarkerPos.ts`:
- Around line 35-44: The useEffect in useAnomalyMarkerPos currently waits a
hardcoded 300ms before calling updateMarkerPos which is brittle; replace the
timeout approach by observing DOM mutations or ApexCharts mount event: use a
MutationObserver on containerRef.current that watches for additions of elements
matching ".apexcharts-point-annotation-marker" and call updateMarkerPos when
they appear (and disconnect the observer in the cleanup), or alternatively
invoke updateMarkerPos from BASE_OPTIONS.chart.events.mounted (or chart instance
mounted callback) so the position is calculated exactly when charts finish
rendering; keep the existing ResizeObserver
(observer.observe(containerRef.current)) but remove the setTimeout and ensure
both observers are properly disconnected in the return cleanup.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 40-54: PlatformRoasTable currently renders a visual grid with divs
(using the COL constant) but lacks semantic table/ARIA roles; update the
component to expose proper table semantics by adding role="table" and an
accessible name (aria-label or aria-labelledby) to the outer container, wrap
header group with role="rowgroup" and the header row div with role="row", and
mark each header cell spans as role="columnheader" (or add scope="col" if you
convert to actual <th> later); similarly ensure data rows use role="row" and
cells use role="cell" so screen readers can interpret the structure (make these
changes in PlatformRoasTable around the header div and the corresponding
row/cell elements that use COL).

In `@src/pages/dashboard/overview/OverviewDashboard.tsx`:
- Around line 45-55: The useMemo for budgetStatusBadge currently has an empty
dependency array which may trigger react-hooks/exhaustive-deps lint warnings;
update the dependency array to include the values used (e.g.,
budgetGaugeChartMock or its fields) or explicitly document intent by adding an
eslint comment. Concretely, modify the useMemo call (budgetStatusBadge) to use
dependencies like [budgetGaugeChartMock] or [budgetGaugeChartMock.totalBudget,
budgetGaugeChartMock.spent, budgetGaugeChartMock.warningThreshold,
budgetGaugeChartMock.dangerThreshold], or if this is intentionally constant, add
a single-line // eslint-disable-next-line react-hooks/exhaustive-deps above the
useMemo to suppress the warning and keep getBudgetStatus and statusBadgeVariant
usage unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1cdad15a-c1a1-4e89-a69f-b787ea9afc09

📥 Commits

Reviewing files that changed from the base of the PR and between 885e566 and e957079.

⛔ Files ignored due to path filters (3)
  • .claude/settings.json is excluded by none and included by none
  • .gitignore is excluded by none and included by none
  • src/assets/icon/common/alert-circle.svg is excluded by !**/*.svg and included by src/**
📒 Files selected for processing (12)
  • src/components/common/card/Card.tsx
  • src/components/common/card/StatCard.tsx
  • src/components/dashboard/charts/BudgetGaugeChart.tsx
  • src/components/dashboard/charts/TrafficChart.tsx
  • src/components/dashboard/charts/trafficChart.config.ts
  • src/components/dashboard/charts/useAnomalyMarkerPos.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/hooks/common/useIsMounted.ts
  • src/pages/dashboard/overview/OverviewDashboard.tsx
💤 Files with no reviewable changes (2)
  • src/components/dashboard/platform/PlatformComparison.tsx
  • src/components/dashboard/platform/PlatformComparisonChart.tsx

Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@Seojegyeong
Seojegyeong requested review from YermIm and jjjsun March 13, 2026 06:58
@YermIm

YermIm commented Mar 13, 2026

Copy link
Copy Markdown
Collaborator

P3: 정보들이 변경된 디자인에서 훨씬 한눈에 잘 보이는 것 같습니다!
플랫폼별 비교에 반응형 처리가 필요할 것 같습니다. 수고하셨습니다!!
스크린샷 2026-03-13 오후 8 09 33

Comment thread src/components/dashboard/charts/TrafficChart.tsx Outdated
Comment thread src/components/dashboard/charts/TrafficChart.tsx
Comment thread src/components/dashboard/charts/BudgetGaugeChart.tsx
Comment thread src/components/dashboard/charts/BudgetGaugeChart.tsx Outdated
Comment thread src/components/dashboard/charts/BudgetGaugeChart.tsx
Comment thread src/pages/dashboard/overview/OverviewDashboard.tsx
Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx Outdated
Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx Outdated
Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx Outdated
@Seojegyeong

Copy link
Copy Markdown
Collaborator Author

P3: 정보들이 변경된 디자인에서 훨씬 한눈에 잘 보이는 것 같습니다!

플랫폼별 비교에 반응형 처리가 필요할 것 같습니다. 수고하셨습니다!!

스크린샷 2026-03-13 오후 8 09 33

넵! 추후 태블릿 기준 반응형 적용 시에 적용하겠습니다!
혹시 DevTool에서 기기모드 바꿔도 동일하게 레이아웃 깨지는지 확인해주실 수 있을까요??

@YermIm

YermIm commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

P3: 정보들이 변경된 디자인에서 훨씬 한눈에 잘 보이는 것 같습니다!
플랫폼별 비교에 반응형 처리가 필요할 것 같습니다. 수고하셨습니다!!
스크린샷 2026-03-13 오후 8 09 33

넵! 추후 태블릿 기준 반응형 적용 시에 적용하겠습니다! 혹시 DevTool에서 기기모드 바꿔도 동일하게 레이아웃 깨지는지 확인해주실 수 있을까요??

제 로컬에서는 깨지는 것 같습니다!

@jjjsun

jjjsun commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator

P3: 정보들이 변경된 디자인에서 훨씬 한눈에 잘 보이는 것 같습니다!
플랫폼별 비교에 반응형 처리가 필요할 것 같습니다. 수고하셨습니다!!
스크린샷 2026-03-13 오후 8 09 33

넵! 추후 태블릿 기준 반응형 적용 시에 적용하겠습니다! 혹시 DevTool에서 기기모드 바꿔도 동일하게 레이아웃 깨지는지 확인해주실 수 있을까요??

image image

제 로컬에서도 깨지는것같습니다!

@Seojegyeong

Copy link
Copy Markdown
Collaborator Author

P3: 정보들이 변경된 디자인에서 훨씬 한눈에 잘 보이는 것 같습니다!
플랫폼별 비교에 반응형 처리가 필요할 것 같습니다. 수고하셨습니다!!
스크린샷 2026-03-13 오후 8 09 33

넵! 추후 태블릿 기준 반응형 적용 시에 적용하겠습니다! 혹시 DevTool에서 기기모드 바꿔도 동일하게 레이아웃 깨지는지 확인해주실 수 있을까요??

image image
제 로컬에서도 깨지는것같습니다!

모바일 반응형은 우선순위가 낮아 추후 일정에 따라 진행 예정입니다. 현재는 데스크탑·태블릿 기준 반응형만 검토 부탁드립니다.

@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@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 (6)
src/components/dashboard/platform/PlatformRoasTable.tsx (3)

16-17: API 연동 시 maxRoas 계산을 컴포넌트 내부로 이동해야 합니다.

현재 maxRoas가 모듈 레벨에서 계산되어 있어서, mock 데이터 기준으로는 문제없지만 API 연동 후에는 데이터가 변경되어도 maxRoas가 갱신되지 않습니다.

♻️ API 연동 대비 구조 제안
-// 바 차트 비율 계산용 최대 ROAS
-const maxRoas = Math.max(...platformRoasRanking.map((p) => p.roas));

 export default function PlatformRoasTable() {
+  // API 연동 후: const { data: platformRoasRanking } = useQuery(...)
+  const maxRoas = useMemo(
+    () => Math.max(...platformRoasRanking.map((p) => p.roas), 1),
+    [platformRoasRanking]
+  );
+
   return (

Also applies to: 41-42

🤖 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 16 -
17, The module-level constant maxRoas is computed from platformRoasRanking at
import time, so it won't update after API data changes; move the calculation
into the PlatformRoasTable component (e.g., inside the component body or a
useMemo) and reference platformRoasRanking prop/state there so maxRoas is
recomputed whenever platformRoasRanking changes; update the other occurrences
noted (lines ~41-42) the same way to ensure all ROAS-derived values depend on
current props/state rather than a frozen module-level value.

27-35: value === 0일 때 중립 상태 표시를 고려해보세요.

현재 value >= 0이면 상승(up)으로 표시되어, 0% 변화도 파란색 상승 화살표로 렌더링됩니다. 사용자에게 "변화 없음"을 명확히 전달하려면 중립 상태를 별도로 처리하는 것이 좋을 것 같아요.

♻️ 중립 상태 처리 제안
 function Delta({ value }: { value: number }) {
-  const isPos = value >= 0;
+  if (value === 0) {
+    return <span className="text-text-disabled font-caption">-</span>;
+  }
+  const isPos = value > 0;
   return (
     <TrendBadge
       direction={isPos ? "up" : "down"}
       value={`${Math.abs(value).toFixed(1)}%`}
     />
   );
 }
🤖 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 27 -
35, The Delta component treats value === 0 as "up"; update it to explicitly
handle neutral/flat values by using a three-way condition and passing a neutral
direction to TrendBadge (e.g., direction={value > 0 ? "up" : value < 0 ? "down"
: "neutral"}), keeping the displayed value as `${Math.abs(value).toFixed(1)}%`
(or `"0.0%"`) so 0% renders a clear neutral state; adjust the TrendBadge usage
if it expects a different neutral token (e.g., "flat" or undefined) accordingly.

43-61: 테이블 구조에 접근성(a11y) 속성 추가를 권장드려요.

현재 div + grid 기반으로 테이블을 구현하셨는데, 스크린 리더 사용자에게는 테이블 구조로 인식되지 않습니다. 코딩 가이드라인에서 시맨틱 HTML 및 ARIA 속성 사용을 권장하고 있어요.

ARIA role을 추가하면 현재 스타일을 유지하면서 접근성을 개선할 수 있습니다:

♿ ARIA role 적용 제안
-    <div className="@container flex flex-col h-full font-pretendard w-full">
+    <div className="@container flex flex-col h-full font-pretendard w-full" role="table" aria-label="플랫폼별 ROAS 비교">
       <div className="flex flex-col flex-1 min-w-0">
         {/* 헤더 */}
         <div
-          className={`grid ${COL} ...`}
+          className={`grid ${COL} ...`}
+          role="row"
         >
-          <span className="text-center">순위</span>
+          <span className="text-center" role="columnheader">순위</span>
           ...
         </div>
         
-        <div className="flex flex-col pb-2 divide-y divide-[`#F2F4F6`]">
+        <div className="flex flex-col pb-2 divide-y divide-[`#F2F4F6`]" role="rowgroup">
           {platformRoasRanking.map((platform, index) => (
             <div
               key={platform.name}
-              className={`group grid ${COL} ...`}
+              className={`group grid ${COL} ...`}
+              role="row"
             >
+              {/* 각 셀에 role="cell" 추가 */}

As per coding guidelines, 시맨틱 HTML, ARIA 속성 사용 확인이 필요합니다.

🤖 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 43 -
61, The grid-based table lacks semantic/ARIA roles, so update PlatformRoasTable:
add role="table" and an accessible aria-label or aria-labelledby to the outer
container div (the one with class "@container flex..."), mark the header div
(the one with class including `${COL} gap-x-4 ...`) as role="rowgroup" and
change each header span to role="columnheader" (add aria-sort where relevant),
mark the body wrapper (the div with "flex flex-col pb-2 divide-y...") as
role="rowgroup", and for each mapped row inside platformRoasRanking (the div
created in map with `key={platform.name}` and `className="group grid..."`) add
role="row" and change each data span inside that row to role="cell"; ensure
keyboard focus if rows are interactive by adding tabIndex and appropriate
aria-selected/aria-pressed attributes as needed.
src/components/dashboard/charts/trafficChart.config.ts (1)

121-124: BASE_OPTIONS를 데이터 의존 팩토리로 분리하는 게 안전합니다.

Line [123]이 mock 데이터에 고정되어 있어 API 데이터 연동 시 y축 스케일이 실제 시리즈와 어긋날 가능성이 큽니다. createTrafficChartOptions(clicks) 형태로 분리해 주세요.
As per coding guidelines, src/**: "2. 구조와 책임 분리: 페이지에 비즈니스 로직이 과도하지 않은지 확인. 커스텀 훅으로의 분리 여부 검토."

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

In `@src/components/dashboard/charts/trafficChart.config.ts` around lines 121 -
124, The BASE_OPTIONS currently hardcodes yaxis max using
trafficChartMock.clicks which will misalign when real API data is used; refactor
BASE_OPTIONS into a factory function createTrafficChartOptions(clicks) that
accepts the clicks series and computes yaxis.max as
Math.ceil(Math.max(...clicks) / 10000) * 10000 (preserve min: 0 and tickAmount:
6), update any usages to call createTrafficChartOptions(series) instead of
referencing BASE_OPTIONS, and keep other base properties intact so the chart
scales correctly for dynamic data.
src/components/dashboard/charts/BudgetGaugeChart.tsx (2)

66-85: 기간 진행률 기준 시각은 상위에서 내려받는 쪽이 더 안정적입니다.

이 카드 안에서 new Date()로 기간 진행률을 계산하면, 대시보드 헤더의 기준 시각이나 추후 API 기준일과 쉽게 분리됩니다. 히스토리 조회, 타임존 차이, 자정 이후 재렌더 같은 경우에 같은 화면 안에서 서로 다른 날짜 기준이 섞일 수 있어서 referenceDate나 이미 계산된 기간 정보(elapsedDays, totalDays)를 prop으로 받는 편이 좋아 보입니다.

Also applies to: 155-170

🤖 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 66 - 85,
The logic that computes periodElapsedRate/percentage inside BudgetGaugeChart
should not call new Date() locally; instead accept a stable referenceDate or
precomputed elapsedDays/totalDays via props and use those to derive
periodElapsedRate and percentage so all cards share the same time basis. Update
the BudgetGaugeChart component signature to add either referenceDate or
elapsedDays/totalDays props, remove any local new Date() usage that feeds
periodElapsedRate, and use the incoming values when evaluating isOverBudget,
dangerThreshold, warningThreshold and building insightDesc so the conditions and
messages remain consistent across the dashboard.

18-25: 상태 계산 기준을 한 곳으로 모아두는 편이 안전해 보여요.

지금은 getBudgetStatus()가 절대 소진율만 보고, insightDesc는 기간 진행률까지 같이 보고 있습니다. 이 구조면 월초처럼 percentage는 아직 낮지만 periodElapsedRate + 15를 넘는 케이스에서 진행 바/배지는 안정인데 하단 문구는 경고로 나올 수 있어요. statusinsightDesc를 같은 평가 함수에서 함께 계산하면 이런 엇갈림을 막기 쉽습니다.

정리 예시
-  const status = getBudgetStatus(percentage, warningThreshold, dangerThreshold);
-
-  let insightDesc = "";
-  if (isOverBudget) {
-    ...
-  } else if (...) {
-    ...
-  }
+  const { status, insightDesc } = evaluateBudgetState({
+    percentage,
+    periodElapsedRate,
+    warningThreshold,
+    dangerThreshold,
+    isOverBudget,
+  });

Also applies to: 64-85, 123-127

🤖 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 18 - 25,
Unify the status and description calculation so both use the same rules: replace
separate logic in getBudgetStatus and wherever insightDesc is computed by
creating/adjusting a single function (e.g., getBudgetStatus or a new
getBudgetEvaluation) that accepts percentage and periodElapsedRate plus
warningThreshold and dangerThreshold and returns a consistent TBudgetStatus and
the insight description together; update callers to use this single result so
the gauge/label and the insight text never diverge (affects getBudgetStatus and
the locations computing insightDesc around the other referenced blocks).
🤖 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/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 90-93: The progress bar(s) in BudgetGaugeChart lack accessible
names; update the progress element(s) in the BudgetGaugeChart component to
include an accessible name by either adding aria-label="이번 달 사용 예산 소진율" (or a
similar descriptive label) or by giving the title span a unique id and
referencing it from the progress with aria-labelledby (apply the same change for
the second progress instance around lines 107-112); ensure the progress elements
(the bar element(s) in BudgetGaugeChart) receive the new aria attribute so
screen readers announce what the value represents.

In `@src/components/dashboard/charts/trafficChart.config.ts`:
- Line 15: ANOMALY_INDEX is hardcoded in trafficChart.config.ts which decouples
marker position from the data and will point to the wrong place when the dataset
changes; remove the exported ANOMALY_INDEX constant and instead compute the
anomaly marker index from the actual chart data where markers are rendered
(e.g., replace usages of ANOMALY_INDEX with a runtime lookup such as
data.findIndex(d => d.isAnomaly) or by matching a timestamp/id field), or expose
the anomaly index alongside the data payload so the marker uses that value;
update any code referencing ANOMALY_INDEX to use the computed/index-from-data
value (search for ANOMALY_INDEX and the marker-rendering logic in
trafficChart.config.ts and the chart component).

In `@src/components/dashboard/charts/TrafficChart.tsx`:
- Around line 104-105: The chart currently only exposes hover handlers
(handleMouseMove, handleMouseLeave) which blocks keyboard and touch users from
seeing anomaly info; update TrafficChart to add keyboard and pointer
accessibility paths by: make the interactive element focusable (add tabindex and
appropriate role/aria attributes), wire onFocus to the same logic as
handleMouseMove (or a new handleFocus) and onBlur to handleMouseLeave (or
handleBlur), and add onPointerMove/onPointerLeave handlers that reuse
handleMouseMove/handleMouseLeave so touch/pointer devices receive the same
feedback; ensure any dynamic text uses aria-describedby or an aria-live region
so screen readers announce the anomaly details.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 70-72: When rendering platform logos in PlatformRoasTable,
accessing platformLogoMap[platform.name] is unsafe and can render undefined for
unknown platforms; update the render to use a type-guarded lookup and a
fallback: check whether platform.name exists as a key in platformLogoMap (or use
Object.prototype.hasOwnProperty.call) and if not render a default/fallback logo
or placeholder component (e.g., a generic icon or initials) instead of
undefined; ensure this logic is applied where platformLogoMap and platform.name
are used in the component so new/unknown platform names do not produce empty UI.

---

Nitpick comments:
In `@src/components/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 66-85: The logic that computes periodElapsedRate/percentage inside
BudgetGaugeChart should not call new Date() locally; instead accept a stable
referenceDate or precomputed elapsedDays/totalDays via props and use those to
derive periodElapsedRate and percentage so all cards share the same time basis.
Update the BudgetGaugeChart component signature to add either referenceDate or
elapsedDays/totalDays props, remove any local new Date() usage that feeds
periodElapsedRate, and use the incoming values when evaluating isOverBudget,
dangerThreshold, warningThreshold and building insightDesc so the conditions and
messages remain consistent across the dashboard.
- Around line 18-25: Unify the status and description calculation so both use
the same rules: replace separate logic in getBudgetStatus and wherever
insightDesc is computed by creating/adjusting a single function (e.g.,
getBudgetStatus or a new getBudgetEvaluation) that accepts percentage and
periodElapsedRate plus warningThreshold and dangerThreshold and returns a
consistent TBudgetStatus and the insight description together; update callers to
use this single result so the gauge/label and the insight text never diverge
(affects getBudgetStatus and the locations computing insightDesc around the
other referenced blocks).

In `@src/components/dashboard/charts/trafficChart.config.ts`:
- Around line 121-124: The BASE_OPTIONS currently hardcodes yaxis max using
trafficChartMock.clicks which will misalign when real API data is used; refactor
BASE_OPTIONS into a factory function createTrafficChartOptions(clicks) that
accepts the clicks series and computes yaxis.max as
Math.ceil(Math.max(...clicks) / 10000) * 10000 (preserve min: 0 and tickAmount:
6), update any usages to call createTrafficChartOptions(series) instead of
referencing BASE_OPTIONS, and keep other base properties intact so the chart
scales correctly for dynamic data.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 16-17: The module-level constant maxRoas is computed from
platformRoasRanking at import time, so it won't update after API data changes;
move the calculation into the PlatformRoasTable component (e.g., inside the
component body or a useMemo) and reference platformRoasRanking prop/state there
so maxRoas is recomputed whenever platformRoasRanking changes; update the other
occurrences noted (lines ~41-42) the same way to ensure all ROAS-derived values
depend on current props/state rather than a frozen module-level value.
- Around line 27-35: The Delta component treats value === 0 as "up"; update it
to explicitly handle neutral/flat values by using a three-way condition and
passing a neutral direction to TrendBadge (e.g., direction={value > 0 ? "up" :
value < 0 ? "down" : "neutral"}), keeping the displayed value as
`${Math.abs(value).toFixed(1)}%` (or `"0.0%"`) so 0% renders a clear neutral
state; adjust the TrendBadge usage if it expects a different neutral token
(e.g., "flat" or undefined) accordingly.
- Around line 43-61: The grid-based table lacks semantic/ARIA roles, so update
PlatformRoasTable: add role="table" and an accessible aria-label or
aria-labelledby to the outer container div (the one with class "@container
flex..."), mark the header div (the one with class including `${COL} gap-x-4
...`) as role="rowgroup" and change each header span to role="columnheader" (add
aria-sort where relevant), mark the body wrapper (the div with "flex flex-col
pb-2 divide-y...") as role="rowgroup", and for each mapped row inside
platformRoasRanking (the div created in map with `key={platform.name}` and
`className="group grid..."`) add role="row" and change each data span inside
that row to role="cell"; ensure keyboard focus if rows are interactive by adding
tabIndex and appropriate aria-selected/aria-pressed attributes as needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5565e823-9a68-4708-9bbe-bba2f88ea4ca

📥 Commits

Reviewing files that changed from the base of the PR and between e957079 and b38293c.

📒 Files selected for processing (5)
  • src/components/dashboard/charts/BudgetGaugeChart.tsx
  • src/components/dashboard/charts/TrafficChart.tsx
  • src/components/dashboard/charts/trafficChart.config.ts
  • src/components/dashboard/charts/useAnomalyMarkerPos.ts
  • src/components/dashboard/platform/PlatformRoasTable.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/dashboard/charts/useAnomalyMarkerPos.ts

Comment thread src/components/dashboard/charts/BudgetGaugeChart.tsx
Comment thread src/components/dashboard/charts/trafficChart.config.ts
Comment thread src/components/dashboard/charts/TrafficChart.tsx Outdated
Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/dashboard/charts/BudgetGaugeChart.tsx (1)

48-65: ⚠️ Potential issue | 🟡 Minor

퍼센트/임계값 경계값 방어가 부족해서 시각 표현이 깨질 수 있어요.

percentage가 음수일 때 Line 130의 scaleX(...)가 음수가 되어 바가 반전 렌더링될 수 있고, 임계값도 0~100 범위를 벗어나면 마커 위치가 카드 밖으로 나갈 수 있습니다. 시각화용 값은 한 번 정규화(clamp)해서 재사용하는 게 안전합니다.

경계값 방어 예시
   const percentage =
     totalBudget > 0 ? Math.round((spent / totalBudget) * 100) : 0;
+  const normalizedPercentage = Math.min(Math.max(percentage, 0), 100);
+  const normalizedWarning = Math.min(Math.max(warningThreshold, 0), 100);
+  const normalizedDanger = Math.min(Math.max(dangerThreshold, 0), 100);

-  const status = getBudgetStatus(percentage, warningThreshold, dangerThreshold);
+  const status = getBudgetStatus(
+    normalizedPercentage,
+    normalizedWarning,
+    normalizedDanger,
+  );

           <div
             className="absolute top-0 bottom-0 w-0.5 bg-white/60 z-10"
-            style={{ left: `${warningThreshold}%` }}
+            style={{ left: `${normalizedWarning}%` }}
           />
           <div
             className="absolute top-0 bottom-0 w-0.5 bg-white/60 z-10"
-            style={{ left: `${dangerThreshold}%` }}
+            style={{ left: `${normalizedDanger}%` }}
           />
...
-              transform: `scaleX(${mounted ? Math.min(percentage, 100) / 100 : 0})`,
+              transform: `scaleX(${mounted ? normalizedPercentage / 100 : 0})`,
             }}

Also applies to: 106-131

🤖 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 48 - 65,
Clamp all visualization values before use: ensure percentage is bounded to 0–100
(use the computed percentage from spent/totalBudget but clamp negatives and
>100), clamp warningThreshold and dangerThreshold into 0–100 and handle
swapped/out-of-order thresholds, guard periodTotalDays from zero and clamp
periodElapsedRate to 0–100, and compute remaining/isOverBudget independently of
clamped display values; update usages of percentage, periodElapsedRate,
warningThreshold and dangerThreshold (and any place that calls scaleX(...) with
percentage) to use the clamped variants so markers and bars never render outside
the card or invert.
♻️ Duplicate comments (1)
src/components/dashboard/platform/PlatformRoasTable.tsx (1)

29-31: ⚠️ Potential issue | 🟡 Minor

maxRoas 분모 0 케이스를 가드해 주세요.

Line 30 계산값이 0이면 Line 100에서 NaN% 너비가 만들어져 ROAS 바가 깨질 수 있습니다(실 API에서 전부 0인 데이터일 때 재현됩니다).

수정 제안
-const maxRoas = Math.max(...platformRoasRanking.map((p) => p.roas));
+const maxRoas = Math.max(...platformRoasRanking.map((p) => p.roas), 1);

Also applies to: 99-101

🤖 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 29 -
31, Guard against a zero denominator when computing bar widths: in
PlatformRoasTable replace the current maxRoas calculation using
Math.max(...platformRoasRanking.map(p => p.roas)) with a safe max that never
equals 0 (for example Math.max(...platformRoasRanking.map(p => p.roas), 1) or
conditionally set to 1 if result is 0), and update any width calculations that
use (p.roas / maxRoas) * 100 so they rely on this non-zero maxRoas; ensure the
same fix is applied to the other ROAS width computation sites that currently
divide by maxRoas.
🧹 Nitpick comments (3)
src/components/dashboard/charts/BudgetGaugeChart.tsx (1)

16-38: 상태 계산/매핑 로직은 컴포넌트 밖 유틸로 분리하는 게 좋아요.

지금처럼 BudgetGaugeChart.tsx에서 getBudgetStatus, statusBadgeVariant를 export하면, 상위 페이지가 “차트 컴포넌트 구현”에 의존하게 됩니다. 재사용 로직은 budgetStatus.ts 같은 별도 유틸로 옮기고, 컴포넌트는 consume만 하도록 분리해 두는 편이 유지보수/의존성 관리에 유리합니다.

권장 리팩터링 예시
-// src/components/dashboard/charts/BudgetGaugeChart.tsx
-export type TBudgetStatus = "안정" | "주의" | "위험";
-export function getBudgetStatus(...) { ... }
-export const statusBadgeVariant: Record<TBudgetStatus, TBadgeVariant> = { ... }

+// src/components/dashboard/charts/utils/budgetStatus.ts
+import { type TBadgeVariant } from "@/components/common/badge/Badge";
+
+export type TBudgetStatus = "안정" | "주의" | "위험";
+
+export function getBudgetStatus(
+  percentage: number,
+  warningThreshold: number,
+  dangerThreshold: number,
+): TBudgetStatus {
+  if (percentage >= dangerThreshold) return "위험";
+  if (percentage >= warningThreshold) return "주의";
+  return "안정";
+}
+
+export const statusBadgeVariant: Record<TBudgetStatus, TBadgeVariant> = {
+  안정: "success",
+  주의: "syncing",
+  위험: "inactive",
+};

As per coding guidelines, "구조와 책임 분리: 페이지에 비즈니스 로직이 과도하지 않은지 확인. 커스텀 훅으로의 분리 여부 검토."

🤖 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 16 - 38,
Move the budget status logic (TBudgetStatus type, getBudgetStatus function,
statusBadgeVariant and statusPointClasses maps) out of BudgetGaugeChart.tsx into
a new utility module (e.g., budgetStatus.ts), export them from that module, and
update BudgetGaugeChart.tsx to import these symbols instead of defining them
inline; ensure getBudgetStatus signature and exported names remain unchanged so
callers don't need changes, and update any tests or imports that referenced the
original exports to the new module.
src/components/dashboard/charts/TrafficChart.tsx (1)

70-75: 마커 좌표 훅 실행 타이밍을 isMounted와 동기화하면 초기 미검출 케이스를 줄일 수 있습니다.

지금은 Line [74]에서 훅이 항상 실행되는데, src/components/dashboard/charts/useAnomalyMarkerPos.ts는 첫 탐색 실패 시 재시도 트리거가 제한적입니다. 마운트 이후에만 좌표 탐색 effect가 돌도록 enabled 인자를 추가하면 안정성이 좋아집니다.

리팩터링 예시
-  const markerPos = useAnomalyMarkerPos(containerRef);
+  const markerPos = useAnomalyMarkerPos(containerRef, isMounted);
// src/components/dashboard/charts/useAnomalyMarkerPos.ts
export function useAnomalyMarkerPos(
  containerRef: RefObject<HTMLDivElement | null>,
  enabled: boolean,
) {
  useEffect(() => {
    if (!enabled) return;
    // 기존 로직
  }, [enabled, updateMarkerPos, containerRef]);
}

As per coding guidelines, src/**: "3. Hook 사용: useEffect 의존성 배열 및 불필요한 사용 검토."

🤖 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 70 - 75, The
hook useAnomalyMarkerPos is called unconditionally which can cause
initial-detection misses; update its signature to accept an enabled boolean and
guard its internal effect with that flag (e.g., add parameter enabled and return
early in the effect if !enabled), then call it from TrafficChart as
useAnomalyMarkerPos(containerRef, isMounted) so the markerPos calculation only
runs after isMounted is true; keep existing refs/state (containerRef, markerPos,
isAnomalyHovered, setIsAnomalyHovered) unchanged except for the new enabled arg.
src/components/dashboard/charts/trafficChart.config.ts (1)

19-19: 파일명 날짜가 UTC 기준이라 로컬 날짜와 하루 어긋날 수 있습니다.

Line [19]의 toISOString()는 UTC 기준이라, KST 자정 근처에는 파일명이 전날로 저장될 수 있어요. 로컬 기준 포맷으로 바꾸는 걸 추천드립니다.

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

In `@src/components/dashboard/charts/trafficChart.config.ts` at line 19, TODAY is
derived via toISOString() which yields UTC dates and can be off by a day in
local time; change the TODAY constant assignment so it formats the current local
date (YYYY-MM-DD) instead of using toISOString(). Update the TODAY definition in
trafficChart.config.ts (the TODAY constant) to generate a local-date string (for
example using a local-safe formatter such as building from new
Date().getFullYear()/getMonth()+1/getDate() with zero-padding or using
toLocaleDateString with a YYYY-MM-DD locale like 'sv' or 'en-CA') so filenames
reflect local (KST) dates rather than UTC.
🤖 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/dashboard/charts/trafficChart.config.ts`:
- Line 31: The forEach callback used on
chartContext.el.querySelectorAll("title") is written as an implicit-return arrow
((el) => el.remove()) which violates the useIterableCallbackReturn lint rule;
change the callback to a block-bodied arrow function so it explicitly contains
the statement (e.g., (el) => { el.remove(); }) in the call to
chartContext.el.querySelectorAll("title").forEach(...) to eliminate the linter
error.

In `@src/components/dashboard/charts/TrafficChart.tsx`:
- Around line 79-84: The container div using role="img" (id CHART_CONTAINER_ID,
ref containerRef, attribute data-hide-tooltip) currently wraps interactive
children (e.g., the chart button rendered later), which can hide those controls
from assistive tech; change the chart wrapper to use role="group" or no role
(leave as a plain div) and move role="img" to the non-interactive
decorative/image-only layer instead so interactive elements remain in the
accessibility tree and the data-hide-tooltip logic (data-hide-tooltip) continues
to apply to the wrapper.

In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 56-68: Add ARIA roles to make the grid behave like a semantic
table: on the outer wrapper in PlatformRoasTable (the div with class "@container
flex...") add role="table" (and optionally aria-label/aria-labelledby), change
the header row div (the one building the column headers using COL) to
role="row", mark each header span as role="columnheader", and ensure all
rendered data rows (the rows generated later in the component between lines
~72-138) are role="row" with their child cells marked role="cell" (and add
aria-rowindex/aria-colindex if required for ordering). Update the header spans
for CTR/CVR and the final sales/ad span similarly so screen readers see
header→cell relationships.

---

Outside diff comments:
In `@src/components/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 48-65: Clamp all visualization values before use: ensure
percentage is bounded to 0–100 (use the computed percentage from
spent/totalBudget but clamp negatives and >100), clamp warningThreshold and
dangerThreshold into 0–100 and handle swapped/out-of-order thresholds, guard
periodTotalDays from zero and clamp periodElapsedRate to 0–100, and compute
remaining/isOverBudget independently of clamped display values; update usages of
percentage, periodElapsedRate, warningThreshold and dangerThreshold (and any
place that calls scaleX(...) with percentage) to use the clamped variants so
markers and bars never render outside the card or invert.

---

Duplicate comments:
In `@src/components/dashboard/platform/PlatformRoasTable.tsx`:
- Around line 29-31: Guard against a zero denominator when computing bar widths:
in PlatformRoasTable replace the current maxRoas calculation using
Math.max(...platformRoasRanking.map(p => p.roas)) with a safe max that never
equals 0 (for example Math.max(...platformRoasRanking.map(p => p.roas), 1) or
conditionally set to 1 if result is 0), and update any width calculations that
use (p.roas / maxRoas) * 100 so they rely on this non-zero maxRoas; ensure the
same fix is applied to the other ROAS width computation sites that currently
divide by maxRoas.

---

Nitpick comments:
In `@src/components/dashboard/charts/BudgetGaugeChart.tsx`:
- Around line 16-38: Move the budget status logic (TBudgetStatus type,
getBudgetStatus function, statusBadgeVariant and statusPointClasses maps) out of
BudgetGaugeChart.tsx into a new utility module (e.g., budgetStatus.ts), export
them from that module, and update BudgetGaugeChart.tsx to import these symbols
instead of defining them inline; ensure getBudgetStatus signature and exported
names remain unchanged so callers don't need changes, and update any tests or
imports that referenced the original exports to the new module.

In `@src/components/dashboard/charts/trafficChart.config.ts`:
- Line 19: TODAY is derived via toISOString() which yields UTC dates and can be
off by a day in local time; change the TODAY constant assignment so it formats
the current local date (YYYY-MM-DD) instead of using toISOString(). Update the
TODAY definition in trafficChart.config.ts (the TODAY constant) to generate a
local-date string (for example using a local-safe formatter such as building
from new Date().getFullYear()/getMonth()+1/getDate() with zero-padding or using
toLocaleDateString with a YYYY-MM-DD locale like 'sv' or 'en-CA') so filenames
reflect local (KST) dates rather than UTC.

In `@src/components/dashboard/charts/TrafficChart.tsx`:
- Around line 70-75: The hook useAnomalyMarkerPos is called unconditionally
which can cause initial-detection misses; update its signature to accept an
enabled boolean and guard its internal effect with that flag (e.g., add
parameter enabled and return early in the effect if !enabled), then call it from
TrafficChart as useAnomalyMarkerPos(containerRef, isMounted) so the markerPos
calculation only runs after isMounted is true; keep existing refs/state
(containerRef, markerPos, isAnomalyHovered, setIsAnomalyHovered) unchanged
except for the new enabled arg.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 09744442-2342-44a6-8f5f-9c1a8e04ba70

📥 Commits

Reviewing files that changed from the base of the PR and between b38293c and f9e98c5.

📒 Files selected for processing (4)
  • src/components/dashboard/charts/BudgetGaugeChart.tsx
  • src/components/dashboard/charts/TrafficChart.tsx
  • src/components/dashboard/charts/trafficChart.config.ts
  • src/components/dashboard/platform/PlatformRoasTable.tsx

Comment thread src/components/dashboard/charts/trafficChart.config.ts Outdated
Comment thread src/components/dashboard/charts/TrafficChart.tsx
Comment thread src/components/dashboard/platform/PlatformRoasTable.tsx
@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@github-actions

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

@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: 1

🤖 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/dashboard/charts/TrafficChart.tsx`:
- Around line 75-77: The anomaly trigger lacks keyboard/touch/assistive support:
update the button element in TrafficChart.tsx to use onFocus/onBlur alongside
onPointerEnter/onPointerLeave to set isAnomalyFocused and isAnomalyHovered (use
setIsAnomalyFocused/setIsAnomalyHovered), ensure CSS no longer forces opacity-0
when focused (add a focus-visible style to make it visible), add an onClick
handler so touch/click opens the tooltip, link the button and tooltip via
aria-controls on the button and aria-expanded reflecting the tooltip state, give
the tooltip role="status" or aria-live="polite" so screen readers announce
updates, and add a keydown handler (Escape) that closes the tooltip by clearing
the focused/hovered state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6dec68e3-aa7d-45a3-bb7a-3cf00d20c2aa

📥 Commits

Reviewing files that changed from the base of the PR and between 63e5db9 and 4d1c8dc.

📒 Files selected for processing (2)
  • src/components/dashboard/charts/TrafficChart.tsx
  • src/components/dashboard/charts/trafficChart.config.ts

Comment thread src/components/dashboard/charts/TrafficChart.tsx
@Seojegyeong
Seojegyeong requested a review from jjjsun March 16, 2026 04:48

@jjjsun jjjsun left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P4: 확인했습니다! 고생하셨어요!

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

Labels

✨ Feature 기능 개발 🎨 Html&css 마크업 & 스타일링 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ [Feature] 실시간 트래픽 차트 tooltip 커스텀 적용

3 participants