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>브랜드 비교표
| 속성 | Foundation | iCignal | Cals |
|---|---|---|---|
| Primary Color | Neutral Gray | Corporate Blue #0066cc | Primary Pink #e91e63 |
| Use Case | 범용 확인 | 기업 워크플로우 | 예약 관리 |
| Header Style | Simple | Corporate (Logo) | Status Border |
| Footer Actions | 2-3 buttons | Cancel + Primary | Cancel + Status Color |
| Size | Default | Large (상세 정보) | Medium (예약 중심) |
Props API
Dialog
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | - | Dialog 열림 상태 (controlled) |
| onOpenChange | (open: boolean) => void | - | 열림 상태 변경 핸들러 |
| defaultOpen | boolean | false | 초기 열림 상태 (uncontrolled) |
| modal | boolean | true | 모달 동작 활성화 |
DialogTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | 자식 요소를 트리거로 사용 |
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | 커스텀 CSS 클래스 |
| onEscapeKeyDown | (event: KeyboardEvent) => void | - | ESC 키 핸들러 |
| onPointerDownOutside | (event: PointerEvent) => void | - | 외부 클릭 핸들러 |
DialogHeader, DialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | 커스텀 CSS 클래스 |
DialogTitle, DialogDescription
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | 커스텀 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 호출
}관련 컴포넌트
Last updated on