@lumir-company/editor 0.2.0 → 0.2.1

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,379 @@
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 에디터 React 컴포넌트
4
+
5
+ ## ✨ 주요 특징
6
+
7
+ - 🖼️ **이미지 전용 에디터**: 이미지 업로드/드래그앤드롭만 지원 (Base64 변환)
8
+ - 🚀 **간소화된 API**: 핵심 기능만 포함한 미니멀한 인터페이스
9
+ - 🎨 **BlockNote Theme 지원**: 공식 theme prop으로 에디터 스타일링
10
+ - 🔧 **TypeScript 지원**: 완전한 타입 안전성
11
+ - 📝 **Pretendard 폰트**: 기본 폰트로 Pretendard 최우선 적용 (14px)
12
+ - ⚡ **경량화**: 비디오/오디오/파일 업로드 기능 제거로 빠른 로딩
13
+
14
+ ## 📦 설치
15
+
16
+ ```bash
17
+ npm install @lumir-company/editor
18
+ ```
19
+
20
+ ## 🚀 기본 사용법
21
+
22
+ ### 1. CSS 임포트 (필수)
23
+
24
+ ```tsx
25
+ import '@lumir-company/editor/style.css';
26
+ ```
27
+
28
+ ### 2. 기본 사용
29
+
30
+ ```tsx
31
+ import { LumirEditor } from '@lumir-company/editor';
32
+ import '@lumir-company/editor/style.css';
33
+
34
+ export default function App() {
35
+ return (
36
+ <div className='w-full h-[400px]'>
37
+ <LumirEditor onContentChange={(blocks) => console.log(blocks)} />
38
+ </div>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### 3. Next.js에서 사용 (SSR 비활성화)
44
+
45
+ ```tsx
46
+ 'use client';
47
+ import dynamic from 'next/dynamic';
48
+
49
+ const LumirEditor = dynamic(
50
+ () => import('@lumir-company/editor').then((m) => m.LumirEditor),
51
+ { ssr: false },
52
+ );
53
+
54
+ export default function Editor() {
55
+ return (
56
+ <div className='w-full h-[500px]'>
57
+ <LumirEditor />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
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 사용 예시
93
+
94
+ ```tsx
95
+ import { LumirEditor } from '@lumir-company/editor';
96
+
97
+ // 1. 초기 콘텐츠 설정
98
+ <LumirEditor
99
+ initialContent="에디터 시작 텍스트"
100
+ />
101
+
102
+ // 2. 블록 배열로 초기 콘텐츠 설정
103
+ <LumirEditor
104
+ initialContent={[
105
+ {
106
+ type: 'paragraph',
107
+ content: [{ type: 'text', text: '안녕하세요!' }]
108
+ },
109
+ {
110
+ type: 'heading',
111
+ props: { level: 2 },
112
+ content: [{ type: 'text', text: '제목입니다' }]
113
+ }
114
+ ]}
115
+ />
116
+
117
+ // 3. 이벤트 핸들러 사용
118
+ <LumirEditor
119
+ onContentChange={(blocks) => {
120
+ console.log('변경된 콘텐츠:', blocks);
121
+ saveToDatabase(blocks);
122
+ }}
123
+ onSelectionChange={() => {
124
+ console.log('선택 영역이 변경되었습니다');
125
+ }}
126
+ />
127
+
128
+ // 4. UI 컴포넌트 제어
129
+ <LumirEditor
130
+ sideMenuAddButton={true} // Add 버튼 표시
131
+ formattingToolbar={false} // 서식 툴바 숨김
132
+ linkToolbar={false} // 링크 툴바 숨김
133
+ slashMenu={false} // 슬래시 메뉴 숨김
134
+ emojiPicker={false} // 이모지 피커 숨김
135
+ />
136
+
137
+ // 5. 읽기 전용 모드
138
+ <LumirEditor
139
+ editable={false}
140
+ initialContent={savedContent}
141
+ formattingToolbar={false}
142
+ sideMenu={false}
143
+ />
144
+
145
+ // 6. 커스텀 이미지 업로더
146
+ <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;
155
+ }}
156
+ storeImagesAsBase64={false}
157
+ />
158
+
159
+ // 7. 테이블과 헤딩 설정
160
+ <LumirEditor
161
+ tables={{
162
+ splitCells: true,
163
+ cellBackgroundColor: true,
164
+ cellTextColor: true,
165
+ headers: true
166
+ }}
167
+ heading={{
168
+ levels: [1, 2, 3, 4] // H1~H4만 허용
169
+ }}
170
+ />
171
+
172
+ // 8. 애니메이션과 스타일 제어
173
+ <LumirEditor
174
+ animations={false} // 애니메이션 비활성화
175
+ defaultStyles={true} // 기본 스타일 사용
176
+ />
177
+
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
+ ```
195
+
196
+ ## 🎨 Theme 스타일링
197
+
198
+ ### 기본 테마
199
+
200
+ ```tsx
201
+ // 라이트 모드
202
+ <LumirEditor theme="light" />
203
+
204
+ // 다크 모드
205
+ <LumirEditor theme="dark" />
206
+ ```
207
+
208
+ ### 커스텀 테마 (권장)
209
+
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
+ };
244
+
245
+ <LumirEditor theme={customTheme} />;
246
+ ```
247
+
248
+ ### 라이트/다크 모드 조장장
249
+
250
+ ```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
+ };
265
+
266
+ <LumirEditor theme={dualTheme} />;
267
+ ```
268
+
269
+ ## 🖼️ 이미지 업로드
270
+
271
+ ### 자동 Base64 변환
272
+
273
+ ```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
+ />
286
+ ```
287
+
288
+ ## 📖 타입 정의
289
+
290
+ ```tsx
291
+ import type {
292
+ LumirEditorProps,
293
+ DefaultPartialBlock,
294
+ ContentUtils,
295
+ EditorConfig,
296
+ } from '@lumir-company/editor';
297
+
298
+ // 콘텐츠 검증
299
+ const isValidContent = ContentUtils.isValidJSONString(jsonString);
300
+ const blocks = ContentUtils.parseJSONContent(jsonString);
301
+
302
+ // 에디터 설정
303
+ const tableConfig = EditorConfig.getDefaultTableConfig();
304
+ const headingConfig = EditorConfig.getDefaultHeadingConfig();
305
+ ```
306
+
307
+ ## 💡 사용
308
+
309
+ ### 1. 컨테이너 크기 설정
310
+
311
+ ```tsx
312
+ // 고정 높이
313
+ <div className='h-[400px]'>
314
+ <LumirEditor />
315
+ </div>
316
+
317
+ // 최소 높이
318
+ <div className='min-h-[300px]'>
319
+ <LumirEditor />
320
+ </div>
321
+ ```
322
+
323
+ ### 2. 반응형 디자인
324
+
325
+ ```tsx
326
+ <div className='w-full h-64 md:h-96 lg:h-[500px]'>
327
+ <LumirEditor className='h-full' theme='light' />
328
+ </div>
329
+ ```
330
+
331
+ ### 3. 읽기 전용 모드
332
+
333
+ ```tsx
334
+ <LumirEditor
335
+ editable={false}
336
+ initialContent={savedContent}
337
+ formattingToolbar={false}
338
+ sideMenu={false}
339
+ />
340
+ ```
341
+
342
+ ## ⚠️ 중요 사항
343
+
344
+ ### 1. CSS 임포트 필수
345
+
346
+ ```tsx
347
+ // 반드시 CSS를 임포트해야 합니다
348
+ import '@lumir-company/editor/style.css';
349
+ ```
350
+
351
+ ### 2. Next.js SSR 비활성화
352
+
353
+ ```tsx
354
+ // 서버 사이드 렌더링을 비활성화해야 합니다
355
+ const LumirEditor = dynamic(
356
+ () => import('@lumir-company/editor').then((m) => m.LumirEditor),
357
+ { ssr: false },
358
+ );
359
+ ```
360
+
361
+ ### 3. 이미지만 지원
362
+
363
+ - ✅ 이미지 파일: PNG, JPG, GIF, WebP, BMP, SVG
364
+ - 비디오, 오디오, 일반 파일 업로드 불가
365
+ - 🔄 자동 Base64 변환 또는 커스텀 업로더 사용
366
+
367
+ ## 📋 변경 기록
368
+
369
+ ### v0.0.1
370
+
371
+ - 🎉 **초기 릴리스**: 이미지 전용 BlockNote 에디터
372
+ - 🖼️ **이미지 업로드**: Base64 변환 및 드래그앤드롭 지원
373
+ - 🎨 **Theme 지원**: BlockNote 공식 theme prop 지원
374
+ - 📝 **Pretendard 폰트**: 기본 폰트 설정 (14px)
375
+ - 🚫 **미디어 제한**: 비디오/오디오/파일 업로드 비활성화
376
+
377
+ ## 📄 라이선스
378
+
379
+ MIT License