Skip to Content

Dialog

개요

Dialog는 사용자의 주의를 집중시키고 중요한 정보를 표시하거나 확인을 요청하는 모달 컴포넌트입니다. Cals에서는 예약 취소 확인, 예약 변경, 고객 정보 수정 등에 사용됩니다.

언제 사용하는가

  • 예약 취소/변경 확인
  • 고객 정보 입력/수정
  • 예약 상세 정보 표시
  • 중요한 작업 확인 요청
  • 폼 입력이 필요한 경우

언제 사용하지 말아야 하는가

  • 간단한 정보 표시 → Popover 사용
  • 일시적인 알림 → Toast 사용
  • 영구적인 정보 → Alert 사용
  • 짧은 설명 → Tooltip 사용

설치

npx @vortex/cli add dialog

기본 사용법

import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; export default function DialogDemo() { return ( <Dialog> <DialogTrigger asChild> <Button>예약 취소</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>예약을 취소하시겠습니까?</DialogTitle> <DialogDescription> 예약을 취소하면 취소 수수료가 부과될 수 있습니다. </DialogDescription> </DialogHeader> <div className="flex justify-end gap-2"> <Button variant="outline">돌아가기</Button> <Button variant="destructive">취소하기</Button> </div> </DialogContent> </Dialog> ); }

Variants

Dialog는 단일 스타일이지만, 용도에 따라 내용과 액션을 다르게 구성합니다.

Confirmation Dialog

사용자의 확인을 요청하는 다이얼로그입니다.

<Dialog> <DialogTrigger asChild> <Button variant="destructive">예약 취소</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>예약을 취소하시겠습니까?</DialogTitle> <DialogDescription> 이 작업은 되돌릴 수 없습니다. 취소 수수료 5,000원이 부과됩니다. </DialogDescription> </DialogHeader> <DialogFooter> <Button variant="outline">돌아가기</Button> <Button variant="destructive">취소하기</Button> </DialogFooter> </DialogContent> </Dialog>

Form Dialog

폼 입력이 필요한 다이얼로그입니다.

<Dialog> <DialogTrigger asChild> <Button>예약 변경</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>예약 시간 변경</DialogTitle> <DialogDescription> 변경하실 날짜와 시간을 선택해주세요. </DialogDescription> </DialogHeader> <div className="space-y-4"> <div> <Label htmlFor="date">날짜</Label> <Input id="date" type="date" /> </div> <div> <Label htmlFor="time">시간</Label> <Select> <SelectTrigger id="time"> <SelectValue placeholder="시간 선택" /> </SelectTrigger> <SelectContent> <SelectItem value="14:00">14:00</SelectItem> <SelectItem value="15:00">15:00</SelectItem> <SelectItem value="16:00">16:00</SelectItem> </SelectContent> </Select> </div> </div> <DialogFooter> <Button variant="outline">취소</Button> <Button>변경하기</Button> </DialogFooter> </DialogContent> </Dialog>

Information Dialog

정보를 표시하는 다이얼로그입니다.

<Dialog> <DialogTrigger asChild> <Button variant="outline">예약 상세</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>예약 상세 정보</DialogTitle> <DialogDescription>2024년 1월 15일 14:00 예약</DialogDescription> </DialogHeader> <div className="space-y-4"> <div> <h4 className="font-semibold">고객명</h4> <p className="text-sm text-muted-foreground">홍길동</p> </div> <div> <h4 className="font-semibold">연락처</h4> <p className="text-sm text-muted-foreground">010-1234-5678</p> </div> <div> <h4 className="font-semibold">서비스</h4> <p className="text-sm text-muted-foreground">컨설팅 (60분)</p> </div> </div> <DialogFooter> <Button>확인</Button> </DialogFooter> </DialogContent> </Dialog>

Cals 브랜딩

브랜드 컬러

Cals의 Primary Pink를 활용한 브랜드 Dialog 스타일입니다.

<Dialog> <DialogTrigger asChild> <Button className="bg-cals-primary hover:bg-cals-primary/90"> 새 예약 </Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle className="text-cals-primary">Cals 예약 생성</DialogTitle> <DialogDescription> 고객 정보와 예약 시간을 입력해주세요. </DialogDescription> </DialogHeader> <div className="space-y-4">{/* Form fields */}</div> <DialogFooter> <Button variant="outline">취소</Button> <Button className="bg-cals-primary hover:bg-cals-primary/90"> 예약하기 </Button> </DialogFooter> </DialogContent> </Dialog>

