You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

128 lines
3.3 KiB
TypeScript

// frontend/components/Dialog.tsx
// 앱 전역 통합 다이얼로그 — window.confirm/alert 대체.
// useDialog().confirm({...}) → Promise<boolean>, .alert({...}) → Promise<void>.
"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
type DialogTone = "danger" | "primary";
export type DialogOptions = {
title?: string;
message?: string;
confirmText?: string;
cancelText?: string; // confirm 전용
tone?: DialogTone;
};
type DialogState = DialogOptions & { kind: "confirm" | "alert" };
type DialogApi = {
confirm: (opts: DialogOptions) => Promise<boolean>;
alert: (opts: DialogOptions) => Promise<void>;
};
const Ctx = createContext<DialogApi | null>(null);
export function useDialog(): DialogApi {
const c = useContext(Ctx);
if (!c) throw new Error("useDialog must be used within <DialogProvider>");
return c;
}
function DialogModal({
state,
onClose,
}: {
state: DialogState;
onClose: (ok: boolean) => void;
}) {
const isConfirm = state.kind === "confirm";
const tone: DialogTone = state.tone ?? (isConfirm ? "danger" : "primary");
const okRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
okRef.current?.focus();
const h = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onClose(false);
} else if (e.key === "Enter") {
e.preventDefault();
onClose(true);
}
};
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, [onClose]);
return (
<div className="dlg-backdrop" onClick={() => onClose(false)} role="presentation">
<div
className="dlg-card"
role="alertdialog"
aria-modal="true"
aria-label={state.title ?? "확인"}
onClick={(e) => e.stopPropagation()}
>
{state.title && <h2 className="dlg-title">{state.title}</h2>}
{state.message && <p className="dlg-msg">{state.message}</p>}
<div className="dlg-actions">
{isConfirm && (
<button className="dlg-btn ghost" onClick={() => onClose(false)}>
{state.cancelText ?? "취소"}
</button>
)}
<button
ref={okRef}
className={"dlg-btn " + tone}
onClick={() => onClose(true)}
>
{state.confirmText ?? "확인"}
</button>
</div>
</div>
</div>
);
}
export function DialogProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<DialogState | null>(null);
const resolver = useRef<((ok: boolean) => void) | null>(null);
const close = useCallback((ok: boolean) => {
resolver.current?.(ok);
resolver.current = null;
setState(null);
}, []);
const confirm = useCallback(
(opts: DialogOptions) =>
new Promise<boolean>((resolve) => {
resolver.current = resolve;
setState({ kind: "confirm", ...opts });
}),
[],
);
const alert = useCallback(
(opts: DialogOptions) =>
new Promise<void>((resolve) => {
resolver.current = () => resolve();
setState({ kind: "alert", ...opts });
}),
[],
);
return (
<Ctx.Provider value={{ confirm, alert }}>
{children}
{state && <DialogModal state={state} onClose={close} />}
</Ctx.Provider>
);
}