Skip to content

Commit 0e7a9b3

Browse files
committed
feat: Image 사이즈 처리
1 parent 092133e commit 0e7a9b3

File tree

9 files changed

+331
-15
lines changed

9 files changed

+331
-15
lines changed

apps/storybook/src/sample-data/notionBlocks.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@
6969
"type": "file",
7070
"file": {
7171
"url": "https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fcd7314a5-d906-43b0-81e7-42eff82c02a3%2F566f127b-9e73-491d-bee6-5afd075653a2%2Fimage.png?table=block&id=17f9c6bf-2b17-8016-bf79-dc83ab79fb78&cache=v2",
72-
"expiry_time": "2025-03-19T16:01:15.810Z"
72+
"expiry_time": "2025-03-20T12:10:05.408Z"
73+
},
74+
"format": {
75+
"block_width": 2998,
76+
"block_height": 1468,
77+
"block_aspect_ratio": 2.042234332425068
7378
}
7479
}
7580
},
@@ -4561,7 +4566,12 @@
45614566
"type": "file",
45624567
"file": {
45634568
"url": "https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2Fcd7314a5-d906-43b0-81e7-42eff82c02a3%2F10d9f59d-d3e2-429a-ad72-c8b15b1f536b%2Fimage.png?table=block&id=1809c6bf-2b17-8096-aaf4-d89c3850bed0&cache=v2",
4564-
"expiry_time": "2025-03-19T16:01:16.374Z"
4569+
"expiry_time": "2025-03-20T12:10:05.632Z"
4570+
},
4571+
"format": {
4572+
"block_width": 721,
4573+
"block_height": 473,
4574+
"block_aspect_ratio": 1.5243128964059196
45654575
}
45664576
}
45674577
},

packages/notion-to-jsx/src/components/Renderer/components/Block/BlockRenderer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const BlockRenderer: React.FC<Props> = ({ block, onFocus, index }) => {
7070
src={block.image.file?.url || block.image.external?.url}
7171
alt={block.image.caption?.[0]?.plain_text || ''}
7272
caption={block.image.caption}
73+
format={block.image.format}
7374
/>
7475
</figure>
7576
);

packages/notion-to-jsx/src/components/Renderer/components/Image/Image.tsx

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,33 @@ import React, { useState, useEffect } from 'react';
22
import { MemoizedRichText } from '../MemoizedComponents';
33
import {
44
imageContainer,
5+
imageWrapper,
56
styledImage,
67
placeholder,
78
caption,
89
} from './styles.css';
910
import { RichTextItem } from '../RichText/RichTexts';
1011

12+
export interface ImageFormat {
13+
block_width?: number;
14+
block_height?: number;
15+
block_aspect_ratio?: number;
16+
}
17+
1118
export interface ImageProps {
1219
src: string;
1320
alt: string;
1421
caption?: RichTextItem[];
1522
priority?: boolean;
23+
format?: ImageFormat;
1624
}
1725

