From aa7e195dd95febe88f433d42e642af69fa1758d7 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:47:51 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Replace=20O(N)=20Array.?= =?UTF-8?q?from().slice=20with=20O(1)=20bounded=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/NetworkGraph.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/NetworkGraph.tsx b/frontend/src/components/NetworkGraph.tsx index f9eb61c71..5558f7b16 100644 --- a/frontend/src/components/NetworkGraph.tsx +++ b/frontend/src/components/NetworkGraph.tsx @@ -286,7 +286,13 @@ export default function NetworkGraph() { const firstEdge = edges[0] ?? null; const relationshipOptions = useMemo(() => { - return Array.from(edgeMap.values()).slice(0, 5).map((edge, index) => ({ + // ⚡ Bolt: Replace O(N) Array.from(edgeMap.values()).slice with bounded O(1) loop + const topEdges = []; + for (const edge of edgeMap.values()) { + if (topEdges.length >= 5) break; + topEdges.push(edge); + } + return topEdges.map((edge, index) => ({ edge, id: String(edge.id), label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, @@ -294,7 +300,13 @@ export default function NetworkGraph() { }, [edgeMap, nodeMap]); const nodeOptions = useMemo(() => { - return Array.from(nodeInstanceMap.values()).slice(0, 8).map((node) => ({ + // ⚡ Bolt: Replace O(N) Array.from(nodeInstanceMap.values()).slice with bounded O(1) loop + const topNodes = []; + for (const node of nodeInstanceMap.values()) { + if (topNodes.length >= 8) break; + topNodes.push(node); + } + return topNodes.map((node) => ({ id: String(node.id), label: `노드: ${String(node.label ?? node.id)}`, node, From d65b05992cee54964114c9011a2bbbddd663062f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:01:59 +0900 Subject: [PATCH 2/2] test(network): cover bounded graph options --- frontend/src/components/NetworkGraph.test.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/frontend/src/components/NetworkGraph.test.tsx b/frontend/src/components/NetworkGraph.test.tsx index 328d7c543..1424caa23 100644 --- a/frontend/src/components/NetworkGraph.test.tsx +++ b/frontend/src/components/NetworkGraph.test.tsx @@ -236,6 +236,78 @@ describe("NetworkGraph", () => { expect(mountedContainer.textContent).not.toContain("nodes and"); }); + it.each([ + [0, 0], + [3, 3], + [5, 5], + [7, 5], + ])("keeps %i relationships in insertion order up to the five-option cap", async (edgeCount, expectedCount) => { + const nodes = Array.from({ length: 8 }, (_, index) => ({ + id: `node-${index}`, + label: `노드 ${index}`, + })); + const edges = Array.from({ length: edgeCount }, (_, index) => ({ + id: `edge-${index}`, + from: `node-${index}`, + to: `node-${index + 1}`, + title: `관계 ${index}`, + })); + vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(jsonResponse({ nodes, edges })))); + + await renderGraph(); + await flushAsyncWork(); + + const options = Array.from( + getMountedContainer().querySelectorAll( + 'select[aria-label="관계 선택"] option:not([value=""])', + ), + ); + expect(options).toHaveLength(expectedCount); + expect(options.map((option) => option.value)).toEqual( + Array.from({ length: expectedCount }, (_, index) => `edge-${index}`), + ); + expect(options.map((option) => option.textContent)).toEqual( + Array.from( + { length: expectedCount }, + (_, index) => `관계 ${index + 1}: 노드 ${index} -> 노드 ${index + 1} (관계 ${index})`, + ), + ); + }); + + it.each([ + [0, 0], + [4, 4], + [8, 8], + [10, 8], + ])("keeps %i nodes in insertion order up to the eight-option cap", async (nodeCount, expectedCount) => { + const nodes = Array.from({ length: nodeCount }, (_, index) => ({ + id: `node-${index}`, + label: `노드 ${index}`, + })); + vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(jsonResponse({ nodes, edges: [] })))); + + await renderGraph(); + await flushAsyncWork(); + + const nodeSelect = getMountedContainer().querySelector( + 'select[aria-label="노드 선택"]', + ); + if (expectedCount === 0) { + expect(nodeSelect).toBeNull(); + return; + } + const options = Array.from( + nodeSelect?.querySelectorAll('option:not([value=""])') ?? [], + ); + expect(options).toHaveLength(expectedCount); + expect(options.map((option) => option.value)).toEqual( + Array.from({ length: expectedCount }, (_, index) => `node-${index}`), + ); + expect(options.map((option) => option.textContent)).toEqual( + Array.from({ length: expectedCount }, (_, index) => `노드: 노드 ${index}`), + ); + }); + it("exposes accessible relationship detail and zoom controls for the graph", async () => { const fetchMock = vi.fn(() => Promise.resolve(