From 74d0e730cd1aca6dc2cfd022053db4d263950099 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Thu, 5 Mar 2026 18:49:33 +0900 Subject: [PATCH] [agent-pipeline] pipe-20260305094105-763v round-3 --- .../app/(main)/admin/system-notices/page.tsx | 548 ++++++++++++++++++ frontend/lib/api/systemNotice.ts | 90 +++ test-results/.last-run.json | 4 + 3 files changed, 642 insertions(+) create mode 100644 frontend/app/(main)/admin/system-notices/page.tsx create mode 100644 frontend/lib/api/systemNotice.ts create mode 100644 test-results/.last-run.json diff --git a/frontend/app/(main)/admin/system-notices/page.tsx b/frontend/app/(main)/admin/system-notices/page.tsx new file mode 100644 index 00000000..c3815de7 --- /dev/null +++ b/frontend/app/(main)/admin/system-notices/page.tsx @@ -0,0 +1,548 @@ +"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 { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +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"; + +// 우선순위 레이블 반환 +function getPriorityLabel(priority: number): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } { + if (priority >= 3) return { label: "높음", variant: "destructive" }; + if (priority === 2) return { label: "보통", variant: "default" }; + return { label: "낮음", 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 [notices, setNotices] = useState([]); + const [filteredNotices, setFilteredNotices] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [errorMsg, setErrorMsg] = useState(null); + + // 검색 필터 + const [searchText, setSearchText] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + // 등록/수정 모달 + const [isFormOpen, setIsFormOpen] = useState(false); + const [editTarget, setEditTarget] = useState(null); + const [formData, setFormData] = useState(EMPTY_FORM); + const [isSaving, setIsSaving] = useState(false); + + // 삭제 확인 모달 + const [deleteTarget, setDeleteTarget] = useState(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 || "공지사항 목록을 불러오는 데 실패했습니다."); + } + 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("제목을 입력해주세요."); + return; + } + if (!formData.content.trim()) { + alert("내용을 입력해주세요."); + 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 || "저장에 실패했습니다."); + } + 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 || "삭제에 실패했습니다."); + } + setIsDeleting(false); + }; + + return ( +
+
+ {/* 페이지 헤더 */} +
+

시스템 공지사항

+

+ 시스템 사용자에게 전달할 공지사항을 관리합니다. +

+
+ + {/* 에러 메시지 */} + {errorMsg && ( +
+
+

오류가 발생했습니다

+ +
+

{errorMsg}

+
+ )} + + {/* 검색 툴바 */} +
+
+ {/* 상태 필터 */} +
+ +
+ + {/* 제목 검색 */} +
+ + setSearchText(e.target.value)} + className="h-10 pl-10 text-sm" + /> +
+
+ +
+ + 총 {filteredNotices.length} 건 + + + +
+
+ + {/* 데스크톱 테이블 */} +
+ + + + 제목 + 상태 + 우선순위 + 작성자 + 작성일 + 관리 + + + + {isLoading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 6 }).map((_, j) => ( + +
+ + ))} + + )) + ) : filteredNotices.length === 0 ? ( + + + 공지사항이 없습니다. + + + ) : ( + filteredNotices.map((notice) => { + const priority = getPriorityLabel(notice.priority); + return ( + + {notice.title} + + + {notice.is_active ? "활성" : "비활성"} + + + + {priority.label} + + + {notice.created_by || "-"} + + + {formatDate(notice.created_at)} + + +
+ + +
+
+
+ ); + }) + )} + +
+
+ + {/* 모바일 카드 뷰 */} +
+ {isLoading ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+ )) + ) : filteredNotices.length === 0 ? ( +
+ 공지사항이 없습니다. +
+ ) : ( + filteredNotices.map((notice) => { + const priority = getPriorityLabel(notice.priority); + return ( +
+
+

{notice.title}

+
+ + +
+
+
+ + {notice.is_active ? "활성" : "비활성"} + + {priority.label} +
+
+
+ 작성자 + {notice.created_by || "-"} +
+
+ 작성일 + {formatDate(notice.created_at)} +
+
+
+ ); + }) + )} +
+
+ + {/* 등록/수정 모달 */} + + + + + {editTarget ? "공지사항 수정" : "공지사항 등록"} + + + {editTarget ? "공지사항 내용을 수정합니다." : "새로운 공지사항을 등록합니다."} + + + +
+ {/* 제목 */} +
+ + setFormData((prev) => ({ ...prev, title: e.target.value }))} + placeholder="공지사항 제목을 입력하세요" + className="mt-1 h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ + {/* 내용 */} +
+ +