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

213 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 모달 창(메모 상세, 파일 라이브러리 등) 생성을 관리하는 모듈
*/
import { API } from '../api.js';
import { escapeHTML } from '../utils.js';
import { renderAttachmentBox } from './AttachmentBox.js';
import { I18nManager } from '../utils/I18nManager.js';
import { Constants } from '../utils/Constants.js';
export const ModalManager = {
// 타이밍 이슈 방지를 위해 lazy getter 패턴 적용
getDOM() {
return {
modal: document.getElementById('memoModal'),
modalContent: document.getElementById('modalContent'),
loadingOverlay: document.getElementById('loadingOverlay'),
explorerModal: document.getElementById('explorerModal'),
explorerContent: document.getElementById('explorerContent')
};
},
/**
* 전체 첨부파일 라이브러리(Asset Library) 모달 열기
*/
async openAssetLibrary(openMemoDetailsCallback) {
const dom = this.getDOM();
if (!dom.loadingOverlay) return;
dom.loadingOverlay.style.display = 'flex';
try {
const assets = await API.fetchAssets();
let html = `
<div style="padding:20px; position:relative;">
<button class="close-modal-btn">×</button>
<h2 style="margin-bottom:20px;">${I18nManager.t('label_asset_management')}</h2>
<p style="font-size:0.8rem; color:var(--muted); margin-bottom:20px;">${I18nManager.t('label_asset_hint')}</p>
<div style="display:grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap:15px;">
${assets.length > 0 ? assets.map(a => `
<div class="asset-card" data-memo-id="${a.memo_id}" data-url="/api/download/${a.filename}" style="background:rgba(255,255,255,0.05); padding:10px; border-radius:8px; cursor:pointer;">
${['png','jpg','jpeg','gif','webp','svg'].includes(a.file_type?.toLowerCase())
? `<img src="/api/download/${a.filename}" style="width:100%; height:120px; object-fit:cover; border-radius:4px; margin-bottom:8px;">`
: `<div style="width:100%; height:120px; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.2); border-radius:4px; margin-bottom:8px; font-size:2rem;">📎</div>`
}
<div style="font-size:0.8rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHTML(a.original_name)}</div>
<div style="font-size:0.7rem; color:var(--muted);">${a.memo_title ? `${I18nManager.t('label_memo_ref')}${escapeHTML(a.memo_title)}` : I18nManager.t('label_no_memo_ref')}</div>
</div>
`).join('') : `<div style="grid-column:1/-1; text-align:center; padding:40px; color:var(--muted);">${I18nManager.t('label_no_assets')}</div>`}
</div>
</div>
`;
dom.modalContent.innerHTML = html;
dom.modal.classList.add('active');
// 닫기 버튼 이벤트
dom.modalContent.querySelector('.close-modal-btn').onclick = () => {
dom.modal.classList.remove('active');
};
dom.modalContent.querySelectorAll('.asset-card').forEach(card => {
card.onclick = (e) => {
const url = card.dataset.url;
const filename = url.split('/').pop();
const originalName = card.querySelector('div').innerText;
const memoId = card.dataset.memoId;
if (e.altKey) {
e.stopPropagation();
window.downloadFile(filename, originalName);
} else if (memoId && memoId !== 'null') {
dom.modal.classList.remove('active');
openMemoDetailsCallback(memoId, window.allMemosCache);
} else {
window.downloadFile(filename, originalName);
}
};
});
} catch (err) { alert(err.message); }
finally { dom.loadingOverlay.style.display = 'none'; }
},
/**
* 지식 탐색기(Knowledge Explorer) 모달 열기
*/
openKnowledgeExplorer(memos, activeFilter, onFilterCallback) {
const dom = this.getDOM();
// 1. 그룹 및 태그 카운트 계산
const groupAllKey = 'all';
const groupCounts = { [groupAllKey]: memos.length };
const tagCounts = {};
const tagsSourceMap = new Map(); // 태그명 -> 소스 매핑
memos.forEach(m => {
const g = m.group_name || Constants.GROUPS.DEFAULT;
groupCounts[g] = (groupCounts[g] || 0) + 1;
if (m.tags) {
m.tags.forEach(t => {
tagCounts[t.name] = (tagCounts[t.name] || 0) + 1;
const current = tagsSourceMap.get(t.name);
if (!current || t.source === 'user') tagsSourceMap.set(t.name, t.source);
});
}
});
const sortedGroups = Object.keys(groupCounts)
.filter(g => g !== groupAllKey)
.sort((a,b) => a === Constants.GROUPS.DEFAULT ? -1 : b === Constants.GROUPS.DEFAULT ? 1 : a.localeCompare(b));
const sortedTags = Object.keys(tagCounts).sort().map(tn => ({
name: tn,
source: tagsSourceMap.get(tn),
count: tagCounts[tn]
}));
let html = `
<div class="explorer-section">
<h3 style="margin-bottom:15px; color:var(--accent);">${I18nManager.t('label_group_explorer')}</h3>
<div class="explorer-grid">
<div class="explorer-chip ${activeFilter === 'all' ? 'active' : ''}" data-filter="all">
💡 ${I18nManager.t('nav_all')} <span class="chip-count">${groupCounts[groupAllKey]}</span>
</div>
${sortedGroups.map(g => `
<div class="explorer-chip ${activeFilter === g ? 'active' : ''}" data-filter="${escapeHTML(g)}">
📁 ${escapeHTML(g)} <span class="chip-count">${groupCounts[g]}</span>
</div>
`).join('')}
</div>
</div>
<div class="explorer-section" style="margin-top:30px;">
<h3 style="margin-bottom:15px; color:var(--ai-accent);">${I18nManager.t('label_tag_explorer')}</h3>
<div class="explorer-grid">
${sortedTags.map(t => `
<div class="explorer-chip tag-chip ${activeFilter === `tag:${t.source}:${t.name}` ? 'active' : ''}"
data-filter="tag:${t.source}:${escapeHTML(t.name)}">
${t.source === 'ai' ? '🪄' : '🏷️'} ${escapeHTML(t.name)} <span class="chip-count">${t.count}</span>
</div>
`).join('')}
</div>
</div>
`;
dom.explorerContent.innerHTML = html;
dom.explorerModal.classList.add('active');
// 이벤트 바인딩
const closeBtn = dom.explorerModal.querySelector('.close-explorer-btn');
closeBtn.onclick = () => dom.explorerModal.classList.remove('active');
dom.explorerContent.querySelectorAll('.explorer-chip').forEach(chip => {
chip.onclick = () => {
const filter = chip.dataset.filter;
onFilterCallback(filter);
dom.explorerModal.classList.remove('active');
};
});
},
/**
* 개별 메모 상세 모달 열기
*/
openMemoModal(id, memos) {
const dom = this.getDOM();
const memo = memos.find(m => m.id == id);
if (!memo) return;
import('../utils.js').then(({ parseInternalLinks, fixImagePaths }) => {
// 마크다운 파싱 후 살균 처리 (marked, DOMPurify는 global 사용)
let html = DOMPurify.sanitize(marked.parse(memo.content));
html = parseInternalLinks(html);
html = fixImagePaths(html);
const lastUpdatedTime = new Date(memo.updated_at).toLocaleString();
dom.modalContent.innerHTML = `
<button class="close-modal-btn">×</button>
${memo.title ? `<h2 style="margin-bottom:10px;">${escapeHTML(memo.title)}</h2>` : ''}
${memo.summary ? `
<div class="ai-summary-box" style="margin: 15px 0 25px 0; padding: 15px; background: rgba(56, 189, 248, 0.1); border-left: 4px solid var(--accent); border-radius: 8px; position: relative; overflow: hidden;">
<div style="font-size: 0.7rem; color: var(--accent); font-weight: 800; margin-bottom: 8px; display: flex; align-items: center; gap: 5px; letter-spacing: 0.05em;">
<span>🪄 AI INSIGHT</span>
</div>
<div style="font-size: 0.95rem; line-height: 1.6; color: #e2e8f0; font-weight: 400;">${escapeHTML(memo.summary)}</div>
</div>
` : '<hr style="margin:15px 0; opacity:0.1">'}
<div class="memo-content">${html}</div>
<div style="margin-top:20px; font-size:0.8rem; color:var(--muted)">${I18nManager.t('label_last_updated')}${lastUpdatedTime}</div>
`;
// 닫기 버튼 이벤트
const closeBtn = dom.modalContent.querySelector('.close-modal-btn');
if (closeBtn) {
closeBtn.onclick = () => dom.modal.classList.remove('active');
}
const attachmentsHtml = renderAttachmentBox(memo.attachments);
if (attachmentsHtml) {
const footer = document.createElement('div');
footer.style.cssText = 'margin-top:30px; padding-top:15px; border-top:1px solid rgba(255,255,255,0.05);';
footer.innerHTML = attachmentsHtml;
dom.modalContent.appendChild(footer);
}
dom.modal.classList.add('active');
dom.modalContent.querySelectorAll('.internal-link').forEach(l => {
l.onclick = () => this.openMemoModal(l.dataset.id, memos);
});
});
}
};