diff --git a/realestate-lab/app/db.py b/realestate-lab/app/db.py index 112d99d..6465b85 100644 --- a/realestate-lab/app/db.py +++ b/realestate-lab/app/db.py @@ -116,7 +116,7 @@ def init_db(): conn.execute(""" CREATE TABLE IF NOT EXISTS match_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, - announcement_id INTEGER NOT NULL, + announcement_id INTEGER NOT NULL REFERENCES announcements(id) ON DELETE CASCADE, model_id INTEGER, match_score INTEGER NOT NULL DEFAULT 0, match_reasons TEXT NOT NULL DEFAULT '[]', @@ -341,8 +341,7 @@ def update_announcement(ann_id: int, data: Dict[str, Any]) -> Optional[Dict[str, def delete_announcement(ann_id: int) -> bool: with _conn() as conn: - # 관련 매칭 결과도 삭제 - conn.execute("DELETE FROM match_results WHERE announcement_id = ?", (ann_id,)) + # match_results는 FK CASCADE로 자동 삭제 cur = conn.execute("DELETE FROM announcements WHERE id = ?", (ann_id,)) return cur.rowcount > 0 @@ -351,14 +350,12 @@ def update_all_statuses(): """모든 진행중 공고의 status를 날짜 기반으로 재계산.""" with _conn() as conn: rows = conn.execute( - "SELECT id, receipt_start, receipt_end, winner_date FROM announcements " + "SELECT id, status, 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"]) - if new_status != "완료": - conn.execute("UPDATE announcements SET status = ? WHERE id = ?", (new_status, r["id"])) - else: + if new_status != r["status"]: # only update if status actually changed conn.execute( "UPDATE announcements SET status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?", (new_status, r["id"]), @@ -509,11 +506,6 @@ def mark_match_read(match_id: int) -> bool: return cur.rowcount > 0 -def clear_match_results(): - with _conn() as conn: - conn.execute("DELETE FROM match_results") - - # ── collect_log CRUD ───────────────────────────────────────────────────────── def save_collect_log(new_count: int, total_count: int, error: str = None): diff --git a/realestate-lab/app/main.py b/realestate-lab/app/main.py index 19d2cc9..f2292e6 100644 --- a/realestate-lab/app/main.py +++ b/realestate-lab/app/main.py @@ -1,5 +1,6 @@ import os import logging +import threading from contextlib import asynccontextmanager from fastapi import BackgroundTasks, FastAPI, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -110,9 +111,18 @@ def api_announcement_delete(ann_id: int): # ── 수집 API ───────────────────────────────────────────────────────────────── +_collect_lock = threading.Lock() + + def _run_collect_and_match(): - collect_all() - run_matching() + if not _collect_lock.acquire(blocking=False): + logger.info("수집 이미 진행 중 — 건너뜀") + return + try: + collect_all() + run_matching() + finally: + _collect_lock.release() @app.post("/api/realestate/collect") diff --git a/realestate-lab/app/matcher.py b/realestate-lab/app/matcher.py index beb86c9..bfefd4c 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, _conn +from .db import get_profile, _conn logger = logging.getLogger("realestate-lab") @@ -30,7 +30,8 @@ def _check_eligible_types(profile: Dict[str, Any], ann: Dict[str, Any]) -> List[ elif is_homeless: eligible.append("일반2순위") - # 특별공급 + # 특별공급 — 신혼부부 + # NOTE: 소득기준 검증은 향후 구현 예정 (income_level 필드 활용) if profile.get("is_newlywed") and is_homeless: eligible.append("특별-신혼부부") @@ -117,7 +118,7 @@ def run_matching(): """프로필 기반 매칭을 실행하여 결과를 저장한다.""" profile = get_profile() if not profile: - logger.info("매칭 스킵: 프로필이 설정되지 않음") + logger.info("프로필 미설정 — 매칭 건너뜀") return with _conn() as conn: @@ -125,32 +126,35 @@ def run_matching(): "SELECT * FROM announcements WHERE status IN ('청약예정', '청약중')" ).fetchall() - saved = 0 - for row in anns: - ann = {c: row[c] for c in row.keys()} - - models_rows = conn.execute( + for ann_row in anns: + ann = {c: ann_row[c] for c in ann_row.keys()} + models = conn.execute( "SELECT * FROM announcement_models WHERE house_manage_no = ? AND pblanc_no = ?", (ann["house_manage_no"], ann["pblanc_no"]), ).fetchall() - models = [{c: m[c] for c in m.keys()} for m in models_rows] - - result = _compute_score(profile, ann, models) + model_list = [dict(m) for m in models] + result = _compute_score(profile, ann, model_list) if result["match_score"] > 0: - save_match_result({ - "announcement_id": ann["id"], - "model_id": None, - "match_score": result["match_score"], - "match_reasons": result["match_reasons"], - "eligible_types": result["eligible_types"], - }) - saved += 1 + conn.execute(""" + INSERT INTO match_results (announcement_id, model_id, match_score, match_reasons, eligible_types, is_new) + VALUES (?, ?, ?, ?, ?, 1) + ON CONFLICT(announcement_id, model_id) DO UPDATE SET + match_score=excluded.match_score, + match_reasons=excluded.match_reasons, + eligible_types=excluded.eligible_types + """, ( + ann["id"], + None, + result["match_score"], + json.dumps(result["match_reasons"]), + json.dumps(result["eligible_types"]), + )) - # 완료/결과발표 공고의 매칭 결과 정리 + # Clean up stale match results for completed announcements 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) + logger.info("매칭 완료")