@lumir-company/editor 0.2.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 +925 -0
- package/dist/index.d.mts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +449 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +427 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +13 -0
- package/package.json +71 -0
package/README.md
ADDED
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
# LumirEditor
|
|
2
|
+
|
|
3
|
+
BlockNote 기반의 고급 Rich Text 에디터 React 컴포넌트
|
|
4
|
+
|
|
5
|
+
## ✨ 주요 특징
|
|
6
|
+
|
|
7
|
+
- 🚀 **하이브리드 콘텐츠 지원**: JSON 객체 배열 또는 JSON 문자열 모두 지원
|
|
8
|
+
- 📷 **이미지 처리**: 업로드/붙여넣기/드래그앤드롭 완벽 지원
|
|
9
|
+
- 🎨 **유연한 스타일링**: Tailwind CSS 클래스와 커스텀 CSS 모두 지원
|
|
10
|
+
- 📱 **반응형 UI**: 모든 툴바와 메뉴 개별 제어 가능
|
|
11
|
+
- 🔧 **TypeScript 완벽 지원**: 모든 타입 정의 포함
|
|
12
|
+
- ⚡ **최적화된 성능**: 스마트 렌더링과 메모리 관리
|
|
13
|
+
|
|
14
|
+
## 📦 설치 및 초기 세팅
|
|
15
|
+
|
|
16
|
+
### 1. 패키지 설치
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @lumir-company/editor
|
|
20
|
+
# 또는
|
|
21
|
+
yarn add @lumir-company/editor
|
|
22
|
+
# 또는
|
|
23
|
+
pnpm add @lumir-company/editor
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. 필수 CSS 임포트
|
|
27
|
+
|
|
28
|
+
에디터가 제대로 작동하려면 반드시 CSS 파일을 임포트해야 합니다:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// App.tsx 또는 main.tsx에서
|
|
32
|
+
import "@lumir-company/editor/style.css";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**또는 개별 CSS 임포트:**
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import "@blocknote/core/fonts/inter.css";
|
|
39
|
+
import "@blocknote/mantine/style.css";
|
|
40
|
+
import "@blocknote/react/style.css";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. TypeScript 설정 (권장)
|
|
44
|
+
|
|
45
|
+
`tsconfig.json`에서 모듈 해석 설정:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"compilerOptions": {
|
|
50
|
+
"moduleResolution": "node",
|
|
51
|
+
"allowSyntheticDefaultImports": true,
|
|
52
|
+
"esModuleInterop": true,
|
|
53
|
+
"jsx": "react-jsx",
|
|
54
|
+
"lib": ["dom", "dom.iterable", "es6"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 4. Tailwind CSS 설정 (선택사항)
|
|
60
|
+
|
|
61
|
+
패키지의 Tailwind 클래스를 사용하려면 `tailwind.config.js`에 추가:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
module.exports = {
|
|
65
|
+
content: [
|
|
66
|
+
"./src/**/*.{js,ts,jsx,tsx}", // 기존 경로들
|
|
67
|
+
"./node_modules/@lumir-company/editor/dist/**/*.js", // 패키지 경로 추가
|
|
68
|
+
],
|
|
69
|
+
theme: {
|
|
70
|
+
extend: {},
|
|
71
|
+
},
|
|
72
|
+
plugins: [],
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 5. 번들러별 설정
|
|
77
|
+
|
|
78
|
+
#### Next.js
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// next.config.js
|
|
82
|
+
/** @type {import('next').NextConfig} */
|
|
83
|
+
const nextConfig = {
|
|
84
|
+
transpilePackages: ["@lumir-company/editor"],
|
|
85
|
+
experimental: {
|
|
86
|
+
esmExternals: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
module.exports = nextConfig;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Vite
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// vite.config.ts
|
|
97
|
+
import { defineConfig } from "vite";
|
|
98
|
+
import react from "@vitejs/plugin-react";
|
|
99
|
+
|
|
100
|
+
export default defineConfig({
|
|
101
|
+
plugins: [react()],
|
|
102
|
+
optimizeDeps: {
|
|
103
|
+
include: ["@lumir-company/editor"],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Webpack
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
// webpack.config.js
|
|
112
|
+
module.exports = {
|
|
113
|
+
resolve: {
|
|
114
|
+
alias: {
|
|
115
|
+
// BlockNote 관련 폴리필이 필요한 경우
|
|
116
|
+
crypto: "crypto-browserify",
|
|
117
|
+
stream: "stream-browserify",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 🚀 사용법
|
|
124
|
+
|
|
125
|
+
### 기본 사용법
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
129
|
+
import "@lumir-company/editor/style.css";
|
|
130
|
+
|
|
131
|
+
export default function App() {
|
|
132
|
+
return (
|
|
133
|
+
<LumirEditor
|
|
134
|
+
initialContent="빈 상태에서 시작"
|
|
135
|
+
onContentChange={(blocks) => {
|
|
136
|
+
console.log("변경된 내용:", blocks);
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Next.js에서 사용
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
"use client";
|
|
147
|
+
import dynamic from "next/dynamic";
|
|
148
|
+
|
|
149
|
+
const LumirEditor = dynamic(
|
|
150
|
+
() => import("@lumir-company/editor").then((m) => m.LumirEditor),
|
|
151
|
+
{ ssr: false }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export default function EditorPage() {
|
|
155
|
+
return (
|
|
156
|
+
<div className="container mx-auto p-4">
|
|
157
|
+
<LumirEditor
|
|
158
|
+
initialContent={[
|
|
159
|
+
{
|
|
160
|
+
type: "paragraph",
|
|
161
|
+
content: [{ type: "text", text: "안녕하세요!" }],
|
|
162
|
+
},
|
|
163
|
+
]}
|
|
164
|
+
onContentChange={(blocks) => saveDocument(blocks)}
|
|
165
|
+
uploadFile={async (file) => {
|
|
166
|
+
// 파일 업로드 로직
|
|
167
|
+
const url = await uploadToServer(file);
|
|
168
|
+
return url;
|
|
169
|
+
}}
|
|
170
|
+
theme="light"
|
|
171
|
+
className="min-h-[400px] rounded-lg border"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 고급 설정 예시
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<LumirEditor
|
|
182
|
+
// 콘텐츠 설정
|
|
183
|
+
initialContent='[{"type":"paragraph","content":[{"type":"text","text":"JSON 문자열도 지원"}]}]'
|
|
184
|
+
placeholder="여기에 내용을 입력하세요..."
|
|
185
|
+
initialEmptyBlocks={5}
|
|
186
|
+
// 파일 업로드
|
|
187
|
+
uploadFile={async (file) => await uploadToS3(file)}
|
|
188
|
+
storeImagesAsBase64={false}
|
|
189
|
+
allowVideoUpload={true}
|
|
190
|
+
allowAudioUpload={true}
|
|
191
|
+
// UI 커스터마이징
|
|
192
|
+
theme="dark"
|
|
193
|
+
formattingToolbar={true}
|
|
194
|
+
sideMenuAddButton={false} // Add 버튼 숨기고 드래그만
|
|
195
|
+
className="min-h-[600px] rounded-xl shadow-lg"
|
|
196
|
+
// 이벤트 핸들러
|
|
197
|
+
onContentChange={(blocks) => {
|
|
198
|
+
autoSave(JSON.stringify(blocks));
|
|
199
|
+
}}
|
|
200
|
+
onSelectionChange={() => updateToolbar()}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 📚 Props API
|
|
205
|
+
|
|
206
|
+
### 📝 콘텐츠 관련
|
|
207
|
+
|
|
208
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
209
|
+
| -------------------- | ------------------------------------------ | ----------- | --------------------------------------------- |
|
|
210
|
+
| `initialContent` | `DefaultPartialBlock[] \| string` | `undefined` | 초기 콘텐츠 (JSON 객체 배열 또는 JSON 문자열) |
|
|
211
|
+
| `initialEmptyBlocks` | `number` | `3` | 초기 빈 블록 개수 |
|
|
212
|
+
| `placeholder` | `string` | `undefined` | 첫 번째 블록의 placeholder 텍스트 |
|
|
213
|
+
| `onContentChange` | `(content: DefaultPartialBlock[]) => void` | `undefined` | 콘텐츠 변경 시 호출되는 콜백 |
|
|
214
|
+
|
|
215
|
+
#### 사용 예시:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
// 1. JSON 객체 배열로 초기 콘텐츠 설정
|
|
219
|
+
const initialBlocks = [
|
|
220
|
+
{
|
|
221
|
+
type: "paragraph",
|
|
222
|
+
props: {
|
|
223
|
+
textColor: "default",
|
|
224
|
+
backgroundColor: "default",
|
|
225
|
+
textAlignment: "left"
|
|
226
|
+
},
|
|
227
|
+
content: [{ type: "text", text: "환영합니다!", styles: {} }],
|
|
228
|
+
children: []
|
|
229
|
+
}
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
<LumirEditor initialContent={initialBlocks} />
|
|
233
|
+
|
|
234
|
+
// 2. JSON 문자열로 설정 (API 응답, 로컬스토리지 등)
|
|
235
|
+
const savedContent = localStorage.getItem('editorContent');
|
|
236
|
+
<LumirEditor initialContent={savedContent} />
|
|
237
|
+
|
|
238
|
+
// 3. Placeholder와 빈 블록 개수 설정
|
|
239
|
+
<LumirEditor
|
|
240
|
+
placeholder="제목을 입력하세요..."
|
|
241
|
+
initialEmptyBlocks={1} // 한 개의 빈 블록만 생성
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
// 4. 다양한 초기 상태 조합
|
|
245
|
+
<LumirEditor
|
|
246
|
+
initialContent="" // 빈 문자열
|
|
247
|
+
placeholder="새 문서를 작성하세요"
|
|
248
|
+
initialEmptyBlocks={5} // 5개의 빈 블록 생성
|
|
249
|
+
onContentChange={(content) => {
|
|
250
|
+
// 실시간으로 변경사항 감지
|
|
251
|
+
console.log(`총 ${content.length}개 블록`);
|
|
252
|
+
autosave(JSON.stringify(content));
|
|
253
|
+
}}
|
|
254
|
+
/>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
#### ⚠️ 중요한 사용 팁:
|
|
258
|
+
|
|
259
|
+
- `initialContent`가 있으면 `placeholder`와 `initialEmptyBlocks`는 무시됩니다
|
|
260
|
+
- 콘텐츠 변경 시 `onContentChange`는 항상 `DefaultPartialBlock[]` 타입으로 반환됩니다
|
|
261
|
+
- 빈 문자열이나 잘못된 JSON은 자동으로 빈 블록으로 변환됩니다
|
|
262
|
+
|
|
263
|
+
### 📁 파일 및 미디어
|
|
264
|
+
|
|
265
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
266
|
+
| --------------------- | --------------------------------- | ----------- | ------------------------------------------- |
|
|
267
|
+
| `uploadFile` | `(file: File) => Promise<string>` | `undefined` | 커스텀 파일 업로드 함수 |
|
|
268
|
+
| `storeImagesAsBase64` | `boolean` | `true` | 폴백 이미지 저장 방식 (Base64 vs ObjectURL) |
|
|
269
|
+
| `allowVideoUpload` | `boolean` | `false` | 비디오 업로드 허용 |
|
|
270
|
+
| `allowAudioUpload` | `boolean` | `false` | 오디오 업로드 허용 |
|
|
271
|
+
| `allowFileUpload` | `boolean` | `false` | 일반 파일 업로드 허용 |
|
|
272
|
+
|
|
273
|
+
#### 사용 예시:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// 1. 기본 이미지 업로드 (Base64 저장)
|
|
277
|
+
<LumirEditor /> // storeImagesAsBase64={true}가 기본값
|
|
278
|
+
|
|
279
|
+
// 2. ObjectURL 방식 (브라우저 메모리)
|
|
280
|
+
<LumirEditor storeImagesAsBase64={false} />
|
|
281
|
+
|
|
282
|
+
// 3. 커스텀 업로드 함수 (S3, Cloudinary 등)
|
|
283
|
+
<LumirEditor
|
|
284
|
+
uploadFile={async (file) => {
|
|
285
|
+
// 파일 크기 검증
|
|
286
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
287
|
+
throw new Error('파일 크기는 5MB 이하여야 합니다');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// FormData로 업로드
|
|
291
|
+
const formData = new FormData();
|
|
292
|
+
formData.append("file", file);
|
|
293
|
+
formData.append("folder", "editor-uploads");
|
|
294
|
+
|
|
295
|
+
const response = await fetch("/api/upload", {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: {
|
|
298
|
+
'Authorization': `Bearer ${userToken}`,
|
|
299
|
+
},
|
|
300
|
+
body: formData,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (!response.ok) {
|
|
304
|
+
throw new Error('업로드 실패');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { url } = await response.json();
|
|
308
|
+
return url; // 반드시 접근 가능한 public URL 반환
|
|
309
|
+
}}
|
|
310
|
+
// 비디오와 오디오도 허용
|
|
311
|
+
allowVideoUpload={true}
|
|
312
|
+
allowAudioUpload={true}
|
|
313
|
+
/>
|
|
314
|
+
|
|
315
|
+
// 4. AWS S3 직접 업로드 예시
|
|
316
|
+
<LumirEditor
|
|
317
|
+
uploadFile={async (file) => {
|
|
318
|
+
// 1. Presigned URL 받기
|
|
319
|
+
const presignedResponse = await fetch('/api/s3/presigned-url', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
322
|
+
body: JSON.stringify({
|
|
323
|
+
fileName: file.name,
|
|
324
|
+
fileType: file.type,
|
|
325
|
+
}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const { uploadUrl, fileUrl } = await presignedResponse.json();
|
|
329
|
+
|
|
330
|
+
// 2. S3에 직접 업로드
|
|
331
|
+
await fetch(uploadUrl, {
|
|
332
|
+
method: 'PUT',
|
|
333
|
+
body: file,
|
|
334
|
+
headers: {
|
|
335
|
+
'Content-Type': file.type,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// 3. 공개 URL 반환
|
|
340
|
+
return fileUrl;
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### 🔧 업로드 함수 설계 가이드:
|
|
346
|
+
|
|
347
|
+
**입력:** `File` 객체
|
|
348
|
+
**출력:** `Promise<string>` (접근 가능한 URL)
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
// 올바른 예시
|
|
352
|
+
const uploadFile = async (file: File): Promise<string> => {
|
|
353
|
+
// 업로드 로직...
|
|
354
|
+
return "https://cdn.example.com/uploads/image.jpg"; // ✅ 공개 URL
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// 잘못된 예시
|
|
358
|
+
const uploadFile = async (file: File): Promise<string> => {
|
|
359
|
+
return "file://local/path.jpg"; // ❌ 로컬 경로
|
|
360
|
+
return "blob:http://localhost/temp"; // ❌ Blob URL
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### ⚠️ 중요한 업로드 팁:
|
|
365
|
+
|
|
366
|
+
- `uploadFile`이 없으면 `storeImagesAsBase64` 설정에 따라 Base64 또는 ObjectURL 사용
|
|
367
|
+
- 업로드 실패 시 에러를 던지면 해당 파일은 삽입되지 않음
|
|
368
|
+
- 반환된 URL은 브라우저에서 직접 접근 가능해야 함
|
|
369
|
+
- 대용량 파일은 청크 업로드나 압축을 고려하세요
|
|
370
|
+
|
|
371
|
+
### 🎛️ 에디터 기능
|
|
372
|
+
|
|
373
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
374
|
+
| ------------------- | ----------------------------------------- | ------------------------- | -------------------- |
|
|
375
|
+
| `tables` | `TableConfig` | `모두 true` | 표 기능 설정 |
|
|
376
|
+
| `heading` | `{levels?: (1\|2\|3\|4\|5\|6)[]}` | `{levels: [1,2,3,4,5,6]}` | 헤딩 레벨 설정 |
|
|
377
|
+
| `animations` | `boolean` | `true` | 블록 변환 애니메이션 |
|
|
378
|
+
| `defaultStyles` | `boolean` | `true` | 기본 스타일 적용 |
|
|
379
|
+
| `disableExtensions` | `string[]` | `[]` | 비활성화할 확장 기능 |
|
|
380
|
+
| `tabBehavior` | `"prefer-navigate-ui" \| "prefer-indent"` | `"prefer-navigate-ui"` | Tab 키 동작 |
|
|
381
|
+
| `trailingBlock` | `boolean` | `true` | 문서 끝 빈 블록 유지 |
|
|
382
|
+
|
|
383
|
+
### 🎨 UI 및 테마
|
|
384
|
+
|
|
385
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
386
|
+
| ---------------------- | ----------------------------- | --------- | --------------------- |
|
|
387
|
+
| `theme` | `"light" \| "dark" \| object` | `"light"` | 에디터 테마 |
|
|
388
|
+
| `editable` | `boolean` | `true` | 편집 가능 여부 |
|
|
389
|
+
| `className` | `string` | `""` | 커스텀 CSS 클래스 |
|
|
390
|
+
| `includeDefaultStyles` | `boolean` | `true` | 기본 스타일 포함 여부 |
|
|
391
|
+
|
|
392
|
+
### 🛠️ 툴바 및 메뉴
|
|
393
|
+
|
|
394
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
395
|
+
| ------------------- | --------- | ------ | ------------------------- |
|
|
396
|
+
| `formattingToolbar` | `boolean` | `true` | 서식 툴바 표시 |
|
|
397
|
+
| `linkToolbar` | `boolean` | `true` | 링크 툴바 표시 |
|
|
398
|
+
| `sideMenu` | `boolean` | `true` | 사이드 메뉴 표시 |
|
|
399
|
+
| `sideMenuAddButton` | `boolean` | `true` | 사이드 메뉴 Add 버튼 표시 |
|
|
400
|
+
| `slashMenu` | `boolean` | `true` | 슬래시 메뉴 표시 |
|
|
401
|
+
| `emojiPicker` | `boolean` | `true` | 이모지 피커 표시 |
|
|
402
|
+
| `filePanel` | `boolean` | `true` | 파일 패널 표시 |
|
|
403
|
+
| `tableHandles` | `boolean` | `true` | 표 핸들 표시 |
|
|
404
|
+
| `comments` | `boolean` | `true` | 댓글 기능 표시 |
|
|
405
|
+
|
|
406
|
+
### 🔗 고급 설정
|
|
407
|
+
|
|
408
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
409
|
+
| ------------------- | -------------------------------------------- | ----------- | -------------------- |
|
|
410
|
+
| `editorRef` | `React.MutableRefObject<EditorType \| null>` | `undefined` | 에디터 인스턴스 참조 |
|
|
411
|
+
| `domAttributes` | `Record<string, string>` | `{}` | DOM 속성 추가 |
|
|
412
|
+
| `resolveFileUrl` | `(url: string) => Promise<string>` | `undefined` | 파일 URL 변환 함수 |
|
|
413
|
+
| `onSelectionChange` | `() => void` | `undefined` | 선택 영역 변경 콜백 |
|
|
414
|
+
|
|
415
|
+
## 📖 타입 정의
|
|
416
|
+
|
|
417
|
+
### 주요 타입 가져오기
|
|
418
|
+
|
|
419
|
+
```tsx
|
|
420
|
+
import type {
|
|
421
|
+
LumirEditorProps,
|
|
422
|
+
EditorType,
|
|
423
|
+
DefaultPartialBlock,
|
|
424
|
+
DefaultBlockSchema,
|
|
425
|
+
DefaultInlineContentSchema,
|
|
426
|
+
DefaultStyleSchema,
|
|
427
|
+
PartialBlock,
|
|
428
|
+
BlockNoteEditor,
|
|
429
|
+
} from "@lumir-company/editor";
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### 타입 사용 예시
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
import { useRef } from "react";
|
|
436
|
+
import {
|
|
437
|
+
LumirEditor,
|
|
438
|
+
type EditorType,
|
|
439
|
+
type DefaultPartialBlock,
|
|
440
|
+
} from "@lumir-company/editor";
|
|
441
|
+
|
|
442
|
+
function MyEditor() {
|
|
443
|
+
const editorRef = useRef<EditorType>(null);
|
|
444
|
+
|
|
445
|
+
const handleContentChange = (content: DefaultPartialBlock[]) => {
|
|
446
|
+
console.log("변경된 블록:", content);
|
|
447
|
+
saveToDatabase(JSON.stringify(content));
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const insertImage = () => {
|
|
451
|
+
editorRef.current?.pasteHTML('<img src="/example.jpg" alt="Example" />');
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div>
|
|
456
|
+
<button onClick={insertImage}>이미지 삽입</button>
|
|
457
|
+
<LumirEditor
|
|
458
|
+
editorRef={editorRef}
|
|
459
|
+
onContentChange={handleContentChange}
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## 🎨 스타일 커스터마이징 완벽 가이드
|
|
467
|
+
|
|
468
|
+
### 1. 기본 스타일 시스템
|
|
469
|
+
|
|
470
|
+
LumirEditor는 3가지 스타일링 방법을 제공합니다:
|
|
471
|
+
|
|
472
|
+
1. **기본 스타일**: `includeDefaultStyles={true}` (권장)
|
|
473
|
+
2. **Tailwind CSS**: `className` prop으로 유틸리티 클래스 적용
|
|
474
|
+
3. **커스텀 CSS**: 전통적인 CSS 클래스와 선택자 사용
|
|
475
|
+
|
|
476
|
+
### 2. 기본 설정 및 제어
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
// 기본 스타일 포함 (권장)
|
|
480
|
+
<LumirEditor
|
|
481
|
+
includeDefaultStyles={true} // 기본값
|
|
482
|
+
className="추가-커스텀-클래스"
|
|
483
|
+
/>
|
|
484
|
+
|
|
485
|
+
// 기본 스타일 완전 제거 (고급 사용자)
|
|
486
|
+
<LumirEditor
|
|
487
|
+
includeDefaultStyles={false}
|
|
488
|
+
className="완전-커스텀-에디터-스타일"
|
|
489
|
+
/>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 3. Tailwind CSS 스타일링
|
|
493
|
+
|
|
494
|
+
#### 기본 레이아웃 스타일링
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
<LumirEditor
|
|
498
|
+
className="
|
|
499
|
+
min-h-[500px] max-w-4xl mx-auto
|
|
500
|
+
rounded-xl border border-gray-200 shadow-lg
|
|
501
|
+
bg-white dark:bg-gray-900
|
|
502
|
+
"
|
|
503
|
+
/>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
#### 반응형 스타일링
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
<LumirEditor
|
|
510
|
+
className="
|
|
511
|
+
h-64 md:h-96 lg:h-[500px]
|
|
512
|
+
text-sm md:text-base
|
|
513
|
+
p-2 md:p-4 lg:p-6
|
|
514
|
+
rounded-md md:rounded-lg lg:rounded-xl
|
|
515
|
+
shadow-sm md:shadow-md lg:shadow-lg
|
|
516
|
+
"
|
|
517
|
+
/>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
#### 고급 내부 요소 스타일링
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
<LumirEditor
|
|
524
|
+
className="
|
|
525
|
+
/* 에디터 영역 패딩 조정 */
|
|
526
|
+
[&_.bn-editor]:px-8 [&_.bn-editor]:py-4
|
|
527
|
+
|
|
528
|
+
/* 특정 블록 타입 스타일링 */
|
|
529
|
+
[&_[data-content-type='paragraph']]:text-base [&_[data-content-type='paragraph']]:leading-relaxed
|
|
530
|
+
[&_[data-content-type='heading']]:font-bold [&_[data-content-type='heading']]:text-gray-900
|
|
531
|
+
[&_[data-content-type='list']]:ml-4
|
|
532
|
+
|
|
533
|
+
/* 포커스 상태 스타일링 */
|
|
534
|
+
focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500
|
|
535
|
+
|
|
536
|
+
/* 테마별 스타일링 */
|
|
537
|
+
dark:[&_.bn-editor]:bg-gray-800 dark:[&_.bn-editor]:text-white
|
|
538
|
+
|
|
539
|
+
/* 호버 효과 */
|
|
540
|
+
hover:shadow-md transition-shadow duration-200
|
|
541
|
+
"
|
|
542
|
+
/>
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
#### 테마별 스타일링
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
// 라이트 모드
|
|
549
|
+
<LumirEditor
|
|
550
|
+
theme="light"
|
|
551
|
+
className="
|
|
552
|
+
bg-white text-gray-900 border-gray-200
|
|
553
|
+
[&_.bn-editor]:bg-white
|
|
554
|
+
[&_[data-content-type='paragraph']]:text-gray-800
|
|
555
|
+
"
|
|
556
|
+
/>
|
|
557
|
+
|
|
558
|
+
// 다크 모드
|
|
559
|
+
<LumirEditor
|
|
560
|
+
theme="dark"
|
|
561
|
+
className="
|
|
562
|
+
bg-gray-900 text-white border-gray-700
|
|
563
|
+
[&_.bn-editor]:bg-gray-900
|
|
564
|
+
[&_[data-content-type='paragraph']]:text-gray-100
|
|
565
|
+
"
|
|
566
|
+
/>
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### 4. CSS 클래스 스타일링
|
|
570
|
+
|
|
571
|
+
#### 기본 CSS 구조
|
|
572
|
+
|
|
573
|
+
```css
|
|
574
|
+
/* 메인 에디터 컨테이너 */
|
|
575
|
+
.my-custom-editor {
|
|
576
|
+
border: 2px solid #e5e7eb;
|
|
577
|
+
border-radius: 12px;
|
|
578
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
579
|
+
background: white;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* 에디터 내용 영역 */
|
|
583
|
+
.my-custom-editor .bn-editor {
|
|
584
|
+
font-family: "Pretendard", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
585
|
+
font-size: 14px;
|
|
586
|
+
line-height: 1.6;
|
|
587
|
+
padding: 24px;
|
|
588
|
+
min-height: 200px;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* 포커스 상태 */
|
|
592
|
+
.my-custom-editor:focus-within {
|
|
593
|
+
border-color: #3b82f6;
|
|
594
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
#### 블록별 세부 스타일링
|
|
599
|
+
|
|
600
|
+
```css
|
|
601
|
+
/* 문단 블록 */
|
|
602
|
+
.my-custom-editor .bn-block[data-content-type="paragraph"] {
|
|
603
|
+
margin-bottom: 12px;
|
|
604
|
+
font-size: 14px;
|
|
605
|
+
color: #374151;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* 헤딩 블록 */
|
|
609
|
+
.my-custom-editor .bn-block[data-content-type="heading"] {
|
|
610
|
+
font-weight: 700;
|
|
611
|
+
margin: 24px 0 12px 0;
|
|
612
|
+
color: #111827;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.my-custom-editor .bn-block[data-content-type="heading"][data-level="1"] {
|
|
616
|
+
font-size: 28px;
|
|
617
|
+
border-bottom: 2px solid #e5e7eb;
|
|
618
|
+
padding-bottom: 8px;
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## 🔧 고급 사용법
|
|
623
|
+
|
|
624
|
+
### 명령형 API 사용
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
function AdvancedEditor() {
|
|
628
|
+
const editorRef = useRef<EditorType>(null);
|
|
629
|
+
|
|
630
|
+
const insertTable = () => {
|
|
631
|
+
editorRef.current?.insertBlocks(
|
|
632
|
+
[
|
|
633
|
+
{
|
|
634
|
+
type: "table",
|
|
635
|
+
content: {
|
|
636
|
+
type: "tableContent",
|
|
637
|
+
rows: [{ cells: ["셀 1", "셀 2"] }, { cells: ["셀 3", "셀 4"] }],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
editorRef.current.getTextCursorPosition().block
|
|
642
|
+
);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<div>
|
|
647
|
+
<button onClick={insertTable}>표 삽입</button>
|
|
648
|
+
<button onClick={() => editorRef.current?.focus()}>포커스</button>
|
|
649
|
+
<LumirEditor editorRef={editorRef} />
|
|
650
|
+
</div>
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### 커스텀 붙여넣기 핸들러
|
|
656
|
+
|
|
657
|
+
```tsx
|
|
658
|
+
<LumirEditor
|
|
659
|
+
pasteHandler={({ event, defaultPasteHandler }) => {
|
|
660
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
661
|
+
|
|
662
|
+
// URL 감지 시 자동 링크 생성
|
|
663
|
+
if (text?.startsWith("http")) {
|
|
664
|
+
return defaultPasteHandler({ pasteBehavior: "prefer-html" }) ?? false;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 기본 처리
|
|
668
|
+
return defaultPasteHandler() ?? false;
|
|
669
|
+
}}
|
|
670
|
+
/>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 실시간 자동 저장
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
function AutoSaveEditor() {
|
|
677
|
+
const [saveStatus, setSaveStatus] = useState<"saved" | "saving" | "error">(
|
|
678
|
+
"saved"
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const handleContentChange = useCallback(
|
|
682
|
+
debounce(async (content: DefaultPartialBlock[]) => {
|
|
683
|
+
setSaveStatus("saving");
|
|
684
|
+
try {
|
|
685
|
+
await saveToServer(JSON.stringify(content));
|
|
686
|
+
setSaveStatus("saved");
|
|
687
|
+
} catch (error) {
|
|
688
|
+
setSaveStatus("error");
|
|
689
|
+
}
|
|
690
|
+
}, 1000),
|
|
691
|
+
[]
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div>
|
|
696
|
+
<div className="mb-2">
|
|
697
|
+
상태: <span className={`badge badge-${saveStatus}`}>{saveStatus}</span>
|
|
698
|
+
</div>
|
|
699
|
+
<LumirEditor onContentChange={handleContentChange} />
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
## 📱 반응형 디자인
|
|
706
|
+
|
|
707
|
+
```tsx
|
|
708
|
+
<LumirEditor
|
|
709
|
+
className="
|
|
710
|
+
w-full h-96
|
|
711
|
+
md:h-[500px]
|
|
712
|
+
lg:h-[600px]
|
|
713
|
+
rounded-lg
|
|
714
|
+
border border-gray-300
|
|
715
|
+
md:rounded-xl
|
|
716
|
+
lg:shadow-xl
|
|
717
|
+
"
|
|
718
|
+
// 모바일에서는 일부 툴바 숨김
|
|
719
|
+
formattingToolbar={true}
|
|
720
|
+
filePanel={window.innerWidth > 768}
|
|
721
|
+
tableHandles={window.innerWidth > 1024}
|
|
722
|
+
/>
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
## ⚠️ 주의사항 및 문제 해결
|
|
726
|
+
|
|
727
|
+
### 1. SSR 환경 (필수)
|
|
728
|
+
|
|
729
|
+
Next.js 등 SSR 환경에서는 반드시 클라이언트 사이드에서만 렌더링해야 합니다:
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
// ✅ 올바른 방법
|
|
733
|
+
const LumirEditor = dynamic(
|
|
734
|
+
() => import("@lumir-company/editor").then((m) => m.LumirEditor),
|
|
735
|
+
{ ssr: false }
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// ❌ 잘못된 방법 - SSR 오류 발생
|
|
739
|
+
import { LumirEditor } from "@lumir-company/editor";
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### 2. React StrictMode
|
|
743
|
+
|
|
744
|
+
React 19/Next.js 15 일부 환경에서 StrictMode 이슈가 보고되었습니다. 문제 발생 시 임시로 StrictMode를 비활성화하는 것을 고려해보세요.
|
|
745
|
+
|
|
746
|
+
### 3. 일반적인 설치 문제
|
|
747
|
+
|
|
748
|
+
#### TypeScript 타입 오류
|
|
749
|
+
|
|
750
|
+
```bash
|
|
751
|
+
# TypeScript 타입 문제 해결
|
|
752
|
+
npm install --save-dev @types/react @types/react-dom
|
|
753
|
+
|
|
754
|
+
# 또는 tsconfig.json에서
|
|
755
|
+
{
|
|
756
|
+
"compilerOptions": {
|
|
757
|
+
"skipLibCheck": true
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
#### CSS 스타일이 적용되지 않는 경우
|
|
763
|
+
|
|
764
|
+
```tsx
|
|
765
|
+
// 1. CSS 파일이 올바르게 임포트되었는지 확인
|
|
766
|
+
import "@lumir-company/editor/style.css";
|
|
767
|
+
|
|
768
|
+
// 2. Tailwind CSS 설정 확인
|
|
769
|
+
// tailwind.config.js에 패키지 경로 추가 필요
|
|
770
|
+
|
|
771
|
+
// 3. CSS 우선순위 문제인 경우
|
|
772
|
+
.my-editor {
|
|
773
|
+
/* !important 사용 또는 더 구체적인 선택자 */
|
|
774
|
+
}
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
#### 번들러 호환성 문제
|
|
778
|
+
|
|
779
|
+
```js
|
|
780
|
+
// Webpack 설정
|
|
781
|
+
module.exports = {
|
|
782
|
+
resolve: {
|
|
783
|
+
fallback: {
|
|
784
|
+
crypto: require.resolve("crypto-browserify"),
|
|
785
|
+
stream: require.resolve("stream-browserify"),
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// Vite 설정
|
|
791
|
+
export default defineConfig({
|
|
792
|
+
optimizeDeps: {
|
|
793
|
+
include: ["@lumir-company/editor"],
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
#### 이미지 업로드 문제
|
|
799
|
+
|
|
800
|
+
```tsx
|
|
801
|
+
// CORS 문제 해결
|
|
802
|
+
const uploadFile = async (file: File) => {
|
|
803
|
+
const response = await fetch("/api/upload", {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers: {
|
|
806
|
+
// CORS 헤더 확인
|
|
807
|
+
},
|
|
808
|
+
body: formData,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
if (!response.ok) {
|
|
812
|
+
throw new Error(`업로드 실패: ${response.status}`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return url; // 반드시 접근 가능한 public URL
|
|
816
|
+
};
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### 4. 성능 최적화
|
|
820
|
+
|
|
821
|
+
#### 큰 문서 처리
|
|
822
|
+
|
|
823
|
+
```tsx
|
|
824
|
+
// 대용량 문서의 경우 초기 렌더링 최적화
|
|
825
|
+
<LumirEditor
|
|
826
|
+
initialContent={largeContent}
|
|
827
|
+
// 불필요한 기능 비활성화
|
|
828
|
+
animations={false}
|
|
829
|
+
formattingToolbar={false}
|
|
830
|
+
// 메모리 사용량 줄이기
|
|
831
|
+
storeImagesAsBase64={false}
|
|
832
|
+
/>
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
#### 메모리 누수 방지
|
|
836
|
+
|
|
837
|
+
```tsx
|
|
838
|
+
// 컴포넌트 언마운트 시 정리
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
return () => {
|
|
841
|
+
// 에디터 정리 로직
|
|
842
|
+
if (editorRef.current) {
|
|
843
|
+
editorRef.current = null;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}, []);
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
## 🚀 시작하기 체크리스트
|
|
850
|
+
|
|
851
|
+
프로젝트에 LumirEditor를 성공적으로 통합하기 위한 체크리스트:
|
|
852
|
+
|
|
853
|
+
### 📋 필수 설치 단계
|
|
854
|
+
|
|
855
|
+
- [ ] 패키지 설치: `npm install @lumir-company/editor`
|
|
856
|
+
- [ ] CSS 임포트: `import "@lumir-company/editor/style.css"`
|
|
857
|
+
- [ ] TypeScript 타입 설치: `npm install --save-dev @types/react @types/react-dom`
|
|
858
|
+
- [ ] SSR 환경이라면 dynamic import 설정
|
|
859
|
+
|
|
860
|
+
### 🎨 스타일링 설정
|
|
861
|
+
|
|
862
|
+
- [ ] Tailwind CSS 사용 시 `tailwind.config.js`에 패키지 경로 추가
|
|
863
|
+
- [ ] 기본 스타일 적용 확인: `includeDefaultStyles={true}`
|
|
864
|
+
- [ ] 커스텀 스타일이 필요하면 `className` prop 활용
|
|
865
|
+
|
|
866
|
+
### 🔧 기능 설정
|
|
867
|
+
|
|
868
|
+
- [ ] 파일 업로드가 필요하면 `uploadFile` 함수 구현
|
|
869
|
+
- [ ] 콘텐츠 변경 감지가 필요하면 `onContentChange` 콜백 설정
|
|
870
|
+
- [ ] 필요에 따라 툴바와 메뉴 표시/숨김 설정
|
|
871
|
+
|
|
872
|
+
### ✅ 테스트 확인
|
|
873
|
+
|
|
874
|
+
- [ ] 기본 텍스트 입력 동작 확인
|
|
875
|
+
- [ ] 이미지 업로드/붙여넣기 동작 확인
|
|
876
|
+
- [ ] 스타일이 올바르게 적용되는지 확인
|
|
877
|
+
- [ ] 다양한 브라우저에서 테스트
|
|
878
|
+
|
|
879
|
+
## 📋 변경 기록
|
|
880
|
+
|
|
881
|
+
### v0.2.0 (최신)
|
|
882
|
+
|
|
883
|
+
- ✨ **하이브리드 콘텐츠 지원**: `initialContent`에서 JSON 객체 배열과 JSON 문자열 모두 지원
|
|
884
|
+
- ✨ **Placeholder 기능**: 첫 번째 블록에 placeholder 텍스트 설정 가능
|
|
885
|
+
- ✨ **초기 블록 개수 설정**: `initialEmptyBlocks` prop으로 빈 블록 개수 조정
|
|
886
|
+
- 🔧 **유틸리티 클래스 추가**: `ContentUtils`, `EditorConfig` 클래스로 코드 정리
|
|
887
|
+
- 📁 **타입 분리**: 모든 타입 정의를 별도 파일로 분리하여 관리 개선
|
|
888
|
+
- 🎨 **기본 스타일 최적화**: 더 나은 기본 패딩과 스타일 적용
|
|
889
|
+
|
|
890
|
+
### v0.1.15
|
|
891
|
+
|
|
892
|
+
- 🐛 파일 검증 로직 보완
|
|
893
|
+
|
|
894
|
+
### v0.1.14
|
|
895
|
+
|
|
896
|
+
- 🔧 슬래시 추천 메뉴 항목 변경
|
|
897
|
+
|
|
898
|
+
### v0.1.13
|
|
899
|
+
|
|
900
|
+
- ⚙️ Audio, Video, Movie 업로드 기본값을 false로 변경
|
|
901
|
+
|
|
902
|
+
### v0.1.12
|
|
903
|
+
|
|
904
|
+
- 🐛 조건부 Helper 항목 렌더링 수정
|
|
905
|
+
|
|
906
|
+
### v0.1.11
|
|
907
|
+
|
|
908
|
+
- 🐛 이미지 중복 드롭 이슈 수정
|
|
909
|
+
|
|
910
|
+
### v0.1.10
|
|
911
|
+
|
|
912
|
+
- 🎨 기본 이미지 저장 방식을 Base64로 설정
|
|
913
|
+
- ✨ `storeImagesAsBase64` prop 추가
|
|
914
|
+
- 🐛 드래그앤드롭 중복 삽입 방지
|
|
915
|
+
|
|
916
|
+
### v0.1.0
|
|
917
|
+
|
|
918
|
+
- 🎉 초기 릴리스
|
|
919
|
+
|
|
920
|
+
## 📄 라이선스
|
|
921
|
+
|
|
922
|
+
이 패키지는 BlockNote의 무료 기능만을 사용합니다.
|
|
923
|
+
|
|
924
|
+
- 의존성: `@blocknote/core`, `@blocknote/react`, `@blocknote/mantine`
|
|
925
|
+
- BlockNote 라이선스를 따릅니다.
|