Skip to content

refactor: JDS Card 컴포넌트 vanilla-extract 마이그레이션#500

Open
ccconac wants to merge 61 commits into
devfrom
refactor/474-jds-card
Open

refactor: JDS Card 컴포넌트 vanilla-extract 마이그레이션#500
ccconac wants to merge 61 commits into
devfrom
refactor/474-jds-card

Conversation

@ccconac

@ccconac ccconac commented Jun 30, 2026

Copy link
Copy Markdown
Member

💡 작업 내용

  • Card.Image 제거 후 내부 이미지를 Thumbnail로 일원화
  • Plate preset을 Card.Preset.Plate 하나로 단일화
  • Post를 항상 empty 스타일로 정합 (cardStyle 제거, Card.Meta 가로 배치)
  • Card.Overlay 접근성·타입 강화 (disabled 키보드 차단, as별 prop 강제)
  • 스타일·내부 정리 (공용 유틸 교체, 타입·네이밍 통일)
  • Storybook을 variant별(Plate/Post)로 분리

💡 자세한 설명

이미지 일원화 — Card.Image 제거, Thumbnail로 통합

Thumbnail과 역할이 겹치고 미사용 prop이 쌓여 있던 Card.Image를 제거하고, 내부 이미지를 Thumbnail로 일원화했습니다. Card.Thumbnail은 public으로 노출합니다.

미사용 prop: badgeVisible, fallback 등

- <Card.Image src={src} alt={alt} />
+ <Card.Thumbnail image={{ src, alt }} />

Plate preset 단일화

Figma에 없는 변형이던 WithTitle/WithLabel/Compact와 Card.Label을 제거하고, Card.Preset.Plate 하나로 통합했습니다.

Card.Preset.Plate: title + body + optional caption

- <Card.Preset.PlateWithTitle.Link href={url} title={title} body={body} />
+ <Card.Preset.Plate.Link href={url} title={title} body={body} />

Post 정합

디자인에 맞춰 Post는 항상 empty 스타일로 렌더링하도록 cardStyle/CardStyle을 제거하고, Card.Meta를 가로 배치로 변경했습니다.

- <Card.Preset.Post.Link cardStyle="outlined" title={title} body={body} ... />
+ <Card.Preset.Post.Link title={title} body={body} ... />

Card.Overlay 접근성·타입 강화

disabled가 pointer-events: none만 적용돼 키보드로 활성화되던 문제를 막고, as별 prop을 타입으로 강제했습니다.

- <Card.Overlay as="a" />
+ <Card.Overlay as="a" href={url} />

스타일·내부 정리

카드 전용 인라인 focus/hover 스타일을 공용 focusRing/overlay 유틸로 교체하고, Card.ContentGroup 승격, 타입 정리(CardSlotProps 제네릭·named preset 변형), CardRootOwnPropsCardRootProps 네이밍 통일 등 내부를 정리했습니다.

Storybook 분리

단일 Card.stories.tsx를 variant별로 Plate.stories.tsx / Post.stories.tsx로 분리해, 각 Figma 변형 table(PlateVariantMatrix / PostVariantMatrix)을 독립적으로 확인할 수 있게 했습니다.

📗 참고 자료 (선택)

  • .changeset/card-migration.md — 전체 변경점 및 소비자 영향 (AS-IS → TO-BE)

✅ 셀프 체크리스트

  • 머지할 브랜치 확인했나요?
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 기능이 잘 동작하나요?
  • 불필요한 코드는 제거했나요?

closes #474

Summary by CodeRabbit

  • New Features

    • 카드가 새 레이아웃/변형 체계로 정리되어, 더 일관된 PlatePost 프리셋을 사용할 수 있게 되었습니다.
    • 썸네일, 콘텐츠 그룹, 공통 프레임 구성 요소가 추가되어 카드 조합이 더 유연해졌습니다.
    • Storybook에 Plate/Post 예제가 추가되어 다양한 상태를 쉽게 확인할 수 있습니다.
  • Bug Fixes

    • 비활성 카드의 키보드 및 접근성 동작이 개선되었습니다.
    • 긴 제목, 본문, 메타 정보가 더 안정적으로 줄임 처리됩니다.

