132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { getFullImageUrl } from "@/lib/api/client";
|
|
import type { ImageRendererProps } from "./types";
|
|
|
|
export function ImageRenderer({ component }: ImageRendererProps) {
|
|
const {
|
|
imageUrl,
|
|
objectFit = "contain",
|
|
imageOpacity = 1,
|
|
imageBorderRadius = 0,
|
|
imageCaption,
|
|
imageCaptionPosition = "bottom",
|
|
imageCaptionFontSize = 12,
|
|
imageCaptionColor = "#666666",
|
|
imageCaptionAlign = "center",
|
|
imageAlt = "이미지",
|
|
imageRotation = 0,
|
|
imageFlipH = false,
|
|
imageFlipV = false,
|
|
imageCropX,
|
|
imageCropY,
|
|
imageCropWidth,
|
|
imageCropHeight,
|
|
} = component;
|
|
|
|
const hasCaption = !!imageCaption;
|
|
const hasCrop = imageCropWidth != null && imageCropHeight != null && imageCropWidth < 100;
|
|
|
|
const transformParts = useMemo(() => {
|
|
const parts: string[] = [];
|
|
if (imageRotation) parts.push(`rotate(${imageRotation}deg)`);
|
|
if (imageFlipH) parts.push("scaleX(-1)");
|
|
if (imageFlipV) parts.push("scaleY(-1)");
|
|
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
}, [imageRotation, imageFlipH, imageFlipV]);
|
|
|
|
const containerStyle = useMemo(
|
|
() => ({
|
|
borderRadius: imageBorderRadius,
|
|
overflow: "hidden" as const,
|
|
}),
|
|
[imageBorderRadius],
|
|
);
|
|
|
|
const imageStyle = useMemo(() => {
|
|
if (hasCrop) {
|
|
const scaleX = 100 / (imageCropWidth ?? 100);
|
|
const scaleY = 100 / (imageCropHeight ?? 100);
|
|
const translateX = -(imageCropX ?? 0) * scaleX;
|
|
const translateY = -(imageCropY ?? 0) * scaleY;
|
|
|
|
const cropTransform = `translate(${translateX}%, ${translateY}%) scale(${scaleX}, ${scaleY})`;
|
|
const extraTransform = transformParts ? ` ${transformParts}` : "";
|
|
|
|
return {
|
|
display: "block" as const,
|
|
width: "100%",
|
|
height: "100%",
|
|
objectFit: "none" as const,
|
|
objectPosition: "0% 0%",
|
|
opacity: imageOpacity,
|
|
transform: cropTransform + extraTransform,
|
|
transformOrigin: "0% 0%",
|
|
};
|
|
}
|
|
|
|
return {
|
|
display: "block" as const,
|
|
width: "100%",
|
|
height: "100%",
|
|
objectFit: objectFit,
|
|
opacity: imageOpacity,
|
|
transform: transformParts,
|
|
};
|
|
}, [objectFit, imageOpacity, transformParts, hasCrop, imageCropX, imageCropY, imageCropWidth, imageCropHeight]);
|
|
|
|
if (!imageUrl) {
|
|
const isSmall = component.width < 100 || component.height < 80;
|
|
return (
|
|
<div
|
|
className="flex h-full w-full flex-col items-center justify-center border-2 border-dashed border-gray-300 bg-gray-50"
|
|
style={{ borderRadius: imageBorderRadius, gap: isSmall ? 0 : 8, overflow: "hidden" }}
|
|
>
|
|
<svg
|
|
className={`${isSmall ? "h-6 w-6" : "h-10 w-10"} text-gray-300`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={1.5}
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"
|
|
/>
|
|
</svg>
|
|
{!isSmall && <span className="text-xs text-gray-400">이미지를 업로드하세요</span>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const captionElement = hasCaption && (
|
|
<div
|
|
style={{
|
|
fontSize: imageCaptionFontSize,
|
|
color: imageCaptionColor,
|
|
textAlign: imageCaptionAlign,
|
|
padding: "4px 8px",
|
|
lineHeight: 1.4,
|
|
}}
|
|
>
|
|
{imageCaption}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="flex h-full w-full flex-col" style={containerStyle}>
|
|
{imageCaptionPosition === "top" && captionElement}
|
|
<div className="min-h-0 flex-1 overflow-hidden">
|
|
<img
|
|
src={getFullImageUrl(imageUrl)}
|
|
alt={imageAlt}
|
|
style={imageStyle}
|
|
/>
|
|
</div>
|
|
{imageCaptionPosition === "bottom" && captionElement}
|
|
</div>
|
|
);
|
|
}
|