1826
const Image: React.FC<ImageProps> = ({
1927
src,
2028
alt,
2129
caption: imageCaption,
2230
priority = false,
31+
format,
2332
}) => {
2433
const [isLoaded, setIsLoaded] = useState(false);
2534
const [error, setError] = useState(false);
@@ -31,16 +40,124 @@ const Image: React.FC<ImageProps> = ({
3140

3241
return (
3342
<figure className={imageContainer}>
34-
<div>
35-
{!isLoaded && !error && <div className={placeholder}>Loading...</div>}
36-
{error && <div className={placeholder}>Failed to load image</div>}
43+
<div
44+
className={imageWrapper({
45+
hasWidth: !!format?.block_width,
46+
})}
47+
style={
48+
format?.block_width
49+
? {
50+
width:
51+
format.block_width > 900 ? '100%' : `${format.block_width}px`,
52+
}
53+
: undefined
54+
}
55+
>
56+
{!isLoaded && !error && (
57+
<div
58+
className={placeholder}
59+
style={{
60+
width: format?.block_width
61+
? format.block_width > 900
62+
? '100%'
63+
: `${format.block_width}px`
64+
: '100%',
65+
aspectRatio: format?.block_aspect_ratio
66+
? `${format.block_aspect_ratio}`
67+
: 'auto',
68+
}}
69+
>
70+
<svg
71+
width="38"
72+
height="38"
73+
viewBox="0 0 38 38"
74+
xmlns="http://www.w3.org/2000/svg"
75+
stroke="#888"
76+
>
77+
<g fill="none" fillRule="evenodd">
78+
<g transform="translate(1 1)" strokeWidth="2">
79+
<circle strokeOpacity=".5" cx="18" cy="18" r="18" />
80+
<path d="M36 18c0-9.94-8.06-18-18-18">
81+
<animateTransform
82+
attributeName="transform"
83+
type="rotate"
84+
from="0 18 18"
85+
to="360 18 18"
86+
dur="1s"
87+
repeatCount="indefinite"
88+
/>
89+
</path>
90+
</g>
91+
</g>
92+
</svg>
93+
</div>
94+
)}
95+
{error && (
96+
<div
97+
className={placeholder}
98+
style={{
99+
width: format?.block_width
100+
? format.block_width > 900
101+
? '100%'
102+
: `${format.block_width}px`
103+
: '100%',
104+
aspectRatio: format?.block_aspect_ratio
105+
? `${format.block_aspect_ratio}`
106+
: 'auto',
107+
}}
108+
>
109+
<svg
110+
width="48"
111+
height="48"
112+
viewBox="0 0 24 24"
113+
fill="none"
114+
xmlns="http://www.w3.org/2000/svg"
115+
>
116+
<path
117+
d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
118+
stroke="#FF6B6B"
119+
strokeWidth="2"
120+
strokeLinecap="round"
121+
strokeLinejoin="round"
122+
/>
123+
<path
124+
d="M15 9L9 15"
125+
stroke="#FF6B6B"
126+
strokeWidth="2"
127+
strokeLinecap="round"
128+
strokeLinejoin="round"
129+
/>
130+
<path
131+
d="M9 9L15 15"
132+
stroke="#FF6B6B"
133+
strokeWidth="2"
134+
strokeLinecap="round"
135+
strokeLinejoin="round"
136+
/>
137+
</svg>
138+
<div style={{ width: '10px' }} />
139+
<p style={{ color: '#FF6B6B', fontSize: '14px' }}>
140+
Image Load failed
141+
</p>
142+
</div>
143+
)}
37144
<img
38-
className={styledImage({ loaded: isLoaded })}
145+
className={styledImage({
146+
loaded: isLoaded,
147+
hasAspectRatio: !!format?.block_aspect_ratio,
148+
})}
39149
src={src}
40150
alt={alt}
41151
loading={priority ? 'eager' : 'lazy'}
42152
onLoad={() => setIsLoaded(true)}
43153
onError={() => setError(true)}
154+
width={format?.block_width}
155+
height={format?.block_height}
156+
style={
157+
format?.block_aspect_ratio
158+
? { aspectRatio: `${format.block_aspect_ratio}` }
159+
: undefined
160+
}
44161
/>
45162
</div>
46163
{imageCaption && imageCaption.length > 0 && (

packages/notion-to-jsx/src/components/Renderer/components/Image/styles.css.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,40 @@
11
import { style } from '@vanilla-extract/css';
22
import { recipe } from '@vanilla-extract/recipes';
3+
import { createVar, fallbackVar } from '@vanilla-extract/css';
34
import { vars } from '../../../../styles/theme.css';
45

56
export const imageContainer = style({
67
position: 'relative',
78
width: '100%',
8-
background: vars.colors.code.background,
99
borderRadius: vars.borderRadius.md,
1010
overflow: 'hidden',
11+
margin: '0 auto',
12+
display: 'flex',
13+
flexDirection: 'column',
14+
alignItems: 'center',
15+
});
16+
17+
// CSS 변수 생성
18+
export const imageWidthVar = createVar();
19+
export const imageAspectRatioVar = createVar();
20+
21+
export const imageWrapper = recipe({
22+
base: {
23+
position: 'relative',
24+
maxWidth: '100%',
25+
width: fallbackVar(imageWidthVar, '100%'),
26+
},
27+
variants: {
28+
hasWidth: {
29+
true: {},
30+
false: {
31+
width: '100%',
32+
},
33+
},
34+
},
35+
defaultVariants: {
36+
hasWidth: false,
37+
},
1138
});
1239

1340
export const styledImage = recipe({
@@ -16,6 +43,8 @@ export const styledImage = recipe({
1643
height: 'auto',
1744
display: 'block',
1845
transition: 'opacity 0.3s ease',
46+
objectFit: 'contain',
47+
aspectRatio: fallbackVar(imageAspectRatioVar, 'auto'),
1948
},
2049
variants: {
2150
loaded: {
@@ -26,23 +55,26 @@ export const styledImage = recipe({
2655
opacity: 0,
2756
},
2857
},
58+
hasAspectRatio: {
59+
true: {},
60+
false: {},
61+
},
2962
},
3063
defaultVariants: {
3164
loaded: false,
65+
hasAspectRatio: false,
3266
},
3367
});
3468

3569
export const placeholder = style({
36-
position: 'absolute',
37-
top: 0,
38-
left: 0,
39-
right: 0,
40-
bottom: 0,
70+
// position: 'absolute',
71+
// top: 0,
72+
// left: 0,
73+
// right: 0,
74+
// bottom: 0,
4175
display: 'flex',
4276
alignItems: 'center',
4377
justifyContent: 'center',
44-
background: vars.colors.code.background,
45-
color: vars.colors.secondary,
4678
});
4779

4880
export const caption = style({

packages/notion-to-utils/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
"@repo/eslint-config": "workspace:*",
4040
"@repo/typescript-config": "workspace:*",
4141
"@types/lqip-modern": "^1.1.7",
42+
"@types/probe-image-size": "^7.2.5",
4243
"@vitest/coverage-v8": "2.1.2",
4344
"jsdom": "^26.0.0",
4445
"typescript": "^5.6.3",
4546
"vitest": "^2.1.2"
4647
},
4748
"dependencies": {
4849
"@notionhq/client": "^2.2.15",
49-
"lqip-modern": "^2.2.1"
50+
"lqip-modern": "^2.2.1",
51+
"probe-image-size": "^7.2.3"
5052
}
5153
}

packages/notion-to-utils/src/client/getPageBlocks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PartialBlockObjectResponse,
66
} from '@notionhq/client/build/src/api-endpoints';
77
import { formatNotionImageUrl } from './formatNotionImageUrl';
8+
import { addMetadataToImageBlock } from '../utils';
89

910
// 블록 타입 정의
1011
export type NotionBlock = BlockObjectResponse | PartialBlockObjectResponse;
@@ -80,6 +81,13 @@ async function fetchBlockChildren(
8081
block.id
8182
);
8283
}
84+
85+
// 이미지 메타데이터 추출 및 추가
86+
try {
87+
await addMetadataToImageBlock(imageBlock);
88+
} catch (metadataError) {
89+
console.error('이미지 메타데이터 추출 중 오류:', metadataError);
90+
}
8391
}
8492
}
8593

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import probe from 'probe-image-size';
2+
3+
/**
4+
* 이미지 URL에서 메타데이터(너비, 높이)를 추출합니다.
5+
* @param url 이미지 URL
6+
* @returns 이미지 메타데이터 (너비, 높이, 종횡비) 또는 null (추출 실패 시)
7+
*/
8+
export async function getImageMetadata(url: string): Promise<{
9+
width: number;
10+
height: number;
11+
aspectRatio: number;
12+
} | null> {
13+
try {
14+
// URL이 Notion 이미지 URL인 경우 특별한 처리가 필요할 수 있음
15+
const result = await probe(url);
16+
17+
return {
18+
width: result.width,
19+
height: result.height,
20+
aspectRatio: result.height > 0 ? result.width / result.height : 1,
21+
};
22+
} catch (error) {
23+
console.error('이미지 메타데이터 추출 실패:', error);
24+
return null;
25+
}
26+
}
27+
28+
/**
29+
* Notion 이미지 블록에 메타데이터를 추가합니다.
30+
* @param block Notion 이미지 블록
31+
* @returns 메타데이터가 추가된 이미지 블록
32+
*/
33+
export async function addMetadataToImageBlock(block: any): Promise<any> {
34+
if (block.type !== 'image') {
35+
return block;
36+
}
37+
38+
try {
39+
// 이미지 URL 가져오기
40+
let imageUrl = '';
41+
if (block.image.file && block.image.file.url) {
42+
imageUrl = block.image.file.url;
43+
} else if (block.image.external && block.image.external.url) {
44+
imageUrl = block.image.external.url;
45+
} else {
46+
return block; // URL이 없는 경우 원본 블록 반환
47+
}
48+
49+
// 이미지 메타데이터 추출
50+
const metadata = await getImageMetadata(imageUrl);
51+
if (!metadata) {
52+
return block; // 메타데이터 추출 실패 시 원본 블록 반환
53+
}
54+
55+
// 메타데이터 추가
56+
if (!block.image.format) {
57+
block.image.format = {};
58+
}
59+
60+
block.image.format.block_width = metadata.width;
61+
block.image.format.block_height = metadata.height;
62+
block.image.format.block_aspect_ratio = metadata.aspectRatio;
63+
64+
return block;
65+
} catch (error) {
66+
console.error('이미지 블록 메타데이터 추가 실패:', error);
67+
return block;
68+
}
69+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './makePreviewImage';
2+
export * from './getImageMetadata';

0 commit comments

Comments
 (0)