Skip to content

[Feature] 시리즈 기능 추가 #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dd3d231
feat: 시리즈 스키마 구현
ShipFriend0516 Dec 24, 2024
a0d6fce
feat: Post 스키마에 태그 및 시리즈 아이디 추가
ShipFriend0516 Dec 24, 2024
bd0a1ea
feat: Select 컴포넌트 구현
ShipFriend0516 Dec 24, 2024
e49c860
feat: 시리즈 생성 및 선택 ui 구현 및 버튼들 컴포넌트로 분리
ShipFriend0516 Dec 24, 2024
8439df5
refactor: validate함수 분리
ShipFriend0516 Dec 24, 2024
a86f25f
feat: 시리즈 타입 추가
ShipFriend0516 Dec 24, 2024
512e902
chore: any 타입 경고 수준으로 내리기
ShipFriend0516 Dec 24, 2024
73f8544
feat: series 관련 CRUD API 구현
ShipFriend0516 Dec 24, 2024
71068e1
fix: post 제목 없을 경우 에러처리
ShipFriend0516 Dec 25, 2024
028d27b
fix: post api 제목만 필수로
ShipFriend0516 Dec 25, 2024
1e6b498
feat: 시리즈 생성 오버레이 구현
ShipFriend0516 Dec 25, 2024
ddb72d3
feat: 시리즈 리스트 불러와서 목록으로 보여주기 구현
ShipFriend0516 Dec 25, 2024
5f7f801
feat: 글 생성시 시리즈 선택 가능하게 구현
ShipFriend0516 Dec 25, 2024
f3938ef
fix: 글 생성, 수정, 삭제시 series의 posts 업데이트
ShipFriend0516 Dec 25, 2024
8c713f9
feat: Post type에 seriesId 추가
ShipFriend0516 Dec 25, 2024
7d860ee
feat: 시리즈 기본 값을 첫번째 값으로 사용하도록, 로딩 상태 사용
ShipFriend0516 Dec 25, 2024
853ec9f
feat: 글 리스트에서 시리즈 목록 볼 수 있게
ShipFriend0516 Dec 25, 2024
91cc141
style: 반응형 수정
ShipFriend0516 Dec 26, 2024
94a053e
fix: 글 삭제가 안되던 문제 해결
ShipFriend0516 Dec 26, 2024
d508517
fix: lottie 애니메이션에서 svg 애니메이션으로 변경
ShipFriend0516 Dec 26, 2024
c165f47
feat: 시리즈 목록을 볼 수 있는 페이지 구현
ShipFriend0516 Dec 26, 2024
27a5121
style: 글 컴포넌트 디자인 수정
ShipFriend0516 Dec 26, 2024
0586824
style: 프로필과 타임스탬프 높이가 맞지 않던 문제 해결
ShipFriend0516 Dec 26, 2024
0d8da2a
refactor: 시리즈 프리뷰 컴포넌트로 분리
ShipFriend0516 Dec 26, 2024
0113bd2
feat: 시리즈로 검색 가능하도록 구현
ShipFriend0516 Dec 27, 2024
9438609
feat: 시리즈 페이지에서 이동 가능하게 구현
ShipFriend0516 Dec 27, 2024
55cbbb9
feat: navbar에 series 추가
ShipFriend0516 Dec 27, 2024
e6322ec
feat: 로딩 컴포넌트 분리 후 시리즈 페이지에서 재사용
ShipFriend0516 Dec 27, 2024
d0f72b6
chore: 안쓰는 import 제거
ShipFriend0516 Dec 27, 2024
5df4e03
feat: 시리즈 드롭다운에서 시리즈 검색 가능하게 구현
ShipFriend0516 Dec 27, 2024
deaf308
feat: 검색 결과 없을시 초기화하는 버튼 추가
ShipFriend0516 Dec 27, 2024
e78ab2b
style: 시리즈 프리뷰 커서 포인터
ShipFriend0516 Dec 27, 2024
a0dab56
feat: posts 페이지 suspense로 감싸기
ShipFriend0516 Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn"
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "warn"
}
}
52 changes: 39 additions & 13 deletions app/api/posts/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// app/api/posts/[slug]/route.ts
import dbConnect from '@/app/lib/dbConnect';
import Post from '@/app/models/Post';
import { NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { getThumbnailInMarkdown } from '@/app/lib/utils/parse';
import Series from '@/app/models/Series';

export async function GET(
req: NextRequest,
Expand Down Expand Up @@ -36,6 +37,7 @@ export async function PUT(
try {
await dbConnect();
const body = await req.json();
const post = await Post.findOne({ slug: params.slug });

const updatedPost = await Post.findOneAndUpdate(
{ slug: params.slug },
Expand All @@ -53,6 +55,23 @@ export async function PUT(
);
}

// 시리즈 변경 처리
if (post.seriesId?.toString() !== body.seriesId) {
if (post.seriesId) {
await Series.findByIdAndUpdate(post.seriesId, {
$pull: { posts: post._id },
$inc: { postCount: -1 },
});
}

if (body.seriesId) {
await Series.findByIdAndUpdate(body.seriesId, {
$push: { posts: post._id },
$inc: { postCount: 1 },
});
}
}

return Response.json({ success: true, post: updatedPost });
} catch (error) {
console.error(error);
Expand All @@ -64,28 +83,35 @@ export async function PUT(
}

export async function DELETE(
req: NextRequest,
request: Request,
{ params }: { params: { slug: string } }
) {
try {
await dbConnect();
const deletedPost = await Post.findByIdAndDelete(params.slug).lean();
const post = await Post.findById(params.slug);

if (!deletedPost) {
return Response.json(
{ success: false, error: '삭제할 글을 찾을 수 없습니다.' },
if (!post) {
return NextResponse.json(
{ error: '삭제할 포스트를 찾을 수 없습니다.' },
{ status: 404 }
);
}

return Response.json({
success: true,
message: '글이 삭제되었습니다.',
if (post.seriesId) {
await Series.findByIdAndUpdate(post.seriesId, {
$pull: { posts: post._id },
$inc: { postCount: -1 },
});
}

await Post.deleteOne({ _id: params.slug });

return NextResponse.json({
message: '포스트가 성공적으로 삭제되었습니다.',
});
} catch (error) {
console.error(error);
return Response.json(
{ success: false, error: 'Server Error' },
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '포스트 삭제에 실패했습니다.' },
{ status: 500 }
);
}
Expand Down
34 changes: 32 additions & 2 deletions app/api/posts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import dbConnect from '@/app/lib/dbConnect';
import { getServerSession } from 'next-auth/next';
import { getThumbnailInMarkdown } from '@/app/lib/utils/parse';
import { generateUniqueSlug } from '@/app/lib/utils/post';
import Series from '@/app/models/Series';
import { QuerySelector } from 'mongoose';

// GET /api/posts - 모든 글 조회
export async function GET(req: Request) {
Expand All @@ -11,6 +13,11 @@ export async function GET(req: Request) {
const { searchParams } = new URL(req.url);

const query = searchParams.get('query') || '';
const seriesSlug = searchParams.get('series') || '';

const seriesId = seriesSlug
? await Series.findOne({ slug: seriesSlug }, '_id')
: null;

// 검색 조건 구성
const searchConditions = {
Expand All @@ -19,9 +26,17 @@ export async function GET(req: Request) {
{ content: { $regex: query, $options: 'i' } },
{ subTitle: { $regex: query, $options: 'i' } },
],
$and: [],
};

if (seriesId) {
(searchConditions.$and as QuerySelector<string>[]).push({
seriesId: seriesId,
} as QuerySelector<string>);
}

const posts = await Post.find(searchConditions)

.sort({ date: -1 })
.limit(10);

Expand All @@ -45,8 +60,15 @@ export async function POST(req: Request) {
}

await dbConnect();
const { title, subTitle, author, content, profileImage, thumbnailImage } =
await req.json();
const {
title,
subTitle,
author,
content,
profileImage,
thumbnailImage,
seriesId,
} = await req.json();

if (!title || !content || !author || !content) {
return Response.json(
Expand All @@ -64,6 +86,7 @@ export async function POST(req: Request) {
timeToRead: Math.ceil(content.length / 500),
profileImage,
thumbnailImage: thumbnailImage || getThumbnailInMarkdown(content),
seriesId: seriesId || null,
};

const newPost = await Post.create(post);
Expand All @@ -74,6 +97,13 @@ export async function POST(req: Request) {
);
}

if (post.seriesId) {
await Series.findByIdAndUpdate(post.seriesId, {
$push: { posts: newPost._id },
$inc: { postCount: 1 }, // postCount 1 증가
});
}

return Response.json(
{
success: true,
Expand Down
91 changes: 91 additions & 0 deletions app/api/series/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextResponse } from 'next/server';
import dbConnect from '@/app/lib/dbConnect';
import Series from '@/app/models/Series';

export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
try {
await dbConnect();
const series = await Series.findOne({ slug: params.slug }).populate(
'posts'
);

if (!series) {
return NextResponse.json(
{ error: '해당 시리즈를 찾을 수 없습니다.' },
{ status: 404 }
);
}

return NextResponse.json(series);
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '시리즈 조회에 실패했습니다.' },
{ status: 500 }
);
}
}

export async function PUT(
request: Request,
{ params }: { params: { slug: string } }
) {
try {
await dbConnect();
const body = await request.json();

const updatedSeries = await Series.findOneAndUpdate(
{ slug: params.slug },
{
title: body.title,
description: body.description,
thumbnailImage: body.thumbnailImage,
order: body.order,
posts: body.posts,
},
{ new: true }
).populate('posts');

if (!updatedSeries) {
return NextResponse.json(
{ error: '수정할 시리즈를 찾을 수 없습니다.' },
{ status: 404 }
);
}

return NextResponse.json(updatedSeries);
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '시리즈 수정에 실패했습니다.' },
{ status: 500 }
);
}
}

export async function DELETE(
request: Request,
{ params }: { params: { slug: string } }
) {
try {
await dbConnect();
const deletedSeries = await Series.findOneAndDelete({ slug: params.slug });

if (!deletedSeries) {
return NextResponse.json(
{ error: '삭제할 시리즈를 찾을 수 없습니다.' },
{ status: 404 }
);
}

return NextResponse.json({
message: '시리즈가 성공적으로 삭제되었습니다.',
});
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '시리즈 삭제에 실패했습니다.' },
{ status: 500 }
);
}
}
54 changes: 54 additions & 0 deletions app/api/series/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import dbConnect from '@/app/lib/dbConnect';
import Series from '@/app/models/Series';
import { createPostSlug } from '@/app/lib/utils/post';

export async function POST(request: Request) {
try {
await dbConnect();
const body = await request.json();
if (!body.title) {
return NextResponse.json(
{ error: '시리즈 제목을 입력해주세요.' },
{ status: 400 }
);
}

const series = await Series.create({
slug: createPostSlug(body.title),
title: body.title,
description: body.description,
thumbnailImage: body.thumbnailImage || '',
order: body.order || [],
posts: body.posts || [],
});

return NextResponse.json(series, { status: 201 });
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '시리즈 생성에 실패했습니다.' },
{ status: 500 }
);
}
}

export async function GET(request: Request) {
try {
await dbConnect();
const { searchParams } = new URL(request.url);
const populate = searchParams.get('populate') === 'true';

const query = Series.find({});
if (populate) {
query.populate('posts');
}

const series = await query;
return NextResponse.json(series);
} catch (error: any) {
return NextResponse.json(
{ error: error.message || '시리즈 목록을 불러오는데 실패했습니다.' },
{ status: 500 }
);
}
}
19 changes: 19 additions & 0 deletions app/entities/common/Loading/SVGLoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ImSpinner2 } from 'react-icons/im';

interface SVGLoadingSpinnerProps {
message?: string;
}
const SVGLoadingSpinner = ({ message }: SVGLoadingSpinnerProps) => {
return (
<div
className={
'flex justify-center items-center gap-2 mx-auto col-span-3 w-1/3 h-full pt-20'
}
>
<ImSpinner2 className={'text-3xl animate-spin'} />
{message && <span> {message}</span>}
</div>
);
};

export default SVGLoadingSpinner;
9 changes: 6 additions & 3 deletions app/entities/common/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ const NavBar = () => {
<Profile profileThumbnail={profile} username={'Jeongwoo Seo'} />
</Link>
</div>
<ul className={'inline-flex max-w-5xl w-full justify-end gap-3 '}>
<li className={'px-4 py-2'}>
<ul className={'inline-flex max-w-5xl w-full justify-end gap-6 '}>
<li>
<Link href="/posts">Blog</Link>
</li>
<li className={'px-4 py-2'}>
<li>
<Link href="/series">Series</Link>
</li>
<li>
<Link href="/portfolio">Portfolio</Link>
</li>
</ul>
Expand Down
Loading
Loading