Callout
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
저장 완료
변경사항이 정상적으로 저장되었습니다.
미저장 변경사항
페이지를 떠나기 전에 저장해 주세요.
새 댓글 알림은 설정에서 끌 수 있어요.
Callout
페이지 흐름 안에 박아넣는 인라인 알림(공지) 박스입니다. 모달인 Alert와 달리 본문 레이아웃 안에서 정보를 강조할 때 사용합니다.
Variants
success / info / warning / error / neutral 5종
Source Code
저장 완료
변경사항이 정상적으로 저장되었습니다.
안내
새 버전이 곧 배포될 예정입니다.
주의
이 작업은 되돌릴 수 없습니다.
오류
파일 업로드에 실패했습니다.
참고
기본(neutral) 톤입니다.
Live Preview
옵션 — 제목 없이 / 아이콘 숨김 / 닫기
title 없이 본문만, icon={false}로 아이콘 숨김, onClose로 닫기 버튼을 노출합니다.
Source Code
제목 없이 본문만 있는 간결한 형태입니다.
아이콘을 숨기면 텍스트가 왼쪽 끝에 정렬됩니다.
닫을 수 있는 공지
우측 X로 닫으면 사라집니다.
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Callouttypescript
'use client';
/**
* Callout 컴포넌트
*
* 페이지 흐름 안에 박아넣는 인라인 알림(공지) 박스입니다.
* - 모달인 Alert(useAlert)와 달리, 본문 레이아웃 안에서 정보를 강조할 때 사용합니다.
* - variant별 색/아이콘으로 success / info / warning / error / neutral을 구분합니다.
* - title·아이콘·닫기 버튼은 모두 선택. 본문(children)만으로도 동작합니다.
*/
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';
export type CalloutVariant = 'info' | 'success' | 'warning' | 'error' | 'neutral';
export interface CalloutProps {
/** 색·기본 아이콘을 결정하는 변형 */
variant?: CalloutVariant;
/** 굵은 제목 (선택) */
title?: ReactNode;
/** 커스텀 아이콘. false면 아이콘 영역 자체를 숨김 */
icon?: ReactNode | false;
/** 우측 닫기 버튼 핸들러 (선택) */
onClose?: () => void;
/** 본문 */
children?: ReactNode;
className?: string;
}
const variantStyles: Record<
CalloutVariant,
{ box: string; icon: string; title: string }
> = {
info: { box: 'border-blue-200 bg-blue-50', icon: 'text-blue-500', title: 'text-blue-900' },
success: { box: 'border-emerald-200 bg-emerald-50', icon: 'text-emerald-500', title: 'text-emerald-900' },
warning: { box: 'border-amber-200 bg-amber-50', icon: 'text-amber-500', title: 'text-amber-900' },
error: { box: 'border-rose-200 bg-rose-50', icon: 'text-rose-500', title: 'text-rose-900' },
neutral: { box: 'border-slate-200 bg-slate-50', icon: 'text-slate-500', title: 'text-slate-900' },
};
const ICON_PATHS: Record<CalloutVariant, string> = {
info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
neutral: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
success: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
warning:
'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
error: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z',
};
function DefaultIcon({ variant, className }: { variant: CalloutVariant; className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d={ICON_PATHS[variant]} />
</svg>
);
}
export function Callout({
variant = 'info',
title,
icon,
onClose,
children,
className,
}: CalloutProps) {
const styles = variantStyles[variant];
return (
<div role="alert" className={cn('flex gap-3 rounded-2xl border px-4 py-3.5', styles.box, className)}>
{icon !== false && (
<span className={cn('mt-0.5 shrink-0', styles.icon)}>
{icon ?? <DefaultIcon variant={variant} className="size-5" />}
</span>
)}
<div className="min-w-0 flex-1">
{title && <p className={cn('text-sm font-bold', styles.title)}>{title}</p>}
{children && (
<div className={cn('text-sm leading-relaxed text-slate-600', title ? 'mt-1' : undefined)}>{children}</div>
)}
</div>
{onClose && (
<button
type="button"
onClick={onClose}
aria-label="닫기"
className={cn(
'-mr-1 -mt-1 flex size-7 shrink-0 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-black/5 hover:text-slate-600',
)}
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6L6 18" />
<path d="M6 6L18 18" />
</svg>
</button>
)}
</div>
);
}