Files
web-page/docs/superpowers/specs/2026-07-09-realestate-listings-design.md

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기능 전체):

  1. 매물 목록 조회 + 필터 + 판정 tier 뱃지 + 수동 수집 트리거.
  2. 조건(criteria) 편집 — 특히 equity/annual_income(만원) 입력 UI(예산상한·전세 max_deposit 매칭 반영).
  3. 안전마진 단건 체커(safety-check) — 즉시 전세가율/호가율 판정.
  4. 예산 계산기(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/listingsListings (routes.jsx appRoutes에 추가, lazy import). 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.csslst-* 프리픽스. Subscription의 CSS 변수(--text-bright/--text-dim/--text-muted/--surface/--surface-raised/--line/--radius-sm)·다크 테마 재사용. tier 뱃지 색은 판정 규칙 색(🟢#34d399/🟡#f59e0b/🔴#f87171/#94a3b8).


8. 테스트 (TDD)

listingsUtils.test.js — 순수헬퍼:

  1. tierMeta('safety', ...) 4종 + tierMeta('valuation', ...) 4종 이모지·색.
  2. 미정의 tier → 회색 + 원문 폴백.
  3. formatMoney: 만/억 경계(9999→"9,999만", 10000→"1억", 15000→"1억 5,000만"), null→'-'.
  4. 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는 보조.