@lumir-company/editor 0.4.0 โ†’ 0.4.3

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
@@ -1,6 +1,6 @@
1
1
  # LumirEditor
2
2
 
3
- ๐Ÿ–ผ๏ธ **์ด๋ฏธ์ง€ ์ „์šฉ** BlockNote ๊ธฐ๋ฐ˜ Rich Text ์—๋””ํ„ฐ
3
+ **์ด๋ฏธ์ง€ ์ „์šฉ** BlockNote ๊ธฐ๋ฐ˜ Rich Text ์—๋””ํ„ฐ
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@lumir-company/editor.svg)](https://www.npmjs.com/package/@lumir-company/editor)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -9,32 +9,35 @@
9
9
 
10
10
  ---
11
11
 
12
- ## ๐Ÿ“‹ ๋ชฉ์ฐจ
12
+ ## ๋ชฉ์ฐจ
13
13
 
14
- - [ํŠน์ง•](#-ํŠน์ง•)
15
- - [๋น ๋ฅธ ์‹œ์ž‘](#-๋น ๋ฅธ-์‹œ์ž‘)
16
- - [์ด๋ฏธ์ง€ ์—…๋กœ๋“œ](#-์ด๋ฏธ์ง€-์—…๋กœ๋“œ)
14
+ - [ํŠน์ง•](#ํŠน์ง•)
15
+ - [๋น ๋ฅธ ์‹œ์ž‘](#๋น ๋ฅธ-์‹œ์ž‘)
16
+ - [์ด๋ฏธ์ง€ ์—…๋กœ๋“œ](#์ด๋ฏธ์ง€-์—…๋กœ๋“œ)
17
17
  - [S3 ์—…๋กœ๋“œ ์„ค์ •](#1-s3-์—…๋กœ๋“œ-๊ถŒ์žฅ)
18
- - [ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•](#-ํŒŒ์ผ๋ช…-์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
18
+ - [ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•](#ํŒŒ์ผ๋ช…-์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
19
19
  - [์ปค์Šคํ…€ ์—…๋กœ๋”](#2-์ปค์Šคํ…€-์—…๋กœ๋”)
20
- - [Props API](#-props-api)
21
- - [์‚ฌ์šฉ ์˜ˆ์ œ](#-์‚ฌ์šฉ-์˜ˆ์ œ)
22
- - [์Šคํƒ€์ผ๋ง](#-์Šคํƒ€์ผ๋ง)
23
- - [ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…](#-ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…)
20
+ - [์ด๋ฏธ์ง€ ์‚ญ์ œ](#์ด๋ฏธ์ง€-์‚ญ์ œ)
21
+ - [HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ](#html-๋ฏธ๋ฆฌ๋ณด๊ธฐ)
22
+ - [Props API](#props-api)
23
+ - [์‚ฌ์šฉ ์˜ˆ์ œ](#์‚ฌ์šฉ-์˜ˆ์ œ)
24
+ - [์Šคํƒ€์ผ๋ง](#์Šคํƒ€์ผ๋ง)
25
+ - [ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…](#ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…)
24
26
 
25
27
  ---
26
28
 
27
- ## โœจ ํŠน์ง•
29
+ ## ํŠน์ง•
28
30
 
29
- | ํŠน์ง• | ์„ค๋ช… |
30
- | -------------------------- | ------------------------------------------------------ |
31
- | ๐Ÿ–ผ๏ธ **์ด๋ฏธ์ง€ ์ „์šฉ** | ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ/๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ๋งŒ ์ง€์› (๋น„๋””์˜ค/์˜ค๋””์˜ค ์ œ๊ฑฐ) |
32
- | โ˜๏ธ **S3 ์—ฐ๋™** | Presigned URL ๊ธฐ๋ฐ˜ S3 ์—…๋กœ๋“œ ๋‚ด์žฅ |
33
- | ๐Ÿท๏ธ **ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•** | ์—…๋กœ๋“œ ํŒŒ์ผ๋ช… ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + UUID ์ž๋™ ์ถ”๊ฐ€ ์ง€์› |
34
- | โณ **๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ** | ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ค‘ ์ž๋™ ์Šคํ”ผ๋„ˆ ํ‘œ์‹œ |
35
- | ๐Ÿš€ **์„ฑ๋Šฅ ์ตœ์ ํ™”** | ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™”๋กœ ๋น ๋ฅธ ๋ Œ๋”๋ง |
36
- | ๐Ÿ“ **TypeScript** | ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ |
37
- | ๐ŸŽจ **ํ…Œ๋งˆ ์ง€์›** | ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ ๋ฐ ์ปค์Šคํ…€ ํ…Œ๋งˆ |
31
+ | ํŠน์ง• | ์„ค๋ช… |
32
+ | ----------------------- | ------------------------------------------------------ |
33
+ | **์ด๋ฏธ์ง€ ์ „์šฉ** | ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ/๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ๋งŒ ์ง€์› (๋น„๋””์˜ค/์˜ค๋””์˜ค ์ œ๊ฑฐ) |
34
+ | **HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ** | HTML ํŒŒ์ผ์„ ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญํ•˜์—ฌ iframe์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ |
35
+ | **S3 ์—ฐ๋™** | Presigned URL ๊ธฐ๋ฐ˜ S3 ์—…๋กœ๋“œ ๋‚ด์žฅ |
36
+ | **ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•** | ์—…๋กœ๋“œ ํŒŒ์ผ๋ช… ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + UUID ์ž๋™ ์ถ”๊ฐ€ ์ง€์› |
37
+ | **๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ** | ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ค‘ ์ž๋™ ์Šคํ”ผ๋„ˆ ํ‘œ์‹œ |
38
+ | **์„ฑ๋Šฅ ์ตœ์ ํ™”** | ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋น„ํ™œ์„ฑํ™”๋กœ ๋น ๋ฅธ ๋ Œ๋”๋ง |
39
+ | **TypeScript** | ์™„์ „ํ•œ ํƒ€์ž… ์•ˆ์ „์„ฑ |
40
+ | **ํ…Œ๋งˆ ์ง€์›** | ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ ๋ฐ ์ปค์Šคํ…€ ํ…Œ๋งˆ |
38
41
 
39
42
  ### ์ง€์› ์ด๋ฏธ์ง€ ํ˜•์‹
40
43
 
@@ -44,7 +47,7 @@ PNG, JPEG/JPG, GIF, WebP, BMP, SVG
44
47
 
45
48
  ---
46
49
 
47
- ## ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘
50
+ ## ๋น ๋ฅธ ์‹œ์ž‘
48
51
 
49
52
  ### 1. ์„ค์น˜
50
53
 
@@ -74,7 +77,7 @@ export default function App() {
74
77
  }
75
78
  ```
76
79
 
77
- > โš ๏ธ **์ค‘์š”**: `style.css`๋ฅผ ์ž„ํฌํŠธํ•˜์ง€ ์•Š์œผ๋ฉด ์—๋””ํ„ฐ๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
80
+ > **์ค‘์š”**: `style.css`๋ฅผ ์ž„ํฌํŠธํ•˜์ง€ ์•Š์œผ๋ฉด ์—๋””ํ„ฐ๊ฐ€ ์ •์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
78
81
 
79
82
  ### 3. Next.js์—์„œ ์‚ฌ์šฉ
80
83
 
@@ -102,7 +105,7 @@ export default function EditorPage() {
102
105
 
103
106
  ---
104
107
 
105
- ## ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
108
+ ## ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
106
109
 
107
110
  ### 1. S3 ์—…๋กœ๋“œ (๊ถŒ์žฅ)
108
111
 
@@ -140,10 +143,12 @@ production/blog/images/my-photo.png
140
143
 
141
144
  ---
142
145
 
143
- ### ๐Ÿ“ ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
146
+ ### ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
144
147
 
145
148
  ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ์—…๋กœ๋“œํ•  ๋•Œ ํŒŒ์ผ๋ช… ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
146
149
 
150
+ > **์ฐธ๊ณ **: ๊ธฐ๋ณธ์ ์œผ๋กœ ํ™•์žฅ์ž๋Š” ์ž๋™์œผ๋กœ ๋ถ™์Šต๋‹ˆ๋‹ค. `preserveExtension: false`๋กœ ์„ค์ •ํ•˜๋ฉด ํ™•์žฅ์ž๋ฅผ ๋ถ™์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
151
+
147
152
  #### ์˜ต์…˜ 1: UUID ์ž๋™ ์ถ”๊ฐ€
148
153
 
149
154
  ```tsx
@@ -172,10 +177,11 @@ production/blog/images/my-photo.png
172
177
  apiEndpoint: "/api/s3/presigned",
173
178
  env: "production",
174
179
  path: "uploads",
175
- fileNameTransform: (originalName, file) => {
176
- // ์˜ˆ: ์‚ฌ์šฉ์ž ID ์ถ”๊ฐ€
180
+ fileNameTransform: (nameWithoutExt, file) => {
181
+ // nameWithoutExt๋Š” ํ™•์žฅ์ž๊ฐ€ ์ œ๊ฑฐ๋œ ํŒŒ์ผ๋ช… (์˜ˆ: "photo")
182
+ // ํ™•์žฅ์ž๋Š” ์ž๋™์œผ๋กœ ๋ถ™์Šต๋‹ˆ๋‹ค
177
183
  const userId = getCurrentUserId();
178
- return `${userId}_${originalName}`;
184
+ return `${userId}_${nameWithoutExt}`;
179
185
  },
180
186
  }}
181
187
  />
@@ -185,7 +191,9 @@ production/blog/images/my-photo.png
185
191
 
186
192
  ```
187
193
  ์›๋ณธ: photo.png
188
- ์—…๋กœ๋“œ: user123_photo.png
194
+ โ†’ nameWithoutExt: "photo"
195
+ โ†’ ๋ณ€ํ™˜ ํ›„: "user123_photo"
196
+ โ†’ ์ตœ์ข…: user123_photo.png
189
197
  ```
190
198
 
191
199
  #### ์˜ต์…˜ 3: ์กฐํ•ฉ ์‚ฌ์šฉ (๊ถŒ์žฅ)
@@ -196,7 +204,7 @@ production/blog/images/my-photo.png
196
204
  apiEndpoint: "/api/s3/presigned",
197
205
  env: "production",
198
206
  path: "uploads",
199
- fileNameTransform: (originalName) => `user123_${originalName}`,
207
+ fileNameTransform: (nameWithoutExt) => `user123_${nameWithoutExt}`,
200
208
  appendUUID: true, // ๋ณ€ํ™˜ ํ›„ UUID ์ถ”๊ฐ€
201
209
  }}
202
210
  />
@@ -206,8 +214,10 @@ production/blog/images/my-photo.png
206
214
 
207
215
  ```
208
216
  ์›๋ณธ: photo.png
209
- 1. fileNameTransform ์ ์šฉ: user123_photo.png
210
- 2. appendUUID ์ ์šฉ: user123_photo_550e8400-e29b-41d4.png
217
+ โ†’ nameWithoutExt: "photo"
218
+ 1. fileNameTransform ์ ์šฉ: "user123_photo"
219
+ 2. appendUUID ์ ์šฉ: "user123_photo_550e8400-e29b-41d4"
220
+ 3. ํ™•์žฅ์ž ๋ถ™์ด๊ธฐ: user123_photo_550e8400-e29b-41d4.png
211
221
  ```
212
222
 
213
223
  #### ์‹ค์ „ ์˜ˆ์ œ: ํƒ€์ž„์Šคํƒฌํ”„ + UUID
@@ -220,11 +230,10 @@ function MyEditor() {
220
230
  apiEndpoint: "/api/s3/presigned",
221
231
  env: "production",
222
232
  path: "uploads",
223
- fileNameTransform: (originalName, file) => {
233
+ fileNameTransform: (nameWithoutExt, file) => {
234
+ // nameWithoutExt๋Š” ์ด๋ฏธ ํ™•์žฅ์ž๊ฐ€ ์ œ๊ฑฐ๋จ
224
235
  const timestamp = new Date().toISOString().split("T")[0]; // 2024-01-15
225
- const ext = originalName.split(".").pop();
226
- const nameWithoutExt = originalName.replace(`.${ext}`, "");
227
- return `${timestamp}_${nameWithoutExt}.${ext}`;
236
+ return `${timestamp}_${nameWithoutExt}`;
228
237
  },
229
238
  appendUUID: true,
230
239
  }}
@@ -236,7 +245,41 @@ function MyEditor() {
236
245
  **๊ฒฐ๊ณผ:**
237
246
 
238
247
  ```
239
- 2024-01-15_photo_550e8400-e29b-41d4.png
248
+ ์›๋ณธ: photo.png
249
+ โ†’ nameWithoutExt: "photo"
250
+ 1. fileNameTransform: "2024-01-15_photo"
251
+ 2. appendUUID: "2024-01-15_photo_550e8400-e29b-41d4"
252
+ 3. ํ™•์žฅ์ž ๋ถ™์ด๊ธฐ: 2024-01-15_photo_550e8400-e29b-41d4.png
253
+ ```
254
+
255
+ #### ์˜ต์…˜ 4: ํ™•์žฅ์ž ์ œ๊ฑฐ (preserveExtension: false)
256
+
257
+ ```tsx
258
+ <LumirEditor
259
+ s3Upload={{
260
+ apiEndpoint: "/api/s3/presigned",
261
+ env: "production",
262
+ path: "uploads",
263
+ fileNameTransform: (nameWithoutExt) => `${nameWithoutExt}_custom`,
264
+ preserveExtension: false, // ํ™•์žฅ์ž ์•ˆ ๋ถ™์ž„
265
+ }}
266
+ />
267
+ ```
268
+
269
+ **๊ฒฐ๊ณผ:**
270
+
271
+ ```
272
+ ์›๋ณธ: photo.png
273
+ โ†’ nameWithoutExt: "photo"
274
+ โ†’ ๋ณ€ํ™˜ ํ›„: "photo_custom"
275
+ โ†’ ์ตœ์ข…: photo_custom (ํ™•์žฅ์ž ์—†์Œ)
276
+ ```
277
+
278
+ **์‚ฌ์šฉ ์‚ฌ๋ก€**: WebP ๋ณ€ํ™˜ ๋“ฑ ์„œ๋ฒ„์—์„œ ํ™•์žฅ์ž๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ
279
+
280
+ ```tsx
281
+ fileNameTransform: (nameWithoutExt) => `${nameWithoutExt}.webp`,
282
+ preserveExtension: false,
240
283
  ```
241
284
 
242
285
  ---
@@ -289,19 +332,290 @@ const imageUrl = await s3Uploader(imageFile);
289
332
 
290
333
  ---
291
334
 
292
- ## ๐Ÿ“š Props API
335
+ ## ์ด๋ฏธ์ง€ ์‚ญ์ œ
336
+
337
+ ์—๋””ํ„ฐ์—์„œ ์ด๋ฏธ์ง€๊ฐ€ ์‚ญ์ œ๋  ๋•Œ S3 ๋“ฑ ์™ธ๋ถ€ ์Šคํ† ๋ฆฌ์ง€์—์„œ๋„ ์ž๋™์œผ๋กœ ์‚ญ์ œํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด `onImageDelete` ์ฝœ๋ฐฑ์„ ์‚ฌ์šฉํ•˜์„ธ์š”.
338
+
339
+ ### ๊ธฐ๋ณธ ์‚ฌ์šฉ
340
+
341
+ ```tsx
342
+ <LumirEditor
343
+ s3Upload={{
344
+ apiEndpoint: "/api/s3/presigned",
345
+ env: "production",
346
+ path: "images",
347
+ }}
348
+ onImageDelete={(imageUrl) => {
349
+ console.log("์ด๋ฏธ์ง€ ์‚ญ์ œ๋จ:", imageUrl);
350
+ // S3์—์„œ ์‚ญ์ œ ๋กœ์ง ๊ตฌํ˜„
351
+ }}
352
+ />
353
+ ```
354
+
355
+ ### ๊ถŒ์žฅ: ์ง€์—ฐ ์‚ญ์ œ (Undo/Redo ๋Œ€์‘)
356
+
357
+ Undo๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋ณต์›ํ•  ์ˆ˜ ์žˆ๋„๋ก **์ง€์—ฐ ์‚ญ์ œ**๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
358
+
359
+ ```tsx
360
+ "use client";
361
+
362
+ import { useState, useRef, useCallback } from "react";
363
+
364
+ function Editor() {
365
+ const pendingDeletes = useRef(new Map());
366
+
367
+ const handleImageDelete = useCallback((imageUrl: string) => {
368
+ // ์ด๋ฏธ ์˜ˆ์•ฝ๋œ ์‚ญ์ œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฌด์‹œ
369
+ if (pendingDeletes.current.has(imageUrl)) return;
370
+
371
+ // 5๋ถ„ ํ›„ ์‚ญ์ œ ์˜ˆ์•ฝ
372
+ const timeoutId = setTimeout(async () => {
373
+ pendingDeletes.current.delete(imageUrl);
374
+
375
+ // S3์—์„œ ์‹ค์ œ ์‚ญ์ œ
376
+ await fetch(`/api/s3/delete?url=${encodeURIComponent(imageUrl)}`, {
377
+ method: "DELETE",
378
+ });
379
+ }, 5 * 60 * 1000); // 5๋ถ„
380
+
381
+ pendingDeletes.current.set(imageUrl, timeoutId);
382
+ }, []);
383
+
384
+ return (
385
+ <LumirEditor
386
+ s3Upload={{ /* ... */ }}
387
+ onImageDelete={handleImageDelete}
388
+ />
389
+ );
390
+ }
391
+ ```
392
+
393
+ ### S3 ์‚ญ์ œ API ์˜ˆ์‹œ
394
+
395
+ > **์ฐธ๊ณ **: `onImageDelete`๋Š” **ํ”„๋ ˆ์ž„์›Œํฌ ๋…๋ฆฝ์ **์ž…๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ๊ฐ ํ™˜๊ฒฝ๋ณ„ ๊ตฌํ˜„ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.
396
+
397
+ #### Next.js API Route
398
+
399
+ **ํŒŒ์ผ**: `app/api/s3/delete/route.ts`
400
+
401
+ ```typescript
402
+ import { NextRequest, NextResponse } from "next/server";
403
+ import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
404
+
405
+ const s3 = new S3Client({
406
+ region: process.env.AWS_REGION!,
407
+ credentials: {
408
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
409
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
410
+ },
411
+ });
412
+
413
+ export async function DELETE(req: NextRequest) {
414
+ const { searchParams } = new URL(req.url);
415
+ const imageUrl = searchParams.get("url");
416
+
417
+ if (!imageUrl) {
418
+ return NextResponse.json({ error: "url is required" }, { status: 400 });
419
+ }
420
+
421
+ // URL์—์„œ S3 ํ‚ค ์ถ”์ถœ
422
+ const key = extractKeyFromUrl(imageUrl);
423
+
424
+ await s3.send(
425
+ new DeleteObjectCommand({
426
+ Bucket: process.env.AWS_S3_BUCKET!,
427
+ Key: key,
428
+ })
429
+ );
430
+
431
+ return NextResponse.json({ success: true });
432
+ }
433
+
434
+ function extractKeyFromUrl(url: string): string {
435
+ const urlObj = new URL(url);
436
+ return decodeURIComponent(urlObj.pathname.slice(1));
437
+ }
438
+ ```
439
+
440
+ **ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„**:
441
+
442
+ ```tsx
443
+ const handleImageDelete = (imageUrl: string) => {
444
+ fetch(`/api/s3/delete?url=${encodeURIComponent(imageUrl)}`, {
445
+ method: "DELETE",
446
+ });
447
+ };
448
+
449
+ <LumirEditor onImageDelete={handleImageDelete} />
450
+ ```
451
+
452
+ #### React + Express
453
+
454
+ **์„œ๋ฒ„** (`server.js`):
455
+
456
+ ```javascript
457
+ app.delete('/api/images', async (req, res) => {
458
+ const { imageUrl } = req.body;
459
+ const key = extractKeyFromS3Url(imageUrl);
460
+
461
+ await s3Client.send(new DeleteObjectCommand({
462
+ Bucket: process.env.S3_BUCKET,
463
+ Key: key
464
+ }));
465
+
466
+ res.json({ success: true });
467
+ });
468
+ ```
469
+
470
+ **ํด๋ผ์ด์–ธํŠธ**:
471
+
472
+ ```tsx
473
+ const handleImageDelete = async (imageUrl: string) => {
474
+ await fetch('https://api.myapp.com/api/images', {
475
+ method: 'DELETE',
476
+ headers: { 'Content-Type': 'application/json' },
477
+ body: JSON.stringify({ imageUrl })
478
+ });
479
+ };
480
+
481
+ <LumirEditor onImageDelete={handleImageDelete} />
482
+ ```
483
+
484
+ #### React Native + Firebase Storage
485
+
486
+ ```tsx
487
+ import storage from '@react-native-firebase/storage';
488
+
489
+ const handleImageDelete = async (imageUrl: string) => {
490
+ const ref = storage().refFromURL(imageUrl);
491
+ await ref.delete();
492
+ };
493
+
494
+ <LumirEditor onImageDelete={handleImageDelete} />
495
+ ```
496
+
497
+ #### Vue + Axios + FastAPI
498
+
499
+ ```typescript
500
+ const handleImageDelete = async (imageUrl: string) => {
501
+ await axios.delete('https://api.myapp.com/v1/images', {
502
+ data: { imageUrl }
503
+ });
504
+ };
505
+ ```
506
+
507
+ ### ์ฃผ์˜์‚ฌํ•ญ
508
+
509
+ | ํ•ญ๋ชฉ | ์„ค๋ช… |
510
+ |------|------|
511
+ | **Undo/Redo** | ์ง€์—ฐ ์‚ญ์ œ๋กœ ๋ณต์› ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ตฌํ˜„ (๊ถŒ์žฅ: 5-10๋ถ„) |
512
+ | **๊ถŒํ•œ ๊ฒ€์ฆ** | ํ”„๋กœ๋•์…˜์—์„œ๋Š” ์ธ์ฆ/์ธ๊ฐ€ ํ•„์ˆ˜ |
513
+ | **์ฐธ์กฐ ์นด์šดํŠธ** | ๊ฐ™์€ ์ด๋ฏธ์ง€๋ฅผ ์—ฌ๋Ÿฌ ๋ฌธ์„œ์—์„œ ์‚ฌ์šฉํ•˜๋Š”์ง€ ํ™•์ธ |
514
+ | **์‚ญ์ œ ๋กœ๊ทธ** | ๊ฐ์‚ฌ ์ถ”์ ์„ ์œ„ํ•œ ์‚ญ์ œ ๊ธฐ๋ก ์ €์žฅ ๊ถŒ์žฅ |
515
+
516
+ ---
517
+
518
+ ## HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ
519
+
520
+ LumirEditor๋Š” HTML ํŒŒ์ผ์„ iframe์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐํ•  ์ˆ˜ ์žˆ๋Š” ์ปค์Šคํ…€ ๋ธ”๋ก์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํŽธ์ง‘ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ˆœ์ˆ˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ์œผ๋กœ, HTML ๋ฌธ์„œ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
521
+
522
+ ### ์‚ฌ์šฉ ๋ฐฉ๋ฒ•
523
+
524
+ #### 1. ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ
525
+
526
+ HTML ํŒŒ์ผ(`.html`, `.htm`)์„ ์—๋””ํ„ฐ์— ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญํ•˜๋ฉด ์ž๋™์œผ๋กœ iframe ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ธ”๋ก์ด ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค.
527
+
528
+ ```tsx
529
+ <LumirEditor />
530
+ ```
531
+
532
+ - **์ง€์› ํŒŒ์ผ ํ˜•์‹**: `.html`, `.htm`
533
+ - **ํŠน์ง•**:
534
+ - ํŽธ์ง‘ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ˆœ์ˆ˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ
535
+ - ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ๊ธฐ๋Šฅ
536
+ - ์•ˆ์ „ํ•œ sandbox ์ฒ˜๋ฆฌ (`allow-same-origin`, JavaScript ์‹คํ–‰ ๋น„ํ™œ์„ฑํ™”)
537
+ - ํŒŒ์ผ๋ช… ํ‘œ์‹œ
538
+
539
+ #### 2. ์Šฌ๋ž˜์‹œ ๋ฉ”๋‰ด
540
+
541
+ ์—๋””ํ„ฐ์—์„œ `/`๋ฅผ ์ž…๋ ฅํ•˜๊ณ  "HTML Preview"๋ฅผ ์„ ํƒํ•˜๋ฉด ์˜ˆ์ œ HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ธ”๋ก์ด ์‚ฝ์ž…๋ฉ๋‹ˆ๋‹ค.
542
+
543
+ ```
544
+ / โ†’ HTML Preview
545
+ ```
546
+
547
+ ### ํŠน์ง•
548
+
549
+ - **iframe ๊ธฐ๋ฐ˜**: HTML ๋ฌธ์„œ๋ฅผ ๋…๋ฆฝ๋œ iframe์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ Œ๋”๋ง
550
+ - **Sandbox ๋ณด์•ˆ**: `sandbox="allow-same-origin"` ์†์„ฑ์œผ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™” (JavaScript ์‹คํ–‰ ์˜๋„์  ๋น„ํ™œ์„ฑํ™”)
551
+ - **์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ**: ํ—ค๋” ํด๋ฆญ์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์˜์—ญ ํ† ๊ธ€
552
+ - **๋“œ๋ž˜๊ทธ ๋ฆฌ์‚ฌ์ด์ฆˆ**: ํ•˜๋‹จ ํ•ธ๋“ค์„ ๋“œ๋ž˜๊ทธํ•˜์—ฌ ๋†’์ด ์กฐ์ ˆ ๊ฐ€๋Šฅ (100px ~ 1200px)
553
+ - **์ƒˆ ์ฐฝ ์—ด๊ธฐ**: HTML ๋ฌธ์„œ๋ฅผ ์ƒˆ ์ฐฝ์—์„œ ์ „์ฒด ํ™”๋ฉด์œผ๋กœ ํ™•์ธ
554
+ - **๋‹ค์šด๋กœ๋“œ**: HTML ํŒŒ์ผ๋กœ ๋‹ค์šด๋กœ๋“œ
555
+ - **ํŽธ์ง‘ ๋ถˆ๊ฐ€**: ์ˆœ์ˆ˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ „์šฉ
556
+
557
+ ### ์‚ฌ์šฉ ์˜ˆ์ œ
558
+
559
+ ```tsx
560
+ import { LumirEditor } from "@lumir-company/editor";
561
+ import "@lumir-company/editor/style.css";
562
+
563
+ function App() {
564
+ return (
565
+ <div className="w-full h-[600px]">
566
+ <LumirEditor
567
+ onContentChange={(blocks) => {
568
+ // HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋ธ”๋ก๋„ ์ผ๋ฐ˜ ๋ธ”๋ก๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋จ
569
+ console.log(blocks);
570
+ }}
571
+ />
572
+ </div>
573
+ );
574
+ }
575
+ ```
576
+
577
+ ### ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์œผ๋กœ ๋ธ”๋ก ์‚ฝ์ž…
578
+
579
+ ```tsx
580
+ import { HtmlPreview } from "@lumir-company/editor";
581
+
582
+ // ์—๋””ํ„ฐ ์ธ์Šคํ„ด์Šค์—์„œ ์ง์ ‘ ๋ธ”๋ก ์‚ฝ์ž…
583
+ editor.insertBlocks([
584
+ {
585
+ type: "htmlPreview",
586
+ props: {
587
+ htmlContent: "<h1>Hello World</h1><p>This is HTML content</p>",
588
+ fileName: "example.html",
589
+ height: "400px",
590
+ },
591
+ },
592
+ ]);
593
+ ```
594
+
595
+ ### ์ฃผ์˜์‚ฌํ•ญ
596
+
597
+ - HTML ๋‚ด์šฉ์€ iframe์˜ `sandbox="allow-same-origin"` ์†์„ฑ์œผ๋กœ ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค
598
+ - **JavaScript๋Š” ์˜๋„์ ์œผ๋กœ ๋น„ํ™œ์„ฑํ™”**๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค (๋ณด์•ˆ์ƒ ์ด์œ )
599
+ - ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค(CSS, JS, ์ด๋ฏธ์ง€ ๋“ฑ)๋Š” ์ƒ๋Œ€ ๊ฒฝ๋กœ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
600
+ - ์ธ๋ผ์ธ CSS ์Šคํƒ€์ผ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค
601
+
602
+ ---
603
+
604
+ ## Props API
293
605
 
294
606
  ### ํ•ต์‹ฌ Props
295
607
 
296
- | Prop | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
297
- | ----------------- | --------------------------------- | ----------- | ------------------ |
298
- | `s3Upload` | `S3UploaderConfig` | `undefined` | S3 ์—…๋กœ๋“œ ์„ค์ • |
299
- | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | ์ปค์Šคํ…€ ์—…๋กœ๋“œ ํ•จ์ˆ˜ |
300
- | `onContentChange` | `(blocks) => void` | `undefined` | ์ฝ˜ํ…์ธ  ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ |
301
- | `initialContent` | `Block[] \| string` | `undefined` | ์ดˆ๊ธฐ ์ฝ˜ํ…์ธ  |
302
- | `editable` | `boolean` | `true` | ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ |
303
- | `theme` | `"light" \| "dark"` | `"light"` | ํ…Œ๋งˆ |
304
- | `className` | `string` | `""` | CSS ํด๋ž˜์Šค |
608
+ | Prop | ํƒ€์ž… | ๊ธฐ๋ณธ๊ฐ’ | ์„ค๋ช… |
609
+ | ----------------- | ------------------------------------- | ----------- | ----------------------- |
610
+ | `s3Upload` | `S3UploaderConfig` | `undefined` | S3 ์—…๋กœ๋“œ ์„ค์ • |
611
+ | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | ์ปค์Šคํ…€ ์—…๋กœ๋“œ ํ•จ์ˆ˜ |
612
+ | `onContentChange` | `(blocks) => void` | `undefined` | ์ฝ˜ํ…์ธ  ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ |
613
+ | `onImageDelete` | `(imageUrl: string) => void` | `undefined` | ์ด๋ฏธ์ง€ ์‚ญ์ œ ์‹œ ์ฝœ๋ฐฑ |
614
+ | `onError` | `(error: LumirEditorError) => void` | `undefined` | ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ฝœ๋ฐฑ |
615
+ | `initialContent` | `Block[] \| string` | `undefined` | ์ดˆ๊ธฐ ์ฝ˜ํ…์ธ  |
616
+ | `editable` | `boolean` | `true` | ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ |
617
+ | `theme` | `"light" \| "dark"` | `"light"` | ํ…Œ๋งˆ |
618
+ | `className` | `string` | `""` | CSS ํด๋ž˜์Šค |
305
619
 
306
620
  ### S3UploaderConfig
307
621
 
@@ -313,8 +627,9 @@ interface S3UploaderConfig {
313
627
  path: string; // S3 ์ €์žฅ ๊ฒฝ๋กœ
314
628
 
315
629
  // ์„ ํƒ (ํŒŒ์ผ๋ช… ์ปค์Šคํ„ฐ๋งˆ์ด์ง•)
316
- fileNameTransform?: (originalName: string, file: File) => string;
317
- appendUUID?: boolean; // true: ํŒŒ์ผ๋ช… ๋’ค์— UUID ์ถ”๊ฐ€
630
+ fileNameTransform?: (nameWithoutExt: string, file: File) => string; // ํ™•์žฅ์ž ์ œ์™ธํ•œ ์ด๋ฆ„ ๋ณ€ํ™˜
631
+ appendUUID?: boolean; // true: ํŒŒ์ผ๋ช… ๋’ค์— UUID ์ถ”๊ฐ€ (ํ™•์žฅ์ž ์•ž์— ์‚ฝ์ž…)
632
+ preserveExtension?: boolean; // false: ํ™•์žฅ์ž๋ฅผ ๋ถ™์ด์ง€ ์•Š์Œ (๊ธฐ๋ณธ: true)
318
633
  }
319
634
  ```
320
635
 
@@ -328,12 +643,22 @@ interface LumirEditorProps {
328
643
  // === ์—๋””ํ„ฐ ์„ค์ • ===
329
644
  initialContent?: DefaultPartialBlock[] | string; // ์ดˆ๊ธฐ ์ฝ˜ํ…์ธ  (๋ธ”๋ก ๋ฐฐ์—ด ๋˜๋Š” JSON ๋ฌธ์ž์—ด)
330
645
  initialEmptyBlocks?: number; // ์ดˆ๊ธฐ ๋นˆ ๋ธ”๋ก ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ: 3)
646
+ placeholder?: string; // ๋นˆ ๋ธ”๋ก์— ํ‘œ์‹œํ•  ์•ˆ๋‚ด ํ…์ŠคํŠธ (์˜ˆ: "๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”...")
331
647
  uploadFile?: (file: File) => Promise<string>; // ์ปค์Šคํ…€ ํŒŒ์ผ ์—…๋กœ๋“œ ํ•จ์ˆ˜
332
- s3Upload?: S3UploaderConfig; // S3 ์—…๋กœ๋“œ ์„ค์ • (apiEndpoint, env, path ๋“ฑ)
648
+ s3Upload?: {
649
+ apiEndpoint: string;
650
+ env: "development" | "production";
651
+ path: string;
652
+ fileNameTransform?: (nameWithoutExt: string, file: File) => string; // ํ™•์žฅ์ž ์ œ์™ธํ•œ ์ด๋ฆ„ ๋ณ€ํ™˜
653
+ appendUUID?: boolean; // UUID ์ž๋™ ์ถ”๊ฐ€ (ํ™•์žฅ์ž ์•ž)
654
+ preserveExtension?: boolean; // ํ™•์žฅ์ž ์ž๋™ ๋ถ™์ด๊ธฐ (๊ธฐ๋ณธ: true)
655
+ };
333
656
 
334
657
  // === ์ฝœ๋ฐฑ ===
335
658
  onContentChange?: (blocks: DefaultPartialBlock[]) => void; // ์ฝ˜ํ…์ธ  ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ
659
+ onImageDelete?: (imageUrl: string) => void; // ์ด๋ฏธ์ง€ ์‚ญ์ œ ์‹œ ํ˜ธ์ถœ (S3 ์‚ญ์ œ ๋“ฑ)
336
660
  onSelectionChange?: () => void; // ์„ ํƒ ์˜์—ญ ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ
661
+ onError?: (error: LumirEditorError) => void; // ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํ˜ธ์ถœ
337
662
 
338
663
  // ๊ธฐ๋Šฅ ์„ค์ •
339
664
  tables?: TableConfig; // ํ…Œ์ด๋ธ” ๊ธฐ๋Šฅ ์„ค์ • (splitCells, cellBackgroundColor ๋“ฑ)
@@ -355,6 +680,11 @@ interface LumirEditorProps {
355
680
  tableHandles?: boolean; // ํ…Œ์ด๋ธ” ํ•ธ๋“ค ํ‘œ์‹œ (๊ธฐ๋ณธ: true)
356
681
  className?: string; // ์ปจํ…Œ์ด๋„ˆ CSS ํด๋ž˜์Šค
357
682
 
683
+ // === ๋งํฌ ํ”„๋ฆฌ๋ทฐ ์„ค์ • ===
684
+ linkPreview?: {
685
+ apiEndpoint: string; // ๋งํฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ API ์—”๋“œํฌ์ธํŠธ (์˜ˆ: "/api/link-preview")
686
+ };
687
+
358
688
  // ๋ฏธ๋””์–ด ์—…๋กœ๋“œ ํ—ˆ์šฉ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: ๋ชจ๋‘ ๋น„ํ™œ์„ฑ)
359
689
  allowVideoUpload?: boolean; // ๋น„๋””์˜ค ์—…๋กœ๋“œ ํ—ˆ์šฉ (๊ธฐ๋ณธ: false)
360
690
  allowAudioUpload?: boolean; // ์˜ค๋””์˜ค ์—…๋กœ๋“œ ํ—ˆ์šฉ (๊ธฐ๋ณธ: false)
@@ -366,7 +696,7 @@ interface LumirEditorProps {
366
696
 
367
697
  ---
368
698
 
369
- ## ๐Ÿ’ก ์‚ฌ์šฉ ์˜ˆ์ œ
699
+ ## ์‚ฌ์šฉ ์˜ˆ์ œ
370
700
 
371
701
  ### ์ฝ๊ธฐ ์ „์šฉ ๋ชจ๋“œ
372
702
 
@@ -416,7 +746,7 @@ function EditorWithSave() {
416
746
 
417
747
  ---
418
748
 
419
- ## ๐ŸŽจ ์Šคํƒ€์ผ๋ง
749
+ ## ์Šคํƒ€์ผ๋ง
420
750
 
421
751
  ### Tailwind CSS์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ
422
752
 
@@ -454,7 +784,7 @@ import { LumirEditor, cn } from "@lumir-company/editor";
454
784
 
455
785
  ---
456
786
 
457
- ## โš ๏ธ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…
787
+ ## ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…
458
788
 
459
789
  ### ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
460
790
 
@@ -468,10 +798,10 @@ import { LumirEditor, cn } from "@lumir-company/editor";
468
798
  #### 1. ์—๋””ํ„ฐ๊ฐ€ ๋ณด์ด์ง€ ์•Š์Œ
469
799
 
470
800
  ```tsx
471
- // โŒ ์ž˜๋ชป๋จ
801
+ // ์ž˜๋ชป๋จ
472
802
  <LumirEditor />;
473
803
 
474
- // โœ… ์˜ฌ๋ฐ”๋ฆ„
804
+ // ์˜ฌ๋ฐ”๋ฆ„
475
805
  import "@lumir-company/editor/style.css";
476
806
  <div className="h-[400px]">
477
807
  <LumirEditor />
@@ -481,10 +811,10 @@ import "@lumir-company/editor/style.css";
481
811
  #### 2. Next.js Hydration ์˜ค๋ฅ˜
482
812
 
483
813
  ```tsx
484
- // โŒ ์ž˜๋ชป๋จ
814
+ // ์ž˜๋ชป๋จ
485
815
  import { LumirEditor } from "@lumir-company/editor";
486
816
 
487
- // โœ… ์˜ฌ๋ฐ”๋ฆ„
817
+ // ์˜ฌ๋ฐ”๋ฆ„
488
818
  const LumirEditor = dynamic(
489
819
  () =>
490
820
  import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
@@ -508,7 +838,7 @@ const LumirEditor = dynamic(
508
838
  #### 4. ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ ์ค‘๋ณต ๋ฌธ์ œ
509
839
 
510
840
  ```tsx
511
- // โœ… ํ•ด๊ฒฐ: appendUUID ์‚ฌ์šฉ
841
+ // ํ•ด๊ฒฐ: appendUUID ์‚ฌ์šฉ
512
842
  <LumirEditor
513
843
  s3Upload={{
514
844
  apiEndpoint: "/api/s3/presigned",
@@ -521,7 +851,7 @@ const LumirEditor = dynamic(
521
851
 
522
852
  ---
523
853
 
524
- ## ๐Ÿ› ๏ธ ์œ ํ‹ธ๋ฆฌํ‹ฐ API
854
+ ## ์œ ํ‹ธ๋ฆฌํ‹ฐ API
525
855
 
526
856
  ### ContentUtils
527
857
 
@@ -554,23 +884,83 @@ const uploader = createS3Uploader({
554
884
  const url = await uploader(imageFile);
555
885
  ```
556
886
 
557
- ## ๐Ÿ”— ๊ด€๋ จ ๋งํฌ
887
+ ## ๊ด€๋ จ ๋งํฌ
558
888
 
559
889
  - [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
560
890
  - [BlockNote Documentation](https://www.blocknotejs.org/)
561
891
 
562
892
  ---
563
893
 
564
- ## ๐Ÿ“ ๋ณ€๊ฒฝ ๋กœ๊ทธ
894
+ ## ๋ณ€๊ฒฝ ๋กœ๊ทธ
895
+
896
+ ### v0.4.3
897
+
898
+ - **๋งํฌ ํ”„๋ฆฌ๋ทฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
899
+ - `linkPreview` prop์œผ๋กœ ๋งํฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™œ์„ฑํ™” (์นด์นด์˜คํ†ก ์Šคํƒ€์ผ OG ์นด๋“œ)
900
+ - URL ๋ถ™์—ฌ๋„ฃ๊ธฐ ์‹œ ์ž๋™ ๋งํฌ ํ”„๋ฆฌ๋ทฐ ๋ธ”๋ก ์ƒ์„ฑ (๋นˆ ๋ธ”๋ก์ด๋ฉด ๊ต์ฒด, ํ…์ŠคํŠธ ์žˆ์œผ๋ฉด ํ•˜๋‹จ ์‚ฝ์ž…)
901
+ - ์Šฌ๋ž˜์‹œ ๋ฉ”๋‰ด(`/`)์—์„œ Link Preview ํ•ญ๋ชฉ ์ถ”๊ฐ€
902
+ - ๋“œ๋ž˜๊ทธ ๋ฆฌ์‚ฌ์ด์ฆˆ ์ง€์› (์ขŒ์šฐ ๋„ˆ๋น„, ํ•˜๋‹จ ์ด๋ฏธ์ง€ ๋†’์ด ์กฐ์ ˆ)
903
+ - ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ด๋ฏธ์ง€ ์—†์„ ๊ฒฝ์šฐ ์ด๋ฏธ์ง€ ์˜์—ญ ์ƒ๋žต
904
+ - ์—๋Ÿฌ ์นด๋“œ ํด๋ฆญ ์‹œ ๋งํฌ ์ด๋™ ์ง€์›
905
+ - `fetchLinkMetadata`, `clearMetadataCache`, `LinkMetadata` ํƒ€์ž… export
906
+ - **๋งํฌ ํˆด๋ฐ” ์ปค์Šคํ…€**
907
+ - ํ…์ŠคํŠธ ๋งํฌ๋ฅผ ๋งํฌ ํ”„๋ฆฌ๋ทฐ ๋ธ”๋ก์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ๋ฒ„ํŠผ ์ถ”๊ฐ€ (`replaceBlocks` ์‚ฌ์šฉ)
908
+ - `linkPreview.apiEndpoint` ์„ค์ • ์‹œ์—๋งŒ ์ „ํ™˜ ๋ฒ„ํŠผ ํ‘œ์‹œ
909
+ - **placeholder prop ์ถ”๊ฐ€**
910
+ - `placeholder` prop์œผ๋กœ ์—๋””ํ„ฐ ๋นˆ ๋ธ”๋ก ์•ˆ๋‚ด ํ…์ŠคํŠธ ์„ค์ •
911
+ - **์ด๋ฏธ์ง€ ์‚ญ์ œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**
912
+ - `onImageDelete` ์ฝœ๋ฐฑ prop ์ถ”๊ฐ€ - ์—๋””ํ„ฐ์—์„œ ์ด๋ฏธ์ง€ ์‚ญ์ œ ์‹œ ํ˜ธ์ถœ
913
+ - S3 ๋“ฑ ์™ธ๋ถ€ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ด๋ฏธ์ง€ ์ž๋™ ์‚ญ์ œ ์ง€์›
914
+ - ์ง€์—ฐ ์‚ญ์ œ ํŒจํ„ด์œผ๋กœ Undo/Redo ๋Œ€์‘ ๊ฐ€๋Šฅ
915
+ - ์ด๋ฏธ์ง€ URL ์ถ”์ถœ ๋ฐ ์‚ญ์ œ ๊ฐ์ง€ ํ—ฌํผ ํ•จ์ˆ˜ ๋‚ด์žฅ
916
+ - **๋ณด์•ˆ ๊ฐ•ํ™”**
917
+ - URL ์ด์Šค์ผ€์ดํ”„ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ (XSS ๋ฐฉ์ง€)
918
+ - LinkButton: `javascript:`, `data:`, `vbscript:`, `file:` ํ”„๋กœํ† ์ฝœ ์ฐจ๋‹จ
919
+ - ์œ„ํ—˜ํ•œ URL ์ž…๋ ฅ ์‹œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
920
+ - **๋งํฌ ์‚ฝ์ž… ๋ฒ„๊ทธ ์ˆ˜์ •**
921
+ - ํ”Œ๋กœํŒ… ๋ฉ”๋‰ด ๋งํฌ ๋ฒ„ํŠผ: ํ…์ŠคํŠธ ๋ฏธ์„ ํƒ ์‹œ์—๋„ URL ํ…์ŠคํŠธ๋กœ ๋งํฌ ์‚ฝ์ž… ์ง€์›
922
+ - `editor.focus()` ํ˜ธ์ถœ๋กœ ์„ ํƒ ์ƒํƒœ ๋ณต์›
923
+ - **README ๊ฐœ์„ **
924
+ - ๋งํฌ ํ”„๋ฆฌ๋ทฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ ๋ฐ API ๋ผ์šฐํŠธ ์˜ˆ์‹œ ์ถ”๊ฐ€
925
+ - ์ด๋ฏธ์ง€ ์‚ญ์ œ ์„น์…˜ ์ถ”๊ฐ€ (์ง€์—ฐ ์‚ญ์ œ ์˜ˆ์‹œ ํฌํ•จ)
926
+ - S3 ์‚ญ์ œ API ๊ตฌํ˜„ ์˜ˆ์‹œ ์ถ”๊ฐ€
927
+ - Props API ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ
928
+
929
+ ### v0.4.2
930
+
931
+ - **์ฝ”๋“œ ๊ตฌ์กฐ ๋ฆฌํŒฉํ† ๋ง**
932
+ - FloatingMenu ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ (Icons, ๊ฐœ๋ณ„ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ)
933
+ - ์ƒ‰์ƒ ์ƒ์ˆ˜ ๋ณ„๋„ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ (`constants/colors.ts`)
934
+ - ๋ฏธ์‚ฌ์šฉ ๊ธฐ๋Šฅ ์ œ๊ฑฐ (FontSelect, FontSizeControl)
935
+ - **์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ฐœ์„ **
936
+ - `LumirEditorError` ์ปค์Šคํ…€ ์—๋Ÿฌ ํด๋ž˜์Šค ์ถ”๊ฐ€
937
+ - `onError` ์ฝœ๋ฐฑ prop ์ถ”๊ฐ€ - ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž ์ •์˜ ํ•ธ๋“ค๋ง ๊ฐ€๋Šฅ
938
+ - ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‚ฌ์šฉ์ž ์นœํ™”์  ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ ์ž๋™ ํ‘œ์‹œ
939
+ - **HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐœ์„ **
940
+ - sandbox ์„ค์ • ๋ช…ํ™•ํ™” (JavaScript ์˜๋„์  ๋น„ํ™œ์„ฑํ™”)
941
+ - ๋“œ๋ž˜๊ทธ ๋ฆฌ์‚ฌ์ด์ฆˆ, ์ƒˆ ์ฐฝ ์—ด๊ธฐ, ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ ๋ฌธ์„œํ™”
942
+ - **ํƒ€์ž… ๊ฐœ์„ **
943
+ - `LumirErrorCode`, `LumirErrorDetails` ํƒ€์ž… export
944
+ - `ColorItem` ํƒ€์ž… export
945
+
946
+ ### v0.4.1
947
+
948
+ - `preserveExtension` prop ์ถ”๊ฐ€ - ํ™•์žฅ์ž ์ž๋™ ๋ถ™์ด๊ธฐ ์ œ์–ด (๊ธฐ๋ณธ: true)
949
+ - **์ค‘์š”**: ํŒŒ์ผ๋ช… ๋ณ€ํ™˜ ์‹œ ํ™•์žฅ์ž ์œ„์น˜ ์ˆ˜์ • (ํ™•์žฅ์ž๊ฐ€ ํ•ญ์ƒ ๋งจ ๋’ค์— ์˜ค๋„๋ก)
950
+ - **Breaking Change**: `fileNameTransform` ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ€๊ฒฝ - ์ด์ œ ํ™•์žฅ์ž ์ œ์™ธํ•œ ํŒŒ์ผ๋ช…๋งŒ ์ „๋‹ฌ๋จ
951
+ - ์ด์ „: `fileNameTransform: (originalName, file) => ...` โ†’ originalName์— ํ™•์žฅ์ž ํฌํ•จ
952
+ - ๋ณ€๊ฒฝ: `fileNameTransform: (nameWithoutExt, file) => ...` โ†’ nameWithoutExt์— ํ™•์žฅ์ž ์ œ์™ธ
953
+ - ํ™•์žฅ์ž ์ œ๊ฑฐ ์‚ฌ์šฉ ์‚ฌ๋ก€ ๋ฌธ์„œํ™”
954
+ - README ์˜ˆ์ œ ๋ฐ ์„ค๋ช… ๊ฐœ์„ 
565
955
 
566
956
  ### v0.4.0
567
957
 
568
- - โœจ ํŒŒ์ผ๋ช… ๋ณ€ํ™˜ ์ฝœ๋ฐฑ (`fileNameTransform`) ์ถ”๊ฐ€
569
- - โœจ UUID ์ž๋™ ์ถ”๊ฐ€ ์˜ต์…˜ (`appendUUID`) ์ถ”๊ฐ€
570
- - ๐Ÿ› ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ ๋™์‹œ ์—…๋กœ๋“œ ์‹œ ์ค‘๋ณต ๋ฌธ์ œ ํ•ด๊ฒฐ
571
- - ๐Ÿ“ ๋ฌธ์„œ ๋Œ€ํญ ๊ฐœ์„ 
958
+ - ํŒŒ์ผ๋ช… ๋ณ€ํ™˜ ์ฝœ๋ฐฑ (`fileNameTransform`) ์ถ”๊ฐ€
959
+ - UUID ์ž๋™ ์ถ”๊ฐ€ ์˜ต์…˜ (`appendUUID`) ์ถ”๊ฐ€
960
+ - ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ ๋™์‹œ ์—…๋กœ๋“œ ์‹œ ์ค‘๋ณต ๋ฌธ์ œ ํ•ด๊ฒฐ
961
+ - ๋ฌธ์„œ ๋Œ€ํญ ๊ฐœ์„ 
572
962
 
573
963
  ### v0.3.3
574
964
 
575
- - ๐Ÿ› ์—๋””ํ„ฐ ์žฌ์ƒ์„ฑ ๋ฐฉ์ง€ ์ตœ์ ํ™”
576
- - ๐Ÿ“ ํƒ€์ž… ์ •์˜ ๊ฐœ์„ 
965
+ - ์—๋””ํ„ฐ ์žฌ์ƒ์„ฑ ๋ฐฉ์ง€ ์ตœ์ ํ™”
966
+ - ํƒ€์ž… ์ •์˜ ๊ฐœ์„