mirror of
https://github.com/sotam0316/brain_dogfood.git
synced 2026-04-24 19:48:35 +09:00
v1.5: Integrated optional category feature, i18n stabilization, and documentation update
This commit is contained in:
+23
-2
@@ -10,6 +10,7 @@ import { CalendarManager } from './js/components/CalendarManager.js';
|
||||
import { Visualizer } from './js/components/Visualizer.js';
|
||||
import { HeatmapManager } from './js/components/HeatmapManager.js';
|
||||
import { DrawerManager } from './js/components/DrawerManager.js';
|
||||
import { CategoryManager } from './js/components/CategoryManager.js';
|
||||
import { ModalManager } from './js/components/ModalManager.js';
|
||||
import { I18nManager } from './js/utils/I18nManager.js';
|
||||
import { Constants } from './js/utils/Constants.js';
|
||||
@@ -27,17 +28,20 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
AppService.setFilter({ date }, updateSidebarCallback);
|
||||
});
|
||||
DrawerManager.init();
|
||||
CategoryManager.init(() => AppService.refreshData(updateSidebarCallback));
|
||||
Visualizer.init('graphContainer');
|
||||
UI.initSidebarToggle();
|
||||
|
||||
// --- 🔹 Callbacks ---
|
||||
const updateSidebarCallback = (memos, activeGroup) => {
|
||||
UI.updateSidebar(memos, activeGroup, (newFilter) => {
|
||||
const updateSidebarCallback = (memos, activeGroup, activeCategory) => {
|
||||
UI.updateSidebar(memos, activeGroup, activeCategory, (newFilter) => {
|
||||
if (newFilter === Constants.GROUPS.FILES) {
|
||||
ModalManager.openAssetLibrary((id, ms) => UI.openMemoModal(id, ms));
|
||||
} else {
|
||||
AppService.setFilter({ group: newFilter }, updateSidebarCallback);
|
||||
}
|
||||
}, (newCat) => {
|
||||
AppService.setFilter({ category: newCat }, updateSidebarCallback);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -138,6 +142,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
};
|
||||
|
||||
// --- 🔹 Category Management ---
|
||||
document.getElementById('manageCategoryBtn').onclick = () => {
|
||||
CategoryManager.open();
|
||||
};
|
||||
|
||||
// --- 🔹 Global Shortcuts (Comprehensive Shift to Ctrl-based System) ---
|
||||
|
||||
// --- 🔹 Global Shortcuts (Comprehensive Shift to Ctrl-based System) ---
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const isCtrl = e.ctrlKey || e.metaKey;
|
||||
@@ -189,6 +200,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (isAlt && key === '`') {
|
||||
e.preventDefault();
|
||||
ComposerManager.openEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Category Slots: Alt + 1~4
|
||||
if (isAlt && (key >= '1' && key <= '4')) {
|
||||
if (ComposerManager.DOM.composer.style.display === 'block') {
|
||||
e.preventDefault();
|
||||
const slotIndex = parseInt(key) - 1; // 1->0 (Done), 2->1 (Cat1)...
|
||||
ComposerManager.toggleCategoryBySlot(slotIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -105,3 +105,155 @@
|
||||
|
||||
.action-btn:hover { background: rgba(184, 59, 94, 0.8); }
|
||||
.ai-btn:hover { background: var(--ai-accent); color: white; }
|
||||
|
||||
/* Memo Footer Metadata Styling */
|
||||
.memo-metadata-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.memo-metadata-footer p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
/* === Composer Category Chips === */
|
||||
#composerCategoryBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cat-chip {
|
||||
padding: 6px 14px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--text-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cat-chip:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent-light);
|
||||
transform: translateY(-1px);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cat-chip.active {
|
||||
background: var(--accent-light);
|
||||
color: white;
|
||||
border-color: var(--accent-light);
|
||||
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.3);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cat-chip.done-chip.active {
|
||||
background: #10b981; /* Success Green */
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* 💡 외부 카테고리 강조칩 (핀에 없지만 지정된 경우) */
|
||||
.cat-chip.external-active {
|
||||
border: 1px dashed #ff4d4d !important;
|
||||
background: rgba(255, 77, 77, 0.05);
|
||||
color: #ff4d4d !important;
|
||||
}
|
||||
|
||||
.cat-chip.external-active:hover {
|
||||
background: rgba(255, 77, 77, 0.12);
|
||||
}
|
||||
|
||||
.cat-chip kbd {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
margin-left: 4px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Category Management List */
|
||||
.cat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 14px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cat-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cat-item .cat-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.cat-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.cat-action-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
padding: 2px;
|
||||
opacity: 0.4;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.cat-action-btn:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.cat-action-btn.pin.active {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
filter: drop-shadow(0 0 5px var(--accent));
|
||||
}
|
||||
|
||||
.cat-action-btn.delete:hover {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Category List */
|
||||
#categoryListContainer::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
#categoryListContainer::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
#categoryListContainer::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
#categoryListContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
+24
-9
@@ -10,6 +10,7 @@ export const AppService = {
|
||||
state: {
|
||||
memosCache: [],
|
||||
currentFilterGroup: 'all',
|
||||
currentFilterCategory: null, // NEW: 카테고리 필터
|
||||
currentFilterDate: null,
|
||||
currentSearchQuery: '',
|
||||
offset: 0,
|
||||
@@ -42,11 +43,11 @@ export const AppService = {
|
||||
if (this.state.isLoading || !this.state.hasMore) return;
|
||||
|
||||
this.state.isLoading = true;
|
||||
// UI.showLoading(true)는 호출부에서 관리하거나 여기서 직접 호출 가능
|
||||
|
||||
try {
|
||||
const filters = {
|
||||
group: this.state.currentFilterGroup,
|
||||
category: this.state.currentFilterCategory, // NEW
|
||||
date: this.state.currentFilterDate,
|
||||
query: this.state.currentSearchQuery,
|
||||
offset: this.state.offset,
|
||||
@@ -68,13 +69,10 @@ export const AppService = {
|
||||
|
||||
this.state.offset += newMemos.length;
|
||||
|
||||
// 캘린더 점 표시는 첫 로드 시에면 하면 부족할 수 있으므로,
|
||||
// 필요 시 전체 데이터를 새로 고침하는 별도 API가 필요할 수 있음.
|
||||
// 여기서는 현재 캐시된 데이터 기반으로 업데이트.
|
||||
CalendarManager.updateMemoDates(this.state.memosCache);
|
||||
|
||||
if (onUpdateSidebar) {
|
||||
onUpdateSidebar(this.state.memosCache, this.state.currentFilterGroup);
|
||||
onUpdateSidebar(this.state.memosCache, this.state.currentFilterGroup, this.state.currentFilterCategory);
|
||||
}
|
||||
|
||||
UI.setHasMore(this.state.hasMore);
|
||||
@@ -90,17 +88,34 @@ export const AppService = {
|
||||
/**
|
||||
* 필터 상태를 변경하고 데이터 초기화 후 다시 로딩
|
||||
*/
|
||||
async setFilter({ group, date, query }, onUpdateSidebar) {
|
||||
async setFilter({ group, category, date, query }, onUpdateSidebar) {
|
||||
let changed = false;
|
||||
if (group !== undefined && this.state.currentFilterGroup !== group) {
|
||||
this.state.currentFilterGroup = group;
|
||||
|
||||
// 1. 그룹 선택 처리
|
||||
if (group !== undefined) {
|
||||
// 그룹이 바뀌거나, 혹은 카테고리가 켜져있는 상태에서 그룹을 누르면 카테고리 해제
|
||||
if (this.state.currentFilterGroup !== group || this.state.currentFilterCategory !== null) {
|
||||
this.state.currentFilterGroup = group;
|
||||
this.state.currentFilterCategory = null;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 카테고리 선택 처리
|
||||
if (category !== undefined) {
|
||||
if (this.state.currentFilterCategory === category) {
|
||||
// 이미 선택된 카테고리 재클릭 시 해제 (Toggle)
|
||||
this.state.currentFilterCategory = null;
|
||||
} else {
|
||||
this.state.currentFilterCategory = category;
|
||||
}
|
||||
this.state.currentFilterGroup = 'all'; // 카테고리 필터 적용/변경 시 그룹 초기화
|
||||
changed = true;
|
||||
}
|
||||
if (date !== undefined && this.state.currentFilterDate !== date) {
|
||||
this.state.currentFilterDate = date;
|
||||
changed = true;
|
||||
|
||||
// UI 동기화
|
||||
CalendarManager.setSelectedDate(date);
|
||||
if (HeatmapManager.setSelectedDate) {
|
||||
HeatmapManager.setSelectedDate(date);
|
||||
|
||||
+14
-4
@@ -18,12 +18,22 @@ export const API = {
|
||||
|
||||
async fetchMemos(filters = {}) {
|
||||
const { limit = 20, offset = 0, group = 'all', query = '' } = filters;
|
||||
const date = filters.date || ''; // null이나 undefined를 빈 문자열로 변환
|
||||
const params = new URLSearchParams({ limit, offset, group, query, date });
|
||||
const date = filters.date || '';
|
||||
const category = (filters.category === null || filters.category === undefined) ? '' : filters.category;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
limit,
|
||||
offset,
|
||||
group,
|
||||
query,
|
||||
category,
|
||||
date,
|
||||
_t: Date.now() // 브라우저 캐시 방지용 타임스탬프
|
||||
});
|
||||
return await this.request(`/api/memos?${params.toString()}`);
|
||||
},
|
||||
async fetchHeatmapData(days = 365) {
|
||||
return await this.request(`/api/stats/heatmap?days=${days}`);
|
||||
return await this.request(`/api/stats/heatmap?days=${days}&_t=${Date.now()}`);
|
||||
},
|
||||
|
||||
async saveMemo(payload, id = null) {
|
||||
@@ -52,7 +62,7 @@ export const API = {
|
||||
},
|
||||
|
||||
async fetchAssets() {
|
||||
return await this.request('/api/assets');
|
||||
return await this.request(`/api/assets?_t=${Date.now()}`);
|
||||
},
|
||||
|
||||
async uploadFile(file) {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 카테고리 관리 모달 (Category Management Modal)
|
||||
*/
|
||||
import { API } from '../api.js';
|
||||
import { I18nManager } from '../utils/I18nManager.js';
|
||||
import { ThemeManager } from './ThemeManager.js';
|
||||
|
||||
export const CategoryManager = {
|
||||
DOM: {},
|
||||
onUpdateCallback: null,
|
||||
|
||||
init(onUpdateCallback) {
|
||||
this.onUpdateCallback = onUpdateCallback;
|
||||
this.DOM = {
|
||||
modal: document.getElementById('categoryModal'),
|
||||
closeBtn: document.getElementById('closeCategoryBtn'),
|
||||
container: document.getElementById('categoryListContainer'),
|
||||
input: document.getElementById('newCategoryInput'),
|
||||
addBtn: document.getElementById('addCategoryBtn')
|
||||
};
|
||||
|
||||
if (!this.DOM.modal) return;
|
||||
|
||||
this.DOM.closeBtn.onclick = () => this.close();
|
||||
this.DOM.addBtn.onclick = () => this.handleAdd();
|
||||
this.DOM.input.onkeydown = (e) => { if (e.key === 'Enter') this.handleAdd(); };
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === this.DOM.modal) this.close();
|
||||
});
|
||||
},
|
||||
|
||||
open() {
|
||||
this.render();
|
||||
this.DOM.modal.classList.add('active');
|
||||
this.DOM.input.focus();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.DOM.modal.classList.remove('active');
|
||||
},
|
||||
|
||||
async render() {
|
||||
const settings = ThemeManager.settings || {};
|
||||
const categories = settings.categories || [];
|
||||
const pinned = settings.pinned_categories || [];
|
||||
|
||||
this.DOM.container.innerHTML = '';
|
||||
|
||||
categories.forEach(cat => {
|
||||
const isPinned = pinned.includes(cat);
|
||||
const item = document.createElement('div');
|
||||
item.className = 'cat-item';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="cat-name">${cat}</div>
|
||||
<div class="cat-actions">
|
||||
<button class="cat-action-btn pin ${isPinned ? 'active' : ''}" title="Pin/Unpin Slot">
|
||||
${isPinned ? '📍' : '📌'}
|
||||
</button>
|
||||
<button class="cat-action-btn delete" title="Delete Category">🗑️</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 핀 토글
|
||||
item.querySelector('.pin').onclick = () => this.togglePin(cat);
|
||||
|
||||
// 삭제
|
||||
item.querySelector('.delete').onclick = () => this.deleteCategory(cat);
|
||||
|
||||
this.DOM.container.appendChild(item);
|
||||
});
|
||||
|
||||
if (categories.length === 0) {
|
||||
this.DOM.container.innerHTML = `<p style="text-align:center; color:var(--muted); font-size:0.9rem; padding:20px;">${I18nManager.t('label_no_category')}</p>`;
|
||||
}
|
||||
},
|
||||
|
||||
async handleAdd() {
|
||||
const name = this.DOM.input.value.trim();
|
||||
if (!name) return;
|
||||
if (name.length > 20) {
|
||||
alert("Name too long (max 20)");
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = { ...ThemeManager.settings };
|
||||
if (settings.categories.includes(name)) {
|
||||
alert("Already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
settings.categories.push(name);
|
||||
// 공간이 있으면 자동 핀 고정
|
||||
if (settings.pinned_categories.length < 3) {
|
||||
settings.pinned_categories.push(name);
|
||||
}
|
||||
|
||||
await this.save(settings);
|
||||
this.DOM.input.value = '';
|
||||
this.render();
|
||||
},
|
||||
|
||||
async togglePin(cat) {
|
||||
const settings = { ...ThemeManager.settings };
|
||||
const idx = settings.pinned_categories.indexOf(cat);
|
||||
|
||||
if (idx > -1) {
|
||||
settings.pinned_categories.splice(idx, 1);
|
||||
} else {
|
||||
if (settings.pinned_categories.length >= 3) {
|
||||
alert(I18nManager.t('msg_category_limit'));
|
||||
return;
|
||||
}
|
||||
settings.pinned_categories.push(cat);
|
||||
}
|
||||
|
||||
await this.save(settings);
|
||||
this.render();
|
||||
},
|
||||
|
||||
async deleteCategory(cat) {
|
||||
if (!confirm(I18nManager.t('msg_confirm_delete_category'))) return;
|
||||
|
||||
const settings = { ...ThemeManager.settings };
|
||||
settings.categories = settings.categories.filter(c => c !== cat);
|
||||
settings.pinned_categories = settings.pinned_categories.filter(c => c !== cat);
|
||||
|
||||
await this.save(settings);
|
||||
this.render();
|
||||
},
|
||||
|
||||
async save(settings) {
|
||||
try {
|
||||
await API.saveSettings(settings);
|
||||
// 전역 세팅 업데이트 및 UI 리프레시
|
||||
ThemeManager.settings = settings;
|
||||
if (window.UI) window.UI._updateSettingsCache(settings);
|
||||
if (this.onUpdateCallback) this.onUpdateCallback();
|
||||
} catch (err) {
|
||||
alert("Save failed: " + err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -5,12 +5,17 @@ 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) {
|
||||
// 타이밍 이슈 방지를 위해 DOM 요소 지연 할당
|
||||
this.DOM = {
|
||||
trigger: document.getElementById('composerTrigger'),
|
||||
composer: document.getElementById('composer'),
|
||||
@@ -21,11 +26,15 @@ export const ComposerManager = {
|
||||
encryptionToggle: document.getElementById('encryptionToggle'),
|
||||
password: document.getElementById('memoPassword'),
|
||||
foldBtn: document.getElementById('foldBtn'),
|
||||
discardBtn: document.getElementById('discardBtn')
|
||||
discardBtn: document.getElementById('discardBtn'),
|
||||
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();
|
||||
@@ -44,28 +53,41 @@ export const ComposerManager = {
|
||||
};
|
||||
|
||||
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} ▲`;
|
||||
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')} ▲`;
|
||||
};
|
||||
}
|
||||
|
||||
// --- 자동 임시저장 (Auto-Draft) ---
|
||||
this.draftTimer = setInterval(() => this.saveDraft(), 3000);
|
||||
this.checkDraftRestore();
|
||||
},
|
||||
|
||||
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';
|
||||
this.renderCategoryChips(); // 💡 초기화 후 칩 렌더링
|
||||
this.DOM.title.focus();
|
||||
},
|
||||
|
||||
@@ -77,6 +99,10 @@ export const ComposerManager = {
|
||||
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 || []);
|
||||
|
||||
@@ -86,6 +112,7 @@ export const ComposerManager = {
|
||||
|
||||
this.DOM.composer.style.display = 'block';
|
||||
this.DOM.trigger.style.display = 'none';
|
||||
this.renderCategoryChips(); // 💡 렌더링
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
@@ -94,6 +121,8 @@ export const ComposerManager = {
|
||||
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(),
|
||||
@@ -106,7 +135,7 @@ export const ComposerManager = {
|
||||
try {
|
||||
await API.saveMemo(data, this.DOM.id.value);
|
||||
EditorManager.sessionFiles.clear();
|
||||
this.clearDraft();
|
||||
ComposerDraft.clear(); // 💡 서브 모듈 위임
|
||||
if (callback) await callback();
|
||||
this.clear();
|
||||
this.close();
|
||||
@@ -123,9 +152,12 @@ export const ComposerManager = {
|
||||
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() {
|
||||
@@ -137,89 +169,70 @@ export const ComposerManager = {
|
||||
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();
|
||||
}
|
||||
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));
|
||||
ComposerDraft.save(
|
||||
this.DOM.id.value,
|
||||
this.DOM.title.value,
|
||||
this.DOM.group.value,
|
||||
this.DOM.tags.value,
|
||||
EditorManager.getMarkdown()
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 페이지 로드 시 임시저장된 내용이 있으면 복원 확인
|
||||
*/
|
||||
checkDraftRestore() {
|
||||
const raw = localStorage.getItem('memo_draft');
|
||||
if (!raw) return;
|
||||
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 || '');
|
||||
},
|
||||
|
||||
try {
|
||||
const draft = JSON.parse(raw);
|
||||
|
||||
// 24시간 이상 된 임시저장은 자동 삭제
|
||||
if (Date.now() - draft.timestamp > 86400000) {
|
||||
this.clearDraft();
|
||||
return;
|
||||
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();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// 내용이 실제로 있는 경우에만 복원 확인
|
||||
if (!draft.title && !draft.content) {
|
||||
this.clearDraft();
|
||||
return;
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.DOM.composer.style.display !== 'block') return;
|
||||
if (!e.altKey) 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();
|
||||
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();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Draft] Failed to parse draft, deleting:', e);
|
||||
this.clearDraft();
|
||||
} else if (key === '5') {
|
||||
e.preventDefault();
|
||||
this.selectedCategory = null;
|
||||
this.renderCategoryChips();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 임시저장 데이터 삭제
|
||||
*/
|
||||
clearDraft() {
|
||||
localStorage.removeItem('memo_draft');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,8 +36,13 @@ export function createMemoCardHtml(memo, isDone) {
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// 본문에서 하단 메타데이터 블록(--- 이후)을 제외하고 렌더링 (중복 표시 방지)
|
||||
let content = memo.content || '';
|
||||
const footerIndex = content.lastIndexOf('\n\n---\n');
|
||||
const displayContent = footerIndex !== -1 ? content.substring(0, footerIndex) : content;
|
||||
|
||||
// marked로 파싱한 후 DOMPurify로 살균하여 XSS 방지
|
||||
htmlContent = DOMPurify.sanitize(marked.parse(memo.content || ''));
|
||||
htmlContent = DOMPurify.sanitize(marked.parse(displayContent));
|
||||
htmlContent = parseInternalLinks(htmlContent);
|
||||
htmlContent = fixImagePaths(htmlContent);
|
||||
}
|
||||
|
||||
@@ -165,8 +165,21 @@ export const ModalManager = {
|
||||
if (!memo) return;
|
||||
|
||||
import('../utils.js').then(({ parseInternalLinks, fixImagePaths }) => {
|
||||
// 마크다운 파싱 후 살균 처리 (marked, DOMPurify는 global 사용)
|
||||
let html = DOMPurify.sanitize(marked.parse(memo.content));
|
||||
// 메모 본문과 메타데이터 푸터 분리 렌더링
|
||||
let content = memo.content || '';
|
||||
const footerIndex = content.lastIndexOf('\n\n---\n');
|
||||
let html;
|
||||
|
||||
if (footerIndex !== -1) {
|
||||
const mainBody = content.substring(0, footerIndex);
|
||||
const footerPart = content.substring(footerIndex + 5).trim(); // '---' 이후
|
||||
|
||||
html = DOMPurify.sanitize(marked.parse(mainBody));
|
||||
html += `<div class="memo-metadata-footer"><hr style="border:none; border-top:1px dashed rgba(255,255,255,0.1); margin-bottom:15px;">${DOMPurify.sanitize(marked.parse(footerPart))}</div>`;
|
||||
} else {
|
||||
html = DOMPurify.sanitize(marked.parse(content));
|
||||
}
|
||||
|
||||
html = parseInternalLinks(html);
|
||||
html = fixImagePaths(html);
|
||||
|
||||
|
||||
@@ -45,3 +45,27 @@ export function renderGroupList(container, groups, activeGroup, onGroupClick) {
|
||||
container.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 목록 HTML 렌더링 (Pinned Categories 전용)
|
||||
*/
|
||||
export function renderCategoryList(container, pinnedCategories, activeCategory, onCategoryClick) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
pinnedCategories.forEach(cat => {
|
||||
const li = document.createElement('li');
|
||||
li.className = (cat === activeCategory) ? 'active' : '';
|
||||
li.title = cat; // 💡 사이드바 축소 시 이름을 보여주기 위해 title 추가
|
||||
li.innerHTML = `<span class="icon">🏷️</span> <span class="text">${escapeHTML(cat)}</span>`;
|
||||
li.onclick = () => onCategoryClick(cat);
|
||||
container.appendChild(li);
|
||||
});
|
||||
|
||||
if (pinnedCategories.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.style.cssText = 'font-size: 0.8rem; color: var(--muted); padding: 5px 15px; cursor: default;';
|
||||
li.textContent = I18nManager.t('label_no_category');
|
||||
container.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ export const ThemeManager = {
|
||||
}
|
||||
|
||||
// ... 나머지 모달 제어 로직 유지 (기존 코드와 동일)
|
||||
if (settingsBtn) settingsBtn.onclick = () => settingsModal.classList.add('active');
|
||||
if (settingsBtn) {
|
||||
settingsBtn.onclick = () => {
|
||||
const langSelect = document.getElementById('set-lang');
|
||||
if (langSelect) this.initialLang = langSelect.value;
|
||||
settingsModal.classList.add('active');
|
||||
};
|
||||
}
|
||||
if (closeSettingsBtn) closeSettingsBtn.onclick = () => settingsModal.classList.remove('active');
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
@@ -57,13 +63,23 @@ export const ThemeManager = {
|
||||
data[mapping[p.id]] = p.value;
|
||||
});
|
||||
data['enable_ai'] = document.getElementById('set-enable-ai').checked;
|
||||
data['enable_categories'] = document.getElementById('set-enable-categories').checked;
|
||||
|
||||
// 언어 설정이 UI에 있다면 추가 (현재는 config.json 수동 명시 권장이나 대비책 마련)
|
||||
const langSelect = document.getElementById('set-lang');
|
||||
if (langSelect) data['lang'] = langSelect.value;
|
||||
const newLang = langSelect ? langSelect.value : (this.initialLang || 'ko');
|
||||
if (langSelect) data['lang'] = newLang;
|
||||
|
||||
try {
|
||||
await API.saveSettings(data);
|
||||
|
||||
// 언어가 변경되었다면 페이지를 새로고침하여 모든 매니저들을 새로운 언어로 재초기화합니다.
|
||||
if (this.initialLang && this.initialLang !== newLang) {
|
||||
alert(I18nManager.t('msg_settings_saved'));
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applyTheme(data);
|
||||
alert(I18nManager.t('msg_settings_saved'));
|
||||
settingsModal.classList.remove('active');
|
||||
@@ -80,7 +96,8 @@ export const ThemeManager = {
|
||||
card_color: "rgba(30, 41, 59, 0.85)",
|
||||
encrypted_border: "#00f3ff",
|
||||
ai_accent: "#8b5cf6",
|
||||
lang: "ko"
|
||||
lang: "ko",
|
||||
enable_categories: false
|
||||
};
|
||||
this.applyTheme(defaults);
|
||||
}
|
||||
@@ -92,6 +109,11 @@ export const ThemeManager = {
|
||||
* 테마 데이터를 실제 CSS 변수 및 UI 요소에 반영
|
||||
*/
|
||||
async applyTheme(settings) {
|
||||
this.settings = settings; // NEW: 설정 캐시 저장
|
||||
if (window.UI) {
|
||||
window.UI._updateSettingsCache(settings);
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
'bg_color': '--bg',
|
||||
'sidebar_color': '--sidebar',
|
||||
@@ -117,7 +139,15 @@ export const ThemeManager = {
|
||||
const aiToggle = document.getElementById('set-enable-ai');
|
||||
if (aiToggle) aiToggle.checked = enableAI;
|
||||
|
||||
// 3. i18n 적용
|
||||
// 3. 카테고리 활성화 상태 적용 (고급 옵션)
|
||||
const enableCategories = (settings.enable_categories === true);
|
||||
const catToggle = document.getElementById('set-enable-categories');
|
||||
if (catToggle) catToggle.checked = enableCategories;
|
||||
if (window.UI && typeof window.UI.applyCategoryVisibility === 'function') {
|
||||
window.UI.applyCategoryVisibility(enableCategories);
|
||||
}
|
||||
|
||||
// 4. i18n 적용
|
||||
const lang = settings.lang || 'ko';
|
||||
await I18nManager.init(lang);
|
||||
const langSelect = document.getElementById('set-lang');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,13 @@ export const EditorManager = {
|
||||
sessionFiles: new Set(), // 이번 세션에 새로 추가된 파일 트래킹 (취소 시 삭제용)
|
||||
|
||||
init(elSelector, onCtrlEnter) {
|
||||
// 이미 초기화된 경우 기존 에디터 인스턴스 반환 및 중복 방지
|
||||
const container = document.querySelector(elSelector);
|
||||
if (this.editor && container && container.querySelector('.toastui-editor-defaultUI')) {
|
||||
console.log('[Editor] Already initialized, skipping init.');
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// --- 플러그인 설정 (글자 색상) ---
|
||||
|
||||
+44
-1
@@ -19,6 +19,9 @@ const DOM = {
|
||||
scrollSentinel: document.getElementById('scrollSentinel')
|
||||
};
|
||||
|
||||
// 모듈 레벨의 설정 캐시 관리 (this 바인딩 문제 해결)
|
||||
let settingsCache = {};
|
||||
|
||||
export const UI = {
|
||||
/**
|
||||
* 사이드바 및 로그아웃 버튼 초기화
|
||||
@@ -98,14 +101,51 @@ export const UI = {
|
||||
/**
|
||||
* 사이드바 시스템 고정 메뉴 상태 갱신
|
||||
*/
|
||||
updateSidebar(memos, activeGroup, onGroupClick) {
|
||||
updateSidebar(memos, activeGroup, activeCategory, onGroupClick, onCategoryClick) {
|
||||
if (!DOM.systemNav) return;
|
||||
|
||||
// 1. 시스템 그룹 동기화
|
||||
DOM.systemNav.querySelectorAll('li').forEach(li => {
|
||||
const group = li.dataset.group;
|
||||
li.className = (group === activeGroup) ? 'active' : '';
|
||||
li.onclick = () => onGroupClick(group);
|
||||
});
|
||||
|
||||
// 2. 카테고리 동기화 (Pinned Categories)
|
||||
import('./components/SidebarUI.js').then(({ renderCategoryList }) => {
|
||||
const categoryNav = document.getElementById('categoryNav');
|
||||
|
||||
// 💡 settingsCache가 비어있을 경우 ThemeManager에서 직접 복구 시도
|
||||
const pinned = settingsCache.pinned_categories || (ThemeManager.settings ? ThemeManager.settings.pinned_categories : []);
|
||||
|
||||
renderCategoryList(categoryNav, pinned, activeCategory, onCategoryClick);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 카테고리 기능 활성화 여부에 따라 UI 요소 노출 제어
|
||||
*/
|
||||
applyCategoryVisibility(enabled) {
|
||||
const composerBar = document.getElementById('composerCategoryBar');
|
||||
const sidebarSection = document.getElementById('categorySidebarSection');
|
||||
|
||||
if (composerBar) {
|
||||
// 작성기 칩 영역은 가로 정렬을 위해 flex 레이아웃이 필수입니다.
|
||||
composerBar.style.display = enabled ? 'flex' : 'none';
|
||||
}
|
||||
if (sidebarSection) {
|
||||
// 사이드바 섹션은 기본 블록 레이아웃을 사용합니다.
|
||||
sidebarSection.style.display = enabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
console.log(`Category UI visibility updated: ${enabled ? 'VISIBLE' : 'HIDDEN'}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 설정 캐시 업데이트 (내부용)
|
||||
*/
|
||||
_updateSettingsCache(settings) {
|
||||
settingsCache = settings;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -201,6 +241,9 @@ export const UI = {
|
||||
}
|
||||
};
|
||||
|
||||
// 전역 동기화를 위해 window 객체에 할당
|
||||
window.UI = UI;
|
||||
|
||||
/**
|
||||
* 전역 파일 다운로드 함수 (항상 전역 스코프 유지)
|
||||
*/
|
||||
|
||||
+12
-1
@@ -11,6 +11,8 @@
|
||||
"nav_logout": "Logout",
|
||||
"nav_settings": "Settings",
|
||||
"nav_toggle": "Toggle Sidebar",
|
||||
"nav_categories": "Categories",
|
||||
"nav_category_manage": "Manage Categories",
|
||||
|
||||
"search_placeholder": "Search memos... (Title, Content, Tag)",
|
||||
"composer_placeholder": "Leave a fragment of knowledge...",
|
||||
@@ -31,6 +33,7 @@
|
||||
"settings_security": "Security Border Color",
|
||||
"settings_ai_accent": "AI Accent Color",
|
||||
"settings_ai_enable": "Enable AI Features",
|
||||
"settings_category_enable": "Enable Category Feature (Advanced)",
|
||||
"settings_lang": "Language",
|
||||
"settings_save": "Save Settings",
|
||||
"settings_reset": "Reset",
|
||||
@@ -90,6 +93,12 @@
|
||||
"label_group_explorer": "📁 Group Explorer",
|
||||
"label_tag_explorer": "🏷️ Tag Explorer",
|
||||
"label_last_updated": "Last updated: ",
|
||||
"label_category_done": "Done",
|
||||
"label_no_category": "No Category",
|
||||
"tooltip_add_category": "Add/Edit Category",
|
||||
"prompt_category_name": "Enter category name (max 10 chars):",
|
||||
"msg_confirm_delete_category": "Delete this category?",
|
||||
"msg_category_limit": "Maximum 3 categories can be pinned.",
|
||||
|
||||
"shortcuts_label": "⌨️ Shortcuts",
|
||||
"shortcut_save": "Save",
|
||||
@@ -109,7 +118,9 @@
|
||||
"h2": "Heading 2",
|
||||
"h3": "Heading 3",
|
||||
"ai_summary": "AI Summary",
|
||||
"ai_tags": "AI Tags"
|
||||
"ai_tags": "AI Tags",
|
||||
"shortcut_cat_3": "Alt+4: %s",
|
||||
"shortcut_cat_clear": "Alt+5: Clear Category"
|
||||
},
|
||||
|
||||
"calendar_months": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"nav_logout": "로그아웃",
|
||||
"nav_settings": "환경 설정",
|
||||
"nav_toggle": "사이드바 토글",
|
||||
"nav_categories": "카테고리",
|
||||
"nav_category_manage": "카테고리 관리",
|
||||
|
||||
"search_placeholder": "메모 검색... (제목, 내용, 태그)",
|
||||
"composer_placeholder": "지식의 파편을 남겨주세요...",
|
||||
@@ -31,6 +33,7 @@
|
||||
"settings_security": "보안 테두리색",
|
||||
"settings_ai_accent": "AI 분석 강조색",
|
||||
"settings_ai_enable": "AI 기능 활성화",
|
||||
"settings_category_enable": "카테고리 기능 활성화 (고급)",
|
||||
"settings_lang": "언어 설정",
|
||||
"settings_save": "저장",
|
||||
"settings_reset": "초기화",
|
||||
@@ -89,6 +92,12 @@
|
||||
"label_group_explorer": "📁 그룹별 탐색",
|
||||
"label_tag_explorer": "🏷️ 태그별 탐색",
|
||||
"label_last_updated": "마지막 수정: ",
|
||||
"label_category_done": "완료",
|
||||
"label_no_category": "카테고리 없음",
|
||||
"tooltip_add_category": "카테고리 추가/편집",
|
||||
"prompt_category_name": "새 카테고리 이름을 입력하세요 (최대 10자):",
|
||||
"msg_confirm_delete_category": "이 카테고리를 삭제할까요?",
|
||||
"msg_category_limit": "카테고리는 최대 3개까지만 핀(Pin) 고정 가능합니다.",
|
||||
|
||||
"shortcuts_label": "⌨️ 단축키",
|
||||
"shortcut_save": "저장",
|
||||
@@ -96,6 +105,8 @@
|
||||
"shortcut_nebula": "네뷸라",
|
||||
"shortcut_slash": "슬래시 명령",
|
||||
"shortcut_edit": "즉시 수정",
|
||||
"shortcut_cat_3": "Alt+4: %s",
|
||||
"shortcut_cat_clear": "Alt+5: 분류 해제",
|
||||
|
||||
"slash": {
|
||||
"task": "체크박스",
|
||||
|
||||
Reference in New Issue
Block a user