예약 상태 컬러

예약 상태별 Dialog 헤더 스타일을 제공합니다.

// Pending - 승인 대기 <Dialog> <DialogTrigger asChild> <Button>승인 대기</Button> </DialogTrigger> <DialogContent> <DialogHeader className="border-l-4 border-orange-500 pl-4"> <DialogTitle className="text-orange-900">승인 대기</DialogTitle> <DialogDescription> 예약 요청을 승인하거나 거절할 수 있습니다. </DialogDescription> </DialogHeader> <div className="space-y-4"> {/* Reservation details */} </div> <DialogFooter> <Button variant="outline">거절</Button> <Button className="bg-orange-500 hover:bg-orange-600">승인</Button> </DialogFooter> </DialogContent> </Dialog> // Confirmed - 예약 확정 <Dialog> <DialogTrigger asChild> <Button>예약 확정</Button> </DialogTrigger> <DialogContent> <DialogHeader className="border-l-4 border-blue-500 pl-4"> <DialogTitle className="text-blue-900">예약 확정</DialogTitle> <DialogDescription> 예약이 확정되었습니다. </DialogDescription> </DialogHeader> <div className="space-y-4"> {/* Confirmed reservation details */} </div> <DialogFooter> <Button className="bg-blue-500 hover:bg-blue-600">확인</Button> </DialogFooter> </DialogContent> </Dialog> // Cancelled - 예약 취소 <Dialog> <DialogTrigger asChild> <Button variant="destructive">예약 취소</Button> </DialogTrigger> <DialogContent> <DialogHeader className="border-l-4 border-red-500 pl-4"> <DialogTitle className="text-red-900">예약 취소 확인</DialogTitle> <DialogDescription> 정말 예약을 취소하시겠습니까? 취소 수수료가 부과됩니다. </DialogDescription> </DialogHeader> <div className="space-y-4"> <p className="text-sm text-red-600"> 취소 수수료: 5,000원 </p> </div> <DialogFooter> <Button variant="outline">돌아가기</Button> <Button variant="destructive">취소하기</Button> </DialogFooter> </DialogContent> </Dialog>

브랜드 비교표

속성FoundationiCignalCals
Primary ColorNeutral GrayCorporate Blue #0066ccPrimary Pink #e91e63
Use Case범용 확인기업 워크플로우예약 관리
Header StyleSimpleCorporate (Logo)Status Border
Footer Actions2-3 buttonsCancel + PrimaryCancel + Status Color
SizeDefaultLarge (상세 정보)Medium (예약 중심)

Props API

Dialog

PropTypeDefaultDescription
openboolean-Dialog 열림 상태 (controlled)
onOpenChange(open: boolean) => void-열림 상태 변경 핸들러
defaultOpenbooleanfalse초기 열림 상태 (uncontrolled)
modalbooleantrue모달 동작 활성화

DialogTrigger

PropTypeDefaultDescription
asChildbooleanfalse자식 요소를 트리거로 사용

DialogContent

PropTypeDefaultDescription
classNamestring-커스텀 CSS 클래스
onEscapeKeyDown(event: KeyboardEvent) => void-ESC 키 핸들러
onPointerDownOutside(event: PointerEvent) => void-외부 클릭 핸들러

DialogHeader, DialogFooter

PropTypeDefaultDescription
classNamestring-커스텀 CSS 클래스

DialogTitle, DialogDescription

PropTypeDefaultDescription
classNamestring-커스텀 CSS 클래스

접근성

  • Role: role="dialog" 자동 적용
  • ARIA: aria-labelledby, aria-describedby 자동 연결
  • Focus Trap: Dialog 내부에서만 Tab 이동
  • ESC Key: ESC 키로 닫기
  • Backdrop: 외부 클릭으로 닫기
  • Initial Focus: 첫 번째 포커스 가능 요소로 자동 이동
  • Return Focus: 닫힐 때 트리거로 포커스 복귀

예제

예약 취소 확인

