@lumir-company/editor
v0.5.7
Published
LumirEditor — vanilla (HTML/JS/CSS) rich text editor, BlockNote-JSON round-trip compatible.
Readme
@lumir-company/editor
의존성 0(vanilla HTML/JS/CSS) 리치 텍스트 에디터 — BlockNote-JSON 라운드트립 호환.
구 BlockNote 기반
@lumir-company/[email protected]의 드롭인 대체입니다. 동일한 API 표면(LumirEditor컴포넌트·props·타입·s3Upload계약)을 유지하되, 코어에서 React / BlockNote / ProseMirror 런타임을 완전히 제거했습니다. 코어는 react-free라 서버·명령형 환경에서도 사용할 수 있고, React 컴포넌트는 선택적 래퍼로 제공됩니다.
목차
- 특징
- 설치
- 빠른 시작
- Exports (서브패스)
- 이미지 업로드
- 동영상·오디오·파일 업로드
- 업로드 진행률
- 이미지·비디오 삭제
- 테이블
- 다단 컬럼 (2·3단)
- 글자 크기
- 링크
- Placeholder
- 테마
- Props API
- react-free 코어 API
- 유틸리티 API
- 스타일링
- 트러블슈팅
- 0.4.x → 0.5.0 마이그레이션
- 변경 이력
- 라이선스
특징
| 특징 | 설명 |
| --- | --- |
| 의존성 0 | 코어에 React/BlockNote/ProseMirror 런타임 없음(vanilla HTML/JS/CSS). React는 선택적 peer |
| 드롭인 호환 | 구 @lumir-company/[email protected]와 동일한 컴포넌트·props·타입·저장 JSON |
| react-free 코어 | mountLumirEditor() 명령형 API로 서버 컴포넌트·비-React 환경에서도 사용 가능 |
| 이미지 업로드 | 드래그앤드롭·붙여넣기·슬래시 메뉴, S3 presigned URL 내장, 파일명 커스터마이징, 로딩 표시 |
| 동영상/오디오/파일 | allowVideoUpload·allowAudioUpload·allowFileUpload opt-in |
| 테이블 | Notion 스타일 grip 핸들, 셀 배경/글자색·정렬, 병합/분할, 행 높이·열 너비 리사이즈, 표 전체 종횡비 스케일, 에디터 폭 자동 맞춤, Excel/Word 붙여넣기 |
| 2·3단 컬럼 | 노션식 다단 컬럼 레이아웃(MIT 자체 구현), 슬래시로 2·3단 삽입, 블록 DnD로 2단 생성, 블록별/전역 구분선, 마지막 블록에서 Enter 두 번으로 컬럼 탈출 |
| 글자 크기 | 인라인 글자 크기(프리셋 + 1px 스테퍼, 8~96px), 구버전 안전 직렬화 |
| 여러 줄 선택·편집 | 블록 경계를 넘는 드래그로 여러 줄 선택 → 서식 일괄 적용, 선택 영역 삭제·타이핑·붙여넣기·부분 복사(무손실), Shift+클릭·Ctrl/Cmd+A 전체선택, 단일 Undo |
| 링크 | URL 붙여넣기 → 인라인 링크, Notion식 링크 툴바(hover 툴팁 → 편집 popup) |
| 직렬화 | 블록 JSON ↔ HTML ↔ Markdown 상호 변환 유틸 공개 export |
| TypeScript | 전 표면 .d.ts 제공 |
| 테마 | 라이트/다크 + 커스텀 테마 객체 |
지원 파일 형식
| 종류 | 형식 | 기본 용량 한도 |
| --- | --- | --- |
| 이미지 | PNG · JPEG/JPG · GIF · WebP · BMP (SVG는 XSS 방지로 제외) | 10MB |
| 동영상 | MP4 · WebM · OGG · MOV | 100MB |
| 오디오/파일 | allowAudioUpload·allowFileUpload 활성화 시 | maxAudioFileSize 등으로 설정 |
설치
npm i @lumir-company/editor
# 또는
yarn add @lumir-company/editor
# 또는
pnpm add @lumir-company/editorPeer dependencies (선택):
react≥ 18.0.0react-dom≥ 18.0.0
React/
react-dom은 React 컴포넌트(LumirEditor)나 드롭인 루트를 사용할 때만 필요합니다../core서브패스(vanilla)만 사용한다면 React 없이 동작합니다.
빠른 시작
중요:
@lumir-company/editor/style.css를 반드시 임포트하세요. 임포트하지 않으면 에디터가 정상적으로 렌더링되지 않습니다.
1. React 드롭인
import LumirEditor from "@lumir-company/editor"; // default export
// 또는: import { LumirEditor } from "@lumir-company/editor";
import "@lumir-company/editor/style.css";
export default function App() {
return (
<div style={{ height: 500 }}>
<LumirEditor
initialContent={blocks}
editable
s3Upload={{ apiEndpoint: "/api/s3/presigned", env: "production", path: "cms/wiki", appendUUID: true }}
onContentChange={(blocks) => save(JSON.stringify(blocks))}
onImageDelete={(url) => deleteFromS3(url)}
/>
</div>
);
}컨테이너에 높이를 지정해야 에디터가 보입니다.
2. Next.js
브라우저 전용 API를 사용하므로 SSR을 비활성화합니다.
"use client";
import dynamic from "next/dynamic";
import "@lumir-company/editor/style.css";
const LumirEditor = dynamic(
() => import("@lumir-company/editor").then((m) => ({ default: m.LumirEditor })),
{ ssr: false },
);
export default function EditorPage() {
return (
<div style={{ height: 500 }}>
<LumirEditor />
</div>
);
}3. react-free 코어 (vanilla)
React 없이 임의의 DOM에 마운트하는 명령형 API입니다. 서버 컴포넌트·순수 JS·다른 프레임워크에서 사용할 수 있습니다.
import { mountLumirEditor } from "@lumir-company/editor/core";
import "@lumir-company/editor/style.css";
const editor = mountLumirEditor(document.getElementById("host"), {
initialContent: blocks,
s3Upload: { apiEndpoint: "/api/s3/presigned", env: "production", path: "docs" },
onContentChange: (blocks) => save(blocks),
});
editor.getDocument(); // 현재 블록 JSON
editor.getHTML(); // HTML 문자열
editor.getMarkdown(); // Markdown 문자열
editor.destroy(); // 정리자세한 인스턴스 메서드는 react-free 코어 API를 참고하세요.
Exports (서브패스)
| 서브패스 | 내용 | React 필요 |
| --- | --- | --- |
| . | React LumirEditor(default + named) + 코어/유틸 전체 ("use client") | ✅ |
| ./react | .과 동일 표면(하위호환 별칭) | ✅ |
| ./core | react-free 코어(mountLumirEditor·직렬화·유틸) — 서버 컴포넌트 안전 | ❌ |
| ./style.css | 단일 번들 CSS | — |
각 서브패스는 types(.d.ts) / import(ESM) / require(CJS) 조건을 모두 제공합니다.
참고: 구버전에 있던
@lumir-company/editor/api/link-preview서브패스는 제거되었습니다. 마이그레이션을 참고하세요.
이미지 업로드
이미지는 붙여넣기 · 드래그 앤 드롭 · 슬래시 메뉴(/ → Image) · 사이드/플로팅 메뉴로 삽입됩니다. 업로드 방식은 아래 우선순위로 결정됩니다.
uploadFileprop이 있으면 → 해당 함수로 업로드- 없고
s3Upload가 있으면 → S3 presigned URL 업로드 - 둘 다 없으면 → 파일 삽입 시 업로드 실패
S3 업로드 (권장)
<LumirEditor
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: "production",
path: "blog/images",
}}
/>S3 저장 경로: {env}/{path}/{filename} — 예: production/blog/images/my-photo.png
API 엔드포인트 계약
클라이언트는 GET {apiEndpoint}?key={파일키}&contentType={MIME} 형태로 요청하고, 서버는 다음 JSON을 반환해야 합니다.
{
"presignedUrl": "https://s3.amazonaws.com/bucket/upload-url",
"publicUrl": "https://cdn.example.com/production/blog/images/my-photo.png"
}Presigned URL API 예시 (Next.js App Router)
app/api/s3/presigned/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
// ⚠️ 필수: AWS SDK v3(≥3.729)는 기본 체크섬(WHEN_SUPPORTED)으로 presigned PUT 서명에
// x-amz-checksum-crc32 를 넣는데, 브라우저 PUT은 Content-Type만 전송 → 불일치로 S3 403.
// 아래처럼 체크섬을 "요구될 때만" 계산하도록 낮춰야 브라우저 직접 업로드가 통과됩니다.
requestChecksumCalculation: "WHEN_REQUIRED",
responseChecksumValidation: "WHEN_REQUIRED",
});
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const key = searchParams.get("key"); // 업로드할 파일 키 ({env}/{path}/{filename})
const contentType = searchParams.get("contentType"); // MIME (선택, 없으면 application/octet-stream)
if (!key) return NextResponse.json({ error: "key is required" }, { status: 400 });
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET!,
Key: key,
ContentType: contentType || "application/octet-stream",
});
const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 60 }); // 유효 60초
const publicUrl = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
return NextResponse.json({ presignedUrl, publicUrl, key });
}필요 환경 변수: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET.
주의:
requestChecksumCalculation/responseChecksumValidation를"WHEN_REQUIRED"로 두지 않으면 최신 AWS SDK v3에서 브라우저 업로드가 403으로 실패합니다. 이 레포의 동작 예시는lumir-editor-test/src/app/api/s3/presigned/route.ts에 있습니다.
Express / Remix / SvelteKit 등도 동일하게
key·contentType을 받아{ presignedUrl, publicUrl }을 반환하는 GET 라우트를 만들면 됩니다.apiEndpoint만 해당 서버 주소로 맞추세요.
파일명 커스터마이징
여러 파일을 동시에 올릴 때 이름 충돌을 방지합니다. 기본적으로 확장자는 자동으로 붙습니다(preserveExtension: false로 끌 수 있음).
<LumirEditor
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: "production",
path: "uploads",
// 확장자를 제거한 이름(nameWithoutExt)이 전달됩니다
fileNameTransform: (nameWithoutExt, file) => `user123_${nameWithoutExt}`,
appendUUID: true, // 변환 후 UUID를 확장자 앞에 추가
// preserveExtension: false // 확장자를 붙이지 않음(서버에서 WebP 변환 등)
}}
/>결과: photo.png → user123_photo_550e8400-…-446655440000.png
커스텀 업로더
<LumirEditor
uploadFile={async (file) => {
const fd = new FormData();
fd.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: fd });
const { url } = await res.json();
return url; // 공개 URL 문자열 반환
}}
/>헬퍼: createS3Uploader
import { createS3Uploader } from "@lumir-company/editor";
const s3Uploader = createS3Uploader({
apiEndpoint: "/api/s3/presigned",
env: "production",
path: "images",
appendUUID: true,
});
<LumirEditor uploadFile={s3Uploader} />;
// 또는 독립 사용: const url = await s3Uploader(file);동영상·오디오·파일 업로드
동영상은 allowVideoUpload={true}일 때만 활성화됩니다. 업로드 설정(s3Upload/uploadFile)은 이미지와 동일하게 공유됩니다.
<LumirEditor
allowVideoUpload
maxVideoFileSize={200 * 1024 * 1024} // 200MB (기본 100MB)
s3Upload={{ apiEndpoint: "/api/s3/presigned", env: "production", path: "videos", appendUUID: true }}
/>- 삽입 경로: 붙여넣기, 드래그 앤 드롭, 슬래시 메뉴("Video"), 플로팅/사이드 메뉴
- 지원 URL: 비디오 블록의
url은 직접 재생 가능한 파일 URL(예:.mp4,.webm,.ogg)만 지원합니다. YouTube/Vimeo 등 스트리밍 페이지 URL은<video>로 재생되지 않습니다. - 오디오/일반 파일:
allowAudioUpload/allowFileUpload로 opt-in. 용량은maxAudioFileSize등으로 조절합니다.
이미지·동영상 경로 분리
fileNameTransform의 두 번째 인자 file로 종류를 구분해 prefix를 나눌 수 있습니다.
fileNameTransform: (nameWithoutExt, file) => {
const isVideo = file.type.startsWith("video/");
return `${isVideo ? "videos" : "images"}/${nameWithoutExt}`;
}
// 결과: production/uploads/videos/clip_xxx.mp4, production/uploads/images/photo_xxx.png저장 데이터 구조
{ "type": "image", "props": { "url": "https://cdn/…/photo.png", "caption": "", "previewWidth": 512 }, "content": [], "children": [] }
{ "type": "video", "props": { "url": "https://cdn/…/clip.mp4" }, "content": [], "children": [] }업로드 진행률
S3 업로드 시 s3Upload.onProgress 콜백으로 0~100% 진행률을 받을 수 있습니다. 에디터는 이 값을 툴바에 n%로 자동 표시합니다. 대용량 파일에서 브라우저가 progress 이벤트를 드물게 보내는 경우를 대비해 중간 진행률을 보간하여 부드럽게 갱신합니다.
<LumirEditor
allowVideoUpload
s3Upload={{
apiEndpoint: "/api/s3/presigned",
env: "production",
path: "videos",
uploadTimeoutMs: 180000, // PUT 타임아웃(ms). 기본 120000
maxRetries: 2, // PUT 실패 재시도. 기본 2(최대 3회 시도)
onProgress: (percent) => console.log(`${percent}%`),
}}
/>onProgress는 S3 PUT 요청 시에만 호출됩니다(presigned URL 요청 단계 제외).- 업로드 시작 직후
onProgress(0), 완료 시onProgress(100)이 보장됩니다.
이미지·비디오 삭제
에디터에서 이미지 또는 비디오 블록이 삭제되면 onImageDelete(url)이 호출됩니다(둘 다 동일 콜백).
<LumirEditor
s3Upload={{ /* … */ }}
onImageDelete={(url) => {
// S3 등 외부 스토리지 삭제
}}
/>권장: 지연 삭제 (Undo/Redo 대응)
Undo로 복원 가능하도록 실제 삭제를 지연시키는 패턴을 권장합니다.
"use client";
import { useRef, useCallback } from "react";
function Editor() {
const pending = useRef(new Map<string, ReturnType<typeof setTimeout>>());
const handleImageDelete = useCallback((url: string) => {
if (pending.current.has(url)) return;
const t = setTimeout(async () => {
pending.current.delete(url);
await fetch(`/api/s3/delete?url=${encodeURIComponent(url)}`, { method: "DELETE" });
}, 5 * 60 * 1000); // 5분 후 삭제
pending.current.set(url, t);
}, []);
return <LumirEditor s3Upload={{ /* … */ }} onImageDelete={handleImageDelete} />;
}| 항목 | 권장 | | --- | --- | | Undo/Redo | 지연 삭제(5~10분)로 복원 가능하게 구현 | | 권한 검증 | 프로덕션에서는 인증/인가 필수 | | 참조 카운트 | 같은 URL을 여러 문서에서 쓰는지 확인 |
테이블
슬래시 메뉴(/ → Table)나 Excel/Word 셀 붙여넣기로 표를 만듭니다.
Notion 스타일 grip 핸들
셀에 포커스하면 주변에 grip 핸들이 표시됩니다.
| 위치 | 동작 | | --- | --- | | 상단 grip | 클릭 → 열 메뉴(삭제 / 좌·우 열 추가 / 색) · 드래그 → 열 이동 | | 좌측 grip | 클릭 → 행 메뉴(삭제 / 위·아래 행 추가 / 색) · 드래그 → 행 이동 | | 우측 grip | hover → 셀 메뉴(셀 배경색 등) |
- 행/열 메뉴가 열리면 해당 행/열 전체가 하이라이트됩니다.
- 표 우측/하단 가장자리 hover 시 행/열 추가 버튼이 나타납니다.
셀 색·정렬
- 단일 셀: 우측 grip 또는 행/열 메뉴의 "색"에서 배경색/글자색 적용
- 다중 셀: 드래그로 범위 선택 후 플로팅 툴바 색상/정렬 버튼으로 일괄 적용
- 표 전체 선택: 셀 포커스 상태에서
Ctrl/Cmd + A→ 표의 모든 셀 선택(다시 누르면 문서 전체로 확장)
리사이즈 · 스케일 · 정렬
- 열 너비: 열 경계 드래그
- 행 높이: 행 경계 드래그(셀
rowHeightattr로 저장·라운드트립) - 표 전체 스케일: 표 우하단 모서리 hover → 대각 드래그로 종횡비를 고정한 채 표 전체를 균일 배율로 확대/축소
- 표 정렬: 포매팅 툴바/드래그핸들 메뉴에서 표 전체를 좌/가운데/우 정렬
- 에디터 폭 자동 맞춤: 에디터보다 넓은 표를 붙여넣으면 열 비율을 유지한 채 폭에 맞춰 축소(가로 스크롤 미제공)
Excel/Word 붙여넣기
Excel·Word 등에서 복사한 셀 범위를 붙여넣으면(Ctrl+V) 이미지가 아닌 편집 가능한 테이블로 삽입됩니다. 셀 배경색·글자색·정렬·글자 크기·세로 정렬·굵게/기울임/밑줄이 함께 변환됩니다(글꼴·정확한 hex 색은 10색 팔레트로 근사).
테이블 기능 설정
<LumirEditor
tables={{
splitCells: true, // 셀 병합/분할 (기본 true)
cellBackgroundColor: true, // 셀 배경색 (기본 true)
cellTextColor: true, // 셀 글자색 (기본 true)
headers: true, // 헤더 행/열 (기본 true)
}}
tableHandles={true} // 핸들/코너 스케일 전체 게이트 (기본 true, false면 모두 끔)
/>다단 컬럼 (2·3단)
노션식 다단 컬럼 레이아웃입니다. 공식 @blocknote/xl-multi-column(AGPL) 대신 MIT 자체 구현을 사용합니다. 2단·3단을 지원합니다.
- 삽입: 슬래시 메뉴
/2단 컬럼·/2단 컬럼 (구분선)·/3단 컬럼·/3단 컬럼 (구분선). - 블록 DnD 생성: 블록을 다른 블록의 좌/우 가장자리로 끌어다 놓으면 2단 컬럼이 생성됩니다(세로 드롭 인디케이터). (3단은 슬래시 메뉴로 생성)
- 컬럼 간 이동: 블록 그립(⠿)을 다른 컬럼 안으로 드래그. 컬럼의 마지막 블록을 옮겨도 원래 컬럼은 빈 문단으로 유지되어(사라지지 않음) 다시 블록을 넣을 수 있습니다.
- 컬럼 탈출: 컬럼 맨 마지막 블록에서 Enter 두 번 → 컬럼 아래에 일반 문단 생성·포커스(어느 컬럼에서든 동일).
- 너비 리사이즈: 컬럼 사이 경계를 드래그해 인접 두 컬럼의 비율 조절(합 유지).
- 구분선:
- 블록별: 생성 시 "구분선" 항목을 고르면 그 블록에 고정되어 저장·라운드트립(
showDivider). - 전역:
columnDividerprop(기본false)으로 모든 컬럼 사이에 세로 구분선 표시.
- 블록별: 생성 시 "구분선" 항목을 고르면 그 블록에 고정되어 저장·라운드트립(
<LumirEditor columnDivider />구분선 색은 CSS 변수 --lumir-column-divider-color로 조절합니다(미지정 시 라이트 #d1d5db / 다크 #4a4a4a). 구분선 양옆에는 블록 그립이 뜰 여백(약 18–20px)이 예약됩니다.
하위호환: 4단 이상(레거시/외부 데이터)으로 저장된
columnList는 로드 시 초과 컬럼의 블록이 마지막(3번째) 컬럼으로 병합되어 3단으로 정규화됩니다. 1단은 펼쳐서 일반 블록으로 승격됩니다.
글자 크기
텍스트를 선택한 뒤 포매팅 툴바 또는 상단 고정 툴바(FloatingMenu) 의 글자 크기 컨트롤로 인라인 크기를 변경합니다.
- 프리셋: 기본(14px, 스타일 제거) / 10 · 12 · 14 · 16 · 18 · 20 · 24 · 28 (px)
- 1px 스테퍼: 드롭다운 상단의
−/+버튼과 직접 입력(↑/↓키)으로 8~96px 범위 내 임의 값 지정. 명시 크기가 없으면 14px 기준으로 증감 - 테이블 셀 텍스트에도 동일하게 적용됩니다.
- 외부 HTML(웹페이지·Excel 등)을 붙여넣을 때의 글자 크기는 가져오지 않습니다.
하위호환 직렬화 포맷 (중요)
글자 크기는 저장 JSON에서 styles 맵이 아니라 styled-text의 형제(sibling) 키 fontSize 로 직렬화됩니다.
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "큰 글씨", "styles": { "bold": true }, "fontSize": "18px" }
]
}이유: BlockNote는 styles 맵에 스키마에 없는 키가 있으면 예외를 던집니다. styles.fontSize로 저장하면 fontSize 스펙이 없는 구버전 SDK(≤0.4.15) 가 그 JSON을 로드할 때 크래시합니다. 형제 키 방식은 구버전에서 조용히 무시되어(글자 크기만 미표시) 안전하게 로드됩니다.
- 에디터 로드/저장 시 변환은 자동입니다(
initialContent↔onContentChange). - 외부 렌더러에서 저장 JSON을 직접 다룬다면, 공개 export된
liftFontSize(blocks)로 형제 키를styles.fontSize로 복원한 뒤 사용하세요. 반대로 외부로 내보낼 때는 반드시lowerFontSize(blocks)를 거쳐야 합니다(styles.fontSize가 유출되면 구버전 소비 앱이 크래시). - 직렬화 형태 타입은
SerializedStyledText로 export됩니다.
링크
URL을 텍스트에 붙여넣으면 자동으로 인라인 링크로 변환됩니다(구버전의 자동 OG 카드 생성은 제거됨).
- 링크 툴바(Notion식): 링크에 hover하면 URL 툴팁이 뜨고, 클릭하면 URL·텍스트를 편집하는 popup이 열립니다.
- 보안:
javascript:·data:·vbscript:·file:등 위험 프로토콜은 차단됩니다. linkToolbarprop(기본true)으로 링크 툴바 표시를 제어합니다.
구버전의
linkPreviewprop과/api/link-preview서브패스는 제거되었습니다.
Placeholder
에디터가 비어있을 때 안내 텍스트를 표시합니다.
<LumirEditor placeholder="내용을 입력하세요..." />- 빈 블록에 연한 색으로 표시되고, 입력을 시작하면 사라집니다.
- 모든 빈 블록(첫 블록 포함)에 동일하게 적용됩니다.
테마
<LumirEditor theme="dark" />
<LumirEditor theme="light" />
<LumirEditor theme={{ /* 커스텀 테마 객체 */ }} />theme은 "light" | "dark" | 커스텀 테마 객체를 받습니다(기본 "light").
Props API
자주 쓰는 Props
| Prop | 타입 | 기본값 | 설명 |
| --- | --- | --- | --- |
| initialContent | DefaultPartialBlock[] \| string | undefined | 초기 콘텐츠(블록 배열 또는 JSON 문자열) |
| onContentChange | (blocks: DefaultPartialBlock[]) => void | undefined | 콘텐츠 변경 콜백 |
| s3Upload | S3UploaderConfig | undefined | S3 업로드 설정 |
| uploadFile | (file: File) => Promise<string> | undefined | 커스텀 업로드 함수 |
| onImageDelete | (url: string) => void | undefined | 이미지·비디오 삭제 콜백 |
| onError | (error: LumirEditorError) => void | undefined | 에러 콜백 |
| editable | boolean | true | 편집 가능 여부 |
| placeholder | string | undefined | 빈 블록 안내 텍스트 |
| theme | "light" \| "dark" \| object | "light" | 테마 |
| allowVideoUpload | boolean | false | 동영상 업로드 허용 |
| tables | TableConfig | 모두 true | 테이블 기능 |
| maxImageFileSize | number | 10MB | 이미지 최대 용량(바이트) |
| maxVideoFileSize | number | 100MB | 동영상 최대 용량(바이트) |
| className | string | "" | 컨테이너 CSS 클래스 |
전체 Props
interface LumirEditorProps {
// 콘텐츠
initialContent?: DefaultPartialBlock[] | string;
initialEmptyBlocks?: number; // 초기 빈 블록 개수 (기본 3)
placeholder?: string;
// 업로드
uploadFile?: (file: File) => Promise<string>;
s3Upload?: S3UploaderConfig;
allowVideoUpload?: boolean; // 기본 false
allowAudioUpload?: boolean; // 기본 false
allowFileUpload?: boolean; // 기본 false
maxImageFileSize?: number; // 미설정 시 10MB
maxVideoFileSize?: number; // 미설정 시 100MB
maxAudioFileSize?: number;
// 기능
tables?: { splitCells?: boolean; cellBackgroundColor?: boolean; cellTextColor?: boolean; headers?: boolean };
heading?: { levels?: (1 | 2 | 3 | 4 | 5 | 6)[] };
defaultStyles?: boolean; // 기본 true
disableExtensions?: string[];
tabBehavior?: "prefer-navigate-ui" | "prefer-indent"; // 기본 "prefer-navigate-ui"
trailingBlock?: boolean; // 기본 true
trailingBlockType?: string;
// UI
editable?: boolean; // 기본 true
theme?: "light" | "dark" | Record<string, unknown>; // 기본 "light"
formattingToolbar?: boolean; // 기본 true
linkToolbar?: boolean; // 기본 true
sideMenu?: boolean; // 기본 true
sideMenuAddButton?: boolean; // 기본 false
slashMenu?: boolean; // 기본 true
emojiPicker?: boolean; // 기본 true
filePanel?: boolean; // 기본 true
tableHandles?: boolean; // 기본 true
columnDivider?: boolean; // 2단 컬럼 세로 구분선 (기본 false)
floatingMenu?: boolean; // 상단 고정 툴바 (기본 false)
floatingMenuPosition?: "sticky" | "fixed"; // 기본 "sticky"
className?: string;
// 로케일/기타
locale?: string | LumirLocale; // "ko" | "en" 등
resolveFileUrl?: (url: string) => string | Promise<string>;
fetchLinkPreview?: (url: string) => Promise<LinkPreview> | LinkPreview; // 링크 카드 메타 조회(서버 프록시 권장)
showErrorToast?: boolean;
// 콜백
onContentChange?: (blocks: DefaultPartialBlock[]) => void;
onError?: (error: LumirEditorError) => void;
onSelectionChange?: () => void;
onImageDelete?: (url: string) => void;
onUploadStart?: (file: File) => void;
onUploadEnd?: (file: File) => void;
}S3UploaderConfig
interface S3UploaderConfig {
apiEndpoint: string; // Presigned URL API 엔드포인트
env: "development" | "production";
path: string; // S3 저장 경로
fileNameTransform?: (nameWithoutExt: string, file: File) => string;
appendUUID?: boolean; // 파일명 뒤(확장자 앞)에 UUID 추가
preserveExtension?: boolean; // 기본 true. false면 확장자 미부착
onProgress?: (percent: number) => void; // 0~100, S3 PUT 시만 호출(보간)
uploadTimeoutMs?: number; // PUT 타임아웃. 기본 120000
maxRetries?: number; // PUT 재시도. 기본 2
}React ref (imperative API)
import { useRef } from "react";
import { LumirEditor, type LumirEditorReactRef } from "@lumir-company/editor";
const ref = useRef<LumirEditorReactRef>(null);
<LumirEditor ref={ref} />;
ref.current?.getDocument(); // DefaultPartialBlock[] | null
ref.current?.undo(); ref.current?.redo();
ref.current?.canUndo(); ref.current?.canRedo();
ref.current?.insertFile(file);
ref.current?.setTheme("dark");
ref.current?.setEditable(false);
ref.current?.editor; // 내부 VanillaEditor 인스턴스react-free 코어 API
mountLumirEditor(host, options)는 ./core(또는 루트)에서 import하며 VanillaEditor 인스턴스를 반환합니다.
import { mountLumirEditor, type VanillaEditor } from "@lumir-company/editor/core";
const editor: VanillaEditor = mountLumirEditor(hostEl, options /* LumirEditorProps */);VanillaEditor 인스턴스 메서드
| 메서드 | 반환 | 설명 |
| --- | --- | --- |
| getDocument() | DefaultPartialBlock[] | 현재 블록 JSON |
| getHTML() | string | 콘텐츠 HTML |
| getFullHTML(opts?) | string | <html> 문서 HTML({ title, style }) |
| getMarkdown() | string | 콘텐츠 Markdown |
| setBlocks(blocks) | void | 블록 교체 |
| setMarkdown(md) | void | Markdown으로 콘텐츠 설정 |
| commit() | boolean | 편집 커밋 |
| undo() / redo() | boolean | 실행 취소/재실행 |
| canUndo() / canRedo() | boolean | 가능 여부 |
| insertFile(file) | void | 파일 삽입(업로드) |
| isEditable() / setEditable(on) | — | 편집 가능 상태 |
| setTheme(theme) | void | 테마 변경 |
| setOnContentChange(fn) | void | 변경 콜백 교체 |
| destroy() | void | 정리(리스너 해제) |
직렬화·변환 유틸
블록 JSON ↔ HTML ↔ Markdown ↔ DOM 변환 함수가 공개 export됩니다.
import {
blocksToHtml, blocksToFullHtml, parseHtmlToBlocks,
blocksToMarkdown, markdownToBlocks,
blocksToDom, domToBlocks, blockToElement, elementToBlock,
inlineContentToHtml, htmlToInlineContent, blocksFromText,
} from "@lumir-company/editor/core";
const html = blocksToHtml(blocks);
const blocks2 = parseHtmlToBlocks(html);
const md = blocksToMarkdown(blocks);색상·표 모델(TableModel, newTableModel, modelToTableContent 등) 저수준 유틸도 함께 export됩니다. 전체 목록은 타입 선언(dist/core.d.ts)을 참고하세요.
유틸리티 API
ContentUtils
import { ContentUtils } from "@lumir-company/editor";
ContentUtils.isValidJSONString('[{"type":"paragraph"}]'); // boolean
ContentUtils.parseJSONContent(jsonString); // DefaultPartialBlock[] | null
ContentUtils.createDefaultBlock(); // DefaultPartialBlock
ContentUtils.validateContent(content, emptyBlockCount); // DefaultPartialBlock[]
ContentUtils.createEmptyBlocks(3); // DefaultPartialBlock[]글자 크기 유틸
import {
liftFontSize, lowerFontSize,
FONT_SIZE_PRESETS, FONT_SIZE_MIN, FONT_SIZE_MAX, FONT_SIZE_DEFAULT_PX, FONT_SIZE_STEP,
parseFontSizePx, clampFontSizePx, toFontSizeValue,
} from "@lumir-company/editor";색상 상수
import { TEXT_COLORS, BACKGROUND_COLORS, getHexFromColorValue } from "@lumir-company/editor";에러
import { LumirEditorError, LUMIR_ERROR_CODES } from "@lumir-company/editor";
// code: "UPLOAD_FAILED" | "INVALID_FILE_TYPE" | "S3_CONFIG_ERROR" | "NETWORK_ERROR" | "CONTENT_PARSE_ERROR" | "UNKNOWN_ERROR"로케일
import { KO, EN, LOCALES, resolveLocale } from "@lumir-company/editor";
<LumirEditor locale="ko" />; // 또는 locale={EN}기타
createS3Uploader, generateUUID, cn(className 결합) 등도 export됩니다.
스타일링
@lumir-company/editor/style.css가 모든 스타일을 단일 번들로 포함합니다(별도 CSS import 불필요).
Tailwind CSS
import { LumirEditor, cn } from "@lumir-company/editor";
<LumirEditor
className={cn(
"min-h-[400px] rounded-xl border border-gray-200 shadow-lg",
"focus-within:ring-2 focus-within:ring-blue-500",
)}
/>;커스텀 CSS
에디터 본문은 .bn-editor, 블록은 [data-content-type="..."] 셀렉터로 접근합니다.
.my-editor .bn-editor {
padding: 20px 30px;
font-size: 16px;
line-height: 1.6;
}
.my-editor [data-content-type="heading"] {
font-weight: 700;
}<LumirEditor className="my-editor" />CSS 변수
| 변수 | 기본값 | 용도 |
| --- | --- | --- |
| --lumir-column-divider-color | #e5e7eb | 2단 컬럼 세로 구분선 색 |
| --lumir-column-grip-space | 28px | 컬럼 구분선 양쪽 여백 |
| --lumir-inactive-selection | — | blur 상태 선택 하이라이트 색 |
트러블슈팅
필수 체크리스트
- [ ] CSS 임포트:
import "@lumir-company/editor/style.css"; - [ ] 컨테이너에 높이 지정(부모 요소)
- [ ] Next.js:
dynamic(..., { ssr: false })사용 - [ ] React 사용 시 버전 ≥ 18
에디터가 보이지 않음 → CSS 임포트 누락 또는 컨테이너 높이 미지정.
Next.js hydration 오류 → dynamic으로 ssr: false 처리(위 Next.js 예시).
이미지 업로드 실패 → uploadFile 또는 s3Upload 중 하나는 반드시 설정.
여러 이미지 업로드 시 파일명 중복 → s3Upload.appendUUID: true.
0.4.x → 0.5.0 마이그레이션
대부분 코드 변경 없이 버전만 올리면 됩니다. import 경로·컴포넌트·props·타입(LumirEditorProps/DefaultPartialBlock)·s3Upload 계약이 동일합니다. package.json에서 버전을 ^0.5.0으로 올리세요. (엔진 교체가 파괴적이지 않도록 마이너 버전으로 옵트인)
제거된 기능 — 사용처 정리 필요:
| 제거 항목 | 조치 |
| --- | --- |
| linkPreview prop | 제거. URL 붙여넣기는 이제 항상 인라인 링크로 처리됩니다 |
| @lumir-company/editor/api/link-preview 서브패스 | 제거. 해당 API 라우트/import 삭제 |
| htmlPreview 블록 · HtmlPreviewBlock export | 제거. 저장 JSON에 남아 있어도 로드는 무에러(알 수 없는 블록은 안전하게 폴백) |
| 독립 FloatingMenu 컴포넌트 | <LumirEditor floatingMenu floatingMenuPosition="sticky|fixed" /> prop으로 대체(기존 export는 null 렌더 스텁으로 유지) |
| 독립 FontSizeButton 컴포넌트 | 포매팅 툴바에 내장(기존 export는 null 렌더 스텁) |
위 제거 항목을 참조하지 않는 프로젝트라면 별도 조치 없이 그대로 동작합니다.
변경 이력
v0.5.7 (2026-07-15)
- 3단 컬럼: 슬래시 메뉴에
/3단 컬럼·/3단 컬럼 (구분선)추가(기존 2단과 함께). 로드·라운드트립에서 최대 3단 보존, 4단 이상은 마지막 컬럼으로 병합해 3단 정규화. 슬래시 메뉴 컬럼 아이콘을 단 수(2·3)·구분선 유무에 따라 구분. - 컬럼 탈출(Enter 두 번): 컬럼 맨 마지막 블록에서 Enter를 두 번 누르면 컬럼 아래에 일반 문단을 만들고 포커스(1·2·3단 어느 컬럼에서든). 코드블록·토글 탈출과 동일한 UX.
- 빈 컬럼 방지(수정): 컬럼의 마지막 블록을 다른 컬럼으로 드래그해 옮겨도 원래 컬럼이 사라지거나 드롭 불가가 되지 않도록 빈 문단 플레이스홀더 유지. 빈 플레이스홀더만 있는 컬럼에 드롭하면 불필요한 빈 줄 없이 교체.
- 컬럼 구분선·그립 정렬: 구분선을 오른쪽 컬럼 콘텐츠 옆에 붙이고 블록 그립(⠿)을 콘텐츠에 밀착, 구분선 좌우 여백을 균형 있게 배치. 구분선 색은
--lumir-column-divider-colorCSS 변수로 커스터마이즈.
v0.5.6 (2026-07-13)
- 여러 줄(크로스블록) 선택 + 일괄 서식: 블록 경계를 넘는 마우스 드래그로 여러 줄 선택(첫/끝 줄 중간의 부분 선택 포함), 선택 범위 전체에 굵기·색·크기 등 인라인 서식을 한 번에 적용(전 범위 균일 토글 — Word/Notion 정합). Shift+클릭 확장,
Ctrl+B/I/U지원. 일괄 서식은 단일 Undo로 원복. - 선택 영역 편집: 타이핑(선택 대체) ·
Backspace/Delete(삭제·블록 병합, 완전 커버 시 빈 블록 정리) ·Enter(삭제 후 분할) · 붙여넣기(선택 대체) · 부분 복사/잘라내기(무손실 직렬화). 한글(IME) 입력 포함 모든 동작이 단일 Undo 단위. Ctrl/Cmd+A전체선택 서식: 문서 전체 선택 상태에서도 서식 툴바 표시·일괄 적용. 서식 툴바는 화면 세로 1/4 지점에 고정하되 에디터 영역을 벗어나지 않게 클램프.- v1 범위: 표·2단 컬럼·중첩 목록 경계를 건드리는 선택은 대상에서 제외(기존 동작 보존).
v0.5.3 (2026-07-07)
- 표 열 리사이즈 폭 튐 수정: 내부 열 경계 드래그 시 표 전체 폭이 변하던 문제 해결(드래그 시작 시 유효한
columnWidths를 재측정 없이 사용, 측정 경로는 병합/colspan 안전한 경계 기반으로 교체,min-width정합). - 리사이즈 드래그 피드백: 드래그 중에도 리사이즈 커서·경계 하이라이트 유지(
.resizing) + 하이라이트가 이동하는 경계를 실시간 추종. 행 리사이즈 후 핸들 재배치 및 최소높이에서 보더·하이라이트 동시 정지. - 표 폭 스케일 복원:
fitWidths가 예산에 정확히 도달하도록 보정 — 표 폭을 줄였다가 다시 늘릴 때 최대폭(최초 채움 폭)으로 복원. - 에디터 표 폭 정합: 표가 콘텐츠 영역 좌·우변에 flush(네이티브 표와 정렬).
- 플로팅 툴바 표시 수정: 표·서식 툴바
z-index상향 — 호스트 앱의position:fixed컨테이너(예:z-index:100) 위에 정상 표시. - 블록 그립 노출 개선: 좌측 거터에 마우스를 올려도 해당 블록의 드래그 그립(⠿)이 뜨도록(블록 콘텐츠 밖에서도 노출).
- (구조) 내부 리사이저 세그먼트화 + 행/열 이동 그립·드래그 재정렬 제거.
v0.5.2 (2026-07-06)
- 링크 카드·임베드 블록(
bookmark): 인라인 링크에 hover → 링크 툴바의 “카드/임베드” 버튼으로 전환. 카드는fetchLinkPreview(url)콜백(서버 프록시 권장)으로 제목·설명·썸네일을 채우고, 임베드는 YouTube/Vimeo 만 허용(도메인 화이트리스트 +sandboxiframe). 붙여넣기는 기존대로 인라인 링크 유지(부작용 최소). - 카드/임베드 폭 리사이즈: 좌/우 핸들 드래그로
previewWidth조절(이미지/비디오와 동일 UX). 임베드는 16:9 비율 유지, 읽기전용·모바일에선 핸들 숨김. - 상단 고정 툴바(FloatingMenu) 모바일 개선: 모바일 폭(<440px) 진입 시 기본으로 펼쳐진 상태로 표시(접기 토글은 유지).
- 표 전체 선택/해제: 셀에서
Ctrl/Cmd+A전체 셀 선택,Esc로 선택 해제. - 표 모바일 가로 스크롤: 모바일(≤640px)에서 표가 편집영역보다 넓으면 열 압축 대신 가로 스크롤(데스크톱은 기존 fit-to-width 유지).
- 수정: 링크 툴바 버튼(편집/카드/임베드) 세로 줄바뀜 교정.
- 커스텀
bookmark블록은 HTML/Markdown 내보내기 시 링크로 degrade(라운드트립 안전).
v0.5.1 (2026-07-03)
- 키보드 블록 내비게이션: 방향키(←→↑↓)로 블록 간 캐럿 이동,
Ctrl/Cmd+A단계 전체선택(블록 텍스트 → 전체 블록 선택 모드: 삭제/복사). - 코드블록 탈출: 빈 마지막 줄에서 Enter 2번 → 하단 문단으로 탈출. 후행 개행 렌더 보정.
- 인라인 코드 탈출: →/← 및 스페이스 2번으로 코드 밖 평문 이어쓰기. Enter 분할 시 빈
<code>잔존 제거. - 표 셀 방향키: 상하좌우 셀 이동, 첫 행 ↑·마지막 행 ↓로 표 밖 인접 블록 탈출, 셀 Enter 줄바꿈(
<br>). - 디바이더: 점선 → 검정 실선. 목록 마커: 절대배치로 커서가 마커 앞에 붙던 문제 교정.
v0.5.0
- 엔진 교체: BlockNote/ProseMirror/React 런타임 제거 → vanilla(의존성 0) 코어. 저장 JSON은 BlockNote 형식과 라운드트립 호환 유지.
- 드롭인 대체: 구
@lumir-company/[email protected]와 동일한 컴포넌트·props·타입·s3Upload계약. 코드 변경 없이 버전만 상향. - react-free 코어(
./core) +mountLumirEditor()공개: 서버 컴포넌트·비-React 환경에서 명령형 사용. - 직렬화 유틸 공개 export: 블록 JSON ↔ HTML ↔ Markdown ↔ DOM 변환 함수, 표 모델·색상 유틸.
- 패키징: ESM + CJS +
.d.ts+ 단일 번들 CSS.././react/./core/./style.css서브패스. - 제거:
linkPreview(prop·/api/link-preview서브패스),htmlPreview블록/HtmlPreviewBlock. - 링크 동작 변경: URL 붙여넣기 → 항상 인라인 링크, Notion식 링크 툴바(hover 툴팁 → 편집 popup).
v0.4.23 (2026-06-22)
- 다중 블록 선택 시 글자 크기 스테퍼 누적 버그 수정(
readSelectionFontSize+ 낙관적 갱신). - 툴바 드롭다운 조작 시 선택 하이라이트 유지(
InactiveSelectionExtension,--lumir-inactive-selection).
v0.4.22 (2026-06-22)
- 글자 크기 1px 스테퍼 + 직접 입력(8~96px) 추가.
FONT_SIZE_MIN/MAX/DEFAULT_PX/STEP,parseFontSizePx/clampFontSizePx/toFontSizeValue공개. - 글자 크기 스타일을 동기 스펙으로 변경(적용 직후 툴바가 (0,0)으로 튀는 문제 수정).
v0.4.21 (2026-06-18)
- Word/docx 표 붙여넣기 개선: 에디터 폭 자동 맞춤, 셀 서식(글자 크기·세로 정렬 포함) 보존 확대.
v0.4.20 (2026-06-18)
- 2단 컬럼 구분선을 생성 시 블록별로 선택·고정(
showDivider라운드트립). - 표 셀에서
Ctrl/Cmd+A→ 표 전체 선택(재입력 시 문서 전체로 확장).
v0.4.19 (2026-06-17)
- 2단 컬럼 중앙 세로 구분선 전역 옵션(
columnDivider), CSS 변수 제어. - 표 전체 종횡비 고정 스케일(우하단 코너 드래그),
tableHandles로 게이트.
v0.4.18 (2026-06-17)
- 표 열 삭제 버그 수정(병합 셀·첫 열 포함), 병합 셀 collapse 시 행 높이 보존.
v0.4.17 (2026-06-16)
- 표 행 높이 리사이즈(
rowHeight라운드트립), 표 블록 정렬(좌/가운데/우), 표 하단 여백 축소. - 2단(다단) 컬럼 레이아웃 신규(슬래시 메뉴·블록 DnD, MIT 자체 구현).
v0.4.16 (2026-06-05)
- 인라인 글자 크기(기본 + 10~28px 8단계), 구버전 호환 형제 키 직렬화,
liftFontSize/lowerFontSize·SerializedStyledText공개. floatingMenu사용 시 중복 팝업 툴바 억제.
v0.4.15 (2026-06-05)
- Notion 스타일 테이블 셀 색상·정렬·포커스 핸들(
LumirTableHandlesController), 다중 셀 일괄 색 적용.
v0.4.14 (2026-05-29)
- Excel/스프레드시트 셀 붙여넣기 → 편집 가능한 테이블(클립보드
<table>우선 파싱, 서식 매핑).
v0.4.13 (2026-04-03)
@tiptap/core외부화로 중복 인스턴스 런타임 에러 수정.
v0.4.12 (2026-04-03)
- Numbered/Bullet List 글자 크기 14px로 통일.
v0.4.10 (2026-03-18)
- 업로드 진행률 보간 표시(
onProgress0→…→100 부드럽게 갱신).
v0.4.9 (2026-03-17)
maxImageFileSize/maxVideoFileSize추가.
v0.4.5 (2026-03-06)
- 이미지·동영상 업로드 문서화(형식/용량/삽입 경로/삭제 콜백/에러 처리).
v0.4.3 (2026-02-23)
- 이미지 삭제 콜백(
onImageDelete) 추가,placeholderprop 추가, URL 프로토콜 보안 강화.
v0.4.2 (2026-02-23)
LumirEditorError커스텀 에러 클래스·onError콜백 추가, 코드 구조 리팩토링.
v0.4.1 (2026-01-15)
preserveExtension추가. Breaking:fileNameTransform이 확장자 제외 이름(nameWithoutExt)을 전달.
v0.4.0 (2026-01-15)
fileNameTransform·appendUUID추가, 다중 이미지 업로드 중복 해결.
라이선스
MIT
