From 84922d655b57adb8a65acdce45c1c8f5830e9601 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 16:00:48 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20=EA=B8=80=20=EB=B3=B8=EB=AC=B8?= =?UTF-8?q?=20=ED=97=A4=EB=8D=94=EC=97=90=20=EC=88=98=EC=A0=95=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/detail/PostDetail.tsx | 5 ++++- app/entities/post/detail/PostHeader.tsx | 15 +++++++++++++++ app/posts/[slug]/page.tsx | 4 +++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/entities/post/detail/PostDetail.tsx b/app/entities/post/detail/PostDetail.tsx index 903692d..d585aee 100644 --- a/app/entities/post/detail/PostDetail.tsx +++ b/app/entities/post/detail/PostDetail.tsx @@ -4,19 +4,22 @@ import PostBody from '@/app/entities/post/detail/PostBody'; interface PostArticleProps { post: PostType; + isAdmin?: boolean; } -const PostDetail = ({ post }: PostArticleProps) => { +const PostDetail = ({ post, isAdmin = false }: PostArticleProps) => { const defaultThumbnail = '/images/placeholder/thumbnail_example2.webp'; return (
{ const [displayTitle, setDisplayTitle] = useState(''); const [isTypingComplete, setIsTypingComplete] = useState(false); @@ -108,6 +113,16 @@ const PostHeader = ({ {timeToRead} min read + {isAdmin && ( + + )} diff --git a/app/posts/[slug]/page.tsx b/app/posts/[slug]/page.tsx index 570942a..c60356d 100644 --- a/app/posts/[slug]/page.tsx +++ b/app/posts/[slug]/page.tsx @@ -6,6 +6,7 @@ import PostJSONLd from '@/app/entities/post/detail/PostJSONLd'; import PostActionSection from '@/app/entities/post/detail/PostActionSection'; import PostRecommendation from '@/app/entities/post/detail/PostRecommendation'; import PostDetail from '@/app/entities/post/detail/PostDetail'; +import { getServerSession } from 'next-auth'; const defaultThumbnail = '/images/placeholder/thumbnail_example2.webp'; @@ -64,12 +65,13 @@ export const generateMetadata = async ({ }; const BlogDetailPage = async ({ params }: { params: { slug: string } }) => { + const session = await getServerSession(); const { post } = await getPostDetail(params.slug); return ( <>
- + Date: Wed, 15 Oct 2025 16:03:36 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EA=B8=80=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=AA=A8=EB=93=9C=EC=9D=BC=EC=8B=9C=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EC=88=98=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/write/page.tsx | 1 - app/entities/post/detail/PostHeader.tsx | 2 +- app/entities/post/write/BlogForm.tsx | 3 +++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/admin/write/page.tsx b/app/admin/write/page.tsx index 89a608a..5a3baa1 100644 --- a/app/admin/write/page.tsx +++ b/app/admin/write/page.tsx @@ -10,7 +10,6 @@ export const generateMetadata = async (): Promise => { const BlogWritePage = () => { return (
-

글 작성

); diff --git a/app/entities/post/detail/PostHeader.tsx b/app/entities/post/detail/PostHeader.tsx index e79f2dd..edd8641 100644 --- a/app/entities/post/detail/PostHeader.tsx +++ b/app/entities/post/detail/PostHeader.tsx @@ -120,7 +120,7 @@ const PostHeader = ({ window.open(editUrl, '_blank'); }} > - Edit + Edit )} diff --git a/app/entities/post/write/BlogForm.tsx b/app/entities/post/write/BlogForm.tsx index 3e0c1ac..e8acda9 100644 --- a/app/entities/post/write/BlogForm.tsx +++ b/app/entities/post/write/BlogForm.tsx @@ -50,6 +50,9 @@ const BlogForm = () => { return (
+

+ 글 {slug ? '수정' : '작성'} +

setTitle(e.target.value)} title={title} From 0b0c4d76e2deaa40238f519d356acc3dc5836637 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 16:11:02 +0900 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20=ED=83=80=EC=9D=B4=ED=95=91?= =?UTF-8?q?=20=ED=9A=A8=EA=B3=BC=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/common/Typography/TypingText.tsx | 52 +++++++++++++++++++ app/entities/post/detail/PostHeader.tsx | 28 +++------- 2 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 app/entities/common/Typography/TypingText.tsx diff --git a/app/entities/common/Typography/TypingText.tsx b/app/entities/common/Typography/TypingText.tsx new file mode 100644 index 0000000..4643dfa --- /dev/null +++ b/app/entities/common/Typography/TypingText.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; + +interface TypingTextProps { + title: string; + delay?: number; + className?: string; + onComplete?: () => void; +} + +const TypingText = ({ + title, + delay = 50, + onComplete, + className, +}: TypingTextProps) => { + const [displayTitle, setDisplayTitle] = useState(''); + const [isTypingComplete, setIsTypingComplete] = useState(false); + + useEffect(() => { + if (!title) return; + + let currentIndex = 0; + const interval = setInterval(() => { + if (currentIndex <= title.length) { + setDisplayTitle(title.slice(0, currentIndex)); + currentIndex++; + } else { + clearInterval(interval); + setIsTypingComplete(true); + } + }, delay); + + return () => clearInterval(interval); + }, [title]); + + useEffect(() => { + if (isTypingComplete && onComplete) { + onComplete(); + } + }, [isTypingComplete, onComplete]); + + return ( +

+ {displayTitle} + {!isTypingComplete && ( + + )} +

+ ); +}; + +export default TypingText; diff --git a/app/entities/post/detail/PostHeader.tsx b/app/entities/post/detail/PostHeader.tsx index edd8641..16e9ede 100644 --- a/app/entities/post/detail/PostHeader.tsx +++ b/app/entities/post/detail/PostHeader.tsx @@ -6,6 +6,7 @@ import { StaticImport } from 'next/dist/shared/lib/get-img-props'; import Profile from '@/app/entities/common/Profile'; import { FaBook } from 'react-icons/fa'; import { useSession } from 'next-auth/react'; +import TypingText from '../../common/Typography/TypingText'; interface Props { title: string; @@ -28,26 +29,8 @@ const PostHeader = ({ backgroundThumbnail, isAdmin = false, }: Props) => { - const [displayTitle, setDisplayTitle] = useState(''); const [isTypingComplete, setIsTypingComplete] = useState(false); - useEffect(() => { - if (!title) return; - - let currentIndex = 0; - const interval = setInterval(() => { - if (currentIndex <= title.length) { - setDisplayTitle(title.slice(0, currentIndex)); - currentIndex++; - } else { - clearInterval(interval); - setIsTypingComplete(true); - } - }, 50); - - return () => clearInterval(interval); - }, [title]); - return (
- {displayTitle} - {!isTypingComplete && ( - - )} + setIsTypingComplete(true)} + />

Date: Wed, 15 Oct 2025 17:12:25 +0900 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/write/BlogForm.tsx | 62 ++++++------ app/hooks/post/usePost.ts | 141 ++++++++++++++++++--------- 2 files changed, 124 insertions(+), 79 deletions(-) diff --git a/app/entities/post/write/BlogForm.tsx b/app/entities/post/write/BlogForm.tsx index e8acda9..ca26769 100644 --- a/app/entities/post/write/BlogForm.tsx +++ b/app/entities/post/write/BlogForm.tsx @@ -17,23 +17,13 @@ const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }); const BlogForm = () => { const params = useSearchParams(); const slug = params.get('slug'); + const isEditMode = Boolean(slug); const { - title, - subTitle, - submitLoading, - seriesLoading, - seriesId, + formData, + setFormData, + uiState, seriesList, - content, - setTitle, - setSubTitle, - setContent, - setSeriesId, - setIsPrivate, - isPrivate, - tags, - setTags, uploadedImages, setUploadedImages, overwriteDraft, @@ -41,12 +31,11 @@ const BlogForm = () => { clearDraftInStore, submitHandler, postBody, - errors, handleLinkCopy, } = usePost(slug || ''); const [createSeriesOpen, setCreateSeriesOpen] = useState(false); - useBlockNavigate({ title, content: content || '' }); + useBlockNavigate({ title: formData.title, content: formData.content || '' }); return (
@@ -54,25 +43,31 @@ const BlogForm = () => { 글 {slug ? '수정' : '작성'}

setTitle(e.target.value)} - title={title} - onSubTitleChange={(e) => setSubTitle(e.target.value)} - subTitle={subTitle} - seriesLoading={seriesLoading} + onTitleChange={(e) => setFormData({ title: e.target.value })} + title={formData.title} + onSubTitleChange={(e) => setFormData({ subTitle: e.target.value })} + subTitle={formData.subTitle} + seriesLoading={uiState.seriesLoading} series={seriesList} callbackfn={(s) => ({ value: s._id, label: s.title, })} - defaultSeries={setSeriesId} - seriesId={seriesId} + defaultSeries={(value) => { + if (typeof value === 'function') { + setFormData({ seriesId: value(formData.seriesId) }); + } else if (value !== undefined) { + setFormData({ seriesId: value }); + } + }} + seriesId={formData.seriesId} onClickNewSeries={() => setCreateSeriesOpen(true)} onClickOverwrite={overwriteDraft} clearDraft={clearDraftInStore} - tags={tags} - setTags={setTags} - isPrivate={isPrivate} - onPrivateChange={(isPrivate) => setIsPrivate(isPrivate)} + tags={formData.tags} + setTags={(tags: string[]) => setFormData({ tags })} + isPrivate={formData.isPrivate} + onPrivateChange={(isPrivate: boolean) => setFormData({ isPrivate })} /> { setFormData({ content: value })} height={500} visibleDragbar={false} /> @@ -94,14 +89,19 @@ const BlogForm = () => { setUploadedImages={setUploadedImages} onClick={handleLinkCopy} /> - + + {isEditMode && uiState.seriesLoading && ( +
+

Loading series information...

+
+ )}
); }; diff --git a/app/hooks/post/usePost.ts b/app/hooks/post/usePost.ts index 3c5b1f4..b1cf8c8 100644 --- a/app/hooks/post/usePost.ts +++ b/app/hooks/post/usePost.ts @@ -10,19 +10,40 @@ import { useRouter } from 'next/navigation'; import useDraft from '@/app/hooks/post/useDraft'; import { validatePost } from '@/app/lib/utils/validate/validate'; +interface FormData { + title: string; + subTitle: string; + content: string | undefined; + seriesId: string; + tags: string[]; + isPrivate: boolean; +} + +interface UIState { + submitLoading: boolean; + seriesLoading: boolean; + errors: string[]; +} + const usePost = (slug = '') => { - const [submitLoading, setSubmitLoading] = useState(false); - const [title, setTitle] = useState(''); - const [subTitle, setSubTitle] = useState(''); - const [content, setContent] = useState(''); + const [formData, setFormData] = useState({ + title: '', + subTitle: '', + content: '', + seriesId: '', + tags: [], + isPrivate: false, + }); + + const [uiState, setUIState] = useState({ + submitLoading: false, + seriesLoading: true, + errors: [], + }); + const [profileImage, setProfileImage] = useState(); const [thumbnailImage, setThumbnailImage] = useState(); const [seriesList, setSeriesList] = useState([]); - const [seriesId, setSeriesId] = useState(); - const [seriesLoading, setSeriesLoading] = useState(true); - const [errors, setErrors] = useState([]); - const [tags, setTags] = useState([]); - const [isPrivate, setIsPrivate] = useState(false); const [uploadedImages, setUploadedImages] = useState([]); const NICKNAME = '개발자 서정우'; @@ -31,15 +52,15 @@ const usePost = (slug = '') => { const { draft, draftImages, updateDraft, clearDraft } = useDraft(); const postBody: PostBody = { - title, - subTitle, + title: formData.title, + subTitle: formData.subTitle, author: NICKNAME, - content: content || '', + content: formData.content || '', profileImage, thumbnailImage, - seriesId: seriesId || '', - tags: tags, - isPrivate: isPrivate, + seriesId: formData.seriesId || '', + tags: formData.tags, + isPrivate: formData.isPrivate, }; useEffect(() => { @@ -57,8 +78,8 @@ const usePost = (slug = '') => { try { const data = await getAllSeriesData(); setSeriesList(data); - setSeriesId(data[0]._id); - setSeriesLoading(false); + setFormData((prev) => ({ ...prev, seriesId: data[0]._id })); + setUIState((prev) => ({ ...prev, seriesLoading: false })); } catch (e) { console.error('시리즈 조회 중 오류 발생', e); } @@ -105,13 +126,15 @@ const usePost = (slug = '') => { if (draft !== null) { if (confirm('임시 저장된 글이 있습니다. 덮어쓰시겠습니까?')) { const { title, content, subTitle, seriesId, isPrivate, tags } = draft; - setTitle(title || ''); - setContent(content); - setSubTitle(subTitle || ''); - setSeriesId(seriesId); + setFormData({ + title: title || '', + subTitle: subTitle || '', + content: content, + seriesId: seriesId || '', + tags: tags || [], + isPrivate: isPrivate || false, + }); setUploadedImages(draftImages || []); - setIsPrivate(isPrivate || false); - setTags(tags || []); } } else { toast.error('임시 저장된 글이 없습니다.'); @@ -125,13 +148,13 @@ const usePost = (slug = '') => { const submitHandler = (post: PostBody) => { try { - setSubmitLoading(true); + setUIState((prev) => ({ ...prev, submitLoading: true })); const { isValid, errors } = validatePost(post); - setErrors(errors); + setUIState((prev) => ({ ...prev, errors })); if (!isValid) { toast.error('유효성 검사 실패'); console.error('유효성 검사 실패', errors); - setSubmitLoading(false); + setUIState((prev) => ({ ...prev, submitLoading: false })); return; } @@ -143,7 +166,7 @@ const usePost = (slug = '') => { clearDraft(); } catch (e) { console.error('글 발행 중 오류 발생', e); - setSubmitLoading(false); + setUIState((prev) => ({ ...prev, submitLoading: false })); } }; @@ -151,12 +174,14 @@ const usePost = (slug = '') => { try { const response = await axios.get(`/api/posts/${slug}`); const data = await response.data; - setTitle(data.post.title || ''); - setSubTitle(data.post.subTitle); - setContent(data.post.content); - setSeriesId(data.post.seriesId || ''); - setTags(data.post.tags || []); - setIsPrivate(data.post.isPrivate || false); + setFormData({ + title: data.post.title || '', + subTitle: data.post.subTitle, + content: data.post.content, + seriesId: data.post.seriesId || '', + tags: data.post.tags || [], + isPrivate: data.post.isPrivate || false, + }); } catch (e) { console.error('글 조회 중 오류 발생', e); } @@ -167,31 +192,51 @@ const usePost = (slug = '') => { toast.success('이미지 링크가 복사되었습니다.'); }; + // Helper functions to update form data + const updateFormData = (updates: Partial) => { + setFormData((prev) => ({ ...prev, ...updates })); + }; + return { - title, - subTitle, - content, - seriesId, - isPrivate, - tags, - submitLoading, + // Form data (individual values for backward compatibility) + title: formData.title, + subTitle: formData.subTitle, + content: formData.content, + seriesId: formData.seriesId, + isPrivate: formData.isPrivate, + tags: formData.tags, + + // Form data object + formData, + setFormData: updateFormData, + + // UI state + submitLoading: uiState.submitLoading, + seriesLoading: uiState.seriesLoading, + errors: uiState.errors, + + uiState, + setUIState, + // Other state postBody, - seriesLoading, seriesList, uploadedImages, setUploadedImages, - setTitle, - setSubTitle, - setContent, - setSeriesId, - setIsPrivate, - setTags, + + // Individual setters for backward compatibility + setTitle: (title: string) => updateFormData({ title }), + setSubTitle: (subTitle: string) => updateFormData({ subTitle }), + setContent: (content: string | undefined) => updateFormData({ content }), + setSeriesId: (seriesId: string) => updateFormData({ seriesId }), + setIsPrivate: (isPrivate: boolean) => updateFormData({ isPrivate }), + setTags: (tags: string[]) => updateFormData({ tags }), + + // Methods overwriteDraft, saveToDraft, clearDraftInStore, submitHandler, handleLinkCopy, - errors, }; }; From c038d73159d62c6ba4e3bd1a1b9353cd075de620 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:12:34 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/detail/PostHeader.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/entities/post/detail/PostHeader.tsx b/app/entities/post/detail/PostHeader.tsx index 16e9ede..2ed365d 100644 --- a/app/entities/post/detail/PostHeader.tsx +++ b/app/entities/post/detail/PostHeader.tsx @@ -31,6 +31,11 @@ const PostHeader = ({ }: Props) => { const [isTypingComplete, setIsTypingComplete] = useState(false); + const handleEditClick = () => { + const editUrl = `/admin/write?slug=${slug}`; + window.open(editUrl, '_blank')?.focus(); + }; + return (
{isAdmin && ( - )} From 1806daabc80d0f0f4062a879fcb3dedbae46ea57 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:27:04 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EA=B8=80=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EC=9D=BC=20=EB=95=8C,=20=EA=B8=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=A1=9C=EB=94=A9=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/common/Loading/LoadingSpinner.tsx | 8 ++++++-- app/entities/post/write/BlogForm.tsx | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/entities/common/Loading/LoadingSpinner.tsx b/app/entities/common/Loading/LoadingSpinner.tsx index 1e5564b..a81dfd5 100644 --- a/app/entities/common/Loading/LoadingSpinner.tsx +++ b/app/entities/common/Loading/LoadingSpinner.tsx @@ -2,12 +2,16 @@ import { AiOutlineLoading3Quarters } from 'react-icons/ai'; interface LoadingSpinnerProps { content?: string; + size?: number; } -const LoadingSpinner = ({ content }: LoadingSpinnerProps) => { +const LoadingSpinner = ({ content, size }: LoadingSpinnerProps) => { return (
- + {content ? content : 'Loading...'}
); diff --git a/app/entities/post/write/BlogForm.tsx b/app/entities/post/write/BlogForm.tsx index ca26769..96ed907 100644 --- a/app/entities/post/write/BlogForm.tsx +++ b/app/entities/post/write/BlogForm.tsx @@ -11,6 +11,7 @@ import CreateSeriesOverlayContainer from '@/app/entities/series/CreateSeriesOver import UploadImageContainer from '@/app/entities/post/write/UploadImageContainer'; import PostMetadataForm from '@/app/entities/post/write/PostMetadataForm'; import usePost from '@/app/hooks/post/usePost'; +import LoadingSpinner from '../../common/Loading/LoadingSpinner'; const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }); @@ -98,14 +99,25 @@ const BlogForm = () => { saveToDraft={saveToDraft} /> {isEditMode && uiState.seriesLoading && ( -
-

Loading series information...

-
+ +
+ +

수정할 글을 불러오고 있습니다.

+
+
)}
); }; +const LoadingBackdrop = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + const ErrorBox = ({ errors }: { errors: string[] | null }) => { if (!errors) return null; From d23c73df9abeef86fb02c6589b70e9a23aca2001 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:41:10 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20BlogForm=20fallback=20ui=20?= =?UTF-8?q?=EB=A1=9C=20LoadingIndicator=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=8C=80=EC=8B=A0=20=EC=A0=84=EC=9A=A9=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=88=EB=A0=88=ED=86=A4=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/write/layout.tsx | 3 +- .../common/Skeleton/BlogFormSkeleton.tsx | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 app/entities/common/Skeleton/BlogFormSkeleton.tsx diff --git a/app/admin/write/layout.tsx b/app/admin/write/layout.tsx index 9f1f380..08d2e6a 100644 --- a/app/admin/write/layout.tsx +++ b/app/admin/write/layout.tsx @@ -1,11 +1,12 @@ import { Suspense } from 'react'; import LoadingIndicator from '@/app/entities/common/Loading/LoadingIndicator'; +import BlogFormSkeleton from '@/app/entities/common/Skeleton/BlogFormSkeleton'; interface LayoutProps { children: React.ReactNode; } const Layout = ({ children }: LayoutProps) => { - return }>{children}; + return }>{children}; }; export default Layout; diff --git a/app/entities/common/Skeleton/BlogFormSkeleton.tsx b/app/entities/common/Skeleton/BlogFormSkeleton.tsx new file mode 100644 index 0000000..ab75716 --- /dev/null +++ b/app/entities/common/Skeleton/BlogFormSkeleton.tsx @@ -0,0 +1,62 @@ +import Skeleton from '@/app/entities/common/Skeleton/Skeleton'; + +const BlogFormSkeleton = () => { + return ( +
+

+ +

+
+
+ + + + +
+
+ + + + +
+
+
+ + + + + + +
+
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
+ +
+
+ +
+
+ + +
+
+ ); +}; + +export default BlogFormSkeleton; From 113efab3f2b109b8c5b54a90147be642854a4d67 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:42:47 +0900 Subject: [PATCH 08/10] =?UTF-8?q?style:=20=ED=83=9C=EA=B7=B8=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B8=B0=20=EB=94=94=ED=85=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/write/PostMetadataForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/entities/post/write/PostMetadataForm.tsx b/app/entities/post/write/PostMetadataForm.tsx index c342b44..b498ef2 100644 --- a/app/entities/post/write/PostMetadataForm.tsx +++ b/app/entities/post/write/PostMetadataForm.tsx @@ -84,7 +84,7 @@ const PostMetadataForm = (props: PostMetadataFormProps) => {
- + 태그 입력 {(props.tags || []).map((tag, index) => ( From 3b0e3eba8c4efa7430f24005fbb2492d3c904bd5 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:50:20 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20margin=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/write/PostMetadataForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/entities/post/write/PostMetadataForm.tsx b/app/entities/post/write/PostMetadataForm.tsx index b498ef2..c342b44 100644 --- a/app/entities/post/write/PostMetadataForm.tsx +++ b/app/entities/post/write/PostMetadataForm.tsx @@ -84,7 +84,7 @@ const PostMetadataForm = (props: PostMetadataFormProps) => {
- + 태그 입력 {(props.tags || []).map((tag, index) => ( From bcbfbb8bdf02864915481e55687400d3cf71f695 Mon Sep 17 00:00:00 2001 From: sjw4371 Date: Wed, 15 Oct 2025 17:51:25 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=EB=B3=B8=EB=AC=B8=20=EB=82=B4=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=EC=9D=B4=20=EC=B2=A8=EB=B6=80=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=98=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=EC=8B=9C=EB=A6=AC=EC=A6=88=EC=9D=98=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/posts/route.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/api/posts/route.ts b/app/api/posts/route.ts index 8c5228d..012e4b2 100644 --- a/app/api/posts/route.ts +++ b/app/api/posts/route.ts @@ -118,7 +118,6 @@ export async function POST(req: Request) { author, content, profileImage, - thumbnailImage, seriesId, tags, isPrivate, @@ -131,6 +130,15 @@ export async function POST(req: Request) { ); } + let thumbnailOfPost = getThumbnailInMarkdown(content); + if (!thumbnailOfPost) { + const defaultSeriesThumbnail = + await Series.findById(seriesId).select('thumbnailImage'); + thumbnailOfPost = + defaultSeriesThumbnail?.thumbnailImage || + '/images/placeholder/thumbnail_example2.webp'; + } + const post = { slug: await generateUniqueSlug(title, Post), title, @@ -139,7 +147,7 @@ export async function POST(req: Request) { content, timeToRead: Math.ceil(content.length / 500), profileImage, - thumbnailImage: thumbnailImage || getThumbnailInMarkdown(content), + thumbnailImage: thumbnailOfPost, seriesId: seriesId || null, tags: tags || [], isPrivate: isPrivate || false,