@lumir-company/editor 0.3.3 β†’ 0.4.1

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,97 +1,82 @@
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)
7
7
 
8
- ## πŸ“‹ λͺ©μ°¨
8
+ > 이미지 μ—…λ‘œλ“œμ— μ΅œμ ν™”λœ κ²½λŸ‰ 에디터. S3 μ—…λ‘œλ“œ, 파일λͺ… μ»€μŠ€ν„°λ§ˆμ΄μ§•, λ‘œλ”© μŠ€ν”Όλ„ˆ λ‚΄μž₯.
9
9
 
10
- - [✨ 핡심 νŠΉμ§•](#-핡심-νŠΉμ§•)
11
- - [πŸ“¦ μ„€μΉ˜](#-μ„€μΉ˜)
12
- - [πŸš€ λΉ λ₯Έ μ‹œμž‘](#-λΉ λ₯Έ-μ‹œμž‘)
13
- - [πŸ“š Props 레퍼런슀](#-props-레퍼런슀)
14
- - [πŸ–ΌοΈ 이미지 μ—…λ‘œλ“œ](#️-이미지-μ—…λ‘œλ“œ)
15
- - [πŸ› οΈ μœ ν‹Έλ¦¬ν‹° API](#️-μœ ν‹Έλ¦¬ν‹°-api)
16
- - [πŸ“– νƒ€μž… μ •μ˜](#-νƒ€μž…-μ •μ˜)
17
- - [πŸ’‘ μ‚¬μš© 예제](#-μ‚¬μš©-예제)
18
- - [🎨 μŠ€νƒ€μΌλ§ κ°€μ΄λ“œ](#-μŠ€νƒ€μΌλ§-κ°€μ΄λ“œ)
19
- - [⚠️ μ£Όμ˜μ‚¬ν•­ 및 νŠΈλŸ¬λΈ”μŠˆνŒ…](#️-μ£Όμ˜μ‚¬ν•­-및-νŠΈλŸ¬λΈ”μŠˆνŒ…)
20
- - [πŸ“„ λΌμ΄μ„ μŠ€](#-λΌμ΄μ„ μŠ€)
10
+ ---
11
+
12
+ ## λͺ©μ°¨
13
+
14
+ - [νŠΉμ§•](#νŠΉμ§•)
15
+ - [λΉ λ₯Έ μ‹œμž‘](#λΉ λ₯Έ-μ‹œμž‘)
16
+ - [이미지 μ—…λ‘œλ“œ](#이미지-μ—…λ‘œλ“œ)
17
+ - [S3 μ—…λ‘œλ“œ μ„€μ •](#1-s3-μ—…λ‘œλ“œ-ꢌμž₯)
18
+ - [파일λͺ… μ»€μŠ€ν„°λ§ˆμ΄μ§•](#파일λͺ…-μ»€μŠ€ν„°λ§ˆμ΄μ§•)
19
+ - [μ»€μŠ€ν…€ μ—…λ‘œλ”](#2-μ»€μŠ€ν…€-μ—…λ‘œλ”)
20
+ - [Props API](#props-api)
21
+ - [μ‚¬μš© 예제](#μ‚¬μš©-예제)
22
+ - [μŠ€νƒ€μΌλ§](#μŠ€νƒ€μΌλ§)
23
+ - [νŠΈλŸ¬λΈ”μŠˆνŒ…](#νŠΈλŸ¬λΈ”μŠˆνŒ…)
21
24
 
22
25
  ---
23
26
 
24
- ## ✨ 핡심 νŠΉμ§•
27
+ ## νŠΉμ§•
25
28
 
26
- | νŠΉμ§• | μ„€λͺ… |
27
- | ------------------------ | ----------------------------------------------------------- |
28
- | πŸ–ΌοΈ **이미지 μ „μš©** | 이미지 μ—…λ‘œλ“œ/λ“œλž˜κ·Έμ•€λ“œλ‘­λ§Œ 지원 (λΉ„λ””μ˜€/μ˜€λ””μ˜€/파일 제거) |
29
- | ☁️ **S3 연동** | Presigned URL 기반 S3 μ—…λ‘œλ“œ λ‚΄μž₯ |
30
- | 🎯 **μ»€μŠ€ν…€ μ—…λ‘œλ”** | 자체 μ—…λ‘œλ“œ 둜직 적용 κ°€λŠ₯ |
31
- | ⏳ **λ‘œλ”© μŠ€ν”Όλ„ˆ** | 이미지 μ—…λ‘œλ“œ 쀑 μžλ™ μŠ€ν”Όλ„ˆ ν‘œμ‹œ |
32
- | πŸš€ **μ• λ‹ˆλ©”μ΄μ…˜ μ΅œμ ν™”** | κΈ°λ³Έ μ• λ‹ˆλ©”μ΄μ…˜ λΉ„ν™œμ„±ν™”λ‘œ μ„±λŠ₯ ν–₯상 |
33
- | πŸ“ **TypeScript** | μ™„μ „ν•œ νƒ€μž… μ•ˆμ „μ„± |
34
- | 🎨 **ν…Œλ§ˆ 지원** | 라이트/닀크 ν…Œλ§ˆ 및 μ»€μŠ€ν…€ ν…Œλ§ˆ 지원 |
35
- | πŸ“± **λ°˜μ‘ν˜•** | λͺ¨λ°”일/λ°μŠ€ν¬ν†± μ΅œμ ν™” |
29
+ | νŠΉμ§• | μ„€λͺ… |
30
+ | ----------------------- | ------------------------------------------------------ |
31
+ | **이미지 μ „μš©** | 이미지 μ—…λ‘œλ“œ/λ“œλž˜κ·Έμ•€λ“œλ‘­λ§Œ 지원 (λΉ„λ””μ˜€/μ˜€λ””μ˜€ 제거) |
32
+ | **S3 연동** | Presigned URL 기반 S3 μ—…λ‘œλ“œ λ‚΄μž₯ |
33
+ | **파일λͺ… μ»€μŠ€ν„°λ§ˆμ΄μ§•** | μ—…λ‘œλ“œ 파일λͺ… λ³€κ²½ 콜백 + UUID μžλ™ μΆ”κ°€ 지원 |
34
+ | **λ‘œλ”© μŠ€ν”Όλ„ˆ** | 이미지 μ—…λ‘œλ“œ 쀑 μžλ™ μŠ€ν”Όλ„ˆ ν‘œμ‹œ |
35
+ | **μ„±λŠ₯ μ΅œμ ν™”** | μ• λ‹ˆλ©”μ΄μ…˜ λΉ„ν™œμ„±ν™”λ‘œ λΉ λ₯Έ λ Œλ”λ§ |
36
+ | **TypeScript** | μ™„μ „ν•œ νƒ€μž… μ•ˆμ „μ„± |
37
+ | **ν…Œλ§ˆ 지원** | 라이트/닀크 ν…Œλ§ˆ 및 μ»€μŠ€ν…€ ν…Œλ§ˆ |
36
38
 
37
39
  ### 지원 이미지 ν˜•μ‹
38
40
 
39
41
  ```
40
- PNG, JPEG/JPG, GIF (μ• λ‹ˆλ©”μ΄μ…˜ 포함), WebP, BMP, SVG
42
+ PNG, JPEG/JPG, GIF, WebP, BMP, SVG
41
43
  ```
42
44
 
43
45
  ---
44
46
 
45
- ## πŸ“¦ μ„€μΉ˜
47
+ ## λΉ λ₯Έ μ‹œμž‘
48
+
49
+ ### 1. μ„€μΉ˜
46
50
 
47
51
  ```bash
48
- # npm
49
52
  npm install @lumir-company/editor
50
-
51
- # yarn
53
+ # λ˜λŠ”
52
54
  yarn add @lumir-company/editor
53
-
54
- # pnpm
55
- pnpm add @lumir-company/editor
56
- ```
57
-
58
- ### Peer Dependencies
59
-
60
- ```json
61
- {
62
- "react": ">=18.0.0",
63
- "react-dom": ">=18.0.0"
64
- }
65
55
  ```
66
56
 
67
- ---
68
-
69
- ## πŸš€ λΉ λ₯Έ μ‹œμž‘
70
-
71
- ### 1단계: CSS μž„ν¬νŠΈ (ν•„μˆ˜)
72
-
73
- ```tsx
74
- import "@lumir-company/editor/style.css";
75
- ```
57
+ **ν•„μˆ˜ Peer Dependencies:**
76
58
 
77
- > ⚠️ **μ€‘μš”**: CSSλ₯Ό μž„ν¬νŠΈν•˜μ§€ μ•ŠμœΌλ©΄ 에디터가 μ •μƒμ μœΌλ‘œ λ Œλ”λ§λ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
59
+ - `react` >= 18.0.0
60
+ - `react-dom` >= 18.0.0
78
61
 
79
- ### 2단계: κΈ°λ³Έ μ‚¬μš©
62
+ ### 2. κΈ°λ³Έ μ‚¬μš©
80
63
 
81
64
  ```tsx
82
65
  import { LumirEditor } from "@lumir-company/editor";
83
- import "@lumir-company/editor/style.css";
66
+ import "@lumir-company/editor/style.css"; // ν•„μˆ˜!
84
67
 
85
68
  export default function App() {
86
69
  return (
87
- <div className="w-full h-[400px]">
70
+ <div className="w-full h-[500px]">
88
71
  <LumirEditor onContentChange={(blocks) => console.log(blocks)} />
89
72
  </div>
90
73
  );
91
74
  }
92
75
  ```
93
76
 
94
- ### 3단계: Next.jsμ—μ„œ μ‚¬μš© (SSR λΉ„ν™œμ„±ν™” ν•„μˆ˜)
77
+ > **μ€‘μš”**: `style.css`λ₯Ό μž„ν¬νŠΈν•˜μ§€ μ•ŠμœΌλ©΄ 에디터가 정상 μž‘λ™ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
78
+
79
+ ### 3. Next.jsμ—μ„œ μ‚¬μš©
95
80
 
96
81
  ```tsx
97
82
  "use client";
@@ -99,6 +84,7 @@ export default function App() {
99
84
  import dynamic from "next/dynamic";
100
85
  import "@lumir-company/editor/style.css";
101
86
 
87
+ // SSR λΉ„ν™œμ„±ν™” ν•„μˆ˜
102
88
  const LumirEditor = dynamic(
103
89
  () =>
104
90
  import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
@@ -107,10 +93,8 @@ const LumirEditor = dynamic(
107
93
 
108
94
  export default function EditorPage() {
109
95
  return (
110
- <div className="w-full h-[500px]">
111
- <LumirEditor
112
- onContentChange={(blocks) => console.log("Content:", blocks)}
113
- />
96
+ <div className="h-[500px]">
97
+ <LumirEditor />
114
98
  </div>
115
99
  );
116
100
  }
@@ -118,108 +102,188 @@ export default function EditorPage() {
118
102
 
119
103
  ---
120
104
 
121
- ## πŸ“š Props 레퍼런슀
122
-
123
- ### 에디터 μ˜΅μ…˜ (Editor Options)
124
-
125
- | Prop | νƒ€μž… | κΈ°λ³Έκ°’ | μ„€λͺ… |
126
- | -------------------- | ----------------------------------------- | --------------------------- | ---------------------------------------- |
127
- | `initialContent` | `DefaultPartialBlock[] \| string` | `undefined` | 초기 μ½˜ν…μΈ  (블둝 λ°°μ—΄ λ˜λŠ” JSON λ¬Έμžμ—΄) |
128
- | `initialEmptyBlocks` | `number` | `3` | 초기 빈 블둝 개수 |
129
- | `placeholder` | `string` | `undefined` | 첫 번째 λΈ”λ‘μ˜ placeholder ν…μŠ€νŠΈ |
130
- | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | μ»€μŠ€ν…€ 파일 μ—…λ‘œλ“œ ν•¨μˆ˜ |
131
- | `s3Upload` | `S3UploaderConfig` | `undefined` | S3 μ—…λ‘œλ“œ μ„€μ • |
132
- | `tables` | `TableConfig` | `{...}` | ν…Œμ΄λΈ” κΈ°λŠ₯ μ„€μ • |
133
- | `heading` | `{ levels?: (1\|2\|3\|4\|5\|6)[] }` | `{ levels: [1,2,3,4,5,6] }` | ν—€λ”© 레벨 μ„€μ • |
134
- | `defaultStyles` | `boolean` | `true` | κΈ°λ³Έ μŠ€νƒ€μΌ ν™œμ„±ν™” |
135
- | `disableExtensions` | `string[]` | `[]` | λΉ„ν™œμ„±ν™”ν•  ν™•μž₯ κΈ°λŠ₯ λͺ©λ‘ |
136
- | `tabBehavior` | `"prefer-navigate-ui" \| "prefer-indent"` | `"prefer-navigate-ui"` | νƒ­ ν‚€ λ™μž‘ |
137
- | `trailingBlock` | `boolean` | `true` | λ§ˆμ§€λ§‰μ— 빈 블둝 μžλ™ μΆ”κ°€ |
138
- | `allowVideoUpload` | `boolean` | `false` | λΉ„λ””μ˜€ μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ λΉ„ν™œμ„±) |
139
- | `allowAudioUpload` | `boolean` | `false` | μ˜€λ””μ˜€ μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ λΉ„ν™œμ„±) |
140
- | `allowFileUpload` | `boolean` | `false` | 일반 파일 μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ λΉ„ν™œμ„±) |
141
-
142
- ### λ·° μ˜΅μ…˜ (View Options)
143
-
144
- | Prop | νƒ€μž… | κΈ°λ³Έκ°’ | μ„€λͺ… |
145
- | ------------------- | ---------------------------------- | --------- | ---------------------------------------------------- |
146
- | `editable` | `boolean` | `true` | νŽΈμ§‘ κ°€λŠ₯ μ—¬λΆ€ |
147
- | `theme` | `"light" \| "dark" \| ThemeObject` | `"light"` | 에디터 ν…Œλ§ˆ |
148
- | `formattingToolbar` | `boolean` | `true` | μ„œμ‹ νˆ΄λ°” ν‘œμ‹œ |
149
- | `linkToolbar` | `boolean` | `true` | 링크 νˆ΄λ°” ν‘œμ‹œ |
150
- | `sideMenu` | `boolean` | `true` | μ‚¬μ΄λ“œ 메뉴 ν‘œμ‹œ |
151
- | `sideMenuAddButton` | `boolean` | `false` | μ‚¬μ΄λ“œ 메뉴 + λ²„νŠΌ ν‘œμ‹œ (falseμ‹œ λ“œλž˜κ·Έ ν•Έλ“€λ§Œ ν‘œμ‹œ) |
152
- | `emojiPicker` | `boolean` | `true` | 이λͺ¨μ§€ 선택기 ν‘œμ‹œ |
153
- | `filePanel` | `boolean` | `true` | 파일 νŒ¨λ„ ν‘œμ‹œ |
154
- | `tableHandles` | `boolean` | `true` | ν…Œμ΄λΈ” ν•Έλ“€ ν‘œμ‹œ |
155
- | `className` | `string` | `""` | μ»¨ν…Œμ΄λ„ˆ CSS 클래슀 |
156
-
157
- ### 콜백 (Callbacks)
158
-
159
- | Prop | νƒ€μž… | μ„€λͺ… |
160
- | ------------------- | ----------------------------------------- | ---------------------- |
161
- | `onContentChange` | `(blocks: DefaultPartialBlock[]) => void` | μ½˜ν…μΈ  λ³€κ²½ μ‹œ 호좜 |
162
- | `onSelectionChange` | `() => void` | 선택 μ˜μ—­ λ³€κ²½ μ‹œ 호좜 |
105
+ ## 이미지 μ—…λ‘œλ“œ
163
106
 
164
- ### S3UploaderConfig
107
+ ### 1. S3 μ—…λ‘œλ“œ (ꢌμž₯)
108
+
109
+ Presigned URL을 μ‚¬μš©ν•œ μ•ˆμ „ν•œ S3 μ—…λ‘œλ“œ λ°©μ‹μž…λ‹ˆλ‹€.
165
110
 
166
111
  ```tsx
167
- interface S3UploaderConfig {
168
- apiEndpoint: string; // Presigned URL API μ—”λ“œν¬μΈνŠΈ (ν•„μˆ˜)
169
- env: "development" | "production"; // ν™˜κ²½ (ν•„μˆ˜)
170
- path: string; // S3 경둜 (ν•„μˆ˜)
171
- }
112
+ <LumirEditor
113
+ s3Upload={{
114
+ apiEndpoint: "/api/s3/presigned",
115
+ env: "production",
116
+ path: "blog/images",
117
+ }}
118
+ />
172
119
  ```
173
120
 
174
- ### TableConfig
121
+ #### S3 파일 μ €μž₯ 경둜
175
122
 
176
- ```tsx
177
- interface TableConfig {
178
- splitCells?: boolean; // μ…€ λΆ„ν•  (κΈ°λ³Έ: true)
179
- cellBackgroundColor?: boolean; // μ…€ 배경색 (κΈ°λ³Έ: true)
180
- cellTextColor?: boolean; // μ…€ ν…μŠ€νŠΈ 색상 (κΈ°λ³Έ: true)
181
- headers?: boolean; // 헀더 ν–‰ (κΈ°λ³Έ: true)
123
+ ```
124
+ {env}/{path}/{filename}
125
+
126
+ μ˜ˆμ‹œ:
127
+ production/blog/images/my-photo.png
128
+ ```
129
+
130
+ #### API μ—”λ“œν¬μΈνŠΈ 응닡 ν˜•μ‹
131
+
132
+ μ„œλ²„λŠ” λ‹€μŒ ν˜•μ‹μœΌλ‘œ 응닡해야 ν•©λ‹ˆλ‹€:
133
+
134
+ ```json
135
+ {
136
+ "presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
137
+ "publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
182
138
  }
183
139
  ```
184
140
 
185
141
  ---
186
142
 
187
- ## πŸ–ΌοΈ 이미지 μ—…λ‘œλ“œ
143
+ ### 파일λͺ… μ»€μŠ€ν„°λ§ˆμ΄μ§•
188
144
 
189
- ### 방법 1: S3 μ—…λ‘œλ“œ (ꢌμž₯)
145
+ μ—¬λŸ¬ 이미지λ₯Ό λ™μ‹œμ— μ—…λ‘œλ“œν•  λ•Œ 파일λͺ… 쀑볡을 λ°©μ§€ν•˜κ³  κ΄€λ¦¬ν•˜κΈ° μ‰½κ²Œ λ§Œλ“œλŠ” κΈ°λŠ₯μž…λ‹ˆλ‹€.
190
146
 
191
- Presigned URL을 μ‚¬μš©ν•œ μ•ˆμ „ν•œ S3 μ—…λ‘œλ“œ λ°©μ‹μž…λ‹ˆλ‹€.
147
+ > **μ°Έκ³ **: 기본적으둜 ν™•μž₯μžλŠ” μžλ™μœΌλ‘œ λΆ™μŠ΅λ‹ˆλ‹€. `preserveExtension: false`둜 μ„€μ •ν•˜λ©΄ ν™•μž₯자λ₯Ό 뢙이지 μ•ŠμŠ΅λ‹ˆλ‹€.
148
+
149
+ #### μ˜΅μ…˜ 1: UUID μžλ™ μΆ”κ°€
192
150
 
193
151
  ```tsx
194
152
  <LumirEditor
195
153
  s3Upload={{
196
154
  apiEndpoint: "/api/s3/presigned",
197
- env: "development",
198
- path: "blog/images",
155
+ env: "production",
156
+ path: "uploads",
157
+ appendUUID: true, // 파일λͺ… 뒀에 UUID μžλ™ μΆ”κ°€
199
158
  }}
200
- onContentChange={(blocks) => console.log(blocks)}
201
159
  />
202
160
  ```
203
161
 
204
- **S3 파일 μ €μž₯ 경둜 ꡬ쑰:**
162
+ **κ²°κ³Ό:**
205
163
 
206
164
  ```
207
- {env}/{path}/{filename}
208
- 예: development/blog/images/my-image.png
165
+ 원본: photo.png
166
+ μ—…λ‘œλ“œ: photo_550e8400-e29b-41d4-a716-446655440000.png
209
167
  ```
210
168
 
211
- **API μ—”λ“œν¬μΈνŠΈ 응닡 μ˜ˆμ‹œ:**
169
+ #### μ˜΅μ…˜ 2: 파일λͺ… λ³€ν™˜ 콜백
212
170
 
213
- ```json
214
- {
215
- "presignedUrl": "https://s3.amazonaws.com/bucket/...",
216
- "publicUrl": "https://cdn.example.com/development/blog/images/my-image.png"
171
+ ```tsx
172
+ <LumirEditor
173
+ s3Upload={{
174
+ apiEndpoint: "/api/s3/presigned",
175
+ env: "production",
176
+ path: "uploads",
177
+ fileNameTransform: (nameWithoutExt, file) => {
178
+ // nameWithoutExtλŠ” ν™•μž₯μžκ°€ 제거된 파일λͺ… (예: "photo")
179
+ // ν™•μž₯μžλŠ” μžλ™μœΌλ‘œ λΆ™μŠ΅λ‹ˆλ‹€
180
+ const userId = getCurrentUserId();
181
+ return `${userId}_${nameWithoutExt}`;
182
+ },
183
+ }}
184
+ />
185
+ ```
186
+
187
+ **κ²°κ³Ό:**
188
+
189
+ ```
190
+ 원본: photo.png
191
+ β†’ nameWithoutExt: "photo"
192
+ β†’ λ³€ν™˜ ν›„: "user123_photo"
193
+ β†’ μ΅œμ’…: user123_photo.png
194
+ ```
195
+
196
+ #### μ˜΅μ…˜ 3: μ‘°ν•© μ‚¬μš© (ꢌμž₯)
197
+
198
+ ```tsx
199
+ <LumirEditor
200
+ s3Upload={{
201
+ apiEndpoint: "/api/s3/presigned",
202
+ env: "production",
203
+ path: "uploads",
204
+ fileNameTransform: (nameWithoutExt) => `user123_${nameWithoutExt}`,
205
+ appendUUID: true, // λ³€ν™˜ ν›„ UUID μΆ”κ°€
206
+ }}
207
+ />
208
+ ```
209
+
210
+ **κ²°κ³Ό:**
211
+
212
+ ```
213
+ 원본: photo.png
214
+ β†’ nameWithoutExt: "photo"
215
+ 1. fileNameTransform 적용: "user123_photo"
216
+ 2. appendUUID 적용: "user123_photo_550e8400-e29b-41d4"
217
+ 3. ν™•μž₯자 뢙이기: user123_photo_550e8400-e29b-41d4.png
218
+ ```
219
+
220
+ #### μ‹€μ „ 예제: νƒ€μž„μŠ€νƒ¬ν”„ + UUID
221
+
222
+ ```tsx
223
+ function MyEditor() {
224
+ return (
225
+ <LumirEditor
226
+ s3Upload={{
227
+ apiEndpoint: "/api/s3/presigned",
228
+ env: "production",
229
+ path: "uploads",
230
+ fileNameTransform: (nameWithoutExt, file) => {
231
+ // nameWithoutExtλŠ” 이미 ν™•μž₯μžκ°€ 제거됨
232
+ const timestamp = new Date().toISOString().split("T")[0]; // 2024-01-15
233
+ return `${timestamp}_${nameWithoutExt}`;
234
+ },
235
+ appendUUID: true,
236
+ }}
237
+ />
238
+ );
217
239
  }
218
240
  ```
219
241
 
220
- ### 방법 2: μ»€μŠ€ν…€ μ—…λ‘œλ”
242
+ **κ²°κ³Ό:**
243
+
244
+ ```
245
+ 원본: photo.png
246
+ β†’ nameWithoutExt: "photo"
247
+ 1. fileNameTransform: "2024-01-15_photo"
248
+ 2. appendUUID: "2024-01-15_photo_550e8400-e29b-41d4"
249
+ 3. ν™•μž₯자 뢙이기: 2024-01-15_photo_550e8400-e29b-41d4.png
250
+ ```
221
251
 
222
- 자체 μ—…λ‘œλ“œ λ‘œμ§μ„ μ‚¬μš©ν•  λ•Œ ν™œμš©ν•©λ‹ˆλ‹€.
252
+ #### μ˜΅μ…˜ 4: ν™•μž₯자 제거 (preserveExtension: false)
253
+
254
+ ```tsx
255
+ <LumirEditor
256
+ s3Upload={{
257
+ apiEndpoint: "/api/s3/presigned",
258
+ env: "production",
259
+ path: "uploads",
260
+ fileNameTransform: (nameWithoutExt) => `${nameWithoutExt}_custom`,
261
+ preserveExtension: false, // ν™•μž₯자 μ•ˆ λΆ™μž„
262
+ }}
263
+ />
264
+ ```
265
+
266
+ **κ²°κ³Ό:**
267
+
268
+ ```
269
+ 원본: photo.png
270
+ β†’ nameWithoutExt: "photo"
271
+ β†’ λ³€ν™˜ ν›„: "photo_custom"
272
+ β†’ μ΅œμ’…: photo_custom (ν™•μž₯자 μ—†μŒ)
273
+ ```
274
+
275
+ **μ‚¬μš© 사둀**: WebP λ³€ν™˜ λ“± μ„œλ²„μ—μ„œ ν™•μž₯자λ₯Ό λ³€κ²½ν•˜λŠ” 경우
276
+
277
+ ```tsx
278
+ fileNameTransform: (nameWithoutExt) => `${nameWithoutExt}.webp`,
279
+ preserveExtension: false,
280
+ ```
281
+
282
+ ---
283
+
284
+ ### 2. μ»€μŠ€ν…€ μ—…λ‘œλ”
285
+
286
+ 자체 μ—…λ‘œλ“œ λ‘œμ§μ„ μ‚¬μš©ν•  λ•Œ:
223
287
 
224
288
  ```tsx
225
289
  <LumirEditor
@@ -232,24 +296,22 @@ Presigned URL을 μ‚¬μš©ν•œ μ•ˆμ „ν•œ S3 μ—…λ‘œλ“œ λ°©μ‹μž…λ‹ˆλ‹€.
232
296
  body: formData,
233
297
  });
234
298
 
235
- const data = await response.json();
236
- return data.url; // μ—…λ‘œλ“œλœ μ΄λ―Έμ§€μ˜ URL λ°˜ν™˜
299
+ const { url } = await response.json();
300
+ return url; // μ—…λ‘œλ“œλœ 이미지 URL λ°˜ν™˜
237
301
  }}
238
302
  />
239
303
  ```
240
304
 
241
- ### 방법 3: createS3Uploader 헬퍼 ν•¨μˆ˜
242
-
243
- S3 μ—…λ‘œλ”λ₯Ό 직접 μƒμ„±ν•˜μ—¬ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
305
+ ### 3. 헬퍼 ν•¨μˆ˜ μ‚¬μš©
244
306
 
245
307
  ```tsx
246
- import { LumirEditor, createS3Uploader } from "@lumir-company/editor";
308
+ import { createS3Uploader } from "@lumir-company/editor";
247
309
 
248
- // S3 μ—…λ‘œλ” 생성
249
310
  const s3Uploader = createS3Uploader({
250
311
  apiEndpoint: "/api/s3/presigned",
251
312
  env: "production",
252
- path: "uploads/images",
313
+ path: "images",
314
+ appendUUID: true,
253
315
  });
254
316
 
255
317
  // 에디터에 적용
@@ -262,203 +324,97 @@ const imageUrl = await s3Uploader(imageFile);
262
324
  ### μ—…λ‘œλ“œ μš°μ„ μˆœμœ„
263
325
 
264
326
  1. `uploadFile` prop이 있으면 μš°μ„  μ‚¬μš©
265
- 2. `uploadFile`이 μ—†κ³  `s3Upload`κ°€ 있으면 S3 μ—…λ‘œλ“œ μ‚¬μš©
327
+ 2. `uploadFile` μ—†κ³  `s3Upload`κ°€ 있으면 S3 μ—…λ‘œλ“œ μ‚¬μš©
266
328
  3. λ‘˜ λ‹€ μ—†μœΌλ©΄ μ—…λ‘œλ“œ μ‹€νŒ¨
267
329
 
268
330
  ---
269
331
 
270
- ## πŸ› οΈ μœ ν‹Έλ¦¬ν‹° API
332
+ ## Props API
271
333
 
272
- ### ContentUtils
334
+ ### 핡심 Props
273
335
 
274
- μ½˜ν…μΈ  관리 μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€μž…λ‹ˆλ‹€.
336
+ | Prop | νƒ€μž… | κΈ°λ³Έκ°’ | μ„€λͺ… |
337
+ | ----------------- | --------------------------------- | ----------- | ------------------ |
338
+ | `s3Upload` | `S3UploaderConfig` | `undefined` | S3 μ—…λ‘œλ“œ μ„€μ • |
339
+ | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | μ»€μŠ€ν…€ μ—…λ‘œλ“œ ν•¨μˆ˜ |
340
+ | `onContentChange` | `(blocks) => void` | `undefined` | μ½˜ν…μΈ  λ³€κ²½ 콜백 |
341
+ | `initialContent` | `Block[] \| string` | `undefined` | 초기 μ½˜ν…μΈ  |
342
+ | `editable` | `boolean` | `true` | νŽΈμ§‘ κ°€λŠ₯ μ—¬λΆ€ |
343
+ | `theme` | `"light" \| "dark"` | `"light"` | ν…Œλ§ˆ |
344
+ | `className` | `string` | `""` | CSS 클래슀 |
275
345
 
276
- ```tsx
277
- import { ContentUtils } from "@lumir-company/editor";
278
-
279
- // JSON λ¬Έμžμ—΄ μœ νš¨μ„± 검증
280
- const isValid = ContentUtils.isValidJSONString('[{"type":"paragraph"}]');
281
- // true
282
-
283
- // JSON λ¬Έμžμ—΄μ„ 블둝 λ°°μ—΄λ‘œ νŒŒμ‹±
284
- const blocks = ContentUtils.parseJSONContent(jsonString);
285
- // DefaultPartialBlock[] | null
286
-
287
- // κΈ°λ³Έ 빈 블둝 생성
288
- const emptyBlock = ContentUtils.createDefaultBlock();
289
- // { type: "paragraph", props: {...}, content: [...], children: [] }
290
-
291
- // μ½˜ν…μΈ  μœ νš¨μ„± 검증 및 κΈ°λ³Έκ°’ μ„€μ •
292
- const validatedContent = ContentUtils.validateContent(content, 3);
293
- // 빈 μ½˜ν…μΈ λ©΄ 3개의 빈 블둝 λ°˜ν™˜
294
- ```
295
-
296
- ### EditorConfig
297
-
298
- 에디터 μ„€μ • μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€μž…λ‹ˆλ‹€.
299
-
300
- ```tsx
301
- import { EditorConfig } from "@lumir-company/editor";
302
-
303
- // ν…Œμ΄λΈ” κΈ°λ³Έ μ„€μ • κ°€μ Έμ˜€κΈ°
304
- const tableConfig = EditorConfig.getDefaultTableConfig({
305
- splitCells: true,
306
- headers: false,
307
- });
308
-
309
- // ν—€λ”© κΈ°λ³Έ μ„€μ • κ°€μ Έμ˜€κΈ°
310
- const headingConfig = EditorConfig.getDefaultHeadingConfig({
311
- levels: [1, 2, 3],
312
- });
313
-
314
- // λΉ„ν™œμ„±ν™” ν™•μž₯ λͺ©λ‘ 생성
315
- const disabledExt = EditorConfig.getDisabledExtensions(
316
- ["codeBlock"], // μ‚¬μš©μž μ •μ˜ λΉ„ν™œμ„± ν™•μž₯
317
- false, // allowVideo
318
- false, // allowAudio
319
- false // allowFile
320
- );
321
- // ["codeBlock", "video", "audio", "file"]
322
- ```
323
-
324
- ### cn (className μœ ν‹Έλ¦¬ν‹°)
325
-
326
- 쑰건뢀 className κ²°ν•© μœ ν‹Έλ¦¬ν‹°μž…λ‹ˆλ‹€.
346
+ ### S3UploaderConfig
327
347
 
328
348
  ```tsx
329
- import { cn } from "@lumir-company/editor";
330
-
331
- <LumirEditor
332
- className={cn(
333
- "min-h-[400px] rounded-lg",
334
- isFullscreen && "fixed inset-0 z-50",
335
- isDarkMode && "dark-theme"
336
- )}
337
- />;
349
+ interface S3UploaderConfig {
350
+ // ν•„μˆ˜
351
+ apiEndpoint: string; // Presigned URL API μ—”λ“œν¬μΈνŠΈ
352
+ env: "development" | "production";
353
+ path: string; // S3 μ €μž₯ 경둜
354
+
355
+ // 선택 (파일λͺ… μ»€μŠ€ν„°λ§ˆμ΄μ§•)
356
+ fileNameTransform?: (nameWithoutExt: string, file: File) => string; // ν™•μž₯자 μ œμ™Έν•œ 이름 λ³€ν™˜
357
+ appendUUID?: boolean; // true: 파일λͺ… 뒀에 UUID μΆ”κ°€ (ν™•μž₯자 μ•žμ— μ‚½μž…)
358
+ preserveExtension?: boolean; // false: ν™•μž₯자λ₯Ό 뢙이지 μ•ŠμŒ (κΈ°λ³Έ: true)
359
+ }
338
360
  ```
339
361
 
340
- ---
341
-
342
- ## πŸ“– νƒ€μž… μ •μ˜
343
-
344
- ### μ£Όμš” νƒ€μž… import
362
+ ### 전체 Props
345
363
 
346
- ```tsx
347
- import type {
348
- // 에디터 Props
349
- LumirEditorProps,
350
-
351
- // 에디터 μΈμŠ€ν„΄μŠ€ νƒ€μž…
352
- EditorType,
353
-
354
- // 블둝 κ΄€λ ¨ νƒ€μž…
355
- DefaultPartialBlock,
356
- DefaultBlockSchema,
357
- DefaultInlineContentSchema,
358
- DefaultStyleSchema,
359
- PartialBlock,
360
- BlockNoteEditor,
361
- } from "@lumir-company/editor";
362
-
363
- import type { S3UploaderConfig } from "@lumir-company/editor";
364
- ```
365
-
366
- ### LumirEditorProps 전체 μΈν„°νŽ˜μ΄μŠ€
364
+ <details>
365
+ <summary>전체 Props 보기</summary>
367
366
 
368
367
  ```tsx
369
368
  interface LumirEditorProps {
370
- // === Editor Options ===
371
- initialContent?: DefaultPartialBlock[] | string;
372
- initialEmptyBlocks?: number;
373
- placeholder?: string;
374
- uploadFile?: (file: File) => Promise<string>;
369
+ // === 에디터 μ„€μ • ===
370
+ initialContent?: DefaultPartialBlock[] | string; // 초기 μ½˜ν…μΈ  (블둝 λ°°μ—΄ λ˜λŠ” JSON λ¬Έμžμ—΄)
371
+ initialEmptyBlocks?: number; // 초기 빈 블둝 개수 (κΈ°λ³Έ: 3)
372
+ uploadFile?: (file: File) => Promise<string>; // μ»€μŠ€ν…€ 파일 μ—…λ‘œλ“œ ν•¨μˆ˜
375
373
  s3Upload?: {
376
374
  apiEndpoint: string;
377
375
  env: "development" | "production";
378
376
  path: string;
377
+ fileNameTransform?: (nameWithoutExt: string, file: File) => string; // ν™•μž₯자 μ œμ™Έν•œ 이름 λ³€ν™˜
378
+ appendUUID?: boolean; // UUID μžλ™ μΆ”κ°€ (ν™•μž₯자 μ•ž)
379
+ preserveExtension?: boolean; // ν™•μž₯자 μžλ™ 뢙이기 (κΈ°λ³Έ: true)
379
380
  };
380
- allowVideoUpload?: boolean;
381
- allowAudioUpload?: boolean;
382
- allowFileUpload?: boolean;
383
- tables?: {
384
- splitCells?: boolean;
385
- cellBackgroundColor?: boolean;
386
- cellTextColor?: boolean;
387
- headers?: boolean;
388
- };
389
- heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] };
390
- defaultStyles?: boolean;
391
- disableExtensions?: string[];
392
- tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
393
- trailingBlock?: boolean;
394
-
395
- // === View Options ===
396
- editable?: boolean;
397
- theme?:
398
- | "light"
399
- | "dark"
400
- | Partial<Record<string, unknown>>
401
- | {
402
- light: Partial<Record<string, unknown>>;
403
- dark: Partial<Record<string, unknown>>;
404
- };
405
- formattingToolbar?: boolean;
406
- linkToolbar?: boolean;
407
- sideMenu?: boolean;
408
- sideMenuAddButton?: boolean;
409
- emojiPicker?: boolean;
410
- filePanel?: boolean;
411
- tableHandles?: boolean;
412
- onSelectionChange?: () => void;
413
- className?: string;
414
-
415
- // === Callbacks ===
416
- onContentChange?: (content: DefaultPartialBlock[]) => void;
417
- }
418
- ```
419
-
420
- ---
421
-
422
- ## πŸ’‘ μ‚¬μš© 예제
423
381
 
424
- ### κΈ°λ³Έ 에디터
425
-
426
- ```tsx
427
- import { LumirEditor } from "@lumir-company/editor";
428
- import "@lumir-company/editor/style.css";
429
-
430
- function BasicEditor() {
431
- return (
432
- <div className="h-[400px]">
433
- <LumirEditor />
434
- </div>
435
- );
382
+ // === 콜백 ===
383
+ onContentChange?: (blocks: DefaultPartialBlock[]) => void; // μ½˜ν…μΈ  λ³€κ²½ μ‹œ 호좜
384
+ onSelectionChange?: () => void; // 선택 μ˜μ—­ λ³€κ²½ μ‹œ 호좜
385
+
386
+ // κΈ°λŠ₯ μ„€μ •
387
+ tables?: TableConfig; // ν…Œμ΄λΈ” κΈ°λŠ₯ μ„€μ • (splitCells, cellBackgroundColor λ“±)
388
+ heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] }; // ν—€λ”© 레벨 μ„€μ • (κΈ°λ³Έ: [1,2,3,4,5,6])
389
+ defaultStyles?: boolean; // κΈ°λ³Έ μŠ€νƒ€μΌ ν™œμ„±ν™” (κΈ°λ³Έ: true)
390
+ disableExtensions?: string[]; // λΉ„ν™œμ„±ν™”ν•  ν™•μž₯ κΈ°λŠ₯ λͺ©λ‘
391
+ tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; // νƒ­ ν‚€ λ™μž‘ (κΈ°λ³Έ: "prefer-navigate-ui")
392
+ trailingBlock?: boolean; // λ§ˆμ§€λ§‰μ— 빈 블둝 μžλ™ μΆ”κ°€ (κΈ°λ³Έ: true)
393
+
394
+ // === UI μ„€μ • ===
395
+ editable?: boolean; // νŽΈμ§‘ κ°€λŠ₯ μ—¬λΆ€ (κΈ°λ³Έ: true)
396
+ theme?: "light" | "dark" | ThemeObject; // 에디터 ν…Œλ§ˆ (κΈ°λ³Έ: "light")
397
+ formattingToolbar?: boolean; // μ„œμ‹ νˆ΄λ°” ν‘œμ‹œ (κΈ°λ³Έ: true)
398
+ linkToolbar?: boolean; // 링크 νˆ΄λ°” ν‘œμ‹œ (κΈ°λ³Έ: true)
399
+ sideMenu?: boolean; // μ‚¬μ΄λ“œ 메뉴 ν‘œμ‹œ (κΈ°λ³Έ: true)
400
+ sideMenuAddButton?: boolean; // μ‚¬μ΄λ“œ 메뉴 + λ²„νŠΌ ν‘œμ‹œ (κΈ°λ³Έ: false, λ“œλž˜κ·Έ ν•Έλ“€λ§Œ ν‘œμ‹œ)
401
+ emojiPicker?: boolean; // 이λͺ¨μ§€ 선택기 ν‘œμ‹œ (κΈ°λ³Έ: true)
402
+ filePanel?: boolean; // 파일 νŒ¨λ„ ν‘œμ‹œ (κΈ°λ³Έ: true)
403
+ tableHandles?: boolean; // ν…Œμ΄λΈ” ν•Έλ“€ ν‘œμ‹œ (κΈ°λ³Έ: true)
404
+ className?: string; // μ»¨ν…Œμ΄λ„ˆ CSS 클래슀
405
+
406
+ // λ―Έλ””μ–΄ μ—…λ‘œλ“œ ν—ˆμš© μ—¬λΆ€ (κΈ°λ³Έ: λͺ¨λ‘ λΉ„ν™œμ„±)
407
+ allowVideoUpload?: boolean; // λΉ„λ””μ˜€ μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ: false)
408
+ allowAudioUpload?: boolean; // μ˜€λ””μ˜€ μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ: false)
409
+ allowFileUpload?: boolean; // 일반 파일 μ—…λ‘œλ“œ ν—ˆμš© (κΈ°λ³Έ: false)
436
410
  }
437
411
  ```
438
412
 
439
- ### 초기 μ½˜ν…μΈ  μ„€μ •
413
+ </details>
440
414
 
441
- ```tsx
442
- // 방법 1: 블둝 λ°°μ—΄
443
- <LumirEditor
444
- initialContent={[
445
- {
446
- type: "heading",
447
- props: { level: 1 },
448
- content: [{ type: "text", text: "제λͺ©μž…λ‹ˆλ‹€", styles: {} }],
449
- },
450
- {
451
- type: "paragraph",
452
- content: [{ type: "text", text: "λ³Έλ¬Έ λ‚΄μš©...", styles: {} }],
453
- },
454
- ]}
455
- />
415
+ ---
456
416
 
457
- // 방법 2: JSON λ¬Έμžμ—΄
458
- <LumirEditor
459
- initialContent='[{"type":"paragraph","content":[{"type":"text","text":"Hello World","styles":{}}]}]'
460
- />
461
- ```
417
+ ## μ‚¬μš© 예제
462
418
 
463
419
  ### 읽기 μ „μš© λͺ¨λ“œ
464
420
 
@@ -477,46 +433,6 @@ function BasicEditor() {
477
433
  <LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
478
434
  ```
479
435
 
480
- ### S3 이미지 μ—…λ‘œλ“œ
481
-
482
- ```tsx
483
- <LumirEditor
484
- s3Upload={{
485
- apiEndpoint: "/api/s3/presigned",
486
- env: process.env.NODE_ENV as "development" | "production",
487
- path: "articles/images",
488
- }}
489
- onContentChange={(blocks) => {
490
- // μ €μž₯ 둜직
491
- saveToDatabase(JSON.stringify(blocks));
492
- }}
493
- />
494
- ```
495
-
496
- ### λ°˜μ‘ν˜• λ””μžμΈ
497
-
498
- ```tsx
499
- <div className="w-full h-64 md:h-96 lg:h-[600px]">
500
- <LumirEditor className="h-full rounded-md md:rounded-lg shadow-sm md:shadow-md" />
501
- </div>
502
- ```
503
-
504
- ### ν…Œμ΄λΈ” μ„€μ • μ»€μŠ€ν„°λ§ˆμ΄μ§•
505
-
506
- ```tsx
507
- <LumirEditor
508
- tables={{
509
- splitCells: true,
510
- cellBackgroundColor: true,
511
- cellTextColor: false, // μ…€ ν…μŠ€νŠΈ 색상 λΉ„ν™œμ„±
512
- headers: true,
513
- }}
514
- heading={{
515
- levels: [1, 2, 3], // H4-H6 λΉ„ν™œμ„±
516
- }}
517
- />
518
- ```
519
-
520
436
  ### μ½˜ν…μΈ  μ €μž₯ 및 뢈러였기
521
437
 
522
438
  ```tsx
@@ -524,72 +440,31 @@ import { useState, useEffect } from "react";
524
440
  import { LumirEditor, ContentUtils } from "@lumir-company/editor";
525
441
 
526
442
  function EditorWithSave() {
527
- const [content, setContent] = useState<string>("");
443
+ const [content, setContent] = useState("");
528
444
 
529
- // μ €μž₯된 μ½˜ν…μΈ  뢈러였기
445
+ // 뢈러였기
530
446
  useEffect(() => {
531
- const saved = localStorage.getItem("editor-content");
447
+ const saved = localStorage.getItem("content");
532
448
  if (saved && ContentUtils.isValidJSONString(saved)) {
533
449
  setContent(saved);
534
450
  }
535
451
  }, []);
536
452
 
537
- // μ½˜ν…μΈ  μ €μž₯
538
- const handleContentChange = (blocks) => {
539
- const jsonContent = JSON.stringify(blocks);
540
- localStorage.setItem("editor-content", jsonContent);
453
+ // μ €μž₯
454
+ const handleChange = (blocks) => {
455
+ const json = JSON.stringify(blocks);
456
+ localStorage.setItem("content", json);
541
457
  };
542
458
 
543
459
  return (
544
- <LumirEditor
545
- initialContent={content}
546
- onContentChange={handleContentChange}
547
- />
460
+ <LumirEditor initialContent={content} onContentChange={handleChange} />
548
461
  );
549
462
  }
550
463
  ```
551
464
 
552
465
  ---
553
466
 
554
- ## 🎨 μŠ€νƒ€μΌλ§ κ°€μ΄λ“œ
555
-
556
- ### 기본 CSS ꡬ쑰
557
-
558
- ```css
559
- /* 메인 μ»¨ν…Œμ΄λ„ˆ - μŠ¬λž˜μ‹œ 메뉴 μ˜€λ²„ν”Œλ‘œμš° ν—ˆμš© */
560
- .lumirEditor {
561
- width: 100%;
562
- height: 100%;
563
- min-width: 200px;
564
- overflow: visible; /* μŠ¬λž˜μ‹œ 메뉴가 μ»¨ν…Œμ΄λ„ˆλ₯Ό λ„˜μ–΄ ν‘œμ‹œλ˜λ„λ‘ */
565
- background-color: #ffffff;
566
- }
567
-
568
- /* 에디터 λ‚΄λΆ€ μ½˜ν…μΈ  μ˜μ—­ 슀크둀 */
569
- .lumirEditor .bn-container {
570
- overflow: auto;
571
- max-height: 100%;
572
- }
573
-
574
- /* μŠ¬λž˜μ‹œ 메뉴 z-index 보μž₯ */
575
- .bn-suggestion-menu,
576
- .bn-slash-menu,
577
- .mantine-Menu-dropdown,
578
- .mantine-Popover-dropdown {
579
- z-index: 9999 !important;
580
- }
581
-
582
- /* 에디터 λ‚΄μš© μ˜μ—­ */
583
- .lumirEditor .bn-editor {
584
- font-family: "Pretendard", "Noto Sans KR", -apple-system, sans-serif;
585
- padding: 5px 10px 0 25px;
586
- }
587
-
588
- /* 문단 블둝 */
589
- .lumirEditor [data-content-type="paragraph"] {
590
- font-size: 14px;
591
- }
592
- ```
467
+ ## μŠ€νƒ€μΌλ§
593
468
 
594
469
  ### Tailwind CSS와 ν•¨κ»˜ μ‚¬μš©
595
470
 
@@ -605,14 +480,14 @@ import { LumirEditor, cn } from "@lumir-company/editor";
605
480
  />;
606
481
  ```
607
482
 
608
- ### μ»€μŠ€ν…€ μŠ€νƒ€μΌ 적용
483
+ ### μ»€μŠ€ν…€ μŠ€νƒ€μΌ
609
484
 
610
485
  ```css
611
486
  /* globals.css */
612
487
  .my-editor .bn-editor {
613
- padding-left: 30px;
614
- padding-right: 20px;
488
+ padding: 20px 30px;
615
489
  font-size: 16px;
490
+ line-height: 1.6;
616
491
  }
617
492
 
618
493
  .my-editor [data-content-type="heading"] {
@@ -627,37 +502,37 @@ import { LumirEditor, cn } from "@lumir-company/editor";
627
502
 
628
503
  ---
629
504
 
630
- ## ⚠️ μ£Όμ˜μ‚¬ν•­ 및 νŠΈλŸ¬λΈ”μŠˆνŒ…
505
+ ## νŠΈλŸ¬λΈ”μŠˆνŒ…
631
506
 
632
507
  ### ν•„μˆ˜ 체크리슀트
633
508
 
634
- | ν•­λͺ© | 체크 |
635
- | -------------------- | ------------------------------------------- |
636
- | CSS μž„ν¬νŠΈ | `import "@lumir-company/editor/style.css";` |
637
- | μ»¨ν…Œμ΄λ„ˆ 높이 μ„€μ • | λΆ€λͺ¨ μš”μ†Œμ— 높이 μ§€μ • ν•„μˆ˜ |
638
- | Next.js SSR λΉ„ν™œμ„±ν™” | `dynamic(..., { ssr: false })` μ‚¬μš© |
639
- | React 버전 | 18.0.0 이상 ν•„μš” |
509
+ - [ ] CSS μž„ν¬νŠΈ: `import "@lumir-company/editor/style.css"`
510
+ - [ ] μ»¨ν…Œμ΄λ„ˆ 높이 μ„€μ •: λΆ€λͺ¨ μš”μ†Œμ— 높이 μ§€μ • ν•„μˆ˜
511
+ - [ ] Next.js: `dynamic(..., { ssr: false })` μ‚¬μš©
512
+ - [ ] React 버전: 18.0.0 이상
640
513
 
641
- ### 일반적인 문제 ν•΄κ²°
514
+ ### 자주 λ°œμƒν•˜λŠ” 문제
642
515
 
643
- #### 1. 에디터가 λ Œλ”λ§λ˜μ§€ μ•ŠμŒ
516
+ #### 1. 에디터가 보이지 μ•ŠμŒ
644
517
 
645
518
  ```tsx
646
- // ❌ 잘λͺ»λœ μ‚¬μš©
519
+ // 잘λͺ»λ¨
647
520
  <LumirEditor />;
648
521
 
649
- // βœ… μ˜¬λ°”λ₯Έ μ‚¬μš© - CSS μž„ν¬νŠΈ ν•„μš”
522
+ // μ˜¬λ°”λ¦„
650
523
  import "@lumir-company/editor/style.css";
651
- <LumirEditor />;
524
+ <div className="h-[400px]">
525
+ <LumirEditor />
526
+ </div>;
652
527
  ```
653
528
 
654
- #### 2. Next.jsμ—μ„œ hydration 였λ₯˜
529
+ #### 2. Next.js Hydration 였λ₯˜
655
530
 
656
531
  ```tsx
657
- // ❌ 잘λͺ»λœ μ‚¬μš©
532
+ // 잘λͺ»λ¨
658
533
  import { LumirEditor } from "@lumir-company/editor";
659
534
 
660
- // βœ… μ˜¬λ°”λ₯Έ μ‚¬μš© - dynamic import μ‚¬μš©
535
+ // μ˜¬λ°”λ¦„
661
536
  const LumirEditor = dynamic(
662
537
  () =>
663
538
  import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
@@ -665,78 +540,95 @@ const LumirEditor = dynamic(
665
540
  );
666
541
  ```
667
542
 
668
- #### 3. 높이가 0으둜 ν‘œμ‹œλ¨
543
+ #### 3. 이미지 μ—…λ‘œλ“œ μ‹€νŒ¨
669
544
 
670
545
  ```tsx
671
- // ❌ 잘λͺ»λœ μ‚¬μš©
672
- <LumirEditor />
673
-
674
- // βœ… μ˜¬λ°”λ₯Έ μ‚¬μš© - λΆ€λͺ¨ μš”μ†Œμ— 높이 μ„€μ •
675
- <div className="h-[400px]">
676
- <LumirEditor />
677
- </div>
546
+ // uploadFile λ˜λŠ” s3Upload 쀑 ν•˜λ‚˜λŠ” λ°˜λ“œμ‹œ μ„€μ •!
547
+ <LumirEditor
548
+ s3Upload={{
549
+ apiEndpoint: "/api/s3/presigned",
550
+ env: "development",
551
+ path: "images",
552
+ }}
553
+ />
678
554
  ```
679
555
 
680
- #### 4. 이미지 μ—…λ‘œλ“œ μ‹€νŒ¨
556
+ #### 4. μ—¬λŸ¬ 이미지 μ—…λ‘œλ“œ μ‹œ 쀑볡 문제
681
557
 
682
558
  ```tsx
683
- // uploadFile λ˜λŠ” s3Upload 쀑 ν•˜λ‚˜ λ°˜λ“œμ‹œ μ„€μ •
559
+ // ν•΄κ²°: appendUUID μ‚¬μš©
684
560
  <LumirEditor
685
- uploadFile={async (file) => {
686
- // μ—…λ‘œλ“œ 둜직
687
- return imageUrl;
688
- }}
689
- // λ˜λŠ”
690
561
  s3Upload={{
691
562
  apiEndpoint: "/api/s3/presigned",
692
- env: "development",
563
+ env: "production",
693
564
  path: "images",
565
+ appendUUID: true, // κ³ μœ ν•œ 파일λͺ… 보μž₯
694
566
  }}
695
567
  />
696
568
  ```
697
569
 
698
- ### μ„±λŠ₯ μ΅œμ ν™” 팁
570
+ ---
699
571
 
700
- 1. **μ• λ‹ˆλ©”μ΄μ…˜ κΈ°λ³Έ λΉ„ν™œμ„±**: 이미 `animations: false`둜 μ„€μ •λ˜μ–΄ μ„±λŠ₯ μ΅œμ ν™”λ¨
701
- 2. **큰 μ½˜ν…μΈ  처리**: 초기 μ½˜ν…μΈ κ°€ 클 경우 lazy loading κ³ λ €
702
- 3. **이미지 μ΅œμ ν™”**: μ—…λ‘œλ“œ μ „ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 이미지 리사이징 ꢌμž₯
572
+ ## μœ ν‹Έλ¦¬ν‹° API
703
573
 
704
- ---
574
+ ### ContentUtils
705
575
 
706
- ## πŸ—οΈ ν”„λ‘œμ νŠΈ ꡬ쑰
707
-
708
- ```
709
- @lumir-company/editor/
710
- β”œβ”€β”€ dist/ # λΉŒλ“œ 좜λ ₯
711
- β”‚ β”œβ”€β”€ index.js # CommonJS λΉŒλ“œ
712
- β”‚ β”œβ”€β”€ index.mjs # ESM λΉŒλ“œ
713
- β”‚ β”œβ”€β”€ index.d.ts # TypeScript νƒ€μž… μ •μ˜
714
- β”‚ └── style.css # μŠ€νƒ€μΌμ‹œνŠΈ
715
- β”œβ”€β”€ src/
716
- β”‚ β”œβ”€β”€ components/
717
- β”‚ β”‚ └── LumirEditor.tsx # 메인 에디터 μ»΄ν¬λ„ŒνŠΈ
718
- β”‚ β”œβ”€β”€ types/
719
- β”‚ β”‚ β”œβ”€β”€ editor.ts # 에디터 νƒ€μž… μ •μ˜
720
- β”‚ β”‚ └── index.ts # νƒ€μž… export
721
- β”‚ β”œβ”€β”€ utils/
722
- β”‚ β”‚ β”œβ”€β”€ cn.ts # className μœ ν‹Έλ¦¬ν‹°
723
- β”‚ β”‚ └── s3-uploader.ts # S3 μ—…λ‘œλ”
724
- β”‚ β”œβ”€β”€ index.ts # 메인 export
725
- β”‚ └── style.css # μ†ŒμŠ€ μŠ€νƒ€μΌ
726
- └── examples/
727
- └── tailwind-integration.md # Tailwind 톡합 κ°€μ΄λ“œ
576
+ ```tsx
577
+ import { ContentUtils } from "@lumir-company/editor";
578
+
579
+ // JSON 검증
580
+ ContentUtils.isValidJSONString('[{"type":"paragraph"}]'); // true
581
+
582
+ // JSON νŒŒμ‹±
583
+ const blocks = ContentUtils.parseJSONContent(jsonString);
584
+
585
+ // κΈ°λ³Έ 블둝 생성
586
+ const emptyBlock = ContentUtils.createDefaultBlock();
728
587
  ```
729
588
 
730
- ---
589
+ ### createS3Uploader
731
590
 
732
- ## πŸ“„ λΌμ΄μ„ μŠ€
591
+ ```tsx
592
+ import { createS3Uploader } from "@lumir-company/editor";
733
593
 
734
- MIT License
594
+ const uploader = createS3Uploader({
595
+ apiEndpoint: "/api/s3/presigned",
596
+ env: "production",
597
+ path: "uploads",
598
+ appendUUID: true,
599
+ });
735
600
 
736
- ---
601
+ // 직접 μ‚¬μš©
602
+ const url = await uploader(imageFile);
603
+ ```
737
604
 
738
- ## πŸ”— κ΄€λ ¨ 링크
605
+ ## κ΄€λ ¨ 링크
739
606
 
740
- - [GitHub Repository](https://github.com/lumir-company/editor)
741
607
  - [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
742
608
  - [BlockNote Documentation](https://www.blocknotejs.org/)
609
+
610
+ ---
611
+
612
+ ## λ³€κ²½ 둜그
613
+
614
+ ### v0.4.1
615
+
616
+ - `preserveExtension` prop μΆ”κ°€ - ν™•μž₯자 μžλ™ 뢙이기 μ œμ–΄ (κΈ°λ³Έ: true)
617
+ - **μ€‘μš”**: 파일λͺ… λ³€ν™˜ μ‹œ ν™•μž₯자 μœ„μΉ˜ μˆ˜μ • (ν™•μž₯μžκ°€ 항상 맨 뒀에 μ˜€λ„λ‘)
618
+ - **Breaking Change**: `fileNameTransform` νŒŒλΌλ―Έν„° λ³€κ²½ - 이제 ν™•μž₯자 μ œμ™Έν•œ 파일λͺ…λ§Œ 전달됨
619
+ - 이전: `fileNameTransform: (originalName, file) => ...` β†’ originalName에 ν™•μž₯자 포함
620
+ - λ³€κ²½: `fileNameTransform: (nameWithoutExt, file) => ...` β†’ nameWithoutExt에 ν™•μž₯자 μ œμ™Έ
621
+ - ν™•μž₯자 제거 μ‚¬μš© 사둀 λ¬Έμ„œν™”
622
+ - README 예제 및 μ„€λͺ… κ°œμ„ 
623
+
624
+ ### v0.4.0
625
+
626
+ - 파일λͺ… λ³€ν™˜ 콜백 (`fileNameTransform`) μΆ”κ°€
627
+ - UUID μžλ™ μΆ”κ°€ μ˜΅μ…˜ (`appendUUID`) μΆ”κ°€
628
+ - μ—¬λŸ¬ 이미지 λ™μ‹œ μ—…λ‘œλ“œ μ‹œ 쀑볡 문제 ν•΄κ²°
629
+ - λ¬Έμ„œ λŒ€ν­ κ°œμ„ 
630
+
631
+ ### v0.3.3
632
+
633
+ - 에디터 μž¬μƒμ„± λ°©μ§€ μ΅œμ ν™”
634
+ - νƒ€μž… μ •μ˜ 개μ„