[Refactor/#200] 디자인 시스템 컬러 및 폰트 위계 점검 및 개선 - #207
Conversation
- Split index.css into tokens, base, utilities, and print modules. - Move @font-face to the top of base.css. - Co-locate GuideTimeline scrollbar rules with the component.
- tokens: 텍스트/서페이스/프라이머리 팔레트 정리, 타이포 스케일 조정 - 전역 컴포넌트 반경·높이·모달 크기를 Tailwind 기본 스케일로 통일 - utilities: 미사용 CSS 애니메이션 제거, 쉬머만 유지(reduced-motion 대응) - Modal·DropdownMenu·ComingSoon: AnimatePresence/motion으로 전환 - Storybook Colors·Typography·BorderRadius를 토큰과 맞춤
- Split index.css into tokens, base, utilities, and print modules. - Move @font-face to the top of base.css. - Co-locate GuideTimeline scrollbar rules with the component.
- tokens: 텍스트/서페이스/프라이머리 팔레트 정리, 타이포 스케일 조정 - 전역 컴포넌트 반경·높이·모달 크기를 Tailwind 기본 스케일로 통일 - utilities: 미사용 CSS 애니메이션 제거, 쉬머만 유지(reduced-motion 대응) - Modal·DropdownMenu·ComingSoon: AnimatePresence/motion으로 전환 - Storybook Colors·Typography·BorderRadius를 토큰과 맞춤
- Login: 카카오 버튼에 배경색(#FEE500)·flex 중앙 정렬 적용 - kakao.svg: fill 색상 #040000 → #181600 통일 - Signup: 네이버 아이콘 크기 w-5/h-5 → w-6/h-6 - AdRow, PlatformRoasTable: 임의값 클래스를 Tailwind 유틸 클래스로 교체
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough디자인 토큰·유틸리티 도입과 전역 스타일 리팩터, 광고/캠페인 테이블의 선택·대량 작업(광고 소재/캠페인 일괄 중단·재개) 추가, 워크스페이스 경로 동적화 및 네비게이션/레이아웃 변경, 모달/드롭다운에 framer-motion 애니메이션이 도입되었습니다. Changes디자인 시스템 및 전역 스타일 통합
광고·캠페인 선택 및 대량 작업(기능 변화)
네비게이션·워크스페이스 경로·레이아웃 변경
모달·드롭다운에 framer-motion 도입 및 ModalContent 확장
워크스페이스 관리 UI 현대화(스타일·구조)
타입 변경 및 훅 제거/문구 수정(작은 API 변화)
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Client UI
participant Store as Workspace Store
participant API as Backend API
participant Nav as Navigation System
User->>UI: 캠페인/광고 체크박스 선택
UI->>Store: selectedIds 업데이트
User->>UI: 대량 중단/재개 클릭
UI->>API: updateCampaignStatus/updateAdStatus (selection 또는 all)
API-->>UI: 성공 응답
UI->>API: refetch 캠페인/광고
API-->>UI: 갱신된 데이터
UI->>Store: selectedIds 초기화
UI->>Nav: 워크스페이스 전환
Nav->>UI: applyWorkspacePathsToNav()로 nav 항목 갱신
sequenceDiagram
participant Dev as Developer
participant Tokens as Design Tokens (CSS)
participant Component as Components
Dev->>Tokens: tokens.css / utilities.css 작성
Tokens->>Component: .font-*, .shadow-*, var(--color-*) 제공
Component->>Component: tailwind class 교체, twMerge 적용
Component-->>Dev: 컴포넌트 렌더링 (새 토큰 반영)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 분 Possibly related issues
Possibly related PRs
Suggested reviewers
주의/검토 포인트(요약, 팀원에게 전달하듯 간결히)
간단한 테스트 시나리오: 캠페인 목록에서 일부 선택 → 중단 모달 열기(세부 리스트 확인) → 확인 → 네트워크 실패/성공 케이스 → 선택 초기화/유지 여부 확인; 워크스페이스 전환 후 사이드바 항목 경로가 즉시 변경되는지 확인. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/dashboard/charts/AdStatusChart.tsx (1)
5-9: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win차트 색상 하드코딩을 토큰으로 치환해 주세요.
PLATFORM_COLORS의 hex 값("#f9ab00"등)은 디자인 토큰 정책과 충돌합니다.tokens.css의@theme색상 토큰으로 매핑해서 사용해 주세요.As per coding guidelines
src/**/*.{css,tsx,ts}: Use only@themetokens from tokens.css for colors. Do not rename tokens or add arbitrary colors.및src/components/**/*.{tsx,ts}: Avoid adding arbitrary hex colors.Also applies to: 39-42
🤖 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/charts/AdStatusChart.tsx` around lines 5 - 9, PLATFORM_COLORS currently hardcodes hex values; replace those hex strings with the corresponding `@theme` color tokens from tokens.css (e.g. use the CSS variable form from `@theme` like "var(--theme-...)" or the project's token accessor) so the chart uses design tokens instead of raw hex; update the PLATFORM_COLORS constant in AdStatusChart.tsx (and the other similar entries noted around lines 39-42) to map GOOGLE, NAVER, META to their respective `@theme` token names found in tokens.css and ensure any usage of PLATFORM_COLORS in the component continues to accept the token string values.src/components/setting/PasswordSection.tsx (1)
87-90:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winaria-label이 잘못된 상태를 참조하고 있어요
새 비밀번호 입력의 토글 버튼인데
showCurrent상태를 참조하고 있습니다.showNew로 수정이 필요합니다.🔧 수정 제안
<button type="button" aria-label={ - showCurrent ? "현재 비밀번호 숨기기" : "현재 비밀번호 보기" + showNew ? "새 비밀번호 숨기기" : "새 비밀번호 보기" } - aria-pressed={showCurrent} + aria-pressed={showNew} onClick={() => setShowNew((p) => !p)} >🤖 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.tsx` around lines 87 - 90, The aria attributes for the new-password toggle are incorrectly using showCurrent; update the toggle in PasswordSection (the new-password visibility button that currently references showCurrent) to reference showNew instead so aria-label values ("새 비밀번호 숨기기"/"새 비밀번호 보기") and aria-pressed reflect the new-password visibility state; ensure the button that toggles new password visibility also uses the showNew state in its onClick handler if not already wired.src/components/ads/AdDetailContent.tsx (1)
174-183:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
orgId검증 없이 호출되어 API에NaN이 전달될 수 있습니다Line 182에서
Number(orgId)를 바로 사용하고 있어, 라우트 파라미터가 비어있는 경우 추적 링크 발급 요청이 잘못 전송됩니다. 호출 전에 유효성 검증을 넣어 주세요.수정 예시
onConfirm={() => trackControl.handleConfirm(async () => { + const parsedOrgId = Number(orgId); + if (!orgId || Number.isNaN(parsedOrgId)) { + toast.error("조직 정보가 올바르지 않아 발급할 수 없습니다."); + throw new Error("유효하지 않은 orgId"); + } + if (!ad.landingUrl) { toast.error( "광고에 등록된 랜딩 URL이 없어 발급이 불가능합니다.", ); throw new Error("랜딩 URL이 없습니다."); } - await createTrackingUrl(Number(orgId), ad.id, ad.landingUrl); + await createTrackingUrl(parsedOrgId, ad.id, ad.landingUrl); }) }🤖 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/ads/AdDetailContent.tsx` around lines 174 - 183, The call uses Number(orgId) directly which can produce NaN if the route param is missing; before invoking createTrackingUrl inside trackControl.handleConfirm (and before using ad.landingUrl), validate orgId by parsing it (e.g., parseInt or Number) and ensure it's a finite integer (not NaN) — if invalid, show a user-facing error (toast) and abort (throw or return) so createTrackingUrl(orgId, ad.id, ad.landingUrl) is never called with NaN; update the block around trackControl.handleConfirm and createTrackingUrl to perform this check and early-exit on invalid orgId.
🧹 Nitpick comments (15)
src/hooks/dashboard/useOverviewCampaignList.ts (1)
14-14: 💤 Low valueNon-null assertion이 약간 중복되긴 한데, 안전함
orgId!를 쓰고 있는데enabled: !!orgId옵션 덕분에 orgId가 없을 때는 쿼리가 실행 안 돼서 안전하다. 다만 조금 더 깔끔하게 하려면 fetcher를 다음처럼 바꿀 수도 있다:() => (orgId ? getCampaignList(orgId) : Promise.resolve([]))근데 현재 코드도 문제는 없으니 굳이 바꿀 필요는 없다.
🤖 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/dashboard/useOverviewCampaignList.ts` at line 14, 현재 fetcher에 orgId! 비무효(assertion) 사용이 중복되어 있어 깔끔하게 바꿀 수 있습니다; getCampaignList 호출부를 수정하여 orgId가 있을 때만 getCampaignList(orgId)를 호출하고 그렇지 않으면 빈 배열을 반환하는 Promise를 반환하도록 변경하세요(참조: fetcher 람다, getCampaignList, orgId, enabled: !!orgId).src/components/landing/LandingFooter.tsx (2)
16-33: 💤 Low value링크 스타일 클래스 중복 제거 고려
세 개의 링크가 모두 동일한 hover/focus-visible 스타일을 사용하고 있어서 클래스 문자열이 반복되고 있어. 유지보수성을 위해 공통 스타일을 상수로 추출하는 것도 고려해볼 만해.
♻️ 제안하는 리팩토링
+const linkClassName = "hover:text-primary-500 transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/35 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-200"; + export default function LandingFooter() { return ( <footer className="bg-surface-200 text-text-title py-16 px-6 md:px-12"> <div className="max-w-7xl mx-auto"> <div className="flex flex-col md:flex-row items-center md:items-start justify-between gap-10"> <div className="flex flex-col items-center md:items-start gap-6"> <div className="text-center font-body2 text-text-auth-sub md:text-left"> <p>Where You Ad</p> </div> </div> <div className="flex items-center gap-8 font-body2 text-surface-500"> <a href="https://www.notion.so/Where-you-ad-351085b3a16c8040b7bef7ac311da984?source=copy_link" target="_blank" rel="noreferrer" - className="hover:text-primary-500 transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/35 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-200" + className={linkClassName} > 이용약관 </a> <a href="https://www.notion.so/Where-you-ad-351085b3a16c80c3b171f97238e447ca?source=copy_link" target="_blank" rel="noreferrer" - className="hover:text-primary-500 transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/35 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-200" + className={linkClassName} > 개인정보처리방침 </a> <a href="mailto:whereyouadofficial@gmail.com" - className="hover:text-primary-500 transition-colors rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/35 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-200" + className={linkClassName} > 고객센터 </a> </div> </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/components/landing/LandingFooter.tsx` around lines 16 - 33, LandingFooter contains three anchor elements that repeat the same hover/focus class string; extract that repeated class string into a single constant (e.g., linkClass or LINK_STYLE) at the top of the LandingFooter component and replace the className values of the anchors (the links for "이용약관", "개인정보처리방침", and "고객센터") with that constant to remove duplication and improve maintainability.
11-11: ⚡ Quick win텍스트 색상 토큰 일관성 검토 필요
PR의 디자인 시스템 원칙에서 "텍스트 색은 역할 토큰(text-text-*) 통일"을 명시하고 있는데, 현재 링크의 기본 색상이
text-surface-500을 사용하고 있어.text-text-sub또는text-text-muted같은 텍스트 역할 토큰으로 변경하는 게 디자인 시스템 원칙과 더 일관성 있을 것 같은데, 의도적으로surface-500을 사용한 거라면 괜찮아.♻️ 제안하는 수정안
- <div className="flex items-center gap-8 font-body2 text-surface-500"> + <div className="flex items-center gap-8 font-body2 text-text-sub">As per coding guidelines: "텍스트 색은 역할 토큰(text-text-*) 통일"
🤖 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/LandingFooter.tsx` at line 11, The footer row currently uses a surface color token in the className ("flex items-center gap-8 font-body2 text-surface-500") inside LandingFooter.tsx; change that to a text role token (e.g., text-text-sub or text-text-muted) so it follows the design system guideline "텍스트 색은 역할 토큰(text-text-*) 통일" — update the className on the element that contains text-surface-500 to use the chosen text-text-* token and run a quick visual check to confirm contrast/appearance.src/components/common/modal/Modal.tsx (1)
53-58: 💤 Low valuerequestAnimationFrame 사용의 필요성을 재검토해보면 좋을 것 같아요.
useLayoutEffect는 paint 전에 동기적으로 실행되므로,modalRef.current는 이미 DOM에 존재합니다.requestAnimationFrame을 사용하면 다음 프레임까지 포커스가 지연되는데,AnimatePresence와motion.div가 이미 마운트를 올바르게 처리하고 있어서 직접modalRef.current?.focus()를 호출해도 안전할 것 같아요.다만 현재 코드도 동작에 문제는 없으니, 선호도에 따라 판단하시면 됩니다.
♻️ rAF 제거 제안
useLayoutEffect(() => { if (!isOpen) return; previousActiveElement.current = document.activeElement as HTMLElement; - const id = requestAnimationFrame(() => modalRef.current?.focus()); - return () => cancelAnimationFrame(id); + modalRef.current?.focus(); }, [isOpen]);🤖 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/common/modal/Modal.tsx` around lines 53 - 58, The useLayoutEffect currently wraps modalRef.current?.focus() in requestAnimationFrame/cancelAnimationFrame; remove the rAF logic and call modalRef.current?.focus() synchronously inside useLayoutEffect when isOpen is true, keeping previousActiveElement.current assignment intact; update references to requestAnimationFrame and cancelAnimationFrame (remove them) and ensure modalRef, previousActiveElement, and the useLayoutEffect callback are the only items touched.src/components/common/dropdownmenu/DropdownMenu.tsx (1)
58-74: ⚡ Quick win트리거의 키보드 접근성을 개선하면 좋을 것 같아요.
role="button"이 있는 div에tabIndex={0}을 추가하면 키보드만 사용하는 사용자도 트리거에 포커스할 수 있습니다. 현재는 트리거가 실제 버튼 엘리먼트가 아닌 경우 키보드로 접근이 어려울 수 있어요.♿ tabIndex 추가 제안
<div role="button" + tabIndex={0} aria-haspopup="menu" aria-expanded={open} aria-controls={menuId} aria-label={ariaLabel} onClick={() => setOpen((v) => !v)}🤖 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/common/dropdownmenu/DropdownMenu.tsx` around lines 58 - 74, The trigger wrapper div in DropdownMenu lacks keyboard focusability; update the element (the div inside the DropdownMenu component that currently has role="button" and onKeyDown/onClick handlers) to include tabIndex={0} so keyboard-only users can focus it, keeping the existing onKeyDown handling for "Enter" and " " to toggle setOpen and leaving aria attributes (aria-haspopup, aria-expanded, aria-controls, aria-label) intact; ensure any relevant tests or accessibility docs reference the change.src/components/ads/CampaignRow.tsx (1)
59-72: ⚡ Quick win체크박스 컨테이너의
role="presentation"을 제거하는 게 좋을 것 같아요.체크박스(
<input type="checkbox">)는 이미 의미 있는 요소이므로, 감싸는 div에role="presentation"을 추가하면 스크린 리더에 혼란을 줄 수 있습니다. 단순 레이아웃 컨테이너라면 role을 생략하는 게 더 명확해요.♿ role 속성 제거 제안
<div className="flex w-11 shrink-0 items-center justify-center tablet:w-10" - role="presentation" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} >🤖 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/ads/CampaignRow.tsx` around lines 59 - 72, Remove the unnecessary role="presentation" from the div that wraps the checkbox in CampaignRow (the container currently around the <input type="checkbox"> using isSelected, onToggleSelect and name props); since the input is an interactive, semantic element, simply omit the role on that wrapper (leave the div for layout and keep the onClick/onKeyDown stopPropagation handlers and the input's checked/onChange/aria-label intact) so screen readers receive the checkbox semantics from the input itself.src/components/setting/ProfileSection.tsx (1)
116-122: ⚡ Quick win
disabled와readOnly속성이 중복 적용되어 있습니다.
Input컴포넌트에disabled={true}와readOnly를 동시에 사용하고 있는데,readOnly만으로도 수정 불가 상태를 표현할 수 있습니다.disabled는 포커스 자체를 차단하고 폼 제출 시 값이 포함되지 않으므로, 읽기 전용 필드에는readOnly만 사용하는 것이 적절합니다.♻️ 제안하는 수정안
<Input value={`${org.name} (${org.position})`} - disabled containerClassName="bg-surface-200" readOnly /><Input label="이메일" value={email} - disabled={true} rightElement={<CheckIcon className="h-6 w-6 text-primary-500" />} readOnly /><Input label="전화번호" value={phoneNumber} - disabled={true} rightElement={<CheckIcon className="h-6 w-6 text-primary-500" />} readOnly />Also applies to: 133-155
🤖 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/ProfileSection.tsx` around lines 116 - 122, Remove the redundant disabled prop from the Input usage and leave the field as read-only only: update the Input component instances (e.g., the one rendering value={`${org.name} (${org.position})`} and the similar blocks referenced at the other occurrences) to drop disabled={true} and keep readOnly (and existing containerClassName) so the field remains non-editable but still focusable and included in form submission.src/stories/Colors.stories.tsx (1)
10-61: 💤 Low value
colorSections데이터와tokens.css의 수동 동기화 위험.Storybook 문서에 표시할 색상 토큰 목록을
colorSections배열에 하드코딩했는데,tokens.css에 새 토큰이 추가되거나 변경될 때 이 파일을 함께 업데이트해야 합니다. 토큰과 문서가 불일치할 위험이 있으므로, 가능하다면tokens.css를 파싱하거나 자동 생성하는 방식을 고려하는 것이 좋습니다.현재 구현으로도 동작에는 문제가 없지만, 장기적으로는 유지보수 부담이 될 수 있습니다.
🤖 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/stories/Colors.stories.tsx` around lines 10 - 61, colorSections is hardcoded and can drift from tokens.css; replace the manual array with an automated generator that parses tokens.css (or an exported tokens JSON) and builds the same structure. Implement a helper like generateColorSectionsFromTokens()/parseCssVariables() that reads CSS variable names (e.g., --primary-*, --color-surface-*, --color-text-*) and groups them into section objects matching the shape of colorSections, then export/use that generator in Colors.stories.tsx instead of the static colorSections constant so the story always reflects tokens.css changes.src/components/dashboard/platform/PlatformDetailTable.tsx (1)
99-103: ⚡ Quick win행 key에 인덱스 대신 안정 식별자를 써주세요.
현재
key={idx}는 데이터 순서가 바뀌면 잘못된 DOM 재사용으로 이어질 수 있습니다.row.date(또는 고유 ID)가 보장된다면 해당 값을 key로 쓰는 쪽이 안전합니다.제안 diff
- {data.map((row, idx) => ( + {data.map((row) => ( <tr - key={idx} + key={row.date} className="hover:bg-surface-200/30 transition-colors group" >🤖 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.tsx` around lines 99 - 103, The table rows use data.map with key={idx}, which is unstable; update the key to use a stable unique identifier from each row (e.g., key={row.date} or key={row.id}) in the PlatformDetailTable component so React can correctly reconcile rows when order changes; if neither date nor id exists, add or compute a stable id on the row objects before mapping and use that instead.src/pages/dashboard/platform/PlatformDashboard.tsx (1)
76-89: ⚡ Quick win텍스트/아이콘 색상에 surface 토큰 대신 text 역할 토큰을 사용해주세요.
PR 본문에서 "텍스트 색은 역할 토큰(text-text-*)로 통일"한다고 정리하셨는데, 여기서는 선택된 상태(primary 배경 위)일 때
text-surface-100을 텍스트/아이콘 색으로 쓰고 있어요. surface 계열은 배경 역할 토큰이라 의미상 어긋나고, 향후 토큰 리네이밍/리매핑 시 가독성이 깨질 위험이 있습니다. primary 배경 위의 전경색 역할을 담당하는 토큰(예:text-text-on-primary같은 역할 토큰)을 새로 정의하거나 기존 역할 토큰으로 통일하시는 걸 권장드립니다.As per coding guidelines: "Use only
@themetokens fromtokens.cssfor colors. Do not rename tokens or add arbitrary colors." 그리고 PR에서 "텍스트 색은 역할 토큰(text-text-*) 통일" 원칙을 따르도록 정리되어 있습니다.🤖 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/platform/PlatformDashboard.tsx` around lines 76 - 89, The span and ChevronDownIcon use the background/token `text-surface-100` for the selected state; change these to the proper text role token (e.g., `text-text-on-primary` or the existing `text-text-*` token used for foregrounds) so colors use only `@theme` role tokens; update the twMerge className entries in PlatformDashboard (the span rendering selectedPlatform and the ChevronDownIcon) to replace `text-surface-100` with the appropriate `text-text-*` role token consistent with other components and token.css.src/components/sidebar/Sidebar.tsx (1)
29-32: ⚡ Quick win활성 상태 텍스트 색에 surface 토큰 사용 — 역할 토큰 권장.
text-surface-100은 의미상 배경/표면용 토큰입니다. PR에서 정리한 토큰 원칙(텍스트는text-text-*역할 토큰)에 맞춰 primary 배경 위 전경색용 역할 토큰(예:text-text-on-primary등)을 도입하시는 게 일관성 측면에서 더 좋아 보입니다. PlatformDashboard에서 동일한 사용이 있어 함께 정리하면 유지보수가 편할 것 같아요.As per coding guidelines: "Use only
@themetokens fromtokens.cssfor colors. Do not rename tokens or add arbitrary colors."🤖 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/sidebar/Sidebar.tsx` around lines 29 - 32, The active-state class in Sidebar (the ternary that returns "bg-primary-400 text-surface-100") uses a surface token for foreground; replace "text-surface-100" with the correct role/text token from tokens.css (e.g., the project’s on-primary text token such as "text-text-on-primary" or the established equivalent) so that active items use a text-role token instead of a surface token; ensure you import/use only existing `@theme` tokens defined in tokens.css and do not rename or add new arbitrary color tokens; apply the same replacement pattern where PlatformDashboard uses "text-surface-100" to keep consistency.src/pages/ads/list/CampaignDetail.tsx (1)
143-153: 💤 Low value
openAdPauseModal/openAdResumeModal은useCallback으로 감싸는 걸 권장드립니다.매 렌더마다 새 참조로 생성되어 하위
Button에 prop으로 전달됩니다. 지금은 큰 부담은 없지만, 동일 파일 내 다른 핸들러들(toggleAd,toggleSelectAllVisible,clearAdSelection)은useCallback으로 정리되어 있어 일관성도 함께 챙기실 수 있습니다.As per coding guidelines: "Hook 사용: ... useCallback, useMemo의 적절한 사용."
🤖 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/ads/list/CampaignDetail.tsx` around lines 143 - 153, Wrap openAdPauseModal and openAdResumeModal in useCallback to avoid recreating handlers each render; implement openAdPauseModal with useCallback(() => { const scope = selectedOngoingIds.length > 0 ? "selection" : "all"; setPauseScope(scope); bulkAdPause.openModal(); }, [selectedOngoingIds, setPauseScope, bulkAdPause]) and implement openAdResumeModal with useCallback(() => { const scope = selectedPausedIds.length > 0 ? "selection" : "all"; setResumeScope(scope); bulkAdResume.openModal(); }, [selectedPausedIds, setResumeScope, bulkAdResume]); ensure you include the referenced symbols (selectedOngoingIds, selectedPausedIds, setPauseScope, setResumeScope, bulkAdPause, bulkAdResume) in the dependency arrays.src/layout/main/MainLayout.tsx (2)
81-94: 💤 Low valueads 캠페인 상세 경로 정규식이 두 번 반복됩니다.
/^\/ads\/[^/]+\/[^/]+$/패턴이 82번 줄과 94번 줄에 동일하게 사용되고 있습니다. 정의 위치를 한 곳으로 모으거나 이미 계산해 둔isAdsCampaignDetailPath를 useMemo 내부에서 그대로 분기 조건으로 사용하시면 정의가 한 곳에 모여 추후 경로 규칙 변경 시 누락 위험이 줄어듭니다.♻️ 제안 패치
- const { parentLabel, currentLabel, parentTo, currentTo } = useMemo(() => { - const path = pathname; - if (/^\/ads\/[^/]+\/[^/]+$/.test(path)) { + const { parentLabel, currentLabel, parentTo, currentTo } = useMemo(() => { + const path = pathname; + if (isAdsCampaignDetailPath) { return { parentLabel: "", currentLabel: "", parentTo: null as string | null, currentTo: null as string | null, }; } - }, [pathname, navForHeader]); + }, [pathname, navForHeader, isAdsCampaignDetailPath]);🤖 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/layout/main/MainLayout.tsx` around lines 81 - 94, The regex /^\/ads\/[^/]+\/[^/]+$/ is duplicated; replace the second inline test inside the useMemo that computes { parentLabel, currentLabel, parentTo, currentTo } with the already computed isAdsCampaignDetailPath value, and centralize the regex only where isAdsCampaignDetailPath is defined (the useMemo that depends on pathname); update the conditional in that useMemo to use isAdsCampaignDetailPath instead of re-evaluating the regex and leave setCampaignDetailHeaderTitle logic unchanged so all path checks reference the single isAdsCampaignDetailPath symbol.
146-197: 🏗️ Heavy lift브레드크럼 영역에 시맨틱/ARIA 보강을 권장드립니다.
지금 구조는
<div>안에<Link>/<span>들이 슬래시 구분자로 나열되어 있어서 스크린리더 사용자에게 "브레드크럼"이라는 의미가 전달되지 않습니다.<nav aria-label="breadcrumb">+<ol>/<li>구조로 감싸고, 현재 페이지에 해당하는 마지막 항목에는aria-current="page"를 주시는 게 좋습니다. 슬래시 구분자에aria-hidden처리하신 부분은 이미 좋은 처리이지만, 컨테이너 의미가 빠져 있어 보완이 필요합니다.As per coding guidelines: "접근성: 시맨틱 HTML, ARIA 속성 사용 확인."
🤖 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/layout/main/MainLayout.tsx` around lines 146 - 197, Wrap the breadcrumb markup in MainLayout with a semantic landmark by replacing the current plain div container with a <nav aria-label="breadcrumb"> that contains an ordered list (<ol>) of list items (<li>); render each crumb (the links rendered via Link for parentTo/currentTo and the plain spans like parentLabel/currentLabel or the ads-specific items using isAdsCampaignDetailPath and campaignDetailHeaderTitle) as an li, keep the slash separators aria-hidden, and add aria-current="page" to the final/current breadcrumb item (the element that currently renders currentLabel || parentLabel || " " or campaignDetailHeaderTitle) so screen readers recognize the current page; preserve existing class names like crumbLinkBody and crumbLinkTitle and behavior of parentTo/currentTo when converting their wrappers to list items.src/utils/navigation/workspaceNavPaths.ts (1)
32-34: ⚡ Quick win
workspaceSubpath를 정규식에 그대로 보간하기보다 문자열 비교로 처리하시면 더 안전하고 명확합니다.지금은
workspaceSubpath가 sidebarNav 상수("settings","members","billing")에서만 오기 때문에 실제 ReDoS나 잘못된 매칭 위험은 사실상 없지만, 정규식 메타문자가 섞일 여지가 생기면 의도치 않은 매칭으로 이어질 수 있습니다. 호출 빈도(렌더마다 매칭 검사)를 고려하면 단순 문자열 분해가 가독성·성능 모두 유리합니다.♻️ 제안 패치 (문자열 매칭)
- if (item.workspaceSubpath) { - return new RegExp(`^/workspace/[^/]+/${item.workspaceSubpath}$`).test(norm); - } + if (item.workspaceSubpath) { + const segments = norm.split("/").filter(Boolean); + return ( + segments.length === 3 && + segments[0] === "workspace" && + segments[2] === item.workspaceSubpath + ); + }🤖 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/workspaceNavPaths.ts` around lines 32 - 34, The current code builds a RegExp with item.workspaceSubpath and tests norm, which risks accidental regex meta-character interpretation and is less efficient; change the logic that handles item.workspaceSubpath to perform a direct string comparison instead (e.g., normalize and split norm by '/' or verify it startsWith '/workspace/' + workspaceId + '/' then compare the trailing segment to item.workspaceSubpath) so that workspaceSubpath is matched exactly without constructing a RegExp; update the branch that checks item.workspaceSubpath to use norm and item.workspaceSubpath string operations (referencing item.workspaceSubpath and norm) and return the boolean result of that string equality check.
🤖 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/common/card/Card.stories.tsx`:
- Line 23: The Storybook examples in Card.stories.tsx use the auth-specific
token `text-text-auth-sub`; update those instances (the <p> elements at the
occurrences referenced) to a consistent body token used by the project such as
`text-text-title` for primary card content or `text-text-muted` for
descriptive/subtle text to match Card.tsx (description uses `text-text-muted`)
and other components like InfoCard/TextareaField; replace `text-text-auth-sub`
with the chosen project-wide token across all occurrences in Card.stories.tsx.
In `@src/components/common/modal/ModalContent.tsx`:
- Around line 151-156: The Tailwind important-prefix syntax used in the
ModalContent component's className (inside the twMerge call) uses deprecated
leading "!" tokens (e.g., "!cursor-not-allowed", "!border", "!bg-info-red/12") —
update those to the Tailwind v4 suffix syntax by moving the "!" to the end of
each utility (e.g., "cursor-not-allowed!", "border!", "bg-info-red/12!") for all
occurrences within the conditional expression that uses showMutedDangerConfirm
and variant === "danger" so the utilities remain functionally important while
matching v4.1.18 syntax.
In `@src/components/common/select/SearchSelect.tsx`:
- Line 110: SearchSelect 컴포넌트의 입력 엘리먼트(className 문자열)에서 현재 outline-none과 기본/포커스
동일한 border-info-blue 조합으로 포커스 가시성이 사라지므로, focus-visible 기준의 명확한 시각 피드백을 추가해 주세요:
입력에 focus-visible:ring-2 (또는 적정 굵기)과 focus-visible:ring-info-blue(또는 더 진한
shade), focus-visible:ring-offset-2 등 링/오프셋 클래스를 추가하고 포커스 상태에서 border 색상을 더 명확히
바꾸도록 focus:border-info-blue-600(또는 적절한 대비 색) 같은 클래스를 추가해 키보드 포커스가 시각적으로 명확히
구분되도록 수정하세요 (대상 식별자: SearchSelect 컴포넌트의 해당 input/element의 className).
In `@src/components/dashboard/charts/performanceEfficiencyChart.config.ts`:
- Line 18: Replace all hardcoded hex color literals in this chart config with
the design token references: swap strokeColors, the series color array (`#0084fe`,
`#2f5bea`, `#7a9bf8`) and the other hex values referenced in this file to the
appropriate `@theme` tokens (e.g., primary-*, neutral-*, etc.). Locate the
properties named strokeColors and the series color array (and any other color
keys in this config) and import/consume the theme tokens or token helper used
across the codebase, then map each hex to the matching token (map the three
brand-like series colors to primary-* tokens). Ensure every color value in this
config uses the token reference instead of literal hex strings.
In `@src/components/dashboard/platform/AllPlatformTrafficChart.tsx`:
- Line 108: The chart component AllPlatformTrafficChart is using hardcoded hex
colors in inline style (e.g., style: { colors: "#9ca3af", fontSize: "12px" }) —
replace those hex values with the corresponding `@theme` tokens from tokens.css
(use the same token names used in PlatformTrafficChart for consistency, e.g.,
the secondary text and background tokens) and update any other occurrences in
this file (the other color uses referenced in the review) so all color values
come from `@theme` tokens; ensure you update the inline style entries and any
chart options/constants inside the AllPlatformTrafficChart component to
reference the token variables instead of literal hex strings.
In `@src/components/dashboard/platform/PlatformTrafficChart.tsx`:
- Line 103: Replace hardcoded hex color values used in the ApexCharts
configuration with CSS design tokens from tokens.css (use the `@theme` CSS
variables) instead of literal strings; locate the chart options / style objects
in PlatformTrafficChart.tsx (e.g., the style: { colors: "#9ca3af", fontSize:
"12px" } and other similar entries around the chartOptions/annotations/series
config referenced at lines ~103, ~114, ~124) and swap each "#9ca3af" and
"#f4f6fb" for the corresponding tokens.css variables (as CSS variable strings
acceptable to ApexCharts), keeping fontSize unchanged. Ensure all color usages
in functions/components like PlatformTrafficChart and its
chartOptions/annotations are updated to use the `@theme` tokens.
In `@src/components/landing/GuideTimeline.tsx`:
- Around line 50-64: The two inert controls (the button elements with
aria-label="이전 기간" and aria-label="다음 기간" that render ChevronLeftIcon and
ChevronRightIcon in GuideTimeline) must not be focusable interactive elements
when they do nothing; either add the disabled attribute and appropriate disabled
styling/classes and ensure they are non-focusable, or replace them with
non-interactive elements (e.g., span) with the same visual classes and an
appropriate aria-hidden or aria-label if needed. Update the elements that
reference ChevronLeftIcon and ChevronRightIcon accordingly so keyboard and
screen-reader users aren't presented with focusable, non-functional buttons.
In `@src/components/landing/LandingMultiDevice.tsx`:
- Around line 11-12: MOCKUP_OVERLAY_CLASS uses hardcoded colors ("from-black/70
via-black/15")—replace them with theme tokens by referencing the tokens system:
add a new overlay token (e.g., --color-overlay-dark) to tokens.css under `@theme`
or reuse an existing token like --color-text-400, then update
MOCKUP_OVERLAY_CLASS to use the token-based gradient stops (instead of
from-black/70 via-black/15) so the overlay gradient uses the project tokens
(reference symbol: MOCKUP_OVERLAY_CLASS and tokens.css/@theme).
In `@src/index.css`:
- Around line 5-8: The `@import` rules are placed after a `@custom-variant`,
violating the no-invalid-position-at-import-rule; move all `@import` statements
(e.g., `@import` "./styles/tokens.css"; `@import` "./styles/base.css"; `@import`
"./styles/utilities.css"; `@import` "./styles/print.css";) so they appear before
any `@custom-variant` declarations in the file (ensure no other at-rules precede
them) to comply with the CSS spec and the linter rule.
In `@src/layout/workspace/WorkspaceManageLayout.tsx`:
- Around line 11-13: The current parse of workspaceId in
WorkspaceManageLayout.tsx converts workspaceId to Number and only checks
Number.isFinite(id) && id > 0, which allows non-integer values like "1.5";
update the validation to ensure the parsed id is an integer (use
Number.isInteger on the parsed id) before calling setSelectedOrgId so only
positive integer org IDs are stored and used for path construction (validate
workspaceId -> id, then call setSelectedOrgId(id) only if integer && > 0).
In `@src/pages/ads/list/AdsListPage.tsx`:
- Around line 248-251: The early-return on missing orgId inside the bulk action
confirmation callback can make the confirm Promise resolve as success; in
bulkStop.handleConfirm (and the similar block at the other bulk action around
pauseScope handling) replace the silent `if (!orgId) return;` with an explicit
failure — either throw a descriptive Error (e.g., `throw new Error("orgId
missing")`) or reject the Promise so the confirm flow treats it as an error and
prevents success toasts/onSuccess; alternatively, block opening the modal or
disable the confirm button when `orgId` is falsy, but do not silently return
inside the async callback.
In `@src/pages/ads/list/CampaignDetail.tsx`:
- Around line 360-374: The current bulk pause flow inside
bulkAdPause.handleConfirm (using Promise.all over ids derived from pauseScope /
adsList / selectedOngoingIds and calling updateAdStatus) can leave server/UI out
of sync when one or more requests fail; replace Promise.all with
Promise.allSettled, count successes vs failures from the settled results, show
appropriate success/error toast/messages based on counts, and ensure refetchAds
is always invoked (in a finally or onSettled path) so UI is synchronized; apply
the same change to the corresponding resume modal flow (the similar block around
lines 401–420 that performs resume updates).
In `@src/pages/auth/RedirectPage.tsx`:
- Around line 44-45: Update the loading text container in RedirectPage (the div
that currently renders "로그인 중...") to expose its loading status to assistive
tech by adding role="status" and aria-live="polite" to that element, and if
there is a surrounding wrapper representing the ongoing operation (e.g., the
RedirectPage component root or a parent container), add aria-busy={true} while
the redirect/login is in progress; ensure these attributes are present on the
same JSX elements referenced in RedirectPage.tsx so screen readers reliably
announce the loading state.
In `@src/stories/Typography.stories.tsx`:
- Around line 170-171: Replace direct Tailwind gray color classes used in the
JSX (e.g., the div with className "grid ... border-b border-gray-200 ...
text-gray-400" and any other occurrences noted) with the project theme token
utility classes (for example use text-text-* and border-surface-* equivalents
from `@theme/tokens` instead of text-gray-* and border-gray-*). Locate the
affected JSX elements in Typography.stories.tsx (the grid header div and the
other story elements around the listed ranges) and swap every text-gray-* and
border-gray-* usage to the corresponding token utility class name, keeping the
rest of the className string intact and ensuring no new token names are
invented.
In `@src/styles/base.css`:
- Line 2: Remove the unnecessary quotes around the single-word font name in the
font-family declarations (e.g., replace the rule using "Pretendard" with
Pretendard) to satisfy Stylelint; update all occurrences including the other
instance noted (line 9) so every font-family: "Pretendard"; becomes font-family:
Pretendard;.
In `@src/styles/utilities.css`:
- Around line 59-73: Replace hardcoded RGBA values in the shadow utility classes
by using theme tokens with color-mix and CSS variables: update .shadow-Soft,
.shadow-Medium, .shadow-card and .shadow-card-hover (and the occurrences around
lines referenced) to derive the shadow color from var(--color-*) tokens via
color-mix() instead of literal rgba(...) so the project uses tokens from
tokens.css; ensure you pick the appropriate token (e.g.,
--color-foreground/--color-black or the designated shadow token) and apply
color-mix(<token> 10% , transparent) or equivalent percentages to match the
original opacities, keeping class names unchanged.
---
Outside diff comments:
In `@src/components/ads/AdDetailContent.tsx`:
- Around line 174-183: The call uses Number(orgId) directly which can produce
NaN if the route param is missing; before invoking createTrackingUrl inside
trackControl.handleConfirm (and before using ad.landingUrl), validate orgId by
parsing it (e.g., parseInt or Number) and ensure it's a finite integer (not NaN)
— if invalid, show a user-facing error (toast) and abort (throw or return) so
createTrackingUrl(orgId, ad.id, ad.landingUrl) is never called with NaN; update
the block around trackControl.handleConfirm and createTrackingUrl to perform
this check and early-exit on invalid orgId.
In `@src/components/dashboard/charts/AdStatusChart.tsx`:
- Around line 5-9: PLATFORM_COLORS currently hardcodes hex values; replace those
hex strings with the corresponding `@theme` color tokens from tokens.css (e.g. use
the CSS variable form from `@theme` like "var(--theme-...)" or the project's token
accessor) so the chart uses design tokens instead of raw hex; update the
PLATFORM_COLORS constant in AdStatusChart.tsx (and the other similar entries
noted around lines 39-42) to map GOOGLE, NAVER, META to their respective `@theme`
token names found in tokens.css and ensure any usage of PLATFORM_COLORS in the
component continues to accept the token string values.
In `@src/components/setting/PasswordSection.tsx`:
- Around line 87-90: The aria attributes for the new-password toggle are
incorrectly using showCurrent; update the toggle in PasswordSection (the
new-password visibility button that currently references showCurrent) to
reference showNew instead so aria-label values ("새 비밀번호 숨기기"/"새 비밀번호 보기") and
aria-pressed reflect the new-password visibility state; ensure the button that
toggles new password visibility also uses the showNew state in its onClick
handler if not already wired.
---
Nitpick comments:
In `@src/components/ads/CampaignRow.tsx`:
- Around line 59-72: Remove the unnecessary role="presentation" from the div
that wraps the checkbox in CampaignRow (the container currently around the
<input type="checkbox"> using isSelected, onToggleSelect and name props); since
the input is an interactive, semantic element, simply omit the role on that
wrapper (leave the div for layout and keep the onClick/onKeyDown stopPropagation
handlers and the input's checked/onChange/aria-label intact) so screen readers
receive the checkbox semantics from the input itself.
In `@src/components/common/dropdownmenu/DropdownMenu.tsx`:
- Around line 58-74: The trigger wrapper div in DropdownMenu lacks keyboard
focusability; update the element (the div inside the DropdownMenu component that
currently has role="button" and onKeyDown/onClick handlers) to include
tabIndex={0} so keyboard-only users can focus it, keeping the existing onKeyDown
handling for "Enter" and " " to toggle setOpen and leaving aria attributes
(aria-haspopup, aria-expanded, aria-controls, aria-label) intact; ensure any
relevant tests or accessibility docs reference the change.
In `@src/components/common/modal/Modal.tsx`:
- Around line 53-58: The useLayoutEffect currently wraps
modalRef.current?.focus() in requestAnimationFrame/cancelAnimationFrame; remove
the rAF logic and call modalRef.current?.focus() synchronously inside
useLayoutEffect when isOpen is true, keeping previousActiveElement.current
assignment intact; update references to requestAnimationFrame and
cancelAnimationFrame (remove them) and ensure modalRef, previousActiveElement,
and the useLayoutEffect callback are the only items touched.
In `@src/components/dashboard/platform/PlatformDetailTable.tsx`:
- Around line 99-103: The table rows use data.map with key={idx}, which is
unstable; update the key to use a stable unique identifier from each row (e.g.,
key={row.date} or key={row.id}) in the PlatformDetailTable component so React
can correctly reconcile rows when order changes; if neither date nor id exists,
add or compute a stable id on the row objects before mapping and use that
instead.
In `@src/components/landing/LandingFooter.tsx`:
- Around line 16-33: LandingFooter contains three anchor elements that repeat
the same hover/focus class string; extract that repeated class string into a
single constant (e.g., linkClass or LINK_STYLE) at the top of the LandingFooter
component and replace the className values of the anchors (the links for "이용약관",
"개인정보처리방침", and "고객센터") with that constant to remove duplication and improve
maintainability.
- Line 11: The footer row currently uses a surface color token in the className
("flex items-center gap-8 font-body2 text-surface-500") inside
LandingFooter.tsx; change that to a text role token (e.g., text-text-sub or
text-text-muted) so it follows the design system guideline "텍스트 색은 역할
토큰(text-text-*) 통일" — update the className on the element that contains
text-surface-500 to use the chosen text-text-* token and run a quick visual
check to confirm contrast/appearance.
In `@src/components/setting/ProfileSection.tsx`:
- Around line 116-122: Remove the redundant disabled prop from the Input usage
and leave the field as read-only only: update the Input component instances
(e.g., the one rendering value={`${org.name} (${org.position})`} and the
similar blocks referenced at the other occurrences) to drop disabled={true} and
keep readOnly (and existing containerClassName) so the field remains
non-editable but still focusable and included in form submission.
In `@src/components/sidebar/Sidebar.tsx`:
- Around line 29-32: The active-state class in Sidebar (the ternary that returns
"bg-primary-400 text-surface-100") uses a surface token for foreground; replace
"text-surface-100" with the correct role/text token from tokens.css (e.g., the
project’s on-primary text token such as "text-text-on-primary" or the
established equivalent) so that active items use a text-role token instead of a
surface token; ensure you import/use only existing `@theme` tokens defined in
tokens.css and do not rename or add new arbitrary color tokens; apply the same
replacement pattern where PlatformDashboard uses "text-surface-100" to keep
consistency.
In `@src/hooks/dashboard/useOverviewCampaignList.ts`:
- Line 14: 현재 fetcher에 orgId! 비무효(assertion) 사용이 중복되어 있어 깔끔하게 바꿀 수 있습니다;
getCampaignList 호출부를 수정하여 orgId가 있을 때만 getCampaignList(orgId)를 호출하고 그렇지 않으면 빈
배열을 반환하는 Promise를 반환하도록 변경하세요(참조: fetcher 람다, getCampaignList, orgId, enabled:
!!orgId).
In `@src/layout/main/MainLayout.tsx`:
- Around line 81-94: The regex /^\/ads\/[^/]+\/[^/]+$/ is duplicated; replace
the second inline test inside the useMemo that computes { parentLabel,
currentLabel, parentTo, currentTo } with the already computed
isAdsCampaignDetailPath value, and centralize the regex only where
isAdsCampaignDetailPath is defined (the useMemo that depends on pathname);
update the conditional in that useMemo to use isAdsCampaignDetailPath instead of
re-evaluating the regex and leave setCampaignDetailHeaderTitle logic unchanged
so all path checks reference the single isAdsCampaignDetailPath symbol.
- Around line 146-197: Wrap the breadcrumb markup in MainLayout with a semantic
landmark by replacing the current plain div container with a <nav
aria-label="breadcrumb"> that contains an ordered list (<ol>) of list items
(<li>); render each crumb (the links rendered via Link for parentTo/currentTo
and the plain spans like parentLabel/currentLabel or the ads-specific items
using isAdsCampaignDetailPath and campaignDetailHeaderTitle) as an li, keep the
slash separators aria-hidden, and add aria-current="page" to the final/current
breadcrumb item (the element that currently renders currentLabel || parentLabel
|| " " or campaignDetailHeaderTitle) so screen readers recognize the current
page; preserve existing class names like crumbLinkBody and crumbLinkTitle and
behavior of parentTo/currentTo when converting their wrappers to list items.
In `@src/pages/ads/list/CampaignDetail.tsx`:
- Around line 143-153: Wrap openAdPauseModal and openAdResumeModal in
useCallback to avoid recreating handlers each render; implement openAdPauseModal
with useCallback(() => { const scope = selectedOngoingIds.length > 0 ?
"selection" : "all"; setPauseScope(scope); bulkAdPause.openModal(); },
[selectedOngoingIds, setPauseScope, bulkAdPause]) and implement
openAdResumeModal with useCallback(() => { const scope =
selectedPausedIds.length > 0 ? "selection" : "all"; setResumeScope(scope);
bulkAdResume.openModal(); }, [selectedPausedIds, setResumeScope, bulkAdResume]);
ensure you include the referenced symbols (selectedOngoingIds,
selectedPausedIds, setPauseScope, setResumeScope, bulkAdPause, bulkAdResume) in
the dependency arrays.
In `@src/pages/dashboard/platform/PlatformDashboard.tsx`:
- Around line 76-89: The span and ChevronDownIcon use the background/token
`text-surface-100` for the selected state; change these to the proper text role
token (e.g., `text-text-on-primary` or the existing `text-text-*` token used for
foregrounds) so colors use only `@theme` role tokens; update the twMerge className
entries in PlatformDashboard (the span rendering selectedPlatform and the
ChevronDownIcon) to replace `text-surface-100` with the appropriate
`text-text-*` role token consistent with other components and token.css.
In `@src/stories/Colors.stories.tsx`:
- Around line 10-61: colorSections is hardcoded and can drift from tokens.css;
replace the manual array with an automated generator that parses tokens.css (or
an exported tokens JSON) and builds the same structure. Implement a helper like
generateColorSectionsFromTokens()/parseCssVariables() that reads CSS variable
names (e.g., --primary-*, --color-surface-*, --color-text-*) and groups them
into section objects matching the shape of colorSections, then export/use that
generator in Colors.stories.tsx instead of the static colorSections constant so
the story always reflects tokens.css changes.
In `@src/utils/navigation/workspaceNavPaths.ts`:
- Around line 32-34: The current code builds a RegExp with item.workspaceSubpath
and tests norm, which risks accidental regex meta-character interpretation and
is less efficient; change the logic that handles item.workspaceSubpath to
perform a direct string comparison instead (e.g., normalize and split norm by
'/' or verify it startsWith '/workspace/' + workspaceId + '/' then compare the
trailing segment to item.workspaceSubpath) so that workspaceSubpath is matched
exactly without constructing a RegExp; update the branch that checks
item.workspaceSubpath to use norm and item.workspaceSubpath string operations
(referencing item.workspaceSubpath and norm) and return the boolean result of
that string equality check.
🪄 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: 524ca632-480c-426d-9400-934542da4bc1
⛔ Files ignored due to path filters (15)
.cursor/rules/private-always.mdcis excluded by none and included by none.gitignoreis excluded by none and included by noneeslint/import.mjsis excluded by none and included by nonepackage-lock.jsonis excluded by!**/package-lock.jsonand included by nonepackage.jsonis excluded by none and included by nonesrc/assets/icon/ai/ai-요약버튼.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/camera.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/eye-off.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/common/eye.svgis excluded by!**/*.svgand included bysrc/**src/assets/icon/sidebar/logout.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/service-logo/logo.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/service-logo/symbol-white.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/social-logo/circle/kakao-circle.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/social-logo/plain/google.svgis excluded by!**/*.svgand included bysrc/**src/assets/logo/social-logo/plain/kakao.svgis excluded by!**/*.svgand included bysrc/**
📒 Files selected for processing (135)
src/components/ads/AdDetailContent.tsxsrc/components/ads/AdListTable.tsxsrc/components/ads/AdRow.tsxsrc/components/ads/CampaignInfoCard.tsxsrc/components/ads/CampaignPlatformDropdown.tsxsrc/components/ads/CampaignRow.tsxsrc/components/ads/CampaignTable.tsxsrc/components/ads/PlatformCard.tsxsrc/components/auth/common/CommonAuthInput.tsxsrc/components/auth/common/InputActions.tsxsrc/components/auth/common/PasswordForm.tsxsrc/components/auth/flows/find-email/EnterPhoneStep.tsxsrc/components/auth/flows/find-email/ShowEmailResultStep.tsxsrc/components/auth/flows/reset-password/EmailVerificationStep.tsxsrc/components/auth/flows/signup/EnterEmailStep.tsxsrc/components/auth/flows/signup/ProfileSetupStep.tsxsrc/components/auth/skeleton/AuthFormSkeleton.tsxsrc/components/auth/skeleton/LoginPageSkeleton.tsxsrc/components/auth/skeleton/SignupEmailStepSkeleton.tsxsrc/components/auth/skeleton/SignupPageSkeleton.tsxsrc/components/common/ComingSoonPlaceholder.tsxsrc/components/common/alert/Alert.tsxsrc/components/common/badge/Badge.stories.tsxsrc/components/common/badge/Badge.tsxsrc/components/common/button/Button.tsxsrc/components/common/card/Card.stories.tsxsrc/components/common/card/Card.tsxsrc/components/common/card/InfoCard.stories.tsxsrc/components/common/card/InfoCard.tsxsrc/components/common/card/StatCard.stories.tsxsrc/components/common/card/StatCard.tsxsrc/components/common/chart/ChartLegend.tsxsrc/components/common/controlbox/ControlBox.stories.tsxsrc/components/common/controlbox/ControlBox.tsxsrc/components/common/drawer/Drawer.tsxsrc/components/common/dropdownmenu/DropdownMenu.tsxsrc/components/common/input/Input.stories.tsxsrc/components/common/input/Input.tsxsrc/components/common/modal/Modal.stories.tsxsrc/components/common/modal/Modal.tsxsrc/components/common/modal/ModalContent.tsxsrc/components/common/progressbar/ProgressBar.tsxsrc/components/common/select/DropdownSelect.tsxsrc/components/common/select/SearchSelect.tsxsrc/components/common/skeleton/Skeleton.tsxsrc/components/common/textarea/TextareaField.tsxsrc/components/common/toggle/Toggle.tsxsrc/components/dashboard/charts/AdStatusChart.tsxsrc/components/dashboard/charts/BudgetGaugeChart.tsxsrc/components/dashboard/charts/TrafficChart.tsxsrc/components/dashboard/charts/performanceEfficiencyChart.config.tssrc/components/dashboard/platform/AllPlatformTrafficChart.tsxsrc/components/dashboard/platform/AllPlatformView.tsxsrc/components/dashboard/platform/PlatformDetailCard.tsxsrc/components/dashboard/platform/PlatformDetailTable.tsxsrc/components/dashboard/platform/PlatformRoasTable.tsxsrc/components/dashboard/platform/PlatformTrafficChart.tsxsrc/components/dashboard/platform/SinglePlatformView.tsxsrc/components/dashboard/platform/TopPerformanceList.tsxsrc/components/dashboard/platform/skeleton/PlatformSkeleton.tsxsrc/components/landing/GuideOverviewChart.tsxsrc/components/landing/GuidePlatform.tsxsrc/components/landing/GuideTimeline.tsxsrc/components/landing/LandingFAQ.tsxsrc/components/landing/LandingFeatures.tsxsrc/components/landing/LandingFooter.tsxsrc/components/landing/LandingGuide.tsxsrc/components/landing/LandingHeader.tsxsrc/components/landing/LandingHero.tsxsrc/components/landing/LandingMultiDevice.tsxsrc/components/landing/LandingPricing.tsxsrc/components/landing/LandingSectionHeader.tsxsrc/components/modal/privacyModal/AgreementItem.tsxsrc/components/modal/privacyModal/PrivacyModal.tsxsrc/components/setting/PasswordSection.tsxsrc/components/setting/ProfileSection.tsxsrc/components/sidebar/LogoutConfirmModal.tsxsrc/components/sidebar/Sidebar.tsxsrc/components/sidebar/SubMenu.tsxsrc/components/sidebar/WorkspaceSwitcher.tsxsrc/components/workspace/DeleteMemberModal.tsxsrc/components/workspace/InviteMemberModal.tsxsrc/components/workspace/MemberItem.tsxsrc/components/workspace/MemberList.tsxsrc/components/workspace/MemberManagementLoading.tsxsrc/components/workspace/MemberRoleSelect.tsxsrc/components/workspace/MemberSearchSelect.tsxsrc/components/workspace/PermissionTable.tsxsrc/components/workspace/WorkspaceCard.tsxsrc/components/workspace/WorkspaceEmptyState.tsxsrc/components/workspace/WorkspaceListError.tsxsrc/components/workspace/WorkspaceListLoading.tsxsrc/components/workspace/WorkspaceSettingLoading.tsxsrc/constants/landing/overviewChart.tssrc/constants/landing/timeline.tssrc/constants/sidebarNav.tssrc/hooks/ads/useAdList.tssrc/hooks/ads/useCampaignGroup.tssrc/hooks/auth/useLogout.tssrc/hooks/dashboard/useOverviewCampaignList.tssrc/index.csssrc/layout/auth/AuthLayout.tsxsrc/layout/main/MainLayout.tsxsrc/layout/workspace/WorkspaceManageLayout.tsxsrc/pages/ads/list/AdsListPage.tsxsrc/pages/ads/list/CampaignDetail.tsxsrc/pages/ads/new/CampaignGroup.tsxsrc/pages/auth/Login.tsxsrc/pages/auth/RedirectPage.tsxsrc/pages/auth/Signup.tsxsrc/pages/dashboard/overview/OverviewAiDrawer.tsxsrc/pages/dashboard/overview/OverviewAiReportPanel.tsxsrc/pages/dashboard/overview/OverviewBudgetSection.tsxsrc/pages/dashboard/overview/OverviewCampaignSnapshotCard.tsxsrc/pages/dashboard/overview/OverviewDashboard.tsxsrc/pages/dashboard/overview/OverviewKpiSection.tsxsrc/pages/dashboard/overview/OverviewPlatformSection.tsxsrc/pages/dashboard/platform/PlatformDashboard.tsxsrc/pages/landing/LandingPage.tsxsrc/pages/workspace/MemberManagement.tsxsrc/pages/workspace/Workspace.tsxsrc/pages/workspace/WorkspaceBillingRedirect.tsxsrc/pages/workspace/WorkspaceSetting.tsxsrc/routes/MainRoutes.tsxsrc/stories/BorderRadius.stories.tsxsrc/stories/Colors.stories.tsxsrc/stories/Typography.stories.tsxsrc/styles/base.csssrc/styles/print.csssrc/styles/tokens.csssrc/styles/utilities.csssrc/types/ads/campaign.tssrc/types/navigation/navItem.tssrc/utils/navigation/mainNavSidebar.tssrc/utils/navigation/workspaceNavPaths.ts
💤 Files with no reviewable changes (5)
- src/components/sidebar/LogoutConfirmModal.tsx
- src/components/common/select/DropdownSelect.tsx
- src/components/common/controlbox/ControlBox.stories.tsx
- src/hooks/auth/useLogout.ts
- src/components/common/controlbox/ControlBox.tsx
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/components/dashboard/platform/AllPlatformTrafficChart.tsx (1)
13-17: 💤 Low value플랫폼 브랜드 컬러도 토큰화를 고려해보면 좋을 것 같아요.
코딩 가이드라인에 따르면
@theme토큰만 사용하도록 되어 있는데,PLATFORM_COLORS객체는 여전히 하드코딩된 hex 값을 사용하고 있습니다.Google, Naver, Meta의 브랜드 컬러가 다른 곳에서도 재사용된다면
tokens.css에 정의하는 것이 일관성 유지에 도움이 될 것 같습니다. 예를 들어:
--color-platform-google--color-platform-naver--color-platform-meta다만 이 컬러들이 이 컴포넌트에서만 사용되고 디자인 시스템의 일부가 아니라면 현재 구조를 유지하는 것도 합리적입니다.
🤖 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/AllPlatformTrafficChart.tsx` around lines 13 - 17, PLATFORM_COLORS in AllPlatformTrafficChart.tsx is using hardcoded hex values; move these into your design tokens and reference them instead: add CSS variables in tokens.css like --color-platform-google, --color-platform-naver, --color-platform-meta with the current hex values, then replace usages of PLATFORM_COLORS (and any direct hex literals) in the AllPlatformTrafficChart component to read from the theme tokens (e.g., via var(--color-platform-google)) or the `@theme` token accessor your codebase uses; if these colors are truly component-scoped and not shared, keep PLATFORM_COLORS, otherwise remove the hardcoded object and use the new tokens.
🤖 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.
Nitpick comments:
In `@src/components/dashboard/platform/AllPlatformTrafficChart.tsx`:
- Around line 13-17: PLATFORM_COLORS in AllPlatformTrafficChart.tsx is using
hardcoded hex values; move these into your design tokens and reference them
instead: add CSS variables in tokens.css like --color-platform-google,
--color-platform-naver, --color-platform-meta with the current hex values, then
replace usages of PLATFORM_COLORS (and any direct hex literals) in the
AllPlatformTrafficChart component to read from the theme tokens (e.g., via
var(--color-platform-google)) or the `@theme` token accessor your codebase uses;
if these colors are truly component-scoped and not shared, keep PLATFORM_COLORS,
otherwise remove the hardcoded object and use the new tokens.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5a4d01f7-d00f-47ae-afdd-3e275690e298
⛔ Files ignored due to path filters (1)
src/assets/logo/social-logo/circle/kakao-circle.svgis excluded by!**/*.svgand included bysrc/**
📒 Files selected for processing (19)
src/components/ads/AdRow.tsxsrc/components/ads/CampaignRow.tsxsrc/components/ads/PlatformCard.tsxsrc/components/common/button/Button.tsxsrc/components/common/card/Card.stories.tsxsrc/components/common/modal/ModalContent.tsxsrc/components/dashboard/charts/performanceEfficiencyChart.config.tssrc/components/dashboard/charts/trafficChart.config.tssrc/components/dashboard/platform/AllPlatformTrafficChart.tsxsrc/components/dashboard/platform/PlatformTrafficChart.tsxsrc/components/setting/PasswordSectionSkeleton.tsxsrc/components/setting/ProfileSectionSkeleton.tsxsrc/pages/ads/new/CampaignGroup.tsxsrc/pages/auth/Login.tsxsrc/pages/dashboard/platform/PlatformDashboard.tsxsrc/stories/BorderRadius.stories.tsxsrc/stories/Colors.stories.tsxsrc/stories/Shadows.stories.tsxsrc/stories/Typography.stories.tsx
✅ Files skipped from review due to trivial changes (3)
- src/components/setting/PasswordSectionSkeleton.tsx
- src/components/setting/ProfileSectionSkeleton.tsx
- src/components/dashboard/charts/performanceEfficiencyChart.config.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- src/pages/dashboard/platform/PlatformDashboard.tsx
- src/stories/Typography.stories.tsx
- src/stories/Colors.stories.tsx
- src/components/common/button/Button.tsx
- src/components/common/modal/ModalContent.tsx
- src/components/ads/AdRow.tsx
- src/pages/ads/new/CampaignGroup.tsx
📚 Storybook 배포 완료
|
🚨 관련 이슈
#200
✨ 변경사항
✏️ 작업 내용
1. 내비·헤더·카피
캠페인 그룹 정보 설정→ 통합 캠페인 등록Link로 클릭 시 해당 경로 이동 가능하게 개선2. 캠페인·광고 목록 UI - 전체 디자인 수정
3. 워크스페이스
ModalContent+WorkspaceSetting)info-red계열 연한 배경/테두리로 어느 정도 보이게 처리4. 설정 페이지
currentColor로 통일!!! - 이거 적용해달라고 말씀 드렸을텐데…안되어 있더라구요………..5. 공통 컴포넌트·토큰 원칙
tokens.css/@theme에 있는 유틸만 사용 (임의 hex·새 색 추가 하지 마세요)surface-*,text-*,primary-*,info-red등6. 페이지에서 컬러 토큰 쓰는 방법
7. 텍스트 컬러는 역할 토큰 사용
안정, 주의, 위험 의미 담은 그래프에서는 색 고정으로 사용 바람
8. Badge
😅 미완성 작업
N/A
📢 논의 사항 및 참고 사항
N/A
Summary by CodeRabbit
업데이트 사항
새로운 기능
디자인 개선
제거됨
기타