@lumir-company/editor 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,379 +1,576 @@
1
1
  # LumirEditor
2
2
 
3
- 이미지 전용 BlockNote 기반 Rich Text 에디터 React 컴포넌트
3
+ 🖼️ **이미지 전용** BlockNote 기반 Rich Text 에디터
4
4
 
5
- ## ✨ 주요 특징
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)
6
7
 
7
- - 🖼️ **이미지 전용 에디터**: 이미지 업로드/드래그앤드롭만 지원 (Base64 변환)
8
- - 🚀 **간소화된 API**: 핵심 기능만 포함한 미니멀한 인터페이스
9
- - 🎨 **BlockNote Theme 지원**: 공식 theme prop으로 에디터 스타일링
10
- - 🔧 **TypeScript 지원**: 완전한 타입 안전성
11
- - 📝 **Pretendard 폰트**: 기본 폰트로 Pretendard 최우선 적용 (14px)
12
- - ⚡ **경량화**: 비디오/오디오/파일 업로드 기능 제거로 빠른 로딩
8
+ > 이미지 업로드에 최적화된 경량 에디터. S3 업로드, 파일명 커스터마이징, 로딩 스피너 내장.
13
9
 
14
- ## 📦 설치
10
+ ---
15
11
 
16
- ```bash
17
- npm install @lumir-company/editor
12
+ ## 📋 목차
13
+
14
+ - [특징](#-특징)
15
+ - [빠른 시작](#-빠른-시작)
16
+ - [이미지 업로드](#-이미지-업로드)
17
+ - [S3 업로드 설정](#1-s3-업로드-권장)
18
+ - [파일명 커스터마이징](#-파일명-커스터마이징)
19
+ - [커스텀 업로더](#2-커스텀-업로더)
20
+ - [Props API](#-props-api)
21
+ - [사용 예제](#-사용-예제)
22
+ - [스타일링](#-스타일링)
23
+ - [트러블슈팅](#-트러블슈팅)
24
+
25
+ ---
26
+
27
+ ## ✨ 특징
28
+
29
+ | 특징 | 설명 |
30
+ | -------------------------- | ------------------------------------------------------ |
31
+ | 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오 제거) |
32
+ | ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
33
+ | 🏷️ **파일명 커스터마이징** | 업로드 파일명 변경 콜백 + UUID 자동 추가 지원 |
34
+ | ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
35
+ | 🚀 **성능 최적화** | 애니메이션 비활성화로 빠른 렌더링 |
36
+ | 📝 **TypeScript** | 완전한 타입 안전성 |
37
+ | 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 |
38
+
39
+ ### 지원 이미지 형식
40
+
41
+ ```
42
+ PNG, JPEG/JPG, GIF, WebP, BMP, SVG
18
43
  ```
19
44
 
20
- ## 🚀 기본 사용법
45
+ ---
21
46
 
22
- ### 1. CSS 임포트 (필수)
47
+ ## 🚀 빠른 시작
23
48
 
24
- ```tsx
25
- import '@lumir-company/editor/style.css';
49
+ ### 1. 설치
50
+
51
+ ```bash
52
+ npm install @lumir-company/editor
53
+ # 또는
54
+ yarn add @lumir-company/editor
26
55
  ```
27
56
 
57
+ **필수 Peer Dependencies:**
58
+
59
+ - `react` >= 18.0.0
60
+ - `react-dom` >= 18.0.0
61
+
28
62
  ### 2. 기본 사용
29
63
 
30
64
  ```tsx
31
- import { LumirEditor } from '@lumir-company/editor';
32
- import '@lumir-company/editor/style.css';
65
+ import { LumirEditor } from "@lumir-company/editor";
66
+ import "@lumir-company/editor/style.css"; // 필수!
33
67
 
34
68
  export default function App() {
35
69
  return (
36
- <div className='w-full h-[400px]'>
70
+ <div className="w-full h-[500px]">
37
71
  <LumirEditor onContentChange={(blocks) => console.log(blocks)} />
38
72
  </div>
39
73
  );
40
74
  }
41
75
  ```
42
76
 
43
- ### 3. Next.js에서 사용 (SSR 비활성화)
77
+ > ⚠️ **중요**: `style.css`를 임포트하지 않으면 에디터가 정상 작동하지 않습니다.
78
+
79
+ ### 3. Next.js에서 사용
44
80
 
