Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 31 additions & 36 deletions src/pages/InsightMapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,6 @@ export default function InsightMapPage() {
return `url(#arrow-${source?.source_layer ?? 'ontology'})`
})

// 관계 라벨(related_to 등)을 모든 엣지에 항상 그리면 그래프가 난잡해지므로
// 기본 상태에서는 숨기고, 노드 선택으로 하이라이트된 엣지에서만 보여준다.
// 호버 시에는 네이티브 <title> 툴팁으로 확인 가능.
linkSel.append('title').text((d) => d.relation)

const edgeLabelSel = g
.append('g')
.selectAll('text')
.data(simLinks)
.join('text')
.attr('font-size', 9)
.attr('fill', '#45475a')
.attr('pointer-events', 'none')
.attr('opacity', 0)
.text((d) => d.relation)

const nodeSel = g
.append('g')
.selectAll<SVGGElement, SimNode>('g')
Expand Down Expand Up @@ -294,7 +278,7 @@ export default function InsightMapPage() {
nodeSel.select('text').attr('opacity', (n) => (ids.has(n.id) ? 1 : 0.15))
linkSel.attr('stroke', (_, i) => (linkIdx.has(i) ? '#89b4fa' : '#313244'))
linkSel.attr('stroke-width', (_, i) => (linkIdx.has(i) ? 2.5 : 1.5))
edgeLabelSel.attr('opacity', (_, i) => (linkIdx.has(i) ? 1 : 0))
linkSel.attr('opacity', (_, i) => (linkIdx.has(i) ? 1 : 0.15))
selectNode(d)
})
.on('mouseover', function () {
Expand All @@ -307,8 +291,7 @@ export default function InsightMapPage() {
svg.on('click', () => {
nodeSel.select('circle').attr('opacity', 1)
nodeSel.select('text').attr('opacity', 1)
linkSel.attr('stroke', '#313244').attr('stroke-width', 1.5)
edgeLabelSel.attr('opacity', 0)
linkSel.attr('stroke', '#313244').attr('stroke-width', 1.5).attr('opacity', 1)
})

sim.on('tick', () => {
Expand All @@ -318,14 +301,26 @@ export default function InsightMapPage() {
.attr('x2', (d) => (d.target as SimNode).x ?? 0)
.attr('y2', (d) => (d.target as SimNode).y ?? 0)

edgeLabelSel
.attr('x', (d) => (((d.source as SimNode).x ?? 0) + ((d.target as SimNode).x ?? 0)) / 2)
.attr('y', (d) => (((d.source as SimNode).y ?? 0) + ((d.target as SimNode).y ?? 0)) / 2)

nodeSel.attr('transform', (d) => `translate(${d.x ?? 0},${d.y ?? 0})`)
})

// 근거 패널이 열리고 닫힐 때 그래프 영역(flex-1)의 실제 폭이 바뀌므로,
// 컨테이너 크기 변화를 감지해 SVG와 시뮬레이션 중심을 다시 맞춰준다.
// 이게 없으면 패널이 열려도 노드는 예전 폭 기준 위치에 그대로 남아
// 패널에 가려지게 된다.
const resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]
if (!entry) return
const newWidth = entry.contentRect.width
const newHeight = entry.contentRect.height
svg.attr('width', newWidth).attr('height', newHeight)
sim.force('center', d3.forceCenter(newWidth / 2, newHeight / 2))
sim.alpha(0.3).restart()
})
resizeObserver.observe(container)

return () => {
resizeObserver.disconnect()
sim.stop()
}
}, [graph])
Expand All @@ -334,7 +329,7 @@ export default function InsightMapPage() {

return (
<div className="flex h-full w-full bg-white text-[#1e1e2e] font-mono overflow-hidden">
<div ref={containerRef} className="flex-1 relative">
<div ref={containerRef} className="flex-1 relative overflow-hidden">
{/* header overlay */}
<div className="absolute top-0 left-0 right-0 z-10 px-4 py-3 flex items-center gap-3 bg-gradient-to-b from-white to-transparent">
<Icon name="network" size={18} className="text-[#89b4fa]" />
Expand Down Expand Up @@ -423,12 +418,12 @@ export default function InsightMapPage() {

{/* side panel */}
{selectedNode && (
<div className="w-80 flex-shrink-0 border-l border-[#313244] bg-[#1e1e2e] p-4 overflow-y-auto">
<div className="w-72 flex-shrink-0 border-l border-[#d0d7de] bg-[#f4faf6] p-4 overflow-y-auto">
<div className="flex items-center justify-between mb-3">
<p className="text-[13px] font-bold text-[#cdd6f4]">{selectedNode.label}</p>
<p className="text-[13px] font-bold text-[#1e1e2e]">{selectedNode.label}</p>
<button
onClick={() => selectNode(null)}
className="text-[#6c7086] hover:text-[#cdd6f4]"
className="text-[#6c7086] hover:text-[#1e1e2e]"
>
<Icon name="x" size={14} />
</button>
Expand All @@ -444,45 +439,45 @@ export default function InsightMapPage() {
</span>

{selectedNode.summary && (
<p className="text-[12px] text-[#a6adc8] leading-relaxed mb-3">{selectedNode.summary}</p>
<p className="text-[12px] text-[#45475a] leading-relaxed mb-3">{selectedNode.summary}</p>
)}

{selectedNode.type === 'patient_metric_trend' && (
<pre className="text-[11px] bg-[#11111b] border border-[#313244] rounded-lg p-2 overflow-x-auto text-[#a6adc8]">
<pre className="text-[11px] bg-white border border-[#d0d7de] rounded-lg p-2 overflow-x-auto text-[#45475a]">
{JSON.stringify(selectedNode.data.trend, null, 2)}
</pre>
)}

{selectedNode.type === 'soap_case_index' && (
<button
onClick={() => handleViewSoapNote(selectedNode)}
className="w-full mt-2 py-1.5 bg-[#89b4fa] text-[#1e1e2e] text-[11px] font-semibold rounded-lg hover:brightness-110 transition-all"
className="w-full mt-2 py-1.5 bg-[#89b4fa] text-white text-[11px] font-semibold rounded-lg hover:brightness-110 transition-all"
>
SOAP note 보기
</button>
)}

{selectedNode.source_layer === 'ontology' && (
<div className="mt-3 pt-3 border-t border-[#313244]">
<div className="mt-3 pt-3 border-t border-[#d0d7de]">
{evidence === null && (
<button
onClick={() => handleLoadEvidence(selectedNode)}
disabled={evidenceLoading}
className="w-full py-1.5 border border-[#313244] text-[#a6adc8] text-[11px] font-semibold rounded-lg hover:bg-[#11111b] transition-colors disabled:opacity-60"
className="w-full py-1.5 border border-[#d0d7de] text-[#45475a] text-[11px] font-semibold rounded-lg hover:bg-white transition-colors disabled:opacity-60"
>
{evidenceLoading ? '문헌 근거 찾는 중…' : '문헌 근거 보기'}
</button>
)}

{evidence !== null && evidence.length === 0 && (
<p className="text-[11px] text-[#45475a]">관련 문헌 근거를 찾지 못했습니다.</p>
<p className="text-[11px] text-[#6c7086]">관련 문헌 근거를 찾지 못했습니다.</p>
)}

{evidence !== null && evidence.length > 0 && (
<div className="flex flex-col gap-2">
<p className="text-[10px] uppercase tracking-wide text-[#6c7086]">문헌 근거</p>
{evidence.map((item, i) => (
<div key={i} className="bg-[#11111b] border border-[#313244] rounded-lg p-2">
<div key={i} className="bg-white border border-[#d0d7de] rounded-lg p-2">
{item.doi_url ? (
<a
href={item.doi_url}
Expand All @@ -493,11 +488,11 @@ export default function InsightMapPage() {
{item.title}
</a>
) : (
<p className="text-[11px] font-semibold text-[#cdd6f4]">{item.title}</p>
<p className="text-[11px] font-semibold text-[#1e1e2e]">{item.title}</p>
)}
<p className="text-[10px] text-[#6c7086] mt-1 line-clamp-3">{item.content}</p>
{!item.doi_url && (
<p className="text-[9px] text-[#45475a] mt-1">원문 링크 없음 (국내 자료)</p>
<p className="text-[9px] text-[#6c7086] mt-1">원문 링크 없음 (국내 자료)</p>
)}
</div>
))}
Expand Down
Loading