256 lines
9.4 KiB
JavaScript
256 lines
9.4 KiB
JavaScript
import { Api } from './api.js';
|
|
import { UI } from './ui.js';
|
|
|
|
/**
|
|
* editor.js - Quill 에디터 및 미디어 업로드 관리
|
|
*/
|
|
|
|
let quillInstance = null; // 모듈 내부에서만 사용하는 Quill 인스턴스
|
|
|
|
export let Editor = {
|
|
/**
|
|
* 에디터 초기화 함수
|
|
* @param {boolean} useEditor - 편집 모드 여부 (true: 편집, false: 읽기)
|
|
* @param {object} baseData - 게시물 데이터 (제목, 내용 등)
|
|
*/
|
|
init(useEditor = false, baseData = {}) {
|
|
console.log(`### Editor Init (EditMode: ${useEditor}) ###`);
|
|
|
|
const editorContainer = document.querySelector('#editor');
|
|
if (!editorContainer) return;
|
|
|
|
// 1. 수동 입력 필드 초기화 (날짜, 좌표)
|
|
this.initManualFields(baseData);
|
|
|
|
// 2. Quill 모듈 및 포맷 등록
|
|
this.registerQuillModules();
|
|
|
|
// 3. Quill 옵션 설정
|
|
const quillOptions = {
|
|
theme: 'snow',
|
|
modules: useEditor ? {
|
|
imageResize: { displaySize: true },
|
|
toolbar: {
|
|
container: [
|
|
[{ font: [] }, { size: ['small', false, 'large', 'huge'] }],
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
[{ color: [] }, { background: [] }],
|
|
[{ header: 1 }, { header: 2 }, 'blockquote', 'code-block'],
|
|
[{ list: 'ordered'}, { list: 'bullet' }],
|
|
[{ align: [] }],
|
|
['table-better'], ['clean'], ['link', 'image', 'video']
|
|
],
|
|
handlers: {
|
|
image: () => this.selectLocalImage(),
|
|
video: () => this.selectLocalVideo()
|
|
}
|
|
},
|
|
'table-better': { language: 'en_US', toolbarTable: true },
|
|
keyboard: { bindings: QuillTableBetter.keyboardBindings }
|
|
} : {
|
|
imageResize: { displaySize: true },
|
|
toolbar: false // 읽기 모드
|
|
},
|
|
readOnly: !useEditor
|
|
};
|
|
|
|
// 4. Quill 인스턴스 생성
|
|
quillInstance = new Quill(editorContainer, quillOptions);
|
|
|
|
// 5. 초기 콘텐츠 로드
|
|
if (baseData.content) {
|
|
this.loadContent(baseData.content);
|
|
}
|
|
|
|
// 6. UI 스타일 처리 (Sticky Toolbar, ReadOnly Class)
|
|
this.setupEditorUI(editorContainer, useEditor, baseData.title);
|
|
|
|
// 7. 붙여넣기 핸들러 (Markdown 지원 등)
|
|
this.setupPasteHandler();
|
|
},
|
|
|
|
/**
|
|
* Quill 인스턴스 반환 (저장 시 사용)
|
|
*/
|
|
getCookies() {
|
|
return quillInstance ? quillInstance.getContents() : null;
|
|
},
|
|
|
|
/**
|
|
* 로컬 이미지 선택 및 업로드 핸들러
|
|
*/
|
|
selectLocalImage() {
|
|
const url = prompt("이미지 URL을 입력하거나 취소하여 파일을 업로드하세요.");
|
|
if (url) {
|
|
this.insertToEditor('image', url);
|
|
} else {
|
|
const input = document.createElement('input');
|
|
input.setAttribute('type', 'file');
|
|
input.setAttribute('accept', 'image/*');
|
|
input.click();
|
|
|
|
input.onchange = () => {
|
|
const file = input.files[0];
|
|
if (file) {
|
|
if (!file.type.startsWith('image/')) {
|
|
UI.showAlert('알림', '이미지 파일만 업로드 가능합니다.');
|
|
return;
|
|
}
|
|
this.uploadMedia(file, 'image');
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 로컬 비디오 선택 및 업로드 핸들러
|
|
*/
|
|
selectLocalVideo() {
|
|
const input = document.createElement('input');
|
|
input.setAttribute('type', 'file');
|
|
input.setAttribute('accept', 'video/*');
|
|
input.click();
|
|
|
|
input.onchange = () => {
|
|
const file = input.files[0];
|
|
if (file) {
|
|
if (!file.type.startsWith('video/')) {
|
|
UI.showAlert('알림', '동영상 파일만 업로드 가능합니다.');
|
|
return;
|
|
}
|
|
this.uploadMedia(file, 'video'); // 비디오 업로드 로직은 서버 API 필요
|
|
}
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 파일 업로드 및 에디터 삽입 (이미지/비디오 공용)
|
|
*/
|
|
async uploadMedia(file, type) {
|
|
const formData = new FormData();
|
|
const uploadUrl = type === 'image'
|
|
? `${Api.getMainPath()}/api/images/upload`
|
|
: `${Api.getMainPath()}/api/upload/video`; // 비디오용 경로는 필요시 수정
|
|
|
|
formData.append('file', file); // 서버 파라미터명 'file'
|
|
|
|
try {
|
|
// Api 모듈을 사용하여 업로드 (CSRF 토큰 자동 처리)
|
|
const data = await Api.request(uploadUrl, 'POST', formData);
|
|
|
|
if (data.fileName) {
|
|
// 이미지 경로 구성 (/api/images/파일명)
|
|
const mediaUrl = `${Api.getMainPath()}/api/images/${data.fileName}`;
|
|
this.insertToEditor(type, mediaUrl);
|
|
} else {
|
|
throw new Error('Upload successful but no filename returned');
|
|
}
|
|
} catch (error) {
|
|
console.error(`${type} upload failed:`, error);
|
|
UI.showAlert('오류', `${type} 업로드에 실패했습니다.`);
|
|
}
|
|
},
|
|
|
|
insertToEditor(type, url) {
|
|
if (!quillInstance) return;
|
|
const range = quillInstance.getSelection(true);
|
|
quillInstance.insertEmbed(range.index, type, url);
|
|
quillInstance.setSelection(range.index + 1);
|
|
},
|
|
|
|
loadContent(content) {
|
|
try {
|
|
const delta = JSON.parse(content);
|
|
if (delta && Array.isArray(delta.ops)) {
|
|
quillInstance.setContents(delta);
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
// JSON 파싱 실패 시 HTML로 로드
|
|
quillInstance.clipboard.dangerouslyPasteHTML(content);
|
|
},
|
|
|
|
// --- 내부 헬퍼 함수들 ---
|
|
|
|
initManualFields(baseData) {
|
|
const dateInput = document.getElementById('manual_date');
|
|
const latInput = document.getElementById('manual_lat');
|
|
const lonInput = document.getElementById('manual_lon');
|
|
|
|
if (dateInput) {
|
|
const time = baseData.writeTime > 0 ? baseData.writeTime : Date.now();
|
|
dateInput.value = new Date(time - (new Date().getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
|
|
}
|
|
if (latInput && lonInput) {
|
|
latInput.value = baseData.modifyLat || baseData.firstPostLat || '';
|
|
lonInput.value = baseData.modifyLon || baseData.firstPostLon || '';
|
|
}
|
|
},
|
|
|
|
registerQuillModules() {
|
|
const Font = Quill.import('formats/font');
|
|
Font.whitelist = ['monospace', 'sans-serif', 'serif', 'arial', 'georgia', 'comic-sans-ms', 'courier-new', 'roboto', 'playfair-display'];
|
|
Quill.register(Font, true);
|
|
|
|
if (typeof QuillTableBetter !== 'undefined') {
|
|
Quill.register({ 'modules/table-better': QuillTableBetter }, true);
|
|
}
|
|
if (typeof QuillResizeModule !== 'undefined') {
|
|
Quill.register('modules/imageResize', QuillResizeModule);
|
|
}
|
|
|
|
// Custom Image Blot (style 속성 지원)
|
|
const ImageBlot = Quill.import('formats/image');
|
|
class StyledImage extends ImageBlot {
|
|
static formats(domNode) {
|
|
const formats = super.formats(domNode);
|
|
if (domNode.hasAttribute('style')) formats.style = domNode.getAttribute('style');
|
|
return formats;
|
|
}
|
|
format(name, value) {
|
|
if (name === 'style') {
|
|
value ? this.domNode.setAttribute(name, value) : this.domNode.removeAttribute(name);
|
|
} else {
|
|
super.format(name, value);
|
|
}
|
|
}
|
|
}
|
|
Quill.register(StyledImage, true);
|
|
},
|
|
|
|
setupEditorUI(container, useEditor, title) {
|
|
if (useEditor) {
|
|
container.classList.remove('readonly-mode');
|
|
const toolbar = document.querySelector('.ql-toolbar');
|
|
window.addEventListener('scroll', () => {
|
|
if (window.scrollY > container.offsetTop) {
|
|
toolbar.classList.add('sticky');
|
|
container.classList.add('has-sticky-toolbar');
|
|
} else {
|
|
toolbar.classList.remove('sticky');
|
|
container.classList.remove('has-sticky-toolbar');
|
|
}
|
|
});
|
|
|
|
// 제목 필드 설정
|
|
const titleField = document.querySelector("#title_field");
|
|
if (titleField) titleField.value = title || '';
|
|
|
|
} else {
|
|
container.classList.add('readonly-mode');
|
|
}
|
|
},
|
|
|
|
setupPasteHandler() {
|
|
quillInstance.root.addEventListener('paste', (event) => {
|
|
const pasteText = (event.clipboardData || window.clipboardData).getData('text');
|
|
// 마크다운 감지
|
|
if (/^(#|\*|-|>|`)/.test(pasteText.trim()) && typeof marked !== 'undefined') {
|
|
event.preventDefault();
|
|
const html = marked.parse(pasteText, { gfm: true, breaks: true });
|
|
const range = quillInstance.getSelection(true);
|
|
quillInstance.clipboard.dangerouslyPasteHTML(range.index, html);
|
|
}
|
|
}, true);
|
|
}
|
|
}; |