feat(realestate-lab): 즐겨찾기 + 가격 표시 + 일정 없는 공고 필터링
- announcements 테이블에 is_bookmarked 컬럼 추가 (마이그레이션 포함)
- PATCH /announcements/{id}/bookmark 토글 API 추가
- 공고 목록에 모델 기반 가격 범위(min_price, max_price_display) 포함
- 대시보드에 즐겨찾기 목록 + 개별 이벤트 일정 형식 반환
- 지역 검색을 LIKE 부분 매칭으로 변경
- 수집 시 일정 정보 없는 공고 건너뛰기
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,7 @@ def init_db():
|
||||
is_price_cap TEXT,
|
||||
contact TEXT,
|
||||
status TEXT NOT NULL DEFAULT '청약예정',
|
||||
is_bookmarked INTEGER NOT NULL DEFAULT 0,
|
||||
source TEXT NOT NULL DEFAULT 'manual',
|
||||
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')),
|
||||
@@ -62,6 +63,12 @@ def init_db():
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ann_status ON announcements(status);")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ann_region ON announcements(region_name);")
|
||||
|
||||
# ── 마이그레이션: is_bookmarked 컬럼 추가 ──
|
||||
try:
|
||||
conn.execute("SELECT is_bookmarked FROM announcements LIMIT 1")
|
||||
except Exception:
|
||||
conn.execute("ALTER TABLE announcements ADD COLUMN is_bookmarked INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
# ── announcement_models ──────────────────────────────────────────
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS announcement_models (
|
||||
@@ -228,25 +235,45 @@ def upsert_announcement(data: Dict[str, Any]) -> tuple:
|
||||
return _ann_row_to_dict(row), is_new
|
||||
|
||||
|
||||
def _enrich_with_price(conn, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""공고 목록에 모델 기반 가격 범위를 추가한다."""
|
||||
for item in items:
|
||||
hmno = item.get("house_manage_no")
|
||||
pno = item.get("pblanc_no")
|
||||
if hmno and pno:
|
||||
price_row = conn.execute(
|
||||
"SELECT MIN(top_amount) as min_price, MAX(top_amount) as max_price "
|
||||
"FROM announcement_models WHERE house_manage_no = ? AND pblanc_no = ? AND top_amount IS NOT NULL",
|
||||
(hmno, pno),
|
||||
).fetchone()
|
||||
if price_row and price_row["min_price"] is not None:
|
||||
item["min_price"] = price_row["min_price"]
|
||||
item["max_price_display"] = price_row["max_price"]
|
||||
return items
|
||||
|
||||
|
||||
def get_announcements(
|
||||
region: str = None,
|
||||
status: str = None,
|
||||
house_type: str = None,
|
||||
matched_only: bool = False,
|
||||
bookmarked: bool = False,
|
||||
sort: str = "date",
|
||||
page: int = 1,
|
||||
size: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
conditions, params = [], []
|
||||
if region:
|
||||
conditions.append("a.region_name = ?")
|
||||
params.append(region)
|
||||
conditions.append("a.region_name LIKE ?")
|
||||
params.append(f"%{region}%")
|
||||
if status:
|
||||
conditions.append("a.status = ?")
|
||||
params.append(status)
|
||||
if house_type:
|
||||
conditions.append("a.house_secd = ?")
|
||||
params.append(house_type)
|
||||
if bookmarked:
|
||||
conditions.append("a.is_bookmarked = 1")
|
||||
|
||||
if matched_only:
|
||||
conditions.append("a.id IN (SELECT announcement_id FROM match_results)")
|
||||
@@ -268,8 +295,10 @@ def get_announcements(
|
||||
f"SELECT a.* FROM announcements a {where} ORDER BY {order} LIMIT ? OFFSET ?",
|
||||
params + [size, offset],
|
||||
).fetchall()
|
||||
items = [_ann_row_to_dict(r) for r in rows]
|
||||
items = _enrich_with_price(conn, items)
|
||||
return {
|
||||
"items": [_ann_row_to_dict(r) for r in rows],
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
@@ -339,6 +368,20 @@ def update_announcement(ann_id: int, data: Dict[str, Any]) -> Optional[Dict[str,
|
||||
return get_announcement(ann_id)
|
||||
|
||||
|
||||
def toggle_bookmark(ann_id: int) -> Optional[Dict[str, Any]]:
|
||||
with _conn() as conn:
|
||||
row = conn.execute("SELECT id, is_bookmarked FROM announcements WHERE id = ?", (ann_id,)).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
new_val = 0 if row["is_bookmarked"] else 1
|
||||
conn.execute(
|
||||
"UPDATE announcements SET is_bookmarked = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?",
|
||||
(new_val, ann_id),
|
||||
)
|
||||
updated = conn.execute("SELECT * FROM announcements WHERE id = ?", (ann_id,)).fetchone()
|
||||
return _ann_row_to_dict(updated)
|
||||
|
||||
|
||||
def delete_announcement(ann_id: int) -> bool:
|
||||
with _conn() as conn:
|
||||
# match_results는 FK CASCADE로 자동 삭제
|
||||
@@ -532,15 +575,54 @@ def get_dashboard() -> Dict[str, Any]:
|
||||
new_matches = conn.execute(
|
||||
"SELECT COUNT(*) FROM match_results WHERE is_new = 1"
|
||||
).fetchone()[0]
|
||||
upcoming = conn.execute("""
|
||||
SELECT id, house_nm, receipt_start, receipt_end, status
|
||||
bookmarked_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM announcements WHERE is_bookmarked = 1"
|
||||
).fetchone()[0]
|
||||
|
||||
# 다가오는 일정을 개별 이벤트로 분해
|
||||
upcoming_rows = conn.execute("""
|
||||
SELECT id, house_nm, receipt_start, receipt_end,
|
||||
spsply_start, gnrl_rank1_start, winner_date,
|
||||
contract_start, status
|
||||
FROM announcements
|
||||
WHERE status IN ('청약예정', '청약중')
|
||||
ORDER BY receipt_start ASC
|
||||
LIMIT 5
|
||||
LIMIT 20
|
||||
""").fetchall()
|
||||
|
||||
today = date.today().isoformat()
|
||||
schedules = []
|
||||
for r in upcoming_rows:
|
||||
events = [
|
||||
("특별공급 접수", r["spsply_start"]),
|
||||
("1순위 접수", r["gnrl_rank1_start"]),
|
||||
("청약 접수", r["receipt_start"]),
|
||||
("당첨자 발표", r["winner_date"]),
|
||||
("계약 시작", r["contract_start"]),
|
||||
]
|
||||
for event, d in events:
|
||||
if d and d >= today:
|
||||
schedules.append({
|
||||
"announcement_id": r["id"],
|
||||
"house_nm": r["house_nm"],
|
||||
"event": event,
|
||||
"date": d,
|
||||
})
|
||||
schedules.sort(key=lambda s: s["date"])
|
||||
schedules = schedules[:10]
|
||||
|
||||
# 즐겨찾기 공고
|
||||
bookmarked_rows = conn.execute("""
|
||||
SELECT * FROM announcements WHERE is_bookmarked = 1
|
||||
ORDER BY receipt_start ASC
|
||||
""").fetchall()
|
||||
bookmarked_items = [_ann_row_to_dict(r) for r in bookmarked_rows]
|
||||
bookmarked_items = _enrich_with_price(conn, bookmarked_items)
|
||||
|
||||
return {
|
||||
"active_count": active,
|
||||
"new_match_count": new_matches,
|
||||
"upcoming": [dict(r) for r in upcoming],
|
||||
"bookmarked_count": bookmarked_count,
|
||||
"upcoming_schedules": schedules,
|
||||
"bookmarked": bookmarked_items,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user