ERP-node/frontend/components/report/designer/renderers/ImageRenderer.tsx

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