@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 CHANGED
@@ -1,379 +1,742 @@
1
1
  # LumirEditor
2
2
 
3
- 이미지 전용 BlockNote 기반 Rich Text 에디터 React 컴포넌트
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
+ ### 지원 이미지 형식
4
38
 
5
- ## ✨ 주요 특징
39
+ ```
40
+ PNG, JPEG/JPG, GIF (애니메이션 포함), WebP, BMP, SVG
41
+ ```
6
42
 
7
- - 🖼️ **이미지 전용 에디터**: 이미지 업로드/드래그앤드롭만 지원 (Base64 변환)
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
- ### 1. CSS 임포트 (필수)
69
+ ## 🚀 빠른 시작
70
+
71
+ ### 1단계: CSS 임포트 (필수)
23
72
 
24
73
  ```tsx
25
- import '@lumir-company/editor/style.css';
74
+ import "@lumir-company/editor/style.css";
26
75
  ```
27
76
 
28
- ### 2. 기본 사용
77
+ > ⚠️ **중요**: CSS를 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
78
+
79
+ ### 2단계: 기본 사용
29
80
 
30
81
  ```tsx
31
- import { LumirEditor } from '@lumir-company/editor';
32
- import '@lumir-company/editor/style.css';
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='w-full h-[400px]'>
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. Next.js에서 사용 (SSR 비활성화)
94
+ ### 3단계: Next.js에서 사용 (SSR 비활성화 필수)
44
95
 
45
96
  ```tsx
46
- 'use client';
47
- import dynamic from 'next/dynamic';
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
- () => import('@lumir-company/editor').then((m) => m.LumirEditor),
51
- { ssr: false },
103
+ () =>
104
+ import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
105
+ { ssr: false }
52
106
  );
53
107
 
54
- export default function Editor() {
108
+ export default function EditorPage() {
55
109
  return (
56
- <div className='w-full h-[500px]'>
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
- ## 📚 핵심 Props
64
-
65
- | Prop | 타입 | 기본값 | 설명 |
66
- | ------------------- | ---------------------------------- | --------- | ---------------- |
67
- | `initialContent` | `DefaultPartialBlock[] \| string` | - | 초기 콘텐츠 |
68
- | `className` | `string` | `""` | CSS 클래스 |
69
- | `theme` | `"light" \| "dark" \| ThemeObject` | `"light"` | 에디터 테마 |
70
- | `onContentChange` | `(blocks) => void` | - | 콘텐츠 변경 콜백 |
71
- | `editable` | `boolean` | `true` | 편집 가능 여부 |
72
- | `sideMenuAddButton` | `boolean` | `false` | Add 버튼 표시 |
73
-
74
- ### 고급 Props
75
-
76
- | Prop | 타입 | 기본값 | 설명 |
77
- | --------------------- | --------------------------------- | ------ | -------------------- |
78
- | `uploadFile` | `(file: File) => Promise<string>` | - | 커스텀 이미지 업로더 |
79
- | `storeImagesAsBase64` | `boolean` | `true` | Base64 저장 여부 |
80
- | `formattingToolbar` | `boolean` | `true` | 서식 툴바 표시 |
81
- | `linkToolbar` | `boolean` | `true` | 링크 툴바 표시 |
82
- | `sideMenu` | `boolean` | `true` | 사이드 메뉴 표시 |
83
- | `slashMenu` | `boolean` | `true` | 슬래시 메뉴 표시 |
84
- | `emojiPicker` | `boolean` | `true` | 이모지 피커 표시 |
85
- | `filePanel` | `boolean` | `true` | 파일 패널 표시 |
86
- | `tableHandles` | `boolean` | `true` | 표 핸들 표시 |
87
- | `tables` | `TableConfig` | - | 테이블 설정 |
88
- | `heading` | `HeadingConfig` | - | 헤딩 설정 |
89
- | `animations` | `boolean` | `true` | 애니메이션 활성화 |
90
- | `defaultStyles` | `boolean` | `true` | 기본 스타일 적용 |
91
-
92
- ### Props 사용 예시
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
- import { LumirEditor } from '@lumir-company/editor';
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
- // 1. 초기 콘텐츠 설정
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
- initialContent="에디터 시작 텍스트"
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
- // 2. 블록 배열로 초기 콘텐츠 설정
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: 'paragraph',
107
- content: [{ type: 'text', text: '안녕하세요!' }]
446
+ type: "heading",
447
+ props: { level: 1 },
448
+ content: [{ type: "text", text: "제목입니다", styles: {} }],
108
449
  },
109
450
  {
110
- type: 'heading',
111
- props: { level: 2 },
112
- content: [{ type: 'text', text: '제목입니다' }]
113
- }
451
+ type: "paragraph",
452
+ content: [{ type: "text", text: "본문 내용...", styles: {} }],
453
+ },
114
454
  ]}
