mirror of
https://github.com/sotam0316/brain_dogfood.git
synced 2026-04-25 03:48:38 +09:00
Initial Global Release v1.0 (Localization & Security Hardening)
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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(); }
|
||||
};
|
||||
Reference in New Issue
Block a user