From 833b590afb66c6c4371b957fca00fdf65765add4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 09:14:03 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent-office):=20useActivityFeed=20stale=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=AC=B4=EC=8B=9C=20(=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EC=A4=91=20in-flight=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/pages/agent-office/hooks/useActivityFeed.js | 13 ++++++++++--- .../agent-office/hooks/useActivityFeed.test.js | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/pages/agent-office/hooks/useActivityFeed.js b/src/pages/agent-office/hooks/useActivityFeed.js index 893998e..ed2e44f 100644 --- a/src/pages/agent-office/hooks/useActivityFeed.js +++ b/src/pages/agent-office/hooks/useActivityFeed.js @@ -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); + } } }, []); diff --git a/src/pages/agent-office/hooks/useActivityFeed.test.js b/src/pages/agent-office/hooks/useActivityFeed.test.js index 4a1f52a..e2fd6bd 100644 --- a/src/pages/agent-office/hooks/useActivityFeed.test.js +++ b/src/pages/agent-office/hooks/useActivityFeed.test.js @@ -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); + }); });