Skip to content

[Feature/#188] 실시간 트래픽 변화 UI 구현 및 AI 요약 버튼 수정 - #192

Merged
YermIm merged 5 commits into
developfrom
feature/#188
May 6, 2026
Merged

[Feature/#188] 실시간 트래픽 변화 UI 구현 및 AI 요약 버튼 수정#192
YermIm merged 5 commits into
developfrom
feature/#188

Conversation

@YermIm

@YermIm YermIm commented May 5, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

close #188

✨ 변경사항

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

✏️ 작업 내용

스크린샷 2026-05-06 오전 5 12 58 스크린샷 2026-05-06 오전 5 13 44

😅 미완성 작업

  • Task1

📢 논의 사항 및 참고 사항

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

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 새로운 랜딩 페이지 출시 (히어로 섹션, 기능 소개, 가이드, 요금제, FAQ)
    • 플랫폼별 실시간 트래픽 비교 차트
    • 캠페인 성과 상세 분석 테이블
    • 대시보드 AI 요약 기능 추가
    • 사용자 프로필 및 비밀번호 설정 페이지
  • UI/UX Improvements

    • 대시보드 레이아웃 및 차트 시각화 개선
    • 네비게이션 및 사이드바 기능 강화

@YermIm
YermIm requested review from Seojegyeong and jjjsun May 5, 2026 20:14
@YermIm YermIm self-assigned this May 5, 2026
@YermIm YermIm added 🎨 Html&css 마크업 & 스타일링 ✨ Feature 기능 개발 labels May 5, 2026
@vercel

vercel Bot commented May 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
where-you-ad Ready Ready Preview, Comment May 5, 2026 8:22pm

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@YermIm has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 47 minutes and 45 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9311800e-9aef-4e4b-8ffb-9d18f69d8727

📥 Commits

Reviewing files that changed from the base of the PR and between b6d2fc9 and 2cd67a9.

📒 Files selected for processing (1)
  • src/pages/dashboard/platform/platformDashboard.mock.ts
📝 Walkthrough

Walkthrough

란딩 페이지 전체 구현(헤더, 네비게이션, 여러 섹션 컴포넌트), 플랫폼 대시보드 실시간 트래픽 차트, 대시보드 개요 페이지 섹션들, 설정 페이지 컴포넌트, 네비게이션 유틸리티 및 사이드바 상태 관리, 공통 컴포넌트와 훅을 추가합니다.

Changes

Landing Page Implementation

Layer / File(s) Summary
Constants & Setup
src/constants/landing 2.ts
LANDING_META, PROBLEMS, FEATURES, HOW_IT_WORKS_STEPS, SOCIAL_PROOF_STATS를 as const로 정의
Animation Hooks
src/hooks/useScrollAnimation 2.ts
Framer Motion 기반 scroll 애니메이션 variants 및 useScrollAnimation hook 구현
Guide Components
src/components/landing/GuideOverviewChart 2.tsx, GuideTimeline 2.tsx, GuidePlatform 2.tsx
차트, 타임라인, 플랫폼 선택기 등 가이드 섹션 컴포넌트
Major Sections
src/components/landing/HeroSection 2.tsx, HowItWorksSection 2.tsx, ProblemSection 2.tsx, LandingFeatures 2.tsx, LandingPricing 2.tsx, LandingFAQ 2.tsx, LandingGuide 2.tsx
메인 란딩 섹션들 (hero, 단계별, 문제점, 기능, 가격, FAQ, 가이드)
Layout & Utilities
src/components/landing/LandingHeader 2.tsx, LandingNav 2.tsx, LandingHero 2.tsx, LandingMultiDevice 2.tsx, LandingFooter 2.tsx, LandingSectionHeader 2.tsx, SocialProofSection 2.tsx, CTASection 2.tsx, FeatureSection 2.tsx
헤더, 네비게이션, 푸터, 섹션 레이아웃 및 개별 섹션 컴포넌트
Page Assembly
src/pages/landing/LandingPage 2.tsx, src/pages/LandingPage 2.tsx
React.lazy와 Suspense를 활용한 란딩 페이지 메인 레이아웃

Platform Traffic Charts

Layer / File(s) Summary
Types & Mock Data
src/pages/dashboard/platform/platformDashboard.mock.ts
ITimeSeriesData, IClickStreamResponse 인터페이스 및 platformTrafficMock (GOOGLE, NAVER, META별 데이터)
Chart Components
src/components/dashboard/platform/PlatformTrafficChart.tsx, AllPlatformTrafficChart.tsx
ApexCharts 기반 단일/멀티 플랫폼 트래픽 차트 (시간대별 클릭 수 시각화)
Data Table
src/components/dashboard/platform/PlatformDetailTable 2.tsx
플랫폼 일일 성능 테이블 (useMemo 기반 집계 계산)
View Integration
src/components/dashboard/platform/AllPlatformView.tsx, SinglePlatformView.tsx
차트를 포함하는 플랫폼 뷰 컴포넌트들 (레이아웃 및 로딩 상태 관리)

Dashboard Overview Sections

Layer / File(s) Summary
Hooks
src/hooks/dashboard/useOverviewCampaignList 2.ts
useWorkspaceStore와 useCoreQuery로 캠페인 목록 fetch
Budget & KPI
src/pages/dashboard/overview/OverviewBudgetSection 2.tsx, OverviewKpiSection 2.tsx
예산 소진 및 KPI 통계 섹션 (에러/로딩/데이터 상태 관리)
Rankings & Campaigns
src/pages/dashboard/overview/OverviewPlatformSection 2.tsx, OverviewCampaignSnapshotCard 2.tsx
플랫폼 ROAS 순위 및 캠페인 스냅샷 카드
AI Integration
src/pages/dashboard/overview/OverviewAiDrawer 2.tsx
React.lazy 기반 AI 요약 드로어 (공유/PDF 다운로드 액션)
Dashboard Integration
src/pages/dashboard/platform/PlatformDashboard.tsx
Overview 섹션들을 마운트하고 AI 패널 상태 관리

Settings & Common UI

Layer / File(s) Summary
Hooks
src/hooks/common/useImageUploader 2.ts, useComingSoon 2.ts
파일 업로드 및 토스트 기반 "Coming Soon" 메시지 훅
Auth & Placeholders
src/components/auth/common/AuthFormShell 2.tsx, src/components/common/ComingSoonPlaceholder 2.tsx
인증 폼 쉘 (variant별 레이아웃) 및 Coming Soon 플레이스홀더
Settings Components
src/components/setting/PasswordSection 2.tsx, ProfileSection 2.tsx
비밀번호 변경 (show/hide toggle) 및 프로필 설정 (이미지 업로드, 조직 표시)

Navigation System

Layer / File(s) Summary
Path Utilities
src/utils/navigation/pathMatch 2.ts
normalizePathname (trailing slash 제거), isPathMatch (경로 매칭 로직)
Navigation Helpers
src/utils/navigation/mainNavSidebar 2.ts
mainNav에서 parent/child ID 매핑 및 활성 상태(isChildActive, isParentActive) 계산
Sidebar State
src/store/useSidebarStore 2.ts
Zustand 기반 sidebar 접기/펼치기 상태 관리 (toggle 기능)

Minor Chart Update

Layer / File(s) Summary
Styling Adjustment
src/components/dashboard/charts/BudgetGaugeChart.tsx
Insight footer 컨테이너에서 mt-auto 제거 (레이아웃 정렬 변경)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

분석 근거:

  • 파일 수 및 다양성: 40개 이상 파일, 컴포넌트·훅·유틸·상수·타입이 혼재
  • 로직 밀도: 애니메이션(Framer Motion), ApexCharts 차트, 상태 관리(Zustand), 폼 로직 등 다양
  • 독립적 검토 필요: 각 코호트별로 다른 패턴과 의존성을 가짐
  • 긍정 요소: 새 파일 위주라 기존 코드와의 상호작용 최소화, 테스트 없음

Possibly related PRs


Suggested reviewers

  • jjjsun
  • Seojegyeong

주요 검토 포인트

1. 란딩 페이지 섹션들

구조 & 성능 확인

  • Framer Motion 애니메이션의 초기 렌더 성능: useCounter hook이 requestAnimationFrame으로 구현되어 있는지 확인 (메인 스레드 블로킹 우려)
  • LandingFeaturesWorkspaceGraphic에서 setInterval 사용 시 cleanup이 제대로 되는지 확인 (메모리 누수 방지)
  • Lazy loading 섹션들의 Suspense fallback 높이가 레이아웃 shift를 유발할 수 있는지 검토

접근성 & 의미론

  • aria-hidden 속성을 장식 요소에 올바르게 적용했는지 확인
  • 애니메이션 시작 지점 (trigger threshold)이 사용자 기대치와 맞는지 확인

2. 플랫폼 대시보드 차트

차트 데이터 변환 & 성능

  • PlatformTrafficChartAllPlatformTrafficChart에서 useMemo의 의존성 배열이 정확한지 확인 (불필요한 재계산 방지)
  • 시계열 데이터의 minute 문자열을 Date로 변환할 때 타임존 처리가 정확한가? (현재는 로컬 타임존으로 변환되는 것 같음)
  • platformDashboard.mock.tsgenerateRealTimeTraffic() 함수가 서버 데이터와의 인터페이스를 올바르게 반영하는지 확인

차트 옵션 일관성

  • 두 차트 컴포넌트의 ApexOptions (축 레이블 포맷, 그라디언트 등)이 일관성 있게 적용되었는지 비교 검토
  • yMax 계산 로직이 비어있거나 잘못된 데이터에 대해 안전한가?

3. 대시보드 개요 페이지

데이터 흐름 & 상태 관리

  • useOverviewCampaignListuseWorkspaceStore에서 orgId를 가져올 때, orgId가 undefined인 초기 상태 처리가 명확한가? (현재 enabled: !!orgId로 처리 중)
  • OverviewCampaignSnapshotCard에서 selectedOrgId가 null일 때 render return을 null로 하는데, UI적으로 빈 공간이 생기지 않는가?
  • OverviewKpiSection의 에러 메시지 표시 로직 (isBudgetError, isKpisError)이 중복되지 않는가?

AI 드로어 액션 처리

  • onShareLink에서 window.location.href를 clipboard에 복사하는데, 복사 실패 시 사용자 피드백이 충분한가?
  • printAsPdf("ai-report-printing") 함수의 동작 방식이 정의되어 있는가?

4. 설정 컴포넌트

폼 검증 & 사용자 경험

  • PasswordSection의 3개 입력 필드 중 하나라도 에러가 있으면 제출 버튼이 disabled되는 로직이 있는가? (부모 컴포넌트에서 처리 필요)
  • ProfileSection의 이미지 미리보기: URL.createObjectURL() 생성 후 revoke 타이밍이 명확한가? (useImageUploader에서 cleanup으로 처리 중)

권한 & 보안

  • 프로필의 이메일/핸드폰이 disabled되어 있는데, 실제로 수정 불가능하도록 백엔드에서도 검증하는가?
  • 조직 필드도 disabled 상태인데, 조직 변경 UI가 다른 곳에 있는가?

5. 네비게이션 시스템

경로 매칭 로직

  • normalizePathname에서 trailing slash를 제거할 때, 루트 경로(/)는 보존되는가? (현재 .trim() || "/" 처리로 보임)
  • isPathMatch의 exact match + prefix match 로직이 라우트 우선순위와 맞는가? (예: /ads/ads/123의 구분)

Sidebar 상태 관리

  • useSidebarStoreisCollapsed 상태가 localStorage에 저장되지 않으면, 페이지 새로고침 시 초기값으로 리셋되는가? (현재 코드에서는 보이지 않음 - 개선 기회)

mainNavSidebar의 성능

  • createMainNavSidebarHelpers() 함수가 모듈 로드 시 1회만 실행되는가? (현재 mainNavSidebar로 singleton 처리된 것 같음 - 좋음)
  • childPathEntries의 정렬 및 longest-match 로직이 대규모 네비게이션 트리에서도 효율적인가?

추가 고려사항

  1. 번들 크기: 많은 새 컴포넌트와 Framer Motion, ApexCharts 의존성 추가로 인한 번들 크기 증가 검토
  2. 타입 안전성: 모든 새 인터페이스와 컴포넌트 props가 명확하게 typed되어 있는지 확인
  3. 국제화(i18n): 한글 문자열이 하드코딩되어 있는데, 추후 다국어 지원을 위한 계획이 있는가?
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#188

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

📚 Storybook 배포 완료

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

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
src/hooks/common/useImageUploader 2.ts-12-20 (1)

12-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

파일 선택 시 MIME/용량 검증을 추가해 주세요.

현재는 accept 속성에만 의존하고 있어 우회 입력이 가능합니다. onPickFile에서 f.typef.size를 직접 검증하고, 실패 시 상태를 초기화하거나 사용자 메시지를 주는 쪽이 안전합니다.

예시 수정안
  const onPickFile = (e: ChangeEvent<HTMLInputElement>) => {
    const f = e.target.files?.[0];
    if (!f) return;
+   const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
+   const maxBytes = 5 * 1024 * 1024; // 5MB
+   if (!allowedTypes.includes(f.type) || f.size > maxBytes) {
+     if (fileRef.current) fileRef.current.value = "";
+     setFile(null);
+     setPreview(null);
+     return;
+   }

    setFile(f);

    const url = URL.createObjectURL(f);
    setPreview(url);
  };
As per coding guidelines, `src/**`: "에러 처리: API 실패 대응 및 사용자 피드백 적절성 검토. ... 접근성 ... 안정성 중심으로 리뷰".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/common/useImageUploader` 2.ts around lines 12 - 20, onPickFile
currently trusts the input accept attribute; add explicit MIME and size checks
inside onPickFile (validate f.type against allowed types and f.size against max
bytes) and if validation fails call setFile(null)/setPreview('') (or equivalent
state reset) and trigger user feedback (e.g., setError or toast) instead of
creating the object URL; ensure you still revoke any created URL via
URL.revokeObjectURL when replacing preview and keep references to
URL.createObjectURL usage in this function to locate where to add the checks.
src/components/dashboard/platform/PlatformDetailTable 2.tsx-107-108 (1)

107-108: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

합계 행과 일별 행의 CPC 표시 형식이 일치하지 않습니다.

  • 합계 행 (Line 108): ₩{Math.round(total.cpc).toLocaleString()} — 정수로 반올림
  • 일별 행 (Line 140): ₩{row.cpc.toLocaleString()} — 소수점 그대로 표시

같은 컬럼에서 표현 방식이 다르면 사용자가 혼란을 느낄 수 있습니다. 소수점 자리 수를 통일해 주세요.

🛠️ 수정 제안 (소수점 없이 통일)
-                  ₩{row.cpc.toLocaleString()}
+                  ₩{Math.round(row.cpc).toLocaleString()}

또는 소수점 1자리로 통일:

-                  ₩{Math.round(total.cpc).toLocaleString()}
+                  ₩{total.cpc.toLocaleString(undefined, { maximumFractionDigits: 0 })}
-                  ₩{row.cpc.toLocaleString()}
+                  ₩{row.cpc.toLocaleString(undefined, { maximumFractionDigits: 0 })}

Also applies to: 139-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx around lines 107
- 108, In PlatformDetailTable 2.tsx the CPC in the total row (total.cpc) is
rounded with Math.round while the daily rows use row.cpc.toLocaleString(),
causing inconsistent display; update both renderings (the JSX cells that output
total.cpc and row.cpc) to use the same toLocaleString formatting with explicit
fraction options (e.g., toLocaleString(undefined, { maximumFractionDigits: 0 })
for no decimals or { minimumFractionDigits: 1, maximumFractionDigits: 1 } for
one decimal) so the CPC column is formatted identically across total and row
values.
src/components/dashboard/platform/PlatformDetailTable 2.tsx-93-93 (1)

93-93: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

sticky top-13은 헤더 높이에 종속된 매직 넘버입니다.

thead 높이가 변경되면 합계 행이 헤더와 겹치거나 간격이 생깁니다. CSS 변수나 calc()를 활용하거나, top-[var(--thead-height)] 방식으로 동적으로 관리하는 것이 더 견고합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx at line 93, The
table footer row in the PlatformDetailTable component uses a hard-coded magic
value "top-13" which will break if the thead height changes; replace this with a
CSS variable-driven value (e.g., top-[var(--thead-height)] or
top-[calc(var(--thead-height))]) and set/update --thead-height on the thead (or
container) to the actual header height (via CSS or computed in JS on
mount/resize). Locate the <tr className="... sticky top-13 ..."> in
PlatformDetailTable and swap the static class for a variable-based top value and
ensure the thead (or wrapper) defines --thead-height so the sticky footer stays
correctly positioned.
src/components/dashboard/platform/PlatformDetailTable 2.tsx-119-122 (1)

119-122: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

key={idx} (인덱스)보다 안정적인 식별자를 사용하는 것이 좋습니다.

날짜 데이터를 기반으로 정렬/필터링이 추가될 경우 리렌더링 시 DOM 불일치 문제가 발생할 수 있습니다. row.date가 유니크하다면 이를 key로 사용하는 것이 더 안전합니다.

-              key={idx}
+              key={row.date}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx around lines 119
- 122, 현재 data.map((row, idx) => ...)에서 tr에 key={idx}를 사용해 리렌더링 시 DOM 불일치가 발생할 수
있으니 안정적인 고유 식별자를 사용하도록 변경하세요; PlatformDetailTable 컴포넌트의 데이터 객체에 유니크한 필드(row.date
또는 row.id 등)가 있다면 key prop을 key={row.date} 또는 key={row.id}로 바꾸고, 만약 고유값이 확실치 않다면
데이터 소스에서 고유 id를 생성해 할당한 후 해당 필드를 사용하도록 수정하세요.
src/pages/dashboard/overview/OverviewBudgetSection 2.tsx-94-108 (1)

94-108: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

budget이 없는 정상 케이스에 빈 상태 메시지가 필요해요.

지금은 로딩/에러가 아닌데 데이터가 없으면 본문이 비어 보여서, 사용자 입장에서 상태를 판단하기 어렵습니다.

제안 코드
-            ) : budget ? (
-              <BudgetGaugeChart {...budget} />
-            ) : null}
+            ) : budget ? (
+              <BudgetGaugeChart {...budget} />
+            ) : (
+              <div className="flex flex-1 items-center justify-center px-4 py-4 text-center font-body2 text-text-placeholder">
+                표시할 예산 데이터가 없습니다.
+              </div>
+            )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/dashboard/overview/OverviewBudgetSection` 2.tsx around lines 94 -
108, When neither isBudgetLoading nor isBudgetError and budget is null, render a
clear empty state instead of null; update the conditional block that currently
chooses between BudgetGaugeSkeleton, BudgetGaugeChart and null to return an
empty-state UI (e.g., a simple message or a new EmptyBudgetState component) so
users can tell there's no budget data, referencing the existing symbols
isBudgetError, isBudgetLoading, budget, BudgetGaugeSkeleton and BudgetGaugeChart
and replacing the final `: null` branch with that empty-state render.
src/utils/navigation/mainNavSidebar 2.ts-23-28 (1)

23-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

resolveParentId에서 pathname을 정규화하지 않아 Map 캐시 미스 발생 가능

childPathToParentId 맵의 키는 normalizePathname(child.path)로 정규화되어 있지만, resolveParentId에 전달되는 pathname은 정규화 없이 그대로 Map.get에 사용됩니다. /dashboard/ 처럼 trailing slash가 있는 경우 exact lookup에 실패하고 항상 fallback 선형 탐색으로 빠지게 됩니다.

React Router location.pathname이 일반적으로 trailing slash가 없어 현재 환경에서는 크게 문제되지 않지만, 방어적으로 입력 시점에 정규화하는 것이 일관성 측면에서 낫습니다.

🛠️ 제안: 입력 정규화 추가
 function resolveParentId(pathname: string): string | undefined {
+  const normalized = normalizePathname(pathname);
   return (
-    childPathToParentId.get(pathname) ??
-    childPathEntries.find(([p]) => isPathMatch(pathname, p))?.[1]
+    childPathToParentId.get(normalized) ??
+    childPathEntries.find(([p]) => isPathMatch(normalized, p))?.[1]
   );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/navigation/mainNavSidebar` 2.ts around lines 23 - 28, Normalize the
incoming pathname before using it to query the cache: in resolveParentId, call
normalizePathname(pathname) and use that normalized value for
childPathToParentId.get(...) and for the isPathMatch fallback (or normalize the
candidate path entries similarly) so lookups are consistent with how keys were
stored; update resolveParentId to use normalizedPathname =
normalizePathname(pathname) and replace references to pathname when calling
childPathToParentId.get and isPathMatch.
src/components/landing/GuideTimeline 2.tsx-80-86 (1)

80-86: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

아이콘 전용 버튼에 aria-label을 추가해주세요.

Line 80-86(이전/다음)과 Line 163-165(카드 메뉴)는 텍스트가 없어 스크린리더에서 버튼 목적을 알기 어렵습니다. 각 버튼에 의미 있는 aria-label이 필요합니다.

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

Also applies to: 163-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/GuideTimeline` 2.tsx around lines 80 - 86, The
icon-only buttons in GuideTimeline (the buttons rendering ChevronLeftIcon and
ChevronRightIcon and the card menu buttons near ChevronDown/MoreIcon at the card
area) lack accessible names; update the button elements (the ones wrapping
ChevronLeftIcon, ChevronRightIcon, and the card menu button) to include
descriptive aria-label attributes (e.g., aria-label="Previous week",
aria-label="Next week", and aria-label="Open card menu" or "More options") so
screen readers can convey their purpose while preserving existing classes and
handlers.
src/components/landing/LandingGuide 2.tsx-12-24 (1)

12-24: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

imagealt가 선택적(optional)인데 else 브랜치에서 null 체크 없이 사용됩니다

TGuidePage에서 imagealt는 모두 optional로 정의되어 있습니다. 현재 데이터에서는 else 브랜치가 실행되지 않지만, 앞으로 use* 플래그 없이 페이지를 추가하면 <img src={undefined} alt={undefined} />가 렌더링되어 깨진 이미지와 접근성 문제가 발생합니다.

🛡️ 수정 제안
-                ) : (
+                ) : page.image ? (
                   <img
                     src={page.image}
-                    alt={page.alt}
+                    alt={page.alt ?? ""}
                     loading="lazy"
                     decoding="async"
                     className="w-full h-auto object-cover object-top"
                   />
+                ) : null}

또는 타입 레벨에서 보장하려면, use* 플래그가 없는 경우 imagealt를 필수값으로 만드는 discriminated union 활용을 검토해 보세요.

Also applies to: 155-163

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/LandingGuide` 2.tsx around lines 12 - 24, TGuidePage
declares image and alt as optional but the component renders an <img> in the
else branch without guarding against undefined; update the rendering logic in
the LandingGuide component (where TGuidePage pages are mapped/rendered) to only
render <img src=... alt=... /> when both page.image and page.alt are truthy
(e.g., if (page.image && page.alt) ...), or alternatively change the TGuidePage
type to a discriminated union that requires image and alt when none of the
useOverview/useTimeline/usePlatform flags are set so the compiler enforces
presence of image/alt; modify either the runtime check around the <img> or the
TGuidePage definition (useOverview/useTimeline/usePlatform discriminant)
accordingly to prevent <img src={undefined} alt={undefined} /> from rendering.
src/components/landing/GuidePlatform 2.tsx-96-116 (1)

96-116: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

플랫폼 선택 버튼에 aria-pressed 속성이 없습니다

isSelected 상태가 배경색으로만 표현되어 스크린 리더 사용자는 어떤 플랫폼이 선택되어 있는지 알 수 없습니다. 토글 버튼에는 aria-pressed를 사용해 선택 상태를 프로그래밍적으로 전달해야 합니다.

♿️ 수정 제안
                   <button
                     key={platform.id}
                     type="button"
                     onClick={() => togglePlatform(platform.id)}
+                    aria-pressed={isSelected}
                     className={`...`}
                   >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/GuidePlatform` 2.tsx around lines 96 - 116, The
platform toggle button uses visual styling only to indicate selection; update
the button rendered in the map (where isSelected is computed from selectedIds
and togglePlatform(platform.id) is called) to include an accessible state by
adding aria-pressed={isSelected} on the <button> (keep the existing onClick,
key, className, and children), so screen readers receive the selection state
programmatically.
src/components/landing/HeroSection 2.tsx-244-271 (1)

244-271: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

CTA 버튼에 클릭 핸들러가 없습니다

무료로 시작하기데모 보기 버튼 모두 onClick 핸들러가 없어 현재 클릭해도 아무 동작도 하지 않습니다. 랜딩 페이지의 핵심 전환(conversion) 경로인 만큼, 네비게이션이나 모달 로직이 조속히 연결되어야 합니다. 또한 type="button"이 없어 <form> 안에 배치될 경우 의도치 않은 폼 제출이 발생할 수 있습니다.

💡 수정 예시
           <motion.button
+            type="button"
+            onClick={() => { /* 회원가입 / 온보딩 페이지로 이동 */ }}
             className="relative rounded-xl bg-indigo-600 px-7 py-3 text-sm font-semibold text-white"
             ...
           >
             무료로 시작하기
           </motion.button>

           <motion.button
+            type="button"
+            onClick={() => { /* 데모 섹션으로 스크롤 또는 모달 오픈 */ }}
             className="rounded-xl border border-white/20 ..."
             ...
           >
             데모 보기
           </motion.button>

CTA 버튼에 라우팅이나 스크롤 로직을 연결하는 구현이 필요하다면 새 이슈로 트래킹하거나 코드를 바로 작성해 드릴 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/HeroSection` 2.tsx around lines 244 - 271, Both CTA
buttons (the motion.button elements rendering "무료로 시작하기" and "데모 보기") lack
onClick handlers and are missing type="button", which can cause no action or
accidental form submission; add type="button" to both buttons and wire their
onClick props to appropriate handlers (e.g., navigateToSignup or openSignupModal
for "무료로 시작하기", and openDemoModal or navigateToDemo for "데모 보기") or dispatch
routing/scroll logic, ensuring the handler names are declared/imported in the
component and used on the motion.button elements so clicks perform the intended
navigation/modal behavior.
🧹 Nitpick comments (11)
src/components/setting/PasswordSection 2.tsx (1)

65-67: ⚡ Quick win

현재/확인 비밀번호 토글 버튼에도 접근성 레이블을 추가해 주세요.

아이콘 버튼에 접근성 이름이 없어 스크린리더 사용자가 기능을 파악하기 어렵습니다. aria-labelaria-pressedshowCurrent, showConfirm에 맞춰 동일 패턴으로 넣는 게 좋습니다.

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

Also applies to: 102-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/setting/PasswordSection` 2.tsx around lines 65 - 67, The
password visibility toggle buttons lack accessible names and state; update the
button rendering around setShowCurrent/showCurrent (the button that toggles
EyeIcon/EyeOffIcon) and the corresponding confirm toggle
(setShowConfirm/showConfirm) to include aria-label (e.g., "Show current
password"/"Hide current password" or a single label like "Toggle current
password visibility") and aria-pressed={showCurrent} (and
aria-pressed={showConfirm} for the confirm button) so screen readers receive
both a descriptive name and the pressed state.
src/components/dashboard/platform/PlatformDetailTable 2.tsx (2)

40-58: ⚡ Quick win

인라인 <style> 태그로 전역 CSS 클래스를 주입하고 있어요 — 스타일 충돌 위험이 있습니다.

.custom-scrollbar는 전역 클래스명으로 주입되어 동일한 이름을 사용하는 다른 컴포넌트와 스타일 충돌을 일으킬 수 있습니다. 또한 컴포넌트가 렌더링될 때마다 <style> 태그가 DOM에 추가됩니다. Tailwind 커스텀 유틸리티(@utility) 또는 CSS 모듈로 이동하는 것을 권장합니다.

♻️ 개선 방안: CSS 모듈 사용

PlatformDetailTable.module.css 파일 생성:

/* PlatformDetailTable.module.css */
.customScrollbar {
  scrollbar-gutter: stable;
}
.customScrollbar::-webkit-scrollbar {
  width: 5px;
  height: 5px;
}
.customScrollbar::-webkit-scrollbar-track {
  background: transparent;
}
.customScrollbar::-webkit-scrollbar-thumb {
  background: `#D1D1D1`;
  border-radius: 10px;
}
.customScrollbar::-webkit-scrollbar-thumb:hover {
  background: `#B1B1B1`;
}

컴포넌트에서:

+import styles from "./PlatformDetailTable.module.css";
 // ...
-      <style>{`...`}</style>
-      <div className="overflow-auto max-h-125 relative custom-scrollbar border-t border-bg-disabled">
+      <div className={`overflow-auto max-h-125 relative border-t border-bg-disabled ${styles.customScrollbar}`}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx around lines 40
- 58, The inline <style> block in PlatformDetailTable injects a global
.custom-scrollbar and should be moved to a CSS module to avoid collisions and
repeated DOM injection: create PlatformDetailTable.module.css with the scrollbar
rules (use .customScrollbar as the class name and include the
::-webkit-scrollbar pseudo-elements), import it in the PlatformDetailTable
component (e.g., import styles from './PlatformDetailTable.module.css'), replace
usages of "custom-scrollbar" with className={styles.customScrollbar} in the JSX,
and remove the inline <style> tag from the component.

13-22: ⚡ Quick win

4번의 개별 reduce 호출을 단일 패스로 합칠 수 있습니다.

같은 배열을 4번 순회하는 것은 불필요한 연산입니다. 데이터가 많아질 경우 성능에 영향을 줄 수 있으니, 하나의 reduce로 합산하는 방식을 권장합니다.

♻️ 단일 reduce 패스로 리팩터링
-    const totalSpend = data.reduce((acc, curr) => acc + curr.spend, 0);
-    const totalImpressions = data.reduce(
-      (acc, curr) => acc + curr.impressions,
-      0,
-    );
-    const totalClicks = data.reduce((acc, curr) => acc + curr.clicks, 0);
-    const totalConversions = data.reduce(
-      (acc, curr) => acc + curr.conversions,
-      0,
-    );
+    const { totalSpend, totalImpressions, totalClicks, totalConversions, weightedRoas } =
+      data.reduce(
+        (acc, curr) => ({
+          totalSpend: acc.totalSpend + curr.spend,
+          totalImpressions: acc.totalImpressions + curr.impressions,
+          totalClicks: acc.totalClicks + curr.clicks,
+          totalConversions: acc.totalConversions + curr.conversions,
+          weightedRoas: acc.weightedRoas + curr.roas * curr.spend,
+        }),
+        { totalSpend: 0, totalImpressions: 0, totalClicks: 0, totalConversions: 0, weightedRoas: 0 },
+      );
     return {
       spend: totalSpend,
       impressions: totalImpressions,
       clicks: totalClicks,
       ctr: totalImpressions > 0 ? (totalClicks / totalImpressions) * 100 : 0,
       cpc: totalClicks > 0 ? totalSpend / totalClicks : 0,
       conversions: totalConversions,
-      roas:
-        totalSpend > 0
-          ? data.reduce((acc, curr) => acc + curr.roas * curr.spend, 0) /
-            totalSpend
-          : 0,
+      roas: totalSpend > 0 ? weightedRoas / totalSpend : 0,
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx around lines 13
- 22, Combine the four separate data.reduce calls into a single pass by
replacing the multiple reduces with one data.reduce that accumulates an object
containing spend, impressions, clicks, and conversions totals; update
totalSpend, totalImpressions, totalClicks, and totalConversions to derive their
values from that accumulator (reference the existing variables
totalSpend/totalImpressions/totalClicks/totalConversions and the current
data.reduce usage) so the array is only iterated once and the sums are computed
in one place.
src/pages/dashboard/overview/OverviewAiDrawer 2.tsx (1)

53-56: ⚡ Quick win

Drawer 패널 로딩 중 null fallback 대신 최소 스켈레톤을 권장해요.

지금은 열자마자 내용이 비어 보일 수 있어서, 짧은 로딩 피드백을 주는 편이 UX가 더 안정적입니다.

제안 코드
-      {isOpen && (
-        <Suspense fallback={null}>
+      {isOpen && (
+        <Suspense
+          fallback={
+            <div className="flex h-full min-h-40 items-center justify-center">
+              <span className="font-body2 text-text-placeholder">AI 요약을 불러오는 중…</span>
+            </div>
+          }
+        >
           <OverviewAiReportPanel />
         </Suspense>
       )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/dashboard/overview/OverviewAiDrawer` 2.tsx around lines 53 - 56,
The Suspense wrapper around OverviewAiReportPanel uses fallback={null}, causing
no visual feedback while loading; replace the null fallback with a lightweight
skeleton component (e.g., OverviewAiSkeleton or a small shared Skeleton) and
render that as Suspense's fallback so users see immediate loading UI; add a
minimal skeleton component if none exists and update the Suspense usage
(symbols: Suspense, fallback, OverviewAiReportPanel, isOpen) to pass the new
skeleton instead of null.
src/components/dashboard/platform/SinglePlatformView.tsx (1)

125-132: ⚡ Quick win

플랫폼 색상 상수는 공용 모듈로 분리하는 게 좋아요.

현재 색상 맵이 여러 파일에 중복되어 있어 값이 쉽게 드리프트날 수 있습니다. src/constants/platformTheme.ts 같은 공용 상수로 빼고 재사용해 주세요.

♻️ 제안 구조
-  const PLATFORM_THEME_COLORS: Record<string, string> = {
-    GOOGLE: "#f9ab00",
-    NAVER: "#03c75a",
-    META: "#1877f2",
-  };
+  // import { PLATFORM_THEME_COLORS } from "@/constants/platformTheme";

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/platform/SinglePlatformView.tsx` around lines 125 -
132, Extract the PLATFORM_THEME_COLORS constant out of SinglePlatformView and
into a shared module (e.g., export const PLATFORM_THEME_COLORS from a new
platform theme constants file), then import that constant into
SinglePlatformView and use it to compute platformColor as before; ensure you
export the same type (Record<string,string>) and preserve the lookup logic
(platform.toUpperCase()) so existing references to PLATFORM_THEME_COLORS and
platformColor remain unchanged.
src/utils/navigation/mainNavSidebar 2.ts (1)

30-40: ⚡ Quick win

getItemActiveState에서 c.path 미정규화 — pathMatch 2.ts 주석과 동일한 맥락

isPathMatch(pathname, c.path)isPathMatch(pathname, item.path) 모두 child/parent 경로를 정규화 없이 전달합니다. 현재 sidebarNav.ts의 경로들은 trailing slash가 없어 실제 동작에는 문제가 없지만, isPathMatch 내부 정규화 제안(pathMatch 2.ts 리뷰 참조)을 적용하면 이 부분도 함께 해결됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/navigation/mainNavSidebar` 2.ts around lines 30 - 40,
getItemActiveState passes raw item.path and child c.path into isPathMatch
without normalizing, so if you apply the path normalization changes from
pathMatch 2.ts those mismatches will remain; update getItemActiveState to
normalize paths before calling isPathMatch (e.g., call the existing
normalize/normalizePath helper used in pathMatch) for both item.path and c.path,
ensuring you only call isPathMatch with normalized strings and preserving the
same null/undefined checks for optional paths within getItemActiveState.
src/utils/navigation/pathMatch 2.ts (1)

4-5: ⚡ Quick win

isPathMatch의 암묵적 정규화 계약 — 내부 정규화 권장

isPathMatch는 입력 경로를 정규화하지 않아, 호출자가 미리 정규화된 경로를 전달해야 한다는 암묵적 계약이 생깁니다. mainNavSidebargetItemActiveState에서는 c.pathpathname이 정규화 없이 그대로 전달되고 있어, 라우터가 /dashboard/ 같은 trailing slash를 포함한 경로를 반환하는 경우 targetPath가 정규화되지 않았다면 예상치 못한 결과가 생길 수 있습니다.

함수 내부에서 직접 정규화하면 외부 의존성을 제거할 수 있습니다.

♻️ 제안: 함수 내부에서 정규화 처리
+import { normalizePathname } from "./pathMatch";
+
 export const isPathMatch = (currentPath: string, targetPath: string) =>
-  currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
+  (currentPath === targetPath ||
+    currentPath.startsWith(`${targetPath}/`)) ||
+  (() => {
+    const n = normalizePathname(targetPath);
+    const c = normalizePathname(currentPath);
+    return c === n || c.startsWith(`${n}/`);
+  })();

또는 더 간결하게:

 export const isPathMatch = (currentPath: string, targetPath: string) => {
+  const c = normalizePathname(currentPath);
+  const t = normalizePathname(targetPath);
-  return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
+  return c === t || c.startsWith(`${t}/`);
 };

(이 경우 normalizePathname도 같은 파일에 위치시키거나 임포트 필요)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/navigation/pathMatch` 2.ts around lines 4 - 5, isPathMatch
currently relies on callers to pre-normalize paths which causes brittle behavior
when callers like mainNavSidebar.getItemActiveState pass c.path and pathname
that may include trailing slashes; update isPathMatch to normalize both inputs
internally (e.g., use or add a normalizePathname helper that strips trailing
slashes and ensures consistent leading slash) and call normalizePathname on
currentPath and targetPath before comparing (retain the existing equality and
startsWith checks after normalization) so callers no longer need to normalize.
src/components/landing/FeatureSection 2.tsx (1)

1-3: 아직 구현되지 않은 컴포넌트입니다.

FeatureSection, CTASection, SocialProofSection 세 컴포넌트가 모두 빈 fragment를 반환하고 있습니다. PR 설명에서 Task1이 미완료로 표시된 점은 확인했습니다. 구현이 필요한 시점에 도움이 필요하시면 말씀해 주세요.

세 컴포넌트 구현 초안 생성이 필요하시면 요청해 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/FeatureSection` 2.tsx around lines 1 - 3, 세
컴포넌트(FeatureSection, CTASection, SocialProofSection)가 빈 fragment만 반환하므로 각 컴포넌트를
실제 UI로 구현해 주세요: FeatureSection은 주요 기능 목록(아이콘, 제목, 설명)을 그리드로 렌더링하는 컴포넌트로 구현하고,
CTASection은 강력한 콜투액션 문구와 버튼(onClick 핸들러 prop)을 포함하도록 만들며, SocialProofSection은
사용자 로고/리뷰 리스트(프로필, 이름, 인용문)를 받아 반복 렌더링하도록 만드세요; 각 컴포넌트의 props와 기본 스타일/접근성(alt
텍스트, button type, aria 속성)을 정의하고 현재 파일의 FeatureSection, CTASection,
SocialProofSection 식별자를 찾아 해당 반환부를 채워 넣으세요.
src/components/landing/ProblemSection 2.tsx (1)

174-177: ⚡ Quick win

accent.icon.split(" ")[0]은 문자열 포맷에 암묵적으로 의존합니다

번호 텍스트 색상을 위해 icon 필드 문자열을 공백 기준으로 분리해 첫 번째 클래스를 추출하는 방식은, CARD_ACCENTicon 값 순서나 포맷이 바뀌면 의도치 않은 클래스가 적용되어 조용히 깨집니다. CARD_ACCENT에 전용 필드를 추가하는 것이 명확합니다.

♻️ 리팩터 제안
 const CARD_ACCENT = [
   {
     border: "border-blue-500/20 hover:border-blue-500/60",
     icon: "text-blue-400 bg-blue-500/10",
     glow: "group-hover:shadow-blue-500/10",
     badge: "text-blue-400 bg-blue-500/10",
+    number: "text-blue-400",
   },
   {
     border: "border-orange-500/20 hover:border-orange-500/60",
     icon: "text-orange-400 bg-orange-500/10",
     glow: "group-hover:shadow-orange-500/10",
     badge: "text-orange-400 bg-orange-500/10",
+    number: "text-orange-400",
   },
   {
     border: "border-rose-500/20 hover:border-rose-500/60",
     icon: "text-rose-400 bg-rose-500/10",
     glow: "group-hover:shadow-rose-500/10",
     badge: "text-rose-400 bg-rose-500/10",
+    number: "text-rose-400",
   },
 ] as const;
-                  className={`absolute bottom-5 right-6 text-3xl font-black tabular-nums opacity-10 ${accent.icon.split(" ")[0]}`}
+                  className={`absolute bottom-5 right-6 text-3xl font-black tabular-nums opacity-10 ${accent.number}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/ProblemSection` 2.tsx around lines 174 - 177, The code
currently derives a classname by splitting accent.icon (accent.icon.split("
")[0]) which is brittle; add an explicit field on CARD_ACCENT (e.g., textClass
or numberClass) that contains the exact classname to apply for the number text,
update the component to use accent.textClass (or accent.numberClass) instead of
splitting accent.icon, and provide a safe fallback (e.g., '' or a default class)
in the span rendering inside ProblemSection where the number is rendered so
formatting won’t break if the accent entry is missing the new field.
src/hooks/useScrollAnimation 2.ts (1)

1-4: 💤 Low value

framer-motion 패키지 이름이 motion/react로 변경되었습니다

2025년 중반에 패키지가 motion으로 독립 분리되면서 공식 권장 임포트 경로가 "motion/react"로 바뀌었습니다. "framer-motion" 패키지는 현재도 동작하지만, 향후 마이그레이션을 대비해 임포트 경로를 통일하는 것이 좋습니다.

♻️ 임포트 경로 변경 예시
-import type { Variants } from "framer-motion";
-import { useAnimation, useReducedMotion } from "framer-motion";
+import type { Variants } from "motion/react";
+import { useAnimation, useReducedMotion } from "motion/react";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScrollAnimation` 2.ts around lines 1 - 4, Update the
framer-motion imports to the new package path: replace imports of Variants,
useAnimation, and useReducedMotion from "framer-motion" with imports from
"motion/react" (e.g., import type { Variants } and import { useAnimation,
useReducedMotion } from "motion/react"); ensure you remove the old
"framer-motion" import and keep useEffect and useInView as-is so useAnimation,
useReducedMotion and the Variants type resolve from the new module.
src/pages/landing/LandingPage 2.tsx (1)

28-39: lazy 섹션에 ErrorBoundary가 없습니다

Suspense는 로딩 상태만 처리하며, lazy 컴포넌트에서 런타임 에러가 발생하면 페이지 전체가 크래시될 수 있습니다. 각 섹션을 ErrorBoundary로 감싸면 에러 발생 시 해당 섹션만 폴백으로 대체되어 나머지 페이지는 정상 동작합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/landing/LandingPage` 2.tsx around lines 28 - 39, Wrap each
lazy-loaded section (Suspense with LandingFeatures, LandingGuide,
LandingPricing, LandingFAQ) in your ErrorBoundary component so runtime errors
inside those lazy components don't crash the whole page; replace the current
direct Suspense usage by nesting Suspense inside an ErrorBoundary (using
ErrorBoundary around Suspense or vice versa depending on your ErrorBoundary API)
and provide a per-section fallback (e.g., SectionFallback) to display when that
section errors. Ensure you import/reference the existing ErrorBoundary symbol
and keep SectionFallback as the UI shown on error/loading for each respective
section.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx:
- Around line 62-88: Add missing table accessibility semantics: update the table
element in PlatformDetailTable 2 (the <thead> block and its <th> cells) to
include a descriptive table label (either a <caption> inside the table or an
aria-label on the table element) and add scope="col" to each column header <th>
so screen readers can correctly associate headers with data cells; ensure the
caption text or aria-label clearly describes the table (e.g., "Platform detail
metrics by date") and add scope="col" to the <th> elements for 날짜, 비용(지출), 노출 수,
클릭 수, CTR(클릭률), CPC, 전환 수, and ROAS.
- Line 3: Move the IPlatformDailyPerformance type out of the mock file into a
dedicated types module (e.g., create a platform types file) and update imports:
extract the interface from platformDashboard.mock (IPlatformDailyPerformance)
into a new types file and then import that type both in PlatformDetailTable
component and in the mock file instead of importing from the mock; ensure any
exports use the same name (IPlatformDailyPerformance) so PlatformDetailTable and
the mock both reference the new shared type.

In `@src/components/landing/GuideTimeline` 2.tsx:
- Around line 89-97: Replace the clickable divs that render the "Sort" and
"Filter" controls with semantic buttons (use button type="button") to restore
keyboard accessibility; target the wrapper that currently contains SortIcon and
FilterIcon (the two inner divs with className "flex items-center gap-1.5
cursor-pointer hover:text-text-main transition-colors") and change them to
buttons, add visible focus styles (e.g., focus:outline/ ring classes consistent
with your design system) and keep the existing icon and label structure and
classes otherwise so behavior and styling remain consistent.

In `@src/components/landing/HowItWorksSection` 2.tsx:
- Around line 261-266: 버튼에 보조기기용 단계별 라벨이 빠져 있습니다; HowItWorksSection 컴포넌트의 해당 버튼
요소(사용중인 symbols: setActiveStep, activeStep, key i)에 단계 번호를 기반으로 한 aria-label을
추가하세요 (예: aria-label={`${i + 1}단계로 이동`} 또는 동일한 한국어 문자열), 버튼이 현재 활성 단계임을 명확히 하려면
필요시 aria-current="step"을 함께 설정하도록 구현하세요.
- Around line 244-248: The clickable step container currently uses a
non-semantic div (className "flex-1" with onClick={() => setActiveStep(i)})
which breaks keyboard and screen reader access; replace that div with a semantic
<button type="button"> that calls setActiveStep(i) (or wrap an accessible <a> if
navigation), ensure it retains the existing layout classes (e.g., "flex-1") and
add visible focus styles (outline or ring classes) and any aria attributes as
needed (e.g., aria-pressed or aria-current) so keyboard users can tab to and
activate the step and screen readers get state information.

In `@src/components/landing/LandingPricing` 2.tsx:
- Around line 6-18: The CTA branching relies on string-matching the plan name
which is brittle; update the TPlan type to include a discriminant field (e.g.,
ctaType: "mailto" | "signup" | "learnMore") and set this on each plan, then
change handleCta to switch on plan.ctaType (not planName === "프로") and update
any onClick handlers to pass the plan object (or plan.ctaType) so the CTA
behavior is driven by the new ctaType field rather than hardcoded plan names.
- Around line 193-203: The CTA button lacks an accessible aria-label with the
plan context; modify the button element that calls handleCta(plan.name) (the JSX
using plan.buttonText and plan.featured) to include an aria-label that combines
the button text and plan name (e.g., `${plan.buttonText} — ${plan.name}`) so
screen readers can distinguish between multiple similar CTAs; keep the existing
onClick and className intact and only add the aria-label attribute to that
button.

In `@src/components/landing/ProblemSection` 2.tsx:
- Around line 136-139: PROBLEMS is being indexed into PROBLEM_ICONS and
CARD_ACCENT without guarding against length mismatches and accent.icon is parsed
fragily with .split(" ")[0]; update the rendering loop that uses PROBLEMS.map to
(a) compute a safe index or iterate by index = Math.min(i, PROBLEM_ICONS.length
- 1, CARD_ACCENT.length - 1) or early-skip when PROBLEM_ICONS[i] or
CARD_ACCENT[i] is undefined, (b) provide a sensible fallback Icon and accent
when those arrays are shorter, and (c) stop relying on accent.icon.split(" ")[0]
— instead read a dedicated property (e.g., accent.colorClass or
accent.baseClass) or guard the split with a null check and a fallback string;
reference PROBLEMS, PROBLEM_ICONS, CARD_ACCENT, Icon, and accent.icon in your
changes.

In `@src/components/setting/PasswordSection` 2.tsx:
- Around line 83-87: The ARIA attributes for the new-password toggle are
referencing the wrong state variable; update the aria-label and aria-pressed
used in the new-password toggle so they read from showNew (not showCurrent) and
ensure the onClick toggles via setShowNew((p) => !p); specifically modify the
component JSX where aria-label, aria-pressed, and onClick are defined for the
new-password button to use showNew and setShowNew to fix accessibility
semantics.

In `@src/components/setting/ProfileSection` 2.tsx:
- Around line 118-120: The tooltip div (the element with class
"pointer-events-none absolute top-full mt-1 whitespace-nowrap rounded
bg-gray-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity
group-hover:opacity-100") is currently hover-only; make it keyboard-accessible
by adding the Tailwind state class "group-focus-within:opacity-100" to the same
class list so it becomes visible on focus, and give the tooltip an id (e.g.,
tooltip-org-info) and add aria-describedby="tooltip-org-info" to the
corresponding interactive container/input element that forms the group (the
element using "group" to trigger the tooltip). Apply the same changes for the
other two tooltip instances referenced (around lines 133-135 and 145-147).

In `@src/pages/dashboard/platform/platformDashboard.mock.ts`:
- Around line 200-203: The function generateRealTimeTraffic currently declares
an unused parameter named platform; remove this parameter from its signature
(change generateRealTimeTraffic(platform: string, baseCount: number) to
generateRealTimeTraffic(baseCount: number): IClickStreamResponse) and update
every call site to pass only baseCount, ensuring any related type annotations or
overloads referencing the platform parameter are updated accordingly; search for
all references to generateRealTimeTraffic and remove the extra argument so
TypeScript noUnusedParameters linting will pass.

In `@src/pages/dashboard/platform/PlatformDashboard.tsx`:
- Around line 129-133: The onShareLink click handler currently assumes
navigator.clipboard exists and only handles the Promise rejection, so browsers
without clipboard support can throw a synchronous exception; update the
onShareLink handler to first check for navigator.clipboard (and
navigator.clipboard.writeText) and wrap the writeText call in a try/catch; if
unsupported, show toast.error("링크 복사에 실패했습니다.") (or a more specific message) and
if writeText is used, await or then/catch the Promise to call toast.success("링크가
복사되었습니다.") on success and toast.error(...) on failure. Ensure you update the
handler that references navigator.clipboard and window.location.href and
preserve existing toast.success/toast.error behavior.

---

Minor comments:
In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx:
- Around line 107-108: In PlatformDetailTable 2.tsx the CPC in the total row
(total.cpc) is rounded with Math.round while the daily rows use
row.cpc.toLocaleString(), causing inconsistent display; update both renderings
(the JSX cells that output total.cpc and row.cpc) to use the same toLocaleString
formatting with explicit fraction options (e.g., toLocaleString(undefined, {
maximumFractionDigits: 0 }) for no decimals or { minimumFractionDigits: 1,
maximumFractionDigits: 1 } for one decimal) so the CPC column is formatted
identically across total and row values.
- Line 93: The table footer row in the PlatformDetailTable component uses a
hard-coded magic value "top-13" which will break if the thead height changes;
replace this with a CSS variable-driven value (e.g., top-[var(--thead-height)]
or top-[calc(var(--thead-height))]) and set/update --thead-height on the thead
(or container) to the actual header height (via CSS or computed in JS on
mount/resize). Locate the <tr className="... sticky top-13 ..."> in
PlatformDetailTable and swap the static class for a variable-based top value and
ensure the thead (or wrapper) defines --thead-height so the sticky footer stays
correctly positioned.
- Around line 119-122: 현재 data.map((row, idx) => ...)에서 tr에 key={idx}를 사용해 리렌더링
시 DOM 불일치가 발생할 수 있으니 안정적인 고유 식별자를 사용하도록 변경하세요; PlatformDetailTable 컴포넌트의 데이터 객체에
유니크한 필드(row.date 또는 row.id 등)가 있다면 key prop을 key={row.date} 또는 key={row.id}로
바꾸고, 만약 고유값이 확실치 않다면 데이터 소스에서 고유 id를 생성해 할당한 후 해당 필드를 사용하도록 수정하세요.

In `@src/components/landing/GuidePlatform` 2.tsx:
- Around line 96-116: The platform toggle button uses visual styling only to
indicate selection; update the button rendered in the map (where isSelected is
computed from selectedIds and togglePlatform(platform.id) is called) to include
an accessible state by adding aria-pressed={isSelected} on the <button> (keep
the existing onClick, key, className, and children), so screen readers receive
the selection state programmatically.

In `@src/components/landing/GuideTimeline` 2.tsx:
- Around line 80-86: The icon-only buttons in GuideTimeline (the buttons
rendering ChevronLeftIcon and ChevronRightIcon and the card menu buttons near
ChevronDown/MoreIcon at the card area) lack accessible names; update the button
elements (the ones wrapping ChevronLeftIcon, ChevronRightIcon, and the card menu
button) to include descriptive aria-label attributes (e.g., aria-label="Previous
week", aria-label="Next week", and aria-label="Open card menu" or "More
options") so screen readers can convey their purpose while preserving existing
classes and handlers.

In `@src/components/landing/HeroSection` 2.tsx:
- Around line 244-271: Both CTA buttons (the motion.button elements rendering
"무료로 시작하기" and "데모 보기") lack onClick handlers and are missing type="button",
which can cause no action or accidental form submission; add type="button" to
both buttons and wire their onClick props to appropriate handlers (e.g.,
navigateToSignup or openSignupModal for "무료로 시작하기", and openDemoModal or
navigateToDemo for "데모 보기") or dispatch routing/scroll logic, ensuring the
handler names are declared/imported in the component and used on the
motion.button elements so clicks perform the intended navigation/modal behavior.

In `@src/components/landing/LandingGuide` 2.tsx:
- Around line 12-24: TGuidePage declares image and alt as optional but the
component renders an <img> in the else branch without guarding against
undefined; update the rendering logic in the LandingGuide component (where
TGuidePage pages are mapped/rendered) to only render <img src=... alt=... />
when both page.image and page.alt are truthy (e.g., if (page.image && page.alt)
...), or alternatively change the TGuidePage type to a discriminated union that
requires image and alt when none of the useOverview/useTimeline/usePlatform
flags are set so the compiler enforces presence of image/alt; modify either the
runtime check around the <img> or the TGuidePage definition
(useOverview/useTimeline/usePlatform discriminant) accordingly to prevent <img
src={undefined} alt={undefined} /> from rendering.

In `@src/hooks/common/useImageUploader` 2.ts:
- Around line 12-20: onPickFile currently trusts the input accept attribute; add
explicit MIME and size checks inside onPickFile (validate f.type against allowed
types and f.size against max bytes) and if validation fails call
setFile(null)/setPreview('') (or equivalent state reset) and trigger user
feedback (e.g., setError or toast) instead of creating the object URL; ensure
you still revoke any created URL via URL.revokeObjectURL when replacing preview
and keep references to URL.createObjectURL usage in this function to locate
where to add the checks.

In `@src/pages/dashboard/overview/OverviewBudgetSection` 2.tsx:
- Around line 94-108: When neither isBudgetLoading nor isBudgetError and budget
is null, render a clear empty state instead of null; update the conditional
block that currently chooses between BudgetGaugeSkeleton, BudgetGaugeChart and
null to return an empty-state UI (e.g., a simple message or a new
EmptyBudgetState component) so users can tell there's no budget data,
referencing the existing symbols isBudgetError, isBudgetLoading, budget,
BudgetGaugeSkeleton and BudgetGaugeChart and replacing the final `: null` branch
with that empty-state render.

In `@src/utils/navigation/mainNavSidebar` 2.ts:
- Around line 23-28: Normalize the incoming pathname before using it to query
the cache: in resolveParentId, call normalizePathname(pathname) and use that
normalized value for childPathToParentId.get(...) and for the isPathMatch
fallback (or normalize the candidate path entries similarly) so lookups are
consistent with how keys were stored; update resolveParentId to use
normalizedPathname = normalizePathname(pathname) and replace references to
pathname when calling childPathToParentId.get and isPathMatch.

---

Nitpick comments:
In `@src/components/dashboard/platform/PlatformDetailTable` 2.tsx:
- Around line 40-58: The inline <style> block in PlatformDetailTable injects a
global .custom-scrollbar and should be moved to a CSS module to avoid collisions
and repeated DOM injection: create PlatformDetailTable.module.css with the
scrollbar rules (use .customScrollbar as the class name and include the
::-webkit-scrollbar pseudo-elements), import it in the PlatformDetailTable
component (e.g., import styles from './PlatformDetailTable.module.css'), replace
usages of "custom-scrollbar" with className={styles.customScrollbar} in the JSX,
and remove the inline <style> tag from the component.
- Around line 13-22: Combine the four separate data.reduce calls into a single
pass by replacing the multiple reduces with one data.reduce that accumulates an
object containing spend, impressions, clicks, and conversions totals; update
totalSpend, totalImpressions, totalClicks, and totalConversions to derive their
values from that accumulator (reference the existing variables
totalSpend/totalImpressions/totalClicks/totalConversions and the current
data.reduce usage) so the array is only iterated once and the sums are computed
in one place.

In `@src/components/dashboard/platform/SinglePlatformView.tsx`:
- Around line 125-132: Extract the PLATFORM_THEME_COLORS constant out of
SinglePlatformView and into a shared module (e.g., export const
PLATFORM_THEME_COLORS from a new platform theme constants file), then import
that constant into SinglePlatformView and use it to compute platformColor as
before; ensure you export the same type (Record<string,string>) and preserve the
lookup logic (platform.toUpperCase()) so existing references to
PLATFORM_THEME_COLORS and platformColor remain unchanged.

In `@src/components/landing/FeatureSection` 2.tsx:
- Around line 1-3: 세 컴포넌트(FeatureSection, CTASection, SocialProofSection)가 빈
fragment만 반환하므로 각 컴포넌트를 실제 UI로 구현해 주세요: FeatureSection은 주요 기능 목록(아이콘, 제목, 설명)을
그리드로 렌더링하는 컴포넌트로 구현하고, CTASection은 강력한 콜투액션 문구와 버튼(onClick 핸들러 prop)을 포함하도록 만들며,
SocialProofSection은 사용자 로고/리뷰 리스트(프로필, 이름, 인용문)를 받아 반복 렌더링하도록 만드세요; 각 컴포넌트의
props와 기본 스타일/접근성(alt 텍스트, button type, aria 속성)을 정의하고 현재 파일의 FeatureSection,
CTASection, SocialProofSection 식별자를 찾아 해당 반환부를 채워 넣으세요.

In `@src/components/landing/ProblemSection` 2.tsx:
- Around line 174-177: The code currently derives a classname by splitting
accent.icon (accent.icon.split(" ")[0]) which is brittle; add an explicit field
on CARD_ACCENT (e.g., textClass or numberClass) that contains the exact
classname to apply for the number text, update the component to use
accent.textClass (or accent.numberClass) instead of splitting accent.icon, and
provide a safe fallback (e.g., '' or a default class) in the span rendering
inside ProblemSection where the number is rendered so formatting won’t break if
the accent entry is missing the new field.

In `@src/components/setting/PasswordSection` 2.tsx:
- Around line 65-67: The password visibility toggle buttons lack accessible
names and state; update the button rendering around setShowCurrent/showCurrent
(the button that toggles EyeIcon/EyeOffIcon) and the corresponding confirm
toggle (setShowConfirm/showConfirm) to include aria-label (e.g., "Show current
password"/"Hide current password" or a single label like "Toggle current
password visibility") and aria-pressed={showCurrent} (and
aria-pressed={showConfirm} for the confirm button) so screen readers receive
both a descriptive name and the pressed state.

In `@src/hooks/useScrollAnimation` 2.ts:
- Around line 1-4: Update the framer-motion imports to the new package path:
replace imports of Variants, useAnimation, and useReducedMotion from
"framer-motion" with imports from "motion/react" (e.g., import type { Variants }
and import { useAnimation, useReducedMotion } from "motion/react"); ensure you
remove the old "framer-motion" import and keep useEffect and useInView as-is so
useAnimation, useReducedMotion and the Variants type resolve from the new
module.

In `@src/pages/dashboard/overview/OverviewAiDrawer` 2.tsx:
- Around line 53-56: The Suspense wrapper around OverviewAiReportPanel uses
fallback={null}, causing no visual feedback while loading; replace the null
fallback with a lightweight skeleton component (e.g., OverviewAiSkeleton or a
small shared Skeleton) and render that as Suspense's fallback so users see
immediate loading UI; add a minimal skeleton component if none exists and update
the Suspense usage (symbols: Suspense, fallback, OverviewAiReportPanel, isOpen)
to pass the new skeleton instead of null.

In `@src/pages/landing/LandingPage` 2.tsx:
- Around line 28-39: Wrap each lazy-loaded section (Suspense with
LandingFeatures, LandingGuide, LandingPricing, LandingFAQ) in your ErrorBoundary
component so runtime errors inside those lazy components don't crash the whole
page; replace the current direct Suspense usage by nesting Suspense inside an
ErrorBoundary (using ErrorBoundary around Suspense or vice versa depending on
your ErrorBoundary API) and provide a per-section fallback (e.g.,
SectionFallback) to display when that section errors. Ensure you
import/reference the existing ErrorBoundary symbol and keep SectionFallback as
the UI shown on error/loading for each respective section.

In `@src/utils/navigation/mainNavSidebar` 2.ts:
- Around line 30-40: getItemActiveState passes raw item.path and child c.path
into isPathMatch without normalizing, so if you apply the path normalization
changes from pathMatch 2.ts those mismatches will remain; update
getItemActiveState to normalize paths before calling isPathMatch (e.g., call the
existing normalize/normalizePath helper used in pathMatch) for both item.path
and c.path, ensuring you only call isPathMatch with normalized strings and
preserving the same null/undefined checks for optional paths within
getItemActiveState.

In `@src/utils/navigation/pathMatch` 2.ts:
- Around line 4-5: isPathMatch currently relies on callers to pre-normalize
paths which causes brittle behavior when callers like
mainNavSidebar.getItemActiveState pass c.path and pathname that may include
trailing slashes; update isPathMatch to normalize both inputs internally (e.g.,
use or add a normalizePathname helper that strips trailing slashes and ensures
consistent leading slash) and call normalizePathname on currentPath and
targetPath before comparing (retain the existing equality and startsWith checks
after normalization) so callers no longer need to normalize.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f85acac4-10d4-4070-8e9a-425128ce411e

📥 Commits

Reviewing files that changed from the base of the PR and between 20c42f5 and b6d2fc9.

⛔ Files ignored due to path filters (20)
  • scripts/strip-cursor-attribution 2.mjs is excluded by none and included by none
  • src/assets/icon/ai/warning 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/chevron/chevron-down 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/camera 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/check-white 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/lock 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/userProfileCircle 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/timeline/chevron-left 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/timeline/chevron-right 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/timeline/filter 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/timeline/kebab 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/timeline/sort 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/service-logo/logo 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/social-logo/plain/google_ads 2.png is excluded by !**/*.png and included by src/**
  • src/assets/logo/social-logo/plain/meta 2.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/social-logo/wordmark/naver-wordmark 2.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/iOS app dock 2.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/iPad Air mockup 2.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/laptop_mockup 2.png is excluded by !**/*.png and included by src/**
  • vercel 2.json is excluded by none and included by none
📒 Files selected for processing (46)
  • src/components/auth/common/AuthFormShell 2.tsx
  • src/components/common/ComingSoonPlaceholder 2.tsx
  • src/components/dashboard/charts/BudgetGaugeChart.tsx
  • src/components/dashboard/platform/AllPlatformTrafficChart.tsx
  • src/components/dashboard/platform/AllPlatformView.tsx
  • src/components/dashboard/platform/PlatformDetailTable 2.tsx
  • src/components/dashboard/platform/PlatformTrafficChart.tsx
  • src/components/dashboard/platform/SinglePlatformView.tsx
  • src/components/landing/CTASection 2.tsx
  • src/components/landing/FeatureSection 2.tsx
  • src/components/landing/GuideOverviewChart 2.tsx
  • src/components/landing/GuidePlatform 2.tsx
  • src/components/landing/GuideTimeline 2.tsx
  • src/components/landing/HeroSection 2.tsx
  • src/components/landing/HowItWorksSection 2.tsx
  • src/components/landing/LandingFAQ 2.tsx
  • src/components/landing/LandingFeatures 2.tsx
  • src/components/landing/LandingFooter 2.tsx
  • src/components/landing/LandingGuide 2.tsx
  • src/components/landing/LandingHeader 2.tsx
  • src/components/landing/LandingHero 2.tsx
  • src/components/landing/LandingMultiDevice 2.tsx
  • src/components/landing/LandingNav 2.tsx
  • src/components/landing/LandingPricing 2.tsx
  • src/components/landing/LandingSectionHeader 2.tsx
  • src/components/landing/ProblemSection 2.tsx
  • src/components/landing/SocialProofSection 2.tsx
  • src/components/setting/PasswordSection 2.tsx
  • src/components/setting/ProfileSection 2.tsx
  • src/constants/landing 2.ts
  • src/hooks/common/useComingSoon 2.ts
  • src/hooks/common/useImageUploader 2.ts
  • src/hooks/dashboard/useOverviewCampaignList 2.ts
  • src/hooks/useScrollAnimation 2.ts
  • src/pages/LandingPage 2.tsx
  • src/pages/dashboard/overview/OverviewAiDrawer 2.tsx
  • src/pages/dashboard/overview/OverviewBudgetSection 2.tsx
  • src/pages/dashboard/overview/OverviewCampaignSnapshotCard 2.tsx
  • src/pages/dashboard/overview/OverviewKpiSection 2.tsx
  • src/pages/dashboard/overview/OverviewPlatformSection 2.tsx
  • src/pages/dashboard/platform/PlatformDashboard.tsx
  • src/pages/dashboard/platform/platformDashboard.mock.ts
  • src/pages/landing/LandingPage 2.tsx
  • src/store/useSidebarStore 2.ts
  • src/utils/navigation/mainNavSidebar 2.ts
  • src/utils/navigation/pathMatch 2.ts

Comment thread src/components/dashboard/platform/PlatformDetailTable 2.tsx
Comment thread src/components/dashboard/platform/PlatformDetailTable 2.tsx
Comment thread src/components/landing/GuideTimeline 2.tsx
Comment thread src/components/landing/HowItWorksSection 2.tsx
Comment thread src/components/landing/HowItWorksSection 2.tsx
Comment thread src/components/landing/ProblemSection 2.tsx
Comment thread src/components/setting/PasswordSection 2.tsx
Comment thread src/components/setting/ProfileSection 2.tsx
Comment thread src/pages/dashboard/platform/platformDashboard.mock.ts
Comment thread src/pages/dashboard/platform/PlatformDashboard.tsx

@Seojegyeong Seojegyeong 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: 고생하셨습니다!

@YermIm
YermIm merged commit d7b0f47 into develop May 6, 2026
4 checks passed
@YermIm
YermIm deleted the feature/#188 branch May 6, 2026 02:30
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] 실시간 트래픽 변화 UI 구현

2 participants