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');
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}`);