45
81
  ```tsx
46
- 'use client';
47
- import dynamic from 'next/dynamic';
82
+ "use client";
83
+
84
+ import dynamic from "next/dynamic";
85
+ import "@lumir-company/editor/style.css";
48
86
 
87
+ // SSR 비활성화 필수
49
88
  const LumirEditor = dynamic(
50
- () => import('@lumir-company/editor').then((m) => m.LumirEditor),
51
- { ssr: false },
89
+ () =>
90
+ import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
91
+ { ssr: false }
52
92
  );
53
93
 
54
- export default function Editor() {
94
+ export default function EditorPage() {
55
95
  return (
56
- <div className='w-full h-[500px]'>
96
+ <div className="h-[500px]">
57
97
  <LumirEditor />
58
98
  </div>
59
99
  );
60
100
  }
61
101
  ```
62
102
 
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 사용 예시
103
+ ---
93
104
 
94
- ```tsx
95
- import { LumirEditor } from '@lumir-company/editor';
105
+ ## 🖼️ 이미지 업로드
96
106
 
97
- // 1. 초기 콘텐츠 설정
98
- <LumirEditor
99
- initialContent="에디터 시작 텍스트"
100
- />
107
+ ### 1. S3 업로드 (권장)
101
108
 
102
- // 2. 블록 배열로 초기 콘텐츠 설정
109
+ Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
110
+
111
+ ```tsx
103
112
  <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
- ]}
113
+ s3Upload={{
114
+ apiEndpoint: "/api/s3/presigned",
115
+ env: "production",
116
+ path: "blog/images",
117
+ }}
115
118
  />
