diff --git a/realestate-lab/app/collector.py b/realestate-lab/app/collector.py index b686355..7a47fb9 100644 --- a/realestate-lab/app/collector.py +++ b/realestate-lab/app/collector.py @@ -145,8 +145,10 @@ def collect_all() -> Dict[str, Any]: for raw in detail_rows: try: parsed = _parse_apt_detail(raw) - upsert_announcement(parsed) + _, is_new = upsert_announcement(parsed) total_count += 1 + if is_new: + new_count += 1 except Exception as e: logger.error("공고 upsert 실패 [%s]: %s", detail_ep, e) @@ -158,8 +160,6 @@ def collect_all() -> Dict[str, Any]: upsert_model(parsed) except Exception as e: logger.error("모델 upsert 실패 [%s]: %s", model_ep, e) - - new_count = total_count # 첫 수집 시 전부 신규로 기록 save_collect_log(new_count, total_count) logger.info("수집 완료: new=%d, total=%d", new_count, total_count) return {"new_count": new_count, "total_count": total_count} diff --git a/realestate-lab/app/db.py b/realestate-lab/app/db.py index 23274e6..112d99d 100644 --- a/realestate-lab/app/db.py +++ b/realestate-lab/app/db.py @@ -160,14 +160,19 @@ def _ann_row_to_dict(r) -> Dict[str, Any]: return {c: r[c] for c in r.keys()} -def upsert_announcement(data: Dict[str, Any]) -> Dict[str, Any]: - """공고 upsert — house_manage_no + pblanc_no 기준.""" +def upsert_announcement(data: Dict[str, Any]) -> tuple: + """공고 upsert — house_manage_no + pblanc_no 기준. Returns (dict, is_new: bool).""" status = compute_status( data.get("receipt_start", ""), data.get("receipt_end", ""), data.get("winner_date", ""), ) with _conn() as conn: + exists = conn.execute( + "SELECT 1 FROM announcements WHERE house_manage_no = ? AND pblanc_no = ?", + (data["house_manage_no"], data["pblanc_no"]), + ).fetchone() + is_new = exists is None conn.execute(""" INSERT INTO announcements ( house_manage_no, pblanc_no, house_nm, house_secd, house_dtl_secd, @@ -220,7 +225,7 @@ def upsert_announcement(data: Dict[str, Any]) -> Dict[str, Any]: "SELECT * FROM announcements WHERE house_manage_no = ? AND pblanc_no = ?", (data["house_manage_no"], data["pblanc_no"]), ).fetchone() - return _ann_row_to_dict(row) + return _ann_row_to_dict(row), is_new def get_announcements( @@ -243,25 +248,24 @@ def get_announcements( conditions.append("a.house_secd = ?") params.append(house_type) - join_clause = "" if matched_only: - join_clause = "INNER JOIN match_results m ON m.announcement_id = a.id" + conditions.append("a.id IN (SELECT announcement_id FROM match_results)") where = f"WHERE {' AND '.join(conditions)}" if conditions else "" order_map = {"date": "a.rcrit_date DESC", "score": "a.id DESC", "price": "a.id ASC"} order = order_map.get(sort, "a.rcrit_date DESC") if matched_only and sort == "score": - order = "m.match_score DESC" + order = "(SELECT MAX(match_score) FROM match_results WHERE announcement_id = a.id) DESC" offset = (page - 1) * size with _conn() as conn: total = conn.execute( - f"SELECT COUNT(*) FROM announcements a {join_clause} {where}", params + f"SELECT COUNT(*) FROM announcements a {where}", params ).fetchone()[0] rows = conn.execute( - f"SELECT a.* FROM announcements a {join_clause} {where} ORDER BY {order} LIMIT ? OFFSET ?", + f"SELECT a.* FROM announcements a {where} ORDER BY {order} LIMIT ? OFFSET ?", params + [size, offset], ).fetchall() return { @@ -292,11 +296,23 @@ def create_announcement(data: Dict[str, Any]) -> Dict[str, Any]: data["house_manage_no"] = data.get("house_manage_no", f"MANUAL-{uuid.uuid4().hex[:8]}") data["pblanc_no"] = data.get("pblanc_no", "00") data["source"] = "manual" - return upsert_announcement(data) + result, _ = upsert_announcement(data) + return result + + +ANNOUNCEMENT_COLUMNS = { + "house_nm", "house_secd", "house_dtl_secd", "rent_secd", + "region_code", "region_name", "address", "total_units", + "rcrit_date", "receipt_start", "receipt_end", "spsply_start", "spsply_end", + "gnrl_rank1_start", "gnrl_rank1_end", "winner_date", + "contract_start", "contract_end", "homepage_url", "pblanc_url", + "constructor", "developer", "move_in_month", + "is_speculative_area", "is_price_cap", "contact", +} def update_announcement(ann_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - fields = {k: v for k, v in data.items() if v is not None} + fields = {k: v for k, v in data.items() if v is not None and k in ANNOUNCEMENT_COLUMNS} if not fields: return get_announcement(ann_id) @@ -335,7 +351,8 @@ def update_all_statuses(): """모든 진행중 공고의 status를 날짜 기반으로 재계산.""" with _conn() as conn: rows = conn.execute( - "SELECT id, receipt_start, receipt_end, winner_date FROM announcements WHERE status != '완료'" + "SELECT id, receipt_start, receipt_end, winner_date FROM announcements " + "WHERE status != '완료' AND (receipt_start IS NOT NULL OR receipt_end IS NOT NULL OR winner_date IS NOT NULL)" ).fetchall() for r in rows: new_status = compute_status(r["receipt_start"], r["receipt_end"], r["winner_date"]) @@ -402,10 +419,20 @@ def get_profile() -> Optional[Dict[str, Any]]: return _profile_row_to_dict(r) if r else None +PROFILE_COLUMNS = { + "name", "age", "is_homeless", "is_householder", + "subscription_months", "subscription_amount", "family_members", + "has_dependents", "children_count", "is_newlywed", "marriage_months", + "has_newborn", "is_first_home", "income_level", + "preferred_regions", "preferred_types", + "min_area", "max_area", "max_price", +} + + def upsert_profile(data: Dict[str, Any]) -> Dict[str, Any]: updates = {} for k, v in data.items(): - if v is None: + if v is None or k not in PROFILE_COLUMNS: continue if isinstance(v, bool): updates[k] = 1 if v else 0 diff --git a/realestate-lab/app/main.py b/realestate-lab/app/main.py index c8ac0ad..19d2cc9 100644 --- a/realestate-lab/app/main.py +++ b/realestate-lab/app/main.py @@ -1,7 +1,7 @@ import os import logging from contextlib import asynccontextmanager -from fastapi import FastAPI, Query, HTTPException +from fastapi import BackgroundTasks, FastAPI, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware from apscheduler.schedulers.background import BackgroundScheduler @@ -110,11 +110,15 @@ def api_announcement_delete(ann_id: int): # ── 수집 API ───────────────────────────────────────────────────────────────── -@app.post("/api/realestate/collect") -def api_collect(): - result = collect_all() +def _run_collect_and_match(): + collect_all() run_matching() - return result + + +@app.post("/api/realestate/collect") +def api_collect(background_tasks: BackgroundTasks): + background_tasks.add_task(_run_collect_and_match) + return {"ok": True, "message": "수집 시작됨"} @app.get("/api/realestate/collect/status") diff --git a/realestate-lab/app/matcher.py b/realestate-lab/app/matcher.py index 297e101..beb86c9 100644 --- a/realestate-lab/app/matcher.py +++ b/realestate-lab/app/matcher.py @@ -2,7 +2,7 @@ import json import logging from typing import Dict, Any, List -from .db import get_profile, save_match_result, clear_match_results, _conn +from .db import get_profile, save_match_result, _conn logger = logging.getLogger("realestate-lab") @@ -120,8 +120,6 @@ def run_matching(): logger.info("매칭 스킵: 프로필이 설정되지 않음") return - clear_match_results() - with _conn() as conn: anns = conn.execute( "SELECT * FROM announcements WHERE status IN ('청약예정', '청약중')" @@ -149,4 +147,10 @@ def run_matching(): }) saved += 1 + # 완료/결과발표 공고의 매칭 결과 정리 + conn.execute( + "DELETE FROM match_results WHERE announcement_id NOT IN " + "(SELECT id FROM announcements WHERE status IN ('청약예정', '청약중'))" + ) + logger.info("매칭 완료: %d건 공고 중 %d건 매칭됨", len(anns), saved)