[Feature/#76] 캠페인 상세 정보 레이아웃 및 카드 컴포넌트 구현 - #81
Conversation
📝 WalkthroughWalkthrough캠페인 상세 페이지 레이아웃과 정보 섹션을 추가하고, InfoCard 계열 컴포넌트들을 도입했으며, 캠페인 테이블의 행 클릭으로 상세 페이지로 네비게이션되도록 연결했습니다. Sidebar의 경로 매칭 로직도 강화되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
actor 사용자
participant CampaignTable
participant CampaignRow
participant Router as Navigation
participant CampaignDetail
사용자->>CampaignRow: 캠페인 행 클릭 (마우스/Enter/Space)
CampaignRow->>CampaignTable: onClick 트리거
CampaignTable->>Router: onRowClick(id) -> navigate(`/ads/${id}`)
Router->>CampaignDetail: 라우팅 /ads/:id 로드
CampaignDetail->>CampaignDetail: CampaignInfoCard, PlatformCard 렌더링
CampaignDetail-->>사용자: 상세 화면 표시
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 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 |
📚 Storybook 배포 완료
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (6)
src/components/ads/CampaignTable.tsx (1)
10-43: 하드코딩된 캠페인 데이터를 props로 분리하는 것이 좋습니다.현재
campaigns데이터가 컴포넌트 내부에 하드코딩되어 있습니다. 개발 단계에서는 괜찮지만, 실제 사용 시에는 props로 전달받거나 React Query를 통해 서버에서 가져오도록 분리하면 컴포넌트 재사용성이 높아집니다.♻️ Props로 분리 제안
+import type { ICampaign } from "@/types/ads/campaign"; + interface ICampaignTableProps { + campaigns: ICampaign[]; onRowClick?: (id: number) => void; } -export default function CampaignTable({ onRowClick }: ICampaignTableProps) { - const campaigns: ICampaign[] = [ - // ... hardcoded data - ]; +export default function CampaignTable({ campaigns, onRowClick }: ICampaignTableProps) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ads/CampaignTable.tsx` around lines 10 - 43, The campaigns array is hardcoded inside the CampaignTable component; refactor by moving the campaigns data into a prop (e.g., add a campaigns: ICampaign[] prop to the CampaignTable component) and update all internal references from the local campaigns variable to the prop (or alternatively fetch via React Query outside the component and pass results in). Ensure the component signature (CampaignTable) accepts the new prop, provide a sensible default or mark it optional if needed, and update any callers to pass the campaign list so the component no longer declares the const campaigns = [...] internally.src/components/Sidebar/Sidebar.tsx (1)
115-122: 주석 처리된 레거시 코드를 제거하는 것이 좋습니다.새로운 로직이 정상 동작하는 것이 확인되면, 주석 처리된 이전 코드는 제거해주세요. Git 히스토리에서 언제든 복구할 수 있으니 코드베이스를 깔끔하게 유지하는 것이 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Sidebar/Sidebar.tsx` around lines 115 - 122, Remove the commented legacy code block in Sidebar.tsx that defines isChildActive, isParentActive, and showChevron (the three commented const lines and their related ternary logic), since the new logic is in place; simply delete those commented lines to clean up the file—these identifiers (isChildActive, isParentActive, showChevron) are the unique symbols to look for when locating the obsolete comments.src/components/ads/PlatformCard.tsx (1)
16-20:LogoMap이CampaignRow.tsx와 중복됩니다.동일한
LogoMap상수가CampaignRow.tsx에도 정의되어 있습니다. 공통 상수 파일로 추출하면 유지보수가 편해지고, 새로운 플랫폼 추가 시 한 곳만 수정하면 됩니다.♻️ 공통 상수로 추출 제안
// src/constants/platformLogos.tsx import type { ReactNode } from "react"; import type { TPlatform } from "@/types/ads/campaign"; import GoogleLogo from "@/assets/icon/ads/google-logo.svg?react"; import KakaoLogo from "@/assets/icon/ads/kakao-logo.svg?react"; import NaverLogo from "@/assets/icon/ads/naver-logo.svg?react"; export const PlatformLogoMap: Record<TPlatform, ReactNode> = { kakao: <KakaoLogo className="w-full h-full" />, google: <GoogleLogo className="w-full h-full" />, naver: <NaverLogo className="w-full h-full" />, };그 후
CampaignRow.tsx와PlatformCard.tsx에서 import하여 사용:-const LogoMap: Record<TPlatform, ReactNode> = { - kakao: <KakaoLogo className="w-full h-full" />, - google: <GoogleLogo className="w-full h-full" />, - naver: <NaverLogo className="w-full h-full" />, -}; +import { PlatformLogoMap } from "@/constants/platformLogos";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ads/PlatformCard.tsx` around lines 16 - 20, Extract the duplicated LogoMap constant into a single shared constant (e.g., PlatformLogoMap) and import it from both files: remove the LogoMap definition in PlatformCard.tsx and CampaignRow.tsx and add an import of PlatformLogoMap from a new module (suggested name: src/constants/platformLogos.tsx) that exports PlatformLogoMap:Record<TPlatform,ReactNode> with the same mappings (KakaoLogo, GoogleLogo, NaverLogo). Update references in PlatformCard (and CampaignRow) to use PlatformLogoMap instead of the local LogoMap and ensure imports for TPlatform and SVG components are moved into the new constants file.src/components/ads/CampaignInfoCard.tsx (1)
22-24: Badge variant를 커스텀 스타일로 완전히 덮어쓰고 있습니다.
variant="running"을 지정했지만badgeStyle이 모든 스타일을 덮어씁니다. 이런 경우 Badge에 새로운 variant(예:neutral또는label)를 추가하거나, 별도의 Label 컴포넌트를 만드는 것이 더 명확할 수 있습니다.Also applies to: 32-34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ads/CampaignInfoCard.tsx` around lines 22 - 24, The Badge in CampaignInfoCard is being fully overridden by badgeStyle which negates the variant="running" intent; update the component so we either add a new Badge variant (e.g., "neutral" or "label") and use that variant instead of overriding, or create a small Label component and replace Badge usage; locate the Badge usages in CampaignInfoCard (the Badge elements with prop variant="running" and className={badgeStyle}) and change them to use the new variant or the new Label component, and adjust the badgeStyle CSS to only extend (compose) the existing badge styles rather than replace them so the Badge's built-in styling remains intact.src/pages/ads/list/AdsListPage.tsx (1)
8-10:handleCampaignClick함수의useCallback래핑 고려현재 구현에서는 컴포넌트가 리렌더링될 때마다
handleCampaignClick함수가 새로 생성됩니다. CampaignTable이React.memo로 최적화되어 있다면 불필요한 리렌더링을 유발할 수 있습니다.다만 현재 페이지가 단순하고 상태 변경이 적어 실질적인 성능 영향은 미미할 것으로 보여, 선택적으로 적용하시면 됩니다.
♻️ useCallback 적용 예시
+import { useCallback } from "react"; import { useNavigate } from "react-router-dom"; import CampaignTable from "@/components/ads/CampaignTable"; import ControlBox from "@/components/common/controlbox/ControlBox"; export default function AdsListPage() { const navigate = useNavigate(); - const handleCampaignClick = (id: string | number) => { - navigate(`/ads/${id}`); - }; + const handleCampaignClick = useCallback( + (id: string | number) => { + navigate(`/ads/${id}`); + }, + [navigate] + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ads/list/AdsListPage.tsx` around lines 8 - 10, Wrap the handleCampaignClick function with React's useCallback to avoid recreating it on every render (which can cause unnecessary re-renders of memoized children like CampaignTable); change the declaration of handleCampaignClick to useCallback(() => { navigate(`/ads/${id}`); }, [navigate]) (ensure you import useCallback from React) so the callback is stable unless the navigate reference changes.src/pages/ads/list/CampaignDetail.tsx (1)
43-44: 빈 placeholder div에 대한 주석 보완현재
{/* ads list */}주석 아래 빈 div가 있습니다. 향후 구현 예정인 내용이라면 TODO 주석으로 명시해두면 추적하기 좋습니다.✏️ 주석 보완 예시
- {/* ads list */} - <div className="min-w-180" /> - {/* control box */} + {/* TODO: ads list - 광고 소재 목록 테이블 구현 예정 */} + <div className="min-w-180" /> + {/* TODO: control box - 캠페인 제어 박스 구현 예정 */}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ads/list/CampaignDetail.tsx` around lines 43 - 44, The placeholder empty div under the JSX comment "{/* ads list */}" (the <div className="min-w-180" /> element) needs a clearer TODO-style comment so future work is discoverable; replace or augment the current comment with a TODO that states the intended purpose (e.g., "TODO: render ads list here / placeholder for ads list layout") and optionally include an owner or ticket reference; update the comment near the div with that TODO text so tools and reviewers can track the pending implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/ads/CampaignRow.tsx`:
- Around line 36-39: The list item in CampaignRow (the <li> that currently only
uses onClick) lacks keyboard accessibility; update the element in the
CampaignRow component to behave like a clickable button by adding role="button",
tabIndex={0}, and an onKeyDown handler that triggers the same onClick logic when
Enter or Space is pressed (handle Space with preventDefault to avoid page
scroll), ensuring the existing onClick handler is reused so mouse and keyboard
activate identical behavior.
In `@src/components/common/card/InfoCard.tsx`:
- Around line 4-15: The IInfoCardProps interface declares isActive but InfoCard
does not destructure or use it; either remove isActive from IInfoCardProps (and
any external usages) to eliminate the unused prop, or update the InfoCard
function signature to accept isActive (export default function InfoCard({ title,
children, className, isActive }: IInfoCardProps)) and apply it (e.g.,
conditionally append an active class to className or pass it to the root
element) or at minimum add a TODO comment in InfoCard noting planned use of
isActive; reference IInfoCardProps and InfoCard when making the change.
In `@src/pages/ads/list/AdsListPage.tsx`:
- Line 25: The onRowClick handler is hardcoded to pass id 1 which causes every
click to navigate to /ads/1; change the JSX to pass the existing
handleCampaignClick callback directly so CampaignTable can supply the clicked
row's campaign.id (i.e., replace onRowClick={() => handleCampaignClick(1)} with
onRowClick={handleCampaignClick}) ensuring the types match between
CampaignTable's onRowClick and the handleCampaignClick function.
In `@src/pages/ads/list/CampaignDetail.tsx`:
- Around line 5-6: CampaignDetail 컴포넌트가 URL에서 캠페인 ID를 읽지 않아 항상 정적 데이터를 렌더링합니다;
수정하려면 React Router의 useParams를 사용해 ads/:id 라우트의 id를 추출하고(예: const { id } =
useParams()), 해당 id로 데이터를 조회하거나 props/상태를 초기화하도록 CampaignDetail 내부 로직을 연결하세요; 만약
아직 데이터 로딩을 구현하지 않은 초기 레이아웃 단계라면 useParams를 남겨두고 TODO 주석을 추가해 향후 fetch 함수(또는
useEffect로 fetchCampaignById)를 호출하도록 명확히 표시하세요.
---
Nitpick comments:
In `@src/components/ads/CampaignInfoCard.tsx`:
- Around line 22-24: The Badge in CampaignInfoCard is being fully overridden by
badgeStyle which negates the variant="running" intent; update the component so
we either add a new Badge variant (e.g., "neutral" or "label") and use that
variant instead of overriding, or create a small Label component and replace
Badge usage; locate the Badge usages in CampaignInfoCard (the Badge elements
with prop variant="running" and className={badgeStyle}) and change them to use
the new variant or the new Label component, and adjust the badgeStyle CSS to
only extend (compose) the existing badge styles rather than replace them so the
Badge's built-in styling remains intact.
In `@src/components/ads/CampaignTable.tsx`:
- Around line 10-43: The campaigns array is hardcoded inside the CampaignTable
component; refactor by moving the campaigns data into a prop (e.g., add a
campaigns: ICampaign[] prop to the CampaignTable component) and update all
internal references from the local campaigns variable to the prop (or
alternatively fetch via React Query outside the component and pass results in).
Ensure the component signature (CampaignTable) accepts the new prop, provide a
sensible default or mark it optional if needed, and update any callers to pass
the campaign list so the component no longer declares the const campaigns =
[...] internally.
In `@src/components/ads/PlatformCard.tsx`:
- Around line 16-20: Extract the duplicated LogoMap constant into a single
shared constant (e.g., PlatformLogoMap) and import it from both files: remove
the LogoMap definition in PlatformCard.tsx and CampaignRow.tsx and add an import
of PlatformLogoMap from a new module (suggested name:
src/constants/platformLogos.tsx) that exports
PlatformLogoMap:Record<TPlatform,ReactNode> with the same mappings (KakaoLogo,
GoogleLogo, NaverLogo). Update references in PlatformCard (and CampaignRow) to
use PlatformLogoMap instead of the local LogoMap and ensure imports for
TPlatform and SVG components are moved into the new constants file.
In `@src/components/Sidebar/Sidebar.tsx`:
- Around line 115-122: Remove the commented legacy code block in Sidebar.tsx
that defines isChildActive, isParentActive, and showChevron (the three commented
const lines and their related ternary logic), since the new logic is in place;
simply delete those commented lines to clean up the file—these identifiers
(isChildActive, isParentActive, showChevron) are the unique symbols to look for
when locating the obsolete comments.
In `@src/pages/ads/list/AdsListPage.tsx`:
- Around line 8-10: Wrap the handleCampaignClick function with React's
useCallback to avoid recreating it on every render (which can cause unnecessary
re-renders of memoized children like CampaignTable); change the declaration of
handleCampaignClick to useCallback(() => { navigate(`/ads/${id}`); },
[navigate]) (ensure you import useCallback from React) so the callback is stable
unless the navigate reference changes.
In `@src/pages/ads/list/CampaignDetail.tsx`:
- Around line 43-44: The placeholder empty div under the JSX comment "{/* ads
list */}" (the <div className="min-w-180" /> element) needs a clearer TODO-style
comment so future work is discoverable; replace or augment the current comment
with a TODO that states the intended purpose (e.g., "TODO: render ads list here
/ placeholder for ads list layout") and optionally include an owner or ticket
reference; update the comment near the div with that TODO text so tools and
reviewers can track the pending implementation.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/components/Sidebar/Sidebar.tsxsrc/components/ads/CampaignInfoCard.tsxsrc/components/ads/CampaignRow.tsxsrc/components/ads/CampaignTable.tsxsrc/components/ads/PlatformCard.tsxsrc/components/common/card/InfoCard.stories.tsxsrc/components/common/card/InfoCard.tsxsrc/pages/ads/list/AdsListPage.tsxsrc/pages/ads/list/CampaignDetail.tsxsrc/routes/MainRoutes.tsx
📚 Storybook 배포 완료
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/ads/CampaignTable.tsx (1)
25-30: 스프레드 연산자 사용 시 불필요한 props 전달에 주의해 주세요.
{...campaign}으로 전달하면CampaignRow에서 사용하지 않는id,budget,startDate,description등의 props도 함께 전달됩니다. 현재는 동작에 문제가 없지만, 명시적으로 필요한 props만 전달하는 방식이 더 명확합니다.♻️ 명시적 props 전달 예시
{MOCK_CAMPAIGNS.map((campaign) => ( <CampaignRow key={campaign.id} - {...campaign} + platforms={campaign.platforms} + name={campaign.name} + status={campaign.status} + statusText={campaign.statusText} + progress={campaign.progress} onClick={() => onRowClick?.(campaign.id)} /> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ads/CampaignTable.tsx` around lines 25 - 30, The component is passing entire campaign objects via the spread operator into CampaignRow (see MOCK_CAMPAIGNS and CampaignRow usage), which sends unnecessary props (id, budget, startDate, description) to the child; fix by replacing {...campaign} with only the explicit props CampaignRow needs (e.g., title, status, clicks, impressions — whatever props CampaignRow declares) and keep the existing key and onClick handler (onRowClick?.(campaign.id)) so only required data flows into CampaignRow.src/pages/ads/list/campaign.mock.ts (1)
15-27: 목 데이터의 상태값 일관성을 확인해 주세요.
id: 1캠페인의 경우status: "success",statusText: "완료"인데progress: 65로 설정되어 있습니다. 완료 상태라면 progress가 100이어야 하지 않을까요? 현재 목 데이터 단계이므로 큰 문제는 아니지만, 추후 실제 API 연동 시 상태와 진행률 간의 정합성을 맞춰주시면 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/ads/list/campaign.mock.ts` around lines 15 - 27, The MOCK_CAMPAIGNS entry with id: 1 has inconsistent state fields (status: "success", statusText: "완료" but progress: 65); update the mock so the fields are consistent—either set progress to 100 when status === "success"/statusText === "완료" or change status/statusText to match a 65% in-progress state; locate the MOCK_CAMPAIGNS array (ICampaign entries) and make the fix for the object with id 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/components/ads/CampaignTable.tsx`:
- Around line 25-30: The component is passing entire campaign objects via the
spread operator into CampaignRow (see MOCK_CAMPAIGNS and CampaignRow usage),
which sends unnecessary props (id, budget, startDate, description) to the child;
fix by replacing {...campaign} with only the explicit props CampaignRow needs
(e.g., title, status, clicks, impressions — whatever props CampaignRow declares)
and keep the existing key and onClick handler (onRowClick?.(campaign.id)) so
only required data flows into CampaignRow.
In `@src/pages/ads/list/campaign.mock.ts`:
- Around line 15-27: The MOCK_CAMPAIGNS entry with id: 1 has inconsistent state
fields (status: "success", statusText: "완료" but progress: 65); update the mock
so the fields are consistent—either set progress to 100 when status ===
"success"/statusText === "완료" or change status/statusText to match a 65%
in-progress state; locate the MOCK_CAMPAIGNS array (ICampaign entries) and make
the fix for the object with id 1.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/components/ads/CampaignRow.tsxsrc/components/ads/CampaignTable.tsxsrc/components/common/card/InfoCard.tsxsrc/pages/ads/list/AdsListPage.tsxsrc/pages/ads/list/CampaignDetail.tsxsrc/pages/ads/list/campaign.mock.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/pages/ads/list/CampaignDetail.tsx
- src/components/ads/CampaignRow.tsx
- src/components/common/card/InfoCard.tsx
- src/pages/ads/list/AdsListPage.tsx
🚨 관련 이슈
close #76
✨ 변경사항
✏️ 작업 내용
InfoCard를 공통 레이아웃으로 활용하여CampaignInfoCard,PlatformCard컴포넌트 구현😅 미완성 작업
📢 논의 사항 및 참고 사항
Summary by CodeRabbit