v1.5: Integrated optional category feature, i18n stabilization, and documentation update

This commit is contained in:
leeyj
2026-04-16 15:42:02 +09:00
parent df8ae62b0e
commit aef0179c56
47 changed files with 1699 additions and 544 deletions
@@ -0,0 +1,73 @@
/**
* 작성기 카테고리/핀 UI 렌더링 엔진
*/
import { I18nManager } from '../../utils/I18nManager.js';
import { ThemeManager } from '../ThemeManager.js';
export const ComposerCategoryUI = {
/**
* 카테고리 칩 및 상태 UI 렌더링
*/
render(container, selectedCategory, isDoneStatus, handlers) {
if (!container) return;
const settings = ThemeManager.settings || {};
const slots = settings.pinned_categories || [];
container.innerHTML = '';
// 💡 인라인 스타일 대신 클래스로 관리하거나 layout.css의 #composerCategoryBar 설정을 따름
// 1. 완료 칩 (Alt + 1)
const doneChip = document.createElement('div');
doneChip.className = `cat-chip done-chip ${isDoneStatus ? 'active' : ''}`;
doneChip.innerHTML = `<span class="icon">✅</span> <span class="text">${I18nManager.t('label_category_done')}</span> <kbd>Alt+1</kbd>`;
doneChip.onclick = () => handlers.onToggleDone();
container.appendChild(doneChip);
// 2. 외부 카테고리 강조칩 (핀에 없지만 지정된 경우)
const isExternal = selectedCategory && !slots.includes(selectedCategory);
if (isExternal) {
const extChip = document.createElement('div');
extChip.className = 'cat-chip external-active active';
extChip.innerHTML = `<span class="icon">📍</span> ${selectedCategory}`;
extChip.title = `Current: ${selectedCategory}`;
extChip.onclick = () => handlers.onSelect(selectedCategory);
container.appendChild(extChip);
const divider = document.createElement('div');
divider.className = 'chip-divider';
// 구분선 스타일은 CSS에서 관리하거나 최소한으로 유지
divider.style.cssText = 'width: 1px; height: 12px; background: var(--muted); opacity: 0.3; margin: 0 5px;';
container.appendChild(divider);
}
// 3. 핀 슬롯(1~3번) 렌더링
slots.forEach((cat, idx) => {
const slotNum = idx + 2; // 완료(1) 다음부터 시작
const key = `shortcut_cat_${idx + 1}`;
const label = I18nManager.t(key).replace('%s', cat);
const chip = document.createElement('div');
chip.className = `cat-chip ${selectedCategory === cat ? 'active' : ''}`;
chip.innerHTML = `<span class="icon">🏷️</span> <span class="text">${cat}</span> <kbd>Alt+${slotNum}</kbd>`;
chip.title = label;
chip.onclick = () => handlers.onSelect(cat);
container.appendChild(chip);
});
// 4. Alt+5: 분류 해제 힌트
const clearHint = document.createElement('div');
clearHint.className = 'shortcut-hint';
clearHint.textContent = I18nManager.t('shortcut_cat_clear');
container.appendChild(clearHint);
},
/**
* 슬롯 인덱스 기반으로 어떤 카테고리를 토글할지 결정
*/
getCategoryBySlot(index) {
const settings = ThemeManager.settings || {};
const slots = settings.pinned_categories || [];
return slots[index - 1] || null;
}
};
@@ -0,0 +1,69 @@
/**
* 작성기 임시저장(Draft) 관리 모듈
*/
import { I18nManager } from '../../utils/I18nManager.js';
import { Constants } from '../../utils/Constants.js';
export const ComposerDraft = {
/**
* 현재 에디터 내용을 localStorage에 자동 저장
*/
save(id, title, group, tags, content) {
// 내용이 비어있으면 저장하지 않음
if (!title && !content) return;
const draft = {
title,
content,
group: group || Constants.GROUPS.DEFAULT,
tags: tags || '',
editingId: id || '',
timestamp: Date.now()
};
localStorage.setItem('memo_draft', JSON.stringify(draft));
},
/**
* 임시저장된 내용이 있는지 확인하고 복원 처리
*/
checkRestore(onRestore) {
const raw = localStorage.getItem('memo_draft');
if (!raw) return;
try {
const draft = JSON.parse(raw);
// 24시간 이상 된 임시저장은 자동 삭제
if (Date.now() - draft.timestamp > 86400000) {
this.clear();
return;
}
// 내용이 실제로 있는 경우에만 복원 확인
if (!draft.title && !draft.content) {
this.clear();
return;
}
const titlePreview = draft.title || I18nManager.t('label_untitled');
const confirmMsg = I18nManager.t('msg_draft_restore_confirm')
.replace('{title}', titlePreview);
if (confirm(confirmMsg)) {
onRestore(draft);
} else {
this.clear();
}
} catch (e) {
console.warn('[Draft] Failed to parse draft, deleting:', e);
this.clear();
}
},
/**
* 임시저장 데이터 삭제
*/
clear() {
localStorage.removeItem('memo_draft');
}
};