@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 +742 -925
- package/dist/index.d.mts +17 -21
- package/dist/index.d.ts +17 -21
- package/dist/index.js +230 -183
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -183
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +77 -4
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,925 +1,742 @@
|
|
|
1
|
-
# LumirEditor
|
|
2
|
-
|
|
3
|
-
BlockNote
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}}
|
|
200
|
-
|
|
201
|
-
/>
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
|
399
|
-
|
|
|
400
|
-
|
|
|
401
|
-
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
/>
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
```tsx
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
/*
|
|
575
|
-
.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/* 에디터 내용 영역 */
|
|
583
|
-
.
|
|
584
|
-
font-family: "Pretendard", -apple-system,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@lumir-company/editor)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## 📋 목차
|
|
9
|
+
|
|
10
|
+
- [✨ 핵심 특징](#-핵심-특징)
|
|
11
|
+
- [📦 설치](#-설치)
|
|
12
|
+
- [🚀 빠른 시작](#-빠른-시작)
|
|
13
|
+
- [📚 Props 레퍼런스](#-props-레퍼런스)
|
|
14
|
+
- [🖼️ 이미지 업로드](#️-이미지-업로드)
|
|
15
|
+
- [🛠️ 유틸리티 API](#️-유틸리티-api)
|
|
16
|
+
- [📖 타입 정의](#-타입-정의)
|
|
17
|
+
- [💡 사용 예제](#-사용-예제)
|
|
18
|
+
- [🎨 스타일링 가이드](#-스타일링-가이드)
|
|
19
|
+
- [⚠️ 주의사항 및 트러블슈팅](#️-주의사항-및-트러블슈팅)
|
|
20
|
+
- [📄 라이선스](#-라이선스)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✨ 핵심 특징
|
|
25
|
+
|
|
26
|
+
| 특징 | 설명 |
|
|
27
|
+
| ------------------------ | ----------------------------------------------------------- |
|
|
28
|
+
| 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오/파일 제거) |
|
|
29
|
+
| ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
|
|
30
|
+
| 🎯 **커스텀 업로더** | 자체 업로드 로직 적용 가능 |
|
|
31
|
+
| ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
|
|
32
|
+
| 🚀 **애니메이션 최적화** | 기본 애니메이션 비활성화로 성능 향상 |
|
|
33
|
+
| 📝 **TypeScript** | 완전한 타입 안전성 |
|
|
34
|
+
| 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 지원 |
|
|
35
|
+
| 📱 **반응형** | 모바일/데스크톱 최적화 |
|
|
36
|
+
|
|
37
|
+
### 지원 이미지 형식
|
|
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/)
|