fix(realestate-lab): 최종 리뷰 이슈 수정 — FK CASCADE, 단일 연결, 동시성 가드
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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("매칭 완료")
|
||||
|
||||
Reference in New Issue
Block a user