ccconac added 30 commits June 30, 2026 13:47
ccconac added 22 commits June 30, 2026 13:47
@ccconac ccconac self-assigned this Jun 30, 2026
@ccconac ccconac added the ✨feature 구현, 개선 사항 관련 부분 label Jun 30, 2026
@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ject-official-web-site-client-web Ready Ready Preview, Comment Jun 30, 2026 6:59am

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Card 컴포넌트 전체를 Emotion styled-components 기반에서 Vanilla Extract CSS 모듈 기반으로 전환하고, Figma 디자인 변형 기준으로 타입·컨텍스트·프리셋 API를 재정리했습니다. Card.Image/Card.Label/세 개의 Plate 프리셋이 제거되고 Card.Thumbnail/Card.ContentGroup 및 단일 Card.Preset.Plate가 추가됩니다.

Changes

Card 컴포넌트 API 전면 재설계

Layer / File(s) Summary
타입 계약 및 컨텍스트 재정의
packages/jds/src/components/Card/card.types.ts, packages/jds/src/components/Card/cardContext.ts
card.types.ts를 새로 추가해 CardLayout/CardVariant/CardRootProps/CardOverlayProps(a·button discriminated union)/CardThumbnailProps/프리셋 props를 정의합니다. cardContext.ts를 새로 추가해 CardContextValue·CardContext·useCardContext·useCardRootGuard를 구현하고, 기존 Card.types.tsCard.context.ts를 제거합니다.
Vanilla Extract CSS 모듈 정의
packages/jds/src/components/Card/compound/card.css.ts, packages/jds/src/components/Card/compound/compound.styles.ts
card.css.ts를 신규 추가해 root/imageContainer/thumbnailFill/content/contentGroup/meta/metaItem/title/body/caption/overlay 레시피를 정의합니다. Emotion 기반 compound.styles.ts는 전체 삭제됩니다.
CardRoot·CardThumbnail 구현 전환
packages/jds/src/components/Card/compound/CardRoot.tsx, packages/jds/src/components/Card/compound/CardThumbnail.tsx, packages/jds/src/components/Card/compound/CardImage.tsx
CardRootStyledCardRoot에서 div+card.css 기반으로 전환되고, useId()titleId를 생성해 컨텍스트에 추가합니다. CardThumbnail이 신규 추가되어 layout×variant 조합별 비율 매핑과 appearance를 자동 결정합니다. CardImageCardLabel은 제거됩니다.
나머지 compound 슬롯 전환
packages/jds/src/components/Card/compound/CardContent.tsx, packages/jds/src/components/Card/compound/CardContentGroup.tsx, packages/jds/src/components/Card/compound/CardTitle.tsx, packages/jds/src/components/Card/compound/CardBody.tsx, packages/jds/src/components/Card/compound/CardCaption.tsx, packages/jds/src/components/Card/compound/CardMeta.tsx, packages/jds/src/components/Card/compound/CardMetaItem.tsx
모든 슬롯 컴포넌트가 Styled* 컴포넌트에서 card.css+clsx 기반으로 전환됩니다. CardContentGroup이 신규 추가되고, CardContent의 자식 분류 로직이 제거됩니다. CardCaptionstandalone prop이 제거되고 data-card-slot 속성이 추가됩니다.
CardOverlay a/button 분기 구현
packages/jds/src/components/Card/compound/CardOverlay.tsx
PolymorphicForwardRef 기반에서 forwardRef로 전환되고 as="a"/as="button" 분기 렌더링을 직접 구현합니다. as="button"은 native disabled, as="a"href 제거+aria-disabled를 적용하며, titleId 기반 aria-labelledby 자동 보강 로직이 추가됩니다.
프리셋 통합 및 공유 컴포넌트 추가
packages/jds/src/components/Card/presets/shared.tsx, packages/jds/src/components/Card/presets/Plate.tsx, packages/jds/src/components/Card/presets/Post.tsx, packages/jds/src/components/Card/presets/index.ts, packages/jds/src/components/Card/presets/Plate*.tsx
PlateWithTitle/PlateWithLabel/PlateCompact 세 프리셋을 단일 Plate(Link/Button)로 통합합니다. PostPostBase+PresetFrame 구조로 재구성됩니다. shared.tsxTitleBody·PresetFrame 공용 컴포넌트가 추가됩니다.
공개 API 재정리
packages/jds/src/components/Card/index.ts, packages/jds/src/components/Card/compound/index.ts
Card 객체에 Thumbnail/ContentGroup이 추가되고 Image/Label이 제거됩니다. Card.PresetPlate/Post로 단순화되고, 모든 타입 re-export가 ./card.types 경로로 전환됩니다.
Storybook 스토리 재작성
packages/jds/src/components/Card/Plate.stories.tsx, packages/jds/src/components/Card/Post.stories.tsx, packages/jds/src/components/Card/Card.stories.tsx
기존 단일 Card.stories.tsx(9개 스토리)를 제거하고 Plate.stories.tsxPost.stories.tsx로 분리합니다. 각 파일은 layout×isDisabled 조합의 PlaygroundOverview 스토리를 제공합니다.
마이그레이션 changeset 문서
.changeset/card-migration.md
API 변경 요약, 소비자 영향 표, 구체적인 코드 변경 예시(diff), 동작 변경 목록(overlay disabled 접근성, height 제거 등)을 추가합니다.

