/** * 모달 창(메모 상세, 파일 라이브러리 등) 생성을 관리하는 모듈 */ 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 = `

${I18nManager.t('label_asset_management')}

${I18nManager.t('label_asset_hint')}

${assets.length > 0 ? assets.map(a => `
${['png','jpg','jpeg','gif','webp','svg'].includes(a.file_type?.toLowerCase()) ? `` : `
📎
` }
${escapeHTML(a.original_name)}
${a.memo_title ? `${I18nManager.t('label_memo_ref')}${escapeHTML(a.memo_title)}` : I18nManager.t('label_no_memo_ref')}
`).join('') : `
${I18nManager.t('label_no_assets')}
`}
`; 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 = `

${I18nManager.t('label_group_explorer')}

💡 ${I18nManager.t('nav_all')} ${groupCounts[groupAllKey]}
${sortedGroups.map(g => `
📁 ${escapeHTML(g)} ${groupCounts[g]}
`).join('')}

${I18nManager.t('label_tag_explorer')}

${sortedTags.map(t => `
${t.source === 'ai' ? '🪄' : '🏷️'} ${escapeHTML(t.name)} ${t.count}
`).join('')}
`; 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 = ` ${memo.title ? `

${escapeHTML(memo.title)}

` : ''} ${memo.summary ? `
🪄 AI INSIGHT
${escapeHTML(memo.summary)}
` : '
'}
${html}
${I18nManager.t('label_last_updated')}${lastUpdatedTime}
`; // 닫기 버튼 이벤트 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); }); }); } };