Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
9.9 KiB
실매물 매물·안전마진 UI (/realestate/listings) — FE 설계
- 작성일: 2026-07-09
- 역할/저장소: FE (
web-ui) - 상위(BE): realestate-lab 매물 알림 + 안전마진 배포 완료(main a7be8f7),
web-backend§9 realestate /service_realestate.md. co-gahusb BE→FE msg id 3. - 범위: FE(web-ui)만. BE 계약(8 엔드포인트)을 소비하는 신규 전용 페이지. 매물 수집은 NAS 전담(web-ai 워커 불필요).
1. 배경 & 목표
BE가 실매물(매매/전세) 매물 수집 + 안전마진(전세가율)·적정성(호가율) 판정 + 예산 계산 기능을 배포했다. 이는 기존 /realestate(청약, Subscription)와 다른 도메인이므로 신규 전용 라우트 /realestate/listings에 구현한다.
목표(v1, 하위 4기능 전체):
- 매물 목록 조회 + 필터 + 판정 tier 뱃지 + 수동 수집 트리거.
- 조건(criteria) 편집 — 특히
equity/annual_income(만원) 입력 UI(예산상한·전세 max_deposit 매칭 반영). - 안전마진 단건 체커(safety-check) — 즉시 전세가율/호가율 판정.
- 예산 계산기(budget) — 자기자본·소득 기반 전세/매매/지역 한도.
비목표(YAGNI): 매물 상세 모달·지도, 실시간 알림 UI(텔레그램은 BE, FE는 notify_enabled 토글만), /announcements·청약 관련.
2. 소비할 BE 계약 (8 엔드포인트, 전부 /api/realestate/* 상대경로)
| 메서드 | 경로 | 요청/응답 |
|---|---|---|
| GET | /listings?dong=&deal_type=&tier=&matched_only=&page=&size= |
→ { listings: [ <매물아이템> ] } (+판정 join) |
| POST | /listings/collect |
수동 수집 트리거 |
| GET | /listings/collect/status |
→ { status, collected_at?, new_count?, total_count?, error? } (예: {status:"never_run"}) |
| GET | /listings/criteria |
→ 아래 criteria shape |
| PUT | /listings/criteria |
body=변경 필드만(부분 업데이트) → 갱신된 criteria |
| GET | /listings/matches |
→ { matches: [ <매칭+판정> ] } |
| POST | /safety-check |
body { area:float, deal_type, amount:만원, dong_code?, complex_name?, dong? } → { median, ratio, tier, sample, is_toheo, disclaimer } |
| POST | /budget |
body { equity, annual_income, is_homeless, is_householder, is_first_home, target_dong? } → { jeonse:{loan_limit,max_deposit,notes}, purchase:{ltv_pct,loan_cap,max_price,dsr_note,regulation_flags}, region:{is_toheo,is_regulated,notes}, disclaimer } |
criteria shape (프로덕션 확인):
{ id, dongs:string[], deal_types:string[]("전세"|"반전세"|"매매"), max_deposit:int(만원),
max_sale_price:int|null(만원), min_area:float, house_types:string[],
min_safety_tier:string|null, notify_enabled:0|1, equity:int|null(만원),
annual_income:int|null(만원), is_homeless:0|1, is_householder:0|1, is_first_home:0|1, updated_at }
판정 tier (표시 규칙):
- 임차(전세/반전세)
safety_tier(전세가율): 안전🟢 / 주의🟡 / 위험🔴 / 보류⚪ - 매매
valuation_tier(호가율): 저평가🟢 / 시세🟡 / 고가🔴 / 보류⚪ - 표본(sample) < 3 → tier="보류".
매물 아이템 shape (추론 — 수집 전이라 실샘플 없음, FE는 방어적 렌더): BE "매물 목록(+판정 join)" 설명 기준 예상 필드 = { id, complex_name?, dong?, deal_type?, area?, price?/deposit?/monthly?(만원), safety_tier?, valuation_tier?, ratio?, median?, regulation_flags?:[], reasons?:[], matched?, url? }. 존재하는 필드만 렌더하고 없는 필드는 생략. 수집 실행 후 실데이터로 필드명 확인·조정(구현 단계 verify).
⚠️
regulation_flags/reasons는 이미 배열(list)로 파싱되어 옴 —JSON.parse금지, 그대로 렌더. (BE 명시. Subscription의 옛JSON.parse패턴 답습 금지.)
3. 배치 & 라우팅
- 신규 라우트
realestate/listings→Listings(routes.jsxappRoutes에 추가,lazyimport).realestate/property(RealEstate) 서브라우트 패턴과 동일. - 교차 링크: Subscription(청약) 헤더에 "실매물 →"(
/realestate/listings), Listings 헤더에 "청약으로"(/realestate). 홈 허브realestate카드 description은 유지(선택적으로 "매물" 언급).
4. 컴포넌트 구조 (전용 모듈 — 접근안 A)
기존 Subscription.jsx(1642줄)에 얹지 않고 독립 모듈. 각 파일 단일 책임.
src/pages/listings/
├── Listings.jsx # 페이지 shell + 내부 탭바(매물/판정도구/조건) + useIsMobile
├── Listings.css # lst-* (Subscription의 --text-*/--surface/--line/--radius-* 토큰 재사용)
├── components/
│ ├── ListingsTab.jsx # 매물 목록 + 필터(동/거래유형/tier/매칭만) + 페이지네이션 + tier 뱃지 + 수집
│ ├── ToolsTab.jsx # 안전마진 체커 카드 + 예산 계산기 카드 (입력→결과, disclaimer)
│ └── CriteriaTab.jsx # 조건 편집 폼 (equity·annual_income 포함, 부분 PUT)
├── listingsUtils.js # 순수헬퍼 (tier 매핑·금액 포맷)
└── listingsUtils.test.js
내부 탭 3개(기능 4종 수용): 매물 / 판정 도구(안전마진+예산) / 조건.
4.1 listingsUtils.js (순수헬퍼 — 테스트 대상)
SAFETY_TIER_META = { 안전:{emoji:'🟢',color}, 주의:{emoji:'🟡',color}, 위험:{emoji:'🔴',color}, 보류:{emoji:'⚪',color} }
VALUATION_TIER_META = { 저평가:{'🟢'}, 시세:{'🟡'}, 고가:{'🔴'}, 보류:{'⚪'} }
tierMeta(kind, tier) // kind: 'safety'|'valuation'; 미정의 tier → 회색⚪ + 원문
formatMoney(만원) // int(만원) → "X억 Y만"/"X억"/"X만" 정밀 표기(9999→"9,999만", 10000→"1억", 15000→"1억 5,000만"), null→'-'
formatRatio(ratio) // 0.812 → "81.2%", null→'-'
4.2 컴포넌트 데이터 규칙
- 응답 방어적 파싱:
data.listings ?? [],data.matches ?? [],data.criteria ?? data. regulation_flags/reasons: 배열 그대로.map(JSON.parse 금지).- 어떤 필드든 객체면 직접 JSX 렌더 금지(watchlist detail 교훈) — tier/금액/문자열만 렌더, 배열은 칩/리스트.
5. API 레이어 (src/api.js 추가)
// ── Realestate Listings / 안전마진 / 예산 ──
export const getListings = ({dong,deal_type,tier,matched_only,page=1,size=20}={}) => { /* querystring, 빈값 제외 */ };
export const collectListings = () => apiPost('/api/realestate/listings/collect');
export const getListingsCollectStatus = () => apiGet('/api/realestate/listings/collect/status');
export const getListingsCriteria = () => apiGet('/api/realestate/listings/criteria');
export const putListingsCriteria = (body) => apiPut('/api/realestate/listings/criteria', body); // 부분 업데이트
export const getListingsMatches = () => apiGet('/api/realestate/listings/matches');
export const safetyCheck = (body) => apiPost('/api/realestate/safety-check', body);
export const budgetCalc = (body) => apiPost('/api/realestate/budget', body);
6. UX / 안전장치 (BE 강조)
- disclaimer 항상 노출: safety-check·budget 결과 카드 하단, matches/판정 영역에 disclaimer 문자열 표시. 등기부 선순위·토허·정책수치 근사 경고 → 잘못된 금융/안전 조언 방지. disclaimer가 응답에 있으면 반드시 렌더.
- 보류⚪: 표본<3 등 판정 불가 시 tier="보류" 그대로 표시(회색).
- 금액 만원 단위: 입력·표시 모두 만원. 큰 값은 억 환산 표시.
- is_toheo(토지거래허가)/regulation_flags: budget/safety 결과에 플래그 칩으로 표시.
- 조건 저장: 부분 PUT(변경 필드만). equity·annual_income 미입력 시 null 유지(매칭에 미반영됨을 힌트로 안내).
- 로딩/에러/빈 상태(수집 전
never_run이면 "수집을 실행해 매물을 모아보세요" 안내).
7. 스타일
Listings.css에 lst-* 프리픽스. Subscription의 CSS 변수(--text-bright/--text-dim/--text-muted/--surface/--surface-raised/--line/--radius-sm)·다크 테마 재사용. tier 뱃지 색은 판정 규칙 색(🟢#34d399/🟡#f59e0b/🔴#f87171/⚪#94a3b8).
8. 테스트 (TDD)
listingsUtils.test.js — 순수헬퍼:
tierMeta('safety', ...)4종 +tierMeta('valuation', ...)4종 이모지·색.- 미정의 tier → 회색⚪ + 원문 폴백.
formatMoney: 만/억 경계(9999→"9,999만", 10000→"1억", 15000→"1억 5,000만"), null→'-'.formatRatio: 0.812→"81.2%", null→'-'.
컴포넌트는 스모크(빈 상태 렌더 + 객체 필드 크래시 없음) + 빌드/lint 통과. safety-check/budget 폼 제출→결과 렌더는 mock 훅으로 스모크 가능(선택).
9. 완료 기준 (Acceptance)
/realestate/listings라우트 노출, Subscription↔Listings 교차 링크.- 매물 탭: 목록/필터/페이지네이션/tier 뱃지/수집 동작(빈 상태 안내 포함).
- 판정 도구 탭: 안전마진·예산 입력→결과 + disclaimer 노출.
- 조건 탭: criteria GET/부분 PUT, equity·annual_income 입력.
regulation_flags/reasons배열 그대로 렌더(JSON.parse 없음).listingsUtils.test.js통과,npm run lint·npm run build통과.
10. 리스크 / 오픈 이슈
- 매물 아이템 실스키마 미확정: 수집(
collect)이 아직 안 돌아/listings가 빈 배열. FE는 방어적 렌더(존재 필드만)로 대응하고, 수집 실행 후 실데이터로 필드명 확인·조정(구현 verify 단계). 필요 시 BE에 아이템 필드 목록 확인 요청. - safety-check dong_code: 동 코드(예 "11590")/이름 매핑 필요 시 criteria.dongs 활용. 입력은 동 이름 select + 선택적 dong_code.
- matches vs listings(matched_only): 매칭 결과는
matched_only=true필터 또는/matches중 택1 — v1은 매물 탭의 "매칭만" 토글로 통일,/matches는 보조.