Skip to Content
ExamplesBooking System Example (Cals)

Booking System Example (Cals)

예약 시스템 구현 예제


개요

Cals의 BookingCard 컴포넌트를 활용한 실시간 예약 시스템 예제입니다.

주요 특징

  • ✅ 실시간 예약 가능 여부 확인
  • ✅ 다양한 자원 타입 관리
  • ✅ 캘린더 통합
  • ✅ 예약 확인 플로우
  • ✅ 충돌 방지

전체 코드

"use client"; import { useState } from "react"; import { BookingCard } from "@vortex/ui-cals"; import { Container } from "@/components/ui/container"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@vortex/ui-foundation"; import { Calendar } from "@/components/ui/calendar"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@vortex/ui-foundation"; export default function BookingSystem() { const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedTime, setSelectedTime] = useState(""); const [selectedRoom, setSelectedRoom] = useState(null); const rooms = [ { id: 1, title: "Conference Room A", capacity: 10, equipment: ["Projector", "Whiteboard", "Video Conference"], available: true, floor: "3F", }, { id: 2, title: "Meeting Room B", capacity: 6, equipment: ["TV", "Whiteboard"], available: true, floor: "2F", }, { id: 3, title: "Executive Suite", capacity: 4, equipment: ["Projector", "Video Conference", "Coffee Machine"], available: false, floor: "5F", }, { id: 4, title: "Workshop Space", capacity: 20, equipment: ["Projector", "Sound System", "Stage"], available: true, floor: "1F", }, ]; const timeSlots = [ "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", ]; const handleBooking = () => { if (!selectedRoom || !selectedTime) { alert("Please select a room and time slot"); return; } alert( `Booking confirmed:\n${ selectedRoom.title }\nDate: ${selectedDate.toLocaleDateString()}\nTime: ${selectedTime}` ); }; return ( <Container size="xl" className="py-8 space-y-8"> {/* Header */} <div> <h1 className="text-3xl font-bold">Room Booking System</h1> <p className="text-muted-foreground">실시간 회의실 예약 시스템</p> </div> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> {/* Calendar and Time Selection */} <div className="lg:col-span-1 space-y-6"> <Card> <CardHeader> <CardTitle>Select Date</CardTitle> </CardHeader> <CardContent> <Calendar mode="single" selected={selectedDate} onSelect={setSelectedDate} className="rounded-md border" /> </CardContent> </Card> <Card> <CardHeader> <CardTitle>Select Time</CardTitle> </CardHeader> <CardContent> <div className="grid grid-cols-3 gap-2"> {timeSlots.map((time) => ( <Button key={time} variant={selectedTime === time ? "default" : "outline"} size="sm" onClick={() => setSelectedTime(time)} > {time} </Button> ))} </div> </CardContent> </Card> {/* Booking Summary */} {selectedRoom && selectedTime && ( <Card className="border-primary"> <CardHeader> <CardTitle>Booking Summary</CardTitle> </CardHeader> <CardContent className="space-y-4"> <div> <p className="text-sm text-muted-foreground">Room</p> <p className="font-medium">{selectedRoom.title}</p> </div> <div> <p className="text-sm text-muted-foreground">Date</p> <p className="font-medium"> {selectedDate.toLocaleDateString()} </p> </div> <div> <p className="text-sm text-muted-foreground">Time</p> <p className="font-medium">{selectedTime}</p> </div> <Button className="w-full" onClick={handleBooking}> Confirm Booking </Button> </CardContent> </Card> )} </div> {/* Available Rooms */} <div className="lg:col-span-2"> <Card> <CardHeader> <CardTitle>Available Rooms</CardTitle> </CardHeader> <CardContent> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {rooms.map((room) => ( <BookingCard key={room.id} title={room.title} capacity={room.capacity} available={room.available} onClick={() => room.available && setSelectedRoom(room)} className={ selectedRoom?.id === room.id ? "border-primary" : "" } > <div className="space-y-2 mt-4"> <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground"> Floor </span> <Badge variant="secondary">{room.floor}</Badge> </div> <div className="flex items-center justify-between"> <span className="text-sm text-muted-foreground"> Capacity </span> <span className="text-sm font-medium"> {room.capacity} people </span> </div> <div> <p className="text-sm text-muted-foreground mb-2"> Equipment </p> <div className="flex flex-wrap gap-1"> {room.equipment.map((item) => ( <Badge key={item} variant="outline" className="text-xs" > {item} </Badge> ))} </div> </div> <div className="pt-2"> {room.available ? ( <Badge className="bg-green-500">Available</Badge> ) : ( <Badge variant="destructive">Booked</Badge> )} </div> </div> </BookingCard> ))} </div> </CardContent> </Card> {/* Today's Schedule */} <Card className="mt-6"> <CardHeader> <CardTitle>Today's Schedule</CardTitle> </CardHeader> <CardContent> <div className="space-y-3"> <div className="flex items-center justify-between p-3 border rounded-lg"> <div> <p className="font-medium">Conference Room A</p> <p className="text-sm text-muted-foreground"> Team Meeting </p> </div> <div className="text-right"> <p className="text-sm font-medium">10:00 - 11:00</p> <Badge variant="secondary">In Progress</Badge> </div> </div> <div className="flex items-center justify-between p-3 border rounded-lg"> <div> <p className="font-medium">Executive Suite</p> <p className="text-sm text-muted-foreground"> Board Meeting </p> </div> <div className="text-right"> <p className="text-sm font-medium">14:00 - 16:00</p> <Badge>Upcoming</Badge> </div> </div> <div className="flex items-center justify-between p-3 border rounded-lg"> <div> <p className="font-medium">Workshop Space</p> <p className="text-sm text-muted-foreground"> Training Session </p> </div> <div className="text-right"> <p className="text-sm font-medium">09:00 - 12:00</p> <Badge variant="outline">Completed</Badge> </div> </div> </div> </CardContent> </Card> </div> </div> </Container> ); }

주요 기능

BookingCard 컴포넌트

<BookingCard title="Conference Room A" capacity={10} available={true} onClick={() => handleRoomSelect(room)} > {/* 커스텀 컨텐츠 */} </BookingCard>

Props:

  • title: 자원 이름
  • capacity: 수용 인원
  • available: 예약 가능 여부
  • onClick: 선택 핸들러
  • children: 추가 정보 표시

실시간 가용성 확인

const checkAvailability = async (roomId, date, time) => { const response = await fetch(`/api/bookings/check`, { method: "POST", body: JSON.stringify({ roomId, date, time }), }); const { available } = await response.json(); return available; };

예약 생성

const createBooking = async (bookingData) => { const response = await fetch("/api/bookings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ roomId: bookingData.roomId, date: bookingData.date, time: bookingData.time, duration: bookingData.duration, userId: currentUser.id, }), }); if (!response.ok) { throw new Error("Booking failed"); } return response.json(); };

캘린더 통합

react-calendar 사용

import { Calendar } from "@/components/ui/calendar"; function BookingCalendar() { const [date, setDate] = useState(new Date()); return ( <Calendar mode="single" selected={date} onSelect={setDate} disabled={(date) => date < new Date()} /> ); }

시간대 선택

function TimeSlotSelector({ selectedTime, onSelect, bookedSlots }) { const timeSlots = generateTimeSlots(9, 18); // 09:00 - 18:00 return ( <div className="grid grid-cols-3 gap-2"> {timeSlots.map((time) => ( <Button key={time} variant={selectedTime === time ? "default" : "outline"} disabled={bookedSlots.includes(time)} onClick={() => onSelect(time)} > {time} </Button> ))} </div> ); }

충돌 방지

낙관적 잠금 (Optimistic Locking)

const handleBooking = async () => { try { // 1. 최신 상태 확인 const isAvailable = await checkAvailability(roomId, date, time); if (!isAvailable) { alert("This slot is no longer available"); return; } // 2. 예약 생성 await createBooking({ roomId, date, time, version: currentVersion, // 버전 체크 }); alert("Booking confirmed!"); } catch (error) { if (error.code === "CONFLICT") { alert("Someone just booked this slot"); } } };

실시간 업데이트 (WebSocket)

useEffect(() => { const ws = new WebSocket("ws://api.example.com/bookings"); ws.onmessage = (event) => { const { type, data } = JSON.parse(event.data); if (type === "BOOKING_CREATED") { // 예약 목록 업데이트 setBookings((prev) => [...prev, data]); } }; return () => ws.close(); }, []);

데이터 구조

Booking 모델

interface Booking { id: string; roomId: string; userId: string; date: string; startTime: string; endTime: string; purpose: string; attendees: number; status: "pending" | "confirmed" | "cancelled"; createdAt: string; }

Room 모델

interface Room { id: string; title: string; capacity: number; floor: string; equipment: string[]; available: boolean; imageUrl?: string; }

API 라우트 예제

GET /api/bookings

// app/api/bookings/route.ts export async function GET(request: Request) { const { searchParams } = new URL(request.url); const date = searchParams.get("date"); const roomId = searchParams.get("roomId"); const bookings = await db.bookings.findMany({ where: { date, roomId, status: "confirmed", }, }); return Response.json(bookings); }

POST /api/bookings

export async function POST(request: Request) { const body = await request.json(); // 충돌 확인 const conflict = await db.bookings.findFirst({ where: { roomId: body.roomId, date: body.date, startTime: { lte: body.endTime }, endTime: { gte: body.startTime }, }, }); if (conflict) { return Response.json({ error: "Time slot not available" }, { status: 409 }); } const booking = await db.bookings.create({ data: body, }); return Response.json(booking); }

접근성

키보드 네비게이션

<BookingCard tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { handleSelect() } }} >

ARIA 속성

<div role="button" aria-label={`Book ${room.title}, capacity ${room.capacity}`} aria-disabled={!room.available} >

다음 단계

Last updated on