Skip to content
Open
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
9 changes: 7 additions & 2 deletions src/components/editor/use-toolbar.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getImageSize } from "@/lib/utils";
import { useUploadImage } from "@/services/storage";
import { useCurrentEditor } from "@tiptap/react";
import { nanoid } from "nanoid";
Expand Down Expand Up @@ -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();

Expand Down
69 changes: 57 additions & 12 deletions src/components/extensions/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { IconButton } from "../ui/icon-button";
declare module "@tiptap/core" {
interface Commands<ReturnType> {
image: {
insertImage: (options: { url: string | null; id: string }) => ReturnType;
insertImage: (options: {
url: string | null;
id: string;
width?: number;
height?: number;
}) => ReturnType;
removeImage: () => ReturnType;
};
}
Expand Down Expand Up @@ -52,6 +57,12 @@ export const ImageExtension = Node.create({
id: {
default: null,
},
width: {
default: 0,
},
height: {
default: 0,
},
};
},

Expand Down Expand Up @@ -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 (
<NodeViewWrapper>
<AsyncImage url={url} id={id} onDelete={deleteNode} />
<AsyncImage url={url} id={id} width={width} height={height} onDelete={deleteNode} />
</NodeViewWrapper>
);
};
Expand All @@ -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;

Expand All @@ -116,21 +126,56 @@ const AsyncImage = ({ url, id, onDelete }: AsyncImageProps) => {
}
}, [id, url, editor, cachedImageUrl]);

if (imageSrc === null) {
return <div className="h-[320px] w-full bg-gray-200" />;
// 크기 계산 - 너비만 에디터 크기로 제한
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 (
<div
className="flex max-w-full items-center justify-center bg-gray-200"
style={{
width: `${finalWidth}px`,
height: `${finalHeight}px`,
}}
>
<span className="text-sm text-gray-500">로딩 중...</span>
</div>
);
}

return (
<>
<img className="w-full" src={imageSrc} alt="" />
<div className="flex w-fit flex-col">
<img src={imageSrc} alt="" style={style} />
<div className="mt-2 flex justify-center">
<div className={cn("hidden", styles["image-toolbar"])}>
<IconButton size="small" aria-label="이미지 삭제" variant="outlined" onClick={onDelete}>
<Trash2Icon size={16} className="text-error" />
</IconButton>
</div>
</div>
</>
</div>
);
};

Expand Down
19 changes: 18 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
});
};