119
+ ```
120
+
121
+ #### S3 파일 저장 경로
122
+
123
+ ```
124
+ {env}/{path}/{filename}
125
+
126
+ 예시:
127
+ production/blog/images/my-photo.png
128
+ ```
129
+
130
+ #### API 엔드포인트 응답 형식
131
+
132
+ 서버는 다음 형식으로 응답해야 합니다:
116
133
 
117
- // 3. 이벤트 핸들러 사용
134
+ ```json
135
+ {
136
+ "presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
137
+ "publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ### 📝 파일명 커스터마이징
144
+
145
+ 여러 이미지를 동시에 업로드할 때 파일명 중복을 방지하고 관리하기 쉽게 만드는 기능입니다.
146
+
147
+ #### 옵션 1: UUID 자동 추가
148
+
149
+ ```tsx
118
150
  <LumirEditor
119
- onContentChange={(blocks) => {
120
- console.log('변경된 콘텐츠:', blocks);
121
- saveToDatabase(blocks);
122
- }}
123
- onSelectionChange={() => {
124
- console.log('선택 영역이 변경되었습니다');
151
+ s3Upload={{
152
+ apiEndpoint: "/api/s3/presigned",
153
+ env: "production",
154
+ path: "uploads",
155
+ appendUUID: true, // 파일명 뒤에 UUID 자동 추가
125
156
  }}
126
157
  />
158
+ ```
159
+
160
+ **결과:**
127
161
 
128
- // 4. UI 컴포넌트 제어
162
+ ```
163
+ 원본: photo.png
164
+ 업로드: photo_550e8400-e29b-41d4-a716-446655440000.png
165
+ ```
166
+
167
+ #### 옵션 2: 파일명 변환 콜백
168
+
169
+ ```tsx
129
170
  <LumirEditor
130
- sideMenuAddButton={true} // Add 버튼 표시
131
- formattingToolbar={false} // 서식 툴바 숨김
132
- linkToolbar={false} // 링크 툴바 숨김
133
- slashMenu={false} // 슬래시 메뉴 숨김
134
- emojiPicker={false} // 이모지 피커 숨김
171
+ s3Upload={{
172
+ apiEndpoint: "/api/s3/presigned",
173
+ env: "production",
174
+ path: "uploads",
175
+ fileNameTransform: (originalName, file) => {
176
+ // 예: 사용자 ID 추가
177
+ const userId = getCurrentUserId();
178
+ return `${userId}_${originalName}`;
179
+ },
180
+ }}
135
181
  />
182
+ ```
183
+
184
+ **결과:**
185
+
186
+ ```
187
+ 원본: photo.png
188
+ 업로드: user123_photo.png
189
+ ```
136
190
 
137
- // 5. 읽기 전용 모드
191
+ #### 옵션 3: 조합 사용 (권장)
192
+
193
+ ```tsx
138
194
  <LumirEditor
139
- editable={false}
140
- initialContent={savedContent}
141
- formattingToolbar={false}
142
- sideMenu={false}
195
+ s3Upload={{
196
+ apiEndpoint: "/api/s3/presigned",
197
+ env: "production",
198
+ path: "uploads",
199
+ fileNameTransform: (originalName) => `user123_${originalName}`,
200
+ appendUUID: true, // 변환 후 UUID 추가
201
+ }}
143
202
  />
203
+ ```
204
+
205
+ **결과:**
144
206
 
145
- // 6. 커스텀 이미지 업로더
207
+ ```
208
+ 원본: photo.png
209
+ 1. fileNameTransform 적용: user123_photo.png
210
+ 2. appendUUID 적용: user123_photo_550e8400-e29b-41d4.png
211
+ ```
212
+
213
+ #### 실전 예제: 타임스탬프 + UUID
214
+
215
+ ```tsx
216
+ function MyEditor() {
217
+ return (
218
+ <LumirEditor
219
+ s3Upload={{
220
+ apiEndpoint: "/api/s3/presigned",
221
+ env: "production",
222
+ path: "uploads",
223
+ fileNameTransform: (originalName, file) => {
224
+ const timestamp = new Date().toISOString().split("T")[0]; // 2024-01-15
225
+ const ext = originalName.split(".").pop();
226
+ const nameWithoutExt = originalName.replace(`.${ext}`, "");
227
+ return `${timestamp}_${nameWithoutExt}.${ext}`;
228
+ },
229
+ appendUUID: true,
230
+ }}
231
+ />
232
+ );
233
+ }
234
+ ```
235
+
236
+ **결과:**
237
+
238
+ ```
239
+ 2024-01-15_photo_550e8400-e29b-41d4.png
240
+ ```
241
+
242
+ ---
243
+
244
+ ### 2. 커스텀 업로더
245
+
246
+ 자체 업로드 로직을 사용할 때:
247
+
248
+ ```tsx
146
249
  <LumirEditor
147
250
  uploadFile={async (file) => {
148
251
  const formData = new FormData();
149
- formData.append('image', file);
150
- const response = await fetch('/api/upload', {
151
- method: 'POST',
152
- body: formData
252
+ formData.append("image", file);
253
+
254
+ const response = await fetch("/api/upload", {
255
+ method: "POST",
256
+ body: formData,
153
257
  });
154
- return (await response.json()).url;
155
- }}
156
- storeImagesAsBase64={false}
157
- />
158
258
 
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만 허용
259
+ const { url } = await response.json();
260
+ return url; // 업로드된 이미지 URL 반환
169
261
  }}
170
262
  />
263
+ ```
171
264
 
172
- // 8. 애니메이션과 스타일 제어
173
- <LumirEditor
174
- animations={false} // 애니메이션 비활성화
175
- defaultStyles={true} // 기본 스타일 사용
176
- />
265
+ ### 3. 헬퍼 함수 사용
177
266
 
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
- />
267
+ ```tsx
268
+ import { createS3Uploader } from "@lumir-company/editor";
269
+
270
+ const s3Uploader = createS3Uploader({
271
+ apiEndpoint: "/api/s3/presigned",
272
+ env: "production",
273
+ path: "images",
274
+ appendUUID: true,
275
+ });
276
+
277
+ // 에디터에 적용
278
+ <LumirEditor uploadFile={s3Uploader} />;
279
+
280
+ // 또는 별도로 사용
281
+ const imageUrl = await s3Uploader(imageFile);
194
282
  ```
195
283
 
196
- ## 🎨 Theme 스타일링
284
+ ### 업로드 우선순위
285
+
286
+ 1. `uploadFile` prop이 있으면 우선 사용
287
+ 2. `uploadFile` 없고 `s3Upload`가 있으면 S3 업로드 사용
288
+ 3. 둘 다 없으면 업로드 실패
289
+
290
+ ---
291
+
292
+ ## 📚 Props API
293
+
294
+ ### 핵심 Props
197
295
 
198
- ### 기본 테마
296
+ | Prop | 타입 | 기본값 | 설명 |
297
+ | ----------------- | --------------------------------- | ----------- | ------------------ |
298
+ | `s3Upload` | `S3UploaderConfig` | `undefined` | S3 업로드 설정 |
299
+ | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | 커스텀 업로드 함수 |
300
+ | `onContentChange` | `(blocks) => void` | `undefined` | 콘텐츠 변경 콜백 |
301
+ | `initialContent` | `Block[] \| string` | `undefined` | 초기 콘텐츠 |
302
+ | `editable` | `boolean` | `true` | 편집 가능 여부 |
303
+ | `theme` | `"light" \| "dark"` | `"light"` | 테마 |
304
+ | `className` | `string` | `""` | CSS 클래스 |
305
+
306
+ ### S3UploaderConfig
199
307
 
200
308
  ```tsx
201
- // 라이트 모드
202
- <LumirEditor theme="light" />
309
+ interface S3UploaderConfig {
310
+ // 필수
311
+ apiEndpoint: string; // Presigned URL API 엔드포인트
312
+ env: "development" | "production";
313
+ path: string; // S3 저장 경로
314
+
315
+ // 선택 (파일명 커스터마이징)
316
+ fileNameTransform?: (originalName: string, file: File) => string;
317
+ appendUUID?: boolean; // true: 파일명 뒤에 UUID 추가
318
+ }
319
+ ```
203
320
 
204
- // 다크 모드
205
- <LumirEditor theme="dark" />
321
+ ### 전체 Props
322
+
323
+ <details>
324
+ <summary>전체 Props 보기</summary>
325
+
326
+ ```tsx
327
+ interface LumirEditorProps {
328
+ // === 에디터 설정 ===
329
+ initialContent?: DefaultPartialBlock[] | string; // 초기 콘텐츠 (블록 배열 또는 JSON 문자열)
330
+ initialEmptyBlocks?: number; // 초기 빈 블록 개수 (기본: 3)
331
+ uploadFile?: (file: File) => Promise<string>; // 커스텀 파일 업로드 함수
332
+ s3Upload?: S3UploaderConfig; // S3 업로드 설정 (apiEndpoint, env, path 등)
333
+
334
+ // === 콜백 ===
335
+ onContentChange?: (blocks: DefaultPartialBlock[]) => void; // 콘텐츠 변경 시 호출
336
+ onSelectionChange?: () => void; // 선택 영역 변경 시 호출
337
+
338
+ // 기능 설정
339
+ tables?: TableConfig; // 테이블 기능 설정 (splitCells, cellBackgroundColor 등)
340
+ heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] }; // 헤딩 레벨 설정 (기본: [1,2,3,4,5,6])
341
+ defaultStyles?: boolean; // 기본 스타일 활성화 (기본: true)
342
+ disableExtensions?: string[]; // 비활성화할 확장 기능 목록
343
+ tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; // 탭 키 동작 (기본: "prefer-navigate-ui")
344
+ trailingBlock?: boolean; // 마지막에 빈 블록 자동 추가 (기본: true)
345
+
346
+ // === UI 설정 ===
347
+ editable?: boolean; // 편집 가능 여부 (기본: true)
348
+ theme?: "light" | "dark" | ThemeObject; // 에디터 테마 (기본: "light")
349
+ formattingToolbar?: boolean; // 서식 툴바 표시 (기본: true)
350
+ linkToolbar?: boolean; // 링크 툴바 표시 (기본: true)
351
+ sideMenu?: boolean; // 사이드 메뉴 표시 (기본: true)
352
+ sideMenuAddButton?: boolean; // 사이드 메뉴 + 버튼 표시 (기본: false, 드래그 핸들만 표시)
353
+ emojiPicker?: boolean; // 이모지 선택기 표시 (기본: true)
354
+ filePanel?: boolean; // 파일 패널 표시 (기본: true)
355
+ tableHandles?: boolean; // 테이블 핸들 표시 (기본: true)
356
+ className?: string; // 컨테이너 CSS 클래스
357
+
358
+ // 미디어 업로드 허용 여부 (기본: 모두 비활성)
359
+ allowVideoUpload?: boolean; // 비디오 업로드 허용 (기본: false)
360
+ allowAudioUpload?: boolean; // 오디오 업로드 허용 (기본: false)
361
+ allowFileUpload?: boolean; // 일반 파일 업로드 허용 (기본: false)
362
+ }
206
363
  ```
207
364
 
208
- ### 커스텀 테마 (권장)
365
+ </details>
366
+
367
+ ---
368
+
369
+ ## 💡 사용 예제
370
+
371
+ ### 읽기 전용 모드
209
372
 
210
373
  ```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
- };
374
+ <LumirEditor
375
+ editable={false}
376
+ initialContent={savedContent}
377
+ sideMenu={false}
378
+ formattingToolbar={false}
379
+ />
380
+ ```
381
+
382
+ ### 다크 테마
244
383
 
245
- <LumirEditor theme={customTheme} />;
384
+ ```tsx
385
+ <LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
246
386
  ```
247
387
 
248
- ### 라이트/다크 모드 조장장
388
+ ### 콘텐츠 저장 및 불러오기
249
389
 
250
390
  ```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
- };
391
+ import { useState, useEffect } from "react";
392
+ import { LumirEditor, ContentUtils } from "@lumir-company/editor";
393
+
394
+ function EditorWithSave() {
395
+ const [content, setContent] = useState("");
265
396
 
266
- <LumirEditor theme={dualTheme} />;
397
+ // 불러오기
398
+ useEffect(() => {
399
+ const saved = localStorage.getItem("content");
400
+ if (saved && ContentUtils.isValidJSONString(saved)) {
401
+ setContent(saved);
402
+ }
403
+ }, []);
404
+
405
+ // 저장
406
+ const handleChange = (blocks) => {
407
+ const json = JSON.stringify(blocks);
408
+ localStorage.setItem("content", json);
409
+ };
410
+
411
+ return (
412
+ <LumirEditor initialContent={content} onContentChange={handleChange} />
413
+ );
414
+ }
267
415
  ```
268
416
 
269
- ## 🖼️ 이미지 업로드
417
+ ---
418
+
419
+ ## 🎨 스타일링
270
420
 
271
- ### 자동 Base64 변환
421
+ ### Tailwind CSS와 함께 사용
272
422
 
273
423
  ```tsx
274
- // 기본적으로 이미지가 Base64로 자동 변환됩니다
424
+ import { LumirEditor, cn } from "@lumir-company/editor";
425
+
275
426
  <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
- />
427
+ className={cn(
428
+ "min-h-[400px] rounded-xl",
429
+ "border border-gray-200 shadow-lg",
430
+ "focus-within:ring-2 focus-within:ring-blue-500"
431
+ )}
432
+ />;
286
433
  ```
287
434
 
288
- ## 📖 타입 정의
435
+ ### 커스텀 스타일
289
436
 
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);
437
+ ```css
438
+ /* globals.css */
439
+ .my-editor .bn-editor {
440
+ padding: 20px 30px;
441
+ font-size: 16px;
442
+ line-height: 1.6;
443
+ }
444
+
445
+ .my-editor [data-content-type="heading"] {
446
+ font-weight: 700;
447
+ margin-top: 24px;
448
+ }
449
+ ```
301
450
 
