@@ -138,8 +138,8 @@ export default function GraphPage() {
) : (
<>
- 노드 {snapshot.nodes.length}개 · 관계 {snapshot.links.length}개
- {highlight.length > 0 && " · 흰 테두리 = RAG 근거 노드"}
+ 노드 {snapshot.nodes.length}개 / 관계 {snapshot.links.length}개
+ {highlight.length > 0 && " (흰색 노드: GraphRAG 질의 근거)"}
>
@@ -154,7 +154,7 @@ export default function GraphPage() {
)}
- Standing by · 스캔 대기
+ Standing by / 스캔 대기
버튼을 눌러 Neo4j 그래프 스냅샷을 시각화하세요.
diff --git a/frontend/src/components/GraphView.tsx b/frontend/src/components/GraphView.tsx
index fba0b93..0b55731 100644
--- a/frontend/src/components/GraphView.tsx
+++ b/frontend/src/components/GraphView.tsx
@@ -12,9 +12,78 @@ const ForceGraph2D = dynamic(() => import("react-force-graph-2d"), {
loading: () =>
그래프 로딩 중…
,
});
-// 폴백색 — 실제로는 렌더 시 토큰(--accent 등)을 실측해 쓴다. 인디고/앰버 듀오 유지.
-const NODE_COLOR = "#6c5cff";
-const NODE_HI = "#faad13";
+// 5대 핵심 도메인 시맨틱 팔레트 (Neon Ledger 디자인 시스템 연동)
+export interface NodeCategory {
+ id: string;
+ label: string;
+ color: string;
+}
+
+export const NODE_CATEGORIES: NodeCategory[] = [
+ { id: "tax", label: "세법 / 세율 규정", color: "#6c5cff" },
+ { id: "legal", label: "법령 조문 / 근거", color: "#faad13" },
+ { id: "asset", label: "부동산 / 자산 유형", color: "#05c168" },
+ { id: "condition", label: "감면 / 공제 / 요건", color: "#06b6d4" },
+ { id: "household", label: "가구 / 기간 요건", color: "#ff4d67" },
+ { id: "etc", label: "기타 엔티티", color: "#9094a8" },
+];
+
+export function getNodeCategory(rawGroup?: string): NodeCategory {
+ if (!rawGroup) return NODE_CATEGORIES[5];
+ const g = rawGroup.toUpperCase();
+ if (
+ g.includes("TAX") ||
+ g.includes("RATE") ||
+ g.includes("BRACKET") ||
+ g.includes("AMOUNT") ||
+ g.includes("BASE") ||
+ g.includes("LIMIT") ||
+ g.includes("보유기간")
+ ) {
+ if (g.includes("EXEMPT")) return NODE_CATEGORIES[3];
+ return NODE_CATEGORIES[0];
+ }
+ if (
+ g.includes("LEGAL") ||
+ g.includes("LAW") ||
+ g.includes("ARTICLE") ||
+ g.includes("POLICY") ||
+ g.includes("CLAUSE")
+ ) {
+ return NODE_CATEGORIES[1];
+ }
+ if (
+ g.includes("HOUSING") ||
+ g.includes("ASSET") ||
+ g.includes("PORTFOLIO") ||
+ g.includes("PROJECT") ||
+ g.includes("REGION") ||
+ g.includes("STOCK") ||
+ g.includes("PROPERTY")
+ ) {
+ return NODE_CATEGORIES[2];
+ }
+ if (
+ g.includes("CONDITION") ||
+ g.includes("EXEMPT") ||
+ g.includes("DEDUCTION") ||
+ g.includes("INCOME") ||
+ g.includes("REQUIREMENT")
+ ) {
+ return NODE_CATEGORIES[3];
+ }
+ if (
+ g.includes("HOUSEHOLD") ||
+ g.includes("FAMILY") ||
+ g.includes("DATE") ||
+ g.includes("TIMEFRAME") ||
+ g.includes("COMPOSITE") ||
+ g.includes("PERSON")
+ ) {
+ return NODE_CATEGORIES[4];
+ }
+ return NODE_CATEGORIES[5];
+}
// canvas fillStyle 은 var() 를 파싱하지 못하므로 렌더 시점에 토큰 값을 실측해 치환한다.
function tokenColor(varName: string, fallback: string): string {
@@ -33,9 +102,6 @@ export default function GraphView({
const [width, setWidth] = useState(800);
const [selected, setSelected] = useState
(null);
const hi = useMemo(() => new Set(highlight), [highlight]);
- // 표면/헤어라인은 --ink-1/--line, 강조(하이라이트 노드)만 --accent.
- const accentColor = tokenColor("--accent", NODE_COLOR);
- const surfaceColor = tokenColor("--ink-1", NODE_HI);
const lineColor = tokenColor("--line", "#2a2d35");
const nodeById = useMemo(() => {
@@ -56,6 +122,26 @@ export default function GraphView({
return (
+ {/* 상단 5대 시맨틱 카테고리 범례 바 */}
+
+
도메인 범례:
+ {NODE_CATEGORIES.slice(0, 5).map((cat) => (
+
+
+ {cat.label}
+
+ ))}
+ {hi.size > 0 && (
+
+
+ RAG 근거 노드 ({hi.size})
+
+ )}
+
+
- hi.has(String(n.id)) ? surfaceColor : accentColor
- }
- nodeVal={(n: NodeObject) => (hi.has(String(n.id)) ? 3 : 1)}
+ nodeColor={(n: NodeObject) => {
+ if (hi.has(String(n.id))) return "#ffffff";
+ const group = (n as { group?: string }).group;
+ return getNodeCategory(group).color;
+ }}
+ nodeVal={(n: NodeObject) => (hi.has(String(n.id)) ? 3.5 : 1.2)}
nodeLabel={(n: NodeObject) =>
`${n.id} (${(n as { group?: string }).group ?? ""})`
}
@@ -84,9 +172,12 @@ export default function GraphView({
/>
{selected && (
-
+
-
+
{selected.group}
);