File name
Commit message
Commit date
08-14
File name
Commit message
Commit date
"use client";
import { useEffect, useId, type ReactNode } from "react";
import { IconButton } from "@/components/ui/icon-button";
interface ModalProps {
title: string;
children: ReactNode;
footer?: ReactNode;
/** scrim 클릭·ESC·닫기 버튼에서 호출된다. 실제로 마운트 해제할지는 호출부(feature) 책임. */
onClose: () => void;
}
/**
* 조회/입력 팝업 셸(scrim + 패널). 열림 여부는 호출부가 마운트/언마운트로 제어한다.
* alert.tsx(확인 다이얼로그)와 달리 폼 필드를 담는 용도라 ESC 키·scrim 클릭·닫기 버튼에서
* onClose를 호출하는 상호작용을 자체적으로 가진다 — 그래서 이 컴포넌트만 'use client'.
* 포커스 트랩은 다루지 않는다(호출부 책임).
*/
export function Modal({ title, children, footer, onClose }: ModalProps) {
const titleId = useId();
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
onClose();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-scrim p-4"
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
onClick={(event) => event.stopPropagation()}
className="w-full max-w-lg animate-dialog-in rounded-xl bg-surface p-6 shadow-dialog"
>
<div className="flex items-center justify-between gap-4">
<h2 id={titleId} className="text-title-lg text-foreground">
{title}
</h2>
<IconButton label="닫기" onClick={onClose}>
<span aria-hidden="true" className="text-title-md leading-none">
×
</span>
</IconButton>
</div>
<div className="mt-4">{children}</div>
{footer && <div className="mt-6 flex justify-end gap-2">{footer}</div>}
</div>
</div>
);
}