청약 관리 API 추가 (/api/subscription)
- subscription_items 테이블: 청약 목록 CRUD (GET/POST/PUT/DELETE) - subscription_profile 테이블: 내 청약 조건 프로필 싱글톤 (GET/PUT, upsert) - specialQuals JSON 배열, bool → int SQLite 변환 처리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,64 @@ def init_db() -> None:
|
||||
"CREATE INDEX IF NOT EXISTS idx_realestate_status ON realestate_complexes(status);"
|
||||
)
|
||||
|
||||
# ── subscription_items 테이블 ──────────────────────────────────────────
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS subscription_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
complex_name TEXT NOT NULL,
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
pyeong TEXT,
|
||||
total_price INTEGER,
|
||||
type TEXT,
|
||||
special_type TEXT,
|
||||
supply_type TEXT,
|
||||
status TEXT NOT NULL DEFAULT '검토중',
|
||||
min_score INTEGER,
|
||||
max_income INTEGER,
|
||||
homeless_required INTEGER,
|
||||
subscription_start TEXT,
|
||||
subscription_end TEXT,
|
||||
contract_date TEXT,
|
||||
interim_date TEXT,
|
||||
balance_date TEXT,
|
||||
result_date TEXT,
|
||||
deposit_rate INTEGER DEFAULT 10,
|
||||
interim_rate INTEGER DEFAULT 60,
|
||||
balance_rate INTEGER DEFAULT 30,
|
||||
loan_type TEXT,
|
||||
loan_rate REAL,
|
||||
memo TEXT NOT NULL DEFAULT '',
|
||||
naver_url 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_sub_items_created ON subscription_items(created_at DESC);"
|
||||
)
|
||||
|
||||
# ── subscription_profile 테이블 (싱글톤 id=1) ──────────────────────────
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS subscription_profile (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||
is_household_head INTEGER DEFAULT 1,
|
||||
is_homeless INTEGER DEFAULT 1,
|
||||
homeless_period INTEGER,
|
||||
savings_months INTEGER,
|
||||
savings_count INTEGER,
|
||||
dependents INTEGER DEFAULT 0,
|
||||
residency_area TEXT,
|
||||
is_married INTEGER,
|
||||
marriage_months INTEGER,
|
||||
monthly_income INTEGER,
|
||||
special_quals TEXT NOT NULL DEFAULT '[]'
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
# ── todos CRUD ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -828,3 +886,229 @@ def delete_complex(complex_id: int) -> bool:
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
# ── subscription_items CRUD ───────────────────────────────────────────────────
|
||||
|
||||
_SUB_ITEM_FIELD_MAP = {
|
||||
"complexName": "complex_name",
|
||||
"address": "address",
|
||||
"pyeong": "pyeong",
|
||||
"totalPrice": "total_price",
|
||||
"type": "type",
|
||||
"specialType": "special_type",
|
||||
"supplyType": "supply_type",
|
||||
"status": "status",
|
||||
"minScore": "min_score",
|
||||
"maxIncome": "max_income",
|
||||
"homelessRequired": "homeless_required",
|
||||
"subscriptionStart": "subscription_start",
|
||||
"subscriptionEnd": "subscription_end",
|
||||
"contractDate": "contract_date",
|
||||
"interimDate": "interim_date",
|
||||
"balanceDate": "balance_date",
|
||||
"resultDate": "result_date",
|
||||
"depositRate": "deposit_rate",
|
||||
"interimRate": "interim_rate",
|
||||
"balanceRate": "balance_rate",
|
||||
"loanType": "loan_type",
|
||||
"loanRate": "loan_rate",
|
||||
"memo": "memo",
|
||||
"naverUrl": "naver_url",
|
||||
}
|
||||
|
||||
|
||||
def _sub_item_row_to_dict(r) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": r["id"],
|
||||
"complexName": r["complex_name"],
|
||||
"address": r["address"],
|
||||
"pyeong": r["pyeong"],
|
||||
"totalPrice": r["total_price"],
|
||||
"type": r["type"],
|
||||
"specialType": r["special_type"],
|
||||
"supplyType": r["supply_type"],
|
||||
"status": r["status"],
|
||||
"minScore": r["min_score"],
|
||||
"maxIncome": r["max_income"],
|
||||
"homelessRequired": r["homeless_required"],
|
||||
"subscriptionStart": r["subscription_start"],
|
||||
"subscriptionEnd": r["subscription_end"],
|
||||
"contractDate": r["contract_date"],
|
||||
"interimDate": r["interim_date"],
|
||||
"balanceDate": r["balance_date"],
|
||||
"resultDate": r["result_date"],
|
||||
"depositRate": r["deposit_rate"],
|
||||
"interimRate": r["interim_rate"],
|
||||
"balanceRate": r["balance_rate"],
|
||||
"loanType": r["loan_type"],
|
||||
"loanRate": r["loan_rate"],
|
||||
"memo": r["memo"],
|
||||
"naverUrl": r["naver_url"],
|
||||
"created_at": r["created_at"],
|
||||
"updated_at": r["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
def get_all_subscription_items() -> List[Dict[str, Any]]:
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM subscription_items ORDER BY created_at DESC"
|
||||
).fetchall()
|
||||
return [_sub_item_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def create_subscription_item(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
with _conn() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO subscription_items
|
||||
(complex_name, address, pyeong, total_price, type, special_type, supply_type,
|
||||
status, min_score, max_income, homeless_required,
|
||||
subscription_start, subscription_end, contract_date, interim_date,
|
||||
balance_date, result_date, deposit_rate, interim_rate, balance_rate,
|
||||
loan_type, loan_rate, memo, naver_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
data["complexName"],
|
||||
data.get("address", ""),
|
||||
data.get("pyeong"),
|
||||
data.get("totalPrice"),
|
||||
data.get("type"),
|
||||
data.get("specialType"),
|
||||
data.get("supplyType"),
|
||||
data.get("status", "검토중"),
|
||||
data.get("minScore"),
|
||||
data.get("maxIncome"),
|
||||
data.get("homelessRequired"),
|
||||
data.get("subscriptionStart"),
|
||||
data.get("subscriptionEnd"),
|
||||
data.get("contractDate"),
|
||||
data.get("interimDate"),
|
||||
data.get("balanceDate"),
|
||||
data.get("resultDate"),
|
||||
data.get("depositRate", 10),
|
||||
data.get("interimRate", 60),
|
||||
data.get("balanceRate", 30),
|
||||
data.get("loanType"),
|
||||
data.get("loanRate"),
|
||||
data.get("memo", ""),
|
||||
data.get("naverUrl", ""),
|
||||
),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM subscription_items WHERE rowid = last_insert_rowid()"
|
||||
).fetchone()
|
||||
return _sub_item_row_to_dict(row)
|
||||
|
||||
|
||||
def update_subscription_item(item_id: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
updates: Dict[str, Any] = {}
|
||||
for camel, snake in _SUB_ITEM_FIELD_MAP.items():
|
||||
if camel in data:
|
||||
updates[snake] = data[camel]
|
||||
|
||||
if not updates:
|
||||
with _conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM subscription_items WHERE id = ?", (item_id,)
|
||||
).fetchone()
|
||||
return _sub_item_row_to_dict(row) if row else None
|
||||
|
||||
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()) + [item_id]
|
||||
|
||||
with _conn() as conn:
|
||||
conn.execute(
|
||||
f"UPDATE subscription_items SET {set_clauses} WHERE id = ?", args
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM subscription_items WHERE id = ?", (item_id,)
|
||||
).fetchone()
|
||||
return _sub_item_row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def delete_subscription_item(item_id: int) -> bool:
|
||||
with _conn() as conn:
|
||||
cur = conn.execute("DELETE FROM subscription_items WHERE id = ?", (item_id,))
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
# ── subscription_profile CRUD (싱글톤) ────────────────────────────────────────
|
||||
|
||||
def _profile_row_to_dict(r) -> Dict[str, Any]:
|
||||
return {
|
||||
"isHouseholdHead": bool(r["is_household_head"]) if r["is_household_head"] is not None else None,
|
||||
"isHomeless": bool(r["is_homeless"]) if r["is_homeless"] is not None else None,
|
||||
"homelessPeriod": r["homeless_period"],
|
||||
"savingsMonths": r["savings_months"],
|
||||
"savingsCount": r["savings_count"],
|
||||
"dependents": r["dependents"],
|
||||
"residencyArea": r["residency_area"],
|
||||
"isMarried": bool(r["is_married"]) if r["is_married"] is not None else None,
|
||||
"marriageMonths": r["marriage_months"],
|
||||
"monthlyIncome": r["monthly_income"],
|
||||
"specialQuals": json.loads(r["special_quals"]) if r["special_quals"] else [],
|
||||
}
|
||||
|
||||
|
||||
def get_subscription_profile() -> Optional[Dict[str, Any]]:
|
||||
with _conn() as conn:
|
||||
r = conn.execute(
|
||||
"SELECT * FROM subscription_profile WHERE id = 1"
|
||||
).fetchone()
|
||||
return _profile_row_to_dict(r) if r else None
|
||||
|
||||
|
||||
def upsert_subscription_profile(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
field_map = {
|
||||
"isHouseholdHead": "is_household_head",
|
||||
"isHomeless": "is_homeless",
|
||||
"homelessPeriod": "homeless_period",
|
||||
"savingsMonths": "savings_months",
|
||||
"savingsCount": "savings_count",
|
||||
"dependents": "dependents",
|
||||
"residencyArea": "residency_area",
|
||||
"isMarried": "is_married",
|
||||
"marriageMonths": "marriage_months",
|
||||
"monthlyIncome": "monthly_income",
|
||||
}
|
||||
|
||||
updates: Dict[str, Any] = {}
|
||||
for camel, snake in field_map.items():
|
||||
if camel in data:
|
||||
val = data[camel]
|
||||
# bool → int (SQLite)
|
||||
if isinstance(val, bool):
|
||||
val = 1 if val else 0
|
||||
updates[snake] = val
|
||||
if "specialQuals" in data:
|
||||
updates["special_quals"] = json.dumps(data["specialQuals"])
|
||||
|
||||
with _conn() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM subscription_profile WHERE id = 1"
|
||||
).fetchone()
|
||||
|
||||
if existing:
|
||||
if updates:
|
||||
set_clauses = ", ".join(f"{k} = ?" for k in updates)
|
||||
conn.execute(
|
||||
f"UPDATE subscription_profile SET {set_clauses} WHERE id = 1",
|
||||
list(updates.values()),
|
||||
)
|
||||
else:
|
||||
cols = ["id"] + list(updates.keys())
|
||||
vals = [1] + list(updates.values())
|
||||
placeholders = ", ".join("?" for _ in vals)
|
||||
conn.execute(
|
||||
f"INSERT INTO subscription_profile ({', '.join(cols)}) VALUES ({placeholders})",
|
||||
vals,
|
||||
)
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT * FROM subscription_profile WHERE id = 1"
|
||||
).fetchone()
|
||||
return _profile_row_to_dict(row)
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ from .db import (
|
||||
get_all_posts, create_post, update_post, delete_post,
|
||||
# realestate
|
||||
get_all_complexes, get_complex, create_complex, update_complex, delete_complex,
|
||||
# subscription
|
||||
get_all_subscription_items, create_subscription_item,
|
||||
update_subscription_item, delete_subscription_item,
|
||||
get_subscription_profile, upsert_subscription_profile,
|
||||
)
|
||||
from .recommender import recommend_numbers, recommend_with_heatmap
|
||||
from .collector import sync_latest, sync_ensure_all
|
||||
@@ -751,3 +755,110 @@ def api_realestate_delete(complex_id: int):
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="Complex not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Subscription API ───────────────────────────────────────────────────────────
|
||||
|
||||
class SubscriptionItemCreate(BaseModel):
|
||||
complexName: str
|
||||
address: str = ""
|
||||
pyeong: Optional[str] = None
|
||||
totalPrice: Optional[int] = None
|
||||
type: Optional[str] = None
|
||||
specialType: Optional[str] = None
|
||||
supplyType: Optional[str] = None
|
||||
status: str = "검토중"
|
||||
minScore: Optional[int] = None
|
||||
maxIncome: Optional[int] = None
|
||||
homelessRequired: Optional[int] = None
|
||||
subscriptionStart: Optional[str] = None
|
||||
subscriptionEnd: Optional[str] = None
|
||||
contractDate: Optional[str] = None
|
||||
interimDate: Optional[str] = None
|
||||
balanceDate: Optional[str] = None
|
||||
resultDate: Optional[str] = None
|
||||
depositRate: int = 10
|
||||
interimRate: int = 60
|
||||
balanceRate: int = 30
|
||||
loanType: Optional[str] = None
|
||||
loanRate: Optional[float] = None
|
||||
memo: str = ""
|
||||
naverUrl: str = ""
|
||||
|
||||
|
||||
class SubscriptionItemUpdate(BaseModel):
|
||||
complexName: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
pyeong: Optional[str] = None
|
||||
totalPrice: Optional[int] = None
|
||||
type: Optional[str] = None
|
||||
specialType: Optional[str] = None
|
||||
supplyType: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
minScore: Optional[int] = None
|
||||
maxIncome: Optional[int] = None
|
||||
homelessRequired: Optional[int] = None
|
||||
subscriptionStart: Optional[str] = None
|
||||
subscriptionEnd: Optional[str] = None
|
||||
contractDate: Optional[str] = None
|
||||
interimDate: Optional[str] = None
|
||||
balanceDate: Optional[str] = None
|
||||
resultDate: Optional[str] = None
|
||||
depositRate: Optional[int] = None
|
||||
interimRate: Optional[int] = None
|
||||
balanceRate: Optional[int] = None
|
||||
loanType: Optional[str] = None
|
||||
loanRate: Optional[float] = None
|
||||
memo: Optional[str] = None
|
||||
naverUrl: Optional[str] = None
|
||||
|
||||
|
||||
class SubscriptionProfile(BaseModel):
|
||||
isHouseholdHead: Optional[bool] = None
|
||||
isHomeless: Optional[bool] = None
|
||||
homelessPeriod: Optional[int] = None
|
||||
savingsMonths: Optional[int] = None
|
||||
savingsCount: Optional[int] = None
|
||||
dependents: Optional[int] = None
|
||||
residencyArea: Optional[str] = None
|
||||
isMarried: Optional[bool] = None
|
||||
marriageMonths: Optional[int] = None
|
||||
monthlyIncome: Optional[int] = None
|
||||
specialQuals: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.get("/api/subscription/items")
|
||||
def api_subscription_list():
|
||||
return get_all_subscription_items()
|
||||
|
||||
|
||||
@app.post("/api/subscription/items", status_code=201)
|
||||
def api_subscription_create(body: SubscriptionItemCreate):
|
||||
return create_subscription_item(body.model_dump())
|
||||
|
||||
|
||||
@app.put("/api/subscription/items/{item_id}")
|
||||
def api_subscription_update(item_id: int, body: SubscriptionItemUpdate):
|
||||
updated = update_subscription_item(item_id, body.model_dump(exclude_none=True))
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return updated
|
||||
|
||||
|
||||
@app.delete("/api/subscription/items/{item_id}")
|
||||
def api_subscription_delete(item_id: int):
|
||||
ok = delete_subscription_item(item_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/subscription/profile")
|
||||
def api_subscription_profile_get():
|
||||
profile = get_subscription_profile()
|
||||
return profile if profile is not None else {}
|
||||
|
||||
|
||||
@app.put("/api/subscription/profile")
|
||||
def api_subscription_profile_put(body: SubscriptionProfile):
|
||||
return upsert_subscription_profile(body.model_dump(exclude_none=True))
|
||||
|
||||
Reference in New Issue
Block a user