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

k-editor-korean

v0.1.0

Published

Korean-focused WYSIWYG Editor for Next.js with enhanced Korean support and image upload

Readme

K-Editor

Next.js에서 쓸 에디터를 찾다가 하나 만듦 바이브코딩으로 만든거라 버그가 많을 수 있음

기능

  • 한글 입력 완벽 지원: IME 조합 문자 처리 및 한글 특화 기능
  • 이미지 업로드: 드래그 앤 드롭, 붙여넣기, 파일 선택 지원
  • 다양한 서식: 텍스트 스타일, 색상, 정렬, 목록 등
  • 실행취소/다시실행: 완전한 히스토리 관리
  • 반응형 디자인: 모바일 및 데스크톱 지원
  • TypeScript: 완전한 타입 지원
  • 범용성: React 프로젝트에서도 사용 가능

설치

npm install k-editor
# or
yarn add k-editor
# or
pnpm add k-editor

사용법

기본 사용법

import KEditor, { EditorContent, ImageMetadata } from 'k-editor';

function MyComponent() {
  const [editorContent, setEditorContent] = useState<EditorContent | null>(null);
  
  const handleImageUpload = async (file: File): Promise<ImageMetadata> => {
    // 이미지를 서버에 업로드하고 메타데이터 반환
    const formData = new FormData();
    formData.append('image', file);
    
    const response = await fetch('/api/upload', {
      method: 'POST',
      body: formData,
    });
    
    const data = await response.json();
    
    return {
      id: data.id || `img_${Date.now()}`,
      fileName: file.name,
      url: data.url,
      filePath: data.filePath,
      fileKey: data.fileKey, // S3 키 등
      size: file.size,
      mimeType: file.type,
      width: data.width,
      height: data.height,
      uploadedAt: new Date(),
      lastUsed: new Date()
    };
  };
  
  const handleContentChange = (content: EditorContent) => {
    setEditorContent(content);
    
    console.log('HTML:', content.html);
    console.log('Images:', content.images);
    console.log('Text:', content.textContent);
    console.log('Words:', content.wordCount);
  };
  
  return (
    <KEditor
      initialValue={editorContent?.html || ''}
      onChange={handleContentChange}
      onImageUpload={handleImageUpload}
      config={{
        placeholder: '내용을 입력하세요...',
        locale: 'ko',
        theme: 'light',
      }}
    />
  );
}

고급 설정 및 호환성

import KEditor, { KEditorConfig, EditorContent, ImageMetadata } from 'k-editor';

const config: KEditorConfig = {
  placeholder: '여기에 텍스트를 입력하세요...',
  maxLength: 50000,
  allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
  maxFileSize: 5 * 1024 * 1024, // 5MB
  enableImageTracking: true,     // 이미지 추적 활성화
  autoCleanup: false,            // 자동 정리 비활성화 (운영환경에서 주의)
  autoSave: true,
  autoSaveInterval: 30000, // 30초
  locale: 'ko', // 'ko' | 'en' | 'auto'
  theme: 'light', // 'light' | 'dark' | 'auto'
};

