import os import logging from contextlib import asynccontextmanager from fastapi import BackgroundTasks, FastAPI, Query, HTTPException from fastapi.middleware.cors import CORSMiddleware from apscheduler.schedulers.background import BackgroundScheduler from .db import ( init_db, get_announcements, get_announcement, create_announcement, update_announcement, delete_announcement, update_all_statuses, get_profile, upsert_profile, get_matches, mark_match_read, get_last_collect_log, get_dashboard, ) from .collector import collect_all from .matcher import run_matching from .models import AnnouncementCreate, AnnouncementUpdate, ProfileUpdate logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s") logger = logging.getLogger("realestate-lab") scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul")) def scheduled_collect(): """매일 09:00 — 수집 + 매칭""" logger.info("스케줄 수집 시작") collect_all() run_matching() logger.info("스케줄 수집 + 매칭 완료") def scheduled_status_update(): """매일 00:00 — 상태 갱신 + 재매칭""" logger.info("상태 갱신 시작") update_all_statuses() run_matching() logger.info("상태 갱신 + 재매칭 완료") @asynccontextmanager async def lifespan(app: FastAPI): init_db() scheduler.add_job(scheduled_collect, "cron", hour=9, minute=0, id="collect") scheduler.add_job(scheduled_status_update, "cron", hour=0, minute=0, id="status_update") scheduler.start() logger.info("realestate-lab 시작") yield scheduler.shutdown() app = FastAPI(lifespan=lifespan) _cors_origins = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080").split(",") app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in _cors_origins], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Content-Type"], ) @app.get("/health") def health(): return {"status": "ok"} # ── 공고 API ───────────────────────────────────────────────────────────────── @app.get("/api/realestate/announcements") def api_announcements( region: str = None, status: str = None, house_type: str = None, matched_only: bool = False, sort: str = "date", page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100), ): return get_announcements(region, status, house_type, matched_only, sort, page, size) @app.get("/api/realestate/announcements/{ann_id}") def api_announcement_detail(ann_id: int): ann = get_announcement(ann_id) if not ann: raise HTTPException(status_code=404, detail="Announcement not found") return ann @app.post("/api/realestate/announcements", status_code=201) def api_announcement_create(body: AnnouncementCreate): return create_announcement(body.model_dump()) @app.put("/api/realestate/announcements/{ann_id}") def api_announcement_update(ann_id: int, body: AnnouncementUpdate): updated = update_announcement(ann_id, body.model_dump(exclude_none=True)) if not updated: raise HTTPException(status_code=404, detail="Announcement not found") return updated @app.delete("/api/realestate/announcements/{ann_id}") def api_announcement_delete(ann_id: int): if not delete_announcement(ann_id): raise HTTPException(status_code=404, detail="Announcement not found") return {"ok": True} # ── 수집 API ───────────────────────────────────────────────────────────────── def _run_collect_and_match(): collect_all() run_matching() @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") def api_collect_status(): log = get_last_collect_log() return log if log else {"collected_at": None, "new_count": 0, "total_count": 0, "error": None} # ── 프로필 API ─────────────────────────────────────────────────────────────── @app.get("/api/realestate/profile") def api_profile_get(): profile = get_profile() return profile if profile else {} @app.put("/api/realestate/profile") def api_profile_update(body: ProfileUpdate): return upsert_profile(body.model_dump(exclude_none=True)) # ── 매칭 API ───────────────────────────────────────────────────────────────── @app.get("/api/realestate/matches") def api_matches(page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100)): return get_matches(page, size) @app.post("/api/realestate/matches/refresh") def api_matches_refresh(): run_matching() return {"ok": True} @app.patch("/api/realestate/matches/{match_id}/read") def api_match_read(match_id: int): if not mark_match_read(match_id): raise HTTPException(status_code=404, detail="Match not found") return {"ok": True} # ── 대시보드 API ───────────────────────────────────────────────────────────── @app.get("/api/realestate/dashboard") def api_dashboard(): return get_dashboard()