Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down
8 changes: 3 additions & 5 deletions src/app/(landing)/class/[slug]/(learning)/feed/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -691,7 +689,7 @@ export default function FeedDetailPage({
</div>
<div className="p-200">
<p className="line-clamp-2 font-designer-14r text-gray-800">
{f.content}
{stripHtml(f.content)}
</p>
<div className="mt-100 flex items-center gap-75">
<FeedHeartIcon className="h-200 w-200 text-gray-400" />
Expand Down
11 changes: 5 additions & 6 deletions src/app/(landing)/class/[slug]/(learning)/qa/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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');
Expand Down
40 changes: 16 additions & 24 deletions src/components/common/ui/editor/image-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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() ??
''
);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 },
);
};

Expand All @@ -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}`);
Expand Down
3 changes: 3 additions & 0 deletions src/utils/markdown-content-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Loading