{"_id":"k-editor-korean","name":"k-editor-korean","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"k-editor-korean","version":"0.1.0","description":"Korean-focused WYSIWYG Editor for Next.js with enhanced Korean support and image upload","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"dev":"next dev","build":"next build && npm run build:lib","build:lib":"tsup src/lib/index.ts --dts --format cjs,esm --tsconfig tsconfig.lib.json","start":"next start","lint":"next lint","test":"jest"},"dependencies":{"@types/node":"^20.0.0","@types/react":"^18.0.0","@types/react-dom":"^18.0.0","next":"^14.0.0","react":">=16.8.0","react-dom":">=16.8.0","styled-jsx":"^5.1.7","typescript":"^5.0.0"},"devDependencies":{"@testing-library/jest-dom":"^6.0.0","@testing-library/react":"^14.0.0","@types/styled-jsx":"^2.2.9","eslint":"^8.0.0","eslint-config-next":"^14.0.0","jest":"^29.0.0","jest-environment-jsdom":"^29.0.0","tsup":"^8.0.0"},"peerDependencies":{"react":">=16.8.0","react-dom":">=16.8.0"},"keywords":["k-editor","korean-editor","wysiwyg","editor","nextjs","react","korean","한글","rich-text","contenteditable","ime","hangul"],"author":{"name":"inmedi"},"license":"MIT","_id":"k-editor-korean@0.1.0","_nodeVersion":"23.10.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-Nw6XH+s2n/bLAkEkbYfxfc9sSJVnFYlP4Li1Jp5wdYWBAKBV0H76FOGOiC3OpMs6MHvc39xSmVNdaMl9dCF2gA==","shasum":"54f01a009057c96cdfc7b95c6863b99942efa05f","tarball":"https://registry.npmjs.org/k-editor-korean/-/k-editor-korean-0.1.0.tgz","fileCount":8,"unpackedSize":872933,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICA7gcStARjEvFmzi1kFFECmxdny4nl1VEqY93hOHS8iAiAwkpqxHenfZxsHCgnuZKnvoZQhkUncpcG+f7w8J2Zayw=="}]},"_npmUser":{"name":"inmedi","email":"inmedi_17@naver.com"},"directories":{},"maintainers":[{"name":"inmedi","email":"inmedi_17@naver.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/k-editor-korean_0.1.0_1769850761079_0.3184968679769551"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-31T09:12:41.078Z","0.1.0":"2026-01-31T09:12:41.243Z","modified":"2026-01-31T09:12:41.449Z"},"maintainers":[{"name":"inmedi","email":"inmedi_17@naver.com"}],"description":"Korean-focused WYSIWYG Editor for Next.js with enhanced Korean support and image upload","keywords":["k-editor","korean-editor","wysiwyg","editor","nextjs","react","korean","한글","rich-text","contenteditable","ime","hangul"],"author":{"name":"inmedi"},"license":"MIT","readme":"# K-Editor\n\nNext.js에서 쓸 에디터를 찾다가 하나 만듦\n바이브코딩으로 만든거라 버그가 많을 수 있음\n\n## 기능\n\n- ✅ **한글 입력 완벽 지원**: IME 조합 문자 처리 및 한글 특화 기능\n- ✅ **이미지 업로드**: 드래그 앤 드롭, 붙여넣기, 파일 선택 지원\n- ✅ **다양한 서식**: 텍스트 스타일, 색상, 정렬, 목록 등\n- ✅ **실행취소/다시실행**: 완전한 히스토리 관리\n- ✅ **반응형 디자인**: 모바일 및 데스크톱 지원\n- ✅ **TypeScript**: 완전한 타입 지원\n- ✅ **범용성**: React 프로젝트에서도 사용 가능\n\n## 설치\n\n```bash\nnpm install k-editor\n# or\nyarn add k-editor\n# or\npnpm add k-editor\n```\n\n## 사용법\n\n### 기본 사용법\n\n```tsx\nimport KEditor, { EditorContent, ImageMetadata } from 'k-editor';\n\nfunction MyComponent() {\n  const [editorContent, setEditorContent] = useState<EditorContent | null>(null);\n  \n  const handleImageUpload = async (file: File): Promise<ImageMetadata> => {\n    // 이미지를 서버에 업로드하고 메타데이터 반환\n    const formData = new FormData();\n    formData.append('image', file);\n    \n    const response = await fetch('/api/upload', {\n      method: 'POST',\n      body: formData,\n    });\n    \n    const data = await response.json();\n    \n    return {\n      id: data.id || `img_${Date.now()}`,\n      fileName: file.name,\n      url: data.url,\n      filePath: data.filePath,\n      fileKey: data.fileKey, // S3 키 등\n      size: file.size,\n      mimeType: file.type,\n      width: data.width,\n      height: data.height,\n      uploadedAt: new Date(),\n      lastUsed: new Date()\n    };\n  };\n  \n  const handleContentChange = (content: EditorContent) => {\n    setEditorContent(content);\n    \n    console.log('HTML:', content.html);\n    console.log('Images:', content.images);\n    console.log('Text:', content.textContent);\n    console.log('Words:', content.wordCount);\n  };\n  \n  return (\n    <KEditor\n      initialValue={editorContent?.html || ''}\n      onChange={handleContentChange}\n      onImageUpload={handleImageUpload}\n      config={{\n        placeholder: '내용을 입력하세요...',\n        locale: 'ko',\n        theme: 'light',\n      }}\n    />\n  );\n}\n```\n\n### 고급 설정 및 호환성\n\n```tsx\nimport KEditor, { KEditorConfig, EditorContent, ImageMetadata } from 'k-editor';\n\nconst config: KEditorConfig = {\n  placeholder: '여기에 텍스트를 입력하세요...',\n  maxLength: 50000,\n  allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],\n  maxFileSize: 5 * 1024 * 1024, // 5MB\n  enableImageTracking: true,     // 이미지 추적 활성화\n  autoCleanup: false,            // 자동 정리 비활성화 (운영환경에서 주의)\n  autoSave: true,\n  autoSaveInterval: 30000, // 30초\n  locale: 'ko', // 'ko' | 'en' | 'auto'\n  theme: 'light', // 'light' | 'dark' | 'auto'\n};\n\nfunction AdvancedEditor() {\n  // 새로운 형식 (권장)\n  const handleNewFormatChange = (content: EditorContent) => {\n    console.log('HTML:', content.html);\n    console.log('Images:', content.images.map(img => ({\n      id: img.id,\n      fileName: img.fileName,\n      url: img.url,\n      filePath: img.filePath,\n      fileKey: img.fileKey,\n      size: img.size\n    })));\n    \n    // 데이터베이스 저장\n    saveToDatabase({\n      html: content.html,\n      images: content.images,\n      wordCount: content.wordCount\n    });\n  };\n  \n  // 기존 호환성 지원\n  const handleLegacyChange = (htmlContent: string) => {\n    console.log('Legacy format:', htmlContent);\n  };\n  \n  const handleImageUpload = async (file: File): Promise<ImageMetadata> => {\n    // 서버 업로드 및 메타데이터 반환\n    const uploadResult = await uploadImageToServer(file);\n    \n    return {\n      id: uploadResult.id,\n      fileName: file.name,\n      url: uploadResult.url,\n      filePath: uploadResult.path,\n      fileKey: uploadResult.s3Key,\n      size: file.size,\n      mimeType: file.type,\n      width: uploadResult.dimensions.width,\n      height: uploadResult.dimensions.height,\n      uploadedAt: new Date(),\n      lastUsed: new Date()\n    };\n  };\n  \n  const handleImageRemove = async (imageId: string): Promise<boolean> => {\n    try {\n      await fetch(`/api/delete-image/${imageId}`, { method: 'DELETE' });\n      return true;\n    } catch (error) {\n      console.error('이미지 삭제 실패:', error);\n      return false;\n    }\n  };\n\n  return (\n    <KEditor\n      config={config}\n      onChange={handleNewFormatChange}        // 새 형식\n      onContentChange={handleLegacyChange}    // 기존 호환성\n      onSelectionChange={(selection) => console.log('Selection:', selection)}\n      onImageUpload={handleImageUpload}\n      onImageRemove={handleImageRemove}\n      className=\"my-editor\"\n      style={{ minHeight: '400px' }}\n    />\n  );\n}\n```\n\n### Next.js API Route 예시 (이미지 업로드)\n\n```typescript\n// pages/api/upload.ts 또는 app/api/upload/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\nimport { writeFile } from 'fs/promises';\nimport sharp from 'sharp'; // 이미지 크기 추출용\nimport path from 'path';\n\nexport async function POST(request: NextRequest) {\n  try {\n    const formData = await request.formData();\n    const file = formData.get('image') as File;\n    \n    if (!file) {\n      return NextResponse.json({ error: 'No file received' }, { status: 400 });\n    }\n    \n    const bytes = await file.arrayBuffer();\n    const buffer = Buffer.from(bytes);\n    \n    // 파일 저장 경로\n    const timestamp = Date.now();\n    const fileExtension = path.extname(file.name);\n    const baseName = path.basename(file.name, fileExtension);\n    const filename = `${timestamp}-${baseName}${fileExtension}`;\n    const filepath = path.join(process.cwd(), 'public/uploads', filename);\n    \n    await writeFile(filepath, buffer);\n    \n    // 이미지 크기 정보 추출\n    let dimensions = { width: 0, height: 0 };\n    try {\n      const metadata = await sharp(buffer).metadata();\n      dimensions = {\n        width: metadata.width || 0,\n        height: metadata.height || 0\n      };\n    } catch (error) {\n      console.warn('이미지 메타데이터 추출 실패:', error);\n    }\n    \n    // ImageMetadata 형식으로 응답\n    return NextResponse.json({ \n      id: `img_${timestamp}_${baseName}`,\n      fileName: file.name,\n      url: `/uploads/${filename}`,\n      filePath: `/uploads/${filename}`,\n      fileKey: filename, // 삭제 시 사용\n      size: file.size,\n      mimeType: file.type,\n      width: dimensions.width,\n      height: dimensions.height,\n      uploadedAt: new Date().toISOString(),\n      lastUsed: new Date().toISOString(),\n      message: 'File uploaded successfully' \n    });\n  } catch (error) {\n    return NextResponse.json({ error: 'Upload failed' }, { status: 500 });\n  }\n}\n```\n\n## 컴포넌트 및 Hook\n\n### KEditor (메인 컴포넌트)\n\n```tsx\ninterface KEditorProps {\n  config?: KEditorConfig;\n  initialValue?: string;\n  onChange?: (content: EditorContent) => void;       // 새 형식\n  onContentChange?: (content: string) => void;       // 기존 호환성\n  onSelectionChange?: (selection: { start: number; end: number }) => void;\n  onImageUpload?: (file: File) => Promise<ImageMetadata>; // 메타데이터 반환\n  onImageRemove?: (imageId: string) => Promise<boolean>;  // 이미지 삭제\n  className?: string;\n  style?: React.CSSProperties;\n  disabled?: boolean;\n  readOnly?: boolean;\n}\n\n// EditorContent 구조\ninterface EditorContent {\n  html: string;           // HTML 콘텐츠\n  images: ImageMetadata[]; // 사용된 이미지들\n  textContent?: string;   // 순수 텍스트 (검색용)\n  wordCount?: number;     // 단어 수\n  imageCount?: number;    // 이미지 개수\n}\n\n// ImageMetadata 구조  \ninterface ImageMetadata {\n  id: string;           // 고유 식별자\n  fileName: string;     // 원본 파일명\n  url: string;          // 접근 가능한 URL\n  filePath: string;     // 서버상 파일 경로\n  fileKey?: string;     // S3 키 또는 삭제용 키\n  size?: number;        // 파일 크기 (bytes)\n  mimeType?: string;    // MIME 타입\n  width?: number;       // 이미지 너비\n  height?: number;      // 이미지 높이\n  uploadedAt: Date;     // 업로드 시간\n  lastUsed: Date;       // 마지막 사용 시간\n}\n```\n\n### useEditorState (상태 관리 Hook)\n\n```tsx\nimport { useEditorState } from 'k-editor';\n\nfunction CustomEditor() {\n  const {\n    state,\n    updateContent,\n    updateSelection,\n    undo,\n    redo,\n    canUndo,\n    canRedo,\n  } = useEditorState('초기 내용');\n  \n  // 커스텀 로직...\n}\n```\n\n### useEditorCommands (명령 실행 Hook)\n\n```tsx\nimport { useEditorCommands } from 'k-editor';\n\nfunction CustomToolbar({ state, setState }) {\n  const { executeCommand } = useEditorCommands(state, setState);\n  \n  return (\n    <button onClick={() => executeCommand({ type: 'bold' })}>\n      굵게\n    </button>\n  );\n}\n```\n\n## 설정 옵션\n\n### KEditorConfig\n\n```typescript\ninterface KEditorConfig {\n  placeholder?: string;           // 플레이스홀더 텍스트\n  maxLength?: number;             // 최대 글자 수\n  allowedFileTypes?: string[];    // 허용되는 이미지 타입\n  maxFileSize?: number;           // 최대 파일 크기 (bytes)\n  imageUploadUrl?: string;        // 이미지 업로드 URL (선택사항)\n  onImageUpload?: (file: File) => Promise<string>; // 커스텀 업로드 핸들러\n  autoSave?: boolean;             // 자동 저장 사용 여부\n  autoSaveInterval?: number;      // 자동 저장 간격 (ms)\n  locale?: 'ko' | 'en' | 'auto'; // 언어 설정\n  theme?: 'light' | 'dark' | 'auto'; // 테마\n}\n```\n\n## 이미지 업로드 및 최적화\n\n### 🎯 권장 이미지 업로드 전략\n\n**K-Editor는 이미지를 DB에 직접 저장하지 않습니다. 대신 다음 방법들을 권장합니다:**\n\n#### 1. 클라우드 스토리지 + CDN (권장)\n\n```tsx\nimport { compressImage } from 'k-editor/utils/imageOptimization';\n\nconst handleImageUpload = async (file: File): Promise<string> => {\n  // 1. 이미지 압축 (DB 부담 줄이기)\n  const compressed = await compressImage(file, {\n    maxWidth: 1920,\n    maxHeight: 1080,\n    quality: 0.8,\n    format: 'jpeg'\n  });\n\n  // 2. 클라우드 스토리지에 업로드\n  const formData = new FormData();\n  formData.append('image', compressed, file.name);\n  \n  const response = await fetch('/api/upload-to-s3', {\n    method: 'POST',\n    body: formData,\n  });\n  \n  const { cdnUrl } = await response.json();\n  return cdnUrl; // https://cdn.example.com/images/abc123.jpg\n};\n```\n\n#### 2. 서버 스토리지 방식\n\n```tsx\nconst handleImageUpload = async (file: File): Promise<string> => {\n  const formData = new FormData();\n  formData.append('image', file);\n  \n  const response = await fetch('/api/upload-image', {\n    method: 'POST',\n    body: formData,\n  });\n  \n  const { url } = await response.json();\n  return url; // /uploads/image-123.jpg\n};\n```\n\n### 🛠 이미지 최적화 유틸리티\n\n```tsx\nimport { compressImage, getImageDimensions, formatFileSize } from 'k-editor/utils/imageOptimization';\n\n// 이미지 압축\nconst compressed = await compressImage(file, {\n  maxWidth: 1920,\n  maxHeight: 1080,\n  quality: 0.8,\n  format: 'jpeg' // 또는 'webp', 'png'\n});\n\n// 이미지 크기 확인\nconst { width, height } = await getImageDimensions(file);\nconsole.log(`원본 크기: ${width}x${height}`);\n\n// 파일 크기 포맷팅\nconsole.log(`압축 전: ${formatFileSize(file.size)}`);\nconsole.log(`압축 후: ${formatFileSize(compressed.size)}`);\n```\n\n### ☁️ AWS S3 업로드 예시 (Next.js API Route)\n\n```typescript\n// app/api/upload-to-s3/route.ts\nimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { NextRequest, NextResponse } from 'next/server';\n\nconst s3Client = new S3Client({\n  region: process.env.AWS_REGION,\n  credentials: {\n    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,\n    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,\n  },\n});\n\nexport async function POST(request: NextRequest) {\n  try {\n    const formData = await request.formData();\n    const file = formData.get('image') as File;\n    \n    const bytes = await file.arrayBuffer();\n    const buffer = Buffer.from(bytes);\n    \n    const key = `images/${Date.now()}-${file.name}`;\n    \n    await s3Client.send(new PutObjectCommand({\n      Bucket: process.env.S3_BUCKET_NAME,\n      Key: key,\n      Body: buffer,\n      ContentType: file.type,\n    }));\n    \n    const cdnUrl = `https://${process.env.CLOUDFRONT_DOMAIN}/${key}`;\n    \n    return NextResponse.json({ \n      cdnUrl,\n      message: '업로드 성공'\n    });\n  } catch (error) {\n    return NextResponse.json({ error: '업로드 실패' }, { status: 500 });\n  }\n}\n```\n\n### 📊 용량 최적화 비교\n\n| 방식 | DB 용량 | 장점 | 단점 |\n|------|---------|------|------|\n| **Base64 (비권장)** | 33% 증가 | 단순함 | DB 부담, 느린 로딩 |\n| **서버 스토리지** | URL만 저장 | 빠른 구현 | 서버 디스크 관리 필요 |\n| **클라우드 + CDN** | URL만 저장 | 확장성, CDN 캐싱 | 초기 설정 복잡 |\n\n## 한글 처리 기능\n\n```tsx\nimport {\n  isKorean,\n  KoreanIMEHandler,\n  disassembleHangul,\n  assembleHangul\n} from 'k-editor';\n\n// 한글 문자 확인\nconsole.log(isKorean('한')); // true\nconsole.log(isKorean('A')); // false\n\n// 한글 분해/조합\nconst decomposed = disassembleHangul('한');\nconsole.log(decomposed); // { cho: 'ㅎ', jung: 'ㅏ', jong: 'ㄴ' }\n\nconst composed = assembleHangul('ㅎ', 'ㅏ', 'ㄴ');\nconsole.log(composed); // '한'\n```\n\n## 스타일링\n\n### CSS 클래스\n\n```css\n.k-editor {\n  /* 에디터 컨테이너 */\n}\n\n.k-editor .toolbar {\n  /* 툴바 스타일 */\n}\n\n.k-editor .editor-content {\n  /* 편집 영역 스타일 */\n}\n\n.k-editor.dark {\n  /* 다크 테마 스타일 */\n}\n```\n\n### 커스텀 스타일\n\n```tsx\n<KEditor\n  className=\"my-custom-editor\"\n  style={{\n    border: '2px solid #007bff',\n    borderRadius: '12px',\n    minHeight: '300px',\n  }}\n/>\n```\n\n## 개발 및 빌드\n\n```bash\n# 개발 서버 실행\nnpm run dev\n\n# 라이브러리 빌드\nnpm run build:lib\n\n# Next.js 앱 빌드\nnpm run build\n\n# 테스트 실행\nnpm test\n```\n\n## 브라우저 지원\n\n- Chrome/Edge 90+\n- Firefox 88+\n- Safari 14+\n- 모바일 브라우저 (iOS Safari, Chrome Mobile)\n\n## 라이센스\n\nMIT\n\n## 기여하기\n\n1. Fork the repository\n2. Create your feature branch\n3. Commit your changes\n4. Push to the branch\n5. Create a Pull Request\n\n## 문제 신고\n\nGitHub Issues를 통해 버그 리포트나 기능 요청을 해주세요.\n\n## 파일 관리 및 정리\n\n### 🧹 자동 파일 정리\n\n**문제**: 포스트 삭제나 이미지 변경 시 고아 파일(orphaned files)이 누적되어 스토리지 낭비\n\n**해결책**: 이미지 참조 추적 및 자동 정리 시스템\n\n#### 기본 설정\n\n```tsx\n<KEditor\n  config={{\n    enableImageTracking: true,    // 이미지 참조 추적 (기본값: true)\n    autoCleanup: true,           // 자동 정리 활성화 (기본값: false)\n    autoCleanupInterval: 24,     // 24시간마다 정리 (기본값: 24)\n    onImageDelete: async (url) => {\n      // 커스텀 삭제 로직\n      const response = await fetch('/api/delete-image', {\n        method: 'DELETE',\n        body: JSON.stringify({ url }),\n      });\n      return response.ok;\n    }\n  }}\n/>\n```\n\n#### 수동 파일 정리\n\n```tsx\nimport { ImageCleanupPanel } from 'k-editor';\n\nfunction AdminPage() {\n  return (\n    <div>\n      <h2>파일 관리</h2>\n      <ImageCleanupPanel\n        onCleanupComplete={(result) => {\n          console.log(`${result.deletedFiles.length}개 파일 삭제 완료`);\n        }}\n      />\n    </div>\n  );\n}\n```\n\n#### 프로그래밍 방식 정리\n\n```tsx\nimport { globalImageManager, deleteFiles } from 'k-editor';\n\n// 포스트 삭제 시\nconst deletePost = async (postId: string) => {\n  // 1. 포스트의 이미지 참조 제거 및 고아 파일 목록 가져오기\n  const orphanedUrls = globalImageManager.removePostImages(postId);\n  \n  // 2. 실제 파일 삭제\n  const result = await deleteFiles(orphanedUrls);\n  \n  // 3. 포스트 데이터 삭제\n  await deletePostFromDB(postId);\n  \n  console.log(`포스트 삭제 완료: ${result.deletedFiles.length}개 이미지 정리`);\n};\n\n// 이미지 참조 통계 확인\nconst stats = globalImageManager.getStatistics();\nconsole.log(`총 ${stats.totalImages}개 이미지, ${stats.totalSize} 사용 중`);\n\n// 정리 대상 파일 확인\nconst candidates = globalImageManager.getCleanupCandidates({ maxAge: 30 });\nconsole.log(`${candidates.length}개 파일 정리 가능`);\n```\n\n### 🔧 파일 삭제 API 구현\n\n#### Next.js API Route\n\n```typescript\n// pages/api/delete-image.ts\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport { unlink, access } from 'fs/promises';\nimport path from 'path';\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  if (req.method !== 'DELETE') {\n    return res.status(405).json({ success: false, message: 'Method not allowed' });\n  }\n\n  const { url } = req.body;\n  const filePath = path.join(process.cwd(), 'public', url);\n  \n  try {\n    await access(filePath); // 파일 존재 확인\n    await unlink(filePath); // 파일 삭제\n    \n    res.json({ success: true, message: '파일 삭제 완료' });\n  } catch (error) {\n    res.status(500).json({ success: false, message: '파일 삭제 실패' });\n  }\n}\n```\n\n#### AWS S3 삭제 예시\n\n```typescript\n// pages/api/delete-s3-image.ts\nimport { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';\n\nconst s3Client = new S3Client({ region: process.env.AWS_REGION });\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  const { url } = req.body;\n  \n  // URL에서 S3 키 추출\n  const key = url.replace(`https://${process.env.CLOUDFRONT_DOMAIN}/`, '');\n  \n  try {\n    await s3Client.send(new DeleteObjectCommand({\n      Bucket: process.env.S3_BUCKET_NAME,\n      Key: key,\n    }));\n    \n    res.json({ success: true });\n  } catch (error) {\n    res.status(500).json({ success: false, error: error.message });\n  }\n}\n```\n\n### 📊 스토리지 최적화 전략\n\n| 전략 | 설명 | 장점 | 단점 |\n|------|------|------|------|\n| **실시간 추적** | 이미지 사용/삭제를 실시간으로 추적 | 정확한 참조 관리 | 약간의 성능 오버헤드 |\n| **배치 정리** | 정기적으로 사용되지 않는 파일 정리 | 안전하고 효율적 | 일시적 스토리지 사용량 증가 |\n| **수명 관리** | 파일 업로드 후 일정 기간 후 자동 삭제 | 간단한 구현 | 필요한 파일도 삭제될 위험 |\n\n### 🛡️ 안전 기능\n\n- **Dry Run 모드**: 실제 삭제 전 테스트 가능\n- **백업 확인**: 삭제 전 파일 백업 여부 확인\n- **복구 기능**: 실수로 삭제된 파일 복구 가능\n- **권한 검증**: 삭제 권한이 있는 파일만 처리\n\n## 변경사항\n\n### v0.3.0\n\n- **🔄 개선된 반환 형식**: HTML + 이미지 메타데이터 배열로 변경\n- **📊 이미지 메타데이터**: 파일명, 경로, 키, 크기, 삭제 정보 포함\n- **🔧 향상된 콜백**: `onChange`는 `EditorContent` 객체 반환\n- **⚡ 호환성 지원**: `onContentChange`로 기존 코드 호환\n- **🏷️ 이미지 ID**: 각 이미지에 고유 식별자 자동 할당\n- **📈 통계 기능**: 단어 수, 이미지 수, 텍스트 추출 자동\n\n### v0.2.0\n\n- 이미지 참조 추적 시스템 추가\n- 자동 파일 정리 기능 구현\n- 파일 삭제 API 및 관리 패널 추가\n- 스토리지 최적화 및 고아 파일 방지\n\n### v0.1.0\n\n- 초기 릴리스\n- 한글 입력 지원\n- 이미지 업로드 기능\n- 기본 WYSIWYG 기능\n","readmeFilename":"README.md","_rev":"1-592992173f1461e41ce024dfed02cd7c"}