302
- // 에디터 설정
303
- const tableConfig = EditorConfig.getDefaultTableConfig();
304
- const headingConfig = EditorConfig.getDefaultHeadingConfig();
451
+ ```tsx
452
+ <LumirEditor className="my-editor" />
305
453
  ```
306
454
 
307
- ## 💡 사용 팁
455
+ ---
456
+
457
+ ## ⚠️ 트러블슈팅
458
+
459
+ ### 필수 체크리스트
460
+
461
+ - [ ] CSS 임포트: `import "@lumir-company/editor/style.css"`
462
+ - [ ] 컨테이너 높이 설정: 부모 요소에 높이 지정 필수
463
+ - [ ] Next.js: `dynamic(..., { ssr: false })` 사용
464
+ - [ ] React 버전: 18.0.0 이상
308
465
 
309
- ### 1. 컨테이너 크기 설정
466
+ ### 자주 발생하는 문제
467
+
468
+ #### 1. 에디터가 보이지 않음
310
469
 
311
470
  ```tsx
312
- // 고정 높이
313
- <div className='h-[400px]'>
314
- <LumirEditor />
315
- </div>
471
+ // 잘못됨
472
+ <LumirEditor />;
316
473
 
317
- // 최소 높이
318
- <div className='min-h-[300px]'>
474
+ // 올바름
475
+ import "@lumir-company/editor/style.css";
476
+ <div className="h-[400px]">
319
477
  <LumirEditor />
320
- </div>
478
+ </div>;
479
+ ```
480
+
481
+ #### 2. Next.js Hydration 오류
482
+
483
+ ```tsx
484
+ // ❌ 잘못됨
485
+ import { LumirEditor } from "@lumir-company/editor";
486
+
487
+ // ✅ 올바름
488
+ const LumirEditor = dynamic(
489
+ () =>
490
+ import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
491
+ { ssr: false }
492
+ );
321
493
  ```
