feat(agent-office-telegram): realestate match formatter + keyboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
93
agent-office/app/telegram/realestate_message.py
Normal file
93
agent-office/app/telegram/realestate_message.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""청약 매칭 알림 — 텔레그램 메시지 포맷터 + 인라인 키보드 빌더."""
|
||||||
|
import os
|
||||||
|
from html import escape as _h
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
DASHBOARD_URL = os.getenv("REALESTATE_DASHBOARD_URL", "https://example.com/realestate")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_one_compact(m: dict) -> str:
|
||||||
|
score = m.get("match_score", 0)
|
||||||
|
name = _h(m.get("house_nm") or "(제목 없음)")
|
||||||
|
district = m.get("district") or ""
|
||||||
|
region = m.get("region_name") or ""
|
||||||
|
where = f"{region.split()[0] if region else ''} {district}".strip() or "위치 미상"
|
||||||
|
rstart = m.get("receipt_start") or ""
|
||||||
|
rend = m.get("receipt_end") or ""
|
||||||
|
return (
|
||||||
|
f"⭐ {score}점 — <b>{name}</b>\n"
|
||||||
|
f"📍 {_h(where)} 📅 {_h(rstart)} ~ {_h(rend)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_one_full(m: dict) -> str:
|
||||||
|
score = m.get("match_score", 0)
|
||||||
|
name = _h(m.get("house_nm") or "(제목 없음)")
|
||||||
|
district = m.get("district") or ""
|
||||||
|
region = m.get("region_name") or ""
|
||||||
|
flags = []
|
||||||
|
if m.get("is_speculative_area") == "Y":
|
||||||
|
flags.append("투기과열")
|
||||||
|
if m.get("is_price_cap") == "Y":
|
||||||
|
flags.append("분양가상한제")
|
||||||
|
flag_str = f" ({', '.join(flags)})" if flags else ""
|
||||||
|
|
||||||
|
rstart = m.get("receipt_start") or ""
|
||||||
|
rend = m.get("receipt_end") or ""
|
||||||
|
elig = m.get("eligible_types") or []
|
||||||
|
reasons = m.get("match_reasons") or []
|
||||||
|
|
||||||
|
where = f"{region.split()[0] if region else ''} {district}".strip() or "위치 미상"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"⭐ {score}점 — <b>{name}</b>",
|
||||||
|
f"📍 {_h(where)}{_h(flag_str)}",
|
||||||
|
f"📅 청약 {_h(rstart)} ~ {_h(rend)}",
|
||||||
|
]
|
||||||
|
if elig:
|
||||||
|
lines.append(f"✓ 자격: {_h(', '.join(elig))}")
|
||||||
|
if reasons:
|
||||||
|
lines.append(f"💡 {_h(' / '.join(reasons[:4]))}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def format_realestate_matches(matches: list[dict]) -> str:
|
||||||
|
"""매칭 목록을 텔레그램 HTML 메시지로 변환.
|
||||||
|
1~2건은 풀 카드, 3건 이상은 묶음 카드(상위 5건).
|
||||||
|
"""
|
||||||
|
if not matches:
|
||||||
|
return "🏢 새 청약 매칭이 없습니다."
|
||||||
|
|
||||||
|
if len(matches) <= 2:
|
||||||
|
body = "\n\n".join(_format_one_full(m) for m in matches)
|
||||||
|
return f"🏢 <b>새 청약 매칭 {len(matches)}건</b>\n━━━━━━━━━━\n\n{body}"
|
||||||
|
|
||||||
|
top = matches[:5]
|
||||||
|
body = "\n\n".join(_format_one_compact(m) for m in top)
|
||||||
|
suffix = f"\n\n…외 {len(matches) - 5}건" if len(matches) > 5 else ""
|
||||||
|
return f"🏢 <b>새 청약 매칭 {len(matches)}건</b>\n━━━━━━━━━━\n\n{body}{suffix}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_match_keyboard(matches: list[dict]) -> Optional[dict]:
|
||||||
|
"""1~2건: 매치별 [북마크][공고 보기] 행. 3건 이상: [전체 보기] 단일 행."""
|
||||||
|
if not matches:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(matches) <= 2:
|
||||||
|
rows = []
|
||||||
|
for m in matches:
|
||||||
|
buttons = [{
|
||||||
|
"text": "🔖 북마크",
|
||||||
|
"callback_data": f"realestate_bookmark_{m['id']}",
|
||||||
|
}]
|
||||||
|
url = m.get("pblanc_url")
|
||||||
|
if url:
|
||||||
|
buttons.append({"text": "📄 공고 보기", "url": url})
|
||||||
|
rows.append(buttons)
|
||||||
|
return {"inline_keyboard": rows}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"inline_keyboard": [[
|
||||||
|
{"text": "📋 전체 보기", "url": DASHBOARD_URL},
|
||||||
|
]],
|
||||||
|
}
|
||||||
59
agent-office/tests/test_realestate_message.py
Normal file
59
agent-office/tests/test_realestate_message.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
def test_format_realestate_match_full_card_single():
|
||||||
|
from app.telegram.realestate_message import format_realestate_matches
|
||||||
|
matches = [{
|
||||||
|
"id": 1,
|
||||||
|
"match_score": 90,
|
||||||
|
"house_nm": "디에이치 강남",
|
||||||
|
"region_name": "서울특별시",
|
||||||
|
"district": "강남구",
|
||||||
|
"is_speculative_area": "Y",
|
||||||
|
"is_price_cap": "Y",
|
||||||
|
"receipt_start": "2026-05-15",
|
||||||
|
"receipt_end": "2026-05-19",
|
||||||
|
"match_reasons": ["광역 일치", "자치구 S티어: 강남구 (+25)", "예산 범위"],
|
||||||
|
"eligible_types": ["일반1순위", "특별-신혼부부"],
|
||||||
|
"pblanc_url": "https://example.com/p/1",
|
||||||
|
}]
|
||||||
|
text = format_realestate_matches(matches)
|
||||||
|
assert "디에이치 강남" in text
|
||||||
|
assert "90점" in text
|
||||||
|
assert "강남구" in text
|
||||||
|
assert "2026-05-15" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_realestate_match_compact_when_three_or_more():
|
||||||
|
from app.telegram.realestate_message import format_realestate_matches
|
||||||
|
matches = [
|
||||||
|
{"id": i, "match_score": 90 - i, "house_nm": f"단지{i}", "district": "강남구",
|
||||||
|
"region_name": "서울특별시", "receipt_start": "2026-05-15", "receipt_end": "2026-05-19",
|
||||||
|
"match_reasons": [], "eligible_types": [], "pblanc_url": ""}
|
||||||
|
for i in range(3)
|
||||||
|
]
|
||||||
|
text = format_realestate_matches(matches)
|
||||||
|
assert "3건" in text or "3" in text
|
||||||
|
for i in range(3):
|
||||||
|
assert f"단지{i}" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_keyboard_single_match_has_bookmark_and_url():
|
||||||
|
from app.telegram.realestate_message import build_match_keyboard
|
||||||
|
matches = [{"id": 42, "pblanc_url": "https://example.com/p/42"}]
|
||||||
|
kb = build_match_keyboard(matches)
|
||||||
|
rows = kb["inline_keyboard"]
|
||||||
|
flat = [b for row in rows for b in row]
|
||||||
|
assert any(b.get("callback_data", "").startswith("realestate_bookmark_42") for b in flat)
|
||||||
|
assert any(b.get("url") == "https://example.com/p/42" for b in flat)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_keyboard_multi_matches_uses_dashboard_link():
|
||||||
|
from app.telegram.realestate_message import build_match_keyboard
|
||||||
|
matches = [{"id": i, "pblanc_url": ""} for i in range(3)]
|
||||||
|
kb = build_match_keyboard(matches)
|
||||||
|
flat = [b for row in kb["inline_keyboard"] for b in row]
|
||||||
|
# 3건 이상이면 [전체 보기] 단일 URL 버튼
|
||||||
|
assert any("전체" in b.get("text", "") for b in flat)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_keyboard_empty_returns_none():
|
||||||
|
from app.telegram.realestate_message import build_match_keyboard
|
||||||
|
assert build_match_keyboard([]) is None
|
||||||
Reference in New Issue
Block a user