@lumir-company/editor 0.2.1 → 0.3.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 +613 -250
- package/dist/index.d.mts +18 -10
- package/dist/index.d.ts +18 -10
- package/dist/index.js +162 -85
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -85
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +86 -37
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,379 +1,742 @@
|
|
|
1
1
|
# LumirEditor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
🖼️ **이미지 전용** BlockNote 기반 Rich Text 에디터
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@lumir-company/editor)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## 📋 목차
|
|
9
|
+
|
|
10
|
+
- [✨ 핵심 특징](#-핵심-특징)
|
|
11
|
+
- [📦 설치](#-설치)
|
|
12
|
+
- [🚀 빠른 시작](#-빠른-시작)
|
|
13
|
+
- [📚 Props 레퍼런스](#-props-레퍼런스)
|
|
14
|
+
- [🖼️ 이미지 업로드](#️-이미지-업로드)
|
|
15
|
+
- [🛠️ 유틸리티 API](#️-유틸리티-api)
|
|
16
|
+
- [📖 타입 정의](#-타입-정의)
|
|
17
|
+
- [💡 사용 예제](#-사용-예제)
|
|
18
|
+
- [🎨 스타일링 가이드](#-스타일링-가이드)
|
|
19
|
+
- [⚠️ 주의사항 및 트러블슈팅](#️-주의사항-및-트러블슈팅)
|
|
20
|
+
- [📄 라이선스](#-라이선스)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ 핵심 특징
|
|
25
|
+
|
|
26
|
+
| 특징 | 설명 |
|
|
27
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
28
|
+
| 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오/파일 제거) |
|
|
29
|
+
| ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
|
|
30
|
+
| 🎯 **커스텀 업로더** | 자체 업로드 로직 적용 가능 |
|
|
31
|
+
| ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
|
|
32
|
+
| 🚀 **애니메이션 최적화** | 기본 애니메이션 비활성화로 성능 향상 |
|
|
33
|
+
| 📝 **TypeScript** | 완전한 타입 안전성 |
|
|
34
|
+
| 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 지원 |
|
|
35
|
+
| 📱 **반응형** | 모바일/데스크톱 최적화 |
|
|
36
|
+
|
|
37
|
+
### 지원 이미지 형식
|
|
4
38
|
|
|
5
|
-
|
|
39
|
+
```
|
|
40
|
+
PNG, JPEG/JPG, GIF (애니메이션 포함), WebP, BMP, SVG
|
|
41
|
+
```
|
|
6
42
|
|
|
7
|
-
|
|
8
|
-
- 🚀 **간소화된 API**: 핵심 기능만 포함한 미니멀한 인터페이스
|
|
9
|
-
- 🎨 **BlockNote Theme 지원**: 공식 theme prop으로 에디터 스타일링
|
|
10
|
-
- 🔧 **TypeScript 지원**: 완전한 타입 안전성
|
|
11
|
-
- 📝 **Pretendard 폰트**: 기본 폰트로 Pretendard 최우선 적용 (14px)
|
|
12
|
-
- ⚡ **경량화**: 비디오/오디오/파일 업로드 기능 제거로 빠른 로딩
|
|
43
|
+
---
|
|
13
44
|
|
|
14
45
|
## 📦 설치
|
|
15
46
|
|
|
16
47
|
```bash
|
|
48
|
+
# npm
|
|
17
49
|
npm install @lumir-company/editor
|
|
50
|
+
|
|
51
|
+
# yarn
|
|
52
|
+
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
|
+
}
|
|
18
65
|
```
|
|
19
66
|
|
|
20
|
-
|
|
67
|
+
---
|
|
21
68
|
|
|
22
|
-
|
|
69
|
+
## 🚀 빠른 시작
|
|
70
|
+
|
|
71
|
+
### 1단계: CSS 임포트 (필수)
|
|
23
72
|
|
|
24
73
|
```tsx
|
|
25
|
-
import
|
|
74
|
+
import "@lumir-company/editor/style.css";
|
|
26
75
|
```
|
|
27
76
|
|
|
28
|
-
|
|
77
|
+
> ⚠️ **중요**: CSS를 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
|
|
78
|
+
|
|
79
|
+
### 2단계: 기본 사용
|
|
29
80
|
|
|
30
81
|
```tsx
|
|
31
|
-
import { LumirEditor } from
|
|
32
|
-
import
|
|
82
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
83
|
+
import "@lumir-company/editor/style.css";
|
|
33
84
|
|
|
34
85
|
export default function App() {
|
|
35
86
|
return (
|
|
36
|
-
<div className=
|
|
87
|
+
<div className="w-full h-[400px]">
|
|
37
88
|
<LumirEditor onContentChange={(blocks) => console.log(blocks)} />
|
|
38
89
|
</div>
|
|
39
90
|
);
|
|
40
91
|
}
|
|
41
92
|
```
|
|
42
93
|
|
|
43
|
-
### 3
|
|
94
|
+
### 3단계: Next.js에서 사용 (SSR 비활성화 필수)
|
|
44
95
|
|
|
45
96
|
```tsx
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
"use client";
|
|
98
|
+
|
|
99
|
+
import dynamic from "next/dynamic";
|
|
100
|
+
import "@lumir-company/editor/style.css";
|
|
48
101
|
|
|
49
102
|
const LumirEditor = dynamic(
|
|
50
|
-
() =>
|
|
51
|
-
|
|
103
|
+
() =>
|
|
104
|
+
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
105
|
+
{ ssr: false }
|
|
52
106
|
);
|
|
53
107
|
|
|
54
|
-
export default function
|
|
108
|
+
export default function EditorPage() {
|
|
55
109
|
return (
|
|
56
|
-
<div className=
|
|
57
|
-
<LumirEditor
|
|
110
|
+
<div className="w-full h-[500px]">
|
|
111
|
+
<LumirEditor
|
|
112
|
+
onContentChange={(blocks) => console.log("Content:", blocks)}
|
|
113
|
+
/>
|
|
58
114
|
</div>
|
|
59
115
|
);
|
|
60
116
|
}
|
|
61
117
|
```
|
|
62
118
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| `
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
---
|
|
120
|
+
|
|
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` | 선택 영역 변경 시 호출 |
|
|
163
|
+
|
|
164
|
+
### S3UploaderConfig
|
|
93
165
|
|
|
94
166
|
```tsx
|
|
95
|
-
|
|
167
|
+
interface S3UploaderConfig {
|
|
168
|
+
apiEndpoint: string; // Presigned URL API 엔드포인트 (필수)
|
|
169
|
+
env: "development" | "production"; // 환경 (필수)
|
|
170
|
+
path: string; // S3 경로 (필수)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### TableConfig
|
|
96
175
|
|
|
97
|
-
|
|
176
|
+
```tsx
|
|
177
|
+
interface TableConfig {
|
|
178
|
+
splitCells?: boolean; // 셀 분할 (기본: true)
|
|
179
|
+
cellBackgroundColor?: boolean; // 셀 배경색 (기본: true)
|
|
180
|
+
cellTextColor?: boolean; // 셀 텍스트 색상 (기본: true)
|
|
181
|
+
headers?: boolean; // 헤더 행 (기본: true)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 🖼️ 이미지 업로드
|
|
188
|
+
|
|
189
|
+
### 방법 1: S3 업로드 (권장)
|
|
190
|
+
|
|
191
|
+
Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
98
194
|
<LumirEditor
|
|
99
|
-
|
|
195
|
+
s3Upload={{
|
|
196
|
+
apiEndpoint: "/api/s3/presigned",
|
|
197
|
+
env: "development",
|
|
198
|
+
path: "blog/images",
|
|
199
|
+
}}
|
|
200
|
+
onContentChange={(blocks) => console.log(blocks)}
|
|
100
201
|
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**S3 파일 저장 경로 구조:**
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
{env}/{path}/{filename}
|
|
208
|
+
예: development/blog/images/my-image.png
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**API 엔드포인트 응답 예시:**
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"presignedUrl": "https://s3.amazonaws.com/bucket/...",
|
|
216
|
+
"publicUrl": "https://cdn.example.com/development/blog/images/my-image.png"
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 방법 2: 커스텀 업로더
|
|
221
|
+
|
|
222
|
+
자체 업로드 로직을 사용할 때 활용합니다.
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
<LumirEditor
|
|
226
|
+
uploadFile={async (file) => {
|
|
227
|
+
const formData = new FormData();
|
|
228
|
+
formData.append("image", file);
|
|
229
|
+
|
|
230
|
+
const response = await fetch("/api/upload", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
body: formData,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const data = await response.json();
|
|
236
|
+
return data.url; // 업로드된 이미지의 URL 반환
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 방법 3: createS3Uploader 헬퍼 함수
|
|
242
|
+
|
|
243
|
+
S3 업로더를 직접 생성하여 사용할 수 있습니다.
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
import { LumirEditor, createS3Uploader } from "@lumir-company/editor";
|
|
247
|
+
|
|
248
|
+
// S3 업로더 생성
|
|
249
|
+
const s3Uploader = createS3Uploader({
|
|
250
|
+
apiEndpoint: "/api/s3/presigned",
|
|
251
|
+
env: "production",
|
|
252
|
+
path: "uploads/images",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 에디터에 적용
|
|
256
|
+
<LumirEditor uploadFile={s3Uploader} />;
|
|
257
|
+
|
|
258
|
+
// 또는 별도로 사용
|
|
259
|
+
const imageUrl = await s3Uploader(imageFile);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 업로드 우선순위
|
|
263
|
+
|
|
264
|
+
1. `uploadFile` prop이 있으면 우선 사용
|
|
265
|
+
2. `uploadFile`이 없고 `s3Upload`가 있으면 S3 업로드 사용
|
|
266
|
+
3. 둘 다 없으면 업로드 실패
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 🛠️ 유틸리티 API
|
|
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
|
|
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 결합 유틸리티입니다.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { cn } from "@lumir-company/editor";
|
|
101
330
|
|
|
102
|
-
|
|
331
|
+
<LumirEditor
|
|
332
|
+
className={cn(
|
|
333
|
+
"min-h-[400px] rounded-lg",
|
|
334
|
+
isFullscreen && "fixed inset-0 z-50",
|
|
335
|
+
isDarkMode && "dark-theme"
|
|
336
|
+
)}
|
|
337
|
+
/>;
|
|
338
|
+
```
|
|
339
|
+
|
|
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
|
+
```
|
|
365
|
+
|
|
366
|
+
### LumirEditorProps 전체 인터페이스
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
interface LumirEditorProps {
|
|
370
|
+
// === Editor Options ===
|
|
371
|
+
initialContent?: DefaultPartialBlock[] | string;
|
|
372
|
+
initialEmptyBlocks?: number;
|
|
373
|
+
placeholder?: string;
|
|
374
|
+
uploadFile?: (file: File) => Promise<string>;
|
|
375
|
+
s3Upload?: {
|
|
376
|
+
apiEndpoint: string;
|
|
377
|
+
env: "development" | "production";
|
|
378
|
+
path: string;
|
|
379
|
+
};
|
|
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
|
+
|
|
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: 블록 배열
|
|
103
443
|
<LumirEditor
|
|
104
444
|
initialContent={[
|
|
105
445
|
{
|
|
106
|
-
type:
|
|
107
|
-
|
|
446
|
+
type: "heading",
|
|
447
|
+
props: { level: 1 },
|
|
448
|
+
content: [{ type: "text", text: "제목입니다", styles: {} }],
|
|
108
449
|
},
|
|
109
450
|
{
|
|
110
|
-
type:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
451
|
+
type: "paragraph",
|
|
452
|
+
content: [{ type: "text", text: "본문 내용...", styles: {} }],
|
|
453
|
+
},
|
|
114
454
|
]}
|
|
115
455
|
/>
|
|
116
456
|
|
|
117
|
-
//
|
|
457
|
+
// 방법 2: JSON 문자열
|
|
118
458
|
<LumirEditor
|
|
119
|
-
|
|
120
|
-
console.log('변경된 콘텐츠:', blocks);
|
|
121
|
-
saveToDatabase(blocks);
|
|
122
|
-
}}
|
|
123
|
-
onSelectionChange={() => {
|
|
124
|
-
console.log('선택 영역이 변경되었습니다');
|
|
125
|
-
}}
|
|
459
|
+
initialContent='[{"type":"paragraph","content":[{"type":"text","text":"Hello World","styles":{}}]}]'
|
|
126
460
|
/>
|
|
461
|
+
```
|
|
127
462
|
|
|
128
|
-
|
|
129
|
-
<LumirEditor
|
|
130
|
-
sideMenuAddButton={true} // Add 버튼 표시
|
|
131
|
-
formattingToolbar={false} // 서식 툴바 숨김
|
|
132
|
-
linkToolbar={false} // 링크 툴바 숨김
|
|
133
|
-
slashMenu={false} // 슬래시 메뉴 숨김
|
|
134
|
-
emojiPicker={false} // 이모지 피커 숨김
|
|
135
|
-
/>
|
|
463
|
+
### 읽기 전용 모드
|
|
136
464
|
|
|
137
|
-
|
|
465
|
+
```tsx
|
|
138
466
|
<LumirEditor
|
|
139
467
|
editable={false}
|
|
140
468
|
initialContent={savedContent}
|
|
141
|
-
formattingToolbar={false}
|
|
142
469
|
sideMenu={false}
|
|
470
|
+
formattingToolbar={false}
|
|
143
471
|
/>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 다크 테마
|
|
144
475
|
|
|
145
|
-
|
|
476
|
+
```tsx
|
|
477
|
+
<LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### S3 이미지 업로드
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
146
483
|
<LumirEditor
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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));
|
|
155
492
|
}}
|
|
156
|
-
storeImagesAsBase64={false}
|
|
157
493
|
/>
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 반응형 디자인
|
|
158
497
|
|
|
159
|
-
|
|
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
|
|
160
507
|
<LumirEditor
|
|
161
508
|
tables={{
|
|
162
509
|
splitCells: true,
|
|
163
510
|
cellBackgroundColor: true,
|
|
164
|
-
cellTextColor:
|
|
165
|
-
headers: true
|
|
511
|
+
cellTextColor: false, // 셀 텍스트 색상 비활성
|
|
512
|
+
headers: true,
|
|
166
513
|
}}
|
|
167
514
|
heading={{
|
|
168
|
-
levels: [1, 2, 3,
|
|
515
|
+
levels: [1, 2, 3], // H4-H6 비활성
|
|
169
516
|
}}
|
|
170
517
|
/>
|
|
518
|
+
```
|
|
171
519
|
|
|
172
|
-
|
|
173
|
-
<LumirEditor
|
|
174
|
-
animations={false} // 애니메이션 비활성화
|
|
175
|
-
defaultStyles={true} // 기본 스타일 사용
|
|
176
|
-
/>
|
|
520
|
+
### 콘텐츠 저장 및 불러오기
|
|
177
521
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
className="min-h-[400px] border rounded-lg"
|
|
182
|
-
theme="light"
|
|
183
|
-
editable={true}
|
|
184
|
-
sideMenuAddButton={true}
|
|
185
|
-
formattingToolbar={true}
|
|
186
|
-
linkToolbar={true}
|
|
187
|
-
slashMenu={true}
|
|
188
|
-
emojiPicker={true}
|
|
189
|
-
onContentChange={(blocks) => console.log(blocks)}
|
|
190
|
-
onSelectionChange={() => console.log('선택 변경')}
|
|
191
|
-
uploadFile={customUploader}
|
|
192
|
-
storeImagesAsBase64={false}
|
|
193
|
-
/>
|
|
194
|
-
```
|
|
522
|
+
```tsx
|
|
523
|
+
import { useState, useEffect } from "react";
|
|
524
|
+
import { LumirEditor, ContentUtils } from "@lumir-company/editor";
|
|
195
525
|
|
|
196
|
-
|
|
526
|
+
function EditorWithSave() {
|
|
527
|
+
const [content, setContent] = useState<string>("");
|
|
197
528
|
|
|
198
|
-
|
|
529
|
+
// 저장된 콘텐츠 불러오기
|
|
530
|
+
useEffect(() => {
|
|
531
|
+
const saved = localStorage.getItem("editor-content");
|
|
532
|
+
if (saved && ContentUtils.isValidJSONString(saved)) {
|
|
533
|
+
setContent(saved);
|
|
534
|
+
}
|
|
535
|
+
}, []);
|
|
199
536
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
537
|
+
// 콘텐츠 저장
|
|
538
|
+
const handleContentChange = (blocks) => {
|
|
539
|
+
const jsonContent = JSON.stringify(blocks);
|
|
540
|
+
localStorage.setItem("editor-content", jsonContent);
|
|
541
|
+
};
|
|
203
542
|
|
|
204
|
-
|
|
205
|
-
<LumirEditor
|
|
543
|
+
return (
|
|
544
|
+
<LumirEditor
|
|
545
|
+
initialContent={content}
|
|
546
|
+
onContentChange={handleContentChange}
|
|
547
|
+
/>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
206
550
|
```
|
|
207
551
|
|
|
208
|
-
|
|
552
|
+
---
|
|
209
553
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
};
|
|
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
|
+
}
|
|
244
573
|
|
|
245
|
-
|
|
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
|
+
}
|
|
246
592
|
```
|
|
247
593
|
|
|
248
|
-
###
|
|
594
|
+
### Tailwind CSS와 함께 사용
|
|
249
595
|
|
|
250
596
|
```tsx
|
|
251
|
-
|
|
252
|
-
light: {
|
|
253
|
-
colors: {
|
|
254
|
-
editor: { text: '#374151', background: '#ffffff' },
|
|
255
|
-
menu: { text: '#111827', background: '#f9fafb' },
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
dark: {
|
|
259
|
-
colors: {
|
|
260
|
-
editor: { text: '#f9fafb', background: '#111827' },
|
|
261
|
-
menu: { text: '#e5e7eb', background: '#1f2937' },
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
};
|
|
597
|
+
import { LumirEditor, cn } from "@lumir-company/editor";
|
|
265
598
|
|
|
266
|
-
<LumirEditor
|
|
599
|
+
<LumirEditor
|
|
600
|
+
className={cn(
|
|
601
|
+
"min-h-[400px] rounded-xl",
|
|
602
|
+
"border border-gray-200 shadow-lg",
|
|
603
|
+
"focus-within:ring-2 focus-within:ring-blue-500"
|
|
604
|
+
)}
|
|
605
|
+
/>;
|
|
267
606
|
```
|
|
268
607
|
|
|
269
|
-
|
|
608
|
+
### 커스텀 스타일 적용
|
|
270
609
|
|
|
271
|
-
|
|
610
|
+
```css
|
|
611
|
+
/* globals.css */
|
|
612
|
+
.my-editor .bn-editor {
|
|
613
|
+
padding-left: 30px;
|
|
614
|
+
padding-right: 20px;
|
|
615
|
+
font-size: 16px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.my-editor [data-content-type="heading"] {
|
|
619
|
+
font-weight: 700;
|
|
620
|
+
margin-top: 24px;
|
|
621
|
+
}
|
|
622
|
+
```
|
|
272
623
|
|
|
273
624
|
```tsx
|
|
274
|
-
|
|
275
|
-
<LumirEditor
|
|
276
|
-
onContentChange={(blocks) => {
|
|
277
|
-
// 이미지가 포함된 블록들을 확인
|
|
278
|
-
const hasImages = blocks.some((block) =>
|
|
279
|
-
block.content?.some((content) => content.type === 'image'),
|
|
280
|
-
);
|
|
281
|
-
if (hasImages) {
|
|
282
|
-
console.log('이미지가 포함된 콘텐츠:', blocks);
|
|
283
|
-
}
|
|
284
|
-
}}
|
|
285
|
-
/>
|
|
625
|
+
<LumirEditor className="my-editor" />
|
|
286
626
|
```
|
|
287
627
|
|
|
288
|
-
|
|
628
|
+
---
|
|
289
629
|
|
|
290
|
-
|
|
291
|
-
import type {
|
|
292
|
-
LumirEditorProps,
|
|
293
|
-
DefaultPartialBlock,
|
|
294
|
-
ContentUtils,
|
|
295
|
-
EditorConfig,
|
|
296
|
-
} from '@lumir-company/editor';
|
|
630
|
+
## ⚠️ 주의사항 및 트러블슈팅
|
|
297
631
|
|
|
298
|
-
|
|
299
|
-
const isValidContent = ContentUtils.isValidJSONString(jsonString);
|
|
300
|
-
const blocks = ContentUtils.parseJSONContent(jsonString);
|
|
632
|
+
### 필수 체크리스트
|
|
301
633
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
634
|
+
| 항목 | 체크 |
|
|
635
|
+
| -------------------- | ------------------------------------------- |
|
|
636
|
+
| CSS 임포트 | `import "@lumir-company/editor/style.css";` |
|
|
637
|
+
| 컨테이너 높이 설정 | 부모 요소에 높이 지정 필수 |
|
|
638
|
+
| Next.js SSR 비활성화 | `dynamic(..., { ssr: false })` 사용 |
|
|
639
|
+
| React 버전 | 18.0.0 이상 필요 |
|
|
306
640
|
|
|
307
|
-
|
|
641
|
+
### 일반적인 문제 해결
|
|
308
642
|
|
|
309
|
-
|
|
643
|
+
#### 1. 에디터가 렌더링되지 않음
|
|
310
644
|
|
|
311
645
|
```tsx
|
|
312
|
-
//
|
|
313
|
-
<
|
|
314
|
-
<LumirEditor />
|
|
315
|
-
</div>
|
|
646
|
+
// ❌ 잘못된 사용
|
|
647
|
+
<LumirEditor />;
|
|
316
648
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
649
|
+
// ✅ 올바른 사용 - CSS 임포트 필요
|
|
650
|
+
import "@lumir-company/editor/style.css";
|
|
651
|
+
<LumirEditor />;
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
#### 2. Next.js에서 hydration 오류
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
// ❌ 잘못된 사용
|
|
658
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
659
|
+
|
|
660
|
+
// ✅ 올바른 사용 - dynamic import 사용
|
|
661
|
+
const LumirEditor = dynamic(
|
|
662
|
+
() =>
|
|
663
|
+
import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
|
|
664
|
+
{ ssr: false }
|
|
665
|
+
);
|
|
321
666
|
```
|
|
322
667
|
|
|
323
|
-
|
|
668
|
+
#### 3. 높이가 0으로 표시됨
|
|
324
669
|
|
|
325
670
|
```tsx
|
|
326
|
-
|
|
327
|
-
|
|
671
|
+
// ❌ 잘못된 사용
|
|
672
|
+
<LumirEditor />
|
|
673
|
+
|
|
674
|
+
// ✅ 올바른 사용 - 부모 요소에 높이 설정
|
|
675
|
+
<div className="h-[400px]">
|
|
676
|
+
<LumirEditor />
|
|
328
677
|
</div>
|
|
329
678
|
```
|
|
330
679
|
|
|
331
|
-
|
|
680
|
+
#### 4. 이미지 업로드 실패
|
|
332
681
|
|
|
333
682
|
```tsx
|
|
683
|
+
// uploadFile 또는 s3Upload 중 하나 반드시 설정
|
|
334
684
|
<LumirEditor
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
685
|
+
uploadFile={async (file) => {
|
|
686
|
+
// 업로드 로직
|
|
687
|
+
return imageUrl;
|
|
688
|
+
}}
|
|
689
|
+
// 또는
|
|
690
|
+
s3Upload={{
|
|
691
|
+
apiEndpoint: "/api/s3/presigned",
|
|
692
|
+
env: "development",
|
|
693
|
+
path: "images",
|
|
694
|
+
}}
|
|
339
695
|
/>
|
|
340
696
|
```
|
|
341
697
|
|
|
342
|
-
|
|
698
|
+
### 성능 최적화 팁
|
|
343
699
|
|
|
344
|
-
|
|
700
|
+
1. **애니메이션 기본 비활성**: 이미 `animations: false`로 설정되어 성능 최적화됨
|
|
701
|
+
2. **큰 콘텐츠 처리**: 초기 콘텐츠가 클 경우 lazy loading 고려
|
|
702
|
+
3. **이미지 최적화**: 업로드 전 클라이언트에서 이미지 리사이징 권장
|
|
345
703
|
|
|
346
|
-
|
|
347
|
-
// 반드시 CSS를 임포트해야 합니다
|
|
348
|
-
import '@lumir-company/editor/style.css';
|
|
349
|
-
```
|
|
704
|
+
---
|
|
350
705
|
|
|
351
|
-
|
|
706
|
+
## 🏗️ 프로젝트 구조
|
|
352
707
|
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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 통합 가이드
|
|
359
728
|
```
|
|
360
729
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
- ✅ 이미지 파일: PNG, JPG, GIF, WebP, BMP, SVG
|
|
364
|
-
- ❌ 비디오, 오디오, 일반 파일 업로드 불가
|
|
365
|
-
- 🔄 자동 Base64 변환 또는 커스텀 업로더 사용
|
|
730
|
+
---
|
|
366
731
|
|
|
367
|
-
##
|
|
732
|
+
## 📄 라이선스
|
|
368
733
|
|
|
369
|
-
|
|
734
|
+
MIT License
|
|
370
735
|
|
|
371
|
-
|
|
372
|
-
- 🖼️ **이미지 업로드**: Base64 변환 및 드래그앤드롭 지원
|
|
373
|
-
- 🎨 **Theme 지원**: BlockNote 공식 theme prop 지원
|
|
374
|
-
- 📝 **Pretendard 폰트**: 기본 폰트 설정 (14px)
|
|
375
|
-
- 🚫 **미디어 제한**: 비디오/오디오/파일 업로드 비활성화
|
|
736
|
+
---
|
|
376
737
|
|
|
377
|
-
##
|
|
738
|
+
## 🔗 관련 링크
|
|
378
739
|
|
|
379
|
-
|
|
740
|
+
- [GitHub Repository](https://github.com/lumir-company/editor)
|
|
741
|
+
- [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
|
|
742
|
+
- [BlockNote Documentation](https://www.blocknotejs.org/)
|