추정 코드 리뷰 노력

🎯 4 (Complex) | ⏱️ ~60 minutes

연관 가능성이 있는 PR

제안 레이블

♻refactor

제안 리뷰어

  • areumH
  • itwillbeoptimal
  • Zero-1016

🐇 토끼가 카드를 들고 깡충!

Emotion 옷 벗고 Vanilla Extract 입었네,
Card.Image 사라지고 Thumbnail 등장~
Plate 셋이 하나로 합쳐졌어요 🃏
aria-disabled로 접근성도 탄탄!
콘텐츠 높이 기준으로 새 출발이에요 ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Card 컴포넌트의 vanilla-extract 마이그레이션이라는 핵심 변경을 짧고 명확하게 요약합니다.
Description check ✅ Passed 작업 내용, 자세한 설명, 참고 자료, 체크리스트, 이슈 종료 문구까지 있어 템플릿을 대부분 충족합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/474-jds-card

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
.changeset/card-migration.md (3)

42-48: 📐 Maintainability & Code Quality | 🔵 Trivial

Card.Content 자식 분류 로직 제거 누락

동작 변경 목록에 Card.Content의 자식 자동 분류(classification) 로직 제거가 빠져 있습니다. 기존에 Card.Content가 자식 타입에 따라 자동 배치하던 동작이 사라졌으므로, 마이그레이션 시 compound 조합 방식이 달라질 수 있습니다. 이 변경을 "동작 변경" 또는 "소비자 영향"에 추가하는 것을 권장합니다.

🤖 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 @.changeset/card-migration.md around lines 42 - 48, Add the missing
Card.Content behavior change to the migration notes: its child
auto-classification/auto-placement logic has been removed, so compound
composition may behave differently for consumers. Update the
.changeset/card-migration.md summary alongside the existing Card.Meta, Plate,
and Card.Overlay items, referencing Card.Content explicitly so the behavior
change is clearly documented.

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial

단독 Thumbnail 노출 미지원 명시

Card.Thumbnail 또는 Thumbnail으로 표기되어 있으나, index.ts에서는 Card.Thumbnail만 공개하고 단독 Thumbnail은 export하지 않습니다. Thumbnail 단독 사용은 불가능하므로 Card.Thumbnail로만 안내하세요.

🤖 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 @.changeset/card-migration.md at line 19, The migration note currently
suggests both Card.Thumbnail and standalone Thumbnail, but index.ts only exposes
Card.Thumbnail. Update the Card.Image/CardImageProps replacement guidance to
mention Card.Thumbnail only, and remove any standalone Thumbnail wording so the
documented API matches the actual export.

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial

Card.ContentGroup 설명 수정 필요

"title·body를 묶는 그룹 컨테이너"라는 설명은 실제 용도와 다릅니다. CardContentGroupProps는 일반 div 슬롯으로, TitleBody 내부 컴포넌트가 아닙니다. Card.Content의 자식 분류 로직 제거를 대체하는 용도이므로, 설명을 "콘텐츠 영역 그룹핑 컨테이너" 등으로 수정하세요.

🤖 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 @.changeset/card-migration.md at line 11, `Card.ContentGroup` in the
changeset description is inaccurate and should be updated to match its real
role. Adjust the wording in the `CardContentGroupProps` entry so it describes a
general content-area grouping `div` slot, not a title·body wrapper or
`TitleBody` internal component; use a phrase like “콘텐츠 영역 그룹핑 컨테이너” to reflect
its purpose as the replacement for `Card.Content` child classification logic.
packages/jds/src/components/Card/Plate.stories.tsx (1)

