fix(agent-office): useActivityFeed stale 응답 무시 (필터 변경 중 in-flight 요청)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:14:03 +09:00
parent ce980b6eff
commit 833b590afb
2 changed files with 24 additions and 3 deletions

View File

@@ -12,27 +12,34 @@ export function useActivityFeed(filters, refreshTrigger = 0) {
const offsetRef = useRef(0);
const loadingRef = useRef(false);
const requestIdRef = useRef(0);
const filtersRef = useRef(filters);
filtersRef.current = filters;
const filterKey = JSON.stringify(filters);
const fetchPage = useCallback(async (offset, replace) => {
if (loadingRef.current) return;
// append(loadMore)만 중복 방지. replace(필터/refresh 재조회)는 항상 우선 진행.
if (!replace && loadingRef.current) return;
const reqId = ++requestIdRef.current;
loadingRef.current = true;
setLoading(true);
setError(null);
try {
const data = await agentActivity({ ...filtersRef.current, limit: PAGE_SIZE, offset });
if (reqId !== requestIdRef.current) return; // 더 새로운 요청이 시작됨 → stale 응답 무시
const newItems = Array.isArray(data?.items) ? data.items : [];
setTotal(data?.total || 0);
setItems(prev => (replace ? newItems : [...prev, ...newItems]));
offsetRef.current = offset + newItems.length;
} catch (e) {
if (reqId !== requestIdRef.current) return;
setError(e.message || '불러오기 실패');
} finally {
loadingRef.current = false;
setLoading(false);
if (reqId === requestIdRef.current) {
loadingRef.current = false;
setLoading(false);
}
}
}, []);

View File

@@ -56,4 +56,18 @@ describe('useActivityFeed', () => {
await waitFor(() => expect(result.current.items).toHaveLength(1));
expect(result.current.hasMore).toBe(true);
});
it('필터 변경 중이던 이전(stale) 요청 응답은 무시된다', async () => {
let resolveFirst;
const firstPromise = new Promise(r => { resolveFirst = r; });
mockAgentActivity
.mockReturnValueOnce(firstPromise) // 초기 요청 — 느리게 resolve
.mockResolvedValueOnce({ items: [item({ task_id: 'fresh', agent_id: 'insta' })], total: 1 });
const { result, rerender } = renderHook(({ f }) => useActivityFeed(f, 0), { initialProps: { f: { days: 7 } } });
rerender({ f: { days: 7, agent_id: 'insta' } }); // 첫 요청 resolve 전에 필터 변경
await waitFor(() => expect(result.current.items[0]?.task_id).toBe('fresh'));
await act(async () => { resolveFirst({ items: [item({ task_id: 'stale' })], total: 99 }); });
expect(result.current.items[0].task_id).toBe('fresh'); // stale이 덮어쓰지 않음
expect(result.current.total).toBe(1);
});
});