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 offsetRef = useRef(0);
const loadingRef = useRef(false); const loadingRef = useRef(false);
const requestIdRef = useRef(0);
const filtersRef = useRef(filters); const filtersRef = useRef(filters);
filtersRef.current = filters; filtersRef.current = filters;
const filterKey = JSON.stringify(filters); const filterKey = JSON.stringify(filters);
const fetchPage = useCallback(async (offset, replace) => { 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; loadingRef.current = true;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const data = await agentActivity({ ...filtersRef.current, limit: PAGE_SIZE, offset }); const data = await agentActivity({ ...filtersRef.current, limit: PAGE_SIZE, offset });
if (reqId !== requestIdRef.current) return; // 더 새로운 요청이 시작됨 → stale 응답 무시
const newItems = Array.isArray(data?.items) ? data.items : []; const newItems = Array.isArray(data?.items) ? data.items : [];
setTotal(data?.total || 0); setTotal(data?.total || 0);
setItems(prev => (replace ? newItems : [...prev, ...newItems])); setItems(prev => (replace ? newItems : [...prev, ...newItems]));
offsetRef.current = offset + newItems.length; offsetRef.current = offset + newItems.length;
} catch (e) { } catch (e) {
if (reqId !== requestIdRef.current) return;
setError(e.message || '불러오기 실패'); setError(e.message || '불러오기 실패');
} finally { } finally {
loadingRef.current = false; if (reqId === requestIdRef.current) {
setLoading(false); loadingRef.current = false;
setLoading(false);
}
} }
}, []); }, []);

View File

@@ -56,4 +56,18 @@ describe('useActivityFeed', () => {
await waitFor(() => expect(result.current.items).toHaveLength(1)); await waitFor(() => expect(result.current.items).toHaveLength(1));
expect(result.current.hasMore).toBe(true); 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);
});
}); });