15-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

스토리 예시에 Card.ContentGroup를 반영해 주세요.

실제 프리셋 구현은 packages/jds/src/components/Card/presets/shared.tsxTitleBody를 통해 Card.Title/Card.BodyCard.ContentGroup으로 묶고 있고, 이번 PR에서도 그 슬롯을 public API로 노출했습니다. 지금 문서 설명과 예시는 여전히 Card.Content 바로 아래에 두고 있어서, 새 권장 조합과 마이그레이션 방향이 Storybook에 드러나지 않습니다.

예시
 <Card.Content>
-  <Card.Title>플레이트 카드 제목</Card.Title>
-  <Card.Body>카드 내용은 두 줄을 넘어가면 말줄임(...) 표시합니다.</Card.Body>
+  <Card.ContentGroup>
+    <Card.Title>플레이트 카드 제목</Card.Title>
+    <Card.Body>카드 내용은 두 줄을 넘어가면 말줄임(...) 표시합니다.</Card.Body>
+  </Card.ContentGroup>
   <Card.Caption>캡션 레이블</Card.Caption>
 </Card.Content>

Also applies to: 62-66, 115-119

🤖 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 `@packages/jds/src/components/Card/Plate.stories.tsx` around lines 15 - 23, The
Card Plate story still shows the old composition using Card.Content, but the new
public pattern should demonstrate Card.ContentGroup as the wrapper for
Card.Title and Card.Body. Update the story content and example in
Plate.stories.tsx to reflect the current preset structure used by TitleBody from
presets/shared.tsx, so Storybook documents the recommended slot hierarchy and
migration path. Also adjust any related description text to mention
Card.ContentGroup instead of implying Card.Content is the direct parent for the
title/body slots.
packages/jds/src/components/Card/Post.stories.tsx (1)

15-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

여기도 Card.ContentGroup 기준으로 예시를 맞춰 주세요.

Post 프리셋도 내부적으로는 TitleBody를 통해 제목/본문을 Card.ContentGroup으로 묶습니다. 스토리가 그 구조를 생략하면 새 슬롯 API가 문서에서 보이지 않고, consumers가 Card.Content의 권장 자식 구조를 오해하기 쉽습니다.

예시
 <Card.Content>
-  <Card.Title>포스트 카드 제목</Card.Title>
-  <Card.Body>카드 내용은 두 줄을 넘어가면 말줄임(...) 표시합니다.</Card.Body>
+  <Card.ContentGroup>
+    <Card.Title>포스트 카드 제목</Card.Title>
+    <Card.Body>카드 내용은 두 줄을 넘어가면 말줄임(...) 표시합니다.</Card.Body>
+  </Card.ContentGroup>
   <Card.Meta>
     <Card.MetaItem>김젝트</Card.MetaItem>
     <Card.MetaItem>2026년 2월 25일(수)</Card.MetaItem>
   </Card.Meta>
 </Card.Content>

Also applies to: 55-61, 114-120

🤖 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 `@packages/jds/src/components/Card/Post.stories.tsx` around lines 15 - 23, The
Post story copy is still describing the old slot structure, so update the
`component` documentation in `Post.stories.tsx` to show `Card.ContentGroup` and
`TitleBody` as the recommended composition for `variant='post'`. Make the
example and surrounding text reflect that `Post` uses `Card.ContentGroup` to
wrap title/body content, while keeping the existing references to `Card.Root`,
`Card.Thumbnail`, `Card.Content`, `Card.Meta`, and `Card.Overlay` accurate.
🤖 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 `@packages/jds/src/components/Card/card.types.ts`:
- Around line 35-38: `CardThumbnailImage.src` is too permissive and allows
invalid `image` objects to compile; update the `CardThumbnailImage` type so
`src` is required, or if empty thumbnails are intended, add explicit placeholder
handling in both `CardThumbnailImage` and `CardThumbnail.tsx` so `Thumbnail` is
never called with a missing source. Use the `CardThumbnailImage` interface and
the `CardThumbnail` component as the main touchpoints when applying the fix.
- Around line 57-60: The PresetButtonProps onClick type is too narrow and blocks
standard button handlers, so update the PresetButtonProps definition to reuse
the native button onClick signature from
ComponentPropsWithoutRef<"button">["onClick"] instead of () => void. Keep the
existing as: "button" and type fields intact, and ensure CardOverlay can pass
its button handler through without a type mismatch.

