Grid
반응형 그리드 레이아웃을 구성하는 컴포넌트입니다.
개요
Grid는 요소들을 행과 열로 배치하는 레이아웃 컴포넌트입니다. Cals 예약 관리 시스템에서 예약 목록, 서비스 카탈로그, 시간대 표시 등에 사용됩니다.
주요 특징:
- 반응형 컬럼 수 자동 조정
- 간격(gap) 설정
- 정렬 옵션
- 자동 행 높이 조정
설치
npx @vortex/cli add grid기본 사용법
import { Grid } from "@vortex/ui";
export default function ServiceGrid() {
return (
<Grid cols={3} gap={4}>
<div className="p-4 border rounded">서비스 1</div>
<div className="p-4 border rounded">서비스 2</div>
<div className="p-4 border rounded">서비스 3</div>
</Grid>
);
}Variants
컬럼 수 설정
{
/* 1 컬럼 */
}
<Grid cols={1} gap={4}>
<div className="p-4 bg-gray-100 rounded">컬럼 1</div>
<div className="p-4 bg-gray-100 rounded">컬럼 2</div>
</Grid>;
{
/* 2 컬럼 */
}
<Grid cols={2} gap={4}>
<div className="p-4 bg-gray-100 rounded">컬럼 1</div>
<div className="p-4 bg-gray-100 rounded">컬럼 2</div>
</Grid>;
{
/* 3 컬럼 */
}
<Grid cols={3} gap={4}>
<div className="p-4 bg-gray-100 rounded">컬럼 1</div>
<div className="p-4 bg-gray-100 rounded">컬럼 2</div>
<div className="p-4 bg-gray-100 rounded">컬럼 3</div>
</Grid>;
{
/* 4 컬럼 */
}
<Grid cols={4} gap={4}>
<div className="p-4 bg-gray-100 rounded">컬럼 1</div>
<div className="p-4 bg-gray-100 rounded">컬럼 2</div>
<div className="p-4 bg-gray-100 rounded">컬럼 3</div>
<div className="p-4 bg-gray-100 rounded">컬럼 4</div>
</Grid>;반응형 컬럼
{
/* 모바일 1컬럼, 태블릿 2컬럼, 데스크탑 3컬럼 */
}
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}>
<div className="p-4 bg-gray-100 rounded">항목 1</div>
<div className="p-4 bg-gray-100 rounded">항목 2</div>
<div className="p-4 bg-gray-100 rounded">항목 3</div>
<div className="p-4 bg-gray-100 rounded">항목 4</div>
<div className="p-4 bg-gray-100 rounded">항목 5</div>
<div className="p-4 bg-gray-100 rounded">항목 6</div>
</Grid>;
{
/* 모바일 1컬럼, 태블릿 2컬럼, 데스크탑 4컬럼 */
}
<Grid cols={{ sm: 1, md: 2, lg: 4 }} gap={6}>
<div className="p-4 bg-gray-100 rounded">항목 1</div>
<div className="p-4 bg-gray-100 rounded">항목 2</div>
<div className="p-4 bg-gray-100 rounded">항목 3</div>
<div className="p-4 bg-gray-100 rounded">항목 4</div>
</Grid>;간격(Gap) 설정
{
/* 간격 없음 */
}
<Grid cols={3} gap={0}>
<div className="p-4 bg-gray-100">간격 0</div>
<div className="p-4 bg-gray-100">간격 0</div>
<div className="p-4 bg-gray-100">간격 0</div>
</Grid>;
{
/* 작은 간격 */
}
<Grid cols={3} gap={2}>
<div className="p-4 bg-gray-100 rounded">간격 2</div>
<div className="p-4 bg-gray-100 rounded">간격 2</div>
<div className="p-4 bg-gray-100 rounded">간격 2</div>
</Grid>;
{
/* 중간 간격 (기본값) */
}
<Grid cols={3} gap={4}>
<div className="p-4 bg-gray-100 rounded">간격 4</div>
<div className="p-4 bg-gray-100 rounded">간격 4</div>
<div className="p-4 bg-gray-100 rounded">간격 4</div>
</Grid>;
{
/* 큰 간격 */
}
<Grid cols={3} gap={8}>
<div className="p-4 bg-gray-100 rounded">간격 8</div>
<div className="p-4 bg-gray-100 rounded">간격 8</div>
<div className="p-4 bg-gray-100 rounded">간격 8</div>
</Grid>;자동 채우기 (Auto-fit)
{
/* 최소 250px, 최대 1fr */
}
<Grid autoFit={{ min: "250px", max: "1fr" }} gap={4}>
<div className="p-4 bg-gray-100 rounded">자동 크기 1</div>
<div className="p-4 bg-gray-100 rounded">자동 크기 2</div>
<div className="p-4 bg-gray-100 rounded">자동 크기 3</div>
<div className="p-4 bg-gray-100 rounded">자동 크기 4</div>
</Grid>;
{
/* 최소 200px, 최대 1fr */
}
<Grid autoFit={{ min: "200px", max: "1fr" }} gap={6}>
<div className="p-4 bg-gray-100 rounded">자동 크기 1</div>
<div className="p-4 bg-gray-100 rounded">자동 크기 2</div>
<div className="p-4 bg-gray-100 rounded">자동 크기 3</div>
</Grid>;Cals 브랜딩
예약 상태별 그리드
import { Grid } from "@vortex/ui";
const reservations = [
{ id: 1, status: "available", time: "10:00", label: "예약 가능" },
{ id: 2, status: "pending", time: "11:00", label: "대기 중" },
{ id: 3, status: "confirmed", time: "12:00", label: "확정됨" },
{ id: 4, status: "available", time: "13:00", label: "예약 가능" },
{ id: 5, status: "confirmed", time: "14:00", label: "확정됨" },
{ id: 6, status: "cancelled", time: "15:00", label: "취소됨" },
];
const statusColors = {
available: {
bg: "bg-[#4caf50]/10",
border: "border-[#4caf50]",
text: "text-[#4caf50]",
},
pending: {
bg: "bg-[#ff9800]/10",
border: "border-[#ff9800]",
text: "text-[#ff9800]",
},
confirmed: {
bg: "bg-[#03a9f4]/10",
border: "border-[#03a9f4]",
text: "text-[#03a9f4]",
},
cancelled: {
bg: "bg-[#f44336]/10",
border: "border-[#f44336]",
text: "text-[#f44336]",
},
completed: {
bg: "bg-[#9c27b0]/10",
border: "border-[#9c27b0]",
text: "text-[#9c27b0]",
},
};
export default function TimeSlotGrid() {
return (
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4}>
{reservations.map((slot) => {
const colors = statusColors[slot.status];
return (
<div
key={slot.id}
className={`p-6 rounded-lg border-2 ${colors.bg} ${colors.border}`}
>
<p className={`text-2xl font-bold ${colors.text}`}>{slot.time}</p>
<p className="text-sm text-muted-foreground mt-2">{slot.label}</p>
</div>
);
})}
</Grid>
);
}Primary Pink 서비스 카탈로그
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={6}>
<div className="p-6 rounded-lg border-2 border-[#e91e63] bg-gradient-to-br from-[#e91e63]/10 to-[#9c27b0]/10">
<h3 className="text-xl font-bold text-[#e91e63] mb-2">헤어 커트</h3>
<p className="text-muted-foreground mb-4">프리미엄 커트 서비스</p>
<p className="text-2xl font-bold text-[#e91e63]">30,000원</p>
</div>
<div className="p-6 rounded-lg border-2 border-[#03a9f4] bg-[#03a9f4]/5">
<h3 className="text-xl font-bold text-[#03a9f4] mb-2">헤어 펌</h3>
<p className="text-muted-foreground mb-4">자연스러운 웨이브</p>
<p className="text-2xl font-bold text-[#03a9f4]">80,000원</p>
</div>
<div className="p-6 rounded-lg border-2 border-[#9c27b0] bg-[#9c27b0]/5">
<h3 className="text-xl font-bold text-[#9c27b0] mb-2">헤어 염색</h3>
<p className="text-muted-foreground mb-4">트렌디한 컬러</p>
<p className="text-2xl font-bold text-[#9c27b0]">100,000원</p>
</div>
</Grid>대시보드 통계 그리드
<Grid cols={{ sm: 1, md: 2, lg: 4 }} gap={4}>
<div className="p-6 rounded-lg bg-gradient-to-br from-[#e91e63] to-[#c2185b] text-white">
<p className="text-sm opacity-90 mb-1">오늘 예약</p>
<p className="text-4xl font-bold">12</p>
</div>
<div className="p-6 rounded-lg bg-gradient-to-br from-[#03a9f4] to-[#0288d1] text-white">
<p className="text-sm opacity-90 mb-1">확정 예약</p>
<p className="text-4xl font-bold">9</p>
</div>
<div className="p-6 rounded-lg bg-gradient-to-br from-[#ff9800] to-[#f57c00] text-white">
<p className="text-sm opacity-90 mb-1">대기 중</p>
<p className="text-4xl font-bold">3</p>
</div>
<div className="p-6 rounded-lg bg-gradient-to-br from-[#9c27b0] to-[#7b1fa2] text-white">
<p className="text-sm opacity-90 mb-1">이번 주</p>
<p className="text-4xl font-bold">48</p>
</div>
</Grid>브랜드별 비교
| 특성 | Foundation | iCignal | Cals |
|---|---|---|---|
| 목적 | 범용 그리드 | 제품 진열, 이벤트 | 예약 목록, 시간대 표시 |
| 디자인 | 중립적 | 다크 테마 | 밝고 친근한 |
| Primary | - | Blue #0066cc | Pink #e91e63 |
| 기본 컬럼 | 3 | 3-4 | 2-3 |
| 주요 사용 | 범용 레이아웃 | 제품 그리드 | 예약 카드, 서비스 카탈로그 |
| 특화 기능 | - | 다크 모드 | 예약 상태 컬러 시스템 |
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
| cols | number | { sm?: number, md?: number, lg?: number, xl?: number } | 3 | 컬럼 수 (반응형 설정 가능) |
| gap | number | 4 | 그리드 간격 (Tailwind spacing 단위) |
| autoFit | { min: string, max: string } | - | 자동 채우기 설정 |
| className | string | - | 추가 CSS 클래스 |
| children | ReactNode | - | 그리드 항목들 |
접근성
- 시맨틱 HTML: 적절한 컨테이너 요소 사용
- 키보드 네비게이션: 그리드 항목이 인터랙티브한 경우
tabIndex설정 - ARIA 속성: 복잡한 그리드는
role="grid"및 관련 속성 사용 - 반응형: 모든 화면 크기에서 읽기 쉬운 레이아웃
{
/* 접근성 개선 예제 */
}
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={4} role="list" aria-label="예약 목록">
<div
role="listitem"
tabIndex={0}
className="p-4 border rounded cursor-pointer"
>
예약 1
</div>
<div
role="listitem"
tabIndex={0}
className="p-4 border rounded cursor-pointer"
>
예약 2
</div>
<div
role="listitem"
tabIndex={0}
className="p-4 border rounded cursor-pointer"
>
예약 3
</div>
</Grid>;예제
1. 예약 목록 그리드
import { Grid } from "@vortex/ui";
import { Calendar, Clock, User, MapPin } from "lucide-react";
const reservations = [
{
id: 1,
status: "confirmed",
customer: "홍길동",
service: "헤어 커트",
time: "2024-01-15 14:00",
location: "강남점",
},
{
id: 2,
status: "pending",
customer: "김고객",
service: "헤어 펌",
time: "2024-01-15 15:00",
location: "강남점",
},
{
id: 3,
status: "confirmed",
customer: "이고객",
service: "헤어 염색",
time: "2024-01-15 16:00",
location: "홍대점",
},
{
id: 4,
status: "completed",
customer: "박고객",
service: "헤어 커트",
time: "2024-01-14 14:00",
location: "강남점",
},
];
const statusConfig = {
pending: {
bg: "bg-[#ff9800]/5",
border: "border-[#ff9800]",
text: "text-[#ff9800]",
label: "대기",
},
confirmed: {
bg: "bg-[#03a9f4]/5",
border: "border-[#03a9f4]",
text: "text-[#03a9f4]",
label: "확정",
},
completed: {
bg: "bg-[#9c27b0]/5",
border: "border-[#9c27b0]",
text: "text-[#9c27b0]",
label: "완료",
},
};
export default function ReservationListGrid() {
return (
<Grid cols={{ sm: 1, md: 2, lg: 3 }} gap={6}>
{reservations.map((reservation) => {
const config = statusConfig[reservation.status];
return (
<div
key={reservation.id}
className={`p-6 rounded-lg border-2 ${config.border} ${config.bg} hover:shadow-lg transition-shadow cursor-pointer`}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold">{reservation.service}</h3>
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${config.text} bg-white`}
>
{config.label}
</span>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm">
<User className="w-4 h-4 text-muted-foreground" />
<span>{reservation.customer}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Clock className="w-4 h-4 text-muted-foreground" />
<span>{reservation.time}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<MapPin className="w-4 h-4 text-muted-foreground" />
<span>{reservation.location}</span>
</div>
</div>
<button
className={`mt-4 w-full py-2 rounded-md border ${config.border} ${config.text} hover:bg-white transition-colors`}
>
상세 보기
</button>
</div>
);
})}
</Grid>
);
}2. 서비스 카탈로그 그리드
import { Grid } from "@vortex/ui";
import { Scissors, Wind, Droplet, Sparkles } from "lucide-react";
const services = [
{
id: 1,
name: "헤어 커트",
description: "프리미엄 커트 서비스",
price: "30,000원",
duration: "60분",
icon: Scissors,
color: "#e91e63",
},
{
id: 2,
name: "헤어 펌",
description: "자연스러운 웨이브",
price: "80,000원",
duration: "120분",
icon: Wind,
color: "#03a9f4",
},
{
id: 3,
name: "헤어 염색",
description: "트렌디한 컬러",
price: "100,000원",
duration: "180분",
icon: Droplet,
color: "#9c27b0",
},
{
id: 4,
name: "트리트먼트",
description: "영양 케어",
price: "40,000원",
duration: "45분",
icon: Sparkles,
color: "#4caf50",
},
];
export default function ServiceCatalogGrid() {
return (
<Grid cols={{ sm: 1, md: 2, lg: 4 }} gap={6}>
{services.map((service) => {
const Icon = service.icon;
return (
<div
key={service.id}
className="p-6 rounded-lg border-2 border-gray-200 hover:border-[#e91e63] bg-white hover:shadow-xl transition-all cursor-pointer group"
>
<div
className="w-16 h-16 rounded-full flex items-center justify-center mb-4"
style={{ backgroundColor: `${service.color}20` }}
>
<Icon className="w-8 h-8" style={{ color: service.color }} />
</div>
<h3 className="text-xl font-bold mb-2 group-hover:text-[#e91e63] transition-colors">
{service.name}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{service.description}
</p>
<div className="flex items-center justify-between pt-4 border-t">
<div>
<p
className="text-2xl font-bold"
style={{ color: service.color }}
>
{service.price}
</p>
<p className="text-xs text-muted-foreground">
{service.duration}
</p>
</div>
</div>
<button className="mt-4 w-full py-2 rounded-md bg-[#e91e63] text-white hover:bg-[#c2185b] transition-colors">
예약하기
</button>
</div>
);
})}
</Grid>
);
}3. 주간 시간대 그리드
import { Grid } from "@vortex/ui";
const weekDays = ["월", "화", "수", "목", "금", "토", "일"];
const timeSlots = [
"10:00",
"11:00",
"12:00",
"13:00",
"14:00",
"15:00",
"16:00",
"17:00",
];
const reservationData = {
0: { "10:00": "available", "11:00": "confirmed", "14:00": "confirmed" },
1: { "10:00": "confirmed", "13:00": "pending", "15:00": "confirmed" },
2: { "11:00": "available", "14:00": "confirmed", "16:00": "confirmed" },
3: { "10:00": "confirmed", "12:00": "confirmed", "15:00": "available" },
4: { "11:00": "confirmed", "13:00": "confirmed", "17:00": "pending" },
5: { "10:00": "available", "14:00": "available", "16:00": "available" },
6: { "12:00": "available", "15:00": "available" },
};
const statusColors = {
available: "#4caf50",
pending: "#ff9800",
confirmed: "#03a9f4",
};
export default function WeeklyScheduleGrid() {
return (
<div className="space-y-4">
<Grid cols={8} gap={2}>
<div className="p-2 text-center font-bold">시간</div>
{weekDays.map((day) => (
<div
key={day}
className="p-2 text-center font-bold bg-[#e91e63]/10 rounded"
>
{day}
</div>
))}
</Grid>
{timeSlots.map((time) => (
<Grid key={time} cols={8} gap={2}>
<div className="p-2 text-center font-medium text-sm bg-gray-100 rounded flex items-center justify-center">
{time}
</div>
{weekDays.map((day, dayIndex) => {
const status = reservationData[dayIndex]?.[time] || "empty";
const bgColor =
status === "empty" ? "#f5f5f5" : statusColors[status];
return (
<div
key={`${day}-${time}`}
className="p-2 rounded cursor-pointer hover:opacity-80 transition-opacity min-h-[60px] flex items-center justify-center"
style={{
backgroundColor:
status === "empty" ? bgColor : `${bgColor}20`,
border:
status === "empty"
? "1px dashed #ddd"
: `2px solid ${bgColor}`,
}}
>
{status !== "empty" && (
<span
className="text-xs font-medium"
style={{ color: bgColor }}
>
{status === "available" && "예약 가능"}
{status === "pending" && "대기"}
{status === "confirmed" && "확정"}
</span>
)}
</div>
);
})}
</Grid>
))}
</div>
);
}관련 컴포넌트
Last updated on