perf(realestate): run_listing_matching 단일 conn+배치 + POST /listings/rematch(MOLIT없이 즉시 재판정)
- get_market_deals_for에 optional conn 파라미터 추가(재사용 시 open/close 안 함, 기존 단건 호출부는 불변) - bulk_upsert_listing_matches 추가(listing_matches 다건 upsert, 단일 connection) - run_listing_matching이 listings 조회+매물별 market_deals 조회+최종 upsert를 단일 _conn()으로 처리 (기존: 269건 매물마다 개별 _conn() 3회 → 병목) - POST /api/realestate/listings/rematch: MOLIT 재수집 없이 기존 listings+market_deals로 즉시 재판정 (알림 미발송, 즉시 피드백용) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9
This commit is contained in:
@@ -1019,10 +1019,14 @@ def bulk_upsert_market_deals(rows: List[Dict[str, Any]]) -> int:
|
|||||||
return len(payload)
|
return len(payload)
|
||||||
|
|
||||||
|
|
||||||
def get_market_deals_for(dong_code, complex_name, area, deal_type, months=6) -> List[Dict[str, Any]]:
|
def get_market_deals_for(dong_code, complex_name, area, deal_type, months=6, conn=None) -> List[Dict[str, Any]]:
|
||||||
with _conn() as conn:
|
"""conn을 넘기면 재사용(open/close 안 함, 호출부가 소유) — run_listing_matching 단일 conn 배치용.
|
||||||
|
conn 없으면 기존대로 자체 _conn() 열고 닫음(safety-check 등 단건 호출 경로 불변)."""
|
||||||
|
own = conn is None
|
||||||
|
c = conn or _conn()
|
||||||
|
try:
|
||||||
def _query(cn):
|
def _query(cn):
|
||||||
rows = conn.execute(
|
rows = c.execute(
|
||||||
"""SELECT * FROM market_deals
|
"""SELECT * FROM market_deals
|
||||||
WHERE dong_code=? AND deal_type=?
|
WHERE dong_code=? AND deal_type=?
|
||||||
AND (complex_name=? OR ? IS NULL OR complex_name IS NULL)
|
AND (complex_name=? OR ? IS NULL OR complex_name IS NULL)
|
||||||
@@ -1038,7 +1042,10 @@ def get_market_deals_for(dong_code, complex_name, area, deal_type, months=6) ->
|
|||||||
# 단지 표본이 부족하면 complex 제약을 풀고 같은 동/거래유형/면적±5/최근성으로
|
# 단지 표본이 부족하면 complex 제약을 풀고 같은 동/거래유형/면적±5/최근성으로
|
||||||
# 광역 재조회한다. 표본을 늘리기만 하므로 안전(잘못된 판정 유발 X).
|
# 광역 재조회한다. 표본을 늘리기만 하므로 안전(잘못된 판정 유발 X).
|
||||||
deals = _query(None)
|
deals = _query(None)
|
||||||
return deals
|
return deals
|
||||||
|
finally:
|
||||||
|
if own:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
def upsert_listing_match(data: Dict[str, Any]) -> None:
|
def upsert_listing_match(data: Dict[str, Any]) -> None:
|
||||||
@@ -1061,6 +1068,39 @@ def upsert_listing_match(data: Dict[str, Any]) -> None:
|
|||||||
""", d)
|
""", d)
|
||||||
|
|
||||||
|
|
||||||
|
def bulk_upsert_listing_matches(recs: List[Dict[str, Any]], conn=None) -> None:
|
||||||
|
"""listing_matches 다건 upsert(단일 connection). conn 주면 재사용(호출부 with가 commit) —
|
||||||
|
run_listing_matching 269건×3conn 병목 개선(Task 2). ON CONFLICT 절은 upsert_listing_match와 동일
|
||||||
|
(notified_at 미포함=보존, 재알림 방지)."""
|
||||||
|
if not recs:
|
||||||
|
return
|
||||||
|
cols = ("listing_id", "category", "passed", "match_score", "jeonse_ratio", "safety_tier",
|
||||||
|
"price_ratio", "valuation_tier", "market_median", "sample_size", "budget_ok",
|
||||||
|
"regulation_flags", "reasons", "is_new")
|
||||||
|
own = conn is None
|
||||||
|
c = conn or _conn()
|
||||||
|
try:
|
||||||
|
for data in recs:
|
||||||
|
d = {col: data.get(col) for col in cols}
|
||||||
|
d["regulation_flags"] = json.dumps(data.get("regulation_flags") or [], ensure_ascii=False)
|
||||||
|
d["reasons"] = json.dumps(data.get("reasons") or [], ensure_ascii=False)
|
||||||
|
c.execute(f"""
|
||||||
|
INSERT INTO listing_matches ({','.join(cols)}) VALUES ({','.join(':'+col for col in cols)})
|
||||||
|
ON CONFLICT(listing_id) DO UPDATE SET
|
||||||
|
passed=excluded.passed, match_score=excluded.match_score,
|
||||||
|
jeonse_ratio=excluded.jeonse_ratio, safety_tier=excluded.safety_tier,
|
||||||
|
price_ratio=excluded.price_ratio, valuation_tier=excluded.valuation_tier,
|
||||||
|
market_median=excluded.market_median, sample_size=excluded.sample_size,
|
||||||
|
budget_ok=excluded.budget_ok, regulation_flags=excluded.regulation_flags,
|
||||||
|
reasons=excluded.reasons, category=excluded.category
|
||||||
|
""", d)
|
||||||
|
if own:
|
||||||
|
c.commit()
|
||||||
|
finally:
|
||||||
|
if own:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
def get_listings(dong=None, deal_type=None, tier=None, matched_only=False, limit=50, offset=0) -> List[Dict[str, Any]]:
|
def get_listings(dong=None, deal_type=None, tier=None, matched_only=False, limit=50, offset=0) -> List[Dict[str, Any]]:
|
||||||
sql = ("SELECT l.*, m.safety_tier, m.valuation_tier, m.category, m.match_score, m.passed "
|
sql = ("SELECT l.*, m.safety_tier, m.valuation_tier, m.category, m.match_score, m.passed "
|
||||||
"FROM listings l LEFT JOIN listing_matches m ON m.listing_id=l.id WHERE 1=1")
|
"FROM listings l LEFT JOIN listing_matches m ON m.listing_id=l.id WHERE 1=1")
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import statistics
|
import statistics
|
||||||
|
|
||||||
from .db import (get_listing_criteria, get_market_deals_for, upsert_listing_match,
|
from .db import (get_listing_criteria, get_market_deals_for, bulk_upsert_listing_matches,
|
||||||
get_listings)
|
_conn)
|
||||||
from . import finance_rules
|
from . import finance_rules
|
||||||
|
|
||||||
logger = logging.getLogger("realestate-lab")
|
logger = logging.getLogger("realestate-lab")
|
||||||
@@ -74,37 +74,47 @@ def match_listing(criteria, listing, budget) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def run_listing_matching() -> None:
|
def run_listing_matching() -> None:
|
||||||
"""미평가 포함 전체 매물 재평가 → listing_matches upsert (청약 run_matching 대칭)."""
|
"""미평가 포함 전체 매물 재평가 → listing_matches upsert (청약 run_matching 대칭).
|
||||||
|
listings 조회 + 매물별 market_deals 조회 + 최종 upsert를 단일 connection으로 처리
|
||||||
|
(Task 2 성능개선: 이전엔 매물마다 get_market_deals_for/upsert_listing_match가 각각
|
||||||
|
자체 _conn()을 열어 269건×~3conn이 발생했다)."""
|
||||||
criteria = get_listing_criteria()
|
criteria = get_listing_criteria()
|
||||||
equity = criteria.get("equity") or 0
|
equity = criteria.get("equity") or 0
|
||||||
income = criteria.get("annual_income") or 0
|
income = criteria.get("annual_income") or 0
|
||||||
jeonse_budget = finance_rules.estimate_jeonse_budget(equity, income)
|
jeonse_budget = finance_rules.estimate_jeonse_budget(equity, income)
|
||||||
for l in get_listings(limit=1000):
|
with _conn() as conn:
|
||||||
is_rent = l["deal_type"] != "매매"
|
listings = [dict(r) for r in conn.execute(
|
||||||
reg = finance_rules.region_regulation(l.get("dong"))
|
"SELECT * FROM listings ORDER BY created_at DESC LIMIT 1000"
|
||||||
if is_rent:
|
).fetchall()]
|
||||||
budget = jeonse_budget
|
recs = []
|
||||||
else:
|
for l in listings:
|
||||||
budget = finance_rules.estimate_purchase_budget(
|
is_rent = l["deal_type"] != "매매"
|
||||||
equity, income, reg, criteria.get("is_first_home"), criteria.get("is_homeless"))
|
reg = finance_rules.region_regulation(l.get("dong"))
|
||||||
m = match_listing(criteria, l, budget)
|
if is_rent:
|
||||||
deals = get_market_deals_for(l.get("dong_code"), l.get("complex_name"),
|
budget = jeonse_budget
|
||||||
l.get("area_exclusive"), "전세" if is_rent else "매매")
|
else:
|
||||||
rec = {"listing_id": l["id"], "category": "임차" if is_rent else "매매",
|
budget = finance_rules.estimate_purchase_budget(
|
||||||
"passed": 1 if m["passed"] else 0, "match_score": m["score"],
|
equity, income, reg, criteria.get("is_first_home"), criteria.get("is_homeless"))
|
||||||
"sample_size": 0, "reasons": m["reasons"], "is_new": 1,
|
m = match_listing(criteria, l, budget)
|
||||||
"regulation_flags": reg_flags(reg)}
|
deals = get_market_deals_for(l.get("dong_code"), l.get("complex_name"),
|
||||||
if is_rent:
|
l.get("area_exclusive"), "전세" if is_rent else "매매",
|
||||||
s = compute_safety(l.get("deposit"), l["deal_type"], deals)
|
conn=conn)
|
||||||
rec.update({"jeonse_ratio": s["jeonse_ratio"], "safety_tier": s["tier"],
|
rec = {"listing_id": l["id"], "category": "임차" if is_rent else "매매",
|
||||||
"market_median": s["median"], "sample_size": s["sample"]})
|
"passed": 1 if m["passed"] else 0, "match_score": m["score"],
|
||||||
else:
|
"sample_size": 0, "reasons": m["reasons"], "is_new": 1,
|
||||||
v = compute_valuation(l.get("sale_price"), deals)
|
"regulation_flags": reg_flags(reg)}
|
||||||
rec.update({"price_ratio": v["price_ratio"], "valuation_tier": v["tier"],
|
if is_rent:
|
||||||
"market_median": v["median"], "sample_size": v["sample"],
|
s = compute_safety(l.get("deposit"), l["deal_type"], deals)
|
||||||
"budget_ok": 1 if (l.get("sale_price") or 0) <= (budget.get("max_price") or 0) else 0})
|
rec.update({"jeonse_ratio": s["jeonse_ratio"], "safety_tier": s["tier"],
|
||||||
upsert_listing_match(rec)
|
"market_median": s["median"], "sample_size": s["sample"]})
|
||||||
logger.info("매물 매칭 완료")
|
else:
|
||||||
|
v = compute_valuation(l.get("sale_price"), deals)
|
||||||
|
rec.update({"price_ratio": v["price_ratio"], "valuation_tier": v["tier"],
|
||||||
|
"market_median": v["median"], "sample_size": v["sample"],
|
||||||
|
"budget_ok": 1 if (l.get("sale_price") or 0) <= (budget.get("max_price") or 0) else 0})
|
||||||
|
recs.append(rec)
|
||||||
|
bulk_upsert_listing_matches(recs, conn=conn)
|
||||||
|
logger.info("매물 매칭 완료(%d건)", len(recs))
|
||||||
|
|
||||||
|
|
||||||
def reg_flags(reg: dict) -> list:
|
def reg_flags(reg: dict) -> list:
|
||||||
|
|||||||
@@ -286,6 +286,18 @@ def api_listing_matches():
|
|||||||
return {"matches": get_listing_matches()}
|
return {"matches": get_listing_matches()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/realestate/listings/rematch")
|
||||||
|
def api_listings_rematch():
|
||||||
|
"""MOLIT 재수집 없이 기존 listings+market_deals로 즉시 재판정(즉시 피드백용, 알림 미발송)."""
|
||||||
|
with match_notify_lock:
|
||||||
|
run_listing_matching()
|
||||||
|
matches = get_listing_matches()
|
||||||
|
passed = sum(1 for m in matches if m.get("passed"))
|
||||||
|
judged = sum(1 for m in matches if m.get("passed")
|
||||||
|
and (m.get("safety_tier") or m.get("valuation_tier")) not in (None, "보류"))
|
||||||
|
return {"total": len(matches), "passed": passed, "judged": judged}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/realestate/safety-check")
|
@app.post("/api/realestate/safety-check")
|
||||||
def api_safety_check(req: SafetyCheckRequest):
|
def api_safety_check(req: SafetyCheckRequest):
|
||||||
deals = get_market_deals_for(req.dong_code, req.complex_name, req.area,
|
deals = get_market_deals_for(req.dong_code, req.complex_name, req.area,
|
||||||
|
|||||||
@@ -31,3 +31,28 @@ def test_safety_check_rent():
|
|||||||
|
|
||||||
def test_listings_collect_status():
|
def test_listings_collect_status():
|
||||||
assert _client().get("/api/realestate/listings/collect/status").status_code == 200
|
assert _client().get("/api/realestate/listings/collect/status").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_listings_rematch_endpoint():
|
||||||
|
"""MOLIT 재수집 없이 기존 listings+market_deals로 즉시 재판정 — 200 + 요약 카운트."""
|
||||||
|
from app import db
|
||||||
|
for i, v in enumerate((40000, 42000, 44000)):
|
||||||
|
db.upsert_market_deal({"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
||||||
|
"complex_name": "OO", "area": 42.0, "deal_type": "전세",
|
||||||
|
"deposit": v, "sale_amount": None,
|
||||||
|
"deal_ym": f"2026{i+1:02d}", "floor": "5", "source": "molit"})
|
||||||
|
db.upsert_listing({"article_no": "RM1", "source": "naver", "deal_type": "전세",
|
||||||
|
"deposit": 28000, "area_exclusive": 42.0, "complex_name": "OO",
|
||||||
|
"dong": "신대방동", "dong_code": "11590"})
|
||||||
|
db.update_listing_criteria({"dongs": ["신대방동"], "deal_types": ["전세"], "max_deposit": 32000,
|
||||||
|
"min_area": 40.0, "house_types": ["아파트"]})
|
||||||
|
|
||||||
|
r = _client().post("/api/realestate/listings/rematch")
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["total"] == 1
|
||||||
|
assert body["passed"] == 1
|
||||||
|
assert body["judged"] == 1
|
||||||
|
|
||||||
|
matches = db.get_listing_matches()
|
||||||
|
assert matches[0]["safety_tier"] == "안전"
|
||||||
|
|||||||
@@ -104,3 +104,63 @@ def test_get_listing_matches_json_parsed():
|
|||||||
assert matches[0]["regulation_flags"] == ["토지거래허가구역"]
|
assert matches[0]["regulation_flags"] == ["토지거래허가구역"]
|
||||||
assert isinstance(matches[0]["reasons"], list)
|
assert isinstance(matches[0]["reasons"], list)
|
||||||
assert matches[0]["reasons"] == ["ok"]
|
assert matches[0]["reasons"] == ["ok"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Task 2: run_listing_matching 단일 conn 성능개선 ──────────────────────────
|
||||||
|
|
||||||
|
def test_get_market_deals_for_reuses_passed_conn():
|
||||||
|
"""conn 인자를 넘기면 그 connection을 재사용하고, 넘기지 않았을 때와 동일한
|
||||||
|
결과를 반환해야 한다(1차 complex 스코프 → 표본 부족 시 광역 폴백 로직 불변)."""
|
||||||
|
from app import db
|
||||||
|
base = {"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
||||||
|
"complex_name": "OO", "area": 42.0, "deal_type": "전세",
|
||||||
|
"sale_amount": None, "floor": "5", "source": "molit"}
|
||||||
|
db.upsert_market_deal({**base, "deposit": 40000, "deal_ym": "202601"})
|
||||||
|
db.upsert_market_deal({**base, "deposit": 41000, "deal_ym": "202602"})
|
||||||
|
db.upsert_market_deal({**base, "deposit": 42000, "deal_ym": "202603"})
|
||||||
|
|
||||||
|
without_conn = db.get_market_deals_for("11590", "OO", 42.0, "전세", months=120)
|
||||||
|
with db._conn() as shared:
|
||||||
|
with_conn = db.get_market_deals_for("11590", "OO", 42.0, "전세", months=120, conn=shared)
|
||||||
|
# conn을 넘겼으니 함수가 스스로 닫지 않아야 함 — 같은 conn으로 후속 쿼리 가능해야 한다.
|
||||||
|
still_open = shared.execute("SELECT COUNT(*) FROM market_deals").fetchone()[0]
|
||||||
|
assert len(with_conn) == len(without_conn) == 3
|
||||||
|
assert still_open == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_bulk_upsert_listing_matches_multi_insert_and_update():
|
||||||
|
"""bulk_upsert_listing_matches가 여러 건을 한 번에 upsert하고, 재호출 시
|
||||||
|
기존 upsert_listing_match와 동일하게 갱신(notified_at 보존)해야 한다."""
|
||||||
|
from app import db
|
||||||
|
l1, _ = db.upsert_listing({"article_no": "B1", "source": "naver", "deal_type": "전세",
|
||||||
|
"deposit": 29000, "area_exclusive": 42.0, "dong": "신대방동"})
|
||||||
|
l2, _ = db.upsert_listing({"article_no": "B2", "source": "naver", "deal_type": "매매",
|
||||||
|
"sale_price": 85000, "area_exclusive": 59.0, "dong": "상도동"})
|
||||||
|
db.bulk_upsert_listing_matches([
|
||||||
|
{"listing_id": l1["id"], "category": "임차", "passed": 1, "match_score": 100,
|
||||||
|
"safety_tier": "안전", "sample_size": 5, "reasons": ["ok"], "is_new": 1},
|
||||||
|
{"listing_id": l2["id"], "category": "매매", "passed": 0, "match_score": 0,
|
||||||
|
"valuation_tier": "고가", "sample_size": 4, "reasons": ["매수가능 상한 초과"], "is_new": 1},
|
||||||
|
])
|
||||||
|
matches = {m["listing_id"]: m for m in db.get_listing_matches()}
|
||||||
|
assert len(matches) == 2
|
||||||
|
assert matches[l1["id"]]["safety_tier"] == "안전" and matches[l1["id"]]["passed"] == 1
|
||||||
|
assert matches[l2["id"]]["valuation_tier"] == "고가" and matches[l2["id"]]["passed"] == 0
|
||||||
|
|
||||||
|
# notified_at 마킹 후 재호출해도 보존돼야 함(재알림 방지)
|
||||||
|
match_id = next(m["id"] for m in db.get_listing_matches() if m["listing_id"] == l1["id"])
|
||||||
|
db.mark_listings_notified([match_id])
|
||||||
|
db.bulk_upsert_listing_matches([
|
||||||
|
{"listing_id": l1["id"], "category": "임차", "passed": 1, "match_score": 95,
|
||||||
|
"safety_tier": "주의", "sample_size": 6, "reasons": ["ok"], "is_new": 1},
|
||||||
|
])
|
||||||
|
updated = {m["listing_id"]: m for m in db.get_listing_matches()}
|
||||||
|
assert updated[l1["id"]]["safety_tier"] == "주의" # 갱신됨
|
||||||
|
assert updated[l1["id"]]["notified_at"] is not None # 보존됨
|
||||||
|
|
||||||
|
|
||||||
|
def test_bulk_upsert_listing_matches_empty_noop():
|
||||||
|
"""빈 리스트는 아무 것도 하지 않는다(가드)."""
|
||||||
|
from app import db
|
||||||
|
db.bulk_upsert_listing_matches([])
|
||||||
|
assert db.get_listing_matches() == []
|
||||||
|
|||||||
@@ -27,3 +27,53 @@ def test_match_listing_rent_deposit_cap():
|
|||||||
over = match_listing(crit, {"deal_type": "전세", "deposit": 40000, "area_exclusive": 42.0,
|
over = match_listing(crit, {"deal_type": "전세", "deposit": 40000, "area_exclusive": 42.0,
|
||||||
"dong": "신대방동"}, budget={"max_deposit": 45000})
|
"dong": "신대방동"}, budget={"max_deposit": 45000})
|
||||||
assert over["passed"] is False
|
assert over["passed"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Task 2: run_listing_matching 단일 conn 통합 테스트 ───────────────────────
|
||||||
|
|
||||||
|
def test_run_listing_matching_end_to_end_safe_rent_and_overpriced_sale():
|
||||||
|
"""listings+market_deals+criteria를 시드하고 run_listing_matching()을 돌려
|
||||||
|
listing_matches가 실제로 생성되고 tier/passed가 기대대로인지 확인한다
|
||||||
|
(단일 connection 리팩터 후에도 판정 로직이 보존됐는지 검증하는 회귀 테스트)."""
|
||||||
|
from app import db
|
||||||
|
from app.listing_matcher import run_listing_matching
|
||||||
|
|
||||||
|
# criteria: 신대방동 전세/매매, 보증금 상한 32000, 최소면적 40
|
||||||
|
db.update_listing_criteria({
|
||||||
|
"dongs": ["신대방동"], "deal_types": ["전세", "매매"],
|
||||||
|
"max_deposit": 32000, "min_area": 40.0,
|
||||||
|
"house_types": ["아파트"], "equity": 20000, "annual_income": 6000,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 전세 실거래 시세(median ~42000) — 안전 매물(28000, ratio 0.67)
|
||||||
|
for i, v in enumerate((40000, 42000, 44000)):
|
||||||
|
db.upsert_market_deal({"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
||||||
|
"complex_name": "OO", "area": 42.0, "deal_type": "전세",
|
||||||
|
"deposit": v, "sale_amount": None,
|
||||||
|
"deal_ym": f"2026{i+1:02d}", "floor": "5", "source": "molit"})
|
||||||
|
l_rent, _ = db.upsert_listing({"article_no": "R1", "source": "naver", "deal_type": "전세",
|
||||||
|
"deposit": 28000, "area_exclusive": 42.0,
|
||||||
|
"complex_name": "OO", "dong": "신대방동", "dong_code": "11590"})
|
||||||
|
|
||||||
|
# 매매 실거래 시세(median ~87500) — 고가 매물(95000, ratio 1.09)
|
||||||
|
for i, v in enumerate((80000, 88000, 90000, 87000)):
|
||||||
|
db.upsert_market_deal({"house_type": "아파트", "dong_code": "11590", "dong": "신대방동",
|
||||||
|
"complex_name": "PP", "area": 59.0, "deal_type": "매매",
|
||||||
|
"deposit": None, "sale_amount": v,
|
||||||
|
"deal_ym": f"2026{i+1:02d}", "floor": "10", "source": "molit"})
|
||||||
|
l_sale, _ = db.upsert_listing({"article_no": "R2", "source": "naver", "deal_type": "매매",
|
||||||
|
"sale_price": 95000, "area_exclusive": 59.0,
|
||||||
|
"complex_name": "PP", "dong": "신대방동", "dong_code": "11590"})
|
||||||
|
|
||||||
|
run_listing_matching()
|
||||||
|
|
||||||
|
matches = {m["listing_id"]: m for m in db.get_listing_matches()}
|
||||||
|
assert len(matches) == 2
|
||||||
|
rent_m = matches[l_rent["id"]]
|
||||||
|
assert rent_m["passed"] == 1
|
||||||
|
assert rent_m["safety_tier"] == "안전"
|
||||||
|
assert rent_m["sample_size"] == 3
|
||||||
|
|
||||||
|
sale_m = matches[l_sale["id"]]
|
||||||
|
assert sale_m["valuation_tier"] == "고가"
|
||||||
|
assert sale_m["sample_size"] == 4
|
||||||
|
|||||||
Reference in New Issue
Block a user