diff --git a/src/components/editor/use-toolbar.ts b/src/components/editor/use-toolbar.ts index 28008be..27aff40 100644 --- a/src/components/editor/use-toolbar.ts +++ b/src/components/editor/use-toolbar.ts @@ -1,3 +1,4 @@ +import { getImageSize } from "@/lib/utils"; import { useUploadImage } from "@/services/storage"; import { useCurrentEditor } from "@tiptap/react"; import { nanoid } from "nanoid"; @@ -51,10 +52,14 @@ export const useToolbar = () => { const { mutate: uploadImage } = useUploadImage(); - const insertImage = (file: File) => { + const insertImage = async (file: File) => { const id = nanoid(); - editor.chain().focus().insertImage({ url: null, id }).run(); + const { width, height } = await getImageSize(file); + + console.log(width, height); + + editor.chain().focus().insertImage({ url: null, id, width, height }).run(); editor.commands.enter(); diff --git a/src/components/extensions/image.tsx b/src/components/extensions/image.tsx index 0b12d19..5108f7c 100644 --- a/src/components/extensions/image.tsx +++ b/src/components/extensions/image.tsx @@ -9,7 +9,12 @@ import { IconButton } from "../ui/icon-button"; declare module "@tiptap/core" { interface Commands { image: { - insertImage: (options: { url: string | null; id: string }) => ReturnType; + insertImage: (options: { + url: string | null; + id: string; + width?: number; + height?: number; + }) => ReturnType; removeImage: () => ReturnType; }; } @@ -52,6 +57,12 @@ export const ImageExtension = Node.create({ id: { default: null, }, + width: { + default: 0, + }, + height: { + default: 0, + }, }; }, @@ -79,11 +90,11 @@ export const ImageExtension = Node.create({ }); const ImageComponent = ({ node, deleteNode }: NodeViewProps) => { - const { url, id } = node.attrs; + const { url, id, width, height } = node.attrs; return ( - + ); }; @@ -92,16 +103,15 @@ type AsyncImageProps = { url: string | null; id: string; onDelete: () => void; + width?: number; + height?: number; }; -const AsyncImage = ({ url, id, onDelete }: AsyncImageProps) => { +const AsyncImage = ({ url, id, onDelete, width = 500, height = 500 }: AsyncImageProps) => { const { editor } = useCurrentEditor(); - const cachedImageUrl = uploadedImageMap.get(id); - const imageSrc = cachedImageUrl || url; - // Redo로 인해 마운트되었을 때 이미지 캐시를 확인 useEffect(() => { if (url !== null || editor === null) return; @@ -116,13 +126,48 @@ const AsyncImage = ({ url, id, onDelete }: AsyncImageProps) => { } }, [id, url, editor, cachedImageUrl]); - if (imageSrc === null) { - return
; + // 크기 계산 - 너비만 에디터 크기로 제한 + const getImageSize = () => { + const editorWidth = editor?.view?.dom?.clientWidth || 800; // 에디터 너비 (기본값 800) + + let finalWidth = width; + let finalHeight = height; + + // 너비가 에디터 너비를 초과하면 축소 (높이는 비율에 맞춰 자동 조정) + if (finalWidth > editorWidth) { + finalHeight = (finalHeight * editorWidth) / finalWidth; + finalWidth = editorWidth; + } + + return { width: finalWidth, height: finalHeight }; + }; + + const { width: finalWidth, height: finalHeight } = getImageSize(); + + // 스타일 계산 + const style = { + width: `${finalWidth}px`, + height: `${finalHeight}px`, + objectFit: "contain" as const, + }; + + if (!imageSrc) { + return ( +
+ 로딩 중... +
+ ); } return ( - <> - +
+
@@ -130,7 +175,7 @@ const AsyncImage = ({ url, id, onDelete }: AsyncImageProps) => {
- +
); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c71067e..71fdc1e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,23 @@ -import { twMerge } from "tailwind-merge"; import clsx, { ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export const cn = (...inputs: ClassValue[]) => { return twMerge(clsx(inputs)); }; + +export const getImageSize = (file: File): Promise<{ width: number; height: number }> => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => { + resolve({ + width: img.width, + height: img.height, + }); + }; + + img.onerror = (err) => reject(err); + + img.src = URL.createObjectURL(file); + }); +};