From aebb3d2c4758bc3be6b64a7ed2393738cbe92bbc 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/2] =?UTF-8?q?[fix-image-key-to-main]=20cherry=20:=20[ima?= =?UTF-8?q?ge-key]=20fix=20:=20refactor(image-utils):=20IMAGE=5FMIME=5FTO?= =?UTF-8?q?=5FEXT=20=EC=A0=9C=EA=B1=B0=20=E2=80=94=20IMAGE=5FMIME=5FTO=5FE?= =?UTF-8?q?XTS=20=EB=8B=A8=EC=9D=BC=20=EB=A0=88=EC=A7=80=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=ED=86=B5=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 c1e685d5ecf88da988dd0106b5108779dbcc379d 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 2/2] =?UTF-8?q?[fix-image-key-to-main]=20cherry=20:=20[ima?= =?UTF-8?q?ge-key]=20fix=20:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20normalizeImageFileForUpload=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9,=20stripHtml=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=A1=9C=20=EA=B5=90=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');