function AdvancedEditor() {
  // 새로운 형식 (권장)
  const handleNewFormatChange = (content: EditorContent) => {
    console.log('HTML:', content.html);
    console.log('Images:', content.images.map(img => ({
      id: img.id,
      fileName: img.fileName,
      url: img.url,
      filePath: img.filePath,
      fileKey: img.fileKey,
      size: img.size
    })));
    
    // 데이터베이스 저장
    saveToDatabase({
      html: content.html,
      images: content.images,
      wordCount: content.wordCount
    });
  };
  
  // 기존 호환성 지원
  const handleLegacyChange = (htmlContent: string) => {
    console.log('Legacy format:', htmlContent);
  };
  
  const handleImageUpload = async (file: File): Promise<ImageMetadata> => {
    // 서버 업로드 및 메타데이터 반환
    const uploadResult = await uploadImageToServer(file);
    
    return {
      id: uploadResult.id,
      fileName: file.name,
      url: uploadResult.url,
      filePath: uploadResult.path,
      fileKey: uploadResult.s3Key,
      size: file.size,
      mimeType: file.type,
      width: uploadResult.dimensions.width,
      height: uploadResult.dimensions.height,
      uploadedAt: new Date(),
      lastUsed: new Date()
    };
  };
  
  const handleImageRemove = async (imageId: string): Promise<boolean> => {
    try {
      await fetch(`/api/delete-image/${imageId}`, { method: 'DELETE' });
      return true;
    } catch (error) {
      console.error('이미지 삭제 실패:', error);
      return false;
    }
  };

  return (
    <KEditor
      config={config}
      onChange={handleNewFormatChange}        // 새 형식
      onContentChange={handleLegacyChange}    // 기존 호환성
      onSelectionChange={(selection) => console.log('Selection:', selection)}
      onImageUpload={handleImageUpload}
      onImageRemove={handleImageRemove}
      className="my-editor"
      style={{ minHeight: '400px' }}
    />
  );
}

Next.js API Route 예시 (이미지 업로드)

// pages/api/upload.ts 또는 app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { writeFile } from 'fs/promises';
import sharp from 'sharp'; // 이미지 크기 추출용
import path from 'path';

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const file = formData.get('image') as File;
    
    if (!file) {
      return NextResponse.json({ error: 'No file received' }, { status: 400 });
    }
    
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
    
    // 파일 저장 경로
    const timestamp = Date.now();
    const fileExtension = path.extname(file.name);
    const baseName = path.basename(file.name, fileExtension);
    const filename = `${timestamp}-${baseName}${fileExtension}`;
    const filepath = path.join(process.cwd(), 'public/uploads', filename);
    
    await writeFile(filepath, buffer);
    
    // 이미지 크기 정보 추출
    let dimensions = { width: 0, height: 0 };
    try {
      const metadata = await sharp(buffer).metadata();
      dimensions = {
        width: metadata.width || 0,
        height: metadata.height || 0
      };
    } catch (error) {
      console.warn('이미지 메타데이터 추출 실패:', error);
    }
    
    // ImageMetadata 형식으로 응답
    return NextResponse.json({ 
      id: `img_${timestamp}_${baseName}`,
      fileName: file.name,
      url: `/uploads/${filename}`,
      filePath: `/uploads/${filename}`,
      fileKey: filename, // 삭제 시 사용
      size: file.size,
      mimeType: file.type,
      width: dimensions.width,
      height: dimensions.height,
      uploadedAt: new Date().toISOString(),
      lastUsed: new Date().toISOString(),
      message: 'File uploaded successfully' 
    });
  } catch (error) {
    return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
  }
}

컴포넌트 및 Hook

KEditor (메인 컴포넌트)

interface KEditorProps {
  config?: KEditorConfig;
  initialValue?: string;
  onChange?: (content: EditorContent) => void;       // 새 형식
  onContentChange?: (content: string) => void;       // 기존 호환성
  onSelectionChange?: (selection: { start: number; end: number }) => void;
  onImageUpload?: (file: File) => Promise<ImageMetadata>; // 메타데이터 반환
  onImageRemove?: (imageId: string) => Promise<boolean>;  // 이미지 삭제
  className?: string;
  style?: React.CSSProperties;
  disabled?: boolean;
  readOnly?: boolean;
}

// EditorContent 구조
interface EditorContent {
  html: string;           // HTML 콘텐츠
  images: ImageMetadata[]; // 사용된 이미지들
  textContent?: string;   // 순수 텍스트 (검색용)
  wordCount?: number;     // 단어 수
  imageCount?: number;    // 이미지 개수
}

