mirror of
https://github.com/sotam0316/brain_dogfood.git
synced 2026-04-24 19:48:35 +09:00
191 lines
7.8 KiB
JavaScript
191 lines
7.8 KiB
JavaScript
import { API } from './api.js';
|
|
import { renderAttachmentBox } from './components/AttachmentBox.js';
|
|
import { SlashCommand } from './components/SlashCommand.js';
|
|
import { I18nManager } from './utils/I18nManager.js';
|
|
|
|
export const EditorManager = {
|
|
editor: null,
|
|
attachedFiles: [], // 현재 에디터에 첨부된 파일들
|
|
sessionFiles: new Set(), // 이번 세션에 새로 추가된 파일 트래킹 (취소 시 삭제용)
|
|
|
|
init(elSelector, onCtrlEnter) {
|
|
const isMobile = window.innerWidth <= 768;
|
|
|
|
// --- 플러그인 설정 (글자 색상) ---
|
|
const colorPlugin = (window.toastui && window.toastui.EditorPluginColorSyntax) ||
|
|
(window.toastui && window.toastui.Editor && window.toastui.Editor.plugin && window.toastui.Editor.plugin.colorSyntax);
|
|
|
|
const plugins = (typeof colorPlugin === 'function') ? [colorPlugin] : [];
|
|
|
|
this.editor = new toastui.Editor({
|
|
el: document.querySelector(elSelector),
|
|
height: '100%',
|
|
initialEditType: 'wysiwyg',
|
|
previewStyle: isMobile ? 'tab' : 'vertical',
|
|
theme: 'dark',
|
|
placeholder: I18nManager.t('composer_placeholder'),
|
|
plugins: plugins,
|
|
toolbarItems: isMobile ? [
|
|
['heading', 'bold', 'italic', 'strike'],
|
|
['hr', 'quote'],
|
|
['ul', 'ol', 'task'],
|
|
['table', 'image', 'link'],
|
|
['code', 'codeblock']
|
|
] : [
|
|
['heading', 'bold', 'italic', 'strike'],
|
|
['hr', 'quote'],
|
|
['ul', 'ol', 'task', 'indent', 'outdent'],
|
|
['table', 'image', 'link'],
|
|
['code', 'codeblock'],
|
|
['scrollSync']
|
|
],
|
|
hooks: {
|
|
addImageBlobHook: async (blob, callback) => {
|
|
try {
|
|
const data = await API.uploadFile(blob);
|
|
if (data.url) {
|
|
const filename = data.url.split('/').pop();
|
|
callback(`/api/download/${filename}`, data.name || 'image');
|
|
|
|
this.attachedFiles.push({
|
|
filename: filename,
|
|
original_name: data.name || 'image',
|
|
file_type: blob.type
|
|
});
|
|
this.sessionFiles.add(filename);
|
|
this.refreshAttachmentUI();
|
|
}
|
|
} catch (err) { alert(err.message); }
|
|
}
|
|
}
|
|
});
|
|
|
|
// --- 키보드 단축키 시스템 ---
|
|
const editorEl = document.querySelector(elSelector);
|
|
|
|
// Ctrl+Shift 조합 단축키 맵 (toolbar 메뉴 대체)
|
|
const shortcutMap = {
|
|
'x': 'taskList', // Ctrl+Shift+X : 체크박스(Task) 토글
|
|
'u': 'bulletList', // Ctrl+Shift+U : 순서 없는 목록
|
|
'o': 'orderedList', // Ctrl+Shift+O : 순서 있는 목록
|
|
'q': 'blockQuote', // Ctrl+Shift+Q : 인용 블록
|
|
'k': 'codeBlock', // Ctrl+Shift+K : 코드 블록
|
|
'l': 'thematicBreak', // Ctrl+Shift+L : 수평선(구분선)
|
|
']': 'indent', // Ctrl+Shift+] : 들여쓰기
|
|
'[': 'outdent', // Ctrl+Shift+[ : 내어쓰기
|
|
};
|
|
|
|
editorEl.addEventListener('keydown', (e) => {
|
|
// 1. Ctrl+Enter → 저장
|
|
if (onCtrlEnter && e.ctrlKey && !e.shiftKey && (e.key === 'Enter' || e.keyCode === 13)) {
|
|
onCtrlEnter();
|
|
return;
|
|
}
|
|
|
|
// 2. Ctrl+Shift+[Key] → toolbar 명령 실행
|
|
if (e.ctrlKey && e.shiftKey) {
|
|
const cmd = shortcutMap[e.key.toLowerCase()];
|
|
if (cmd) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.editor.exec(cmd);
|
|
}
|
|
}
|
|
}, true); // capture 단계에서 잡아서 에디터 내부 이벤트보다 먼저 처리
|
|
|
|
// --- 슬래시 명령(/) 팝업 초기화 ---
|
|
SlashCommand.init(this.editor, editorEl);
|
|
|
|
return this.editor;
|
|
},
|
|
|
|
setAttachedFiles(files) {
|
|
console.log('[Editor] Loading attachments:', files);
|
|
this.attachedFiles = (files || []).map(f => ({
|
|
filename: f.filename || f.file_name,
|
|
original_name: f.original_name || f.name || 'file',
|
|
file_type: f.file_type || f.type || ''
|
|
}));
|
|
this.sessionFiles.clear(); // 기존 파일을 로드할 때는 세션 트래킹 초기화 (기존 파일은 삭제 대상 제외)
|
|
this.refreshAttachmentUI();
|
|
},
|
|
|
|
refreshAttachmentUI() {
|
|
const container = document.getElementById('editorAttachments');
|
|
if (!container) {
|
|
console.warn('[Editor] #editorAttachments element not found in DOM!');
|
|
return;
|
|
}
|
|
|
|
console.log('[Editor] Refreshing UI with:', this.attachedFiles);
|
|
container.innerHTML = renderAttachmentBox(this.attachedFiles);
|
|
},
|
|
|
|
bindDropEvent(wrapperSelector, onDropComplete) {
|
|
const wrapper = document.querySelector(wrapperSelector);
|
|
wrapper.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); });
|
|
wrapper.addEventListener('drop', async (e) => {
|
|
e.preventDefault(); e.stopPropagation();
|
|
|
|
const files = e.dataTransfer.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
// 에디터가 닫혀있다면 상위에서 열어줘야 함
|
|
onDropComplete(true);
|
|
|
|
for (let file of files) {
|
|
try {
|
|
const data = await API.uploadFile(file);
|
|
if (data.url) {
|
|
const filename = data.url.split('/').pop();
|
|
const isImg = ['png','jpg','jpeg','gif','webp','svg'].includes(data.ext?.toLowerCase());
|
|
const name = data.name || 'file';
|
|
|
|
// Ensure editor is focused before inserting
|
|
this.editor.focus();
|
|
|
|
if (isImg) {
|
|
this.editor.exec('addImage', { altText: name, imageUrl: data.url });
|
|
}
|
|
|
|
// 공통: 첨부 파일 목록에 추가 및 UI 갱신
|
|
this.attachedFiles.push({
|
|
filename: filename,
|
|
original_name: name,
|
|
file_type: file.type
|
|
});
|
|
this.sessionFiles.add(filename); // 세션 트래킹 추가
|
|
this.refreshAttachmentUI();
|
|
}
|
|
} catch (err) { console.error(err); }
|
|
}
|
|
});
|
|
},
|
|
|
|
getAttachedFilenames() {
|
|
return this.attachedFiles.map(f => f.filename);
|
|
},
|
|
|
|
/**
|
|
* 취소(삭제) 시 세션 동안 추가된 파일들을 서버에서 지움
|
|
*/
|
|
async cleanupSessionFiles() {
|
|
if (this.sessionFiles.size === 0) return;
|
|
|
|
console.log(`[Editor] Cleaning up ${this.sessionFiles.size} session files...`);
|
|
const filesToDelete = Array.from(this.sessionFiles);
|
|
for (const filename of filesToDelete) {
|
|
try {
|
|
await API.deleteAttachment(filename);
|
|
} catch (err) {
|
|
console.error(`Failed to delete session file ${filename}:`, err);
|
|
}
|
|
}
|
|
this.sessionFiles.clear();
|
|
},
|
|
|
|
getMarkdown() { return this.editor.getMarkdown().trim(); },
|
|
setMarkdown(md) { this.editor.setMarkdown(md); },
|
|
focus() { this.editor.focus(); }
|
|
};
|