@kingdoo/editor
v0.3.3
Published
Image-only BlockNote rich text editor with S3 upload and optimized loading spinner
Maintainers
Readme
LumirEditor
🖼️ 이미지 전용 BlockNote 기반 Rich Text 에디터
📋 목차
✨ 핵심 특징
| 특징 | 설명 | | ------------------------ | ----------------------------------------------------------- | | 🖼️ 이미지 전용 | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오/파일 제거) | | ☁️ S3 연동 | Presigned URL 기반 S3 업로드 내장 | | 🎯 커스텀 업로더 | 자체 업로드 로직 적용 가능 | | ⏳ 로딩 스피너 | 이미지 업로드 중 자동 스피너 표시 | | 🚀 애니메이션 최적화 | 기본 애니메이션 비활성화로 성능 향상 | | 📝 TypeScript | 완전한 타입 안전성 | | 🎨 테마 지원 | 라이트/다크 테마 및 커스텀 테마 지원 | | 📱 반응형 | 모바일/데스크톱 최적화 |
지원 이미지 형식
PNG, JPEG/JPG, GIF (애니메이션 포함), WebP, BMP, SVG📦 설치
# npm
npm install @kingdoo/editor
# yarn
yarn add @kingdoo/editor
# pnpm
pnpm add @kingdoo/editorPeer Dependencies
{
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}🚀 빠른 시작
1단계: CSS 임포트 (필수)
import "@kingdoo/editor/style.css";⚠️ 중요: CSS를 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
2단계: 기본 사용
import { LumirEditor } from "@kingdoo/editor";
import "@kingdoo/editor/style.css";
export default function App() {
return (
<div className="w-full h-[400px]">
<LumirEditor onContentChange={(blocks) => console.log(blocks)} />
</div>
);
}3단계: Next.js에서 사용 (SSR 비활성화 필수)
"use client";
import dynamic from "next/dynamic";
import "@kingdoo/editor/style.css";
const LumirEditor = dynamic(
() => import("@kingdoo/editor").then((m) => ({ default: m.LumirEditor })),
{ ssr: false }
);
export default function EditorPage() {
return (
<div className="w-full h-[500px]">
<LumirEditor
onContentChange={(blocks) => console.log("Content:", blocks)}
/>
</div>
);
}📚 Props 레퍼런스
에디터 옵션 (Editor Options)
| Prop | 타입 | 기본값 | 설명 |
| -------------------- | ----------------------------------------- | --------------------------- | ---------------------------------------- |
| initialContent | DefaultPartialBlock[] \| string | undefined | 초기 콘텐츠 (블록 배열 또는 JSON 문자열) |
| initialEmptyBlocks | number | 3 | 초기 빈 블록 개수 |
| placeholder | string | undefined | 첫 번째 블록의 placeholder 텍스트 |
| uploadFile | (file: File) => Promise<string> | undefined | 커스텀 파일 업로드 함수 |
| s3Upload | S3UploaderConfig | undefined | S3 업로드 설정 |
| tables | TableConfig | {...} | 테이블 기능 설정 |
| heading | { levels?: (1\|2\|3\|4\|5\|6)[] } | { levels: [1,2,3,4,5,6] } | 헤딩 레벨 설정 |
| defaultStyles | boolean | true | 기본 스타일 활성화 |
| disableExtensions | string[] | [] | 비활성화할 확장 기능 목록 |
| tabBehavior | "prefer-navigate-ui" \| "prefer-indent" | "prefer-navigate-ui" | 탭 키 동작 |
| trailingBlock | boolean | true | 마지막에 빈 블록 자동 추가 |
| allowVideoUpload | boolean | false | 비디오 업로드 허용 (기본 비활성) |
| allowAudioUpload | boolean | false | 오디오 업로드 허용 (기본 비활성) |
| allowFileUpload | boolean | false | 일반 파일 업로드 허용 (기본 비활성) |
뷰 옵션 (View Options)
| Prop | 타입 | 기본값 | 설명 |
| ------------------- | ---------------------------------- | --------- | ---------------------------------------------------- |
| editable | boolean | true | 편집 가능 여부 |
| theme | "light" \| "dark" \| ThemeObject | "light" | 에디터 테마 |
| formattingToolbar | boolean | true | 서식 툴바 표시 |
| linkToolbar | boolean | true | 링크 툴바 표시 |
| sideMenu | boolean | true | 사이드 메뉴 표시 |
| sideMenuAddButton | boolean | false | 사이드 메뉴 + 버튼 표시 (false시 드래그 핸들만 표시) |
| emojiPicker | boolean | true | 이모지 선택기 표시 |
| filePanel | boolean | true | 파일 패널 표시 |
| tableHandles | boolean | true | 테이블 핸들 표시 |
| className | string | "" | 컨테이너 CSS 클래스 |
콜백 (Callbacks)
| Prop | 타입 | 설명 |
| ------------------- | ----------------------------------------- | ---------------------- |
| onContentChange | (blocks: DefaultPartialBlock[]) => void | 콘텐츠 변경 시 호출 |
| onSelectionChange | () => void | 선택 영역 변경 시 호출 |
S3UploaderConfig
interface S3UploaderConfig {
apiEndpoint: string; // Presigned URL API 엔드포인트 (필수)
env: "development" | "production"; // 환경 (필수)
path: string; // S3 경로 (필수)
}TableConfig
interface TableConfig {
splitCells?: boolean; // 셀 분할 (기본: true)
cellBackgroundColor?: boolean; // 셀 배경색 (기본: true)
cellTextColor?: boolean; // 셀 텍스트 색상 (기본: true)
headers?: boolean; // 헤더 행 (기본: true)
}🖼️ 이미지 업로드
방법 1: S3 업로드 (권장)
Presigned URL을 사용한 안전한 S3 업로드 방식입니다.
<LumirEditor
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: "development",
path: "blog/images",
}}
onContentChange={(blocks) => console.log(blocks)}
/>S3 파일 저장 경로 구조:
{env}/{path}/{filename}
예: development/blog/images/my-image.pngAPI 엔드포인트 응답 예시:
{
"presignedUrl": "https://s3.amazonaws.com/bucket/...",
"publicUrl": "https://cdn.example.com/development/blog/images/my-image.png"
}방법 2: 커스텀 업로더
자체 업로드 로직을 사용할 때 활용합니다.
<LumirEditor
uploadFile={async (file) => {
const formData = new FormData();
formData.append("image", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const data = await response.json();
return data.url; // 업로드된 이미지의 URL 반환
}}
/>방법 3: createS3Uploader 헬퍼 함수
S3 업로더를 직접 생성하여 사용할 수 있습니다.
import { LumirEditor, createS3Uploader } from "@kingdoo/editor";
// S3 업로더 생성
const s3Uploader = createS3Uploader({
apiEndpoint: "/api/s3/presigned",
env: "production",
path: "uploads/images",
});
// 에디터에 적용
<LumirEditor uploadFile={s3Uploader} />;
// 또는 별도로 사용
const imageUrl = await s3Uploader(imageFile);업로드 우선순위
uploadFileprop이 있으면 우선 사용uploadFile이 없고s3Upload가 있으면 S3 업로드 사용- 둘 다 없으면 업로드 실패
🛠️ 유틸리티 API
ContentUtils
콘텐츠 관리 유틸리티 클래스입니다.
import { ContentUtils } from "@kingdoo/editor";
// JSON 문자열 유효성 검증
const isValid = ContentUtils.isValidJSONString('[{"type":"paragraph"}]');
// true
// JSON 문자열을 블록 배열로 파싱
const blocks = ContentUtils.parseJSONContent(jsonString);
// DefaultPartialBlock[] | null
// 기본 빈 블록 생성
const emptyBlock = ContentUtils.createDefaultBlock();
// { type: "paragraph", props: {...}, content: [...], children: [] }
// 콘텐츠 유효성 검증 및 기본값 설정
const validatedContent = ContentUtils.validateContent(content, 3);
// 빈 콘텐츠면 3개의 빈 블록 반환EditorConfig
에디터 설정 유틸리티 클래스입니다.
import { EditorConfig } from "@kingdoo/editor";
// 테이블 기본 설정 가져오기
const tableConfig = EditorConfig.getDefaultTableConfig({
splitCells: true,
headers: false,
});
// 헤딩 기본 설정 가져오기
const headingConfig = EditorConfig.getDefaultHeadingConfig({
levels: [1, 2, 3],
});
// 비활성화 확장 목록 생성
const disabledExt = EditorConfig.getDisabledExtensions(
["codeBlock"], // 사용자 정의 비활성 확장
false, // allowVideo
false, // allowAudio
false // allowFile
);
// ["codeBlock", "video", "audio", "file"]cn (className 유틸리티)
조건부 className 결합 유틸리티입니다.
import { cn } from "@kingdoo/editor";
<LumirEditor
className={cn(
"min-h-[400px] rounded-lg",
isFullscreen && "fixed inset-0 z-50",
isDarkMode && "dark-theme"
)}
/>;📖 타입 정의
주요 타입 import
import type {
// 에디터 Props
LumirEditorProps,
// 에디터 인스턴스 타입
EditorType,
// 블록 관련 타입
DefaultPartialBlock,
DefaultBlockSchema,
DefaultInlineContentSchema,
DefaultStyleSchema,
PartialBlock,
BlockNoteEditor,
} from "@kingdoo/editor";
import type { S3UploaderConfig } from "@kingdoo/editor";LumirEditorProps 전체 인터페이스
interface LumirEditorProps {
// === Editor Options ===
initialContent?: DefaultPartialBlock[] | string;
initialEmptyBlocks?: number;
placeholder?: string;
uploadFile?: (file: File) => Promise<string>;
s3Upload?: {
apiEndpoint: string;
env: "development" | "production";
path: string;
};
allowVideoUpload?: boolean;
allowAudioUpload?: boolean;
allowFileUpload?: boolean;
tables?: {
splitCells?: boolean;
cellBackgroundColor?: boolean;
cellTextColor?: boolean;
headers?: boolean;
};
heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] };
defaultStyles?: boolean;
disableExtensions?: string[];
tabBehavior?: "prefer-navigate-ui" | "prefer-indent";
trailingBlock?: boolean;
// === View Options ===
editable?: boolean;
theme?:
| "light"
| "dark"
| Partial<Record<string, unknown>>
| {
light: Partial<Record<string, unknown>>;
dark: Partial<Record<string, unknown>>;
};
formattingToolbar?: boolean;
linkToolbar?: boolean;
sideMenu?: boolean;
sideMenuAddButton?: boolean;
emojiPicker?: boolean;
filePanel?: boolean;
tableHandles?: boolean;
onSelectionChange?: () => void;
className?: string;
// === Callbacks ===
onContentChange?: (content: DefaultPartialBlock[]) => void;
}💡 사용 예제
기본 에디터
import { LumirEditor } from "@kingdoo/editor";
import "@kingdoo/editor/style.css";
function BasicEditor() {
return (
<div className="h-[400px]">
<LumirEditor />
</div>
);
}초기 콘텐츠 설정
// 방법 1: 블록 배열
<LumirEditor
initialContent={[
{
type: "heading",
props: { level: 1 },
content: [{ type: "text", text: "제목입니다", styles: {} }],
},
{
type: "paragraph",
content: [{ type: "text", text: "본문 내용...", styles: {} }],
},
]}
/>
// 방법 2: JSON 문자열
<LumirEditor
initialContent='[{"type":"paragraph","content":[{"type":"text","text":"Hello World","styles":{}}]}]'
/>읽기 전용 모드
<LumirEditor
editable={false}
initialContent={savedContent}
sideMenu={false}
formattingToolbar={false}
/>다크 테마
<LumirEditor theme="dark" className="bg-gray-900 rounded-lg" />S3 이미지 업로드
<LumirEditor
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: process.env.NODE_ENV as "development" | "production",
path: "articles/images",
}}
onContentChange={(blocks) => {
// 저장 로직
saveToDatabase(JSON.stringify(blocks));
}}
/>반응형 디자인
<div className="w-full h-64 md:h-96 lg:h-[600px]">
<LumirEditor className="h-full rounded-md md:rounded-lg shadow-sm md:shadow-md" />
</div>테이블 설정 커스터마이징
<LumirEditor
tables={{
splitCells: true,
cellBackgroundColor: true,
cellTextColor: false, // 셀 텍스트 색상 비활성
headers: true,
}}
heading={{
levels: [1, 2, 3], // H4-H6 비활성
}}
/>콘텐츠 저장 및 불러오기
import { useState, useEffect } from "react";
import { LumirEditor, ContentUtils } from "@kingdoo/editor";
function EditorWithSave() {
const [content, setContent] = useState<string>("");
// 저장된 콘텐츠 불러오기
useEffect(() => {
const saved = localStorage.getItem("editor-content");
if (saved && ContentUtils.isValidJSONString(saved)) {
setContent(saved);
}
}, []);
// 콘텐츠 저장
const handleContentChange = (blocks) => {
const jsonContent = JSON.stringify(blocks);
localStorage.setItem("editor-content", jsonContent);
};
return (
<LumirEditor
initialContent={content}
onContentChange={handleContentChange}
/>
);
}🎨 스타일링 가이드
기본 CSS 구조
/* 메인 컨테이너 */
.lumirEditor {
width: 100%;
height: 100%;
min-width: 200px;
overflow: auto;
background-color: #ffffff;
}
/* 에디터 내용 영역 */
.lumirEditor .bn-editor {
font-family: "Pretendard", "Noto Sans KR", -apple-system, sans-serif;
padding: 5px 10px 0 25px;
}
/* 문단 블록 */
.lumirEditor [data-content-type="paragraph"] {
font-size: 14px;
}
/* 업로드 스피너 */
.lumirEditor-upload-overlay {
...;
}
.lumirEditor-spinner {
...;
}Tailwind CSS와 함께 사용
import { LumirEditor, cn } from "@kingdoo/editor";
<LumirEditor
className={cn(
"min-h-[400px] rounded-xl",
"border border-gray-200 shadow-lg",
"focus-within:ring-2 focus-within:ring-blue-500"
)}
/>;커스텀 스타일 적용
/* globals.css */
.my-editor .bn-editor {
padding-left: 30px;
padding-right: 20px;
font-size: 16px;
}
.my-editor [data-content-type="heading"] {
font-weight: 700;
margin-top: 24px;
}<LumirEditor className="my-editor" />⚠️ 주의사항 및 트러블슈팅
필수 체크리스트
| 항목 | 체크 |
| -------------------- | ------------------------------------- |
| CSS 임포트 | import "@kingdoo/editor/style.css"; |
| 컨테이너 높이 설정 | 부모 요소에 높이 지정 필수 |
| Next.js SSR 비활성화 | dynamic(..., { ssr: false }) 사용 |
| React 버전 | 18.0.0 이상 필요 |
일반적인 문제 해결
1. 에디터가 렌더링되지 않음
// ❌ 잘못된 사용
<LumirEditor />;
// ✅ 올바른 사용 - CSS 임포트 필요
import "@kingdoo/editor/style.css";
<LumirEditor />;2. Next.js에서 hydration 오류
// ❌ 잘못된 사용
import { LumirEditor } from "@kingdoo/editor";
// ✅ 올바른 사용 - dynamic import 사용
const LumirEditor = dynamic(
() => import("@kingdoo/editor").then((m) => ({ default: m.LumirEditor })),
{ ssr: false }
);3. 높이가 0으로 표시됨
// ❌ 잘못된 사용
<LumirEditor />
// ✅ 올바른 사용 - 부모 요소에 높이 설정
<div className="h-[400px]">
<LumirEditor />
</div>4. 이미지 업로드 실패
// uploadFile 또는 s3Upload 중 하나 반드시 설정
<LumirEditor
uploadFile={async (file) => {
// 업로드 로직
return imageUrl;
}}
// 또는
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: "development",
path: "images",
}}
/>성능 최적화 팁
- 애니메이션 기본 비활성: 이미
animations: false로 설정되어 성능 최적화됨 - 큰 콘텐츠 처리: 초기 콘텐츠가 클 경우 lazy loading 고려
- 이미지 최적화: 업로드 전 클라이언트에서 이미지 리사이징 권장
🏗️ 프로젝트 구조
@kingdoo/editor/
├── dist/ # 빌드 출력
│ ├── index.js # CommonJS 빌드
│ ├── index.mjs # ESM 빌드
│ ├── index.d.ts # TypeScript 타입 정의
│ └── style.css # 스타일시트
├── src/
│ ├── components/
│ │ └── LumirEditor.tsx # 메인 에디터 컴포넌트
│ ├── types/
│ │ ├── editor.ts # 에디터 타입 정의
│ │ └── index.ts # 타입 export
│ ├── utils/
│ │ ├── cn.ts # className 유틸리티
│ │ └── s3-uploader.ts # S3 업로더
│ ├── index.ts # 메인 export
│ └── style.css # 소스 스타일
└── examples/
└── tailwind-integration.md # Tailwind 통합 가이드📄 라이선스
MIT License
