Skip to content

feat: JDS Radio 컴포넌트 신규 스펙 반영 및 구조 개선#490

Merged
itwillbeoptimal merged 6 commits into
devfrom
feat/489-radio-new-spec
Jun 29, 2026
Merged

feat: JDS Radio 컴포넌트 신규 스펙 반영 및 구조 개선#490
itwillbeoptimal merged 6 commits into
devfrom
feat/489-radio-new-spec

Conversation

@itwillbeoptimal

@itwillbeoptimal itwillbeoptimal commented Jun 25, 2026

Copy link
Copy Markdown
Member

💡 작업 내용

  • API 정비 및 미사용 코드 제거
  • 신규 디자인 스펙 반영
  • 구조 및 접근성 개선
  • 그룹 내 Radio.Basic ref 미연결 수정
  • Changeset 문서 작성

💡 자세한 설명

1. 개요

Radio는 이미 vanilla-extract 기반으로 마이그레이션되어 있습니다. 이번 PR에서는 신규 디자인 스펙을 반영하고, Checkbox에서 정립한 패턴에 맞춰 API와 내부 구조를 정비했습니다.

구조, 접근성, 슬롯 배치 등의 설계 배경은 Checkbox PR(#462)과 동일합니다.


2. API 변경 사항

prop 이름/값, public 타입, 보조 텍스트 컴포넌트가 변경됩니다.

AS-IS TO-BE
radioSize / radioStyle size / variant
radioStyle = "empty" | "outline" variant = "hollow" | "outlined"
radioAlign / RadioAlign 제거
Radio.SubLabel Radio.Helper
RadioStyle / RADIO_STYLE_OPTIONS RadioVariant / RADIO_VARIANT_OPTIONS
RadioSubLabelProps RadioHelperProps

또한 패키지 외부로 노출되지 않던 RadioContent 프리셋을 제거했습니다.


3. 구조 개선

다음 사항들에 대해 Checkbox와 동일한 구조를 적용했습니다.

  • Radio.Item<label>로 전환하여 인디케이터뿐 아니라 라벨/헬퍼 텍스트를 클릭해도 선택할 수 있도록 했습니다.
  • 라벨은 aria-labelledby, 헬퍼 텍스트는 aria-describedby로 input과 연결했습니다.
  • 인디케이터의 focus ring/overlay를 interaction variant로 분리해 컨테이너가 인터랙션을 담당하도록 정리했습니다. 이에 따라 .visual 리터럴 의존성과 focus 억제를 위한 globalStyle을 제거했습니다.
  • 자식 요소 배치를 위치 기반 globalStyle(:nth-child) 대신 역할 기반 슬롯 클래스로 관리하도록 변경했습니다.
  • 타이포그래피 적용 방식을 getLabelClassName으로 통일했습니다.
  • Radio.Label, Radio.Helper<span>으로 변경하고, 컨텍스트를 configitem으로 분리했습니다.

4. 신규 디자인 스펙 반영

수정 전후의 시각적 차이가 크지 않아 수정 전 스크린샷은 생략합니다.

image
  • 인디케이터 및 disabled 상태의 색상, border 스타일을 신규 디자인 스펙에 맞게 갱신했습니다.
  • disabled 상태에서 not-allowed 커서를 적용했습니다.
  • hollow variant의 overlay inset, outlined variant의 padding 값을 최신 스펙에 맞게 조정했습니다.

5. 버그 수정

그룹 모드에서 Radio.BasicuseObjectRef로 생성한 ref를 실제 <input>에 연결하지 않던 문제를 수정했습니다. 이제 forwardRef로 전달한 ref가 input DOM 요소를 정상적으로 참조합니다.

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

✅ 셀프 체크리스트

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

closes #489

Summary by CodeRabbit

  • 새 기능

    • Radio가 새로운 디자인 스펙에 맞게 업데이트되어, 크기·변형 옵션과 도움말 텍스트를 더 일관되게 사용할 수 있게 되었습니다.
    • 라디오 항목과 라벨/헬퍼 텍스트를 함께 클릭해도 선택이 동작하도록 개선되었습니다.
  • 버그 수정

    • disabled 상태의 시각 표현과 커서 동작이 개선되었습니다.
    • 그룹/단일 라디오의 선택 상태 표시와 접근성 연결이 더 안정적으로 동작합니다.
  • 문서/예시

    • 스토리북 예시가 새 사용 방식 중심으로 정리되어, 다양한 상태와 조합을 더 쉽게 확인할 수 있습니다.

@itwillbeoptimal itwillbeoptimal self-assigned this Jun 25, 2026
@itwillbeoptimal itwillbeoptimal added the ✨feature 구현, 개선 사항 관련 부분 label Jun 25, 2026
@vercel

vercel Bot commented Jun 25, 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 25, 2026 8:09am

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Radio의 공개 prop과 컨텍스트가 size/variant 중심으로 바뀌고, Item/Basic/Label/Helper 렌더링과 스타일이 새 스펙에 맞게 재구성되었습니다. 스토리와 SelectRadio 연동이 갱신됐고, 기존 RadioContent 프리셋은 제거되었습니다.

Changes

Radio 신규 스펙 반영

Layer / File(s) Summary
공개 계약 및 컨텍스트
.changeset/radio-new-spec.md, packages/jds/src/components/Radio/radio.types.ts, packages/jds/src/components/Radio/RadioContext.tsx, packages/jds/src/components/Radio/index.ts
Radio의 공개 prop, variant 타입, 컨텍스트 분리, export 목록이 size/variant/Helper 구조로 갱신됩니다.
렌더링 재구성
packages/jds/src/components/Radio/Radio.tsx
Radio.Root, Radio.Item, Radio.Basic, Radio.Label, Radio.Helper가 새 컨텍스트와 <label>/helper 구조로 렌더됩니다.
스타일 레시피
packages/jds/src/components/Radio/radio.css.ts
radioVisualradioItem recipe가 control/slot 구조, interaction variants, outlined/hollow variants로 재구성됩니다.
스토리 및 연동
packages/jds/src/components/Radio/Radio.stories.tsx, packages/jds/src/components/Select/SelectRadio.tsx
Storybook 예시가 size/state/variant/group 조합으로 교체되고, SelectRadio가 새 size prop으로 Radio.Basic을 호출합니다.

Sequence Diagram(s)

sequenceDiagram
  participant "Radio.Root" as RadioRoot
  participant "RadioConfigProvider" as RadioConfigProvider
  participant "Radio.Item" as RadioItem
  participant "RadioItemProvider" as RadioItemProvider
  participant "Radio.Basic" as RadioBasic
  participant "RadioControl" as RadioControl
  participant "Radio.Helper" as RadioHelper

  RadioRoot->>RadioConfigProvider: provide size, variant, disabled, state
  RadioItem->>RadioItemProvider: provide labelId and helperId
  RadioBasic->>RadioConfigProvider: read size and variant
  RadioBasic->>RadioControl: render input and visual span
  RadioHelper->>RadioItemProvider: signal helper mount change
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

♻refactor

Suggested reviewers

  • areumH
  • Zero-1016

Poem

깡총깡총 라디오 들판에
라벨은 포근한 <label>로 뛰놀고
Helper는 살짝 토닥이며 반짝
size와 variant가 바람에 맞춰 춤추네
오늘도 토끼는 스펙을 쏙쏙 맞췄어요 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Radio 신규 스펙 반영과 구조 개선이라는 PR의 핵심 변경을 간결하게 요약합니다.
Description check ✅ Passed 요구된 섹션과 closes #489를 포함하고, 작업 내용·세부 설명·체크리스트가 대부분 충족됩니다.
Linked Issues check ✅ Passed 직접 이슈 #489의 신규 Radio 스펙 반영 요구를 prop·타입·구조·스타일 변경으로 충족합니다.
Out of Scope Changes check ✅ Passed 스토리와 변경세트 추가를 제외하면, 변경 대부분이 Radio 신규 스펙과 구조 개선 범위에 맞습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/489-radio-new-spec

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: 3

🤖 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 @.changeset/radio-new-spec.md:
- Around line 1-3: The changeset version bump for `@jects/jds` should remain minor
because the package is still on 0.x and Breaking Change handling follows 0.x
semver conventions; keep the frontmatter in the changeset file as minor and do
not change it to patch or major.

In `@packages/jds/src/components/Radio/Radio.tsx`:
- Line 181: The Radio input is currently allowing external props to override its
semantic type, which can break it by rendering as something other than a radio.
Update RadioBasicProps to omit type as well, and in the Radio component’s
mergeProps/inputProps composition ensure the internal type: "radio" is applied
last so grouped and standalone paths both cannot be overridden by restProps or
inputProps.
- Around line 125-130: `Radio`의 input props 병합에서 `aria-labelledby`와
`aria-describedby`를 무조건 덮어쓰는 문제가 있습니다. `Radio.Item` 내부의 `Radio.Label` 존재 여부를
추적하는 상태(예: `hasLabel`)를 `Radio.Label`/`hasHelper`와 같은 방식으로 관리해 `labelId`는 실제 라벨이
mount된 경우에만 넣고, `inputProps`에 이미 들어온 ARIA id는 유지하면서 생성한 id와 병합하도록 `Radio` 컴포넌트의
input 렌더링 로직을 수정하세요.
🪄 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: 335a8ed8-b96c-4bf1-8753-48e3dde4b08a

📥 Commits

Reviewing files that changed from the base of the PR and between 97d5bf9 and b2a6b80.

📒 Files selected for processing (11)
  • .changeset/radio-new-spec.md
  • packages/jds/src/components/Radio/Radio.stories.tsx
  • packages/jds/src/components/Radio/Radio.tsx
  • packages/jds/src/components/Radio/RadioContext.tsx
  • packages/jds/src/components/Radio/index.ts
  • packages/jds/src/components/Radio/preset/RadioContent.stories.tsx
  • packages/jds/src/components/Radio/preset/RadioContent.tsx
  • packages/jds/src/components/Radio/preset/radioContent.types.ts
  • packages/jds/src/components/Radio/radio.css.ts
  • packages/jds/src/components/Radio/radio.types.ts
  • packages/jds/src/components/Select/SelectRadio.tsx
💤 Files with no reviewable changes (3)
  • packages/jds/src/components/Radio/preset/RadioContent.tsx
  • packages/jds/src/components/Radio/preset/radioContent.types.ts
  • packages/jds/src/components/Radio/preset/RadioContent.stories.tsx

Comment on lines +1 to +3
---
"@jects/jds": minor
---

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: `@jects/jds` 현재 버전과 changeset 정책 근거를 확인합니다.
fd -a 'package.json' . | xargs -I{} sh -c 'printf "\n--- %s ---\n" "$1"; jq -r ".name, .version" "$1" 2>/dev/null' sh {}
rg -n -C2 '"`@jects/jds`"|changeset|semver|major|minor|breaking' README.md .changeset package.json .github 2>/dev/null

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

Length of output: 6525


Breaking Change 에 대해 minor 로 설정하신 것이 적절합니다.

현재 @jects/jds0.4.0 버전 (0.x 대) 이며, 시맨틱 버저닝 표준에 따라 0.x 버전에서는 Breaking Change 를 minor 증가로 처리하는 것이 일반적입니다.

---
"`@jects/jds`": minor
---
🤖 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/radio-new-spec.md around lines 1 - 3, The changeset version bump
for `@jects/jds` should remain minor because the package is still on 0.x and
Breaking Change handling follows 0.x semver conventions; keep the frontmatter in
the changeset file as minor and do not change it to patch or major.

Comment on lines +125 to +130
<input
{...inputProps}
ref={inputRef}
aria-labelledby={isWithinItem ? labelId : undefined}
aria-describedby={describedById}
className={radioInput}

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 | 🏗️ Heavy lift

aria-labelledby를 무조건 덮어쓰지 마세요.

Radio.Item 안에 Radio.Label이 없으면 Line 128이 존재하지 않는 id를 참조하고, wrapping <label>의 native label 계산도 aria-labelledby에 의해 가려질 수 있습니다. 소비자가 넘긴 aria-describedby도 Line 129에서 덮어써집니다. Label mount 여부를 추적하거나, 기존 ARIA id와 생성 id를 병합해 주세요.

수정 방향 예시
-        aria-labelledby={isWithinItem ? labelId : undefined}
-        aria-describedby={describedById}
+        aria-labelledby={
+          [inputProps["aria-labelledby"], isWithinItem ? labelId : undefined]
+            .filter(Boolean)
+            .join(" ") || undefined
+        }
+        aria-describedby={
+          [inputProps["aria-describedby"], describedById].filter(Boolean).join(" ") ||
+          undefined
+        }

labelId는 실제 Radio.Label이 mount된 경우에만 전달되도록 hasLabelhasHelper와 같은 방식으로 관리하는 편이 안전합니다.

📝 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
<input
{...inputProps}
ref={inputRef}
aria-labelledby={isWithinItem ? labelId : undefined}
aria-describedby={describedById}
className={radioInput}
<input
{...inputProps}
ref={inputRef}
aria-labelledby={
[inputProps["aria-labelledby"], isWithinItem ? labelId : undefined]
.filter(Boolean)
.join(" ") || undefined
}
aria-describedby={
[inputProps["aria-describedby"], describedById].filter(Boolean).join(" ") ||
undefined
}
className={radioInput}
🤖 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/Radio/Radio.tsx` around lines 125 - 130, `Radio`의
input props 병합에서 `aria-labelledby`와 `aria-describedby`를 무조건 덮어쓰는 문제가 있습니다.
`Radio.Item` 내부의 `Radio.Label` 존재 여부를 추적하는 상태(예: `hasLabel`)를
`Radio.Label`/`hasHelper`와 같은 방식으로 관리해 `labelId`는 실제 라벨이 mount된 경우에만 넣고,
`inputProps`에 이미 들어온 ARIA id는 유지하면서 생성한 id와 병합하도록 `Radio` 컴포넌트의 input 렌더링 로직을
수정하세요.

size={size}
interaction={interaction}
inputRef={ref}
inputProps={mergeProps(inputProps, restProps, { onChange })}

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

외부 props가 radio input의 type을 바꾸지 못하게 막아 주세요.

RadioBasicPropstype을 허용하고, grouped/standalone 병합에서 외부 props가 내부 radio 속성보다 뒤에 적용될 수 있어 type='checkbox' 같은 값이 실제 input semantics를 깨뜨릴 수 있습니다. 타입에서 type을 제외하고 렌더링에서도 type: "radio"가 마지막에 보장되게 해 주세요.

수정 방향 예시
-  inputProps={mergeProps(inputProps, restProps, { onChange })}
+  inputProps={mergeProps(restProps, inputProps, { onChange, type: "radio" })}
-        inputProps={{
-          type: "radio",
+        inputProps={{
+          ...restProps,
           value,
           checked,
           disabled: isDisabled,
           onChange,
           name,
-          ...restProps,
+          type: "radio",
         }}

그리고 RadioBasicProps에서도 typeOmit 대상에 추가해 주세요.

Also applies to: 224-231

🤖 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/Radio/Radio.tsx` at line 181, The Radio input is
currently allowing external props to override its semantic type, which can break
it by rendering as something other than a radio. Update RadioBasicProps to omit
type as well, and in the Radio component’s mergeProps/inputProps composition
ensure the internal type: "radio" is applied last so grouped and standalone
paths both cannot be overridden by restProps or inputProps.

@Zero-1016 Zero-1016 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

큰 변경사항은 없는 것을 확인하였습니다. Checkbox와 동일한 구조로 변했네요.

코멘트 하나만 확인 부탁드립니다. 🙏

export interface RadioBasicProps extends Omit<ComponentPropsWithoutRef<"input">, "value"> {
radioSize?: RadioSize;
export interface RadioBasicProps
extends Omit<ComponentPropsWithoutRef<"input">, "value" | "size"> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Radio와 Checkbox 둘 다, type으로 override가 가능한 것을 확인하였어요. 해당 props를 받지 않도록 해야할 것 같아요~

<Radio.Basic value='lg' size='lg' type='radio' />

@itwillbeoptimal itwillbeoptimal Jun 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

확인했습니다~ 두 컴포넌트 모두 수정해야 하는 사항이라 머지된 후에 묶어서 별도 작업으로 진행하겠습니다 🙂

@Zero-1016 Zero-1016 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

요 PR은 승인할게요~

@itwillbeoptimal itwillbeoptimal merged commit a72ce8e into dev Jun 29, 2026
5 of 6 checks passed
@itwillbeoptimal itwillbeoptimal deleted the feat/489-radio-new-spec branch June 29, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

feat: (JDS) Radio 신규 스펙 반영

3 participants