import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; export default function CancelReservationDialog() { const [open, setOpen] = useState(false); const { toast } = useToast(); const handleCancel = () => { // API 호출 cancelReservation(); // Toast 표시 toast({ title: "예약 취소", description: "예약이 취소되었습니다.", className: "border-red-500 bg-red-50 text-red-900", }); // Dialog 닫기 setOpen(false); }; return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="destructive">예약 취소</Button> </DialogTrigger> <DialogContent> <DialogHeader className="border-l-4 border-red-500 pl-4"> <DialogTitle className="text-red-900"> 예약을 취소하시겠습니까? </DialogTitle> <DialogDescription> 이 작업은 되돌릴 수 없습니다. 취소 수수료 5,000원이 부과됩니다. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> <div className="space-y-2"> <h4 className="font-semibold">예약 정보</h4> <p className="text-sm text-muted-foreground"> 2024년 1월 15일 14:00 - 컨설팅 (60분) </p> </div> <div className="space-y-2"> <h4 className="font-semibold">취소 수수료</h4> <p className="text-sm text-red-600">5,000원</p> </div> </div> <DialogFooter> <Button variant="outline" onClick={() => setOpen(false)}> 돌아가기 </Button> <Button variant="destructive" onClick={handleCancel}> 취소하기 </Button> </DialogFooter> </DialogContent> </Dialog> ); } function cancelReservation() { // API 호출 }

예약 변경 다이얼로그

import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; export default function EditReservationDialog() { const [open, setOpen] = useState(false); const { toast } = useToast(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // API 호출 updateReservation(); // Toast 표시 toast({ title: "예약 변경 완료", description: "예약이 성공적으로 변경되었습니다.", className: "border-blue-500 bg-blue-50 text-blue-900", }); // Dialog 닫기 setOpen(false); }; return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="outline">예약 변경</Button> </DialogTrigger> <DialogContent> <DialogHeader className="border-l-4 border-blue-500 pl-4"> <DialogTitle className="text-blue-900">예약 시간 변경</DialogTitle> <DialogDescription> 변경하실 날짜와 시간을 선택해주세요. </DialogDescription> </DialogHeader> <form onSubmit={handleSubmit}> <div className="space-y-4 py-4"> <div className="space-y-2"> <Label htmlFor="date">날짜</Label> <Input id="date" type="date" required /> </div> <div className="space-y-2"> <Label htmlFor="time">시간</Label> <Select required> <SelectTrigger id="time"> <SelectValue placeholder="시간 선택" /> </SelectTrigger> <SelectContent> <SelectItem value="14:00">14:00</SelectItem> <SelectItem value="15:00">15:00</SelectItem> <SelectItem value="16:00">16:00</SelectItem> </SelectContent> </Select> </div> <div className="space-y-2"> <Label htmlFor="note">변경 사유 (선택)</Label> <Input id="note" placeholder="변경 사유를 입력하세요" /> </div> </div> <DialogFooter> <Button type="button" variant="outline" onClick={() => setOpen(false)} > 취소 </Button> <Button type="submit" className="bg-blue-500 hover:bg-blue-600"> 변경하기 </Button> </DialogFooter> </form> </DialogContent> </Dialog> ); } function updateReservation() { // API 호출 }

고객 정보 수정

import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; export default function EditCustomerDialog() { const [open, setOpen] = useState(false); const { toast } = useToast(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // API 호출 updateCustomer(); // Toast 표시 toast({ title: "정보 수정 완료", description: "고객 정보가 성공적으로 수정되었습니다.", }); // Dialog 닫기 setOpen(false); }; return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="outline">정보 수정</Button> </DialogTrigger> <DialogContent className="max-w-md"> <DialogHeader> <DialogTitle>고객 정보 수정</DialogTitle> <DialogDescription>고객 정보를 수정할 수 있습니다.</DialogDescription> </DialogHeader> <form onSubmit={handleSubmit}> <div className="space-y-4 py-4"> <div className="space-y-2"> <Label htmlFor="name">이름 *</Label> <Input id="name" defaultValue="홍길동" required /> </div> <div className="space-y-2"> <Label htmlFor="phone">연락처 *</Label> <Input id="phone" type="tel" defaultValue="010-1234-5678" required /> </div> <div className="space-y-2"> <Label htmlFor="email">이메일</Label> <Input id="email" type="email" defaultValue="hong@example.com" /> </div> <div className="space-y-2"> <Label htmlFor="note">메모</Label> <Textarea id="note" placeholder="특이사항을 입력하세요" rows={3} /> </div> </div> <DialogFooter> <Button type="button" variant="outline" onClick={() => setOpen(false)} > 취소 </Button> <Button type="submit" className="bg-cals-primary hover:bg-cals-primary/90" > 저장 </Button> </DialogFooter> </form> </DialogContent> </Dialog> ); } function updateCustomer() { // API 호출 }

관련 컴포넌트

  • Alert - 영구적인 알림
  • Toast - 일시적인 알림
  • Popover - 간단한 정보 표시
  • Sheet - 사이드 패널
Last updated on