Skip to Content

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>

브랜드별 비교

특성FoundationiCignalCals
목적범용 그리드제품 진열, 이벤트예약 목록, 시간대 표시
디자인중립적다크 테마밝고 친근한
Primary-Blue #0066ccPink #e91e63
기본 컬럼33-42-3
주요 사용범용 레이아웃제품 그리드예약 카드, 서비스 카탈로그
특화 기능-다크 모드예약 상태 컬러 시스템

Props API

PropTypeDefaultDescription
colsnumber | { sm?: number, md?: number, lg?: number, xl?: number }3컬럼 수 (반응형 설정 가능)
gapnumber4그리드 간격 (Tailwind spacing 단위)
autoFit{ min: string, max: string }-자동 채우기 설정
classNamestring-추가 CSS 클래스
childrenReactNode-그리드 항목들

접근성

  • 시맨틱 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> ); }

관련 컴포넌트

  • Card - 카드 컴포넌트
  • Container - 페이지 레이아웃 컨테이너
  • Stack - 수직/수평 스택
Last updated on