Skip to content

Commit 298126f

Browse files
Merge pull request #61 from ShipFriend0516/feature/edit-post
[Refactor] 어드민 > 글 수정 기능 리팩토링
2 parents 9bdbf22 + bcbfbb8 commit 298126f

File tree

11 files changed

+298
-108
lines changed

11 files changed

+298
-108
lines changed

app/admin/write/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Suspense } from 'react';
22
import LoadingIndicator from '@/app/entities/common/Loading/LoadingIndicator';
3+
import BlogFormSkeleton from '@/app/entities/common/Skeleton/BlogFormSkeleton';
34

45
interface LayoutProps {
56
children: React.ReactNode;
67
}
78
const Layout = ({ children }: LayoutProps) => {
8-
return <Suspense fallback={<LoadingIndicator />}>{children}</Suspense>;
9+
return <Suspense fallback={<BlogFormSkeleton />}>{children}</Suspense>;
910
};
1011

1112
export default Layout;

app/admin/write/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export const generateMetadata = async (): Promise<Metadata> => {
1010
const BlogWritePage = () => {
1111
return (
1212
<section className={'p-6 max-w-7xl mx-auto'}>
13-
<h1 className={'text-2xl text-center mb-4'}>글 작성</h1>
1413
<BlogForm />
1514
</section>
1615
);

app/api/posts/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export async function POST(req: Request) {
118118
author,
119119
content,
120120
profileImage,
121-
thumbnailImage,
122121
seriesId,
123122
tags,
124123
isPrivate,
@@ -131,6 +130,15 @@ export async function POST(req: Request) {
131130
);
132131
}
133132

133+
let thumbnailOfPost = getThumbnailInMarkdown(content);
134+
if (!thumbnailOfPost) {
135+
const defaultSeriesThumbnail =
136+
await Series.findById(seriesId).select('thumbnailImage');
137+
thumbnailOfPost =
138+
defaultSeriesThumbnail?.thumbnailImage ||
139+
'/images/placeholder/thumbnail_example2.webp';
140+
}
141+
134142
const post = {
135143
slug: await generateUniqueSlug(title, Post),
136144
title,
@@ -139,7 +147,7 @@ export async function POST(req: Request) {
139147
content,
140148
timeToRead: Math.ceil(content.length / 500),
141149
profileImage,
142-
thumbnailImage: thumbnailImage || getThumbnailInMarkdown(content),
150+
thumbnailImage: thumbnailOfPost,
143151
seriesId: seriesId || null,
144152
tags: tags || [],
145153
isPrivate: isPrivate || false,

app/entities/common/Loading/LoadingSpinner.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { AiOutlineLoading3Quarters } from 'react-icons/ai';
22

33
interface LoadingSpinnerProps {
44
content?: string;
5+
size?: number;
56
}
67

7-
const LoadingSpinner = ({ content }: LoadingSpinnerProps) => {
8+
const LoadingSpinner = ({ content, size }: LoadingSpinnerProps) => {
89
return (
910
<div className=" text-primary" role="status">
10-
<AiOutlineLoading3Quarters className={'animate-spin '} />
11+
<AiOutlineLoading3Quarters
12+
className={'animate-spin'}
13+
style={{ fontSize: size }}
14+
/>
1115
<span className="hidden">{content ? content : 'Loading...'}</span>
1216
</div>
1317
);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Skeleton from '@/app/entities/common/Skeleton/Skeleton';
2+
3+
const BlogFormSkeleton = () => {
4+
return (
5+
<div className={'px-16'}>
6+
<h1 className={'text-2xl text-center mb-4'}>
7+
<Skeleton className="h-8 w-32 mx-auto" />
8+
</h1>
9+
<div className="mb-6">
10+
<div className="flex mb-4 gap-1 items-center">
11+
<span className="font-bold text-default flex-shrink-0">
12+
<Skeleton className="h-8 w-20" />
13+
</span>
14+
<Skeleton className="h-8 flex-grow" />
15+
</div>
16+
<div className="flex mb-4 gap-1 items-center">
17+
<span className="font-bold text-default flex-shrink-0">
18+
<Skeleton className="h-8 w-20" />
19+
</span>
20+
<Skeleton className="h-8 flex-grow" />
21+
</div>
22+
<div className="flex justify-start items-center">
23+
<div className="flex flex-wrap mb-4 gap-1 items-center">
24+
<span className="w-12 font-bold mr-3 flex-shrink text-nowrap flex-nowrap">
25+
<Skeleton className="h-8 w-16" />
26+
</span>
27+
<Skeleton className="h-8 w-32 rounded-full" />
28+
<Skeleton className="h-8 w-24 rounded-full" />
29+
<Skeleton className="h-8 w-28 rounded-full" />
30+
</div>
31+
</div>
32+
<div className="flex items-center w-full gap-2 mb-4">
33+
<div className="w-1/2 flex justify-start items-center gap-6">
34+
<div className="inline-flex items-center text-nowrap gap-2">
35+
<Skeleton className="h-8 w-16" />
36+
<Skeleton className="h-10 w-40" />
37+
</div>
38+
<div className="flex items-center gap-2">
39+
<Skeleton className="h-5 w-20" />
40+
<Skeleton className="h-4 w-4" />
41+
</div>
42+
</div>
43+
<Skeleton className="h-8 w-32 rounded-full" />
44+
<Skeleton className="h-8 w-28 rounded-full" />
45+
<Skeleton className="h-8 w-32 rounded-full" />
46+
</div>
47+
</div>
48+
<div className="mb-4">
49+
<Skeleton className="h-[500px] w-full rounded-lg" />
50+
</div>
51+
<div className="mb-4">
52+
<Skeleton className="h-32 w-full rounded-lg" />
53+
</div>
54+
<div className="flex justify-end gap-2 mt-6">
55+
<Skeleton className="h-10 w-24 rounded-md" />
56+
<Skeleton className="h-10 w-24 rounded-md" />
57+
</div>
58+
</div>
59+
);
60+
};
61+
62+
export default BlogFormSkeleton;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect, useState } from 'react';
2+
3+
interface TypingTextProps {
4+
title: string;
5+
delay?: number;
6+
className?: string;
7+
onComplete?: () => void;
8+
}
9+
10+
const TypingText = ({
11+
title,
12+
delay = 50,
13+
onComplete,
14+
className,
15+
}: TypingTextProps) => {
16+
const [displayTitle, setDisplayTitle] = useState('');
17+
const [isTypingComplete, setIsTypingComplete] = useState(false);
18+
19+
useEffect(() => {
20+
if (!title) return;
21+
22+
let currentIndex = 0;
23+
const interval = setInterval(() => {
24+
if (currentIndex <= title.length) {
25+
setDisplayTitle(title.slice(0, currentIndex));
26+
currentIndex++;
27+
} else {
28+
clearInterval(interval);
29+
setIsTypingComplete(true);
30+
}
31+
}, delay);
32+
33+
return () => clearInterval(interval);
34+
}, [title]);
35+
36+
useEffect(() => {
37+
if (isTypingComplete && onComplete) {
38+
onComplete();
39+
}
40+
}, [isTypingComplete, onComplete]);
41+
42+
return (
43+
<p className="typing-text">
44+
{displayTitle}
45+
{!isTypingComplete && (
46+
<span className="inline-block w-1 h-8 ml-1 bg-black animate-blink" />
47+
)}
48+
</p>
49+
);
50+
};
51+
52+
export default TypingText;

app/entities/post/detail/PostDetail.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ import PostBody from '@/app/entities/post/detail/PostBody';
44

55
interface PostArticleProps {
66
post: PostType;
7+
isAdmin?: boolean;
78
}
89

9-
const PostDetail = ({ post }: PostArticleProps) => {
10+
const PostDetail = ({ post, isAdmin = false }: PostArticleProps) => {
1011
const defaultThumbnail = '/images/placeholder/thumbnail_example2.webp';
1112
return (
1213
<article className="post">
1314
<PostHeader
1415
title={post?.title || ''}
1516
subTitle={post?.subTitle || ''}
17+
slug={post?.slug || ''}
1618
author={post?.author || ''}
1719
date={post?.date || 0}
1820
timeToRead={post?.timeToRead || 0}
1921
backgroundThumbnail={post?.thumbnailImage || defaultThumbnail}
22+
isAdmin={isAdmin}
2023
/>
2124
<PostBody
2225
loading={false}

app/entities/post/detail/PostHeader.tsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,36 @@ import Image from 'next/image';
55
import { StaticImport } from 'next/dist/shared/lib/get-img-props';
66
import Profile from '@/app/entities/common/Profile';
77
import { FaBook } from 'react-icons/fa';
8+
import { useSession } from 'next-auth/react';
9+
import TypingText from '../../common/Typography/TypingText';
810

911
interface Props {
1012
title: string;
1113
subTitle: string;
14+
slug: string;
1215
author: string;
1316
date: number;
1417
timeToRead: number;
1518
backgroundThumbnail?: StaticImport | string;
19+
isAdmin?: boolean;
1620
}
1721

1822
const PostHeader = ({
1923
title,
2024
subTitle,
25+
slug,
2126
author,
2227
date,
2328
timeToRead,
2429
backgroundThumbnail,
30+
isAdmin = false,
2531
}: Props) => {
26-
const [displayTitle, setDisplayTitle] = useState('');
2732
const [isTypingComplete, setIsTypingComplete] = useState(false);
2833

29-
useEffect(() => {
30-
if (!title) return;
31-
32-
let currentIndex = 0;
33-
const interval = setInterval(() => {
34-
if (currentIndex <= title.length) {
35-
setDisplayTitle(title.slice(0, currentIndex));
36-
currentIndex++;
37-
} else {
38-
clearInterval(interval);
39-
setIsTypingComplete(true);
40-
}
41-
}, 50);
42-
43-
return () => clearInterval(interval);
44-
}, [title]);
34+
const handleEditClick = () => {
35+
const editUrl = `/admin/write?slug=${slug}`;
36+
window.open(editUrl, '_blank')?.focus();
37+
};
4538

4639
return (
4740
<div
@@ -76,10 +69,11 @@ const PostHeader = ({
7669
'font-bold mb-4 pt-10 md:pt-20 text-3xl md:text-5xl z-10 px-2 break-keep'
7770
}
7871
>
79-
{displayTitle}
80-
{!isTypingComplete && (
81-
<span className="inline-block w-1 h-6 ml-1 bg-black animate-blink" />
82-
)}
72+
<TypingText
73+
title={title}
74+
delay={50}
75+
onComplete={() => setIsTypingComplete(true)}
76+
/>
8377
</h1>
8478
<h2
8579
className={`md:text-2xl font-bold mb-4 transition-opacity duration-500 ${
@@ -108,6 +102,11 @@ const PostHeader = ({
108102
<FaBook />
109103
{timeToRead} min read
110104
</span>
105+
{isAdmin && (
106+
<button onClick={handleEditClick}>
107+
<span className="underline">Edit</span>
108+
</button>
109+
)}
111110
</div>
112111
</div>
113112
</div>

0 commit comments

Comments
 (0)