/** * Mandu Component * 서버 사이드 이미지 최적화 — srcset, lazy loading, LCP preload */ import React from "react"; // ========== Types ========== export interface ImageProps extends Omit, "src" | "srcSet"> { /** 이미지 소스 경로 (/public 기준) */ src: string; /** alt 텍스트 (필수 — 접근성) */ alt: string; /** 너비 (px) */ width: number; /** 높이 (px) */ height: number; /** 반응형 sizes 속성 */ sizes?: string; /** LCP 이미지 — preload 힌트 삽입 (기본: false) */ priority?: boolean; /** 이미지 품질 (1-100, 기본: 80) */ quality?: number; /** 플레이스홀더 (기본: "empty") */ placeholder?: "empty" | "blur"; /** 생성할 srcset 너비 목록 (기본: [640, 750, 828, 1080, 1200]) */ widths?: number[]; } // ========== Constants ========== const DEFAULT_WIDTHS = [640, 750, 828, 1080, 1200]; const IMAGE_HANDLER_PATH = "/_mandu/image"; // ========== Helper ========== function buildImageUrl(src: string, width: number, quality: number): string { return `${IMAGE_HANDLER_PATH}?url=${encodeURIComponent(src)}&w=${width}&q=${quality}`; } function buildSrcSet(src: string, widths: number[], quality: number): string { return widths .map(w => `${buildImageUrl(src, w, quality)} ${w}w`) .join(", "); } // ========== Component ========== /** * 최적화된 이미지 컴포넌트 * * @example * ```tsx * import { Image } from "@mandujs/core/components/Image"; * * Hero * ``` */ export function Image({ src, alt, width, height, sizes, priority = false, quality = 80, placeholder = "empty", widths = DEFAULT_WIDTHS, style, ...rest }: ImageProps) { const optimizedSrc = buildImageUrl(src, width, quality); const srcSet = buildSrcSet(src, widths, quality); const blurPlaceholderSrc = buildImageUrl(src, Math.min(width, 32), Math.max(10, Math.min(quality, 30))); const imgStyle: React.CSSProperties = { display: "block", aspectRatio: `${width}/${height}`, maxWidth: "100%", height: "auto", position: placeholder === "blur" ? "relative" : undefined, zIndex: placeholder === "blur" ? 1 : undefined, ...style, }; const wrapperStyle: React.CSSProperties | undefined = placeholder === "blur" ? { position: "relative", display: "inline-block", overflow: "hidden", maxWidth: "100%", } : undefined; const blurPlaceholderStyle: React.CSSProperties | undefined = placeholder === "blur" ? { position: "absolute", inset: 0, backgroundImage: `url("${blurPlaceholderSrc}")`, backgroundSize: "cover", backgroundPosition: "center", filter: "blur(16px)", transform: "scale(1.05)", } : undefined; return ( <> {/* React 19: 자동 호이스팅 지원 */} {priority && ( React.createElement("link", { rel: "preload", as: "image", href: optimizedSrc, imageSrcSet: srcSet, imageSizes: sizes, fetchPriority: "high", }) )} {placeholder === "blur" ? ( ) : ( {alt} )} ); }