256 lines
9.4 KiB
JavaScript
Raw Normal View History

2025-12-09 17:50:06 +09:00
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);
}
};