Skip to Content

Radio

개요

Radio는 사용자가 여러 옵션 중 하나만 선택할 수 있는 폼 컴포넌트입니다. Cals 예약 관리 시스템에서는 예약 타입 선택, 결제 방법 선택, 시간대 선택 등 단일 선택이 필요한 상황에서 활용됩니다.

Cals 특화 기능:

  • 예약 상태별 색상 시스템으로 선택 옵션 시각화
  • 예약 타입(즉시/승인 필요) 선택 최적화
  • 결제 방법 및 서비스 패키지 단일 선택
  • 시간대 및 날짜 선택을 위한 라디오 그룹

설치

npx @vortex/cli add radio-group --package cals

기본 사용법

import "@vortex/ui-cals/theme"; import { RadioGroup, RadioGroupItem } from "@vortex/ui-cals"; export default function ReservationType() { return ( <RadioGroup defaultValue="instant"> <div className="flex items-center space-x-2"> <RadioGroupItem value="instant" id="instant" /> <label htmlFor="instant">즉시 예약</label> </div> <div className="flex items-center space-x-2"> <RadioGroupItem value="approval" id="approval" /> <label htmlFor="approval">승인 후 예약</label> </div> </RadioGroup> ); }

Variants

Default

기본 스타일의 Radio입니다.

<RadioGroup defaultValue="option1"> <div className="flex items-center space-x-2"> <RadioGroupItem value="option1" id="option1" /> <label htmlFor="option1">옵션 1</label> </div> <div className="flex items-center space-x-2"> <RadioGroupItem value="option2" id="option2" /> <label htmlFor="option2">옵션 2</label> </div> </RadioGroup>

With Label

레이블과 함께 사용하는 Radio입니다.

<RadioGroup defaultValue="option1"> <div className="flex items-center space-x-2"> <RadioGroupItem value="option1" id="r-option1" /> <label htmlFor="r-option1" className="text-sm font-medium"> 레이블이 있는 옵션 1 </label> </div> <div className="flex items-center space-x-2"> <RadioGroupItem value="option2" id="r-option2" /> <label htmlFor="r-option2" className="text-sm font-medium"> 레이블이 있는 옵션 2 </label> </div> </RadioGroup>

With Description

설명과 함께 사용하는 Radio입니다.

<RadioGroup defaultValue="option1"> <div className="flex items-start space-x-2"> <RadioGroupItem value="option1" id="desc-option1" className="mt-1" /> <div> <label htmlFor="desc-option1" className="text-sm font-medium"> 옵션 제목 </label> <p className="text-sm text-gray-500">옵션에 대한 상세 설명</p> </div> </div> </RadioGroup>

Sizes

Small

작은 크기의 Radio입니다.

<RadioGroupItem value="small" className="h-4 w-4" />

Medium (Default)

기본 크기의 Radio입니다.

<RadioGroupItem value="medium" className="h-5 w-5" />

Large

큰 크기의 Radio입니다.

<RadioGroupItem value="large" className="h-6 w-6" />

States

Default

기본 상태의 Radio입니다.

<RadioGroupItem value="default" />

Selected

선택된 Radio입니다.

<RadioGroup value="selected"> <RadioGroupItem value="selected" /> </RadioGroup>

Disabled

비활성화된 Radio입니다.

<RadioGroupItem value="disabled" disabled />

Disabled Selected

비활성화되고 선택된 Radio입니다.

<RadioGroup value="disabled-selected"> <RadioGroupItem value="disabled-selected" disabled /> </RadioGroup>

🆕 Cals 브랜딩

브랜드 컬러

