@lumir-company/editor 0.2.0 → 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 CHANGED
@@ -1,925 +1,742 @@
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 라이선스를 따릅니다.
1
+ # LumirEditor
2
+
3
+ 🖼️ **이미지 전용** BlockNote 기반 Rich Text 에디터
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@lumir-company/editor.svg)](https://www.npmjs.com/package/@lumir-company/editor)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ ### 지원 이미지 형식
38
+
39
+ ```
40
+ PNG, JPEG/JPG, GIF (애니메이션 포함), WebP, BMP, SVG
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 📦 설치
46
+
47
+ ```bash
48
+ # npm
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
+ }
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 🚀 빠른 시작
70
+
71
+ ### 1단계: CSS 임포트 (필수)
72
+
73
+ ```tsx
74
+ import "@lumir-company/editor/style.css";
75
+ ```
76
+
77
+ > ⚠️ **중요**: CSS를 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
78
+
79
+ ### 2단계: 기본 사용
80
+
81
+ ```tsx
82
+ import { LumirEditor } from "@lumir-company/editor";
83
+ import "@lumir-company/editor/style.css";
84
+
85
+ export default function App() {
86
+ return (
87
+ <div className="w-full h-[400px]">
88
+ <LumirEditor onContentChange={(blocks) => console.log(blocks)} />
89
+ </div>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ### 3단계: Next.js에서 사용 (SSR 비활성화 필수)
95
+
96
+ ```tsx
97
+ "use client";
98
+
99
+ import dynamic from "next/dynamic";
100
+ import "@lumir-company/editor/style.css";
101
+
102
+ const LumirEditor = dynamic(
103
+ () =>
104
+ import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
105
+ { ssr: false }
106
+ );
107
+
108
+ export default function EditorPage() {
109
+ return (
110
+ <div className="w-full h-[500px]">
111
+ <LumirEditor
112
+ onContentChange={(blocks) => console.log("Content:", blocks)}
113
+ />
114
+ </div>
115
+ );
116
+ }
117
+ ```
118
+
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
165
+
166
+ ```tsx
167
+ interface S3UploaderConfig {
168
+ apiEndpoint: string; // Presigned URL API 엔드포인트 (필수)
169
+ env: "development" | "production"; // 환경 (필수)
170
+ path: string; // S3 경로 (필수)
171
+ }
172
+ ```
173
+
174
+ ### TableConfig
175
+
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
194
+ <LumirEditor
195
+ s3Upload={{
196
+ apiEndpoint: "/api/s3/presigned",
197
+ env: "development",
198
+ path: "blog/images",
199
+ }}
200
+ onContentChange={(blocks) => console.log(blocks)}
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";
330
+
331
+ <LumirEditor
332
+ className={cn(
333
+ "min-h-[400px] rounded-lg",
334
+ isFullscreen && "fixed inset-0 z-50",
335
+ isDarkMode && "dark-theme"
336
+ )}
337
+ />;
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: 블록 배열
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
+ ### 읽기 전용 모드
464
+
465
+ ```tsx
466
+ <LumirEditor
467
+ editable={false}
468
+ initialContent={savedContent}
469
+ sideMenu={false}
470
+ formattingToolbar={false}
471
+ />
472
+ ```
473
+
474
+ ### 다크 테마
475
+
476
+ ```tsx
477
+ <LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
478
+ ```
479
+
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
+ ### 콘텐츠 저장 불러오기
521
+
522
+ ```tsx
523
+ import { useState, useEffect } from "react";
524
+ import { LumirEditor, ContentUtils } from "@lumir-company/editor";
525
+
526
+ function EditorWithSave() {
527
+ const [content, setContent] = useState<string>("");
528
+
529
+ // 저장된 콘텐츠 불러오기
530
+ useEffect(() => {
531
+ const saved = localStorage.getItem("editor-content");
532
+ if (saved && ContentUtils.isValidJSONString(saved)) {
533
+ setContent(saved);
534
+ }
535
+ }, []);
536
+
537
+ // 콘텐츠 저장
538
+ const handleContentChange = (blocks) => {
539
+ const jsonContent = JSON.stringify(blocks);
540
+ localStorage.setItem("editor-content", jsonContent);
541
+ };
542
+
543
+ return (
544
+ <LumirEditor
545
+ initialContent={content}
546
+ onContentChange={handleContentChange}
547
+ />
548
+ );
549
+ }
550
+ ```
551
+
552
+ ---
553
+
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
+ ```
593
+
594
+ ### Tailwind CSS와 함께 사용
595
+
596
+ ```tsx
597
+ import { LumirEditor, cn } from "@lumir-company/editor";
598
+
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
+ />;
606
+ ```
607
+
608
+ ### 커스텀 스타일 적용
609
+
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
+ ```
623
+
624
+ ```tsx
625
+ <LumirEditor className="my-editor" />
626
+ ```
627
+
628
+ ---
629
+
630
+ ## ⚠️ 주의사항 트러블슈팅
631
+
632
+ ### 필수 체크리스트
633
+
634
+ | 항목 | 체크 |
635
+ | -------------------- | ------------------------------------------- |
636
+ | CSS 임포트 | `import "@lumir-company/editor/style.css";` |
637
+ | 컨테이너 높이 설정 | 부모 요소에 높이 지정 필수 |
638
+ | Next.js SSR 비활성화 | `dynamic(..., { ssr: false })` 사용 |
639
+ | React 버전 | 18.0.0 이상 필요 |
640
+
641
+ ### 일반적인 문제 해결
642
+
643
+ #### 1. 에디터가 렌더링되지 않음
644
+
645
+ ```tsx
646
+ // ❌ 잘못된 사용
647
+ <LumirEditor />;
648
+
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
+ );
666
+ ```
667
+
668
+ #### 3. 높이가 0으로 표시됨
669
+
670
+ ```tsx
671
+ // ❌ 잘못된 사용
672
+ <LumirEditor />
673
+
674
+ // ✅ 올바른 사용 - 부모 요소에 높이 설정
675
+ <div className="h-[400px]">
676
+ <LumirEditor />
677
+ </div>
678
+ ```
679
+
680
+ #### 4. 이미지 업로드 실패
681
+
682
+ ```tsx
683
+ // uploadFile 또는 s3Upload 중 하나 반드시 설정
684
+ <LumirEditor
685
+ uploadFile={async (file) => {
686
+ // 업로드 로직
687
+ return imageUrl;
688
+ }}
689
+ // 또는
690
+ s3Upload={{
691
+ apiEndpoint: "/api/s3/presigned",
692
+ env: "development",
693
+ path: "images",
694
+ }}
695
+ />
696
+ ```
697
+
698
+ ### 성능 최적화 팁
699
+
700
+ 1. **애니메이션 기본 비활성**: 이미 `animations: false`로 설정되어 성능 최적화됨
701
+ 2. **큰 콘텐츠 처리**: 초기 콘텐츠가 클 경우 lazy loading 고려
702
+ 3. **이미지 최적화**: 업로드 전 클라이언트에서 이미지 리사이징 권장
703
+
704
+ ---
705
+
706
+ ## 🏗️ 프로젝트 구조
707
+
708
+ ```
709
+ @lumir-company/editor/
710
+ ├── dist/ # 빌드 출력
711
+ │ ├── index.js # CommonJS 빌드
712
+ │ ├── index.mjs # ESM 빌드
713
+ │ ├── index.d.ts # TypeScript 타입 정의
714
+ │ └── style.css # 스타일시트
715
+ ├── src/
716
+ │ ├── components/
717
+ │ │ └── LumirEditor.tsx # 메인 에디터 컴포넌트
718
+ │ ├── types/
719
+ │ │ ├── editor.ts # 에디터 타입 정의
720
+ │ │ └── index.ts # 타입 export
721
+ │ ├── utils/
722
+ │ │ ├── cn.ts # className 유틸리티
723
+ │ │ └── s3-uploader.ts # S3 업로더
724
+ │ ├── index.ts # 메인 export
725
+ │ └── style.css # 소스 스타일
726
+ └── examples/
727
+ └── tailwind-integration.md # Tailwind 통합 가이드
728
+ ```
729
+
730
+ ---
731
+
732
+ ## 📄 라이선스
733
+
734
+ MIT License
735
+
736
+ ---
737
+
738
+ ## 🔗 관련 링크
739
+
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/)