Skip to content
8 changes: 8 additions & 0 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -67,6 +68,13 @@ const AdminDashboard = () => {
bgColor: 'bg-purple-950/20', // 짙은 보라색의 투명도 적용
link: '/admin/analytics',
},
{
title: '시리즈 관리',
icon: <FaBuffer />,
description: '블로그 시리즈를 관리합니다.',
bgColor: 'bg-emerald-950/20', // 짙은 보라색의 투명도 적용
link: '/admin/series',
},
{
title: '댓글 확인 및 관리',
icon: <BiCommentDetail />,
Expand Down
116 changes: 116 additions & 0 deletions app/admin/series/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Series[] | null>(null);
const getSeriesListConfig: useDataFetchConfig = {
url: '/api/series',
method: 'GET',
config: {
params: {
compact: 'true',
},
},
onSuccess: (data: Series[]) => {
setSeriesList(data);
},
};
const { loading } = useDataFetch<Series[]>(getSeriesListConfig);
const [createSeriesOpen, setCreateSeriesOpen] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [selectedSeries, setSelectedSeries] = useState<Series | null>(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 (
<section className={'max-w-6xl mx-auto'}>
<h1 className={'text-4xl font-bold mt-4'}>시리즈 관리</h1>
<p className={'text-lg text-weak mb-4'}>
시리즈를 관리하는 페이지입니다. 시리즈를 추가, 수정, 삭제할 수 있습니다.
</p>
<div>
<button
onClick={() => setCreateSeriesOpen(true)}
className={' bg-emerald-500 text-white px-4 py-2 rounded-lg'}
>
시리즈 추가
</button>
</div>
<div>
<h2 className={'text-xl font-bold my-2'}>
등록된 시리즈 목록 ({seriesList?.length || 0})
</h2>
<hr className={'my-4'} />
<AdminSeriesList
handleUpdateSeries={handleUpdateSeries}
handleDeleteClick={handleDeleteClick}
seriesList={seriesList}
loading={loading}
/>
</div>
<Overlay
overlayOpen={createSeriesOpen}
setOverlayOpen={setCreateSeriesOpen}
>
<CreateSeriesOverlayContainer
setCreateSeriesOpen={setCreateSeriesOpen}
handleCloseOverlay={handleCloseOverlay}
series={selectedSeries || undefined}
/>
</Overlay>
{showDeleteDialog && (
<DeleteModal
message={
'이 시리즈를 삭제하시겠습니까? 이 작업은 영구적으로 영향을 미치는 작업입니다.'
}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={() => handleDeleteSeries(selectedSeries?.slug || '')}
/>
)}
</section>
);
};

export default AdminSeriesPage;
13 changes: 13 additions & 0 deletions app/api/series/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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 });

Expand Down
2 changes: 1 addition & 1 deletion app/api/series/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion app/entities/common/Modal/DeleteModal.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
const DeleteModal = (props: {
onCancel: () => void;
onConfirm: () => void;
message?: string;
}) => {
const defaultMessage =
'게시글을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다.';
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 text-black">
<h2 className="text-xl font-semibold">게시글을 삭제하시겠습니까?</h2>
<p className="mt-2 text-gray-600">
이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다.
{props.message ? props.message : defaultMessage}
</p>
<div className="mt-6 flex justify-end gap-3">
<button
Expand Down
50 changes: 43 additions & 7 deletions app/entities/series/CreateSeriesOverlayContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { ChangeEvent, useState } from 'react';
import { createSeries } from '@/app/entities/series/api/series';
import { createSeries, updateSeries } from '@/app/entities/series/api/series';
import useToast from '@/app/hooks/useToast';
import { Series } from '@/app/types/Series';

interface CreateSeriesOverlayContainerProps {
setCreateSeriesOpen: (open: boolean) => void;
series?: Series;
handleCloseOverlay?: () => void;
}

const CreateSeriesOverlayContainer = ({
setCreateSeriesOpen,
series,
handleCloseOverlay,
}: CreateSeriesOverlayContainerProps) => {
const [seriesTitle, setSeriesTitle] = useState<string>('');
const [seriesDescription, setSeriesDescription] = useState<string>('');
const [seriesThumbnail, setSeriesThumbnail] = useState<string>('');
const isEditMode = !!series;
const [seriesTitle, setSeriesTitle] = useState<string>(
isEditMode ? series?.title || '' : ''
);
const [seriesDescription, setSeriesDescription] = useState<string>(
isEditMode ? series?.description : ''
);
const [seriesThumbnail, setSeriesThumbnail] = useState<string>(
isEditMode ? series?.thumbnailImage || '' : ''
);
const toast = useToast();

const postSeries = async () => {
Expand All @@ -34,10 +46,34 @@ const CreateSeriesOverlayContainer = ({
}
};

const editSeries = async () => {
try {
if (isEditMode) {
const result = await updateSeries(series.slug, {
title: seriesTitle,
description: seriesDescription,
thumbnailImage: seriesThumbnail,
});
if (result._id) {
toast.success('시리즈가 성공적으로 수정되었습니다.');
}
}
} catch (e) {
toast.error('시리즈 수정 중 오류가 발생했습니다.');
console.error('시리즈 수정 중 오류 발생', e);
} finally {
if (handleCloseOverlay) {
handleCloseOverlay();
} else {
setCreateSeriesOpen(false);
}
}
};

return (
<div className="max-w-lg mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold text-gray-800 mb-4 text-center">
새로운 시리즈 만들기
{isEditMode ? '시리즈 수정하기' : '새로운 시리즈 만들기'}
</h2>
<p className="text-gray-600 text-sm mb-6 text-center">
새로운 시리즈를 생성합니다. 제목은 필수로 작성해야합니다.
Expand Down Expand Up @@ -91,10 +127,10 @@ const CreateSeriesOverlayContainer = ({
취소
</button>
<button
onClick={postSeries}
onClick={isEditMode ? editSeries : postSeries}
className="flex-1 py-2.5 px-4 bg-emerald-500 text-white rounded-md hover:bg-emerald-600 transition font-medium"
>
생성
{isEditMode ? '수정' : '생성'}
</button>
</div>
</div>
Expand Down
7 changes: 5 additions & 2 deletions app/entities/series/api/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
38 changes: 38 additions & 0 deletions app/entities/series/list/AdminSeriesList.tsx
Original file line number Diff line number Diff line change
@@ -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 <p className={'text-lg text-gray-500'}>로딩 중...</p>;
}
if (!seriesList || seriesList.length === 0) {
return <p className={'text-lg text-gray-500'}>등록된 시리즈가 없습니다.</p>;
}
return (
<ul>
{loading && <p className={'text-lg text-gray-500'}>로딩 중...</p>}
{seriesList.map((series, index) => (
<AdminSeriesListItem
key={index}
series={series}
handleUpdateSeries={handleUpdateSeries}
handleDeleteClick={handleDeleteClick}
/>
))}
</ul>
);
};

export default AdminSeriesList;
Loading