Cals Radio는 기본적으로 Cals의 Primary 색상(Pink #e91e63)을 선택 상태에서 사용합니다.

<div className="space-y-4"> {/* Primary (Pink) - 기본 선택 */} <RadioGroup defaultValue="primary"> <div className="flex items-center space-x-2"> <RadioGroupItem value="primary" id="primary" className="border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <label htmlFor="primary">Primary (Pink) - 기본 서비스</label> </div> </RadioGroup> {/* Secondary (Blue) - 정보성 선택 */} <RadioGroup defaultValue="secondary"> <div className="flex items-center space-x-2"> <RadioGroupItem value="secondary" id="secondary" className="border-[#03a9f4] data-[state=checked]:bg-[#03a9f4] data-[state=checked]:border-[#03a9f4]" /> <label htmlFor="secondary">Secondary (Blue) - 알림 방식</label> </div> </RadioGroup> {/* Accent (Purple) - 강조 선택 */} <RadioGroup defaultValue="accent"> <div className="flex items-center space-x-2"> <RadioGroupItem value="accent" id="accent" className="border-[#9c27b0] data-[state=checked]:bg-[#9c27b0] data-[state=checked]:border-[#9c27b0]" /> <label htmlFor="accent">Accent (Purple) - 프리미엄 패키지</label> </div> </RadioGroup> </div>

예약 상태 컬러

Cals Radio는 예약 관리의 5가지 상태를 반영하여 선택 옵션의 시각적 피드백을 제공합니다.

<RadioGroup defaultValue="available"> <div className="space-y-3"> {/* Available - 선택 가능 */} <div className="flex items-center space-x-2"> <RadioGroupItem value="available" id="status-available" className="border-[#4caf50] data-[state=checked]:bg-[#4caf50] data-[state=checked]:border-[#4caf50]" /> <label htmlFor="status-available" className="text-sm"> <span className="font-medium">예약 가능</span> <span className="text-gray-500"> - 즉시 예약 가능한 시간</span> </label> </div> {/* Pending - 승인 필요 */} <div className="flex items-center space-x-2"> <RadioGroupItem value="pending" id="status-pending" disabled className="border-[#ff9800] data-[state=checked]:bg-[#ff9800] data-[state=checked]:border-[#ff9800]" /> <label htmlFor="status-pending" className="text-sm text-gray-500"> <span className="font-medium">승인 대기</span> <span> - 담당자 승인 필요</span> </label> </div> {/* Confirmed - 확정됨 */} <div className="flex items-center space-x-2"> <RadioGroupItem value="confirmed" id="status-confirmed" disabled className="border-[#03a9f4] data-[state=checked]:bg-[#03a9f4] data-[state=checked]:border-[#03a9f4]" /> <label htmlFor="status-confirmed" className="text-sm text-gray-500"> <span className="font-medium">예약 확정</span> <span> - 이미 예약된 시간</span> </label> </div> {/* Cancelled - 취소됨 */} <div className="flex items-center space-x-2"> <RadioGroupItem value="cancelled" id="status-cancelled" disabled className="border-[#f44336] data-[state=checked]:bg-[#f44336] data-[state=checked]:border-[#f44336]" /> <label htmlFor="status-cancelled" className="text-sm text-gray-500 line-through" > <span className="font-medium">예약 취소</span> <span> - 취소된 시간</span> </label> </div> {/* Completed - 완료됨 */} <div className="flex items-center space-x-2"> <RadioGroupItem value="completed" id="status-completed" disabled className="border-[#9c27b0] data-[state=checked]:bg-[#9c27b0] data-[state=checked]:border-[#9c27b0]" /> <label htmlFor="status-completed" className="text-sm text-gray-500"> <span className="font-medium">서비스 완료</span> <span> - 완료된 서비스</span> </label> </div> </div> </RadioGroup>

Cals 특화 사용 가이드

예약 타입 선택

<div className="space-y-3"> <h3 className="text-sm font-medium text-gray-900"> 예약 방식 선택 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup defaultValue="instant"> <div className="flex items-start space-x-3 p-3 border border-gray-200 rounded-md hover:border-[#e91e63] transition-colors"> <RadioGroupItem value="instant" id="instant" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="instant" className="text-sm font-medium cursor-pointer"> 즉시 예약 </label> <p className="text-xs text-gray-500 mt-1"> 선택 즉시 예약이 확정됩니다. 별도 승인 과정 없이 바로 서비스를 받을 수 있습니다. </p> </div> </div> <div className="flex items-start space-x-3 p-3 border border-gray-200 rounded-md hover:border-[#e91e63] transition-colors"> <RadioGroupItem value="approval" id="approval" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="approval" className="text-sm font-medium cursor-pointer" > 승인 후 예약 </label> <p className="text-xs text-gray-500 mt-1"> 담당자가 예약을 검토한 후 승인됩니다. 승인까지 최대 1시간 소요될 수 있습니다. </p> </div> </div> </RadioGroup> </div>

결제 방법 선택

<div className="space-y-3"> <h3 className="text-sm font-medium text-gray-900"> 결제 방법 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup defaultValue="card"> <div className="flex items-center space-x-3 p-3 border border-gray-200 rounded-md"> <RadioGroupItem value="card" id="card" className="border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <label htmlFor="card" className="text-sm font-medium cursor-pointer flex-1" > 신용/체크카드 </label> <span className="text-xs text-[#4caf50] font-medium">추천</span> </div> <div className="flex items-center space-x-3 p-3 border border-gray-200 rounded-md"> <RadioGroupItem value="cash" id="cash" className="border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <label htmlFor="cash" className="text-sm font-medium cursor-pointer"> 현장 결제 (현금) </label> </div> <div className="flex items-center space-x-3 p-3 border border-gray-200 rounded-md"> <RadioGroupItem value="transfer" id="transfer" className="border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <label htmlFor="transfer" className="text-sm font-medium cursor-pointer"> 계좌 이체 </label> </div> </RadioGroup> </div>

서비스 패키지 선택

<div className="space-y-3"> <h3 className="text-sm font-medium text-gray-900"> 서비스 패키지 선택 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup defaultValue="basic"> <div className="flex items-start space-x-3 p-4 border-2 border-gray-200 rounded-lg hover:border-[#e91e63] transition-colors"> <RadioGroupItem value="basic" id="basic" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="basic" className="text-sm font-medium cursor-pointer"> 기본 패키지 </label> <p className="text-xs text-gray-500 mt-1">컷 + 샴푸 (50분)</p> <p className="text-base font-semibold text-[#e91e63] mt-2">₩40,000</p> </div> </div> <div className="flex items-start space-x-3 p-4 border-2 border-[#9c27b0] bg-purple-50 rounded-lg"> <RadioGroupItem value="premium" id="premium" className="mt-1 border-[#9c27b0] data-[state=checked]:bg-[#9c27b0] data-[state=checked]:border-[#9c27b0]" /> <div className="flex-1"> <label htmlFor="premium" className="text-sm font-medium cursor-pointer"> 프리미엄 패키지 </label> <p className="text-xs text-gray-500 mt-1"> 컷 + 펌 + 트리트먼트 + 샴푸 (150분) </p> <div className="flex items-center gap-2 mt-2"> <p className="text-base font-semibold text-[#9c27b0]">₩180,000</p> <span className="text-xs text-gray-500 line-through">₩200,000</span> <span className="text-xs text-[#9c27b0] font-medium">10% 할인</span> </div> </div> <span className="text-xs bg-[#9c27b0] text-white px-2 py-1 rounded-full"> 인기 </span> </div> <div className="flex items-start space-x-3 p-4 border-2 border-gray-200 rounded-lg hover:border-[#e91e63] transition-colors"> <RadioGroupItem value="deluxe" id="deluxe" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="deluxe" className="text-sm font-medium cursor-pointer"> 디럭스 패키지 </label> <p className="text-xs text-gray-500 mt-1"> 컷 + 염색 + 클리닉 + 트리트먼트 + 샴푸 (180분) </p> <p className="text-base font-semibold text-[#e91e63] mt-2">₩280,000</p> </div> </div> </RadioGroup> </div>

🆕 Foundation/iCignal과의 차이점

속성FoundationiCignalCals
Primary Color#3B82F6 (Blue)#2196f3 (Blue)#e91e63 (Pink)
Secondary Color#10B981 (Green)#4caf50 (Green)#03a9f4 (Blue)
Accent Color#8B5CF6 (Purple)#ff5722 (Deep Orange)#9c27b0 (Purple)
사용 맥락범용 웹 애플리케이션Analytics 대시보드예약 관리 시스템
예약 상태없음없음5가지 상태 (Available/Pending/Confirmed/Cancelled/Completed)
주요 사용범용 단일 선택필터 옵션 선택예약 타입, 결제 방법, 서비스 패키지 선택
선택 스타일Blue backgroundBlue backgroundPink background (Primary)
카드형 레이아웃기본 레이아웃없음패키지 선택 카드, 상세 설명 포함

사용 시나리오 비교

Foundation Radio: 범용 단일 선택, 설정 옵션, 항목 선택 iCignal Radio: 데이터 필터, 차트 타입 선택, 분석 기간 선택 Cals Radio: 예약 타입 선택, 결제 방법 선택, 서비스 패키지 선택, 시간대 선택

Props API

RadioGroup

PropTypeDefaultDescription
valuestring-선택된 값 (제어 컴포넌트)
defaultValuestring-기본 선택 값 (비제어 컴포넌트)
onValueChange(value: string) => void-값 변경 이벤트 핸들러
disabledbooleanfalse전체 그룹 비활성화 여부
namestring-Form name 속성
requiredbooleanfalse필수 선택 여부
classNamestring-추가 CSS 클래스

RadioGroupItem

PropTypeDefaultDescription
valuestring-Radio 값 (필수)
idstring-HTML id 속성 (label 연결용)
disabledbooleanfalse비활성화 여부
classNamestring-추가 CSS 클래스

접근성

Radio 컴포넌트는 웹 접근성 표준을 준수합니다:

  • Radix UI 기반: WAI-ARIA 1.2 Radio Group 패턴 구현
  • 시맨틱 마크업: role="radiogroup"role="radio" 사용
  • 키보드 네비게이션:
    • Arrow Up/Down 또는 Arrow Left/Right: 옵션 간 이동 및 선택
    • Tab: 다음 Radio Group으로 이동
    • Shift + Tab: 이전 Radio Group으로 이동
    • Space: 현재 옵션 선택
  • 스크린 리더: aria-checked 상태 안내
  • Label 연결: htmlForid를 통한 label-radio 연결
  • Focus 관리: Roving tabindex로 그룹 내 포커스 관리
  • Required 표시: aria-required="true" 지원

접근성 예제

<div> <h3 id="payment-method" className="text-sm font-medium text-gray-900 mb-2"> 결제 방법{" "} <span className="text-[#e91e63]" aria-label="필수"> * </span> </h3> <RadioGroup aria-labelledby="payment-method" required aria-required="true"> <div className="flex items-center space-x-2"> <RadioGroupItem value="card" id="payment-card" /> <label htmlFor="payment-card" className="text-sm"> 신용/체크카드 </label> </div> <div className="flex items-center space-x-2"> <RadioGroupItem value="cash" id="payment-cash" /> <label htmlFor="payment-cash" className="text-sm"> 현장 결제 </label> </div> </RadioGroup> </div>

예제

예약 타입 선택 폼

import { RadioGroup, RadioGroupItem } from "@vortex/ui-cals"; import { useState } from "react"; export default function ReservationTypeForm() { const [reservationType, setReservationType] = useState("instant"); return ( <div className="space-y-4 max-w-md"> <h3 className="text-base font-semibold text-gray-900"> 예약 방식 선택 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup value={reservationType} onValueChange={setReservationType}> <div className="flex items-start space-x-3 p-4 border-2 border-gray-200 rounded-lg hover:border-[#e91e63] transition-colors cursor-pointer"> <RadioGroupItem value="instant" id="instant-reservation" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="instant-reservation" className="text-sm font-medium cursor-pointer" > 즉시 예약 </label> <p className="text-xs text-gray-500 mt-1"> 선택 즉시 예약이 확정됩니다. 별도 승인 과정 없이 바로 서비스를 받을 수 있습니다. </p> <div className="flex items-center gap-2 mt-2"> <span className="text-xs bg-[#4caf50] text-white px-2 py-0.5 rounded"> 빠른 예약 </span> <span className="text-xs text-gray-500"> 평균 처리 시간: 즉시 </span> </div> </div> </div> <div className="flex items-start space-x-3 p-4 border-2 border-gray-200 rounded-lg hover:border-[#e91e63] transition-colors cursor-pointer"> <RadioGroupItem value="approval" id="approval-reservation" className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor="approval-reservation" className="text-sm font-medium cursor-pointer" > 승인 후 예약 </label> <p className="text-xs text-gray-500 mt-1"> 담당자가 예약을 검토한 후 승인됩니다. 승인까지 최대 1시간 소요될 수 있습니다. </p> <div className="flex items-center gap-2 mt-2"> <span className="text-xs bg-[#ff9800] text-white px-2 py-0.5 rounded"> 승인 필요 </span> <span className="text-xs text-gray-500"> 평균 처리 시간: 1시간 이내 </span> </div> </div> </div> </RadioGroup> {reservationType === "instant" && ( <div className="p-3 bg-green-50 rounded-md border border-green-200"> <p className="text-sm text-green-800"> 즉시 예약이 가능한 시간입니다. 예약 후 바로 서비스를 받으실 수 있습니다. </p> </div> )} {reservationType === "approval" && ( <div className="p-3 bg-orange-50 rounded-md border border-orange-200"> <p className="text-sm text-orange-800"> 담당자 승인 후 예약이 확정됩니다. SMS/이메일로 승인 알림을 받으실 수 있습니다. </p> </div> )} </div> ); }

결제 방법 선택 폼

import { RadioGroup, RadioGroupItem } from "@vortex/ui-cals"; import { CreditCard, Banknote, Building2 } from "lucide-react"; import { useState } from "react"; export default function PaymentMethodForm() { const [paymentMethod, setPaymentMethod] = useState("card"); const methods = [ { value: "card", label: "신용/체크카드", description: "즉시 결제되며, 모든 카드사 사용 가능", icon: CreditCard, badge: "추천", badgeColor: "bg-[#4caf50]", }, { value: "cash", label: "현장 결제 (현금)", description: "서비스 완료 후 현금으로 결제", icon: Banknote, badge: null, }, { value: "transfer", label: "계좌 이체", description: "예약 후 24시간 내 입금 확인", icon: Building2, badge: null, }, ]; return ( <div className="space-y-4 max-w-md"> <h3 className="text-base font-semibold text-gray-900"> 결제 방법 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup value={paymentMethod} onValueChange={setPaymentMethod}> {methods.map((method) => { const Icon = method.icon; return ( <div key={method.value} className={` flex items-center space-x-3 p-4 border-2 rounded-lg transition-all cursor-pointer ${ paymentMethod === method.value ? "border-[#e91e63] bg-pink-50" : "border-gray-200 hover:border-[#e91e63]" } `} > <RadioGroupItem value={method.value} id={method.value} className="border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <Icon className="h-5 w-5 text-gray-600" /> <div className="flex-1"> <label htmlFor={method.value} className="text-sm font-medium cursor-pointer" > {method.label} </label> <p className="text-xs text-gray-500 mt-0.5"> {method.description} </p> </div> {method.badge && ( <span className={`text-xs text-white px-2 py-1 rounded-full ${method.badgeColor}`} > {method.badge} </span> )} </div> ); })} </RadioGroup> <div className="p-3 bg-gray-50 rounded-md"> <p className="text-xs text-gray-600"> 💡 결제 수단은 예약 후에도 변경 가능합니다 </p> </div> </div> ); }

서비스 패키지 선택 폼

import { RadioGroup, RadioGroupItem } from "@vortex/ui-cals"; import { useState } from "react"; export default function ServicePackageForm() { const [selectedPackage, setSelectedPackage] = useState("premium"); const packages = [ { value: "basic", name: "기본 패키지", description: "컷 + 샴푸", duration: 50, price: 40000, originalPrice: null, discount: null, badge: null, borderColor: "border-gray-200", bgColor: "", }, { value: "premium", name: "프리미엄 패키지", description: "컷 + 펌 + 트리트먼트 + 샴푸", duration: 150, price: 180000, originalPrice: 200000, discount: "10% 할인", badge: "인기", borderColor: "border-[#9c27b0]", bgColor: "bg-purple-50", }, { value: "deluxe", name: "디럭스 패키지", description: "컷 + 염색 + 클리닉 + 트리트먼트 + 샴푸", duration: 180, price: 280000, originalPrice: null, discount: null, badge: null, borderColor: "border-gray-200", bgColor: "", }, ]; return ( <div className="space-y-4 max-w-md"> <h3 className="text-base font-semibold text-gray-900"> 서비스 패키지 선택 <span className="text-[#e91e63]">*</span> </h3> <RadioGroup value={selectedPackage} onValueChange={setSelectedPackage}> {packages.map((pkg) => ( <div key={pkg.value} className={` flex items-start space-x-3 p-4 border-2 rounded-lg transition-all cursor-pointer ${pkg.bgColor} ${ selectedPackage === pkg.value ? "border-[#e91e63] ring-2 ring-[#e91e63]/20" : pkg.borderColor } `} > <RadioGroupItem value={pkg.value} id={pkg.value} className="mt-1 border-[#e91e63] data-[state=checked]:bg-[#e91e63] data-[state=checked]:border-[#e91e63]" /> <div className="flex-1"> <label htmlFor={pkg.value} className="text-sm font-medium cursor-pointer" > {pkg.name} </label> <p className="text-xs text-gray-500 mt-1">{pkg.description}</p> <p className="text-xs text-gray-400 mt-1"> 소요 시간: {pkg.duration}분 </p> <div className="flex items-center gap-2 mt-2"> <p className="text-base font-semibold text-[#e91e63]"> ₩{pkg.price.toLocaleString()} </p> {pkg.originalPrice && ( <> <span className="text-xs text-gray-500 line-through"> ₩{pkg.originalPrice.toLocaleString()} </span> <span className="text-xs text-[#9c27b0] font-medium"> {pkg.discount} </span> </> )} </div> </div> {pkg.badge && ( <span className="text-xs bg-[#9c27b0] text-white px-2 py-1 rounded-full"> {pkg.badge} </span> )} </div> ))} </RadioGroup> <div className="p-4 bg-pink-50 rounded-lg border border-[#e91e63]/20"> <p className="text-sm font-medium text-[#e91e63]"> 선택한 패키지:{" "} {packages.find((p) => p.value === selectedPackage)?.name} </p> <p className="text-sm text-gray-700 mt-1"> 총 비용: ₩ {packages .find((p) => p.value === selectedPackage) ?.price.toLocaleString()} </p> <p className="text-xs text-gray-500 mt-1"> 소요 시간:{" "} {packages.find((p) => p.value === selectedPackage)?.duration}분 </p> </div> </div> ); }

관련 컴포넌트

Last updated on