diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..1027e6939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased] +- **Performance (Bolt)**: `NetworkGraph` 컴포넌트의 세 옵션 생성 경로를 bounded `for...of` 순회로 바꿔 제품 상한에서 조기에 멈추고 불필요한 중간 컬렉션 생성을 줄였습니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/frontend/src/components/NetworkGraph.map-lookup.test.ts b/frontend/src/components/NetworkGraph.map-lookup.test.ts index 3ba76c75c..6d2d3b133 100644 --- a/frontend/src/components/NetworkGraph.map-lookup.test.ts +++ b/frontend/src/components/NetworkGraph.map-lookup.test.ts @@ -63,4 +63,33 @@ describe("NetworkGraph constant-time selection lookup contract", () => { expect(networkGraphSource).toContain("firstGraphEntryById(nodes"); expect(networkGraphSource).not.toMatch(/new Map\((edges|nodes)\.map\(/); }); + + it("bounds option construction before traversing complete graph collections", () => { + const nodeLabels = sourceBetween("const nodeLabels = useMemo", "const firstEdge ="); + const relationshipOptions = sourceBetween( + "const relationshipOptions = useMemo", + "const nodeOptions = useMemo", + ); + const nodeOptions = sourceBetween( + "const nodeOptions = useMemo", + "const selectRelationship =", + ); + + expect(nodeLabels).toContain("for (const node of nodes)"); + expect(nodeLabels).toContain("if (result.length >= 5) break;"); + expect(nodeLabels).not.toContain(".map("); + expect(nodeLabels).not.toContain(".filter("); + + expect(relationshipOptions).toContain("for (const edge of edgeMap.values())"); + expect(relationshipOptions).toContain("if (result.length >= 5) break;"); + expect(relationshipOptions).not.toContain(".map("); + expect(relationshipOptions).not.toContain(".filter("); + expect(relationshipOptions).not.toContain("Array.from("); + + expect(nodeOptions).toContain("for (const node of nodeInstanceMap.values())"); + expect(nodeOptions).toContain("if (result.length >= 8) break;"); + expect(nodeOptions).not.toContain(".map("); + expect(nodeOptions).not.toContain(".filter("); + expect(nodeOptions).not.toContain("Array.from("); + }); }); diff --git a/frontend/src/components/NetworkGraph.tsx b/frontend/src/components/NetworkGraph.tsx index f9eb61c71..0fbca4332 100644 --- a/frontend/src/components/NetworkGraph.tsx +++ b/frontend/src/components/NetworkGraph.tsx @@ -278,27 +278,44 @@ export default function NetworkGraph() { }, [nodes, edges, nodeMap, edgeMap]); const nodeLabels = useMemo(() => { - return nodes - .map((node) => String(node.label ?? node.id)) - .filter(Boolean) - .slice(0, 5); + const result: string[] = []; + for (const node of nodes) { + const label = String(node.label ?? node.id); + if (label) { + result.push(label); + if (result.length >= 5) break; + } + } + return result; }, [nodes]); const firstEdge = edges[0] ?? null; const relationshipOptions = useMemo(() => { - return Array.from(edgeMap.values()).slice(0, 5).map((edge, index) => ({ - edge, - id: String(edge.id), - label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, - })); + const result = []; + let index = 0; + for (const edge of edgeMap.values()) { + result.push({ + edge, + id: String(edge.id), + label: `관계 ${index + 1}: ${describeEdge(edge, nodeMap)}`, + }); + index++; + if (result.length >= 5) break; + } + return result; }, [edgeMap, nodeMap]); const nodeOptions = useMemo(() => { - return Array.from(nodeInstanceMap.values()).slice(0, 8).map((node) => ({ - id: String(node.id), - label: `노드: ${String(node.label ?? node.id)}`, - node, - })); + const result = []; + for (const node of nodeInstanceMap.values()) { + result.push({ + id: String(node.id), + label: `노드: ${String(node.label ?? node.id)}`, + node, + }); + if (result.length >= 8) break; + } + return result; }, [nodeInstanceMap]); const selectRelationship = (edge: Edge, status: string) => {