From f7c7394a94ee1d864817c8b2387cef5917bb6187 Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 9 Jul 2026 16:39:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(listings):=20ToolsTab=20=EC=A0=9C=EC=B6=9C?= =?UTF-8?q?=C2=B7disclaimer=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20+=20=EC=88=98=EC=A7=91=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=8F=B4=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 최종 리뷰 반영: (1) ToolsTab 제출→중첩객체·disclaimer 렌더 경로 테스트 2건 추가(최고위험 미검증 경로), (2) handleCollect의 고정 3초 타임아웃을 collected_at 변경 기반 폴링(최대 60s)으로 교체 — 진행 중을 완료로 오표시 하지 않도록. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS --- src/pages/listings/components/ListingsTab.jsx | 19 +++++++++++- .../listings/components/ToolsTab.test.jsx | 29 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/pages/listings/components/ListingsTab.jsx b/src/pages/listings/components/ListingsTab.jsx index 79cef86..2fe81f4 100644 --- a/src/pages/listings/components/ListingsTab.jsx +++ b/src/pages/listings/components/ListingsTab.jsx @@ -68,13 +68,30 @@ const ListingsTab = () => { const handleCollect = async () => { setCollecting(true); + setError(''); + const prevAt = collectStatus?.collected_at ?? null; try { await collectListings(); - setTimeout(() => { loadStatus(); load(); setCollecting(false); }, 3000); } catch (e) { setError(e?.message ?? String(e)); setCollecting(false); + return; } + let tries = 0; + const poll = async () => { + tries += 1; + let st = null; + try { st = await getListingsCollectStatus(); } catch { /* noop */ } + if (st) setCollectStatus(st); + const done = st && (st.collected_at !== prevAt || st.error); + if (done || tries >= 12) { + setCollecting(false); + load(); + return; + } + window.setTimeout(poll, 5000); + }; + window.setTimeout(poll, 5000); }; return ( diff --git a/src/pages/listings/components/ToolsTab.test.jsx b/src/pages/listings/components/ToolsTab.test.jsx index 285c7b7..cca7678 100644 --- a/src/pages/listings/components/ToolsTab.test.jsx +++ b/src/pages/listings/components/ToolsTab.test.jsx @@ -1,7 +1,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; vi.mock('../../../api', () => ({ safetyCheck: vi.fn(), budgetCalc: vi.fn() })); +import { safetyCheck, budgetCalc } from '../../../api'; import ToolsTab from './ToolsTab.jsx'; beforeEach(() => vi.clearAllMocks()); @@ -14,4 +15,30 @@ describe('ToolsTab', () => { // 제출 버튼 존재 expect(screen.getAllByRole('button', { name: /판정|계산/ }).length).toBeGreaterThan(0); }); + + it('안전마진 제출 → tier·비율·disclaimer 렌더 (객체 아님)', async () => { + safetyCheck.mockResolvedValue({ median: 45000, ratio: 0.72, tier: '안전', sample: 7, is_toheo: false, disclaimer: '등기부 선순위·근사치 주의' }); + render(); + fireEvent.change(screen.getByPlaceholderText(/면적/), { target: { value: '84' } }); + fireEvent.change(screen.getByPlaceholderText(/금액/), { target: { value: '45000' } }); + fireEvent.click(screen.getByRole('button', { name: '판정' })); + await waitFor(() => expect(screen.getByText(/등기부 선순위/)).toBeInTheDocument()); + // "안전마진 체커" 헤딩과 구분하기 위해 "마진"이 뒤따르지 않는 "안전"만 매칭 + expect(screen.getByText(/안전(?!마진)/)).toBeInTheDocument(); + }); + + it('예산 제출 → 전세/매매 금액·disclaimer 렌더 (중첩 객체 필드별 렌더)', async () => { + budgetCalc.mockResolvedValue({ + jeonse: { loan_limit: 30000, max_deposit: 40000, notes: '' }, + purchase: { ltv_pct: 70, loan_cap: 50000, max_price: 90000, dsr_note: 'DSR 40%', regulation_flags: ['규제지역'] }, + region: { is_toheo: true, is_regulated: true, notes: '토허 구역' }, + disclaimer: '정책 수치는 근사치입니다', + }); + render(); + fireEvent.change(screen.getByPlaceholderText(/자기자본/), { target: { value: '20000' } }); + fireEvent.click(screen.getByRole('button', { name: '계산' })); + await waitFor(() => expect(screen.getByText(/정책 수치는 근사치/)).toBeInTheDocument()); + // purchase.regulation_flags 와 region.is_regulated 플래그 둘 다 "규제지역" 텍스트를 렌더함 + expect(screen.getAllByText('규제지역').length).toBeGreaterThan(0); + }); });