@mesoft/tiptap-editor
v1.0.1
Published
Universal Tiptap editor component with S3 presigned URL upload support for React, Next.js, and Svelte
Downloads
15
Maintainers
Readme
@mesoft/tiptap-editor
React, Next.js, Svelte를 지원하는 S3 사전 서명 URL 업로드 기능이 포함된 범용 Tiptap 에디터 컴포넌트입니다.
주요 기능
- 🎨 리치 텍스트 편집 - Tiptap으로 구동되는 완전한 기능의 WYSIWYG 에디터
- 📤 S3 업로드 지원 - Data URL로 자동 폴백되는 내장 S3 사전 서명 URL 업로드
- ⚡ 프레임워크 독립적 - React, Next.js, Svelte에서 작동
- 🎯 TypeScript - 타입 정의가 포함된 완전한 TypeScript 지원
- 🎭 슬래시 명령어 - 키보드 단축키로 빠른 서식 지정
- 🌙 다크 모드 - 자동 다크 모드 지원
- 📱 반응형 - 모바일 친화적인 툴바 및 에디터
- 🔧 커스터마이징 가능 - 커스텀 업로드 핸들러 및 설정으로 확장 가능
설치
npm 레지스트리에서 설치 (npm에 발행된 경우)
npm install @mesoft/tiptap-editorGitHub에서 직접 설치
npm 레지스트리에 발행되지 않은 경우, GitHub 저장소에서 직접 설치할 수 있습니다:
npm install github:mesoftkor/tiptap-editor또는 package.json에 직접 추가:
{
"dependencies": {
"@mesoft/tiptap-editor": "github:mesoftkor/tiptap-editor"
}
}사용법
Svelte / SvelteKit
<script lang="ts">
import { RichTextEditor, type UploadConfig } from '@mesoft/tiptap-editor/svelte';
let content = $state('');
// S3 업로드 설정
const uploadConfig: UploadConfig = {
getPresignedUrl: async (filename, contentType, category, token) => {
const response = await fetch('/api/uploads/presign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ filename, contentType, category })
});
const data = await response.json();
return {
uploadUrl: data.upload_url,
publicUrl: data.file_url
};
},
token: 'your-auth-token' // 선택사항
};
</script>
<RichTextEditor
bind:value={content}
uploadCategory="notice"
minHeight="400px"
{uploadConfig}
/>React
import React, { useState } from "react";
import { RichTextEditor, type UploadConfig } from "@mesoft/tiptap-editor/react";
function MyComponent() {
const [content, setContent] = useState("");
// S3 업로드 설정
const uploadConfig: UploadConfig = {
getPresignedUrl: async (filename, contentType, category, token) => {
const response = await fetch("/api/uploads/presign", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ filename, contentType, category }),
});
const data = await response.json();
return {
uploadUrl: data.upload_url,
publicUrl: data.file_url,
};
},
token: "your-auth-token", // 선택사항
};
return (
<RichTextEditor
value={content}
onChange={setContent}
uploadCategory="notice"
minHeight="400px"
uploadConfig={uploadConfig}
/>
);
}Next.js (App Router)
"use client";
import { useState } from "react";
import { RichTextEditor, type UploadConfig } from "@mesoft/tiptap-editor/react";
export default function EditorPage() {
const [content, setContent] = useState("");
// S3 업로드 설정
const uploadConfig: UploadConfig = {
getPresignedUrl: async (filename, contentType, category) => {
const response = await fetch("/api/uploads/presign", {
method: "POST",
body: JSON.stringify({ filename, contentType, category }),
});
const data = await response.json();
return {
uploadUrl: data.upload_url,
publicUrl: data.file_url,
};
},
};
return (
<div>
<h1>My Editor</h1>
<RichTextEditor value={content} onChange={setContent} uploadConfig={uploadConfig} />
</div>
);
}API Reference
Props
RichTextEditor / TiptapEditor
| Prop | Type | Default | 설명 |
| ------------------------------ | ----------------------------------------------- | ------------------------ | ----------------------------- |
| value / content | string | '' | 에디터 내용 (HTML) |
| onChange / onContentChange | (content: string) => void | - | 내용 변경 시 콜백 |
| placeholder | string | '내용을 입력하세요...' | 플레이스홀더 텍스트 |
| editable | boolean | true | 편집 활성화/비활성화 |
| minHeight | string | '200px' | 에디터 최소 높이 |
| uploadCategory | UploadCategory | 'temp' | S3 업로드 카테고리 |
| uploadConfig | UploadConfig | - | S3 업로드 설정 |
| initialImages | string[] | [] | 초기 이미지 URL (편집 모드용) |
| onUnusedImagesChange | (urls: string[]) => void | - | 사용하지 않는 이미지 콜백 |
| onImageUpload | (file: File) => Promise<string \| ImageAttrs> | - | 커스텀 이미지 업로드 핸들러 |
Types
UploadCategory
type UploadCategory = "profile" | "product" | "temp" | "document" | "notice";UploadConfig
interface UploadConfig {
getPresignedUrl: (
filename: string,
contentType: string,
category: UploadCategory,
token?: string
) => Promise<{
uploadUrl: string;
publicUrl: string;
headers?: Record<string, string>;
}>;
deleteImages?: (
imageUrls: string[],
token?: string
) => Promise<{
success: boolean;
deletedCount: number;
message?: string;
}>;
token?: string;
convertToCustomDomain?: (s3Url: string) => string;
}고급 사용법
커스텀 이미지 업로드 핸들러
const customUploadHandler = async (file: File) => {
// 커스텀 업로드 로직
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const { url } = await response.json();
return url; // 또는 return { src: url, alt: 'description' }
};
<RichTextEditor value={content} onChange={setContent} onImageUpload={customUploadHandler} />;사용하지 않는 이미지 추적
const [unusedImages, setUnusedImages] = useState<string[]>([]);
<RichTextEditor value={content} onChange={setContent} initialImages={existingImageUrls} onUnusedImagesChange={setUnusedImages} />;
// 저장 시 사용하지 않는 이미지 삭제
const handleSave = async () => {
if (unusedImages.length > 0) {
await deleteImagesFromS3(unusedImages);
}
// 내용 저장...
};읽기 전용 모드
<RichTextEditor value={content} onChange={() => {}} editable={false} />키보드 단축키
- 굵게:
Ctrl/Cmd + B - 기울임:
Ctrl/Cmd + I - 밑줄:
Ctrl/Cmd + U - 취소선:
Ctrl/Cmd + Shift + X - 코드:
Ctrl/Cmd + E - 실행 취소:
Ctrl/Cmd + Z - 다시 실행:
Ctrl/Cmd + Shift + Z - 슬래시 명령어:
/입력하여 명령 메뉴 열기
슬래시 명령어
에디터에서 /를 입력하면 명령 메뉴가 열립니다:
/→ 제목 1, 2, 3/→ 단락/→ 글머리 기호 목록/→ 번호 매기기 목록/→ 인용문/→ 코드 블록/→ 구분선
백엔드 API 예제
S3 사전 서명 URL 생성을 위한 백엔드 엔드포인트 예제입니다:
// API Route: /api/uploads/presign
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Client = new S3Client({
region: "ap-northeast-2",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function POST(request: Request) {
const { filename, contentType, category } = await request.json();
const key = `${category}/${Date.now()}-${filename}`;
const bucket = "your-bucket-name";
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
const uploadUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600,
});
const fileUrl = `https://${bucket}.s3.ap-northeast-2.amazonaws.com/${key}`;
return Response.json({
upload_url: uploadUrl,
file_url: fileUrl,
key,
bucket,
});
}스타일링
에디터는 기본 스타일이 포함되어 있습니다. 커스터마이징하려면 다음 방법을 사용할 수 있습니다:
- CSS 변수 덮어쓰기 (권장):
:root {
--mesoft-editor-border: #e2e8f0;
--mesoft-editor-bg: white;
--mesoft-editor-text: #1e293b;
}- 스타일 가져오기 및 확장:
@import "@mesoft/tiptap-editor/styles.css";
.mesoft-tiptap-wrapper {
/* 커스텀 스타일 */
}브라우저 지원
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
버전 관리
이 프로젝트는 Semantic Versioning을 따릅니다. 버전 관리 및 릴리즈 프로세스는 VERSIONING.md를 참조하세요.
변경사항은 CHANGELOG.md에서 확인할 수 있습니다.
라이선스
MIT
기여하기
기여자를 환영합니다! 이슈를 열거나 풀 리퀘스트를 제출해 주세요.
지원
문제나 질문이 있으시면 다음을 방문해 주세요:
- GitHub Issues: [mesoft/tiptap-editor/issues]
- 문서: [docs.mesoft.kr]
크레딧
구현 기술:
- Tiptap - 헤드리스 에디터 프레임워크
- ProseMirror - 에디터 툴킷
- AWS SDK - S3 통합
