From 8164f0382f9a709c86ee394d9b90a9206ef7842a Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sat, 16 May 2026 22:12:36 +0900 Subject: [PATCH 001/115] =?UTF-8?q?fix:=20QnA=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=82=A4=20=EC=A0=84=EC=86=A1=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 레슨 질문 모달에서 업로드 공개 URL을 imageKeys로 보내 백엔드가 잘못된 R2 key를 재서명하던 문제를 막기 위함. Constraint: origin/develop 기반 클린브랜치에서 레슨 QnA 이미지 key 변환만 수정하고 API 계약은 변경하지 않음. Tested: yarn eslint src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx --fix Tested: yarn biome format --write src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx Tested: yarn typecheck Tested: git diff --check Co-authored-by: OmX --- .../lesson/[id]/_components/lesson-qna-submission-modal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx index aed767d53..fbae42841 100644 --- a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx +++ b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx @@ -71,8 +71,9 @@ export function LessonQnaSubmissionModal({ setIsUploadingImage(true); try { const publicUrl = await uploadCommunityMarkdownImage(file); + const key = new URL(publicUrl).pathname.slice(1); const previewUrl = URL.createObjectURL(file); - setImages((prev) => [...prev, { previewUrl, key: publicUrl }]); + setImages((prev) => [...prev, { previewUrl, key }]); } catch { showToast('이미지 업로드에 실패했습니다.', 'error'); } finally { From 3ed8b2d6dcd7109c52d03493781a1e3af5779f95 Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 17 May 2026 17:49:32 +0900 Subject: [PATCH 002/115] =?UTF-8?q?fix:=20QnA=20=EC=A0=9C=EC=B6=9C=20?= =?UTF-8?q?=EC=84=B1=EA=B3=B5=20=EC=8B=9C=20blob=20URL=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OmX --- .../[id]/_components/lesson-qna-submission-modal.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx index fbae42841..8f5072b68 100644 --- a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx +++ b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx @@ -81,6 +81,15 @@ export function LessonQnaSubmissionModal({ } } + function clearImagePreviews() { + setImages((prev) => { + prev.forEach((img) => { + URL.revokeObjectURL(img.previewUrl); + }); + return []; + }); + } + function handleImageRemove(index: number) { setImages((prev) => { const next = [...prev]; @@ -117,7 +126,7 @@ export function LessonQnaSubmissionModal({ showToast('질문이 등록되었어요!'); localStorage.removeItem(`lesson-qna-draft-${lessonId}`); setContent(''); - setImages([]); + clearImagePreviews(); onClose(); }, onError: (error) => { From 4d311a6224595e94741fff147e64c950b198a57d Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 17 May 2026 17:52:30 +0900 Subject: [PATCH 003/115] =?UTF-8?q?fix:=20QnA=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OmX --- .../lesson-qna-submission-modal.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx index 8f5072b68..916d9d5e2 100644 --- a/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx +++ b/src/app/(class-lesson)/class/vibe-intro/lesson/[id]/_components/lesson-qna-submission-modal.tsx @@ -35,6 +35,7 @@ export function LessonQnaSubmissionModal({ const noticeVisible = autoVisible || hoverVisible; const [isUploadingImage, setIsUploadingImage] = useState(false); const fileInputRef = useRef(null); + const imageListRef = useRef([]); const showToast = useToastStore((s) => s.showToast); const createQna = useCreateLessonQna(); @@ -61,6 +62,24 @@ export function LessonQnaSubmissionModal({ } }, [open, lessonId]); + useEffect(() => { + imageListRef.current = images; + }, [images]); + + useEffect(() => { + return () => { + imageListRef.current.forEach((img) => { + URL.revokeObjectURL(img.previewUrl); + }); + }; + }, []); + + useEffect(() => { + if (!open) { + clearImagePreviews(); + } + }, [open]); + if (!open) return null; async function handleImageAdd(file: File) { @@ -83,9 +102,7 @@ export function LessonQnaSubmissionModal({ function clearImagePreviews() { setImages((prev) => { - prev.forEach((img) => { - URL.revokeObjectURL(img.previewUrl); - }); + prev.forEach((img) => URL.revokeObjectURL(img.previewUrl)); return []; }); } From 86a7cd44169b917b6747c23475395e72d59bbcb2 Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 17 May 2026 18:06:52 +0900 Subject: [PATCH 004/115] =?UTF-8?q?fix:=20=EC=BB=A4=EB=A6=AC=ED=81=98?= =?UTF-8?q?=EB=9F=BC=20=EB=B0=B0=EC=A7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?locator=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OmX --- e2e/class/curriculum-drawer.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/e2e/class/curriculum-drawer.spec.ts b/e2e/class/curriculum-drawer.spec.ts index d1e0f7204..ba27b7e33 100644 --- a/e2e/class/curriculum-drawer.spec.ts +++ b/e2e/class/curriculum-drawer.spec.ts @@ -185,16 +185,24 @@ test.describe('커리큘럼 드로어 배지 렌더링 @auth', () => { }); test('isFree=false, isLocked=true → 잠금 배지 표시', async ({ page }) => { + const lessonLockBadge = page.getByRole('link', { + name: '잠금 Lesson 02심화 레슨', + exact: true, + }); await expect( - page.getByRole('img', { name: '잠금', exact: true }), + lessonLockBadge.getByRole('img', { name: '잠금', exact: true }), ).toBeVisible(); }); test('isFree=false, isLocked=false → 잠금 해제 배지 표시', async ({ page, }) => { + const lessonUnlockBadge = page.getByRole('link', { + name: '잠금 해제 Lesson 03결제 완료 레슨', + exact: true, + }); await expect( - page.getByRole('img', { name: '잠금 해제', exact: true }), + lessonUnlockBadge.getByRole('img', { name: '잠금 해제', exact: true }), ).toBeVisible(); }); }); From 5bb61f6de79119075d8c4d9bcd206049034cfc8f Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 17 May 2026 18:50:15 +0900 Subject: [PATCH 005/115] =?UTF-8?q?fix:=20=EB=85=B8=EC=85=98=20=EB=B3=B5?= =?UTF-8?q?=EB=B6=99=20=EB=B3=B8=EB=AC=B8=20=EC=9C=A0=EC=8B=A4=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OmX --- .../common/ui/editor/clipboard-utils.ts | 51 +++++++++++++++++++ .../common/ui/editor/markdown-editor.tsx | 8 ++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/components/common/ui/editor/clipboard-utils.ts b/src/components/common/ui/editor/clipboard-utils.ts index e2bd0a36f..0654254d5 100644 --- a/src/components/common/ui/editor/clipboard-utils.ts +++ b/src/components/common/ui/editor/clipboard-utils.ts @@ -8,6 +8,21 @@ const URL_PATTERNS: Record = { 'data-image': /^data:image\/[a-z0-9.+-]+;base64,/i, }; +const IMAGE_URL_PART = + /https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|webp|gif|bmp|avif|svg)(?:\?[^\s"'<>]*)?/gi; +const DATA_IMAGE_TEXT_PART = + /data:image\/[a-z0-9.+-]+;base64,[a-zA-Z0-9+/=\s]+/gi; + +const removeHtmlWrappers = (html: string) => { + return html + .replace(//gi, ' ') + .replace(//gi, ' ') + .replace(/]*>/gi, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +}; + /** * URLs가 지정된 타입 선택 패턴과 일치하는지 검증합니다. * @param text 검증할 URL 문자열 @@ -106,3 +121,39 @@ export const hasClipboardImageHint = (clipboardData: DataTransfer) => { return isAllowedUrl(pastedText, ['image', 'data-image']); }; + +/** + * 클립보드 내용이 실제 텍스트보다 이미지 데이터가 주도적일 때 true를 반환합니다. + * 텍스트가 존재하더라도 이미지 URL만 단독으로 들어 있으면 true로 간주됩니다. + * @param clipboardData 클립보드 데이터 + * @returns 이미지 전용으로 보이면 true + */ +export const isClipboardImageOnly = (clipboardData: DataTransfer) => { + const pastedText = clipboardData.getData('text/plain').trim(); + const pastedHtml = clipboardData.getData('text/html').trim(); + + if (pastedText) { + const textWithoutImageRefs = pastedText + .replace(IMAGE_URL_PART, '') + .replace(DATA_IMAGE_TEXT_PART, '') + .replace(/\s+/g, '') + .trim(); + + if (textWithoutImageRefs) { + return false; + } + + return !!isAllowedUrl(pastedText, ['image', 'data-image']); + } + + if (!pastedHtml) { + return extractClipboardImageFiles(clipboardData).length > 0; + } + + const htmlText = removeHtmlWrappers(pastedHtml); + if (htmlText) { + return false; + } + + return true; +}; diff --git a/src/components/common/ui/editor/markdown-editor.tsx b/src/components/common/ui/editor/markdown-editor.tsx index 882752d48..606edce03 100644 --- a/src/components/common/ui/editor/markdown-editor.tsx +++ b/src/components/common/ui/editor/markdown-editor.tsx @@ -34,7 +34,7 @@ import { cn } from '@/components/common/ui/(shadcn)/lib/utils'; import Button from '@/components/common/ui/button'; import { normalizeMarkdownContent } from '@/utils/markdown-content-normalize'; import { getRichContentVisibleTextLength } from '@/utils/markdown-content-text'; -import { hasClipboardImageHint } from './clipboard-utils'; +import { hasClipboardImageHint, isClipboardImageOnly } from './clipboard-utils'; import EditorVisibleTextCounter from './editor-visible-text-counter'; import { InstantCodeBlockExtension, @@ -265,7 +265,11 @@ function MarkdownEditor({ return false; } - if (hasClipboardImageHint(clipboardData)) { + const clipboardImageOnly = + hasClipboardImageHint(clipboardData) && + isClipboardImageOnly(clipboardData); + + if (clipboardImageOnly) { event.preventDefault(); if (!resolvedImageConfig) { setImageInsertError( From b873a29c724da90fe8870298c4fbdf8b44a945ea Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Sun, 17 May 2026 19:19:08 +0900 Subject: [PATCH 006/115] =?UTF-8?q?[builder-feed-update-and-delete]=20feat?= =?UTF-8?q?=20:=20chore(types):=20BuilderFeedUpdateRequest=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../(learning)/feed/[id]/edit/page.tsx | 16 +- .../vibe-intro/(learning)/feed/[id]/page.tsx | 58 +++- .../vibe-intro/(learning)/feed/write/page.tsx | 297 +++++++++++------- src/hooks/queries/course/course-api.ts | 38 +++ src/types/api/course.types.ts | 7 + 5 files changed, 293 insertions(+), 123 deletions(-) diff --git a/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/edit/page.tsx b/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/edit/page.tsx index 2005c5c25..8444ea441 100644 --- a/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/edit/page.tsx +++ b/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/edit/page.tsx @@ -1,19 +1,11 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { use, useEffect } from 'react'; +import { redirect } from 'next/navigation'; +import { use } from 'react'; export default function FeedEditPage({ params, }: { params: Promise<{ id: string }>; -}): null { +}) { const { id } = use(params); - const router = useRouter(); - - useEffect(() => { - router.replace(`/class/vibe-intro/feed/${id}`); - }, [id, router]); - - return null; + redirect(`/class/vibe-intro/feed/write?feedId=${id}`); } diff --git a/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/page.tsx b/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/page.tsx index 60b713f49..d41fd6d55 100644 --- a/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/page.tsx +++ b/src/app/(landing)/class/vibe-intro/(learning)/feed/[id]/page.tsx @@ -3,6 +3,7 @@ import { ArrowLeft, MoreVertical } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; +import { useRouter } from 'next/navigation'; import { use, useState } from 'react'; import { ROLE_LABELS, @@ -21,6 +22,7 @@ import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-conte import { useAuth } from '@/features/auth/model/use-auth'; import { useCreateFeedComment, + useDeleteBuilderFeed, useGetBuilderFeedDetail, useGetBuilderFeeds, useGetFeedComments, @@ -38,10 +40,12 @@ export default function FeedDetailPage({ const feedId = parseInt(id, 10); const { memberId } = useAuth(); const showToast = useToastStore((s) => s.showToast); + const router = useRouter(); const [comment, setComment] = useState(''); const [moreOpen, setMoreOpen] = useState(false); const [showReportModal, setShowReportModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [reportReason, setReportReason] = useState(''); const [isProfileOpen, setIsProfileOpen] = useState(false); @@ -56,6 +60,7 @@ export default function FeedDetailPage({ const toggleLikeMutation = useToggleFeedLike(); const createCommentMutation = useCreateFeedComment(); const reportFeedMutation = useReportBuilderFeed(); + const deleteFeedMutation = useDeleteBuilderFeed(); const liked = feed?.isLiked ?? false; const likeCount = feed?.likeCount ?? 0; @@ -141,6 +146,47 @@ export default function FeedDetailPage({ )} + {/* Delete confirm modal */} + {showDeleteConfirm && ( +
+
+
+

+ 피드를 삭제하시겠습니까? +

+

+ 삭제된 피드는 복구할 수 없습니다. +

+
+
+ + +
+
+
+ )} + {feed !== undefined && feed.author.memberId !== undefined && ( - {courseOpen && ( -
- {['바이브 코딩 입문자 코스'].map((c) => ( - - ))} -
- )} - - - - {/* Lesson selector */} -
- - {lessonOpen && ( -
- {allLessons.map((l) => ( + {/* Course/Lesson selectors — create mode only */} + {!isEditMode && ( + <> +
+

+ 어떤 코스에서 만든 결과물인가요? +

+
- ))} + {courseOpen && ( +
+ {['바이브 코딩 입문자 코스'].map((c) => ( + + ))} +
+ )} +
- )} -
+ +
+ + {lessonOpen && ( +
+ {allLessons.map((l) => ( + + ))} +
+ )} +
+ + )} {/* Image upload */}
@@ -285,26 +348,42 @@ export default function FeedWritePage() { {/* CTAs */}
- + {isEditMode ? ( + + ) : ( + + )}
diff --git a/src/hooks/queries/course/course-api.ts b/src/hooks/queries/course/course-api.ts index 7a8b66550..cbb58d68f 100644 --- a/src/hooks/queries/course/course-api.ts +++ b/src/hooks/queries/course/course-api.ts @@ -16,6 +16,7 @@ import type { BuilderFeedReportCreateRequest, BuilderFeedShowcaseResponse, BuilderFeedStatsResponse, + BuilderFeedUpdateRequest, CourseCompletionRecapResponse, CourseCurriculumResponse, CourseDetailResponse, @@ -801,6 +802,43 @@ export const useReportBuilderFeed = () => { }); }; +export const useDeleteBuilderFeed = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (feedId: number) => { + await axiosInstanceV5.delete(`builder-feeds/${feedId}`); + }, + onSuccess: async (_, feedId) => { + queryClient.removeQueries({ queryKey: ['builderFeedDetail', feedId] }); + await queryClient.invalidateQueries({ queryKey: ['builderFeeds'] }); + }, + }); +}; + +export const useUpdateBuilderFeed = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + feedId, + request, + }: { + feedId: number; + request: BuilderFeedUpdateRequest; + }) => { + const { data } = await axiosInstanceV5.put<{ + content: { feedId: number }; + }>(`builder-feeds/${feedId}`, request); + return data.content; + }, + onSuccess: async (_, variables) => { + await queryClient.invalidateQueries({ + queryKey: ['builderFeedDetail', variables.feedId], + }); + await queryClient.invalidateQueries({ queryKey: ['builderFeeds'] }); + }, + }); +}; + // ─── Builder Feed (lesson preview) ──────────────────────────────────────────── export const useGetLessonBuilderFeedPreview = (lessonId: number) => { diff --git a/src/types/api/course.types.ts b/src/types/api/course.types.ts index defc798f0..f86912d03 100644 --- a/src/types/api/course.types.ts +++ b/src/types/api/course.types.ts @@ -632,6 +632,13 @@ export interface GiftEmailResponse { email?: string; } +// ─── Builder Feed Mutations ─────────────────────────────────────────────────── + +export interface BuilderFeedUpdateRequest { + content: string; + imageKeys?: string[]; +} + export interface GiftEmailCreateRequest { email: string; } From b51a51ae5596a1844ed705bb5ebe52da56012ba3 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Sun, 17 May 2026 19:19:35 +0900 Subject: [PATCH 007/115] =?UTF-8?q?[builder-feed-update-and-delete]=20feat?= =?UTF-8?q?=20:=20style(feed):=20biome=20import=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../(landing)/class/vibe-intro/(learning)/feed/write/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx b/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx index a3db9218a..a182f2eab 100644 --- a/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx +++ b/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx @@ -8,7 +8,6 @@ import { Suspense, useEffect, useRef, useState } from 'react'; import { cn } from '@/components/common/ui/(shadcn)/lib/utils'; import MarkdownEditor from '@/components/common/ui/editor/markdown-editor'; import { uploadCommunityMarkdownImage } from '@/features/community/model/community-markdown-image-upload'; -import { extractPlainTextFromHtml } from '@/utils/markdown-content'; import { useCreateBuilderFeed, useGetBuilderFeedDetail, @@ -17,6 +16,7 @@ import { useUpdateBuilderFeed, } from '@/hooks/queries/course/course-api'; import { useToastStore } from '@/stores/use-toast-store'; +import { extractPlainTextFromHtml } from '@/utils/markdown-content'; interface AttachedImage { previewUrl: string; From 35c194102a6634621c30cdb38fb00275a9e5eebd Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Sun, 17 May 2026 19:33:44 +0900 Subject: [PATCH 008/115] =?UTF-8?q?[builder-feed-update-and-delete]=20fix?= =?UTF-8?q?=20:=20NaN=20feedId=20edit=20=EB=AA=A8=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EB=B0=8F=20=EC=BA=90=EC=8B=9C=20=EB=AC=B4?= =?UTF-8?q?=ED=9A=A8=ED=99=94=20=EB=B2=94=EC=9C=84=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../class/vibe-intro/(learning)/feed/write/page.tsx | 6 ++++-- src/app/global.css | 1 + src/hooks/queries/course/course-api.ts | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx b/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx index a182f2eab..6d2168b8c 100644 --- a/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx +++ b/src/app/(landing)/class/vibe-intro/(learning)/feed/write/page.tsx @@ -37,7 +37,9 @@ function FeedWriteContent() { const showToast = useToastStore((s) => s.showToast); const feedIdParam = searchParams.get('feedId'); - const editFeedId = feedIdParam ? parseInt(feedIdParam, 10) : null; + const parsedFeedId = feedIdParam ? parseInt(feedIdParam, 10) : null; + const editFeedId = + parsedFeedId !== null && !Number.isNaN(parsedFeedId) ? parsedFeedId : null; const isEditMode = editFeedId !== null; const course = '바이브 코딩 입문자 코스'; @@ -266,7 +268,7 @@ function FeedWriteContent() { /> {lessonOpen && ( -
+
{allLessons.map((l) => ( +
+ {copyMessage && ( +

+ {copyMessage} +

+ )} +