322
494
 
323
- ### 2. 반응형 디자인
495
+ #### 3. 이미지 업로드 실패
324
496
 
325
497
  ```tsx
326
- <div className='w-full h-64 md:h-96 lg:h-[500px]'>
327
- <LumirEditor className='h-full' theme='light' />
328
- </div>
498
+ // uploadFile 또는 s3Upload 중 하나는 반드시 설정!
499
+ <LumirEditor
500
+ s3Upload={{
501
+ apiEndpoint: "/api/s3/presigned",
502
+ env: "development",
503
+ path: "images",
504
+ }}
505
+ />
329
506
  ```
330
507
 
331
- ### 3. 읽기 전용 모드
508
+ #### 4. 여러 이미지 업로드 시 중복 문제
332
509
 
333
510
  ```tsx
511
+ // ✅ 해결: appendUUID 사용
334
512
  <LumirEditor
335
- editable={false}
336
- initialContent={savedContent}
337
- formattingToolbar={false}
338
- sideMenu={false}
513
+ s3Upload={{
514
+ apiEndpoint: "/api/s3/presigned",
515
+ env: "production",
516
+ path: "images",
517
+ appendUUID: true, // 고유한 파일명 보장
518
+ }}
339
519
  />
340
520
  ```
341
521
 
342
- ## ⚠️ 중요 사항
522
+ ---
523
+
524
+ ## 🛠️ 유틸리티 API
343
525
 
