@lumir-company/editor 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +358 -466
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +64 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,12 @@ interface LumirEditorProps {
|
|
|
22
22
|
apiEndpoint: string;
|
|
23
23
|
env: "development" | "production";
|
|
24
24
|
path: string;
|
|
25
|
+
/** 파일명 변환 콜백 - 확장자를 제외한 파일명을 받아 변환합니다 */
|
|
26
|
+
fileNameTransform?: (nameWithoutExt: string, file: File) => string;
|
|
27
|
+
/** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */
|
|
28
|
+
appendUUID?: boolean;
|
|
29
|
+
/** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */
|
|
30
|
+
preserveExtension?: boolean;
|
|
25
31
|
};
|
|
26
32
|
allowVideoUpload?: boolean;
|
|
27
33
|
allowAudioUpload?: boolean;
|
|
@@ -134,6 +140,12 @@ interface S3UploaderConfig {
|
|
|
134
140
|
apiEndpoint: string;
|
|
135
141
|
env: "production" | "development";
|
|
136
142
|
path: string;
|
|
143
|
+
/** 파일명 변환 콜백 - 확장자를 제외한 파일명을 받아 변환합니다 */
|
|
144
|
+
fileNameTransform?: (nameWithoutExt: string, file: File) => string;
|
|
145
|
+
/** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */
|
|
146
|
+
appendUUID?: boolean;
|
|
147
|
+
/** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */
|
|
148
|
+
preserveExtension?: boolean;
|
|
137
149
|
}
|
|
138
150
|
declare const createS3Uploader: (config: S3UploaderConfig) => (file: File) => Promise<string>;
|
|
139
151
|
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ interface LumirEditorProps {
|
|
|
22
22
|
apiEndpoint: string;
|
|
23
23
|
env: "development" | "production";
|
|
24
24
|
path: string;
|
|
25
|
+
/** 파일명 변환 콜백 - 확장자를 제외한 파일명을 받아 변환합니다 */
|
|
26
|
+
fileNameTransform?: (nameWithoutExt: string, file: File) => string;
|
|
27
|
+
/** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */
|
|
28
|
+
appendUUID?: boolean;
|
|
29
|
+
/** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */
|
|
30
|
+
preserveExtension?: boolean;
|
|
25
31
|
};
|
|
26
32
|
allowVideoUpload?: boolean;
|
|
27
33
|
allowAudioUpload?: boolean;
|
|
@@ -134,6 +140,12 @@ interface S3UploaderConfig {
|
|
|
134
140
|
apiEndpoint: string;
|
|
135
141
|
env: "production" | "development";
|
|
136
142
|
path: string;
|
|
143
|
+
/** 파일명 변환 콜백 - 확장자를 제외한 파일명을 받아 변환합니다 */
|
|
144
|
+
fileNameTransform?: (nameWithoutExt: string, file: File) => string;
|
|
145
|
+
/** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */
|
|
146
|
+
appendUUID?: boolean;
|
|
147
|
+
/** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */
|
|
148
|
+
preserveExtension?: boolean;
|
|
137
149
|
}
|
|
138
150
|
declare const createS3Uploader: (config: S3UploaderConfig) => (file: File) => Promise<string>;
|
|
139
151
|
|
package/dist/index.js
CHANGED
|
@@ -40,8 +40,25 @@ function cn(...inputs) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// src/utils/s3-uploader.ts
|
|
43
|
+
var generateUUID = () => {
|
|
44
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
45
|
+
return crypto.randomUUID();
|
|
46
|
+
}
|
|
47
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
48
|
+
const r = Math.random() * 16 | 0;
|
|
49
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
50
|
+
return v.toString(16);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
43
53
|
var createS3Uploader = (config) => {
|
|
44
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
apiEndpoint,
|
|
56
|
+
env,
|
|
57
|
+
path,
|
|
58
|
+
fileNameTransform,
|
|
59
|
+
appendUUID,
|
|
60
|
+
preserveExtension = true
|
|
61
|
+
} = config;
|
|
45
62
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
46
63
|
throw new Error(
|
|
47
64
|
"apiEndpoint is required for S3 upload. Please provide a valid API endpoint."
|
|
@@ -53,9 +70,30 @@ var createS3Uploader = (config) => {
|
|
|
53
70
|
if (!path || path.trim() === "") {
|
|
54
71
|
throw new Error("path is required and cannot be empty.");
|
|
55
72
|
}
|
|
73
|
+
const appendUUIDToFileName = (filename) => {
|
|
74
|
+
const lastDotIndex = filename.lastIndexOf(".");
|
|
75
|
+
if (lastDotIndex === -1) {
|
|
76
|
+
return `${filename}_${generateUUID()}`;
|
|
77
|
+
}
|
|
78
|
+
const name = filename.substring(0, lastDotIndex);
|
|
79
|
+
const ext = filename.substring(lastDotIndex);
|
|
80
|
+
return `${name}_${generateUUID()}${ext}`;
|
|
81
|
+
};
|
|
56
82
|
const generateHierarchicalFileName = (file) => {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
83
|
+
const originalName = file.name;
|
|
84
|
+
const lastDotIndex = originalName.lastIndexOf(".");
|
|
85
|
+
const nameWithoutExt = lastDotIndex === -1 ? originalName : originalName.substring(0, lastDotIndex);
|
|
86
|
+
const extension = lastDotIndex === -1 ? "" : originalName.substring(lastDotIndex);
|
|
87
|
+
let filename = nameWithoutExt;
|
|
88
|
+
if (fileNameTransform) {
|
|
89
|
+
filename = fileNameTransform(filename, file);
|
|
90
|
+
}
|
|
91
|
+
if (appendUUID) {
|
|
92
|
+
filename = `${filename}_${generateUUID()}`;
|
|
93
|
+
}
|
|
94
|
+
if (preserveExtension) {
|
|
95
|
+
filename = `${filename}${extension}`;
|
|
96
|
+
}
|
|
59
97
|
return `${env}/${path}/${filename}`;
|
|
60
98
|
};
|
|
61
99
|
return async (file) => {
|
|
@@ -271,9 +309,30 @@ function LumirEditor({
|
|
|
271
309
|
allowFileUpload
|
|
272
310
|
);
|
|
273
311
|
}, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
|
|
312
|
+
const fileNameTransformRef = (0, import_react.useRef)(s3Upload?.fileNameTransform);
|
|
313
|
+
(0, import_react.useEffect)(() => {
|
|
314
|
+
fileNameTransformRef.current = s3Upload?.fileNameTransform;
|
|
315
|
+
}, [s3Upload?.fileNameTransform]);
|
|
274
316
|
const memoizedS3Upload = (0, import_react.useMemo)(() => {
|
|
275
|
-
return
|
|
276
|
-
|
|
317
|
+
if (!s3Upload) return void 0;
|
|
318
|
+
return {
|
|
319
|
+
apiEndpoint: s3Upload.apiEndpoint,
|
|
320
|
+
env: s3Upload.env,
|
|
321
|
+
path: s3Upload.path,
|
|
322
|
+
appendUUID: s3Upload.appendUUID,
|
|
323
|
+
preserveExtension: s3Upload.preserveExtension,
|
|
324
|
+
// 최신 콜백을 항상 사용하도록 ref를 통해 접근
|
|
325
|
+
fileNameTransform: (originalName, file) => {
|
|
326
|
+
return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}, [
|
|
330
|
+
s3Upload?.apiEndpoint,
|
|
331
|
+
s3Upload?.env,
|
|
332
|
+
s3Upload?.path,
|
|
333
|
+
s3Upload?.appendUUID,
|
|
334
|
+
s3Upload?.preserveExtension
|
|
335
|
+
]);
|
|
277
336
|
const editor = (0, import_react2.useCreateBlockNote)(
|
|
278
337
|
{
|
|
279
338
|
initialContent: validatedContent,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
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, useRef } 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 // fileNameTransform 콜백을 ref로 관리 (에디터 재생성 방지)\r\n const fileNameTransformRef = useRef(s3Upload?.fileNameTransform);\r\n useEffect(() => {\r\n fileNameTransformRef.current = s3Upload?.fileNameTransform;\r\n }, [s3Upload?.fileNameTransform]);\r\n\r\n // S3 업로드 설정 메모이제이션 (객체 참조 안정화)\r\n // 주의: fileNameTransform은 ref로 관리하므로 의존성에서 제외\r\n const memoizedS3Upload = useMemo(() => {\r\n if (!s3Upload) return undefined;\r\n return {\r\n apiEndpoint: s3Upload.apiEndpoint,\r\n env: s3Upload.env,\r\n path: s3Upload.path,\r\n appendUUID: s3Upload.appendUUID,\r\n preserveExtension: s3Upload.preserveExtension,\r\n // 최신 콜백을 항상 사용하도록 ref를 통해 접근\r\n fileNameTransform: ((originalName: string, file: File) => {\r\n return fileNameTransformRef.current\r\n ? fileNameTransformRef.current(originalName, file)\r\n : originalName;\r\n }) as ((originalName: string, file: File) => string) | undefined,\r\n };\r\n }, [\r\n s3Upload?.apiEndpoint,\r\n s3Upload?.env,\r\n s3Upload?.path,\r\n s3Upload?.appendUUID,\r\n s3Upload?.preserveExtension,\r\n ]);\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 fileNameTransform?: (nameWithoutExt: string, file: File) => string;\r\n /** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */\r\n appendUUID?: boolean;\r\n /** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */\r\n preserveExtension?: boolean;\r\n}\r\n\r\n// UUID 생성 함수 (crypto.randomUUID 또는 폴백)\r\nconst generateUUID = (): string => {\r\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n // 폴백: 간단한 UUID v4 형식 생성\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n};\r\n\r\nexport const createS3Uploader = (config: S3UploaderConfig) => {\r\n const {\r\n apiEndpoint,\r\n env,\r\n path,\r\n fileNameTransform,\r\n appendUUID,\r\n preserveExtension = true,\r\n } = 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 // 파일명에 UUID 추가하는 함수\r\n const appendUUIDToFileName = (filename: string): string => {\r\n const lastDotIndex = filename.lastIndexOf(\".\");\r\n if (lastDotIndex === -1) {\r\n // 확장자가 없는 경우\r\n return `${filename}_${generateUUID()}`;\r\n }\r\n const name = filename.substring(0, lastDotIndex);\r\n const ext = filename.substring(lastDotIndex);\r\n return `${name}_${generateUUID()}${ext}`;\r\n };\r\n\r\n // 계층 구조 파일명 생성 함수\r\n const generateHierarchicalFileName = (file: File): string => {\r\n // 0. 확장자 분리\r\n const originalName = file.name;\r\n const lastDotIndex = originalName.lastIndexOf(\".\");\r\n const nameWithoutExt =\r\n lastDotIndex === -1\r\n ? originalName\r\n : originalName.substring(0, lastDotIndex);\r\n const extension =\r\n lastDotIndex === -1 ? \"\" : originalName.substring(lastDotIndex);\r\n\r\n let filename = nameWithoutExt;\r\n\r\n // 1. 사용자 정의 파일명 변환 콜백 적용 (확장자 제외한 이름만)\r\n if (fileNameTransform) {\r\n filename = fileNameTransform(filename, file);\r\n }\r\n\r\n // 2. UUID 자동 추가 (appendUUID가 true인 경우)\r\n if (appendUUID) {\r\n filename = `${filename}_${generateUUID()}`;\r\n }\r\n\r\n // 3. 확장자 다시 붙이기 (preserveExtension이 true인 경우만)\r\n if (preserveExtension) {\r\n filename = `${filename}${extension}`;\r\n }\r\n\r\n // {env}/{path}/{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"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/components/LumirEditor.tsx
|
|
4
|
-
import { useEffect, useMemo, useCallback, useState } from "react";
|
|
4
|
+
import { useEffect, useMemo, useCallback, useState, useRef } from "react";
|
|
5
5
|
import {
|
|
6
6
|
useCreateBlockNote,
|
|
7
7
|
SideMenu as BlockSideMenu,
|
|
@@ -18,8 +18,25 @@ function cn(...inputs) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// src/utils/s3-uploader.ts
|
|
21
|
+
var generateUUID = () => {
|
|
22
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
23
|
+
return crypto.randomUUID();
|
|
24
|
+
}
|
|
25
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
26
|
+
const r = Math.random() * 16 | 0;
|
|
27
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
28
|
+
return v.toString(16);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
21
31
|
var createS3Uploader = (config) => {
|
|
22
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
apiEndpoint,
|
|
34
|
+
env,
|
|
35
|
+
path,
|
|
36
|
+
fileNameTransform,
|
|
37
|
+
appendUUID,
|
|
38
|
+
preserveExtension = true
|
|
39
|
+
} = config;
|
|
23
40
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
24
41
|
throw new Error(
|
|
25
42
|
"apiEndpoint is required for S3 upload. Please provide a valid API endpoint."
|
|
@@ -31,9 +48,30 @@ var createS3Uploader = (config) => {
|
|
|
31
48
|
if (!path || path.trim() === "") {
|
|
32
49
|
throw new Error("path is required and cannot be empty.");
|
|
33
50
|
}
|
|
51
|
+
const appendUUIDToFileName = (filename) => {
|
|
52
|
+
const lastDotIndex = filename.lastIndexOf(".");
|
|
53
|
+
if (lastDotIndex === -1) {
|
|
54
|
+
return `${filename}_${generateUUID()}`;
|
|
55
|
+
}
|
|
56
|
+
const name = filename.substring(0, lastDotIndex);
|
|
57
|
+
const ext = filename.substring(lastDotIndex);
|
|
58
|
+
return `${name}_${generateUUID()}${ext}`;
|
|
59
|
+
};
|
|
34
60
|
const generateHierarchicalFileName = (file) => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
61
|
+
const originalName = file.name;
|
|
62
|
+
const lastDotIndex = originalName.lastIndexOf(".");
|
|
63
|
+
const nameWithoutExt = lastDotIndex === -1 ? originalName : originalName.substring(0, lastDotIndex);
|
|
64
|
+
const extension = lastDotIndex === -1 ? "" : originalName.substring(lastDotIndex);
|
|
65
|
+
let filename = nameWithoutExt;
|
|
66
|
+
if (fileNameTransform) {
|
|
67
|
+
filename = fileNameTransform(filename, file);
|
|
68
|
+
}
|
|
69
|
+
if (appendUUID) {
|
|
70
|
+
filename = `${filename}_${generateUUID()}`;
|
|
71
|
+
}
|
|
72
|
+
if (preserveExtension) {
|
|
73
|
+
filename = `${filename}${extension}`;
|
|
74
|
+
}
|
|
37
75
|
return `${env}/${path}/${filename}`;
|
|
38
76
|
};
|
|
39
77
|
return async (file) => {
|
|
@@ -249,9 +287,30 @@ function LumirEditor({
|
|
|
249
287
|
allowFileUpload
|
|
250
288
|
);
|
|
251
289
|
}, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
|
|
290
|
+
const fileNameTransformRef = useRef(s3Upload?.fileNameTransform);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
fileNameTransformRef.current = s3Upload?.fileNameTransform;
|
|
293
|
+
}, [s3Upload?.fileNameTransform]);
|
|
252
294
|
const memoizedS3Upload = useMemo(() => {
|
|
253
|
-
return
|
|
254
|
-
|
|
295
|
+
if (!s3Upload) return void 0;
|
|
296
|
+
return {
|
|
297
|
+
apiEndpoint: s3Upload.apiEndpoint,
|
|
298
|
+
env: s3Upload.env,
|
|
299
|
+
path: s3Upload.path,
|
|
300
|
+
appendUUID: s3Upload.appendUUID,
|
|
301
|
+
preserveExtension: s3Upload.preserveExtension,
|
|
302
|
+
// 최신 콜백을 항상 사용하도록 ref를 통해 접근
|
|
303
|
+
fileNameTransform: (originalName, file) => {
|
|
304
|
+
return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}, [
|
|
308
|
+
s3Upload?.apiEndpoint,
|
|
309
|
+
s3Upload?.env,
|
|
310
|
+
s3Upload?.path,
|
|
311
|
+
s3Upload?.appendUUID,
|
|
312
|
+
s3Upload?.preserveExtension
|
|
313
|
+
]);
|
|
255
314
|
const editor = useCreateBlockNote(
|
|
256
315
|
{
|
|
257
316
|
initialContent: validatedContent,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/LumirEditor.tsx","../src/utils/cn.ts","../src/utils/s3-uploader.ts"],"sourcesContent":["\"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":["editor"]}
|
|
1
|
+
{"version":3,"sources":["../src/components/LumirEditor.tsx","../src/utils/cn.ts","../src/utils/s3-uploader.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport { useEffect, useMemo, useCallback, useState, useRef } 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 // fileNameTransform 콜백을 ref로 관리 (에디터 재생성 방지)\r\n const fileNameTransformRef = useRef(s3Upload?.fileNameTransform);\r\n useEffect(() => {\r\n fileNameTransformRef.current = s3Upload?.fileNameTransform;\r\n }, [s3Upload?.fileNameTransform]);\r\n\r\n // S3 업로드 설정 메모이제이션 (객체 참조 안정화)\r\n // 주의: fileNameTransform은 ref로 관리하므로 의존성에서 제외\r\n const memoizedS3Upload = useMemo(() => {\r\n if (!s3Upload) return undefined;\r\n return {\r\n apiEndpoint: s3Upload.apiEndpoint,\r\n env: s3Upload.env,\r\n path: s3Upload.path,\r\n appendUUID: s3Upload.appendUUID,\r\n preserveExtension: s3Upload.preserveExtension,\r\n // 최신 콜백을 항상 사용하도록 ref를 통해 접근\r\n fileNameTransform: ((originalName: string, file: File) => {\r\n return fileNameTransformRef.current\r\n ? fileNameTransformRef.current(originalName, file)\r\n : originalName;\r\n }) as ((originalName: string, file: File) => string) | undefined,\r\n };\r\n }, [\r\n s3Upload?.apiEndpoint,\r\n s3Upload?.env,\r\n s3Upload?.path,\r\n s3Upload?.appendUUID,\r\n s3Upload?.preserveExtension,\r\n ]);\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 fileNameTransform?: (nameWithoutExt: string, file: File) => string;\r\n /** true일 경우 파일명 뒤에 UUID를 자동으로 추가합니다 (예: image_abc123.png) */\r\n appendUUID?: boolean;\r\n /** false로 설정하면 확장자를 자동으로 붙이지 않음 (기본: true) */\r\n preserveExtension?: boolean;\r\n}\r\n\r\n// UUID 생성 함수 (crypto.randomUUID 또는 폴백)\r\nconst generateUUID = (): string => {\r\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\r\n return crypto.randomUUID();\r\n }\r\n // 폴백: 간단한 UUID v4 형식 생성\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = (Math.random() * 16) | 0;\r\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\r\n return v.toString(16);\r\n });\r\n};\r\n\r\nexport const createS3Uploader = (config: S3UploaderConfig) => {\r\n const {\r\n apiEndpoint,\r\n env,\r\n path,\r\n fileNameTransform,\r\n appendUUID,\r\n preserveExtension = true,\r\n } = 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 // 파일명에 UUID 추가하는 함수\r\n const appendUUIDToFileName = (filename: string): string => {\r\n const lastDotIndex = filename.lastIndexOf(\".\");\r\n if (lastDotIndex === -1) {\r\n // 확장자가 없는 경우\r\n return `${filename}_${generateUUID()}`;\r\n }\r\n const name = filename.substring(0, lastDotIndex);\r\n const ext = filename.substring(lastDotIndex);\r\n return `${name}_${generateUUID()}${ext}`;\r\n };\r\n\r\n // 계층 구조 파일명 생성 함수\r\n const generateHierarchicalFileName = (file: File): string => {\r\n // 0. 확장자 분리\r\n const originalName = file.name;\r\n const lastDotIndex = originalName.lastIndexOf(\".\");\r\n const nameWithoutExt =\r\n lastDotIndex === -1\r\n ? originalName\r\n : originalName.substring(0, lastDotIndex);\r\n const extension =\r\n lastDotIndex === -1 ? \"\" : originalName.substring(lastDotIndex);\r\n\r\n let filename = nameWithoutExt;\r\n\r\n // 1. 사용자 정의 파일명 변환 콜백 적용 (확장자 제외한 이름만)\r\n if (fileNameTransform) {\r\n filename = fileNameTransform(filename, file);\r\n }\r\n\r\n // 2. UUID 자동 추가 (appendUUID가 true인 경우)\r\n if (appendUUID) {\r\n filename = `${filename}_${generateUUID()}`;\r\n }\r\n\r\n // 3. 확장자 다시 붙이기 (preserveExtension이 true인 경우만)\r\n if (preserveExtension) {\r\n filename = `${filename}${extension}`;\r\n }\r\n\r\n // {env}/{path}/{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":";;;AAEA,SAAS,WAAW,SAAS,aAAa,UAAU,cAAc;AAClE;AAAA,EACE;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qBAAqB;;;ACRvB,SAAS,MAAM,QAA+C;AACnE,SAAO,OAAO,OAAO,OAAO,EAAE,KAAK,GAAG;AACxC;;;ACQA,IAAM,eAAe,MAAc;AACjC,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,UAAM,IAAI,MAAM,MAAM,IAAK,IAAI,IAAO;AACtC,WAAO,EAAE,SAAS,EAAE;AAAA,EACtB,CAAC;AACH;AAEO,IAAM,mBAAmB,CAAC,WAA6B;AAC5D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,EACtB,IAAI;AAGJ,MAAI,CAAC,eAAe,YAAY,KAAK,MAAM,IAAI;AAC7C,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AAEA,MAAI,CAAC,QAAQ,KAAK,KAAK,MAAM,IAAI;AAC/B,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AAGA,QAAM,uBAAuB,CAAC,aAA6B;AACzD,UAAM,eAAe,SAAS,YAAY,GAAG;AAC7C,QAAI,iBAAiB,IAAI;AAEvB,aAAO,GAAG,QAAQ,IAAI,aAAa,CAAC;AAAA,IACtC;AACA,UAAM,OAAO,SAAS,UAAU,GAAG,YAAY;AAC/C,UAAM,MAAM,SAAS,UAAU,YAAY;AAC3C,WAAO,GAAG,IAAI,IAAI,aAAa,CAAC,GAAG,GAAG;AAAA,EACxC;AAGA,QAAM,+BAA+B,CAAC,SAAuB;AAE3D,UAAM,eAAe,KAAK;AAC1B,UAAM,eAAe,aAAa,YAAY,GAAG;AACjD,UAAM,iBACJ,iBAAiB,KACb,eACA,aAAa,UAAU,GAAG,YAAY;AAC5C,UAAM,YACJ,iBAAiB,KAAK,KAAK,aAAa,UAAU,YAAY;AAEhE,QAAI,WAAW;AAGf,QAAI,mBAAmB;AACrB,iBAAW,kBAAkB,UAAU,IAAI;AAAA,IAC7C;AAGA,QAAI,YAAY;AACd,iBAAW,GAAG,QAAQ,IAAI,aAAa,CAAC;AAAA,IAC1C;AAGA,QAAI,mBAAmB;AACrB,iBAAW,GAAG,QAAQ,GAAG,SAAS;AAAA,IACpC;AAGA,WAAO,GAAG,GAAG,IAAI,IAAI,IAAI,QAAQ;AAAA,EACnC;AAEA,SAAO,OAAO,SAAgC;AAC5C,QAAI;AAEF,UAAI,CAAC,eAAe,YAAY,KAAK,MAAM,IAAI;AAC7C,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AAGA,YAAM,WAAW,6BAA6B,IAAI;AAGlD,YAAM,WAAW,MAAM;AAAA,QACrB,GAAG,WAAW,QAAQ,mBAAmB,QAAQ,CAAC;AAAA,MACpD;AAEA,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAa,MAAM,SAAS,KAAK,KAAM;AAC7C,cAAM,IAAI;AAAA,UACR,gCAAgC,SAAS,UAAU,KAAK,SAAS;AAAA,QACnE;AAAA,MACF;AAEA,YAAM,eAAe,MAAM,SAAS,KAAK;AACzC,YAAM,EAAE,cAAc,UAAU,IAAI;AAGpC,YAAM,iBAAiB,MAAM,MAAM,cAAc;AAAA,QAC/C,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB,KAAK,QAAQ;AAAA,QAC/B;AAAA,QACA,MAAM;AAAA,MACR,CAAC;AAED,UAAI,CAAC,eAAe,IAAI;AACtB,cAAM,IAAI,MAAM,0BAA0B,eAAe,UAAU,EAAE;AAAA,MACvE;AAGA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,MAAM,qBAAqB,KAAK;AACxC,YAAM;AAAA,IACR;AAAA,EACF;AACF;;;AF2VQ,cAUF,YAVE;AAzcD,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,OAAO,kBAAkB,YAA6B;AACpD,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,UAAU;AACpC,aAAO,MAAM,QAAQ,MAAM;AAAA,IAC7B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,iBAAiB,YAAkD;AACxE,QAAI;AACF,YAAM,SAAS,KAAK,MAAM,UAAU;AACpC,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,qBAA0C;AAC/C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,OAAO;AAAA,QACL,WAAW;AAAA,QACX,iBAAiB;AAAA,QACjB,eAAe;AAAA,MACjB;AAAA,MACA,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,IAAI,QAAQ,CAAC,EAAE,CAAC;AAAA,MAChD,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,gBACL,SACA,kBAA0B,GACH;AAEvB,QAAI,OAAO,YAAY,UAAU;AAC/B,UAAI,QAAQ,KAAK,MAAM,IAAI;AACzB,eAAO,KAAK,kBAAkB,eAAe;AAAA,MAC/C;AAEA,YAAM,gBAAgB,KAAK,iBAAiB,OAAO;AACnD,UAAI,iBAAiB,cAAc,SAAS,GAAG;AAC7C,eAAO;AAAA,MACT;AAGA,aAAO,KAAK,kBAAkB,eAAe;AAAA,IAC/C;AAGA,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG;AACpC,aAAO,KAAK,kBAAkB,eAAe;AAAA,IAC/C;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,kBACb,iBACuB;AACvB,WAAO,MAAM;AAAA,MAAK,EAAE,QAAQ,gBAAgB;AAAA,MAAG,MAC7C,KAAK,mBAAmB;AAAA,IAC1B;AAAA,EACF;AACF;AAMO,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxB,OAAO,sBAAsB,YAAyC;AACpE,WAAO;AAAA,MACL,YAAY,YAAY,cAAc;AAAA,MACtC,qBAAqB,YAAY,uBAAuB;AAAA,MACxD,eAAe,YAAY,iBAAiB;AAAA,MAC5C,SAAS,YAAY,WAAW;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,wBAAwB,aAA2C;AACxE,WAAO,aAAa,UAAU,YAAY,OAAO,SAAS,IACtD,cACA,EAAE,QAAQ,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,EAA+B;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OAAO,sBACL,gBACA,aAAa,OACb,aAAa,OACb,YAAY,OACF;AACV,UAAM,MAAM,IAAI,IAAY,kBAAkB,CAAC,CAAC;AAChD,QAAI,CAAC,WAAY,KAAI,IAAI,OAAO;AAChC,QAAI,CAAC,WAAY,KAAI,IAAI,OAAO;AAChC,QAAI,CAAC,UAAW,KAAI,IAAI,MAAM;AAC9B,WAAO,MAAM,KAAK,GAAG;AAAA,EACvB;AACF;AAGA,IAAM,cAAc,CAAC,SAAwB;AAC3C,SACE,KAAK,OAAO,MACX,KAAK,MAAM,WAAW,QAAQ,KAC5B,CAAC,KAAK,QAAQ,mCAAmC,KAAK,KAAK,QAAQ,EAAE;AAE5E;AAEe,SAAR,YAA6B;AAAA;AAAA,EAElC;AAAA,EACA,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,kBAAkB;AAAA;AAAA,EAElB,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,oBAAoB;AAAA,EACpB,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,eAAe;AAAA,EACf;AAAA,EACA,YAAY;AAAA,EACZ,oBAAoB;AAAA;AAAA,EAEpB;AACF,GAAqB;AAEnB,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AACpD,QAAM,mBAAmB,QAA+B,MAAM;AAC5D,WAAO,aAAa,gBAAgB,gBAAgB,kBAAkB;AAAA,EACxE,GAAG,CAAC,gBAAgB,kBAAkB,CAAC;AAGvC,QAAM,cAAc,QAAQ,MAAM;AAChC,WAAO,aAAa,sBAAsB,MAAM;AAAA,EAClD,GAAG;AAAA,IACD,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC;AAGD,QAAM,gBAAgB,QAAQ,MAAM;AAClC,WAAO,aAAa,wBAAwB,OAAO;AAAA,EACrD,GAAG,CAAC,SAAS,QAAQ,KAAK,GAAG,KAAK,EAAE,CAAC;AAGrC,QAAM,qBAAqB,QAAQ,MAAM;AACvC,WAAO,aAAa;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,mBAAmB,kBAAkB,kBAAkB,eAAe,CAAC;AAG3E,QAAM,uBAAuB,OAAO,UAAU,iBAAiB;AAC/D,YAAU,MAAM;AACd,yBAAqB,UAAU,UAAU;AAAA,EAC3C,GAAG,CAAC,UAAU,iBAAiB,CAAC;AAIhC,QAAM,mBAAmB,QAAQ,MAAM;AACrC,QAAI,CAAC,SAAU,QAAO;AACtB,WAAO;AAAA,MACL,aAAa,SAAS;AAAA,MACtB,KAAK,SAAS;AAAA,MACd,MAAM,SAAS;AAAA,MACf,YAAY,SAAS;AAAA,MACrB,mBAAmB,SAAS;AAAA;AAAA,MAE5B,mBAAoB,CAAC,cAAsB,SAAe;AACxD,eAAO,qBAAqB,UACxB,qBAAqB,QAAQ,cAAc,IAAI,IAC/C;AAAA,MACN;AAAA,IACF;AAAA,EACF,GAAG;AAAA,IACD,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAED,QAAM,SAAS;AAAA,IAKb;AAAA,MACE,gBAAgB;AAAA,MAChB,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA;AAAA,MACZ;AAAA;AAAA,MAEA,mBAAmB;AAAA,MACnB;AAAA,MACA;AAAA,MACA,YAAY,OAAO,SAAS;AAE1B,YAAI,CAAC,YAAY,IAAI,GAAG;AACtB,gBAAM,IAAI,MAAM,8BAA8B;AAAA,QAChD;AAEA,YAAI;AACF,cAAI;AAGJ,cAAI,YAAY;AACd,uBAAW,MAAM,WAAW,IAAI;AAAA,UAClC,WAES,kBAAkB,aAAa;AACtC,kBAAM,aAAa,iBAAiB,gBAAgB;AACpD,uBAAW,MAAM,WAAW,IAAI;AAAA,UAClC,OAEK;AACH,kBAAM,IAAI,MAAM,4BAA4B;AAAA,UAC9C;AAGA,iBAAO;AAAA,QACT,SAAS,OAAO;AACd,kBAAQ,MAAM,wBAAwB,KAAK;AAC3C,gBAAM,IAAI;AAAA,YACR,qBACG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UAC1D;AAAA,QACF;AAAA,MACF;AAAA,MACA,cAAc,CAAC,QAAQ;AACrB,cAAM,EAAE,OAAO,QAAAA,SAAQ,oBAAoB,IAAI;AAC/C,cAAM,WACH,OAAO,eAAe,SAA6B;AACtD,cAAM,QAAgB,WAAW,MAAM,KAAK,QAAQ,IAAI,CAAC;AACzD,cAAM,gBAAwB,MAAM,OAAO,WAAW;AAGtD,YAAI,MAAM,SAAS,KAAK,cAAc,WAAW,GAAG;AAClD,gBAAM,eAAe;AACrB,iBAAO;AAAA,QACT;AAGA,YAAI,cAAc,WAAW,GAAG;AAC9B,iBAAO,oBAAoB,KAAK;AAAA,QAClC;AAEA,cAAM,eAAe;AACrB,SAAC,YAAY;AAEX,yBAAe,IAAI;AACnB,cAAI;AACF,uBAAW,QAAQ,eAAe;AAChC,kBAAI;AAEF,sBAAM,MAAM,MAAMA,QAAO,WAAW,IAAI;AACxC,gBAAAA,QAAO,UAAU,aAAa,GAAG,kBAAkB;AAAA,cACrD,SAAS,KAAK;AACZ,wBAAQ;AAAA,kBACN;AAAA,kBACA,KAAK,QAAQ;AAAA,kBACb;AAAA,gBACF;AAAA,cACF;AAAA,YACF;AAAA,UACF,UAAE;AACA,2BAAe,KAAK;AAAA,UACtB;AAAA,QACF,GAAG;AACH,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,YAAU,MAAM;AACd,QAAI,QAAQ;AACV,aAAO,aAAa;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAGrB,YAAU,MAAM;AACd,QAAI,CAAC,UAAU,CAAC,gBAAiB;AAEjC,UAAM,sBAAsB,MAAM;AAEhC,YAAM,SAAS,OAAO;AACtB,sBAAgB,MAAM;AAAA,IACxB;AAEA,WAAO,OAAO,sBAAsB,mBAAmB;AAAA,EACzD,GAAG,CAAC,QAAQ,eAAe,CAAC;AAG5B,YAAU,MAAM;AACd,UAAM,KAAK,QAAQ;AACnB,QAAI,CAAC,GAAI;AAET,UAAM,iBAAiB,CAAC,MAAiB;AACvC,UAAI,EAAE,iBAAkB;AACxB,YAAM,WACJ,EAAE,cAAc,OACf,WAAW,OAAO;AACrB,UAAI,UAAU;AACZ,UAAE,eAAe;AACjB,UAAE,gBAAgB;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,aAAa,CAAC,MAAiB;AACnC,UAAI,CAAC,EAAE,aAAc;AACrB,YAAM,YACH,EAAE,aAAa,SAA6C,CAAC,GAC9D,SAAS,OAAO;AAClB,UAAI,CAAC,SAAU;AAEf,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAElB,YAAM,QAAQ,MAAM,KAAK,EAAE,aAAa,SAAS,CAAC,CAAC;AACnD,YAAM,QAAQ,MACX,OAAO,CAAC,OAAO,GAAG,SAAS,MAAM,EACjC,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC,EAC1B,OAAO,CAAC,MAAiB,CAAC,CAAC,CAAC;AAG/B,YAAM,gBAAgB,MAAM,OAAO,WAAW;AAE9C,UAAI,cAAc,WAAW,EAAG;AAEhC,OAAC,YAAY;AAEX,uBAAe,IAAI;AACnB,YAAI;AACF,qBAAW,QAAQ,eAAe;AAChC,gBAAI;AAEF,kBAAI,QAAQ,YAAY;AACtB,sBAAM,MAAM,MAAM,OAAO,WAAW,IAAI;AACxC,oBAAI,KAAK;AACP,yBAAO,UAAU,aAAa,GAAG,kBAAkB;AAAA,gBACrD;AAAA,cACF;AAAA,YACF,SAAS,KAAK;AACZ,sBAAQ;AAAA,gBACN;AAAA,gBACA,KAAK,QAAQ;AAAA,gBACb;AAAA,cACF;AAAA,YACF;AAAA,UACF;AAAA,QACF,UAAE;AACA,yBAAe,KAAK;AAAA,QACtB;AAAA,MACF,GAAG;AAAA,IACL;AAEA,OAAG,iBAAiB,YAAY,gBAAgB,EAAE,SAAS,KAAK,CAAC;AACjE,OAAG,iBAAiB,QAAQ,YAAY,EAAE,SAAS,KAAK,CAAC;AAEzD,WAAO,MAAM;AACX,SAAG,oBAAoB,YAAY,gBAAgB;AAAA,QACjD,SAAS;AAAA,MACX,CAAQ;AACR,SAAG,oBAAoB,QAAQ,YAAY,EAAE,SAAS,KAAK,CAAQ;AAAA,IACrE;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,QAAM,mBAAmB,QAAQ,MAAM;AACrC,WAAO,oBAAoB,WAAW;AAAA,EACxC,GAAG,CAAC,mBAAmB,QAAQ,CAAC;AAGhC,QAAM,yBAAyB,QAAQ,MAAM;AAC3C,WAAO,CAAC,UACN,oBAAC,iBAAe,GAAG,OACjB,8BAAC,oBAAkB,GAAG,OAAO,GAC/B;AAAA,EAEJ,GAAG,CAAC,CAAC;AAEL,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,eAAe,SAAS;AAAA,MACtC,OAAO,EAAE,UAAU,WAAW;AAAA,MAE9B;AAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,UAAU;AAAA,YACV,WAAW;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YAGE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,kBAAiB;AAAA,kBACjB,UAAU;AAAA,oBACR,OAAO,UAAkB;AACvB,4BAAM,QAAQ,8BAA8B,MAAM;AAElD,4BAAM,WAAW,MAAM,OAAO,CAAC,SAAc;AAC3C,8BAAM,OAAO,MAAM,OAAO,IAAI,SAAS,EAAE,YAAY;AACrD,8BAAM,SAAS,MAAM,SAAS,IAAI,SAAS,EAAE,YAAY;AAEzD,4BAAI,CAAC,SAAS,SAAS,MAAM,EAAE,SAAS,GAAG,EAAG,QAAO;AACrD,4BACE,MAAM,SAAS,OAAO,KACtB,MAAM,SAAS,OAAO,KACtB,MAAM,SAAS,MAAM;AAErB,iCAAO;AACT,+BAAO;AAAA,sBACT,CAAC;AAED,0BAAI,CAAC,MAAO,QAAO;AACnB,4BAAM,IAAI,MAAM,YAAY;AAC5B,6BAAO,SAAS;AAAA,wBACd,CAAC,SACC,KAAK,OAAO,YAAY,EAAE,SAAS,CAAC,MACnC,KAAK,WAAW,CAAC,GAAG;AAAA,0BAAK,CAAC,MACzB,EAAE,YAAY,EAAE,SAAS,CAAC;AAAA,wBAC5B;AAAA,sBACJ;AAAA,oBACF;AAAA,oBACA,CAAC,MAAM;AAAA,kBACT;AAAA;AAAA,cACF;AAAA,cAED,CAAC,qBACA,oBAAC,sBAAmB,UAAU,wBAAwB;AAAA;AAAA;AAAA,QAE1D;AAAA,QAGC,eACC,oBAAC,SAAI,WAAU,8BACb,8BAAC,SAAI,WAAU,uBAAsB,GACvC;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":["editor"]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumir-company/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Image-only BlockNote rich text editor with S3 upload and
|
|
5
|
+
"description": "Image-only BlockNote rich text editor with S3 upload, customizable filename transforms, UUID support, and loading spinner",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"editor",
|
|
8
8
|
"blocknote",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"wysiwyg",
|
|
14
14
|
"s3-upload",
|
|
15
15
|
"loading-spinner",
|
|
16
|
+
"filename-transform",
|
|
17
|
+
"uuid",
|
|
18
|
+
"file-upload",
|
|
16
19
|
"lumir"
|
|
17
20
|
],
|
|
18
21
|
"main": "dist/index.js",
|