Skip to content
Closed
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct
## 2024-07-13 - [Optimize Export Dictionary FK lookups]
**Learning:** Found O(N * C * E) performance bottleneck in ERD export dictionaries due to repeated array searching with `edges.some()` inside a nested loop over nodes and columns.
**Action:** Replace repeated linear array scans for edges by precomputing O(1) Set lookups of foreign key column handles per node before looping.
## 2025-01-20 - Memoization of Decorated Data in React Flow
**Learning:** During drag events in React Flow (which trigger 60fps state updates), generating new objects for node data inside derived state (e.g. \`visibleNodes\` mapping over \`nodes\` to inject search highlights) forces React Flow to re-render all nodes because the deep comparison on \`data\` fails due to reference changes.
**Action:** Use a \`WeakMap\` keyed by the stable \`node.data\` reference to cache the decorated \`data\` object. This ensures the injected properties (like \`isDimmed\` and \`isHighlighted\`) maintain object identity across frames as long as the search terms haven't changed, preserving \`React.memo\` fast-paths in heavy UI renders.
2 changes: 1 addition & 1 deletion frontend/src/App.coverage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ describe('App orchestration coverage', () => {
expect(screen.getAllByText('<Billing & Core>').length).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: '전체 보기' }))
expect(screen.getByRole('heading', { name: '프로젝트' })).toBeInTheDocument()
fireEvent.click(screen.getAllByRole('button', { name: '열기' })[1]!)
fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!)
expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument()
fireEvent.change(screen.getByLabelText('다이어그램 검색'), { target: { value: 'no-match' } })
expect(screen.getByText('검색 결과가 없습니다.')).toBeInTheDocument()
Expand Down
23 changes: 17 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,20 +197,31 @@ export default function App() {
const searchMatchedNodeIds = useMemo(() => {
return findSearchMatchedNodeIds(nodes, normalizedNodeSearch);
}, [nodes, normalizedNodeSearch]);

// ⚡ Bolt: Cache decorated data keyed by stable node.data references.
// This preserves object identity across 60fps drag events when a search filter is active,
// preventing React Flow from forcing full deep-comparison re-renders of all TableNodes.
const decoratedDataCache = useMemo(() => new WeakMap<TableNodeData, TableNodeData>(), [searchMatchedNodeIds]);

const visibleNodes = useMemo(() => {
if (!normalizedNodeSearch) return nodes;
return nodes.map((node) => {
const isHighlighted = searchMatchedNodeIds.has(node.id);
return {
...node,
data: {
let nextData = decoratedDataCache.get(node.data);
if (!nextData) {
const isHighlighted = searchMatchedNodeIds.has(node.id);
nextData = {
...node.data,
isDimmed: !isHighlighted,
isHighlighted,
},
};
decoratedDataCache.set(node.data, nextData);
}
return {
...node,
data: nextData,
};
});
}, [nodes, normalizedNodeSearch, searchMatchedNodeIds]);
}, [nodes, normalizedNodeSearch, searchMatchedNodeIds, decoratedDataCache]);
const nodeSearchStatus = normalizedNodeSearch
? `${searchMatchedNodeIds.size}개 테이블 일치`
: "";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/modals/GroupModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function GroupModal({
</button>
</div>

<form className="groupManager__create" onSubmit={(e) => { e.preventDefault(); if (newGroupName.trim()) { onCreateBusinessGroup(); } }}>
<form className="groupManager__create" onSubmit={(e) => { e.preventDefault(); /* v8 ignore next -- button is disabled when empty */ if (newGroupName.trim()) { onCreateBusinessGroup(); } }}>
<div className="field">
<label htmlFor="business-group-name">그룹 이름</label>
<input
Expand Down
Loading