Conversation
📝 WalkthroughWalkthrough플랫폼 대시보드에 부분 실패 처리, URL 기반 플랫폼 선택, 차트 다운로드를 추가했습니다. Google OAuth 반환 흐름과 워크스페이스 삭제 모달을 분리했습니다. 공통 컴포넌트와 설정·워크스페이스 화면의 반응형 레이아웃도 변경했습니다. Changes플랫폼 대시보드 및 차트 기능
Google OAuth 반환 처리
워크스페이스 및 공통 UI
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (7)
src/components/workspace/DeleteWorkspaceModal.tsx (2)
10-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win컴포넌트 props 타입 이름을
I*Props형식으로 변경해 주세요.
TDeleteWorkspaceModalProps는 컴포넌트 props 타입입니다.IDeleteWorkspaceModalProps로 변경해 주세요.As per coding guidelines, “component props use
I*Props.”🤖 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/workspace/DeleteWorkspaceModal.tsx` around lines 10 - 16, Rename the component props type TDeleteWorkspaceModalProps to IDeleteWorkspaceModalProps and update every reference to use the new I*Props naming convention.Source: Coding guidelines
5-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win상대 경로 import를
@/alias로 변경해 주세요.
Button과Inputimport는 상대 경로를 사용합니다. 프로젝트의 모든 import에@/alias를 사용해 주세요.As per coding guidelines, “Use
@/alias for all imports.”🤖 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/workspace/DeleteWorkspaceModal.tsx` around lines 5 - 6, Update the Button and Input imports in DeleteWorkspaceModal to use the project’s `@/` alias instead of relative paths, while preserving the existing imported symbols.Source: Coding guidelines
src/hooks/integration/useGoogleOAuthReturn.ts (2)
38-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value렌더 중에 ref를 변경하고 있습니다.
39행은 렌더 단계에서
onConnectSuccessRef.current에 값을 씁니다. 렌더 함수는 순수해야 합니다. React 19의 동시 렌더링에서는 렌더가 버려지거나 재실행될 수 있으므로 이 패턴은 권장되지 않습니다.별도의 effect에서 할당하도록 바꿔 주세요.
♻️ 수정안
const onConnectSuccessRef = useRef(options?.onConnectSuccess); - onConnectSuccessRef.current = options?.onConnectSuccess; + + useEffect(() => { + onConnectSuccessRef.current = options?.onConnectSuccess; + }, [options?.onConnectSuccess]);동작 순서는 유지됩니다. 이 대입 effect가 처리용 effect보다 먼저 등록되기 때문입니다.
🤖 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/integration/useGoogleOAuthReturn.ts` around lines 38 - 39, Move the onConnectSuccessRef.current assignment out of render and into a separate effect within the hook containing onConnectSuccessRef. Register this synchronization effect before the effect that handles the connection result so the existing callback execution order is preserved.
41-41: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
useLayoutEffect대신useEffect를 사용해 주세요.이 effect는 DOM 레이아웃을 읽거나 쓰지 않습니다. 토스트 표시, 쿼리 무효화, 라우팅만 수행합니다.
useLayoutEffect는 브라우저 페인트를 차단하므로 이 작업에는 불필요합니다. 서버 렌더링 환경에서는 경고도 발생합니다.♻️ 수정안
-import { useLayoutEffect, useRef } from "react"; +import { useEffect, useRef } from "react";- useLayoutEffect(() => { + useEffect(() => {🤖 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/integration/useGoogleOAuthReturn.ts` at line 41, useGoogleOAuthReturn의 effect를 useLayoutEffect에서 useEffect로 변경하세요. 토스트 표시, 쿼리 무효화, 라우팅 동작과 의존성은 그대로 유지하고, import도 일반 effect 훅에 맞게 정리하세요.Source: Path instructions
src/pages/dashboard/platform/PlatformDashboard.tsx (1)
41-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftURL provider 동기화 로직을 커스텀 훅으로 분리하는 것을 검토해 주세요.
현재 페이지 컴포넌트가 다음 책임을 모두 가집니다. 쿼리 파라미터 파싱, 연결 목록 조회, 사용 가능 provider 필터링, 선택 상태 파생, URL 정리 effect, 헤더 주입 effect입니다. 페이지가 렌더링보다 상태 조율에 더 많은 코드를 쓰고 있습니다.
useSelectedPlatform()같은 훅으로 47-128행을 옮기면 페이지는selectedPlatform,platformItems,isPlatformSelectDisabled만 소비하면 됩니다. 테스트도 훅 단위로 가능해집니다.참고로 로직 자체는 정확합니다. 120-128행의 정리 effect는
provider파라미터를 제거한 뒤 다음 실행에서 122행 early return으로 종료되므로 루프는 발생하지 않습니다.🤖 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 41 - 128, Extract the provider URL synchronization and selection-state logic from PlatformDashboard into a dedicated useSelectedPlatform-style hook, including query parsing, connection filtering, derived selectedPlatform, URL cleanup, and platformItems creation. Keep the existing behavior unchanged, including the invalid-provider cleanup effect and its loop-safe early returns, so PlatformDashboard only consumes selectedPlatform, platformItems, and isPlatformSelectDisabled.Source: Path instructions
src/hooks/dashboard/usePlatformPerformance.ts (1)
32-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value부분 실패 처리 로직은 정확합니다. 실패 원인 관측만 보강하면 좋겠습니다.
Promise.allSettled는 입력 순서를 유지하므로PROVIDERS[index]매핑은 안전합니다. 전부 실패했을 때만 throw 하는 분기도 소비자(AllPlatformView)의isPerformanceError처리와 맞습니다.다만 지금은 실패한 provider의
result.reason이 완전히 버려집니다. 운영 중 특정 플랫폼만 계속 실패할 때 원인 추적이 어렵습니다. 실패 사유를 콘솔 또는 로깅 계층으로 남기는 것을 검토해 주세요.♻️ 실패 사유 로깅 예시
} else { + if (import.meta.env.DEV) { + console.warn(`[platform-performance] ${provider} 조회 실패`, result.reason); + } failedProviders.push(provider); }🤖 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/usePlatformPerformance.ts` around lines 32 - 48, In the settled-results loop of the platform performance hook, preserve the existing failedProviders mapping and all-failed error behavior while recording each rejected result’s reason through the project’s established console or logging mechanism, including the associated provider from PROVIDERS[index].src/components/dashboard/platform/AllPlatformView.tsx (1)
44-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
PLATFORM_LOGOS와 카드 헤더 마크업이PlatformDetailCard.tsx와 중복됩니다.
src/components/dashboard/platform/PlatformDetailCard.tsx에도 동일한 이름의PLATFORM_LOGOS상수와 동일한 헤더 블록(로고 + PLATFORM_MAP[provider]제목)이 있습니다. 지금은 로고 매핑을 두 곳에서 관리해야 합니다. 플랫폼을 추가하거나 로고 크기를 바꿀 때 한쪽만 수정되어 화면이 어긋날 수 있습니다.로고 매핑과 카드 헤더를 공용 모듈로 분리하는 것을 검토해 주세요. 예를 들어
platformLogos.tsx로 상수를 빼고, 헤더를PlatformCardHeader컴포넌트로 추출하면 두 카드가 같은 소스를 사용합니다.🤖 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/AllPlatformView.tsx` around lines 44 - 64, Extract the duplicated PLATFORM_LOGOS mapping from PlatformDetailErrorCard and PlatformDetailCard into a shared platformLogos module, then update both cards to consume it. Extract the shared logo-and-PLATFORM_MAP[provider] header markup into a reusable PlatformCardHeader component and replace each card’s inline header while preserving the existing styling and provider-specific rendering.
🤖 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/select/SearchSelect.tsx`:
- Around line 115-116: SearchSelect의 containerClassName과 inputClassName을 Input의
실제 클래스 매핑에 맞게 조정하세요. containerClassName에서 중복된 placeholder 스타일을 제거하거나 명시적으로
재정의하고, inputClassName 또는 wrapper의 focus-within 스타일에 키보드 포커스를 식별할 수 있는 ring 등의
표시를 추가하세요. 기존 outline-none 및 border-none과 충돌하지 않도록 focus:border-info-blue만으로
포커스를 표현하지 말고, 변경 범위는 해당 클래스 설정으로 제한하세요.
In `@src/components/dashboard/platform/AllPlatformView.tsx`:
- Around line 85-87: Stabilize the empty fallback array used by failedProviders
so its reference does not change between renders. Import and apply useMemo
around the failedProviders derivation in AllPlatformView, preserving the
existing populated-data behavior and ensuring the value passed to resetKeys
remains referentially stable.
In `@src/components/dashboard/platform/platformTrafficChartDownload.config.ts`:
- Around line 15-16: Update todayStamp so the filename date is built from the
local Date year, month, and day values rather than toISOString(), preserving the
YYYY-MM-DD format with zero-padded month and day.
In `@src/components/dashboard/platform/PlatformTrafficChartDownload.tsx`:
- Around line 14-21: Rename the component props type from
TPlatformTrafficChartDownloadProps to IPlatformTrafficChartDownloadProps, and
change platform to use the existing TProviderType instead of string. Update
getPlatformTrafficFilename’s signature if needed so it accepts the same
TProviderType, while preserving the existing optional platform behavior.
In `@src/components/dashboard/platform/PlatformViewSwitcher.tsx`:
- Around line 67-80: Update the disabled button in PlatformViewSwitcher to
provide an aria-label that includes the reason platform selection is
unavailable, such as the absence of connected platforms, alongside the existing
selected-platform context. Remove the redundant aria-disabled attribute while
preserving the native disabled state and visible label.
In `@src/components/setting/NotificationSection.tsx`:
- Line 269: Update the wrapperClassName for the Discord Webhook input to replace
the invalid tablet:w-fill utility with the valid tablet:w-full class, preserving
the existing width and shrink classes.
In `@src/components/workspace/DeleteWorkspaceModal.tsx`:
- Line 79: DeleteWorkspaceModal의 aria-label에서 잘못된 “우커스페이스” 표기를 “워크스페이스”로 수정해 스크린
리더에 올바른 레이블이 전달되도록 하세요.
In `@src/hooks/integration/useGoogleOAuthReturn.ts`:
- Around line 43-63: Update the OAuth result guard in the hook around
isTokenInitialized so both success and failure processing wait while orgId is
null, without marking processedRef or navigating to /integrations. Once orgId is
available, preserve the existing finish flow; ensure syncGoogle and query
invalidation run only for successful results, while failures can complete
without those success actions.
In `@src/pages/workspace/Workspace.tsx`:
- Around line 299-301: Update the conditional error message paragraph rendering
createErrorMsg in Workspace.tsx to include role="alert", ensuring screen readers
announce the mutation failure when the message appears.
In `@src/pages/workspace/WorkspaceSetting.tsx`:
- Around line 427-438: Remove the parent WorkspaceSetting deleteConfirmInput
state and its validation from onDelete, since DeleteWorkspaceModal owns
confirmInput and only invokes onConfirm when canDelete is satisfied. Keep
onDelete responsible for executing the deletion mutation, and preserve the
existing DeleteWorkspaceModal callbacks and loading behavior.
---
Nitpick comments:
In `@src/components/dashboard/platform/AllPlatformView.tsx`:
- Around line 44-64: Extract the duplicated PLATFORM_LOGOS mapping from
PlatformDetailErrorCard and PlatformDetailCard into a shared platformLogos
module, then update both cards to consume it. Extract the shared
logo-and-PLATFORM_MAP[provider] header markup into a reusable PlatformCardHeader
component and replace each card’s inline header while preserving the existing
styling and provider-specific rendering.
In `@src/components/workspace/DeleteWorkspaceModal.tsx`:
- Around line 10-16: Rename the component props type TDeleteWorkspaceModalProps
to IDeleteWorkspaceModalProps and update every reference to use the new I*Props
naming convention.
- Around line 5-6: Update the Button and Input imports in DeleteWorkspaceModal
to use the project’s `@/` alias instead of relative paths, while preserving the
existing imported symbols.
In `@src/hooks/dashboard/usePlatformPerformance.ts`:
- Around line 32-48: In the settled-results loop of the platform performance
hook, preserve the existing failedProviders mapping and all-failed error
behavior while recording each rejected result’s reason through the project’s
established console or logging mechanism, including the associated provider from
PROVIDERS[index].
In `@src/hooks/integration/useGoogleOAuthReturn.ts`:
- Around line 38-39: Move the onConnectSuccessRef.current assignment out of
render and into a separate effect within the hook containing
onConnectSuccessRef. Register this synchronization effect before the effect that
handles the connection result so the existing callback execution order is
preserved.
- Line 41: useGoogleOAuthReturn의 effect를 useLayoutEffect에서 useEffect로 변경하세요. 토스트
표시, 쿼리 무효화, 라우팅 동작과 의존성은 그대로 유지하고, import도 일반 effect 훅에 맞게 정리하세요.
In `@src/pages/dashboard/platform/PlatformDashboard.tsx`:
- Around line 41-128: Extract the provider URL synchronization and
selection-state logic from PlatformDashboard into a dedicated
useSelectedPlatform-style hook, including query parsing, connection filtering,
derived selectedPlatform, URL cleanup, and platformItems creation. Keep the
existing behavior unchanged, including the invalid-provider cleanup effect and
its loop-safe early returns, so PlatformDashboard only consumes
selectedPlatform, platformItems, and isPlatformSelectDisabled.
🪄 Autofix
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 Plus
Run ID: ba9f3e73-f847-47e8-99ce-22a2a4816cd7
⛔ Files ignored due to path filters (4)
README.mdis excluded by none and included by nonepackage.jsonis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yaml,!pnpm-lock.yamland included by nonesrc/assets/logo/social-logo/plain/kakao.svgis excluded by!**/*.svgand included bysrc/**
📒 Files selected for processing (44)
src/components/common/dropdownmenu/DropdownMenu.tsxsrc/components/common/modal/Modal.tsxsrc/components/common/select/SearchSelect.tsxsrc/components/dashboard/ai-report/print/downloadAiSummaryPdf.tssrc/components/dashboard/platform/AllPlatformTrafficChart.tsxsrc/components/dashboard/platform/AllPlatformView.tsxsrc/components/dashboard/platform/PlatformRoasTable.tsxsrc/components/dashboard/platform/PlatformTrafficChart.tsxsrc/components/dashboard/platform/PlatformTrafficChartDownload.tsxsrc/components/dashboard/platform/PlatformViewSwitcher.tsxsrc/components/dashboard/platform/SinglePlatformView.tsxsrc/components/dashboard/platform/platformTrafficChartDownload.config.tssrc/components/landing/GuideTimeline.tsxsrc/components/setting/NotificationSection.tsxsrc/components/setting/PasswordSection.tsxsrc/components/setting/ProfileSection.tsxsrc/components/sidebar/Sidebar.tsxsrc/components/sidebar/SidebarItem.tsxsrc/components/sidebar/WorkspaceSwitcher.tsxsrc/components/timeline/TimelineBar.tsxsrc/components/timeline/TimelinePerformancePanel.tsxsrc/components/workspace/DeleteWorkspaceModal.tsxsrc/components/workspace/MemberItem.tsxsrc/components/workspace/MemberSearchSelect.tsxsrc/components/workspace/PermissionTable.tsxsrc/components/workspace/TransferOwnerModal.tsxsrc/hooks/auth/useDeleteMyAccount.tssrc/hooks/auth/useEmailVerification.tssrc/hooks/dashboard/usePlatformPerformance.tssrc/hooks/integration/useGoogleOAuthReturn.tssrc/hooks/integration/useIntegrationOAuthReturn.tssrc/pages/auth/Login.tsxsrc/pages/auth/Signup.tsxsrc/pages/dashboard/platform/PlatformDashboard.tsxsrc/pages/dashboard/timeline/Timeline.tsxsrc/pages/integration/GoogleOAuthResultPage.tsxsrc/pages/integration/PlatformIntegrationsPage.tsxsrc/pages/workspace/Workspace.tsxsrc/pages/workspace/WorkspaceSetting.tsxsrc/routes/AuthRoutes.tsxsrc/styles/tokens.csssrc/types/auth/auth.tssrc/types/dashboard/platform.tssrc/utils/integration/mapPlatformAccounts.ts
💤 Files with no reviewable changes (6)
- src/pages/auth/Signup.tsx
- src/styles/tokens.css
- src/hooks/integration/useIntegrationOAuthReturn.ts
- src/components/common/modal/Modal.tsx
- src/hooks/auth/useEmailVerification.ts
- src/pages/integration/PlatformIntegrationsPage.tsx
| containerClassName="h-13 w-full rounded-2xl border border-info-blue font-body1 outline-none transition-colors placeholder:text-text-placeholder focus:border-info-blue" | ||
| inputClassName="truncate" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -i 'input' src -t f
rg -n -C 12 'containerClassName|inputClassName|rightElement' src --glob '*.tsx'Repository: WhereYouAd/WhereYouAd-Frontend
Length of output: 40624
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,100p' src/components/common/input/Input.tsx
sed -n '1,130p' src/components/common/select/SearchSelect.tsxRepository: WhereYouAd/WhereYouAd-Frontend
Length of output: 6833
SearchSelect의 포커스/placeholder 클래스 위치를 Input 매핑에 맞게 조정해 주세요.
containerClassName은 wrapper div에 적용되고, <input>에 들어가는 기본 클래스에도 outline-none, border-none, placeholder:text-text-placeholder가 이미 있습니다. 현재 placeholder:text-text-placeholder는 중복되고, focus:border-info-blue는 포커스 visible 상태를 표시하지 않습니다. placeholder:*를 없애거나 명시적으로 재지정하고, 포커스 스타일은 inputClassName이나 wrapper의 focus-within:*에서 키보드 사용자를 위한 표시(ring 등)와 함께 적용해 주세요. border-info-blue만 있으면 기존 내부 outline-none/border-none과 어울리지 않을 수 있습니다.
🤖 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/select/SearchSelect.tsx` around lines 115 - 116,
SearchSelect의 containerClassName과 inputClassName을 Input의 실제 클래스 매핑에 맞게 조정하세요.
containerClassName에서 중복된 placeholder 스타일을 제거하거나 명시적으로 재정의하고, inputClassName 또는
wrapper의 focus-within 스타일에 키보드 포커스를 식별할 수 있는 ring 등의 표시를 추가하세요. 기존 outline-none
및 border-none과 충돌하지 않도록 focus:border-info-blue만으로 포커스를 표현하지 말고, 변경 범위는 해당 클래스
설정으로 제한하세요.
Source: Path instructions
| const platformPerformance = performanceData?.platforms; | ||
| const failedProviders = performanceData?.failedProviders ?? []; | ||
| const hasPartialFailure = failedProviders.length > 0; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
failedProviders가 매 렌더마다 새 배열이 되어 resetKeys가 항상 바뀝니다.
86행의 performanceData?.failedProviders ?? []는 데이터가 없을 때 매 렌더마다 새로운 빈 배열을 만듭니다. 이 값이 221행 resetKeys에 그대로 들어갑니다. react-error-boundary는 resetKeys를 참조 비교(Object.is)로 판단합니다. 따라서 하위 트리에서 에러가 발생하면 다음 렌더에서 곧바로 경계가 리셋되고, 같은 에러가 다시 발생하는 루프가 생길 수 있습니다. 폴백 UI가 표시되지 않고 화면이 깜빡이거나 멈출 위험이 있습니다.
useMemo로 참조를 안정화해 주세요.
🐛 참조 안정화 수정안
const platformPerformance = performanceData?.platforms;
- const failedProviders = performanceData?.failedProviders ?? [];
+ const failedProviders = useMemo(
+ () => performanceData?.failedProviders ?? [],
+ [performanceData],
+ );
const hasPartialFailure = failedProviders.length > 0;파일 상단에 useMemo import가 필요합니다.
-import type { ReactNode } from "react";
+import { type ReactNode, useMemo } from "react";Also applies to: 221-221
🤖 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/AllPlatformView.tsx` around lines 85 - 87,
Stabilize the empty fallback array used by failedProviders so its reference does
not change between renders. Import and apply useMemo around the failedProviders
derivation in AllPlatformView, preserving the existing populated-data behavior
and ensuring the value passed to resetKeys remains referentially stable.
Source: Path instructions
| function todayStamp() { | ||
| return new Date().toISOString().slice(0, 10); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
로컬 날짜로 파일명을 생성하세요.
Line 16의 toISOString()은 UTC 날짜를 반환합니다. 한국 시간 기준 오전 9시 이전에는 파일명 날짜가 전날로 저장됩니다. 로컬 연도·월·일로 날짜 문자열을 생성하세요.
수정 예시
function todayStamp() {
- return new Date().toISOString().slice(0, 10);
+ const date = new Date();
+ return [
+ date.getFullYear(),
+ String(date.getMonth() + 1).padStart(2, "0"),
+ String(date.getDate()).padStart(2, "0"),
+ ].join("-");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function todayStamp() { | |
| return new Date().toISOString().slice(0, 10); | |
| function todayStamp() { | |
| const date = new Date(); | |
| return [ | |
| date.getFullYear(), | |
| String(date.getMonth() + 1).padStart(2, "0"), | |
| String(date.getDate()).padStart(2, "0"), | |
| ].join("-"); | |
| } |
🤖 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/platformTrafficChartDownload.config.ts`
around lines 15 - 16, Update todayStamp so the filename date is built from the
local Date year, month, and day values rather than toISOString(), preserving the
YYYY-MM-DD format with zero-padded month and day.
| type TPlatformTrafficChartDownloadProps = { | ||
| /** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */ | ||
| platform?: string; | ||
| }; | ||
|
|
||
| export default function PlatformTrafficChartDownload({ | ||
| platform, | ||
| }: TPlatformTrafficChartDownloadProps) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
props 타입 이름과 platform 타입을 규칙에 맞춰 주세요.
두 가지를 정리해 주세요.
첫째, 코딩 가이드라인은 컴포넌트 props 타입에 I*Props 접두사를 요구합니다. 현재는 TPlatformTrafficChartDownloadProps입니다.
둘째, platform?: string은 너무 넓습니다. 호출부인 SinglePlatformView.tsx 167행은 TProviderType 값을 전달합니다. string으로 두면 오타나 잘못된 코드가 컴파일 단계에서 걸러지지 않고, getPlatformTrafficFilename(platform)에 임의 문자열이 들어갈 수 있습니다.
♻️ 타입 정리 수정안
+import type { TProviderType } from "`@/types/dashboard/provider`";
+
-type TPlatformTrafficChartDownloadProps = {
+interface IPlatformTrafficChartDownloadProps {
/** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */
- platform?: string;
-};
+ platform?: TProviderType;
+}
export default function PlatformTrafficChartDownload({
platform,
-}: TPlatformTrafficChartDownloadProps) {
+}: IPlatformTrafficChartDownloadProps) {getPlatformTrafficFilename의 시그니처도 함께 확인해 주세요.
코딩 가이드라인의 "component props use I*Props" 규칙을 따랐습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| type TPlatformTrafficChartDownloadProps = { | |
| /** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */ | |
| platform?: string; | |
| }; | |
| export default function PlatformTrafficChartDownload({ | |
| platform, | |
| }: TPlatformTrafficChartDownloadProps) { | |
| import type { TProviderType } from "`@/types/dashboard/provider`"; | |
| interface IPlatformTrafficChartDownloadProps { | |
| /** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */ | |
| platform?: TProviderType; | |
| } | |
| export default function PlatformTrafficChartDownload({ | |
| platform, | |
| }: IPlatformTrafficChartDownloadProps) { |
🤖 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/PlatformTrafficChartDownload.tsx` around
lines 14 - 21, Rename the component props type from
TPlatformTrafficChartDownloadProps to IPlatformTrafficChartDownloadProps, and
change platform to use the existing TProviderType instead of string. Update
getPlatformTrafficFilename’s signature if needed so it accepts the same
TProviderType, while preserving the existing optional platform behavior.
Source: Coding guidelines
| {isPlatformSelectDisabled ? ( | ||
| <Button | ||
| type="button" | ||
| size="small" | ||
| variant="custom" | ||
| disabled | ||
| aria-disabled="true" | ||
| className={platformTriggerClassName} | ||
| > | ||
| <span className="truncate font-body1 text-text-muted"> | ||
| {selectedPlatformLabel} | ||
| </span> | ||
| <ChevronDownIcon className="ml-2 h-3 w-3 shrink-0 rotate-180 text-text-muted" /> | ||
| </Button> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
비활성 상태의 이유를 보조 기술에 전달해 주세요.
disabled가 설정된 native <button>은 포커스를 받지 못합니다. 따라서 스크린 리더 사용자는 이 버튼의 존재와 비활성 이유를 알기 어렵습니다. 현재 표시 텍스트는 "플랫폼 선택"뿐이라 "연동된 플랫폼이 없어서 선택할 수 없다"는 맥락이 전달되지 않습니다.
aria-label로 이유를 함께 제공해 주세요. disabled와 aria-disabled를 함께 두면 disabled가 우선하므로 aria-disabled는 제거해도 됩니다.
♿ 접근성 보강 수정안
<Button
type="button"
size="small"
variant="custom"
disabled
- aria-disabled="true"
+ aria-label="연동된 플랫폼이 없어 플랫폼을 선택할 수 없습니다"
className={platformTriggerClassName}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {isPlatformSelectDisabled ? ( | |
| <Button | |
| type="button" | |
| size="small" | |
| variant="custom" | |
| disabled | |
| aria-disabled="true" | |
| className={platformTriggerClassName} | |
| > | |
| <span className="truncate font-body1 text-text-muted"> | |
| {selectedPlatformLabel} | |
| </span> | |
| <ChevronDownIcon className="ml-2 h-3 w-3 shrink-0 rotate-180 text-text-muted" /> | |
| </Button> | |
| {isPlatformSelectDisabled ? ( | |
| <Button | |
| type="button" | |
| size="small" | |
| variant="custom" | |
| disabled | |
| aria-label="연동된 플랫폼이 없어 플랫폼을 선택할 수 없습니다" | |
| className={platformTriggerClassName} | |
| > | |
| <span className="truncate font-body1 text-text-muted"> | |
| {selectedPlatformLabel} | |
| </span> | |
| <ChevronDownIcon className="ml-2 h-3 w-3 shrink-0 rotate-180 text-text-muted" /> | |
| </Button> |
🤖 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/PlatformViewSwitcher.tsx` around lines 67 -
80, Update the disabled button in PlatformViewSwitcher to provide an aria-label
that includes the reason platform selection is unavailable, such as the absence
of connected platforms, alongside the existing selected-platform context. Remove
the redundant aria-disabled attribute while preserving the native disabled state
and visible label.
Source: Path instructions
| helperText={discordWebhookError} | ||
| disabled={channelDisabled || isAnyOrgPending} | ||
| wrapperClassName="w-1/4 shrink-0" | ||
| wrapperClassName="w-1/4 shrink-0 tablet:w-fill" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --glob '*.{ts,tsx,css}' '\b(?:tablet:)?w-fill\b' src || true
fd -a -g 'tailwind.config.*' -g 'package.json' -g 'vite.config.*' -x \
rg -n -C 3 'w-fill|`@utility`' || trueRepository: WhereYouAd/WhereYouAd-Frontend
Length of output: 447
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Tracked config/package files:\n'
git ls-files | rg '(^|/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|npm-shrinkwrap\.json|tailwind\.config\.(js|cjs|ts|mjs)|vite\.config\.(js|ts|mjs|cjs)|postcss\.config\.(js|cjs|ts|json)|src/styles/tokens\.css)$' || true
printf '\nRelevant NotificationSection lines:\n'
sed -n '190,275p' src/components/setting/NotificationSection.tsx
printf '\nTailwind config and utility definitions:\n'
git ls-files | rg '(^|/)(tailwind\.config\.(js|cjs|ts|mjs)|postcss\.config\.(js|cjs|ts|json)|package\.json)$' | while read -r f; do
echo "--- $f"
rg -n -C 4 'w-fill|`@utility`|theme|screen|screens|utility|responsive|prefix|tailwindcss|postcss|autoprefixer|daisyui' "$f" || true
doneRepository: WhereYouAd/WhereYouAd-Frontend
Length of output: 5056
🌐 Web query:
Tailwind CSS v4 width utility w-fill documentation
💡 Result:
Tailwind CSS v4 does not provide a built-in w-fill utility class [1]. The utility class you may be looking for, fill-*, is specifically intended for styling the fill color of SVG elements, not for setting element widths [2]. If you need an element to fill its container, the standard approach in Tailwind CSS is to use w-full, which sets width: 100% [1][3]. If you specifically require the behavior of width: stretch or width: -webkit-fill-available (sometimes referred to as "fill"), Tailwind does not include this as a default utility [4]. You can implement it using arbitrary values in your HTML or by adding it to your configuration: 1. Arbitrary value syntax: You can apply the style directly in your markup using square brackets: class="w-[stretch]" or class="w-[-webkit-fill-available]" [1]. 2. Custom utility: If you need to reuse this frequently, you can define it in your CSS file using the @utility directive (available in v4) [5]: css `@utility` w-fill { width: stretch; width: -webkit-fill-available; }
Citations:
- 1: https://tailwindcss.com/docs/width
- 2: https://tailwindcss.com/docs/fill
- 3: https://leyaa.ai/codefly/learn/tailwind/part-1/tailwind-width-utilities/deep
- 4: Add stretch value to length width, height styles. tailwindlabs/tailwindcss#12424
- 5: https://tailwindcss.com/docs/upgrade-guide
tablet:w-fill을 tablet:w-full로 바꿔 주세요.
프로젝트에 w-fill 유틸리티가 없고, Tailwind CSS에서 유효한 width 클래스도 아닙니다. 그러니까 태블릿 조건에서 디스코드 Webhook 입력이 기대한 전체 너미가 적용되지 않습니다.
수정 예시
- wrapperClassName="w-1/4 shrink-0 tablet:w-fill"
+ wrapperClassName="w-1/4 shrink-0 tablet:w-full"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| wrapperClassName="w-1/4 shrink-0 tablet:w-fill" | |
| wrapperClassName="w-1/4 shrink-0 tablet:w-full" |
🤖 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/NotificationSection.tsx` at line 269, Update the
wrapperClassName for the Discord Webhook input to replace the invalid
tablet:w-fill utility with the valid tablet:w-full class, preserving the
existing width and shrink classes.
Source: Coding guidelines
| placeholder="워크스페이스 이름" | ||
| autoComplete="off" | ||
| disabled={isLoading} | ||
| aria-label="우커스페이스 이름 확인 입력" |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
aria-label의 워크스페이스 오타를 수정해 주세요.
현재 "우커스페이스 이름 확인 입력"이 스크린 리더에 전달됩니다. "워크스페이스 이름 확인 입력"으로 수정해 주세요.
As per path instructions, “접근성: 시맨틱 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/components/workspace/DeleteWorkspaceModal.tsx` at line 79,
DeleteWorkspaceModal의 aria-label에서 잘못된 “우커스페이스” 표기를 “워크스페이스”로 수정해 스크린 리더에 올바른
레이블이 전달되도록 하세요.
Source: Path instructions
| if (processedRef.current || !isTokenInitialized) return; | ||
|
|
||
| const finish = (type: TGoogleOAuthToastType, message: string) => { | ||
| if (!toastShownRef.current) { | ||
| toastShownRef.current = true; | ||
| showGoogleOAuthToast(type, message); | ||
|
|
||
| if (orgId != null) { | ||
| void queryClient.invalidateQueries({ | ||
| queryKey: QUERY_KEYS.platform.connections(orgId), | ||
| }); | ||
| } | ||
|
|
||
| if (type === "success" && orgId != null) { | ||
| onConnectSuccessRef.current?.(orgId); | ||
| } | ||
| } | ||
|
|
||
| processedRef.current = true; | ||
| navigate("/integrations", { replace: true }); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# selectedOrgId 초기화 시점과 persist 여부, isTokenInitialized 설정 위치 확인
fd -t f 'useWorkspaceStore.ts' src/store --exec cat -n
rg -n -C4 'isTokenInitialized' src --type=ts --type=tsx
rg -n -C4 'setSelectedOrgId' src --type=ts --type=tsxRepository: WhereYouAd/WhereYouAd-Frontend
Length of output: 901
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file =="
if [ -f src/hooks/integration/useGoogleOAuthReturn.ts ]; then
nl -ba src/hooks/integration/useGoogleOAuthReturn.ts | sed -n '1,140p'
else
fd -t f 'useGoogleOAuthReturn' src -x sh -c 'echo "--- $1"; nl -ba "$1" | sed -n "1,140p"' sh {}
fi
echo
echo "== isTokenInitialized occurrences =="
rg -n -C5 'isTokenInitialized' src || true
echo
echo "== setSelectedOrgId occurrences =="
rg -n -C5 'setSelectedOrgId' src || true
echo
echo "== GOOGLE_OAUTH/QUERY_KEYS/connections occurrences =="
rg -n -C5 'GOOGLE_OAUTH|QUERY_KEYS|connections\(|queryKey:.*connections|platform\(\)' src || true
echo
echo "== workspace store files =="
fd -t f 'workspace|useWorkspace|store' src/store src || true
echo
echo "== TypeScript files (excluding generated/deploy) =="
git ls-files 'src/**' | grep -E '\.(ts|tsx)$' | sed -n '1,120p'Repository: WhereYouAd/WhereYouAd-Frontend
Length of output: 226
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file =="
if [ -f src/hooks/integration/useGoogleOAuthReturn.ts ]; then
cat -n src/hooks/integration/useGoogleOAuthReturn.ts | sed -n '1,160p'
else
fd -t f 'useGoogleOAuthReturn' src -x sh -c 'echo "--- $1"; cat -n "$1" | sed -n "1,160p"' sh {}
fi
echo
echo "== isTokenInitialized occurrences =="
rg -n -C5 'isTokenInitialized' src || true
echo
echo "== setSelectedOrgId occurrences =="
rg -n -C5 'setSelectedOrgId' src || true
echo
echo "== GOOGLE_OAUTH/QUERY_KEYS/connections occurrences =="
rg -n -C5 'GOOGL.*OAUTH|GOOGLE_OAUTH|QUERY_KEYS|connections\(|queryKey:.*connections|platform\(' src || true
echo
echo "== workspace store files =="
fd -t f 'workspace|useWorkspace|store' src/store src || true
echo
echo "== TypeScript files (first 160) =="
git ls-files 'src/**' | grep -E '\.(ts|tsx)$' | sed -n '1,160p'Repository: WhereYouAd/WhereYouAd-Frontend
Length of output: 50387
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== auth token initialization callers =="
rg -n -C5 'login\(|setAccessToken\(|setTokenInitialized\(' src --glob '*.ts' --glob '*.tsx' || true
echo
echo "== router auth guard =="
cat -n src/routes/Router.tsx | sed -n '1,120p'
echo
echo "== oauth return usages =="
rg -n -C8 'useGoogleOAuthReturn\(|useMetaOAuthReturn\(' src --glob '*.ts' --glob '*.tsx' || true
echo
echo "== auth store exports =="
cat -n src/store/useAuthStore.ts | sed -n '1,120p'
echo
echo "== role guard org fetching behavior =="
cat -n src/routes/RoleGuard.tsx | sed -n '1,160p'Repository: WhereYouAd/WhereYouAd-Frontend
Length of output: 18398
orgId == null일 때는 OAuth 결과 처리를 보류하세요.
MainLayout에서 selectedOrgId를 채우는 경로는 메인 워크스페이스 목록 조회 이후이고, 이 훅의 43행 가드는 isTokenInitialized만 넘기 때문에 orgId가 아직 준비되지 않은 성공 뒤 syncGoogle과 캐시 무효화가 실행되지 않습니다. 이미 토스트를 표시하고 processedRef가 true가 되면 다시 처리하지 못해 사용자는 연동되어도 목록에는 반영되지 않은 상태로 이동합니다.
orgId == null일 때는 성공 결과도 보류해 주세요. 성공 뒤 다시 도달할 수 없는 상태라 OAuth 실패만 누락될 수 있으므로 실패 처리도 함께 고려해야 합니다. workflow에는 워크스페이스가 없으면 MainLayout 자체가 계속 보류하므로, 성공만 보류하고 실패는 곧바로 처리하는 전략은 맞지 않습니다. 실패/성공 모두 orgId == null이면 /integrations 이동 없이 대기하며, 성공 뒤에만 sync/invalidate를 실행하세요.
🤖 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/integration/useGoogleOAuthReturn.ts` around lines 43 - 63, Update
the OAuth result guard in the hook around isTokenInitialized so both success and
failure processing wait while orgId is null, without marking processedRef or
navigating to /integrations. Once orgId is available, preserve the existing
finish flow; ensure syncGoogle and query invalidation run only for successful
results, while failures can complete without those success actions.
| {createErrorMsg && ( | ||
| <p className="font-body2 text-info-red">{createErrorMsg}</p> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
생성 오류를 스크린 리더에도 전달해 주세요.
createErrorMsg는 mutation 실패 후 새로 표시됩니다. 그러나 Line 299-301의 <p>는 일반 텍스트로만 렌더링됩니다. 스크린 리더 사용자는 오류 상태 변경을 자동으로 알리지 못할 수 있습니다. role="alert"를 추가해 주세요.
As per path instructions: src/** 경로는 "접근성: 시맨틱 HTML, ARIA 속성 사용 확인"을 확인해야 합니다.
수정 예시
- <p className="font-body2 text-info-red">{createErrorMsg}</p>
+ <p role="alert" className="font-body2 text-info-red">
+ {createErrorMsg}
+ </p>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {createErrorMsg && ( | |
| <p className="font-body2 text-info-red">{createErrorMsg}</p> | |
| )} | |
| {createErrorMsg && ( | |
| <p role="alert" className="font-body2 text-info-red"> | |
| {createErrorMsg} | |
| </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/pages/workspace/Workspace.tsx` around lines 299 - 301, Update the
conditional error message paragraph rendering createErrorMsg in Workspace.tsx to
include role="alert", ensuring screen readers announce the mutation failure when
the message appears.
Source: Path instructions
| <DeleteWorkspaceModal | ||
| isOpen={deleteOpen} | ||
| onClose={() => { | ||
| if (!deleting) { | ||
| setDeleteOpen(false); | ||
| setDeleteNameSnapshot(""); | ||
| setDeleteConfirmInput(""); | ||
| } | ||
| }} | ||
| title="워크스페이스를 삭제할게요" | ||
| size="lg" | ||
| disableOverlayClick={deleting} | ||
| > | ||
| <ModalContent | ||
| icon={ | ||
| <WarnIcon | ||
| className="h-7 w-7 text-info-red" | ||
| aria-hidden="true" | ||
| /> | ||
| } | ||
| title="워크스페이스를 삭제할게요" | ||
| description={ | ||
| <> | ||
| <p> | ||
| 삭제하면 연결된 모든 데이터가 사라지고, 다시 되돌릴 수 | ||
| 없어요. | ||
| </p> | ||
| <p className="mt-2.5"> | ||
| 아래 워크스페이스 이름을 그대로 입력해 주세요. | ||
| </p> | ||
| </> | ||
| } | ||
| confirmMatchSubheading={false} | ||
| confirmMatchText={deleteNameSnapshot} | ||
| confirmInput={deleteConfirmInput} | ||
| onConfirmInputChange={setDeleteConfirmInput} | ||
| confirmMatchInputPlaceholder="워크스페이스 이름" | ||
| buttonText="영구 삭제" | ||
| onConfirm={() => { | ||
| void onDelete(); | ||
| }} | ||
| isLoading={deleting} | ||
| variant="danger" | ||
| /> | ||
| </Modal> | ||
| workspaceName={deleteNameSnapshot} | ||
| onConfirm={onDelete} | ||
| isLoading={deleting} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
삭제 확인 상태를 한 곳에서만 검증해 주세요.
Line 436의 onDelete는 Line 194에서 deleteConfirmInput을 검사합니다. 그러나 새 DeleteWorkspaceModal은 자체 confirmInput만 갱신합니다. 따라서 이름을 올바르게 입력해도 상위 상태는 빈 문자열로 남고, 삭제 mutation이 실행되지 않습니다.
상위 컴포넌트에서 deleteConfirmInput 상태와 해당 검사를 제거해 주세요. DeleteWorkspaceModal이 canDelete를 만족할 때만 onConfirm을 호출하도록 이미 보호합니다.
수정 방향
const onDelete = () => {
if (orgId === null) return;
- if (deleteConfirmInput.trim() !== deleteNameSnapshot) {
- toast.error("워크스페이스 이름이 일치하지 않습니다");
- return;
- }
deleteMutation.mutate(orgId);
};🤖 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/workspace/WorkspaceSetting.tsx` around lines 427 - 438, Remove the
parent WorkspaceSetting deleteConfirmInput state and its validation from
onDelete, since DeleteWorkspaceModal owns confirmInput and only invokes
onConfirm when canDelete is satisfied. Keep onDelete responsible for executing
the deletion mutation, and preserve the existing DeleteWorkspaceModal callbacks
and loading behavior.
🚨 관련 이슈
N/A
✨ 변경사항
✏️ 작업 내용
N/A
😅 미완성 작업
N/A
📢 논의 사항 및 참고 사항
N/A
Summary by CodeRabbit
새로운 기능
개선 사항
버그 수정