UX 개선 - 입력값 검증 및 confirm을 모달로 변경

This commit is contained in:
dohyeons 2025-12-23 09:49:44 +09:00
parent 7875d8ab86
commit da195200a8
2 changed files with 99 additions and 17 deletions

View File

@ -42,6 +42,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { SaveAsTemplateModal } from "./SaveAsTemplateModal";
import { reportApi } from "@/lib/api/reportApi";
@ -93,6 +103,8 @@ export function ReportDesignerToolbar() {
} = useReportDesigner();
const [showPreview, setShowPreview] = useState(false);
const [showSaveAsTemplate, setShowSaveAsTemplate] = useState(false);
const [showBackConfirm, setShowBackConfirm] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const { toast } = useToast();
// 버튼 활성화 조건
@ -120,16 +132,14 @@ export function ReportDesignerToolbar() {
router.push("/admin/report");
};
const handleReset = async () => {
if (confirm("현재 변경사항을 모두 취소하고 마지막 저장 상태로 되돌리시겠습니까?")) {
await loadLayout();
}
const handleResetConfirm = async () => {
setShowResetConfirm(false);
await loadLayout();
};
const handleBack = () => {
if (confirm("저장하지 않은 변경사항이 있을 수 있습니다. 목록으로 돌아가시겠습니까?")) {
router.push("/admin/report");
}
const handleBackConfirm = () => {
setShowBackConfirm(false);
router.push("/admin/report");
};
const handleSaveAsTemplate = async (data: {
@ -193,7 +203,7 @@ export function ReportDesignerToolbar() {
<>
<div className="flex items-center justify-between border-b bg-white px-4 py-3 shadow-sm">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBack} className="gap-2">
<Button variant="ghost" size="sm" onClick={() => setShowBackConfirm(true)} className="gap-2">
<ArrowLeft className="h-4 w-4" />
</Button>
@ -437,7 +447,7 @@ export function ReportDesignerToolbar() {
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={handleReset} className="gap-2">
<Button variant="outline" size="sm" onClick={() => setShowResetConfirm(true)} className="gap-2">
<RotateCcw className="h-4 w-4" />
</Button>
@ -491,6 +501,40 @@ export function ReportDesignerToolbar() {
onClose={() => setShowSaveAsTemplate(false)}
onSave={handleSaveAsTemplate}
/>
{/* 목록으로 돌아가기 확인 모달 */}
<AlertDialog open={showBackConfirm} onOpenChange={setShowBackConfirm}>
<AlertDialogContent className="max-w-[400px]">
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
.
<br />
?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleBackConfirm}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 초기화 확인 모달 */}
<AlertDialog open={showResetConfirm} onOpenChange={setShowResetConfirm}>
<AlertDialogContent className="max-w-[400px]">
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleResetConfirm}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View File

@ -3,6 +3,16 @@
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Loader2, RefreshCw } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { reportApi } from "@/lib/api/reportApi";
import { useToast } from "@/hooks/use-toast";
@ -19,6 +29,7 @@ export function TemplatePalette() {
const [customTemplates, setCustomTemplates] = useState<Template[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null);
const { toast } = useToast();
const fetchTemplates = async () => {
@ -49,14 +60,18 @@ export function TemplatePalette() {
await applyTemplate(templateId);
};
const handleDeleteTemplate = async (templateId: string, templateName: string) => {
if (!confirm(`"${templateName}" 템플릿을 삭제하시겠습니까?`)) {
return;
}
const handleDeleteClick = (templateId: string, templateName: string) => {
setDeleteTarget({ id: templateId, name: templateName });
};
const handleDeleteConfirm = async () => {
if (!deleteTarget) return;
setDeletingId(deleteTarget.id);
setDeleteTarget(null);
setDeletingId(templateId);
try {
const response = await reportApi.deleteTemplate(templateId);
const response = await reportApi.deleteTemplate(deleteTarget.id);
if (response.success) {
toast({
title: "성공",
@ -108,7 +123,7 @@ export function TemplatePalette() {
size="sm"
onClick={(e) => {
e.stopPropagation();
handleDeleteTemplate(template.template_id, template.template_name_kor);
handleDeleteClick(template.template_id, template.template_name_kor);
}}
disabled={deletingId === template.template_id}
className="absolute top-1/2 right-1 h-6 w-6 -translate-y-1/2 p-0 opacity-0 transition-opacity group-hover:opacity-100"
@ -123,6 +138,29 @@ export function TemplatePalette() {
))
)}
</div>
{/* 삭제 확인 모달 */}
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<AlertDialogContent className="max-w-[400px]">
<AlertDialogHeader>
<AlertDialogTitle>릿 </AlertDialogTitle>
<AlertDialogDescription>
&quot;{deleteTarget?.name}&quot; 릿 ?
<br />
릿 .
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}