[agent-pipeline] pipe-20260305094105-763v round-3
This commit is contained in:
parent
9a328ade78
commit
74d0e730cd
|
|
@ -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<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 || "공지사항 목록을 불러오는 데 실패했습니다.");
|
||||
}
|
||||
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 (
|
||||
<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">시스템 공지사항</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
시스템 사용자에게 전달할 공지사항을 관리합니다.
|
||||
</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">오류가 발생했습니다</p>
|
||||
<button
|
||||
onClick={() => setErrorMsg(null)}
|
||||
className="text-destructive transition-colors hover:text-destructive/80"
|
||||
aria-label="에러 메시지 닫기"
|
||||
>
|
||||
✕
|
||||
</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="상태 필터" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">전체</SelectItem>
|
||||
<SelectItem value="active">활성</SelectItem>
|
||||
<SelectItem value="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="제목 또는 내용으로 검색..."
|
||||
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">
|
||||
총 <span className="font-semibold text-foreground">{filteredNotices.length}</span> 건
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
onClick={loadNotices}
|
||||
aria-label="새로고침"
|
||||
>
|
||||
<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" />
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데스크톱 테이블 */}
|
||||
<div className="hidden rounded-lg border bg-card shadow-sm lg:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
||||
<TableHead className="h-12 text-sm font-semibold">제목</TableHead>
|
||||
<TableHead className="h-12 w-[100px] text-sm font-semibold">상태</TableHead>
|
||||
<TableHead className="h-12 w-[100px] text-sm font-semibold">우선순위</TableHead>
|
||||
<TableHead className="h-12 w-[120px] text-sm font-semibold">작성자</TableHead>
|
||||
<TableHead className="h-12 w-[120px] text-sm font-semibold">작성일</TableHead>
|
||||
<TableHead className="h-12 w-[120px] text-sm font-semibold">관리</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<TableRow key={i} className="border-b">
|
||||
{Array.from({ length: 6 }).map((_, j) => (
|
||||
<TableCell key={j} className="h-16">
|
||||
<div className="h-4 animate-pulse rounded bg-muted" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : filteredNotices.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-32 text-center text-sm text-muted-foreground">
|
||||
공지사항이 없습니다.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredNotices.map((notice) => {
|
||||
const priority = getPriorityLabel(notice.priority);
|
||||
return (
|
||||
<TableRow key={notice.id} className="border-b transition-colors hover:bg-muted/50">
|
||||
<TableCell className="h-16 text-sm font-medium">{notice.title}</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<Badge variant={notice.is_active ? "default" : "secondary"}>
|
||||
{notice.is_active ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<Badge variant={priority.variant}>{priority.label}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm text-muted-foreground">
|
||||
{notice.created_by || "-"}
|
||||
</TableCell>
|
||||
<TableCell className="h-16 text-sm text-muted-foreground">
|
||||
{formatDate(notice.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="h-16">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleOpenEdit(notice)}
|
||||
aria-label="수정"
|
||||
>
|
||||
<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="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 모바일 카드 뷰 */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:hidden">
|
||||
{isLoading ? (
|
||||
Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="rounded-lg border bg-card p-4 shadow-sm">
|
||||
<div className="space-y-2">
|
||||
<div className="h-5 w-3/4 animate-pulse rounded bg-muted" />
|
||||
<div className="h-4 w-1/2 animate-pulse rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : filteredNotices.length === 0 ? (
|
||||
<div className="col-span-2 flex h-32 items-center justify-center rounded-lg border bg-card text-sm text-muted-foreground">
|
||||
공지사항이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
filteredNotices.map((notice) => {
|
||||
const priority = getPriorityLabel(notice.priority);
|
||||
return (
|
||||
<div key={notice.id} className="rounded-lg border bg-card p-4 shadow-sm">
|
||||
<div className="mb-3 flex items-start justify-between">
|
||||
<h3 className="flex-1 text-base font-semibold">{notice.title}</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleOpenEdit(notice)}
|
||||
aria-label="수정"
|
||||
>
|
||||
<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="삭제"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 border-t pt-3">
|
||||
<Badge variant={notice.is_active ? "default" : "secondary"}>
|
||||
{notice.is_active ? "활성" : "비활성"}
|
||||
</Badge>
|
||||
<Badge variant={priority.variant}>{priority.label}</Badge>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">작성자</span>
|
||||
<span className="font-medium">{notice.created_by || "-"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">작성일</span>
|
||||
<span className="font-medium">{formatDate(notice.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 등록/수정 모달 */}
|
||||
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[540px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base sm:text-lg">
|
||||
{editTarget ? "공지사항 수정" : "공지사항 등록"}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
{editTarget ? "공지사항 내용을 수정합니다." : "새로운 공지사항을 등록합니다."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* 제목 */}
|
||||
<div>
|
||||
<Label htmlFor="notice-title" className="text-xs sm:text-sm">
|
||||
제목 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="notice-title"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
||||
placeholder="공지사항 제목을 입력하세요"
|
||||
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">
|
||||
내용 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notice-content"
|
||||
value={formData.content}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, content: e.target.value }))}
|
||||
placeholder="공지사항 내용을 입력하세요"
|
||||
className="mt-1 min-h-[120px] text-xs sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 우선순위 */}
|
||||
<div>
|
||||
<Label htmlFor="notice-priority" className="text-xs sm:text-sm">
|
||||
우선순위
|
||||
</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="우선순위 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">낮음</SelectItem>
|
||||
<SelectItem value="2">보통</SelectItem>
|
||||
<SelectItem value="3">높음</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">
|
||||
활성화 (체크 시 공지사항이 사용자에게 표시됩니다)
|
||||
</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"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
||||
>
|
||||
{isSaving ? "저장 중..." : "저장"}
|
||||
</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">공지사항 삭제</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-sm">
|
||||
아래 공지사항을 삭제하시겠습니까?
|
||||
<br />이 작업은 되돌릴 수 없습니다.
|
||||
<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"
|
||||
>
|
||||
취소
|
||||
</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 ? "삭제 중..." : "삭제"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ScrollToTop />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
export interface SystemNotice {
|
||||
id: number;
|
||||
company_code: string;
|
||||
title: string;
|
||||
content: string;
|
||||
is_active: boolean;
|
||||
priority: number;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateSystemNoticePayload {
|
||||
title: string;
|
||||
content: string;
|
||||
is_active: boolean;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface UpdateSystemNoticePayload {
|
||||
title?: string;
|
||||
content?: string;
|
||||
is_active?: boolean;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 공지사항 목록 조회
|
||||
export async function getSystemNotices(): Promise<ApiResponse<SystemNotice[]>> {
|
||||
try {
|
||||
const response = await apiClient.get("/system-notices");
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || error.message || "공지사항 목록 조회 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 공지사항 등록
|
||||
export async function createSystemNotice(
|
||||
payload: CreateSystemNoticePayload
|
||||
): Promise<ApiResponse<SystemNotice>> {
|
||||
try {
|
||||
const response = await apiClient.post("/system-notices", payload);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || error.message || "공지사항 등록 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 공지사항 수정
|
||||
export async function updateSystemNotice(
|
||||
id: number,
|
||||
payload: UpdateSystemNoticePayload
|
||||
): Promise<ApiResponse<SystemNotice>> {
|
||||
try {
|
||||
const response = await apiClient.put(`/system-notices/${id}`, payload);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || error.message || "공지사항 수정 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 공지사항 삭제
|
||||
export async function deleteSystemNotice(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
const response = await apiClient.delete(`/system-notices/${id}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || error.message || "공지사항 삭제 실패",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
Loading…
Reference in New Issue