Files
brain_dogfood/static/js/components/ComposerManager.js
T

263 lines
9.7 KiB
JavaScript

/**
* 메모 작성 및 수정기 (Composer) 관리 모듈
*/
import { API } from '../api.js';
import { EditorManager } from '../editor.js';
import { I18nManager } from '../utils/I18nManager.js';
import { Constants } from '../utils/Constants.js';
import { AppService } from '../AppService.js';
import { ThemeManager } from './ThemeManager.js';
// --- NEW 서브 모듈 임포트 ---
import { ComposerDraft } from './composer/ComposerDraft.js';
import { ComposerCategoryUI } from './composer/ComposerCategoryUI.js';
export const ComposerManager = {
DOM: {},
init(onSaveSuccess) {
this.DOM = {
trigger: document.getElementById('composerTrigger'),
composer: document.getElementById('composer'),
title: document.getElementById('memoTitle'),
group: document.getElementById('memoGroup'),
tags: document.getElementById('memoTags'),
id: document.getElementById('editingMemoId'),
encryptionToggle: document.getElementById('encryptionToggle'),
password: document.getElementById('memoPassword'),
foldBtn: document.getElementById('foldBtn'),
discardBtn: document.getElementById('discardBtn'),
deleteBtn: document.getElementById('deleteMemoBtn'), // NEW
categoryBar: document.getElementById('composerCategoryBar')
};
if (!this.DOM.composer || !this.DOM.trigger) return;
this.selectedCategory = null;
this.isDoneStatus = false;
// 1. 이벤트 바인딩
this.DOM.trigger.onclick = () => this.openEmpty();
this.DOM.foldBtn.onclick = () => this.close();
this.DOM.discardBtn.onclick = async () => {
const isEditing = !!this.DOM.id.value;
// 💡 기존 메모 수정 중일 때는 확인 없이 바로 닫기
// 💡 새 메모 작성 중일 때만 파일 정리 여부 묻기
if (isEditing || confirm(I18nManager.t('msg_confirm_discard'))) {
this.forceClose();
}
};
// 💡 에디터 내 실제 삭제 버튼
if (this.DOM.deleteBtn) {
this.DOM.deleteBtn.onclick = async () => {
const id = this.DOM.id.value;
if (!id) return;
if (confirm(I18nManager.t('msg_delete_confirm'))) {
await API.deleteMemo(id);
if (onSaveSuccess) onSaveSuccess();
this.clear();
this.close();
}
};
}
this.DOM.composer.onsubmit = (e) => {
e.preventDefault();
this.handleSave(onSaveSuccess);
};
this.DOM.encryptionToggle.onclick = () => this.toggleEncryption();
this.initShortcutHint();
// 2. 자동 임시저장 및 키보드 리스너 등록
this.draftTimer = setInterval(() => this.saveDraft(), 3000);
ComposerDraft.checkRestore((draft) => this.restoreDraft(draft));
document.addEventListener('keydown', (e) => this.handleKeyDown(e));
},
initShortcutHint() {
const toggle = document.getElementById('shortcutToggle');
const details = document.getElementById('shortcutDetails');
if (toggle && details) {
toggle.onclick = () => {
const isVisible = details.style.display !== 'none';
details.style.display = isVisible ? 'none' : 'flex';
toggle.textContent = isVisible ? I18nManager.t('shortcuts_label') : `${I18nManager.t('shortcuts_label')}`;
};
}
},
openEmpty() {
this.clear();
// 컨텍스트 기반 그룹 자동 설정 (all, done, tag 제외)
const currentGroup = AppService.state.currentFilterGroup;
if (currentGroup &&
currentGroup !== 'all' &&
currentGroup !== Constants.GROUPS.DONE &&
!currentGroup.startsWith('tag:')) {
this.DOM.group.value = currentGroup;
}
this.DOM.composer.style.display = 'block';
this.DOM.trigger.style.display = 'none';
if (this.DOM.deleteBtn) this.DOM.deleteBtn.style.display = 'none'; // 새 메모에선 숨김
this.renderCategoryChips(); // 💡 초기화 후 칩 렌더링
this.DOM.title.focus();
},
openForEdit(memo) {
if (!memo) return;
this.clear();
this.DOM.id.value = memo.id;
this.DOM.title.value = memo.title || '';
this.DOM.group.value = memo.group_name || Constants.GROUPS.DEFAULT;
this.DOM.tags.value = (memo.tags || []).filter(t => t.source === 'user').map(t => t.name).join(', ');
// 💡 분류 및 상태 복원
this.selectedCategory = memo.category || null;
this.isDoneStatus = memo.status === 'done';
EditorManager.setMarkdown(memo.content || '');
EditorManager.setAttachedFiles(memo.attachments || []);
if (memo.was_encrypted || memo.is_encrypted) {
this.setLocked(true, memo.tempPassword || '');
}
this.DOM.composer.style.display = 'block';
this.DOM.trigger.style.display = 'none';
if (this.DOM.deleteBtn) this.DOM.deleteBtn.style.display = 'block'; // 수정 시에만 보임
this.renderCategoryChips(); // 💡 렌더링
window.scrollTo({ top: 0, behavior: 'smooth' });
},
async handleSave(callback) {
const data = {
title: this.DOM.title.value.trim(),
content: EditorManager.getMarkdown(),
group_name: this.DOM.group.value.trim() || Constants.GROUPS.DEFAULT,
category: this.selectedCategory,
status: this.isDoneStatus ? 'done' : 'active',
tags: this.DOM.tags.value.split(',').map(t => t.trim()).filter(t => t),
is_encrypted: this.DOM.encryptionToggle.dataset.locked === 'true',
password: this.DOM.password.value.trim(),
attachment_filenames: EditorManager.getAttachedFilenames()
};
if (!data.title && !data.content) { this.close(); return; }
if (data.is_encrypted && !data.password) { alert(I18nManager.t('msg_alert_password_required')); return; }
try {
await API.saveMemo(data, this.DOM.id.value);
EditorManager.sessionFiles.clear();
ComposerDraft.clear(); // 💡 서브 모듈 위임
if (callback) await callback();
this.clear();
this.close();
} catch (err) { alert(err.message); }
},
close() {
this.DOM.composer.style.display = 'none';
this.DOM.trigger.style.display = 'block';
},
forceClose() {
EditorManager.cleanupSessionFiles().catch(e => console.error(e));
this.clear();
this.close();
},
clear() {
this.DOM.id.value = '';
this.DOM.title.value = '';
this.DOM.group.value = Constants.GROUPS.DEFAULT;
this.DOM.tags.value = '';
this.selectedCategory = null;
this.isDoneStatus = false;
EditorManager.setMarkdown('');
EditorManager.setAttachedFiles([]);
this.setLocked(false);
this.renderCategoryChips();
},
toggleEncryption() {
const isLocked = this.DOM.encryptionToggle.dataset.locked === 'true';
this.setLocked(!isLocked);
},
setLocked(locked, password = null) {
this.DOM.encryptionToggle.dataset.locked = locked;
this.DOM.encryptionToggle.innerText = locked ? '🔒' : '🔓';
this.DOM.password.style.display = locked ? 'block' : 'none';
if (password !== null) this.DOM.password.value = password;
if (locked && !this.DOM.password.value) this.DOM.password.focus();
},
// --- 서브 모듈 위임 메서드들 ---
saveDraft() {
if (this.DOM.composer.style.display !== 'block') return;
ComposerDraft.save(
this.DOM.id.value,
this.DOM.title.value,
this.DOM.group.value,
this.DOM.tags.value,
EditorManager.getMarkdown()
);
},
restoreDraft(draft) {
this.openEmpty();
this.DOM.title.value = draft.title || '';
this.DOM.group.value = draft.group || Constants.GROUPS.DEFAULT;
this.DOM.tags.value = draft.tags || '';
if (draft.editingId) this.DOM.id.value = draft.editingId;
EditorManager.setMarkdown(draft.content || '');
},
renderCategoryChips() {
ComposerCategoryUI.render(
this.DOM.categoryBar,
this.selectedCategory,
this.isDoneStatus,
{
onSelect: (cat) => {
this.selectedCategory = (this.selectedCategory === cat) ? null : cat;
this.renderCategoryChips();
},
onToggleDone: () => {
this.isDoneStatus = !this.isDoneStatus;
this.renderCategoryChips();
}
}
);
},
handleKeyDown(e) {
if (this.DOM.composer.style.display !== 'block') return;
if (!e.altKey) return;
const key = e.key;
if (key === '1') {
e.preventDefault();
this.isDoneStatus = !this.isDoneStatus;
this.renderCategoryChips();
} else if (key === '2' || key === '3' || key === '4') {
e.preventDefault();
const cat = ComposerCategoryUI.getCategoryBySlot(parseInt(key) - 1);
if (cat) {
this.selectedCategory = (this.selectedCategory === cat) ? null : cat;
this.renderCategoryChips();
}
} else if (key === '5') {
e.preventDefault();
this.selectedCategory = null;
this.renderCategoryChips();
}
}
};