Spaces:
Running
Running
Upload 25 files
Browse files- scripts/chunk_handlers.py +695 -115
- src/auth/asset_parser.py +213 -0
- src/auth/config.py +128 -0
- src/auth/credit_card_handlers.py +684 -0
- src/auth/tool_handlers.py +128 -0
- src/auth/valuation_handlers.py +530 -0
- src/db/supabase_adapter.py +347 -6
- src/mcp/server_streamable.py +367 -0
scripts/chunk_handlers.py
CHANGED
|
@@ -922,7 +922,14 @@ def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
|
|
| 922 |
if family_fee and isinstance(family_fee, dict) and family_fee.get("amount"):
|
| 923 |
content += f" - ๊ฐ์กฑ์นด๋: {family_fee.get('amount'):,} {family_fee.get('currency', '')}\n"
|
| 924 |
if waiver_conds:
|
| 925 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
|
| 927 |
# ์นด๋ ์ ํ (Phase 3)
|
| 928 |
if card_type:
|
|
@@ -985,7 +992,14 @@ def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
|
|
| 985 |
if lounge_access or lounge_visits:
|
| 986 |
content += "\n๋ผ์ด์ง ํํ:\n"
|
| 987 |
if lounge_access:
|
| 988 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
if lounge_visits:
|
| 990 |
content += f" - ์ฐ๊ฐ ์ด์ฉ: {lounge_visits}ํ\n"
|
| 991 |
if lounge_guest:
|
|
@@ -1195,10 +1209,53 @@ def handle_pricing(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
| 1195 |
"""pricing, pricing_2026 ๋ฑ ๋ค์ํ ํ์ ์ฒ๋ฆฌ"""
|
| 1196 |
chunks = []
|
| 1197 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 1198 |
-
|
| 1199 |
if data is None:
|
| 1200 |
return chunks
|
| 1201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1202 |
# --- ํ์ 1: annual_statistics ๊ตฌ์กฐ (Westin ๋ฑ) ---
|
| 1203 |
annual_stats = data.get("annual_statistics", {})
|
| 1204 |
points_stats = annual_stats.get("points", {})
|
|
@@ -1330,10 +1387,60 @@ def handle_pricing_analysis(data: Any, context: Dict[str, Any]) -> List[Dict[str
|
|
| 1330 |
"""pricing_analysis ํ์ ์ฒ๋ฆฌ (Josun Palace ๋ฑ)"""
|
| 1331 |
chunks = []
|
| 1332 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 1333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
points_stats = data.get("points_price_stats", {})
|
| 1335 |
cash_stats = data.get("cash_price_stats", {})
|
| 1336 |
-
|
| 1337 |
if not points_stats:
|
| 1338 |
return chunks
|
| 1339 |
|
|
@@ -1989,35 +2096,104 @@ def handle_member_rates(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
|
|
| 1989 |
# ===========================================================================
|
| 1990 |
|
| 1991 |
def handle_hotel_facilities(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 1992 |
-
"""hotel_facilities, other_facilities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1993 |
chunks = []
|
| 1994 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 1995 |
|
| 1996 |
if data is None:
|
| 1997 |
return chunks
|
| 1998 |
|
| 1999 |
-
facilities = data if isinstance(data, list) else [data]
|
| 2000 |
-
|
| 2001 |
-
# ์์ค์ ํ๋์ ์ฒญํฌ๋ก ํตํฉ
|
| 2002 |
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
|
| 2003 |
-
content += "ํธํ
์์ค:\n"
|
| 2004 |
|
| 2005 |
-
|
| 2006 |
-
|
| 2007 |
-
|
| 2008 |
-
|
| 2009 |
-
|
| 2010 |
-
|
| 2011 |
-
|
| 2012 |
-
|
| 2013 |
-
|
| 2014 |
-
content += f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2015 |
|
| 2016 |
if len(content) > 100:
|
| 2017 |
chunks.append({
|
| 2018 |
"content": content.strip(),
|
| 2019 |
"metadata": {
|
| 2020 |
-
"type":
|
| 2021 |
"hotel_name": hotel_name
|
| 2022 |
}
|
| 2023 |
})
|
|
@@ -2358,6 +2534,82 @@ def handle_dining_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str,
|
|
| 2358 |
return chunks
|
| 2359 |
|
| 2360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2361 |
# ===========================================================================
|
| 2362 |
# ํธ๋ค๋ฌ: ํญ๊ณต ํ๋ก๊ทธ๋จ (Phase 2)
|
| 2363 |
# ===========================================================================
|
|
@@ -4399,7 +4651,12 @@ def handle_route_classifications(data: Any, context: Dict[str, Any]) -> List[Dic
|
|
| 4399 |
# ===========================================================================
|
| 4400 |
|
| 4401 |
def handle_award_chart_entries(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 4402 |
-
"""award_chart_entries ์ฒ๋ฆฌ - ์ด์๋ ์ฐจํธ ์์ธ ํญ๋ชฉ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4403 |
chunks = []
|
| 4404 |
if not data:
|
| 4405 |
return chunks
|
|
@@ -4407,62 +4664,124 @@ def handle_award_chart_entries(data: Any, context: Dict[str, Any]) -> List[Dict[
|
|
| 4407 |
items = data if isinstance(data, list) else [data]
|
| 4408 |
airline = context.get("chain", "UNKNOWN")
|
| 4409 |
|
| 4410 |
-
# ์ง
|
| 4411 |
-
|
| 4412 |
-
for item in items:
|
| 4413 |
-
region = item.get("region", "OTHER")
|
| 4414 |
-
if region not in region_entries:
|
| 4415 |
-
region_entries[region] = []
|
| 4416 |
-
region_entries[region].append(item)
|
| 4417 |
|
| 4418 |
-
|
| 4419 |
-
|
| 4420 |
-
|
| 4421 |
-
|
| 4422 |
-
|
| 4423 |
-
trip_type = entry.get("trip_type", "")
|
| 4424 |
-
trip_label = "์๋ณต" if trip_type == "ROUND_TRIP" else "ํธ๋"
|
| 4425 |
|
| 4426 |
-
|
|
|
|
|
|
|
| 4427 |
|
| 4428 |
-
|
| 4429 |
-
|
| 4430 |
-
economy_high = entry.get("economy_high")
|
| 4431 |
-
if economy_low or economy_high:
|
| 4432 |
-
content += f"- **์ผ๋ฐ์**: {economy_low:,}~{economy_high:,} ๋ง์ผ\n"
|
| 4433 |
|
| 4434 |
-
|
| 4435 |
-
|
| 4436 |
-
|
| 4437 |
-
content += f"- **ํ๋ฆฌ๋ฏธ์ ์ด์ฝ๋
ธ๋ฏธ**: {premium_economy_low:,}~{premium_economy_high:,} ๋ง์ผ\n"
|
| 4438 |
|
| 4439 |
-
|
| 4440 |
-
|
| 4441 |
-
|
| 4442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4443 |
|
| 4444 |
-
|
| 4445 |
-
first_high = entry.get("first_high")
|
| 4446 |
-
if first_low or first_high:
|
| 4447 |
-
content += f"- **์ผ๋ฑ์**: {first_low:,}~{first_high:,} ๋ง์ผ\n"
|
| 4448 |
|
| 4449 |
-
|
| 4450 |
-
|
| 4451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4452 |
|
| 4453 |
-
|
| 4454 |
-
|
| 4455 |
-
|
| 4456 |
-
|
| 4457 |
-
|
| 4458 |
-
|
| 4459 |
-
|
| 4460 |
-
|
| 4461 |
-
|
| 4462 |
-
|
| 4463 |
-
|
| 4464 |
-
|
| 4465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4466 |
|
| 4467 |
return chunks
|
| 4468 |
|
|
@@ -4739,7 +5058,13 @@ def handle_key_insights(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
|
|
| 4739 |
|
| 4740 |
|
| 4741 |
def handle_usage_recommendations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 4742 |
-
"""usage_recommendations ์ฒ๋ฆฌ - ๋ง์ผ๋ฆฌ์ง ์ฌ์ฉ ๊ถ์ฅ์ฌํญ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4743 |
chunks = []
|
| 4744 |
if not data:
|
| 4745 |
return chunks
|
|
@@ -4748,53 +5073,97 @@ def handle_usage_recommendations(data: Any, context: Dict[str, Any]) -> List[Dic
|
|
| 4748 |
|
| 4749 |
content = f"# {airline} ๋ง์ผ๋ฆฌ์ง ์ฌ์ฉ ๊ถ์ฅ์ฌํญ\n\n"
|
| 4750 |
|
| 4751 |
-
#
|
| 4752 |
-
|
| 4753 |
-
|
| 4754 |
-
|
| 4755 |
-
|
| 4756 |
-
|
| 4757 |
-
|
| 4758 |
-
|
| 4759 |
-
|
| 4760 |
-
|
| 4761 |
-
|
| 4762 |
-
|
| 4763 |
-
|
| 4764 |
-
|
| 4765 |
-
|
| 4766 |
-
|
| 4767 |
-
|
| 4768 |
-
|
| 4769 |
-
|
| 4770 |
-
|
| 4771 |
-
|
| 4772 |
-
|
| 4773 |
-
|
| 4774 |
-
|
| 4775 |
-
|
| 4776 |
-
|
| 4777 |
-
|
| 4778 |
-
content += f"-
|
| 4779 |
-
content += "\n"
|
| 4780 |
|
| 4781 |
-
#
|
| 4782 |
-
|
| 4783 |
-
|
| 4784 |
-
|
| 4785 |
-
|
| 4786 |
-
|
| 4787 |
-
|
| 4788 |
-
|
| 4789 |
-
|
| 4790 |
-
|
| 4791 |
-
|
| 4792 |
-
|
| 4793 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4794 |
|
| 4795 |
content += DISCLAIMER_SHORT
|
| 4796 |
|
| 4797 |
-
if len(content) >
|
| 4798 |
chunks.append({
|
| 4799 |
"content": content.strip(),
|
| 4800 |
"metadata": {
|
|
@@ -5598,6 +5967,116 @@ def handle_news_updates(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
|
|
| 5598 |
return chunks
|
| 5599 |
|
| 5600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5601 |
# ===========================================================================
|
| 5602 |
# ํค-ํธ๋ค๋ฌ ๋งคํ
|
| 5603 |
# ===========================================================================
|
|
@@ -5663,6 +6142,25 @@ CHUNK_HANDLERS = {
|
|
| 5663 |
# ํธํ
์์ค
|
| 5664 |
"hotel_facilities": handle_hotel_facilities,
|
| 5665 |
"other_facilities": handle_hotel_facilities,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5666 |
|
| 5667 |
# ๊ฐ์ค ์ ํ
|
| 5668 |
"room_types": handle_room_types,
|
|
@@ -5742,11 +6240,13 @@ CHUNK_HANDLERS = {
|
|
| 5742 |
# --- ํญ๊ณต ํ๋ก๊ทธ๋จ (Phase 2) ---
|
| 5743 |
"airline_programs": handle_airline_programs,
|
| 5744 |
"airline_program": handle_airline_programs,
|
|
|
|
| 5745 |
"airline_tiers": handle_airline_tiers,
|
| 5746 |
"airline_tier": handle_airline_tiers,
|
| 5747 |
"award_charts": handle_award_charts,
|
| 5748 |
"award_chart": handle_award_charts,
|
| 5749 |
"airline_earning_rules": handle_airline_earning_rules,
|
|
|
|
| 5750 |
|
| 5751 |
# --- ํญ๊ณต ์ด์ (Airline Fares) ---
|
| 5752 |
"airline_fare_tables": handle_airline_fare_tables,
|
|
@@ -5871,6 +6371,74 @@ CHUNK_HANDLERS = {
|
|
| 5871 |
"mileage_purchase_programs": handle_mileage_purchase_programs,
|
| 5872 |
"price_summary": handle_mileage_price_summary,
|
| 5873 |
"insights": handle_mileage_insights,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5874 |
}
|
| 5875 |
|
| 5876 |
# ์ค์ฒฉ ํค ์ง์ (์: facts.pricing_analysis)
|
|
@@ -5933,6 +6501,18 @@ IGNORED_KEYS = {
|
|
| 5933 |
"collection_date", # ์์ง์ผ ๋ฉํ๋ฐ์ดํฐ
|
| 5934 |
"related_documents", # ๊ด๋ จ ๋ฌธ์ ์ฐธ์กฐ
|
| 5935 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5936 |
# Deprecated ํค (์ฌ์ฉํ์ง ์์)
|
| 5937 |
# "loyalty_programs" (๋ณต์) - ๋จ, ํ์ ํธํ์ฑ ์ํด ํธ๋ค๋ฌ๋ ์ ์ง
|
| 5938 |
}
|
|
|
|
| 922 |
if family_fee and isinstance(family_fee, dict) and family_fee.get("amount"):
|
| 923 |
content += f" - ๊ฐ์กฑ์นด๋: {family_fee.get('amount'):,} {family_fee.get('currency', '')}\n"
|
| 924 |
if waiver_conds:
|
| 925 |
+
# Handle both list of strings and list of dicts
|
| 926 |
+
waiver_strs = []
|
| 927 |
+
for item in waiver_conds[:2]:
|
| 928 |
+
if isinstance(item, dict):
|
| 929 |
+
waiver_strs.append(item.get("summary", item.get("condition", str(item))))
|
| 930 |
+
else:
|
| 931 |
+
waiver_strs.append(str(item))
|
| 932 |
+
content += f" - ๋ฉด์ ์กฐ๊ฑด: {', '.join(waiver_strs)}\n"
|
| 933 |
|
| 934 |
# ์นด๋ ์ ํ (Phase 3)
|
| 935 |
if card_type:
|
|
|
|
| 992 |
if lounge_access or lounge_visits:
|
| 993 |
content += "\n๋ผ์ด์ง ํํ:\n"
|
| 994 |
if lounge_access:
|
| 995 |
+
# Handle both list of strings and list of dicts
|
| 996 |
+
lounge_names = []
|
| 997 |
+
for item in lounge_access[:5]:
|
| 998 |
+
if isinstance(item, dict):
|
| 999 |
+
lounge_names.append(item.get("lounge_name", item.get("name", str(item))))
|
| 1000 |
+
else:
|
| 1001 |
+
lounge_names.append(str(item))
|
| 1002 |
+
content += f" - ์ด์ฉ ๊ฐ๋ฅ: {', '.join(lounge_names)}\n"
|
| 1003 |
if lounge_visits:
|
| 1004 |
content += f" - ์ฐ๊ฐ ์ด์ฉ: {lounge_visits}ํ\n"
|
| 1005 |
if lounge_guest:
|
|
|
|
| 1209 |
"""pricing, pricing_2026 ๋ฑ ๋ค์ํ ํ์ ์ฒ๋ฆฌ"""
|
| 1210 |
chunks = []
|
| 1211 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 1212 |
+
|
| 1213 |
if data is None:
|
| 1214 |
return chunks
|
| 1215 |
+
|
| 1216 |
+
# --- ๋ฆฌ์คํธ ๊ตฌ์กฐ ์ฒ๋ฆฌ (raw_pricing_data ๋ฑ) ---
|
| 1217 |
+
if isinstance(data, list):
|
| 1218 |
+
for item in data[:10]:
|
| 1219 |
+
if not isinstance(item, dict):
|
| 1220 |
+
continue
|
| 1221 |
+
|
| 1222 |
+
room_type = item.get("room_type_code", item.get("room_type", ""))
|
| 1223 |
+
room_name = item.get("room_type_full_name", "")
|
| 1224 |
+
rate_type = item.get("rate_type", "")
|
| 1225 |
+
rate_krw = item.get("rate_krw", 0)
|
| 1226 |
+
rate_usd = item.get("rate_usd_approx", 0)
|
| 1227 |
+
|
| 1228 |
+
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
|
| 1229 |
+
content += f"๊ฐ์ค ์ ํ: {room_type}\n"
|
| 1230 |
+
if room_name:
|
| 1231 |
+
content += f"๊ฐ์ค๋ช
: {room_name}\n"
|
| 1232 |
+
if rate_type:
|
| 1233 |
+
content += f"์๊ธ ์ ํ: {rate_type}\n"
|
| 1234 |
+
|
| 1235 |
+
content += f"๊ฐ๊ฒฉ:\n"
|
| 1236 |
+
if rate_krw:
|
| 1237 |
+
content += f" - KRW: โฉ{rate_krw:,}\n"
|
| 1238 |
+
if rate_usd:
|
| 1239 |
+
content += f" - USD: ${rate_usd}\n"
|
| 1240 |
+
|
| 1241 |
+
content += DISCLAIMER_PRICE
|
| 1242 |
+
|
| 1243 |
+
chunks.append({
|
| 1244 |
+
"content": content.strip(),
|
| 1245 |
+
"metadata": {
|
| 1246 |
+
"type": "room_pricing_raw",
|
| 1247 |
+
"hotel_name": hotel_name,
|
| 1248 |
+
"room_type": room_type,
|
| 1249 |
+
"rate_krw": rate_krw
|
| 1250 |
+
}
|
| 1251 |
+
})
|
| 1252 |
+
|
| 1253 |
+
return chunks
|
| 1254 |
+
|
| 1255 |
+
# ๋์
๋๋ฆฌ ๊ตฌ์กฐ๊ฐ ์๋๋ฉด ๋น ๋ฐฐ์ด ๋ฐํ
|
| 1256 |
+
if not isinstance(data, dict):
|
| 1257 |
+
return chunks
|
| 1258 |
+
|
| 1259 |
# --- ํ์ 1: annual_statistics ๊ตฌ์กฐ (Westin ๋ฑ) ---
|
| 1260 |
annual_stats = data.get("annual_statistics", {})
|
| 1261 |
points_stats = annual_stats.get("points", {})
|
|
|
|
| 1387 |
"""pricing_analysis ํ์ ์ฒ๋ฆฌ (Josun Palace ๋ฑ)"""
|
| 1388 |
chunks = []
|
| 1389 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 1390 |
+
|
| 1391 |
+
# ๋ฆฌ์คํธ ๊ตฌ์กฐ์ธ ๊ฒฝ์ฐ ์ฒ๋ฆฌ (ClassicTravel ๋ฑ)
|
| 1392 |
+
if isinstance(data, list):
|
| 1393 |
+
for item in data[:10]:
|
| 1394 |
+
if not isinstance(item, dict):
|
| 1395 |
+
continue
|
| 1396 |
+
|
| 1397 |
+
hotel_id = item.get("hotel_id", "")
|
| 1398 |
+
item_hotel_name = item.get("hotel_name", hotel_name)
|
| 1399 |
+
search_date = item.get("search_date", "")
|
| 1400 |
+
stay_nights = item.get("stay_nights", 1)
|
| 1401 |
+
currency = item.get("currency", "KRW")
|
| 1402 |
+
|
| 1403 |
+
content = format_hotel_header(item_hotel_name, hotel_name_ko, chain)
|
| 1404 |
+
content += f"ํธํ
ID: {hotel_id}\n"
|
| 1405 |
+
content += f"๊ฒ์ ๋ ์ง: {search_date}\n"
|
| 1406 |
+
content += f"์๋ฐ ๊ธฐ๊ฐ: {stay_nights}๋ฐ\n\n"
|
| 1407 |
+
|
| 1408 |
+
room_rates = item.get("room_rates", [])
|
| 1409 |
+
if room_rates:
|
| 1410 |
+
content += "๊ฐ์ค ์๊ธ:\n"
|
| 1411 |
+
for rate in room_rates[:5]:
|
| 1412 |
+
room_type = rate.get("room_type", "")
|
| 1413 |
+
rate_plan = rate.get("rate_plan", "")
|
| 1414 |
+
rate_desc = rate.get("rate_plan_description", "")
|
| 1415 |
+
price_krw = rate.get("price_krw", 0)
|
| 1416 |
+
|
| 1417 |
+
content += f" - {room_type}\n"
|
| 1418 |
+
content += f" ์๊ธ์ : {rate_plan}\n"
|
| 1419 |
+
if rate_desc:
|
| 1420 |
+
content += f" ์ค๋ช
: {rate_desc}\n"
|
| 1421 |
+
content += f" ๊ฐ๊ฒฉ: โฉ{price_krw:,}\n"
|
| 1422 |
+
|
| 1423 |
+
content += DISCLAIMER_PRICE
|
| 1424 |
+
|
| 1425 |
+
chunks.append({
|
| 1426 |
+
"content": content.strip(),
|
| 1427 |
+
"metadata": {
|
| 1428 |
+
"type": "room_pricing",
|
| 1429 |
+
"hotel_name": item_hotel_name,
|
| 1430 |
+
"hotel_id": hotel_id,
|
| 1431 |
+
"currency": currency
|
| 1432 |
+
}
|
| 1433 |
+
})
|
| 1434 |
+
|
| 1435 |
+
return chunks
|
| 1436 |
+
|
| 1437 |
+
# ๊ธฐ์กด ๋์
๋๋ฆฌ ๊ตฌ์กฐ ์ฒ๋ฆฌ
|
| 1438 |
+
if not isinstance(data, dict):
|
| 1439 |
+
return chunks
|
| 1440 |
+
|
| 1441 |
points_stats = data.get("points_price_stats", {})
|
| 1442 |
cash_stats = data.get("cash_price_stats", {})
|
| 1443 |
+
|
| 1444 |
if not points_stats:
|
| 1445 |
return chunks
|
| 1446 |
|
|
|
|
| 2096 |
# ===========================================================================
|
| 2097 |
|
| 2098 |
def handle_hotel_facilities(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 2099 |
+
"""hotel_facilities, other_facilities, hotel_amenities_summary,
|
| 2100 |
+
executive_lounge, pulse8_wellness ๋ฑ ๋ค์ํ ์์ค ๋ฐ์ดํฐ ์ฒ๋ฆฌ
|
| 2101 |
+
|
| 2102 |
+
์ง์ ํํ:
|
| 2103 |
+
1. ๋ฆฌ์คํธ ํํ (๊ธฐ์กด hotel_facilities)
|
| 2104 |
+
2. ๋์
๋๋ฆฌ ํํ (executive_lounge, pulse8_wellness ๋ฑ)
|
| 2105 |
+
3. ๋ฌธ์์ด ๋ฆฌ์คํธ (hotel_amenities_summary)
|
| 2106 |
+
"""
|
| 2107 |
chunks = []
|
| 2108 |
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 2109 |
|
| 2110 |
if data is None:
|
| 2111 |
return chunks
|
| 2112 |
|
|
|
|
|
|
|
|
|
|
| 2113 |
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
|
|
|
|
| 2114 |
|
| 2115 |
+
# ๋ฐ์ดํฐ ํํ์ ๋ฐ๋ฅธ ์ฒ๋ฆฌ
|
| 2116 |
+
if isinstance(data, dict):
|
| 2117 |
+
# ๋์
๋๋ฆฌ ํํ (executive_lounge, pulse8_wellness ๋ฑ)
|
| 2118 |
+
facility_name = data.get("name", data.get("lounge_name", "์์ค"))
|
| 2119 |
+
location = data.get("location", "")
|
| 2120 |
+
hours = data.get("operating_hours", "")
|
| 2121 |
+
|
| 2122 |
+
content += f"์์ค: {facility_name}\n"
|
| 2123 |
+
if location:
|
| 2124 |
+
content += f"์์น: {location}\n"
|
| 2125 |
+
if hours:
|
| 2126 |
+
content += f"์ด์์๊ฐ: {hours}\n"
|
| 2127 |
+
|
| 2128 |
+
# ์ ๋ง
|
| 2129 |
+
if data.get("view"):
|
| 2130 |
+
content += f"์ ๋ง: {data.get('view')}\n"
|
| 2131 |
+
|
| 2132 |
+
# ์๋น์ค/์์ค ๋ชฉ๋ก
|
| 2133 |
+
services = data.get("services", data.get("facilities", []))
|
| 2134 |
+
if services:
|
| 2135 |
+
content += "\n์ ๊ณต ์๋น์ค/์์ค:\n"
|
| 2136 |
+
for service in services[:10]:
|
| 2137 |
+
if isinstance(service, dict):
|
| 2138 |
+
s_name = service.get("name", "N/A")
|
| 2139 |
+
s_desc = service.get("description", "")
|
| 2140 |
+
content += f" - {s_name}"
|
| 2141 |
+
if s_desc:
|
| 2142 |
+
content += f": {s_desc[:60]}"
|
| 2143 |
+
content += "\n"
|
| 2144 |
+
else:
|
| 2145 |
+
content += f" - {service}\n"
|
| 2146 |
+
|
| 2147 |
+
# ๋๋ ์ค์ฝ๋
|
| 2148 |
+
if data.get("dress_code"):
|
| 2149 |
+
content += f"\n๋๋ ์ค์ฝ๋: {data.get('dress_code')}\n"
|
| 2150 |
+
|
| 2151 |
+
# ์ฐ๋ น ์ ํ
|
| 2152 |
+
if data.get("age_restriction"):
|
| 2153 |
+
content += f"์ฐ๋ น ์ ํ: {data.get('age_restriction')}\n"
|
| 2154 |
+
|
| 2155 |
+
# ๋ฉํ๋ฐ์ดํฐ ํ์
๊ฒฐ์
|
| 2156 |
+
meta_type = "hotel_facility"
|
| 2157 |
+
if "lounge" in str(data).lower():
|
| 2158 |
+
meta_type = "executive_lounge"
|
| 2159 |
+
elif "wellness" in str(data).lower() or "pulse" in str(data).lower():
|
| 2160 |
+
meta_type = "wellness_facility"
|
| 2161 |
+
|
| 2162 |
+
elif isinstance(data, list):
|
| 2163 |
+
# ๋ฆฌ์คํธ ํํ
|
| 2164 |
+
facilities = data
|
| 2165 |
+
|
| 2166 |
+
# ์ฒซ ๋ฒ์งธ ํญ๋ชฉ ํ์ธํ์ฌ ํ์
๊ฒฐ์
|
| 2167 |
+
first_item = facilities[0] if facilities else None
|
| 2168 |
+
|
| 2169 |
+
if first_item and isinstance(first_item, str):
|
| 2170 |
+
# ๋ฌธ์์ด ๋ฆฌ์คํธ (hotel_amenities_summary)
|
| 2171 |
+
content += "ํธํ
์์ค ๋ฐ ์๋น์ค ์์ฝ:\n"
|
| 2172 |
+
for amenity in facilities[:20]:
|
| 2173 |
+
content += f" โ {amenity}\n"
|
| 2174 |
+
meta_type = "hotel_amenities_summary"
|
| 2175 |
+
else:
|
| 2176 |
+
# ๋์
๋๋ฆฌ ๋ฆฌ์คํธ (๊ธฐ์กด hotel_facilities)
|
| 2177 |
+
content += "ํธํ
์์ค:\n"
|
| 2178 |
+
for facility in facilities[:15]:
|
| 2179 |
+
if isinstance(facility, dict):
|
| 2180 |
+
name = facility.get("name", facility.get("facility_name", "N/A"))
|
| 2181 |
+
desc = facility.get("description", "")
|
| 2182 |
+
content += f" - {name}"
|
| 2183 |
+
if desc:
|
| 2184 |
+
content += f": {desc[:60]}"
|
| 2185 |
+
content += "\n"
|
| 2186 |
+
elif isinstance(facility, str):
|
| 2187 |
+
content += f" - {facility}\n"
|
| 2188 |
+
meta_type = "hotel_facilities"
|
| 2189 |
+
else:
|
| 2190 |
+
return chunks
|
| 2191 |
|
| 2192 |
if len(content) > 100:
|
| 2193 |
chunks.append({
|
| 2194 |
"content": content.strip(),
|
| 2195 |
"metadata": {
|
| 2196 |
+
"type": meta_type,
|
| 2197 |
"hotel_name": hotel_name
|
| 2198 |
}
|
| 2199 |
})
|
|
|
|
| 2534 |
return chunks
|
| 2535 |
|
| 2536 |
|
| 2537 |
+
# ===========================================================================
|
| 2538 |
+
# ํธ๋ค๋ฌ: ํญ๊ณต ํํธ๋์ญ (Flying Blue โ Korean Air ๋ฑ)
|
| 2539 |
+
# ===========================================================================
|
| 2540 |
+
|
| 2541 |
+
def handle_airline_partnership(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 2542 |
+
"""airline_partnership ์ฒ๋ฆฌ - ํํธ๋ ํ๋ก๊ทธ๋จ์ผ๋ก ํํญ๊ณต์ฌ ์์ฝ ์ ๋ณด"""
|
| 2543 |
+
chunks = []
|
| 2544 |
+
|
| 2545 |
+
if not data or not isinstance(data, dict):
|
| 2546 |
+
return chunks
|
| 2547 |
+
|
| 2548 |
+
partner_program = data.get("partner_program", "N/A")
|
| 2549 |
+
operating_airline = data.get("operating_airline", "UNKNOWN")
|
| 2550 |
+
alliance = data.get("alliance", "")
|
| 2551 |
+
currency_name = data.get("currency_name", "ํฌ์ธํธ")
|
| 2552 |
+
pricing_model = data.get("pricing_model", "")
|
| 2553 |
+
pricing_desc = data.get("pricing_model_description", "")
|
| 2554 |
+
|
| 2555 |
+
content = f"""# {partner_program}๋ก {operating_airline} ์์ฝํ๊ธฐ
|
| 2556 |
+
|
| 2557 |
+
**ํํธ๋ ํ๋ก๊ทธ๋จ**: {partner_program}
|
| 2558 |
+
**์ดํญ ํญ๊ณต์ฌ**: {operating_airline}
|
| 2559 |
+
**์ ํด**: {alliance if alliance else "N/A"}
|
| 2560 |
+
**ํฌ์ธํธ ๋จ์**: {currency_name}
|
| 2561 |
+
"""
|
| 2562 |
+
|
| 2563 |
+
if pricing_model:
|
| 2564 |
+
content += f"**๊ฐ๊ฒฉ ์ ์ฑ
**: {pricing_model}"
|
| 2565 |
+
if pricing_desc:
|
| 2566 |
+
content += f" - {pricing_desc}"
|
| 2567 |
+
content += "\n"
|
| 2568 |
+
|
| 2569 |
+
# ์ด์ ์ฃผ์ฒด
|
| 2570 |
+
operator = data.get("partner_program_operator")
|
| 2571 |
+
if operator:
|
| 2572 |
+
content += f"**ํ๋ก๊ทธ๋จ ์ด์**: {operator}\n"
|
| 2573 |
+
|
| 2574 |
+
# ์ ํ์ฌํญ
|
| 2575 |
+
restrictions = data.get("restrictions", [])
|
| 2576 |
+
if restrictions:
|
| 2577 |
+
content += "\n## โ ๏ธ ์์ฝ ์ ํ์ฌํญ\n"
|
| 2578 |
+
for restriction in restrictions[:10]:
|
| 2579 |
+
if isinstance(restriction, dict):
|
| 2580 |
+
r_type = restriction.get("restriction_type", "")
|
| 2581 |
+
desc = restriction.get("description", "")
|
| 2582 |
+
content += f"- **{r_type}**: {desc}\n"
|
| 2583 |
+
else:
|
| 2584 |
+
content += f"- {restriction}\n"
|
| 2585 |
+
|
| 2586 |
+
# ์์ฝ ๊ฐ๋ฅ ํญ๋ชฉ
|
| 2587 |
+
available = data.get("available_booking_classes", [])
|
| 2588 |
+
if available:
|
| 2589 |
+
content += "\n## โ
์์ฝ ๊ฐ๋ฅ ํด๋์ค\n"
|
| 2590 |
+
for cls in available[:5]:
|
| 2591 |
+
if isinstance(cls, dict):
|
| 2592 |
+
cabin = cls.get("cabin_class", "")
|
| 2593 |
+
is_available = cls.get("available", True)
|
| 2594 |
+
status = "โ
๊ฐ๋ฅ" if is_available else "โ ๋ถ๊ฐ"
|
| 2595 |
+
content += f"- {cabin}: {status}\n"
|
| 2596 |
+
|
| 2597 |
+
content += DISCLAIMER_SHORT
|
| 2598 |
+
|
| 2599 |
+
if len(content) > 100:
|
| 2600 |
+
chunks.append({
|
| 2601 |
+
"content": content.strip(),
|
| 2602 |
+
"metadata": {
|
| 2603 |
+
"type": "airline_partnership",
|
| 2604 |
+
"partner_program": partner_program,
|
| 2605 |
+
"operating_airline": operating_airline,
|
| 2606 |
+
"alliance": alliance
|
| 2607 |
+
}
|
| 2608 |
+
})
|
| 2609 |
+
|
| 2610 |
+
return chunks
|
| 2611 |
+
|
| 2612 |
+
|
| 2613 |
# ===========================================================================
|
| 2614 |
# ํธ๋ค๋ฌ: ํญ๊ณต ํ๋ก๊ทธ๋จ (Phase 2)
|
| 2615 |
# ===========================================================================
|
|
|
|
| 4651 |
# ===========================================================================
|
| 4652 |
|
| 4653 |
def handle_award_chart_entries(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 4654 |
+
"""award_chart_entries ์ฒ๋ฆฌ - ์ด์๋ ์ฐจํธ ์์ธ ํญ๋ชฉ
|
| 4655 |
+
|
| 4656 |
+
๋ ๊ฐ์ง ๊ตฌ์กฐ ์ง์:
|
| 4657 |
+
1) Flat ๊ตฌ์กฐ: [{"region": "...", "route_type": "...", "economy_low": ...}, ...]
|
| 4658 |
+
2) Nested ๊ตฌ์กฐ: [{"region": "...", "routes": [{"route": "...", ...}]}, ...]
|
| 4659 |
+
"""
|
| 4660 |
chunks = []
|
| 4661 |
if not data:
|
| 4662 |
return chunks
|
|
|
|
| 4664 |
items = data if isinstance(data, list) else [data]
|
| 4665 |
airline = context.get("chain", "UNKNOWN")
|
| 4666 |
|
| 4667 |
+
# ๊ตฌ์กฐ ํ์ง: ์ฒซ ๋ฒ์งธ ํญ๋ชฉ์ 'routes' ํค๊ฐ ์์ผ๋ฉด ์ค์ฒฉ ๊ตฌ์กฐ
|
| 4668 |
+
is_nested = any(isinstance(item, dict) and "routes" in item for item in items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4669 |
|
| 4670 |
+
if is_nested:
|
| 4671 |
+
# === ์ค์ฒฉ ๊ตฌ์กฐ ์ฒ๋ฆฌ (Flying Blue ์คํ์ผ) ===
|
| 4672 |
+
for region_data in items:
|
| 4673 |
+
if not isinstance(region_data, dict):
|
| 4674 |
+
continue
|
|
|
|
|
|
|
| 4675 |
|
| 4676 |
+
region = region_data.get("region", "OTHER")
|
| 4677 |
+
region_name = region_data.get("region_name", region)
|
| 4678 |
+
routes = region_data.get("routes", [])
|
| 4679 |
|
| 4680 |
+
if not routes:
|
| 4681 |
+
continue
|
|
|
|
|
|
|
|
|
|
| 4682 |
|
| 4683 |
+
content = f"# {airline} ์ด์๋ ์ฐจํธ - {region_name}\n\n"
|
| 4684 |
+
content += "| ๋
ธ์ | ์ถ๋ฐ | ๋์ฐฉ | ์ด์ฝ๋
ธ๋ฏธ | ๋น์ฆ๋์ค | ๋น๊ณ |\n"
|
| 4685 |
+
content += "|------|------|------|----------|----------|------|\n"
|
|
|
|
| 4686 |
|
| 4687 |
+
for route in routes[:50]: # ์ง์ญ๋น ์ต๋ 50๊ฐ ๋
ธ์
|
| 4688 |
+
if not isinstance(route, dict):
|
| 4689 |
+
continue
|
| 4690 |
+
|
| 4691 |
+
route_code = route.get("route", "")
|
| 4692 |
+
origin_name = route.get("origin_name", route.get("origin", ""))
|
| 4693 |
+
dest_name = route.get("destination_name", route.get("destination", ""))
|
| 4694 |
+
|
| 4695 |
+
# ์ด์ฝ๋
ธ๋ฏธ/๋น์ฆ๋์ค ํฌ์ธํธ
|
| 4696 |
+
eco_low = route.get("economy_low")
|
| 4697 |
+
eco_high = route.get("economy_high")
|
| 4698 |
+
biz_low = route.get("business_low")
|
| 4699 |
+
biz_high = route.get("business_high")
|
| 4700 |
+
|
| 4701 |
+
eco_str = f"{eco_low:,}" if eco_low else "N/A"
|
| 4702 |
+
if eco_high and eco_high != eco_low:
|
| 4703 |
+
eco_str += f"~{eco_high:,}"
|
| 4704 |
+
|
| 4705 |
+
if biz_low:
|
| 4706 |
+
biz_str = f"{biz_low:,}"
|
| 4707 |
+
if biz_high and biz_high != biz_low:
|
| 4708 |
+
biz_str += f"~{biz_high:,}"
|
| 4709 |
+
else:
|
| 4710 |
+
biz_str = "N/A"
|
| 4711 |
+
|
| 4712 |
+
notes = route.get("notes", "") or ""
|
| 4713 |
+
|
| 4714 |
+
content += f"| {route_code} | {origin_name} | {dest_name} | {eco_str} | {biz_str} | {notes} |\n"
|
| 4715 |
|
| 4716 |
+
content += DISCLAIMER_SHORT
|
|
|
|
|
|
|
|
|
|
| 4717 |
|
| 4718 |
+
if len(content) > 150:
|
| 4719 |
+
chunks.append({
|
| 4720 |
+
"content": content.strip(),
|
| 4721 |
+
"metadata": {
|
| 4722 |
+
"type": "award_chart_entries",
|
| 4723 |
+
"airline": airline,
|
| 4724 |
+
"region": region,
|
| 4725 |
+
"region_name": region_name
|
| 4726 |
+
}
|
| 4727 |
+
})
|
| 4728 |
+
else:
|
| 4729 |
+
# === ๊ธฐ์กด Flat ๊ตฌ์กฐ ์ฒ๋ฆฌ ===
|
| 4730 |
+
region_entries = {}
|
| 4731 |
+
for item in items:
|
| 4732 |
+
region = item.get("region", "OTHER")
|
| 4733 |
+
if region not in region_entries:
|
| 4734 |
+
region_entries[region] = []
|
| 4735 |
+
region_entries[region].append(item)
|
| 4736 |
+
|
| 4737 |
+
for region, entries in region_entries.items():
|
| 4738 |
+
content = f"# {airline} ์ด์๋ ์ฐจํธ - {region}\n\n"
|
| 4739 |
|
| 4740 |
+
for entry in entries:
|
| 4741 |
+
route_type = entry.get("route_type", "")
|
| 4742 |
+
trip_type = entry.get("trip_type", "")
|
| 4743 |
+
trip_label = "์๋ณต" if trip_type == "ROUND_TRIP" else "ํธ๋"
|
| 4744 |
+
|
| 4745 |
+
content += f"## {route_type} ({trip_label})\n"
|
| 4746 |
+
|
| 4747 |
+
# ์ข์ ๋ฑ๊ธ๋ณ ๋ง์ผ๋ฆฌ์ง
|
| 4748 |
+
economy_low = entry.get("economy_low")
|
| 4749 |
+
economy_high = entry.get("economy_high")
|
| 4750 |
+
if economy_low or economy_high:
|
| 4751 |
+
content += f"- **์ผ๋ฐ์**: {economy_low:,}~{economy_high:,} ๋ง์ผ\n"
|
| 4752 |
+
|
| 4753 |
+
premium_economy_low = entry.get("premium_economy_low")
|
| 4754 |
+
premium_economy_high = entry.get("premium_economy_high")
|
| 4755 |
+
if premium_economy_low or premium_economy_high:
|
| 4756 |
+
content += f"- **ํ๋ฆฌ๋ฏธ์ ์ด์ฝ๋
ธ๋ฏธ**: {premium_economy_low:,}~{premium_economy_high:,} ๋ง์ผ\n"
|
| 4757 |
+
|
| 4758 |
+
business_low = entry.get("business_low")
|
| 4759 |
+
business_high = entry.get("business_high")
|
| 4760 |
+
if business_low or business_high:
|
| 4761 |
+
content += f"- **๋น์ฆ๋์ค/ํ๋ ์คํฐ์ง**: {business_low:,}~{business_high:,} ๋ง์ผ\n"
|
| 4762 |
+
|
| 4763 |
+
first_low = entry.get("first_low")
|
| 4764 |
+
first_high = entry.get("first_high")
|
| 4765 |
+
if first_low or first_high:
|
| 4766 |
+
content += f"- **์ผ๋ฑ์**: {first_low:,}~{first_high:,} ๋ง์ผ\n"
|
| 4767 |
+
|
| 4768 |
+
notes = entry.get("notes", "")
|
| 4769 |
+
if notes:
|
| 4770 |
+
content += f"- ์ฐธ๊ณ : {notes}\n"
|
| 4771 |
+
|
| 4772 |
+
content += "\n"
|
| 4773 |
+
|
| 4774 |
+
content += DISCLAIMER_SHORT
|
| 4775 |
+
|
| 4776 |
+
if len(content) > 100:
|
| 4777 |
+
chunks.append({
|
| 4778 |
+
"content": content.strip(),
|
| 4779 |
+
"metadata": {
|
| 4780 |
+
"type": "award_chart_entries",
|
| 4781 |
+
"airline": airline,
|
| 4782 |
+
"region": region
|
| 4783 |
+
}
|
| 4784 |
+
})
|
| 4785 |
|
| 4786 |
return chunks
|
| 4787 |
|
|
|
|
| 5058 |
|
| 5059 |
|
| 5060 |
def handle_usage_recommendations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 5061 |
+
"""usage_recommendations ์ฒ๋ฆฌ - ๋ง์ผ๋ฆฌ์ง ์ฌ์ฉ ๊ถ์ฅ์ฌํญ
|
| 5062 |
+
|
| 5063 |
+
์ธ ๊ฐ์ง ๊ตฌ์กฐ ์ง์:
|
| 5064 |
+
1) ์ต์์ List ๊ตฌ์กฐ: [{"category": "...", "routes": [...]}]
|
| 5065 |
+
2) Dict ๊ตฌ์กฐ (๊ฐ์ด dict): {"highly_recommended": {"description": "...", "routes": [...]}}
|
| 5066 |
+
3) Dict ๊ตฌ์กฐ (๊ฐ์ด list): {"highly_recommended": [{route_data...}], "recommended": [...]}
|
| 5067 |
+
"""
|
| 5068 |
chunks = []
|
| 5069 |
if not data:
|
| 5070 |
return chunks
|
|
|
|
| 5073 |
|
| 5074 |
content = f"# {airline} ๋ง์ผ๋ฆฌ์ง ์ฌ์ฉ ๊ถ์ฅ์ฌํญ\n\n"
|
| 5075 |
|
| 5076 |
+
# ์ต์์๊ฐ ๋ฆฌ์คํธ์ธ ๊ฒฝ์ฐ
|
| 5077 |
+
if isinstance(data, list):
|
| 5078 |
+
for idx, item in enumerate(data[:20]):
|
| 5079 |
+
if isinstance(item, dict):
|
| 5080 |
+
category = item.get("category", item.get("recommendation_type", f"ํญ๋ชฉ {idx+1}"))
|
| 5081 |
+
desc = item.get("description", "")
|
| 5082 |
+
routes = item.get("routes", [])
|
| 5083 |
+
|
| 5084 |
+
content += f"## {category}\n"
|
| 5085 |
+
if desc:
|
| 5086 |
+
content += f"{desc}\n\n"
|
| 5087 |
+
|
| 5088 |
+
for route in routes[:10]:
|
| 5089 |
+
if isinstance(route, dict):
|
| 5090 |
+
r_name = route.get("route", route.get("route_code", ""))
|
| 5091 |
+
cabin = route.get("cabin_class", "")
|
| 5092 |
+
season = route.get("recommended_season", "")
|
| 5093 |
+
eff_range = route.get("efficiency_range", route.get("efficiency", ""))
|
| 5094 |
+
content += f"- **{r_name}** ({cabin}"
|
| 5095 |
+
if season:
|
| 5096 |
+
content += f", {season}"
|
| 5097 |
+
content += f"): {eff_range}\n"
|
| 5098 |
+
else:
|
| 5099 |
+
content += f"- {route}\n"
|
| 5100 |
+
content += "\n"
|
| 5101 |
+
else:
|
| 5102 |
+
# ๋จ์ ๋ฌธ์์ด
|
| 5103 |
+
content += f"- {item}\n"
|
|
|
|
| 5104 |
|
| 5105 |
+
# ๋์
๋๋ฆฌ ๊ตฌ์กฐ ์ฒ๋ฆฌ
|
| 5106 |
+
elif isinstance(data, dict):
|
| 5107 |
+
categories = [
|
| 5108 |
+
("highly_recommended", "๊ฐ๋ ฅ ์ถ์ฒ", 5),
|
| 5109 |
+
("recommended", "์ถ์ฒ", 3),
|
| 5110 |
+
("not_recommended", "๋น์ถ์ฒ", 1),
|
| 5111 |
+
("booking_strategy", "์์ฝ ์ ๋ต", 4),
|
| 5112 |
+
]
|
| 5113 |
+
|
| 5114 |
+
for key, default_title, _ in categories:
|
| 5115 |
+
category_data = data.get(key)
|
| 5116 |
+
if not category_data:
|
| 5117 |
+
continue
|
| 5118 |
+
|
| 5119 |
+
# ๊ฐ์ด ๋์
๋๋ฆฌ์ธ ๊ฒฝ์ฐ (๊ธฐ์กด ๊ตฌ์กฐ)
|
| 5120 |
+
if isinstance(category_data, dict):
|
| 5121 |
+
desc = category_data.get("description", default_title)
|
| 5122 |
+
content += f"## {desc}\n"
|
| 5123 |
+
routes = category_data.get("routes", [])
|
| 5124 |
+
for route in routes[:10]:
|
| 5125 |
+
if isinstance(route, dict):
|
| 5126 |
+
r_name = route.get("route", "")
|
| 5127 |
+
cabin = route.get("cabin_class", "")
|
| 5128 |
+
season = route.get("recommended_season", "")
|
| 5129 |
+
eff_range = route.get("efficiency_range", "")
|
| 5130 |
+
if key == "not_recommended":
|
| 5131 |
+
content += f"- **{r_name}** ({cabin}): {eff_range}์/๋ง์ผ\n"
|
| 5132 |
+
else:
|
| 5133 |
+
content += f"- **{r_name}** ({cabin}, {season}): {eff_range}์/๋ง์ผ\n"
|
| 5134 |
+
content += "\n"
|
| 5135 |
+
|
| 5136 |
+
# ๊ฐ์ด ๋ฆฌ์คํธ์ธ ๊ฒฝ์ฐ (Aeroplan ๊ตฌ์กฐ)
|
| 5137 |
+
elif isinstance(category_data, list):
|
| 5138 |
+
content += f"## {default_title}\n"
|
| 5139 |
+
for item in category_data[:10]:
|
| 5140 |
+
if isinstance(item, dict):
|
| 5141 |
+
# route_category ๋๋ route
|
| 5142 |
+
route_cat = item.get("route_category", item.get("route", ""))
|
| 5143 |
+
cabin = item.get("cabin_class", "")
|
| 5144 |
+
points = item.get("points_required", "")
|
| 5145 |
+
recommendation = item.get("recommendation", item.get("reason", ""))
|
| 5146 |
+
title = item.get("title", "")
|
| 5147 |
+
desc = item.get("description", "")
|
| 5148 |
+
|
| 5149 |
+
if title: # booking_strategy ํ์
|
| 5150 |
+
content += f"- **{title}**: {desc}\n"
|
| 5151 |
+
elif route_cat:
|
| 5152 |
+
content += f"- **{route_cat}** ({cabin}"
|
| 5153 |
+
if points:
|
| 5154 |
+
# points๊ฐ ์ซ์์ธ์ง ํ์ธ ํ ์ฒ ๋จ์ ๊ตฌ๋ถ์ ์ ์ฉ
|
| 5155 |
+
if isinstance(points, (int, float)):
|
| 5156 |
+
content += f", {points:,}pts"
|
| 5157 |
+
else:
|
| 5158 |
+
content += f", {points}pts"
|
| 5159 |
+
content += f"): {recommendation}\n"
|
| 5160 |
+
else:
|
| 5161 |
+
content += f"- {item}\n"
|
| 5162 |
+
content += "\n"
|
| 5163 |
|
| 5164 |
content += DISCLAIMER_SHORT
|
| 5165 |
|
| 5166 |
+
if len(content) > 150:
|
| 5167 |
chunks.append({
|
| 5168 |
"content": content.strip(),
|
| 5169 |
"metadata": {
|
|
|
|
| 5967 |
return chunks
|
| 5968 |
|
| 5969 |
|
| 5970 |
+
|
| 5971 |
+
# ===========================================================================
|
| 5972 |
+
# ํธ๋ค๋ฌ: ์๋ณ ๊ฐ๊ฒฉ ์์ธ (ํ๊ธ/ํฌ์ธํธ)
|
| 5973 |
+
# ===========================================================================
|
| 5974 |
+
|
| 5975 |
+
def handle_monthly_prices_detailed(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 5976 |
+
"""monthly_cash_prices_detailed, monthly_points_prices_detailed ์ฒ๋ฆฌ
|
| 5977 |
+
|
| 5978 |
+
์๋ณ ๋ ์ง๋ณ ์์ธ ๊ฐ๊ฒฉ ๋ฐ์ดํฐ๋ฅผ ์์ฆ๋ณ๋ก ์์ฝํ์ฌ ์ฒญํฌ ์์ฑ
|
| 5979 |
+
"""
|
| 5980 |
+
chunks = []
|
| 5981 |
+
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 5982 |
+
|
| 5983 |
+
if not isinstance(data, dict):
|
| 5984 |
+
return chunks
|
| 5985 |
+
|
| 5986 |
+
# ๊ฐ๊ฒฉ ์ ํ ํ๋จ (ํฌ์ธํธ vs ํ๊ธ)
|
| 5987 |
+
is_points = any("points" in str(list(v[0].keys()) if isinstance(v, list) and v else [])
|
| 5988 |
+
for v in data.values() if v)
|
| 5989 |
+
|
| 5990 |
+
price_type = "ํฌ์ธํธ" if is_points else "ํ๊ธ"
|
| 5991 |
+
content_type = "monthly_points_prices" if is_points else "monthly_cash_prices"
|
| 5992 |
+
|
| 5993 |
+
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
|
| 5994 |
+
content += f"์๋ณ {price_type} ๊ฐ๊ฒฉ ์์ธ:\n\n"
|
| 5995 |
+
|
| 5996 |
+
# ์๋ณ๋ก ํต๊ณ ๊ณ์ฐ
|
| 5997 |
+
for month_key, prices_list in list(data.items())[:12]:
|
| 5998 |
+
if not isinstance(prices_list, list) or not prices_list:
|
| 5999 |
+
continue
|
| 6000 |
+
|
| 6001 |
+
# ์ ์ด๋ฆ ํฌ๋งท
|
| 6002 |
+
month_name = month_key.replace("_", " ").title()
|
| 6003 |
+
|
| 6004 |
+
# ๊ฐ๊ฒฉ/ํฌ์ธํธ ์ถ์ถ
|
| 6005 |
+
if is_points:
|
| 6006 |
+
values = [p.get("points", 0) for p in prices_list if isinstance(p, dict) and p.get("points")]
|
| 6007 |
+
else:
|
| 6008 |
+
values = [p.get("price_krw", 0) for p in prices_list if isinstance(p, dict) and p.get("price_krw")]
|
| 6009 |
+
|
| 6010 |
+
if not values:
|
| 6011 |
+
continue
|
| 6012 |
+
|
| 6013 |
+
min_val = min(values)
|
| 6014 |
+
max_val = max(values)
|
| 6015 |
+
|
| 6016 |
+
if is_points:
|
| 6017 |
+
content += f"โข {month_name}: {min_val:,}P ~ {max_val:,}P\n"
|
| 6018 |
+
else:
|
| 6019 |
+
content += f"โข {month_name}: โฉ{min_val:,} ~ โฉ{max_val:,}\n"
|
| 6020 |
+
|
| 6021 |
+
content += DISCLAIMER_PRICE
|
| 6022 |
+
|
| 6023 |
+
if len(content) > 150:
|
| 6024 |
+
chunks.append({
|
| 6025 |
+
"content": content.strip(),
|
| 6026 |
+
"metadata": {
|
| 6027 |
+
"type": content_type,
|
| 6028 |
+
"hotel_name": hotel_name
|
| 6029 |
+
}
|
| 6030 |
+
})
|
| 6031 |
+
|
| 6032 |
+
return chunks
|
| 6033 |
+
|
| 6034 |
+
|
| 6035 |
+
# ===========================================================================
|
| 6036 |
+
# ํธ๋ค๋ฌ: ๊ฐ์ค ์ ํ๋ณ ์๊ธ
|
| 6037 |
+
# ===========================================================================
|
| 6038 |
+
|
| 6039 |
+
def handle_room_rates_by_type(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 6040 |
+
"""room_rates_by_type ์ฒ๋ฆฌ - ๊ฐ์ค ์ ํ๋ณ ๋ฉค๋ฒ ์๊ธ"""
|
| 6041 |
+
chunks = []
|
| 6042 |
+
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
|
| 6043 |
+
|
| 6044 |
+
if not isinstance(data, list) or not data:
|
| 6045 |
+
return chunks
|
| 6046 |
+
|
| 6047 |
+
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
|
| 6048 |
+
content += "๊ฐ์ค ์ ํ๋ณ ๋ฉค๋ฒ ์๊ธ:\n\n"
|
| 6049 |
+
|
| 6050 |
+
for room in data[:15]:
|
| 6051 |
+
if not isinstance(room, dict):
|
| 6052 |
+
continue
|
| 6053 |
+
|
| 6054 |
+
room_type = room.get("room_type", "N/A")
|
| 6055 |
+
rate = room.get("member_rate_krw", room.get("rate", "N/A"))
|
| 6056 |
+
notes = room.get("notes", "")
|
| 6057 |
+
|
| 6058 |
+
if isinstance(rate, (int, float)):
|
| 6059 |
+
content += f"โข {room_type}: โฉ{rate:,}"
|
| 6060 |
+
else:
|
| 6061 |
+
content += f"โข {room_type}: {rate}"
|
| 6062 |
+
|
| 6063 |
+
if notes:
|
| 6064 |
+
content += f" ({notes})"
|
| 6065 |
+
content += "\n"
|
| 6066 |
+
|
| 6067 |
+
content += DISCLAIMER_PRICE
|
| 6068 |
+
|
| 6069 |
+
chunks.append({
|
| 6070 |
+
"content": content.strip(),
|
| 6071 |
+
"metadata": {
|
| 6072 |
+
"type": "room_rates_by_type",
|
| 6073 |
+
"hotel_name": hotel_name
|
| 6074 |
+
}
|
| 6075 |
+
})
|
| 6076 |
+
|
| 6077 |
+
return chunks
|
| 6078 |
+
|
| 6079 |
+
|
| 6080 |
# ===========================================================================
|
| 6081 |
# ํค-ํธ๋ค๋ฌ ๋งคํ
|
| 6082 |
# ===========================================================================
|
|
|
|
| 6142 |
# ํธํ
์์ค
|
| 6143 |
"hotel_facilities": handle_hotel_facilities,
|
| 6144 |
"other_facilities": handle_hotel_facilities,
|
| 6145 |
+
|
| 6146 |
+
# ์ด๊ทธ์ ํํฐ๋ธ ๋ผ์ด์ง - hotel_facilities ํธ๋ค๋ฌ๋ก ํตํฉ ์ฒ๋ฆฌ
|
| 6147 |
+
"executive_lounge": handle_hotel_facilities,
|
| 6148 |
+
|
| 6149 |
+
# ์ฐ๋์ค ์์ค - hotel_facilities ํธ๋ค๋ฌ๋ก ํตํฉ ์ฒ๋ฆฌ
|
| 6150 |
+
"pulse8_wellness": handle_hotel_facilities,
|
| 6151 |
+
"wellness_facility": handle_hotel_facilities,
|
| 6152 |
+
"spa_wellness": handle_hotel_facilities,
|
| 6153 |
+
|
| 6154 |
+
# ํธํ
์ด๋ฉ๋ํฐ ์์ฝ - hotel_facilities ํธ๋ค๋ฌ๋ก ํตํฉ ์ฒ๋ฆฌ
|
| 6155 |
+
"hotel_amenities_summary": handle_hotel_facilities,
|
| 6156 |
+
"amenities_summary": handle_hotel_facilities,
|
| 6157 |
+
|
| 6158 |
+
# ์๋ณ ๊ฐ๊ฒฉ ์์ธ (๊ณ ์ ๊ตฌ์กฐ๋ก ๋ณ๋ ํธ๋ค๋ฌ ์ ์ง)
|
| 6159 |
+
"monthly_cash_prices_detailed": handle_monthly_prices_detailed,
|
| 6160 |
+
"monthly_points_prices_detailed": handle_monthly_prices_detailed,
|
| 6161 |
+
|
| 6162 |
+
# ๊ฐ์ค ์ ํ๋ณ ์๊ธ (๊ณ ์ ๊ตฌ์กฐ๋ก ๋ณ๋ ํธ๋ค๋ฌ ์ ์ง)
|
| 6163 |
+
"room_rates_by_type": handle_room_rates_by_type,
|
| 6164 |
|
| 6165 |
# ๊ฐ์ค ์ ํ
|
| 6166 |
"room_types": handle_room_types,
|
|
|
|
| 6240 |
# --- ํญ๊ณต ํ๋ก๊ทธ๋จ (Phase 2) ---
|
| 6241 |
"airline_programs": handle_airline_programs,
|
| 6242 |
"airline_program": handle_airline_programs,
|
| 6243 |
+
"airline_partnership": handle_airline_partnership, # Flying Blue โ Korean Air ๋ฑ
|
| 6244 |
"airline_tiers": handle_airline_tiers,
|
| 6245 |
"airline_tier": handle_airline_tiers,
|
| 6246 |
"award_charts": handle_award_charts,
|
| 6247 |
"award_chart": handle_award_charts,
|
| 6248 |
"airline_earning_rules": handle_airline_earning_rules,
|
| 6249 |
+
|
| 6250 |
|
| 6251 |
# --- ํญ๊ณต ์ด์ (Airline Fares) ---
|
| 6252 |
"airline_fare_tables": handle_airline_fare_tables,
|
|
|
|
| 6371 |
"mileage_purchase_programs": handle_mileage_purchase_programs,
|
| 6372 |
"price_summary": handle_mileage_price_summary,
|
| 6373 |
"insights": handle_mileage_insights,
|
| 6374 |
+
|
| 6375 |
+
# --- ๋ฏธ์ฒ๋ฆฌ ํค๋ค์ ์ ์ ํ ํธ๋ค๋ฌ์ ๋งคํ ---
|
| 6376 |
+
"raw_pricing_data": handle_pricing, # ClassicTravel ์์ ๊ฐ๊ฒฉ ๋ฐ์ดํฐ
|
| 6377 |
+
"rate_descriptions": handle_benefits, # ClassicTravel ์๊ธ ์ค๋ช
|
| 6378 |
+
"brunch_options": handle_breakfast, # ClassicTravel ๋ธ๋ฐ์น ์ต์
|
| 6379 |
+
"regional_info": handle_hotel_facilities, # ClassicTravel ์ง์ญ ์ ๋ณด
|
| 6380 |
+
"breakfast_benefit_participating_brands": handle_benefits, # GHA Discovery ์กฐ์ ํํ ์ฐธ์ฌ ๋ธ๋๋
|
| 6381 |
+
"direct_channels": handle_benefits, # GHA Discovery ์ง์ ์ฑ๋
|
| 6382 |
+
"irregular_prices_observed": handle_pricing, # ํญ๊ณต์ฌ ๋ถ๊ท์น ๊ฐ๊ฒฉ ๊ด์ฐฐ
|
| 6383 |
+
"route_availability_patterns": handle_airline_programs, # ํญ๊ณต์ฌ ๋
ธ์ ๊ฐ์ฉ์ฑ ํจํด
|
| 6384 |
+
"booking_policies": handle_benefits, # WhataHotel ์์ฝ ์ ์ฑ
(์๋น์ค ์์๋ฃ, ๊ฒฐ์ ์ ์ฑ
, ์ทจ์ ์ ์ฑ
)
|
| 6385 |
+
|
| 6386 |
+
# --- AMEX Platinum ์นด๋ ์ ์ฉ ํค ๋งคํ ---
|
| 6387 |
+
"travel_programs": handle_benefits, # FHR, THC ๋ฑ ์ฌํ ํ๋ก๊ทธ๋จ
|
| 6388 |
+
"statement_credit_timing": handle_benefits, # ์คํ
์ดํธ๋จผํธ ํฌ๋ ๋ง ์ฒ๋ฆฌ ์๊ฐ
|
| 6389 |
+
"statement_credit_troubleshooting": handle_benefits, # ํฌ๋ ๋ง ๋ฌธ์ ํด๊ฒฐ ํ
|
| 6390 |
+
"tier_eligibility_comparison": handle_benefits, # ๋ฑ๊ธ ์๊ฒฉ ๋น๊ตํ
|
| 6391 |
+
"benefit_exclusions": handle_benefits, # ํํ ์ ์ธ ํญ๋ชฉ
|
| 6392 |
+
"benefit_calendar": handle_benefits, # ํํ ์ผ์ (์๊ฐ/๋ถ๊ธฐ/์ฐ๊ฐ)
|
| 6393 |
+
"contact_information": handle_benefits, # ์ฐ๋ฝ์ฒ ์ ๋ณด
|
| 6394 |
+
"important_notices": handle_benefits, # ์ค์ ๊ณต์ง์ฌํญ
|
| 6395 |
+
"benefit_value_summary": handle_benefits, # ํํ ๊ฐ์น ์์ฝ
|
| 6396 |
+
|
| 6397 |
+
# --- Chase ์นด๋ ๊ด๋ จ ํค ๋งคํ ---
|
| 6398 |
+
"lounge_access_details": handle_benefits, # ๊ณตํญ ๋ผ์ด์ง ์ ๊ทผ ์ ๋ณด
|
| 6399 |
+
"spending_tier_benefits": handle_benefits, # ์ง์ถ ๋ฑ๊ธ๋ณ ํํ
|
| 6400 |
+
"balance_transfer_conditions": handle_benefits, # ์์ก ์ด์ฒด ์กฐ๊ฑด
|
| 6401 |
+
"chase_pay_over_time": handle_benefits, # Chase ํ ๋ถ ๊ฒฐ์ ์ต์
|
| 6402 |
+
"state_notices": handle_benefits, # ์ฃผ๋ณ ๊ณ ์ง์ฌํญ
|
| 6403 |
+
"mileageplus_integration": handle_benefits, # MileagePlus ์ฐ๋ ์ ๋ณด
|
| 6404 |
+
"privacy_legal": handle_benefits, # ๊ฐ์ธ์ ๋ณด/๋ฒ์ ๊ณ ์ง
|
| 6405 |
+
"concierge_services": handle_benefits, # ์ปจ์์ด์ง ์๋น์ค
|
| 6406 |
+
"other_info": handle_benefits, # ๊ธฐํ ์ ๋ณด
|
| 6407 |
+
|
| 6408 |
+
# --- ์ถ๊ฐ ๋ฏธ์ฒ๋ฆฌ ํค ๋งคํ (credit card ๊ด๋ จ) ---
|
| 6409 |
+
"ihg_membership_earning_rules": handle_benefits, # IHG ๋ฉค๋ฒ์ญ ์ ๋ฆฝ ๊ท์น
|
| 6410 |
+
"points_expiration_policy": handle_benefits, # ํฌ์ธํธ ๋ง๋ฃ ์ ์ฑ
|
| 6411 |
+
"terms_summary": handle_benefits, # ์ฝ๊ด ์์ฝ
|
| 6412 |
+
"hyatt_brands_reference": handle_hotel_brands, # Hyatt ๋ธ๋๋ ๋ชฉ๋ก
|
| 6413 |
+
"ihg_program_earning_info": handle_benefits, # IHG ํ๋ก๊ทธ๋จ ์ ๋ฆฝ ์ ๋ณด
|
| 6414 |
+
"related_cards_comparison": handle_benefits, # ๊ด๋ จ ์นด๋ ๋น๊ต
|
| 6415 |
+
"value_analysis": handle_benefits, # ๊ฐ์น ๋ถ์
|
| 6416 |
+
|
| 6417 |
+
# --- Disney ์นด๋ ์ ์ฉ ํค ๋งคํ ---
|
| 6418 |
+
"dining_eligible_restaurants": handle_benefits, # Disney ์๋น ํ ์ธ ๋์ ๋ ์คํ ๋
|
| 6419 |
+
"tour_eligible_experiences": handle_benefits, # Disney ํฌ์ด/์ฒดํ ํ ์ธ ๋์
|
| 6420 |
+
"photo_opportunities": handle_benefits, # Disney ์นด๋ ํฌํ ๊ธฐํ
|
| 6421 |
+
|
| 6422 |
+
# --- IHG/United ์นด๋ ๊ด๋ จ ํค ๋งคํ ---
|
| 6423 |
+
"airline_program_integration": handle_benefits, # ํญ๊ณต์ฌ ํ๋ก๊ทธ๋จ ์ฐ๋
|
| 6424 |
+
|
| 6425 |
+
# --- ์ถ๊ฐ ๋ฏธ์ฒ๋ฆฌ ํค ๋งคํ (Citi/Barclays/BoA ์นด๋) ---
|
| 6426 |
+
"aadvantage_program_tiers": handle_benefits, # AAdvantage ํ๋ก๊ทธ๋จ ๋ฑ๊ธ
|
| 6427 |
+
"american_eagle_operators": handle_benefits, # American Eagle ์ดํญ์ฌ
|
| 6428 |
+
"additional_benefits": handle_benefits, # ์ถ๊ฐ ํํ
|
| 6429 |
+
"insurance_benefits": handle_benefits, # ๋ณดํ ํํ
|
| 6430 |
+
"security_features": handle_benefits, # ๋ณด์ ๊ธฐ๋ฅ
|
| 6431 |
+
"entertainment_program": handle_benefits, # ์ํฐํ
์ธ๋จผํธ ํ๋ก๊ทธ๋จ
|
| 6432 |
+
"annual_benefit_value_summary": handle_benefits, # ์ฐ๊ฐ ํํ ๊ฐ์น ์์ฝ
|
| 6433 |
+
"card_benefits": handle_benefits, # ์นด๋ ํํ (์ต์์ ํค)
|
| 6434 |
+
"eligibility_requirements": handle_benefits, # ์๊ฒฉ ์๊ฑด
|
| 6435 |
+
"balance_transfer_terms": handle_benefits, # ์์ก ์ด์ฒด ์กฐ๊ฑด
|
| 6436 |
+
"cash_equivalent_transactions": handle_benefits, # ํ๊ธ ๋ฑ๊ฐ ๊ฑฐ๋
|
| 6437 |
+
"points_forfeiture_conditions": handle_benefits, # ํฌ์ธํธ ๋ชฐ์ ์กฐ๊ฑด
|
| 6438 |
+
"trueblue_integration": handle_benefits, # TrueBlue ์ฐ๋
|
| 6439 |
+
"trueblue_points_important_info": handle_benefits, # TrueBlue ํฌ์ธํธ ์ค์ ์ ๋ณด
|
| 6440 |
+
"additional_info": handle_benefits, # ์ถ๊ฐ ์ ๋ณด
|
| 6441 |
+
"partnership_programs": handle_benefits, # ํํธ๋์ญ ํ๋ก๊ทธ๋จ
|
| 6442 |
}
|
| 6443 |
|
| 6444 |
# ์ค์ฒฉ ํค ์ง์ (์: facts.pricing_analysis)
|
|
|
|
| 6501 |
"collection_date", # ์์ง์ผ ๋ฉํ๋ฐ์ดํฐ
|
| 6502 |
"related_documents", # ๊ด๋ จ ๋ฌธ์ ์ฐธ์กฐ
|
| 6503 |
|
| 6504 |
+
# AMEX ์นด๋ ๊ด๋ จ ์ฆ๊ฑฐ์๋ฃ ๋ฐ ๋ฒ์ ์ ๋ณด (๊ฒ์ ๊ฐ์น ๋ฎ์)
|
| 6505 |
+
"seller_of_travel", # ์ฌํ ํ๋งค์ ๋ฒ์ ์ ๋ณด
|
| 6506 |
+
"statement_credit_evidence", # ์คํ
์ดํธ๋จผํธ ํฌ๋ ๋ง ์ฆ๊ฑฐ
|
| 6507 |
+
"verification_needed_evidence", # ํ์ธ ํ์ ์ฌํญ ์ฆ๊ฑฐ
|
| 6508 |
+
|
| 6509 |
+
# Chase ์นด๋ ๊ด๋ จ ๋ฉํ๋ฐ์ดํฐ (๊ฒ์ ๊ฐ์น ๋ฎ์)
|
| 6510 |
+
"related_links", # ๊ด๋ จ ๋งํฌ (URL ์ฐธ์กฐ์ฉ)
|
| 6511 |
+
"benefit_value_summary_total", # ํํ ๊ฐ์น ์ดํฉ (๋ฉํ๋ฐ์ดํฐ)
|
| 6512 |
+
"spending_tier_120k_additional", # ์ถ๊ฐ ์ง์ถ ๋ฑ๊ธ (์์ธ ๋ด์ฉ ์์)
|
| 6513 |
+
"benefit_value_evidence", # ํํ ๊ฐ์น ์ฆ๊ฑฐ (๋ด๋ถ์ฉ)
|
| 6514 |
+
"legal_notices", # ๋ฒ์ ๊ณ ์ง์ฌํญ
|
| 6515 |
+
|
| 6516 |
# Deprecated ํค (์ฌ์ฉํ์ง ์์)
|
| 6517 |
# "loyalty_programs" (๋ณต์) - ๋จ, ํ์ ํธํ์ฑ ์ํด ํธ๋ค๋ฌ๋ ์ ์ง
|
| 6518 |
}
|
src/auth/asset_parser.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Asset Text Parser
|
| 3 |
+
==================
|
| 4 |
+
|
| 5 |
+
์ฌ์ฉ์๊ฐ ์
๋ ฅํ ํ
์คํธ์์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ์ ๋ณด๋ฅผ ์๋ ์ถ์ถํฉ๋๋ค.
|
| 6 |
+
|
| 7 |
+
์์ ์
๋ ฅ:
|
| 8 |
+
- "๋ํํญ๊ณต 45,000 ๋ง์ผ"
|
| 9 |
+
- "AMEX MR 50000์ "
|
| 10 |
+
- "์ฒด์ด์ค UR 30,000"
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import re
|
| 14 |
+
from typing import Dict, Any, List, Optional
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
from .config import POINT_VALUATIONS
|
| 18 |
+
|
| 19 |
+
# =============================================================================
|
| 20 |
+
# ํ๋ก๊ทธ๋จ ๋ณ์นญ ๋งคํ
|
| 21 |
+
# =============================================================================
|
| 22 |
+
|
| 23 |
+
PROGRAM_ALIASES = {
|
| 24 |
+
# ํ๊ตญ ํญ๊ณต
|
| 25 |
+
"๋ํํญ๊ณต": "KOREAN_AIR",
|
| 26 |
+
"๋ํ": "KOREAN_AIR",
|
| 27 |
+
"kal": "KOREAN_AIR",
|
| 28 |
+
"korean air": "KOREAN_AIR",
|
| 29 |
+
"์ค์นด์ดํจ์ค": "KOREAN_AIR",
|
| 30 |
+
|
| 31 |
+
"์์์๋": "ASIANA",
|
| 32 |
+
"oz": "ASIANA",
|
| 33 |
+
"asiana": "ASIANA",
|
| 34 |
+
|
| 35 |
+
# ๋ฏธ๊ตญ ํญ๊ณต
|
| 36 |
+
"delta": "DELTA_SKYMILES",
|
| 37 |
+
"๋ธํ": "DELTA_SKYMILES",
|
| 38 |
+
"skymiles": "DELTA_SKYMILES",
|
| 39 |
+
|
| 40 |
+
"united": "UNITED_MILEAGEPLUS",
|
| 41 |
+
"์ ๋์ดํฐ๋": "UNITED_MILEAGEPLUS",
|
| 42 |
+
"mileageplus": "UNITED_MILEAGEPLUS",
|
| 43 |
+
|
| 44 |
+
"american": "AA_AADVANTAGE",
|
| 45 |
+
"aa": "AA_AADVANTAGE",
|
| 46 |
+
"์๋ฉ๋ฆฌ์นธ": "AA_AADVANTAGE",
|
| 47 |
+
"aadvantage": "AA_AADVANTAGE",
|
| 48 |
+
|
| 49 |
+
# ์ ์ฉ์นด๋ ํฌ์ธํธ
|
| 50 |
+
"amex": "AMEX_MR",
|
| 51 |
+
"์๋ฉ์ค": "AMEX_MR",
|
| 52 |
+
"mr": "AMEX_MR",
|
| 53 |
+
"membership rewards": "AMEX_MR",
|
| 54 |
+
|
| 55 |
+
"chase": "CHASE_UR",
|
| 56 |
+
"์ฒด์ด์ค": "CHASE_UR",
|
| 57 |
+
"ur": "CHASE_UR",
|
| 58 |
+
"ultimate rewards": "CHASE_UR",
|
| 59 |
+
|
| 60 |
+
"citi": "CITI_TYP",
|
| 61 |
+
"์ํฐ": "CITI_TYP",
|
| 62 |
+
"typ": "CITI_TYP",
|
| 63 |
+
"thankyou": "CITI_TYP",
|
| 64 |
+
|
| 65 |
+
"capital one": "CAPITAL_ONE",
|
| 66 |
+
"์บํผํ์": "CAPITAL_ONE",
|
| 67 |
+
|
| 68 |
+
# ํธํ
ํฌ์ธํธ
|
| 69 |
+
"marriott": "MARRIOTT_BONVOY",
|
| 70 |
+
"๋ฉ๋ฆฌ์ดํธ": "MARRIOTT_BONVOY",
|
| 71 |
+
"๋ณธ๋ณด์ด": "MARRIOTT_BONVOY",
|
| 72 |
+
"bonvoy": "MARRIOTT_BONVOY",
|
| 73 |
+
|
| 74 |
+
"hilton": "HILTON_HONORS",
|
| 75 |
+
"ํํผ": "HILTON_HONORS",
|
| 76 |
+
"honors": "HILTON_HONORS",
|
| 77 |
+
|
| 78 |
+
"ihg": "IHG_REWARDS",
|
| 79 |
+
|
| 80 |
+
"hyatt": "HYATT_WOH",
|
| 81 |
+
"ํ์ํธ": "HYATT_WOH",
|
| 82 |
+
"woh": "HYATT_WOH",
|
| 83 |
+
"world of hyatt": "HYATT_WOH",
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# =============================================================================
|
| 88 |
+
# ํ์ฑ ํจ์
|
| 89 |
+
# =============================================================================
|
| 90 |
+
|
| 91 |
+
def parse_asset_text(text: str) -> List[Dict[str, Any]]:
|
| 92 |
+
"""
|
| 93 |
+
ํ
์คํธ์์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ์ ๋ณด๋ฅผ ์ถ์ถํฉ๋๋ค.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
text: ์ฌ์ฉ์ ์
๋ ฅ ํ
์คํธ
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
์ถ์ถ๋ ์์ฐ ๋ชฉ๋ก [{"program": "...", "amount": ...}, ...]
|
| 100 |
+
"""
|
| 101 |
+
assets = []
|
| 102 |
+
|
| 103 |
+
# ์ค ๋จ์ + ์ผํ ๋จ์๋ก ๋ถ๋ฆฌ
|
| 104 |
+
lines = re.split(r'[\n,]', text)
|
| 105 |
+
|
| 106 |
+
for line in lines:
|
| 107 |
+
line = line.strip()
|
| 108 |
+
if not line:
|
| 109 |
+
continue
|
| 110 |
+
|
| 111 |
+
result = _parse_single_line(line)
|
| 112 |
+
if result:
|
| 113 |
+
# ์ค๋ณต ์ ๊ฑฐ (๊ฐ์ ํ๋ก๊ทธ๋จ์ด๋ฉด ํฉ์ฐ)
|
| 114 |
+
existing = next(
|
| 115 |
+
(a for a in assets if a["program"] == result["program"]),
|
| 116 |
+
None
|
| 117 |
+
)
|
| 118 |
+
if existing:
|
| 119 |
+
existing["amount"] += result["amount"]
|
| 120 |
+
else:
|
| 121 |
+
assets.append(result)
|
| 122 |
+
|
| 123 |
+
return assets
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _parse_single_line(line: str) -> Optional[Dict[str, Any]]:
|
| 127 |
+
"""๋จ์ผ ๋ผ์ธ์์ ํ๋ก๊ทธ๋จ๊ณผ ์๋ ์ถ์ถ."""
|
| 128 |
+
line_lower = line.lower()
|
| 129 |
+
|
| 130 |
+
# ์ซ์ ์ถ์ถ (์ผํ ์ ๊ฑฐ)
|
| 131 |
+
numbers = re.findall(r'[\d,]+', line)
|
| 132 |
+
if not numbers:
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
# ๊ฐ์ฅ ํฐ ์ซ์๋ฅผ amount๋ก ์ฌ์ฉ
|
| 136 |
+
amount = max(int(n.replace(',', '')) for n in numbers if n.replace(',', '').isdigit())
|
| 137 |
+
if amount == 0:
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
# ํ๋ก๊ทธ๋จ ์๋ณ
|
| 141 |
+
program = None
|
| 142 |
+
for alias, prog_id in PROGRAM_ALIASES.items():
|
| 143 |
+
if alias in line_lower:
|
| 144 |
+
program = prog_id
|
| 145 |
+
break
|
| 146 |
+
|
| 147 |
+
if not program:
|
| 148 |
+
# ์ซ์๋ง ์๋ ๊ฒฝ์ฐ - ๋ฌธ๋งฅ์ผ๋ก ์ถ๋ก ๋ถ๊ฐ
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
return {"program": program, "amount": amount}
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def normalize_program_name(name: str) -> Optional[str]:
|
| 155 |
+
"""ํ๋ก๊ทธ๋จ ๋ณ์นญ์ ์ ๊ทํํฉ๋๋ค."""
|
| 156 |
+
name_lower = name.lower().strip()
|
| 157 |
+
return PROGRAM_ALIASES.get(name_lower)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# =============================================================================
|
| 161 |
+
# MCP Tool ํธ๋ค๋ฌ
|
| 162 |
+
# =============================================================================
|
| 163 |
+
|
| 164 |
+
async def handle_parse_asset_text(
|
| 165 |
+
arguments: Dict[str, Any],
|
| 166 |
+
user_id: str
|
| 167 |
+
) -> Dict[str, Any]:
|
| 168 |
+
"""
|
| 169 |
+
user_parse_asset_text Tool ํธ๋ค๋ฌ.
|
| 170 |
+
|
| 171 |
+
์ฌ์ฉ์ ์
๋ ฅ ํ
์คํธ์์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง๋ฅผ ์๋ ์ถ์ถํฉ๋๋ค.
|
| 172 |
+
"""
|
| 173 |
+
text = arguments.get("text", "")
|
| 174 |
+
save_to_profile = arguments.get("save_to_profile", False)
|
| 175 |
+
|
| 176 |
+
if not text:
|
| 177 |
+
return {
|
| 178 |
+
"success": False,
|
| 179 |
+
"error": "ํ์ฑํ ํ
์คํธ๊ฐ ์์ต๋๋ค.",
|
| 180 |
+
"usage": "์: '๋ํํญ๊ณต 45000 ๋ง์ผ, AMEX MR 50000์ '"
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
# ํ์ฑ
|
| 184 |
+
assets = parse_asset_text(text)
|
| 185 |
+
|
| 186 |
+
if not assets:
|
| 187 |
+
return {
|
| 188 |
+
"success": False,
|
| 189 |
+
"error": "์ธ์ ๊ฐ๋ฅํ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ์ ๋ณด๊ฐ ์์ต๋๋ค.",
|
| 190 |
+
"tip": "ํ๋ก๊ทธ๋จ๋ช
๊ณผ ์ซ์๋ฅผ ํจ๊ป ์
๋ ฅํด์ฃผ์ธ์. ์: '๋ํํญ๊ณต 45000'",
|
| 191 |
+
"supported_programs": list(POINT_VALUATIONS.keys())
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
# ํ๋ก๊ทธ๋จ ์ด๋ฆ ๋ณด๊ฐ
|
| 195 |
+
for asset in assets:
|
| 196 |
+
prog_info = POINT_VALUATIONS.get(asset["program"], {})
|
| 197 |
+
asset["program_name"] = prog_info.get("name", asset["program"])
|
| 198 |
+
asset["currency"] = prog_info.get("currency", "USD")
|
| 199 |
+
|
| 200 |
+
result = {
|
| 201 |
+
"success": True,
|
| 202 |
+
"parsed_assets": assets,
|
| 203 |
+
"count": len(assets),
|
| 204 |
+
"parsed_at": datetime.now().isoformat(),
|
| 205 |
+
"tip": "user_get_asset_valuation์ ํธ์ถํ์ฌ ์ด ๊ฐ์น๋ฅผ ๊ณ์ฐํ ์ ์์ต๋๋ค."
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
# ํฅํ: save_to_profile์ด True๋ฉด DB์ ์ ์ฅ
|
| 209 |
+
if save_to_profile:
|
| 210 |
+
result["save_status"] = "not_implemented"
|
| 211 |
+
result["save_note"] = "์์ฐ ์ ์ฅ ๊ธฐ๋ฅ์ ํฅํ ๊ตฌํ ์์ "
|
| 212 |
+
|
| 213 |
+
return result
|
src/auth/config.py
CHANGED
|
@@ -111,3 +111,131 @@ def validate_config():
|
|
| 111 |
)
|
| 112 |
|
| 113 |
logger.info("์ธ์ฆ ์ค์ ๊ฒ์ฆ ์๋ฃ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
)
|
| 112 |
|
| 113 |
logger.info("์ธ์ฆ ์ค์ ๊ฒ์ฆ ์๋ฃ")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# =============================================================================
|
| 117 |
+
# ํฌ์ธํธ ๊ฐ์น ํ๊ฐ (Shadow Valuation)
|
| 118 |
+
# =============================================================================
|
| 119 |
+
|
| 120 |
+
# ๊ธฐ๋ณธ ํฌ์ธํธ ๊ฐ์น (CPP: Cents Per Point)
|
| 121 |
+
# base: ์ผ๋ฐ ์ฌ์ฉ์, premium: ๋น์ฆ๋์ค/ํผ์คํธ ์ ํธ, cashback: ํ๊ธ์ฑ ์ ํธ
|
| 122 |
+
POINT_VALUATIONS = {
|
| 123 |
+
# ์ ์ฉ์นด๋ ํฌ์ธํธ (๋จ์: cents per point)
|
| 124 |
+
"AMEX_MR": {
|
| 125 |
+
"name": "American Express Membership Rewards",
|
| 126 |
+
"currency": "USD",
|
| 127 |
+
"base": 2.0,
|
| 128 |
+
"premium": 2.2,
|
| 129 |
+
"cashback": 0.6,
|
| 130 |
+
},
|
| 131 |
+
"CHASE_UR": {
|
| 132 |
+
"name": "Chase Ultimate Rewards",
|
| 133 |
+
"currency": "USD",
|
| 134 |
+
"base": 2.0,
|
| 135 |
+
"premium": 2.05,
|
| 136 |
+
"cashback": 1.25,
|
| 137 |
+
},
|
| 138 |
+
"CITI_TYP": {
|
| 139 |
+
"name": "Citi ThankYou Points",
|
| 140 |
+
"currency": "USD",
|
| 141 |
+
"base": 1.8,
|
| 142 |
+
"premium": 1.9,
|
| 143 |
+
"cashback": 1.0,
|
| 144 |
+
},
|
| 145 |
+
"CAPITAL_ONE": {
|
| 146 |
+
"name": "Capital One Miles",
|
| 147 |
+
"currency": "USD",
|
| 148 |
+
"base": 1.85,
|
| 149 |
+
"premium": 1.9,
|
| 150 |
+
"cashback": 0.5,
|
| 151 |
+
},
|
| 152 |
+
|
| 153 |
+
# ํญ๊ณต ๋ง์ผ๋ฆฌ์ง - ํ๊ตญ (๋จ์: ์/๋ง์ผ)
|
| 154 |
+
"KOREAN_AIR": {
|
| 155 |
+
"name": "๋ํํญ๊ณต ์ค์นด์ดํจ์ค",
|
| 156 |
+
"currency": "KRW",
|
| 157 |
+
"base": 18,
|
| 158 |
+
"domestic": 12,
|
| 159 |
+
"premium": 25,
|
| 160 |
+
},
|
| 161 |
+
"ASIANA": {
|
| 162 |
+
"name": "์์์๋ ์คํ์ผ๋ผ์ด์ธ์ค",
|
| 163 |
+
"currency": "KRW",
|
| 164 |
+
"base": 17,
|
| 165 |
+
"domestic": 11,
|
| 166 |
+
"premium": 24,
|
| 167 |
+
},
|
| 168 |
+
|
| 169 |
+
# ํญ๊ณต ๋ง์ผ๋ฆฌ์ง - ๋ฏธ๊ตญ (๋จ์: cents per mile)
|
| 170 |
+
"DELTA_SKYMILES": {
|
| 171 |
+
"name": "Delta SkyMiles",
|
| 172 |
+
"currency": "USD",
|
| 173 |
+
"base": 1.2,
|
| 174 |
+
"premium": 1.5,
|
| 175 |
+
"cashback": 1.0,
|
| 176 |
+
},
|
| 177 |
+
"UNITED_MILEAGEPLUS": {
|
| 178 |
+
"name": "United MileagePlus",
|
| 179 |
+
"currency": "USD",
|
| 180 |
+
"base": 1.3,
|
| 181 |
+
"premium": 1.6,
|
| 182 |
+
"cashback": 1.0,
|
| 183 |
+
},
|
| 184 |
+
"AA_AADVANTAGE": {
|
| 185 |
+
"name": "American Airlines AAdvantage",
|
| 186 |
+
"currency": "USD",
|
| 187 |
+
"base": 1.4,
|
| 188 |
+
"premium": 1.7,
|
| 189 |
+
"cashback": 0.8,
|
| 190 |
+
},
|
| 191 |
+
|
| 192 |
+
# ํธํ
ํฌ์ธํธ (๋จ์: cents per point)
|
| 193 |
+
"MARRIOTT_BONVOY": {
|
| 194 |
+
"name": "Marriott Bonvoy",
|
| 195 |
+
"currency": "USD",
|
| 196 |
+
"base": 0.8,
|
| 197 |
+
"premium": 1.0,
|
| 198 |
+
"cashback": 0.6,
|
| 199 |
+
},
|
| 200 |
+
"HILTON_HONORS": {
|
| 201 |
+
"name": "Hilton Honors",
|
| 202 |
+
"currency": "USD",
|
| 203 |
+
"base": 0.5,
|
| 204 |
+
"premium": 0.6,
|
| 205 |
+
"cashback": 0.4,
|
| 206 |
+
},
|
| 207 |
+
"IHG_REWARDS": {
|
| 208 |
+
"name": "IHG One Rewards",
|
| 209 |
+
"currency": "USD",
|
| 210 |
+
"base": 0.5,
|
| 211 |
+
"premium": 0.6,
|
| 212 |
+
"cashback": 0.5,
|
| 213 |
+
},
|
| 214 |
+
"HYATT_WOH": {
|
| 215 |
+
"name": "World of Hyatt",
|
| 216 |
+
"currency": "USD",
|
| 217 |
+
"base": 1.7,
|
| 218 |
+
"premium": 2.0,
|
| 219 |
+
"cashback": 1.5,
|
| 220 |
+
},
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
# ์ฌ์ฉ์ ์ฌํ ์คํ์ผ ํ๋กํ
|
| 224 |
+
TRAVEL_STYLES = {
|
| 225 |
+
"PREMIUM": {
|
| 226 |
+
"description": "๋น์ฆ๋์ค/ํผ์คํธํด๋์ค ์ ํธ",
|
| 227 |
+
"description_en": "Prefers Business/First Class",
|
| 228 |
+
"multiplier_key": "premium",
|
| 229 |
+
},
|
| 230 |
+
"VALUE": {
|
| 231 |
+
"description": "๊ฐ์ฑ๋น ์ค์ (์ด์ฝ๋
ธ๋ฏธ + ํธํ
์
๊ทธ๋ ์ด๋)",
|
| 232 |
+
"description_en": "Value-focused traveler",
|
| 233 |
+
"multiplier_key": "base",
|
| 234 |
+
},
|
| 235 |
+
"CASHBACK": {
|
| 236 |
+
"description": "ํ๊ธ์ฑ ์ฌ์ฉ ์ ํธ (Statement Credit, ๊ธฐํํธ์นด๋)",
|
| 237 |
+
"description_en": "Prefers cash-equivalent redemptions",
|
| 238 |
+
"multiplier_key": "cashback",
|
| 239 |
+
},
|
| 240 |
+
}
|
| 241 |
+
|
src/auth/credit_card_handlers.py
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Credit Card Tool Handlers
|
| 3 |
+
=========================
|
| 4 |
+
|
| 5 |
+
user_* ์ ์ฉ์นด๋ ๊ด๋ จ MCP Tool์ ์ค์ ์คํ ๋ก์ง.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, Any, List, Optional
|
| 10 |
+
from datetime import datetime, date
|
| 11 |
+
|
| 12 |
+
from .config import SERVER_BASE_URL
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger("eodi.auth.credit_card_handlers")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# =============================================================================
|
| 18 |
+
# ์ง์ ์นด๋ ์ ๋ณด
|
| 19 |
+
# =============================================================================
|
| 20 |
+
|
| 21 |
+
# ์ง์ํ๋ ์นด๋ ๋ฐ๊ธ์ฌ ๋ฐ ์นด๋ ID ์ฒด๊ณ
|
| 22 |
+
SUPPORTED_ISSUERS = {
|
| 23 |
+
"AMEX": {
|
| 24 |
+
"name": "American Express",
|
| 25 |
+
"cards": {
|
| 26 |
+
"AMEX_PLATINUM_US": "Platinum Card",
|
| 27 |
+
"AMEX_GOLD_US": "Gold Card",
|
| 28 |
+
"AMEX_GREEN_US": "Green Card",
|
| 29 |
+
"AMEX_MARRIOTT_BONVOY_US": "Marriott Bonvoy Card",
|
| 30 |
+
"AMEX_HILTON_HONORS_US": "Hilton Honors Card",
|
| 31 |
+
"AMEX_HILTON_SURPASS_US": "Hilton Honors Surpass Card",
|
| 32 |
+
"AMEX_HILTON_ASPIRE_US": "Hilton Honors Aspire Card",
|
| 33 |
+
"AMEX_DELTA_SKYMILES_GOLD_US": "Delta SkyMiles Gold Card",
|
| 34 |
+
"AMEX_DELTA_SKYMILES_PLATINUM_US": "Delta SkyMiles Platinum Card",
|
| 35 |
+
"AMEX_DELTA_SKYMILES_RESERVE_US": "Delta SkyMiles Reserve Card",
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"CHASE": {
|
| 39 |
+
"name": "Chase",
|
| 40 |
+
"cards": {
|
| 41 |
+
"CHASE_SAPPHIRE_PREFERRED": "Sapphire Preferred",
|
| 42 |
+
"CHASE_SAPPHIRE_RESERVE": "Sapphire Reserve",
|
| 43 |
+
"CHASE_FREEDOM_UNLIMITED": "Freedom Unlimited",
|
| 44 |
+
"CHASE_FREEDOM_FLEX": "Freedom Flex",
|
| 45 |
+
"CHASE_UNITED_EXPLORER": "United Explorer",
|
| 46 |
+
"CHASE_UNITED_QUEST": "United Quest",
|
| 47 |
+
"CHASE_UNITED_CLUB": "United Club Infinite",
|
| 48 |
+
"CHASE_MARRIOTT_BOUNDLESS": "Marriott Bonvoy Boundless",
|
| 49 |
+
"CHASE_IHG_ONE_PREMIER": "IHG One Rewards Premier",
|
| 50 |
+
"CHASE_HYATT": "World of Hyatt",
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"CITI": {
|
| 54 |
+
"name": "Citi",
|
| 55 |
+
"cards": {
|
| 56 |
+
"CITI_STRATA_PREMIER": "Strata Premier",
|
| 57 |
+
"CITI_DOUBLE_CASH": "Double Cash",
|
| 58 |
+
"CITI_CUSTOM_CASH": "Custom Cash",
|
| 59 |
+
"CITI_AA_EXECUTIVE": "AAdvantage Executive",
|
| 60 |
+
"CITI_AA_PLATINUM": "AAdvantage Platinum Select",
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"CAPITAL_ONE": {
|
| 64 |
+
"name": "Capital One",
|
| 65 |
+
"cards": {
|
| 66 |
+
"CAPITAL_ONE_VENTURE_X": "Venture X",
|
| 67 |
+
"CAPITAL_ONE_VENTURE": "Venture",
|
| 68 |
+
"CAPITAL_ONE_SAVOR_ONE": "SavorOne",
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# ์นด๋๋ณ ํํ ์ถ์ ๊ฐ๋ฅํ ํฌ๋ ๋ง ์ ์ (KB ๋ฐ์ดํฐ์ ์ฐ๋)
|
| 74 |
+
TRACKABLE_BENEFITS = {
|
| 75 |
+
"AMEX_PLATINUM_US": {
|
| 76 |
+
"amex_plat_hotel_credit": {
|
| 77 |
+
"name": "FHR/THC Hotel Credit",
|
| 78 |
+
"period_type": "semi-annually", # monthly, quarterly, semi-annually, yearly
|
| 79 |
+
"amount_per_period": 300,
|
| 80 |
+
"currency": "USD",
|
| 81 |
+
"periods": ["H1", "H2"], # H1=1-6์, H2=7-12์
|
| 82 |
+
},
|
| 83 |
+
"amex_plat_saks_credit": {
|
| 84 |
+
"name": "Saks Fifth Avenue Credit",
|
| 85 |
+
"period_type": "semi-annually",
|
| 86 |
+
"amount_per_period": 50,
|
| 87 |
+
"currency": "USD",
|
| 88 |
+
"periods": ["H1", "H2"],
|
| 89 |
+
},
|
| 90 |
+
"amex_plat_uber_cash": {
|
| 91 |
+
"name": "Uber Cash",
|
| 92 |
+
"period_type": "monthly",
|
| 93 |
+
"amount_per_period": 15,
|
| 94 |
+
"currency": "USD",
|
| 95 |
+
"december_bonus": 20,
|
| 96 |
+
},
|
| 97 |
+
"amex_plat_digital_entertainment": {
|
| 98 |
+
"name": "Digital Entertainment Credit",
|
| 99 |
+
"period_type": "monthly",
|
| 100 |
+
"amount_per_period": 25,
|
| 101 |
+
"currency": "USD",
|
| 102 |
+
},
|
| 103 |
+
"amex_plat_resy_credit": {
|
| 104 |
+
"name": "Resy Credit",
|
| 105 |
+
"period_type": "quarterly",
|
| 106 |
+
"amount_per_period": 100,
|
| 107 |
+
"currency": "USD",
|
| 108 |
+
"periods": ["Q1", "Q2", "Q3", "Q4"],
|
| 109 |
+
},
|
| 110 |
+
"amex_plat_lululemon_credit": {
|
| 111 |
+
"name": "lululemon Credit",
|
| 112 |
+
"period_type": "quarterly",
|
| 113 |
+
"amount_per_period": 75,
|
| 114 |
+
"currency": "USD",
|
| 115 |
+
"periods": ["Q1", "Q2", "Q3", "Q4"],
|
| 116 |
+
},
|
| 117 |
+
"amex_plat_airline_fee_credit": {
|
| 118 |
+
"name": "Airline Fee Credit",
|
| 119 |
+
"period_type": "yearly",
|
| 120 |
+
"amount_per_period": 200,
|
| 121 |
+
"currency": "USD",
|
| 122 |
+
},
|
| 123 |
+
"amex_plat_equinox_credit": {
|
| 124 |
+
"name": "Equinox Credit",
|
| 125 |
+
"period_type": "yearly",
|
| 126 |
+
"amount_per_period": 300,
|
| 127 |
+
"currency": "USD",
|
| 128 |
+
},
|
| 129 |
+
"amex_plat_clear_credit": {
|
| 130 |
+
"name": "CLEAR Plus Credit",
|
| 131 |
+
"period_type": "yearly",
|
| 132 |
+
"amount_per_period": 209,
|
| 133 |
+
"currency": "USD",
|
| 134 |
+
},
|
| 135 |
+
"amex_plat_walmart_credit": {
|
| 136 |
+
"name": "Walmart+ Credit",
|
| 137 |
+
"period_type": "monthly",
|
| 138 |
+
"amount_per_period": 12.95,
|
| 139 |
+
"currency": "USD",
|
| 140 |
+
},
|
| 141 |
+
},
|
| 142 |
+
"CHASE_SAPPHIRE_RESERVE": {
|
| 143 |
+
"chase_sr_travel_credit": {
|
| 144 |
+
"name": "Travel Credit",
|
| 145 |
+
"period_type": "yearly",
|
| 146 |
+
"amount_per_period": 300,
|
| 147 |
+
"currency": "USD",
|
| 148 |
+
},
|
| 149 |
+
"chase_sr_doordash": {
|
| 150 |
+
"name": "DoorDash DashPass",
|
| 151 |
+
"period_type": "yearly",
|
| 152 |
+
"amount_per_period": 0, # Free membership
|
| 153 |
+
"currency": "USD",
|
| 154 |
+
},
|
| 155 |
+
},
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# =============================================================================
|
| 160 |
+
# ์ ํธ๋ฆฌํฐ ํจ์
|
| 161 |
+
# =============================================================================
|
| 162 |
+
|
| 163 |
+
def get_current_period_ids(period_type: str, ref_date: date = None) -> List[str]:
|
| 164 |
+
"""
|
| 165 |
+
ํ์ฌ ๋ ์ง ๊ธฐ์ค ๊ธฐ๊ฐ ID ๋ฐํ.
|
| 166 |
+
|
| 167 |
+
Args:
|
| 168 |
+
period_type: monthly, quarterly, semi-annually, yearly
|
| 169 |
+
ref_date: ๊ธฐ์ค ๋ ์ง (๊ธฐ๋ณธ: ์ค๋)
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
๊ธฐ๊ฐ ID ๋ฆฌ์คํธ (์: ["2026-H1"], ["2026-Q1"], ["2026-01"])
|
| 173 |
+
"""
|
| 174 |
+
if ref_date is None:
|
| 175 |
+
ref_date = date.today()
|
| 176 |
+
|
| 177 |
+
year = ref_date.year
|
| 178 |
+
month = ref_date.month
|
| 179 |
+
|
| 180 |
+
if period_type == "monthly":
|
| 181 |
+
return [f"{year}-{month:02d}"]
|
| 182 |
+
elif period_type == "quarterly":
|
| 183 |
+
quarter = (month - 1) // 3 + 1
|
| 184 |
+
return [f"{year}-Q{quarter}"]
|
| 185 |
+
elif period_type == "semi-annually":
|
| 186 |
+
half = "H1" if month <= 6 else "H2"
|
| 187 |
+
return [f"{year}-{half}"]
|
| 188 |
+
elif period_type == "yearly":
|
| 189 |
+
return [f"{year}"]
|
| 190 |
+
else:
|
| 191 |
+
return [f"{year}"]
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def get_period_deadline(period_type: str, period_id: str) -> Optional[date]:
|
| 195 |
+
"""
|
| 196 |
+
๊ธฐ๊ฐ ID์ ๋ง๊ฐ์ผ ๋ฐํ.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
period_type: ๊ธฐ๊ฐ ์ ํ
|
| 200 |
+
period_id: ๊ธฐ๊ฐ ID (์: "2026-H1")
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
๋ง๊ฐ์ผ
|
| 204 |
+
"""
|
| 205 |
+
try:
|
| 206 |
+
year = int(period_id.split("-")[0])
|
| 207 |
+
|
| 208 |
+
if period_type == "monthly":
|
| 209 |
+
month = int(period_id.split("-")[1])
|
| 210 |
+
if month == 12:
|
| 211 |
+
return date(year + 1, 1, 1)
|
| 212 |
+
else:
|
| 213 |
+
return date(year, month + 1, 1)
|
| 214 |
+
elif period_type == "quarterly":
|
| 215 |
+
quarter = period_id.split("-")[1]
|
| 216 |
+
if quarter == "Q1":
|
| 217 |
+
return date(year, 4, 1)
|
| 218 |
+
elif quarter == "Q2":
|
| 219 |
+
return date(year, 7, 1)
|
| 220 |
+
elif quarter == "Q3":
|
| 221 |
+
return date(year, 10, 1)
|
| 222 |
+
else:
|
| 223 |
+
return date(year + 1, 1, 1)
|
| 224 |
+
elif period_type == "semi-annually":
|
| 225 |
+
half = period_id.split("-")[1]
|
| 226 |
+
if half == "H1":
|
| 227 |
+
return date(year, 7, 1)
|
| 228 |
+
else:
|
| 229 |
+
return date(year + 1, 1, 1)
|
| 230 |
+
elif period_type == "yearly":
|
| 231 |
+
return date(year + 1, 1, 1)
|
| 232 |
+
except Exception as e:
|
| 233 |
+
logger.error(f"๊ธฐ๊ฐ ๋ง๊ฐ์ผ ๊ณ์ฐ ์ค๋ฅ: {e}")
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def days_until_deadline(period_type: str, period_id: str, ref_date: date = None) -> int:
|
| 238 |
+
"""
|
| 239 |
+
๋ง๊ฐ์ผ๊น์ง ๋จ์ ์ผ์ ๊ณ์ฐ.
|
| 240 |
+
"""
|
| 241 |
+
if ref_date is None:
|
| 242 |
+
ref_date = date.today()
|
| 243 |
+
|
| 244 |
+
deadline = get_period_deadline(period_type, period_id)
|
| 245 |
+
if deadline:
|
| 246 |
+
return (deadline - ref_date).days
|
| 247 |
+
return 999 # ์ ์ ์์
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
# =============================================================================
|
| 251 |
+
# Tool ํธ๋ค๋ฌ
|
| 252 |
+
# =============================================================================
|
| 253 |
+
|
| 254 |
+
async def handle_add_credit_card(arguments: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 255 |
+
"""
|
| 256 |
+
user_add_credit_card Tool ํธ๋ค๋ฌ.
|
| 257 |
+
|
| 258 |
+
์ฌ์ฉ์ ์ ์ฉ์นด๋ ๋ฑ๋ก.
|
| 259 |
+
"""
|
| 260 |
+
card_id = arguments.get("card_id", "").upper()
|
| 261 |
+
card_name = arguments.get("card_name", "")
|
| 262 |
+
issuer_code = arguments.get("issuer_code", "").upper()
|
| 263 |
+
region = arguments.get("region", "USA").upper()
|
| 264 |
+
card_open_date = arguments.get("card_open_date") # YYYY-MM-DD
|
| 265 |
+
anniversary_month = arguments.get("anniversary_month")
|
| 266 |
+
annual_fee = arguments.get("annual_fee")
|
| 267 |
+
|
| 268 |
+
# ํ์๊ฐ ๊ฒ์ฆ
|
| 269 |
+
if not card_id:
|
| 270 |
+
return {
|
| 271 |
+
"success": False,
|
| 272 |
+
"error": "card_id๊ฐ ํ์ํฉ๋๋ค.",
|
| 273 |
+
"supported_issuers": list(SUPPORTED_ISSUERS.keys()),
|
| 274 |
+
"example_cards": {
|
| 275 |
+
issuer: list(data["cards"].keys())[:3]
|
| 276 |
+
for issuer, data in SUPPORTED_ISSUERS.items()
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
# ๋ฐ๊ธ์ฌ ๊ฒ์ฆ (card_id์์ ์ถ์ถ ๊ฐ๋ฅ)
|
| 281 |
+
if not issuer_code:
|
| 282 |
+
for issuer, data in SUPPORTED_ISSUERS.items():
|
| 283 |
+
if card_id in data["cards"]:
|
| 284 |
+
issuer_code = issuer
|
| 285 |
+
card_name = card_name or data["cards"][card_id]
|
| 286 |
+
break
|
| 287 |
+
|
| 288 |
+
if not issuer_code:
|
| 289 |
+
issuer_code = card_id.split("_")[0] if "_" in card_id else "OTHER"
|
| 290 |
+
|
| 291 |
+
if not card_name:
|
| 292 |
+
card_name = card_id.replace("_", " ").title()
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 296 |
+
adapter = SupabaseAdapter()
|
| 297 |
+
|
| 298 |
+
result = adapter.upsert_credit_card(
|
| 299 |
+
user_id=user_id,
|
| 300 |
+
card_id=card_id,
|
| 301 |
+
card_name=card_name,
|
| 302 |
+
issuer_code=issuer_code,
|
| 303 |
+
region=region,
|
| 304 |
+
card_open_date=card_open_date,
|
| 305 |
+
anniversary_month=anniversary_month,
|
| 306 |
+
annual_fee=annual_fee
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
if result:
|
| 310 |
+
# ์ถ์ ๊ฐ๋ฅํ ํํ ์ ๋ณด ํฌํจ
|
| 311 |
+
trackable = TRACKABLE_BENEFITS.get(card_id, {})
|
| 312 |
+
|
| 313 |
+
return {
|
| 314 |
+
"success": True,
|
| 315 |
+
"card": {
|
| 316 |
+
"card_id": card_id,
|
| 317 |
+
"card_name": card_name,
|
| 318 |
+
"issuer_code": issuer_code,
|
| 319 |
+
"region": region,
|
| 320 |
+
},
|
| 321 |
+
"trackable_benefits": list(trackable.keys()) if trackable else [],
|
| 322 |
+
"message": f"โ
{card_name} ์นด๋๊ฐ ๋ฑ๋ก๋์์ต๋๋ค." +
|
| 323 |
+
(f"\n\n๐ก ์ถ์ ๊ฐ๋ฅํ ํํ: {len(trackable)}๊ฐ" if trackable else "")
|
| 324 |
+
}
|
| 325 |
+
else:
|
| 326 |
+
return {
|
| 327 |
+
"success": False,
|
| 328 |
+
"error": "์นด๋ ๋ฑ๋ก์ ์คํจํ์ต๋๋ค."
|
| 329 |
+
}
|
| 330 |
+
except Exception as e:
|
| 331 |
+
logger.error(f"์นด๋ ๋ฑ๋ก ์ค๋ฅ: {e}")
|
| 332 |
+
return {
|
| 333 |
+
"success": False,
|
| 334 |
+
"error": f"์นด๋ ๋ฑ๋ก ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
async def handle_get_credit_cards(arguments: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 339 |
+
"""
|
| 340 |
+
user_get_credit_cards Tool ํธ๋ค๋ฌ.
|
| 341 |
+
|
| 342 |
+
์ฌ์ฉ์ ๋ณด์ ์นด๋ ๋ชฉ๋ก ์กฐํ.
|
| 343 |
+
"""
|
| 344 |
+
try:
|
| 345 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 346 |
+
adapter = SupabaseAdapter()
|
| 347 |
+
|
| 348 |
+
cards = adapter.get_user_credit_cards(user_id)
|
| 349 |
+
|
| 350 |
+
return {
|
| 351 |
+
"success": True,
|
| 352 |
+
"cards": [
|
| 353 |
+
{
|
| 354 |
+
"card_id": card["card_id"],
|
| 355 |
+
"card_name": card["card_name"],
|
| 356 |
+
"issuer_code": card["issuer_code"],
|
| 357 |
+
"region": card.get("region", "USA"),
|
| 358 |
+
"is_active": card.get("is_active", True),
|
| 359 |
+
"anniversary_month": card.get("anniversary_month"),
|
| 360 |
+
}
|
| 361 |
+
for card in cards
|
| 362 |
+
],
|
| 363 |
+
"count": len(cards)
|
| 364 |
+
}
|
| 365 |
+
except Exception as e:
|
| 366 |
+
logger.error(f"์นด๋ ๋ชฉ๋ก ์กฐํ ์ค๋ฅ: {e}")
|
| 367 |
+
return {
|
| 368 |
+
"success": False,
|
| 369 |
+
"error": f"์นด๋ ๋ชฉ๋ก ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
async def handle_delete_credit_card(arguments: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 374 |
+
"""
|
| 375 |
+
user_delete_credit_card Tool ํธ๋ค๋ฌ.
|
| 376 |
+
"""
|
| 377 |
+
card_id = arguments.get("card_id", "").upper()
|
| 378 |
+
|
| 379 |
+
if not card_id:
|
| 380 |
+
return {
|
| 381 |
+
"success": False,
|
| 382 |
+
"error": "card_id๊ฐ ํ์ํฉ๋๋ค."
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
try:
|
| 386 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 387 |
+
adapter = SupabaseAdapter()
|
| 388 |
+
|
| 389 |
+
if adapter.delete_credit_card(user_id, card_id):
|
| 390 |
+
return {
|
| 391 |
+
"success": True,
|
| 392 |
+
"message": f"โ
{card_id} ์นด๋๊ฐ ์ญ์ ๋์์ต๋๋ค."
|
| 393 |
+
}
|
| 394 |
+
else:
|
| 395 |
+
return {
|
| 396 |
+
"success": False,
|
| 397 |
+
"error": "์นด๋ ์ญ์ ์ ์คํจํ์ต๋๋ค."
|
| 398 |
+
}
|
| 399 |
+
except Exception as e:
|
| 400 |
+
logger.error(f"์นด๋ ์ญ์ ์ค๋ฅ: {e}")
|
| 401 |
+
return {
|
| 402 |
+
"success": False,
|
| 403 |
+
"error": f"์นด๋ ์ญ์ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
async def handle_update_credit_usage(arguments: Dict[str, Any], user_id: str) -> Dict[str, Any]:
|
| 408 |
+
"""
|
| 409 |
+
user_update_credit_usage Tool ํธ๋ค๋ฌ.
|
| 410 |
+
|
| 411 |
+
ํฌ๋ ๋ง/ํํ ์ฌ์ฉ ๊ธฐ๋ก ์
๋ฐ์ดํธ.
|
| 412 |
+
"""
|
| 413 |
+
card_id = arguments.get("card_id", "").upper()
|
| 414 |
+
benefit_id = arguments.get("benefit_id", "").lower()
|
| 415 |
+
amount_used = arguments.get("amount_used")
|
| 416 |
+
usage_date = arguments.get("usage_date") # YYYY-MM-DD
|
| 417 |
+
description = arguments.get("description", "")
|
| 418 |
+
|
| 419 |
+
if not card_id or not benefit_id:
|
| 420 |
+
return {
|
| 421 |
+
"success": False,
|
| 422 |
+
"error": "card_id์ benefit_id๊ฐ ํ์ํฉ๋๋ค.",
|
| 423 |
+
"available_benefits": list(TRACKABLE_BENEFITS.get(card_id, {}).keys())
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
# ํํ ์ ๋ณด ์กฐํ
|
| 427 |
+
card_benefits = TRACKABLE_BENEFITS.get(card_id, {})
|
| 428 |
+
benefit_info = card_benefits.get(benefit_id)
|
| 429 |
+
|
| 430 |
+
if not benefit_info:
|
| 431 |
+
return {
|
| 432 |
+
"success": False,
|
| 433 |
+
"error": f"์ถ์ ๊ฐ๋ฅํ ํํ์ด ์๋๋๋ค: {benefit_id}",
|
| 434 |
+
"available_benefits": list(card_benefits.keys())
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
# ํ์ฌ ๊ธฐ๊ฐ ๊ณ์ฐ
|
| 438 |
+
ref_date = date.fromisoformat(usage_date) if usage_date else date.today()
|
| 439 |
+
period_ids = get_current_period_ids(benefit_info["period_type"], ref_date)
|
| 440 |
+
period_id = period_ids[0] if period_ids else f"{ref_date.year}"
|
| 441 |
+
|
| 442 |
+
# ์ฌ์ฉ ๊ธ์ก ๊ธฐ๋ณธ๊ฐ: ๊ธฐ๊ฐ๋น ํ๋ ์ ์ฒด
|
| 443 |
+
if amount_used is None:
|
| 444 |
+
amount_used = benefit_info["amount_per_period"]
|
| 445 |
+
|
| 446 |
+
try:
|
| 447 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 448 |
+
adapter = SupabaseAdapter()
|
| 449 |
+
|
| 450 |
+
result = adapter.upsert_credit_usage(
|
| 451 |
+
user_id=user_id,
|
| 452 |
+
card_id=card_id,
|
| 453 |
+
benefit_id=benefit_id,
|
| 454 |
+
usage_period=period_id,
|
| 455 |
+
amount_used=float(amount_used),
|
| 456 |
+
amount_limit=benefit_info["amount_per_period"],
|
| 457 |
+
currency=benefit_info["currency"],
|
| 458 |
+
usage_date=usage_date or ref_date.isoformat(),
|
| 459 |
+
description=description
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
if result:
|
| 463 |
+
return {
|
| 464 |
+
"success": True,
|
| 465 |
+
"usage": {
|
| 466 |
+
"card_id": card_id,
|
| 467 |
+
"benefit_id": benefit_id,
|
| 468 |
+
"benefit_name": benefit_info["name"],
|
| 469 |
+
"period": period_id,
|
| 470 |
+
"amount_used": amount_used,
|
| 471 |
+
"amount_limit": benefit_info["amount_per_period"],
|
| 472 |
+
"currency": benefit_info["currency"],
|
| 473 |
+
},
|
| 474 |
+
"message": f"โ
{benefit_info['name']} ์ฌ์ฉ ๊ธฐ๋ก์ด ์ ์ฅ๋์์ต๋๋ค.\n"
|
| 475 |
+
f" {period_id}: ${amount_used}/${benefit_info['amount_per_period']}"
|
| 476 |
+
}
|
| 477 |
+
else:
|
| 478 |
+
return {
|
| 479 |
+
"success": False,
|
| 480 |
+
"error": "์ฌ์ฉ ๊ธฐ๋ก ์ ์ฅ์ ์คํจํ์ต๋๋ค."
|
| 481 |
+
}
|
| 482 |
+
except Exception as e:
|
| 483 |
+
logger.error(f"์ฌ์ฉ ๊ธฐ๋ก ์ ์ฅ ์ค๋ฅ: {e}")
|
| 484 |
+
return {
|
| 485 |
+
"success": False,
|
| 486 |
+
"error": f"์ฌ์ฉ ๊ธฐ๋ก ์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
async def handle_get_credit_recommendations(
|
| 491 |
+
arguments: Dict[str, Any],
|
| 492 |
+
user_id: str
|
| 493 |
+
) -> Dict[str, Any]:
|
| 494 |
+
"""
|
| 495 |
+
user_get_credit_recommendations Tool ํธ๋ค๋ฌ.
|
| 496 |
+
|
| 497 |
+
ํ์ฌ ์์ ์์ ์ฌ์ฉํด์ผ ํ ํฌ๋ ๋ง/ํํ ์ถ์ฒ.
|
| 498 |
+
"""
|
| 499 |
+
try:
|
| 500 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 501 |
+
adapter = SupabaseAdapter()
|
| 502 |
+
|
| 503 |
+
# 1. ์ฌ์ฉ์ ๋ณด์ ์นด๋ ์กฐํ
|
| 504 |
+
cards = adapter.get_user_credit_cards(user_id)
|
| 505 |
+
|
| 506 |
+
if not cards:
|
| 507 |
+
return {
|
| 508 |
+
"success": True,
|
| 509 |
+
"recommendations": [],
|
| 510 |
+
"message": "๋ฑ๋ก๋ ์ ์ฉ์นด๋๊ฐ ์์ต๋๋ค. user_add_credit_card๋ก ์นด๋๋ฅผ ๋จผ์ ๋ฑ๋กํด์ฃผ์ธ์."
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
# 2. ์ฌ์ฉ ๊ธฐ๋ก ์กฐํ
|
| 514 |
+
usage_records = adapter.get_user_credit_usage(user_id)
|
| 515 |
+
usage_map = {
|
| 516 |
+
(u["card_id"], u["benefit_id"], u["usage_period"]): u
|
| 517 |
+
for u in usage_records
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
# 3. ํ์ฌ ๋ ์ง ๊ธฐ์ค ์ถ์ฒ ์์ฑ
|
| 521 |
+
today = date.today()
|
| 522 |
+
recommendations = []
|
| 523 |
+
|
| 524 |
+
for card in cards:
|
| 525 |
+
card_id = card["card_id"]
|
| 526 |
+
card_benefits = TRACKABLE_BENEFITS.get(card_id, {})
|
| 527 |
+
|
| 528 |
+
for benefit_id, benefit_info in card_benefits.items():
|
| 529 |
+
period_type = benefit_info["period_type"]
|
| 530 |
+
period_ids = get_current_period_ids(period_type, today)
|
| 531 |
+
|
| 532 |
+
for period_id in period_ids:
|
| 533 |
+
# ์ด๋ฏธ ์ฌ์ฉํ ๊ธฐ๋ก ํ์ธ
|
| 534 |
+
usage_key = (card_id, benefit_id, period_id)
|
| 535 |
+
usage = usage_map.get(usage_key)
|
| 536 |
+
|
| 537 |
+
amount_used = usage["amount_used"] if usage else 0
|
| 538 |
+
amount_limit = benefit_info["amount_per_period"]
|
| 539 |
+
remaining = amount_limit - amount_used
|
| 540 |
+
days_left = days_until_deadline(period_type, period_id, today)
|
| 541 |
+
|
| 542 |
+
if remaining > 0:
|
| 543 |
+
# ๊ธด๊ธ๋ ๊ณ์ฐ
|
| 544 |
+
if days_left <= 7:
|
| 545 |
+
urgency = "๐ด ๊ธด๊ธ"
|
| 546 |
+
priority = 1
|
| 547 |
+
elif days_left <= 30:
|
| 548 |
+
urgency = "๐ก ์ฃผ์"
|
| 549 |
+
priority = 2
|
| 550 |
+
else:
|
| 551 |
+
urgency = "๐ข ์ฌ์ "
|
| 552 |
+
priority = 3
|
| 553 |
+
|
| 554 |
+
recommendations.append({
|
| 555 |
+
"card_id": card_id,
|
| 556 |
+
"card_name": card["card_name"],
|
| 557 |
+
"benefit_id": benefit_id,
|
| 558 |
+
"benefit_name": benefit_info["name"],
|
| 559 |
+
"period": period_id,
|
| 560 |
+
"period_type": period_type,
|
| 561 |
+
"amount_remaining": remaining,
|
| 562 |
+
"amount_limit": amount_limit,
|
| 563 |
+
"currency": benefit_info["currency"],
|
| 564 |
+
"days_until_expiry": days_left,
|
| 565 |
+
"urgency": urgency,
|
| 566 |
+
"priority": priority,
|
| 567 |
+
})
|
| 568 |
+
|
| 569 |
+
# ์ฐ์ ์์ ์ ๋ ฌ (๊ธด๊ธ๋ โ ๊ธ์ก)
|
| 570 |
+
recommendations.sort(key=lambda x: (x["priority"], -x["amount_remaining"]))
|
| 571 |
+
|
| 572 |
+
# ์์ 10๊ฐ๋ง ๋ฐํ
|
| 573 |
+
top_recommendations = recommendations[:10]
|
| 574 |
+
|
| 575 |
+
# ๋ฉ์์ง ์์ฑ
|
| 576 |
+
if top_recommendations:
|
| 577 |
+
urgent_count = sum(1 for r in top_recommendations if r["priority"] == 1)
|
| 578 |
+
message_parts = [f"๐ ์ถ์ฒ {len(top_recommendations)}๊ฐ"]
|
| 579 |
+
if urgent_count:
|
| 580 |
+
message_parts.append(f"(๊ธด๊ธ {urgent_count}๊ฐ!)")
|
| 581 |
+
message = " ".join(message_parts)
|
| 582 |
+
else:
|
| 583 |
+
message = "โ
ํ์ฌ ์ฌ์ฉํด์ผ ํ ํฌ๋ ๋ง์ด ์์ต๋๋ค."
|
| 584 |
+
|
| 585 |
+
return {
|
| 586 |
+
"success": True,
|
| 587 |
+
"recommendations": top_recommendations,
|
| 588 |
+
"total_count": len(recommendations),
|
| 589 |
+
"message": message,
|
| 590 |
+
"current_date": today.isoformat()
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
except Exception as e:
|
| 594 |
+
logger.error(f"์ถ์ฒ ์์ฑ ์ค๋ฅ: {e}")
|
| 595 |
+
return {
|
| 596 |
+
"success": False,
|
| 597 |
+
"error": f"์ถ์ฒ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
async def handle_get_credit_usage_summary(
|
| 602 |
+
arguments: Dict[str, Any],
|
| 603 |
+
user_id: str
|
| 604 |
+
) -> Dict[str, Any]:
|
| 605 |
+
"""
|
| 606 |
+
user_get_credit_usage_summary Tool ํธ๋ค๋ฌ.
|
| 607 |
+
|
| 608 |
+
ํน์ ์นด๋์ ํฌ๋ ๋ง ์ฌ์ฉ ํํฉ ์์ฝ.
|
| 609 |
+
"""
|
| 610 |
+
card_id = arguments.get("card_id", "").upper()
|
| 611 |
+
|
| 612 |
+
try:
|
| 613 |
+
from src.db.supabase_adapter import SupabaseAdapter
|
| 614 |
+
adapter = SupabaseAdapter()
|
| 615 |
+
|
| 616 |
+
# ์ฌ์ฉ์ ๋ณด์ ์นด๋ ํ์ธ
|
| 617 |
+
cards = adapter.get_user_credit_cards(user_id)
|
| 618 |
+
user_card_ids = [c["card_id"] for c in cards]
|
| 619 |
+
|
| 620 |
+
if card_id and card_id not in user_card_ids:
|
| 621 |
+
return {
|
| 622 |
+
"success": False,
|
| 623 |
+
"error": f"๋ฑ๋ก๋์ง ์์ ์นด๋์
๋๋ค: {card_id}",
|
| 624 |
+
"registered_cards": user_card_ids
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
# ์ฌ์ฉ ๊ธฐ๋ก ์กฐํ
|
| 628 |
+
usage_records = adapter.get_user_credit_usage(user_id, card_id=card_id if card_id else None)
|
| 629 |
+
|
| 630 |
+
# ์นด๋๋ณ๋ก ๊ทธ๋ฃนํ
|
| 631 |
+
today = date.today()
|
| 632 |
+
summary = {}
|
| 633 |
+
|
| 634 |
+
for card in cards:
|
| 635 |
+
cid = card["card_id"]
|
| 636 |
+
if card_id and cid != card_id:
|
| 637 |
+
continue
|
| 638 |
+
|
| 639 |
+
card_benefits = TRACKABLE_BENEFITS.get(cid, {})
|
| 640 |
+
card_usage = [u for u in usage_records if u["card_id"] == cid]
|
| 641 |
+
|
| 642 |
+
benefits_summary = []
|
| 643 |
+
for benefit_id, benefit_info in card_benefits.items():
|
| 644 |
+
period_type = benefit_info["period_type"]
|
| 645 |
+
period_ids = get_current_period_ids(period_type, today)
|
| 646 |
+
period_id = period_ids[0] if period_ids else f"{today.year}"
|
| 647 |
+
|
| 648 |
+
# ํด๋น ๊ธฐ๊ฐ ์ฌ์ฉ๋
|
| 649 |
+
usage = next(
|
| 650 |
+
(u for u in card_usage
|
| 651 |
+
if u["benefit_id"] == benefit_id and u["usage_period"] == period_id),
|
| 652 |
+
None
|
| 653 |
+
)
|
| 654 |
+
|
| 655 |
+
amount_used = usage["amount_used"] if usage else 0
|
| 656 |
+
amount_limit = benefit_info["amount_per_period"]
|
| 657 |
+
|
| 658 |
+
benefits_summary.append({
|
| 659 |
+
"benefit_id": benefit_id,
|
| 660 |
+
"benefit_name": benefit_info["name"],
|
| 661 |
+
"current_period": period_id,
|
| 662 |
+
"amount_used": amount_used,
|
| 663 |
+
"amount_limit": amount_limit,
|
| 664 |
+
"percent_used": round(amount_used / amount_limit * 100, 1) if amount_limit else 0,
|
| 665 |
+
"currency": benefit_info["currency"],
|
| 666 |
+
})
|
| 667 |
+
|
| 668 |
+
summary[cid] = {
|
| 669 |
+
"card_name": card["card_name"],
|
| 670 |
+
"benefits": benefits_summary,
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
return {
|
| 674 |
+
"success": True,
|
| 675 |
+
"summary": summary,
|
| 676 |
+
"current_date": today.isoformat()
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
except Exception as e:
|
| 680 |
+
logger.error(f"์ฌ์ฉ ํํฉ ์์ฝ ์กฐํ ์ค๋ฅ: {e}")
|
| 681 |
+
return {
|
| 682 |
+
"success": False,
|
| 683 |
+
"error": f"์ฌ์ฉ ํํฉ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: {e}"
|
| 684 |
+
}
|
src/auth/tool_handlers.py
CHANGED
|
@@ -27,6 +27,7 @@ async def execute_user_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, A
|
|
| 27 |
Returns:
|
| 28 |
Tool ์คํ ๊ฒฐ๊ณผ
|
| 29 |
"""
|
|
|
|
| 30 |
if name == "user_get_profile":
|
| 31 |
return await handle_get_profile(arguments)
|
| 32 |
elif name == "user_update_membership":
|
|
@@ -37,10 +38,137 @@ async def execute_user_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, A
|
|
| 37 |
return await handle_request_auth(arguments)
|
| 38 |
elif name == "user_verify_code":
|
| 39 |
return await handle_verify_code(arguments)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
else:
|
| 41 |
return {"success": False, "error": f"Unknown user tool: {name}"}
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
async def handle_get_profile(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
| 45 |
"""
|
| 46 |
user_get_profile Tool ํธ๋ค๋ฌ.
|
|
|
|
| 27 |
Returns:
|
| 28 |
Tool ์คํ ๊ฒฐ๊ณผ
|
| 29 |
"""
|
| 30 |
+
# ๊ธฐ์กด ์ธ์ฆ/๋ฉค๋ฒ์ญ ๊ด๋ จ Tool
|
| 31 |
if name == "user_get_profile":
|
| 32 |
return await handle_get_profile(arguments)
|
| 33 |
elif name == "user_update_membership":
|
|
|
|
| 38 |
return await handle_request_auth(arguments)
|
| 39 |
elif name == "user_verify_code":
|
| 40 |
return await handle_verify_code(arguments)
|
| 41 |
+
|
| 42 |
+
# ์ ์ฉ์นด๋ ๊ด๋ จ Tool (์ธ์ฆ ํ์)
|
| 43 |
+
elif name in [
|
| 44 |
+
"user_add_credit_card",
|
| 45 |
+
"user_get_credit_cards",
|
| 46 |
+
"user_delete_credit_card",
|
| 47 |
+
"user_update_credit_usage",
|
| 48 |
+
"user_get_credit_recommendations",
|
| 49 |
+
"user_get_credit_usage_summary"
|
| 50 |
+
]:
|
| 51 |
+
return await _handle_credit_card_tool(name, arguments)
|
| 52 |
+
|
| 53 |
+
# ๊ฐ์น ํ๊ฐ ๊ด๋ จ Tool (์ธ์ฆ ํ์)
|
| 54 |
+
elif name in [
|
| 55 |
+
"user_get_asset_valuation",
|
| 56 |
+
"user_update_valuation_style",
|
| 57 |
+
"user_get_valuation_styles",
|
| 58 |
+
"user_parse_asset_text",
|
| 59 |
+
"calculate_miles_vs_cashback",
|
| 60 |
+
"generate_award_search_link"
|
| 61 |
+
]:
|
| 62 |
+
return await _handle_valuation_tool(name, arguments)
|
| 63 |
+
|
| 64 |
else:
|
| 65 |
return {"success": False, "error": f"Unknown user tool: {name}"}
|
| 66 |
|
| 67 |
|
| 68 |
+
async def _handle_credit_card_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
| 69 |
+
"""
|
| 70 |
+
์ ์ฉ์นด๋ ๊ด๋ จ Tool์ ์ํ ๊ณตํต ๋ํผ.
|
| 71 |
+
์ธ์
๊ฒ์ฆ ํ ํด๋น ํธ๋ค๋ฌ ํธ์ถ.
|
| 72 |
+
"""
|
| 73 |
+
from .credit_card_handlers import (
|
| 74 |
+
handle_add_credit_card,
|
| 75 |
+
handle_get_credit_cards,
|
| 76 |
+
handle_delete_credit_card,
|
| 77 |
+
handle_update_credit_usage,
|
| 78 |
+
handle_get_credit_recommendations,
|
| 79 |
+
handle_get_credit_usage_summary
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
session_token = arguments.get("session_token")
|
| 83 |
+
user_session_manager = get_user_session_manager()
|
| 84 |
+
|
| 85 |
+
# ์ธ์ฆ ํ์ธ
|
| 86 |
+
if not session_token:
|
| 87 |
+
return {
|
| 88 |
+
"success": False,
|
| 89 |
+
"error": "session_token์ด ํ์ํฉ๋๋ค. ๋จผ์ user_get_profile์ ํธ์ถํ์ฌ ์ธ์ฆ ์ํ๋ฅผ ํ์ธํ์ธ์.",
|
| 90 |
+
"auth_url": f"{SERVER_BASE_URL}/auth/login"
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
session = user_session_manager.get_session(session_token)
|
| 94 |
+
if not session:
|
| 95 |
+
return {
|
| 96 |
+
"success": False,
|
| 97 |
+
"error": "์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.",
|
| 98 |
+
"auth_url": f"{SERVER_BASE_URL}/auth/login"
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
user_id = session.user_id
|
| 102 |
+
|
| 103 |
+
# ๊ฐ ํธ๋ค๋ฌ ํธ์ถ
|
| 104 |
+
if name == "user_add_credit_card":
|
| 105 |
+
return await handle_add_credit_card(arguments, user_id)
|
| 106 |
+
elif name == "user_get_credit_cards":
|
| 107 |
+
return await handle_get_credit_cards(arguments, user_id)
|
| 108 |
+
elif name == "user_delete_credit_card":
|
| 109 |
+
return await handle_delete_credit_card(arguments, user_id)
|
| 110 |
+
elif name == "user_update_credit_usage":
|
| 111 |
+
return await handle_update_credit_usage(arguments, user_id)
|
| 112 |
+
elif name == "user_get_credit_recommendations":
|
| 113 |
+
return await handle_get_credit_recommendations(arguments, user_id)
|
| 114 |
+
elif name == "user_get_credit_usage_summary":
|
| 115 |
+
return await handle_get_credit_usage_summary(arguments, user_id)
|
| 116 |
+
else:
|
| 117 |
+
return {"success": False, "error": f"Unknown credit card tool: {name}"}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
async def _handle_valuation_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
| 121 |
+
"""
|
| 122 |
+
๊ฐ์น ํ๊ฐ ๊ด๋ จ Tool์ ์ํ ๊ณตํต ๋ํผ.
|
| 123 |
+
์ธ์
๊ฒ์ฆ ํ ํด๋น ํธ๋ค๋ฌ ํธ์ถ.
|
| 124 |
+
"""
|
| 125 |
+
from .valuation_handlers import (
|
| 126 |
+
handle_get_asset_valuation,
|
| 127 |
+
handle_update_valuation_style,
|
| 128 |
+
handle_get_valuation_styles,
|
| 129 |
+
handle_calculate_miles_vs_cashback,
|
| 130 |
+
handle_generate_award_search_link
|
| 131 |
+
)
|
| 132 |
+
from .asset_parser import handle_parse_asset_text
|
| 133 |
+
|
| 134 |
+
session_token = arguments.get("session_token")
|
| 135 |
+
user_session_manager = get_user_session_manager()
|
| 136 |
+
|
| 137 |
+
# ์ธ์ฆ ํ์ธ
|
| 138 |
+
if not session_token:
|
| 139 |
+
return {
|
| 140 |
+
"success": False,
|
| 141 |
+
"error": "session_token์ด ํ์ํฉ๋๋ค. ๋จผ์ user_get_profile์ ํธ์ถํ์ฌ ์ธ์ฆ ์ํ๋ฅผ ํ์ธํ์ธ์.",
|
| 142 |
+
"auth_url": f"{SERVER_BASE_URL}/auth/login"
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
session = user_session_manager.get_session(session_token)
|
| 146 |
+
if not session:
|
| 147 |
+
return {
|
| 148 |
+
"success": False,
|
| 149 |
+
"error": "์ธ์
์ด ๋ง๋ฃ๋์์ต๋๋ค. ๋ค์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.",
|
| 150 |
+
"auth_url": f"{SERVER_BASE_URL}/auth/login"
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
user_id = session.user_id
|
| 154 |
+
|
| 155 |
+
# ๊ฐ ํธ๋ค๋ฌ ํธ์ถ
|
| 156 |
+
if name == "user_get_asset_valuation":
|
| 157 |
+
return await handle_get_asset_valuation(arguments, user_id)
|
| 158 |
+
elif name == "user_update_valuation_style":
|
| 159 |
+
return await handle_update_valuation_style(arguments, user_id)
|
| 160 |
+
elif name == "user_get_valuation_styles":
|
| 161 |
+
return await handle_get_valuation_styles(arguments, user_id)
|
| 162 |
+
elif name == "user_parse_asset_text":
|
| 163 |
+
return await handle_parse_asset_text(arguments, user_id)
|
| 164 |
+
elif name == "calculate_miles_vs_cashback":
|
| 165 |
+
return await handle_calculate_miles_vs_cashback(arguments, user_id)
|
| 166 |
+
elif name == "generate_award_search_link":
|
| 167 |
+
return await handle_generate_award_search_link(arguments, user_id)
|
| 168 |
+
else:
|
| 169 |
+
return {"success": False, "error": f"Unknown valuation tool: {name}"}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
async def handle_get_profile(arguments: Dict[str, Any]) -> Dict[str, Any]:
|
| 173 |
"""
|
| 174 |
user_get_profile Tool ํธ๋ค๋ฌ.
|
src/auth/valuation_handlers.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Shadow Valuation Handlers
|
| 3 |
+
=========================
|
| 4 |
+
|
| 5 |
+
ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ๊ฐ์น ํ๊ฐ๋ฅผ ์ํ MCP Tool ํธ๋ค๋ฌ.
|
| 6 |
+
|
| 7 |
+
์ฌ์ฉ์์ ์ฌํ ์คํ์ผ์ ๋ฐ๋ผ ๋์ผํ ํฌ์ธํธ๋ ๋ค๋ฅธ ๊ฐ์น๋ก ํ๊ฐ๋ฉ๋๋ค:
|
| 8 |
+
- PREMIUM: ๋น์ฆ๋์ค/ํผ์คํธ ๋ฐ๊ถ ๋ชฉํ โ ๋์ ๊ฐ์น
|
| 9 |
+
- VALUE: ์ผ๋ฐ ์ฌ์ฉ โ ํ์ค ๊ฐ์น
|
| 10 |
+
- CASHBACK: ํ๊ธ์ฑ ์ฌ์ฉ โ ๋ฎ์ ๊ฐ์น
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from typing import Dict, Any, List, Optional
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
from .config import POINT_VALUATIONS, TRAVEL_STYLES
|
| 17 |
+
|
| 18 |
+
# =============================================================================
|
| 19 |
+
# ์ ํธ๋ฆฌํฐ ํจ์
|
| 20 |
+
# =============================================================================
|
| 21 |
+
|
| 22 |
+
def calculate_point_value(
|
| 23 |
+
program: str,
|
| 24 |
+
amount: float,
|
| 25 |
+
style: str = "VALUE",
|
| 26 |
+
custom_valuation: Optional[float] = None
|
| 27 |
+
) -> Dict[str, Any]:
|
| 28 |
+
"""
|
| 29 |
+
๋จ์ผ ํฌ์ธํธ ํ๋ก๊ทธ๋จ์ ๊ฐ์น๋ฅผ ๊ณ์ฐํฉ๋๋ค.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
program: ํ๋ก๊ทธ๋จ ID (์: AMEX_MR, KOREAN_AIR)
|
| 33 |
+
amount: ํฌ์ธํธ/๋ง์ผ ์๋
|
| 34 |
+
style: ์ฌํ ์คํ์ผ (PREMIUM, VALUE, CASHBACK)
|
| 35 |
+
custom_valuation: ์ฌ์ฉ์ ์ ์ ๊ฐ์น (์์ผ๋ฉด ์ฐ์ ์ ์ฉ)
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
๊ณ์ฐ๋ ๊ฐ์น ์ ๋ณด
|
| 39 |
+
"""
|
| 40 |
+
if program not in POINT_VALUATIONS:
|
| 41 |
+
return {
|
| 42 |
+
"success": False,
|
| 43 |
+
"error": f"์ง์ํ์ง ์๋ ํ๋ก๊ทธ๋จ: {program}",
|
| 44 |
+
"supported_programs": list(POINT_VALUATIONS.keys())
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
prog_info = POINT_VALUATIONS[program]
|
| 48 |
+
currency = prog_info.get("currency", "USD")
|
| 49 |
+
|
| 50 |
+
# ๊ฐ์น ๊ฒฐ์ : ์ฌ์ฉ์ ์ ์ > ์คํ์ผ ๊ธฐ๋ฐ > base
|
| 51 |
+
if custom_valuation is not None:
|
| 52 |
+
cpp = custom_valuation
|
| 53 |
+
source = "custom"
|
| 54 |
+
else:
|
| 55 |
+
style_info = TRAVEL_STYLES.get(style, TRAVEL_STYLES["VALUE"])
|
| 56 |
+
multiplier_key = style_info.get("multiplier_key", "base")
|
| 57 |
+
cpp = prog_info.get(multiplier_key, prog_info.get("base", 1.0))
|
| 58 |
+
source = f"style:{style}"
|
| 59 |
+
|
| 60 |
+
# ๊ฐ์น ๊ณ์ฐ
|
| 61 |
+
if currency == "USD":
|
| 62 |
+
# CPP (cents per point) โ ๋ฌ๋ฌ ๋ณํ
|
| 63 |
+
value_usd = (amount * cpp) / 100
|
| 64 |
+
return {
|
| 65 |
+
"success": True,
|
| 66 |
+
"program": program,
|
| 67 |
+
"program_name": prog_info.get("name", program),
|
| 68 |
+
"amount": amount,
|
| 69 |
+
"cpp": cpp,
|
| 70 |
+
"value": round(value_usd, 2),
|
| 71 |
+
"currency": "USD",
|
| 72 |
+
"source": source,
|
| 73 |
+
}
|
| 74 |
+
elif currency == "KRW":
|
| 75 |
+
# ์/๋ง์ผ โ ๊ทธ๋๋ก
|
| 76 |
+
value_krw = amount * cpp
|
| 77 |
+
return {
|
| 78 |
+
"success": True,
|
| 79 |
+
"program": program,
|
| 80 |
+
"program_name": prog_info.get("name", program),
|
| 81 |
+
"amount": amount,
|
| 82 |
+
"value_per_unit": cpp,
|
| 83 |
+
"value": round(value_krw, 0),
|
| 84 |
+
"currency": "KRW",
|
| 85 |
+
"source": source,
|
| 86 |
+
}
|
| 87 |
+
else:
|
| 88 |
+
return {
|
| 89 |
+
"success": False,
|
| 90 |
+
"error": f"์ง์ํ์ง ์๋ ํตํ: {currency}"
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def calculate_total_value(
|
| 95 |
+
assets: List[Dict[str, Any]],
|
| 96 |
+
style: str = "VALUE",
|
| 97 |
+
custom_valuations: Optional[Dict[str, float]] = None
|
| 98 |
+
) -> Dict[str, Any]:
|
| 99 |
+
"""
|
| 100 |
+
๋ณต์ ์์ฐ์ ์ด ๊ฐ์น๋ฅผ ๊ณ์ฐํฉ๋๋ค.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
assets: [{"program": "AMEX_MR", "amount": 50000}, ...]
|
| 104 |
+
style: ์ฌํ ์คํ์ผ
|
| 105 |
+
custom_valuations: ํ๋ก๊ทธ๋จ๋ณ ์ฌ์ฉ์ ์ ์ ๊ฐ์น
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
์ด ๊ฐ์น ๋ฐ ๊ฐ๋ณ ์์ธ
|
| 109 |
+
"""
|
| 110 |
+
if not assets:
|
| 111 |
+
return {
|
| 112 |
+
"success": True,
|
| 113 |
+
"total_usd": 0,
|
| 114 |
+
"total_krw": 0,
|
| 115 |
+
"breakdown": [],
|
| 116 |
+
"message": "ํ๊ฐํ ์์ฐ์ด ์์ต๋๋ค."
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
custom_valuations = custom_valuations or {}
|
| 120 |
+
breakdown = []
|
| 121 |
+
total_usd = 0
|
| 122 |
+
total_krw = 0
|
| 123 |
+
|
| 124 |
+
for asset in assets:
|
| 125 |
+
program = asset.get("program", "").upper()
|
| 126 |
+
amount = asset.get("amount", 0)
|
| 127 |
+
|
| 128 |
+
if not program or amount <= 0:
|
| 129 |
+
continue
|
| 130 |
+
|
| 131 |
+
custom_val = custom_valuations.get(program)
|
| 132 |
+
result = calculate_point_value(program, amount, style, custom_val)
|
| 133 |
+
|
| 134 |
+
if result.get("success"):
|
| 135 |
+
breakdown.append(result)
|
| 136 |
+
if result.get("currency") == "USD":
|
| 137 |
+
total_usd += result.get("value", 0)
|
| 138 |
+
elif result.get("currency") == "KRW":
|
| 139 |
+
total_krw += result.get("value", 0)
|
| 140 |
+
|
| 141 |
+
return {
|
| 142 |
+
"success": True,
|
| 143 |
+
"style": style,
|
| 144 |
+
"style_description": TRAVEL_STYLES.get(style, {}).get("description", ""),
|
| 145 |
+
"total_usd": round(total_usd, 2),
|
| 146 |
+
"total_krw": round(total_krw, 0),
|
| 147 |
+
"breakdown": breakdown,
|
| 148 |
+
"evaluated_at": datetime.now().isoformat(),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# =============================================================================
|
| 153 |
+
# MCP Tool ํธ๋ค๋ฌ
|
| 154 |
+
# =============================================================================
|
| 155 |
+
|
| 156 |
+
async def handle_get_asset_valuation(
|
| 157 |
+
arguments: Dict[str, Any],
|
| 158 |
+
user_id: str
|
| 159 |
+
) -> Dict[str, Any]:
|
| 160 |
+
"""
|
| 161 |
+
user_get_asset_valuation Tool ํธ๋ค๋ฌ.
|
| 162 |
+
|
| 163 |
+
์ฌ์ฉ์์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ์์ฐ ์ด ๊ฐ์น๋ฅผ ๊ณ์ฐํฉ๋๋ค.
|
| 164 |
+
"""
|
| 165 |
+
from src.db import SupabaseAdapter
|
| 166 |
+
|
| 167 |
+
adapter = SupabaseAdapter()
|
| 168 |
+
|
| 169 |
+
# 1. ์ฌ์ฉ์ ์ค์ ์กฐํ
|
| 170 |
+
prefs = adapter.get_valuation_preferences(user_id)
|
| 171 |
+
style = prefs.get("travel_style", "VALUE") if prefs else "VALUE"
|
| 172 |
+
custom_valuations = prefs.get("custom_valuations", {}) if prefs else {}
|
| 173 |
+
|
| 174 |
+
# 2. ์
๋ ฅ ์์ฐ ํ์ฑ
|
| 175 |
+
# ๋ฐฉ๋ฒ A: ์ง์ ์
๋ ฅ
|
| 176 |
+
assets = arguments.get("assets", [])
|
| 177 |
+
|
| 178 |
+
# ๋ฐฉ๋ฒ B: DB์์ ์กฐํ (ํฅํ user_mileage_assets ํ
์ด๋ธ)
|
| 179 |
+
if not assets and arguments.get("from_db", False):
|
| 180 |
+
# ์์ง ๊ตฌํ๋์ง ์์
|
| 181 |
+
return {
|
| 182 |
+
"success": False,
|
| 183 |
+
"error": "DB ์์ฐ ์กฐํ ๊ธฐ๋ฅ์ ์์ง ๊ตฌํ๋์ง ์์์ต๋๋ค. assets ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ ์
๋ ฅํด์ฃผ์ธ์.",
|
| 184 |
+
"example": {
|
| 185 |
+
"assets": [
|
| 186 |
+
{"program": "AMEX_MR", "amount": 50000},
|
| 187 |
+
{"program": "KOREAN_AIR", "amount": 45000}
|
| 188 |
+
]
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
if not assets:
|
| 193 |
+
return {
|
| 194 |
+
"success": False,
|
| 195 |
+
"error": "ํ๊ฐํ ์์ฐ์ด ์์ต๋๋ค.",
|
| 196 |
+
"usage": "assets ํ๋ผ๋ฏธํฐ์ [{\"program\": \"AMEX_MR\", \"amount\": 50000}] ํ์์ผ๋ก ์
๋ ฅํ์ธ์.",
|
| 197 |
+
"supported_programs": list(POINT_VALUATIONS.keys())
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
# 3. ์คํ์ผ ์ค๋ฒ๋ผ์ด๋ (์ธ์๋ก ์ ๋ฌ๋ ๊ฒฝ์ฐ)
|
| 201 |
+
if "style" in arguments:
|
| 202 |
+
style = arguments["style"].upper()
|
| 203 |
+
if style not in TRAVEL_STYLES:
|
| 204 |
+
return {
|
| 205 |
+
"success": False,
|
| 206 |
+
"error": f"์ง์ํ์ง ์๋ ์คํ์ผ: {style}",
|
| 207 |
+
"valid_styles": list(TRAVEL_STYLES.keys())
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
# 4. ๊ฐ์น ๊ณ์ฐ
|
| 211 |
+
result = calculate_total_value(assets, style, custom_valuations)
|
| 212 |
+
|
| 213 |
+
# 5. ์๋ต ๋ณด๊ฐ
|
| 214 |
+
result["user_style"] = style
|
| 215 |
+
result["tip"] = (
|
| 216 |
+
"์ฌํ ์คํ์ผ์ ๋ฐ๋ผ ๊ฐ์ ํฌ์ธํธ๋ ๋ค๋ฅด๊ฒ ํ๊ฐ๋ฉ๋๋ค. "
|
| 217 |
+
"user_update_valuation_style๋ก ์คํ์ผ์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค."
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
return result
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
async def handle_update_valuation_style(
|
| 224 |
+
arguments: Dict[str, Any],
|
| 225 |
+
user_id: str
|
| 226 |
+
) -> Dict[str, Any]:
|
| 227 |
+
"""
|
| 228 |
+
user_update_valuation_style Tool ํธ๋ค๋ฌ.
|
| 229 |
+
|
| 230 |
+
์ฌ์ฉ์์ ์ฌํ ์คํ์ผ ์ค์ ์ ๋ณ๊ฒฝํฉ๋๋ค.
|
| 231 |
+
"""
|
| 232 |
+
from src.db import SupabaseAdapter
|
| 233 |
+
|
| 234 |
+
style = arguments.get("style", "").upper()
|
| 235 |
+
custom_valuations = arguments.get("custom_valuations")
|
| 236 |
+
|
| 237 |
+
# ์ ํจ์ฑ ๊ฒ์ฌ
|
| 238 |
+
if style and style not in TRAVEL_STYLES:
|
| 239 |
+
return {
|
| 240 |
+
"success": False,
|
| 241 |
+
"error": f"์ ํจํ์ง ์์ ์คํ์ผ: {style}",
|
| 242 |
+
"valid_styles": {
|
| 243 |
+
k: v.get("description") for k, v in TRAVEL_STYLES.items()
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
adapter = SupabaseAdapter()
|
| 248 |
+
|
| 249 |
+
# ํ์ฌ ์ค์ ์กฐํ
|
| 250 |
+
current_prefs = adapter.get_valuation_preferences(user_id)
|
| 251 |
+
|
| 252 |
+
# ์
๋ฐ์ดํธํ ๋ฐ์ดํฐ ๊ตฌ์ฑ
|
| 253 |
+
new_style = style if style else (current_prefs.get("travel_style", "VALUE") if current_prefs else "VALUE")
|
| 254 |
+
new_custom = custom_valuations if custom_valuations is not None else (
|
| 255 |
+
current_prefs.get("custom_valuations", {}) if current_prefs else {}
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# ์ ์ฅ
|
| 259 |
+
result = adapter.upsert_valuation_preferences(
|
| 260 |
+
user_id=user_id,
|
| 261 |
+
travel_style=new_style,
|
| 262 |
+
custom_valuations=new_custom
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
if result:
|
| 266 |
+
return {
|
| 267 |
+
"success": True,
|
| 268 |
+
"message": f"์ฌํ ์คํ์ผ์ด '{new_style}'(์ผ)๋ก ์ค์ ๋์์ต๋๋ค.",
|
| 269 |
+
"style": new_style,
|
| 270 |
+
"style_description": TRAVEL_STYLES.get(new_style, {}).get("description", ""),
|
| 271 |
+
"custom_valuations": new_custom if new_custom else None,
|
| 272 |
+
}
|
| 273 |
+
else:
|
| 274 |
+
return {
|
| 275 |
+
"success": False,
|
| 276 |
+
"error": "์ค์ ์ ์ฅ์ ์คํจํ์ต๋๋ค."
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
async def handle_get_valuation_styles(
|
| 281 |
+
arguments: Dict[str, Any],
|
| 282 |
+
user_id: str
|
| 283 |
+
) -> Dict[str, Any]:
|
| 284 |
+
"""
|
| 285 |
+
์ง์ํ๋ ์ฌํ ์คํ์ผ ๋ฐ ํฌ์ธํธ ํ๋ก๊ทธ๋จ ๋ชฉ๋ก ๋ฐํ.
|
| 286 |
+
"""
|
| 287 |
+
from src.db import SupabaseAdapter
|
| 288 |
+
|
| 289 |
+
adapter = SupabaseAdapter()
|
| 290 |
+
|
| 291 |
+
# ํ์ฌ ์ฌ์ฉ์ ์ค์
|
| 292 |
+
prefs = adapter.get_valuation_preferences(user_id)
|
| 293 |
+
current_style = prefs.get("travel_style", "VALUE") if prefs else "VALUE"
|
| 294 |
+
|
| 295 |
+
# ์คํ์ผ๋ณ ์์ ๊ฐ์น (AMEX MR 10,000์ ๊ธฐ์ค)
|
| 296 |
+
style_examples = {}
|
| 297 |
+
for style_name, style_info in TRAVEL_STYLES.items():
|
| 298 |
+
result = calculate_point_value("AMEX_MR", 10000, style_name)
|
| 299 |
+
if result.get("success"):
|
| 300 |
+
style_examples[style_name] = {
|
| 301 |
+
"description": style_info.get("description"),
|
| 302 |
+
"example_value": f"${result.get('value')} (10,000 MR)"
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
return {
|
| 306 |
+
"success": True,
|
| 307 |
+
"current_style": current_style,
|
| 308 |
+
"current_description": TRAVEL_STYLES.get(current_style, {}).get("description", ""),
|
| 309 |
+
"available_styles": style_examples,
|
| 310 |
+
"supported_programs": {
|
| 311 |
+
k: v.get("name") for k, v in POINT_VALUATIONS.items()
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
# =============================================================================
|
| 317 |
+
# ์-ํฌ-๋ง์ผ ๊ณ์ฐ๊ธฐ
|
| 318 |
+
# =============================================================================
|
| 319 |
+
|
| 320 |
+
async def handle_calculate_miles_vs_cashback(
|
| 321 |
+
arguments: Dict[str, Any],
|
| 322 |
+
user_id: str
|
| 323 |
+
) -> Dict[str, Any]:
|
| 324 |
+
"""
|
| 325 |
+
calculate_miles_vs_cashback Tool ํธ๋ค๋ฌ.
|
| 326 |
+
|
| 327 |
+
๋ง์ผ๋ฆฌ์ง ์ ๋ฆฝ ์นด๋ vs ํ๊ธ ํ ์ธ ์นด๋ ๋น๊ต.
|
| 328 |
+
"""
|
| 329 |
+
from src.db import SupabaseAdapter
|
| 330 |
+
|
| 331 |
+
# ํ์ ํ๋ผ๋ฏธํฐ
|
| 332 |
+
amount = arguments.get("amount", 0)
|
| 333 |
+
|
| 334 |
+
# ๋ง์ผ๋ฆฌ์ง ์ ๋ฆฝ ์ต์
|
| 335 |
+
mile_rate = arguments.get("mile_rate", 1) # 1000์๋น ๋ง์ผ ์
|
| 336 |
+
mile_per = arguments.get("mile_per", 1000) # ๊ธฐ์ค ๊ธ์ก (์)
|
| 337 |
+
mile_program = arguments.get("mile_program", "KOREAN_AIR")
|
| 338 |
+
|
| 339 |
+
# ํ ์ธ ์ต์
|
| 340 |
+
discount_percent = arguments.get("discount_percent", 1.5)
|
| 341 |
+
|
| 342 |
+
if amount <= 0:
|
| 343 |
+
return {
|
| 344 |
+
"success": False,
|
| 345 |
+
"error": "๊ฒฐ์ ๊ธ์ก(amount)์ ์
๋ ฅํด์ฃผ์ธ์.",
|
| 346 |
+
"example": {
|
| 347 |
+
"amount": 100000,
|
| 348 |
+
"mile_rate": 1,
|
| 349 |
+
"mile_per": 1000,
|
| 350 |
+
"discount_percent": 1.5
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
# ์ฌ์ฉ์ ์คํ์ผ ์กฐํ
|
| 355 |
+
adapter = SupabaseAdapter()
|
| 356 |
+
prefs = adapter.get_valuation_preferences(user_id)
|
| 357 |
+
style = prefs.get("travel_style", "VALUE") if prefs else "VALUE"
|
| 358 |
+
|
| 359 |
+
# ๋ง์ผ๋ฆฌ์ง ๊ณ์ฐ
|
| 360 |
+
earned_miles = (amount / mile_per) * mile_rate
|
| 361 |
+
|
| 362 |
+
# ๋ง์ผ ๊ฐ์น ๊ณ์ฐ
|
| 363 |
+
mile_value_result = calculate_point_value(mile_program, earned_miles, style)
|
| 364 |
+
if not mile_value_result.get("success"):
|
| 365 |
+
return mile_value_result
|
| 366 |
+
|
| 367 |
+
# ํตํ ๋ณํ (USD โ KRW ๊ทผ์ฌ: 1 USD = 1350 KRW)
|
| 368 |
+
if mile_value_result.get("currency") == "USD":
|
| 369 |
+
mile_value_krw = mile_value_result.get("value", 0) * 1350
|
| 370 |
+
else:
|
| 371 |
+
mile_value_krw = mile_value_result.get("value", 0)
|
| 372 |
+
|
| 373 |
+
# ํ ์ธ ๊ธ์ก ๊ณ์ฐ
|
| 374 |
+
discount_value_krw = amount * (discount_percent / 100)
|
| 375 |
+
|
| 376 |
+
# ๋น๊ต
|
| 377 |
+
difference = mile_value_krw - discount_value_krw
|
| 378 |
+
winner = "๋ง์ผ๋ฆฌ์ง" if difference > 0 else ("ํ ์ธ" if difference < 0 else "๋๋ฑ")
|
| 379 |
+
|
| 380 |
+
return {
|
| 381 |
+
"success": True,
|
| 382 |
+
"amount": amount,
|
| 383 |
+
"comparison": {
|
| 384 |
+
"mileage": {
|
| 385 |
+
"earned_miles": round(earned_miles, 0),
|
| 386 |
+
"program": mile_program,
|
| 387 |
+
"program_name": mile_value_result.get("program_name"),
|
| 388 |
+
"value_krw": round(mile_value_krw, 0),
|
| 389 |
+
"rate_description": f"{mile_rate}๋ง์ผ/{mile_per}์"
|
| 390 |
+
},
|
| 391 |
+
"cashback": {
|
| 392 |
+
"discount_percent": discount_percent,
|
| 393 |
+
"value_krw": round(discount_value_krw, 0)
|
| 394 |
+
}
|
| 395 |
+
},
|
| 396 |
+
"result": {
|
| 397 |
+
"winner": winner,
|
| 398 |
+
"difference_krw": round(abs(difference), 0),
|
| 399 |
+
"recommendation": (
|
| 400 |
+
f"๋ง์ผ๋ฆฌ์ง ์นด๋๊ฐ {round(abs(difference), 0):,}์ ๋ ์ ๋ฆฌํฉ๋๋ค."
|
| 401 |
+
if difference > 0 else
|
| 402 |
+
f"ํ ์ธ ์นด๋๊ฐ {round(abs(difference), 0):,}์ ๋ ์ ๋ฆฌํฉ๋๋ค."
|
| 403 |
+
if difference < 0 else
|
| 404 |
+
"๋ ์ต์
์ด ๋๋ฑํฉ๋๋ค."
|
| 405 |
+
)
|
| 406 |
+
},
|
| 407 |
+
"style": style,
|
| 408 |
+
"note": f"'{style}' ์คํ์ผ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ๋จ. ์คํ์ผ์ ๋ฐ๋ผ ๋ง์ผ ๊ฐ์น๊ฐ ๋ฌ๋ผ์ง๋๋ค."
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# =============================================================================
|
| 413 |
+
# ๋ฅ๋งํฌ ๋น๋ (Award Search)
|
| 414 |
+
# =============================================================================
|
| 415 |
+
|
| 416 |
+
# ์ฃผ์ ๊ณตํญ ์ฝ๋
|
| 417 |
+
AIRPORT_CODES = {
|
| 418 |
+
# ํ๊ตญ
|
| 419 |
+
"์์ธ": "ICN", "์ธ์ฒ": "ICN", "๊นํฌ": "GMP",
|
| 420 |
+
"๋ถ์ฐ": "PUS", "์ ์ฃผ": "CJU",
|
| 421 |
+
# ์ผ๋ณธ
|
| 422 |
+
"๋์ฟ": "NRT", "๋๋ฆฌํ": "NRT", "ํ๋ค๋ค": "HND",
|
| 423 |
+
"์ค์ฌ์นด": "KIX", "๊ฐ์ฌ์ด": "KIX",
|
| 424 |
+
"ํ์ฟ ์ค์นด": "FUK", "์ฟํฌ๋ก": "CTS",
|
| 425 |
+
# ๋๋จ์
|
| 426 |
+
"๋ฐฉ์ฝ": "BKK", "์ฑ๊ฐํฌ๋ฅด": "SIN",
|
| 427 |
+
"ํธ์น๋ฏผ": "SGN", "๋ค๋ญ": "DAD", "ํ๋
ธ์ด": "HAN",
|
| 428 |
+
"๋ฐ๋ฆฌ": "DPS", "๋ง๋๋ผ": "MNL",
|
| 429 |
+
# ์ค๊ตญ/ํ์ฝฉ
|
| 430 |
+
"ํ์ฝฉ": "HKG", "๋ฒ ์ด์ง": "PEK", "์ํ์ด": "PVG",
|
| 431 |
+
# ์ ๋ฝ
|
| 432 |
+
"๋ฐ๋": "LHR", "ํ๋ฆฌ": "CDG",
|
| 433 |
+
"ํ๋ํฌํธ๋ฅดํธ": "FRA", "์์คํ
๋ฅด๋ด": "AMS",
|
| 434 |
+
"๋ก๋ง": "FCO", "๋ฐ๋ผ๋
ธ": "MXP",
|
| 435 |
+
"๋ฐ๋ฅด์
๋ก๋": "BCN", "๋ง๋๋ฆฌ๋": "MAD",
|
| 436 |
+
# ๋ฏธ๊ตญ
|
| 437 |
+
"๋ด์": "JFK", "๋ก์ค์ค์ ค๋ ์ค": "LAX", "la": "LAX",
|
| 438 |
+
"์ํ๋์์ค์ฝ": "SFO", "์์นด๊ณ ": "ORD",
|
| 439 |
+
"์์ ํ": "SEA", "๋ฌ๋ผ์ค": "DFW",
|
| 440 |
+
"๋ง์ด์ ๋ฏธ": "MIA", "ํ์์ด": "HNL", "ํธ๋๋ฃฐ๋ฃจ": "HNL",
|
| 441 |
+
# ํธ์ฃผ
|
| 442 |
+
"์๋๋": "SYD", "๋ฉ๋ฒ๋ฅธ": "MEL",
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
# ์ข์ ํด๋์ค
|
| 446 |
+
CABIN_CLASSES = {
|
| 447 |
+
"์ด์ฝ๋
ธ๋ฏธ": "Y", "economy": "Y",
|
| 448 |
+
"ํ๋ฆฌ๋ฏธ์ ์ด์ฝ๋
ธ๋ฏธ": "W", "premium economy": "W", "ํ๋ฆฌ์ด์ฝ": "W",
|
| 449 |
+
"๋น์ฆ๋์ค": "J", "business": "J", "๋น์ง๋์ค": "J",
|
| 450 |
+
"ํผ์คํธ": "F", "first": "F", "์ผ๋ฑ์": "F",
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
async def handle_generate_award_search_link(
|
| 455 |
+
arguments: Dict[str, Any],
|
| 456 |
+
user_id: str
|
| 457 |
+
) -> Dict[str, Any]:
|
| 458 |
+
"""
|
| 459 |
+
generate_award_search_link Tool ํธ๋ค๋ฌ.
|
| 460 |
+
|
| 461 |
+
์์ฐ์ด ์
๋ ฅ์ based on Seats.aero/Point.me ๊ฒ์ URL๋ก ๋ณํํฉ๋๋ค.
|
| 462 |
+
"""
|
| 463 |
+
# ํ๋ผ๋ฏธํฐ ์ถ์ถ
|
| 464 |
+
origin = arguments.get("origin", "ICN")
|
| 465 |
+
destination = arguments.get("destination", "")
|
| 466 |
+
date = arguments.get("date", "") # YYYY-MM ๋๋ YYYY-MM-DD
|
| 467 |
+
cabin_class = arguments.get("cabin_class", "J") # ๊ธฐ๋ณธ ๋น์ฆ๋์ค
|
| 468 |
+
|
| 469 |
+
# ๋์๋ช
โ ๊ณตํญ ์ฝ๋ ๋ณํ
|
| 470 |
+
if origin.lower() in AIRPORT_CODES:
|
| 471 |
+
origin = AIRPORT_CODES[origin.lower()]
|
| 472 |
+
if destination.lower() in AIRPORT_CODES:
|
| 473 |
+
destination = AIRPORT_CODES[destination.lower()]
|
| 474 |
+
|
| 475 |
+
# ์ข์ ํด๋์ค ๋ณํ
|
| 476 |
+
if cabin_class.lower() in CABIN_CLASSES:
|
| 477 |
+
cabin_class = CABIN_CLASSES[cabin_class.lower()]
|
| 478 |
+
|
| 479 |
+
if not destination:
|
| 480 |
+
return {
|
| 481 |
+
"success": False,
|
| 482 |
+
"error": "๋ชฉ์ ์ง(destination)๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.",
|
| 483 |
+
"supported_cities": list(AIRPORT_CODES.keys())[:20],
|
| 484 |
+
"example": {
|
| 485 |
+
"origin": "์์ธ",
|
| 486 |
+
"destination": "ํ๋ฆฌ",
|
| 487 |
+
"date": "2026-03",
|
| 488 |
+
"cabin_class": "๋น์ฆ๋์ค"
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
# URL ์์ฑ
|
| 493 |
+
origin = origin.upper()
|
| 494 |
+
destination = destination.upper()
|
| 495 |
+
cabin_class = cabin_class.upper()
|
| 496 |
+
|
| 497 |
+
# Seats.aero URL
|
| 498 |
+
seats_aero_url = f"https://seats.aero/search?origin={origin}&destination={destination}"
|
| 499 |
+
if date:
|
| 500 |
+
seats_aero_url += f"&date={date}"
|
| 501 |
+
if cabin_class:
|
| 502 |
+
seats_aero_url += f"&class={cabin_class}"
|
| 503 |
+
|
| 504 |
+
# Point.me URL (๋ค๋ฅธ ํฌ๋งท)
|
| 505 |
+
point_me_url = f"https://point.me/search/{origin}-{destination}"
|
| 506 |
+
if date:
|
| 507 |
+
point_me_url += f"?date={date}"
|
| 508 |
+
|
| 509 |
+
return {
|
| 510 |
+
"success": True,
|
| 511 |
+
"search_params": {
|
| 512 |
+
"origin": origin,
|
| 513 |
+
"destination": destination,
|
| 514 |
+
"date": date or "flexible",
|
| 515 |
+
"cabin_class": cabin_class,
|
| 516 |
+
"cabin_class_name": {
|
| 517 |
+
"Y": "์ด์ฝ๋
ธ๋ฏธ", "W": "ํ๋ฆฌ๋ฏธ์ ์ด์ฝ๋
ธ๋ฏธ",
|
| 518 |
+
"J": "๋น์ฆ๋์ค", "F": "ํผ์คํธ"
|
| 519 |
+
}.get(cabin_class, cabin_class)
|
| 520 |
+
},
|
| 521 |
+
"links": {
|
| 522 |
+
"seats_aero": seats_aero_url,
|
| 523 |
+
"point_me": point_me_url,
|
| 524 |
+
},
|
| 525 |
+
"tip": (
|
| 526 |
+
"Seats.aero๋ ํน๊ฐ ์๋ฆผ๊ณผ ์์ธ ๊ฒ์์, "
|
| 527 |
+
"Point.me๋ ๋ค์ํ ๋ง์ผ๋ฆฌ์ง ํ๋ก๊ทธ๋จ ๋น๊ต์ ์ ํฉํฉ๋๋ค."
|
| 528 |
+
)
|
| 529 |
+
}
|
| 530 |
+
|
src/db/supabase_adapter.py
CHANGED
|
@@ -204,13 +204,46 @@ class SupabaseAdapter:
|
|
| 204 |
|
| 205 |
records.append(record)
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
try:
|
| 208 |
-
|
| 209 |
-
records
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
except Exception as e:
|
| 216 |
raise RuntimeError(f"์ฒญํฌ ์ ์ฅ ์คํจ: {e}") from e
|
|
@@ -455,6 +488,314 @@ class SupabaseAdapter:
|
|
| 455 |
except Exception as e:
|
| 456 |
print(f"โ ๏ธ ๋ฉค๋ฒ์ญ ์ญ์ ์คํจ ({user_id}, {chain}): {e}")
|
| 457 |
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
|
| 460 |
# =============================================================================
|
|
|
|
| 204 |
|
| 205 |
records.append(record)
|
| 206 |
|
| 207 |
+
# ๋ฐฐ์น ์ฒ๋ฆฌ (Supabase timeout ๋ฐฉ์ง)
|
| 208 |
+
BATCH_SIZE = 30 # 50 โ 30์ผ๋ก ์ค์ฌ ์์ ์ฑ ํฅ์
|
| 209 |
+
MAX_RETRIES = 3
|
| 210 |
+
BATCH_DELAY = 0.5 # ๋ฐฐ์น ๊ฐ 0.5์ด ๋๋ ์ด
|
| 211 |
+
total_saved = 0
|
| 212 |
+
|
| 213 |
+
import time
|
| 214 |
+
|
| 215 |
try:
|
| 216 |
+
for i in range(0, len(records), BATCH_SIZE):
|
| 217 |
+
batch = records[i:i + BATCH_SIZE]
|
| 218 |
+
|
| 219 |
+
# ์ฌ์๋ ๋ก์ง
|
| 220 |
+
for attempt in range(MAX_RETRIES):
|
| 221 |
+
try:
|
| 222 |
+
result = self.client.table("kb_chunks").upsert(
|
| 223 |
+
batch,
|
| 224 |
+
on_conflict="chunk_id"
|
| 225 |
+
).execute()
|
| 226 |
+
|
| 227 |
+
batch_saved = len(result.data) if result.data else len(batch)
|
| 228 |
+
total_saved += batch_saved
|
| 229 |
+
break # ์ฑ๊ณต ์ ์ฌ์๋ ๋ฃจํ ์ข
๋ฃ
|
| 230 |
+
|
| 231 |
+
except Exception as e:
|
| 232 |
+
if attempt < MAX_RETRIES - 1:
|
| 233 |
+
print(f" โ ๏ธ ๋ฐฐ์น ์ ์ฅ ์คํจ (์๋ {attempt + 1}/{MAX_RETRIES}), ์ฌ์๋ ์ค...")
|
| 234 |
+
time.sleep(2) # ์ฌ์๋ ์ 2์ด ๋๊ธฐ
|
| 235 |
+
else:
|
| 236 |
+
raise # ๋ง์ง๋ง ์๋ ์คํจ ์ ์์ธ ์ ํ
|
| 237 |
+
|
| 238 |
+
# ์งํ ์ํฉ ์ถ๋ ฅ (100๊ฐ๋ง๋ค)
|
| 239 |
+
if (i + BATCH_SIZE) % 100 == 0 or i + BATCH_SIZE >= len(records):
|
| 240 |
+
print(f" ๐พ {min(i + BATCH_SIZE, len(records))}/{len(records)} ์ฒญํฌ ์ ์ฅ ์๋ฃ")
|
| 241 |
+
|
| 242 |
+
# ๋ฐฐ์น ๊ฐ ๋๋ ์ด (์๋ฒ ์ฐ๊ฒฐ ์ ์ง)
|
| 243 |
+
if i + BATCH_SIZE < len(records):
|
| 244 |
+
time.sleep(BATCH_DELAY)
|
| 245 |
+
|
| 246 |
+
return total_saved
|
| 247 |
|
| 248 |
except Exception as e:
|
| 249 |
raise RuntimeError(f"์ฒญํฌ ์ ์ฅ ์คํจ: {e}") from e
|
|
|
|
| 488 |
except Exception as e:
|
| 489 |
print(f"โ ๏ธ ๋ฉค๋ฒ์ญ ์ญ์ ์คํจ ({user_id}, {chain}): {e}")
|
| 490 |
return False
|
| 491 |
+
|
| 492 |
+
# =========================================================================
|
| 493 |
+
# ์ ์ฉ์นด๋ (Credit Cards)
|
| 494 |
+
# =========================================================================
|
| 495 |
+
|
| 496 |
+
def get_user_credit_cards(self, user_id: str) -> List[Dict[str, Any]]:
|
| 497 |
+
"""
|
| 498 |
+
์ฌ์ฉ์์ ๋ณด์ ์ ์ฉ์นด๋ ๋ชฉ๋ก ์กฐํ.
|
| 499 |
+
|
| 500 |
+
Args:
|
| 501 |
+
user_id: Supabase Auth UUID
|
| 502 |
+
|
| 503 |
+
Returns:
|
| 504 |
+
์ ์ฉ์นด๋ ๋ฆฌ์คํธ
|
| 505 |
+
"""
|
| 506 |
+
try:
|
| 507 |
+
result = self.client.table("user_credit_cards")\
|
| 508 |
+
.select("*")\
|
| 509 |
+
.eq("user_id", user_id)\
|
| 510 |
+
.eq("is_active", True)\
|
| 511 |
+
.order("created_at")\
|
| 512 |
+
.execute()
|
| 513 |
+
|
| 514 |
+
return result.data or []
|
| 515 |
+
except Exception as e:
|
| 516 |
+
print(f"โ ๏ธ ์ ์ฉ์นด๋ ์กฐํ ์คํจ ({user_id}): {e}")
|
| 517 |
+
return []
|
| 518 |
+
|
| 519 |
+
def upsert_credit_card(
|
| 520 |
+
self,
|
| 521 |
+
user_id: str,
|
| 522 |
+
card_id: str,
|
| 523 |
+
card_name: str,
|
| 524 |
+
issuer_code: str,
|
| 525 |
+
region: str = "USA",
|
| 526 |
+
card_open_date: str = None,
|
| 527 |
+
anniversary_month: int = None,
|
| 528 |
+
annual_fee: float = None
|
| 529 |
+
) -> Optional[Dict[str, Any]]:
|
| 530 |
+
"""
|
| 531 |
+
์ ์ฉ์นด๋ ๋ฑ๋ก/์์ .
|
| 532 |
+
|
| 533 |
+
Args:
|
| 534 |
+
user_id: Supabase Auth UUID
|
| 535 |
+
card_id: ์นด๋ ๊ณ ์ ID (AMEX_PLATINUM_US ๋ฑ)
|
| 536 |
+
card_name: ์นด๋ ์ด๋ฆ
|
| 537 |
+
issuer_code: ๋ฐ๊ธ์ฌ ์ฝ๋
|
| 538 |
+
region: ๋ฐ๊ธ ๊ตญ๊ฐ
|
| 539 |
+
card_open_date: ์นด๋ ๊ฐ์ค์ผ (YYYY-MM-DD)
|
| 540 |
+
anniversary_month: ์ฐํ๋น ๊ฐฑ์ ์ (1-12)
|
| 541 |
+
annual_fee: ์ฐํ๋น
|
| 542 |
+
|
| 543 |
+
Returns:
|
| 544 |
+
์ ์ฅ๋ ์นด๋ ์ ๋ณด ๋๋ None
|
| 545 |
+
"""
|
| 546 |
+
try:
|
| 547 |
+
data = {
|
| 548 |
+
"user_id": user_id,
|
| 549 |
+
"card_id": card_id.upper(),
|
| 550 |
+
"card_name": card_name,
|
| 551 |
+
"issuer_code": issuer_code.upper(),
|
| 552 |
+
"region": region.upper(),
|
| 553 |
+
"is_active": True
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
if card_open_date:
|
| 557 |
+
data["card_open_date"] = card_open_date
|
| 558 |
+
if anniversary_month:
|
| 559 |
+
data["anniversary_month"] = anniversary_month
|
| 560 |
+
if annual_fee is not None:
|
| 561 |
+
data["annual_fee_amount"] = annual_fee
|
| 562 |
+
|
| 563 |
+
# user_id + card_id ๋ณตํฉํค๋ก upsert
|
| 564 |
+
self.client.table("user_credit_cards")\
|
| 565 |
+
.delete()\
|
| 566 |
+
.eq("user_id", user_id)\
|
| 567 |
+
.eq("card_id", card_id.upper())\
|
| 568 |
+
.execute()
|
| 569 |
+
|
| 570 |
+
result = self.client.table("user_credit_cards")\
|
| 571 |
+
.insert(data)\
|
| 572 |
+
.execute()
|
| 573 |
+
|
| 574 |
+
return result.data[0] if result.data else None
|
| 575 |
+
except Exception as e:
|
| 576 |
+
print(f"โ ๏ธ ์ ์ฉ์นด๋ ์ ์ฅ ์คํจ ({user_id}, {card_id}): {e}")
|
| 577 |
+
return None
|
| 578 |
+
|
| 579 |
+
def delete_credit_card(self, user_id: str, card_id: str) -> bool:
|
| 580 |
+
"""
|
| 581 |
+
์ ์ฉ์นด๋ ์ญ์ (soft delete - is_active = False).
|
| 582 |
+
|
| 583 |
+
Args:
|
| 584 |
+
user_id: Supabase Auth UUID
|
| 585 |
+
card_id: ์นด๋ ๊ณ ์ ID
|
| 586 |
+
|
| 587 |
+
Returns:
|
| 588 |
+
์ญ์ ์ฑ๊ณต ์ฌ๋ถ
|
| 589 |
+
"""
|
| 590 |
+
try:
|
| 591 |
+
self.client.table("user_credit_cards")\
|
| 592 |
+
.update({"is_active": False})\
|
| 593 |
+
.eq("user_id", user_id)\
|
| 594 |
+
.eq("card_id", card_id.upper())\
|
| 595 |
+
.execute()
|
| 596 |
+
|
| 597 |
+
return True
|
| 598 |
+
except Exception as e:
|
| 599 |
+
print(f"โ ๏ธ ์ ์ฉ์นด๋ ์ญ์ ์คํจ ({user_id}, {card_id}): {e}")
|
| 600 |
+
return False
|
| 601 |
+
|
| 602 |
+
# =========================================================================
|
| 603 |
+
# ํฌ๋ ๋ง ์ฌ์ฉ ์ถ์ (Credit Usage Tracking)
|
| 604 |
+
# =========================================================================
|
| 605 |
+
|
| 606 |
+
def get_user_credit_usage(
|
| 607 |
+
self,
|
| 608 |
+
user_id: str,
|
| 609 |
+
card_id: str = None,
|
| 610 |
+
benefit_id: str = None
|
| 611 |
+
) -> List[Dict[str, Any]]:
|
| 612 |
+
"""
|
| 613 |
+
์ฌ์ฉ์์ ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์กฐํ.
|
| 614 |
+
|
| 615 |
+
Args:
|
| 616 |
+
user_id: Supabase Auth UUID
|
| 617 |
+
card_id: ํน์ ์นด๋๋ก ํํฐ๋ง (์ ํ)
|
| 618 |
+
benefit_id: ํน์ ํํ์ผ๋ก ํํฐ๋ง (์ ํ)
|
| 619 |
+
|
| 620 |
+
Returns:
|
| 621 |
+
์ฌ์ฉ ๊ธฐ๋ก ๋ฆฌ์คํธ
|
| 622 |
+
"""
|
| 623 |
+
try:
|
| 624 |
+
query = self.client.table("user_credit_usage")\
|
| 625 |
+
.select("*")\
|
| 626 |
+
.eq("user_id", user_id)
|
| 627 |
+
|
| 628 |
+
if card_id:
|
| 629 |
+
query = query.eq("card_id", card_id.upper())
|
| 630 |
+
if benefit_id:
|
| 631 |
+
query = query.eq("benefit_id", benefit_id.lower())
|
| 632 |
+
|
| 633 |
+
result = query.order("usage_period", desc=True).execute()
|
| 634 |
+
|
| 635 |
+
return result.data or []
|
| 636 |
+
except Exception as e:
|
| 637 |
+
print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์กฐํ ์คํจ ({user_id}): {e}")
|
| 638 |
+
return []
|
| 639 |
+
|
| 640 |
+
def upsert_credit_usage(
|
| 641 |
+
self,
|
| 642 |
+
user_id: str,
|
| 643 |
+
card_id: str,
|
| 644 |
+
benefit_id: str,
|
| 645 |
+
usage_period: str,
|
| 646 |
+
amount_used: float,
|
| 647 |
+
amount_limit: float,
|
| 648 |
+
currency: str = "USD",
|
| 649 |
+
usage_date: str = None,
|
| 650 |
+
description: str = None
|
| 651 |
+
) -> Optional[Dict[str, Any]]:
|
| 652 |
+
"""
|
| 653 |
+
ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์ ์ฅ/์์ .
|
| 654 |
+
|
| 655 |
+
Args:
|
| 656 |
+
user_id: Supabase Auth UUID
|
| 657 |
+
card_id: ์นด๋ ID
|
| 658 |
+
benefit_id: ํํ ID
|
| 659 |
+
usage_period: ์ฌ์ฉ ๊ธฐ๊ฐ (2026-H1, 2026-Q1, 2026-01 ๋ฑ)
|
| 660 |
+
amount_used: ์ฌ์ฉ ๊ธ์ก
|
| 661 |
+
amount_limit: ํ๋ ๊ธ์ก
|
| 662 |
+
currency: ํตํ
|
| 663 |
+
usage_date: ์ค์ ์ฌ์ฉ์ผ (YYYY-MM-DD)
|
| 664 |
+
description: ์ฌ์ฉ ๋ด์ญ
|
| 665 |
+
|
| 666 |
+
Returns:
|
| 667 |
+
์ ์ฅ๋ ๊ธฐ๋ก ๋๋ None
|
| 668 |
+
"""
|
| 669 |
+
try:
|
| 670 |
+
data = {
|
| 671 |
+
"user_id": user_id,
|
| 672 |
+
"card_id": card_id.upper(),
|
| 673 |
+
"benefit_id": benefit_id.lower(),
|
| 674 |
+
"usage_period": usage_period,
|
| 675 |
+
"amount_used": amount_used,
|
| 676 |
+
"amount_limit": amount_limit,
|
| 677 |
+
"currency": currency
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
if usage_date:
|
| 681 |
+
data["usage_date"] = usage_date
|
| 682 |
+
if description:
|
| 683 |
+
data["description"] = description
|
| 684 |
+
|
| 685 |
+
# user_id + card_id + benefit_id + usage_period ๋ณตํฉํค๋ก upsert
|
| 686 |
+
self.client.table("user_credit_usage")\
|
| 687 |
+
.delete()\
|
| 688 |
+
.eq("user_id", user_id)\
|
| 689 |
+
.eq("card_id", card_id.upper())\
|
| 690 |
+
.eq("benefit_id", benefit_id.lower())\
|
| 691 |
+
.eq("usage_period", usage_period)\
|
| 692 |
+
.execute()
|
| 693 |
+
|
| 694 |
+
result = self.client.table("user_credit_usage")\
|
| 695 |
+
.insert(data)\
|
| 696 |
+
.execute()
|
| 697 |
+
|
| 698 |
+
return result.data[0] if result.data else None
|
| 699 |
+
except Exception as e:
|
| 700 |
+
print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์ ์ฅ ์คํจ ({user_id}, {card_id}, {benefit_id}): {e}")
|
| 701 |
+
return None
|
| 702 |
+
|
| 703 |
+
def delete_credit_usage(
|
| 704 |
+
self,
|
| 705 |
+
user_id: str,
|
| 706 |
+
card_id: str,
|
| 707 |
+
benefit_id: str,
|
| 708 |
+
usage_period: str
|
| 709 |
+
) -> bool:
|
| 710 |
+
"""
|
| 711 |
+
ํฌ๋ ๋ง ์ฌ์ฉ ๊ธฐ๋ก ์ญ์ .
|
| 712 |
+
|
| 713 |
+
Args:
|
| 714 |
+
user_id: Supabase Auth UUID
|
| 715 |
+
card_id: ์นด๋ ID
|
| 716 |
+
benefit_id: ํํ ID
|
| 717 |
+
usage_period: ์ฌ์ฉ ๊ธฐ๊ฐ
|
| 718 |
+
|
| 719 |
+
Returns:
|
| 720 |
+
์ญ์ ์ฑ๊ณต ์ฌ๋ถ
|
| 721 |
+
"""
|
| 722 |
+
try:
|
| 723 |
+
self.client.table("user_credit_usage")\
|
| 724 |
+
.delete()\
|
| 725 |
+
.eq("user_id", user_id)\
|
| 726 |
+
.eq("card_id", card_id.upper())\
|
| 727 |
+
.eq("benefit_id", benefit_id.lower())\
|
| 728 |
+
.eq("usage_period", usage_period)\
|
| 729 |
+
.execute()
|
| 730 |
+
|
| 731 |
+
return True
|
| 732 |
+
except Exception as e:
|
| 733 |
+
print(f"โ ๏ธ ํฌ๋ ๋ง ์ฌ์ฉ ์ญ์ ์คํจ: {e}")
|
| 734 |
+
return False
|
| 735 |
+
|
| 736 |
+
# =========================================================================
|
| 737 |
+
# Valuation Preferences (๊ฐ์น ํ๊ฐ ์ค์ )
|
| 738 |
+
# =========================================================================
|
| 739 |
+
|
| 740 |
+
def get_valuation_preferences(self, user_id: str) -> Optional[Dict[str, Any]]:
|
| 741 |
+
"""
|
| 742 |
+
์ฌ์ฉ์์ ๊ฐ์น ํ๊ฐ ์ค์ ์กฐํ.
|
| 743 |
+
|
| 744 |
+
Args:
|
| 745 |
+
user_id: Supabase Auth UUID
|
| 746 |
+
|
| 747 |
+
Returns:
|
| 748 |
+
์ค์ ์ ๋ณด ๋๋ None
|
| 749 |
+
"""
|
| 750 |
+
try:
|
| 751 |
+
result = self.client.table("user_valuation_preferences")\
|
| 752 |
+
.select("*")\
|
| 753 |
+
.eq("user_id", user_id)\
|
| 754 |
+
.execute()
|
| 755 |
+
|
| 756 |
+
return result.data[0] if result.data else None
|
| 757 |
+
except Exception as e:
|
| 758 |
+
print(f"โ ๏ธ ๊ฐ์น ํ๊ฐ ์ค์ ์กฐํ ์คํจ ({user_id}): {e}")
|
| 759 |
+
return None
|
| 760 |
+
|
| 761 |
+
def upsert_valuation_preferences(
|
| 762 |
+
self,
|
| 763 |
+
user_id: str,
|
| 764 |
+
travel_style: str = "VALUE",
|
| 765 |
+
custom_valuations: Optional[Dict[str, float]] = None
|
| 766 |
+
) -> Optional[Dict[str, Any]]:
|
| 767 |
+
"""
|
| 768 |
+
์ฌ์ฉ์์ ๊ฐ์น ํ๊ฐ ์ค์ ์ ์ฅ (Upsert).
|
| 769 |
+
|
| 770 |
+
Args:
|
| 771 |
+
user_id: Supabase Auth UUID
|
| 772 |
+
travel_style: ์ฌํ ์คํ์ผ (PREMIUM, VALUE, CASHBACK)
|
| 773 |
+
custom_valuations: ๊ฐ๋ณ ํ๋ก๊ทธ๋จ ๊ฐ์น ์ค๋ฒ๋ผ์ด๋
|
| 774 |
+
|
| 775 |
+
Returns:
|
| 776 |
+
์ ์ฅ๋ ์ค์ ๋๋ None
|
| 777 |
+
"""
|
| 778 |
+
try:
|
| 779 |
+
data = {
|
| 780 |
+
"user_id": user_id,
|
| 781 |
+
"travel_style": travel_style.upper(),
|
| 782 |
+
"custom_valuations": custom_valuations or {}
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
# user_id UNIQUE ์ ์ฝ์ผ๋ก upsert
|
| 786 |
+
self.client.table("user_valuation_preferences")\
|
| 787 |
+
.delete()\
|
| 788 |
+
.eq("user_id", user_id)\
|
| 789 |
+
.execute()
|
| 790 |
+
|
| 791 |
+
result = self.client.table("user_valuation_preferences")\
|
| 792 |
+
.insert(data)\
|
| 793 |
+
.execute()
|
| 794 |
+
|
| 795 |
+
return result.data[0] if result.data else None
|
| 796 |
+
except Exception as e:
|
| 797 |
+
print(f"โ ๏ธ ๊ฐ์น ํ๊ฐ ์ค์ ์ ์ฅ ์คํจ ({user_id}): {e}")
|
| 798 |
+
return None
|
| 799 |
|
| 800 |
|
| 801 |
# =============================================================================
|
src/mcp/server_streamable.py
CHANGED
|
@@ -499,6 +499,373 @@ TOOLS = [
|
|
| 499 |
},
|
| 500 |
"required": ["code"]
|
| 501 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
}
|
| 503 |
]
|
| 504 |
|
|
|
|
| 499 |
},
|
| 500 |
"required": ["code"]
|
| 501 |
}
|
| 502 |
+
},
|
| 503 |
+
# ============================================================
|
| 504 |
+
# Credit Card Tools (์ ์ฉ์นด๋ ๊ด๋ฆฌ/ํํ ์ถ์ฒ)
|
| 505 |
+
# ============================================================
|
| 506 |
+
{
|
| 507 |
+
"name": "user_add_credit_card",
|
| 508 |
+
"description": (
|
| 509 |
+
"๋ณด์ ์ ์ฉ์นด๋๋ฅผ ๋ฑ๋กํฉ๋๋ค. "
|
| 510 |
+
"๋ฑ๋ก๋ ์นด๋๋ฅผ ๋ฐํ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ ํฌ๋ ๋ง/ํํ์ ์ถ์ ํ๊ณ ์ถ์ฒ๋ฐ์ ์ ์์ต๋๋ค."
|
| 511 |
+
),
|
| 512 |
+
"inputSchema": {
|
| 513 |
+
"type": "object",
|
| 514 |
+
"properties": {
|
| 515 |
+
"card_id": {
|
| 516 |
+
"type": "string",
|
| 517 |
+
"description": (
|
| 518 |
+
"์นด๋ ๊ณ ์ ID. ์: AMEX_PLATINUM_US, CHASE_SAPPHIRE_RESERVE. "
|
| 519 |
+
"์ง์ ๋ฐ๊ธ์ฌ: AMEX, CHASE, CITI, CAPITAL_ONE"
|
| 520 |
+
)
|
| 521 |
+
},
|
| 522 |
+
"card_name": {
|
| 523 |
+
"type": "string",
|
| 524 |
+
"description": "์นด๋ ์ด๋ฆ (์ ํ, card_id๋ก ์๋ ์ถ๋ก ๊ฐ๋ฅ)"
|
| 525 |
+
},
|
| 526 |
+
"issuer_code": {
|
| 527 |
+
"type": "string",
|
| 528 |
+
"description": "๋ฐ๊ธ์ฌ ์ฝ๋ (์ ํ, card_id๋ก ์๋ ์ถ๋ก ๊ฐ๋ฅ)"
|
| 529 |
+
},
|
| 530 |
+
"region": {
|
| 531 |
+
"type": "string",
|
| 532 |
+
"default": "USA",
|
| 533 |
+
"description": "๋ฐ๊ธ ๊ตญ๊ฐ (๊ธฐ๋ณธ: USA)"
|
| 534 |
+
},
|
| 535 |
+
"card_open_date": {
|
| 536 |
+
"type": "string",
|
| 537 |
+
"description": "์นด๋ ๊ฐ์ค์ผ (์ ํ, YYYY-MM-DD)"
|
| 538 |
+
},
|
| 539 |
+
"anniversary_month": {
|
| 540 |
+
"type": "integer",
|
| 541 |
+
"minimum": 1,
|
| 542 |
+
"maximum": 12,
|
| 543 |
+
"description": "์ฐํ๋น ๊ฐฑ์ /์ฒญ๊ตฌ ์ (์ ํ, 1-12)"
|
| 544 |
+
},
|
| 545 |
+
"annual_fee": {
|
| 546 |
+
"type": "number",
|
| 547 |
+
"description": "์ฐํ๋น ๊ธ์ก (์ ํ)"
|
| 548 |
+
},
|
| 549 |
+
"session_token": {
|
| 550 |
+
"type": "string",
|
| 551 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 552 |
+
}
|
| 553 |
+
},
|
| 554 |
+
"required": ["card_id", "session_token"]
|
| 555 |
+
}
|
| 556 |
+
},
|
| 557 |
+
{
|
| 558 |
+
"name": "user_get_credit_cards",
|
| 559 |
+
"description": "๋ฑ๋ก๋ ๋ณด์ ์ ์ฉ์นด๋ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.",
|
| 560 |
+
"inputSchema": {
|
| 561 |
+
"type": "object",
|
| 562 |
+
"properties": {
|
| 563 |
+
"session_token": {
|
| 564 |
+
"type": "string",
|
| 565 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 566 |
+
}
|
| 567 |
+
},
|
| 568 |
+
"required": ["session_token"]
|
| 569 |
+
}
|
| 570 |
+
},
|
| 571 |
+
{
|
| 572 |
+
"name": "user_delete_credit_card",
|
| 573 |
+
"description": "๋ฑ๋ก๋ ์ ์ฉ์นด๋๋ฅผ ์ญ์ ํฉ๋๋ค.",
|
| 574 |
+
"inputSchema": {
|
| 575 |
+
"type": "object",
|
| 576 |
+
"properties": {
|
| 577 |
+
"card_id": {
|
| 578 |
+
"type": "string",
|
| 579 |
+
"description": "์ญ์ ํ ์นด๋ ID"
|
| 580 |
+
},
|
| 581 |
+
"session_token": {
|
| 582 |
+
"type": "string",
|
| 583 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 584 |
+
}
|
| 585 |
+
},
|
| 586 |
+
"required": ["card_id", "session_token"]
|
| 587 |
+
}
|
| 588 |
+
},
|
| 589 |
+
{
|
| 590 |
+
"name": "user_update_credit_usage",
|
| 591 |
+
"description": (
|
| 592 |
+
"์ ์ฉ์นด๋ ํฌ๋ ๋ง/ํํ ์ฌ์ฉ ๊ธฐ๋ก์ ์
๋ฐ์ดํธํฉ๋๋ค. "
|
| 593 |
+
"์: AMEX Platinum FHR ํฌ๋ ๋ง $300 ์ฌ์ฉ ์๋ฃ"
|
| 594 |
+
),
|
| 595 |
+
"inputSchema": {
|
| 596 |
+
"type": "object",
|
| 597 |
+
"properties": {
|
| 598 |
+
"card_id": {
|
| 599 |
+
"type": "string",
|
| 600 |
+
"description": "์นด๋ ID (์: AMEX_PLATINUM_US)"
|
| 601 |
+
},
|
| 602 |
+
"benefit_id": {
|
| 603 |
+
"type": "string",
|
| 604 |
+
"description": (
|
| 605 |
+
"ํํ ID. ์: amex_plat_hotel_credit (FHR), "
|
| 606 |
+
"amex_plat_saks_credit, amex_plat_uber_cash, "
|
| 607 |
+
"amex_plat_digital_entertainment, amex_plat_resy_credit"
|
| 608 |
+
)
|
| 609 |
+
},
|
| 610 |
+
"amount_used": {
|
| 611 |
+
"type": "number",
|
| 612 |
+
"description": "์ฌ์ฉ ๊ธ์ก (์ ํ, ๋ฏธ์
๋ ฅ ์ ์ ์ฒด ํ๋๋ก ๊ธฐ๋ก)"
|
| 613 |
+
},
|
| 614 |
+
"usage_date": {
|
| 615 |
+
"type": "string",
|
| 616 |
+
"description": "์ฌ์ฉ์ผ (์ ํ, YYYY-MM-DD, ๋ฏธ์
๋ ฅ ์ ์ค๋)"
|
| 617 |
+
},
|
| 618 |
+
"description": {
|
| 619 |
+
"type": "string",
|
| 620 |
+
"description": "์ฌ์ฉ ๋ด์ญ ๋ฉ๋ชจ (์ ํ)"
|
| 621 |
+
},
|
| 622 |
+
"session_token": {
|
| 623 |
+
"type": "string",
|
| 624 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 625 |
+
}
|
| 626 |
+
},
|
| 627 |
+
"required": ["card_id", "benefit_id", "session_token"]
|
| 628 |
+
}
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
"name": "user_get_credit_recommendations",
|
| 632 |
+
"description": (
|
| 633 |
+
"ํ์ฌ ์์ ์์ ์ฌ์ฉํด์ผ ํ ํฌ๋ ๋ง/ํํ์ ์ถ์ฒํฉ๋๋ค. "
|
| 634 |
+
"๊ธฐ๊ฐ๋ณ ๋ง๊ฐ์ผ์ด ์๋ฐํ ํํ์ ์ฐ์ ์์๋ก ๋ณด์ฌ์ค๋๋ค. "
|
| 635 |
+
"์: 'AMEX Platinum FHR ์๋ฐ๊ธฐ ํฌ๋ ๋ง $300 - 6์ 30์ผ ๋ง๊ฐ'"
|
| 636 |
+
),
|
| 637 |
+
"inputSchema": {
|
| 638 |
+
"type": "object",
|
| 639 |
+
"properties": {
|
| 640 |
+
"session_token": {
|
| 641 |
+
"type": "string",
|
| 642 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 643 |
+
}
|
| 644 |
+
},
|
| 645 |
+
"required": ["session_token"]
|
| 646 |
+
}
|
| 647 |
+
},
|
| 648 |
+
{
|
| 649 |
+
"name": "user_get_credit_usage_summary",
|
| 650 |
+
"description": (
|
| 651 |
+
"ํน์ ์นด๋ ๋๋ ์ ์ฒด ์นด๋์ ํฌ๋ ๋ง ์ฌ์ฉ ํํฉ ์์ฝ์ ์กฐํํฉ๋๋ค. "
|
| 652 |
+
"๊ฐ ํํ๋ณ ์ฌ์ฉ/์์ฌ ๊ธ์ก๊ณผ ๋น์จ์ ๋ณด์ฌ์ค๋๋ค."
|
| 653 |
+
),
|
| 654 |
+
"inputSchema": {
|
| 655 |
+
"type": "object",
|
| 656 |
+
"properties": {
|
| 657 |
+
"card_id": {
|
| 658 |
+
"type": "string",
|
| 659 |
+
"description": "ํน์ ์นด๋ ID๋ก ํํฐ๋ง (์ ํ, ๋ฏธ์
๋ ฅ ์ ์ ์ฒด ์นด๋)"
|
| 660 |
+
},
|
| 661 |
+
"session_token": {
|
| 662 |
+
"type": "string",
|
| 663 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 664 |
+
}
|
| 665 |
+
},
|
| 666 |
+
"required": ["session_token"]
|
| 667 |
+
}
|
| 668 |
+
},
|
| 669 |
+
# ============================================================
|
| 670 |
+
# Shadow Valuation Tools (ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ๊ฐ์น ํ๊ฐ)
|
| 671 |
+
# ============================================================
|
| 672 |
+
{
|
| 673 |
+
"name": "user_get_asset_valuation",
|
| 674 |
+
"description": (
|
| 675 |
+
"๋ณด์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง์ ํ์ฌ ๊ฐ์น๋ฅผ ๊ณ์ฐํฉ๋๋ค. "
|
| 676 |
+
"์ฌ์ฉ์์ ์ฌํ ์คํ์ผ(PREMIUM/VALUE/CASHBACK)์ ๋ฐ๋ผ ๋์ผํ ํฌ์ธํธ๋ ๋ค๋ฅธ ๊ฐ์น๋ก ํ๊ฐ๋ฉ๋๋ค. "
|
| 677 |
+
"์: AMEX MR 50,000์ ์ PREMIUM ์คํ์ผ์์ $1,100, CASHBACK ์คํ์ผ์์ $300๋ก ํ๊ฐ๋ฉ๋๋ค."
|
| 678 |
+
),
|
| 679 |
+
"inputSchema": {
|
| 680 |
+
"type": "object",
|
| 681 |
+
"properties": {
|
| 682 |
+
"assets": {
|
| 683 |
+
"type": "array",
|
| 684 |
+
"items": {
|
| 685 |
+
"type": "object",
|
| 686 |
+
"properties": {
|
| 687 |
+
"program": {
|
| 688 |
+
"type": "string",
|
| 689 |
+
"description": "ํ๋ก๊ทธ๋จ ID (์: AMEX_MR, CHASE_UR, KOREAN_AIR, MARRIOTT_BONVOY)"
|
| 690 |
+
},
|
| 691 |
+
"amount": {
|
| 692 |
+
"type": "number",
|
| 693 |
+
"description": "ํฌ์ธํธ/๋ง์ผ ์๋"
|
| 694 |
+
}
|
| 695 |
+
},
|
| 696 |
+
"required": ["program", "amount"]
|
| 697 |
+
},
|
| 698 |
+
"description": "ํ๊ฐํ ์์ฐ ๋ชฉ๋ก"
|
| 699 |
+
},
|
| 700 |
+
"style": {
|
| 701 |
+
"type": "string",
|
| 702 |
+
"enum": ["PREMIUM", "VALUE", "CASHBACK"],
|
| 703 |
+
"description": "์ด๋ฒ ๊ณ์ฐ์ ์ฌ์ฉํ ์คํ์ผ (์ ํ, ๋ฏธ์
๋ ฅ ์ ์ ์ฅ๋ ์ฌ์ฉ์ ์คํ์ผ ์ฌ์ฉ)"
|
| 704 |
+
},
|
| 705 |
+
"session_token": {
|
| 706 |
+
"type": "string",
|
| 707 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 708 |
+
}
|
| 709 |
+
},
|
| 710 |
+
"required": ["assets", "session_token"]
|
| 711 |
+
}
|
| 712 |
+
},
|
| 713 |
+
{
|
| 714 |
+
"name": "user_update_valuation_style",
|
| 715 |
+
"description": (
|
| 716 |
+
"ํฌ์ธํธ ๊ฐ์น ํ๊ฐ์ ์ฌ์ฉํ ์ฌํ ์คํ์ผ์ ์ค์ ํฉ๋๋ค. "
|
| 717 |
+
"PREMIUM: ๋น์ฆ๋์ค/ํผ์คํธ ๋ฐ๊ถ ๋ชฉํ (๋์ ๊ฐ์น), "
|
| 718 |
+
"VALUE: ์ผ๋ฐ ์ฌ์ฉ (ํ์ค ๊ฐ์น), "
|
| 719 |
+
"CASHBACK: ํ๊ธ์ฑ ์ฌ์ฉ ์ ํธ (๋ฎ์ ๊ฐ์น)"
|
| 720 |
+
),
|
| 721 |
+
"inputSchema": {
|
| 722 |
+
"type": "object",
|
| 723 |
+
"properties": {
|
| 724 |
+
"style": {
|
| 725 |
+
"type": "string",
|
| 726 |
+
"enum": ["PREMIUM", "VALUE", "CASHBACK"],
|
| 727 |
+
"description": "์ค์ ํ ์ฌํ ์คํ์ผ"
|
| 728 |
+
},
|
| 729 |
+
"custom_valuations": {
|
| 730 |
+
"type": "object",
|
| 731 |
+
"description": (
|
| 732 |
+
"๊ฐ๋ณ ํ๋ก๊ทธ๋จ ๊ฐ์น ์ค๋ฒ๋ผ์ด๋ (์ ํ). "
|
| 733 |
+
"์: {\"AMEX_MR\": 2.5, \"KOREAN_AIR\": 20}"
|
| 734 |
+
)
|
| 735 |
+
},
|
| 736 |
+
"session_token": {
|
| 737 |
+
"type": "string",
|
| 738 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 739 |
+
}
|
| 740 |
+
},
|
| 741 |
+
"required": ["session_token"]
|
| 742 |
+
}
|
| 743 |
+
},
|
| 744 |
+
{
|
| 745 |
+
"name": "user_get_valuation_styles",
|
| 746 |
+
"description": (
|
| 747 |
+
"์ง์ํ๋ ์ฌํ ์คํ์ผ๊ณผ ํฌ์ธํธ ํ๋ก๊ทธ๋จ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค. "
|
| 748 |
+
"๊ฐ ์คํ์ผ๋ณ ์์ ๊ฐ์น์ ํ์ฌ ์ฌ์ฉ์ ์ค์ ์ ํ์ธํ ์ ์์ต๋๋ค."
|
| 749 |
+
),
|
| 750 |
+
"inputSchema": {
|
| 751 |
+
"type": "object",
|
| 752 |
+
"properties": {
|
| 753 |
+
"session_token": {
|
| 754 |
+
"type": "string",
|
| 755 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 756 |
+
}
|
| 757 |
+
},
|
| 758 |
+
"required": ["session_token"]
|
| 759 |
+
}
|
| 760 |
+
},
|
| 761 |
+
# ============================================================
|
| 762 |
+
# Asset Parser / Calculator / Deeplink Tools
|
| 763 |
+
# ============================================================
|
| 764 |
+
{
|
| 765 |
+
"name": "user_parse_asset_text",
|
| 766 |
+
"description": (
|
| 767 |
+
"์ฌ์ฉ์๊ฐ ์
๋ ฅํ ํ
์คํธ์์ ํฌ์ธํธ/๋ง์ผ๋ฆฌ์ง ์ ๋ณด๋ฅผ ์๋์ผ๋ก ์ถ์ถํฉ๋๋ค. "
|
| 768 |
+
"์: '๋ํํญ๊ณต 45000 ๋ง์ผ, AMEX MR 50000์ ' โ ๊ตฌ์กฐํ๋ ์์ฐ ๋ชฉ๋ก์ผ๋ก ๋ณํ"
|
| 769 |
+
),
|
| 770 |
+
"inputSchema": {
|
| 771 |
+
"type": "object",
|
| 772 |
+
"properties": {
|
| 773 |
+
"text": {
|
| 774 |
+
"type": "string",
|
| 775 |
+
"description": (
|
| 776 |
+
"ํ์ฑํ ํ
์คํธ. ํ๋ก๊ทธ๋จ๋ช
๊ณผ ์ซ์๋ฅผ ํฌํจํด์ฃผ์ธ์. "
|
| 777 |
+
"์: '๋ํํญ๊ณต 45000\\n์์์๋ 12000\\nAMEX MR 50000'"
|
| 778 |
+
)
|
| 779 |
+
},
|
| 780 |
+
"save_to_profile": {
|
| 781 |
+
"type": "boolean",
|
| 782 |
+
"default": False,
|
| 783 |
+
"description": "ํ์ฑ๋ ์์ฐ์ ํ๋กํ์ ์ ์ฅํ ์ง ์ฌ๋ถ (ํฅํ ๊ธฐ๋ฅ)"
|
| 784 |
+
},
|
| 785 |
+
"session_token": {
|
| 786 |
+
"type": "string",
|
| 787 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 788 |
+
}
|
| 789 |
+
},
|
| 790 |
+
"required": ["text", "session_token"]
|
| 791 |
+
}
|
| 792 |
+
},
|
| 793 |
+
{
|
| 794 |
+
"name": "calculate_miles_vs_cashback",
|
| 795 |
+
"description": (
|
| 796 |
+
"๋ง์ผ๋ฆฌ์ง ์ ๋ฆฝ ์นด๋ vs ํ๊ธ ํ ์ธ ์นด๋ ์ค ์ด๋ ๊ฒ์ด ๋ ์ ๋ฆฌํ์ง ๊ณ์ฐํฉ๋๋ค. "
|
| 797 |
+
"์: 10๋ง์ ๊ฒฐ์ ์ '1000์๋น 1๋ง์ผ' vs '1.5% ํ ์ธ' ๋น๊ต"
|
| 798 |
+
),
|
| 799 |
+
"inputSchema": {
|
| 800 |
+
"type": "object",
|
| 801 |
+
"properties": {
|
| 802 |
+
"amount": {
|
| 803 |
+
"type": "number",
|
| 804 |
+
"description": "๊ฒฐ์ ๊ธ์ก (์)"
|
| 805 |
+
},
|
| 806 |
+
"mile_rate": {
|
| 807 |
+
"type": "number",
|
| 808 |
+
"default": 1,
|
| 809 |
+
"description": "์ ๋ฆฝ ๋ง์ผ ์ (๊ธฐ๋ณธ: 1)"
|
| 810 |
+
},
|
| 811 |
+
"mile_per": {
|
| 812 |
+
"type": "number",
|
| 813 |
+
"default": 1000,
|
| 814 |
+
"description": "๋ง์ผ ์ ๋ฆฝ ๊ธฐ์ค ๊ธ์ก (์, ๊ธฐ๋ณธ: 1000)"
|
| 815 |
+
},
|
| 816 |
+
"mile_program": {
|
| 817 |
+
"type": "string",
|
| 818 |
+
"default": "KOREAN_AIR",
|
| 819 |
+
"description": "๋ง์ผ๋ฆฌ์ง ํ๋ก๊ทธ๋จ (๊ธฐ๋ณธ: KOREAN_AIR)"
|
| 820 |
+
},
|
| 821 |
+
"discount_percent": {
|
| 822 |
+
"type": "number",
|
| 823 |
+
"default": 1.5,
|
| 824 |
+
"description": "ํ ์ธ์จ (%, ๊ธฐ๋ณธ: 1.5)"
|
| 825 |
+
},
|
| 826 |
+
"session_token": {
|
| 827 |
+
"type": "string",
|
| 828 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 829 |
+
}
|
| 830 |
+
},
|
| 831 |
+
"required": ["amount", "session_token"]
|
| 832 |
+
}
|
| 833 |
+
},
|
| 834 |
+
{
|
| 835 |
+
"name": "generate_award_search_link",
|
| 836 |
+
"description": (
|
| 837 |
+
"ํน๊ฐ์ ๊ฒ์์ ์ํ Seats.aero/Point.me URL์ ์์ฑํฉ๋๋ค. "
|
| 838 |
+
"๋์๋ช
(ํ๊ธ/์๋ฌธ)์ ๊ณตํญ ์ฝ๋๋ก ์๋ ๋ณํํฉ๋๋ค. "
|
| 839 |
+
"์: '์์ธ โ ํ๋ฆฌ ๋น์ฆ๋์ค' โ ๊ฒ์ URL ์์ฑ"
|
| 840 |
+
),
|
| 841 |
+
"inputSchema": {
|
| 842 |
+
"type": "object",
|
| 843 |
+
"properties": {
|
| 844 |
+
"origin": {
|
| 845 |
+
"type": "string",
|
| 846 |
+
"default": "ICN",
|
| 847 |
+
"description": "์ถ๋ฐ์ง (๋์๋ช
๋๋ ๊ณตํญ ์ฝ๋, ๊ธฐ๋ณธ: ICN)"
|
| 848 |
+
},
|
| 849 |
+
"destination": {
|
| 850 |
+
"type": "string",
|
| 851 |
+
"description": "๋ชฉ์ ์ง (๋์๋ช
๋๋ ๊ณตํญ ์ฝ๋)"
|
| 852 |
+
},
|
| 853 |
+
"date": {
|
| 854 |
+
"type": "string",
|
| 855 |
+
"description": "๋ ์ง (YYYY-MM ๋๋ YYYY-MM-DD ํ์, ๋ฏธ์
๋ ฅ ์ ์ ์ฐ ๊ฒ์)"
|
| 856 |
+
},
|
| 857 |
+
"cabin_class": {
|
| 858 |
+
"type": "string",
|
| 859 |
+
"default": "J",
|
| 860 |
+
"description": "์ข์ ํด๋์ค (Y/W/J/F ๋๋ ์ด์ฝ๋
ธ๋ฏธ/๋น์ฆ๋์ค/ํผ์คํธ)"
|
| 861 |
+
},
|
| 862 |
+
"session_token": {
|
| 863 |
+
"type": "string",
|
| 864 |
+
"description": "์ธ์ฆ ์ธ์
ํ ํฐ"
|
| 865 |
+
}
|
| 866 |
+
},
|
| 867 |
+
"required": ["destination", "session_token"]
|
| 868 |
+
}
|
| 869 |
}
|
| 870 |
]
|
| 871 |
|