// ImageMetadata 구조  
interface ImageMetadata {
  id: string;           // 고유 식별자
  fileName: string;     // 원본 파일명
  url: string;          // 접근 가능한 URL
  filePath: string;     // 서버상 파일 경로
  fileKey?: string;     // S3 키 또는 삭제용 키
  size?: number;        // 파일 크기 (bytes)
  mimeType?: string;    // MIME 타입
  width?: number;       // 이미지 너비
  height?: number;      // 이미지 높이
  uploadedAt: Date;     // 업로드 시간
  lastUsed: Date;       // 마지막 사용 시간
}

useEditorState (상태 관리 Hook)

import { useEditorState } from 'k-editor';

function CustomEditor() {
  const {
    state,
    updateContent,
    updateSelection,
    undo,
    redo,
    canUndo,
    canRedo,
  } = useEditorState('초기 내용');
  
  // 커스텀 로직...
}

useEditorCommands (명령 실행 Hook)

import { useEditorCommands } from 'k-editor';

function CustomToolbar({ state, setState }) {
  const { executeCommand } = useEditorCommands(state, setState);
  
  return (
    <button onClick={() => executeCommand({ type: 'bold' })}>
      굵게
    </button>
  );
}

설정 옵션

KEditorConfig

interface KEditorConfig {
  placeholder?: string;           // 플레이스홀더 텍스트
  maxLength?: number;             // 최대 글자 수
  allowedFileTypes?: string[];    // 허용되는 이미지 타입
  maxFileSize?: number;           // 최대 파일 크기 (bytes)
  imageUploadUrl?: string;        // 이미지 업로드 URL (선택사항)
  onImageUpload?: (file: File) => Promise<string>; // 커스텀 업로드 핸들러
  autoSave?: boolean;             // 자동 저장 사용 여부
  autoSaveInterval?: number;      // 자동 저장 간격 (ms)
  locale?: 'ko' | 'en' | 'auto'; // 언어 설정
  theme?: 'light' | 'dark' | 'auto'; // 테마
}

이미지 업로드 및 최적화

🎯 권장 이미지 업로드 전략

K-Editor는 이미지를 DB에 직접 저장하지 않습니다. 대신 다음 방법들을 권장합니다:

1. 클라우드 스토리지 + CDN (권장)

import { compressImage } from 'k-editor/utils/imageOptimization';

const handleImageUpload = async (file: File): Promise<string> => {
  // 1. 이미지 압축 (DB 부담 줄이기)
  const compressed = await compressImage(file, {
    maxWidth: 1920,
    maxHeight: 1080,
    quality: 0.8,
    format: 'jpeg'
  });

  // 2. 클라우드 스토리지에 업로드
  const formData = new FormData();
  formData.append('image', compressed, file.name);
  
  const response = await fetch('/api/upload-to-s3', {
    method: 'POST',
    body: formData,
  });
  
  const { cdnUrl } = await response.json();
  return cdnUrl; // https://cdn.example.com/images/abc123.jpg
};

2. 서버 스토리지 방식

const handleImageUpload = async (file: File): Promise<string> => {
  const formData = new FormData();
  formData.append('image', file);
  
  const response = await fetch('/api/upload-image', {
    method: 'POST',
    body: formData,
  });
  
  const { url } = await response.json();
  return url; // /uploads/image-123.jpg
};

🛠 이미지 최적화 유틸리티

import { compressImage, getImageDimensions, formatFileSize } from 'k-editor/utils/imageOptimization';

// 이미지 압축
const compressed = await compressImage(file, {
  maxWidth: 1920,
  maxHeight: 1080,
  quality: 0.8,
  format: 'jpeg' // 또는 'webp', 'png'
});

// 이미지 크기 확인
const { width, height } = await getImageDimensions(file);
console.log(`원본 크기: ${width}x${height}`);

// 파일 크기 포맷팅
console.log(`압축 전: ${formatFileSize(file.size)}`);
console.log(`압축 후: ${formatFileSize(compressed.size)}`);

☁️ AWS S3 업로드 예시 (Next.js API Route)

// app/api/upload-to-s3/route.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { NextRequest, NextResponse } from 'next/server';

