From 2926770d6fcdbe6d08d955f2a29485339bf824f4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 16 Mar 2026 01:23:28 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=80=EB=8F=99=EC=82=B0=20=EC=B2=AD?= =?UTF-8?q?=EC=95=BD=20=EB=8B=A8=EC=A7=80=20=EA=B4=80=EB=A6=AC=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - realestate_complexes 테이블 추가 (lotto.db) - CRUD 엔드포인트 4개: GET/POST /api/realestate/complexes, PUT/DELETE /api/realestate/complexes/:id - status: 청약예정|청약중|결과발표|완료, priority: high|normal|low 검증 Co-Authored-By: Claude Sonnet 4.6 --- backend/app/db.py | 161 ++++++++++++++++++++++++++++++++++++++++++++ backend/app/main.py | 81 ++++++++++++++++++++++ 2 files changed, 242 insertions(+) diff --git a/backend/app/db.py b/backend/app/db.py index 30afb0a..13f035c 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -181,6 +181,38 @@ def init_db() -> None: "CREATE INDEX IF NOT EXISTS idx_blog_date ON blog_posts(date DESC);" ) + # ── realestate_complexes 테이블 ──────────────────────────────────────── + conn.execute( + """ + CREATE TABLE IF NOT EXISTS realestate_complexes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL DEFAULT '', + lat REAL, + lng REAL, + units INTEGER, + types TEXT NOT NULL DEFAULT '[]', + avg_price_per_pyeong INTEGER, + subscription_start TEXT, + subscription_end TEXT, + result_date TEXT, + status TEXT NOT NULL DEFAULT '청약예정' + CHECK(status IN ('청약예정','청약중','결과발표','완료')), + priority TEXT NOT NULL DEFAULT 'normal' + CHECK(priority IN ('high','normal','low')), + tags TEXT NOT NULL DEFAULT '[]', + naver_url TEXT NOT NULL DEFAULT '', + floor_plan_url TEXT NOT NULL DEFAULT '', + memo TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) + ); + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_realestate_status ON realestate_complexes(status);" + ) + # ── todos CRUD ─────────────────────────────────────────────────────────────── @@ -667,3 +699,132 @@ def get_simulation_candidates(run_id: int, limit: int = 100) -> List[Dict[str, A for r in rows ] + +# ── realestate_complexes CRUD ───────────────────────────────────────────────── + +def _complex_row_to_dict(r) -> Dict[str, Any]: + return { + "id": r["id"], + "name": r["name"], + "address": r["address"], + "lat": r["lat"], + "lng": r["lng"], + "units": r["units"], + "types": json.loads(r["types"]) if r["types"] else [], + "avgPricePerPyeong": r["avg_price_per_pyeong"], + "subscriptionStart": r["subscription_start"], + "subscriptionEnd": r["subscription_end"], + "resultDate": r["result_date"], + "status": r["status"], + "priority": r["priority"], + "tags": json.loads(r["tags"]) if r["tags"] else [], + "naverUrl": r["naver_url"], + "floorPlanUrl": r["floor_plan_url"], + "memo": r["memo"], + "created_at": r["created_at"], + "updated_at": r["updated_at"], + } + + +def get_all_complexes() -> List[Dict[str, Any]]: + with _conn() as conn: + rows = conn.execute( + "SELECT * FROM realestate_complexes ORDER BY id DESC" + ).fetchall() + return [_complex_row_to_dict(r) for r in rows] + + +def get_complex(complex_id: int) -> Optional[Dict[str, Any]]: + with _conn() as conn: + r = conn.execute( + "SELECT * FROM realestate_complexes WHERE id = ?", (complex_id,) + ).fetchone() + return _complex_row_to_dict(r) if r else None + + +def create_complex(data: Dict[str, Any]) -> Dict[str, Any]: + with _conn() as conn: + conn.execute( + """ + INSERT INTO realestate_complexes + (name, address, lat, lng, units, types, avg_price_per_pyeong, + subscription_start, subscription_end, result_date, + status, priority, tags, naver_url, floor_plan_url, memo) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + data["name"], + data.get("address", ""), + data.get("lat"), + data.get("lng"), + data.get("units"), + json.dumps(data.get("types", [])), + data.get("avgPricePerPyeong"), + data.get("subscriptionStart"), + data.get("subscriptionEnd"), + data.get("resultDate"), + data.get("status", "청약예정"), + data.get("priority", "normal"), + json.dumps(data.get("tags", [])), + data.get("naverUrl", ""), + data.get("floorPlanUrl", ""), + data.get("memo", ""), + ), + ) + row = conn.execute( + "SELECT * FROM realestate_complexes WHERE rowid = last_insert_rowid()" + ).fetchone() + return _complex_row_to_dict(row) + + +def update_complex(complex_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + field_map = { + "name": "name", + "address": "address", + "lat": "lat", + "lng": "lng", + "units": "units", + "avgPricePerPyeong": "avg_price_per_pyeong", + "subscriptionStart": "subscription_start", + "subscriptionEnd": "subscription_end", + "resultDate": "result_date", + "status": "status", + "priority": "priority", + "naverUrl": "naver_url", + "floorPlanUrl": "floor_plan_url", + "memo": "memo", + } + json_fields = {"types", "tags"} + + updates: Dict[str, Any] = {} + for camel, snake in field_map.items(): + if camel in data: + updates[snake] = data[camel] + for f in json_fields: + if f in data: + updates[f] = json.dumps(data[f]) + + if not updates: + return get_complex(complex_id) + + set_clauses = ", ".join(f"{k} = ?" for k in updates) + set_clauses += ", updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')" + args = list(updates.values()) + [complex_id] + + with _conn() as conn: + conn.execute( + f"UPDATE realestate_complexes SET {set_clauses} WHERE id = ?", args + ) + row = conn.execute( + "SELECT * FROM realestate_complexes WHERE id = ?", (complex_id,) + ).fetchone() + return _complex_row_to_dict(row) if row else None + + +def delete_complex(complex_id: int) -> bool: + with _conn() as conn: + cur = conn.execute( + "DELETE FROM realestate_complexes WHERE id = ?", (complex_id,) + ) + return cur.rowcount > 0 + diff --git a/backend/app/main.py b/backend/app/main.py index 2c721cb..ccd1299 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -14,6 +14,8 @@ from .db import ( get_all_todos, create_todo, update_todo, delete_todo, delete_done_todos, # blog get_all_posts, create_post, update_post, delete_post, + # realestate + get_all_complexes, get_complex, create_complex, update_complex, delete_complex, ) from .recommender import recommend_numbers, recommend_with_heatmap from .collector import sync_latest, sync_ensure_all @@ -670,3 +672,82 @@ def api_blog_delete(post_id: int): if not ok: raise HTTPException(status_code=404, detail="Post not found") return {"ok": True} + + +# ── RealEstate API ───────────────────────────────────────────────────────────── + +VALID_STATUSES = {"청약예정", "청약중", "결과발표", "완료"} +VALID_PRIORITIES = {"high", "normal", "low"} + + +class ComplexCreate(BaseModel): + name: str + address: str = "" + lat: Optional[float] = None + lng: Optional[float] = None + units: Optional[int] = None + types: List[str] = [] + avgPricePerPyeong: Optional[int] = None + subscriptionStart: Optional[str] = None + subscriptionEnd: Optional[str] = None + resultDate: Optional[str] = None + status: str = "청약예정" + priority: str = "normal" + tags: List[str] = [] + naverUrl: str = "" + floorPlanUrl: str = "" + memo: str = "" + + +class ComplexUpdate(BaseModel): + name: Optional[str] = None + address: Optional[str] = None + lat: Optional[float] = None + lng: Optional[float] = None + units: Optional[int] = None + types: Optional[List[str]] = None + avgPricePerPyeong: Optional[int] = None + subscriptionStart: Optional[str] = None + subscriptionEnd: Optional[str] = None + resultDate: Optional[str] = None + status: Optional[str] = None + priority: Optional[str] = None + tags: Optional[List[str]] = None + naverUrl: Optional[str] = None + floorPlanUrl: Optional[str] = None + memo: Optional[str] = None + + +@app.get("/api/realestate/complexes") +def api_realestate_list(): + return get_all_complexes() + + +@app.post("/api/realestate/complexes", status_code=201) +def api_realestate_create(body: ComplexCreate): + if body.status not in VALID_STATUSES: + raise HTTPException(status_code=400, detail=f"status must be one of {VALID_STATUSES}") + if body.priority not in VALID_PRIORITIES: + raise HTTPException(status_code=400, detail=f"priority must be one of {VALID_PRIORITIES}") + return create_complex(body.model_dump()) + + +@app.put("/api/realestate/complexes/{complex_id}") +def api_realestate_update(complex_id: int, body: ComplexUpdate): + data = body.model_dump(exclude_none=True) + if "status" in data and data["status"] not in VALID_STATUSES: + raise HTTPException(status_code=400, detail=f"status must be one of {VALID_STATUSES}") + if "priority" in data and data["priority"] not in VALID_PRIORITIES: + raise HTTPException(status_code=400, detail=f"priority must be one of {VALID_PRIORITIES}") + updated = update_complex(complex_id, data) + if updated is None: + raise HTTPException(status_code=404, detail="Complex not found") + return updated + + +@app.delete("/api/realestate/complexes/{complex_id}") +def api_realestate_delete(complex_id: int): + ok = delete_complex(complex_id) + if not ok: + raise HTTPException(status_code=404, detail="Complex not found") + return {"ok": True}