[Feature/#188] 실시간 트래픽 변화 UI 구현 및 AI 요약 버튼 수정 - #192
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough란딩 페이지 전체 구현(헤더, 네비게이션, 여러 섹션 컴포넌트), 플랫폼 대시보드 실시간 트래픽 차트, 대시보드 개요 페이지 섹션들, 설정 페이지 컴포넌트, 네비게이션 유틸리티 및 사이드바 상태 관리, 공통 컴포넌트와 훅을 추가합니다. ChangesLanding Page Implementation
Platform Traffic Charts
Dashboard Overview Sections
Settings & Common UI
Navigation System
Minor Chart Update
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 분석 근거:
Possibly related PRs
Suggested reviewers
주요 검토 포인트1. 란딩 페이지 섹션들구조 & 성능 확인
접근성 & 의미론
2. 플랫폼 대시보드 차트차트 데이터 변환 & 성능
차트 옵션 일관성
3. 대시보드 개요 페이지데이터 흐름 & 상태 관리
AI 드로어 액션 처리
4. 설정 컴포넌트폼 검증 & 사용자 경험
권한 & 보안
5. 네비게이션 시스템경로 매칭 로직
Sidebar 상태 관리
mainNavSidebar의 성능
추가 고려사항
✨ Finishing Touches🧪 Generate unit tests (beta)
|
📚 Storybook 배포 완료
|
There was a problem hiding this comment.
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.type과f.size를 직접 검증하고, 실패 시 상태를 초기화하거나 사용자 메시지를 주는 쪽이 안전합니다.As per coding guidelines, `src/**`: "에러 처리: API 실패 대응 및 사용자 피드백 적절성 검토. ... 접근성 ... 안정성 중심으로 리뷰".예시 수정안
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); };🤖 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
image와alt가 선택적(optional)인데else브랜치에서 null 체크 없이 사용됩니다
TGuidePage에서image와alt는 모두 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*플래그가 없는 경우image와alt를 필수값으로 만드는 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 winCTA 버튼에 클릭 핸들러가 없습니다
무료로 시작하기와데모 보기버튼 모두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-label과aria-pressed를showCurrent,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 win4번의 개별
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 winDrawer 패널 로딩 중
nullfallback 대신 최소 스켈레톤을 권장해요.지금은 열자마자 내용이 비어 보일 수 있어서, 짧은 로딩 피드백을 주는 편이 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는 입력 경로를 정규화하지 않아, 호출자가 미리 정규화된 경로를 전달해야 한다는 암묵적 계약이 생깁니다.mainNavSidebar의getItemActiveState에서는c.path와pathname이 정규화 없이 그대로 전달되고 있어, 라우터가/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_ACCENT의icon값 순서나 포맷이 바뀌면 의도치 않은 클래스가 적용되어 조용히 깨집니다.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
⛔ Files ignored due to path filters (20)
scripts/strip-cursor-attribution 2.mjsis excluded by none and included by nonesrc/assets/icon/ai/warning 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/chevron/chevron-down 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/camera 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/check-white 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/lock 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/userProfileCircle 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/timeline/chevron-left 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/timeline/chevron-right 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/timeline/filter 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/timeline/kebab 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/timeline/sort 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/service-logo/logo 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/social-logo/plain/google_ads 2.pngis excluded by!**/*.pngand included bysrc/**src/assets/logo/social-logo/plain/meta 2.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/social-logo/wordmark/naver-wordmark 2.pngis excluded by!**/*.pngand included bysrc/**src/assets/mockup/iOS app dock 2.pngis excluded by!**/*.pngand included bysrc/**src/assets/mockup/iPad Air mockup 2.pngis excluded by!**/*.pngand included bysrc/**src/assets/mockup/laptop_mockup 2.pngis excluded by!**/*.pngand included bysrc/**vercel 2.jsonis excluded by none and included by none
📒 Files selected for processing (46)
src/components/auth/common/AuthFormShell 2.tsxsrc/components/common/ComingSoonPlaceholder 2.tsxsrc/components/dashboard/charts/BudgetGaugeChart.tsxsrc/components/dashboard/platform/AllPlatformTrafficChart.tsxsrc/components/dashboard/platform/AllPlatformView.tsxsrc/components/dashboard/platform/PlatformDetailTable 2.tsxsrc/components/dashboard/platform/PlatformTrafficChart.tsxsrc/components/dashboard/platform/SinglePlatformView.tsxsrc/components/landing/CTASection 2.tsxsrc/components/landing/FeatureSection 2.tsxsrc/components/landing/GuideOverviewChart 2.tsxsrc/components/landing/GuidePlatform 2.tsxsrc/components/landing/GuideTimeline 2.tsxsrc/components/landing/HeroSection 2.tsxsrc/components/landing/HowItWorksSection 2.tsxsrc/components/landing/LandingFAQ 2.tsxsrc/components/landing/LandingFeatures 2.tsxsrc/components/landing/LandingFooter 2.tsxsrc/components/landing/LandingGuide 2.tsxsrc/components/landing/LandingHeader 2.tsxsrc/components/landing/LandingHero 2.tsxsrc/components/landing/LandingMultiDevice 2.tsxsrc/components/landing/LandingNav 2.tsxsrc/components/landing/LandingPricing 2.tsxsrc/components/landing/LandingSectionHeader 2.tsxsrc/components/landing/ProblemSection 2.tsxsrc/components/landing/SocialProofSection 2.tsxsrc/components/setting/PasswordSection 2.tsxsrc/components/setting/ProfileSection 2.tsxsrc/constants/landing 2.tssrc/hooks/common/useComingSoon 2.tssrc/hooks/common/useImageUploader 2.tssrc/hooks/dashboard/useOverviewCampaignList 2.tssrc/hooks/useScrollAnimation 2.tssrc/pages/LandingPage 2.tsxsrc/pages/dashboard/overview/OverviewAiDrawer 2.tsxsrc/pages/dashboard/overview/OverviewBudgetSection 2.tsxsrc/pages/dashboard/overview/OverviewCampaignSnapshotCard 2.tsxsrc/pages/dashboard/overview/OverviewKpiSection 2.tsxsrc/pages/dashboard/overview/OverviewPlatformSection 2.tsxsrc/pages/dashboard/platform/PlatformDashboard.tsxsrc/pages/dashboard/platform/platformDashboard.mock.tssrc/pages/landing/LandingPage 2.tsxsrc/store/useSidebarStore 2.tssrc/utils/navigation/mainNavSidebar 2.tssrc/utils/navigation/pathMatch 2.ts
🚨 관련 이슈
close #188
✨ 변경사항
✏️ 작업 내용
😅 미완성 작업
📢 논의 사항 및 참고 사항
Summary by CodeRabbit
릴리스 노트
New Features
UI/UX Improvements