From 7afa4615aa6934a7631b3d3eeceb6a6c9bdf54b6 Mon Sep 17 00:00:00 2001 From: "Eunice(Eunyoung) Shin" Date: Sun, 5 Jul 2026 00:37:41 +0900 Subject: [PATCH 1/2] fix: clean up insight map evidence panel UX - remove relation text labels (e.g. observed_in/measures) drawn on edges - shrink evidence panel and match search box's light theme color - reflow graph on container resize so the panel pushes layout instead of covering nodes --- src/pages/InsightMapPage.tsx | 64 ++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/pages/InsightMapPage.tsx b/src/pages/InsightMapPage.tsx index df422f1..fc08c81 100644 --- a/src/pages/InsightMapPage.tsx +++ b/src/pages/InsightMapPage.tsx @@ -212,22 +212,6 @@ export default function InsightMapPage() { return `url(#arrow-${source?.source_layer ?? 'ontology'})` }) - // 관계 라벨(related_to 등)을 모든 엣지에 항상 그리면 그래프가 난잡해지므로 - // 기본 상태에서는 숨기고, 노드 선택으로 하이라이트된 엣지에서만 보여준다. - // 호버 시에는 네이티브 툴팁으로 확인 가능. - 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') @@ -294,7 +278,6 @@ 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)) selectNode(d) }) .on('mouseover', function () { @@ -308,7 +291,6 @@ export default function InsightMapPage() { 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) }) sim.on('tick', () => { @@ -318,14 +300,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]) @@ -334,7 +328,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]" /> @@ -423,12 +417,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> @@ -444,11 +438,11 @@ 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> )} @@ -456,33 +450,33 @@ export default function InsightMapPage() { {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} @@ -493,11 +487,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> ))} From b58726bc8348830c547419eb24e75abd0a9bb5ed Mon Sep 17 00:00:00 2001 From: "Eunice(Eunyoung) Shin" <eunice.shin54@gmail.com> Date: Sun, 5 Jul 2026 00:41:09 +0900 Subject: [PATCH 2/2] fix: dim unrelated edges on node selection like nodes Previously only stroke color/width changed on selection, so unrelated edges stayed fully opaque and looked just as strong as highlighted ones. --- src/pages/InsightMapPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/InsightMapPage.tsx b/src/pages/InsightMapPage.tsx index fc08c81..04fd428 100644 --- a/src/pages/InsightMapPage.tsx +++ b/src/pages/InsightMapPage.tsx @@ -278,6 +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)) + linkSel.attr('opacity', (_, i) => (linkIdx.has(i) ? 1 : 0.15)) selectNode(d) }) .on('mouseover', function () { @@ -290,7 +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) + linkSel.attr('stroke', '#313244').attr('stroke-width', 1.5).attr('opacity', 1) }) sim.on('tick', () => {