@lumir-company/editor 0.2.1 → 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 +454 -257
- package/dist/index.d.mts +26 -10
- package/dist/index.d.ts +26 -10
- package/dist/index.js +205 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +204 -85
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +86 -37
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,379 +1,576 @@
|
|
|
1
1
|
# LumirEditor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
🖼️ **이미지 전용** BlockNote 기반 Rich Text 에디터
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@lumir-company/editor)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- 🚀 **간소화된 API**: 핵심 기능만 포함한 미니멀한 인터페이스
|
|
9
|
-
- 🎨 **BlockNote Theme 지원**: 공식 theme prop으로 에디터 스타일링
|
|
10
|
-
- 🔧 **TypeScript 지원**: 완전한 타입 안전성
|
|
11
|
-
- 📝 **Pretendard 폰트**: 기본 폰트로 Pretendard 최우선 적용 (14px)
|
|
12
|
-
- ⚡ **경량화**: 비디오/오디오/파일 업로드 기능 제거로 빠른 로딩
|
|
8
|
+
> 이미지 업로드에 최적화된 경량 에디터. S3 업로드, 파일명 커스터마이징, 로딩 스피너 내장.
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
---
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
## 📋 목차
|
|
13
|
+
|
|
14
|
+
- [특징](#-특징)
|
|
15
|
+
- [빠른 시작](#-빠른-시작)
|
|
16
|
+
- [이미지 업로드](#-이미지-업로드)
|
|
17
|
+
- [S3 업로드 설정](#1-s3-업로드-권장)
|
|
18
|
+
- [파일명 커스터마이징](#-파일명-커스터마이징)
|
|
19
|
+
- [커스텀 업로더](#2-커스텀-업로더)
|
|
20
|
+
- [Props API](#-props-api)
|
|
21
|
+
- [사용 예제](#-사용-예제)
|
|
22
|
+
- [스타일링](#-스타일링)
|
|
23
|
+
- [트러블슈팅](#-트러블슈팅)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## ✨ 특징
|
|
28
|
+
|
|
29
|
+
| 특징 | 설명 |
|
|
30
|
+
| -------------------------- | ------------------------------------------------------ |
|
|
31
|
+
| 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오 제거) |
|
|
32
|
+
| ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
|
|
33
|
+
| 🏷️ **파일명 커스터마이징** | 업로드 파일명 변경 콜백 + UUID 자동 추가 지원 |
|
|
34
|
+
| ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
|
|
35
|
+
| 🚀 **성능 최적화** | 애니메이션 비활성화로 빠른 렌더링 |
|
|
36
|
+
| 📝 **TypeScript** | 완전한 타입 안전성 |
|
|
37
|
+
| 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 |
|
|
38
|
+
|
|
39
|
+
### 지원 이미지 형식
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
PNG, JPEG/JPG, GIF, WebP, BMP, SVG
|
|
18
43
|
```
|
|
19
44
|
|
|
20
|
-
|
|
45
|
+
---
|
|
21
46
|
|
|
22
|
-
|
|
47
|
+
## 🚀 빠른 시작
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
### 1. 설치
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @lumir-company/editor
|
|
53
|
+
# 또는
|
|
54
|
+
yarn add @lumir-company/editor
|
|
26
55
|
```
|
|
27
56
|
|
|
57
|
+
**필수 Peer Dependencies:**
|
|
58
|
+
|
|
59
|
+
- `react` >= 18.0.0
|
|
60
|
+
- `react-dom` >= 18.0.0
|
|
61
|
+
|
|
28
62
|
### 2. 기본 사용
|
|
29
63
|
|
|
30
64
|
```tsx
|
|
31
|
-
import { LumirEditor } from
|
|
32
|
-
import
|
|
65
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
66
|
+
import "@lumir-company/editor/style.css"; // 필수!
|
|
33
67
|
|
|
34
68
|
export default function App() {
|
|
35
69
|
return (
|
|
36
|
-
<div className=
|
|
70
|
+
<div className="w-full h-[500px]">
|
|
37
71
|
<LumirEditor onContentChange={(blocks) => console.log(blocks)} />
|
|
38
72
|
</div>
|
|
39
73
|
);
|
|
40
74
|
}
|
|
41
75
|
```
|
|
42
76
|
|
|
43
|
-
|
|
77
|
+
> ⚠️ **중요**: `style.css`를 임포트하지 않으면 에디터가 정상 작동하지 않습니다.
|
|
78
|
+
|
|
79
|
+
### 3. Next.js에서 사용
|
|
44
80
|
|
|
45
81
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
"use client";
|
|
83
|
+
|
|
84
|
+
import dynamic from "next/dynamic";
|
|
85
|
+
import "@lumir-company/editor/style.css";
|
|
48
86
|
|
|
87
|
+
// SSR 비활성화 필수
|
|
49
88
|
const LumirEditor = dynamic(
|
|
50
|
-
() =>
|
|
51
|
-
|
|
89
|
+
() =>
|
|
90
|
+
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
91
|
+
{ ssr: false }
|
|
52
92
|
);
|
|
53
93
|
|
|
54
|
-
export default function
|
|
94
|
+
export default function EditorPage() {
|
|
55
95
|
return (
|
|
56
|
-
<div className=
|
|
96
|
+
<div className="h-[500px]">
|
|
57
97
|
<LumirEditor />
|
|
58
98
|
</div>
|
|
59
99
|
);
|
|
60
100
|
}
|
|
61
101
|
```
|
|
62
102
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
| Prop | 타입 | 기본값 | 설명 |
|
|
66
|
-
| ------------------- | ---------------------------------- | --------- | ---------------- |
|
|
67
|
-
| `initialContent` | `DefaultPartialBlock[] \| string` | - | 초기 콘텐츠 |
|
|
68
|
-
| `className` | `string` | `""` | CSS 클래스 |
|
|
69
|
-
| `theme` | `"light" \| "dark" \| ThemeObject` | `"light"` | 에디터 테마 |
|
|
70
|
-
| `onContentChange` | `(blocks) => void` | - | 콘텐츠 변경 콜백 |
|
|
71
|
-
| `editable` | `boolean` | `true` | 편집 가능 여부 |
|
|
72
|
-
| `sideMenuAddButton` | `boolean` | `false` | Add 버튼 표시 |
|
|
73
|
-
|
|
74
|
-
### 고급 Props
|
|
75
|
-
|
|
76
|
-
| Prop | 타입 | 기본값 | 설명 |
|
|
77
|
-
| --------------------- | --------------------------------- | ------ | -------------------- |
|
|
78
|
-
| `uploadFile` | `(file: File) => Promise<string>` | - | 커스텀 이미지 업로더 |
|
|
79
|
-
| `storeImagesAsBase64` | `boolean` | `true` | Base64 저장 여부 |
|
|
80
|
-
| `formattingToolbar` | `boolean` | `true` | 서식 툴바 표시 |
|
|
81
|
-
| `linkToolbar` | `boolean` | `true` | 링크 툴바 표시 |
|
|
82
|
-
| `sideMenu` | `boolean` | `true` | 사이드 메뉴 표시 |
|
|
83
|
-
| `slashMenu` | `boolean` | `true` | 슬래시 메뉴 표시 |
|
|
84
|
-
| `emojiPicker` | `boolean` | `true` | 이모지 피커 표시 |
|
|
85
|
-
| `filePanel` | `boolean` | `true` | 파일 패널 표시 |
|
|
86
|
-
| `tableHandles` | `boolean` | `true` | 표 핸들 표시 |
|
|
87
|
-
| `tables` | `TableConfig` | - | 테이블 설정 |
|
|
88
|
-
| `heading` | `HeadingConfig` | - | 헤딩 설정 |
|
|
89
|
-
| `animations` | `boolean` | `true` | 애니메이션 활성화 |
|
|
90
|
-
| `defaultStyles` | `boolean` | `true` | 기본 스타일 적용 |
|
|
91
|
-
|
|
92
|
-
### Props 사용 예시
|
|
103
|
+
---
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
import { LumirEditor } from '@lumir-company/editor';
|
|
105
|
+
## 🖼️ 이미지 업로드
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
<LumirEditor
|
|
99
|
-
initialContent="에디터 시작 텍스트"
|
|
100
|
-
/>
|
|
107
|
+
### 1. S3 업로드 (권장)
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
103
112
|
<LumirEditor
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
type: 'heading',
|
|
111
|
-
props: { level: 2 },
|
|
112
|
-
content: [{ type: 'text', text: '제목입니다' }]
|
|
113
|
-
}
|
|
114
|
-
]}
|
|
113
|
+
s3Upload={{
|
|
114
|
+
apiEndpoint: "/api/s3/presigned",
|
|
115
|
+
env: "production",
|
|
116
|
+
path: "blog/images",
|
|
117
|
+
}}
|
|
115
118
|
/>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### S3 파일 저장 경로
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
{env}/{path}/{filename}
|
|
125
|
+
|
|
126
|
+
예시:
|
|
127
|
+
production/blog/images/my-photo.png
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### API 엔드포인트 응답 형식
|
|
131
|
+
|
|
132
|
+
서버는 다음 형식으로 응답해야 합니다:
|
|
116
133
|
|
|
117
|
-
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
|
|
137
|
+
"publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 📝 파일명 커스터마이징
|
|
144
|
+
|
|
145
|
+
여러 이미지를 동시에 업로드할 때 파일명 중복을 방지하고 관리하기 쉽게 만드는 기능입니다.
|
|
146
|
+
|
|
147
|
+
#### 옵션 1: UUID 자동 추가
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
118
150
|
<LumirEditor
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log('선택 영역이 변경되었습니다');
|
|
151
|
+
s3Upload={{
|
|
152
|
+
apiEndpoint: "/api/s3/presigned",
|
|
153
|
+
env: "production",
|
|
154
|
+
path: "uploads",
|
|
155
|
+
appendUUID: true, // 파일명 뒤에 UUID 자동 추가
|
|
125
156
|
}}
|
|
126
157
|
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**결과:**
|
|
127
161
|
|
|
128
|
-
|
|
162
|
+
```
|
|
163
|
+
원본: photo.png
|
|
164
|
+
업로드: photo_550e8400-e29b-41d4-a716-446655440000.png
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### 옵션 2: 파일명 변환 콜백
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
129
170
|
<LumirEditor
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}}
|
|
135
181
|
/>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**결과:**
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
원본: photo.png
|
|
188
|
+
업로드: user123_photo.png
|
|
189
|
+
```
|
|
136
190
|
|
|
137
|
-
|
|
191
|
+
#### 옵션 3: 조합 사용 (권장)
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
138
194
|
<LumirEditor
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
195
|
+
s3Upload={{
|
|
196
|
+
apiEndpoint: "/api/s3/presigned",
|
|
197
|
+
env: "production",
|
|
198
|
+
path: "uploads",
|
|
199
|
+
fileNameTransform: (originalName) => `user123_${originalName}`,
|
|
200
|
+
appendUUID: true, // 변환 후 UUID 추가
|
|
201
|
+
}}
|
|
143
202
|
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**결과:**
|
|
144
206
|
|
|
145
|
-
|
|
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
|
+
);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**결과:**
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
2024-01-15_photo_550e8400-e29b-41d4.png
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 2. 커스텀 업로더
|
|
245
|
+
|
|
246
|
+
자체 업로드 로직을 사용할 때:
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
146
249
|
<LumirEditor
|
|
147
250
|
uploadFile={async (file) => {
|
|
148
251
|
const formData = new FormData();
|
|
149
|
-
formData.append(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
252
|
+
formData.append("image", file);
|
|
253
|
+
|
|
254
|
+
const response = await fetch("/api/upload", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
body: formData,
|
|
153
257
|
});
|
|
154
|
-
return (await response.json()).url;
|
|
155
|
-
}}
|
|
156
|
-
storeImagesAsBase64={false}
|
|
157
|
-
/>
|
|
158
258
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
tables={{
|
|
162
|
-
splitCells: true,
|
|
163
|
-
cellBackgroundColor: true,
|
|
164
|
-
cellTextColor: true,
|
|
165
|
-
headers: true
|
|
166
|
-
}}
|
|
167
|
-
heading={{
|
|
168
|
-
levels: [1, 2, 3, 4] // H1~H4만 허용
|
|
259
|
+
const { url } = await response.json();
|
|
260
|
+
return url; // 업로드된 이미지 URL 반환
|
|
169
261
|
}}
|
|
170
262
|
/>
|
|
263
|
+
```
|
|
171
264
|
|
|
172
|
-
|
|
173
|
-
<LumirEditor
|
|
174
|
-
animations={false} // 애니메이션 비활성화
|
|
175
|
-
defaultStyles={true} // 기본 스타일 사용
|
|
176
|
-
/>
|
|
265
|
+
### 3. 헬퍼 함수 사용
|
|
177
266
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
/>
|
|
267
|
+
```tsx
|
|
268
|
+
import { createS3Uploader } from "@lumir-company/editor";
|
|
269
|
+
|
|
270
|
+
const s3Uploader = createS3Uploader({
|
|
271
|
+
apiEndpoint: "/api/s3/presigned",
|
|
272
|
+
env: "production",
|
|
273
|
+
path: "images",
|
|
274
|
+
appendUUID: true,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 에디터에 적용
|
|
278
|
+
<LumirEditor uploadFile={s3Uploader} />;
|
|
279
|
+
|
|
280
|
+
// 또는 별도로 사용
|
|
281
|
+
const imageUrl = await s3Uploader(imageFile);
|
|
194
282
|
```
|
|
195
283
|
|
|
196
|
-
|
|
284
|
+
### 업로드 우선순위
|
|
285
|
+
|
|
286
|
+
1. `uploadFile` prop이 있으면 우선 사용
|
|
287
|
+
2. `uploadFile` 없고 `s3Upload`가 있으면 S3 업로드 사용
|
|
288
|
+
3. 둘 다 없으면 업로드 실패
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 📚 Props API
|
|
293
|
+
|
|
294
|
+
### 핵심 Props
|
|
197
295
|
|
|
198
|
-
|
|
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 클래스 |
|
|
305
|
+
|
|
306
|
+
### S3UploaderConfig
|
|
199
307
|
|
|
200
308
|
```tsx
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
319
|
+
```
|
|
203
320
|
|
|
204
|
-
|
|
205
|
-
|
|
321
|
+
### 전체 Props
|
|
322
|
+
|
|
323
|
+
<details>
|
|
324
|
+
<summary>전체 Props 보기</summary>
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
interface LumirEditorProps {
|
|
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)
|
|
362
|
+
}
|
|
206
363
|
```
|
|
207
364
|
|
|
208
|
-
|
|
365
|
+
</details>
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## 💡 사용 예제
|
|
370
|
+
|
|
371
|
+
### 읽기 전용 모드
|
|
209
372
|
|
|
210
373
|
```tsx
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
},
|
|
221
|
-
tooltip: {
|
|
222
|
-
text: '#6b7280',
|
|
223
|
-
background: '#f3f4f6',
|
|
224
|
-
},
|
|
225
|
-
hovered: {
|
|
226
|
-
text: '#111827',
|
|
227
|
-
background: '#e5e7eb',
|
|
228
|
-
},
|
|
229
|
-
selected: {
|
|
230
|
-
text: '#ffffff',
|
|
231
|
-
background: '#3b82f6',
|
|
232
|
-
},
|
|
233
|
-
disabled: {
|
|
234
|
-
text: '#9ca3af',
|
|
235
|
-
background: '#f3f4f6',
|
|
236
|
-
},
|
|
237
|
-
shadow: '#000000',
|
|
238
|
-
border: '#d1d5db',
|
|
239
|
-
sideMenu: '#6b7280',
|
|
240
|
-
},
|
|
241
|
-
borderRadius: 8,
|
|
242
|
-
fontFamily: 'Pretendard, system-ui, sans-serif',
|
|
243
|
-
};
|
|
374
|
+
<LumirEditor
|
|
375
|
+
editable={false}
|
|
376
|
+
initialContent={savedContent}
|
|
377
|
+
sideMenu={false}
|
|
378
|
+
formattingToolbar={false}
|
|
379
|
+
/>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 다크 테마
|
|
244
383
|
|
|
245
|
-
|
|
384
|
+
```tsx
|
|
385
|
+
<LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
|
|
246
386
|
```
|
|
247
387
|
|
|
248
|
-
###
|
|
388
|
+
### 콘텐츠 저장 및 불러오기
|
|
249
389
|
|
|
250
390
|
```tsx
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
dark: {
|
|
259
|
-
colors: {
|
|
260
|
-
editor: { text: '#f9fafb', background: '#111827' },
|
|
261
|
-
menu: { text: '#e5e7eb', background: '#1f2937' },
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
};
|
|
391
|
+
import { useState, useEffect } from "react";
|
|
392
|
+
import { LumirEditor, ContentUtils } from "@lumir-company/editor";
|
|
393
|
+
|
|
394
|
+
function EditorWithSave() {
|
|
395
|
+
const [content, setContent] = useState("");
|
|
265
396
|
|
|
266
|
-
|
|
397
|
+
// 불러오기
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
const saved = localStorage.getItem("content");
|
|
400
|
+
if (saved && ContentUtils.isValidJSONString(saved)) {
|
|
401
|
+
setContent(saved);
|
|
402
|
+
}
|
|
403
|
+
}, []);
|
|
404
|
+
|
|
405
|
+
// 저장
|
|
406
|
+
const handleChange = (blocks) => {
|
|
407
|
+
const json = JSON.stringify(blocks);
|
|
408
|
+
localStorage.setItem("content", json);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<LumirEditor initialContent={content} onContentChange={handleChange} />
|
|
413
|
+
);
|
|
414
|
+
}
|
|
267
415
|
```
|
|
268
416
|
|
|
269
|
-
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 🎨 스타일링
|
|
270
420
|
|
|
271
|
-
###
|
|
421
|
+
### Tailwind CSS와 함께 사용
|
|
272
422
|
|
|
273
423
|
```tsx
|
|
274
|
-
|
|
424
|
+
import { LumirEditor, cn } from "@lumir-company/editor";
|
|
425
|
+
|
|
275
426
|
<LumirEditor
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
console.log('이미지가 포함된 콘텐츠:', blocks);
|
|
283
|
-
}
|
|
284
|
-
}}
|
|
285
|
-
/>
|
|
427
|
+
className={cn(
|
|
428
|
+
"min-h-[400px] rounded-xl",
|
|
429
|
+
"border border-gray-200 shadow-lg",
|
|
430
|
+
"focus-within:ring-2 focus-within:ring-blue-500"
|
|
431
|
+
)}
|
|
432
|
+
/>;
|
|
286
433
|
```
|
|
287
434
|
|
|
288
|
-
|
|
435
|
+
### 커스텀 스타일
|
|
289
436
|
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
437
|
+
```css
|
|
438
|
+
/* globals.css */
|
|
439
|
+
.my-editor .bn-editor {
|
|
440
|
+
padding: 20px 30px;
|
|
441
|
+
font-size: 16px;
|
|
442
|
+
line-height: 1.6;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.my-editor [data-content-type="heading"] {
|
|
446
|
+
font-weight: 700;
|
|
447
|
+
margin-top: 24px;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
301
450
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const headingConfig = EditorConfig.getDefaultHeadingConfig();
|
|
451
|
+
```tsx
|
|
452
|
+
<LumirEditor className="my-editor" />
|
|
305
453
|
```
|
|
306
454
|
|
|
307
|
-
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## ⚠️ 트러블슈팅
|
|
458
|
+
|
|
459
|
+
### 필수 체크리스트
|
|
460
|
+
|
|
461
|
+
- [ ] CSS 임포트: `import "@lumir-company/editor/style.css"`
|
|
462
|
+
- [ ] 컨테이너 높이 설정: 부모 요소에 높이 지정 필수
|
|
463
|
+
- [ ] Next.js: `dynamic(..., { ssr: false })` 사용
|
|
464
|
+
- [ ] React 버전: 18.0.0 이상
|
|
308
465
|
|
|
309
|
-
###
|
|
466
|
+
### 자주 발생하는 문제
|
|
467
|
+
|
|
468
|
+
#### 1. 에디터가 보이지 않음
|
|
310
469
|
|
|
311
470
|
```tsx
|
|
312
|
-
//
|
|
313
|
-
<
|
|
314
|
-
<LumirEditor />
|
|
315
|
-
</div>
|
|
471
|
+
// ❌ 잘못됨
|
|
472
|
+
<LumirEditor />;
|
|
316
473
|
|
|
317
|
-
//
|
|
318
|
-
|
|
474
|
+
// ✅ 올바름
|
|
475
|
+
import "@lumir-company/editor/style.css";
|
|
476
|
+
<div className="h-[400px]">
|
|
319
477
|
<LumirEditor />
|
|
320
|
-
</div
|
|
478
|
+
</div>;
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### 2. Next.js Hydration 오류
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
// ❌ 잘못됨
|
|
485
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
486
|
+
|
|
487
|
+
// ✅ 올바름
|
|
488
|
+
const LumirEditor = dynamic(
|
|
489
|
+
() =>
|
|
490
|
+
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
491
|
+
{ ssr: false }
|
|
492
|
+
);
|
|
321
493
|
```
|
|
322
494
|
|
|
323
|
-
|
|
495
|
+
#### 3. 이미지 업로드 실패
|
|
324
496
|
|
|
325
497
|
```tsx
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
498
|
+
// uploadFile 또는 s3Upload 중 하나는 반드시 설정!
|
|
499
|
+
<LumirEditor
|
|
500
|
+
s3Upload={{
|
|
501
|
+
apiEndpoint: "/api/s3/presigned",
|
|
502
|
+
env: "development",
|
|
503
|
+
path: "images",
|
|
504
|
+
}}
|
|
505
|
+
/>
|
|
329
506
|
```
|
|
330
507
|
|
|
331
|
-
|
|
508
|
+
#### 4. 여러 이미지 업로드 시 중복 문제
|
|
332
509
|
|
|
333
510
|
```tsx
|
|
511
|
+
// ✅ 해결: appendUUID 사용
|
|
334
512
|
<LumirEditor
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
513
|
+
s3Upload={{
|
|
514
|
+
apiEndpoint: "/api/s3/presigned",
|
|
515
|
+
env: "production",
|
|
516
|
+
path: "images",
|
|
517
|
+
appendUUID: true, // 고유한 파일명 보장
|
|
518
|
+
}}
|
|
339
519
|
/>
|
|
340
520
|
```
|
|
341
521
|
|
|
342
|
-
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 🛠️ 유틸리티 API
|
|
343
525
|
|
|
344
|
-
###
|
|
526
|
+
### ContentUtils
|
|
345
527
|
|
|
346
528
|
```tsx
|
|
347
|
-
|
|
348
|
-
|
|
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();
|
|
349
539
|
```
|
|
350
540
|
|
|
351
|
-
###
|
|
541
|
+
### createS3Uploader
|
|
352
542
|
|
|
353
543
|
```tsx
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
544
|
+
import { createS3Uploader } from "@lumir-company/editor";
|
|
545
|
+
|
|
546
|
+
const uploader = createS3Uploader({
|
|
547
|
+
apiEndpoint: "/api/s3/presigned",
|
|
548
|
+
env: "production",
|
|
549
|
+
path: "uploads",
|
|
550
|
+
appendUUID: true,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// 직접 사용
|
|
554
|
+
const url = await uploader(imageFile);
|
|
359
555
|
```
|
|
360
556
|
|
|
361
|
-
|
|
557
|
+
## 🔗 관련 링크
|
|
558
|
+
|
|
559
|
+
- [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
|
|
560
|
+
- [BlockNote Documentation](https://www.blocknotejs.org/)
|
|
362
561
|
|
|
363
|
-
|
|
364
|
-
- ❌ 비디오, 오디오, 일반 파일 업로드 불가
|
|
365
|
-
- 🔄 자동 Base64 변환 또는 커스텀 업로더 사용
|
|
562
|
+
---
|
|
366
563
|
|
|
367
|
-
##
|
|
564
|
+
## 📝 변경 로그
|
|
368
565
|
|
|
369
|
-
### v0.0
|
|
566
|
+
### v0.4.0
|
|
370
567
|
|
|
371
|
-
-
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
- 📝
|
|
375
|
-
- 🚫 **미디어 제한**: 비디오/오디오/파일 업로드 비활성화
|
|
568
|
+
- ✨ 파일명 변환 콜백 (`fileNameTransform`) 추가
|
|
569
|
+
- ✨ UUID 자동 추가 옵션 (`appendUUID`) 추가
|
|
570
|
+
- 🐛 여러 이미지 동시 업로드 시 중복 문제 해결
|
|
571
|
+
- 📝 문서 대폭 개선
|
|
376
572
|
|
|
377
|
-
|
|
573
|
+
### v0.3.3
|
|
378
574
|
|
|
379
|
-
|
|
575
|
+
- 🐛 에디터 재생성 방지 최적화
|
|
576
|
+
- 📝 타입 정의 개선
|