Switch
개요
Switch는 사용자가 설정을 On/Off로 토글할 수 있는 폼 컴포넌트입니다. Cals 예약 관리 시스템에서는 알림 설정, 자동 승인 설정, 예약 공개/비공개 전환 등 즉시 적용되는 설정에서 활용됩니다.
Cals 특화 기능:
- 예약 상태별 색상 시스템으로 토글 상태 시각화
- 알림 및 자동 승인 설정 최적화
- 예약 공개/비공개 즉시 전환
- 관리자 설정을 위한 다양한 토글 옵션
설치
npx @vortex/cli add switch --package cals기본 사용법
import "@vortex/ui-cals/theme";
import { Switch } from "@vortex/ui-cals";
export default function NotificationSettings() {
return (
<div className="flex items-center space-x-2">
<Switch id="sms-notification" />
<label htmlFor="sms-notification" className="text-sm font-medium">
SMS 알림
</label>
</div>
);
}Variants
Default
기본 스타일의 Switch입니다.
<Switch />With Label
레이블과 함께 사용하는 Switch입니다.
<div className="flex items-center space-x-2">
<Switch id="option1" />
<label htmlFor="option1" className="text-sm font-medium">
레이블이 있는 스위치
</label>
</div>With Description
설명과 함께 사용하는 Switch입니다.
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="option2" className="text-sm font-medium">
알림 설정
</label>
<p className="text-xs text-gray-500">예약 관련 알림을 받습니다</p>
</div>
<Switch id="option2" />
</div>Sizes
Small
작은 크기의 Switch입니다.
<Switch className="h-5 w-9" />Medium (Default)
기본 크기의 Switch입니다.
<Switch className="h-6 w-11" />Large
큰 크기의 Switch입니다.
<Switch className="h-7 w-14" />States
Default (Off)
기본 상태의 Switch입니다.
<Switch />Checked (On)
활성화된 Switch입니다.
<Switch checked />Disabled
비활성화된 Switch입니다.
<Switch disabled />Disabled Checked
비활성화되고 활성화된 Switch입니다.
<Switch checked disabled />🆕 Cals 브랜딩
브랜드 컬러
Cals Switch는 기본적으로 Cals의 Primary 색상(Pink #e91e63)을 활성화 상태에서 사용합니다.
<div className="space-y-4">
{/* Primary (Pink) - 기본 활성화 */}
<div className="flex items-center space-x-2">
<Switch
id="primary"
checked
className="data-[state=checked]:bg-[#e91e63]"
/>
<label htmlFor="primary">Primary (Pink) - 기본 알림</label>
</div>
{/* Secondary (Blue) - 정보성 설정 */}
<div className="flex items-center space-x-2">
<Switch
id="secondary"
checked
className="data-[state=checked]:bg-[#03a9f4]"
/>
<label htmlFor="secondary">Secondary (Blue) - 이메일 알림</label>
</div>
{/* Accent (Purple) - 강조 설정 */}
<div className="flex items-center space-x-2">
<Switch id="accent" checked className="data-[state=checked]:bg-[#9c27b0]" />
<label htmlFor="accent">Accent (Purple) - 프리미엄 기능</label>
</div>
</div>예약 상태 컬러
Cals Switch는 예약 관리의 5가지 상태를 반영하여 설정의 시각적 피드백을 제공합니다.
<div className="space-y-3">
{/* Available - 활성화 가능 */}
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="available" className="text-sm font-medium">
예약 접수 활성화
</label>
<p className="text-xs text-gray-500">새로운 예약을 받을 수 있습니다</p>
</div>
<Switch
id="available"
checked
className="data-[state=checked]:bg-[#4caf50]"
/>
</div>
{/* Pending - 대기 중 */}
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="pending" className="text-sm font-medium text-gray-500">
수동 승인 모드
</label>
<p className="text-xs text-gray-500">예약 승인 대기 중</p>
</div>
<Switch
id="pending"
checked
disabled
className="data-[state=checked]:bg-[#ff9800]"
/>
</div>
{/* Confirmed - 자동 승인 */}
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="confirmed" className="text-sm font-medium">
자동 승인
</label>
<p className="text-xs text-gray-500">예약이 즉시 확정됩니다</p>
</div>
<Switch
id="confirmed"
checked
className="data-[state=checked]:bg-[#03a9f4]"
/>
</div>
{/* Cancelled - 비활성화 */}
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="cancelled" className="text-sm font-medium text-gray-500">
예약 접수 중단
</label>
<p className="text-xs text-gray-500">예약을 받지 않습니다</p>
</div>
<Switch id="cancelled" className="data-[state=checked]:bg-[#f44336]" />
</div>
{/* Completed - 완료 모드 */}
<div className="flex items-center justify-between max-w-sm">
<div>
<label htmlFor="completed" className="text-sm font-medium">
완료 후 피드백 요청
</label>
<p className="text-xs text-gray-500">서비스 완료 시 리뷰 요청</p>
</div>
<Switch
id="completed"
checked
className="data-[state=checked]:bg-[#9c27b0]"
/>
</div>
</div>Cals 특화 사용 가이드
알림 설정
<div className="space-y-4 max-w-md">
<h3 className="text-sm font-semibold text-gray-900">알림 설정</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label htmlFor="sms" className="text-sm font-medium cursor-pointer">
SMS 알림
</label>
<p className="text-xs text-gray-500 mt-0.5">
예약 확정, 변경, 당일 알림을 문자로 받습니다
</p>
</div>
<Switch
id="sms"
defaultChecked
className="data-[state=checked]:bg-[#03a9f4]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label htmlFor="email" className="text-sm font-medium cursor-pointer">
이메일 알림
</label>
<p className="text-xs text-gray-500 mt-0.5">
예약 내역 및 영수증을 이메일로 받습니다
</p>
</div>
<Switch
id="email"
defaultChecked
className="data-[state=checked]:bg-[#03a9f4]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label htmlFor="push" className="text-sm font-medium cursor-pointer">
앱 푸시 알림
</label>
<p className="text-xs text-gray-500 mt-0.5">
모바일 앱에서 실시간 알림을 받습니다
</p>
</div>
<Switch id="push" className="data-[state=checked]:bg-[#03a9f4]" />
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="marketing"
className="text-sm font-medium cursor-pointer"
>
마케팅 알림
</label>
<p className="text-xs text-gray-500 mt-0.5">
이벤트, 프로모션, 쿠폰 정보를 받습니다
</p>
</div>
<Switch id="marketing" className="data-[state=checked]:bg-[#9c27b0]" />
</div>
</div>
</div>자동 승인 설정 (관리자)
<div className="space-y-4 max-w-md">
<h3 className="text-sm font-semibold text-gray-900">예약 관리 설정</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 border-2 border-gray-200 rounded-lg">
<div className="flex-1">
<label
htmlFor="auto-approve"
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>
<Switch
id="auto-approve"
defaultChecked
className="data-[state=checked]:bg-[#03a9f4]"
/>
</div>
<div className="flex items-center justify-between p-4 border-2 border-gray-200 rounded-lg">
<div className="flex-1">
<label
htmlFor="waiting-list"
className="text-sm font-medium cursor-pointer"
>
대기 명단 활성화
</label>
<p className="text-xs text-gray-500 mt-1">
예약 취소 시 대기자에게 자동 안내
</p>
</div>
<Switch id="waiting-list" className="data-[state=checked]:bg-[#ff9800]" />
</div>
<div className="flex items-center justify-between p-4 border-2 border-gray-200 rounded-lg">
<div className="flex-1">
<label
htmlFor="double-booking"
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-[#e91e63] text-white px-2 py-0.5 rounded">
권장
</span>
</div>
</div>
<Switch
id="double-booking"
defaultChecked
className="data-[state=checked]:bg-[#e91e63]"
/>
</div>
</div>
</div>공개/비공개 설정
<div className="space-y-4 max-w-md">
<h3 className="text-sm font-semibold text-gray-900">공개 설정</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="profile-public"
className="text-sm font-medium cursor-pointer"
>
프로필 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
다른 사용자가 프로필을 볼 수 있습니다
</p>
</div>
<Switch
id="profile-public"
defaultChecked
className="data-[state=checked]:bg-[#e91e63]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="schedule-public"
className="text-sm font-medium cursor-pointer"
>
스케줄 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
예약 가능 시간을 공개합니다
</p>
</div>
<Switch
id="schedule-public"
defaultChecked
className="data-[state=checked]:bg-[#4caf50]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="reviews-public"
className="text-sm font-medium cursor-pointer"
>
리뷰 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">고객 리뷰를 공개합니다</p>
</div>
<Switch
id="reviews-public"
defaultChecked
className="data-[state=checked]:bg-[#9c27b0]"
/>
</div>
</div>
</div>🆕 Foundation/iCignal과의 차이점
| 속성 | Foundation | iCignal | Cals |
|---|---|---|---|
| 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 background | Blue background | Pink/Blue/Purple background (맥락별) |
| 설정 그룹 | 기본 그룹화 | 분석 설정 | 알림, 예약 관리, 공개 설정 |
사용 시나리오 비교
Foundation Switch: 범용 토글 설정, 기능 활성화/비활성화, 다크 모드 iCignal Switch: 데이터 필터 활성화, 실시간 업데이트, 차트 표시 옵션 Cals Switch: 알림 설정, 자동 승인, 공개/비공개, 예약 접수 활성화
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | 체크 상태 (제어 컴포넌트) |
defaultChecked | boolean | false | 기본 체크 상태 (비제어 컴포넌트) |
onCheckedChange | (checked: boolean) => void | - | 상태 변경 이벤트 핸들러 |
disabled | boolean | false | 비활성화 여부 |
required | boolean | false | 필수 여부 |
name | string | - | Form name 속성 |
value | string | 'on' | Form value 속성 |
id | string | - | HTML id 속성 (label 연결용) |
className | string | - | 추가 CSS 클래스 |
접근성
Switch 컴포넌트는 웹 접근성 표준을 준수합니다:
- Radix UI 기반: WAI-ARIA 1.2 Switch 패턴 구현
- 시맨틱 마크업:
role="switch"사용 - 키보드 네비게이션:
Space: 스위치 토글Tab: 다음 스위치로 이동Shift + Tab: 이전 스위치로 이동
- 스크린 리더:
aria-checked상태 안내 - Label 연결:
htmlFor와id를 통한 label-switch 연결 - Focus 표시: 명확한 focus ring으로 현재 위치 표시
- 상태 피드백: 시각적, 청각적 피드백 제공
접근성 예제
<div className="flex items-center justify-between">
<div>
<label
htmlFor="notifications"
className="text-sm font-medium cursor-pointer"
>
알림 받기
</label>
<p id="notifications-description" className="text-xs text-gray-500">
예약 관련 알림을 받습니다
</p>
</div>
<Switch id="notifications" aria-describedby="notifications-description" />
</div>예제
알림 설정 폼
import { Switch } from "@vortex/ui-cals";
import { useState } from "react";
export default function NotificationSettings() {
const [settings, setSettings] = useState({
sms: true,
email: true,
push: false,
marketing: false,
});
const notificationTypes = [
{
id: "sms",
label: "SMS 알림",
description: "예약 확정, 변경, 당일 알림을 문자로 받습니다",
color: "data-[state=checked]:bg-[#03a9f4]",
category: "필수",
},
{
id: "email",
label: "이메일 알림",
description: "예약 내역 및 영수증을 이메일로 받습니다",
color: "data-[state=checked]:bg-[#03a9f4]",
category: "필수",
},
{
id: "push",
label: "앱 푸시 알림",
description: "모바일 앱에서 실시간 알림을 받습니다",
color: "data-[state=checked]:bg-[#03a9f4]",
category: "선택",
},
{
id: "marketing",
label: "마케팅 알림",
description: "이벤트, 프로모션, 쿠폰 정보를 받습니다",
color: "data-[state=checked]:bg-[#9c27b0]",
category: "선택",
},
];
return (
<div className="space-y-4 max-w-md">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-gray-900">알림 설정</h3>
<button
onClick={() =>
setSettings({
sms: false,
email: false,
push: false,
marketing: false,
})
}
className="text-xs text-[#e91e63] hover:underline"
>
모두 끄기
</button>
</div>
<div className="space-y-2">
{notificationTypes.map((type) => (
<div
key={type.id}
className={`
p-3 border rounded-md transition-colors
${
settings[type.id as keyof typeof settings]
? "border-[#e91e63]/30 bg-pink-50/30"
: "border-gray-200"
}
`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<label
htmlFor={type.id}
className="text-sm font-medium cursor-pointer"
>
{type.label}
</label>
<span
className={`
text-xs px-2 py-0.5 rounded-full
${
type.category === "필수"
? "bg-[#e91e63] text-white"
: "bg-gray-200 text-gray-600"
}
`}
>
{type.category}
</span>
</div>
<p className="text-xs text-gray-500 mt-1">{type.description}</p>
</div>
<Switch
id={type.id}
checked={settings[type.id as keyof typeof settings]}
onCheckedChange={(checked) =>
setSettings({ ...settings, [type.id]: checked })
}
className={type.color}
/>
</div>
</div>
))}
</div>
<div className="p-3 bg-blue-50 rounded-md border border-blue-200">
<p className="text-xs text-blue-800">
💡 알림 설정은 언제든지 변경할 수 있습니다
</p>
</div>
</div>
);
}예약 관리 설정 (관리자)
import { Switch } from "@vortex/ui-cals";
import { useState } from "react";
import { CheckCircle, Clock, Users, Ban } from "lucide-react";
export default function ReservationManagementSettings() {
const [settings, setSettings] = useState({
autoApprove: true,
waitingList: false,
doubleBookingPrevention: true,
reminderNotification: true,
});
const managementSettings = [
{
id: "autoApprove",
label: "자동 승인",
description: "새로운 예약을 자동으로 승인합니다",
badge: "빠른 처리",
badgeColor: "bg-[#4caf50]",
icon: CheckCircle,
color: "data-[state=checked]:bg-[#03a9f4]",
},
{
id: "waitingList",
label: "대기 명단 활성화",
description: "예약 취소 시 대기자에게 자동 안내",
badge: null,
icon: Users,
color: "data-[state=checked]:bg-[#ff9800]",
},
{
id: "doubleBookingPrevention",
label: "중복 예약 방지",
description: "동일 시간대 중복 예약을 차단합니다",
badge: "권장",
badgeColor: "bg-[#e91e63]",
icon: Ban,
color: "data-[state=checked]:bg-[#e91e63]",
},
{
id: "reminderNotification",
label: "리마인더 알림",
description: "예약 1시간 전 자동 알림 전송",
badge: null,
icon: Clock,
color: "data-[state=checked]:bg-[#9c27b0]",
},
];
return (
<div className="space-y-4 max-w-md">
<h3 className="text-base font-semibold text-gray-900">예약 관리 설정</h3>
<div className="space-y-3">
{managementSettings.map((setting) => {
const Icon = setting.icon;
return (
<div
key={setting.id}
className={`
p-4 border-2 rounded-lg transition-all
${
settings[setting.id as keyof typeof settings]
? "border-[#e91e63] bg-pink-50"
: "border-gray-200"
}
`}
>
<div className="flex items-start justify-between">
<div className="flex gap-3 flex-1">
<Icon
className={`
h-5 w-5 mt-0.5
${
settings[setting.id as keyof typeof settings]
? "text-[#e91e63]"
: "text-gray-400"
}
`}
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<label
htmlFor={setting.id}
className="text-sm font-medium cursor-pointer"
>
{setting.label}
</label>
{setting.badge && (
<span
className={`text-xs text-white px-2 py-0.5 rounded ${setting.badgeColor}`}
>
{setting.badge}
</span>
)}
</div>
<p className="text-xs text-gray-500 mt-1">
{setting.description}
</p>
</div>
</div>
<Switch
id={setting.id}
checked={settings[setting.id as keyof typeof settings]}
onCheckedChange={(checked) =>
setSettings({ ...settings, [setting.id]: checked })
}
className={setting.color}
/>
</div>
</div>
);
})}
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm font-medium text-gray-900 mb-2">현재 설정 요약</p>
<ul className="text-xs text-gray-600 space-y-1">
<li>
• 자동 승인:{" "}
<span
className={
settings.autoApprove ? "text-[#4caf50]" : "text-gray-400"
}
>
{settings.autoApprove ? "활성화" : "비활성화"}
</span>
</li>
<li>
• 대기 명단:{" "}
<span
className={
settings.waitingList ? "text-[#ff9800]" : "text-gray-400"
}
>
{settings.waitingList ? "활성화" : "비활성화"}
</span>
</li>
<li>
• 중복 예약 방지:{" "}
<span
className={
settings.doubleBookingPrevention
? "text-[#e91e63]"
: "text-gray-400"
}
>
{settings.doubleBookingPrevention ? "활성화" : "비활성화"}
</span>
</li>
<li>
• 리마인더:{" "}
<span
className={
settings.reminderNotification
? "text-[#9c27b0]"
: "text-gray-400"
}
>
{settings.reminderNotification ? "활성화" : "비활성화"}
</span>
</li>
</ul>
</div>
</div>
);
}공개/비공개 설정
import { Switch } from "@vortex/ui-cals";
import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
export default function PrivacySettings() {
const [privacy, setPrivacy] = useState({
profile: true,
schedule: true,
reviews: true,
contact: false,
});
const allPublic = Object.values(privacy).every((value) => value);
return (
<div className="space-y-4 max-w-md">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-gray-900">공개 설정</h3>
<div className="flex items-center gap-2">
{allPublic ? (
<Eye className="h-4 w-4 text-[#4caf50]" />
) : (
<EyeOff className="h-4 w-4 text-gray-400" />
)}
<span className="text-xs text-gray-600">
{allPublic ? "전체 공개" : "일부 비공개"}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="profile"
className="text-sm font-medium cursor-pointer"
>
프로필 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
다른 사용자가 프로필을 볼 수 있습니다
</p>
</div>
<Switch
id="profile"
checked={privacy.profile}
onCheckedChange={(checked) =>
setPrivacy({ ...privacy, profile: checked })
}
className="data-[state=checked]:bg-[#e91e63]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="schedule"
className="text-sm font-medium cursor-pointer"
>
스케줄 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
예약 가능 시간을 공개합니다
</p>
</div>
<Switch
id="schedule"
checked={privacy.schedule}
onCheckedChange={(checked) =>
setPrivacy({ ...privacy, schedule: checked })
}
className="data-[state=checked]:bg-[#4caf50]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="reviews"
className="text-sm font-medium cursor-pointer"
>
리뷰 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
고객 리뷰를 공개합니다
</p>
</div>
<Switch
id="reviews"
checked={privacy.reviews}
onCheckedChange={(checked) =>
setPrivacy({ ...privacy, reviews: checked })
}
className="data-[state=checked]:bg-[#9c27b0]"
/>
</div>
<div className="flex items-center justify-between p-3 border border-gray-200 rounded-md">
<div className="flex-1">
<label
htmlFor="contact"
className="text-sm font-medium cursor-pointer"
>
연락처 공개
</label>
<p className="text-xs text-gray-500 mt-0.5">
연락처를 다른 사용자에게 공개합니다
</p>
</div>
<Switch
id="contact"
checked={privacy.contact}
onCheckedChange={(checked) =>
setPrivacy({ ...privacy, contact: checked })
}
className="data-[state=checked]:bg-[#03a9f4]"
/>
</div>
</div>
<div className="p-3 bg-yellow-50 rounded-md border border-yellow-200">
<p className="text-xs text-yellow-800">
⚠️ 프로필을 비공개로 설정하면 다른 사용자가 예약을 신청할 수 없습니다
</p>
</div>
</div>
);
}관련 컴포넌트
Last updated on