In `@packages/jds/src/components/Card/compound/CardCaption.tsx`:
- Around line 15-23: `CardCaption`에서 내부 슬롯 식별자인 `data-card-slot="caption"`이 소비자
props에 의해 덮어써질 수 있으니, `CardCaption` 컴포넌트의 props 펼침 순서를 조정해 `...restProps`보다
`data-card-slot`이 뒤에서 고정되도록 변경하세요. `CardCaption`의 `<span>` 렌더링 부분에서
`data-card-slot`과 `restProps`의 순서를 바꿔, 외부에서 같은 속성을 넘겨도 `card.css.ts`의
`:has([data-card-slot='caption'])` 분기가 유지되게 하세요.

In `@packages/jds/src/components/Card/compound/CardOverlay.tsx`:
- Around line 45-56: The disabled branch in CardOverlay still forwards
interactive anchor props, so the link can remain clickable or focusable even
when isDisabled is true. Update the CardOverlay anchor rendering to strip or
override interaction props from restProps/anchorProps when disabled,
specifically onClick and tabIndex (and any similar event-driven props you pass
through), while keeping href undefined and the disabled aria/data attributes
intact. Use the existing CardOverlay component and anchorProps handling as the
place to centralize this disabled-state filtering.

In `@packages/jds/src/components/Card/compound/CardTitle.tsx`:
- Around line 15-19: `CardTitle` is allowing `...restProps` to override the
generated `titleId`, which can break `CardOverlay`’s `aria-labelledby` linkage.
Update the `CardTitle` component so the `id` stays stable by giving `titleId`
precedence (or by explicitly supporting a custom `id` and propagating it through
the shared context used by `CardOverlay`). Use the `CardTitle` render path and
its related context/overlay wiring to keep the label reference consistent.

---

Nitpick comments:
In @.changeset/card-migration.md:
- Around line 42-48: Add the missing Card.Content behavior change to the
migration notes: its child auto-classification/auto-placement logic has been
removed, so compound composition may behave differently for consumers. Update
the .changeset/card-migration.md summary alongside the existing Card.Meta,
Plate, and Card.Overlay items, referencing Card.Content explicitly so the
behavior change is clearly documented.
- Line 19: The migration note currently suggests both Card.Thumbnail and
standalone Thumbnail, but index.ts only exposes Card.Thumbnail. Update the
Card.Image/CardImageProps replacement guidance to mention Card.Thumbnail only,
and remove any standalone Thumbnail wording so the documented API matches the
actual export.
- Line 11: `Card.ContentGroup` in the changeset description is inaccurate and
should be updated to match its real role. Adjust the wording in the
`CardContentGroupProps` entry so it describes a general content-area grouping
`div` slot, not a title·body wrapper or `TitleBody` internal component; use a
phrase like “콘텐츠 영역 그룹핑 컨테이너” to reflect its purpose as the replacement for
`Card.Content` child classification logic.

In `@packages/jds/src/components/Card/Plate.stories.tsx`:
- Around line 15-23: The Card Plate story still shows the old composition using
Card.Content, but the new public pattern should demonstrate Card.ContentGroup as
the wrapper for Card.Title and Card.Body. Update the story content and example
in Plate.stories.tsx to reflect the current preset structure used by TitleBody
from presets/shared.tsx, so Storybook documents the recommended slot hierarchy
and migration path. Also adjust any related description text to mention
Card.ContentGroup instead of implying Card.Content is the direct parent for the
title/body slots.

In `@packages/jds/src/components/Card/Post.stories.tsx`:
- Around line 15-23: The Post story copy is still describing the old slot
structure, so update the `component` documentation in `Post.stories.tsx` to show
`Card.ContentGroup` and `TitleBody` as the recommended composition for
`variant='post'`. Make the example and surrounding text reflect that `Post` uses
`Card.ContentGroup` to wrap title/body content, while keeping the existing
references to `Card.Root`, `Card.Thumbnail`, `Card.Content`, `Card.Meta`, and
`Card.Overlay` accurate.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48056627-555c-4d02-9ecf-3f8d10c8f10e

