TipTap 에디터와 이미지 업로드
글 작성/수정 화면의 본문 에디터는 TipTap을 사용합니다.
담당 파일은 다음입니다.
resources/js/editor.js
왜 textarea가 따로 있을까?
글쓰기 화면에는 두 개의 본문 영역이 있습니다.
<div id="editor"></div>
<textarea name="content" id="content" class="hidden"></textarea>
사용자가 직접 보는 것은 div#editor입니다.
하지만 HTML form이 서버로 전송하는 값은 name="content"가 붙은 textarea입니다.
그래서 TipTap 내용이 바뀔 때마다 textarea에 HTML을 복사합니다.
onUpdate({ editor }) {
if (contentInput) {
contentInput.value = editor.getHTML()
}
}
저장 흐름
사용자가 TipTap에 글 작성
-> editor.getHTML()
-> hidden textarea value에 저장
-> 저장 버튼 클릭
-> POST /admin/posts
-> posts.content 컬럼에 저장
수정 화면에서 기존 글 불러오기
수정 화면에서는 기존 본문이 textarea에 들어 있습니다.
<textarea name="content" id="content" class="hidden">
{{ old('content', $post->content) }}
</textarea>
TipTap은 초기화될 때 이 값을 읽습니다.
content: contentInput ? contentInput.value : '',
그래서 수정 화면에 들어가면 기존 글 내용이 에디터에 보입니다.
에디터 확장 기능
사용하는 TipTap 확장은 다음과 같습니다.
| 확장 | 역할 |
|---|---|
StarterKit | 기본 텍스트 편집 기능 |
Image | 이미지 삽입 |
Link | 링크 삽입 |
CodeBlockLowlight | 코드블록 하이라이팅 |
lowlight | 코드 하이라이팅 엔진 |
highlight.js | 언어별 문법 규칙 |
지원 언어는 현재 JavaScript, CSS, PHP, HTML입니다.
툴바 버튼
글쓰기 화면의 버튼은 JS에서 id로 찾아 이벤트를 붙입니다.
document.getElementById('btn-bold')?.addEventListener('click', () => {
editor.chain().focus().toggleBold().run()
})
?.는 요소가 없으면 에러를 내지 않고 넘어가는 문법입니다.
덕분에 에디터가 없는 페이지에서 이 JS가 로드되어도 깨지지 않습니다.
이미지 업로드 흐름
이미지 버튼을 누르면 파일 선택창을 만들고, 선택된 파일을 서버로 보냅니다.
이미지 버튼 클릭
-> input type=file 생성
-> 파일 선택
-> FormData에 image 추가
-> POST /admin/upload-image
-> 서버가 URL 반환
-> TipTap에 이미지 삽입
CSRF 토큰
Laravel은 POST 요청에 CSRF 토큰이 필요합니다.
레이아웃에 토큰이 들어 있습니다.
<meta name="csrf-token" content="{{ csrf_token() }}">
JS에서는 이 값을 읽어서 헤더에 넣습니다.
const token = document.querySelector('meta[name="csrf-token"]').content
서버 이미지 처리
서버는 ImageController@upload가 처리합니다.
$request->validate([
'image' => 'required|image|max:2048'
]);
이미지 파일만 허용하고 최대 2MB까지 허용합니다.
$path = $request->file('image')->store('images', 'public');
$url = '/storage/' . $path;
파일은 storage/app/public/images에 저장되고, 브라우저에서는 /storage/images/...로 접근합니다.
붙여넣기 이미지
클립보드에 이미지가 있으면 paste 이벤트에서 감지합니다.
editor.view.dom.addEventListener('paste', async (event) => {
const items = event.clipboardData?.items
})
이미지 타입이면 기본 붙여넣기를 막고 서버에 업로드합니다.
event.preventDefault()
이렇게 하지 않으면 이미지가 base64 문자열로 본문에 그대로 박힐 수 있습니다.
storage:link
업로드 이미지를 웹에서 보려면 심볼릭 링크가 필요합니다.
php artisan storage:link
Railway 배포 명령에도 이 작업이 포함되어 있습니다.