- Integrated multi-language functionality across the audit log, system notices, collection management, and common code management components, enhancing accessibility for diverse users. - Updated UI elements to utilize translation keys, ensuring that all text is dynamically translated based on user preferences. - Improved error handling messages to be localized, providing a better user experience in case of issues. These changes significantly enhance the usability and internationalization of the management features, making the application more inclusive.
624 lines
22 KiB
TypeScript
624 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from "@/components/ui/dialog";
|
|
import { Plus, Pencil, Trash2, Search, RefreshCw } from "lucide-react";
|
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
|
import {
|
|
SystemNotice,
|
|
CreateSystemNoticePayload,
|
|
getSystemNotices,
|
|
createSystemNotice,
|
|
updateSystemNotice,
|
|
deleteSystemNotice,
|
|
} from "@/lib/api/systemNotice";
|
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
|
import { usePageMultiLang } from "@/hooks/usePageMultiLang";
|
|
|
|
// 다국어 키 목록
|
|
const LANG_KEYS = [
|
|
"notice.page.title",
|
|
"notice.page.description",
|
|
"notice.error.title",
|
|
"notice.error.closeLabel",
|
|
"notice.error.loadFailed",
|
|
"notice.error.saveFailed",
|
|
"notice.error.deleteFailed",
|
|
"notice.filter.statusPlaceholder",
|
|
"notice.filter.all",
|
|
"notice.filter.active",
|
|
"notice.filter.inactive",
|
|
"notice.filter.searchPlaceholder",
|
|
"notice.list.total",
|
|
"notice.list.countSuffix",
|
|
"notice.list.refreshLabel",
|
|
"notice.list.empty",
|
|
"notice.button.create",
|
|
"notice.button.editLabel",
|
|
"notice.button.deleteLabel",
|
|
"notice.column.title",
|
|
"notice.column.status",
|
|
"notice.column.priority",
|
|
"notice.column.author",
|
|
"notice.column.createdAt",
|
|
"notice.column.actions",
|
|
"notice.card.author",
|
|
"notice.card.createdAt",
|
|
"notice.status.active",
|
|
"notice.status.inactive",
|
|
"notice.priority.high",
|
|
"notice.priority.medium",
|
|
"notice.priority.low",
|
|
"notice.form.titleCreate",
|
|
"notice.form.titleEdit",
|
|
"notice.form.descriptionCreate",
|
|
"notice.form.descriptionEdit",
|
|
"notice.form.titleLabel",
|
|
"notice.form.titlePlaceholder",
|
|
"notice.form.contentLabel",
|
|
"notice.form.contentPlaceholder",
|
|
"notice.form.priorityLabel",
|
|
"notice.form.priorityPlaceholder",
|
|
"notice.form.activeLabel",
|
|
"notice.form.cancel",
|
|
"notice.form.save",
|
|
"notice.form.saving",
|
|
"notice.validate.titleRequired",
|
|
"notice.validate.contentRequired",
|
|
"notice.delete.title",
|
|
"notice.delete.description",
|
|
"notice.delete.descriptionIrreversible",
|
|
"notice.delete.cancel",
|
|
"notice.delete.confirm",
|
|
"notice.delete.deleting",
|
|
] as const;
|
|
|
|
// 한국어 기본 텍스트
|
|
const DEFAULT_TEXTS: Record<string, string> = {
|
|
"notice.page.title": "시스템 공지사항",
|
|
"notice.page.description": "시스템 사용자에게 전달할 공지사항을 관리합니다.",
|
|
"notice.error.title": "오류가 발생했습니다",
|
|
"notice.error.closeLabel": "에러 메시지 닫기",
|
|
"notice.error.loadFailed": "공지사항 목록을 불러오는 데 실패했습니다.",
|
|
"notice.error.saveFailed": "저장에 실패했습니다.",
|
|
"notice.error.deleteFailed": "삭제에 실패했습니다.",
|
|
"notice.filter.statusPlaceholder": "상태 필터",
|
|
"notice.filter.all": "전체",
|
|
"notice.filter.active": "활성",
|
|
"notice.filter.inactive": "비활성",
|
|
"notice.filter.searchPlaceholder": "제목 또는 내용으로 검색...",
|
|
"notice.list.total": "총",
|
|
"notice.list.countSuffix": "건",
|
|
"notice.list.refreshLabel": "새로고침",
|
|
"notice.list.empty": "공지사항이 없습니다.",
|
|
"notice.button.create": "등록",
|
|
"notice.button.editLabel": "수정",
|
|
"notice.button.deleteLabel": "삭제",
|
|
"notice.column.title": "제목",
|
|
"notice.column.status": "상태",
|
|
"notice.column.priority": "우선순위",
|
|
"notice.column.author": "작성자",
|
|
"notice.column.createdAt": "작성일",
|
|
"notice.column.actions": "관리",
|
|
"notice.card.author": "작성자",
|
|
"notice.card.createdAt": "작성일",
|
|
"notice.status.active": "활성",
|
|
"notice.status.inactive": "비활성",
|
|
"notice.priority.high": "높음",
|
|
"notice.priority.medium": "보통",
|
|
"notice.priority.low": "낮음",
|
|
"notice.form.titleCreate": "공지사항 등록",
|
|
"notice.form.titleEdit": "공지사항 수정",
|
|
"notice.form.descriptionCreate": "새로운 공지사항을 등록합니다.",
|
|
"notice.form.descriptionEdit": "공지사항 내용을 수정합니다.",
|
|
"notice.form.titleLabel": "제목",
|
|
"notice.form.titlePlaceholder": "공지사항 제목을 입력하세요",
|
|
"notice.form.contentLabel": "내용",
|
|
"notice.form.contentPlaceholder": "공지사항 내용을 입력하세요",
|
|
"notice.form.priorityLabel": "우선순위",
|
|
"notice.form.priorityPlaceholder": "우선순위 선택",
|
|
"notice.form.activeLabel": "활성화 (체크 시 공지사항이 사용자에게 표시됩니다)",
|
|
"notice.form.cancel": "취소",
|
|
"notice.form.save": "저장",
|
|
"notice.form.saving": "저장 중...",
|
|
"notice.validate.titleRequired": "제목을 입력해주세요.",
|
|
"notice.validate.contentRequired": "내용을 입력해주세요.",
|
|
"notice.delete.title": "공지사항 삭제",
|
|
"notice.delete.description": "아래 공지사항을 삭제하시겠습니까?",
|
|
"notice.delete.descriptionIrreversible": "이 작업은 되돌릴 수 없습니다.",
|
|
"notice.delete.cancel": "취소",
|
|
"notice.delete.confirm": "삭제",
|
|
"notice.delete.deleting": "삭제 중...",
|
|
};
|
|
|
|
function getPriorityLabel(priority: number): { langKey: string; variant: "default" | "secondary" | "destructive" | "outline" } {
|
|
if (priority >= 3) return { langKey: "notice.priority.high", variant: "destructive" };
|
|
if (priority === 2) return { langKey: "notice.priority.medium", variant: "default" };
|
|
return { langKey: "notice.priority.low", variant: "secondary" };
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
if (!dateStr) return "-";
|
|
return new Date(dateStr).toLocaleDateString("ko-KR", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
});
|
|
}
|
|
|
|
const EMPTY_FORM: CreateSystemNoticePayload = {
|
|
title: "",
|
|
content: "",
|
|
is_active: true,
|
|
priority: 1,
|
|
};
|
|
|
|
export default function SystemNoticesPage() {
|
|
const { t } = usePageMultiLang({ keys: LANG_KEYS, defaults: DEFAULT_TEXTS, menuCode: "admin.systemNotices" });
|
|
|
|
const [notices, setNotices] = useState<SystemNotice[]>([]);
|
|
const [filteredNotices, setFilteredNotices] = useState<SystemNotice[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
|
const [searchText, setSearchText] = useState("");
|
|
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
|
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
|
const [editTarget, setEditTarget] = useState<SystemNotice | null>(null);
|
|
const [formData, setFormData] = useState<CreateSystemNoticePayload>(EMPTY_FORM);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
const [deleteTarget, setDeleteTarget] = useState<SystemNotice | null>(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
const loadNotices = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setErrorMsg(null);
|
|
const result = await getSystemNotices();
|
|
if (result.success && result.data) {
|
|
setNotices(result.data);
|
|
} else {
|
|
setErrorMsg(result.message || t("notice.error.loadFailed"));
|
|
}
|
|
setIsLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadNotices();
|
|
}, [loadNotices]);
|
|
|
|
useEffect(() => {
|
|
let result = [...notices];
|
|
|
|
if (statusFilter !== "all") {
|
|
const isActive = statusFilter === "active";
|
|
result = result.filter((n) => n.is_active === isActive);
|
|
}
|
|
|
|
if (searchText.trim()) {
|
|
const keyword = searchText.toLowerCase();
|
|
result = result.filter(
|
|
(n) =>
|
|
n.title.toLowerCase().includes(keyword) ||
|
|
n.content.toLowerCase().includes(keyword)
|
|
);
|
|
}
|
|
|
|
setFilteredNotices(result);
|
|
}, [notices, searchText, statusFilter]);
|
|
|
|
const handleOpenCreate = () => {
|
|
setEditTarget(null);
|
|
setFormData(EMPTY_FORM);
|
|
setIsFormOpen(true);
|
|
};
|
|
|
|
const handleOpenEdit = (notice: SystemNotice) => {
|
|
setEditTarget(notice);
|
|
setFormData({
|
|
title: notice.title,
|
|
content: notice.content,
|
|
is_active: notice.is_active,
|
|
priority: notice.priority,
|
|
});
|
|
setIsFormOpen(true);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!formData.title.trim()) {
|
|
alert(t("notice.validate.titleRequired"));
|
|
return;
|
|
}
|
|
if (!formData.content.trim()) {
|
|
alert(t("notice.validate.contentRequired"));
|
|
return;
|
|
}
|
|
|
|
setIsSaving(true);
|
|
let result;
|
|
|
|
if (editTarget) {
|
|
result = await updateSystemNotice(editTarget.id, formData);
|
|
} else {
|
|
result = await createSystemNotice(formData);
|
|
}
|
|
|
|
if (result.success) {
|
|
setIsFormOpen(false);
|
|
await loadNotices();
|
|
} else {
|
|
alert(result.message || t("notice.error.saveFailed"));
|
|
}
|
|
setIsSaving(false);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!deleteTarget) return;
|
|
setIsDeleting(true);
|
|
const result = await deleteSystemNotice(deleteTarget.id);
|
|
if (result.success) {
|
|
setDeleteTarget(null);
|
|
await loadNotices();
|
|
} else {
|
|
alert(result.message || t("notice.error.deleteFailed"));
|
|
}
|
|
setIsDeleting(false);
|
|
};
|
|
|
|
const columns: RDVColumn<SystemNotice>[] = [
|
|
{
|
|
key: "title",
|
|
label: t("notice.column.title"),
|
|
render: (_val, notice) => (
|
|
<span className="font-medium">{notice.title}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "is_active",
|
|
label: t("notice.column.status"),
|
|
width: "100px",
|
|
render: (_val, notice) => (
|
|
<Badge variant={notice.is_active ? "default" : "secondary"}>
|
|
{notice.is_active ? t("notice.status.active") : t("notice.status.inactive")}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
key: "priority",
|
|
label: t("notice.column.priority"),
|
|
width: "100px",
|
|
render: (_val, notice) => {
|
|
const p = getPriorityLabel(notice.priority);
|
|
return <Badge variant={p.variant}>{t(p.langKey)}</Badge>;
|
|
},
|
|
},
|
|
{
|
|
key: "created_by",
|
|
label: t("notice.column.author"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (_val, notice) => (
|
|
<span className="text-muted-foreground">{notice.created_by || "-"}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "created_at",
|
|
label: t("notice.column.createdAt"),
|
|
width: "120px",
|
|
hideOnMobile: true,
|
|
render: (_val, notice) => (
|
|
<span className="text-muted-foreground">{formatDate(notice.created_at)}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
const cardFields: RDVCardField<SystemNotice>[] = [
|
|
{
|
|
label: t("notice.card.author"),
|
|
render: (notice) => notice.created_by || "-",
|
|
},
|
|
{
|
|
label: t("notice.card.createdAt"),
|
|
render: (notice) => formatDate(notice.created_at),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="flex min-h-screen flex-col bg-background">
|
|
<div className="space-y-6 p-6">
|
|
{/* 페이지 헤더 */}
|
|
<div className="space-y-2 border-b pb-4">
|
|
<h1 className="text-3xl font-bold tracking-tight">{t("notice.page.title")}</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("notice.page.description")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{errorMsg && (
|
|
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm font-semibold text-destructive">{t("notice.error.title")}</p>
|
|
<button
|
|
onClick={() => setErrorMsg(null)}
|
|
className="text-destructive transition-colors hover:text-destructive/80"
|
|
aria-label={t("notice.error.closeLabel")}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<p className="mt-1.5 text-sm text-destructive/80">{errorMsg}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 검색 툴바 */}
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
<div className="w-full sm:w-[160px]">
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="h-10">
|
|
<SelectValue placeholder={t("notice.filter.statusPlaceholder")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">{t("notice.filter.all")}</SelectItem>
|
|
<SelectItem value="active">{t("notice.filter.active")}</SelectItem>
|
|
<SelectItem value="inactive">{t("notice.filter.inactive")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="relative w-full sm:w-[300px]">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder={t("notice.filter.searchPlaceholder")}
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
className="h-10 pl-10 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm text-muted-foreground">
|
|
{t("notice.list.total")} <span className="font-semibold text-foreground">{filteredNotices.length}</span> {t("notice.list.countSuffix")}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-10 w-10"
|
|
onClick={loadNotices}
|
|
aria-label={t("notice.list.refreshLabel")}
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
</Button>
|
|
<Button className="h-10 gap-2 text-sm font-medium" onClick={handleOpenCreate}>
|
|
<Plus className="h-4 w-4" />
|
|
{t("notice.button.create")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ResponsiveDataView<SystemNotice>
|
|
data={filteredNotices}
|
|
columns={columns}
|
|
keyExtractor={(n) => String(n.id)}
|
|
isLoading={isLoading}
|
|
emptyMessage={t("notice.list.empty")}
|
|
skeletonCount={5}
|
|
cardTitle={(n) => n.title}
|
|
cardHeaderRight={(n) => (
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => handleOpenEdit(n)}
|
|
aria-label={t("notice.button.editLabel")}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
onClick={() => setDeleteTarget(n)}
|
|
aria-label={t("notice.button.deleteLabel")}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
cardSubtitle={(n) => {
|
|
const p = getPriorityLabel(n.priority);
|
|
return (
|
|
<span className="flex flex-wrap gap-2 pt-1">
|
|
<Badge variant={n.is_active ? "default" : "secondary"}>
|
|
{n.is_active ? t("notice.status.active") : t("notice.status.inactive")}
|
|
</Badge>
|
|
<Badge variant={p.variant}>{t(p.langKey)}</Badge>
|
|
</span>
|
|
);
|
|
}}
|
|
cardFields={cardFields}
|
|
actionsLabel={t("notice.column.actions")}
|
|
actionsWidth="120px"
|
|
renderActions={(notice) => (
|
|
<>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => handleOpenEdit(notice)}
|
|
aria-label={t("notice.button.editLabel")}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
onClick={() => setDeleteTarget(notice)}
|
|
aria-label={t("notice.button.deleteLabel")}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{/* 등록/수정 모달 */}
|
|
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[540px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
{editTarget ? t("notice.form.titleEdit") : t("notice.form.titleCreate")}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
{editTarget ? t("notice.form.descriptionEdit") : t("notice.form.descriptionCreate")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="notice-title" className="text-xs sm:text-sm">
|
|
{t("notice.form.titleLabel")} <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="notice-title"
|
|
value={formData.title}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
|
placeholder={t("notice.form.titlePlaceholder")}
|
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="notice-content" className="text-xs sm:text-sm">
|
|
{t("notice.form.contentLabel")} <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Textarea
|
|
id="notice-content"
|
|
value={formData.content}
|
|
onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))}
|
|
placeholder={t("notice.form.contentPlaceholder")}
|
|
className="mt-1 min-h-[120px] text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="notice-priority" className="text-xs sm:text-sm">
|
|
{t("notice.form.priorityLabel")}
|
|
</Label>
|
|
<Select
|
|
value={String(formData.priority)}
|
|
onValueChange={(val) =>
|
|
setFormData((prev) => ({ ...prev, priority: Number(val) }))
|
|
}
|
|
>
|
|
<SelectTrigger id="notice-priority" className="mt-1 h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue placeholder={t("notice.form.priorityPlaceholder")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1">{t("notice.priority.low")}</SelectItem>
|
|
<SelectItem value="2">{t("notice.priority.medium")}</SelectItem>
|
|
<SelectItem value="3">{t("notice.priority.high")}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="notice-active"
|
|
checked={formData.is_active}
|
|
onCheckedChange={(checked) =>
|
|
setFormData((prev) => ({ ...prev, is_active: !!checked }))
|
|
}
|
|
/>
|
|
<Label htmlFor="notice-active" className="cursor-pointer text-xs sm:text-sm">
|
|
{t("notice.form.activeLabel")}
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsFormOpen(false)}
|
|
disabled={isSaving}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{t("notice.form.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{isSaving ? t("notice.form.saving") : t("notice.form.save")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* 삭제 확인 모달 */}
|
|
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[440px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">{t("notice.delete.title")}</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
{t("notice.delete.description")}
|
|
<br />{t("notice.delete.descriptionIrreversible")}
|
|
<br />
|
|
<span className="mt-2 block font-medium text-foreground">
|
|
"{deleteTarget?.title}"
|
|
</span>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setDeleteTarget(null)}
|
|
disabled={isDeleting}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{t("notice.delete.cancel")}
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDelete}
|
|
disabled={isDeleting}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{isDeleting ? t("notice.delete.deleting") : t("notice.delete.confirm")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<ScrollToTop />
|
|
</div>
|
|
);
|
|
}
|