diff --git a/agent-office/app/telegram/realestate_message.py b/agent-office/app/telegram/realestate_message.py new file mode 100644 index 0000000..30db65e --- /dev/null +++ b/agent-office/app/telegram/realestate_message.py @@ -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}점 — {name}\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}점 — {name}", + 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"🏢 새 청약 매칭 {len(matches)}건\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"🏢 새 청약 매칭 {len(matches)}건\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}, + ]], + } diff --git a/agent-office/tests/test_realestate_message.py b/agent-office/tests/test_realestate_message.py new file mode 100644 index 0000000..c02b11d --- /dev/null +++ b/agent-office/tests/test_realestate_message.py @@ -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