Skip to content

Commit 922e459

Browse files
Merge pull request #53 from ShipFriend0516/feature/series-management
시리즈 관리 기능 추가
2 parents fc31254 + 664de05 commit 922e459

9 files changed

Lines changed: 308 additions & 11 deletions

File tree

app/admin/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import GithubLogin from '@/app/entities/common/Button/GithubLogin';
1212
import BubbleBackground from '@/app/entities/common/Background/BubbleBackground';
1313
import { useEffect } from 'react';
1414
import useToast from '@/app/hooks/useToast';
15+
import { FaBuffer } from 'react-icons/fa6';
1516

1617
const AdminDashboard = () => {
1718
const { data: session } = useSession();
@@ -67,6 +68,13 @@ const AdminDashboard = () => {
6768
bgColor: 'bg-purple-950/20', // 짙은 보라색의 투명도 적용
6869
link: '/admin/analytics',
6970
},
71+
{
72+
title: '시리즈 관리',
73+
icon: <FaBuffer />,
74+
description: '블로그 시리즈를 관리합니다.',
75+
bgColor: 'bg-emerald-950/20', // 짙은 보라색의 투명도 적용
76+
link: '/admin/series',
77+
},
7078
{
7179
title: '댓글 확인 및 관리',
7280
icon: <BiCommentDetail />,

app/admin/series/page.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use client';
2+
import { Series } from '@/app/types/Series';
3+
import useDataFetch, {
4+
useDataFetchConfig,
5+
} from '@/app/hooks/common/useDataFetch';
6+
import { useState } from 'react';
7+
import AdminSeriesList from '@/app/entities/series/list/AdminSeriesList';
8+
import Overlay from '@/app/entities/common/Overlay/Overlay';
9+
import CreateSeriesOverlayContainer from '@/app/entities/series/CreateSeriesOverlayContainer';
10+
import { deleteSeries } from '@/app/entities/series/api/series';
11+
import DeleteModal from '@/app/entities/common/Modal/DeleteModal';
12+
const AdminSeriesPage = () => {
13+
const [seriesList, setSeriesList] = useState<Series[] | null>(null);
14+
const getSeriesListConfig: useDataFetchConfig = {
15+
url: '/api/series',
16+
method: 'GET',
17+
config: {
18+
params: {
19+
compact: 'true',
20+
},
21+
},
22+
onSuccess: (data: Series[]) => {
23+
setSeriesList(data);
24+
},
25+
};
26+
const { loading } = useDataFetch<Series[]>(getSeriesListConfig);
27+
const [createSeriesOpen, setCreateSeriesOpen] = useState(false);
28+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
29+
const [selectedSeries, setSelectedSeries] = useState<Series | null>(null);
30+
const handleUpdateSeries = (series: Series) => {
31+
setCreateSeriesOpen(true);
32+
setSelectedSeries(series);
33+
};
34+
const handleCloseOverlay = () => {
35+
setCreateSeriesOpen(false);
36+
setSelectedSeries(null);
37+
};
38+
39+
const handleDeleteSeries = async (slug: string) => {
40+
if (!seriesList) return;
41+
try {
42+
const data = await deleteSeries(slug);
43+
if (data.success) {
44+
console.log('시리즈 삭제 성공:', data);
45+
} else {
46+
console.error('시리즈 삭제 실패:', data);
47+
}
48+
} catch (error) {
49+
console.error('시리즈 삭제 중 오류 발생:', error);
50+
}
51+
52+
const updatedSeriesList = seriesList.filter(
53+
(series) => series.slug !== slug
54+
);
55+
setSeriesList(updatedSeriesList);
56+
setShowDeleteDialog(false);
57+
setSelectedSeries(null);
58+
};
59+
60+
const handleDeleteClick = (slug: string) => {
61+
setShowDeleteDialog(true);
62+
setSelectedSeries(
63+
seriesList?.find((series) => series.slug === slug) || null
64+
);
65+
};
66+
67+
return (
68+
<section className={'max-w-6xl mx-auto'}>
69+
<h1 className={'text-4xl font-bold mt-4'}>시리즈 관리</h1>
70+
<p className={'text-lg text-weak mb-4'}>
71+
시리즈를 관리하는 페이지입니다. 시리즈를 추가, 수정, 삭제할 수 있습니다.
72+
</p>
73+
<div>
74+
<button
75+
onClick={() => setCreateSeriesOpen(true)}
76+
className={' bg-emerald-500 text-white px-4 py-2 rounded-lg'}
77+
>
78+
시리즈 추가
79+
</button>
80+
</div>
81+
<div>
82+
<h2 className={'text-xl font-bold my-2'}>
83+
등록된 시리즈 목록 ({seriesList?.length || 0})
84+
</h2>
85+
<hr className={'my-4'} />
86+
<AdminSeriesList
87+
handleUpdateSeries={handleUpdateSeries}
88+
handleDeleteClick={handleDeleteClick}
89+
seriesList={seriesList}
90+
loading={loading}
91+
/>
92+
</div>
93+
<Overlay
94+
overlayOpen={createSeriesOpen}
95+
setOverlayOpen={setCreateSeriesOpen}
96+
>
97+
<CreateSeriesOverlayContainer
98+
setCreateSeriesOpen={setCreateSeriesOpen}
99+
handleCloseOverlay={handleCloseOverlay}
100+
series={selectedSeries || undefined}
101+
/>
102+
</Overlay>
103+
{showDeleteDialog && (
104+
<DeleteModal
105+
message={
106+
'이 시리즈를 삭제하시겠습니까? 이 작업은 영구적으로 영향을 미치는 작업입니다.'
107+
}
108+
onCancel={() => setShowDeleteDialog(false)}
109+
onConfirm={() => handleDeleteSeries(selectedSeries?.slug || '')}
110+
/>
111+
)}
112+
</section>
113+
);
114+
};
115+
116+
export default AdminSeriesPage;

app/api/series/[slug]/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
22
import dbConnect from '@/app/lib/dbConnect';
33
import Series from '@/app/models/Series';
44
import '@/app/models/Post';
5+
import { getServerSession } from 'next-auth';
56

67
export async function GET(
78
request: Request,
@@ -41,6 +42,12 @@ export async function PUT(
4142
{ params }: { params: { slug: string } }
4243
) {
4344
try {
45+
const session = await getServerSession();
46+
47+
if (!session) {
48+
return new Response('Unauthorized', { status: 401 });
49+
}
50+
4451
await dbConnect();
4552
const body = await request.json();
4653

@@ -77,6 +84,12 @@ export async function DELETE(
7784
{ params }: { params: { slug: string } }
7885
) {
7986
try {
87+
const session = await getServerSession();
88+
89+
if (!session) {
90+
return new Response('Unauthorized', { status: 401 });
91+
}
92+
8093
await dbConnect();
8194
const deletedSeries = await Series.findOneAndDelete({ slug: params.slug });
8295

app/api/series/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export async function GET(request: Request) {
5454
return NextResponse.json(series, {
5555
status: 200,
5656
headers: {
57-
'Cache-Control': 'public, max-age=60, s-maxage=60',
57+
'Cache-Control': 'public, max-age=30, s-maxage=30',
5858
},
5959
});
6060
} catch (error: any) {

app/entities/common/Modal/DeleteModal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
const DeleteModal = (props: {
22
onCancel: () => void;
33
onConfirm: () => void;
4+
message?: string;
45
}) => {
6+
const defaultMessage =
7+
'게시글을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다.';
58
return (
69
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
710
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4 text-black">
811
<h2 className="text-xl font-semibold">게시글을 삭제하시겠습니까?</h2>
912
<p className="mt-2 text-gray-600">
10-
이 작업은 되돌릴 수 없습니다. 게시글이 영구적으로 삭제됩니다.
13+
{props.message ? props.message : defaultMessage}
1114
</p>
1215
<div className="mt-6 flex justify-end gap-3">
1316
<button

app/entities/series/CreateSeriesOverlayContainer.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import { ChangeEvent, useState } from 'react';
2-
import { createSeries } from '@/app/entities/series/api/series';
2+
import { createSeries, updateSeries } from '@/app/entities/series/api/series';
33
import useToast from '@/app/hooks/useToast';
4+
import { Series } from '@/app/types/Series';
45

56
interface CreateSeriesOverlayContainerProps {
67
setCreateSeriesOpen: (open: boolean) => void;
8+
series?: Series;
9+
handleCloseOverlay?: () => void;
710
}
811

912
const CreateSeriesOverlayContainer = ({
1013
setCreateSeriesOpen,
14+
series,
15+
handleCloseOverlay,
1116
}: CreateSeriesOverlayContainerProps) => {
12-
const [seriesTitle, setSeriesTitle] = useState<string>('');
13-
const [seriesDescription, setSeriesDescription] = useState<string>('');
14-
const [seriesThumbnail, setSeriesThumbnail] = useState<string>('');
17+
const isEditMode = !!series;
18+
const [seriesTitle, setSeriesTitle] = useState<string>(
19+
isEditMode ? series?.title || '' : ''
20+
);
21+
const [seriesDescription, setSeriesDescription] = useState<string>(
22+
isEditMode ? series?.description : ''
23+
);
24+
const [seriesThumbnail, setSeriesThumbnail] = useState<string>(
25+
isEditMode ? series?.thumbnailImage || '' : ''
26+
);
1527
const toast = useToast();
1628

1729
const postSeries = async () => {
@@ -34,10 +46,34 @@ const CreateSeriesOverlayContainer = ({
3446
}
3547
};
3648

49+
const editSeries = async () => {
50+
try {
51+
if (isEditMode) {
52+
const result = await updateSeries(series.slug, {
53+
title: seriesTitle,
54+
description: seriesDescription,
55+
thumbnailImage: seriesThumbnail,
56+
});
57+
if (result._id) {
58+
toast.success('시리즈가 성공적으로 수정되었습니다.');
59+
}
60+
}
61+
} catch (e) {
62+
toast.error('시리즈 수정 중 오류가 발생했습니다.');
63+
console.error('시리즈 수정 중 오류 발생', e);
64+
} finally {
65+
if (handleCloseOverlay) {
66+
handleCloseOverlay();
67+
} else {
68+
setCreateSeriesOpen(false);
69+
}
70+
}
71+
};
72+
3773
return (
3874
<div className="max-w-lg mx-auto p-6 bg-white rounded-lg shadow-md">
3975
<h2 className="text-2xl font-bold text-gray-800 mb-4 text-center">
40-
새로운 시리즈 만들기
76+
{isEditMode ? '시리즈 수정하기' : '새로운 시리즈 만들기'}
4177
</h2>
4278
<p className="text-gray-600 text-sm mb-6 text-center">
4379
새로운 시리즈를 생성합니다. 제목은 필수로 작성해야합니다.
@@ -91,10 +127,10 @@ const CreateSeriesOverlayContainer = ({
91127
취소
92128
</button>
93129
<button
94-
onClick={postSeries}
130+
onClick={isEditMode ? editSeries : postSeries}
95131
className="flex-1 py-2.5 px-4 bg-emerald-500 text-white rounded-md hover:bg-emerald-600 transition font-medium"
96132
>
97-
생성
133+
{isEditMode ? '수정' : '생성'}
98134
</button>
99135
</div>
100136
</div>

app/entities/series/api/series.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ export const updateSeries = async (
3131
title: string;
3232
description: string;
3333
thumbnailImage: string;
34-
order: string[];
35-
posts: string[];
3634
}
3735
) => {
3836
const response = await axios.put(`/api/series/${slug}`, data);
3937
return response.data;
4038
};
39+
40+
export const deleteSeries = async (slug: string) => {
41+
const response = await axios.delete(`/api/series/${slug}`);
42+
return response.data;
43+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Series } from '@/app/types/Series';
2+
import React from 'react';
3+
import AdminSeriesListItem from '@/app/entities/series/list/AdminSeriesListItem';
4+
5+
interface AdminSeriesListProps {
6+
seriesList: Series[] | null | undefined;
7+
loading: boolean;
8+
handleUpdateSeries: (series: Series) => void;
9+
handleDeleteClick: (slug: string) => void;
10+
}
11+
const AdminSeriesList = ({
12+
loading,
13+
seriesList,
14+
handleUpdateSeries,
15+
handleDeleteClick,
16+
}: AdminSeriesListProps) => {
17+
if (loading) {
18+
return <p className={'text-lg text-gray-500'}>로딩 중...</p>;
19+
}
20+
if (!seriesList || seriesList.length === 0) {
21+
return <p className={'text-lg text-gray-500'}>등록된 시리즈가 없습니다.</p>;
22+
}
23+
return (
24+
<ul>
25+
{loading && <p className={'text-lg text-gray-500'}>로딩 중...</p>}
26+
{seriesList.map((series, index) => (
27+
<AdminSeriesListItem
28+
key={index}
29+
series={series}
30+
handleUpdateSeries={handleUpdateSeries}
31+
handleDeleteClick={handleDeleteClick}
32+
/>
33+
))}
34+
</ul>
35+
);
36+
};
37+
38+
export default AdminSeriesList;

0 commit comments

Comments
 (0)