import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import { Button } from '@shared/components/ui/button'; import { PageContainer, PageHeader } from '@shared/components/layout'; import { useAuth } from '@/app/auth/AuthContext'; import type { BadgeIntent } from '@lib/theme/variants'; import { Bell, Plus, Edit2, Trash2, Eye, Megaphone, AlertTriangle, Info, Clock, Pin, Monitor, MessageSquare, X, } from 'lucide-react'; import { toDateParam } from '@shared/utils/dateFormat'; import { SaveButton } from '@shared/components/common/SaveButton'; import type { SystemNotice, NoticeType, NoticeDisplay } from '@shared/components/common/NotificationBanner'; /* * SFR-02: 공통알림(팝업, 배너, 시스템 공지 등) 관리 * - 노출 기간 설정 (시작일 ~ 종료일) * - 대상 설정 (역할 기반) * - 알림 유형: 정보, 경고, 긴급, 점검 * - 표시 방식: 배너, 팝업, 토스트 */ // ─── 시뮬레이션 데이터 ────────────────── const INITIAL_NOTICES: SystemNotice[] = [ { id: 'N-001', type: 'urgent', display: 'banner', title: '서해 NLL 인근 경보 강화', message: '2026-04-03부터 서해 NLL 인근 해역에 대한 경계 경보가 강화되었습니다. 모든 상황실 운영자는 경계 태세를 유지하시기 바랍니다.', startDate: '2026-04-03', endDate: '2026-04-10', targetRoles: ['ADMIN', 'OPERATOR'], dismissible: true, pinned: true, }, { id: 'N-002', type: 'maintenance', display: 'popup', title: '정기 시스템 점검 안내', message: '2026-04-05(토) 02:00~06:00 시스템 정기점검이 예정되어 있습니다. 점검 중 서비스 이용이 제한될 수 있습니다.', startDate: '2026-04-03', endDate: '2026-04-05', targetRoles: [], dismissible: true, pinned: false, }, { id: 'N-003', type: 'info', display: 'banner', title: 'AI 탐지 모델 v2.3 업데이트', message: '다크베셀 탐지 정확도가 89% → 93%로 개선되었습니다. 환적 탐지 알고리즘이 업데이트되었습니다.', startDate: '2026-04-01', endDate: '2026-04-15', targetRoles: ['ADMIN', 'ANALYST'], dismissible: true, pinned: false, }, { id: 'N-004', type: 'warning', display: 'banner', title: '비밀번호 변경 권고', message: '비밀번호 변경 주기(90일)가 도래한 사용자는 보안정책에 따라 비밀번호를 변경해 주세요.', startDate: '2026-03-25', endDate: '2026-04-25', targetRoles: [], dismissible: true, pinned: false, }, { id: 'N-005', type: 'info', display: 'toast', title: 'S-57 해도 데이터 갱신', message: '해경GIS통합위치정보시스템 S-57 전자해도 데이터가 2026-04-01 기준으로 갱신되었습니다.', startDate: '2026-04-01', endDate: '2026-04-07', targetRoles: ['ADMIN', 'OPERATOR', 'ANALYST'], dismissible: true, pinned: false, }, { id: 'N-006', type: 'urgent', display: 'popup', title: '중국어선 대규모 출항 정보', message: '중국 산동성 위해·영성 항에서 대규모 어선단(약 300척) 출항이 감지되었습니다. 서해 EEZ 진입 예상시간: 2026-04-04 06:00경.', startDate: '2026-04-03', endDate: '2026-04-06', targetRoles: ['ADMIN', 'OPERATOR'], dismissible: false, pinned: true, }, ]; const TYPE_OPTIONS: { key: NoticeType; label: string; icon: React.ElementType; color: string }[] = [ { key: 'info', label: '정보', icon: Info, color: 'text-label' }, { key: 'warning', label: '경고', icon: AlertTriangle, color: 'text-label' }, { key: 'urgent', label: '긴급', icon: Bell, color: 'text-label' }, { key: 'maintenance', label: '점검', icon: Megaphone, color: 'text-label' }, ]; const DISPLAY_OPTIONS: { key: NoticeDisplay; label: string; icon: React.ElementType }[] = [ { key: 'banner', label: '배너', icon: Monitor }, { key: 'popup', label: '팝업', icon: MessageSquare }, { key: 'toast', label: '토스트', icon: Bell }, ]; const ROLE_OPTIONS = ['ADMIN', 'OPERATOR', 'ANALYST', 'FIELD', 'VIEWER']; export function NoticeManagement() { const { t } = useTranslation('admin'); const { t: tc } = useTranslation('common'); const { hasPermission } = useAuth(); const canCreate = hasPermission('admin:notices', 'CREATE'); const canUpdate = hasPermission('admin:notices', 'UPDATE'); const canDelete = hasPermission('admin:notices', 'DELETE'); const [notices, setNotices] = useState(INITIAL_NOTICES); const [editingId, setEditingId] = useState(null); const [showForm, setShowForm] = useState(false); const [form, setForm] = useState({ id: '', type: 'info', display: 'banner', title: '', message: '', startDate: toDateParam(), endDate: toDateParam(new Date(Date.now() + 7 * 86400000)), targetRoles: [], dismissible: true, pinned: false, }); const now = toDateParam(); const openNew = () => { setForm({ id: `N-${String(notices.length + 1).padStart(3, '0')}`, type: 'info', display: 'banner', title: '', message: '', startDate: now, endDate: toDateParam(new Date(Date.now() + 7 * 86400000)), targetRoles: [], dismissible: true, pinned: false, }); setEditingId(null); setShowForm(true); }; const openEdit = (notice: SystemNotice) => { setForm({ ...notice }); setEditingId(notice.id); setShowForm(true); }; const handleSave = () => { if (editingId) { setNotices((prev) => prev.map((n) => n.id === editingId ? form : n)); } else { setNotices((prev) => [form, ...prev]); } setShowForm(false); }; const handleDelete = (id: string) => { setNotices((prev) => prev.filter((n) => n.id !== id)); }; const toggleRole = (role: string) => { setForm((prev) => ({ ...prev, targetRoles: prev.targetRoles.includes(role) ? prev.targetRoles.filter((r) => r !== role) : [...prev.targetRoles, role], })); }; const getStatus = (n: SystemNotice): { label: string; intent: BadgeIntent } => { if (n.endDate < now) return { label: '종료', intent: 'muted' }; if (n.startDate > now) return { label: '예약', intent: 'info' }; return { label: '노출 중', intent: 'success' }; }; // KPI const activeCount = notices.filter((n) => n.startDate <= now && n.endDate >= now).length; const scheduledCount = notices.filter((n) => n.startDate > now).length; const urgentCount = notices.filter((n) => n.type === 'urgent' && n.startDate <= now && n.endDate >= now).length; return ( }> 새 알림 등록 } /> {/* KPI — 가로 한 줄 */}
{[ { label: '전체 알림', count: notices.length, icon: Bell, color: 'text-label', bg: 'bg-muted' }, { label: '현재 노출 중', count: activeCount, icon: Eye, color: 'text-label', bg: 'bg-green-500/10' }, { label: '예약됨', count: scheduledCount, icon: Clock, color: 'text-label', bg: 'bg-blue-500/10' }, { label: '긴급 알림', count: urgentCount, icon: AlertTriangle, color: 'text-label', bg: 'bg-red-500/10' }, ].map((kpi) => (
{kpi.count} {kpi.label}
))}
{/* 알림 목록 */} {notices.map((n) => { const status = getStatus(n); const typeOpt = TYPE_OPTIONS.find((t) => t.key === n.type)!; const dispOpt = DISPLAY_OPTIONS.find((d) => d.key === n.display)!; return ( ); })}
상태 유형 표시 제목 노출기간 대상 고정 관리
{status.label} {typeOpt.label} {dispOpt.label} {n.title} {n.message.slice(0, 50)}… {n.startDate.slice(5)} ~ {n.endDate.slice(5)} {n.targetRoles.length === 0 ? ( 전체 ) : ( {n.targetRoles.join(' · ')} )} {n.pinned && }
{/* ── 등록/수정 폼 모달 ── */} {showForm && (
{editingId ? '알림 수정' : '새 알림 등록'}
{/* 제목 */}
setForm({ ...form, title: e.target.value })} className="w-full bg-surface-overlay border border-slate-700/50 rounded-lg px-3 py-2 text-[11px] text-heading placeholder:text-hint focus:outline-none focus:border-blue-500/50" placeholder="알림 제목 입력" />
{/* 내용 */}