@lumir-company/editor 0.3.3 → 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
@@ -5,93 +5,78 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@lumir-company/editor.svg)](https://www.npmjs.com/package/@lumir-company/editor)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
+ > 이미지 업로드에 최적화된 경량 에디터. S3 업로드, 파일명 커스터마이징, 로딩 스피너 내장.
9
+
10
+ ---
11
+
8
12
  ## 📋 목차
9
13
 
10
- - [✨ 핵심 특징](#-핵심-특징)
11
- - [📦 설치](#-설치)
12
- - [🚀 빠른 시작](#-빠른-시작)
13
- - [📚 Props 레퍼런스](#-props-레퍼런스)
14
- - [🖼️ 이미지 업로드](#️-이미지-업로드)
15
- - [🛠️ 유틸리티 API](#️-유틸리티-api)
16
- - [📖 타입 정의](#-타입-정의)
17
- - [💡 사용 예제](#-사용-예제)
18
- - [🎨 스타일링 가이드](#-스타일링-가이드)
19
- - [⚠️ 주의사항 및 트러블슈팅](#️-주의사항-및-트러블슈팅)
20
- - [📄 라이선스](#-라이선스)
14
+ - [특징](#-특징)
15
+ - [빠른 시작](#-빠른-시작)
16
+ - [이미지 업로드](#-이미지-업로드)
17
+ - [S3 업로드 설정](#1-s3-업로드-권장)
18
+ - [파일명 커스터마이징](#-파일명-커스터마이징)
19
+ - [커스텀 업로더](#2-커스텀-업로더)
20
+ - [Props API](#-props-api)
21
+ - [사용 예제](#-사용-예제)
22
+ - [스타일링](#-스타일링)
23
+ - [트러블슈팅](#-트러블슈팅)
21
24
 
22
25
  ---
23
26
 
24
- ## ✨ 핵심 특징
27
+ ## ✨ 특징
25
28
 
26
- | 특징 | 설명 |
27
- | ------------------------ | ----------------------------------------------------------- |
28
- | 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오/파일 제거) |
29
- | ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
30
- | 🎯 **커스텀 업로더** | 자체 업로드 로직 적용 가능 |
31
- | ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
32
- | 🚀 **애니메이션 최적화** | 기본 애니메이션 비활성화로 성능 향상 |
33
- | 📝 **TypeScript** | 완전한 타입 안전성 |
34
- | 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 지원 |
35
- | 📱 **반응형** | 모바일/데스크톱 최적화 |
29
+ | 특징 | 설명 |
30
+ | -------------------------- | ------------------------------------------------------ |
31
+ | 🖼️ **이미지 전용** | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오 제거) |
32
+ | ☁️ **S3 연동** | Presigned URL 기반 S3 업로드 내장 |
33
+ | 🏷️ **파일명 커스터마이징** | 업로드 파일명 변경 콜백 + UUID 자동 추가 지원 |
34
+ | ⏳ **로딩 스피너** | 이미지 업로드 중 자동 스피너 표시 |
35
+ | 🚀 **성능 최적화** | 애니메이션 비활성화로 빠른 렌더링 |
36
+ | 📝 **TypeScript** | 완전한 타입 안전성 |
37
+ | 🎨 **테마 지원** | 라이트/다크 테마 및 커스텀 테마 |
36
38
 
37
39
  ### 지원 이미지 형식
38
40
 
39
41
  ```
40
- PNG, JPEG/JPG, GIF (애니메이션 포함), WebP, BMP, SVG
42
+ PNG, JPEG/JPG, GIF, WebP, BMP, SVG
41
43
  ```
42
44
 
43
45
  ---
44
46
 
45
- ## 📦 설치
47
+ ## 🚀 빠른 시작
48
+
49
+ ### 1. 설치
46
50
 
47
51
  ```bash
48
- # npm
49
52
  npm install @lumir-company/editor
50
-
51
- # yarn
53
+ # 또는
52
54
  yarn add @lumir-company/editor
53
-
54
- # pnpm
55
- pnpm add @lumir-company/editor
56
55
  ```
57
56
 
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
- ```
57
+ **필수 Peer Dependencies:**
76
58
 
77
- > ⚠️ **중요**: CSS를 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
59
+ - `react` >= 18.0.0
60
+ - `react-dom` >= 18.0.0
78
61
 
79
- ### 2단계: 기본 사용
62
+ ### 2. 기본 사용
80
63
 
81
64
  ```tsx
82
65
  import { LumirEditor } from "@lumir-company/editor";
83
- import "@lumir-company/editor/style.css";
66
+ import "@lumir-company/editor/style.css"; // 필수!
84
67
 
85
68
  export default function App() {
86
69
  return (
87
- <div className="w-full h-[400px]">
70
+ <div className="w-full h-[500px]">
88
71
  <LumirEditor onContentChange={(blocks) => console.log(blocks)} />
89
72
  </div>
90
73
  );
91
74
  }
92
75
  ```
93
76
 
94
- ### 3단계: Next.js에서 사용 (SSR 비활성화 필수)
77
+ > ⚠️ **중요**: `style.css`를 임포트하지 않으면 에디터가 정상 작동하지 않습니다.
78
+
79
+ ### 3. Next.js에서 사용
95
80
 
96
81
  ```tsx
97
82
  "use client";
@@ -99,6 +84,7 @@ export default function App() {
99
84
  import dynamic from "next/dynamic";
100
85
  import "@lumir-company/editor/style.css";
101
86
 
87
+ // SSR 비활성화 필수
102
88
  const LumirEditor = dynamic(
103
89
  () =>
104
90
  import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
@@ -107,10 +93,8 @@ const LumirEditor = dynamic(
107
93
 
108
94
  export default function EditorPage() {
109
95
  return (
110
- <div className="w-full h-[500px]">
111
- <LumirEditor
112
- onContentChange={(blocks) => console.log("Content:", blocks)}
113
- />
96
+ <div className="h-[500px]">
97
+ <LumirEditor />
114
98
  </div>
115
99
  );
116
100
  }
@@ -118,108 +102,148 @@ export default function EditorPage() {
118
102
 
119
103
  ---
120
104
 
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` | 선택 영역 변경 시 호출 |
105
+ ## 🖼️ 이미지 업로드
163
106
 
164
- ### S3UploaderConfig
107
+ ### 1. S3 업로드 (권장)
108
+
109
+ Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
165
110
 
166
111
  ```tsx
167
- interface S3UploaderConfig {
168
- apiEndpoint: string; // Presigned URL API 엔드포인트 (필수)
169
- env: "development" | "production"; // 환경 (필수)
170
- path: string; // S3 경로 (필수)
171
- }
112
+ <LumirEditor
113
+ s3Upload={{
114
+ apiEndpoint: "/api/s3/presigned",
115
+ env: "production",
116
+ path: "blog/images",
117
+ }}
118
+ />
172
119
  ```
173
120
 
174
- ### TableConfig
121
+ #### S3 파일 저장 경로
175
122
 
176
- ```tsx
177
- interface TableConfig {
178
- splitCells?: boolean; // 셀 분할 (기본: true)
179
- cellBackgroundColor?: boolean; // 셀 배경색 (기본: true)
180
- cellTextColor?: boolean; // 셀 텍스트 색상 (기본: true)
181
- headers?: boolean; // 헤더 행 (기본: true)
123
+ ```
124
+ {env}/{path}/{filename}
125
+
126
+ 예시:
127
+ production/blog/images/my-photo.png
128
+ ```
129
+
130
+ #### API 엔드포인트 응답 형식
131
+
132
+ 서버는 다음 형식으로 응답해야 합니다:
133
+
134
+ ```json
135
+ {
136
+ "presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
137
+ "publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
182
138
  }
183
139
  ```
184
140
 
185
141
  ---
186
142
 
187
- ## 🖼️ 이미지 업로드
143
+ ### 📝 파일명 커스터마이징
188
144
 
189
- ### 방법 1: S3 업로드 (권장)
145
+ 여러 이미지를 동시에 업로드할 파일명 중복을 방지하고 관리하기 쉽게 만드는 기능입니다.
190
146
 
191
- Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
147
+ #### 옵션 1: UUID 자동 추가
192
148
 
193
149
  ```tsx
194
150
  <LumirEditor
195
151
  s3Upload={{
196
152
  apiEndpoint: "/api/s3/presigned",
197
- env: "development",
198
- path: "blog/images",
153
+ env: "production",
154
+ path: "uploads",
155
+ appendUUID: true, // 파일명 뒤에 UUID 자동 추가
199
156
  }}
200
- onContentChange={(blocks) => console.log(blocks)}
201
157
  />
202
158
  ```
203
159
 
204
- **S3 파일 저장 경로 구조:**
160
+ **결과:**
205
161
 
206
162
  ```
207
- {env}/{path}/{filename}
208
- 예: development/blog/images/my-image.png
163
+ 원본: photo.png
164
+ 업로드: photo_550e8400-e29b-41d4-a716-446655440000.png
209
165
  ```
210
166
 
211
- **API 엔드포인트 응답 예시:**
167
+ #### 옵션 2: 파일명 변환 콜백
212
168
 
213
- ```json
214
- {
215
- "presignedUrl": "https://s3.amazonaws.com/bucket/...",
216
- "publicUrl": "https://cdn.example.com/development/blog/images/my-image.png"
169
+ ```tsx
170
+ <LumirEditor
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
+ }}
181
+ />
182
+ ```
183
+
184
+ **결과:**
185
+
186
+ ```
187
+ 원본: photo.png
188
+ 업로드: user123_photo.png
189
+ ```
190
+
191
+ #### 옵션 3: 조합 사용 (권장)
192
+
193
+ ```tsx
194
+ <LumirEditor
195
+ s3Upload={{
196
+ apiEndpoint: "/api/s3/presigned",
197
+ env: "production",
198
+ path: "uploads",
199
+ fileNameTransform: (originalName) => `user123_${originalName}`,
200
+ appendUUID: true, // 변환 후 UUID 추가
201
+ }}
202
+ />
203
+ ```
204
+
205
+ **결과:**
206
+
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
+ );
217
233
  }
218
234
  ```
219
235
 
220
- ### 방법 2: 커스텀 업로더
236
+ **결과:**
237
+
238
+ ```
239
+ 2024-01-15_photo_550e8400-e29b-41d4.png
240
+ ```
241
+
242
+ ---
243
+
244
+ ### 2. 커스텀 업로더
221
245
 
222
- 자체 업로드 로직을 사용할 때 활용합니다.
246
+ 자체 업로드 로직을 사용할 때:
223
247
 
224
248
  ```tsx
225
249
  <LumirEditor
@@ -232,24 +256,22 @@ Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
232
256
  body: formData,
233
257
  });
234
258
 
235
- const data = await response.json();
236
- return data.url; // 업로드된 이미지의 URL 반환
259
+ const { url } = await response.json();
260
+ return url; // 업로드된 이미지 URL 반환
237
261
  }}
238
262
  />
239
263
  ```
240
264
 
241
- ### 방법 3: createS3Uploader 헬퍼 함수
242
-
243
- S3 업로더를 직접 생성하여 사용할 수 있습니다.
265
+ ### 3. 헬퍼 함수 사용
244
266
 
245
267
  ```tsx
246
- import { LumirEditor, createS3Uploader } from "@lumir-company/editor";
268
+ import { createS3Uploader } from "@lumir-company/editor";
247
269
 
248
- // S3 업로더 생성
249
270
  const s3Uploader = createS3Uploader({
250
271
  apiEndpoint: "/api/s3/presigned",
251
272
  env: "production",
252
- path: "uploads/images",
273
+ path: "images",
274
+ appendUUID: true,
253
275
  });
254
276
 
255
277
  // 에디터에 적용
@@ -262,204 +284,90 @@ const imageUrl = await s3Uploader(imageFile);
262
284
  ### 업로드 우선순위
263
285
 
264
286
  1. `uploadFile` prop이 있으면 우선 사용
265
- 2. `uploadFile`이 없고 `s3Upload`가 있으면 S3 업로드 사용
287
+ 2. `uploadFile` 없고 `s3Upload`가 있으면 S3 업로드 사용
266
288
  3. 둘 다 없으면 업로드 실패
267
289
 
268
290
  ---
269
291
 
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
292
+ ## 📚 Props API
297
293
 
298
- 에디터 설정 유틸리티 클래스입니다.
294
+ ### 핵심 Props
299
295
 
300
- ```tsx
301
- import { EditorConfig } from "@lumir-company/editor";
302
-
303
- // 테이블 기본 설정 가져오기
304
- const tableConfig = EditorConfig.getDefaultTableConfig({
305
- splitCells: true,
306
- headers: false,
307
- });
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 클래스 |
308
305
 
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 결합 유틸리티입니다.
306
+ ### S3UploaderConfig
327
307
 
328
308
  ```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
- />;
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
+ }
338
319
  ```
339
320
 
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
- ```
321
+ ### 전체 Props
365
322
 
366
- ### LumirEditorProps 전체 인터페이스
323
+ <details>
324
+ <summary>전체 Props 보기</summary>
367
325
 
368
326
  ```tsx
369
327
  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;
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)
417
362
  }
418
363
  ```
419
364
 
365
+ </details>
366
+
420
367
  ---
421
368
 
422
369
  ## 💡 사용 예제
423
370
 
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
371
  ### 읽기 전용 모드
464
372
 
465
373
  ```tsx
@@ -477,46 +385,6 @@ function BasicEditor() {
477
385
  <LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />
478
386
  ```
479
387
 
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
388
  ### 콘텐츠 저장 및 불러오기
521
389
 
522
390
  ```tsx
@@ -524,72 +392,31 @@ import { useState, useEffect } from "react";
524
392
  import { LumirEditor, ContentUtils } from "@lumir-company/editor";
525
393
 
526
394
  function EditorWithSave() {
527
- const [content, setContent] = useState<string>("");
395
+ const [content, setContent] = useState("");
528
396
 
529
- // 저장된 콘텐츠 불러오기
397
+ // 불러오기
530
398
  useEffect(() => {
531
- const saved = localStorage.getItem("editor-content");
399
+ const saved = localStorage.getItem("content");
532
400
  if (saved && ContentUtils.isValidJSONString(saved)) {
533
401
  setContent(saved);
534
402
  }
535
403
  }, []);
536
404
 
537
- // 콘텐츠 저장
538
- const handleContentChange = (blocks) => {
539
- const jsonContent = JSON.stringify(blocks);
540
- localStorage.setItem("editor-content", jsonContent);
405
+ // 저장
406
+ const handleChange = (blocks) => {
407
+ const json = JSON.stringify(blocks);
408
+ localStorage.setItem("content", json);
541
409
  };
542
410
 
543
411
  return (
544
- <LumirEditor
545
- initialContent={content}
546
- onContentChange={handleContentChange}
547
- />
412
+ <LumirEditor initialContent={content} onContentChange={handleChange} />
548
413
  );
549
414
  }
550
415
  ```
551
416
 
552
417
  ---
553
418
 
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
- ```
419
+ ## 🎨 스타일링
593
420
 
594
421
  ### Tailwind CSS와 함께 사용
595
422
 
@@ -605,14 +432,14 @@ import { LumirEditor, cn } from "@lumir-company/editor";
605
432
  />;
606
433
  ```
607
434
 
608
- ### 커스텀 스타일 적용
435
+ ### 커스텀 스타일
609
436
 
610
437
  ```css
611
438
  /* globals.css */
612
439
  .my-editor .bn-editor {
613
- padding-left: 30px;
614
- padding-right: 20px;
440
+ padding: 20px 30px;
615
441
  font-size: 16px;
442
+ line-height: 1.6;
616
443
  }
617
444
 
618
445
  .my-editor [data-content-type="heading"] {
@@ -627,37 +454,37 @@ import { LumirEditor, cn } from "@lumir-company/editor";
627
454
 
628
455
  ---
629
456
 
630
- ## ⚠️ 주의사항 및 트러블슈팅
457
+ ## ⚠️ 트러블슈팅
631
458
 
632
459
  ### 필수 체크리스트
633
460
 
634
- | 항목 | 체크 |
635
- | -------------------- | ------------------------------------------- |
636
- | CSS 임포트 | `import "@lumir-company/editor/style.css";` |
637
- | 컨테이너 높이 설정 | 부모 요소에 높이 지정 필수 |
638
- | Next.js SSR 비활성화 | `dynamic(..., { ssr: false })` 사용 |
639
- | React 버전 | 18.0.0 이상 필요 |
461
+ - [ ] CSS 임포트: `import "@lumir-company/editor/style.css"`
462
+ - [ ] 컨테이너 높이 설정: 부모 요소에 높이 지정 필수
463
+ - [ ] Next.js: `dynamic(..., { ssr: false })` 사용
464
+ - [ ] React 버전: 18.0.0 이상
640
465
 
641
- ### 일반적인 문제 해결
466
+ ### 자주 발생하는 문제
642
467
 
643
- #### 1. 에디터가 렌더링되지 않음
468
+ #### 1. 에디터가 보이지 않음
644
469
 
645
470
  ```tsx
646
- // ❌ 잘못된 사용
471
+ // ❌ 잘못됨
647
472
  <LumirEditor />;
648
473
 
649
- // ✅ 올바른 사용 - CSS 임포트 필요
474
+ // ✅ 올바름
650
475
  import "@lumir-company/editor/style.css";
651
- <LumirEditor />;
476
+ <div className="h-[400px]">
477
+ <LumirEditor />
478
+ </div>;
652
479
  ```
653
480
 
654
- #### 2. Next.js에서 hydration 오류
481
+ #### 2. Next.js Hydration 오류
655
482
 
656
483
  ```tsx
657
- // ❌ 잘못된 사용
484
+ // ❌ 잘못됨
658
485
  import { LumirEditor } from "@lumir-company/editor";
659
486
 
660
- // ✅ 올바른 사용 - dynamic import 사용
487
+ // ✅ 올바름
661
488
  const LumirEditor = dynamic(
662
489
  () =>
663
490
  import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
@@ -665,78 +492,85 @@ const LumirEditor = dynamic(
665
492
  );
666
493
  ```
667
494
 
668
- #### 3. 높이가 0으로 표시됨
495
+ #### 3. 이미지 업로드 실패
669
496
 
670
497
  ```tsx
671
- // 잘못된 사용
672
- <LumirEditor />
673
-
674
- // ✅ 올바른 사용 - 부모 요소에 높이 설정
675
- <div className="h-[400px]">
676
- <LumirEditor />
677
- </div>
498
+ // uploadFile 또는 s3Upload 중 하나는 반드시 설정!
499
+ <LumirEditor
500
+ s3Upload={{
501
+ apiEndpoint: "/api/s3/presigned",
502
+ env: "development",
503
+ path: "images",
504
+ }}
505
+ />
678
506
  ```
679
507
 
680
- #### 4. 이미지 업로드 실패
508
+ #### 4. 여러 이미지 업로드 시 중복 문제
681
509
 
682
510
  ```tsx
683
- // uploadFile 또는 s3Upload 중 하나 반드시 설정
511
+ // 해결: appendUUID 사용
684
512
  <LumirEditor
685
- uploadFile={async (file) => {
686
- // 업로드 로직
687
- return imageUrl;
688
- }}
689
- // 또는
690
513
  s3Upload={{
691
514
  apiEndpoint: "/api/s3/presigned",
692
- env: "development",
515
+ env: "production",
693
516
  path: "images",
517
+ appendUUID: true, // 고유한 파일명 보장
694
518
  }}
695
519
  />
696
520
  ```
697
521
 
698
- ### 성능 최적화 팁
522
+ ---
699
523
 
700
- 1. **애니메이션 기본 비활성**: 이미 `animations: false`로 설정되어 성능 최적화됨
701
- 2. **큰 콘텐츠 처리**: 초기 콘텐츠가 클 경우 lazy loading 고려
702
- 3. **이미지 최적화**: 업로드 전 클라이언트에서 이미지 리사이징 권장
524
+ ## 🛠️ 유틸리티 API
703
525
 
704
- ---
526
+ ### ContentUtils
705
527
 
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 통합 가이드
528
+ ```tsx
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();
728
539
  ```
729
540
 
730
- ---
541
+ ### createS3Uploader
731
542
 
732
- ## 📄 라이선스
543
+ ```tsx
544
+ import { createS3Uploader } from "@lumir-company/editor";
733
545
 
734
- MIT License
546
+ const uploader = createS3Uploader({
547
+ apiEndpoint: "/api/s3/presigned",
548
+ env: "production",
549
+ path: "uploads",
550
+ appendUUID: true,
551
+ });
735
552
 
736
- ---
553
+ // 직접 사용
554
+ const url = await uploader(imageFile);
555
+ ```
737
556
 
738
557
  ## 🔗 관련 링크
739
558
 
740
- - [GitHub Repository](https://github.com/lumir-company/editor)
741
559
  - [npm Package](https://www.npmjs.com/package/@lumir-company/editor)
742
560
  - [BlockNote Documentation](https://www.blocknotejs.org/)
561
+
562
+ ---
563
+
564
+ ## 📝 변경 로그
565
+
566
+ ### v0.4.0
567
+
568
+ - ✨ 파일명 변환 콜백 (`fileNameTransform`) 추가
569
+ - ✨ UUID 자동 추가 옵션 (`appendUUID`) 추가
570
+ - 🐛 여러 이미지 동시 업로드 시 중복 문제 해결
571
+ - 📝 문서 대폭 개선
572
+
573
+ ### v0.3.3
574
+
575
+ - 🐛 에디터 재생성 방지 최적화
576
+ - 📝 타입 정의 개선