/* ============================================================ atlas.js — Protein Atlas 탭 (단백질 그리드 + NGL 3D 구조) ============================================================ */ (function () { 'use strict'; const AXIS_LABELS = { AND: "안드로겐(AR/DHT)", Wnt: "Wnt/β-catenin", BMP: "BMP/TGFβ", SHH: "Hedgehog", INF: "염증/JAK-STAT", APO: "세포사멸/산화", DP: "진피유두/성장인자", HFSC: "모낭 줄기세포", }; const AXIS_COLOR = { AND: "#f87171", Wnt: "#6699ff", BMP: "#a78bfa", SHH: "#22d3ee", INF: "#fbbf24", APO: "#fb923c", DP: "#34d399", HFSC: "#f472b6", "—": "#64748b", }; let nglStage = null, selectedGene = null; function el(id) { return document.getElementById(id); } function init() { const axisSel = el('atlas-axis'), disSel = el('atlas-disease'); const axes = [...new Set(Store.catalog.proteins.map(p => p.twin_node))].filter(a => a && a !== '—'); axes.forEach(a => axisSel.add(new Option(AXIS_LABELS[a] || a, a))); const diseases = [...new Set(Store.catalog.proteins.flatMap(p => p.diseases || []))].sort(); diseases.forEach(d => disSel.add(new Option(d, d))); ['atlas-search', 'atlas-axis', 'atlas-disease', 'atlas-sort'].forEach(id => el(id).addEventListener('input', renderGrid)); // resize 핸들러는 단 한 번만 등록 (이전: loadStructure 마다 추가 → 누수) window.addEventListener('resize', () => nglStage && nglStage.handleResize()); renderGrid(); } function filtered() { const q = el('atlas-search').value.trim().toLowerCase(); const ax = el('atlas-axis').value, dis = el('atlas-disease').value, sort = el('atlas-sort').value; let list = Store.catalog.proteins.filter(p => { if (ax && p.twin_node !== ax) return false; if (dis && !(p.diseases || []).includes(dis)) return false; if (q && !(p.gene.toLowerCase().includes(q) || (p.name || '').toLowerCase().includes(q))) return false; return true; }); const plddt = p => (p.structure && p.structure.mean_plddt) || 0; const sorters = { evidence: (a, b) => b.n_evidence - a.n_evidence || b.mention_count - a.mention_count, mention: (a, b) => b.mention_count - a.mention_count, plddt: (a, b) => plddt(b) - plddt(a), gene: (a, b) => a.gene.localeCompare(b.gene), }; return list.sort(sorters[sort] || sorters.evidence); } function renderGrid() { const list = filtered(); el('atlas-count').textContent = list.length; const grid = el('atlas-grid'); grid.innerHTML = list.map(p => { const ax = p.twin_node || '—'; const seed = p.in_model ? '●seed' : `${p.n_evidence}편`; return `
${seed}
${p.gene}
${p.name || ''}
${ax} ${p.role || ''}
`; }).join(''); grid.querySelectorAll('.prot-card').forEach(c => c.onclick = () => showDetail(c.dataset.gene)); } function hex(h, a) { const n = parseInt((h || '#64748b').slice(1), 16); return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`; } function showDetail(gene) { const p = Store.proteinByGene[gene]; if (!p) return; if (gene === selectedGene && !el('atlas-detail').classList.contains('hidden')) return; // 동일 단백질 재로딩 방지 selectedGene = gene; document.querySelectorAll('.prot-card').forEach(c => c.classList.toggle('selected', c.dataset.gene === gene)); el('atlas-detail-empty').classList.add('hidden'); el('atlas-detail').classList.remove('hidden'); el('d-gene').textContent = p.gene; el('d-name').textContent = p.uniprot_name || p.name || ''; const st = p.structure || {}; el('d-badges').innerHTML = [ p.in_model ? '트윈 wired' : '', `${AXIS_LABELS[p.twin_node] || '축 미배정'}`, `${p.role || ''}`, ].filter(Boolean).join(''); // 메타 그리드 el('d-meta').innerHTML = [ ['경로 (Pathway)', p.pathway || '—'], ['UniProt', st.accession || '—'], ['질환', (p.diseases || []).join(', ') || '—'], ['서열 길이', p.length ? p.length + ' aa' : '—'], ['PDB 실험구조', (p.pdb_count || 0) + '개'], ['논문 언급', p.mention_count + '회'], ].map(([k, v]) => `
${k}
${v}
`).join(''); el('d-mech').textContent = p.mechanism || p.function || '기전 정보 없음'; el('d-drugs').innerHTML = (p.drugs || []).length ? p.drugs.map(d => `${d}`).join('') : '등록된 표적 약물 없음'; // 근거 논문 const ev = (p.evidence_paper_ids || []); el('d-evcount').textContent = ev.length; el('d-evidence').innerHTML = ev.length ? ev.slice(0, 30).map(id => { const info = Store.papersIdx[id] || {}; return ` ${info.year || ''}${info.title || ('PMID ' + id)}`; }).join('') : '초록 스캔/추출 근거 없음 (캐논 표적)'; // 실제 논문 전문 근거 (검증 인용) — 요소 없을 때(구버전 HTML 캐시) 안전 가드 const gr = p.grounding || []; const gcEl = el('d-groundcount'); if (gcEl) gcEl.textContent = gr.length; const grEl = el('d-grounding'); if (grEl) grEl.innerHTML = gr.length ? gr.map(h => `
📄 ${h.paper} ${h.n_hits}회 언급
"…${(h.quote || '').replace(/"/g, '"')}…"
`).join('') : (p.grounded_in_corpus === false ? '분석 코퍼스(papers/) 전문에 근거 없음 — 웹 발굴(2024–26) 또는 canonical 경로 멤버' : ''); // 외부 링크 el('d-links').innerHTML = [ st.uniprot_url ? `UniProt ↗` : '', st.af_entry_url ? `AlphaFold DB ↗` : '', ].filter(Boolean).join(''); el('d-plddt').textContent = st.mean_plddt ? `평균 pLDDT ${st.mean_plddt} (${st.plddt_band || ''})` : '구조 신뢰도 미계산'; loadStructure(p); } function loadStructure(p) { const viewer = el('ngl-viewer'); const st = p.structure || {}; const afLink = st.af_entry_url || (st.accession ? `https://alphafold.ebi.ac.uk/entry/${st.accession}` : null); const fallback = (msg) => { viewer.innerHTML = `
${msg}` + (afLink ? `AlphaFold에서 3D 구조 보기 ↗` : '') + `
`; }; viewer.innerHTML = '
AlphaFold 구조 로딩 중…
'; if (typeof NGL === 'undefined') { fallback('NGL 라이브러리 미로딩'); return; } if (!st.accession) { fallback('구조 정보 없음'); return; } if (nglStage) { try { nglStage.dispose(); } catch (e) {} nglStage = null; } try { nglStage = new NGL.Stage('ngl-viewer', { backgroundColor: '#0e0d0b' }); } catch (e) { fallback('3D 뷰어(WebGL) 사용 불가'); return; } // 서빙 디렉토리/오프라인 무관 — 로컬(프로젝트루트) → 원격 AlphaFold 순 폴백 const candidates = []; if (st.local_pdb) candidates.push(['../' + st.local_pdb.replace(/\\/g, '/'), 'pdb']); if (st.af_cif_url) candidates.push([st.af_cif_url, 'cif']); if (st.af_pdb_url) candidates.push([st.af_pdb_url, 'pdb']); (function tryLoad(i) { if (i >= candidates.length) { fallback('구조 로드 실패 (네트워크/경로)'); return; } const [url, ext] = candidates[i]; let comp_done = false; nglStage.loadFile(url, { ext }).then(comp => { const ld = viewer.querySelector('.ngl-loading'); if (ld) ld.remove(); comp.addRepresentation('cartoon', { colorScheme: 'bfactor', colorScale: ['#FF7D45', '#FFDB13', '#65CBF3', '#0053D6'], colorDomain: [40, 100], smoothSheet: true, }); comp.autoView(); try { nglStage.setSpin(true); } catch (e) {} comp_done = true; }).catch(() => tryLoad(i + 1)); })(0); } window.AtlasTab = { init, resize: () => nglStage && nglStage.handleResize() }; window.openProtein = function (gene) { document.querySelector('.tab-btn[data-tab="atlas"]').click(); setTimeout(() => showDetail(gene), 60); }; })();