UNPKG

@kingdoo/editor

Version:

Advanced BlockNote-based React rich text editor with hybrid content support and flexible styling

926 lines (739 loc) 25.2 kB
# LumirEditor BlockNote 기반의 고급 Rich Text 에디터 React 컴포넌트 ## ✨ 주요 특징 - 🚀 **하이브리드 콘텐츠 지원**: JSON 객체 배열 또는 JSON 문자열 모두 지원 - 📷 **이미지 처리**: 업로드/붙여넣기/드래그앤드롭 완벽 지원 - 🎨 **유연한 스타일링**: Tailwind CSS 클래스와 커스텀 CSS 모두 지원 - 📱 **반응형 UI**: 모든 툴바와 메뉴 개별 제어 가능 - 🔧 **TypeScript 완벽 지원**: 모든 타입 정의 포함 - ⚡ **최적화된 성능**: 스마트 렌더링과 메모리 관리 ## 📦 설치 및 초기 세팅 ### 1. 패키지 설치 ```bash npm install @kingdoo/editor # 또는 yarn add @kingdoo/editor # 또는 pnpm add @kingdoo/editor ``` ### 2. 필수 CSS 임포트 에디터가 제대로 작동하려면 반드시 CSS 파일을 임포트해야 합니다: ```tsx // App.tsx 또는 main.tsx에서 import "@kingdoo/editor/style.css"; ``` **또는 개별 CSS 임포트:** ```tsx import "@blocknote/core/fonts/inter.css"; import "@blocknote/mantine/style.css"; import "@blocknote/react/style.css"; ``` ### 3. TypeScript 설정 (권장) `tsconfig.json`에서 모듈 해석 설정: ```json { "compilerOptions": { "moduleResolution": "node", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "jsx": "react-jsx", "lib": ["dom", "dom.iterable", "es6"] } } ``` ### 4. Tailwind CSS 설정 (선택사항) 패키지의 Tailwind 클래스를 사용하려면 `tailwind.config.js`에 추가: ```js module.exports = { content: [ "./src/**/*.{js,ts,jsx,tsx}", // 기존 경로들 "./node_modules/@kingdoo/editor/dist/**/*.js", // 패키지 경로 추가 ], theme: { extend: {}, }, plugins: [], }; ``` ### 5. 번들러별 설정 #### Next.js ```tsx // next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { transpilePackages: ["@kingdoo/editor"], experimental: { esmExternals: true, }, }; module.exports = nextConfig; ``` #### Vite ```ts // vite.config.ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], optimizeDeps: { include: ["@kingdoo/editor"], }, }); ``` #### Webpack ```js // webpack.config.js module.exports = { resolve: { alias: { // BlockNote 관련 폴리필이 필요한 경우 crypto: "crypto-browserify", stream: "stream-browserify", }, }, }; ``` ## 🚀 사용법 ### 기본 사용법 ```tsx import { LumirEditor } from "@kingdoo/editor"; import "@kingdoo/editor/style.css"; export default function App() { return ( <LumirEditor initialContent="빈 상태에서 시작" onContentChange={(blocks) => { console.log("변경된 내용:", blocks); }} /> ); } ``` ### Next.js에서 사용 ```tsx "use client"; import dynamic from "next/dynamic"; const LumirEditor = dynamic( () => import("@kingdoo/editor").then((m) => m.LumirEditor), { ssr: false } ); export default function EditorPage() { return ( <div className="container mx-auto p-4"> <LumirEditor initialContent={[ { type: "paragraph", content: [{ type: "text", text: "안녕하세요!" }], }, ]} onContentChange={(blocks) => saveDocument(blocks)} uploadFile={async (file) => { // 파일 업로드 로직 const url = await uploadToServer(file); return url; }} theme="light" className="min-h-[400px] rounded-lg border" /> </div> ); } ``` ### 고급 설정 예시 ```tsx <LumirEditor // 콘텐츠 설정 initialContent='[{"type":"paragraph","content":[{"type":"text","text":"JSON 문자열도 지원"}]}]' placeholder="여기에 내용을 입력하세요..." initialEmptyBlocks={5} // 파일 업로드 uploadFile={async (file) => await uploadToS3(file)} storeImagesAsBase64={false} allowVideoUpload={true} allowAudioUpload={true} // UI 커스터마이징 theme="dark" formattingToolbar={true} sideMenuAddButton={false} // Add 버튼 숨기고 드래그만 className="min-h-[600px] rounded-xl shadow-lg" // 이벤트 핸들러 onContentChange={(blocks) => { autoSave(JSON.stringify(blocks)); }} onSelectionChange={() => updateToolbar()} /> ``` ## 📚 Props API ### 📝 콘텐츠 관련 | Prop | 타입 | 기본값 | 설명 | | -------------------- | ------------------------------------------ | ----------- | --------------------------------------------- | | `initialContent` | `DefaultPartialBlock[] \| string` | `undefined` | 초기 콘텐츠 (JSON 객체 배열 또는 JSON 문자열) | | `initialEmptyBlocks` | `number` | `3` | 초기 빈 블록 개수 | | `placeholder` | `string` | `undefined` | 첫 번째 블록의 placeholder 텍스트 | | `onContentChange` | `(content: DefaultPartialBlock[]) => void` | `undefined` | 콘텐츠 변경 시 호출되는 콜백 | #### 사용 예시: ```tsx // 1. JSON 객체 배열로 초기 콘텐츠 설정 const initialBlocks = [ { type: "paragraph", props: { textColor: "default", backgroundColor: "default", textAlignment: "left" }, content: [{ type: "text", text: "환영합니다!", styles: {} }], children: [] } ]; <LumirEditor initialContent={initialBlocks} /> // 2. JSON 문자열로 설정 (API 응답, 로컬스토리지 등) const savedContent = localStorage.getItem('editorContent'); <LumirEditor initialContent={savedContent} /> // 3. Placeholder와 빈 블록 개수 설정 <LumirEditor placeholder="제목을 입력하세요..." initialEmptyBlocks={1} // 한 개의 빈 블록만 생성 /> // 4. 다양한 초기 상태 조합 <LumirEditor initialContent="" // 빈 문자열 placeholder="새 문서를 작성하세요" initialEmptyBlocks={5} // 5개의 빈 블록 생성 onContentChange={(content) => { // 실시간으로 변경사항 감지 console.log(`총 ${content.length}개 블록`); autosave(JSON.stringify(content)); }} /> ``` #### ⚠️ 중요한 사용 팁: - `initialContent`가 있으면 `placeholder`와 `initialEmptyBlocks`는 무시됩니다 - 콘텐츠 변경 시 `onContentChange`는 항상 `DefaultPartialBlock[]` 타입으로 반환됩니다 - 빈 문자열이나 잘못된 JSON은 자동으로 빈 블록으로 변환됩니다 ### 📁 파일 및 미디어 | Prop | 타입 | 기본값 | 설명 | | --------------------- | --------------------------------- | ----------- | ------------------------------------------- | | `uploadFile` | `(file: File) => Promise<string>` | `undefined` | 커스텀 파일 업로드 함수 | | `storeImagesAsBase64` | `boolean` | `true` | 폴백 이미지 저장 방식 (Base64 vs ObjectURL) | | `allowVideoUpload` | `boolean` | `false` | 비디오 업로드 허용 | | `allowAudioUpload` | `boolean` | `false` | 오디오 업로드 허용 | | `allowFileUpload` | `boolean` | `false` | 일반 파일 업로드 허용 | #### 사용 예시: ```tsx // 1. 기본 이미지 업로드 (Base64 저장) <LumirEditor /> // storeImagesAsBase64={true}가 기본값 // 2. ObjectURL 방식 (브라우저 메모리) <LumirEditor storeImagesAsBase64={false} /> // 3. 커스텀 업로드 함수 (S3, Cloudinary 등) <LumirEditor uploadFile={async (file) => { // 파일 크기 검증 if (file.size > 5 * 1024 * 1024) { throw new Error('파일 크기는 5MB 이하여야 합니다'); } // FormData로 업로드 const formData = new FormData(); formData.append("file", file); formData.append("folder", "editor-uploads"); const response = await fetch("/api/upload", { method: "POST", headers: { 'Authorization': `Bearer ${userToken}`, }, body: formData, }); if (!response.ok) { throw new Error('업로드 실패'); } const { url } = await response.json(); return url; // 반드시 접근 가능한 public URL 반환 }} // 비디오와 오디오도 허용 allowVideoUpload={true} allowAudioUpload={true} /> // 4. AWS S3 직접 업로드 예시 <LumirEditor uploadFile={async (file) => { // 1. Presigned URL 받기 const presignedResponse = await fetch('/api/s3/presigned-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: file.name, fileType: file.type, }), }); const { uploadUrl, fileUrl } = await presignedResponse.json(); // 2. S3에 직접 업로드 await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type, }, }); // 3. 공개 URL 반환 return fileUrl; }} /> ``` #### 🔧 업로드 함수 설계 가이드: **입력:** `File` 객체 **출력:** `Promise<string>` (접근 가능한 URL) ```tsx // 올바른 예시 const uploadFile = async (file: File): Promise<string> => { // 업로드 로직... return "https://cdn.example.com/uploads/image.jpg"; // ✅ 공개 URL }; // 잘못된 예시 const uploadFile = async (file: File): Promise<string> => { return "file://local/path.jpg"; // ❌ 로컬 경로 return "blob:http://localhost/temp"; // ❌ Blob URL }; ``` #### ⚠️ 중요한 업로드 팁: - `uploadFile`이 없으면 `storeImagesAsBase64` 설정에 따라 Base64 또는 ObjectURL 사용 - 업로드 실패 시 에러를 던지면 해당 파일은 삽입되지 않음 - 반환된 URL은 브라우저에서 직접 접근 가능해야 함 - 대용량 파일은 청크 업로드나 압축을 고려하세요 ### 🎛️ 에디터 기능 | Prop | 타입 | 기본값 | 설명 | | ------------------- | ----------------------------------------- | ------------------------- | -------------------- | | `tables` | `TableConfig` | `모두 true` | 표 기능 설정 | | `heading` | `{levels?: (1\|2\|3\|4\|5\|6)[]}` | `{levels: [1,2,3,4,5,6]}` | 헤딩 레벨 설정 | | `animations` | `boolean` | `true` | 블록 변환 애니메이션 | | `defaultStyles` | `boolean` | `true` | 기본 스타일 적용 | | `disableExtensions` | `string[]` | `[]` | 비활성화할 확장 기능 | | `tabBehavior` | `"prefer-navigate-ui" \| "prefer-indent"` | `"prefer-navigate-ui"` | Tab 키 동작 | | `trailingBlock` | `boolean` | `true` | 문서 끝 빈 블록 유지 | ### 🎨 UI 및 테마 | Prop | 타입 | 기본값 | 설명 | | ---------------------- | ----------------------------- | --------- | --------------------- | | `theme` | `"light" \| "dark" \| object` | `"light"` | 에디터 테마 | | `editable` | `boolean` | `true` | 편집 가능 여부 | | `className` | `string` | `""` | 커스텀 CSS 클래스 | | `includeDefaultStyles` | `boolean` | `true` | 기본 스타일 포함 여부 | ### 🛠️ 툴바 및 메뉴 | Prop | 타입 | 기본값 | 설명 | | ------------------- | --------- | ------ | ------------------------- | | `formattingToolbar` | `boolean` | `true` | 서식 툴바 표시 | | `linkToolbar` | `boolean` | `true` | 링크 툴바 표시 | | `sideMenu` | `boolean` | `true` | 사이드 메뉴 표시 | | `sideMenuAddButton` | `boolean` | `true` | 사이드 메뉴 Add 버튼 표시 | | `slashMenu` | `boolean` | `true` | 슬래시 메뉴 표시 | | `emojiPicker` | `boolean` | `true` | 이모지 피커 표시 | | `filePanel` | `boolean` | `true` | 파일 패널 표시 | | `tableHandles` | `boolean` | `true` | 표 핸들 표시 | | `comments` | `boolean` | `true` | 댓글 기능 표시 | ### 🔗 고급 설정 | Prop | 타입 | 기본값 | 설명 | | ------------------- | -------------------------------------------- | ----------- | -------------------- | | `editorRef` | `React.MutableRefObject<EditorType \| null>` | `undefined` | 에디터 인스턴스 참조 | | `domAttributes` | `Record<string, string>` | `{}` | DOM 속성 추가 | | `resolveFileUrl` | `(url: string) => Promise<string>` | `undefined` | 파일 URL 변환 함수 | | `onSelectionChange` | `() => void` | `undefined` | 선택 영역 변경 콜백 | ## 📖 타입 정의 ### 주요 타입 가져오기 ```tsx import type { LumirEditorProps, EditorType, DefaultPartialBlock, DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema, PartialBlock, BlockNoteEditor, } from "@kingdoo/editor"; ``` ### 타입 사용 예시 ```tsx import { useRef } from "react"; import { LumirEditor, type EditorType, type DefaultPartialBlock, } from "@kingdoo/editor"; function MyEditor() { const editorRef = useRef<EditorType>(null); const handleContentChange = (content: DefaultPartialBlock[]) => { console.log("변경된 블록:", content); saveToDatabase(JSON.stringify(content)); }; const insertImage = () => { editorRef.current?.pasteHTML('<img src="/example.jpg" alt="Example" />'); }; return ( <div> <button onClick={insertImage}>이미지 삽입</button> <LumirEditor editorRef={editorRef} onContentChange={handleContentChange} /> </div> ); } ``` ## 🎨 스타일 커스터마이징 완벽 가이드 ### 1. 기본 스타일 시스템 LumirEditor는 3가지 스타일링 방법을 제공합니다: 1. **기본 스타일**: `includeDefaultStyles={true}` (권장) 2. **Tailwind CSS**: `className` prop으로 유틸리티 클래스 적용 3. **커스텀 CSS**: 전통적인 CSS 클래스와 선택자 사용 ### 2. 기본 설정 및 제어 ```tsx // 기본 스타일 포함 (권장) <LumirEditor includeDefaultStyles={true} // 기본값 className="추가-커스텀-클래스" /> // 기본 스타일 완전 제거 (고급 사용자) <LumirEditor includeDefaultStyles={false} className="완전-커스텀-에디터-스타일" /> ``` ### 3. Tailwind CSS 스타일링 #### 기본 레이아웃 스타일링 ```tsx <LumirEditor className=" min-h-[500px] max-w-4xl mx-auto rounded-xl border border-gray-200 shadow-lg bg-white dark:bg-gray-900 " /> ``` #### 반응형 스타일링 ```tsx <LumirEditor className=" h-64 md:h-96 lg:h-[500px] text-sm md:text-base p-2 md:p-4 lg:p-6 rounded-md md:rounded-lg lg:rounded-xl shadow-sm md:shadow-md lg:shadow-lg " /> ``` #### 고급 내부 요소 스타일링 ```tsx <LumirEditor className=" /* 에디터 영역 패딩 조정 */ [&_.bn-editor]:px-8 [&_.bn-editor]:py-4 /* 특정 블록 타입 스타일링 */ [&_[data-content-type='paragraph']]:text-base [&_[data-content-type='paragraph']]:leading-relaxed [&_[data-content-type='heading']]:font-bold [&_[data-content-type='heading']]:text-gray-900 [&_[data-content-type='list']]:ml-4 /* 포커스 상태 스타일링 */ focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 /* 테마별 스타일링 */ dark:[&_.bn-editor]:bg-gray-800 dark:[&_.bn-editor]:text-white /* 호버 효과 */ hover:shadow-md transition-shadow duration-200 " /> ``` #### 테마별 스타일링 ```tsx // 라이트 모드 <LumirEditor theme="light" className=" bg-white text-gray-900 border-gray-200 [&_.bn-editor]:bg-white [&_[data-content-type='paragraph']]:text-gray-800 " /> // 다크 모드 <LumirEditor theme="dark" className=" bg-gray-900 text-white border-gray-700 [&_.bn-editor]:bg-gray-900 [&_[data-content-type='paragraph']]:text-gray-100 " /> ``` ### 4. CSS 클래스 스타일링 #### 기본 CSS 구조 ```css /* 메인 에디터 컨테이너 */ .my-custom-editor { border: 2px solid #e5e7eb; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); background: white; } /* 에디터 내용 영역 */ .my-custom-editor .bn-editor { font-family: "Pretendard", -apple-system, BlinkMacSystemFont, sans-serif; font-size: 14px; line-height: 1.6; padding: 24px; min-height: 200px; } /* 포커스 상태 */ .my-custom-editor:focus-within { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } ``` #### 블록별 세부 스타일링 ```css /* 문단 블록 */ .my-custom-editor .bn-block[data-content-type="paragraph"] { margin-bottom: 12px; font-size: 14px; color: #374151; } /* 헤딩 블록 */ .my-custom-editor .bn-block[data-content-type="heading"] { font-weight: 700; margin: 24px 0 12px 0; color: #111827; } .my-custom-editor .bn-block[data-content-type="heading"][data-level="1"] { font-size: 28px; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; } ``` ## 🔧 고급 사용법 ### 명령형 API 사용 ```tsx function AdvancedEditor() { const editorRef = useRef<EditorType>(null); const insertTable = () => { editorRef.current?.insertBlocks( [ { type: "table", content: { type: "tableContent", rows: [{ cells: ["셀 1", "셀 2"] }, { cells: ["셀 3", "셀 4"] }], }, }, ], editorRef.current.getTextCursorPosition().block ); }; return ( <div> <button onClick={insertTable}>표 삽입</button> <button onClick={() => editorRef.current?.focus()}>포커스</button> <LumirEditor editorRef={editorRef} /> </div> ); } ``` ### 커스텀 붙여넣기 핸들러 ```tsx <LumirEditor pasteHandler={({ event, defaultPasteHandler }) => { const text = event.clipboardData?.getData("text/plain"); // URL 감지 시 자동 링크 생성 if (text?.startsWith("http")) { return defaultPasteHandler({ pasteBehavior: "prefer-html" }) ?? false; } // 기본 처리 return defaultPasteHandler() ?? false; }} /> ``` ### 실시간 자동 저장 ```tsx function AutoSaveEditor() { const [saveStatus, setSaveStatus] = useState<"saved" | "saving" | "error">( "saved" ); const handleContentChange = useCallback( debounce(async (content: DefaultPartialBlock[]) => { setSaveStatus("saving"); try { await saveToServer(JSON.stringify(content)); setSaveStatus("saved"); } catch (error) { setSaveStatus("error"); } }, 1000), [] ); return ( <div> <div className="mb-2"> 상태: <span className={`badge badge-${saveStatus}`}>{saveStatus}</span> </div> <LumirEditor onContentChange={handleContentChange} /> </div> ); } ``` ## 📱 반응형 디자인 ```tsx <LumirEditor className=" w-full h-96 md:h-[500px] lg:h-[600px] rounded-lg border border-gray-300 md:rounded-xl lg:shadow-xl " // 모바일에서는 일부 툴바 숨김 formattingToolbar={true} filePanel={window.innerWidth > 768} tableHandles={window.innerWidth > 1024} /> ``` ## ⚠️ 주의사항 및 문제 해결 ### 1. SSR 환경 (필수) Next.js 등 SSR 환경에서는 반드시 클라이언트 사이드에서만 렌더링해야 합니다: ```tsx // ✅ 올바른 방법 const LumirEditor = dynamic( () => import("@kingdoo/editor").then((m) => m.LumirEditor), { ssr: false } ); // ❌ 잘못된 방법 - SSR 오류 발생 import { LumirEditor } from "@kingdoo/editor"; ``` ### 2. React StrictMode React 19/Next.js 15 일부 환경에서 StrictMode 이슈가 보고되었습니다. 문제 발생 시 임시로 StrictMode를 비활성화하는 것을 고려해보세요. ### 3. 일반적인 설치 문제 #### TypeScript 타입 오류 ```bash # TypeScript 타입 문제 해결 npm install --save-dev @types/react @types/react-dom # 또는 tsconfig.json에서 { "compilerOptions": { "skipLibCheck": true } } ``` #### CSS 스타일이 적용되지 않는 경우 ```tsx // 1. CSS 파일이 올바르게 임포트되었는지 확인 import "@kingdoo/editor/style.css"; // 2. Tailwind CSS 설정 확인 // tailwind.config.js에 패키지 경로 추가 필요 // 3. CSS 우선순위 문제인 경우 .my-editor { /* !important 사용 또는 더 구체적인 선택자 */ } ``` #### 번들러 호환성 문제 ```js // Webpack 설정 module.exports = { resolve: { fallback: { crypto: require.resolve("crypto-browserify"), stream: require.resolve("stream-browserify"), }, }, }; // Vite 설정 export default defineConfig({ optimizeDeps: { include: ["@kingdoo/editor"], }, }); ``` #### 이미지 업로드 문제 ```tsx // CORS 문제 해결 const uploadFile = async (file: File) => { const response = await fetch("/api/upload", { method: "POST", headers: { // CORS 헤더 확인 }, body: formData, }); if (!response.ok) { throw new Error(`업로드 실패: ${response.status}`); } return url; // 반드시 접근 가능한 public URL }; ``` ### 4. 성능 최적화 #### 큰 문서 처리 ```tsx // 대용량 문서의 경우 초기 렌더링 최적화 <LumirEditor initialContent={largeContent} // 불필요한 기능 비활성화 animations={false} formattingToolbar={false} // 메모리 사용량 줄이기 storeImagesAsBase64={false} /> ``` #### 메모리 누수 방지 ```tsx // 컴포넌트 언마운트 시 정리 useEffect(() => { return () => { // 에디터 정리 로직 if (editorRef.current) { editorRef.current = null; } }; }, []); ``` ## 🚀 시작하기 체크리스트 프로젝트에 LumirEditor를 성공적으로 통합하기 위한 체크리스트: ### 📋 필수 설치 단계 - [ ] 패키지 설치: `npm install @kingdoo/editor` - [ ] CSS 임포트: `import "@kingdoo/editor/style.css"` - [ ] TypeScript 타입 설치: `npm install --save-dev @types/react @types/react-dom` - [ ] SSR 환경이라면 dynamic import 설정 ### 🎨 스타일링 설정 - [ ] Tailwind CSS 사용 시 `tailwind.config.js`에 패키지 경로 추가 - [ ] 기본 스타일 적용 확인: `includeDefaultStyles={true}` - [ ] 커스텀 스타일이 필요하면 `className` prop 활용 ### 🔧 기능 설정 - [ ] 파일 업로드가 필요하면 `uploadFile` 함수 구현 - [ ] 콘텐츠 변경 감지가 필요하면 `onContentChange` 콜백 설정 - [ ] 필요에 따라 툴바와 메뉴 표시/숨김 설정 ### ✅ 테스트 확인 - [ ] 기본 텍스트 입력 동작 확인 - [ ] 이미지 업로드/붙여넣기 동작 확인 - [ ] 스타일이 올바르게 적용되는지 확인 - [ ] 다양한 브라우저에서 테스트 ## 📋 변경 기록 ### v0.2.0 (최신) - ✨ **하이브리드 콘텐츠 지원**: `initialContent`에서 JSON 객체 배열과 JSON 문자열 모두 지원 - ✨ **Placeholder 기능**: 첫 번째 블록에 placeholder 텍스트 설정 가능 - ✨ **초기 블록 개수 설정**: `initialEmptyBlocks` prop으로 빈 블록 개수 조정 - 🔧 **유틸리티 클래스 추가**: `ContentUtils`, `EditorConfig` 클래스로 코드 정리 - 📁 **타입 분리**: 모든 타입 정의를 별도 파일로 분리하여 관리 개선 - 🎨 **기본 스타일 최적화**: 더 나은 기본 패딩과 스타일 적용 ### v0.1.15 - 🐛 파일 검증 로직 보완 ### v0.1.14 - 🔧 슬래시 추천 메뉴 항목 변경 ### v0.1.13 - ⚙️ Audio, Video, Movie 업로드 기본값을 false로 변경 ### v0.1.12 - 🐛 조건부 Helper 항목 렌더링 수정 ### v0.1.11 - 🐛 이미지 중복 드롭 이슈 수정 ### v0.1.10 - 🎨 기본 이미지 저장 방식을 Base64로 설정 - ✨ `storeImagesAsBase64` prop 추가 - 🐛 드래그앤드롭 중복 삽입 방지 ### v0.1.0 - 🎉 초기 릴리스 ## 📄 라이선스 이 패키지는 BlockNote의 무료 기능만을 사용합니다. - 의존성: `@blocknote/core`, `@blocknote/react`, `@blocknote/mantine` - BlockNote 라이선스를 따릅니다.