344
- ### 1. CSS 임포트 필수
526
+ ### ContentUtils
345
527
 
346
528
  ```tsx
347
- // 반드시 CSS를 임포트해야 합니다
348
- import '@lumir-company/editor/style.css';
529
+ import { ContentUtils } from "@lumir-company/editor";
530
+
531
+ // JSON 검증
532
+ ContentUtils.isValidJSONString('[{"type":"paragraph"}]'); // true
533
+
534
+ // JSON 파싱
535
+ const blocks = ContentUtils.parseJSONContent(jsonString);
536
+
537
+ // 기본 블록 생성
538
+ const emptyBlock = ContentUtils.createDefaultBlock();
349
539
  ```
350
540
 
351
- ### 2. Next.js SSR 비활성화
541
+ ### createS3Uploader
352
542
 
353
543
  ```tsx
354
- // 서버 사이드 렌더링을 비활성화해야 합니다
355
- const LumirEditor = dynamic(
356
- () => import('@lumir-company/editor').then((m) => m.LumirEditor),
357
- { ssr: false },
358
- );
544
+ import { createS3Uploader } from "@lumir-company/editor";
545
+
546
+ const uploader = createS3Uploader({
547
+ apiEndpoint: "/api/s3/presigned",
548
+ env: "production",
549
+ path: "uploads",
550
+ appendUUID: true,
551
+ });
552
+
553
+ // 직접 사용
554
+ const url = await uploader(imageFile);
359
555
  ```
360
556
 
361
- ### 3. 이미지만 지원
557
+ ## 🔗 관련 링크
558
+
559
+ - [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
560
+ - [BlockNote Documentation](https://www.blocknotejs.org/)
362
561
 
363
- - ✅ 이미지 파일: PNG, JPG, GIF, WebP, BMP, SVG
364
- - ❌ 비디오, 오디오, 일반 파일 업로드 불가
365
- - 🔄 자동 Base64 변환 또는 커스텀 업로더 사용
562
+ ---
366
563
 
367
- ## 📋 변경 기록
564
+ ## 📝 변경 로그
368
565
 
369
- ### v0.0.1
566
+ ### v0.4.0
370
567
 
371
- - 🎉 **초기 릴리스**: 이미지 전용 BlockNote 에디터
372
- - 🖼️ **이미지 업로드**: Base64 변환 드래그앤드롭 지원
373
- - 🎨 **Theme 지원**: BlockNote 공식 theme prop 지원
374
- - 📝 **Pretendard 폰트**: 기본 폰트 설정 (14px)
375
- - 🚫 **미디어 제한**: 비디오/오디오/파일 업로드 비활성화
568
+ - 파일명 변환 콜백 (`fileNameTransform`) 추가
569
+ - UUID 자동 추가 옵션 (`appendUUID`) 추가
570
+ - 🐛 여러 이미지 동시 업로드 중복 문제 해결
571
+ - 📝 문서 대폭 개선
376
572
 
377
- ## 📄 라이선스
573
+ ### v0.3.3
378
574
 
379
- MIT License
575
+ - 🐛 에디터 재생성 방지 최적화
576
+ - 📝 타입 정의 개선