const s3Client = new S3Client({
  region: process.env.AWS_REGION,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function POST(request: NextRequest) {
  try {
    const formData = await request.formData();
    const file = formData.get('image') as File;
    
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
    
    const key = `images/${Date.now()}-${file.name}`;
    
    await s3Client.send(new PutObjectCommand({
      Bucket: process.env.S3_BUCKET_NAME,
      Key: key,
      Body: buffer,
      ContentType: file.type,
    }));
    
    const cdnUrl = `https://${process.env.CLOUDFRONT_DOMAIN}/${key}`;
    
    return NextResponse.json({ 
      cdnUrl,
      message: '업로드 성공'
    });
  } catch (error) {
    return NextResponse.json({ error: '업로드 실패' }, { status: 500 });
  }
}

📊 용량 최적화 비교

| 방식 | DB 용량 | 장점 | 단점 | |------|---------|------|------| | Base64 (비권장) | 33% 증가 | 단순함 | DB 부담, 느린 로딩 | | 서버 스토리지 | URL만 저장 | 빠른 구현 | 서버 디스크 관리 필요 | | 클라우드 + CDN | URL만 저장 | 확장성, CDN 캐싱 | 초기 설정 복잡 |

한글 처리 기능

import {
  isKorean,
  KoreanIMEHandler,
  disassembleHangul,
  assembleHangul
} from 'k-editor';

// 한글 문자 확인
console.log(isKorean('한')); // true
console.log(isKorean('A')); // false

// 한글 분해/조합
const decomposed = disassembleHangul('한');
console.log(decomposed); // { cho: 'ㅎ', jung: 'ㅏ', jong: 'ㄴ' }

const composed = assembleHangul('ㅎ', 'ㅏ', 'ㄴ');
console.log(composed); // '한'

스타일링

CSS 클래스

.k-editor {
  /* 에디터 컨테이너 */
}

.k-editor .toolbar {
  /* 툴바 스타일 */
}

.k-editor .editor-content {
  /* 편집 영역 스타일 */
}

.k-editor.dark {
  /* 다크 테마 스타일 */
}

커스텀 스타일

<KEditor
  className="my-custom-editor"
  style={{
    border: '2px solid #007bff',
    borderRadius: '12px',
    minHeight: '300px',
  }}
/>

개발 및 빌드

# 개발 서버 실행
npm run dev

# 라이브러리 빌드
npm run build:lib

# Next.js 앱 빌드
npm run build

# 테스트 실행
npm test

브라우저 지원

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • 모바일 브라우저 (iOS Safari, Chrome Mobile)

라이센스

MIT

기여하기

  1. Fork the repository
  2. Create your feature branch
  3. Commit your changes
  4. Push to the branch
  5. Create a Pull Request

문제 신고

GitHub Issues를 통해 버그 리포트나 기능 요청을 해주세요.

파일 관리 및 정리

🧹 자동 파일 정리

문제: 포스트 삭제나 이미지 변경 시 고아 파일(orphaned files)이 누적되어 스토리지 낭비

해결책: 이미지 참조 추적 및 자동 정리 시스템

기본 설정

<KEditor
  config={{
    enableImageTracking: true,    // 이미지 참조 추적 (기본값: true)
    autoCleanup: true,           // 자동 정리 활성화 (기본값: false)
    autoCleanupInterval: 24,     // 24시간마다 정리 (기본값: 24)
    onImageDelete: async (url) => {
      // 커스텀 삭제 로직
      const response = await fetch('/api/delete-image', {
        method: 'DELETE',
        body: JSON.stringify({ url }),
      });
      return response.ok;
    }
  }}
/>

수동 파일 정리

import { ImageCleanupPanel } from 'k-editor';

function AdminPage() {
  return (
    <div>
      <h2>파일 관리</h2>
      <ImageCleanupPanel
        onCleanupComplete={(result) => {
          console.log(`${result.deletedFiles.length}개 파일 삭제 완료`);
        }}
      />
    </div>
  );
}

프로그래밍 방식 정리

import { globalImageManager, deleteFiles } from 'k-editor';

// 포스트 삭제 시
const deletePost = async (postId: string) => {
  // 1. 포스트의 이미지 참조 제거 및 고아 파일 목록 가져오기
  const orphanedUrls = globalImageManager.removePostImages(postId);
  
  // 2. 실제 파일 삭제
  const result = await deleteFiles(orphanedUrls);
  
  // 3. 포스트 데이터 삭제
  await deletePostFromDB(postId);
  
  console.log(`포스트 삭제 완료: ${result.deletedFiles.length}개 이미지 정리`);
};

// 이미지 참조 통계 확인
const stats = globalImageManager.getStatistics();
console.log(`총 ${stats.totalImages}개 이미지, ${stats.totalSize} 사용 중`);

// 정리 대상 파일 확인
const candidates = globalImageManager.getCleanupCandidates({ maxAge: 30 });
console.log(`${candidates.length}개 파일 정리 가능`);

🔧 파일 삭제 API 구현

Next.js API Route

// pages/api/delete-image.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { unlink, access } from 'fs/promises';
import path from 'path';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'DELETE') {
    return res.status(405).json({ success: false, message: 'Method not allowed' });
  }

  const { url } = req.body;
  const filePath = path.join(process.cwd(), 'public', url);
  
  try {
    await access(filePath); // 파일 존재 확인
    await unlink(filePath); // 파일 삭제
    
    res.json({ success: true, message: '파일 삭제 완료' });
  } catch (error) {
    res.status(500).json({ success: false, message: '파일 삭제 실패' });
  }
}