📥 Commits

Reviewing files that changed from the base of the PR and between f0d0ab8 and 3a9881b.

📒 Files selected for processing (31)
  • .changeset/card-migration.md
  • packages/jds/src/components/Card/Card.context.ts
  • packages/jds/src/components/Card/Card.stories.tsx
  • packages/jds/src/components/Card/Card.types.ts
  • packages/jds/src/components/Card/Plate.stories.tsx
  • packages/jds/src/components/Card/Post.stories.tsx
  • packages/jds/src/components/Card/card.types.ts
  • packages/jds/src/components/Card/cardContext.ts
  • packages/jds/src/components/Card/compound/CardBody.tsx
  • packages/jds/src/components/Card/compound/CardCaption.tsx
  • packages/jds/src/components/Card/compound/CardContent.tsx
  • packages/jds/src/components/Card/compound/CardContentGroup.tsx
  • packages/jds/src/components/Card/compound/CardImage.tsx
  • packages/jds/src/components/Card/compound/CardLabel.tsx
  • packages/jds/src/components/Card/compound/CardMeta.tsx
  • packages/jds/src/components/Card/compound/CardMetaItem.tsx
  • packages/jds/src/components/Card/compound/CardOverlay.tsx
  • packages/jds/src/components/Card/compound/CardRoot.tsx
  • packages/jds/src/components/Card/compound/CardThumbnail.tsx
  • packages/jds/src/components/Card/compound/CardTitle.tsx
  • packages/jds/src/components/Card/compound/card.css.ts
  • packages/jds/src/components/Card/compound/compound.styles.ts
  • packages/jds/src/components/Card/compound/index.ts
  • packages/jds/src/components/Card/index.ts
  • packages/jds/src/components/Card/presets/Plate.tsx
  • packages/jds/src/components/Card/presets/PlateCompact.tsx
  • packages/jds/src/components/Card/presets/PlateWithLabel.tsx
  • packages/jds/src/components/Card/presets/PlateWithTitle.tsx
  • packages/jds/src/components/Card/presets/Post.tsx
  • packages/jds/src/components/Card/presets/index.ts
  • packages/jds/src/components/Card/presets/shared.tsx
💤 Files with no reviewable changes (9)
  • packages/jds/src/components/Card/presets/PlateWithLabel.tsx
  • packages/jds/src/components/Card/presets/PlateCompact.tsx
  • packages/jds/src/components/Card/compound/CardImage.tsx
  • packages/jds/src/components/Card/Card.context.ts
  • packages/jds/src/components/Card/compound/compound.styles.ts
  • packages/jds/src/components/Card/compound/CardLabel.tsx
  • packages/jds/src/components/Card/presets/PlateWithTitle.tsx
  • packages/jds/src/components/Card/Card.stories.tsx
  • packages/jds/src/components/Card/Card.types.ts

