diff --git a/app/admin/page.tsx b/app/admin/page.tsx index bc1590a..487ce41 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -12,6 +12,7 @@ import GithubLogin from '@/app/entities/common/Button/GithubLogin'; import BubbleBackground from '@/app/entities/common/Background/BubbleBackground'; import { useEffect } from 'react'; import useToast from '@/app/hooks/useToast'; +import { FaBuffer } from 'react-icons/fa6'; const AdminDashboard = () => { const { data: session } = useSession(); @@ -67,6 +68,13 @@ const AdminDashboard = () => { bgColor: 'bg-purple-950/20', // 짙은 보라색의 투명도 적용 link: '/admin/analytics', }, + { + title: '시리즈 관리', + icon: , + description: '블로그 시리즈를 관리합니다.', + bgColor: 'bg-emerald-950/20', // 짙은 보라색의 투명도 적용 + link: '/admin/series', + }, { title: '댓글 확인 및 관리', icon: , diff --git a/app/admin/series/page.tsx b/app/admin/series/page.tsx new file mode 100644 index 0000000..8210cfc --- /dev/null +++ b/app/admin/series/page.tsx @@ -0,0 +1,116 @@ +'use client'; +import { Series } from '@/app/types/Series'; +import useDataFetch, { + useDataFetchConfig, +} from '@/app/hooks/common/useDataFetch'; +import { useState } from 'react'; +import AdminSeriesList from '@/app/entities/series/list/AdminSeriesList'; +import Overlay from '@/app/entities/common/Overlay/Overlay'; +import CreateSeriesOverlayContainer from '@/app/entities/series/CreateSeriesOverlayContainer'; +import { deleteSeries } from '@/app/entities/series/api/series'; +import DeleteModal from '@/app/entities/common/Modal/DeleteModal'; +const AdminSeriesPage = () => { + const [seriesList, setSeriesList] = useState(null); + const getSeriesListConfig: useDataFetchConfig = { + url: '/api/series', + method: 'GET', + config: { + params: { + compact: 'true', + }, + }, + onSuccess: (data: Series[]) => { + setSeriesList(data); + }, + }; + const { loading } = useDataFetch(getSeriesListConfig); + const [createSeriesOpen, setCreateSeriesOpen] = useState(false); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [selectedSeries, setSelectedSeries] = useState(null); + const handleUpdateSeries = (series: Series) => { + setCreateSeriesOpen(true); + setSelectedSeries(series); + }; + const handleCloseOverlay = () => { + setCreateSeriesOpen(false); + setSelectedSeries(null); + }; + + const handleDeleteSeries = async (slug: string) => { + if (!seriesList) return; + try { + const data = await deleteSeries(slug); + if (data.success) { + console.log('시리즈 삭제 성공:', data); + } else { + console.error('시리즈 삭제 실패:', data); + } + } catch (error) { + console.error('시리즈 삭제 중 오류 발생:', error); + } + + const updatedSeriesList = seriesList.filter( + (series) => series.slug !== slug + ); + setSeriesList(updatedSeriesList); + setShowDeleteDialog(false); + setSelectedSeries(null); + }; + + const handleDeleteClick = (slug: string) => { + setShowDeleteDialog(true); + setSelectedSeries( + seriesList?.find((series) => series.slug === slug) || null + ); + }; + + return ( +
+

시리즈 관리

+

+ 시리즈를 관리하는 페이지입니다. 시리즈를 추가, 수정, 삭제할 수 있습니다. +

+
+ +
+
+

+ 등록된 시리즈 목록 ({seriesList?.length || 0}) +

+
+ +
+ + + + {showDeleteDialog && ( + setShowDeleteDialog(false)} + onConfirm={() => handleDeleteSeries(selectedSeries?.slug || '')} + /> + )} +
+ ); +}; + +export default AdminSeriesPage; diff --git a/app/api/series/[slug]/route.ts b/app/api/series/[slug]/route.ts index 79e3530..420453c 100644 --- a/app/api/series/[slug]/route.ts +++ b/app/api/series/[slug]/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import dbConnect from '@/app/lib/dbConnect'; import Series from '@/app/models/Series'; import '@/app/models/Post'; +import { getServerSession } from 'next-auth'; export async function GET( request: Request, @@ -41,6 +42,12 @@ export async function PUT( { params }: { params: { slug: string } } ) { try { + const session = await getServerSession(); + + if (!session) { + return new Response('Unauthorized', { status: 401 }); + } + await dbConnect(); const body = await request.json(); @@ -77,6 +84,12 @@ export async function DELETE( { params }: { params: { slug: string } } ) { try { + const session = await getServerSession(); + + if (!session) { + return new Response('Unauthorized', { status: 401 }); + } + await dbConnect(); const deletedSeries = await Series.findOneAndDelete({ slug: params.slug }); diff --git a/app/api/series/route.ts b/app/api/series/route.ts index a771374..9bf9cd6 100644 --- a/app/api/series/route.ts +++ b/app/api/series/route.ts @@ -54,7 +54,7 @@ export async function GET(request: Request) { return NextResponse.json(series, { status: 200, headers: { - 'Cache-Control': 'public, max-age=60, s-maxage=60', + 'Cache-Control': 'public, max-age=30, s-maxage=30', }, }); } catch (error: any) { diff --git a/app/entities/common/Modal/DeleteModal.tsx b/app/entities/common/Modal/DeleteModal.tsx index fea3201..82e5913 100644 --- a/app/entities/common/Modal/DeleteModal.tsx +++ b/app/entities/common/Modal/DeleteModal.tsx @@ -1,13 +1,16 @@ const DeleteModal = (props: { onCancel: () => void; onConfirm: () => void; + message?: string; }) => { + const defaultMessage = + '게시글을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다.'; return (

게시글을 삭제하시겠습니까?

- 이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다. + {props.message ? props.message : defaultMessage}

diff --git a/app/entities/series/api/series.ts b/app/entities/series/api/series.ts index b9d38cb..f0af9c8 100644 --- a/app/entities/series/api/series.ts +++ b/app/entities/series/api/series.ts @@ -31,10 +31,13 @@ export const updateSeries = async ( title: string; description: string; thumbnailImage: string; - order: string[]; - posts: string[]; } ) => { const response = await axios.put(`/api/series/${slug}`, data); return response.data; }; + +export const deleteSeries = async (slug: string) => { + const response = await axios.delete(`/api/series/${slug}`); + return response.data; +}; diff --git a/app/entities/series/list/AdminSeriesList.tsx b/app/entities/series/list/AdminSeriesList.tsx new file mode 100644 index 0000000..4cdd705 --- /dev/null +++ b/app/entities/series/list/AdminSeriesList.tsx @@ -0,0 +1,38 @@ +import { Series } from '@/app/types/Series'; +import React from 'react'; +import AdminSeriesListItem from '@/app/entities/series/list/AdminSeriesListItem'; + +interface AdminSeriesListProps { + seriesList: Series[] | null | undefined; + loading: boolean; + handleUpdateSeries: (series: Series) => void; + handleDeleteClick: (slug: string) => void; +} +const AdminSeriesList = ({ + loading, + seriesList, + handleUpdateSeries, + handleDeleteClick, +}: AdminSeriesListProps) => { + if (loading) { + return

로딩 중...

; + } + if (!seriesList || seriesList.length === 0) { + return

등록된 시리즈가 없습니다.

; + } + return ( +
    + {loading &&

    로딩 중...

    } + {seriesList.map((series, index) => ( + + ))} +
+ ); +}; + +export default AdminSeriesList; diff --git a/app/entities/series/list/AdminSeriesListItem.tsx b/app/entities/series/list/AdminSeriesListItem.tsx new file mode 100644 index 0000000..33860bc --- /dev/null +++ b/app/entities/series/list/AdminSeriesListItem.tsx @@ -0,0 +1,80 @@ +import { Series } from '@/app/types/Series'; +import Image from 'next/image'; +import { FaBookOpen, FaCalendar } from 'react-icons/fa'; +import React from 'react'; + +interface AdminSeriesListItemProps { + series: Series; + handleUpdateSeries: (series: Series) => void; + handleDeleteClick: (slug: string) => void; +} + +const AdminSeriesListItem = ({ + series, + handleUpdateSeries, + handleDeleteClick, +}: AdminSeriesListItemProps) => { + return ( +
  • +
    + {series.thumbnailImage ? ( + {series.title} + ) : ( +
    + +
    + )} +
    +
    +

    + {series.title} +

    + +
    + + + {new Date(series.date).toLocaleDateString()} + + + + {series.posts.length || 0} posts + +
    + +

    + {series.description || 'No description available'} +

    +
      + + +
    +
    +
  • + ); +}; + +export default AdminSeriesListItem;