@roottale/cms-media 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,8 @@ import { CfImage } from "@roottale/cms-media/react";
20
20
  ```
21
21
 
22
22
  - CF imagedelivery URL이면 sm/md/lg srcset 자동, 기본 src `/md`. 비-CF(로컬 SVG·외부)면 변형 없이 그대로.
23
- - `placeholder`(로더): `'blur'`(기본, CF thumbnail 흐림) | `'skeleton'`(shimmer) | `'image'` | `'none'`. 로딩 중 표시 후 `onLoad` 페이드인.
23
+ - `placeholder`(로더): `'blur'`(기본) | `'skeleton'`(shimmer) | `'image'` | `'none'`. 로딩 중 표시 후 `onLoad` 페이드인.
24
+ - `'blur'`(CF): skeleton shimmer를 **즉시** 깔아 마운트 순간부터 로딩을 인지시키고, CF thumbnail이 받아지는 순간 그 위로 흐림을 페이드인 → 원본 도착 시 둘 다 페이드아웃. (blur 썸네일도 네트워크라 받기 전 빈 화면 갭을 skeleton이 메움. CF가 아니면 skeleton 단독.)
24
25
  - `'image'` + `loaderImage="<url>"`: 로딩 중 가운데 커스텀 이미지(브랜드 스피너/로고 등).
25
26
  - `loader={<MySpinner/>}`: 임의 ReactNode를 로더로 직접 주입(최우선). `loaderBackground`로 배경색 지정.
26
27
  - `priority`로 LCP 즉시 로드. `fill`/`sizes`/`width`/`height`/`loading`/`onError`/`style` 등 next/image·`<img>` 호환.
package/dist/react.d.ts CHANGED
@@ -9,9 +9,13 @@ import { ImgHTMLAttributes, CSSProperties, ReactNode } from 'react';
9
9
  * srcset으로 브라우저가 직접 받게 한다 → Vercel 컴퓨트 0. (ADR-0034 / docs/image-standard.md)
10
10
  *
11
11
  * - CF imagedelivery URL: srcset(sm/md/lg) + 기본 src=md. 비-CF URL(로컬 SVG·외부)은 변형 없이 그대로.
12
- * - placeholder(로더): 'blur'(CF thumbnail 흐림) | 'skeleton'(shimmer) | 'image'(loaderImage) | 'none'. 기본 'blur'.
13
- * · loaderImage: placeholder='image'일 때 로딩 중 가운데 표시할 커스텀 이미지 URL(브랜드 스피너/로고 등).
14
- * · loader: 임의 ReactNode를 로더로 직접 주입(최우선). 무엇이든 커스텀 가능.
12
+ * - placeholder(로더): 'blur' | 'skeleton' | 'image' | 'none'. 기본 'blur'.
13
+ * · 'blur'(CF): skeleton shimmer 를 즉시 깔고(=마운트 순간 로딩 인지) 그 위로 CF thumbnail
14
+ * 흐림을 썸네일이 받아지는 순간 페이드인 → 원본 받아지면 둘 다 페이드아웃. blur 썸네일도
15
+ * 네트워크라 받기 전 빈 화면이 생기던 갭을 skeleton 이 메운다. (CF 아니면 skeleton 단독)
16
+ * · 'skeleton': shimmer 단독.
17
+ * · 'image': loaderImage(브랜드 스피너/로고 등)를 가운데 표시.
18
+ * · loader: 임의 ReactNode 를 로더로 직접 주입(최우선). 무엇이든 커스텀 가능.
15
19
  * - 원본 onLoad 시 opacity 0→1 페이드인.
16
20
  *
17
21
  * next/image의 흔한 사용(fill, sizes, priority, width/height, style, loading, onError)과 호환.
package/dist/react.js CHANGED
@@ -63,6 +63,7 @@ function CfImage({
63
63
  ...rest
64
64
  }) {
65
65
  const [loaded, setLoaded] = useState(false);
66
+ const [blurReady, setBlurReady] = useState(false);
66
67
  const imgRef = useRef(null);
67
68
  useEffect(() => {
68
69
  if (imgRef.current?.complete) setLoaded(true);
@@ -76,9 +77,25 @@ function CfImage({
76
77
  else if (placeholder === "blur") mode = cf ? "blur" : "skeleton";
77
78
  else if (placeholder === "skeleton") mode = "skeleton";
78
79
  else mode = "none";
80
+ const usesSkeleton = mode === "skeleton" || mode === "blur";
81
+ const thumbSrc = mode === "blur" ? cfImageVariant(src, "thumbnail") : null;
79
82
  useEffect(() => {
80
- if (mode === "skeleton") ensureShimmerKeyframes();
81
- }, [mode]);
83
+ if (usesSkeleton) ensureShimmerKeyframes();
84
+ }, [usesSkeleton]);
85
+ useEffect(() => {
86
+ if (!thumbSrc || typeof Image === "undefined") return;
87
+ setBlurReady(false);
88
+ let cancelled = false;
89
+ const pre = new Image();
90
+ pre.onload = () => {
91
+ if (!cancelled) setBlurReady(true);
92
+ };
93
+ pre.src = thumbSrc;
94
+ if (pre.complete) setBlurReady(true);
95
+ return () => {
96
+ cancelled = true;
97
+ };
98
+ }, [thumbSrc]);
82
99
  const baseSrc = cf ? cfImageVariant(src, "md") : src;
83
100
  const srcSet = cf ? cfImageSrcSet(src) : void 0;
84
101
  const showLoader = mode !== "none" && !loaded && !priority;
@@ -98,47 +115,78 @@ function CfImage({
98
115
  position: "absolute",
99
116
  inset: 0,
100
117
  pointerEvents: "none",
101
- opacity: showLoader ? 1 : 0,
102
118
  transition: "opacity 0.4s ease"
103
119
  };
104
- let overlayStyle = overlayBase;
105
- let overlayChildren = null;
106
- if (mode === "blur") {
107
- overlayStyle = {
108
- ...overlayBase,
109
- backgroundImage: `url(${cfImageVariant(src, "thumbnail")})`,
110
- backgroundSize: "cover",
111
- backgroundPosition: "center",
112
- filter: "blur(16px)",
113
- transform: "scale(1.1)"
114
- };
115
- } else if (mode === "skeleton") {
116
- overlayStyle = {
117
- ...overlayBase,
118
- background: `linear-gradient(100deg,${loaderBackground} 30%,#f5f7f8 50%,${loaderBackground} 70%)`,
119
- backgroundSize: "200% 100%",
120
- animation: "rtCfImgShimmer 1.4s ease-in-out infinite"
121
- };
122
- } else if (mode === "image" || mode === "custom") {
123
- overlayStyle = {
124
- ...overlayBase,
125
- display: "flex",
126
- alignItems: "center",
127
- justifyContent: "center",
128
- background: loaderBackground
129
- };
130
- overlayChildren = mode === "image" ? /* @__PURE__ */ jsx(
131
- "img",
132
- {
133
- src: loaderImage,
134
- alt: "",
135
- "aria-hidden": true,
136
- style: { maxWidth: "44%", maxHeight: "44%", objectFit: "contain" }
137
- }
138
- ) : loader;
120
+ const layers = [];
121
+ if (usesSkeleton) {
122
+ const skeletonVisible = showLoader && !(mode === "blur" && blurReady);
123
+ layers.push(
124
+ /* @__PURE__ */ jsx(
125
+ "span",
126
+ {
127
+ "aria-hidden": true,
128
+ style: {
129
+ ...overlayBase,
130
+ opacity: skeletonVisible ? 1 : 0,
131
+ background: `linear-gradient(100deg,${loaderBackground} 30%,#f5f7f8 50%,${loaderBackground} 70%)`,
132
+ backgroundSize: "200% 100%",
133
+ animation: "rtCfImgShimmer 1.4s ease-in-out infinite"
134
+ }
135
+ },
136
+ "skeleton"
137
+ )
138
+ );
139
+ }
140
+ if (mode === "blur" && thumbSrc) {
141
+ layers.push(
142
+ /* @__PURE__ */ jsx(
143
+ "span",
144
+ {
145
+ "aria-hidden": true,
146
+ style: {
147
+ ...overlayBase,
148
+ opacity: showLoader && blurReady ? 1 : 0,
149
+ backgroundImage: `url(${thumbSrc})`,
150
+ backgroundSize: "cover",
151
+ backgroundPosition: "center",
152
+ filter: "blur(16px)",
153
+ transform: "scale(1.1)"
154
+ }
155
+ },
156
+ "blur"
157
+ )
158
+ );
159
+ }
160
+ if (mode === "image" || mode === "custom") {
161
+ layers.push(
162
+ /* @__PURE__ */ jsx(
163
+ "span",
164
+ {
165
+ "aria-hidden": true,
166
+ style: {
167
+ ...overlayBase,
168
+ opacity: showLoader ? 1 : 0,
169
+ display: "flex",
170
+ alignItems: "center",
171
+ justifyContent: "center",
172
+ background: loaderBackground
173
+ },
174
+ children: mode === "image" ? /* @__PURE__ */ jsx(
175
+ "img",
176
+ {
177
+ src: loaderImage,
178
+ alt: "",
179
+ "aria-hidden": true,
180
+ style: { maxWidth: "44%", maxHeight: "44%", objectFit: "contain" }
181
+ }
182
+ ) : loader
183
+ },
184
+ "loader"
185
+ )
186
+ );
139
187
  }
140
188
  return /* @__PURE__ */ jsxs("span", { className: wrapperClassName, style: wrapper, children: [
141
- mode !== "none" && /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: overlayStyle, children: overlayChildren }),
189
+ layers,
142
190
  /* @__PURE__ */ jsx(
143
191
  "img",
144
192
  {
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/CfImage.tsx","../src/variants.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ImgHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { cfImageVariant, cfImageSrcSet, isCfImageUrl } from \"../variants.js\";\n\n/**\n * RootTale 공유 CF Images 렌더 컴포넌트 — next/image 대체. (`@roottale/cms-media/react`)\n *\n * 왜: next/image는 Vercel 이미지 최적화 함수(/_next/image, Fluid Compute)를 태워 비용·지연 발생.\n * 콘텐츠 이미지는 Cloudflare Images(imagedelivery.net)에 두고 CF variant(sm/md/lg, 자동 webp/avif)\n * srcset으로 브라우저가 직접 받게 한다 → Vercel 컴퓨트 0. (ADR-0034 / docs/image-standard.md)\n *\n * - CF imagedelivery URL: srcset(sm/md/lg) + 기본 src=md. 비-CF URL(로컬 SVG·외부)은 변형 없이 그대로.\n * - placeholder(로더): 'blur'(CF thumbnail 흐림) | 'skeleton'(shimmer) | 'image'(loaderImage) | 'none'. 기본 'blur'.\n * · loaderImage: placeholder='image'일 때 로딩 중 가운데 표시할 커스텀 이미지 URL(브랜드 스피너/로고 등).\n * · loader: 임의 ReactNode를 로더로 직접 주입(최우선). 무엇이든 커스텀 가능.\n * - 원본 onLoad 시 opacity 0→1 페이드인.\n *\n * next/image의 흔한 사용(fill, sizes, priority, width/height, style, loading, onError)과 호환.\n */\n\nexport type CfImagePlaceholder = \"blur\" | \"skeleton\" | \"image\" | \"none\";\n\nexport interface CfImageProps\n extends Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"placeholder\"> {\n src: string;\n alt: string;\n /** next/image의 fill 처럼 부모(position:relative)를 채운다. */\n fill?: boolean;\n /** LCP 요소면 true → eager + fetchPriority high + 페이드 생략(즉시). */\n priority?: boolean;\n /** fill일 때 object-fit (기본 cover). */\n objectFit?: CSSProperties[\"objectFit\"];\n /** 로딩 중 표시(로더). 기본 'blur'(CF가 아니면 skeleton로 폴백). 'image'면 loaderImage 사용. */\n placeholder?: CfImagePlaceholder;\n /** placeholder='image'일 때 로딩 중 가운데 표시할 커스텀 이미지 URL. */\n loaderImage?: string;\n /** 임의 커스텀 로더(스피너 컴포넌트 등). 지정 시 placeholder/loaderImage보다 우선. */\n loader?: ReactNode;\n /** 로더 배경색(image/custom/skeleton 컨테이너). 기본 연한 중립. */\n loaderBackground?: string;\n /** 래퍼 클래스/스타일 (fill 레이아웃 제어용). */\n wrapperClassName?: string;\n wrapperStyle?: CSSProperties;\n}\n\nconst SHIMMER_STYLE_ID = \"roottale-cf-image-shimmer\";\nconst SHIMMER_KEYFRAMES = `@keyframes rtCfImgShimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`;\n\nfunction ensureShimmerKeyframes() {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(SHIMMER_STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = SHIMMER_STYLE_ID;\n el.textContent = SHIMMER_KEYFRAMES;\n document.head.appendChild(el);\n}\n\nexport function CfImage({\n src,\n alt,\n fill,\n priority,\n objectFit = \"cover\",\n placeholder = \"blur\",\n loaderImage,\n loader,\n loaderBackground = \"#eceff1\",\n sizes,\n width,\n height,\n loading,\n style,\n className,\n wrapperClassName,\n wrapperStyle,\n onLoad,\n onError,\n ...rest\n}: CfImageProps) {\n const [loaded, setLoaded] = useState(false);\n const imgRef = useRef<HTMLImageElement>(null);\n\n // 캐시 히트 시 onLoad가 안 올 수 있어 마운트 시 complete 체크.\n useEffect(() => {\n if (imgRef.current?.complete) setLoaded(true);\n }, []);\n\n const cf = isCfImageUrl(src);\n\n // 로더 모드 결정. 우선순위: 커스텀 loader 노드 > placeholder.\n const hasCustomLoader = loader != null;\n let mode: \"custom\" | \"image\" | \"blur\" | \"skeleton\" | \"none\";\n if (hasCustomLoader) mode = \"custom\";\n else if (placeholder === \"image\" && loaderImage) mode = \"image\";\n else if (placeholder === \"image\") mode = cf ? \"blur\" : \"skeleton\"; // 이미지 미지정 폴백\n else if (placeholder === \"blur\") mode = cf ? \"blur\" : \"skeleton\";\n else if (placeholder === \"skeleton\") mode = \"skeleton\";\n else mode = \"none\";\n\n useEffect(() => {\n if (mode === \"skeleton\") ensureShimmerKeyframes();\n }, [mode]);\n\n const baseSrc = cf ? cfImageVariant(src, \"md\") : src;\n const srcSet = cf ? cfImageSrcSet(src) : undefined;\n const showLoader = mode !== \"none\" && !loaded && !priority;\n\n const wrapper: CSSProperties = fill\n ? { position: \"absolute\", inset: 0, overflow: \"hidden\", ...wrapperStyle }\n : {\n position: \"relative\",\n display: \"inline-block\",\n overflow: \"hidden\",\n ...wrapperStyle,\n };\n\n const imgStyle: CSSProperties = {\n ...(fill\n ? { position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", objectFit }\n : { display: \"block\", width: width ? \"100%\" : undefined, height: \"auto\", objectFit }),\n opacity: loaded || priority ? 1 : 0,\n transition: \"opacity 0.4s ease\",\n ...style,\n };\n\n const overlayBase: CSSProperties = {\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n opacity: showLoader ? 1 : 0,\n transition: \"opacity 0.4s ease\",\n };\n\n let overlayStyle: CSSProperties = overlayBase;\n let overlayChildren: ReactNode = null;\n\n if (mode === \"blur\") {\n overlayStyle = {\n ...overlayBase,\n backgroundImage: `url(${cfImageVariant(src, \"thumbnail\")})`,\n backgroundSize: \"cover\",\n backgroundPosition: \"center\",\n filter: \"blur(16px)\",\n transform: \"scale(1.1)\",\n };\n } else if (mode === \"skeleton\") {\n overlayStyle = {\n ...overlayBase,\n background: `linear-gradient(100deg,${loaderBackground} 30%,#f5f7f8 50%,${loaderBackground} 70%)`,\n backgroundSize: \"200% 100%\",\n animation: \"rtCfImgShimmer 1.4s ease-in-out infinite\",\n };\n } else if (mode === \"image\" || mode === \"custom\") {\n overlayStyle = {\n ...overlayBase,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: loaderBackground,\n };\n overlayChildren =\n mode === \"image\" ? (\n <img\n src={loaderImage}\n alt=\"\"\n aria-hidden\n style={{ maxWidth: \"44%\", maxHeight: \"44%\", objectFit: \"contain\" }}\n />\n ) : (\n loader\n );\n }\n\n return (\n <span className={wrapperClassName} style={wrapper}>\n {mode !== \"none\" && (\n <span aria-hidden style={overlayStyle}>\n {overlayChildren}\n </span>\n )}\n <img\n {...rest}\n ref={imgRef}\n src={baseSrc}\n srcSet={srcSet}\n sizes={sizes}\n alt={alt}\n width={fill ? undefined : width}\n height={fill ? undefined : height}\n loading={loading ?? (priority ? \"eager\" : \"lazy\")}\n fetchPriority={priority ? \"high\" : undefined}\n decoding=\"async\"\n className={className}\n style={imgStyle}\n onLoad={(e) => {\n setLoaded(true);\n onLoad?.(e);\n }}\n onError={onError}\n />\n </span>\n );\n}\n\nexport default CfImage;\n","// ADR-0034 §7.1.1 test #4 — CF Images variant 4종 URL 생성기.\n//\n// Phase 1 = thumbnail/sm/md/lg. CF Images delivery URL pattern:\n// https://imagedelivery.net/<account-hash>/<image-id>/<variant>\n\nexport type ImageVariant = \"thumbnail\" | \"sm\" | \"md\" | \"lg\";\n\nexport const VARIANT_NAMES: readonly ImageVariant[] = [\n \"thumbnail\",\n \"sm\",\n \"md\",\n \"lg\",\n] as const;\n\nexport interface VariantSizes {\n thumbnail: { width: 96; height: 96 };\n sm: { width: 320; height: 320 };\n md: { width: 768; height: 768 };\n lg: { width: 1600; height: 1600 };\n}\n\nexport const VARIANT_SIZES: VariantSizes = {\n thumbnail: { width: 96, height: 96 },\n sm: { width: 320, height: 320 },\n md: { width: 768, height: 768 },\n lg: { width: 1600, height: 1600 },\n};\n\nexport interface CfImagesConfig {\n accountHash: string;\n}\n\n/** CF Images delivery URL 생성. */\nexport function variantUrl(\n config: CfImagesConfig,\n imageId: string,\n variant: ImageVariant,\n): string {\n return `https://imagedelivery.net/${config.accountHash}/${imageId}/${variant}`;\n}\n\n/** 모든 variant 의 URL map. */\nexport function allVariantUrls(\n config: CfImagesConfig,\n imageId: string,\n): Record<ImageVariant, string> {\n return Object.fromEntries(\n VARIANT_NAMES.map((v) => [v, variantUrl(config, imageId, v)]),\n ) as Record<ImageVariant, string>;\n}\n\n// ── delivery URL 의 variant 세그먼트 교체 헬퍼 (account-hash/image-id 를 몰라도\n// 이미 받은 full delivery URL 만으로 variant 를 바꾼다. CfImage 렌더용.) ──\n\nconst CF_HOST = \"imagedelivery.net\";\nconst KNOWN_VARIANT_SUFFIX =\n /\\/(public|lg|md|sm|thumbnail|jhwebp|jhfull|jhthumb|jhmobile)$/;\n\n/** imagedelivery delivery URL 여부. */\nexport function isCfImageUrl(url: string): boolean {\n return typeof url === \"string\" && url.includes(CF_HOST);\n}\n\n/**\n * delivery URL 의 끝 variant 세그먼트를 교체. 세그먼트가 없으면 덧붙인다.\n * CF URL 이 아니면 원본 그대로 반환(외부/로컬 URL 안전 통과).\n */\nexport function cfImageVariant(\n url: string,\n variant: ImageVariant | \"public\",\n): string {\n if (!isCfImageUrl(url)) return url;\n return KNOWN_VARIANT_SUFFIX.test(url)\n ? url.replace(KNOWN_VARIANT_SUFFIX, `/${variant}`)\n : `${url}/${variant}`;\n}\n\n/** sm/md/lg srcset 문자열(CF URL 일 때만, 아니면 undefined). 폭은 VARIANT_SIZES 기준. */\nexport function cfImageSrcSet(\n url: string,\n variants: readonly ImageVariant[] = [\"sm\", \"md\", \"lg\"],\n): string | undefined {\n if (!isCfImageUrl(url)) return undefined;\n return variants\n .map((v) => `${cfImageVariant(url, v)} ${VARIANT_SIZES[v].width}w`)\n .join(\", \");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/react/CfImage.tsx","../src/variants.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type ImgHTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { cfImageVariant, cfImageSrcSet, isCfImageUrl } from \"../variants.js\";\n\n/**\n * RootTale 공유 CF Images 렌더 컴포넌트 — next/image 대체. (`@roottale/cms-media/react`)\n *\n * 왜: next/image는 Vercel 이미지 최적화 함수(/_next/image, Fluid Compute)를 태워 비용·지연 발생.\n * 콘텐츠 이미지는 Cloudflare Images(imagedelivery.net)에 두고 CF variant(sm/md/lg, 자동 webp/avif)\n * srcset으로 브라우저가 직접 받게 한다 → Vercel 컴퓨트 0. (ADR-0034 / docs/image-standard.md)\n *\n * - CF imagedelivery URL: srcset(sm/md/lg) + 기본 src=md. 비-CF URL(로컬 SVG·외부)은 변형 없이 그대로.\n * - placeholder(로더): 'blur' | 'skeleton' | 'image' | 'none'. 기본 'blur'.\n * · 'blur'(CF): skeleton shimmer 를 즉시 깔고(=마운트 순간 로딩 인지) 그 위로 CF thumbnail\n * 흐림을 썸네일이 받아지는 순간 페이드인 → 원본 받아지면 둘 다 페이드아웃. blur 썸네일도\n * 네트워크라 받기 전 빈 화면이 생기던 갭을 skeleton 이 메운다. (CF 아니면 skeleton 단독)\n * · 'skeleton': shimmer 단독.\n * · 'image': loaderImage(브랜드 스피너/로고 등)를 가운데 표시.\n * · loader: 임의 ReactNode 를 로더로 직접 주입(최우선). 무엇이든 커스텀 가능.\n * - 원본 onLoad 시 opacity 0→1 페이드인.\n *\n * next/image의 흔한 사용(fill, sizes, priority, width/height, style, loading, onError)과 호환.\n */\n\nexport type CfImagePlaceholder = \"blur\" | \"skeleton\" | \"image\" | \"none\";\n\nexport interface CfImageProps\n extends Omit<ImgHTMLAttributes<HTMLImageElement>, \"src\" | \"placeholder\"> {\n src: string;\n alt: string;\n /** next/image의 fill 처럼 부모(position:relative)를 채운다. */\n fill?: boolean;\n /** LCP 요소면 true → eager + fetchPriority high + 페이드 생략(즉시). */\n priority?: boolean;\n /** fill일 때 object-fit (기본 cover). */\n objectFit?: CSSProperties[\"objectFit\"];\n /** 로딩 중 표시(로더). 기본 'blur'(CF가 아니면 skeleton로 폴백). 'image'면 loaderImage 사용. */\n placeholder?: CfImagePlaceholder;\n /** placeholder='image'일 때 로딩 중 가운데 표시할 커스텀 이미지 URL. */\n loaderImage?: string;\n /** 임의 커스텀 로더(스피너 컴포넌트 등). 지정 시 placeholder/loaderImage보다 우선. */\n loader?: ReactNode;\n /** 로더 배경색(image/custom/skeleton 컨테이너). 기본 연한 중립. */\n loaderBackground?: string;\n /** 래퍼 클래스/스타일 (fill 레이아웃 제어용). */\n wrapperClassName?: string;\n wrapperStyle?: CSSProperties;\n}\n\nconst SHIMMER_STYLE_ID = \"roottale-cf-image-shimmer\";\nconst SHIMMER_KEYFRAMES = `@keyframes rtCfImgShimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`;\n\nfunction ensureShimmerKeyframes() {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(SHIMMER_STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = SHIMMER_STYLE_ID;\n el.textContent = SHIMMER_KEYFRAMES;\n document.head.appendChild(el);\n}\n\nexport function CfImage({\n src,\n alt,\n fill,\n priority,\n objectFit = \"cover\",\n placeholder = \"blur\",\n loaderImage,\n loader,\n loaderBackground = \"#eceff1\",\n sizes,\n width,\n height,\n loading,\n style,\n className,\n wrapperClassName,\n wrapperStyle,\n onLoad,\n onError,\n ...rest\n}: CfImageProps) {\n const [loaded, setLoaded] = useState(false);\n const [blurReady, setBlurReady] = useState(false);\n const imgRef = useRef<HTMLImageElement>(null);\n\n // 캐시 히트 시 onLoad가 안 올 수 있어 마운트 시 complete 체크.\n useEffect(() => {\n if (imgRef.current?.complete) setLoaded(true);\n }, []);\n\n const cf = isCfImageUrl(src);\n\n // 로더 모드 결정. 우선순위: 커스텀 loader 노드 > placeholder.\n const hasCustomLoader = loader != null;\n let mode: \"custom\" | \"image\" | \"blur\" | \"skeleton\" | \"none\";\n if (hasCustomLoader) mode = \"custom\";\n else if (placeholder === \"image\" && loaderImage) mode = \"image\";\n else if (placeholder === \"image\") mode = cf ? \"blur\" : \"skeleton\"; // 이미지 미지정 폴백\n else if (placeholder === \"blur\") mode = cf ? \"blur\" : \"skeleton\";\n else if (placeholder === \"skeleton\") mode = \"skeleton\";\n else mode = \"none\";\n\n // blur 모드는 skeleton 을 base 로 깐다(썸네일 받기 전 갭 메움). skeleton 단독도 포함.\n const usesSkeleton = mode === \"skeleton\" || mode === \"blur\";\n const thumbSrc = mode === \"blur\" ? cfImageVariant(src, \"thumbnail\") : null;\n\n useEffect(() => {\n if (usesSkeleton) ensureShimmerKeyframes();\n }, [usesSkeleton]);\n\n // blur 썸네일은 background-image 라 onLoad 가 없다 → 프리로드로 준비 시점 추적.\n // 준비되기 전엔 skeleton 만 보이고, 준비되면 blur 가 그 위로 페이드인.\n useEffect(() => {\n if (!thumbSrc || typeof Image === \"undefined\") return;\n setBlurReady(false);\n let cancelled = false;\n const pre = new Image();\n pre.onload = () => {\n if (!cancelled) setBlurReady(true);\n };\n pre.src = thumbSrc;\n if (pre.complete) setBlurReady(true); // 캐시 히트\n return () => {\n cancelled = true;\n };\n }, [thumbSrc]);\n\n const baseSrc = cf ? cfImageVariant(src, \"md\") : src;\n const srcSet = cf ? cfImageSrcSet(src) : undefined;\n const showLoader = mode !== \"none\" && !loaded && !priority;\n\n const wrapper: CSSProperties = fill\n ? { position: \"absolute\", inset: 0, overflow: \"hidden\", ...wrapperStyle }\n : {\n position: \"relative\",\n display: \"inline-block\",\n overflow: \"hidden\",\n ...wrapperStyle,\n };\n\n const imgStyle: CSSProperties = {\n ...(fill\n ? { position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", objectFit }\n : { display: \"block\", width: width ? \"100%\" : undefined, height: \"auto\", objectFit }),\n opacity: loaded || priority ? 1 : 0,\n transition: \"opacity 0.4s ease\",\n ...style,\n };\n\n const overlayBase: CSSProperties = {\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n transition: \"opacity 0.4s ease\",\n };\n\n // 레이어 합성. 아래→위 순서로 쌓는다.\n // · skeleton: 즉시 표시(로딩 인지). blur 모드면 그 위로 blur 가 덮는다.\n // · blur: 썸네일 준비(blurReady) 시 skeleton 위로 페이드인.\n // · image/custom: 단독.\n const layers: ReactNode[] = [];\n\n if (usesSkeleton) {\n // blur 모드에서 썸네일이 준비되면 skeleton 은 가려지므로 미리 페이드아웃.\n const skeletonVisible =\n showLoader && !(mode === \"blur\" && blurReady);\n layers.push(\n <span\n key=\"skeleton\"\n aria-hidden\n style={{\n ...overlayBase,\n opacity: skeletonVisible ? 1 : 0,\n background: `linear-gradient(100deg,${loaderBackground} 30%,#f5f7f8 50%,${loaderBackground} 70%)`,\n backgroundSize: \"200% 100%\",\n animation: \"rtCfImgShimmer 1.4s ease-in-out infinite\",\n }}\n />,\n );\n }\n\n if (mode === \"blur\" && thumbSrc) {\n layers.push(\n <span\n key=\"blur\"\n aria-hidden\n style={{\n ...overlayBase,\n opacity: showLoader && blurReady ? 1 : 0,\n backgroundImage: `url(${thumbSrc})`,\n backgroundSize: \"cover\",\n backgroundPosition: \"center\",\n filter: \"blur(16px)\",\n transform: \"scale(1.1)\",\n }}\n />,\n );\n }\n\n if (mode === \"image\" || mode === \"custom\") {\n layers.push(\n <span\n key=\"loader\"\n aria-hidden\n style={{\n ...overlayBase,\n opacity: showLoader ? 1 : 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n background: loaderBackground,\n }}\n >\n {mode === \"image\" ? (\n <img\n src={loaderImage}\n alt=\"\"\n aria-hidden\n style={{ maxWidth: \"44%\", maxHeight: \"44%\", objectFit: \"contain\" }}\n />\n ) : (\n loader\n )}\n </span>,\n );\n }\n\n return (\n <span className={wrapperClassName} style={wrapper}>\n {layers}\n <img\n {...rest}\n ref={imgRef}\n src={baseSrc}\n srcSet={srcSet}\n sizes={sizes}\n alt={alt}\n width={fill ? undefined : width}\n height={fill ? undefined : height}\n loading={loading ?? (priority ? \"eager\" : \"lazy\")}\n fetchPriority={priority ? \"high\" : undefined}\n decoding=\"async\"\n className={className}\n style={imgStyle}\n onLoad={(e) => {\n setLoaded(true);\n onLoad?.(e);\n }}\n onError={onError}\n />\n </span>\n );\n}\n\nexport default CfImage;\n","// ADR-0034 §7.1.1 test #4 — CF Images variant 4종 URL 생성기.\n//\n// Phase 1 = thumbnail/sm/md/lg. CF Images delivery URL pattern:\n// https://imagedelivery.net/<account-hash>/<image-id>/<variant>\n\nexport type ImageVariant = \"thumbnail\" | \"sm\" | \"md\" | \"lg\";\n\nexport const VARIANT_NAMES: readonly ImageVariant[] = [\n \"thumbnail\",\n \"sm\",\n \"md\",\n \"lg\",\n] as const;\n\nexport interface VariantSizes {\n thumbnail: { width: 96; height: 96 };\n sm: { width: 320; height: 320 };\n md: { width: 768; height: 768 };\n lg: { width: 1600; height: 1600 };\n}\n\nexport const VARIANT_SIZES: VariantSizes = {\n thumbnail: { width: 96, height: 96 },\n sm: { width: 320, height: 320 },\n md: { width: 768, height: 768 },\n lg: { width: 1600, height: 1600 },\n};\n\nexport interface CfImagesConfig {\n accountHash: string;\n}\n\n/** CF Images delivery URL 생성. */\nexport function variantUrl(\n config: CfImagesConfig,\n imageId: string,\n variant: ImageVariant,\n): string {\n return `https://imagedelivery.net/${config.accountHash}/${imageId}/${variant}`;\n}\n\n/** 모든 variant 의 URL map. */\nexport function allVariantUrls(\n config: CfImagesConfig,\n imageId: string,\n): Record<ImageVariant, string> {\n return Object.fromEntries(\n VARIANT_NAMES.map((v) => [v, variantUrl(config, imageId, v)]),\n ) as Record<ImageVariant, string>;\n}\n\n// ── delivery URL 의 variant 세그먼트 교체 헬퍼 (account-hash/image-id 를 몰라도\n// 이미 받은 full delivery URL 만으로 variant 를 바꾼다. CfImage 렌더용.) ──\n\nconst CF_HOST = \"imagedelivery.net\";\nconst KNOWN_VARIANT_SUFFIX =\n /\\/(public|lg|md|sm|thumbnail|jhwebp|jhfull|jhthumb|jhmobile)$/;\n\n/** imagedelivery delivery URL 여부. */\nexport function isCfImageUrl(url: string): boolean {\n return typeof url === \"string\" && url.includes(CF_HOST);\n}\n\n/**\n * delivery URL 의 끝 variant 세그먼트를 교체. 세그먼트가 없으면 덧붙인다.\n * CF URL 이 아니면 원본 그대로 반환(외부/로컬 URL 안전 통과).\n */\nexport function cfImageVariant(\n url: string,\n variant: ImageVariant | \"public\",\n): string {\n if (!isCfImageUrl(url)) return url;\n return KNOWN_VARIANT_SUFFIX.test(url)\n ? url.replace(KNOWN_VARIANT_SUFFIX, `/${variant}`)\n : `${url}/${variant}`;\n}\n\n/** sm/md/lg srcset 문자열(CF URL 일 때만, 아니면 undefined). 폭은 VARIANT_SIZES 기준. */\nexport function cfImageSrcSet(\n url: string,\n variants: readonly ImageVariant[] = [\"sm\", \"md\", \"lg\"],\n): string | undefined {\n if (!isCfImageUrl(url)) return undefined;\n return variants\n .map((v) => `${cfImageVariant(url, v)} ${VARIANT_SIZES[v].width}w`)\n .join(\", \");\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roottale/cms-media",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "description": "RootTale 미디어 — R2 원본 보관 + CF Images variants + 한글 파일명 정규화 + CfImage 렌더 컴포넌트(@roottale/cms-media/react). ADR-0034 §3 / docs/image-standard.md.",
6
6
  "main": "./dist/index.js",
@@ -22,12 +22,6 @@
22
22
  "dist/",
23
23
  "README.md"
24
24
  ],
25
- "scripts": {
26
- "build": "tsup",
27
- "prepack": "pnpm build",
28
- "type-check": "tsc --noEmit",
29
- "test": "vitest run --passWithNoTests"
30
- },
31
25
  "dependencies": {},
32
26
  "peerDependencies": {
33
27
  "react": "^18.3.0 || ^19.0.0",
@@ -68,5 +62,10 @@
68
62
  "type": "git",
69
63
  "url": "https://github.com/RootTale/roottale-platform.git",
70
64
  "directory": "packages/cms-media"
65
+ },
66
+ "scripts": {
67
+ "build": "tsup",
68
+ "type-check": "tsc --noEmit",
69
+ "test": "vitest run --passWithNoTests"
71
70
  }
72
- }
71
+ }