From f4a80d3db0969d864ddcab60401cf4b8186530b1 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 25 May 2026 10:46:20 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[image-key]=20fix=20:=20refactor(image-util?= =?UTF-8?q?s):=20IMAGE=5FMIME=5FTO=5FEXT=20=EC=A0=9C=EA=B1=B0=20=E2=80=94?= =?UTF-8?q?=20IMAGE=5FMIME=5FTO=5FEXTS=20=EB=8B=A8=EC=9D=BC=20=EB=A0=88?= =?UTF-8?q?=EC=A7=80=EC=8A=A4=ED=8A=B8=EB=A6=AC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IMAGE_MIME_TO_EXT 삭제 (IMAGE_MIME_TO_EXTS[mime][0] 중복) - getExtensionFromMime: IMAGE_MIME_TO_EXTS[mime][0] 참조로 변경 - toImageInputAccept: exts.some() 로 alias 확장자(jpeg 등) 포함 MIME 매칭 Co-Authored-By: Claude Sonnet 4.6 --- .../common/ui/editor/image-utils.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/components/common/ui/editor/image-utils.ts b/src/components/common/ui/editor/image-utils.ts index 8c9a1aa01..2eb60e6c6 100644 --- a/src/components/common/ui/editor/image-utils.ts +++ b/src/components/common/ui/editor/image-utils.ts @@ -37,15 +37,6 @@ export const MARKDOWN_IMAGE_DEFAULT_ALLOWED_EXTENSIONS = [ 'heif', ] as const; -const IMAGE_MIME_TO_EXT = { - 'image/gif': 'gif', - 'image/heic': 'heic', - 'image/heif': 'heif', - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/webp': 'webp', -} as const; - const IMAGE_MIME_TO_EXTS = { 'image/gif': ['gif'], 'image/heic': ['heic'], @@ -123,7 +114,9 @@ const normalizeImageMimeType = ( export const getExtensionFromMime = (mimeType: string): string => { const normalizedMimeType = normalizeImageMimeType(mimeType); return ( - (normalizedMimeType ? IMAGE_MIME_TO_EXT[normalizedMimeType] : undefined) ?? + (normalizedMimeType + ? IMAGE_MIME_TO_EXTS[normalizedMimeType][0] + : undefined) ?? mimeType.split('/')[1]?.toLowerCase() ?? '' ); @@ -245,20 +238,17 @@ const convertHeicImageFileToJpeg = async (file: File) => { }; export const normalizeImageFileForUpload = async (file: File) => { - const detectedMimeType = detectImageMimeTypeFromHeader( - new Uint8Array(await file.slice(0, IMAGE_HEADER_BYTE_LENGTH).arrayBuffer()), + const headerBytes = new Uint8Array( + await file.slice(0, IMAGE_HEADER_BYTE_LENGTH).arrayBuffer(), ); + const detectedMimeType = detectImageMimeTypeFromHeader(headerBytes); const reportedMimeType = normalizeImageMimeType(file.type); + const resolvedMimeType = detectedMimeType ?? reportedMimeType; - if (isHeicLikeImageMimeType(detectedMimeType)) { - return convertHeicImageFileToJpeg(file); - } - - if (!detectedMimeType && isHeicLikeImageMimeType(reportedMimeType)) { + if (isHeicLikeImageMimeType(resolvedMimeType)) { return convertHeicImageFileToJpeg(file); } - const resolvedMimeType = detectedMimeType ?? reportedMimeType; if (!resolvedMimeType) { return file; } @@ -273,10 +263,7 @@ export const normalizeImageFileForUpload = async (file: File) => { return new File( [file], replaceFileExtension(file.name, getExtensionFromMime(resolvedMimeType)), - { - type: resolvedMimeType, - lastModified: file.lastModified, - }, + { type: resolvedMimeType, lastModified: file.lastModified }, ); }; @@ -298,8 +285,13 @@ export const getImageFileNormalizationErrorMessage = ( * toImageInputAccept(['jpg', 'png', 'webp']) // '.jpg,.png,.webp' */ export const toImageInputAccept = (extensions: readonly string[]) => { - const mimeTypes = Object.entries(IMAGE_MIME_TO_EXT) - .filter(([, ext]) => extensions.includes(ext)) + const mimeTypes = ( + Object.entries(IMAGE_MIME_TO_EXTS) as [ + SupportedImageMimeType, + readonly string[], + ][] + ) + .filter(([, exts]) => exts.some((ext) => extensions.includes(ext))) .map(([mime]) => mime); const extensionParts = extensions.map((extension) => `.${extension}`); From b0cc24a4c19c9cf227e4effedd2c61efaf9bf6e2 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 25 May 2026 10:49:36 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[image-key]=20fix=20:=20chore(utils):=20str?= =?UTF-8?q?ipHtml=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20export=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 --- src/utils/markdown-content-text.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/markdown-content-text.ts b/src/utils/markdown-content-text.ts index f50d490d7..5914f06a9 100644 --- a/src/utils/markdown-content-text.ts +++ b/src/utils/markdown-content-text.ts @@ -38,3 +38,6 @@ export const getRichContentVisibleText = (content: unknown): string => { export const getRichContentVisibleTextLength = (content: unknown): number => { return getRichContentVisibleText(content).length; }; + +export const stripHtml = (html: string): string => + getRichContentVisibleText(html); From 6c1a583ce1b391c49799f693c3f6b7da619b7ad5 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 25 May 2026 10:50:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[image-key]=20fix=20:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20normalize?= =?UTF-8?q?ImageFileForUpload=20=EC=A0=81=EC=9A=A9,=20stripHtml=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=ED=95=A8=EC=88=98=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lesson-qna, qa 페이지 이미지 업로드에 normalizeImageFileForUpload 적용 - qa 페이지 로컬 stripHtml 함수 제거, 공용 유틸로 교체 - feed 페이지도 공용 stripHtml 사용 Co-Authored-By: Claude Sonnet 4.6 --- .../[id]/_components/lesson-qna-submission-modal.tsx | 6 ++++-- .../class/[slug]/(learning)/feed/[id]/page.tsx | 8 +++----- .../class/[slug]/(learning)/qa/[id]/page.tsx | 11 +++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx b/src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx index b6cfb563a..a13cf9b54 100644 --- a/src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx +++ b/src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx @@ -4,6 +4,7 @@ import { ImagePlus, Info, X } from 'lucide-react'; import Image from 'next/image'; import { useEffect, useRef, useState } from 'react'; import { cn } from '@/components/common/ui/(shadcn)/lib/utils'; +import { normalizeImageFileForUpload } from '@/components/common/ui/editor/image-utils'; import MarkdownEditor from '@/components/common/ui/editor/markdown-editor'; import { uploadCommunityMarkdownImage } from '@/features/community/model/community-markdown-image-upload'; import { useCreateLessonQna } from '@/hooks/queries/course/course-api'; @@ -89,9 +90,10 @@ export function LessonQnaSubmissionModal({ } setIsUploadingImage(true); try { - const publicUrl = await uploadCommunityMarkdownImage(file); + const normalizedFile = await normalizeImageFileForUpload(file); + const publicUrl = await uploadCommunityMarkdownImage(normalizedFile); const key = new URL(publicUrl).pathname.slice(1); - const previewUrl = URL.createObjectURL(file); + const previewUrl = URL.createObjectURL(normalizedFile); setImages((prev) => [...prev, { previewUrl, key }]); } catch { showToast('이미지 업로드에 실패했습니다.', 'error'); diff --git a/src/app/(landing)/class/[slug]/(learning)/feed/[id]/page.tsx b/src/app/(landing)/class/[slug]/(learning)/feed/[id]/page.tsx index 3c4c8040b..cced6259a 100644 --- a/src/app/(landing)/class/[slug]/(learning)/feed/[id]/page.tsx +++ b/src/app/(landing)/class/[slug]/(learning)/feed/[id]/page.tsx @@ -15,10 +15,7 @@ import { FeedShareIcon, } from '@/components/common/ui/icons/course-icons'; import MarkdownContentCore from '@/components/common/ui/rich-text/markdown-content-core'; -import { - ROLE_LABELS, - RoleBadge, -} from '@/components/pages/class/utils/builder-feed-utils'; +import { RoleBadge } from '@/components/pages/class/utils/builder-feed-utils'; import { useAuth } from '@/features/auth/model/use-auth'; import { useCreateFeedComment, @@ -30,6 +27,7 @@ import { useToggleFeedLike, } from '@/hooks/queries/course/course-api'; import { useToastStore } from '@/stores/use-toast-store'; +import { stripHtml } from '@/utils/markdown-content-text'; function isImageUrl(url: string): boolean { return /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url); @@ -691,7 +689,7 @@ export default function FeedDetailPage({

- {f.content} + {stripHtml(f.content)}

diff --git a/src/app/(landing)/class/[slug]/(learning)/qa/[id]/page.tsx b/src/app/(landing)/class/[slug]/(learning)/qa/[id]/page.tsx index cdf2fbc56..33254f16f 100644 --- a/src/app/(landing)/class/[slug]/(learning)/qa/[id]/page.tsx +++ b/src/app/(landing)/class/[slug]/(learning)/qa/[id]/page.tsx @@ -16,6 +16,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { use, useRef, useState } from 'react'; import { cn } from '@/components/common/ui/(shadcn)/lib/utils'; +import { normalizeImageFileForUpload } from '@/components/common/ui/editor/image-utils'; import MarkdownEditor from '@/components/common/ui/editor/markdown-editor'; import { RoleBadge } from '@/components/pages/class/utils/builder-feed-utils'; import { useAuth } from '@/features/auth/model/use-auth'; @@ -34,16 +35,13 @@ import { import { useToastStore } from '@/stores/use-toast-store'; import { AUTH_ROLE_IDS } from '@/types/auth/domain'; import { analyzeError } from '@/utils/error-handler'; +import { stripHtml } from '@/utils/markdown-content-text'; function formatDate(dateStr: string) { const d = new Date(dateStr); return `${d.getMonth() + 1}월 ${d.getDate()}일`; } -function stripHtml(html: string): string { - return html.replace(/<[^>]*>/g, '').trim(); -} - function HtmlContent({ html }: { html: string }) { const isHtml = /<[a-z]/i.test(html); @@ -268,9 +266,10 @@ export default function QnaDetailPage({ } setIsUploadingAnswerImage(true); try { - const publicUrl = await uploadCommunityMarkdownImage(file); + const normalizedFile = await normalizeImageFileForUpload(file); + const publicUrl = await uploadCommunityMarkdownImage(normalizedFile); const key = new URL(publicUrl).pathname.slice(1); - const previewUrl = URL.createObjectURL(file); + const previewUrl = URL.createObjectURL(normalizedFile); setAnswerImages((prev) => [...prev, { previewUrl, key }]); } catch { showToast('이미지 업로드에 실패했습니다.', 'error');