AWS S3 삭제 예시

// pages/api/delete-s3-image.ts
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';

const s3Client = new S3Client({ region: process.env.AWS_REGION });

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { url } = req.body;
  
  // URL에서 S3 키 추출
  const key = url.replace(`https://${process.env.CLOUDFRONT_DOMAIN}/`, '');
  
  try {
    await s3Client.send(new DeleteObjectCommand({
      Bucket: process.env.S3_BUCKET_NAME,
      Key: key,
    }));
    
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
}

📊 스토리지 최적화 전략

| 전략 | 설명 | 장점 | 단점 | |------|------|------|------| | 실시간 추적 | 이미지 사용/삭제를 실시간으로 추적 | 정확한 참조 관리 | 약간의 성능 오버헤드 | | 배치 정리 | 정기적으로 사용되지 않는 파일 정리 | 안전하고 효율적 | 일시적 스토리지 사용량 증가 | | 수명 관리 | 파일 업로드 후 일정 기간 후 자동 삭제 | 간단한 구현 | 필요한 파일도 삭제될 위험 |

🛡️ 안전 기능

  • Dry Run 모드: 실제 삭제 전 테스트 가능
  • 백업 확인: 삭제 전 파일 백업 여부 확인
  • 복구 기능: 실수로 삭제된 파일 복구 가능
  • 권한 검증: 삭제 권한이 있는 파일만 처리

변경사항

v0.3.0

  • 🔄 개선된 반환 형식: HTML + 이미지 메타데이터 배열로 변경
  • 📊 이미지 메타데이터: 파일명, 경로, 키, 크기, 삭제 정보 포함
  • 🔧 향상된 콜백: onChangeEditorContent 객체 반환
  • ⚡ 호환성 지원: onContentChange로 기존 코드 호환
  • 🏷️ 이미지 ID: 각 이미지에 고유 식별자 자동 할당
  • 📈 통계 기능: 단어 수, 이미지 수, 텍스트 추출 자동

v0.2.0

  • 이미지 참조 추적 시스템 추가
  • 자동 파일 정리 기능 구현
  • 파일 삭제 API 및 관리 패널 추가
  • 스토리지 최적화 및 고아 파일 방지

v0.1.0

  • 초기 릴리스
  • 한글 입력 지원
  • 이미지 업로드 기능
  • 기본 WYSIWYG 기능