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,225 @@
|
||||
/**
|
||||
* 메모 작성 및 수정기 (Composer) 관리 모듈
|
||||
*/
|
||||
import { API } from '../api.js';
|
||||
import { EditorManager } from '../editor.js';
|
||||
import { I18nManager } from '../utils/I18nManager.js';
|
||||
import { Constants } from '../utils/Constants.js';
|
||||
|
||||
export const ComposerManager = {
|
||||
DOM: {},
|
||||
|
||||
init(onSaveSuccess) {
|
||||
// 타이밍 이슈 방지를 위해 DOM 요소 지연 할당
|
||||
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')
|
||||
};
|
||||
|
||||
if (!this.DOM.composer || !this.DOM.trigger) return;
|
||||
|
||||
// 1. 이벤트 바인딩
|
||||
this.DOM.trigger.onclick = () => this.openEmpty();
|
||||
this.DOM.foldBtn.onclick = () => this.close();
|
||||
|
||||
this.DOM.discardBtn.onclick = async () => {
|
||||
if (confirm(I18nManager.t('msg_confirm_discard'))) {
|
||||
await EditorManager.cleanupSessionFiles();
|
||||
this.clear();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
this.DOM.composer.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.handleSave(onSaveSuccess);
|
||||
};
|
||||
|
||||
this.DOM.encryptionToggle.onclick = () => this.toggleEncryption();
|
||||
|
||||
// 단축키 힌트 토글 바인딩
|
||||
const shortcutToggle = document.getElementById('shortcutToggle');
|
||||
const shortcutDetails = document.getElementById('shortcutDetails');
|
||||
if (shortcutToggle && shortcutDetails) {
|
||||
shortcutToggle.onclick = () => {
|
||||
const isVisible = shortcutDetails.style.display !== 'none';
|
||||
shortcutDetails.style.display = isVisible ? 'none' : 'flex';
|
||||
const label = I18nManager.t('shortcuts_label');
|
||||
shortcutToggle.textContent = isVisible ? label : `${label} ▲`;
|
||||
};
|
||||
}
|
||||
|
||||
// --- 자동 임시저장 (Auto-Draft) ---
|
||||
this.draftTimer = setInterval(() => this.saveDraft(), 3000);
|
||||
this.checkDraftRestore();
|
||||
},
|
||||
|
||||
openEmpty() {
|
||||
this.clear();
|
||||
this.DOM.composer.style.display = 'block';
|
||||
this.DOM.trigger.style.display = 'none';
|
||||
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(', ');
|
||||
|
||||
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';
|
||||
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,
|
||||
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();
|
||||
this.clearDraft();
|
||||
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';
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.DOM.id.value = '';
|
||||
this.DOM.title.value = '';
|
||||
this.DOM.group.value = Constants.GROUPS.DEFAULT;
|
||||
this.DOM.tags.value = '';
|
||||
EditorManager.setMarkdown('');
|
||||
EditorManager.setAttachedFiles([]);
|
||||
this.setLocked(false);
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
// === 자동 임시저장 (Auto-Draft) ===
|
||||
|
||||
/**
|
||||
* 현재 에디터 내용을 localStorage에 자동 저장
|
||||
*/
|
||||
saveDraft() {
|
||||
// 컴포저가 닫혀있으면 저장하지 않음
|
||||
if (this.DOM.composer.style.display !== 'block') return;
|
||||
|
||||
const title = this.DOM.title.value;
|
||||
const content = EditorManager.getMarkdown();
|
||||
|
||||
// 내용이 비어있으면 저장하지 않음
|
||||
if (!title && !content) return;
|
||||
|
||||
const draft = {
|
||||
title: title,
|
||||
content: content,
|
||||
group: this.DOM.group.value,
|
||||
tags: this.DOM.tags.value,
|
||||
editingId: this.DOM.id.value,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem('memo_draft', JSON.stringify(draft));
|
||||
},
|
||||
|
||||
/**
|
||||
* 페이지 로드 시 임시저장된 내용이 있으면 복원 확인
|
||||
*/
|
||||
checkDraftRestore() {
|
||||
const raw = localStorage.getItem('memo_draft');
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const draft = JSON.parse(raw);
|
||||
|
||||
// 24시간 이상 된 임시저장은 자동 삭제
|
||||
if (Date.now() - draft.timestamp > 86400000) {
|
||||
this.clearDraft();
|
||||
return;
|
||||
}
|
||||
|
||||
// 내용이 실제로 있는 경우에만 복원 확인
|
||||
if (!draft.title && !draft.content) {
|
||||
this.clearDraft();
|
||||
return;
|
||||
}
|
||||
|
||||
const titlePreview = draft.title || I18nManager.t('label_untitled');
|
||||
const confirmMsg = I18nManager.t('msg_draft_restore_confirm')
|
||||
.replace('{title}', titlePreview);
|
||||
|
||||
if (confirm(confirmMsg)) {
|
||||
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 || '');
|
||||
} else {
|
||||
this.clearDraft();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Draft] Failed to parse draft, deleting:', e);
|
||||
this.clearDraft();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 임시저장 데이터 삭제
|
||||
*/
|
||||
clearDraft() {
|
||||
localStorage.removeItem('memo_draft');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user