@lumir-company/editor 0.3.3 → 0.4.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 +302 -468
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +48 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -5,93 +5,78 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lumir-company/editor)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
+
> 이미지 업로드에 최적화된 경량 에디터. S3 업로드, 파일명 커스터마이징, 로딩 스피너 내장.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
8
12
|
## 📋 목차
|
|
9
13
|
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [📄 라이선스](#-라이선스)
|
|
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 연동**
|
|
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
|
|
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
55
|
```
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```json
|
|
61
|
-
{
|
|
62
|
-
"react": ">=18.0.0",
|
|
63
|
-
"react-dom": ">=18.0.0"
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## 🚀 빠른 시작
|
|
70
|
-
|
|
71
|
-
### 1단계: CSS 임포트 (필수)
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
import "@lumir-company/editor/style.css";
|
|
75
|
-
```
|
|
57
|
+
**필수 Peer Dependencies:**
|
|
76
58
|
|
|
77
|
-
|
|
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-[
|
|
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
|
-
|
|
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="
|
|
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,148 @@ export default function EditorPage() {
|
|
|
118
102
|
|
|
119
103
|
---
|
|
120
104
|
|
|
121
|
-
##
|
|
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
|
-
###
|
|
107
|
+
### 1. S3 업로드 (권장)
|
|
108
|
+
|
|
109
|
+
Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
|
|
165
110
|
|
|
166
111
|
```tsx
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
112
|
+
<LumirEditor
|
|
113
|
+
s3Upload={{
|
|
114
|
+
apiEndpoint: "/api/s3/presigned",
|
|
115
|
+
env: "production",
|
|
116
|
+
path: "blog/images",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
172
119
|
```
|
|
173
120
|
|
|
174
|
-
|
|
121
|
+
#### S3 파일 저장 경로
|
|
175
122
|
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
145
|
+
여러 이미지를 동시에 업로드할 때 파일명 중복을 방지하고 관리하기 쉽게 만드는 기능입니다.
|
|
190
146
|
|
|
191
|
-
|
|
147
|
+
#### 옵션 1: UUID 자동 추가
|
|
192
148
|
|
|
193
149
|
```tsx
|
|
194
150
|
<LumirEditor
|
|
195
151
|
s3Upload={{
|
|
196
152
|
apiEndpoint: "/api/s3/presigned",
|
|
197
|
-
env: "
|
|
198
|
-
path: "
|
|
153
|
+
env: "production",
|
|
154
|
+
path: "uploads",
|
|
155
|
+
appendUUID: true, // 파일명 뒤에 UUID 자동 추가
|
|
199
156
|
}}
|
|
200
|
-
onContentChange={(blocks) => console.log(blocks)}
|
|
201
157
|
/>
|
|
202
158
|
```
|
|
203
159
|
|
|
204
|
-
|
|
160
|
+
**결과:**
|
|
205
161
|
|
|
206
162
|
```
|
|
207
|
-
|
|
208
|
-
|
|
163
|
+
원본: photo.png
|
|
164
|
+
업로드: photo_550e8400-e29b-41d4-a716-446655440000.png
|
|
209
165
|
```
|
|
210
166
|
|
|
211
|
-
|
|
167
|
+
#### 옵션 2: 파일명 변환 콜백
|
|
212
168
|
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
169
|
+
```tsx
|
|
170
|
+
<LumirEditor
|
|
171
|
+
s3Upload={{
|
|
172
|
+
apiEndpoint: "/api/s3/presigned",
|
|
173
|
+
env: "production",
|
|
174
|
+
path: "uploads",
|
|
175
|
+
fileNameTransform: (originalName, file) => {
|
|
176
|
+
// 예: 사용자 ID 추가
|
|
177
|
+
const userId = getCurrentUserId();
|
|
178
|
+
return `${userId}_${originalName}`;
|
|
179
|
+
},
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**결과:**
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
원본: photo.png
|
|
188
|
+
업로드: user123_photo.png
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### 옵션 3: 조합 사용 (권장)
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<LumirEditor
|
|
195
|
+
s3Upload={{
|
|
196
|
+
apiEndpoint: "/api/s3/presigned",
|
|
197
|
+
env: "production",
|
|
198
|
+
path: "uploads",
|
|
199
|
+
fileNameTransform: (originalName) => `user123_${originalName}`,
|
|
200
|
+
appendUUID: true, // 변환 후 UUID 추가
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**결과:**
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
원본: photo.png
|
|
209
|
+
1. fileNameTransform 적용: user123_photo.png
|
|
210
|
+
2. appendUUID 적용: user123_photo_550e8400-e29b-41d4.png
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### 실전 예제: 타임스탬프 + UUID
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
function MyEditor() {
|
|
217
|
+
return (
|
|
218
|
+
<LumirEditor
|
|
219
|
+
s3Upload={{
|
|
220
|
+
apiEndpoint: "/api/s3/presigned",
|
|
221
|
+
env: "production",
|
|
222
|
+
path: "uploads",
|
|
223
|
+
fileNameTransform: (originalName, file) => {
|
|
224
|
+
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}`;
|
|
228
|
+
},
|
|
229
|
+
appendUUID: true,
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
217
233
|
}
|
|
218
234
|
```
|
|
219
235
|
|
|
220
|
-
|
|
236
|
+
**결과:**
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
2024-01-15_photo_550e8400-e29b-41d4.png
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 2. 커스텀 업로더
|
|
221
245
|
|
|
222
|
-
자체 업로드 로직을 사용할
|
|
246
|
+
자체 업로드 로직을 사용할 때:
|
|
223
247
|
|
|
224
248
|
```tsx
|
|
225
249
|
<LumirEditor
|
|
@@ -232,24 +256,22 @@ Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
|
|
|
232
256
|
body: formData,
|
|
233
257
|
});
|
|
234
258
|
|
|
235
|
-
const
|
|
236
|
-
return
|
|
259
|
+
const { url } = await response.json();
|
|
260
|
+
return url; // 업로드된 이미지 URL 반환
|
|
237
261
|
}}
|
|
238
262
|
/>
|
|
239
263
|
```
|
|
240
264
|
|
|
241
|
-
###
|
|
242
|
-
|
|
243
|
-
S3 업로더를 직접 생성하여 사용할 수 있습니다.
|
|
265
|
+
### 3. 헬퍼 함수 사용
|
|
244
266
|
|
|
245
267
|
```tsx
|
|
246
|
-
import {
|
|
268
|
+
import { createS3Uploader } from "@lumir-company/editor";
|
|
247
269
|
|
|
248
|
-
// S3 업로더 생성
|
|
249
270
|
const s3Uploader = createS3Uploader({
|
|
250
271
|
apiEndpoint: "/api/s3/presigned",
|
|
251
272
|
env: "production",
|
|
252
|
-
path: "
|
|
273
|
+
path: "images",
|
|
274
|
+
appendUUID: true,
|
|
253
275
|
});
|
|
254
276
|
|
|
255
277
|
// 에디터에 적용
|
|
@@ -262,204 +284,90 @@ const imageUrl = await s3Uploader(imageFile);
|
|
|
262
284
|
### 업로드 우선순위
|
|
263
285
|
|
|
264
286
|
1. `uploadFile` prop이 있으면 우선 사용
|
|
265
|
-
2. `uploadFile
|
|
287
|
+
2. `uploadFile` 없고 `s3Upload`가 있으면 S3 업로드 사용
|
|
266
288
|
3. 둘 다 없으면 업로드 실패
|
|
267
289
|
|
|
268
290
|
---
|
|
269
291
|
|
|
270
|
-
##
|
|
271
|
-
|
|
272
|
-
### ContentUtils
|
|
273
|
-
|
|
274
|
-
콘텐츠 관리 유틸리티 클래스입니다.
|
|
275
|
-
|
|
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
|
|
292
|
+
## 📚 Props API
|
|
297
293
|
|
|
298
|
-
|
|
294
|
+
### 핵심 Props
|
|
299
295
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 클래스 |
|
|
308
305
|
|
|
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 결합 유틸리티입니다.
|
|
306
|
+
### S3UploaderConfig
|
|
327
307
|
|
|
328
308
|
```tsx
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
)
|
|
337
|
-
|
|
309
|
+
interface S3UploaderConfig {
|
|
310
|
+
// 필수
|
|
311
|
+
apiEndpoint: string; // Presigned URL API 엔드포인트
|
|
312
|
+
env: "development" | "production";
|
|
313
|
+
path: string; // S3 저장 경로
|
|
314
|
+
|
|
315
|
+
// 선택 (파일명 커스터마이징)
|
|
316
|
+
fileNameTransform?: (originalName: string, file: File) => string;
|
|
317
|
+
appendUUID?: boolean; // true: 파일명 뒤에 UUID 추가
|
|
318
|
+
}
|
|
338
319
|
```
|
|
339
320
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
## 📖 타입 정의
|
|
343
|
-
|
|
344
|
-
### 주요 타입 import
|
|
345
|
-
|
|
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
|
-
```
|
|
321
|
+
### 전체 Props
|
|
365
322
|
|
|
366
|
-
|
|
323
|
+
<details>
|
|
324
|
+
<summary>전체 Props 보기</summary>
|
|
367
325
|
|
|
368
326
|
```tsx
|
|
369
327
|
interface LumirEditorProps {
|
|
370
|
-
// ===
|
|
371
|
-
initialContent?: DefaultPartialBlock[] | string;
|
|
372
|
-
initialEmptyBlocks?: number;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
328
|
+
// === 에디터 설정 ===
|
|
329
|
+
initialContent?: DefaultPartialBlock[] | string; // 초기 콘텐츠 (블록 배열 또는 JSON 문자열)
|
|
330
|
+
initialEmptyBlocks?: number; // 초기 빈 블록 개수 (기본: 3)
|
|
331
|
+
uploadFile?: (file: File) => Promise<string>; // 커스텀 파일 업로드 함수
|
|
332
|
+
s3Upload?: S3UploaderConfig; // S3 업로드 설정 (apiEndpoint, env, path 등)
|
|
333
|
+
|
|
334
|
+
// === 콜백 ===
|
|
335
|
+
onContentChange?: (blocks: DefaultPartialBlock[]) => void; // 콘텐츠 변경 시 호출
|
|
336
|
+
onSelectionChange?: () => void; // 선택 영역 변경 시 호출
|
|
337
|
+
|
|
338
|
+
// 기능 설정
|
|
339
|
+
tables?: TableConfig; // 테이블 기능 설정 (splitCells, cellBackgroundColor 등)
|
|
340
|
+
heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] }; // 헤딩 레벨 설정 (기본: [1,2,3,4,5,6])
|
|
341
|
+
defaultStyles?: boolean; // 기본 스타일 활성화 (기본: true)
|
|
342
|
+
disableExtensions?: string[]; // 비활성화할 확장 기능 목록
|
|
343
|
+
tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; // 탭 키 동작 (기본: "prefer-navigate-ui")
|
|
344
|
+
trailingBlock?: boolean; // 마지막에 빈 블록 자동 추가 (기본: true)
|
|
345
|
+
|
|
346
|
+
// === UI 설정 ===
|
|
347
|
+
editable?: boolean; // 편집 가능 여부 (기본: true)
|
|
348
|
+
theme?: "light" | "dark" | ThemeObject; // 에디터 테마 (기본: "light")
|
|
349
|
+
formattingToolbar?: boolean; // 서식 툴바 표시 (기본: true)
|
|
350
|
+
linkToolbar?: boolean; // 링크 툴바 표시 (기본: true)
|
|
351
|
+
sideMenu?: boolean; // 사이드 메뉴 표시 (기본: true)
|
|
352
|
+
sideMenuAddButton?: boolean; // 사이드 메뉴 + 버튼 표시 (기본: false, 드래그 핸들만 표시)
|
|
353
|
+
emojiPicker?: boolean; // 이모지 선택기 표시 (기본: true)
|
|
354
|
+
filePanel?: boolean; // 파일 패널 표시 (기본: true)
|
|
355
|
+
tableHandles?: boolean; // 테이블 핸들 표시 (기본: true)
|
|
356
|
+
className?: string; // 컨테이너 CSS 클래스
|
|
357
|
+
|
|
358
|
+
// 미디어 업로드 허용 여부 (기본: 모두 비활성)
|
|
359
|
+
allowVideoUpload?: boolean; // 비디오 업로드 허용 (기본: false)
|
|
360
|
+
allowAudioUpload?: boolean; // 오디오 업로드 허용 (기본: false)
|
|
361
|
+
allowFileUpload?: boolean; // 일반 파일 업로드 허용 (기본: false)
|
|
417
362
|
}
|
|
418
363
|
```
|
|
419
364
|
|
|
365
|
+
</details>
|
|
366
|
+
|
|
420
367
|
---
|
|
421
368
|
|
|
422
369
|
## 💡 사용 예제
|
|
423
370
|
|
|
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
|
-
);
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### 초기 콘텐츠 설정
|
|
440
|
-
|
|
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
|
-
/>
|
|
456
|
-
|
|
457
|
-
// 방법 2: JSON 문자열
|
|
458
|
-
<LumirEditor
|
|
459
|
-
initialContent='[{"type":"paragraph","content":[{"type":"text","text":"Hello World","styles":{}}]}]'
|
|
460
|
-
/>
|
|
461
|
-
```
|
|
462
|
-
|
|
463
371
|
### 읽기 전용 모드
|
|
464
372
|
|
|
465
373
|
```tsx
|
|
@@ -477,46 +385,6 @@ function BasicEditor() {
|
|
|
477
385
|
<LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
|
|
478
386
|
```
|
|
479
387
|
|
|
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
388
|
### 콘텐츠 저장 및 불러오기
|
|
521
389
|
|
|
522
390
|
```tsx
|
|
@@ -524,72 +392,31 @@ import { useState, useEffect } from "react";
|
|
|
524
392
|
import { LumirEditor, ContentUtils } from "@lumir-company/editor";
|
|
525
393
|
|
|
526
394
|
function EditorWithSave() {
|
|
527
|
-
const [content, setContent] = useState
|
|
395
|
+
const [content, setContent] = useState("");
|
|
528
396
|
|
|
529
|
-
//
|
|
397
|
+
// 불러오기
|
|
530
398
|
useEffect(() => {
|
|
531
|
-
const saved = localStorage.getItem("
|
|
399
|
+
const saved = localStorage.getItem("content");
|
|
532
400
|
if (saved && ContentUtils.isValidJSONString(saved)) {
|
|
533
401
|
setContent(saved);
|
|
534
402
|
}
|
|
535
403
|
}, []);
|
|
536
404
|
|
|
537
|
-
//
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
localStorage.setItem("
|
|
405
|
+
// 저장
|
|
406
|
+
const handleChange = (blocks) => {
|
|
407
|
+
const json = JSON.stringify(blocks);
|
|
408
|
+
localStorage.setItem("content", json);
|
|
541
409
|
};
|
|
542
410
|
|
|
543
411
|
return (
|
|
544
|
-
<LumirEditor
|
|
545
|
-
initialContent={content}
|
|
546
|
-
onContentChange={handleContentChange}
|
|
547
|
-
/>
|
|
412
|
+
<LumirEditor initialContent={content} onContentChange={handleChange} />
|
|
548
413
|
);
|
|
549
414
|
}
|
|
550
415
|
```
|
|
551
416
|
|
|
552
417
|
---
|
|
553
418
|
|
|
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
|
-
```
|
|
419
|
+
## 🎨 스타일링
|
|
593
420
|
|
|
594
421
|
### Tailwind CSS와 함께 사용
|
|
595
422
|
|
|
@@ -605,14 +432,14 @@ import { LumirEditor, cn } from "@lumir-company/editor";
|
|
|
605
432
|
/>;
|
|
606
433
|
```
|
|
607
434
|
|
|
608
|
-
### 커스텀 스타일
|
|
435
|
+
### 커스텀 스타일
|
|
609
436
|
|
|
610
437
|
```css
|
|
611
438
|
/* globals.css */
|
|
612
439
|
.my-editor .bn-editor {
|
|
613
|
-
padding
|
|
614
|
-
padding-right: 20px;
|
|
440
|
+
padding: 20px 30px;
|
|
615
441
|
font-size: 16px;
|
|
442
|
+
line-height: 1.6;
|
|
616
443
|
}
|
|
617
444
|
|
|
618
445
|
.my-editor [data-content-type="heading"] {
|
|
@@ -627,37 +454,37 @@ import { LumirEditor, cn } from "@lumir-company/editor";
|
|
|
627
454
|
|
|
628
455
|
---
|
|
629
456
|
|
|
630
|
-
## ⚠️
|
|
457
|
+
## ⚠️ 트러블슈팅
|
|
631
458
|
|
|
632
459
|
### 필수 체크리스트
|
|
633
460
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
| Next.js SSR 비활성화 | `dynamic(..., { ssr: false })` 사용 |
|
|
639
|
-
| React 버전 | 18.0.0 이상 필요 |
|
|
461
|
+
- [ ] CSS 임포트: `import "@lumir-company/editor/style.css"`
|
|
462
|
+
- [ ] 컨테이너 높이 설정: 부모 요소에 높이 지정 필수
|
|
463
|
+
- [ ] Next.js: `dynamic(..., { ssr: false })` 사용
|
|
464
|
+
- [ ] React 버전: 18.0.0 이상
|
|
640
465
|
|
|
641
|
-
###
|
|
466
|
+
### 자주 발생하는 문제
|
|
642
467
|
|
|
643
|
-
#### 1. 에디터가
|
|
468
|
+
#### 1. 에디터가 보이지 않음
|
|
644
469
|
|
|
645
470
|
```tsx
|
|
646
|
-
// ❌
|
|
471
|
+
// ❌ 잘못됨
|
|
647
472
|
<LumirEditor />;
|
|
648
473
|
|
|
649
|
-
// ✅
|
|
474
|
+
// ✅ 올바름
|
|
650
475
|
import "@lumir-company/editor/style.css";
|
|
651
|
-
<
|
|
476
|
+
<div className="h-[400px]">
|
|
477
|
+
<LumirEditor />
|
|
478
|
+
</div>;
|
|
652
479
|
```
|
|
653
480
|
|
|
654
|
-
#### 2. Next.js
|
|
481
|
+
#### 2. Next.js Hydration 오류
|
|
655
482
|
|
|
656
483
|
```tsx
|
|
657
|
-
// ❌
|
|
484
|
+
// ❌ 잘못됨
|
|
658
485
|
import { LumirEditor } from "@lumir-company/editor";
|
|
659
486
|
|
|
660
|
-
// ✅
|
|
487
|
+
// ✅ 올바름
|
|
661
488
|
const LumirEditor = dynamic(
|
|
662
489
|
() =>
|
|
663
490
|
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
@@ -665,78 +492,85 @@ const LumirEditor = dynamic(
|
|
|
665
492
|
);
|
|
666
493
|
```
|
|
667
494
|
|
|
668
|
-
#### 3.
|
|
495
|
+
#### 3. 이미지 업로드 실패
|
|
669
496
|
|
|
670
497
|
```tsx
|
|
671
|
-
//
|
|
672
|
-
<LumirEditor
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
498
|
+
// uploadFile 또는 s3Upload 중 하나는 반드시 설정!
|
|
499
|
+
<LumirEditor
|
|
500
|
+
s3Upload={{
|
|
501
|
+
apiEndpoint: "/api/s3/presigned",
|
|
502
|
+
env: "development",
|
|
503
|
+
path: "images",
|
|
504
|
+
}}
|
|
505
|
+
/>
|
|
678
506
|
```
|
|
679
507
|
|
|
680
|
-
#### 4. 이미지 업로드
|
|
508
|
+
#### 4. 여러 이미지 업로드 시 중복 문제
|
|
681
509
|
|
|
682
510
|
```tsx
|
|
683
|
-
//
|
|
511
|
+
// ✅ 해결: appendUUID 사용
|
|
684
512
|
<LumirEditor
|
|
685
|
-
uploadFile={async (file) => {
|
|
686
|
-
// 업로드 로직
|
|
687
|
-
return imageUrl;
|
|
688
|
-
}}
|
|
689
|
-
// 또는
|
|
690
513
|
s3Upload={{
|
|
691
514
|
apiEndpoint: "/api/s3/presigned",
|
|
692
|
-
env: "
|
|
515
|
+
env: "production",
|
|
693
516
|
path: "images",
|
|
517
|
+
appendUUID: true, // 고유한 파일명 보장
|
|
694
518
|
}}
|
|
695
519
|
/>
|
|
696
520
|
```
|
|
697
521
|
|
|
698
|
-
|
|
522
|
+
---
|
|
699
523
|
|
|
700
|
-
|
|
701
|
-
2. **큰 콘텐츠 처리**: 초기 콘텐츠가 클 경우 lazy loading 고려
|
|
702
|
-
3. **이미지 최적화**: 업로드 전 클라이언트에서 이미지 리사이징 권장
|
|
524
|
+
## 🛠️ 유틸리티 API
|
|
703
525
|
|
|
704
|
-
|
|
526
|
+
### ContentUtils
|
|
705
527
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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 통합 가이드
|
|
528
|
+
```tsx
|
|
529
|
+
import { ContentUtils } from "@lumir-company/editor";
|
|
530
|
+
|
|
531
|
+
// JSON 검증
|
|
532
|
+
ContentUtils.isValidJSONString('[{"type":"paragraph"}]'); // true
|
|
533
|
+
|
|
534
|
+
// JSON 파싱
|
|
535
|
+
const blocks = ContentUtils.parseJSONContent(jsonString);
|
|
536
|
+
|
|
537
|
+
// 기본 블록 생성
|
|
538
|
+
const emptyBlock = ContentUtils.createDefaultBlock();
|
|
728
539
|
```
|
|
729
540
|
|
|
730
|
-
|
|
541
|
+
### createS3Uploader
|
|
731
542
|
|
|
732
|
-
|
|
543
|
+
```tsx
|
|
544
|
+
import { createS3Uploader } from "@lumir-company/editor";
|
|
733
545
|
|
|
734
|
-
|
|
546
|
+
const uploader = createS3Uploader({
|
|
547
|
+
apiEndpoint: "/api/s3/presigned",
|
|
548
|
+
env: "production",
|
|
549
|
+
path: "uploads",
|
|
550
|
+
appendUUID: true,
|
|
551
|
+
});
|
|
735
552
|
|
|
736
|
-
|
|
553
|
+
// 직접 사용
|
|
554
|
+
const url = await uploader(imageFile);
|
|
555
|
+
```
|
|
737
556
|
|
|
738
557
|
## 🔗 관련 링크
|
|
739
558
|
|
|
740
|
-
- [GitHub Repository](https://github.com/lumir-company/editor)
|
|
741
559
|
- [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
|
|
742
560
|
- [BlockNote Documentation](https://www.blocknotejs.org/)
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## 📝 변경 로그
|
|
565
|
+
|
|
566
|
+
### v0.4.0
|
|
567
|
+
|
|
568
|
+
- ✨ 파일명 변환 콜백 (`fileNameTransform`) 추가
|
|
569
|
+
- ✨ UUID 자동 추가 옵션 (`appendUUID`) 추가
|
|
570
|
+
- 🐛 여러 이미지 동시 업로드 시 중복 문제 해결
|
|
571
|
+
- 📝 문서 대폭 개선
|
|
572
|
+
|
|
573
|
+
### v0.3.3
|
|
574
|
+
|
|
575
|
+
- 🐛 에디터 재생성 방지 최적화
|
|
576
|
+
- 📝 타입 정의 개선
|