Skip to content
60 changes: 60 additions & 0 deletions frontend/src/components/SearchLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe("SearchLayout product events", () => {
root = null;
container?.remove();
container = null;
vi.restoreAllMocks();
vi.unstubAllGlobals();
clearRecordedProductEvents();
});
Expand Down Expand Up @@ -193,4 +194,63 @@ describe("SearchLayout product events", () => {
)).toBe(true);
expect(JSON.stringify(getRecordedProductEvents())).not.toContain("계약");
});

it("does not repeat the active-result lookup for unrelated input state", async () => {
const originalFind = Array.prototype.find;
let resultLookupCount = 0;
const findSpy = vi.spyOn(Array.prototype, "find").mockImplementation(function (
this: unknown[],
predicate: (value: unknown, index: number, obj: unknown[]) => unknown,
thisArg?: unknown,
) {
if (this.some((value) =>
typeof value === "object" && value !== null && "id" in value && "subject" in value
)) {
resultLookupCount += 1;
}
return originalFind.call(this, predicate, thisArg);
});
vi.stubGlobal("fetch", vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.endsWith("/api/search")) {
return Promise.resolve(jsonResponse({
results: [{
id: 101,
source_message_id: "<source@example.com>",
subject: "런칭 캠페인 결과",
sender: "pm@example.com",
date: "2026-05-20T09:00:00Z",
snippet: "검색 결과",
thread_id: "thread-launch",
reply_count: 2,
score: 0.93,
}],
}));
}
if (url.endsWith("/api/search/answer")) {
return Promise.resolve(jsonResponse({ answer: null, citations: [] }));
}
if (url.includes("/api/ontology/relationships?")) {
return Promise.resolve(jsonResponse([]));
}
throw new Error(`Unexpected fetch: ${url}`);
}));

container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(<SearchLayout />);
});
await waitForCondition(() => container?.textContent?.includes("런칭 캠페인 결과") ?? false);
const lookupCountAfterResults = resultLookupCount;
const input = container.querySelector<HTMLInputElement>("#search-input");

await act(async () => {
setInputValue(input as HTMLInputElement, "무관한 입력 상태");
});

expect(resultLookupCount).toBe(lookupCountAfterResults);
findSpy.mockRestore();
});
});
6 changes: 4 additions & 2 deletions frontend/src/components/SearchLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,12 @@ export function SearchLayout() {
return results;
}, [activeFilter, results]);

const activeResult =
// ⚡ Bolt: Memoize activeResult to prevent O(N) re-renders
const activeResult = useMemo(() => (
filteredResults.find((result) => result.id === activeResultId) ??
filteredResults[0] ??
null;
null
), [filteredResults, activeResultId]);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
const activeOntologySourceKey = ontologySourceKey(activeResult);
const activeOntologyUrl = activeResult
? buildOntologyUrl(activeResult)
Expand Down
Loading