npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kingdoo/editor

v0.3.3

Published

Image-only BlockNote rich text editor with S3 upload and optimized loading spinner

Readme

LumirEditor

🖼️ 이미지 전용 BlockNote 기반 Rich Text 에디터

npm version License: MIT

📋 목차


✨ 핵심 특징

| 특징 | 설명 | | ------------------------ | ----------------------------------------------------------- | | 🖼️ 이미지 전용 | 이미지 업로드/드래그앤드롭만 지원 (비디오/오디오/파일 제거) | | ☁️ 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/editor

Peer 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.png

API 엔드포인트 응답 예시:

{
  "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);

업로드 우선순위

  1. uploadFile prop이 있으면 우선 사용
  2. uploadFile이 없고 s3Upload가 있으면 S3 업로드 사용
  3. 둘 다 없으면 업로드 실패

🛠️ 유틸리티 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",
  }}
/>

성능 최적화 팁

  1. 애니메이션 기본 비활성: 이미 animations: false로 설정되어 성능 최적화됨
  2. 큰 콘텐츠 처리: 초기 콘텐츠가 클 경우 lazy loading 고려
  3. 이미지 최적화: 업로드 전 클라이언트에서 이미지 리사이징 권장

🏗️ 프로젝트 구조

@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


🔗 관련 링크