Files
vexplor_dev/frontend/components/common/UnsavedChangesGuard.tsx
2026-03-12 18:47:42 +09:00

117 lines
2.9 KiB
TypeScript

"use client";
import { useState, useCallback, useRef } from "react";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "@/components/ui/alert-dialog";
interface UseUnsavedChangesGuardOptions {
hasChanges: () => boolean;
onClose: () => void;
title?: string;
description?: string;
}
interface UnsavedChangesGuard {
handleOpenChange: (open: boolean) => void;
tryClose: () => void;
doClose: () => void;
showDialog: boolean;
confirmClose: () => void;
cancelClose: () => void;
title: string;
description: string;
}
export function useUnsavedChangesGuard({
hasChanges,
onClose,
title = "변경사항이 있습니다",
description = "저장하지 않은 변경사항이 사라집니다. 정말 닫으시겠습니까?",
}: UseUnsavedChangesGuardOptions): UnsavedChangesGuard {
const [showDialog, setShowDialog] = useState(false);
const hasChangesRef = useRef(hasChanges);
hasChangesRef.current = hasChanges;
const attemptClose = useCallback(() => {
if (hasChangesRef.current()) {
setShowDialog(true);
} else {
onClose();
}
}, [onClose]);
const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
attemptClose();
}
},
[attemptClose],
);
const confirmClose = useCallback(() => {
setShowDialog(false);
onClose();
}, [onClose]);
const cancelClose = useCallback(() => {
setShowDialog(false);
}, []);
const doClose = useCallback(() => {
setShowDialog(false);
onClose();
}, [onClose]);
return {
handleOpenChange,
tryClose: attemptClose,
doClose,
showDialog,
confirmClose,
cancelClose,
title,
description,
};
}
interface UnsavedChangesDialogProps {
guard: UnsavedChangesGuard;
}
export function UnsavedChangesDialog({ guard }: UnsavedChangesDialogProps) {
return (
<AlertDialog open={guard.showDialog} onOpenChange={(open) => !open && guard.cancelClose()}>
<AlertDialogContent className="max-w-[95vw] sm:max-w-[420px]">
<AlertDialogHeader>
<AlertDialogTitle className="text-base sm:text-lg">
{guard.title}
</AlertDialogTitle>
<AlertDialogDescription className="text-xs sm:text-sm">
{guard.description}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="gap-2 sm:gap-0">
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
</AlertDialogCancel>
<AlertDialogAction
onClick={guard.confirmClose}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}