@lumir-company/editor 0.2.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -18,7 +18,11 @@ interface LumirEditorProps {
18
18
  initialEmptyBlocks?: number;
19
19
  placeholder?: string;
20
20
  uploadFile?: (file: File) => Promise<string>;
21
- storeImagesAsBase64?: boolean;
21
+ s3Upload?: {
22
+ apiEndpoint: string;
23
+ env: "development" | "production";
24
+ path: string;
25
+ };
22
26
  allowVideoUpload?: boolean;
23
27
  allowAudioUpload?: boolean;
24
28
  allowFileUpload?: boolean;
@@ -31,21 +35,18 @@ interface LumirEditorProps {
31
35
  heading?: {
32
36
  levels?: (1 | 2 | 3 | 4 | 5 | 6)[];
33
37
  };
34
- animations?: boolean;
35
38
  defaultStyles?: boolean;
36
39
  disableExtensions?: string[];
37
- tabBehavior?: 'prefer-navigate-ui' | 'prefer-indent';
40
+ tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
38
41
  trailingBlock?: boolean;
39
- resolveFileUrl?: (url: string) => Promise<string>;
40
42
  editable?: boolean;
41
- theme?: 'light' | 'dark' | Partial<Record<string, unknown>> | {
43
+ theme?: "light" | "dark" | Partial<Record<string, unknown>> | {
42
44
  light: Partial<Record<string, unknown>>;
43
45
  dark: Partial<Record<string, unknown>>;
44
46
  };
45
47
  formattingToolbar?: boolean;
46
48
  linkToolbar?: boolean;
47
49
  sideMenu?: boolean;
48
- slashMenu?: boolean;
49
50
  emojiPicker?: boolean;
50
51
  filePanel?: boolean;
51
52
  tableHandles?: boolean;
@@ -101,7 +102,7 @@ declare class EditorConfig {
101
102
  * @param userTables 사용자 테이블 설정
102
103
  * @returns 기본값이 적용된 테이블 설정
103
104
  */
104
- static getDefaultTableConfig(userTables?: LumirEditorProps['tables']): {
105
+ static getDefaultTableConfig(userTables?: LumirEditorProps["tables"]): {
105
106
  splitCells: boolean;
106
107
  cellBackgroundColor: boolean;
107
108
  cellTextColor: boolean;
@@ -112,7 +113,7 @@ declare class EditorConfig {
112
113
  * @param userHeading 사용자 헤딩 설정
113
114
  * @returns 기본값이 적용된 헤딩 설정
114
115
  */
115
- static getDefaultHeadingConfig(userHeading?: LumirEditorProps['heading']): {
116
+ static getDefaultHeadingConfig(userHeading?: LumirEditorProps["heading"]): {
116
117
  levels?: (1 | 2 | 3 | 4 | 5 | 6)[];
117
118
  };
118
119
  /**
@@ -125,8 +126,15 @@ declare class EditorConfig {
125
126
  */
126
127
  static getDisabledExtensions(userExtensions?: string[], allowVideo?: boolean, allowAudio?: boolean, allowFile?: boolean): string[];
127
128
  }
128
- declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, tables, heading, animations, defaultStyles, disableExtensions, tabBehavior, trailingBlock, resolveFileUrl, storeImagesAsBase64, allowVideoUpload, allowAudioUpload, allowFileUpload, editable, theme, formattingToolbar, linkToolbar, sideMenu, slashMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, sideMenuAddButton, onContentChange, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
129
+ declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, sideMenuAddButton, onContentChange, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
129
130
 
130
131
  declare function cn(...inputs: (string | undefined | null | false)[]): string;
131
132
 
132
- export { ContentUtils, type DefaultPartialBlock, EditorConfig, type EditorType, LumirEditor, type LumirEditorProps, cn };
133
+ interface S3UploaderConfig {
134
+ apiEndpoint: string;
135
+ env: "production" | "development";
136
+ path: string;
137
+ }
138
+ declare const createS3Uploader: (config: S3UploaderConfig) => (file: File) => Promise<string>;
139
+
140
+ export { ContentUtils, type DefaultPartialBlock, EditorConfig, type EditorType, LumirEditor, type LumirEditorProps, type S3UploaderConfig, cn, createS3Uploader };
package/dist/index.d.ts CHANGED
@@ -18,7 +18,11 @@ interface LumirEditorProps {
18
18
  initialEmptyBlocks?: number;
19
19
  placeholder?: string;
20
20
  uploadFile?: (file: File) => Promise<string>;
21
- storeImagesAsBase64?: boolean;
21
+ s3Upload?: {
22
+ apiEndpoint: string;
23
+ env: "development" | "production";
24
+ path: string;
25
+ };
22
26
  allowVideoUpload?: boolean;
23
27
  allowAudioUpload?: boolean;
24
28
  allowFileUpload?: boolean;
@@ -31,21 +35,18 @@ interface LumirEditorProps {
31
35
  heading?: {
32
36
  levels?: (1 | 2 | 3 | 4 | 5 | 6)[];
33
37
  };
34
- animations?: boolean;
35
38
  defaultStyles?: boolean;
36
39
  disableExtensions?: string[];
37
- tabBehavior?: 'prefer-navigate-ui' | 'prefer-indent';
40
+ tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
38
41
  trailingBlock?: boolean;
39
- resolveFileUrl?: (url: string) => Promise<string>;
40
42
  editable?: boolean;
41
- theme?: 'light' | 'dark' | Partial<Record<string, unknown>> | {
43
+ theme?: "light" | "dark" | Partial<Record<string, unknown>> | {
42
44
  light: Partial<Record<string, unknown>>;
43
45
  dark: Partial<Record<string, unknown>>;
44
46
  };
45
47
  formattingToolbar?: boolean;
46
48
  linkToolbar?: boolean;
47
49
  sideMenu?: boolean;
48
- slashMenu?: boolean;
49
50
  emojiPicker?: boolean;
50
51
  filePanel?: boolean;
51
52
  tableHandles?: boolean;
@@ -101,7 +102,7 @@ declare class EditorConfig {
101
102
  * @param userTables 사용자 테이블 설정
102
103
  * @returns 기본값이 적용된 테이블 설정
103
104
  */
104
- static getDefaultTableConfig(userTables?: LumirEditorProps['tables']): {
105
+ static getDefaultTableConfig(userTables?: LumirEditorProps["tables"]): {
105
106
  splitCells: boolean;
106
107
  cellBackgroundColor: boolean;
107
108
  cellTextColor: boolean;
@@ -112,7 +113,7 @@ declare class EditorConfig {
112
113
  * @param userHeading 사용자 헤딩 설정
113
114
  * @returns 기본값이 적용된 헤딩 설정
114
115
  */
115
- static getDefaultHeadingConfig(userHeading?: LumirEditorProps['heading']): {
116
+ static getDefaultHeadingConfig(userHeading?: LumirEditorProps["heading"]): {
116
117
  levels?: (1 | 2 | 3 | 4 | 5 | 6)[];
117
118
  };
118
119
  /**
@@ -125,8 +126,15 @@ declare class EditorConfig {
125
126
  */
126
127
  static getDisabledExtensions(userExtensions?: string[], allowVideo?: boolean, allowAudio?: boolean, allowFile?: boolean): string[];
127
128
  }
128
- declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, tables, heading, animations, defaultStyles, disableExtensions, tabBehavior, trailingBlock, resolveFileUrl, storeImagesAsBase64, allowVideoUpload, allowAudioUpload, allowFileUpload, editable, theme, formattingToolbar, linkToolbar, sideMenu, slashMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, sideMenuAddButton, onContentChange, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
129
+ declare function LumirEditor({ initialContent, initialEmptyBlocks, uploadFile, s3Upload, tables, heading, defaultStyles, disableExtensions, tabBehavior, trailingBlock, allowVideoUpload, allowAudioUpload, allowFileUpload, editable, theme, formattingToolbar, linkToolbar, sideMenu, emojiPicker, filePanel, tableHandles, onSelectionChange, className, sideMenuAddButton, onContentChange, }: LumirEditorProps): react_jsx_runtime.JSX.Element;
129
130
 
130
131
  declare function cn(...inputs: (string | undefined | null | false)[]): string;
131
132
 
132
- export { ContentUtils, type DefaultPartialBlock, EditorConfig, type EditorType, LumirEditor, type LumirEditorProps, cn };
133
+ interface S3UploaderConfig {
134
+ apiEndpoint: string;
135
+ env: "production" | "development";
136
+ path: string;
137
+ }
138
+ declare const createS3Uploader: (config: S3UploaderConfig) => (file: File) => Promise<string>;
139
+
140
+ export { ContentUtils, type DefaultPartialBlock, EditorConfig, type EditorType, LumirEditor, type LumirEditorProps, type S3UploaderConfig, cn, createS3Uploader };
package/dist/index.js CHANGED
@@ -24,7 +24,8 @@ __export(index_exports, {
24
24
  ContentUtils: () => ContentUtils,
25
25
  EditorConfig: () => EditorConfig,
26
26
  LumirEditor: () => LumirEditor,
27
- cn: () => cn
27
+ cn: () => cn,
28
+ createS3Uploader: () => createS3Uploader
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
30
31
 
@@ -38,6 +39,62 @@ function cn(...inputs) {
38
39
  return inputs.filter(Boolean).join(" ");
39
40
  }
40
41
 
42
+ // src/utils/s3-uploader.ts
43
+ var createS3Uploader = (config) => {
44
+ const { apiEndpoint, env, path } = config;
45
+ if (!apiEndpoint || apiEndpoint.trim() === "") {
46
+ throw new Error(
47
+ "apiEndpoint is required for S3 upload. Please provide a valid API endpoint."
48
+ );
49
+ }
50
+ if (!env) {
51
+ throw new Error("env is required. Must be 'development' or 'production'.");
52
+ }
53
+ if (!path || path.trim() === "") {
54
+ throw new Error("path is required and cannot be empty.");
55
+ }
56
+ const generateHierarchicalFileName = (file) => {
57
+ const now = /* @__PURE__ */ new Date();
58
+ const filename = file.name;
59
+ return `${env}/${path}/${filename}`;
60
+ };
61
+ return async (file) => {
62
+ try {
63
+ if (!apiEndpoint || apiEndpoint.trim() === "") {
64
+ throw new Error(
65
+ "Invalid apiEndpoint: Cannot upload file without a valid API ENDPOINT"
66
+ );
67
+ }
68
+ const fileName = generateHierarchicalFileName(file);
69
+ const response = await fetch(
70
+ `${apiEndpoint}?key=${encodeURIComponent(fileName)}`
71
+ );
72
+ if (!response.ok) {
73
+ const errorText = await response.text() || "";
74
+ throw new Error(
75
+ `Failed to get presigned URL: ${response.statusText}, ${errorText}`
76
+ );
77
+ }
78
+ const responseData = await response.json();
79
+ const { presignedUrl, publicUrl } = responseData;
80
+ const uploadResponse = await fetch(presignedUrl, {
81
+ method: "PUT",
82
+ headers: {
83
+ "Content-Type": file.type || "application/octet-stream"
84
+ },
85
+ body: file
86
+ });
87
+ if (!uploadResponse.ok) {
88
+ throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
89
+ }
90
+ return publicUrl;
91
+ } catch (error) {
92
+ console.error("S3 upload failed:", error);
93
+ throw error;
94
+ }
95
+ };
96
+ };
97
+
41
98
  // src/components/LumirEditor.tsx
42
99
  var import_jsx_runtime = require("react/jsx-runtime");
43
100
  var ContentUtils = class {
@@ -158,32 +215,21 @@ var EditorConfig = class {
158
215
  return Array.from(set);
159
216
  }
160
217
  };
161
- var createObjectUrlUploader = async (file) => {
162
- return URL.createObjectURL(file);
163
- };
164
218
  var isImageFile = (file) => {
165
219
  return file.size > 0 && (file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name || ""));
166
220
  };
167
- var fileToBase64 = async (file) => await new Promise((resolve, reject) => {
168
- const reader = new FileReader();
169
- reader.onload = () => resolve(String(reader.result));
170
- reader.onerror = () => reject(new Error("FileReader failed"));
171
- reader.readAsDataURL(file);
172
- });
173
221
  function LumirEditor({
174
222
  // editor options
175
223
  initialContent,
176
224
  initialEmptyBlocks = 3,
177
225
  uploadFile,
226
+ s3Upload,
178
227
  tables,
179
228
  heading,
180
- animations = true,
181
229
  defaultStyles = true,
182
230
  disableExtensions,
183
231
  tabBehavior = "prefer-navigate-ui",
184
232
  trailingBlock = true,
185
- resolveFileUrl,
186
- storeImagesAsBase64 = true,
187
233
  allowVideoUpload = false,
188
234
  allowAudioUpload = false,
189
235
  allowFileUpload = false,
@@ -193,7 +239,6 @@ function LumirEditor({
193
239
  formattingToolbar = true,
194
240
  linkToolbar = true,
195
241
  sideMenu = true,
196
- slashMenu = true,
197
242
  emojiPicker = true,
198
243
  filePanel = true,
199
244
  tableHandles = true,
@@ -203,6 +248,7 @@ function LumirEditor({
203
248
  // callbacks / refs
204
249
  onContentChange
205
250
  }) {
251
+ const [isUploading, setIsUploading] = (0, import_react.useState)(false);
206
252
  const validatedContent = (0, import_react.useMemo)(() => {
207
253
  return ContentUtils.validateContent(initialContent, initialEmptyBlocks);
208
254
  }, [initialContent, initialEmptyBlocks]);
@@ -225,33 +271,41 @@ function LumirEditor({
225
271
  allowFileUpload
226
272
  );
227
273
  }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
274
+ const memoizedS3Upload = (0, import_react.useMemo)(() => {
275
+ return s3Upload;
276
+ }, [s3Upload?.apiEndpoint, s3Upload?.env, s3Upload?.path]);
228
277
  const editor = (0, import_react2.useCreateBlockNote)(
229
278
  {
230
279
  initialContent: validatedContent,
231
280
  tables: tableConfig,
232
281
  heading: headingConfig,
233
- animations,
282
+ animations: false,
283
+ // 기본적으로 애니메이션 비활성화
234
284
  defaultStyles,
235
285
  // 확장 비활성: 비디오/오디오/파일 제어
236
286
  disableExtensions: disabledExtensions,
237
287
  tabBehavior,
238
288
  trailingBlock,
239
- resolveFileUrl,
240
289
  uploadFile: async (file) => {
241
290
  if (!isImageFile(file)) {
242
291
  throw new Error("Only image files are allowed");
243
292
  }
244
- const custom = uploadFile;
245
- const fallback = storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader;
246
293
  try {
247
- if (custom) return await custom(file);
248
- return await fallback(file);
249
- } catch (_) {
250
- try {
251
- return await createObjectUrlUploader(file);
252
- } catch {
253
- throw new Error("Failed to process file for upload");
294
+ let imageUrl;
295
+ if (uploadFile) {
296
+ imageUrl = await uploadFile(file);
297
+ } else if (memoizedS3Upload?.apiEndpoint) {
298
+ const s3Uploader = createS3Uploader(memoizedS3Upload);
299
+ imageUrl = await s3Uploader(file);
300
+ } else {
301
+ throw new Error("No upload method available");
254
302
  }
303
+ return imageUrl;
304
+ } catch (error) {
305
+ console.error("Image upload failed:", error);
306
+ throw new Error(
307
+ "Upload failed: " + (error instanceof Error ? error.message : String(error))
308
+ );
255
309
  }
256
310
  },
257
311
  pasteHandler: (ctx) => {
@@ -268,17 +322,22 @@ function LumirEditor({
268
322
  }
269
323
  event.preventDefault();
270
324
  (async () => {
271
- for (const file of acceptedFiles) {
272
- try {
273
- const url = await editor2.uploadFile(file);
274
- editor2.pasteHTML(`<img src="${url}" alt="image" />`);
275
- } catch (err) {
276
- console.warn(
277
- "Image upload failed, skipped:",
278
- file.name || "",
279
- err
280
- );
325
+ setIsUploading(true);
326
+ try {
327
+ for (const file of acceptedFiles) {
328
+ try {
329
+ const url = await editor2.uploadFile(file);
330
+ editor2.pasteHTML(`<img src="${url}" alt="image" />`);
331
+ } catch (err) {
332
+ console.warn(
333
+ "Image upload failed, skipped:",
334
+ file.name || "",
335
+ err
336
+ );
337
+ }
281
338
  }
339
+ } finally {
340
+ setIsUploading(false);
282
341
  }
283
342
  })();
284
343
  return true;
@@ -288,14 +347,12 @@ function LumirEditor({
288
347
  validatedContent,
289
348
  tableConfig,
290
349
  headingConfig,
291
- animations,
292
350
  defaultStyles,
293
351
  disabledExtensions,
294
352
  tabBehavior,
295
353
  trailingBlock,
296
- resolveFileUrl,
297
354
  uploadFile,
298
- storeImagesAsBase64
355
+ memoizedS3Upload
299
356
  ]
300
357
  );
301
358
  (0, import_react.useEffect)(() => {
@@ -333,17 +390,26 @@ function LumirEditor({
333
390
  const acceptedFiles = files.filter(isImageFile);
334
391
  if (acceptedFiles.length === 0) return;
335
392
  (async () => {
336
- for (const file of acceptedFiles) {
337
- try {
338
- if (editor?.uploadFile) {
339
- const url = await editor.uploadFile(file);
340
- if (url) {
341
- editor.pasteHTML(`<img src="${url}" alt="image" />`);
393
+ setIsUploading(true);
394
+ try {
395
+ for (const file of acceptedFiles) {
396
+ try {
397
+ if (editor?.uploadFile) {
398
+ const url = await editor.uploadFile(file);
399
+ if (url) {
400
+ editor.pasteHTML(`<img src="${url}" alt="image" />`);
401
+ }
342
402
  }
403
+ } catch (err) {
404
+ console.warn(
405
+ "Image upload failed, skipped:",
406
+ file.name || "",
407
+ err
408
+ );
343
409
  }
344
- } catch (err) {
345
- console.warn("Image upload failed, skipped:", file.name || "", err);
346
410
  }
411
+ } finally {
412
+ setIsUploading(false);
347
413
  }
348
414
  })();
349
415
  };
@@ -362,58 +428,69 @@ function LumirEditor({
362
428
  const DragHandleOnlySideMenu = (0, import_react.useMemo)(() => {
363
429
  return (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.SideMenu, { ...props, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.DragHandleButton, { ...props }) });
364
430
  }, []);
365
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("lumirEditor", className), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
366
- import_mantine.BlockNoteView,
431
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
432
+ "div",
367
433
  {
368
- editor,
369
- editable,
370
- theme,
371
- formattingToolbar,
372
- linkToolbar,
373
- sideMenu: computedSideMenu,
374
- slashMenu: false,
375
- emojiPicker,
376
- filePanel,
377
- tableHandles,
378
- onSelectionChange,
434
+ className: cn("lumirEditor", className),
435
+ style: { position: "relative" },
379
436
  children: [
380
- slashMenu && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
381
- import_react2.SuggestionMenuController,
437
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
438
+ import_mantine.BlockNoteView,
382
439
  {
383
- triggerCharacter: "/",
384
- getItems: (0, import_react.useCallback)(
385
- async (query) => {
386
- const items = (0, import_react2.getDefaultReactSlashMenuItems)(editor);
387
- const filtered = items.filter((item) => {
388
- const key = (item?.key || "").toString().toLowerCase();
389
- const title = (item?.title || "").toString().toLowerCase();
390
- if (["video", "audio", "file"].includes(key)) return false;
391
- if (title.includes("video") || title.includes("audio") || title.includes("file"))
392
- return false;
393
- return true;
394
- });
395
- if (!query) return filtered;
396
- const q = query.toLowerCase();
397
- return filtered.filter(
398
- (item) => item.title?.toLowerCase().includes(q) || (item.aliases || []).some(
399
- (a) => a.toLowerCase().includes(q)
440
+ editor,
441
+ editable,
442
+ theme,
443
+ formattingToolbar,
444
+ linkToolbar,
445
+ sideMenu: computedSideMenu,
446
+ slashMenu: false,
447
+ emojiPicker,
448
+ filePanel,
449
+ tableHandles,
450
+ onSelectionChange,
451
+ children: [
452
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
453
+ import_react2.SuggestionMenuController,
454
+ {
455
+ triggerCharacter: "/",
456
+ getItems: (0, import_react.useCallback)(
457
+ async (query) => {
458
+ const items = (0, import_react2.getDefaultReactSlashMenuItems)(editor);
459
+ const filtered = items.filter((item) => {
460
+ const key = (item?.key || "").toString().toLowerCase();
461
+ const title = (item?.title || "").toString().toLowerCase();
462
+ if (["video", "audio", "file"].includes(key)) return false;
463
+ if (title.includes("video") || title.includes("audio") || title.includes("file"))
464
+ return false;
465
+ return true;
466
+ });
467
+ if (!query) return filtered;
468
+ const q = query.toLowerCase();
469
+ return filtered.filter(
470
+ (item) => item.title?.toLowerCase().includes(q) || (item.aliases || []).some(
471
+ (a) => a.toLowerCase().includes(q)
472
+ )
473
+ );
474
+ },
475
+ [editor]
400
476
  )
401
- );
402
- },
403
- [editor]
404
- )
477
+ }
478
+ ),
479
+ !sideMenuAddButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.SideMenuController, { sideMenu: DragHandleOnlySideMenu })
480
+ ]
405
481
  }
406
482
  ),
407
- !sideMenuAddButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.SideMenuController, { sideMenu: DragHandleOnlySideMenu })
483
+ isUploading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lumirEditor-upload-overlay", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "lumirEditor-spinner" }) })
408
484
  ]
409
485
  }
410
- ) });
486
+ );
411
487
  }
412
488
  // Annotate the CommonJS export names for ESM import in node:
413
489
  0 && (module.exports = {
414
490
  ContentUtils,
415
491
  EditorConfig,
416
492
  LumirEditor,
417
- cn
493
+ cn,
494
+ createS3Uploader
418
495
  });
419
496
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/components/LumirEditor.tsx","../src/utils/cn.ts"],"sourcesContent":["'use client';\r\n\r\n// 컴포넌트 및 유틸리티 export\r\nexport {\r\n default as LumirEditor,\r\n ContentUtils,\r\n EditorConfig,\r\n} from './components/LumirEditor';\r\nexport { cn } from './utils/cn';\r\n\r\n// 타입 export (별도 파일에서 관리)\r\nexport type {\r\n LumirEditorProps,\r\n EditorType,\r\n DefaultPartialBlock,\r\n DefaultBlockSchema,\r\n DefaultInlineContentSchema,\r\n DefaultStyleSchema,\r\n PartialBlock,\r\n BlockNoteEditor,\r\n} from './types';\r\n","'use client';\n\nimport { useEffect, useMemo, useCallback } from 'react';\nimport {\n useCreateBlockNote,\n SideMenu as BlockSideMenu,\n SideMenuController,\n DragHandleButton,\n SuggestionMenuController,\n getDefaultReactSlashMenuItems,\n} from '@blocknote/react';\nimport { BlockNoteView } from '@blocknote/mantine';\nimport { cn } from '../utils/cn';\n\nimport type {\n DefaultPartialBlock,\n LumirEditorProps,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n} from '../types';\n\n// ==========================================\n// 유틸리티 클래스들\n// ==========================================\n\n/**\n * 콘텐츠 관리 유틸리티\n * 기본 블록 생성 및 콘텐츠 검증 로직을 담당\n */\nexport class ContentUtils {\n /**\n * JSON 문자열의 유효성을 검증합니다\n * @param jsonString 검증할 JSON 문자열\n * @returns 유효한 JSON 문자열인지 여부\n */\n static isValidJSONString(jsonString: string): boolean {\n try {\n const parsed = JSON.parse(jsonString);\n return Array.isArray(parsed);\n } catch {\n return false;\n }\n }\n\n /**\n * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다\n * @param jsonString JSON 문자열\n * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)\n */\n static parseJSONContent(jsonString: string): DefaultPartialBlock[] | null {\n try {\n const parsed = JSON.parse(jsonString);\n if (Array.isArray(parsed)) {\n return parsed as DefaultPartialBlock[];\n }\n return null;\n } catch {\n return null;\n }\n }\n\n /**\n * 기본 paragraph 블록 생성\n * @returns 기본 설정이 적용된 DefaultPartialBlock\n */\n static createDefaultBlock(): DefaultPartialBlock {\n return {\n type: 'paragraph',\n props: {\n textColor: 'default',\n backgroundColor: 'default',\n textAlignment: 'left',\n },\n content: [{ type: 'text', text: '', styles: {} }],\n children: [],\n };\n }\n\n /**\n * 콘텐츠 유효성 검증 및 기본값 설정\n * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)\n * @param emptyBlockCount 빈 블록 개수 (기본값: 3)\n * @returns 검증된 콘텐츠 배열\n */\n static validateContent(\n content?: DefaultPartialBlock[] | string,\n emptyBlockCount: number = 3,\n ): DefaultPartialBlock[] {\n // 1. 문자열인 경우 JSON 파싱 시도\n if (typeof content === 'string') {\n if (content.trim() === '') {\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n const parsedContent = this.parseJSONContent(content);\n if (parsedContent && parsedContent.length > 0) {\n return parsedContent;\n }\n\n // 파싱 실패 시 빈 블록 생성\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n // 2. 배열인 경우 기존 로직\n if (!content || content.length === 0) {\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n return content;\n }\n\n /**\n * 빈 블록들을 생성합니다\n * @param emptyBlockCount 생성할 블록 개수\n * @returns 생성된 빈 블록 배열\n */\n private static createEmptyBlocks(\n emptyBlockCount: number,\n ): DefaultPartialBlock[] {\n return Array.from({ length: emptyBlockCount }, () =>\n this.createDefaultBlock(),\n );\n }\n}\n\n/**\n * 에디터 설정 관리 유틸리티\n * 각종 설정의 기본값과 검증 로직을 담당\n */\nexport class EditorConfig {\n /**\n * 테이블 설정 기본값 적용\n * @param userTables 사용자 테이블 설정\n * @returns 기본값이 적용된 테이블 설정\n */\n static getDefaultTableConfig(userTables?: LumirEditorProps['tables']) {\n return {\n splitCells: userTables?.splitCells ?? true,\n cellBackgroundColor: userTables?.cellBackgroundColor ?? true,\n cellTextColor: userTables?.cellTextColor ?? true,\n headers: userTables?.headers ?? true,\n };\n }\n\n /**\n * 헤딩 설정 기본값 적용\n * @param userHeading 사용자 헤딩 설정\n * @returns 기본값이 적용된 헤딩 설정\n */\n static getDefaultHeadingConfig(userHeading?: LumirEditorProps['heading']) {\n return userHeading?.levels && userHeading.levels.length > 0\n ? userHeading\n : { levels: [1, 2, 3, 4, 5, 6] as (1 | 2 | 3 | 4 | 5 | 6)[] };\n }\n\n /**\n * 비활성화할 확장 기능 목록 생성\n * @param userExtensions 사용자 정의 비활성 확장\n * @param allowVideo 비디오 업로드 허용 여부\n * @param allowAudio 오디오 업로드 허용 여부\n * @param allowFile 일반 파일 업로드 허용 여부\n * @returns 비활성화할 확장 기능 목록\n */\n static getDisabledExtensions(\n userExtensions?: string[],\n allowVideo = false,\n allowAudio = false,\n allowFile = false,\n ): string[] {\n const set = new Set<string>(userExtensions ?? []);\n if (!allowVideo) set.add('video');\n if (!allowAudio) set.add('audio');\n if (!allowFile) set.add('file');\n return Array.from(set);\n }\n}\n\nconst createObjectUrlUploader = async (file: File): Promise<string> => {\n return URL.createObjectURL(file);\n};\n\n// 파일 타입 검증 함수\nconst isImageFile = (file: File): boolean => {\n return (\n file.size > 0 &&\n (file.type?.startsWith('image/') ||\n (!file.type && /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name || '')))\n );\n};\n\n// 이미지 파일을 Base64로 변환하는 함수\nconst fileToBase64 = async (file: File): Promise<string> =>\n await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('FileReader failed'));\n reader.readAsDataURL(file);\n });\n\nexport default function LumirEditor({\n // editor options\n initialContent,\n initialEmptyBlocks = 3,\n uploadFile,\n tables,\n heading,\n animations = true,\n defaultStyles = true,\n disableExtensions,\n tabBehavior = 'prefer-navigate-ui',\n trailingBlock = true,\n resolveFileUrl,\n storeImagesAsBase64 = true,\n allowVideoUpload = false,\n allowAudioUpload = false,\n allowFileUpload = false,\n // view options\n editable = true,\n theme = 'light',\n formattingToolbar = true,\n linkToolbar = true,\n sideMenu = true,\n slashMenu = true,\n emojiPicker = true,\n filePanel = true,\n tableHandles = true,\n onSelectionChange,\n className = '',\n sideMenuAddButton = false,\n // callbacks / refs\n onContentChange,\n}: LumirEditorProps) {\n const validatedContent = useMemo<DefaultPartialBlock[]>(() => {\n return ContentUtils.validateContent(initialContent, initialEmptyBlocks);\n }, [initialContent, initialEmptyBlocks]);\n\n // 테이블 설정 메모이제이션\n const tableConfig = useMemo(() => {\n return EditorConfig.getDefaultTableConfig(tables);\n }, [\n tables?.splitCells,\n tables?.cellBackgroundColor,\n tables?.cellTextColor,\n tables?.headers,\n ]);\n\n // 헤딩 설정 메모이제이션\n const headingConfig = useMemo(() => {\n return EditorConfig.getDefaultHeadingConfig(heading);\n }, [heading?.levels?.join(',') ?? '']);\n\n // 비활성화 확장 메모이제이션\n const disabledExtensions = useMemo(() => {\n return EditorConfig.getDisabledExtensions(\n disableExtensions,\n allowVideoUpload,\n allowAudioUpload,\n allowFileUpload,\n );\n }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);\n\n const editor = useCreateBlockNote<\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema\n >(\n {\n initialContent: validatedContent as DefaultPartialBlock[],\n tables: tableConfig,\n heading: headingConfig,\n animations,\n defaultStyles,\n // 확장 비활성: 비디오/오디오/파일 제어\n disableExtensions: disabledExtensions,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile: async (file) => {\n // 이미지 파일만 허용 (이미지 전용 에디터)\n if (!isImageFile(file)) {\n throw new Error('Only image files are allowed');\n }\n\n const custom = uploadFile;\n const fallback = storeImagesAsBase64\n ? fileToBase64\n : createObjectUrlUploader;\n try {\n if (custom) return await custom(file);\n return await fallback(file);\n } catch (_) {\n // Fallback to ObjectURL when FileReader or custom upload fails\n try {\n return await createObjectUrlUploader(file);\n } catch {\n throw new Error('Failed to process file for upload');\n }\n }\n },\n pasteHandler: (ctx) => {\n const { event, editor, defaultPasteHandler } = ctx as any;\n const fileList =\n (event?.clipboardData?.files as FileList | null) ?? null;\n const files: File[] = fileList ? Array.from(fileList) : [];\n const acceptedFiles: File[] = files.filter(isImageFile);\n\n // 파일이 있지만 이미지가 없으면 기본 처리 막고 무시\n if (files.length > 0 && acceptedFiles.length === 0) {\n event.preventDefault();\n return true;\n }\n\n // 이미지가 없으면 기본 처리\n if (acceptedFiles.length === 0) {\n return defaultPasteHandler() ?? false;\n }\n\n event.preventDefault();\n (async () => {\n for (const file of acceptedFiles) {\n try {\n // 에디터의 uploadFile 함수 사용 (통일된 로직)\n const url = await editor.uploadFile(file);\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n console.warn(\n 'Image upload failed, skipped:',\n file.name || '',\n err,\n );\n }\n }\n })();\n return true;\n },\n },\n [\n validatedContent,\n tableConfig,\n headingConfig,\n animations,\n defaultStyles,\n disabledExtensions,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile,\n storeImagesAsBase64,\n ],\n );\n\n // 편집 가능 여부 설정\n useEffect(() => {\n if (editor) {\n editor.isEditable = editable;\n }\n }, [editor, editable]);\n\n // 콘텐츠 변경 감지\n useEffect(() => {\n if (!editor || !onContentChange) return;\n\n const handleContentChange = () => {\n // BlockNote의 올바른 API 사용\n const blocks = editor.topLevelBlocks as DefaultPartialBlock[];\n onContentChange(blocks);\n };\n\n return editor.onEditorContentChange(handleContentChange);\n }, [editor, onContentChange]);\n\n // 드래그앤드롭 이미지 처리\n useEffect(() => {\n const el = editor?.domElement as HTMLElement | undefined;\n if (!el) return;\n\n const handleDragOver = (e: DragEvent) => {\n if (e.defaultPrevented) return;\n const hasFiles = (\n e.dataTransfer?.types as unknown as string[] | undefined\n )?.includes?.('Files');\n if (hasFiles) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const handleDrop = (e: DragEvent) => {\n if (!e.dataTransfer) return;\n const hasFiles = (\n (e.dataTransfer.types as unknown as string[] | undefined) ?? []\n ).includes('Files');\n if (!hasFiles) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n const items = Array.from(e.dataTransfer.items ?? []);\n const files = items\n .filter((it) => it.kind === 'file')\n .map((it) => it.getAsFile())\n .filter((f): f is File => !!f);\n\n // 이미지 파일만 허용\n const acceptedFiles = files.filter(isImageFile);\n\n if (acceptedFiles.length === 0) return;\n\n (async () => {\n for (const file of acceptedFiles) {\n try {\n // 에디터의 uploadFile 함수 사용 (일관된 로직)\n if (editor?.uploadFile) {\n const url = await editor.uploadFile(file);\n if (url) {\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n }\n }\n } catch (err) {\n console.warn('Image upload failed, skipped:', file.name || '', err);\n }\n }\n })();\n };\n\n el.addEventListener('dragover', handleDragOver, { capture: true });\n el.addEventListener('drop', handleDrop, { capture: true });\n\n return () => {\n el.removeEventListener('dragover', handleDragOver, {\n capture: true,\n } as any);\n el.removeEventListener('drop', handleDrop, { capture: true } as any);\n };\n }, [editor]);\n\n // SideMenu 설정 (Add 버튼 제어)\n const computedSideMenu = useMemo(() => {\n return sideMenuAddButton ? sideMenu : false;\n }, [sideMenuAddButton, sideMenu]);\n\n // Add 버튼 없는 사이드 메뉴 (드래그 핸들만) - 메모이제이션\n const DragHandleOnlySideMenu = useMemo(() => {\n return (props: any) => (\n <BlockSideMenu {...props}>\n <DragHandleButton {...props} />\n </BlockSideMenu>\n );\n }, []);\n\n return (\n <div className={cn('lumirEditor', className)}>\n <BlockNoteView\n editor={editor}\n editable={editable}\n theme={theme}\n formattingToolbar={formattingToolbar}\n linkToolbar={linkToolbar}\n sideMenu={computedSideMenu}\n slashMenu={false}\n emojiPicker={emojiPicker}\n filePanel={filePanel}\n tableHandles={tableHandles}\n onSelectionChange={onSelectionChange}>\n {slashMenu && (\n <SuggestionMenuController\n triggerCharacter='/'\n getItems={useCallback(\n async (query: string) => {\n const items = getDefaultReactSlashMenuItems(editor);\n // 비디오, 오디오, 파일 관련 항목 제거\n const filtered = items.filter((item: any) => {\n const key = (item?.key || '').toString().toLowerCase();\n const title = (item?.title || '').toString().toLowerCase();\n // 비디오, 오디오, 파일 관련 항목 제거\n if (['video', 'audio', 'file'].includes(key)) return false;\n if (\n title.includes('video') ||\n title.includes('audio') ||\n title.includes('file')\n )\n return false;\n return true;\n });\n\n if (!query) return filtered;\n const q = query.toLowerCase();\n return filtered.filter(\n (item: any) =>\n item.title?.toLowerCase().includes(q) ||\n (item.aliases || []).some((a: string) =>\n a.toLowerCase().includes(q),\n ),\n );\n },\n [editor],\n )}\n />\n )}\n {!sideMenuAddButton && (\n <SideMenuController sideMenu={DragHandleOnlySideMenu} />\n )}\n </BlockNoteView>\n </div>\n );\n}\n","// clsx와 tailwind-merge를 사용한 className 유틸리티\r\n// 사용자가 직접 설치하도록 권장하거나, 간단한 버전 제공\r\n\r\nexport function cn(...inputs: (string | undefined | null | false)[]) {\r\n return inputs.filter(Boolean).join(' ');\r\n}\r\n"],"mappings":"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","names":["import_react","editor","BlockSideMenu"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/components/LumirEditor.tsx","../src/utils/cn.ts","../src/utils/s3-uploader.ts"],"sourcesContent":["\"use client\";\r\n\r\n// 컴포넌트 및 유틸리티 export\r\nexport {\r\n default as LumirEditor,\r\n ContentUtils,\r\n EditorConfig,\r\n} from \"./components/LumirEditor\";\r\nexport { cn } from \"./utils/cn\";\r\nexport { createS3Uploader } from \"./utils/s3-uploader\";\r\n\r\n// 타입 export (별도 파일에서 관리)\r\nexport type {\r\n LumirEditorProps,\r\n EditorType,\r\n DefaultPartialBlock,\r\n DefaultBlockSchema,\r\n DefaultInlineContentSchema,\r\n DefaultStyleSchema,\r\n PartialBlock,\r\n BlockNoteEditor,\r\n} from \"./types\";\r\nexport type { S3UploaderConfig } from \"./utils/s3-uploader\";\r\n","\"use client\";\r\n\r\nimport { useEffect, useMemo, useCallback, useState } from \"react\";\r\nimport {\r\n useCreateBlockNote,\r\n SideMenu as BlockSideMenu,\r\n SideMenuController,\r\n DragHandleButton,\r\n SuggestionMenuController,\r\n getDefaultReactSlashMenuItems,\r\n} from \"@blocknote/react\";\r\nimport { BlockNoteView } from \"@blocknote/mantine\";\r\nimport { cn } from \"../utils/cn\";\r\n\r\nimport type {\r\n DefaultPartialBlock,\r\n LumirEditorProps,\r\n DefaultBlockSchema,\r\n DefaultInlineContentSchema,\r\n DefaultStyleSchema,\r\n} from \"../types\";\r\n\r\nimport { createS3Uploader } from \"../utils/s3-uploader\";\r\n\r\n// ==========================================\r\n// 유틸리티 클래스들\r\n// ==========================================\r\n\r\n/**\r\n * 콘텐츠 관리 유틸리티\r\n * 기본 블록 생성 및 콘텐츠 검증 로직을 담당\r\n */\r\nexport class ContentUtils {\r\n /**\r\n * JSON 문자열의 유효성을 검증합니다\r\n * @param jsonString 검증할 JSON 문자열\r\n * @returns 유효한 JSON 문자열인지 여부\r\n */\r\n static isValidJSONString(jsonString: string): boolean {\r\n try {\r\n const parsed = JSON.parse(jsonString);\r\n return Array.isArray(parsed);\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다\r\n * @param jsonString JSON 문자열\r\n * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)\r\n */\r\n static parseJSONContent(jsonString: string): DefaultPartialBlock[] | null {\r\n try {\r\n const parsed = JSON.parse(jsonString);\r\n if (Array.isArray(parsed)) {\r\n return parsed as DefaultPartialBlock[];\r\n }\r\n return null;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * 기본 paragraph 블록 생성\r\n * @returns 기본 설정이 적용된 DefaultPartialBlock\r\n */\r\n static createDefaultBlock(): DefaultPartialBlock {\r\n return {\r\n type: \"paragraph\",\r\n props: {\r\n textColor: \"default\",\r\n backgroundColor: \"default\",\r\n textAlignment: \"left\",\r\n },\r\n content: [{ type: \"text\", text: \"\", styles: {} }],\r\n children: [],\r\n };\r\n }\r\n\r\n /**\r\n * 콘텐츠 유효성 검증 및 기본값 설정\r\n * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)\r\n * @param emptyBlockCount 빈 블록 개수 (기본값: 3)\r\n * @returns 검증된 콘텐츠 배열\r\n */\r\n static validateContent(\r\n content?: DefaultPartialBlock[] | string,\r\n emptyBlockCount: number = 3\r\n ): DefaultPartialBlock[] {\r\n // 1. 문자열인 경우 JSON 파싱 시도\r\n if (typeof content === \"string\") {\r\n if (content.trim() === \"\") {\r\n return this.createEmptyBlocks(emptyBlockCount);\r\n }\r\n\r\n const parsedContent = this.parseJSONContent(content);\r\n if (parsedContent && parsedContent.length > 0) {\r\n return parsedContent;\r\n }\r\n\r\n // 파싱 실패 시 빈 블록 생성\r\n return this.createEmptyBlocks(emptyBlockCount);\r\n }\r\n\r\n // 2. 배열인 경우 기존 로직\r\n if (!content || content.length === 0) {\r\n return this.createEmptyBlocks(emptyBlockCount);\r\n }\r\n\r\n return content;\r\n }\r\n\r\n /**\r\n * 빈 블록들을 생성합니다\r\n * @param emptyBlockCount 생성할 블록 개수\r\n * @returns 생성된 빈 블록 배열\r\n */\r\n private static createEmptyBlocks(\r\n emptyBlockCount: number\r\n ): DefaultPartialBlock[] {\r\n return Array.from({ length: emptyBlockCount }, () =>\r\n this.createDefaultBlock()\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * 에디터 설정 관리 유틸리티\r\n * 각종 설정의 기본값과 검증 로직을 담당\r\n */\r\nexport class EditorConfig {\r\n /**\r\n * 테이블 설정 기본값 적용\r\n * @param userTables 사용자 테이블 설정\r\n * @returns 기본값이 적용된 테이블 설정\r\n */\r\n static getDefaultTableConfig(userTables?: LumirEditorProps[\"tables\"]) {\r\n return {\r\n splitCells: userTables?.splitCells ?? true,\r\n cellBackgroundColor: userTables?.cellBackgroundColor ?? true,\r\n cellTextColor: userTables?.cellTextColor ?? true,\r\n headers: userTables?.headers ?? true,\r\n };\r\n }\r\n\r\n /**\r\n * 헤딩 설정 기본값 적용\r\n * @param userHeading 사용자 헤딩 설정\r\n * @returns 기본값이 적용된 헤딩 설정\r\n */\r\n static getDefaultHeadingConfig(userHeading?: LumirEditorProps[\"heading\"]) {\r\n return userHeading?.levels && userHeading.levels.length > 0\r\n ? userHeading\r\n : { levels: [1, 2, 3, 4, 5, 6] as (1 | 2 | 3 | 4 | 5 | 6)[] };\r\n }\r\n\r\n /**\r\n * 비활성화할 확장 기능 목록 생성\r\n * @param userExtensions 사용자 정의 비활성 확장\r\n * @param allowVideo 비디오 업로드 허용 여부\r\n * @param allowAudio 오디오 업로드 허용 여부\r\n * @param allowFile 일반 파일 업로드 허용 여부\r\n * @returns 비활성화할 확장 기능 목록\r\n */\r\n static getDisabledExtensions(\r\n userExtensions?: string[],\r\n allowVideo = false,\r\n allowAudio = false,\r\n allowFile = false\r\n ): string[] {\r\n const set = new Set<string>(userExtensions ?? []);\r\n if (!allowVideo) set.add(\"video\");\r\n if (!allowAudio) set.add(\"audio\");\r\n if (!allowFile) set.add(\"file\");\r\n return Array.from(set);\r\n }\r\n}\r\n\r\n// 파일 타입 검증 함수\r\nconst isImageFile = (file: File): boolean => {\r\n return (\r\n file.size > 0 &&\r\n (file.type?.startsWith(\"image/\") ||\r\n (!file.type && /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name || \"\")))\r\n );\r\n};\r\n\r\nexport default function LumirEditor({\r\n // editor options\r\n initialContent,\r\n initialEmptyBlocks = 3,\r\n uploadFile,\r\n s3Upload,\r\n tables,\r\n heading,\r\n defaultStyles = true,\r\n disableExtensions,\r\n tabBehavior = \"prefer-navigate-ui\",\r\n trailingBlock = true,\r\n allowVideoUpload = false,\r\n allowAudioUpload = false,\r\n allowFileUpload = false,\r\n // view options\r\n editable = true,\r\n theme = \"light\",\r\n formattingToolbar = true,\r\n linkToolbar = true,\r\n sideMenu = true,\r\n emojiPicker = true,\r\n filePanel = true,\r\n tableHandles = true,\r\n onSelectionChange,\r\n className = \"\",\r\n sideMenuAddButton = false,\r\n // callbacks / refs\r\n onContentChange,\r\n}: LumirEditorProps) {\r\n // 이미지 업로드 로딩 상태\r\n const [isUploading, setIsUploading] = useState(false);\r\n const validatedContent = useMemo<DefaultPartialBlock[]>(() => {\r\n return ContentUtils.validateContent(initialContent, initialEmptyBlocks);\r\n }, [initialContent, initialEmptyBlocks]);\r\n\r\n // 테이블 설정 메모이제이션\r\n const tableConfig = useMemo(() => {\r\n return EditorConfig.getDefaultTableConfig(tables);\r\n }, [\r\n tables?.splitCells,\r\n tables?.cellBackgroundColor,\r\n tables?.cellTextColor,\r\n tables?.headers,\r\n ]);\r\n\r\n // 헤딩 설정 메모이제이션\r\n const headingConfig = useMemo(() => {\r\n return EditorConfig.getDefaultHeadingConfig(heading);\r\n }, [heading?.levels?.join(\",\") ?? \"\"]);\r\n\r\n // 비활성화 확장 메모이제이션\r\n const disabledExtensions = useMemo(() => {\r\n return EditorConfig.getDisabledExtensions(\r\n disableExtensions,\r\n allowVideoUpload,\r\n allowAudioUpload,\r\n allowFileUpload\r\n );\r\n }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);\r\n\r\n // S3 업로드 설정 메모이제이션 (객체 참조 안정화)\r\n const memoizedS3Upload = useMemo(() => {\r\n return s3Upload;\r\n }, [s3Upload?.apiEndpoint, s3Upload?.env, s3Upload?.path]);\r\n\r\n const editor = useCreateBlockNote<\r\n DefaultBlockSchema,\r\n DefaultInlineContentSchema,\r\n DefaultStyleSchema\r\n >(\r\n {\r\n initialContent: validatedContent as DefaultPartialBlock[],\r\n tables: tableConfig,\r\n heading: headingConfig,\r\n animations: false, // 기본적으로 애니메이션 비활성화\r\n defaultStyles,\r\n // 확장 비활성: 비디오/오디오/파일 제어\r\n disableExtensions: disabledExtensions,\r\n tabBehavior,\r\n trailingBlock,\r\n uploadFile: async (file) => {\r\n // 이미지 파일만 허용 (이미지 전용 에디터)\r\n if (!isImageFile(file)) {\r\n throw new Error(\"Only image files are allowed\");\r\n }\r\n\r\n try {\r\n let imageUrl: string;\r\n\r\n // 1. 사용자 정의 uploadFile 우선\r\n if (uploadFile) {\r\n imageUrl = await uploadFile(file);\r\n }\r\n // 2. S3 업로드 (uploadFile 없을 때)\r\n else if (memoizedS3Upload?.apiEndpoint) {\r\n const s3Uploader = createS3Uploader(memoizedS3Upload);\r\n imageUrl = await s3Uploader(file);\r\n }\r\n // 3. 업로드 방법이 없으면 에러\r\n else {\r\n throw new Error(\"No upload method available\");\r\n }\r\n\r\n // BlockNote가 자동으로 이미지 블록을 생성하도록 URL만 반환\r\n return imageUrl;\r\n } catch (error) {\r\n console.error(\"Image upload failed:\", error);\r\n throw new Error(\r\n \"Upload failed: \" +\r\n (error instanceof Error ? error.message : String(error))\r\n );\r\n }\r\n },\r\n pasteHandler: (ctx) => {\r\n const { event, editor, defaultPasteHandler } = ctx as any;\r\n const fileList =\r\n (event?.clipboardData?.files as FileList | null) ?? null;\r\n const files: File[] = fileList ? Array.from(fileList) : [];\r\n const acceptedFiles: File[] = files.filter(isImageFile);\r\n\r\n // 파일이 있지만 이미지가 없으면 기본 처리 막고 무시\r\n if (files.length > 0 && acceptedFiles.length === 0) {\r\n event.preventDefault();\r\n return true;\r\n }\r\n\r\n // 이미지가 없으면 기본 처리\r\n if (acceptedFiles.length === 0) {\r\n return defaultPasteHandler() ?? false;\r\n }\r\n\r\n event.preventDefault();\r\n (async () => {\r\n // 붙여넣기로 여러 이미지 업로드 시 로딩 상태 관리\r\n setIsUploading(true);\r\n try {\r\n for (const file of acceptedFiles) {\r\n try {\r\n // 에디터의 uploadFile 함수 사용 (통일된 로직)\r\n const url = await editor.uploadFile(file);\r\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\r\n } catch (err) {\r\n console.warn(\r\n \"Image upload failed, skipped:\",\r\n file.name || \"\",\r\n err\r\n );\r\n }\r\n }\r\n } finally {\r\n setIsUploading(false);\r\n }\r\n })();\r\n return true;\r\n },\r\n },\r\n [\r\n validatedContent,\r\n tableConfig,\r\n headingConfig,\r\n defaultStyles,\r\n disabledExtensions,\r\n tabBehavior,\r\n trailingBlock,\r\n uploadFile,\r\n memoizedS3Upload,\r\n ]\r\n );\r\n\r\n // 편집 가능 여부 설정\r\n useEffect(() => {\r\n if (editor) {\r\n editor.isEditable = editable;\r\n }\r\n }, [editor, editable]);\r\n\r\n // 콘텐츠 변경 감지\r\n useEffect(() => {\r\n if (!editor || !onContentChange) return;\r\n\r\n const handleContentChange = () => {\r\n // BlockNote의 올바른 API 사용\r\n const blocks = editor.topLevelBlocks as DefaultPartialBlock[];\r\n onContentChange(blocks);\r\n };\r\n\r\n return editor.onEditorContentChange(handleContentChange);\r\n }, [editor, onContentChange]);\r\n\r\n // 드래그앤드롭 이미지 처리\r\n useEffect(() => {\r\n const el = editor?.domElement as HTMLElement | undefined;\r\n if (!el) return;\r\n\r\n const handleDragOver = (e: DragEvent) => {\r\n if (e.defaultPrevented) return;\r\n const hasFiles = (\r\n e.dataTransfer?.types as unknown as string[] | undefined\r\n )?.includes?.(\"Files\");\r\n if (hasFiles) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }\r\n };\r\n\r\n const handleDrop = (e: DragEvent) => {\r\n if (!e.dataTransfer) return;\r\n const hasFiles = (\r\n (e.dataTransfer.types as unknown as string[] | undefined) ?? []\r\n ).includes(\"Files\");\r\n if (!hasFiles) return;\r\n\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const items = Array.from(e.dataTransfer.items ?? []);\r\n const files = items\r\n .filter((it) => it.kind === \"file\")\r\n .map((it) => it.getAsFile())\r\n .filter((f): f is File => !!f);\r\n\r\n // 이미지 파일만 허용\r\n const acceptedFiles = files.filter(isImageFile);\r\n\r\n if (acceptedFiles.length === 0) return;\r\n\r\n (async () => {\r\n // 드래그앤드롭으로 여러 이미지 업로드 시 로딩 상태 관리\r\n setIsUploading(true);\r\n try {\r\n for (const file of acceptedFiles) {\r\n try {\r\n // 에디터의 uploadFile 함수 사용 (일관된 로직)\r\n if (editor?.uploadFile) {\r\n const url = await editor.uploadFile(file);\r\n if (url) {\r\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\r\n }\r\n }\r\n } catch (err) {\r\n console.warn(\r\n \"Image upload failed, skipped:\",\r\n file.name || \"\",\r\n err\r\n );\r\n }\r\n }\r\n } finally {\r\n setIsUploading(false);\r\n }\r\n })();\r\n };\r\n\r\n el.addEventListener(\"dragover\", handleDragOver, { capture: true });\r\n el.addEventListener(\"drop\", handleDrop, { capture: true });\r\n\r\n return () => {\r\n el.removeEventListener(\"dragover\", handleDragOver, {\r\n capture: true,\r\n } as any);\r\n el.removeEventListener(\"drop\", handleDrop, { capture: true } as any);\r\n };\r\n }, [editor]);\r\n\r\n // SideMenu 설정 (Add 버튼 제어)\r\n const computedSideMenu = useMemo(() => {\r\n return sideMenuAddButton ? sideMenu : false;\r\n }, [sideMenuAddButton, sideMenu]);\r\n\r\n // Add 버튼 없는 사이드 메뉴 (드래그 핸들만) - 메모이제이션\r\n const DragHandleOnlySideMenu = useMemo(() => {\r\n return (props: any) => (\r\n <BlockSideMenu {...props}>\r\n <DragHandleButton {...props} />\r\n </BlockSideMenu>\r\n );\r\n }, []);\r\n\r\n return (\r\n <div\r\n className={cn(\"lumirEditor\", className)}\r\n style={{ position: \"relative\" }}\r\n >\r\n <BlockNoteView\r\n editor={editor}\r\n editable={editable}\r\n theme={theme}\r\n formattingToolbar={formattingToolbar}\r\n linkToolbar={linkToolbar}\r\n sideMenu={computedSideMenu}\r\n slashMenu={false}\r\n emojiPicker={emojiPicker}\r\n filePanel={filePanel}\r\n tableHandles={tableHandles}\r\n onSelectionChange={onSelectionChange}\r\n >\r\n {\r\n <SuggestionMenuController\r\n triggerCharacter=\"/\"\r\n getItems={useCallback(\r\n async (query: string) => {\r\n const items = getDefaultReactSlashMenuItems(editor);\r\n // 비디오, 오디오, 파일 관련 항목 제거\r\n const filtered = items.filter((item: any) => {\r\n const key = (item?.key || \"\").toString().toLowerCase();\r\n const title = (item?.title || \"\").toString().toLowerCase();\r\n // 비디오, 오디오, 파일 관련 항목 제거\r\n if ([\"video\", \"audio\", \"file\"].includes(key)) return false;\r\n if (\r\n title.includes(\"video\") ||\r\n title.includes(\"audio\") ||\r\n title.includes(\"file\")\r\n )\r\n return false;\r\n return true;\r\n });\r\n\r\n if (!query) return filtered;\r\n const q = query.toLowerCase();\r\n return filtered.filter(\r\n (item: any) =>\r\n item.title?.toLowerCase().includes(q) ||\r\n (item.aliases || []).some((a: string) =>\r\n a.toLowerCase().includes(q)\r\n )\r\n );\r\n },\r\n [editor]\r\n )}\r\n />\r\n }\r\n {!sideMenuAddButton && (\r\n <SideMenuController sideMenu={DragHandleOnlySideMenu} />\r\n )}\r\n </BlockNoteView>\r\n\r\n {/* 이미지 업로드 로딩 스피너 */}\r\n {isUploading && (\r\n <div className=\"lumirEditor-upload-overlay\">\r\n <div className=\"lumirEditor-spinner\" />\r\n </div>\r\n )}\r\n </div>\r\n );\r\n}\r\n","// clsx와 tailwind-merge를 사용한 className 유틸리티\r\n// 사용자가 직접 설치하도록 권장하거나, 간단한 버전 제공\r\n\r\nexport function cn(...inputs: (string | undefined | null | false)[]) {\r\n return inputs.filter(Boolean).join(' ');\r\n}\r\n","export interface S3UploaderConfig {\r\n apiEndpoint: string; // '/api/s3/presigned'(필수)\r\n env: \"production\" | \"development\"; // 환경 (필수)\r\n path: string; // 파일 경로 (필수)\r\n}\r\n\r\nexport const createS3Uploader = (config: S3UploaderConfig) => {\r\n const { apiEndpoint, env, path } = config;\r\n\r\n // 필수 파라미터 검증\r\n if (!apiEndpoint || apiEndpoint.trim() === \"\") {\r\n throw new Error(\r\n \"apiEndpoint is required for S3 upload. Please provide a valid API endpoint.\"\r\n );\r\n }\r\n\r\n if (!env) {\r\n throw new Error(\"env is required. Must be 'development' or 'production'.\");\r\n }\r\n\r\n if (!path || path.trim() === \"\") {\r\n throw new Error(\"path is required and cannot be empty.\");\r\n }\r\n\r\n // 계층 구조 파일명 생성 함수\r\n const generateHierarchicalFileName = (file: File): string => {\r\n const now = new Date();\r\n\r\n // 날짜 (yyyy-mm-dd)\r\n\r\n // 파일명\r\n const filename = file.name;\r\n\r\n // {env}/{path}/{date}/{time}/{filename}\r\n return `${env}/${path}/${filename}`;\r\n };\r\n\r\n return async (file: File): Promise<string> => {\r\n try {\r\n // 파일 업로드 시에도 apiEndpoint 재검증\r\n if (!apiEndpoint || apiEndpoint.trim() === \"\") {\r\n throw new Error(\r\n \"Invalid apiEndpoint: Cannot upload file without a valid API ENDPOINT\"\r\n );\r\n }\r\n\r\n // 1. 계층 구조 파일명 생성\r\n const fileName = generateHierarchicalFileName(file);\r\n\r\n // 2. presigned URL 요청\r\n const response = await fetch(\r\n `${apiEndpoint}?key=${encodeURIComponent(fileName)}`\r\n );\r\n\r\n if (!response.ok) {\r\n const errorText = (await response.text()) || \"\";\r\n throw new Error(\r\n `Failed to get presigned URL: ${response.statusText}, ${errorText}`\r\n );\r\n }\r\n\r\n const responseData = await response.json();\r\n const { presignedUrl, publicUrl } = responseData;\r\n\r\n // 3. S3에 업로드\r\n const uploadResponse = await fetch(presignedUrl, {\r\n method: \"PUT\",\r\n headers: {\r\n \"Content-Type\": file.type || \"application/octet-stream\",\r\n },\r\n body: file,\r\n });\r\n\r\n if (!uploadResponse.ok) {\r\n throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);\r\n }\r\n\r\n // 4. 공개 URL 반환\r\n return publicUrl;\r\n } catch (error) {\r\n console.error(\"S3 upload failed:\", error);\r\n throw error;\r\n }\r\n };\r\n};\r\n"],"mappings":"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","names":["import_react","editor","BlockSideMenu"]}