Comment on lines +35 to +38
export interface CardThumbnailImage {
src?: string;
alt: string;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

CardThumbnailImage.src를 optional로 두면 잘못된 입력이 타입에서 통과합니다.

CardThumbnail.tsximage.src를 그대로 Thumbnail에 넘기고 빈 이미지 상태를 처리하지 않습니다. 지금 타입이면 image={{ alt: "..." }}가 허용돼서 런타임에서 깨진 썸네일을 만들 수 있습니다. src를 필수로 올리거나, 정말 비어 있는 이미지를 지원할 거면 placeholder 분기를 타입과 구현에 같이 추가해 주세요.

🤖 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 `@packages/jds/src/components/Card/card.types.ts` around lines 35 - 38,
`CardThumbnailImage.src` is too permissive and allows invalid `image` objects to
compile; update the `CardThumbnailImage` type so `src` is required, or if empty
thumbnails are intended, add explicit placeholder handling in both
`CardThumbnailImage` and `CardThumbnail.tsx` so `Thumbnail` is never called with
a missing source. Use the `CardThumbnailImage` interface and the `CardThumbnail`
component as the main touchpoints when applying the fix.

Comment on lines +57 to +60
interface PresetButtonProps {
as: "button";
onClick: () => void;
type?: "button" | "submit" | "reset";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and locate related symbols/usages.
git ls-files packages/jds/src/components/Card/card.types.ts
echo '--- outline ---'
ast-grep outline packages/jds/src/components/Card/card.types.ts --view expanded || true
echo '--- usages ---'
rg -n "PresetButtonProps|CardOverlay|as=\"button\"|onClick:" packages/jds/src/components/Card -S || true

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 4696


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- Plate preset ---'
sed -n '1,220p' packages/jds/src/components/Card/presets/Plate.tsx

echo '--- Post preset ---'
sed -n '1,220p' packages/jds/src/components/Card/presets/Post.tsx

echo '--- CardOverlay ---'
sed -n '1,220p' packages/jds/src/components/Card/compound/CardOverlay.tsx

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 5965


프리셋 버튼의 onClick 타입을 네이티브 버튼 시그니처로 맞춰주세요.

지금은 () => void라서 일반적인 React.MouseEvent<HTMLButtonElement> 핸들러를 넘길 수 없습니다. CardOverlay as="button"로 그대로 전달되는 만큼 ComponentPropsWithoutRef<"button">["onClick"]를 재사용하는 편이 맞습니다.

🤖 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 `@packages/jds/src/components/Card/card.types.ts` around lines 57 - 60, The
PresetButtonProps onClick type is too narrow and blocks standard button
handlers, so update the PresetButtonProps definition to reuse the native button
onClick signature from ComponentPropsWithoutRef<"button">["onClick"] instead of
() => void. Keep the existing as: "button" and type fields intact, and ensure
CardOverlay can pass its button handler through without a type mismatch.

Comment on lines +15 to +23
<span
ref={ref}
data-card-slot='caption'
className={clsx(
getLabelClassName({ size: "xs", weight: "subtle" }),
styles.caption,
className,
)}
{...restProps}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the component and the related CSS contract.
sed -n '1,120p' packages/jds/src/components/Card/compound/CardCaption.tsx
printf '\n--- CSS ---\n'
sed -n '1,180p' packages/jds/src/components/Card/compound/card.css.ts

printf '\n--- Search for other Card slot usages ---\n'
rg -n "data-card-slot|:has\\(" packages/jds/src/components/Card/compound -S

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 6554


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/jds/src/components/Card/card.types.ts

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 2395


data-card-slot을 소비자 props보다 뒤에서 고정하세요.
packages/jds/src/components/Card/compound/CardCaption.tsx에서 ...restProps가 마지막에 펼쳐져 같은 속성이 들어오면 card.css.ts:has([data-card-slot='caption']) 분기가 깨집니다. 내부 슬롯 식별자는 덮어써지지 않도록 순서를 바꾸는 게 안전합니다.

🤖 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 `@packages/jds/src/components/Card/compound/CardCaption.tsx` around lines 15 -
23, `CardCaption`에서 내부 슬롯 식별자인 `data-card-slot="caption"`이 소비자 props에 의해 덮어써질 수
있으니, `CardCaption` 컴포넌트의 props 펼침 순서를 조정해 `...restProps`보다 `data-card-slot`이 뒤에서
고정되도록 변경하세요. `CardCaption`의 `<span>` 렌더링 부분에서 `data-card-slot`과 `restProps`의 순서를
바꿔, 외부에서 같은 속성을 넘겨도 `card.css.ts`의 `:has([data-card-slot='caption'])` 분기가 유지되게
하세요.

Comment on lines +45 to +56
const { href, ...anchorProps } = restProps as ComponentPropsWithoutRef<"a">;

return (
<StyledCardOverlay
as={Component}
ref={ref}
<a
ref={ref as Ref<HTMLAnchorElement>}
className={overlayClassName}
{...anchorProps}
aria-labelledby={ariaLabelledby}
data-overlay
$variant={context.variant}
$cardStyle={context.cardStyle}
$isDisabled={context.isDisabled}
{...restProps}
data-disabled={isDisabled || undefined}
aria-disabled={isDisabled || undefined}
href={isDisabled ? undefined : href}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

비활성 링크 분기에서도 클릭 핸들러가 살아 있습니다.

Line 45-56은 href만 제거하고 onClick/tabIndex는 그대로 전달해서, isDisabled여도 소비자가 JS 액션이나 포커스를 다시 살릴 수 있습니다. 비활성 상태에서는 이 상호작용 props도 함께 차단해 주세요.

수정 예시
-    const { href, ...anchorProps } = restProps as ComponentPropsWithoutRef<"a">;
+    const { href, onClick, tabIndex, ...anchorProps } =
+      restProps as ComponentPropsWithoutRef<"a">;

     return (
       <a
         ref={ref as Ref<HTMLAnchorElement>}
         className={overlayClassName}
         {...anchorProps}
+        onClick={isDisabled ? undefined : onClick}
+        tabIndex={isDisabled ? -1 : tabIndex}
         aria-labelledby={ariaLabelledby}
         data-overlay
         data-disabled={isDisabled || undefined}
         aria-disabled={isDisabled || undefined}
         href={isDisabled ? undefined : href}
📝 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.

Suggested change
const { href, ...anchorProps } = restProps as ComponentPropsWithoutRef<"a">;
return (
<StyledCardOverlay
as={Component}
ref={ref}
<a
ref={ref as Ref<HTMLAnchorElement>}
className={overlayClassName}
{...anchorProps}
aria-labelledby={ariaLabelledby}
data-overlay
$variant={context.variant}
$cardStyle={context.cardStyle}
$isDisabled={context.isDisabled}
{...restProps}
data-disabled={isDisabled || undefined}
aria-disabled={isDisabled || undefined}
href={isDisabled ? undefined : href}
const { href, onClick, tabIndex, ...anchorProps } =
restProps as ComponentPropsWithoutRef<"a">;
return (
<a
ref={ref as Ref<HTMLAnchorElement>}
className={overlayClassName}
{...anchorProps}
onClick={isDisabled ? undefined : onClick}
tabIndex={isDisabled ? -1 : tabIndex}
aria-labelledby={ariaLabelledby}
data-overlay
data-disabled={isDisabled || undefined}
aria-disabled={isDisabled || undefined}
href={isDisabled ? undefined : href}
🤖 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 `@packages/jds/src/components/Card/compound/CardOverlay.tsx` around lines 45 -
56, The disabled branch in CardOverlay still forwards interactive anchor props,
so the link can remain clickable or focusable even when isDisabled is true.
Update the CardOverlay anchor rendering to strip or override interaction props
from restProps/anchorProps when disabled, specifically onClick and tabIndex (and
any similar event-driven props you pass through), while keeping href undefined
and the disabled aria/data attributes intact. Use the existing CardOverlay
component and anchorProps handling as the place to centralize this
disabled-state filtering.

Comment on lines +15 to +19
<h3
ref={ref}
id={titleId}
className={clsx(getTitleClassName({ size: "xs" }), styles.title, className)}
{...restProps}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
git ls-files 'packages/jds/src/components/Card/**' | sed -n '1,200p'

echo '--- CardTitle.tsx ---'
sed -n '1,220p' packages/jds/src/components/Card/compound/CardTitle.tsx

echo '--- CardOverlay.tsx ---'
sed -n '1,260p' packages/jds/src/components/Card/compound/CardOverlay.tsx

echo '--- card.types.ts ---'
sed -n '1,260p' packages/jds/src/components/Card/card.types.ts

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 6377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for titleId / aria-labelledby wiring across Card components
rg -n "titleId|aria-labelledby|CardTitle|CardOverlay|Card.Root" packages/jds/src/components/Card -S

Repository: JECT-Study/JECT-Official-WebSite-Client

Length of output: 6183


CardTitle에서 id가 덮이지 않게 하세요. ...restProps가 뒤에 와서 소비자 idtitleId를 덮어쓰면 CardOverlayaria-labelledby가 끊어집니다. id 우선순위를 고정하거나 커스텀 id를 컨텍스트까지 함께 반영해야 합니다.

🤖 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 `@packages/jds/src/components/Card/compound/CardTitle.tsx` around lines 15 -
19, `CardTitle` is allowing `...restProps` to override the generated `titleId`,
which can break `CardOverlay`’s `aria-labelledby` linkage. Update the
`CardTitle` component so the `id` stays stable by giving `titleId` precedence
(or by explicitly supporting a custom `id` and propagating it through the shared
context used by `CardOverlay`). Use the `CardTitle` render path and its related
context/overlay wiring to keep the label reference consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature 구현, 개선 사항 관련 부분

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: (JDS) Card 컴포넌트 리팩토링

1 participant