115
455
  />
116
456
 
117
- // 3. 이벤트 핸들러 사용
457
+ // 방법 2: JSON 문자열
118
458
  <LumirEditor
119
- onContentChange={(blocks) => {
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
- // 4. UI 컴포넌트 제어
129
- <LumirEditor
130
- sideMenuAddButton={true} // Add 버튼 표시
131
- formattingToolbar={false} // 서식 툴바 숨김
132
- linkToolbar={false} // 링크 툴바 숨김
133
- slashMenu={false} // 슬래시 메뉴 숨김
134
- emojiPicker={false} // 이모지 피커 숨김
135
- />
463
+ ### 읽기 전용 모드
136
464
 
137
- // 5. 읽기 전용 모드
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
- // 6. 커스텀 이미지 업로더
476
+ ```tsx
477
+ <LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
478
+ ```
479
+
480
+ ### S3 이미지 업로드
481
+
482
+ ```tsx
146
483
  <LumirEditor
147
- uploadFile={async (file) => {
148
- const formData = new FormData();
149
- formData.append('image', file);
150
- const response = await fetch('/api/upload', {
151
- method: 'POST',
152
- body: formData
153
- });
154
- return (await response.json()).url;
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
- // 7. 테이블과 헤딩 설정
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: true,
165
- headers: true
511
+ cellTextColor: false, // 셀 텍스트 색상 비활성
512
+ headers: true,
166
513
  }}
167
514
  heading={{
168
- levels: [1, 2, 3, 4] // H1~H4 허용
515
+ levels: [1, 2, 3], // H4-H6 비활성
169
516
  }}
170
517
  />
518
+ ```
171
519
 
172
- // 8. 애니메이션과 스타일 제어
173
- <LumirEditor
174
- animations={false} // 애니메이션 비활성화
175
- defaultStyles={true} // 기본 스타일 사용
176
- />
520
+ ### 콘텐츠 저장 불러오기
177
521
 
178
- // 9. 완전한 설정 예시
179
- <LumirEditor
180
- initialContent="시작 텍스트"
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
- ## 🎨 Theme 스타일링
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
- ```tsx
201
- // 라이트 모드
202
- <LumirEditor theme="light" />
537
+ // 콘텐츠 저장
538
+ const handleContentChange = (blocks) => {
539
+ const jsonContent = JSON.stringify(blocks);
540
+ localStorage.setItem("editor-content", jsonContent);
541
+ };
203
542
 
204
- // 다크 모드
205
- <LumirEditor theme="dark" />
543
+ return (
544
+ <LumirEditor
545
+ initialContent={content}
546
+ onContentChange={handleContentChange}
547
+ />
548
+ );
549
+ }
206
550
  ```
207
551
 
208
- ### 커스텀 테마 (권장)
552
+ ---
209
553
 
210
- ```tsx
211
- const customTheme = {
212
- colors: {
213
- editor: {
214
- text: '#1f2937',
215
- background: '#ffffff',
216
- },
217
- menu: {
218
- text: '#374151',
219
- background: '#f9fafb',
220
- },
221
- tooltip: {
222
- text: '#6b7280',
223
- background: '#f3f4f6',
224
- },
225
- hovered: {
226
- text: '#111827',
227
- background: '#e5e7eb',
228
- },
229
- selected: {
230
- text: '#ffffff',
231
- background: '#3b82f6',
232
- },
233
- disabled: {
234
- text: '#9ca3af',
235
- background: '#f3f4f6',
236
- },
237
- shadow: '#000000',
238
- border: '#d1d5db',
239
- sideMenu: '#6b7280',
240
- },
241
- borderRadius: 8,
242
- fontFamily: 'Pretendard, system-ui, sans-serif',
243
- };
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
- <LumirEditor theme={customTheme} />;
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
- const dualTheme = {
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 theme={dualTheme} />;
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
- ### 자동 Base64 변환
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
- // 기본적으로 이미지가 Base64로 자동 변환됩니다
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
- ```tsx
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
- const tableConfig = EditorConfig.getDefaultTableConfig();
304
- const headingConfig = EditorConfig.getDefaultHeadingConfig();
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
- ### 1. 컨테이너 크기 설정
643
+ #### 1. 에디터가 렌더링되지 않음
310
644
 
311
645
  ```tsx
312
- // 고정 높이
313
- <div className='h-[400px]'>
314
- <LumirEditor />
315
- </div>
646
+ // 잘못된 사용
647
+ <LumirEditor />;
316
648
 
317
- // 최소 높이
318
- <div className='min-h-[300px]'>
319
- <LumirEditor />
320
- </div>
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
- ### 2. 반응형 디자인
668
+ #### 3. 높이가 0으로 표시됨
324
669
 
325
670
  ```tsx
326
- <div className='w-full h-64 md:h-96 lg:h-[500px]'>
327
- <LumirEditor className='h-full' theme='light' />
671
+ // 잘못된 사용
672
+ <LumirEditor />
673
+
674
+ // ✅ 올바른 사용 - 부모 요소에 높이 설정
675
+ <div className="h-[400px]">
676
+ <LumirEditor />
328
677
  </div>
329
678
  ```
330
679
 
331
- ### 3. 읽기 전용 모드
680
+ #### 4. 이미지 업로드 실패
332
681
 
333
682
  ```tsx
683
+ // uploadFile 또는 s3Upload 중 하나 반드시 설정
334
684
  <LumirEditor
335
- editable={false}
336
- initialContent={savedContent}
337
- formattingToolbar={false}
338
- sideMenu={false}
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
- ### 1. CSS 임포트 필수
700
+ 1. **애니메이션 기본 비활성**: 이미 `animations: false`로 설정되어 성능 최적화됨
701
+ 2. **큰 콘텐츠 처리**: 초기 콘텐츠가 클 경우 lazy loading 고려
702
+ 3. **이미지 최적화**: 업로드 전 클라이언트에서 이미지 리사이징 권장
345
703
 
346
- ```tsx
347
- // 반드시 CSS를 임포트해야 합니다
348
- import '@lumir-company/editor/style.css';
349
- ```
704
+ ---
350
705
 
351
- ### 2. Next.js SSR 비활성화
706
+ ## 🏗️ 프로젝트 구조
352
707
 
353
- ```tsx
354
- // 서버 사이드 렌더링을 비활성화해야 합니다
355
- const LumirEditor = dynamic(
356
- () => import('@lumir-company/editor').then((m) => m.LumirEditor),
357
- { ssr: false },
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
- ### 3. 이미지만 지원
362
-
363
- - ✅ 이미지 파일: PNG, JPG, GIF, WebP, BMP, SVG
364
- - ❌ 비디오, 오디오, 일반 파일 업로드 불가
365
- - 🔄 자동 Base64 변환 또는 커스텀 업로더 사용
730
+ ---
366
731
 
367
- ## 📋 변경 기록
732
+ ## 📄 라이선스
368
733
 
369
- ### v0.0.1
734
+ MIT License
370
735
 
371
- - 🎉 **초기 릴리스**: 이미지 전용 BlockNote 에디터
372
- - 🖼️ **이미지 업로드**: Base64 변환 및 드래그앤드롭 지원
373
- - 🎨 **Theme 지원**: BlockNote 공식 theme prop 지원
374
- - 📝 **Pretendard 폰트**: 기본 폰트 설정 (14px)
375
- - 🚫 **미디어 제한**: 비디오/오디오/파일 업로드 비활성화
736
+ ---
376
737
 
377
- ## 📄 라이선스
738
+ ## 🔗 관련 링크
378
739
 
379
- MIT License
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/)