eodi-mcp / scripts /chunk_handlers.py
lovelymango's picture
Upload 25 files
978996e verified
"""
์ฒญํฌ ์ƒ์„ฑ ํ•ธ๋“ค๋Ÿฌ
================
YAML์˜ ๊ฐ ์„น์…˜์„ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์ฒญํฌ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋“ค.
sync_to_supabase.py์—์„œ ๋™์ ์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
ํ•ธ๋“ค๋Ÿฌ ๊ทœ์น™:
- ๊ฐ ํ•ธ๋“ค๋Ÿฌ๋Š” (data, context) -> List[Dict] ํ˜•ํƒœ
- context์—๋Š” chain, hotel_name, hotel_name_ko ๋“ฑ ํฌํ•จ
- ๋ฐ˜ํ™˜๊ฐ’์€ [{content: str, metadata: dict}, ...] ํ˜•ํƒœ
"""
from typing import Dict, Any, List, Optional
# ===========================================================================
# ์œ ํ‹ธ๋ฆฌํ‹ฐ
# ===========================================================================
DISCLAIMER_PRICE = "\n\nโš ๏ธ ์ฐธ๊ณ : ์œ„ ํฌ์ธํŠธ/๊ฐ€๊ฒฉ ์ •๋ณด๋Š” ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ฐธ๊ณ ์šฉ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ํ•„์š” ํฌ์ธํŠธ์™€ ๊ฐ€๊ฒฉ์€ ์˜ˆ์•ฝ ์‹œ์ ์— ๋”ฐ๋ผ ๋ณ€๋™๋˜๋ฏ€๋กœ, ๊ณต์‹ ์›น์‚ฌ์ดํŠธ์—์„œ ์ง์ ‘ ํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค."
DISCLAIMER_SHORT = "\n\nโš ๏ธ ์ฐธ๊ณ ์šฉ ์ •๋ณด์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ๋‚ด์šฉ์€ ๊ณต์‹ ์›น์‚ฌ์ดํŠธ์—์„œ ํ™•์ธํ•˜์„ธ์š”."
def safe_format_number(value: Any, suffix: str = "", prefix: str = "") -> str:
"""์ˆซ์ž๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ํฌ๋งทํŒ… - None์ด๋‚˜ ๋น„์ˆซ์ž ๊ฐ’ ์ฒ˜๋ฆฌ
Args:
value: ํฌ๋งทํŒ…ํ•  ๊ฐ’ (int, float, None ๋“ฑ)
suffix: ์ˆซ์ž ๋’ค์— ๋ถ™์ผ ๋ฌธ์ž์—ด (์˜ˆ: '์›', '๋งˆ์ผ', 'P')
prefix: ์ˆซ์ž ์•ž์— ๋ถ™์ผ ๋ฌธ์ž์—ด (์˜ˆ: '์•ฝ ', '์ตœ๋Œ€ ')
Returns:
ํฌ๋งท๋œ ๋ฌธ์ž์—ด ๋˜๋Š” 'N/A'
Examples:
safe_format_number(1000, "์›") -> "1,000์›"
safe_format_number(None, "์›") -> "N/A"
safe_format_number("unknown", "P") -> "N/A"
"""
if value is None:
return "N/A"
try:
if isinstance(value, (int, float)):
return f"{prefix}{value:,}{suffix}"
# ๋ฌธ์ž์—ด์ด ์ˆซ์ž๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ์ง€ ์‹œ๋„
num_value = float(value)
if num_value.is_integer():
return f"{prefix}{int(num_value):,}{suffix}"
return f"{prefix}{num_value:,.2f}{suffix}"
except (ValueError, TypeError):
return str(value) if value else "N/A"
def get_hotel_info(context: Dict[str, Any]) -> tuple:
"""context์—์„œ ํ˜ธํ…” ์ •๋ณด ์ถ”์ถœ"""
hotel_name = context.get("hotel_name", "Unknown Hotel")
hotel_name_ko = context.get("hotel_name_ko")
chain = context.get("chain", "UNKNOWN")
return hotel_name, hotel_name_ko, chain
def format_hotel_header(hotel_name: str, hotel_name_ko: Optional[str], chain: str) -> str:
"""ํ˜ธํ…” ์ •๋ณด ํ—ค๋” ์ƒ์„ฑ"""
content = f"ํ˜ธํ…”๋ช…: {hotel_name}\n"
if hotel_name_ko:
content += f"ํ˜ธํ…”๋ช… (ํ•œ๊ตญ์–ด): {hotel_name_ko}\n"
content += f"์ฒด์ธ: {chain}\n"
return content
def format_evidence(evidence_list: list, max_items: int = 2, max_quote_len: int = 80) -> str:
"""Evidence ๋ฐฐ์—ด์„ ์ฒญํฌ์— ํฌํ•จํ•  ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜
Args:
evidence_list: evidence ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ
max_items: ํฌํ•จํ•  ์ตœ๋Œ€ evidence ์ˆ˜
max_quote_len: ์ธ์šฉ๋ฌธ ์ตœ๋Œ€ ๊ธธ์ด
Returns:
ํฌ๋งท๋œ ์ถœ์ฒ˜ ์ •๋ณด ๋ฌธ์ž์—ด (๋น„์–ด์žˆ์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
"""
if not evidence_list or not isinstance(evidence_list, list):
return ""
content = "\n์ถœ์ฒ˜:\n"
added = 0
for ev in evidence_list:
if added >= max_items:
break
if not isinstance(ev, dict):
continue
section = ev.get("section_path", "")
quote = ev.get("quote", "")
if section or quote:
if section:
content += f" - [{section}] "
else:
content += " - "
if quote:
# ์ธ์šฉ๋ฌธ ๊ธธ์ด ์ œํ•œ
truncated = quote[:max_quote_len]
if len(quote) > max_quote_len:
truncated += "..."
content += f'"{truncated}"\n'
else:
content += "\n"
added += 1
return content if added > 0 else ""
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
# ===========================================================================
def handle_loyalty_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""loyalty_programs ๋˜๋Š” loyalty_program ์ฒ˜๋ฆฌ
LoyaltyProgram ์Šคํ‚ค๋งˆ์˜ ๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ๋„ ์„œ๋ธŒ์ฒญํ‚น:
- points_system โ†’ handle_points_systems
- member_rates โ†’ handle_member_rates
- milestone_program โ†’ handle_milestone_programs
- partner_status_matches โ†’ handle_partner_status_matches
- co_branded_cards โ†’ handle_credit_cards
"""
chunks = []
chain = context.get("chain", "UNKNOWN")
# ๋‹จ์ผ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜
programs = data if isinstance(data, list) else [data]
for program in programs[:5]:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "N/A")
program_chain = program.get("chain", chain)
content = f"""
๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ: {program_name}
์ฒด์ธ: {program_chain}
ํฌ์ธํŠธ ์ด๋ฆ„: {program.get("points_name", "N/A")}
ํฌ์ธํŠธ ๋งŒ๋ฃŒ: {program.get("points_expiry", "N/A")}
"""
# ํšŒ์› ๋“ฑ๊ธ‰
tiers = program.get("tiers", [])
if tiers:
content += "\n๋“ฑ๊ธ‰:\n"
for tier in tiers[:7]:
tier_name = tier.get("tier_name", "N/A")
upgrade_req = tier.get("upgrade_requirement", {})
req_nights = upgrade_req.get("nights")
content += f" - {tier_name}: {req_nights}๋ฐ• ์ด์ƒ\n" if req_nights else f" - {tier_name}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "loyalty_program",
"program_name": program_name,
"chain": program_chain
}
})
# === ๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ ์„œ๋ธŒ์ฒญํ‚น ===
# ํ”„๋กœ๊ทธ๋žจ๋ณ„ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ (chain ์ •๋ณด ๋ณด๊ฐ•)
program_context = {**context, "chain": program_chain}
# 1. points_system ์ฒ˜๋ฆฌ
points_system = program.get("points_system")
if points_system:
try:
sub_chunks = handle_points_systems(points_system, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 2. member_rates ์ฒ˜๋ฆฌ
member_rates = program.get("member_rates")
if member_rates:
try:
sub_chunks = handle_member_rates(member_rates, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 3. milestone_program ์ฒ˜๋ฆฌ
milestone_program = program.get("milestone_program")
if milestone_program:
try:
sub_chunks = handle_milestone_programs(milestone_program, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 4. partner_status_matches ์ฒ˜๋ฆฌ
partner_status_matches = program.get("partner_status_matches")
if partner_status_matches:
try:
sub_chunks = handle_partner_status_matches(partner_status_matches, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 5. co_branded_cards ์ฒ˜๋ฆฌ (์ œํœด ์นด๋“œ)
co_branded_cards = program.get("co_branded_cards")
if co_branded_cards:
try:
sub_chunks = handle_credit_cards(co_branded_cards, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 6. best_rate_guarantee ์ฒ˜๋ฆฌ (BRG)
brg = program.get("best_rate_guarantee")
if brg:
try:
sub_chunks = handle_best_rate_guarantee(brg, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
return chunks
def handle_membership_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""membership_tiers ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
if data is None:
return chunks
tiers = data if isinstance(data, list) else [data]
for tier in tiers[:10]:
if not isinstance(tier, dict):
continue
tier_name = tier.get("tier_name", "N/A")
tier_level = tier.get("tier_level", "N/A")
tier_chain = tier.get("chain", chain)
# ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: qualification (TierQualification)
# ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: upgrade_requirement๋„ ํด๋ฐฑ ์ง€์›
qualification = tier.get("qualification", tier.get("upgrade_requirement", {}))
req_nights = qualification.get("nights_required", qualification.get("nights", "N/A"))
req_stays = qualification.get("stays_required")
req_points = qualification.get("points_required", qualification.get("points"))
req_spending = qualification.get("spending_required")
qual_period = qualification.get("qualification_period", "calendar_year")
# ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: bonus_points_percentage
bonus_pct = tier.get("bonus_points_percentage", tier.get("elite_bonus_points"))
content = f"""
๋“ฑ๊ธ‰: {tier_name} ({tier_level})
์ฒด์ธ: {tier_chain}
๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
์ž๊ฒฉ ์š”๊ฑด:
- ์ˆ™๋ฐ•: {req_nights}๋ฐ•"""
if req_stays:
content += f"\n - ์ฒด๋ฅ˜: {req_stays}ํšŒ"
if req_points:
content += f"\n - ํฌ์ธํŠธ: {req_points:,}P" if isinstance(req_points, int) else f"\n - ํฌ์ธํŠธ: {req_points}"
if req_spending and isinstance(req_spending, dict):
content += f"\n - ์ง€์ถœ: {req_spending.get('amount', 'N/A')} {req_spending.get('currency', '')}"
content += f"\n - ์‚ฐ์ • ๊ธฐ๊ฐ„: {qual_period}\n"
# ๋Œ€์ฒด ์ž๊ฒฉ ๊ฒฝ๋กœ
alt_paths = qualification.get("alternative_paths", [])
if alt_paths:
content += f" - ๋Œ€์ฒด ๊ฒฝ๋กœ: {', '.join(alt_paths[:3])}\n"
if bonus_pct:
content += f"๋ณด๋„ˆ์Šค ํฌ์ธํŠธ: {bonus_pct}%\n"
# ๋“ฑ๊ธ‰ ์œ ์ง€ ์กฐ๊ฑด: retention (TierRetention)
retention = tier.get("retention")
if retention and isinstance(retention, dict):
content += "\n๋“ฑ๊ธ‰ ์œ ์ง€ ์กฐ๊ฑด:\n"
ret_nights = retention.get("nights_required")
ret_points = retention.get("points_required")
if ret_nights:
content += f" - ์ˆ™๋ฐ•: {ret_nights}๋ฐ•\n"
if ret_points:
content += f" - ํฌ์ธํŠธ: {ret_points:,}P\n"
# ํ‰์ƒ ๋“ฑ๊ธ‰
if tier.get("lifetime_tier_available"):
content += f"\nํ‰์ƒ ๋“ฑ๊ธ‰: ๊ฐ€๋Šฅ"
if tier.get("lifetime_requirements"):
content += f" ({tier.get('lifetime_requirements')})"
content += "\n"
# ์ฃผ์š” ํ˜œํƒ
benefits = tier.get("benefits", [])
if benefits:
content += "\n์ฃผ์š” ํ˜œํƒ:\n"
for benefit in benefits[:8]:
if isinstance(benefit, dict):
ben_name = benefit.get("name", benefit.get("benefit_name", "N/A"))
ben_desc = benefit.get("description", "")[:80]
content += f" - {ben_name}"
if ben_desc:
content += f": {ben_desc}"
content += "\n"
# evidence ์ •๋ณด ํฌํ•จ (์„ ํƒ์ )
evidence = tier.get("evidence", [])
if evidence:
content += format_evidence(evidence, max_items=2)
chunks.append({
"content": content.strip(),
"metadata": {
"type": "membership_tier",
"tier_name": tier_name,
"tier_level": tier_level,
"rank": tier.get("rank")
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ
# ===========================================================================
def handle_milestone_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""milestone_program ์ฒ˜๋ฆฌ (IHG Milestone Rewards ๋“ฑ)"""
chunks = []
chain = context.get("chain", "UNKNOWN")
programs = data if isinstance(data, list) else [data]
for program in programs[:3]:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "Milestone Rewards")
program_chain = program.get("chain", chain)
minimum_tier = program.get("minimum_tier", "N/A")
reset_period = program.get("reset_period", "calendar_year")
content = f"""
๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ: {program_name}
์ฒด์ธ: {program_chain}
์ฐธ์—ฌ ์ตœ์†Œ ๋“ฑ๊ธ‰: {minimum_tier}
๋ฆฌ์…‹ ์ฃผ๊ธฐ: {reset_period}
๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ: {"์˜ˆ" if program.get("rollover_enabled") else "์•„๋‹ˆ์˜ค"}
"""
# ๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ ๋“ฑ๊ธ‰
rollover_tiers = program.get("rollover_eligible_tiers", [])
if rollover_tiers:
content += f"๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ ๋“ฑ๊ธ‰: {', '.join(str(t) for t in rollover_tiers)}\n"
# ๋งˆ์ผ์Šคํ†ค ๋ชฉ๋ก
milestones = program.get("milestones", [])
if milestones:
content += "\n๋งˆ์ผ์Šคํ†ค ๋ณด์ƒ:\n"
for milestone in milestones[:10]:
if not isinstance(milestone, dict):
continue
nights = milestone.get("nights_required", "?")
is_bonus = milestone.get("is_bonus_choice", False)
max_sel = milestone.get("max_selections", 1)
content += f"\n [{nights}๋ฐ• ๋‹ฌ์„ฑ]"
if is_bonus:
content += f" (Bonus Choice - {max_sel}๊ฐœ ์„ ํƒ)"
content += ":\n"
# ๋ณด์ƒ ์˜ต์…˜
options = milestone.get("reward_options", [])
for opt in options[:6]:
if isinstance(opt, dict):
opt_name = opt.get("name", "N/A")
bonus_pts = opt.get("bonus_points")
quantity = opt.get("quantity")
opt_str = f" - {opt_name}"
if bonus_pts:
opt_str += f" ({bonus_pts:,}P)"
elif quantity:
opt_str += f" ({quantity}๊ฐœ)"
content += opt_str + "\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "milestone_program",
"program_name": program_name,
"chain": program_chain,
"minimum_tier": str(minimum_tier)
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ
# ===========================================================================
def handle_partner_status_matches(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""partner_status_matches ์ฒ˜๋ฆฌ (ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ)"""
chunks = []
chain = context.get("chain", "UNKNOWN")
matches = data if isinstance(data, list) else [data]
# ์—ฌ๋Ÿฌ ํŒŒํŠธ๋„ˆ๋ฅผ ํ•˜๋‚˜์˜ ์ฒญํฌ๋กœ ๋ฌถ๊ธฐ
if not matches:
return chunks
content = f"""
ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ ํ”„๋กœ๊ทธ๋žจ
์ฒด์ธ: {chain}
"""
for match in matches[:10]:
if not isinstance(match, dict):
continue
partner_name = match.get("partner_name", "N/A")
partner_program = match.get("partner_program", "")
partner_tier = match.get("partner_tier", "N/A")
required_tier = match.get("required_hotel_tier", "N/A")
validity = match.get("validity", "")
content += f"โ€ข {partner_name}"
if partner_program:
content += f" ({partner_program})"
content += f"\n"
content += f" - ํ•„์š” ํ˜ธํ…” ๋“ฑ๊ธ‰: {required_tier}\n"
content += f" - ๋ถ€์—ฌ ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰: {partner_tier}\n"
if validity:
content += f" - ์œ ํšจ ๊ธฐ๊ฐ„: {validity}\n"
# ํŒŒํŠธ๋„ˆ ํ˜œํƒ
partner_benefits = match.get("partner_benefits", [])
if partner_benefits:
content += f" - ํ˜œํƒ: {', '.join(partner_benefits[:3])}\n"
content += "\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "partner_status_match",
"chain": chain
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํฌ์ธํŠธ ์‹œ์Šคํ…œ
# ===========================================================================
def handle_points_systems(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""points_systems ๋˜๋Š” points_system ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
if data is None:
return chunks
systems = data if isinstance(data, list) else [data]
for system in systems[:3]:
if not isinstance(system, dict):
continue
try:
point_name = system.get("point_name", system.get("points_name", "N/A"))
content = f"""
ํฌ์ธํŠธ ์‹œ์Šคํ…œ: {point_name}
์ฒด์ธ: {system.get("chain", chain)}
"""
# ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: expiration_policy (PointExpirationPolicy)
# ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: expiry๋„ ํด๋ฐฑ ์ง€์›
expiration = system.get("expiration_policy", system.get("expiry"))
if expiration:
if isinstance(expiration, dict):
expires = expiration.get("expires", expiration.get("has_expiry", True))
exp_months = expiration.get("expiration_months", expiration.get("expiry_period"))
can_extend = expiration.get("can_extend_with_activity", False)
activity_types = expiration.get("activity_types", [])
content += f"\n๋งŒ๋ฃŒ: {'์žˆ์Œ' if expires else '์—†์Œ'}"
if exp_months:
content += f" ({exp_months}๊ฐœ์›”)"
content += "\n"
if can_extend:
content += f"ํ™œ๋™์œผ๋กœ ์—ฐ์žฅ: ๊ฐ€๋Šฅ"
if activity_types:
content += f" ({', '.join(activity_types)})"
content += "\n"
else:
content += f"\n๋งŒ๋ฃŒ: {expiration}\n"
# ์ ๋ฆฝ ๊ทœ์น™
content += "\n์ ๋ฆฝ ๊ทœ์น™:\n"
earning_rules = system.get("earning_rules", [])
if earning_rules and isinstance(earning_rules, list):
for rule in earning_rules[:6]:
if isinstance(rule, dict):
rule_name = rule.get("rule_name", rule.get("name", "N/A"))
category = rule.get("category", "")
points_per_unit = rule.get("points_per_unit")
spending_unit = rule.get("spending_unit", {})
content += f" - {rule_name}"
if category:
content += f" ({category})"
content += ": "
# ์Šคํ‚ค๋งˆ: points_per_unit, spending_unit (Money ๊ฐ์ฒด)
if points_per_unit and spending_unit:
# spending_unit์ด dict์ธ ๊ฒฝ์šฐ Money ๊ตฌ์กฐ ํ™•์ธ
if isinstance(spending_unit, dict):
unit_amount = spending_unit.get("amount", "N/A")
unit_currency = spending_unit.get("currency", "")
if unit_amount != "N/A" and unit_currency:
content += f"{unit_amount} {unit_currency}๋‹น {points_per_unit}P"
else:
content += f"{points_per_unit}P"
else:
# spending_unit์ด ๋‹จ์ˆœ ๊ฐ’์ธ ๊ฒฝ์šฐ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ)
content += f"{spending_unit}๋‹น {points_per_unit}P"
elif rule.get("rate"):
content += f"{rule.get('rate')}"
else:
content += "N/A"
content += "\n"
# ํ•œ๋„
monthly_cap = rule.get("monthly_cap")
yearly_cap = rule.get("yearly_cap")
if monthly_cap:
content += f" ์›” ํ•œ๋„: {monthly_cap:,}P\n"
if yearly_cap:
content += f" ์—ฐ ํ•œ๋„: {yearly_cap:,}P\n"
# ์‚ฌ์šฉ ๊ทœ์น™
redemption_rules = system.get("redemption_rules", [])
if redemption_rules:
content += "\n์‚ฌ์šฉ ๊ทœ์น™:\n"
for rule in redemption_rules[:3]:
if isinstance(rule, dict):
r_type = rule.get("redemption_type", "N/A")
min_points = rule.get("minimum_points", "N/A")
content += f" - {r_type}: ์ตœ์†Œ {min_points:,}P\n" if isinstance(min_points, int) else f" - {r_type}: ์ตœ์†Œ {min_points}P\n"
# ์ „ํ™˜ ํŒŒํŠธ๋„ˆ
transfer_partners = system.get("transfer_partners", [])
if transfer_partners:
content += f"\n์ „ํ™˜ ํŒŒํŠธ๋„ˆ: {', '.join(transfer_partners[:5])}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "points_system",
"point_name": point_name
}
})
except Exception as e:
# ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์Šคํ‚ตํ•˜๊ณ  ๊ณ„์†
continue
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ˜ธํ…” ํ”„๋กœํผํ‹ฐ
# ===========================================================================
def handle_hotel_properties(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""hotel_properties ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
hotels = data if isinstance(data, list) else [data]
for hotel in hotels[:5]:
if not isinstance(hotel, dict):
continue
hotel_name = hotel.get("name", "Unknown Hotel")
hotel_chain = hotel.get("chain", chain)
location = hotel.get("location", {})
# ํ•œ๊ตญ์–ด ์ด๋ฆ„ ์ถ”์ถœ
name_localized = hotel.get("name_localized", {})
name_ko = name_localized.get("ko") if isinstance(name_localized, dict) else None
content = f"ํ˜ธํ…”๋ช…: {hotel_name}\n"
if name_ko and name_ko != hotel_name:
content += f"ํ˜ธํ…”๋ช… (ํ•œ๊ตญ์–ด): {name_ko}\n"
content += f"""์ฒด์ธ: {hotel_chain}
๋ธŒ๋žœ๋“œ: {hotel.get('brand_code', 'N/A')}
์นดํ…Œ๊ณ ๋ฆฌ: {hotel.get('category', 'N/A')}
๊ฐ์‹ค ์ˆ˜: {hotel.get('room_count', 'N/A')}์‹ค
์ฃผ์†Œ: {location.get('address', 'N/A')}
๋„์‹œ: {location.get('city', 'N/A')}
๊ตญ๊ฐ€: {location.get('country', 'N/A')}
"""
# ๋ ˆ์Šคํ† ๋ž‘
restaurants = hotel.get("restaurant_names", [])
if restaurants:
content += f"๋ ˆ์Šคํ† ๋ž‘: {', '.join(restaurants)}\n"
# ๋ผ์šด์ง€
if hotel.get("lounge_name"):
content += f"๋ผ์šด์ง€: {hotel.get('lounge_name')}\n"
# ์ฐธ์—ฌ ์ฑ„๋„ (FHR, Virtuoso ๋“ฑ)
participating_channels = hotel.get("participating_channels", [])
if participating_channels:
content += f"์ฐธ์—ฌ ์ฑ„๋„: {', '.join(participating_channels)}\n"
# ํ˜ธํ…” ์‹๋ณ„์ž ๋ฐ ์œ„์น˜ ์ •๋ณด ์ถ”์ถœ
hotel_id = hotel.get("hotel_id")
country = location.get("country") if isinstance(location, dict) else None
city = location.get("city") if isinstance(location, dict) else None
region = hotel.get("region")
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ (hotel_id, chain, country ํฌํ•จ์œผ๋กœ ํ•„ํ„ฐ๋ง ๊ฐ•ํ™”)
metadata = {
"type": "hotel_property",
"hotel_name": hotel_name,
"hotel_name_ko": name_ko,
"brand": hotel.get("brand_code"),
"chain": hotel_chain,
}
if hotel_id:
metadata["hotel_id"] = hotel_id
if country:
metadata["country"] = country
if city:
metadata["city"] = city
if region:
metadata["region"] = region
chunks.append({
"content": content.strip(),
"metadata": metadata
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ˜ธํ…” ๋ธŒ๋žœ๋“œ
# ===========================================================================
def handle_hotel_brands(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""hotel_brands ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
brands = data if isinstance(data, list) else [data]
for brand in brands[:10]:
if not isinstance(brand, dict):
continue
brand_code = brand.get("brand_code", "N/A")
brand_name = brand.get("brand_name", "N/A")
brand_chain = brand.get("chain", chain)
tier = brand.get("tier", "N/A")
# ํ•œ๊ตญ์–ด ์ด๋ฆ„
name_localized = brand.get("brand_name_localized", {})
name_ko = name_localized.get("ko") if isinstance(name_localized, dict) else None
content = f"""
ํ˜ธํ…” ๋ธŒ๋žœ๋“œ: {brand_name}
"""
if name_ko and name_ko != brand_name:
content += f"๋ธŒ๋žœ๋“œ๋ช… (ํ•œ๊ตญ์–ด): {name_ko}\n"
content += f"""๋ธŒ๋žœ๋“œ ์ฝ”๋“œ: {brand_code}
์ฒด์ธ: {brand_chain}
ํ‹ฐ์–ด: {tier}
"""
# ๊ธฐ๋ณธ ํ˜œํƒ
default_benefits = brand.get("default_benefits", [])
if default_benefits:
content += "\n๋ธŒ๋žœ๋“œ ๊ธฐ๋ณธ ํ˜œํƒ:\n"
for benefit in default_benefits[:5]:
if isinstance(benefit, dict):
ben_name = benefit.get("name", "N/A")
content += f" - {ben_name}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "hotel_brand",
"brand_code": brand_code,
"brand_name": brand_name,
"tier": str(tier)
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
# ===========================================================================
def handle_tier_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""tier_implementations ์ฒ˜๋ฆฌ - ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ •๋ณด ํฌํ•จ์œผ๋กœ ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
hotel_id_map = context.get("hotel_id_map", {}) # hotel_id โ†’ ํ˜ธํ…” ์ •๋ณด ๋งคํ•‘
impls = data if isinstance(data, list) else [data]
for impl in impls[:10]:
if not isinstance(impl, dict):
continue
hotel_id = impl.get("hotel_id", "Unknown")
tier_level = impl.get("tier_level", "N/A")
impl_chain = impl.get("chain", chain)
# hotel_id_map์—์„œ ํ˜ธํ…” ์ •๋ณด ์กฐํšŒ (์šฐ์„ )
hotel_info = hotel_id_map.get(hotel_id, {})
hotel_name = hotel_info.get("name", "")
hotel_name_ko = hotel_info.get("name_ko", "")
country = hotel_info.get("country", "")
city = hotel_info.get("city", "")
# extra_attributes์—์„œ ๋ณด์™„ (ํด๋ฐฑ)
extra = impl.get("extra_attributes", {})
if not hotel_name:
hotel_name = extra.get("hotel_name", "")
if not country:
country = extra.get("country", "")
region = extra.get("region", "")
content = f"""
ํ˜ธํ…”๋ณ„ ๋ฉค๋ฒ„์‹ญ ํ˜œํƒ ๊ตฌํ˜„
"""
# ์‹ค์ œ ํ˜ธํ…”๋ช…์ด ์žˆ์œผ๋ฉด ์šฐ์„  ํ‘œ์‹œ
if hotel_name:
content += f"ํ˜ธํ…”: {hotel_name}\n"
if hotel_name_ko and hotel_name_ko != hotel_name:
content += f"ํ˜ธํ…” (ํ•œ๊ธ€๋ช…): {hotel_name_ko}\n"
content += f"ํ˜ธํ…” ID: {hotel_id}\n"
# ๊ตญ๊ฐ€/์ง€์—ญ ์ •๋ณด (๊ฒ€์ƒ‰ ๋งค์นญ์— ์ค‘์š”)
if country:
content += f"๊ตญ๊ฐ€: {country}\n"
if city:
content += f"๋„์‹œ: {city}\n"
if region:
content += f"์ง€์—ญ: {region}\n"
content += f"""์ฒด์ธ: {impl_chain}
๋“ฑ๊ธ‰: {tier_level}
๋ผ์šด์ง€ ์ด์šฉ: {'๊ฐ€๋Šฅ' if impl.get('lounge_access') else '๋ถˆ๊ฐ€'}
์กฐ์‹ ํฌํ•จ: {'ํฌํ•จ' if impl.get('breakfast_included') else '๋ฏธํฌํ•จ'}
์กฐ์‹ ์žฅ์†Œ: {impl.get('breakfast_venue', 'N/A')}
ํ˜œํƒ ์ƒ์„ธ:
"""
# ์—…๊ทธ๋ ˆ์ด๋“œ ์ •์ฑ… (upgrade_policy, suite_upgrade_available)
upgrade_policy = impl.get("upgrade_policy")
if upgrade_policy:
content += f"์—…๊ทธ๋ ˆ์ด๋“œ ์ •์ฑ…: {upgrade_policy}\n"
if impl.get("suite_upgrade_available"):
content += "์Šค์œ„ํŠธ ์—…๊ทธ๋ ˆ์ด๋“œ: ๊ฐ€๋Šฅ โœจ\n"
# ์กฐ์‹ ์ƒ์„ธ (breakfast_value)
breakfast_value = impl.get("breakfast_value")
if isinstance(breakfast_value, dict):
amount = breakfast_value.get("amount")
currency = breakfast_value.get("currency", "")
if amount:
content += f"์กฐ์‹ ๊ฐ€์น˜: {amount:,} {currency}\n"
# ๋ผ์šด์ง€ ์‹œ๊ฐ„ (lounge_hours)
lounge_hours = impl.get("lounge_hours")
if lounge_hours:
content += f"๋ผ์šด์ง€ ์šด์˜: {lounge_hours}\n"
content += "\nํ˜œํƒ ์ƒ์„ธ:\n"
# ํ˜œํƒ ์˜ค๋ฒ„๋ผ์ด๋“œ
overrides = impl.get("benefit_overrides", [])
for override in overrides[:5]:
if isinstance(override, dict):
category = override.get("benefit_category", "N/A")
detail = override.get("implementation_detail", "")[:150]
content += f"- {category}: {detail}\n"
# ํŒ ์ถ”๊ฐ€
tips = override.get("tips", [])
for tip in tips[:2]:
content += f" โ†’ {tip}\n"
# ๊ฒ€์ฆ ์ •๋ณด (last_verified) - ์ •๋ณด ์‹ ์„ ๋„ ํ‘œ์‹œ
last_verified = impl.get("last_verified")
if last_verified:
content += f"\n๋งˆ์ง€๋ง‰ ํ™•์ธ: {last_verified}\n"
content += DISCLAIMER_SHORT
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—๋„ ๊ตญ๊ฐ€ ์ •๋ณด ํฌํ•จ
metadata = {
"type": "tier_implementation",
"hotel_id": hotel_id,
"tier_level": tier_level
}
if country:
metadata["country"] = country
if hotel_name:
metadata["hotel_name"] = hotel_name
chunks.append({
"content": content.strip(),
"metadata": metadata
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํฌ๋ ˆ๋”ง ์นด๋“œ
# ===========================================================================
def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""credit_cards ์ฒ˜๋ฆฌ (ํ™•์žฅ: Phase 3 - ์—ฌํ–‰ ํŠนํ™” ํ˜œํƒ ํฌํ•จ)"""
chunks = []
chain = context.get("chain", "UNKNOWN")
cards = data if isinstance(data, list) else [data]
for card in cards[:5]:
if not isinstance(card, dict):
continue
card_name = card.get("card_name", "N/A")
card_chain = card.get("chain", chain)
card_type = card.get("card_type", "")
issuer = card.get("issuer", "N/A")
issuer_code = card.get("issuer_code", "")
# ์Šคํ‚ค๋งˆ ๊ตฌ์กฐ: AnnualFee.total (Money ๊ฐ์ฒด)
# ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๋ ˆ๊ฑฐ์‹œ annual_fee.amount๋„ ์ง€์›
annual_fee = card.get("annual_fee", {})
if isinstance(annual_fee, dict):
# ์Šคํ‚ค๋งˆ ๊ตฌ์กฐ: annual_fee.total.amount
total_fee = annual_fee.get("total", annual_fee) # ํด๋ฐฑ: ์ง์ ‘ ์ ‘๊ทผ
if isinstance(total_fee, dict):
fee_amount = total_fee.get("amount", "N/A")
fee_currency = total_fee.get("currency", "KRW")
else:
fee_amount = annual_fee.get("amount", "N/A")
fee_currency = annual_fee.get("currency", "KRW")
# ์ƒ์„ธ ์—ฐํšŒ๋น„ ์ •๋ณด
base_fee = annual_fee.get("base_fee", {})
partner_fee = annual_fee.get("partner_fee", {})
family_fee = annual_fee.get("family_card_fee", {})
waiver_conds = annual_fee.get("waiver_conditions", [])
else:
fee_amount = annual_fee
fee_currency = "KRW"
base_fee = partner_fee = family_fee = {}
waiver_conds = []
content = f"""
์‹ ์šฉ์นด๋“œ: {card_name}
๋ฐœ๊ธ‰์‚ฌ: {issuer}"""
if issuer_code:
content += f" ({issuer_code})"
# ์—ฐํšŒ๋น„ ํ‘œ์‹œ
if isinstance(fee_amount, (int, float)):
content += f"\n์—ฐํšŒ๋น„: {fee_amount:,} {fee_currency}\n"
else:
content += f"\n์—ฐํšŒ๋น„: {fee_amount} {fee_currency}\n"
# ์ƒ์„ธ ์—ฐํšŒ๋น„ (์Šคํ‚ค๋งˆ ๊ตฌ์กฐ ๋ฐ˜์˜)
if base_fee and isinstance(base_fee, dict) and base_fee.get("amount"):
content += f" - ๊ธฐ๋ณธ: {base_fee.get('amount'):,} {base_fee.get('currency', '')}\n"
if partner_fee and isinstance(partner_fee, dict) and partner_fee.get("amount"):
content += f" - ์ œํœด: {partner_fee.get('amount'):,} {partner_fee.get('currency', '')}\n"
if family_fee and isinstance(family_fee, dict) and family_fee.get("amount"):
content += f" - ๊ฐ€์กฑ์นด๋“œ: {family_fee.get('amount'):,} {family_fee.get('currency', '')}\n"
if waiver_conds:
# Handle both list of strings and list of dicts
waiver_strs = []
for item in waiver_conds[:2]:
if isinstance(item, dict):
waiver_strs.append(item.get("summary", item.get("condition", str(item))))
else:
waiver_strs.append(str(item))
content += f" - ๋ฉด์ œ ์กฐ๊ฑด: {', '.join(waiver_strs)}\n"
# ์นด๋“œ ์œ ํ˜• (Phase 3)
if card_type:
type_labels = {
"HOTEL_PLCC": "ํ˜ธํ…” ์ œํœด์นด๋“œ",
"AIRLINE_PLCC": "ํ•ญ๊ณต ์ œํœด์นด๋“œ",
"TRAVEL_PREMIUM": "ํ”„๋ฆฌ๋ฏธ์—„ ์—ฌํ–‰์นด๋“œ",
"GENERAL_REWARDS": "๋ฆฌ์›Œ๋“œ ์นด๋“œ"
}
content += f"์นด๋“œ ์œ ํ˜•: {type_labels.get(card_type, card_type)}\n"
# ์—ฐ๊ณ„ ํ”„๋กœ๊ทธ๋žจ
if card_chain and card_chain != "OTHER":
content += f"์—ฐ๊ณ„ ํ˜ธํ…”: {card_chain}\n"
linked_airline = card.get("linked_airline")
linked_program = card.get("linked_airline_program")
if linked_airline:
content += f"์—ฐ๊ณ„ ํ•ญ๊ณต์‚ฌ: {linked_airline}\n"
if linked_program:
content += f"๋งˆ์ผ๋ฆฌ์ง€ ํ”„๋กœ๊ทธ๋žจ: {linked_program}\n"
# ๋“ฑ๊ธ‰ ํ˜œํƒ
granted_tier = card.get("granted_membership_tier")
if granted_tier:
content += f"์ œ๊ณต ํ˜ธํ…” ๋“ฑ๊ธ‰: {granted_tier}\n"
granted_airline_tier = card.get("granted_airline_tier")
if granted_airline_tier:
content += f"์ œ๊ณต ํ•ญ๊ณต ๋“ฑ๊ธ‰: {granted_airline_tier}\n"
# ๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ (Phase 3)
miles_rate = card.get("miles_earning_rate")
if miles_rate:
content += f"๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ: {miles_rate}\n"
bonus_categories = card.get("bonus_miles_categories", [])
if bonus_categories:
content += f"๋ณด๋„ˆ์Šค ์ ๋ฆฝ ์นดํ…Œ๊ณ ๋ฆฌ: {', '.join(bonus_categories)}\n"
# ๊ฐ€์ž…/๊ธฐ๋…์ผ ๋ณด๋„ˆ์Šค
welcome_bonus = card.get("welcome_bonus")
welcome_points = card.get("welcome_bonus_points")
welcome_miles = card.get("welcome_bonus_miles")
if welcome_bonus or welcome_points or welcome_miles:
content += "\n๊ฐ€์ž… ํ˜œํƒ:\n"
if welcome_bonus:
content += f" - {welcome_bonus}\n"
if welcome_points:
content += f" - ๊ฐ€์ž… ๋ณด๋„ˆ์Šค: {welcome_points:,} ํฌ์ธํŠธ\n"
if welcome_miles:
content += f" - ๊ฐ€์ž… ๋ณด๋„ˆ์Šค: {welcome_miles:,} ๋งˆ์ผ\n"
# ๋ผ์šด์ง€ ํ˜œํƒ (Phase 3)
lounge_access = card.get("lounge_access", [])
lounge_visits = card.get("lounge_visits_per_year")
lounge_guest = card.get("lounge_guest_allowed", False)
if lounge_access or lounge_visits:
content += "\n๋ผ์šด์ง€ ํ˜œํƒ:\n"
if lounge_access:
# Handle both list of strings and list of dicts
lounge_names = []
for item in lounge_access[:5]:
if isinstance(item, dict):
lounge_names.append(item.get("lounge_name", item.get("name", str(item))))
else:
lounge_names.append(str(item))
content += f" - ์ด์šฉ ๊ฐ€๋Šฅ: {', '.join(lounge_names)}\n"
if lounge_visits:
content += f" - ์—ฐ๊ฐ„ ์ด์šฉ: {lounge_visits}ํšŒ\n"
if lounge_guest:
content += f" - ๋™๋ฐ˜์ž ์ด์šฉ: ๊ฐ€๋Šฅ\n"
# ๊ณตํ•ญ ์„œ๋น„์Šค (Phase 3)
airport_services = []
if card.get("airport_valet"):
airport_services.append("๋ฐœ๋ ›")
if card.get("airport_fast_track"):
airport_services.append("ํŒจ์ŠคํŠธํŠธ๋ž™")
if card.get("airport_transfer"):
airport_services.append("๊ณตํ•ญ ์ƒŒ๋”ฉ")
if airport_services:
content += f"\n๊ณตํ•ญ ์„œ๋น„์Šค: {', '.join(airport_services)}\n"
# ์—ฌํ–‰ ๋ณดํ—˜ (Phase 3)
if card.get("travel_insurance"):
insurance_detail = card.get("travel_insurance_details", "์—ฌํ–‰์ž ๋ณดํ—˜ ํฌํ•จ")
content += f"์—ฌํ–‰ ๋ณดํ—˜: {insurance_detail}\n"
# ์ปจ์‹œ์–ด์ง€/ํ˜ธํ…” ์„œ๋น„์Šค (Phase 3)
if card.get("concierge_service"):
content += "์ปจ์‹œ์–ด์ง€: 24์‹œ๊ฐ„ ์ด์šฉ ๊ฐ€๋Šฅ\n"
hotel_status = card.get("hotel_status_benefit")
if hotel_status:
content += f"ํ˜ธํ…” ํŠน๋ณ„ ํ˜œํƒ: {hotel_status}\n"
# ๊ธฐ์กด ํ˜œํƒ ๋ชฉ๋ก
benefits = card.get("benefits", card.get("card_benefits", []))
if benefits:
content += "\n์ฃผ์š” ํ˜œํƒ:\n"
for benefit in benefits[:7]:
if isinstance(benefit, dict):
ben_name = benefit.get('name', benefit.get('benefit_name', 'N/A'))
content += f" - {ben_name}\n"
elif isinstance(benefit, str):
content += f" - {benefit}\n"
# extra_attributes ์ฃผ์š” ์ •๋ณด ์ถœ๋ ฅ (๊ฒ€์ƒ‰ ๋ˆ„๋ฝ ๋ฐฉ์ง€)
extra_attrs = card.get("extra_attributes", {})
if isinstance(extra_attrs, dict):
target_members = extra_attrs.get("target_members", [])
benefit_period = extra_attrs.get("benefit_period", "")
hotel_count = extra_attrs.get("participating_hotel_count")
if target_members or benefit_period or hotel_count:
content += "\nํ”„๋กœ๊ทธ๋žจ ์ •๋ณด:\n"
if target_members:
if isinstance(target_members, list):
content += f" - ๋Œ€์ƒ ํšŒ์›: {', '.join(target_members[:3])}\n"
else:
content += f" - ๋Œ€์ƒ ํšŒ์›: {target_members}\n"
if benefit_period:
content += f" - ํ˜œํƒ ๊ธฐ๊ฐ„: {benefit_period}\n"
if hotel_count:
content += f" - ์ œํœด ํ˜ธํ…”: {hotel_count}๊ฐœ\n"
# evidence ์ •๋ณด ํฌํ•จ (์„ ํƒ์ )
evidence = card.get("evidence", [])
if evidence:
content += format_evidence(evidence, max_items=2)
content += DISCLAIMER_SHORT
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ ์ •๋ณด ํฌํ•จ
metadata = {
"type": "credit_card",
"card_name": card_name,
"issuer": issuer
}
if card_type:
metadata["card_type"] = card_type
if linked_airline:
metadata["linked_airline"] = linked_airline
chunks.append({
"content": content.strip(),
"metadata": metadata
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๊ตฌ๋… ํ”„๋กœ๊ทธ๋žจ
# ===========================================================================
def handle_subscription_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""subscription_programs ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
programs = data if isinstance(data, list) else [data]
for program in programs[:3]:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "N/A")
content = f"""
๊ตฌ๋… ํ”„๋กœ๊ทธ๋žจ: {program_name}
์ฒด์ธ: {program.get('chain', chain)}
๊ธฐ๋ณธ ๋ฉค๋ฒ„์‹ญ ํ•„์š”: {program.get('requires_base_membership', 'N/A')}
๊ธฐ๋ณธ ํ”„๋กœ๊ทธ๋žจ: {program.get('base_program_name', 'N/A')}
"""
# ํ”Œ๋žœ (์Šคํ‚ค๋งˆ: SubscriptionPlan.prices -> SubscriptionPlanPrice)
plans = program.get("plans", [])
for plan in plans[:5]:
if isinstance(plan, dict):
plan_name = plan.get("plan_name", "N/A")
prices = plan.get("prices", [])
price_str = ""
# ๊ฐ€๊ฒฉ ์ฒ˜๋ฆฌ ๊ฐœ์„  (์Šคํ‚ค๋งˆ์— ๋งž์ถค)
if prices:
for p in prices[:1]: # ์ฒซ ๋ฒˆ์งธ ๊ฐ€๊ฒฉ๋งŒ
if isinstance(p, dict):
billing_cycle = p.get("billing_cycle", "YEARLY")
price_info = p.get("price", {})
if isinstance(price_info, dict):
amount = price_info.get("amount", "N/A")
currency = price_info.get("currency", "")
# ๊ฒฐ์ œ ์ฃผ๊ธฐ๋ฅผ ํ•œ๊ตญ์–ด๋กœ ๋ณ€ํ™˜
cycle_kr = {
"YEARLY": "๋…„",
"MONTHLY": "์›”",
"QUARTERLY": "๋ถ„๊ธฐ",
"WEEKLY": "์ฃผ"
}.get(billing_cycle, billing_cycle.lower())
if amount != "N/A" and currency:
price_str = f"{amount} {currency}/{cycle_kr}"
else:
price_str = "๊ฐ€๊ฒฉ ์ •๋ณด ์—†์Œ"
else:
price_str = str(price_info) if price_info else "๊ฐ€๊ฒฉ ์ •๋ณด ์—†์Œ"
content += f"\nํ”Œ๋žœ: {plan_name}\n๊ฐ€๊ฒฉ: {price_str}\n"
# ํ”Œ๋žœ ํ˜œํƒ
plan_benefits = plan.get("benefits", [])
if plan_benefits:
content += "ํ˜œํƒ:\n"
for pb in plan_benefits[:3]:
if isinstance(pb, dict):
content += f"- {pb.get('name', 'N/A')}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "subscription_program",
"program_name": program_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ”„๋กœ๋ชจ์…˜
# ===========================================================================
def handle_promotions(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""promotions ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
promos = data if isinstance(data, list) else [data]
for promo in promos[:5]:
if not isinstance(promo, dict):
continue
title = promo.get("title", "N/A")
content = f"""
ํ”„๋กœ๋ชจ์…˜: {title}
์ฒด์ธ: {promo.get('chain', chain)}
๋Œ€์ƒ: {promo.get('target_audience', 'N/A')}
๊ธฐ๊ฐ„: {promo.get('valid_from', 'N/A')} ~ {promo.get('valid_until', 'N/A')}
์ง€์—ญ: {', '.join(promo.get('regions', []) or ['N/A'])}
์„ค๋ช…: {promo.get('description', 'N/A')}
"""
chunks.append({
"content": content.strip(),
"metadata": {
"type": "promotion",
"title": title
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๊ฐ€๊ฒฉ/ํฌ์ธํŠธ (pricing, pricing_analysis)
# ===========================================================================
def handle_pricing(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""pricing, pricing_2026 ๋“ฑ ๋‹ค์–‘ํ•œ ํ˜•์‹ ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
# --- ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ (raw_pricing_data ๋“ฑ) ---
if isinstance(data, list):
for item in data[:10]:
if not isinstance(item, dict):
continue
room_type = item.get("room_type_code", item.get("room_type", ""))
room_name = item.get("room_type_full_name", "")
rate_type = item.get("rate_type", "")
rate_krw = item.get("rate_krw", 0)
rate_usd = item.get("rate_usd_approx", 0)
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"๊ฐ์‹ค ์œ ํ˜•: {room_type}\n"
if room_name:
content += f"๊ฐ์‹ค๋ช…: {room_name}\n"
if rate_type:
content += f"์š”๊ธˆ ์œ ํ˜•: {rate_type}\n"
content += f"๊ฐ€๊ฒฉ:\n"
if rate_krw:
content += f" - KRW: โ‚ฉ{rate_krw:,}\n"
if rate_usd:
content += f" - USD: ${rate_usd}\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "room_pricing_raw",
"hotel_name": hotel_name,
"room_type": room_type,
"rate_krw": rate_krw
}
})
return chunks
# ๋”•์…”๋„ˆ๋ฆฌ ๊ตฌ์กฐ๊ฐ€ ์•„๋‹ˆ๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜
if not isinstance(data, dict):
return chunks
# --- ํ˜•์‹ 1: annual_statistics ๊ตฌ์กฐ (Westin ๋“ฑ) ---
annual_stats = data.get("annual_statistics", {})
points_stats = annual_stats.get("points", {})
if points_stats:
min_points = points_stats.get("minimum", {})
max_points = points_stats.get("maximum", {})
avg_points = points_stats.get("average_estimate")
cash_stats = annual_stats.get("cash", {})
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"""์˜ˆ์•ฝ ์œ ํ˜•: ํฌ์ธํŠธ ์ˆ™๋ฐ•
ํฌ์ธํŠธ ํ•„์š”๋Ÿ‰:
- ์ตœ์†Œ: {min_points.get('points', 'N/A'):,}P ({min_points.get('period', '๋น„์ˆ˜๊ธฐ ํ‰์ผ')})
- ์ตœ๋Œ€: {max_points.get('points', 'N/A'):,}P ({max_points.get('period', '์„ฑ์ˆ˜๊ธฐ/์ฃผ๋ง')})
"""
if avg_points:
content += f" - ํ‰๊ท : {avg_points:,}P (์ถ”์ •)\n"
if cash_stats:
min_cash = cash_stats.get("minimum", {})
content += f"""
ํ˜„๊ธˆ ๊ฐ€๊ฒฉ ๋น„๊ต:
- ์ตœ์ €๊ฐ€: โ‚ฉ{min_cash.get('amount', 'N/A'):,} ({min_cash.get('period', '')})
"""
blackout = data.get("points_blackout_dates", [])
if blackout:
content += f"\nํฌ์ธํŠธ ์˜ˆ์•ฝ ๋ถˆ๊ฐ€ ๊ธฐ๊ฐ„: {', '.join(blackout[:5])}"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "points_pricing",
"hotel_name": hotel_name,
"hotel_name_ko": hotel_name_ko,
"min_points": min_points.get("points"),
"max_points": max_points.get("points")
}
})
# --- ํ˜•์‹ 2: points_rates ๋ฐฐ์—ด ๊ตฌ์กฐ (JW Marriott ๋“ฑ) ---
points_rates = data.get("points_rates", [])
if points_rates:
# ํฌ์ธํŠธ ๋ฒ”์œ„ ๊ณ„์‚ฐ
all_min = []
all_max = []
for rate in points_rates:
if rate.get("min_points"):
all_min.append(rate["min_points"])
if rate.get("max_points"):
all_max.append(rate["max_points"])
min_points = min(all_min) if all_min else None
max_points = max(all_max) if all_max else None
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"""์˜ˆ์•ฝ ์œ ํ˜•: ํฌ์ธํŠธ ์ˆ™๋ฐ•
ํฌ์ธํŠธ ํ•„์š”๋Ÿ‰:
- ์ตœ์†Œ: {min_points:,}P (์—ฐ๊ฐ„)
- ์ตœ๋Œ€: {max_points:,}P (์—ฐ๊ฐ„)
"""
# ํ˜„๊ธˆ ๊ฐ€๊ฒฉ๋„ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
cash_rates = data.get("cash_rates", [])
if cash_rates:
all_cash_min = [r.get("min_krw") for r in cash_rates if r.get("min_krw")]
all_cash_max = [r.get("max_krw") for r in cash_rates if r.get("max_krw")]
if all_cash_min:
content += f"""
ํ˜„๊ธˆ ๊ฐ€๊ฒฉ ๋น„๊ต:
- ์ตœ์ €๊ฐ€: โ‚ฉ{min(all_cash_min):,} (์—ฐ๊ฐ„)
- ์ตœ๊ณ ๊ฐ€: โ‚ฉ{max(all_cash_max):,} (์—ฐ๊ฐ„)
"""
# ์›”๋ณ„ ์š”์•ฝ ์ถ”๊ฐ€
content += "\n์›”๋ณ„ ํฌ์ธํŠธ:\n"
for rate in points_rates[:6]:
month = rate.get("month", "?")
min_p = rate.get("min_points", "?")
max_p = rate.get("max_points", "?")
content += f" - {month}์›”: {min_p:,}P ~ {max_p:,}P\n"
if len(points_rates) > 6:
content += f" ... (์™ธ {len(points_rates) - 6}๊ฐœ์›”)\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "points_pricing",
"hotel_name": hotel_name,
"hotel_name_ko": hotel_name_ko,
"min_points": min_points,
"max_points": max_points
}
})
# --- ์›”๋ณ„ ๋ฐ์ดํ„ฐ (๊ธฐ์กด) ---
monthly_data = data.get("monthly_data", {})
if monthly_data:
monthly_content = format_hotel_header(hotel_name, hotel_name_ko, chain)
monthly_content += "์›”๋ณ„ ํฌ์ธํŠธ ์ •๋ณด:\n"
for month, mdata in list(monthly_data.items())[:6]:
points_range = mdata.get("points_range", "N/A")
monthly_content += f" - {month}: {points_range}\n"
monthly_content += DISCLAIMER_SHORT
chunks.append({
"content": monthly_content.strip(),
"metadata": {
"type": "points_monthly",
"hotel_name": hotel_name
}
})
return chunks
def handle_pricing_analysis(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""pricing_analysis ํ˜•์‹ ์ฒ˜๋ฆฌ (Josun Palace ๋“ฑ)"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
# ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ์ธ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ (ClassicTravel ๋“ฑ)
if isinstance(data, list):
for item in data[:10]:
if not isinstance(item, dict):
continue
hotel_id = item.get("hotel_id", "")
item_hotel_name = item.get("hotel_name", hotel_name)
search_date = item.get("search_date", "")
stay_nights = item.get("stay_nights", 1)
currency = item.get("currency", "KRW")
content = format_hotel_header(item_hotel_name, hotel_name_ko, chain)
content += f"ํ˜ธํ…” ID: {hotel_id}\n"
content += f"๊ฒ€์ƒ‰ ๋‚ ์งœ: {search_date}\n"
content += f"์ˆ™๋ฐ• ๊ธฐ๊ฐ„: {stay_nights}๋ฐ•\n\n"
room_rates = item.get("room_rates", [])
if room_rates:
content += "๊ฐ์‹ค ์š”๊ธˆ:\n"
for rate in room_rates[:5]:
room_type = rate.get("room_type", "")
rate_plan = rate.get("rate_plan", "")
rate_desc = rate.get("rate_plan_description", "")
price_krw = rate.get("price_krw", 0)
content += f" - {room_type}\n"
content += f" ์š”๊ธˆ์ œ: {rate_plan}\n"
if rate_desc:
content += f" ์„ค๋ช…: {rate_desc}\n"
content += f" ๊ฐ€๊ฒฉ: โ‚ฉ{price_krw:,}\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "room_pricing",
"hotel_name": item_hotel_name,
"hotel_id": hotel_id,
"currency": currency
}
})
return chunks
# ๊ธฐ์กด ๋”•์…”๋„ˆ๋ฆฌ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
if not isinstance(data, dict):
return chunks
points_stats = data.get("points_price_stats", {})
cash_stats = data.get("cash_price_stats", {})
if not points_stats:
return chunks
min_points = points_stats.get("lowest_points")
max_points = points_stats.get("highest_points")
avg_points = points_stats.get("annual_average_estimated")
min_period = points_stats.get("lowest_points_period", "๋น„์ˆ˜๊ธฐ ํ‰์ผ")
max_period = points_stats.get("highest_points_period", "์„ฑ์ˆ˜๊ธฐ/์ฃผ๋ง")
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"""์˜ˆ์•ฝ ์œ ํ˜•: ํฌ์ธํŠธ ์ˆ™๋ฐ•
ํฌ์ธํŠธ ํ•„์š”๋Ÿ‰:
- ์ตœ์†Œ: {min_points:,}P ({min_period})
- ์ตœ๋Œ€: {max_points:,}P ({max_period})
"""
if avg_points:
content += f" - ํ‰๊ท : {avg_points:,}P (์ถ”์ •)\n"
# ํ˜„๊ธˆ ๊ฐ€๊ฒฉ
if cash_stats:
min_cash = cash_stats.get("lowest_price")
min_cash_period = cash_stats.get("lowest_price_period", "")
if min_cash:
content += f"""
ํ˜„๊ธˆ ๊ฐ€๊ฒฉ ๋น„๊ต:
- ์ตœ์ €๊ฐ€: โ‚ฉ{min_cash:,} ({min_cash_period})
"""
# ์‹œ์ฆŒ๋ณ„ ํŒจํ„ด
seasonal = data.get("seasonal_patterns", [])
if seasonal:
content += "\n์‹œ์ฆŒ๋ณ„ ๊ฐ€๊ฒฉ ํŒจํ„ด:\n"
for pattern in seasonal[:4]:
content += f" - {pattern.get('season', '')}: {pattern.get('price_range', '')} ({pattern.get('period', '')})\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "points_pricing",
"hotel_name": hotel_name,
"hotel_name_ko": hotel_name_ko,
"min_points": min_points,
"max_points": max_points
}
})
# ์ธ์‚ฌ์ดํŠธ
insights = data.get("pricing_insights", [])
if insights:
insight_content = format_hotel_header(hotel_name, hotel_name_ko, chain)
insight_content += "์˜ˆ์•ฝ ํŒ:\n"
for insight in insights[:5]:
insight_content += f" - {insight}\n"
insight_content += DISCLAIMER_SHORT
chunks.append({
"content": insight_content.strip(),
"metadata": {
"type": "pricing_insight",
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ˜œํƒ
# ===========================================================================
def handle_benefits(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""benefits ์ฒ˜๋ฆฌ"""
chunks = []
benefits = data if isinstance(data, list) else [data]
for benefit in benefits[:10]:
if not isinstance(benefit, dict):
continue
ben_name = benefit.get("benefit_name", benefit.get("name", "N/A"))
content = f"""
ํ˜œํƒ: {ben_name}
์นดํ…Œ๊ณ ๋ฆฌ: {benefit.get('category', 'N/A')}
์„ค๋ช…: {benefit.get('description', benefit.get('details', 'N/A'))}
"""
chunks.append({
"content": content.strip(),
"metadata": {
"type": "benefit",
"benefit_name": ben_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: Best Rate Guarantee
# ===========================================================================
def handle_best_rate_guarantee(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""best_rate_guarantee ์ฒ˜๋ฆฌ (์Šคํ‚ค๋งˆ: BestRateGuarantee, BRGRule, BRGRemedy)
์ฒญํฌ ๋ถ„๋ฆฌ ์ „๋žต:
1. ํ”„๋กœ๊ทธ๋žจ ๊ฐœ์š” ์ฒญํฌ (๊ธฐ๋ณธ ์ •๋ณด, ์ œ์™ธ ์‚ฌํ•ญ)
2. ๊ทœ์น™๋ณ„ ์ฒญํฌ (๊ฐ BRGRule์„ ๊ฐœ๋ณ„ ์ฒญํฌ๋กœ)
"""
chunks = []
chain = context.get("chain", "UNKNOWN")
if not isinstance(data, dict):
return chunks
program_name = data.get("program_name", "Best Rate Guarantee")
brg_chain = data.get("chain", chain)
# === ์ฒญํฌ 1: ํ”„๋กœ๊ทธ๋žจ ๊ฐœ์š” ===
overview_content = f"""
์ตœ์ €๊ฐ€ ๋ณด์žฅ ํ”„๋กœ๊ทธ๋žจ: {program_name}
์ฒด์ธ: {brg_chain}
๋ฉค๋ฒ„์‹ญ ํ•„์š”: {"์˜ˆ" if data.get("requires_membership") else "์•„๋‹ˆ์˜ค"}
"""
# ์ œ์™ธ ์‚ฌํ•ญ (์Šคํ‚ค๋งˆ ํ•„๋“œ)
excluded_regions = data.get("excluded_regions", [])
excluded_brands = data.get("excluded_brands", [])
excluded_hotels = data.get("excluded_hotels", [])
if excluded_regions:
overview_content += f"์ œ์™ธ ์ง€์—ญ: {', '.join(excluded_regions[:5])}\n"
if excluded_brands:
overview_content += f"์ œ์™ธ ๋ธŒ๋žœ๋“œ: {', '.join(excluded_brands[:5])}\n"
if excluded_hotels:
overview_content += f"์ œ์™ธ ํ˜ธํ…”: {', '.join(excluded_hotels[:3])}\n"
# ์•ฝ๊ด€
if data.get("governing_law"):
overview_content += f"\n์ค€๊ฑฐ๋ฒ•: {data.get('governing_law')}\n"
if data.get("arbitration_clause"):
overview_content += "์ค‘์žฌ ์กฐํ•ญ: ํฌํ•จ\n"
# ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
if data.get("last_updated"):
overview_content += f"๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {data.get('last_updated')}\n"
overview_content += DISCLAIMER_SHORT
chunks.append({
"content": overview_content.strip(),
"metadata": {
"type": "best_rate_guarantee_overview",
"program_name": program_name,
"chain": brg_chain
}
})
# === ์ฒญํฌ 2+: ๊ทœ์น™๋ณ„ ์ƒ์„ธ (์Šคํ‚ค๋งˆ: BRGRule) ===
rules = data.get("rules", [])
for rule in rules[:5]: # ์ตœ๋Œ€ 5๊ฐœ ๊ทœ์น™
if not isinstance(rule, dict):
continue
rule_name = rule.get("rule_name", "N/A")
summary = rule.get("summary", "")
rule_content = f"""
{program_name} - {rule_name}
์ฒด์ธ: {brg_chain}
"""
if summary:
rule_content += f"์š”์•ฝ: {summary}\n\n"
# ํด๋ ˆ์ž„ ์กฐ๊ฑด (์Šคํ‚ค๋งˆ ํ•„๋“œ)
claim_window = rule.get("claim_window")
min_hours = rule.get("minimum_hours_before_checkin")
rate_diff = rule.get("rate_difference_threshold_percent")
max_rooms = rule.get("max_rooms_per_claim")
if claim_window or min_hours or rate_diff or max_rooms:
rule_content += "ํด๋ ˆ์ž„ ์กฐ๊ฑด:\n"
if claim_window:
rule_content += f" - ํด๋ ˆ์ž„ ๊ฐ€๋Šฅ ๊ธฐ๊ฐ„: {claim_window}\n"
if min_hours:
rule_content += f" - ์ฒดํฌ์ธ ์ „ ์ตœ์†Œ: {min_hours}์‹œ๊ฐ„\n"
if rate_diff:
rule_content += f" - ์ตœ์†Œ ๊ฐ€๊ฒฉ ์ฐจ์ด: {rate_diff}%\n"
if max_rooms:
rule_content += f" - ํด๋ ˆ์ž„๋‹น ์ตœ๋Œ€ ๊ฐ์‹ค: {max_rooms}์‹ค\n"
# ๋น„๊ต ๊ธฐ์ค€
comparison = rule.get("comparison_criteria", [])
if comparison:
rule_content += f"\n๋น„๊ต ๊ธฐ์ค€:\n"
for c in comparison[:6]:
rule_content += f" - {c}\n"
# ์ฑ„๋„ ์š”๊ตฌ์‚ฌํ•ญ
channel_reqs = rule.get("channel_requirements", [])
if channel_reqs:
rule_content += f"\n์˜ˆ์•ฝ ์ฑ„๋„: {', '.join(channel_reqs)}\n"
# ์ ์šฉ/์ œ์™ธ ์š”๊ธˆ
eligible_rates = rule.get("eligible_rates", [])
ineligible_rates = rule.get("ineligible_rates", [])
if eligible_rates:
rule_content += f"\n์ ์šฉ ์š”๊ธˆ:\n"
for r in eligible_rates[:5]:
rule_content += f" - {r}\n"
if ineligible_rates:
rule_content += f"\n์ œ์™ธ ์š”๊ธˆ:\n"
for r in ineligible_rates[:8]:
rule_content += f" - {r}\n"
# ๋ณด์ƒ (์Šคํ‚ค๋งˆ: BRGRemedy)
remedies = rule.get("remedies", [])
if remedies:
rule_content += "\n๋ณด์ƒ:\n"
for remedy in remedies[:3]:
if isinstance(remedy, dict):
r_type = remedy.get("type", "N/A")
r_desc = remedy.get("description", "")
rule_content += f" [{r_type}] {r_desc}\n"
# ๊ฐ€์น˜ (BenefitValue ๊ตฌ์กฐ)
value = remedy.get("value", {})
if isinstance(value, dict):
v_type = value.get("type")
v_text = value.get("text", "")
if v_text:
rule_content += f" - ๊ฐ€์น˜: {v_text}\n"
elif v_type == "PERCENT" and value.get("percent"):
rule_content += f" - ๊ฐ€์น˜: {value.get('percent')}%\n"
elif v_type == "POINTS" and value.get("points"):
rule_content += f" - ๊ฐ€์น˜: {value.get('points'):,}P\n"
# ์ตœ๋Œ€ ๋ณด์ƒ (Money ๊ตฌ์กฐ)
max_val = remedy.get("max_value", {})
if isinstance(max_val, dict) and max_val.get("amount"):
rule_content += f" - ์ตœ๋Œ€: {max_val.get('amount'):,} {max_val.get('currency', '')}\n"
# ์ถ”๊ฐ€ ์กฐ๊ฑด
conditions = rule.get("conditions", [])
if conditions:
rule_content += "\n์ถ”๊ฐ€ ์กฐ๊ฑด:\n"
for cond in conditions[:5]:
if isinstance(cond, dict):
cond_summary = cond.get("summary", "")
if cond_summary:
rule_content += f" - {cond_summary}\n"
rule_content += DISCLAIMER_SHORT
if len(rule_content) > 100:
chunks.append({
"content": rule_content.strip(),
"metadata": {
"type": "best_rate_guarantee_rule",
"program_name": program_name,
"rule_name": rule_name,
"chain": brg_chain
}
})
# === ํ•˜์œ„ ํ˜ธํ™˜: ๊ธฐ์กด conditions ํ˜•์‹ ์ง€์› (rules๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) ===
conditions = data.get("conditions", [])
if conditions and not rules:
cond_content = f"""
{program_name} - ์ด์šฉ ์กฐ๊ฑด
์ฒด์ธ: {brg_chain}
์ด์šฉ ์กฐ๊ฑด:
"""
for cond in conditions[:5]:
if isinstance(cond, dict):
cond_content += f" - {cond.get('description', cond.get('summary', cond))}\n"
else:
cond_content += f" - {cond}\n"
exclusions = data.get("exclusions", [])
if exclusions:
cond_content += "\n์ œ์™ธ ์‚ฌํ•ญ:\n"
for exc in exclusions[:5]:
if isinstance(exc, str):
cond_content += f" - {exc}\n"
cond_content += DISCLAIMER_SHORT
chunks.append({
"content": cond_content.strip(),
"metadata": {
"type": "best_rate_guarantee_conditions",
"program_name": program_name,
"chain": brg_chain
}
})
# === ์ฒญํฌ: ํด๋ ˆ์ž„ ๊ฑฐ๋ถ€ ์‚ฌ์œ  (extra_attributes) ===
extra_attrs = data.get("extra_attributes", {})
if extra_attrs and isinstance(extra_attrs, dict):
rejection_reasons = extra_attrs.get("claim_rejection_reasons", [])
if rejection_reasons:
rejection_content = f"""
{program_name} - ํด๋ ˆ์ž„ ๊ฑฐ๋ถ€ ์‚ฌ์œ 
์ฒด์ธ: {brg_chain}
ํด๋ ˆ์ž„์ด ๊ฑฐ๋ถ€๋  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์œ :
"""
for reason in rejection_reasons:
rejection_content += f" - {reason}\n"
# ์ถ”๊ฐ€ ์†์„ฑ
if extra_attrs.get("nj_resident_exception"):
rejection_content += f"\n๋‰ด์ €์ง€ ๊ฑฐ์ฃผ์ž ์˜ˆ์™ธ: {extra_attrs.get('nj_resident_exception')}\n"
if extra_attrs.get("modification_rights"):
rejection_content += f"ํ”„๋กœ๊ทธ๋žจ ์ˆ˜์ • ๊ถŒํ•œ: {extra_attrs.get('modification_rights')}\n"
if extra_attrs.get("employee_exclusion"):
rejection_content += f"์ง์› ์ œ์™ธ: {extra_attrs.get('employee_exclusion')}\n"
rejection_content += DISCLAIMER_SHORT
chunks.append({
"content": rejection_content.strip(),
"metadata": {
"type": "best_rate_guarantee_rejection",
"program_name": program_name,
"chain": brg_chain
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: Channel Implementations
# ===========================================================================
def handle_channel_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""channel_implementations ์ฒ˜๋ฆฌ - ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ •๋ณด ํฌํ•จ์œผ๋กœ ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
impls = data if isinstance(data, list) else [data]
for impl in impls[:10]:
if not isinstance(impl, dict):
continue
program_name = impl.get("program_name", "N/A")
hotel_id = impl.get("hotel_id", "N/A")
# extra_attributes์—์„œ ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ถ”์ถœ (๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ)
extra = impl.get("extra_attributes", {})
country = extra.get("country", "")
hotel_name = extra.get("hotel_name", "")
region = extra.get("region", "")
content = f"""
์ฑ„๋„ ํ”„๋กœ๊ทธ๋žจ: {program_name}
"""
# ์‹ค์ œ ํ˜ธํ…”๋ช…์ด ์žˆ์œผ๋ฉด ์šฐ์„  ํ‘œ์‹œ
if hotel_name:
content += f"ํ˜ธํ…”: {hotel_name}\n"
else:
content += f"ํ˜ธํ…” ID: {hotel_id}\n"
# ๊ตญ๊ฐ€/์ง€์—ญ ์ •๋ณด (๊ฒ€์ƒ‰ ๋งค์นญ์— ์ค‘์š”)
if country:
content += f"๊ตญ๊ฐ€: {country}\n"
if region:
content += f"์ง€์—ญ: {region}\n"
content += f"""์ฑ„๋„: {impl.get('channel', 'DIRECT')}
"""
# ํ˜œํƒ ์˜ค๋ฒ„๋ผ์ด๋“œ
overrides = impl.get("benefit_overrides", [])
if overrides:
content += "\nํ˜œํƒ:\n"
for override in overrides[:5]:
if isinstance(override, dict):
category = override.get("benefit_category", "N/A")
detail = override.get("implementation_detail", "")[:100]
content += f"- {category}: {detail}\n"
# ํŒ ์ถ”๊ฐ€
tips = override.get("tips", [])
for tip in tips[:2]:
content += f" โ†’ {tip}\n"
# ์ถ”๊ฐ€ ํ˜œํƒ (additional_benefits) ์ถœ๋ ฅ - ๊ฒ€์ƒ‰ ๋ˆ„๋ฝ ๋ฐฉ์ง€
additional = impl.get("additional_benefits", [])
if additional:
content += "\n์ถ”๊ฐ€ ํ˜œํƒ:\n"
for benefit in additional[:5]:
if isinstance(benefit, dict):
ben_name = benefit.get("name", "N/A")
ben_desc = (benefit.get("description", "") or "")[:120]
ben_category = benefit.get("category", "")
content += f"- {ben_name}"
if ben_category:
content += f" [{ben_category}]"
if ben_desc and ben_desc != ben_name:
content += f": {ben_desc}"
content += "\n"
# ๊ฐ€์น˜ ํ‘œ์‹œ
value = benefit.get("value", {})
if isinstance(value, dict):
v_type = value.get("type")
if v_type == "PERCENT" and value.get("percent"):
content += f" โ†’ ํ• ์ธ์œจ: {value.get('percent')}%\n"
elif v_type == "POINTS" and value.get("points"):
content += f" โ†’ ํฌ์ธํŠธ: {value.get('points'):,}P\n"
# ์ผ๋ฐ˜ ํŒ (general_tips) - ๊ฒ€์ƒ‰ ํ’ˆ์งˆ ํ–ฅ์ƒ
general_tips = impl.get("general_tips", [])
if general_tips:
content += "\n์ด์šฉ ํŒ:\n"
for tip in general_tips[:5]:
content += f" ๐Ÿ’ก {tip}\n"
# ์˜ˆ์•ฝ ๊ด€๋ จ ์ •๋ณด (booking_notes, contact_for_benefits)
booking_notes = impl.get("booking_notes")
if booking_notes:
content += f"\n์˜ˆ์•ฝ ์‹œ ์ฐธ๊ณ : {booking_notes}\n"
contact = impl.get("contact_for_benefits")
if contact:
content += f"ํ˜œํƒ ๋ฌธ์˜: {contact}\n"
# ๊ฒ€์ฆ ์ •๋ณด (last_verified, verified_by) - ์ •๋ณด ์‹ ์„ ๋„ ํ‘œ์‹œ
last_verified = impl.get("last_verified")
verified_by = impl.get("verified_by")
if last_verified:
content += f"\n๋งˆ์ง€๋ง‰ ํ™•์ธ: {last_verified}"
if verified_by:
content += f" ({verified_by})"
content += "\n"
content += DISCLAIMER_SHORT
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—๋„ ๊ตญ๊ฐ€ ์ •๋ณด ํฌํ•จ
metadata = {
"type": "channel_implementation",
"program_name": program_name,
"hotel_id": hotel_id,
"chain": context.get("chain", "UNKNOWN") # chain ์ •๋ณด ์ถ”๊ฐ€
}
if country:
metadata["country"] = country
if hotel_name:
metadata["hotel_name"] = hotel_name
if last_verified:
metadata["last_verified"] = str(last_verified)
chunks.append({
"content": content.strip(),
"metadata": metadata
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: Channel Benefit Packages (FHR ๋“ฑ)
# ===========================================================================
def handle_channel_benefit_packages(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""channel_benefit_packages ์ฒ˜๋ฆฌ (FHR, THC ๋“ฑ) - ์Šคํ‚ค๋งˆ: ChannelBenefitPackage"""
chunks = []
packages = data if isinstance(data, list) else [data]
for pkg in packages[:5]:
if not isinstance(pkg, dict):
continue
pkg_name = pkg.get("package_name", pkg.get("name", "N/A"))
channel = pkg.get("channel", pkg.get("agent", "N/A"))
content = f"""
ํ˜œํƒ ํŒจํ‚ค์ง€: {pkg_name}
์ฑ„๋„: {channel}
"""
# ํŠน์ • ์š”๊ธˆ์ œ ํ•„์š” ์—ฌ๋ถ€
if pkg.get("requires_specific_rate"):
content += "ํŠน์ • ์š”๊ธˆ์ œ ํ•„์š”: ์˜ˆ\n"
eligible_rates = pkg.get("eligible_rates", [])
if eligible_rates:
content += f"์ ์šฉ ๊ฐ€๋Šฅ ์š”๊ธˆ: {', '.join(eligible_rates)}\n"
# ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: standard_benefits
# ํ•˜์œ„ ํ˜ธํ™˜: benefits, included_benefits๋„ ์ง€์›
benefits = pkg.get("standard_benefits", pkg.get("benefits", pkg.get("included_benefits", [])))
if benefits:
content += "\nํฌํ•จ ํ˜œํƒ:\n"
for benefit in benefits[:8]:
if isinstance(benefit, dict):
ben_name = benefit.get("name", benefit.get("benefit_name", "N/A"))
ben_desc = benefit.get("description", "")[:60]
ben_category = benefit.get("category", "")
content += f" - {ben_name}"
if ben_category:
content += f" [{ben_category}]"
if ben_desc:
content += f": {ben_desc}"
content += "\n"
# ๊ฐ€์น˜ ํ‘œ์‹œ (BenefitValue ๊ตฌ์กฐ)
value = benefit.get("value", {})
if isinstance(value, dict):
v_type = value.get("type")
if v_type == "MONEY" and value.get("money"):
money = value.get("money", {})
content += f" ๊ฐ€์น˜: {money.get('amount', 'N/A')} {money.get('currency', '')}\n"
elif v_type == "TEXT" and value.get("text"):
content += f" ๊ฐ€์น˜: {value.get('text')}\n"
elif v_type == "PERCENT" and value.get("percent"):
content += f" ๊ฐ€์น˜: {value.get('percent')}%\n"
elif isinstance(benefit, str):
content += f" - {benefit}\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "channel_benefit_package",
"package_name": pkg_name,
"channel": channel
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: Member Rates (ํšŒ์› ์š”๊ธˆ)
# ===========================================================================
def handle_member_rates(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""member_rates ์ฒ˜๋ฆฌ (ํšŒ์› ์ „์šฉ ์š”๊ธˆ) - ์Šคํ‚ค๋งˆ MemberRate ํ•„๋“œ ๊ตฌ์กฐ์— ๋งž์ถค"""
chunks = []
chain = context.get("chain", "UNKNOWN")
if data is None:
return chunks
rates = data if isinstance(data, list) else [data]
for rate in rates[:5]:
if not isinstance(rate, dict):
continue
rate_name = rate.get("rate_name", rate.get("name", "N/A"))
content = f"""
ํšŒ์› ์š”๊ธˆ: {rate_name}
์ฒด์ธ: {rate.get('chain', chain)}
"""
# ํ• ์ธ์œจ (์Šคํ‚ค๋งˆ ๊ตฌ์กฐ ๋ฐ˜์˜)
weekday_discount = rate.get("weekday_discount_percent")
weekend_discount = rate.get("weekend_discount_percent")
min_discount = rate.get("minimum_discount_percent")
max_discount = rate.get("maximum_discount_percent")
# ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๊ธฐ์กด ํ•„๋“œ๋„ ์ง€์›
legacy_discount = rate.get("discount_percentage", rate.get("discount"))
if weekday_discount or weekend_discount:
content += "ํ• ์ธ์œจ:\n"
if weekday_discount:
content += f" - ํ‰์ผ: {weekday_discount}%\n"
if weekend_discount:
content += f" - ์ฃผ๋ง: {weekend_discount}%\n"
elif min_discount or max_discount:
content += f"ํ• ์ธ์œจ: {min_discount or '?'}% ~ {max_discount or '?'}%\n"
elif legacy_discount and legacy_discount != "N/A":
content += f"ํ• ์ธ์œจ: {legacy_discount}%\n"
# ํ•„์ˆ˜ ๋ฉค๋ฒ„์‹ญ
if rate.get("requires_membership", True):
content += "๋ฉค๋ฒ„์‹ญ ํ•„์š”: ์˜ˆ\n"
if rate.get("minimum_tier"):
content += f"์ตœ์†Œ ๋“ฑ๊ธ‰: {rate.get('minimum_tier')}\n"
# ์ ์šฉ ๊ฐ์‹ค
applies_to = rate.get("applies_to_room_types", [])
excluded = rate.get("excluded_room_types", [])
if applies_to:
content += f"์ ์šฉ ๊ฐ์‹ค: {', '.join(applies_to)}\n"
if excluded:
content += f"์ œ์™ธ ๊ฐ์‹ค: {', '.join(excluded)}\n"
# ์˜ˆ์•ฝ ์ฑ„๋„
channels = rate.get("eligible_channels", [])
if channels:
content += f"์˜ˆ์•ฝ ์ฑ„๋„: {', '.join(channels)}\n"
# ์ œํ•œ์‚ฌํ•ญ
max_rooms = rate.get("max_rooms")
if max_rooms:
content += f"์ตœ๋Œ€ ๊ฐ์‹ค: {max_rooms}์‹ค\n"
excluded_rate_types = rate.get("excluded_rate_types", [])
if excluded_rate_types:
content += f"์ œ์™ธ ์š”๊ธˆ ์œ ํ˜•: {', '.join(excluded_rate_types)}\n"
excluded_hotels = rate.get("excluded_hotels", [])
if excluded_hotels:
content += f"์ œ์™ธ ํ˜ธํ…”: {', '.join(excluded_hotels[:3])}\n"
# ์ค‘๋ณต ์ ์šฉ
if rate.get("combinable_with_promotions"):
content += "๋‹ค๋ฅธ ํ”„๋กœ๋ชจ์…˜๊ณผ ์ค‘๋ณต ๊ฐ€๋Šฅ\n"
# ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๊ธฐ์กด ํ•„๋“œ๋“ค
legacy_conditions = rate.get("conditions", rate.get("eligibility"))
if legacy_conditions and legacy_conditions != "N/A":
content += f"์ ์šฉ ์กฐ๊ฑด: {legacy_conditions}\n"
if rate.get("description"):
content += f"์„ค๋ช…: {rate.get('description')}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "member_rate",
"rate_name": rate_name,
"chain": rate.get("chain", chain)
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ˜ธํ…” ์‹œ์„ค
# ===========================================================================
def handle_hotel_facilities(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""hotel_facilities, other_facilities, hotel_amenities_summary,
executive_lounge, pulse8_wellness ๋“ฑ ๋‹ค์–‘ํ•œ ์‹œ์„ค ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
์ง€์› ํ˜•ํƒœ:
1. ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ (๊ธฐ์กด hotel_facilities)
2. ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ (executive_lounge, pulse8_wellness ๋“ฑ)
3. ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ (hotel_amenities_summary)
"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
# ๋ฐ์ดํ„ฐ ํ˜•ํƒœ์— ๋”ฐ๋ฅธ ์ฒ˜๋ฆฌ
if isinstance(data, dict):
# ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ (executive_lounge, pulse8_wellness ๋“ฑ)
facility_name = data.get("name", data.get("lounge_name", "์‹œ์„ค"))
location = data.get("location", "")
hours = data.get("operating_hours", "")
content += f"์‹œ์„ค: {facility_name}\n"
if location:
content += f"์œ„์น˜: {location}\n"
if hours:
content += f"์šด์˜์‹œ๊ฐ„: {hours}\n"
# ์ „๋ง
if data.get("view"):
content += f"์ „๋ง: {data.get('view')}\n"
# ์„œ๋น„์Šค/์‹œ์„ค ๋ชฉ๋ก
services = data.get("services", data.get("facilities", []))
if services:
content += "\n์ œ๊ณต ์„œ๋น„์Šค/์‹œ์„ค:\n"
for service in services[:10]:
if isinstance(service, dict):
s_name = service.get("name", "N/A")
s_desc = service.get("description", "")
content += f" - {s_name}"
if s_desc:
content += f": {s_desc[:60]}"
content += "\n"
else:
content += f" - {service}\n"
# ๋“œ๋ ˆ์Šค์ฝ”๋“œ
if data.get("dress_code"):
content += f"\n๋“œ๋ ˆ์Šค์ฝ”๋“œ: {data.get('dress_code')}\n"
# ์—ฐ๋ น ์ œํ•œ
if data.get("age_restriction"):
content += f"์—ฐ๋ น ์ œํ•œ: {data.get('age_restriction')}\n"
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํƒ€์ž… ๊ฒฐ์ •
meta_type = "hotel_facility"
if "lounge" in str(data).lower():
meta_type = "executive_lounge"
elif "wellness" in str(data).lower() or "pulse" in str(data).lower():
meta_type = "wellness_facility"
elif isinstance(data, list):
# ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ
facilities = data
# ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ ํ™•์ธํ•˜์—ฌ ํƒ€์ž… ๊ฒฐ์ •
first_item = facilities[0] if facilities else None
if first_item and isinstance(first_item, str):
# ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ (hotel_amenities_summary)
content += "ํ˜ธํ…” ์‹œ์„ค ๋ฐ ์„œ๋น„์Šค ์š”์•ฝ:\n"
for amenity in facilities[:20]:
content += f" โœ“ {amenity}\n"
meta_type = "hotel_amenities_summary"
else:
# ๋”•์…”๋„ˆ๋ฆฌ ๋ฆฌ์ŠคํŠธ (๊ธฐ์กด hotel_facilities)
content += "ํ˜ธํ…” ์‹œ์„ค:\n"
for facility in facilities[:15]:
if isinstance(facility, dict):
name = facility.get("name", facility.get("facility_name", "N/A"))
desc = facility.get("description", "")
content += f" - {name}"
if desc:
content += f": {desc[:60]}"
content += "\n"
elif isinstance(facility, str):
content += f" - {facility}\n"
meta_type = "hotel_facilities"
else:
return chunks
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": meta_type,
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๊ฐ์‹ค ์œ ํ˜•
# ===========================================================================
def handle_room_types(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""room_types ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
rooms = data if isinstance(data, list) else [data]
# ๊ฐ ๊ฐ์‹ค ์œ ํ˜•์„ ์ฒญํฌ๋กœ
for room in rooms[:10]:
if not isinstance(room, dict):
continue
room_name = room.get("name", room.get("room_name", "N/A"))
room_name_ko = room.get("name_ko", room.get("name_localized", {}).get("ko"))
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"""๊ฐ์‹ค ์œ ํ˜•: {room_name}
"""
if room_name_ko:
content += f"๊ฐ์‹ค๋ช… (ํ•œ๊ตญ์–ด): {room_name_ko}\n"
content += f"""๋ฉด์ : {room.get("area_sqm", room.get("size", "N/A"))}ใŽก
์นจ๋Œ€: {room.get("bed_type", "N/A")}
์ตœ๋Œ€ ํˆฌ์ˆ™: {room.get("max_occupancy", "N/A")}๋ช…
๋ทฐ: {room.get("view", "N/A")}
์นดํ…Œ๊ณ ๋ฆฌ: {room.get("category", "N/A")}
์กฐ์‹ ํฌํ•จ: {'์˜ˆ' if room.get("breakfast_included") else '์•„๋‹ˆ์˜ค'}
๋ผ์šด์ง€: {'๊ฐ€๋Šฅ' if room.get("lounge_access") else '๋ถˆ๊ฐ€'}
"""
# ํŠน์ง•
features = room.get("features", [])
if features:
content += "ํŠน์ง•:\n"
for feat in features[:5]:
content += f" - {feat}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "room_type",
"hotel_name": hotel_name,
"room_name": room_name,
"category": room.get("category")
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๊ฐ์‹ค ์–ด๋ฉ”๋‹ˆํ‹ฐ
# ===========================================================================
def handle_room_amenities(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""room_common_amenities, room_common_features ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += "๊ฐ์‹ค ๊ณตํ†ต ์–ด๋ฉ”๋‹ˆํ‹ฐ:\n"
if isinstance(data, dict):
for category, items in data.items():
if isinstance(items, list):
content += f"\n{category}:\n"
for item in items[:8]:
content += f" - {item}\n"
elif isinstance(data, list):
for item in data[:15]:
content += f" - {item}\n"
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "room_amenities",
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋ ˆ์Šคํ† ๋ž‘
# ===========================================================================
def handle_restaurants(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""restaurants ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
restaurants = data if isinstance(data, list) else [data]
for rest in restaurants[:5]:
if not isinstance(rest, dict):
continue
rest_name = rest.get("name", rest.get("restaurant_name", "N/A"))
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"""๋ ˆ์Šคํ† ๋ž‘: {rest_name}
์œ ํ˜•: {rest.get("type", rest.get("cuisine_type", "N/A"))}
์œ„์น˜: {rest.get("location", "N/A")}
์ขŒ์„์ˆ˜: {rest.get("seating", rest.get("capacity", "N/A"))}
"""
# ์šด์˜ ์‹œ๊ฐ„
hours = rest.get("operating_hours", {})
if hours:
content += "์šด์˜ ์‹œ๊ฐ„:\n"
if isinstance(hours, dict):
for period, time in hours.items():
content += f" - {period}: {time}\n"
# ๋ฉ”๋‰ด/๊ฐ€๊ฒฉ
prices = rest.get("prices", rest.get("menu_prices", {}))
if isinstance(prices, dict):
content += "๊ฐ€๊ฒฉ:\n"
for item, price in list(prices.items())[:3]:
content += f" - {item}: โ‚ฉ{price:,}\n" if isinstance(price, (int, float)) else f" - {item}: {price}\n"
chunks.append({
"content": content.strip(),
"metadata": {
"type": "restaurant",
"hotel_name": hotel_name,
"restaurant_name": rest_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ์กฐ์‹ ์˜ต์…˜
# ===========================================================================
def handle_breakfast(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""breakfast_options ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None:
return chunks
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += "์กฐ์‹ ์˜ต์…˜:\n"
if isinstance(data, dict):
for key, value in data.items():
if isinstance(value, dict):
content += f"\n{key}:\n"
for k, v in value.items():
content += f" - {k}: {v}\n"
else:
content += f" - {key}: {value}\n"
elif isinstance(data, list):
for item in data[:10]:
if isinstance(item, dict):
name = item.get("name", "N/A")
price = item.get("price", "")
content += f" - {name}"
if price:
content += f": โ‚ฉ{price:,}" if isinstance(price, (int, float)) else f": {price}"
content += "\n"
else:
content += f" - {item}\n"
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "breakfast_options",
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ‰์ 
# ===========================================================================
def handle_ratings(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""ratings ์ฒ˜๋ฆฌ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if data is None or not isinstance(data, dict):
return chunks
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += "ํ˜ธํ…” ํ‰์ :\n"
for source, rating in data.items():
if isinstance(rating, dict):
score = rating.get("score", rating.get("rating", "N/A"))
content += f" - {source}: {score}\n"
else:
content += f" - {source}: {rating}\n"
if len(content) > 80:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "hotel_ratings",
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋‹ค์ด๋‹ ํ”„๋กœ๊ทธ๋žจ
# ===========================================================================
def handle_dining_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""dining_programs ์ฒ˜๋ฆฌ"""
chunks = []
chain = context.get("chain", "UNKNOWN")
if not isinstance(data, list):
return chunks
for program in data:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "Unknown Program")
program_id = program.get("program_id", "unknown")
region = program.get("region", "Unknown")
# ๊ธฐ๋ณธ ํ”„๋กœ๊ทธ๋žจ ์ •๋ณด ์ฒญํฌ
content = f"๋‹ค์ด๋‹ ํ”„๋กœ๊ทธ๋žจ: {program_name}\n"
content += f"์ฒด์ธ: {chain}\n"
content += f"์ง€์—ญ: {region}\n"
# ์œ ํšจ ๊ธฐ๊ฐ„
validity = program.get("validity", {})
if validity:
start_date = validity.get("start_date")
end_date = validity.get("end_date")
if start_date and end_date:
content += f"์œ ํšจ ๊ธฐ๊ฐ„: {start_date} ~ {end_date}\n"
# ๋“ฑ๊ธ‰๋ณ„ ํ˜œํƒ
tier_benefits = program.get("tier_benefits", [])
if tier_benefits:
content += "\n๋“ฑ๊ธ‰๋ณ„ ํ˜œํƒ:\n"
for benefit in tier_benefits:
tier = benefit.get("tier", "Unknown")
discount = benefit.get("discount_percentage", 0)
bonus_points = benefit.get("bonus_points", 0)
min_spend = benefit.get("minimum_spend_usd", 0)
content += f" {tier}:\n"
content += f" - ํ• ์ธ: {discount}%\n"
content += f" - ๋ณด๋„ˆ์Šค ํฌ์ธํŠธ: {bonus_points}์ \n"
if min_spend:
content += f" - ์ตœ์†Œ ์†Œ๋น„ ๊ธˆ์•ก: ${min_spend}\n"
# ํ”„๋กœ๊ทธ๋žจ ์ถ”๊ฐ€ ์ •๋ณด (์Šคํ‚ค๋งˆ ํ•„๋“œ ๊ธฐ๋ฐ˜)
# ์ฐธ์—ฌ ๋งค์žฅ ์ •๋ณด
participating_outlets = program.get("participating_outlets")
if participating_outlets:
content += f"\n์ฐธ์—ฌ ๋งค์žฅ ์ˆ˜: {participating_outlets}๊ฐœ\n"
outlet_types = program.get("outlet_types", [])
if outlet_types:
content += f"๋งค์žฅ ์œ ํ˜•: {', '.join(outlet_types)}\n"
# ์ œํ•œ์‚ฌํ•ญ
restrictions_added = False
max_group = program.get("max_group_size")
if max_group:
if not restrictions_added:
content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
restrictions_added = True
content += f" - ์ตœ๋Œ€ ๊ทธ๋ฃน ํฌ๊ธฐ: {max_group}๋ช…\n"
split_bills = program.get("split_bills_allowed")
if split_bills is not None:
if not restrictions_added:
content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
restrictions_added = True
content += f" - ๋ถ„ํ•  ๊ฒฐ์ œ: {'ํ—ˆ์šฉ' if split_bills else '๋ถˆํ—ˆ์šฉ'}\n"
member_present = program.get("member_must_be_present")
if member_present is not None:
if not restrictions_added:
content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
restrictions_added = True
content += f" - ํšŒ์› ๋ณธ์ธ ์ฐธ์„: {'ํ•„์ˆ˜' if member_present else '๋ถˆํ•„์š”'}\n"
# ์ œ์™ธ์‚ฌํ•ญ
exclusions = program.get("exclusions", [])
if exclusions:
content += f"\n์ œ์™ธ์‚ฌํ•ญ: {', '.join(exclusions[:3])}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "dining_program",
"program_id": program_id,
"program_name": program_name,
"chain": chain,
"region": region
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ•ญ๊ณต ํŒŒํŠธ๋„ˆ์‹ญ (Flying Blue โ†’ Korean Air ๋“ฑ)
# ===========================================================================
def handle_airline_partnership(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""airline_partnership ์ฒ˜๋ฆฌ - ํŒŒํŠธ๋„ˆ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ํƒ€ํ•ญ๊ณต์‚ฌ ์˜ˆ์•ฝ ์ •๋ณด"""
chunks = []
if not data or not isinstance(data, dict):
return chunks
partner_program = data.get("partner_program", "N/A")
operating_airline = data.get("operating_airline", "UNKNOWN")
alliance = data.get("alliance", "")
currency_name = data.get("currency_name", "ํฌ์ธํŠธ")
pricing_model = data.get("pricing_model", "")
pricing_desc = data.get("pricing_model_description", "")
content = f"""# {partner_program}๋กœ {operating_airline} ์˜ˆ์•ฝํ•˜๊ธฐ
**ํŒŒํŠธ๋„ˆ ํ”„๋กœ๊ทธ๋žจ**: {partner_program}
**์šดํ•ญ ํ•ญ๊ณต์‚ฌ**: {operating_airline}
**์ œํœด**: {alliance if alliance else "N/A"}
**ํฌ์ธํŠธ ๋‹จ์œ„**: {currency_name}
"""
if pricing_model:
content += f"**๊ฐ€๊ฒฉ ์ •์ฑ…**: {pricing_model}"
if pricing_desc:
content += f" - {pricing_desc}"
content += "\n"
# ์šด์˜ ์ฃผ์ฒด
operator = data.get("partner_program_operator")
if operator:
content += f"**ํ”„๋กœ๊ทธ๋žจ ์šด์˜**: {operator}\n"
# ์ œํ•œ์‚ฌํ•ญ
restrictions = data.get("restrictions", [])
if restrictions:
content += "\n## โš ๏ธ ์˜ˆ์•ฝ ์ œํ•œ์‚ฌํ•ญ\n"
for restriction in restrictions[:10]:
if isinstance(restriction, dict):
r_type = restriction.get("restriction_type", "")
desc = restriction.get("description", "")
content += f"- **{r_type}**: {desc}\n"
else:
content += f"- {restriction}\n"
# ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ
available = data.get("available_booking_classes", [])
if available:
content += "\n## โœ… ์˜ˆ์•ฝ ๊ฐ€๋Šฅ ํด๋ž˜์Šค\n"
for cls in available[:5]:
if isinstance(cls, dict):
cabin = cls.get("cabin_class", "")
is_available = cls.get("available", True)
status = "โœ… ๊ฐ€๋Šฅ" if is_available else "โŒ ๋ถˆ๊ฐ€"
content += f"- {cabin}: {status}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_partnership",
"partner_program": partner_program,
"operating_airline": operating_airline,
"alliance": alliance
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ•ญ๊ณต ํ”„๋กœ๊ทธ๋žจ (Phase 2)
# ===========================================================================
def handle_airline_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""airline_programs ๋˜๋Š” airline_program ์ฒ˜๋ฆฌ"""
chunks = []
programs = data if isinstance(data, list) else [data]
for program in programs[:5]:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "N/A")
airline = program.get("airline", "UNKNOWN")
alliance = program.get("alliance", "")
miles_name = program.get("miles_name", "๋งˆ์ผ๋ฆฌ์ง€")
content = f"""
ํ•ญ๊ณต ๋งˆ์ผ๋ฆฌ์ง€ ํ”„๋กœ๊ทธ๋žจ: {program_name}
ํ•ญ๊ณต์‚ฌ: {airline}
๋™๋งน: {alliance if alliance else "์—†์Œ"}
๋งˆ์ผ๋ฆฌ์ง€ ๋ช…์นญ: {miles_name}
๋งˆ์ผ๋ฆฌ์ง€ ๋งŒ๋ฃŒ: {program.get("miles_expiration_months", "N/A")}๊ฐœ์›”
ํ™œ๋™์œผ๋กœ ์—ฐ์žฅ: {"๊ฐ€๋Šฅ" if program.get("can_extend_with_activity") else "๋ถˆ๊ฐ€"}
"""
# ๋“ฑ๊ธ‰ ์ •๋ณด
tiers = program.get("tiers", [])
if tiers:
content += "\n๋“ฑ๊ธ‰:\n"
for tier in tiers[:6]:
if isinstance(tier, dict):
tier_name = tier.get("tier_name", "N/A")
miles_req = tier.get("miles_required")
alliance_tier = tier.get("alliance_tier", "")
tier_info = f" - {tier_name}"
if miles_req:
tier_info += f" ({miles_req:,} ๋งˆ์ผ)"
if alliance_tier:
tier_info += f" [๋™๋งน: {alliance_tier}]"
content += tier_info + "\n"
# ํŒŒํŠธ๋„ˆ ํ•ญ๊ณต์‚ฌ
partners = program.get("partner_airlines", [])
if partners:
content += f"\n์ œํœด ํ•ญ๊ณต์‚ฌ: {', '.join(partners[:8])}\n"
if len(partners) > 8:
content += f" ... ์™ธ {len(partners) - 8}๊ฐœ\n"
# ํ˜ธํ…” ํŒŒํŠธ๋„ˆ
hotel_partners = program.get("hotel_partners", [])
if hotel_partners:
content += f"์ œํœด ํ˜ธํ…”: {', '.join(hotel_partners[:5])}\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_program",
"program_name": program_name,
"airline": airline,
"alliance": alliance
}
})
# === ๋‚ด๋ถ€ ๊ตฌ์„ฑ ์š”์†Œ ์„œ๋ธŒ์ฒญํ‚น ===
# ํ”„๋กœ๊ทธ๋žจ๋ณ„ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
program_context = {**context, "chain": airline, "airline": airline}
# 1. earning_rules ์ฒ˜๋ฆฌ (๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ ๊ทœ์น™)
earning_rules = program.get("earning_rules")
if earning_rules:
try:
sub_chunks = handle_airline_earning_rules(earning_rules, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 2. award_chart ์ฒ˜๋ฆฌ (๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ์ฐจํŠธ)
award_chart = program.get("award_chart")
if award_chart:
try:
sub_chunks = handle_award_charts(award_chart, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 3. co_branded_cards ์ฒ˜๋ฆฌ (์ œํœด ์‹ ์šฉ์นด๋“œ)
co_branded_cards = program.get("co_branded_cards")
if co_branded_cards:
try:
sub_chunks = handle_credit_cards(co_branded_cards, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
# 4. tiers๋ฅผ ๋ณ„๋„ ์ฒญํฌ๋กœ ์ฒ˜๋ฆฌ (์ƒ์„ธ ๋“ฑ๊ธ‰ ์ •๋ณด)
tiers_data = program.get("tiers")
if tiers_data and isinstance(tiers_data, list) and len(tiers_data) > 0:
try:
sub_chunks = handle_airline_tiers(tiers_data, program_context)
chunks.extend(sub_chunks)
except Exception:
pass
return chunks
def handle_airline_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""airline_tiers ์ฒ˜๋ฆฌ"""
chunks = []
if data is None:
return chunks
tiers = data if isinstance(data, list) else [data]
for tier in tiers[:10]:
if not isinstance(tier, dict):
continue
tier_name = tier.get("tier_name", "N/A")
airline = tier.get("airline", context.get("chain", "UNKNOWN"))
# f-string ์˜ค๋ฅ˜ ์ˆ˜์ •: miles_required ์ฒ˜๋ฆฌ
miles_req = tier.get("miles_required")
miles_str = f"{miles_req:,}" if isinstance(miles_req, int) else "N/A"
content = f"""
ํ•ญ๊ณต ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰: {tier_name}
ํ•ญ๊ณต์‚ฌ: {airline}
๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
์ž๊ฒฉ ์š”๊ฑด:
- ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€: {miles_str}
- ํ•„์š” ๊ตฌ๊ฐ„: {tier.get("segments_required", "N/A")}
- ์‚ฐ์ • ๊ธฐ๊ฐ„: {tier.get("qualification_period", "calendar_year")}
"""
# ๋™๋งน ๋“ฑ๊ธ‰
if tier.get("alliance") or tier.get("alliance_tier"):
content += f"\n๋™๋งน: {tier.get('alliance', 'N/A')}\n"
content += f"๋™๋งน ๋“ฑ๊ธ‰: {tier.get('alliance_tier', 'N/A')}\n"
# ์ฃผ์š” ํ˜œํƒ
content += "\n์ฃผ์š” ํ˜œํƒ:\n"
if tier.get("lounge_access"):
lounge_details = tier.get("lounge_access_details", "๋ผ์šด์ง€ ์ด์šฉ ๊ฐ€๋Šฅ")
content += f" - ๋ผ์šด์ง€: {lounge_details}\n"
if tier.get("earning_bonus_percent"):
content += f" - ๋งˆ์ผ๋ฆฌ์ง€ ๋ณด๋„ˆ์Šค: {tier.get('earning_bonus_percent')}% ์ถ”๊ฐ€\n"
if tier.get("complimentary_upgrades"):
content += f" - ๋ฌด๋ฃŒ ์—…๊ทธ๋ ˆ์ด๋“œ: ์—ฐ {tier.get('complimentary_upgrades')}ํšŒ\n"
if tier.get("extra_baggage_kg") or tier.get("extra_baggage_pieces"):
baggage_info = []
if tier.get("extra_baggage_kg"):
baggage_info.append(f"+{tier.get('extra_baggage_kg')}kg")
if tier.get("extra_baggage_pieces"):
baggage_info.append(f"+{tier.get('extra_baggage_pieces')}๊ฐœ")
content += f" - ์ถ”๊ฐ€ ์ˆ˜ํ•˜๋ฌผ: {', '.join(baggage_info)}\n"
# ๊ธฐํƒ€ ํ˜œํƒ
benefits = tier.get("benefits", [])
for benefit in benefits[:5]:
if isinstance(benefit, dict):
content += f" - {benefit.get('name', 'N/A')}\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_tier",
"tier_name": tier_name,
"airline": airline,
"rank": tier.get("rank")
}
})
return chunks
def handle_award_charts(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""award_charts ๋˜๋Š” award_chart ์ฒ˜๋ฆฌ"""
chunks = []
charts = data if isinstance(data, list) else [data]
for chart in charts[:3]:
if not isinstance(chart, dict):
continue
chart_name = chart.get("chart_name", "๋งˆ์ผ๋ฆฌ์ง€ ์ขŒ์„ ์ฐจํŠธ")
airline = chart.get("airline", context.get("chain", "UNKNOWN"))
content = f"""
๋งˆ์ผ๋ฆฌ์ง€ ์ขŒ์„ ์ฐจํŠธ: {chart_name}
ํ•ญ๊ณต์‚ฌ: {airline}
์‹œํ–‰์ผ: {chart.get("effective_date", "N/A")}
์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ: {"์ ์šฉ" if chart.get("fuel_surcharge_applies", True) else "๋ฏธ์ ์šฉ"}
์„ธ๊ธˆ: {"๋ณ„๋„" if chart.get("taxes_additional", True) else "ํฌํ•จ"}
"""
# ๊ตฌ๊ฐ„๋ณ„ ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€
entries = chart.get("entries", [])
regions = chart.get("regions", []) # regions ํ‚ค ์ง€์› ์ถ”๊ฐ€
if entries:
content += "\n๊ตฌ๊ฐ„๋ณ„ ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€ (์™•๋ณต ๊ธฐ์ค€):\n"
for entry in entries[:10]:
if isinstance(entry, dict):
route = entry.get("route_type", "N/A")
content += f"\n{route}:\n"
if entry.get("economy"):
content += f" - ์ด์ฝ”๋…ธ๋ฏธ: {entry['economy']:,} ๋งˆ์ผ\n"
if entry.get("premium_economy"):
content += f" - ํ”„๋ฆฌ๋ฏธ์—„ ์ด์ฝ”๋…ธ๋ฏธ: {entry['premium_economy']:,} ๋งˆ์ผ\n"
if entry.get("business"):
content += f" - ๋น„์ฆˆ๋‹ˆ์Šค: {entry['business']:,} ๋งˆ์ผ\n"
if entry.get("first"):
content += f" - ํผ์ŠคํŠธ: {entry['first']:,} ๋งˆ์ผ\n"
# regions ๊ตฌ์กฐ ์ฒ˜๋ฆฌ (Asiana ๋“ฑ)
elif regions:
content += "\n๊ตฌ๊ฐ„๋ณ„ ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€:\n"
for region in regions[:10]:
if isinstance(region, dict):
r_name = region.get("region_name", region.get("region_code", "N/A"))
trip_type = region.get("trip_type", "")
trip_label = "์™•๋ณต" if trip_type == "ROUND_TRIP" else "ํŽธ๋„" if trip_type == "ONE_WAY" else ""
content += f"\n{r_name} ({trip_label}):\n"
cabin_classes = region.get("cabin_classes", [])
for cabin in cabin_classes:
if isinstance(cabin, dict):
c_name = cabin.get("cabin_class", "N/A")
cabin_labels = {
"ECONOMY": "์ผ๋ฐ˜์„",
"PREMIUM_ECONOMY": "ํ”„๋ฆฌ๋ฏธ์—„ ์ด์ฝ”๋…ธ๋ฏธ",
"BUSINESS": "๋น„์ฆˆ๋‹ˆ์Šค",
"BUSINESS_SMARTIUM": "๋น„์ฆˆ๋‹ˆ์Šค ์Šค๋งˆํ‹ฐ์›€",
"FIRST": "ํผ์ŠคํŠธ"
}
c_label = cabin_labels.get(c_name, c_name)
low = cabin.get("low_season_miles", 0)
high = cabin.get("high_season_miles", 0)
if low and high:
content += f" - {c_label}: {low:,}~{high:,} ๋งˆ์ผ\n"
elif low:
content += f" - {c_label}: {low:,} ๋งˆ์ผ\n"
# ์ฐธ๊ณ ์‚ฌํ•ญ
notes = chart.get("notes", [])
if notes:
content += "\n์ฐธ๊ณ :\n"
for note in notes[:5]:
content += f" - {note}\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "award_chart",
"chart_name": chart_name,
"airline": airline
}
})
return chunks
def handle_airline_earning_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""airline_earning_rules ์ฒ˜๋ฆฌ (์Šคํ‚ค๋งˆ AirlineEarningRule ํ•„๋“œ ๊ตฌ์กฐ์— ๋งž์ถค)"""
chunks = []
rules = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
# ๊ทœ์น™๋“ค์„ ํ•˜๋‚˜์˜ ์ฒญํฌ๋กœ ๋ฌถ๊ธฐ
if not rules:
return chunks
content = f"""
๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ ๊ทœ์น™
ํ•ญ๊ณต์‚ฌ: {airline}
"""
for rule in rules[:15]:
if not isinstance(rule, dict):
continue
rule_name = rule.get("rule_name", "N/A")
cabin = rule.get("cabin_class", "")
content += f"โ€ข {rule_name}\n"
if cabin:
content += f" - ๊ฐ์‹ค: {cabin}\n"
# ๋…ธ์„  ์œ ํ˜•
route = rule.get("route_type", "")
if route:
content += f" - ๋…ธ์„ : {route}\n"
# ์ ๋ฆฝ๋ฅ  (์Šคํ‚ค๋งˆ ํ•„๋“œ ๋ฐ˜์˜)
earning_rate = rule.get("earning_rate_percent")
base_miles_type = rule.get("base_miles_type", "FLOWN")
fixed_miles = rule.get("fixed_miles")
if earning_rate:
content += f" - ์ ๋ฆฝ๋ฅ : {earning_rate}%\n"
elif base_miles_type == "FIXED" and fixed_miles:
content += f" - ๊ณ ์ • ๋งˆ์ผ: {fixed_miles:,}๋งˆ์ผ\n"
# ์˜ˆ์•ฝ ํด๋ž˜์Šค
booking_classes = rule.get("booking_classes", [])
if booking_classes:
content += f" - ์˜ˆ์•ฝ ํด๋ž˜์Šค: {', '.join(booking_classes)}\n"
# ๋ณด๋„ˆ์Šค ๋งˆ์ผ
bonus_miles = rule.get("bonus_miles")
if bonus_miles:
content += f" - ๋ณด๋„ˆ์Šค: +{bonus_miles:,}๋งˆ์ผ\n"
# ํ•„์š” ๋“ฑ๊ธ‰
req_tier = rule.get("requires_membership_tier")
if req_tier:
content += f" - ํ•„์š” ๋“ฑ๊ธ‰: {req_tier}\n"
# ์œ ํšจ ๊ธฐ๊ฐ„
validity = rule.get("validity")
if validity and isinstance(validity, dict):
start = validity.get("start_date")
end = validity.get("end_date")
if start or end:
content += f" - ์œ ํšจ ๊ธฐ๊ฐ„: {start or '?'} ~ {end or '?'}\n"
# ์ง€์—ญ
region = rule.get("region")
if region and region != "GLOBAL":
content += f" - ์ ์šฉ ์ง€์—ญ: {region}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_earning_rules",
"airline": airline
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ•ญ๊ณต ์šด์ž„ (Airline Fares)
# ===========================================================================
def handle_airline_fare_tables(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""airline_fare_tables ์ฒ˜๋ฆฌ - ํ•ญ๊ณต ์šด์ž„ํ‘œ ๊ธฐ๋ณธ ์ •๋ณด"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", "UNKNOWN")
program = item.get("program_name", "")
currency = item.get("currency", "KRW")
effective_date = item.get("effective_date", "")
content = f"# {airline} ํ•ญ๊ณต ์šด์ž„ํ‘œ ์ •๋ณด\n\n"
content += f"**ํ•ญ๊ณต์‚ฌ**: {airline}\n"
if program:
content += f"**ํ”„๋กœ๊ทธ๋žจ**: {program}\n"
content += f"**ํ†ตํ™”**: {currency}\n"
if effective_date:
content += f"**์ ์šฉ์ผ**: {effective_date}\n"
# ์ผ๋ฐ˜ ์ •๋ณด
general_info = item.get("general_info", {})
if general_info:
content += f"\n## ์šด์ž„ ํฌํ•จ ์‚ฌํ•ญ\n"
fare_includes = general_info.get("fare_includes", [])
if fare_includes:
content += "- " + "\n- ".join(fare_includes) + "\n"
fare_type = general_info.get("fare_type", "")
if fare_type:
content += f"**์šด์ž„ ์œ ํ˜•**: {fare_type}\n"
variability = general_info.get("variability_note", "")
if variability:
content += f"**์ฐธ๊ณ **: {variability}\n"
# ์ขŒ์„ ๋“ฑ๊ธ‰
cabin_classes = item.get("cabin_classes", [])
if cabin_classes:
content += f"\n## ์ขŒ์„ ๋“ฑ๊ธ‰\n"
for cabin in cabin_classes:
code = cabin.get("cabin_code", "")
name = cabin.get("cabin_name", "")
name_en = cabin.get("cabin_name_en", "")
booking_classes = cabin.get("booking_classes", [])
content += f"- **{name}** ({name_en}, {code}): {', '.join(booking_classes)}\n"
# ์‹œ์ฆŒ ์ •๋ณด
seasons = item.get("seasons", [])
if seasons:
content += f"\n## ์‹œ์ฆŒ ๊ตฌ๋ถ„\n"
for season in seasons:
code = season.get("season_code", "")
name = season.get("season_name", "")
content += f"- {name} ({code})\n"
# ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ
fuel_table = item.get("fuel_surcharge_table", [])
if fuel_table:
content += f"\n## ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ\n"
for fuel in fuel_table:
tier = fuel.get("distance_tier", "")
desc = fuel.get("description", "")
trip = fuel.get("trip_type", "")
amount = fuel.get("amount", fuel.get("amount_range", {}).get("min", ""))
content += f"- {tier}"
if desc:
content += f" ({desc})"
content += f": {amount:,}์›" if isinstance(amount, int) else f": {amount}"
if trip:
content += f" ({trip})"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_fare_table",
"airline": airline,
"program": program
}
})
return chunks
def handle_domestic_fare_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""domestic_fare_rules ์ฒ˜๋ฆฌ - ๊ตญ๋‚ด์„  ์šด์ž„ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", "UNKNOWN")
region = item.get("region", "DOMESTIC")
content = f"# {airline} ๊ตญ๋‚ด์„  ์šด์ž„ ๊ทœ์ •\n\n"
fare_basis = item.get("fare_basis", "")
if fare_basis:
content += f"**์šด์ž„ ๊ธฐ์ค€**: {fare_basis}\n"
fuel = item.get("fuel_surcharge_oneway", 0)
airport = item.get("airport_fee_oneway", 0)
if fuel or airport:
content += f"**์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ (ํŽธ๋„)**: {fuel:,}์›\n"
content += f"**๊ณตํ•ญ์ด์šฉ๋ฃŒ (ํŽธ๋„)**: {airport:,}์›\n"
# ์š”์ผ ๊ตฌ๋ถ„
weekday = item.get("weekday_days", [])
weekend = item.get("weekend_days", [])
if weekday or weekend:
content += f"\n## ์š”์ผ ๊ตฌ๋ถ„\n"
if weekday:
content += f"- **์ฃผ์ค‘**: {', '.join(weekday)}\n"
if weekend:
content += f"- **์ฃผ๋ง**: {', '.join(weekend)}\n"
# ์‹œ๊ฐ„๋Œ€ ๊ตฌ๋ถ„
time_zones = item.get("time_zones", [])
if time_zones:
content += f"\n## ์‹œ๊ฐ„๋Œ€ ๊ตฌ๋ถ„\n"
for tz in time_zones:
direction = tz.get("direction", "")
preferred = tz.get("preferred_time", "")
standard = tz.get("standard_time", "")
content += f"- **{direction}**: ์„ ํ˜ธ {preferred}, ์ผ๋ฐ˜ {standard}\n"
# ํƒ„๋ ฅํ• ์ฆ์šด์ž„
flex_routes = item.get("flexible_surcharge_routes", [])
if flex_routes:
content += f"\n## ํƒ„๋ ฅํ• ์ฆ์šด์ž„ ์ ์šฉ\n"
for route in flex_routes:
direction = route.get("direction", "")
applies = route.get("applies_to", "")
content += f"- **{direction}**: {applies}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "domestic_fare_rules",
"airline": airline,
"region": region
}
})
return chunks
def handle_domestic_peak_seasons(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""domestic_peak_seasons ์ฒ˜๋ฆฌ - ๊ตญ๋‚ด์„  ์„ฑ์ˆ˜๊ธฐ ๊ธฐ๊ฐ„"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
content = "# ๊ตญ๋‚ด์„  ์„ฑ์ˆ˜๊ธฐ ์ ์šฉ ๊ธฐ๊ฐ„\n\n"
for item in items:
year = item.get("year", "")
dates = item.get("dates", [])
if year and dates:
content += f"## {year}๋…„ ์„ฑ์ˆ˜๊ธฐ\n"
content += "- " + "\n- ".join(dates) + "\n\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "domestic_peak_seasons",
"airline": context.get("chain", "UNKNOWN")
}
})
return chunks
def handle_domestic_route_fares(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""domestic_route_fares ์ฒ˜๋ฆฌ - ๊ตญ๋‚ด์„  ๋…ธ์„ ๋ณ„ ์šด์ž„"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
# ์ƒˆ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
origin_name = item.get("origin_name", "")
dest_name = item.get("destination_name", "")
route = item.get("route", f"{origin_name}-{dest_name}")
airline = item.get("airline", context.get("chain", "UNKNOWN"))
cabin_name = item.get("cabin_name", "")
cabin_class = item.get("cabin_class", "")
trip_type = item.get("trip_type", "ONE_WAY")
content = f"# {airline} {route} ๊ตญ๋‚ด์„  ์šด์ž„\n\n"
content += f"**๋…ธ์„ **: {origin_name} โ†’ {dest_name}\n"
if cabin_name:
content += f"**์ขŒ์„๋“ฑ๊ธ‰**: {cabin_name} ({cabin_class})\n"
content += f"**์šด์ž„์œ ํ˜•**: {'ํŽธ๋„' if trip_type == 'ONE_WAY' else '์™•๋ณต'}\n"
# fares๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ์ธ ๊ฒฝ์šฐ (์ƒˆ ๊ตฌ์กฐ)
fares = item.get("fares", {})
if isinstance(fares, dict):
content += f"\n## ์šด์ž„ํ‘œ\n"
wd_pref = fares.get("weekday_preferred", "-")
wd_std = fares.get("weekday_standard", "-")
we_pref = fares.get("weekend_preferred", "-")
we_std = fares.get("weekend_standard", "-")
peak_flex = fares.get("peak_flexible", "-")
if isinstance(wd_pref, int):
wd_pref = f"{wd_pref:,}"
if isinstance(wd_std, int):
wd_std = f"{wd_std:,}"
if isinstance(we_pref, int):
we_pref = f"{we_pref:,}"
if isinstance(we_std, int):
we_std = f"{we_std:,}"
if isinstance(peak_flex, int):
peak_flex = f"{peak_flex:,}"
content += f"- **์ฃผ์ค‘์„ ํ˜ธ**: {wd_pref}์›\n"
content += f"- **์ฃผ์ค‘์ผ๋ฐ˜**: {wd_std}์›\n"
content += f"- **์ฃผ๋ง์„ ํ˜ธ**: {we_pref}์›\n"
content += f"- **์ฃผ๋ง์ผ๋ฐ˜**: {we_std}์›\n"
if peak_flex != "-":
content += f"- **์„ฑ์ˆ˜๊ธฐํƒ„๋ ฅ**: {peak_flex}์›\n"
# fares๊ฐ€ ๋ฆฌ์ŠคํŠธ์ธ ๊ฒฝ์šฐ (์ด์ „ ๊ตฌ์กฐ)
elif isinstance(fares, list) and fares:
content += f"\n## ์šด์ž„ํ‘œ (ํŽธ๋„)\n"
content += "| ๋“ฑ๊ธ‰ | ์˜ˆ์•ฝํด๋ž˜์Šค | ์ฃผ์ค‘์ผ๋ฐ˜ | ์ฃผ์ค‘์„ ํ˜ธ | ์ฃผ๋ง์ผ๋ฐ˜ | ์ฃผ๋ง์„ ํ˜ธ |\n"
content += "|------|-----------|---------|---------|---------|--------|\n"
for fare in fares:
if isinstance(fare, dict):
cabin = fare.get("cabin_name", "")
booking = fare.get("booking_class", "")
wd_std = fare.get("weekday_standard", "-")
wd_pref = fare.get("weekday_preferred", "-")
we_std = fare.get("weekend_standard", "-")
we_pref = fare.get("weekend_preferred", "-")
if isinstance(wd_std, int):
wd_std = f"{wd_std:,}"
if isinstance(wd_pref, int):
wd_pref = f"{wd_pref:,}"
if isinstance(we_std, int):
we_std = f"{we_std:,}"
if isinstance(we_pref, int):
we_pref = f"{we_pref:,}"
content += f"| {cabin} | {booking} | {wd_std} | {wd_pref} | {we_std} | {we_pref} |\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "domestic_route_fares",
"airline": airline,
"route": route,
"cabin_class": cabin_class
}
})
return chunks
def handle_codeshare_domestic_fares(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""codeshare_domestic_fares ์ฒ˜๋ฆฌ - ๊ณต๋™์šดํ•ญ ๊ตญ๋‚ด์„  ์šด์ž„"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
route = item.get("route", "")
operating_carrier = item.get("operating_carrier", "")
marketing_carrier = item.get("marketing_carrier", context.get("chain", ""))
content = f"# {marketing_carrier} ๊ณต๋™์šดํ•ญ ({operating_carrier}) {route} ์šด์ž„\n\n"
content += f"**์šดํ•ญ์‚ฌ**: {operating_carrier}\n"
content += f"**ํŒ๋งค์‚ฌ**: {marketing_carrier}\n"
# ์šด์ž„ ์ •๋ณด
fares = item.get("fares", [])
if fares:
content += f"\n## ์šด์ž„ํ‘œ (ํŽธ๋„)\n"
content += "| ๋“ฑ๊ธ‰ | ์˜ˆ์•ฝํด๋ž˜์Šค | ์ฃผ์ค‘์ผ๋ฐ˜ | ์ฃผ์ค‘์„ ํ˜ธ | ์ฃผ๋ง์ผ๋ฐ˜ | ์ฃผ๋ง์„ ํ˜ธ |\n"
content += "|------|-----------|---------|---------|---------|--------|\n"
for fare in fares:
cabin = fare.get("cabin_name", "")
booking = fare.get("booking_class", "")
wd_std = fare.get("weekday_standard", "-")
wd_pref = fare.get("weekday_preferred", "-")
we_std = fare.get("weekend_standard", "-")
we_pref = fare.get("weekend_preferred", "-")
if isinstance(wd_std, int):
wd_std = f"{wd_std:,}"
if isinstance(wd_pref, int):
wd_pref = f"{wd_pref:,}"
if isinstance(we_std, int):
we_std = f"{we_std:,}"
if isinstance(we_pref, int):
we_pref = f"{we_pref:,}"
content += f"| {cabin} | {booking} | {wd_std} | {wd_pref} | {we_std} | {we_pref} |\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "codeshare_domestic_fares",
"airline": marketing_carrier,
"operating_carrier": operating_carrier,
"route": route
}
})
return chunks
def handle_international_route_fares(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""international_route_fares ์ฒ˜๋ฆฌ - ๊ตญ์ œ์„  ๋…ธ์„ ๋ณ„ ์šด์ž„"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
route = item.get("route", "")
region = item.get("region", "")
airline = item.get("airline", context.get("chain", "UNKNOWN"))
trip_type = item.get("trip_type", "ROUND_TRIP")
content = f"# {airline} {route} ๊ตญ์ œ์„  ์šด์ž„ ({region})\n\n"
content += f"**๋…ธ์„ **: {route}\n"
content += f"**์ง€์—ญ**: {region}\n"
content += f"**์šด์ž„ ์œ ํ˜•**: {'์™•๋ณต' if trip_type == 'ROUND_TRIP' else 'ํŽธ๋„'}\n"
# ์„ธ๊ธˆ/๋ถ€๊ฐ€์„ธ
fuel = item.get("fuel_surcharge", 0)
tax = item.get("tax", 0)
if fuel or tax:
content += f"**์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ**: {fuel:,}์›\n"
content += f"**์„ธ๊ธˆ**: {tax:,}์›\n"
# ์šด์ž„ ์ •๋ณด
fares = item.get("fares", [])
if fares:
content += f"\n## ์šด์ž„ํ‘œ\n"
content += "| ๋“ฑ๊ธ‰ | ์˜ˆ์•ฝํด๋ž˜์Šค | ๋น„์ˆ˜๊ธฐ | ์ค€์„ฑ์ˆ˜๊ธฐ | ์„ฑ์ˆ˜๊ธฐ |\n"
content += "|------|-----------|---------|---------|--------|\n"
for fare in fares:
cabin = fare.get("cabin_name", "")
booking = fare.get("booking_class", "")
low = fare.get("low_season", "-")
shoulder = fare.get("shoulder_season", "-")
high = fare.get("high_season", "-")
if isinstance(low, int):
low = f"{low:,}"
if isinstance(shoulder, int):
shoulder = f"{shoulder:,}"
if isinstance(high, int):
high = f"{high:,}"
content += f"| {cabin} | {booking} | {low} | {shoulder} | {high} |\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "international_route_fares",
"airline": airline,
"route": route,
"region": region
}
})
return chunks
def handle_fare_comparison_summary(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""fare_comparison_summary ์ฒ˜๋ฆฌ - ์šด์ž„ ๋น„๊ต ์š”์•ฝ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
content = "# ๋…ธ์„ ๋ณ„ ์ตœ์ €๊ฐ€ ์šด์ž„ ๋น„๊ต\n\n"
content += "| ์ง€์—ญ | ๋…ธ์„  | ์™•๋ณต์œ ํ˜• | ์ตœ์ €์šด์ž„ | ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ | ์„ธ๊ธˆ | ์ด์•ก |\n"
content += "|------|------|----------|---------|-----------|-----|------|\n"
for item in items:
region = item.get("region", "")
route = item.get("route", "")
trip = item.get("trip_type", "")
lowest = item.get("lowest_economy", 0)
fuel = item.get("fuel_surcharge", 0)
tax = item.get("tax", 0)
total = item.get("total", 0)
note = item.get("note", "")
trip_str = "์™•๋ณต" if trip == "ROUND_TRIP" else "ํŽธ๋„"
content += f"| {region} | {route} | {trip_str} | {lowest:,} | {fuel:,} | {tax:,} | {total:,} |\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "fare_comparison_summary",
"airline": context.get("chain", "UNKNOWN")
}
})
return chunks
def handle_important_notes(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""important_notes ์ฒ˜๋ฆฌ - ์ค‘์š” ์ฐธ๊ณ ์‚ฌํ•ญ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
content = "# ์šด์ž„ ๊ด€๋ จ ์ค‘์š” ์ฐธ๊ณ ์‚ฌํ•ญ\n\n"
for item in items:
note_id = item.get("note_id", "")
description = item.get("description", "")
if description:
content += f"- **{note_id}**: {description}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "important_notes",
"category": context.get("chain", "UNKNOWN")
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ•ญ๊ณต ๋ถ€๊ฐ€ ์š”๊ธˆ ๊ทœ์ • (Extra Fee Rules)
# ===========================================================================
def handle_baggage_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""baggage_rules ์ฒ˜๋ฆฌ - ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", context.get("chain", "UNKNOWN"))
rule_name = item.get("rule_name", "์ˆ˜ํ•˜๋ฌผ ๊ทœ์ •")
route_type = item.get("route_type", "")
content = f"# {airline} {rule_name}\n\n"
if route_type:
route_label = "๊ตญ๋‚ด์„ " if route_type == "DOMESTIC" else "๊ตญ์ œ์„ " if route_type == "INTERNATIONAL" else route_type
content += f"**๋…ธ์„  ์œ ํ˜•**: {route_label}\n"
# Allowances
allowances = item.get("allowances", [])
if allowances:
content += f"\n## ๋ฌด๋ฃŒ ์ˆ˜ํ•˜๋ฌผ ํ—ˆ์šฉ๋Ÿ‰\n"
for allow in allowances:
if isinstance(allow, dict):
cabin = allow.get("cabin_class_name") or allow.get("cabin_class", "")
fare = allow.get("fare_class", "")
pieces = allow.get("pieces", "")
weight = allow.get("weight_kg") or allow.get("max_weight_per_piece_kg", "")
info = f"- **{cabin}**"
if fare:
info += f" ({fare})"
info += ": "
if pieces:
info += f"{pieces}๊ฐœ"
if weight:
info += f" (๊ฐ {weight}kg)" if pieces else f"{weight}kg"
content += info + "\n"
# Excess fee
excess = item.get("excess_fee", {})
if excess:
desc = excess.get("description", "")
amount = excess.get("amount", 0)
currency = excess.get("currency", "KRW")
unit = excess.get("unit", "")
if desc:
content += f"\n**์ดˆ๊ณผ ์š”๊ธˆ**: {desc}\n"
elif amount:
content += f"\n**์ดˆ๊ณผ ์š”๊ธˆ**: {amount:,} {currency} ({unit})\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "baggage_rules",
"airline": airline,
"route_type": route_type
}
})
return chunks
def handle_baggage_general_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""baggage_general_rules ์ฒ˜๋ฆฌ - ์ผ๋ฐ˜ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ • (์ •์ฑ…, ๊ธˆ์ง€ ํ’ˆ๋ชฉ, ์ œํ•œ ์‚ฌํ•ญ ๋“ฑ)
baggage_rules์™€ ๋‹ฌ๋ฆฌ ๊ตฌ์ฒด์ ์ธ ํ—ˆ์šฉ๋Ÿ‰๋ณด๋‹ค ์ •์ฑ…์ ์ธ ๋‚ด์šฉ์„ ๋‹ค๋ฃธ:
- ์ˆ˜ํ•˜๋ฌผ ์ •์ฑ… ๊ฐœ์š”
- ์œ„ํƒ/๊ธฐ๋‚ด/ํœด๋Œ€ ์ˆ˜ํ•˜๋ฌผ ์ œํ•œ
- ๊ธˆ์ง€ ํ’ˆ๋ชฉ
- ํŠน์ˆ˜ ์ˆ˜ํ•˜๋ฌผ ์ •์ฑ…
"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
for item in items:
if not isinstance(item, dict):
continue
rule_name = item.get("rule_name", "์ผ๋ฐ˜ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ •")
category = item.get("category", "")
route_type = item.get("route_type", "")
content = f"# {airline} {rule_name}\n\n"
if route_type:
route_label = "๊ตญ๋‚ด์„ " if route_type == "DOMESTIC" else "๊ตญ์ œ์„ " if route_type == "INTERNATIONAL" else route_type
content += f"**๋…ธ์„  ์œ ํ˜•**: {route_label}\n"
if category:
content += f"**๋ถ„๋ฅ˜**: {category}\n"
# ์ •์ฑ… ์„ค๋ช…
description = item.get("description", "")
if description:
content += f"\n{description}\n"
# ๊ธˆ์ง€ ํ’ˆ๋ชฉ (prohibited_items)
prohibited = item.get("prohibited_items", [])
if prohibited:
content += "\n## ๊ธˆ์ง€ ํ’ˆ๋ชฉ\n"
for p_item in prohibited:
if isinstance(p_item, dict):
item_name = p_item.get("item", p_item.get("name", ""))
reason = p_item.get("reason", "")
location = p_item.get("location", "") # cabin_only, checked_only, etc.
content += f"- **{item_name}**"
if location:
content += f" ({location})"
if reason:
content += f": {reason}"
content += "\n"
elif isinstance(p_item, str):
content += f"- {p_item}\n"
# ์ œํ•œ ํ’ˆ๋ชฉ (restricted_items)
restricted = item.get("restricted_items", [])
if restricted:
content += "\n## ์ œํ•œ ํ’ˆ๋ชฉ\n"
for r_item in restricted:
if isinstance(r_item, dict):
item_name = r_item.get("item", r_item.get("name", ""))
condition = r_item.get("condition", "")
max_qty = r_item.get("max_quantity", "")
content += f"- **{item_name}**"
if max_qty:
content += f" (์ตœ๋Œ€ {max_qty}๊ฐœ)"
if condition:
content += f": {condition}"
content += "\n"
elif isinstance(r_item, str):
content += f"- {r_item}\n"
# ๊ธฐ๋‚ด ์ˆ˜ํ•˜๋ฌผ ์ •์ฑ… (carry_on_policy)
carry_on = item.get("carry_on_policy", {})
if carry_on and isinstance(carry_on, dict):
content += "\n## ๊ธฐ๋‚ด ์ˆ˜ํ•˜๋ฌผ\n"
max_weight = carry_on.get("max_weight_kg", "")
max_dims = carry_on.get("max_dimensions", "")
max_pieces = carry_on.get("max_pieces", "")
if max_weight:
content += f"- ์ตœ๋Œ€ ๋ฌด๊ฒŒ: {max_weight}kg\n"
if max_dims:
content += f"- ์ตœ๋Œ€ ๊ทœ๊ฒฉ: {max_dims}\n"
if max_pieces:
content += f"- ์ตœ๋Œ€ ๊ฐœ์ˆ˜: {max_pieces}๊ฐœ\n"
# ์œ„ํƒ ์ˆ˜ํ•˜๋ฌผ ์ •์ฑ… (checked_policy)
checked = item.get("checked_policy", {})
if checked and isinstance(checked, dict):
content += "\n## ์œ„ํƒ ์ˆ˜ํ•˜๋ฌผ\n"
max_weight = checked.get("max_weight_kg", "")
max_dims = checked.get("max_dimensions", "")
if max_weight:
content += f"- ์ตœ๋Œ€ ๋ฌด๊ฒŒ: {max_weight}kg\n"
if max_dims:
content += f"- ์ตœ๋Œ€ ๊ทœ๊ฒฉ: {max_dims}\n"
# ํŠน๋ณ„ ๊ทœ์ • (special_rules)
special_rules = item.get("special_rules", [])
if special_rules:
content += "\n## ํŠน๋ณ„ ๊ทœ์ •\n"
for rule in special_rules:
if isinstance(rule, dict):
rule_title = rule.get("title", rule.get("rule", ""))
rule_desc = rule.get("description", "")
content += f"- **{rule_title}**"
if rule_desc:
content += f": {rule_desc}"
content += "\n"
elif isinstance(rule, str):
content += f"- {rule}\n"
# ์ฐธ๊ณ  ์‚ฌํ•ญ (notes)
notes = item.get("notes", [])
if notes:
content += "\n## ์ฐธ๊ณ  ์‚ฌํ•ญ\n"
for note in notes:
content += f"- {note}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "baggage_general_rules",
"airline": airline,
"category": category,
"route_type": route_type
}
})
return chunks
def handle_sports_equipment_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""sports_equipment_rules ์ฒ˜๋ฆฌ - ์Šคํฌ์ธ  ์žฅ๋น„ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ์Šคํฌ์ธ  ์žฅ๋น„ ์šด์†ก ๊ทœ์ •\n\n"
for item in items:
rule_name = item.get("rule_name", "")
route_type = item.get("route_type", "")
equipment_type = item.get("equipment_type", "")
if rule_name:
content += f"## {rule_name}\n"
max_weight = item.get("max_weight_kg", "")
max_dims = item.get("max_dimensions_cm", "")
if max_weight:
content += f"- **์ตœ๋Œ€ ๋ฌด๊ฒŒ**: {max_weight}kg\n"
if max_dims:
note = item.get("dimensions_note", "")
content += f"- **์ตœ๋Œ€ ๊ทœ๊ฒฉ**: {max_dims}cm ({note})\n" if note else f"- **์ตœ๋Œ€ ๊ทœ๊ฒฉ**: {max_dims}cm\n"
equipment_types = item.get("equipment_types", [])
if equipment_types:
content += f"- **์ ์šฉ ์žฅ๋น„**: {', '.join(equipment_types)}\n"
exceptions = item.get("exceptions", [])
if exceptions:
content += f"\n**์˜ˆ์™ธ ๊ทœ์ •**:\n"
for exc in exceptions:
cond = exc.get("condition", "")
waiver = exc.get("waiver", "")
content += f"- {cond}: {waiver}\n"
policy = item.get("policy", "")
if policy:
content += f"- **์ •์ฑ…**: {policy}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "sports_equipment_rules",
"airline": airline
}
})
return chunks
def handle_pet_transport_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""pet_transport_rules ์ฒ˜๋ฆฌ - ๋ฐ˜๋ ค๋™๋ฌผ ์šด์†ก ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋ฐ˜๋ ค๋™๋ฌผ ์šด์†ก ๊ทœ์ •\n\n"
for item in items:
rule_name = item.get("rule_name", "")
route_type = item.get("route_type", "")
if rule_name:
content += f"## {rule_name}\n"
# Basic conditions
allowed_pets = item.get("allowed_pets", [])
if allowed_pets:
content += f"- **ํ—ˆ์šฉ ๋™๋ฌผ**: {', '.join(allowed_pets)}\n"
cabin_weight = item.get("cabin_max_weight_kg", "")
cargo_weight = item.get("cargo_max_weight_kg", "")
if cabin_weight:
content += f"- **๊ธฐ๋‚ด ๋ฐ˜์ž… ์ตœ๋Œ€ ๋ฌด๊ฒŒ**: {cabin_weight}kg (์šฉ๊ธฐ ํฌํ•จ)\n"
if cargo_weight:
content += f"- **ํ™”๋ฌผ์นธ ์ตœ๋Œ€ ๋ฌด๊ฒŒ**: {cargo_weight}kg (์šฉ๊ธฐ ํฌํ•จ)\n"
# Fees
fees = item.get("fees", [])
if fees:
content += f"\n**์š”๊ธˆ**:\n"
for fee in fees:
if isinstance(fee, dict):
weight = fee.get("weight_range", "") or fee.get("route_type", "")
route_desc = fee.get("route_description", "")
fee_krw = fee.get("fee_krw", 0)
transport_type = fee.get("transport_type", "")
info = f"- {weight}"
if route_desc:
info += f" ({route_desc})"
if transport_type:
info += f" [{transport_type}]"
info += f": {fee_krw:,}์›"
content += info + "\n"
# Container requirements
container = item.get("container_requirements", {})
if container:
content += f"\n**์šฉ๊ธฐ ์กฐ๊ฑด**:\n"
max_dims = container.get("max_dimensions_cm", "")
max_height = container.get("max_height_cm", "")
material = container.get("material", "")
if max_dims:
content += f"- ์ตœ๋Œ€ ํฌ๊ธฐ: {max_dims}cm\n"
if max_height:
content += f"- ์ตœ๋Œ€ ๋†’์ด: {max_height}cm\n"
if material:
content += f"- ์žฌ์งˆ: {material}\n"
# Restrictions
restrictions = item.get("restrictions", {})
if restrictions:
content += f"\n**์ œํ•œ ์‚ฌํ•ญ**:\n"
cargo = restrictions.get("cargo_transport", "")
cabin = restrictions.get("cabin_transport", "")
if cargo:
content += f"- ํ™”๋ฌผ์นธ: {cargo}\n"
if cabin:
content += f"- ๊ธฐ๋‚ด: {cabin}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "pet_transport_rules",
"airline": airline
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ํ•ญ๊ณต ์šด์ž„ ์ถ”๊ฐ€ ์ •๋ณด (์‹œ์ฆŒ, ํด๋ž˜์Šค, ์„ธ๊ธˆ, ๊ณตํ•ญ, ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ, ํŒฉํŠธ)
# ===========================================================================
def handle_international_season_definitions(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""international_season_definitions ์ฒ˜๋ฆฌ - ๊ตญ์ œ์„  ์‹œ์ฆŒ ์ •์˜"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", context.get("chain", "UNKNOWN"))
description = item.get("description", "")
content = f"# {airline} ๊ตญ์ œ์„  ์‹œ์ฆŒ ๊ตฌ๋ถ„\n\n"
if description:
content += f"{description}\n\n"
seasons = item.get("seasons", [])
for season in seasons:
season_type = season.get("season_type", "")
season_name = season.get("season_name", "")
content += f"## {season_name} ({season_type})\n"
periods = season.get("typical_periods", [])
if periods:
content += "**๊ธฐ๊ฐ„**:\n"
for period in periods:
content += f"- {period}\n"
notes = item.get("notes", [])
if notes:
content += "\n**์ฐธ๊ณ ์‚ฌํ•ญ**:\n"
for note in notes:
content += f"- {note}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "international_season_definitions",
"airline": airline
}
})
return chunks
def handle_booking_class_definitions(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""booking_class_definitions ์ฒ˜๋ฆฌ - ์˜ˆ์•ฝ ํด๋ž˜์Šค ์ •์˜"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", context.get("chain", "UNKNOWN"))
cabin_class = item.get("cabin_class", "")
cabin_name = "๋น„์ฆˆ๋‹ˆ์Šค์„" if cabin_class == "BUSINESS" else "์ผ๋ฐ˜์„" if cabin_class == "ECONOMY" else cabin_class
content = f"# {airline} {cabin_name} ์˜ˆ์•ฝ ํด๋ž˜์Šค ์•ˆ๋‚ด\n\n"
classes = item.get("classes", [])
for cls in classes:
code = cls.get("code", "")
name = cls.get("name", "")
desc = cls.get("description", "")
flexibility = cls.get("flexibility", "")
refundable = cls.get("refundable")
changeable = cls.get("changeable")
content += f"## {code} ํด๋ž˜์Šค - {name}\n"
if desc:
content += f"- {desc}\n"
if flexibility:
flex_label = {"HIGH": "๋†’์Œ", "MEDIUM": "์ค‘๊ฐ„", "LOW": "๋‚ฎ์Œ", "VERY_LOW": "๋งค์šฐ ๋‚ฎ์Œ"}.get(flexibility, flexibility)
content += f"- **์œ ์—ฐ์„ฑ**: {flex_label}\n"
if refundable is not None:
content += f"- **ํ™˜๋ถˆ ๊ฐ€๋Šฅ**: {'์˜ˆ' if refundable else '์•„๋‹ˆ์˜ค'}\n"
if changeable is not None:
content += f"- **๋ณ€๊ฒฝ ๊ฐ€๋Šฅ**: {'์˜ˆ' if changeable else '์•„๋‹ˆ์˜ค'}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "booking_class_definitions",
"airline": airline,
"cabin_class": cabin_class
}
})
return chunks
def handle_special_tax_structures(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""special_tax_structures ์ฒ˜๋ฆฌ - ํŠน์ˆ˜ ์„ธ๊ธˆ ๊ตฌ์กฐ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋…ธ์„ ๋ณ„ ํŠน์ˆ˜ ์„ธ๊ธˆ ์•ˆ๋‚ด\n\n"
content += "์ผ๋ถ€ ๋…ธ์„ ์€ ๋„์ฐฉ ๊ตญ๊ฐ€์˜ ํŠน๋ณ„ ์„ธ๊ธˆ์œผ๋กœ ์ธํ•ด ํƒ€ ๋…ธ์„  ๋Œ€๋น„ ์„ธ๊ธˆ์ด ๋†’์Šต๋‹ˆ๋‹ค.\n\n"
for item in items:
route = item.get("route", "")
route_name = item.get("route_name", "")
tax_note = item.get("tax_note", "")
content += f"## {route_name} ({route})\n"
if tax_note:
content += f"**ํŠน์ด์‚ฌํ•ญ**: {tax_note}\n\n"
tax_by_class = item.get("tax_by_class", [])
if tax_by_class:
content += "| ์ขŒ์„ ๋“ฑ๊ธ‰ | ์„ธ๊ธˆ |\n|----------|------|\n"
for tax in tax_by_class:
cabin = tax.get("cabin_class", "")
cabin_label = "๋น„์ฆˆ๋‹ˆ์Šค" if cabin == "BUSINESS" else "์ผ๋ฐ˜์„" if cabin == "ECONOMY" else cabin
amount = tax.get("tax_amount", 0)
currency = tax.get("currency", "KRW")
content += f"| {cabin_label} | {amount:,}์› |\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "special_tax_structures",
"airline": airline
}
})
return chunks
def handle_multi_airport_routes(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""multi_airport_routes ์ฒ˜๋ฆฌ - ๋ณต์ˆ˜ ๊ณตํ•ญ ๋…ธ์„  ์ •๋ณด"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋ณต์ˆ˜ ๊ณตํ•ญ ๋…ธ์„  ์ •๋ณด\n\n"
content += "์ผ๋ถ€ ๋„์‹œ๋Š” ์—ฌ๋Ÿฌ ๊ณตํ•ญ์—์„œ ์šดํ•ญ๋ฉ๋‹ˆ๋‹ค.\n\n"
for item in items:
city = item.get("city", "")
note = item.get("note", "")
content += f"## {city}\n"
airports = item.get("airports", [])
for airport in airports:
code = airport.get("code", "")
name = airport.get("name", "")
ap_type = airport.get("type", "")
type_label = "(๋ฉ”์ธ)" if ap_type == "MAIN" else "(์‹œํ‹ฐ)" if ap_type == "CITY" else ""
content += f"- **{code}** - {name} {type_label}\n"
routes_from = item.get("routes_from_gmp", [])
if routes_from:
content += "\n**๊น€ํฌ ์ถœ๋ฐœ ๊ฐ€๋Šฅ ๋…ธ์„ **:\n"
for route in routes_from:
dest = route.get("destination", "")
dest_name = route.get("destination_name", "")
content += f"- {dest_name} ({dest})\n"
if note:
content += f"\n**์ฐธ๊ณ **: {note}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "multi_airport_routes",
"airline": airline
}
})
return chunks
def handle_fuel_surcharge_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""fuel_surcharge_tiers ์ฒ˜๋ฆฌ - ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ ๋‹จ๊ณ„๋ณ„ ์ •๋ณด"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
for item in items:
airline = item.get("airline", context.get("chain", "UNKNOWN"))
effective_date = item.get("effective_date", "")
currency = item.get("currency", "KRW")
content = f"# {airline} ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ ์•ˆ๋‚ด\n\n"
if effective_date:
content += f"**์ ์šฉ ๊ธฐ์ค€์ผ**: {effective_date}\n\n"
tiers = item.get("tiers", [])
if tiers:
content += "| ๊ตฌ๊ฐ„ | ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ | ํฌํ•จ ๋…ธ์„  |\n|------|----------|----------|\n"
for tier in tiers:
tier_name = tier.get("tier_name", "")
amount = tier.get("amount", 0)
routes = tier.get("routes", [])
desc = tier.get("description", "")
routes_str = ", ".join(routes[:5])
if len(routes) > 5:
routes_str += f" ์™ธ {len(routes)-5}๊ฐœ"
if desc:
tier_name = f"{tier_name} ({desc})"
content += f"| {tier_name} | {amount:,}์› | {routes_str} |\n"
content += "\n"
notes = item.get("notes", [])
if notes:
content += "**์ฐธ๊ณ ์‚ฌํ•ญ**:\n"
for note in notes:
content += f"- {note}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "fuel_surcharge_tiers",
"airline": airline,
"effective_date": effective_date
}
})
return chunks
def handle_facts_router(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""facts ๋ผ์šฐํ„ฐ - ๋„๋ฉ”์ธ๋ณ„๋กœ ์ ์ ˆํ•œ ํ•ธ๋“ค๋Ÿฌ๋กœ ๋ถ„๊ธฐ
ํ˜ธํ…” ๋„๋ฉ”์ธ: NESTED_HANDLERS๋กœ ์œ„์ž„ (facts.pricing_analysis, facts.facilities ๋“ฑ)
ํ•ญ๊ณต ๋„๋ฉ”์ธ: handle_airline_facts๋กœ ์ฒ˜๋ฆฌ (price_analysis, booking_tips, warnings)
์ด ๋ผ์šฐํ„ฐ๊ฐ€ ํ•„์š”ํ•œ ์ด์œ :
- ํ˜ธํ…”๊ณผ ํ•ญ๊ณต ๋ฌธ์„œ ๋ชจ๋‘ 'facts' ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ ๋‚ด๋ถ€ ๊ตฌ์กฐ๊ฐ€ ๋‹ค๋ฆ„
- ํ˜ธํ…” facts: facilities, room_types, pricing_analysis ๋“ฑ
- ํ•ญ๊ณต facts: price_analysis, booking_tips, warnings ๋“ฑ
"""
if not data or not isinstance(data, dict):
return []
# ๋„๋ฉ”์ธ ๊ฐ์ง€
chain = context.get("chain", "UNKNOWN")
doc_type = context.get("document_type", "") or ""
doc_category = context.get("document_category", "") or ""
# ํ•ญ๊ณต์‚ฌ ๋„๋ฉ”์ธ ํŒ๋ณ„
airline_chains = {
"KOREAN_AIR", "ASIANA", "DELTA", "UNITED", "AMERICAN",
"SINGAPORE", "ANA", "JAL", "CATHAY", "EMIRATES",
"AIRLINE", "ALLIANCE"
}
is_airline = (
chain in airline_chains or
"AIRLINE" in doc_type.upper() or
"AIRLINE" in doc_category.upper() or
"FARE" in doc_type.upper() or
"MILEAGE" in doc_type.upper()
)
if is_airline:
# ํ•ญ๊ณต facts - ์ „์šฉ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ
return handle_airline_facts(data, context)
# ํ˜ธํ…”/๊ธฐํƒ€ ๋„๋ฉ”์ธ - NESTED_HANDLERS๋กœ ์œ„์ž„
# ๋นˆ ์ฒญํฌ ๋ฐ˜ํ™˜ โ†’ sync_to_supabase.py์˜ ์ค‘์ฒฉ ํ•ธ๋“ค๋Ÿฌ ๋ฃจํ”„์—์„œ ๊ฐœ๋ณ„ ์ฒ˜๋ฆฌ๋จ
# (facts.pricing_analysis, facts.facilities ๋“ฑ์ด ๊ฐ๊ฐ ์ฒ˜๋ฆฌ๋จ)
return []
def handle_airline_facts(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""facts (ํ•ญ๊ณต ์šด์ž„ ๊ด€๋ จ) ์ฒ˜๋ฆฌ - ๊ฐ€๊ฒฉ ๋ถ„์„, ์˜ˆ์•ฝ ํŒ, ์ฃผ์˜์‚ฌํ•ญ
์ฃผ์˜: facts๊ฐ€ dict๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ (์˜ˆ: string) ๋นˆ ์ฒญํฌ ๋ฐ˜ํ™˜
์ด ํ•จ์ˆ˜๋Š” handle_facts_router์—์„œ ํ•ญ๊ณต ๋„๋ฉ”์ธ์ผ ๋•Œ๋งŒ ํ˜ธ์ถœ๋จ
"""
chunks = []
if not data:
return chunks
# ํƒ€์ž… ์•ˆ์ „ ์ฒดํฌ: facts๊ฐ€ dict๊ฐ€ ์•„๋‹ˆ๋ฉด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์Œ
if not isinstance(data, dict):
# string์ด๋‚˜ list์ธ ๊ฒฝ์šฐ ๋นˆ ์ฒญํฌ ๋ฐ˜ํ™˜ (์˜ค๋ฅ˜ ๋ฐฉ์ง€)
return chunks
airline = context.get("chain", "UNKNOWN")
# ๊ฐ€๊ฒฉ ๋ถ„์„
price_analysis = data.get("price_analysis", [])
if price_analysis and isinstance(price_analysis, list):
content = f"# {airline} ๋…ธ์„ ๋ณ„ ๊ฐ€๊ฒฉ ๋ถ„์„\n\n"
for analysis in price_analysis:
# analysis๊ฐ€ dict๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต
if not isinstance(analysis, dict):
continue
category = analysis.get("category", "")
description = analysis.get("description", "")
if category == "cheapest_international_routes":
content += "## ๊ฐ€์žฅ ์ €๋ ดํ•œ ๊ตญ์ œ์„  ๋…ธ์„ \n"
elif category == "most_expensive_routes":
content += "## ๊ฐ€์žฅ ๋น„์‹ผ ๊ตญ์ œ์„  ๋…ธ์„ \n"
elif category == "seasonal_price_difference":
content += "## ๋น„์ˆ˜๊ธฐ-์„ฑ์ˆ˜๊ธฐ ๊ฐ€๊ฒฉ ์ฐจ์ด๊ฐ€ ํฐ ๋…ธ์„ \n"
else:
content += f"## {description}\n"
routes = analysis.get("routes", [])
for route in routes:
# route๊ฐ€ dict๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต
if not isinstance(route, dict):
continue
route_name = route.get("route_name", "")
route_code = route.get("route", "")
min_price = route.get("min_price")
max_price = route.get("max_price")
booking_class = route.get("booking_class", "")
note = route.get("note", "")
content += f"- **{route_name}** ({route_code}): "
if min_price is not None:
content += safe_format_number(min_price, "์›")
if max_price is not None:
if min_price is not None:
content += "~"
content += safe_format_number(max_price, "์›")
if booking_class:
content += f" ({booking_class} ํด๋ž˜์Šค)"
if note:
content += f" - {note}"
content += "\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_price_analysis",
"airline": airline
}
})
# ์˜ˆ์•ฝ ํŒ
booking_tips = data.get("booking_tips", [])
if booking_tips and isinstance(booking_tips, list):
content = f"# {airline} ํ•ญ๊ณต๊ถŒ ์˜ˆ์•ฝ ํŒ\n\n"
for tip in booking_tips:
# tip์ด dict๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต
if not isinstance(tip, dict):
continue
title = tip.get("title", "")
description = tip.get("description", "")
content += f"## {title}\n"
content += f"{description}\n\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_booking_tips",
"airline": airline
}
})
# ์ฃผ์˜์‚ฌํ•ญ
warnings = data.get("warnings", [])
if warnings and isinstance(warnings, list):
content = f"# {airline} ํ•ญ๊ณต๊ถŒ ๊ตฌ๋งค ์‹œ ์ฃผ์˜์‚ฌํ•ญ\n\n"
for warning in warnings:
# warning์ด dict๊ฐ€ ์•„๋‹ˆ๋ฉด ์Šคํ‚ต
if not isinstance(warning, dict):
continue
title = warning.get("title", "")
description = warning.get("description", "")
severity = warning.get("severity", "")
severity_icon = "๐Ÿ”ด" if severity == "HIGH" else "๐ŸŸก" if severity == "MEDIUM" else "๐ŸŸข"
content += f"## {severity_icon} {title}\n"
content += f"{description}\n\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "airline_warnings",
"airline": airline
}
})
return chunks
def handle_unaccompanied_minor_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""unaccompanied_minor_rules ์ฒ˜๋ฆฌ - ๋น„๋™๋ฐ˜ ์†Œ์•„ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋น„๋™๋ฐ˜ ์†Œ์•„ (UM) ์„œ๋น„์Šค ๊ทœ์ •\n\n"
for item in items:
rule_name = item.get("rule_name", "")
route_type = item.get("route_type", "")
age_range = item.get("age_range", "")
route_label = "๊ตญ๋‚ด์„ " if route_type == "DOMESTIC" else "๊ตญ์ œ์„ " if route_type == "INTERNATIONAL" else route_type
content += f"## {rule_name}\n" if rule_name else f"## {route_label}\n"
content += f"- **๋Œ€์ƒ ๋‚˜์ด**: {age_range}\n"
fare_rule = item.get("fare_rule", "")
if fare_rule:
content += f"- **์šด์ž„ ๊ทœ์ •**: {fare_rule}\n"
service_fees = item.get("service_fees", [])
if service_fees:
content += f"- **์„œ๋น„์Šค ์ˆ˜์ˆ˜๋ฃŒ**:\n"
for fee in service_fees:
currency = fee.get("currency", "")
amount = fee.get("amount", 0)
unit = fee.get("unit", "")
content += f" - {currency} {amount:,} ({unit})\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "unaccompanied_minor_rules",
"airline": airline
}
})
return chunks
def handle_refund_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""refund_rules ์ฒ˜๋ฆฌ - ํ™˜๋ถˆ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
for item in items:
rule_name = item.get("rule_name", "ํ™˜๋ถˆ ๊ทœ์ •")
route_type = item.get("route_type", "")
cabin_class = item.get("cabin_class", "")
route_category = item.get("route_category", "")
content = f"# {airline} {rule_name}\n\n"
if route_type:
route_label = "๊ตญ๋‚ด์„ " if route_type == "DOMESTIC" else "๊ตญ์ œ์„ " if route_type == "INTERNATIONAL" else route_type
content += f"**๋…ธ์„  ์œ ํ˜•**: {route_label}\n"
if cabin_class:
content += f"**์ขŒ์„ ๋“ฑ๊ธ‰**: {cabin_class}\n"
if route_category:
content += f"**๋…ธ์„  ๊ตฌ๋ถ„**: {route_category}\n"
# Required documents
docs = item.get("required_documents", {})
if docs:
content += f"\n## ํ•„์š” ์„œ๋ฅ˜\n"
for doc_type, doc_list in docs.items():
if isinstance(doc_list, list):
content += f"**{doc_type}**:\n"
for doc in doc_list:
content += f"- {doc}\n"
# Fees
fees = item.get("fees", [])
if fees:
content += f"\n## ์ˆ˜์ˆ˜๋ฃŒ\n"
for fee in fees:
if isinstance(fee, dict):
fare_class = fee.get("fare_class", "")
fare_type = fee.get("fare_type", "")
fee_krw = fee.get("fee_krw", 0)
amount = fee.get("amount", 0)
currency = fee.get("currency", "")
if fare_class:
content += f"- {fare_class} ({fare_type}): {fee_krw:,}์›\n"
elif currency:
content += f"- {currency}: {amount:,}\n"
# Penalties
penalties = item.get("penalties", [])
if penalties:
content += f"\n## ํ™˜๋ถˆ ์œ„์•ฝ๊ธˆ\n"
for penalty in penalties:
timing = penalty.get("timing", "")
# Get all fee values
fee_values = []
for key, value in penalty.items():
if key != "timing" and isinstance(value, (int, float)) and value >= 0:
fee_values.append(f"{key}: {value:,}์›")
if fee_values:
content += f"- **{timing}**: {', '.join(fee_values)}\n"
# Waivers
waivers = item.get("waivers", {})
if waivers:
content += f"\n## ๋ฉด์ œ ์กฐ๊ฑด\n"
for waiver_type, waiver_list in waivers.items():
if isinstance(waiver_list, list):
content += f"**{waiver_type}**:\n"
for waiver in waiver_list:
content += f"- {waiver}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "refund_rules",
"airline": airline,
"route_type": route_type,
"cabin_class": cabin_class
}
})
return chunks
def handle_no_show_penalties(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""no_show_penalties ์ฒ˜๋ฆฌ - ์˜ˆ์•ฝ๋ถ€๋„ ์œ„์•ฝ๊ธˆ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ์˜ˆ์•ฝ๋ถ€๋„ (No-Show) ์œ„์•ฝ๊ธˆ\n\n"
for item in items:
rule_name = item.get("rule_name", "")
route_type = item.get("route_type", "")
route_label = "๊ตญ๋‚ด์„ " if route_type == "DOMESTIC" else "๊ตญ์ œ์„ " if route_type == "INTERNATIONAL" else route_type
content += f"## {rule_name or route_label}\n"
penalty_krw = item.get("penalty_krw", 0)
if penalty_krw:
unit = item.get("unit", "")
content += f"- **์œ„์•ฝ๊ธˆ**: {penalty_krw:,}์› ({unit})\n"
condition = item.get("condition", "")
if condition:
content += f"- **์ ์šฉ ์กฐ๊ฑด**: {condition}\n"
penalties = item.get("penalties", [])
if penalties:
for penalty in penalties:
cabin = penalty.get("cabin_class_name", "") or penalty.get("cabin_class", "")
amount = penalty.get("penalty_krw", 0)
content += f"- {cabin}: {amount:,}์›\n"
additional = item.get("additional_penalty", {})
if additional:
cond = additional.get("condition", "")
add_krw = additional.get("additional_krw", 0)
content += f"- **์ถ”๊ฐ€ ์œ„์•ฝ๊ธˆ**: {cond} - {add_krw:,}์› ์ถ”๊ฐ€\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "no_show_penalties",
"airline": airline
}
})
return chunks
def handle_lounge_penalties(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""lounge_penalties ์ฒ˜๋ฆฌ - ๋ผ์šด์ง€ ์œ„์•ฝ๊ธˆ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋ผ์šด์ง€ ์œ„์•ฝ๊ธˆ\n\n"
for item in items:
rule_name = item.get("rule_name", "")
condition = item.get("condition", "")
if rule_name:
content += f"## {rule_name}\n"
if condition:
content += f"**์ ์šฉ ์กฐ๊ฑด**: {condition}\n"
fees = item.get("fees", [])
if fees:
content += f"\n**์œ„์•ฝ๊ธˆ**:\n"
for fee in fees:
route = fee.get("route_type", "")
fee_krw = fee.get("fee_krw", 0)
fee_usd = fee.get("fee_usd", 0)
content += f"- {route}: {fee_krw:,}์› / USD {fee_usd}\n"
notes = item.get("notes", [])
if notes:
content += f"\n**์œ ์˜์‚ฌํ•ญ**:\n"
for note in notes:
content += f"- {note}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "lounge_penalties",
"airline": airline
}
})
return chunks
def handle_medical_oxygen_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""medical_oxygen_rules ์ฒ˜๋ฆฌ - ๊ธฐ๋‚ด ์˜๋ฃŒ์šฉ ์‚ฐ์†Œ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๊ธฐ๋‚ด ์˜๋ฃŒ์šฉ ์‚ฐ์†Œ ์„œ๋น„์Šค\n\n"
for item in items:
rule_name = item.get("rule_name", "")
if rule_name:
content += f"## {rule_name}\n"
fees = item.get("fees", [])
if fees:
content += f"\n**์š”๊ธˆ**:\n"
for fee in fees:
route = fee.get("route_type", "")
fee_krw = fee.get("fee_krw", 0)
fee_usd = fee.get("fee_usd", 0)
unit = fee.get("unit", "")
if fee_krw:
content += f"- {route}: {fee_krw:,}์› ({unit})\n"
elif fee_usd:
content += f"- {route}: USD {fee_usd} ({unit})\n"
intl_by_country = item.get("international_by_country", [])
if intl_by_country:
content += f"\n**๊ตญ๊ฐ€๋ณ„ ๊ตญ์ œ์„  ์š”๊ธˆ**:\n"
for country in intl_by_country:
name = country.get("departure_country", "")
fee_cad = country.get("fee_cad", 0)
fee_idr = country.get("fee_idr", 0)
fee_usd = country.get("fee_usd", 0)
if fee_cad:
content += f"- {name}: CAD {fee_cad}\n"
elif fee_idr:
content += f"- {name}: IDR {fee_idr:,}\n"
elif fee_usd:
content += f"- {name}: USD {fee_usd}\n"
additional = item.get("additional_charge", "")
if additional:
content += f"\n**์ถ”๊ฐ€ ๋น„์šฉ**: {additional}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "medical_oxygen_rules",
"airline": airline
}
})
return chunks
def handle_codeshare_baggage_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""codeshare_baggage_rules ์ฒ˜๋ฆฌ - ๊ณต๋™์šดํ•ญ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ •"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๊ณต๋™์šดํ•ญํŽธ ์ˆ˜ํ•˜๋ฌผ ๊ทœ์ •\n\n"
for item in items:
rule_name = item.get("rule_name", "")
policy_type = item.get("policy_type", "")
if rule_name:
content += f"## {rule_name}\n"
applicable_airlines = item.get("applicable_airlines", [])
if applicable_airlines:
content += f"**์ ์šฉ ํ•ญ๊ณต์‚ฌ**:\n"
for airline_name in applicable_airlines:
content += f"- {airline_name}\n"
notes = item.get("notes", [])
if notes:
content += f"\n**์œ ์˜์‚ฌํ•ญ**:\n"
for note in notes:
content += f"- {note}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "codeshare_baggage_rules",
"airline": airline
}
})
return chunks
def handle_route_classifications(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""route_classifications ์ฒ˜๋ฆฌ - ๋…ธ์„  ๊ตฌ๋ถ„"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋…ธ์„  ๊ตฌ๋ถ„ ๊ธฐ์ค€\n\n"
for item in items:
rule_name = item.get("rule_name", "")
classification_type = item.get("classification_type", "")
if rule_name:
content += f"## {rule_name}\n"
routes = item.get("routes", [])
if routes:
for route in routes:
route_type = route.get("route_type", "")
route_name = route.get("route_name", "")
destinations = route.get("destinations", [])
content += f"### {route_name} ({route_type})\n"
if destinations:
content += "- " + ", ".join(destinations) + "\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "route_classifications",
"airline": airline
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ (Mileage Efficiency Analysis)
# ===========================================================================
def handle_award_chart_entries(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""award_chart_entries ์ฒ˜๋ฆฌ - ์–ด์›Œ๋“œ ์ฐจํŠธ ์ƒ์„ธ ํ•ญ๋ชฉ
๋‘ ๊ฐ€์ง€ ๊ตฌ์กฐ ์ง€์›:
1) Flat ๊ตฌ์กฐ: [{"region": "...", "route_type": "...", "economy_low": ...}, ...]
2) Nested ๊ตฌ์กฐ: [{"region": "...", "routes": [{"route": "...", ...}]}, ...]
"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
# ๊ตฌ์กฐ ํƒ์ง€: ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์— 'routes' ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด ์ค‘์ฒฉ ๊ตฌ์กฐ
is_nested = any(isinstance(item, dict) and "routes" in item for item in items)
if is_nested:
# === ์ค‘์ฒฉ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ (Flying Blue ์Šคํƒ€์ผ) ===
for region_data in items:
if not isinstance(region_data, dict):
continue
region = region_data.get("region", "OTHER")
region_name = region_data.get("region_name", region)
routes = region_data.get("routes", [])
if not routes:
continue
content = f"# {airline} ์–ด์›Œ๋“œ ์ฐจํŠธ - {region_name}\n\n"
content += "| ๋…ธ์„  | ์ถœ๋ฐœ | ๋„์ฐฉ | ์ด์ฝ”๋…ธ๋ฏธ | ๋น„์ฆˆ๋‹ˆ์Šค | ๋น„๊ณ  |\n"
content += "|------|------|------|----------|----------|------|\n"
for route in routes[:50]: # ์ง€์—ญ๋‹น ์ตœ๋Œ€ 50๊ฐœ ๋…ธ์„ 
if not isinstance(route, dict):
continue
route_code = route.get("route", "")
origin_name = route.get("origin_name", route.get("origin", ""))
dest_name = route.get("destination_name", route.get("destination", ""))
# ์ด์ฝ”๋…ธ๋ฏธ/๋น„์ฆˆ๋‹ˆ์Šค ํฌ์ธํŠธ
eco_low = route.get("economy_low")
eco_high = route.get("economy_high")
biz_low = route.get("business_low")
biz_high = route.get("business_high")
eco_str = f"{eco_low:,}" if eco_low else "N/A"
if eco_high and eco_high != eco_low:
eco_str += f"~{eco_high:,}"
if biz_low:
biz_str = f"{biz_low:,}"
if biz_high and biz_high != biz_low:
biz_str += f"~{biz_high:,}"
else:
biz_str = "N/A"
notes = route.get("notes", "") or ""
content += f"| {route_code} | {origin_name} | {dest_name} | {eco_str} | {biz_str} | {notes} |\n"
content += DISCLAIMER_SHORT
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "award_chart_entries",
"airline": airline,
"region": region,
"region_name": region_name
}
})
else:
# === ๊ธฐ์กด Flat ๊ตฌ์กฐ ์ฒ˜๋ฆฌ ===
region_entries = {}
for item in items:
region = item.get("region", "OTHER")
if region not in region_entries:
region_entries[region] = []
region_entries[region].append(item)
for region, entries in region_entries.items():
content = f"# {airline} ์–ด์›Œ๋“œ ์ฐจํŠธ - {region}\n\n"
for entry in entries:
route_type = entry.get("route_type", "")
trip_type = entry.get("trip_type", "")
trip_label = "์™•๋ณต" if trip_type == "ROUND_TRIP" else "ํŽธ๋„"
content += f"## {route_type} ({trip_label})\n"
# ์ขŒ์„ ๋“ฑ๊ธ‰๋ณ„ ๋งˆ์ผ๋ฆฌ์ง€
economy_low = entry.get("economy_low")
economy_high = entry.get("economy_high")
if economy_low or economy_high:
content += f"- **์ผ๋ฐ˜์„**: {economy_low:,}~{economy_high:,} ๋งˆ์ผ\n"
premium_economy_low = entry.get("premium_economy_low")
premium_economy_high = entry.get("premium_economy_high")
if premium_economy_low or premium_economy_high:
content += f"- **ํ”„๋ฆฌ๋ฏธ์—„ ์ด์ฝ”๋…ธ๋ฏธ**: {premium_economy_low:,}~{premium_economy_high:,} ๋งˆ์ผ\n"
business_low = entry.get("business_low")
business_high = entry.get("business_high")
if business_low or business_high:
content += f"- **๋น„์ฆˆ๋‹ˆ์Šค/ํ”„๋ ˆ์Šคํ‹ฐ์ง€**: {business_low:,}~{business_high:,} ๋งˆ์ผ\n"
first_low = entry.get("first_low")
first_high = entry.get("first_high")
if first_low or first_high:
content += f"- **์ผ๋“ฑ์„**: {first_low:,}~{first_high:,} ๋งˆ์ผ\n"
notes = entry.get("notes", "")
if notes:
content += f"- ์ฐธ๊ณ : {notes}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "award_chart_entries",
"airline": airline,
"region": region
}
})
return chunks
def handle_route_efficiency_data(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""route_efficiency_data ์ฒ˜๋ฆฌ - nested dict ๋ฐ ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ ๋ชจ๋‘ ์ง€์›"""
chunks = []
if not data:
return chunks
# ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: ๋”•์…”๋„ˆ๋ฆฌ(domestic_economy, japan_business ๋“ฑ)๋ผ๋ฉด ๋ชจ๋“  ๊ฐ’์„ ํ‰ํƒ„ํ™”
items = []
if isinstance(data, dict):
for key, val in data.items():
if isinstance(val, list):
items.extend(val)
elif isinstance(val, dict):
items.append(val)
elif isinstance(data, list):
items = data
else:
items = [data]
if not items:
return chunks
airline = context.get("chain", "UNKNOWN")
# ์ง€์—ญ๋ณ„๋กœ ๊ทธ๋ฃนํ™”
region_entries = {}
for item in items:
region = item.get("region", "OTHER")
if region not in region_entries:
region_entries[region] = []
region_entries[region].append(item)
for region, entries in region_entries.items():
# ์ฒญํฌ๊ฐ€ ๋„ˆ๋ฌด ์ปค์ง€์ง€ ์•Š๋„๋ก ์ขŒ์„ ๋“ฑ๊ธ‰๋ณ„๋กœ ๋ถ„ํ• 
class_entries = {}
for entry in entries:
cabin_class = entry.get("cabin_class", "ECONOMY")
if cabin_class not in class_entries:
class_entries[cabin_class] = []
class_entries[cabin_class].append(entry)
for cabin_class, class_items in class_entries.items():
cabin_labels = {
"ECONOMY": "์ผ๋ฐ˜์„",
"PREMIUM_ECONOMY": "ํ”„๋ฆฌ๋ฏธ์—„ ์ด์ฝ”๋…ธ๋ฏธ",
"BUSINESS": "ํ”„๋ ˆ์Šคํ‹ฐ์ง€/๋น„์ฆˆ๋‹ˆ์Šค",
"FIRST": "์ผ๋“ฑ์„"
}
cabin_label = cabin_labels.get(cabin_class, cabin_class)
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ - {region} {cabin_label}\n\n"
content += "| ๋…ธ์„  | ์šด์ž„ ์œ ํ˜• | ํ˜„๊ธˆ๊ฐ€ | ํ•„์š” ๋งˆ์ผ | ํšจ์œจ (์›/๋งˆ์ผ) |\n"
content += "|------|----------|--------|----------|---------------|\n"
for item in class_items[:30]: # ์ตœ๋Œ€ 30๊ฐœ
route = item.get("route", "")
fare_type = item.get("fare_type", "")
cash_fare = item.get("cash_fare_krw", 0)
required_miles = item.get("required_miles", 0)
efficiency = item.get("efficiency_krw_per_mile", 0)
season = item.get("season", "")
season_label = " (์„ฑ์ˆ˜๊ธฐ)" if season == "HIGH" else " (๋น„์ˆ˜๊ธฐ)" if season == "LOW" else ""
content += f"| {route} | {fare_type}{season_label} | {cash_fare:,}์› | {required_miles:,} | {efficiency:.2f} |\n"
content += f"\n**ํšจ์œจ ํ•ด์„**: ์ˆซ์ž๊ฐ€ ๋†’์„์ˆ˜๋ก ๋งˆ์ผ๋ฆฌ์ง€ ๊ฐ€์น˜๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "route_efficiency_data",
"airline": airline,
"region": region,
"cabin_class": cabin_class
}
})
return chunks
def handle_efficiency_ranking(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""efficiency_ranking ์ฒ˜๋ฆฌ - nested dict (top_20/worst_20) ๋ฐ ๋ฆฌ์ŠคํŠธ ๊ตฌ์กฐ ๋ชจ๋‘ ์ง€์›"""
chunks = []
if not data:
return chunks
# ๋ฐ์ดํ„ฐ ์ •๊ทœํ™”: ๋”•์…”๋„ˆ๋ฆฌ(top_20, worst_20 ๋“ฑ)๋ผ๋ฉด ๋ชจ๋“  ๊ฐ’์„ ํ‰ํƒ„ํ™”
items = []
if isinstance(data, dict):
for key, val in data.items():
if isinstance(val, list):
items.extend(val)
elif isinstance(data, list):
items = data
else:
items = [data]
if not items:
return chunks
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ TOP ๋žญํ‚น\n\n"
content += "| ์ˆœ์œ„ | ๋…ธ์„  | ์ขŒ์„ ๋“ฑ๊ธ‰ | ํšจ์œจ (์›/๋งˆ์ผ) |\n"
content += "|------|------|----------|---------------|\n"
for item in items[:20]:
rank = item.get("rank", "")
route = item.get("route", "")
cabin_class = item.get("cabin_class", "")
efficiency = item.get("efficiency_krw_per_mile", 0)
cabin_labels = {
"ECONOMY": "์ผ๋ฐ˜์„",
"PREMIUM_ECONOMY": "ํ”„๋ฆฌ๋ฏธ์—„",
"BUSINESS": "ํ”„๋ ˆ์Šคํ‹ฐ์ง€",
"FIRST": "์ผ๋“ฑ์„"
}
cabin_label = cabin_labels.get(cabin_class, cabin_class)
content += f"| {rank} | {route} | {cabin_label} | {efficiency:.2f} |\n"
content += "\n**ํšจ์œจ ํ•ด์„**: ์ˆซ์ž๊ฐ€ ๋†’์„์ˆ˜๋ก ๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ์‹œ ๋” ํฐ ๊ฐ€์น˜๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "efficiency_ranking",
"airline": airline
}
})
return chunks
def handle_mileage_efficiency_analysis(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""mileage_efficiency_analysis ์ฒ˜๋ฆฌ - ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ"""
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
program = data.get("program", "")
content = f"# {airline} {program} ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ ๊ฐœ์š”\n\n"
analysis_date = data.get("analysis_date", "")
if analysis_date:
content += f"**๋ถ„์„ ๊ธฐ์ค€์ผ**: {analysis_date}\n\n"
method = data.get("calculation_method", "")
if method:
content += f"**๊ณ„์‚ฐ ๋ฐฉ๋ฒ•**: {method}\n\n"
fare_basis_types = data.get("fare_basis_types", [])
if fare_basis_types:
content += "## ์šด์ž„ ๊ธฐ์ค€ ์œ ํ˜•\n"
for fb in fare_basis_types:
basis_type = fb.get("basis_type", "")
description = fb.get("description", "")
applies_to = fb.get("applies_to", "")
content += f"- **{basis_type}**: {description}\n"
if applies_to:
content += f" - ์ ์šฉ ๋Œ€์ƒ: {applies_to}\n"
content += "\n"
evidence = data.get("evidence", [])
evidence_str = format_evidence(evidence)
if evidence_str:
content += evidence_str
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "mileage_efficiency_analysis",
"airline": airline,
"program": program
}
})
return chunks
def handle_fare_basis_comparison(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""fare_basis_comparison ์ฒ˜๋ฆฌ - ์šด์ž„ ๊ธฐ์ค€๋ณ„ ๋น„๊ต"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ์šด์ž„ ๊ธฐ์ค€๋ณ„ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋น„๊ต\n\n"
content += "| ์ง€์—ญ | ์ขŒ์„ ๋“ฑ๊ธ‰ | ์ตœ์ € ์šด์ž„ ํšจ์œจ | ์ตœ๊ณ  ์šด์ž„ ํšจ์œจ | ํšจ์œจ ํ–ฅ์ƒ |\n"
content += "|------|----------|--------------|--------------|----------|\n"
for item in items:
region = item.get("region", "")
cabin_class = item.get("cabin_class", "")
lowest_efficiency = item.get("lowest_fare_efficiency", 0)
highest_efficiency = item.get("highest_fare_efficiency", 0)
improvement = item.get("efficiency_improvement", "")
cabin_labels = {
"ECONOMY": "์ผ๋ฐ˜์„",
"PREMIUM_ECONOMY": "ํ”„๋ฆฌ๋ฏธ์—„",
"BUSINESS": "ํ”„๋ ˆ์Šคํ‹ฐ์ง€",
"FIRST": "์ผ๋“ฑ์„"
}
cabin_label = cabin_labels.get(cabin_class, cabin_class)
content += f"| {region} | {cabin_label} | {lowest_efficiency:.2f} | {highest_efficiency:.2f} | {improvement} |\n"
content += "\n**๋ถ„์„**: ์ตœ๊ณ  ์šด์ž„(ํ’€ํ”Œ๋ ‰์Šค ์š”๊ธˆ)์—์„œ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ์ด ๋” ๋†’์Šต๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "fare_basis_comparison",
"airline": airline
}
})
return chunks
def handle_key_insights(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""key_insights ์ฒ˜๋ฆฌ - ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ\n\n"
for item in items[:10]:
if not isinstance(item, dict):
continue
title = item.get("title", "")
insight_content = item.get("content", "")
if title and insight_content:
content += f"## {title}\n"
content += f"{insight_content}\n\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "key_insights",
"airline": airline
}
})
return chunks
def handle_usage_recommendations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""usage_recommendations ์ฒ˜๋ฆฌ - ๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ๊ถŒ์žฅ์‚ฌํ•ญ
์„ธ ๊ฐ€์ง€ ๊ตฌ์กฐ ์ง€์›:
1) ์ตœ์ƒ์œ„ List ๊ตฌ์กฐ: [{"category": "...", "routes": [...]}]
2) Dict ๊ตฌ์กฐ (๊ฐ’์ด dict): {"highly_recommended": {"description": "...", "routes": [...]}}
3) Dict ๊ตฌ์กฐ (๊ฐ’์ด list): {"highly_recommended": [{route_data...}], "recommended": [...]}
"""
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ๊ถŒ์žฅ์‚ฌํ•ญ\n\n"
# ์ตœ์ƒ์œ„๊ฐ€ ๋ฆฌ์ŠคํŠธ์ธ ๊ฒฝ์šฐ
if isinstance(data, list):
for idx, item in enumerate(data[:20]):
if isinstance(item, dict):
category = item.get("category", item.get("recommendation_type", f"ํ•ญ๋ชฉ {idx+1}"))
desc = item.get("description", "")
routes = item.get("routes", [])
content += f"## {category}\n"
if desc:
content += f"{desc}\n\n"
for route in routes[:10]:
if isinstance(route, dict):
r_name = route.get("route", route.get("route_code", ""))
cabin = route.get("cabin_class", "")
season = route.get("recommended_season", "")
eff_range = route.get("efficiency_range", route.get("efficiency", ""))
content += f"- **{r_name}** ({cabin}"
if season:
content += f", {season}"
content += f"): {eff_range}\n"
else:
content += f"- {route}\n"
content += "\n"
else:
# ๋‹จ์ˆœ ๋ฌธ์ž์—ด
content += f"- {item}\n"
# ๋”•์…”๋„ˆ๋ฆฌ ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
elif isinstance(data, dict):
categories = [
("highly_recommended", "๊ฐ•๋ ฅ ์ถ”์ฒœ", 5),
("recommended", "์ถ”์ฒœ", 3),
("not_recommended", "๋น„์ถ”์ฒœ", 1),
("booking_strategy", "์˜ˆ์•ฝ ์ „๋žต", 4),
]
for key, default_title, _ in categories:
category_data = data.get(key)
if not category_data:
continue
# ๊ฐ’์ด ๋”•์…”๋„ˆ๋ฆฌ์ธ ๊ฒฝ์šฐ (๊ธฐ์กด ๊ตฌ์กฐ)
if isinstance(category_data, dict):
desc = category_data.get("description", default_title)
content += f"## {desc}\n"
routes = category_data.get("routes", [])
for route in routes[:10]:
if isinstance(route, dict):
r_name = route.get("route", "")
cabin = route.get("cabin_class", "")
season = route.get("recommended_season", "")
eff_range = route.get("efficiency_range", "")
if key == "not_recommended":
content += f"- **{r_name}** ({cabin}): {eff_range}์›/๋งˆ์ผ\n"
else:
content += f"- **{r_name}** ({cabin}, {season}): {eff_range}์›/๋งˆ์ผ\n"
content += "\n"
# ๊ฐ’์ด ๋ฆฌ์ŠคํŠธ์ธ ๊ฒฝ์šฐ (Aeroplan ๊ตฌ์กฐ)
elif isinstance(category_data, list):
content += f"## {default_title}\n"
for item in category_data[:10]:
if isinstance(item, dict):
# route_category ๋˜๋Š” route
route_cat = item.get("route_category", item.get("route", ""))
cabin = item.get("cabin_class", "")
points = item.get("points_required", "")
recommendation = item.get("recommendation", item.get("reason", ""))
title = item.get("title", "")
desc = item.get("description", "")
if title: # booking_strategy ํ˜•์‹
content += f"- **{title}**: {desc}\n"
elif route_cat:
content += f"- **{route_cat}** ({cabin}"
if points:
# points๊ฐ€ ์ˆซ์ž์ธ์ง€ ํ™•์ธ ํ›„ ์ฒœ ๋‹จ์œ„ ๊ตฌ๋ถ„์ž ์ ์šฉ
if isinstance(points, (int, float)):
content += f", {points:,}pts"
else:
content += f", {points}pts"
content += f"): {recommendation}\n"
else:
content += f"- {item}\n"
content += "\n"
content += DISCLAIMER_SHORT
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "usage_recommendations",
"airline": airline
}
})
return chunks
def handle_regional_efficiency_summary(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""regional_efficiency_summary ์ฒ˜๋ฆฌ - ์ง€์—ญ๋ณ„ ํšจ์œจ ์š”์•ฝ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ์ง€์—ญ๋ณ„ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ์š”์•ฝ\n\n"
content += "| ์ง€์—ญ | ์ผ๋ฐ˜์„ ํ‰๊ท  | ๋น„์ฆˆ๋‹ˆ์Šค ํ‰๊ท  | ๋น„์ฆˆ๋‹ˆ์Šค ์Šค๋งˆํ‹ฐ์›€ ํ‰๊ท  |\n"
content += "|------|------------|--------------|---------------------|\n"
for item in items:
if not isinstance(item, dict):
continue
region_name = item.get("region_name", item.get("region", ""))
economy_avg = item.get("economy_avg", 0) or "-"
business_avg = item.get("business_avg", 0) or "-"
smartium_avg = item.get("business_smartium_avg") or "-"
if isinstance(economy_avg, (int, float)):
economy_avg = f"{economy_avg:.2f}"
if isinstance(business_avg, (int, float)):
business_avg = f"{business_avg:.2f}"
if isinstance(smartium_avg, (int, float)):
smartium_avg = f"{smartium_avg:.2f}"
content += f"| {region_name} | {economy_avg} | {business_avg} | {smartium_avg} |\n"
content += "\n**ํšจ์œจ ํ•ด์„**: ์ˆซ์ž๊ฐ€ ๋†’์„์ˆ˜๋ก ๋งˆ์ผ๋ฆฌ์ง€ ๊ฐ€์น˜๊ฐ€ ๋†’์Šต๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "regional_efficiency_summary",
"airline": airline
}
})
return chunks
def handle_seasonal_efficiency_summary(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""seasonal_efficiency_summary ์ฒ˜๋ฆฌ - ์‹œ์ฆŒ๋ณ„ ํšจ์œจ ์š”์•ฝ"""
chunks = []
if not data:
return chunks
items = data if isinstance(data, list) else [data]
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ์‹œ์ฆŒ๋ณ„ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋น„๊ต\n\n"
content += "| ์‹œ์ฆŒ | ์ผ๋ฐ˜์„ ํ‰๊ท ํšจ์œจ | ๋น„์ฆˆ๋‹ˆ์Šค ํ‰๊ท ํšจ์œจ |\n"
content += "|------|----------------|------------------|\n"
season_labels = {
"LOW": "๋น„์ˆ˜๊ธฐ",
"SHOULDER": "์ค€์„ฑ์ˆ˜๊ธฐ",
"HIGH": "์„ฑ์ˆ˜๊ธฐ"
}
for item in items:
if not isinstance(item, dict):
continue
season = item.get("season", "")
season_name = item.get("season_name", season_labels.get(season, season))
economy_avg = item.get("economy_avg", 0) or 0
business_avg = item.get("business_avg", 0) or 0
content += f"| {season_name} | {economy_avg:.2f} | {business_avg:.2f} |\n"
content += "\n**๋ถ„์„**: ์„ฑ์ˆ˜๊ธฐ์—๋Š” ์ถ”๊ฐ€ ๋งˆ์ผ๋ฆฌ์ง€ ๊ณต์ œ๋กœ ํšจ์œจ์ด ํ•˜๋ฝํ•ฉ๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "seasonal_efficiency_summary",
"airline": airline
}
})
return chunks
def handle_mileage_usage_strategy(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""mileage_usage_strategy ์ฒ˜๋ฆฌ - ๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ์ „๋žต"""
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ์‚ฌ์šฉ ์ „๋žต ๊ฐ€์ด๋“œ\n\n"
# ๋ฆฌ์ŠคํŠธ ํ˜•ํƒœ์ผ ๊ฒฝ์šฐ ์ง์ ‘ recommendations๋กœ ์ฒ˜๋ฆฌ
if isinstance(data, list):
recommendations = data
else:
recommendations = data.get("recommendations", [])
if recommendations:
content += "## ์ถ”์ฒœ ์ „๋žต\n"
for rec in recommendations:
if not isinstance(rec, dict):
content += f"- {rec}\n"
continue
# strategy_id/title/content ํ˜•์‹ ์ง€์› (Asiana ๋“ฑ)
if rec.get("strategy_id") or rec.get("title"):
title = rec.get("title", rec.get("strategy_id", ""))
rec_content = rec.get("content", "")
content += f"### {title}\n"
if rec_content:
content += f"{rec_content}\n\n"
# priority/strategy ํ˜•์‹ ์ง€์› (๊ธฐ์กด)
else:
priority = rec.get("priority", "")
strategy = rec.get("strategy", "")
routes = rec.get("routes", [])
efficiency_range = rec.get("efficiency_range", "")
if priority or strategy:
content += f"### {priority}: {strategy}\n"
if routes:
content += f"- **์ถ”์ฒœ ๋…ธ์„ **: {', '.join(routes[:5])}\n"
if efficiency_range:
content += f"- **ํšจ์œจ ๋ฒ”์œ„**: {efficiency_range}\n"
content += "\n"
# ๋”•์…”๋„ˆ๋ฆฌ์ผ ๊ฒฝ์šฐ์—๋งŒ ์ถ”๊ฐ€ ํ•„๋“œ ์ฒ˜๋ฆฌ
if isinstance(data, dict):
# Efficiency Tiers
efficiency_tiers = data.get("efficiency_tiers", [])
if efficiency_tiers:
content += "## ํšจ์œจ ๋“ฑ๊ธ‰\n"
for tier in efficiency_tiers:
if isinstance(tier, dict):
tier_name = tier.get("tier", "")
efficiency_range = tier.get("efficiency_range", "")
description = tier.get("description", "")
content += f"- **{tier_name}** ({efficiency_range}): {description}\n"
content += "\n"
# Seasonal Insight
seasonal_insight = data.get("seasonal_insight", {})
if seasonal_insight and isinstance(seasonal_insight, dict):
content += "## ์‹œ์ฆŒ๋ณ„ ์ธ์‚ฌ์ดํŠธ\n"
low_season = seasonal_insight.get("low_season_tip", "")
high_season = seasonal_insight.get("high_season_tip", "")
if low_season:
content += f"- **๋น„์ˆ˜๊ธฐ**: {low_season}\n"
if high_season:
content += f"- **์„ฑ์ˆ˜๊ธฐ**: {high_season}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "mileage_usage_strategy",
"airline": airline
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋งˆ์ผ๋ฆฌ์ง€ ๊ทœ์ • (Mileage Rules)
# ===========================================================================
def handle_mileage_rules_generic(data: Any, context: Dict[str, Any], rule_type: str, title: str) -> List[Dict[str, Any]]:
"""๋ฒ”์šฉ ๋งˆ์ผ๋ฆฌ์ง€ ๊ทœ์ • ํ•ธ๋“ค๋Ÿฌ"""
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} {title}\n\n"
if isinstance(data, list):
for item in data[:20]:
if isinstance(item, dict):
for k, v in item.items():
content += f"- **{k}**: {v}\n"
else:
content += f"- {item}\n"
elif isinstance(data, dict):
for k, v in data.items():
if isinstance(v, list):
content += f"## {k}\n"
for item in v[:10]:
if isinstance(item, dict):
desc = item.get("description", item.get("name", str(item)))
content += f"- {desc}\n"
else:
content += f"- {item}\n"
content += "\n"
elif isinstance(v, dict):
content += f"## {k}\n"
for sub_k, sub_v in v.items():
content += f"- **{sub_k}**: {sub_v}\n"
content += "\n"
else:
content += f"- **{k}**: {v}\n"
else:
content += str(data)
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": rule_type,
"airline": airline
}
})
return chunks
def handle_peak_season_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""peak_seasons, peak_season_rules, peak_season_exemptions ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "peak_season_rules", "์„ฑ์ˆ˜๊ธฐ ๊ทœ์ •")
def handle_refund_change_fees(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""refund_fees, change_fees ๋“ฑ ํ™˜๋ถˆ/๋ณ€๊ฒฝ ์ˆ˜์ˆ˜๋ฃŒ ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "refund_change_fees", "ํ™˜๋ถˆ/๋ณ€๊ฒฝ ์ˆ˜์ˆ˜๋ฃŒ")
def handle_mileage_booking_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""booking_process, booking_notes ๋“ฑ ์˜ˆ์•ฝ ๊ทœ์ • ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "mileage_booking_rules", "๋งˆ์ผ๋ฆฌ์ง€ ํ•ญ๊ณต๊ถŒ ์˜ˆ์•ฝ ๊ทœ์ •")
def handle_mileage_deduction_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""business_smartium_rules, child_mileage_deduction_rules ๋“ฑ ๋งˆ์ผ๋ฆฌ์ง€ ์ฐจ๊ฐ ๊ทœ์ • ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "mileage_deduction_rules", "๋งˆ์ผ๋ฆฌ์ง€ ์ฐจ๊ฐ ๊ทœ์ •")
def handle_route_transfer_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""route_restrictions, transfer_rules ๋“ฑ ๋…ธ์„ /ํ™˜์Šน ๊ทœ์ • ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "route_transfer_rules", "๋…ธ์„ /ํ™˜์Šน ๊ทœ์ •")
def handle_no_show_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""no_show_definitions, no_show_post_processing ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "no_show_rules", "๋…ธ์‡ผ ๊ทœ์ •")
def handle_itinerary_change_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""itinerary_change_rules, itinerary_change_examples ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "itinerary_change_rules", "์—ฌ์ • ๋ณ€๊ฒฝ ๊ทœ์ •")
def handle_mileage_program_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""mileage_now_program, taxes_and_fees ๋“ฑ ๊ธฐํƒ€ ํ”„๋กœ๊ทธ๋žจ ๊ทœ์ • ์ฒ˜๋ฆฌ"""
return handle_mileage_rules_generic(data, context, "mileage_program_rules", "๋งˆ์ผ๋ฆฌ์ง€ ํ”„๋กœ๊ทธ๋žจ ๊ทœ์ •")
def handle_excluded_routes(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""excluded_routes ์ฒ˜๋ฆฌ - ์ œ์™ธ ๋…ธ์„ """
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
content = f"# {airline} ๋งˆ์ผ๋ฆฌ์ง€ ํ•ญ๊ณต๊ถŒ ์ œ์™ธ/์ฃผ์˜ ๋…ธ์„ \n\n"
if isinstance(data, list):
for route in data[:30]:
if isinstance(route, dict):
origin = route.get("origin", "")
dest = route.get("destination", "")
reason = route.get("reason", route.get("note", ""))
content += f"- **{origin} โ†’ {dest}**: {reason}\n"
else:
content += f"- {route}\n"
elif isinstance(data, dict):
for region, routes in data.items():
content += f"\n## {region}\n"
if isinstance(routes, list):
for route in routes[:10]:
content += f"- {route}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "excluded_routes",
"airline": airline
}
})
return chunks
def handle_regional_efficiency_data(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""route_efficiency_data_* ์ง€์—ญ๋ณ„ ํšจ์œจ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (domestic, japan, ๋“ฑ)"""
chunks = []
if not data:
return chunks
airline = context.get("chain", "UNKNOWN")
# ์ง€์—ญ๋ช… ์ถ”์ถœ ์‹œ๋„
region_name = context.get("current_key", "").replace("route_efficiency_data_", "").replace("_", " ").title()
if not region_name:
region_name = "Unknown Region"
content = f"# {airline} {region_name} ์ง€์—ญ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ฐ์ดํ„ฐ\n\n"
if isinstance(data, list):
content += "| ๋…ธ์„  | ์ขŒ์„ | ์‹œ์ฆŒ | ํšจ์œจ |\n"
content += "|------|------|------|------|\n"
for item in data[:30]:
if isinstance(item, dict):
route = item.get("route", item.get("destination", "-"))
cabin = item.get("cabin_class", item.get("class", "-"))
season = item.get("season", "-")
efficiency = item.get("efficiency", item.get("value", "-"))
content += f"| {route} | {cabin} | {season} | {efficiency} |\n"
elif isinstance(data, dict):
for cabin_class, routes in data.items():
content += f"\n## {cabin_class}\n"
if isinstance(routes, list):
for route in routes[:10]:
if isinstance(route, dict):
route_name = route.get("route", route.get("destination", "-"))
eff = route.get("efficiency", route.get("value", "-"))
content += f"- **{route_name}**: {eff}\n"
content += DISCLAIMER_SHORT
if len(content) > 100:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "regional_efficiency_data",
"airline": airline,
"region": region_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ํ”„๋กœ๊ทธ๋žจ (Mileage Purchase Programs)
# ===========================================================================
def handle_mileage_purchase_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""mileage_purchase_programs ์ฒ˜๋ฆฌ - ํ•ญ๊ณต์‚ฌ๋ณ„ ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ๊ฐ€๊ฒฉ ๋ถ„์„"""
chunks = []
if not data:
return chunks
programs = data if isinstance(data, list) else [data]
for program in programs:
if not isinstance(program, dict):
continue
program_name = program.get("program_name", "Unknown Program")
airline = program.get("airline", context.get("chain", "UNKNOWN"))
alliance = program.get("alliance", "")
currency_name = program.get("currency_name", "๋งˆ์ผ")
content = f"# {program_name} ({airline}) ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ๊ฐ€๊ฒฉ ๋ถ„์„\n\n"
if alliance:
content += f"**์ œํœด**: {alliance}\n\n"
# ๊ธฐ๋ณธ ๊ฐ€๊ฒฉ ์ •๋ณด
base_price = program.get("base_price_cents")
if base_price:
content += f"**๊ธฐ๋ณธ ๊ตฌ๋งค๊ฐ€**: {base_price}์„ผํŠธ/{currency_name}\n"
# ๊ตฌ๋งค ํ•œ๋„
limits = program.get("purchase_limits", {})
if limits:
content += "\n## ๊ตฌ๋งค ํ•œ๋„\n"
if limits.get("minimum"):
content += f"- ์ตœ์†Œ ๊ตฌ๋งค: {limits.get('minimum'):,}\n"
if limits.get("maximum_annual"):
content += f"- ์—ฐ๊ฐ„ ์ตœ๋Œ€: {limits.get('maximum_annual'):,}\n"
if limits.get("maximum_monthly"):
content += f"- ์›”๊ฐ„ ์ตœ๋Œ€: {limits.get('maximum_monthly'):,}\n"
# ๋ณด๋„ˆ์Šค ํ‹ฐ์–ด
bonus_tiers = program.get("bonus_tiers", [])
if bonus_tiers:
content += "\n## ๋ณด๋„ˆ์Šค ํ‹ฐ์–ด\n"
content += "| ๊ตฌ๋งค๋Ÿ‰ | ๋ณด๋„ˆ์Šค์œจ |\n|--------|----------|\n"
for tier in bonus_tiers[:10]:
if isinstance(tier, dict):
min_amt = tier.get("purchase_amount_min", 0)
max_amt = tier.get("purchase_amount_max", "โˆž")
bonus_pct = tier.get("bonus_percent", 0)
if max_amt is None:
max_amt = "โˆž"
content += f"| {min_amt:,}~{max_amt} | {bonus_pct}% |\n"
# ๊ถŒ์žฅ ๊ตฌ๋งค ๊ฐ€๊ฒฉ
recommended = program.get("recommended_price", {})
if not recommended:
# ๊ตฌ/์‹  ์ฒด๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
old_system = program.get("old_system", {})
new_system = program.get("new_system", {})
if old_system.get("recommended_price"):
rec = old_system["recommended_price"]
content += "\n## ๊ถŒ์žฅ ๊ตฌ๋งค ๊ฐ€๊ฒฉ (๊ตฌ ์ฒด๊ณ„)\n"
content += f"- **๊ถŒ์žฅ๊ฐ€**: {rec.get('standard_cents', 'N/A')}์„ผํŠธ\n"
content += f"- **์ตœ์ €๊ฐ€**: {rec.get('lowest_cents', 'N/A')}์„ผํŠธ\n"
if rec.get("rationale"):
content += f"- **๊ทผ๊ฑฐ**: {rec.get('rationale')}\n"
if new_system.get("recommended_price"):
rec = new_system["recommended_price"]
content += "\n## ๊ถŒ์žฅ ๊ตฌ๋งค ๊ฐ€๊ฒฉ (์‹  ์ฒด๊ณ„)\n"
content += f"- **๊ถŒ์žฅ๊ฐ€**: {rec.get('standard_cents', 'N/A')}์„ผํŠธ\n"
content += f"- **์ตœ์ €๊ฐ€**: {rec.get('lowest_cents', 'N/A')}์„ผํŠธ\n"
if rec.get("rationale"):
content += f"- **๊ทผ๊ฑฐ**: {rec.get('rationale')}\n"
else:
content += "\n## ๊ถŒ์žฅ ๊ตฌ๋งค ๊ฐ€๊ฒฉ\n"
content += f"- **๊ถŒ์žฅ๊ฐ€**: {recommended.get('standard_cents', 'N/A')}์„ผํŠธ\n"
content += f"- **์ตœ์ €๊ฐ€**: {recommended.get('lowest_cents', 'N/A')}์„ผํŠธ\n"
if recommended.get("lowest_condition"):
content += f"- **์ตœ์ € ์กฐ๊ฑด**: {recommended.get('lowest_condition')}\n"
if recommended.get("rationale"):
content += f"- **๊ทผ๊ฑฐ**: {recommended.get('rationale')}\n"
# ํ”„๋กœ๋ชจ์…˜ ํžˆ์Šคํ† ๋ฆฌ ์š”์•ฝ
promo_history = program.get("promotion_history", [])
if not promo_history:
old_system = program.get("old_system", {})
new_system = program.get("new_system", {})
promo_history = old_system.get("promotion_history", []) + new_system.get("promotion_history", [])
if promo_history:
content += "\n## ์ตœ๊ทผ ํ”„๋กœ๋ชจ์…˜ (์ตœ๊ทผ 5๊ฑด)\n"
content += "| ์‹œ์ž‘์ผ | ์ข…๋ฃŒ์ผ | ๋ณด๋„ˆ์Šค์œจ | ์‹ค์งˆ ๋‹จ๊ฐ€ |\n|--------|--------|----------|----------|\n"
for promo in promo_history[:5]:
if isinstance(promo, dict):
start = promo.get("start_date", "-")
end = promo.get("end_date", "-")
bonus = promo.get("bonus_percent", "-")
eff_price = promo.get("effective_price_cents", "-")
content += f"| {start} | {end} | {bonus}% | {eff_price}์„ผํŠธ |\n"
# ๋งŒ๋ฃŒ ์ •์ฑ…
expiration = program.get("expiration_policy", {})
if expiration:
content += "\n## ๋งŒ๋ฃŒ ์ •์ฑ…\n"
if expiration.get("expires"):
content += f"- **๋งŒ๋ฃŒ ๊ธฐ๊ฐ„**: {expiration.get('expiration_period', 'N/A')}\n"
if expiration.get("extension_method"):
content += f"- **์—ฐ์žฅ ๋ฐฉ๋ฒ•**: {expiration.get('extension_method')}\n"
else:
content += "- ๋งˆ์ผ๋ฆฌ์ง€ ๋งŒ๋ฃŒ ์—†์Œ\n"
# Clube Smiles ๋“ฑ ํŠน๋ณ„ ํ”„๋กœ๊ทธ๋žจ
clube = program.get("clube_smiles", {})
if clube:
content += "\n## Clube Smiles ๊ตฌ๋… ํ”Œ๋žœ\n"
plans = clube.get("plans", [])
for plan in plans[:5]:
if isinstance(plan, dict):
plan_name = plan.get("plan_name", "")
bonus_pct = plan.get("purchase_bonus_percent", 0)
max_bonus = plan.get("max_total_bonus_percent", 0)
content += f"- **{plan_name}**: ๊ตฌ๋งค ๋ณด๋„ˆ์Šค +{bonus_pct}%, ์ตœ๋Œ€ {max_bonus}%\n"
content += DISCLAIMER_SHORT
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "mileage_purchase_program",
"airline": airline,
"program_name": program_name,
"alliance": alliance
}
})
return chunks
def handle_mileage_price_summary(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""price_summary ์ฒ˜๋ฆฌ - ํ”„๋กœ๊ทธ๋žจ๋ณ„ ๊ถŒ์žฅ ๊ตฌ๋งค ๊ฐ€๊ฒฉ ์š”์•ฝ"""
chunks = []
if not data:
return chunks
content = "# ํ•ญ๊ณต์‚ฌ ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ๊ฐ€๊ฒฉ ์š”์•ฝ\n\n"
content += "| ํ”„๋กœ๊ทธ๋žจ | ๊ถŒ์žฅ๊ฐ€ | ์ตœ์ €๊ฐ€ | ๋ณด๋„ˆ์Šค์œจ | ๋น„๊ณ  |\n"
content += "|----------|--------|--------|----------|------|\n"
programs = data if isinstance(data, list) else [data]
for program in programs:
if not isinstance(program, dict):
continue
name = program.get("program_name", "-")
rec_price = program.get("recommended_price_cents", "-")
low_price = program.get("lowest_price_cents", "-")
bonus = program.get("typical_bonus_percent", "-")
notes = program.get("notes", "-")
content += f"| {name} | {rec_price}ยข | {low_price}ยข | {bonus}% | {notes} |\n"
content += "\n> **์ฐธ๊ณ **: ๊ฐ€๊ฒฉ์€ ์„ผํŠธ/๋งˆ์ผ ๊ธฐ์ค€์ด๋ฉฐ, ์„ธ๊ธˆ ๋ณ„๋„์ž…๋‹ˆ๋‹ค.\n"
content += DISCLAIMER_SHORT
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "mileage_price_summary"
}
})
return chunks
def handle_mileage_insights(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""insights ์ฒ˜๋ฆฌ - ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ์ฃผ์š” ์ธ์‚ฌ์ดํŠธ"""
chunks = []
if not data or not isinstance(data, dict):
return chunks
content = "# ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ์ฃผ์š” ์ธ์‚ฌ์ดํŠธ\n\n"
# ํ”„๋กœ๊ทธ๋žจ๋ณ„ ์ „๋žต
strategies = data.get("program_strategies", [])
if strategies:
content += "## ํ”„๋กœ๊ทธ๋žจ๋ณ„ ๊ตฌ๋งค ์ „๋žต\n"
for strategy in strategies[:10]:
if isinstance(strategy, dict):
program = strategy.get("program", "")
strat = strategy.get("strategy", "")
best_use = strategy.get("best_use", "")
risk = strategy.get("risk", "")
content += f"\n### {program}\n"
if strat:
content += f"- **์ „๋žต**: {strat}\n"
if best_use:
content += f"- **์ตœ์  ์‚ฌ์šฉ์ฒ˜**: {best_use}\n"
if risk:
content += f"- **๋ฆฌ์Šคํฌ**: {risk}\n"
# ์‹œ๊ธฐ๋ณ„ ์ „๋žต
timing = data.get("timing_strategies", [])
if timing:
content += "\n## ๊ตฌ๋งค ์‹œ๊ธฐ ์ „๋žต\n"
for t in timing[:5]:
if isinstance(t, dict):
period = t.get("period", "")
tip = t.get("tip", t.get("strategy", ""))
content += f"- **{period}**: {tip}\n"
# ์ผ๋ฐ˜ ํŒ
general_tips = data.get("general_tips", [])
if general_tips:
content += "\n## ์ผ๋ฐ˜ ํŒ\n"
for tip in general_tips[:10]:
if isinstance(tip, dict):
content += f"- {tip.get('tip', str(tip))}\n"
else:
content += f"- {tip}\n"
# ์ฃผ์˜์‚ฌํ•ญ
warnings = data.get("warnings", [])
if warnings:
content += "\n## โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ\n"
for w in warnings[:5]:
if isinstance(w, dict):
content += f"- {w.get('warning', str(w))}\n"
else:
content += f"- {w}\n"
content += DISCLAIMER_SHORT
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": "mileage_purchase_insights"
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๋”œ/ํ”„๋กœ๋ชจ์…˜ (Phase 5)
# ===========================================================================
def handle_deal_alerts(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""deal_alerts ๋˜๋Š” deal_alert ์ฒ˜๋ฆฌ"""
chunks = []
deals = data if isinstance(data, list) else [data]
for deal in deals[:10]:
if not isinstance(deal, dict):
continue
title = deal.get("title", "N/A")
category = deal.get("category", "OTHER")
domain = deal.get("domain", "GENERAL")
content = f"""
๋”œ/ํ”„๋กœ๋ชจ์…˜: {title}
์นดํ…Œ๊ณ ๋ฆฌ: {category}
๋„๋ฉ”์ธ: {domain}
"""
# ๊ด€๋ จ ํ”„๋กœ๊ทธ๋žจ
if deal.get("hotel_chain"):
content += f"ํ˜ธํ…”: {deal.get('hotel_chain')}\n"
if deal.get("airline"):
content += f"ํ•ญ๊ณต์‚ฌ: {deal.get('airline')}\n"
if deal.get("card_issuer"):
content += f"์นด๋“œ์‚ฌ: {deal.get('card_issuer')}\n"
# ๊ธฐ๊ฐ„
start_date = deal.get("start_date", "N/A")
end_date = deal.get("end_date", "N/A")
content += f"\n๊ธฐ๊ฐ„: {start_date} ~ {end_date}\n"
if deal.get("registration_deadline"):
content += f"๋“ฑ๋ก ๋งˆ๊ฐ: {deal.get('registration_deadline')}\n"
# ์š”์•ฝ ๋ฐ ์„ค๋ช…
summary = deal.get("summary", "")
if summary:
content += f"\n์š”์•ฝ: {summary}\n"
description = deal.get("description", "")
if description:
content += f"\n์ƒ์„ธ:\n{description}\n"
# ํ˜œํƒ
benefits = []
if deal.get("bonus_percentage"):
benefits.append(f"๋ณด๋„ˆ์Šค {deal.get('bonus_percentage')}%")
if deal.get("bonus_points"):
benefits.append(f"+{deal.get('bonus_points'):,} ํฌ์ธํŠธ")
if deal.get("bonus_miles"):
benefits.append(f"+{deal.get('bonus_miles'):,} ๋งˆ์ผ")
if deal.get("discount_percentage"):
benefits.append(f"ํ• ์ธ {deal.get('discount_percentage')}%")
if benefits:
content += f"\nํ˜œํƒ: {', '.join(benefits)}\n"
# ์กฐ๊ฑด
conditions = deal.get("conditions", [])
if conditions:
content += "\n์กฐ๊ฑด:\n"
for cond in conditions[:5]:
content += f" - {cond}\n"
# ํ”„๋กœ๋ชจ์…˜ ์ฝ”๋“œ
if deal.get("promo_code"):
content += f"\nํ”„๋กœ๋ชจ์…˜ ์ฝ”๋“œ: {deal.get('promo_code')}\n"
# ํƒ€๊ฒŸ/์ค‘๋ณต ๊ฐ€๋Šฅ ์—ฌ๋ถ€
flags = []
if deal.get("is_targeted"):
flags.append("ํƒ€๊ฒŸ ์˜คํผ")
if deal.get("is_stackable"):
flags.append("์ค‘๋ณต ๊ฐ€๋Šฅ")
if flags:
content += f"์ฐธ๊ณ : {', '.join(flags)}\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "deal_alert",
"title": title,
"category": category,
"end_date": str(end_date) if end_date else None
}
})
return chunks
def handle_news_updates(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""news_updates ๋˜๋Š” news_update ์ฒ˜๋ฆฌ"""
chunks = []
news_list = data if isinstance(data, list) else [data]
for news in news_list[:10]:
if not isinstance(news, dict):
continue
title = news.get("title", "N/A")
category = news.get("category", "OTHER")
domain = news.get("domain", "GENERAL")
content = f"""
๋‰ด์Šค/์—…๋ฐ์ดํŠธ: {title}
์นดํ…Œ๊ณ ๋ฆฌ: {category}
๋„๋ฉ”์ธ: {domain}
"""
# ๊ด€๋ จ ํ”„๋กœ๊ทธ๋žจ
if news.get("hotel_chain"):
content += f"ํ˜ธํ…”: {news.get('hotel_chain')}\n"
if news.get("airline"):
content += f"ํ•ญ๊ณต์‚ฌ: {news.get('airline')}\n"
if news.get("card_issuer"):
content += f"์นด๋“œ์‚ฌ: {news.get('card_issuer')}\n"
# ๋‚ ์งœ
if news.get("published_date"):
content += f"๋ฐœํ–‰์ผ: {news.get('published_date')}\n"
if news.get("effective_date"):
content += f"์‹œํ–‰์ผ: {news.get('effective_date')}\n"
# ์š”์•ฝ ๋ฐ ๋‚ด์šฉ
summary = news.get("summary", "")
if summary:
content += f"\n์š”์•ฝ: {summary}\n"
full_content = news.get("content", "")
if full_content:
# ๊ธด ๋‚ด์šฉ์€ ์ผ๋ถ€๋งŒ
if len(full_content) > 500:
content += f"\n๋‚ด์šฉ:\n{full_content[:500]}...\n"
else:
content += f"\n๋‚ด์šฉ:\n{full_content}\n"
# ์˜ํ–ฅ๋„
impact = news.get("impact")
impact_summary = news.get("impact_summary")
if impact or impact_summary:
content += "\n์˜ํ–ฅ:\n"
if impact:
impact_labels = {
"positive": "๊ธ์ •์  โœ…",
"negative": "๋ถ€์ •์  โŒ",
"neutral": "์ค‘๋ฆฝ โšช"
}
content += f" - ์˜ํ–ฅ๋„: {impact_labels.get(impact, impact)}\n"
if impact_summary:
content += f" - {impact_summary}\n"
# ํƒœ๊ทธ
tags = news.get("tags", [])
if tags:
content += f"\nํƒœ๊ทธ: {', '.join(tags[:5])}\n"
content += DISCLAIMER_SHORT
chunks.append({
"content": content.strip(),
"metadata": {
"type": "news_update",
"title": title,
"category": category,
"impact": impact
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ์›”๋ณ„ ๊ฐ€๊ฒฉ ์ƒ์„ธ (ํ˜„๊ธˆ/ํฌ์ธํŠธ)
# ===========================================================================
def handle_monthly_prices_detailed(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""monthly_cash_prices_detailed, monthly_points_prices_detailed ์ฒ˜๋ฆฌ
์›”๋ณ„ ๋‚ ์งœ๋ณ„ ์ƒ์„ธ ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์ฆŒ๋ณ„๋กœ ์š”์•ฝํ•˜์—ฌ ์ฒญํฌ ์ƒ์„ฑ
"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if not isinstance(data, dict):
return chunks
# ๊ฐ€๊ฒฉ ์œ ํ˜• ํŒ๋‹จ (ํฌ์ธํŠธ vs ํ˜„๊ธˆ)
is_points = any("points" in str(list(v[0].keys()) if isinstance(v, list) and v else [])
for v in data.values() if v)
price_type = "ํฌ์ธํŠธ" if is_points else "ํ˜„๊ธˆ"
content_type = "monthly_points_prices" if is_points else "monthly_cash_prices"
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += f"์›”๋ณ„ {price_type} ๊ฐ€๊ฒฉ ์ƒ์„ธ:\n\n"
# ์›”๋ณ„๋กœ ํ†ต๊ณ„ ๊ณ„์‚ฐ
for month_key, prices_list in list(data.items())[:12]:
if not isinstance(prices_list, list) or not prices_list:
continue
# ์›” ์ด๋ฆ„ ํฌ๋งท
month_name = month_key.replace("_", " ").title()
# ๊ฐ€๊ฒฉ/ํฌ์ธํŠธ ์ถ”์ถœ
if is_points:
values = [p.get("points", 0) for p in prices_list if isinstance(p, dict) and p.get("points")]
else:
values = [p.get("price_krw", 0) for p in prices_list if isinstance(p, dict) and p.get("price_krw")]
if not values:
continue
min_val = min(values)
max_val = max(values)
if is_points:
content += f"โ€ข {month_name}: {min_val:,}P ~ {max_val:,}P\n"
else:
content += f"โ€ข {month_name}: โ‚ฉ{min_val:,} ~ โ‚ฉ{max_val:,}\n"
content += DISCLAIMER_PRICE
if len(content) > 150:
chunks.append({
"content": content.strip(),
"metadata": {
"type": content_type,
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ•ธ๋“ค๋Ÿฌ: ๊ฐ์‹ค ์œ ํ˜•๋ณ„ ์š”๊ธˆ
# ===========================================================================
def handle_room_rates_by_type(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""room_rates_by_type ์ฒ˜๋ฆฌ - ๊ฐ์‹ค ์œ ํ˜•๋ณ„ ๋ฉค๋ฒ„ ์š”๊ธˆ"""
chunks = []
hotel_name, hotel_name_ko, chain = get_hotel_info(context)
if not isinstance(data, list) or not data:
return chunks
content = format_hotel_header(hotel_name, hotel_name_ko, chain)
content += "๊ฐ์‹ค ์œ ํ˜•๋ณ„ ๋ฉค๋ฒ„ ์š”๊ธˆ:\n\n"
for room in data[:15]:
if not isinstance(room, dict):
continue
room_type = room.get("room_type", "N/A")
rate = room.get("member_rate_krw", room.get("rate", "N/A"))
notes = room.get("notes", "")
if isinstance(rate, (int, float)):
content += f"โ€ข {room_type}: โ‚ฉ{rate:,}"
else:
content += f"โ€ข {room_type}: {rate}"
if notes:
content += f" ({notes})"
content += "\n"
content += DISCLAIMER_PRICE
chunks.append({
"content": content.strip(),
"metadata": {
"type": "room_rates_by_type",
"hotel_name": hotel_name
}
})
return chunks
# ===========================================================================
# ํ‚ค-ํ•ธ๋“ค๋Ÿฌ ๋งคํ•‘
# ===========================================================================
# ํ‚ค ์ด๋ฆ„ โ†’ ํ•ธ๋“ค๋Ÿฌ ๋งคํ•‘
# ๊ฐ™์€ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์—ฌ๋Ÿฌ ํ‚ค์— ๋งคํ•‘ ๊ฐ€๋Šฅ
CHUNK_HANDLERS = {
# ๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
"loyalty_programs": handle_loyalty_programs,
"loyalty_program": handle_loyalty_programs,
# ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰
"membership_tiers": handle_membership_tiers,
# ๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ
"milestone_program": handle_milestone_programs,
"milestone_programs": handle_milestone_programs,
"milestones": handle_milestone_programs,
# ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ
"partner_status_matches": handle_partner_status_matches,
"partner_status_match": handle_partner_status_matches,
# ํฌ์ธํŠธ ์‹œ์Šคํ…œ
"points_systems": handle_points_systems,
"points_system": handle_points_systems,
# ํ˜ธํ…”
"hotel_properties": handle_hotel_properties,
"hotel_brands": handle_hotel_brands,
# ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
"tier_implementations": handle_tier_implementations,
# ํฌ๋ ˆ๋”ง ์นด๋“œ
"credit_cards": handle_credit_cards,
# ๊ตฌ๋… ํ”„๋กœ๊ทธ๋žจ
"subscription_programs": handle_subscription_programs,
# ํ”„๋กœ๋ชจ์…˜
"promotions": handle_promotions,
# ๊ฐ€๊ฒฉ/ํฌ์ธํŠธ
"pricing": handle_pricing,
"pricing_analysis": handle_pricing_analysis,
"pricing_2025": handle_pricing, # ์—ฐ๋„๋ณ„ ์š”๊ธˆ ์ •๋ณด
"pricing_2026": handle_pricing,
"pricing_2027": handle_pricing,
# ํ˜œํƒ
"benefits": handle_benefits,
# Best Rate Guarantee
"best_rate_guarantee": handle_best_rate_guarantee,
# ์ฑ„๋„ ๊ตฌํ˜„
"channel_implementations": handle_channel_implementations,
# ์ฑ„๋„ ํ˜œํƒ ํŒจํ‚ค์ง€
"channel_benefit_packages": handle_channel_benefit_packages,
# ํ˜ธํ…” ์‹œ์„ค
"hotel_facilities": handle_hotel_facilities,
"other_facilities": handle_hotel_facilities,
# ์ด๊ทธ์ œํํ‹ฐ๋ธŒ ๋ผ์šด์ง€ - hotel_facilities ํ•ธ๋“ค๋Ÿฌ๋กœ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ
"executive_lounge": handle_hotel_facilities,
# ์›ฐ๋‹ˆ์Šค ์‹œ์„ค - hotel_facilities ํ•ธ๋“ค๋Ÿฌ๋กœ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ
"pulse8_wellness": handle_hotel_facilities,
"wellness_facility": handle_hotel_facilities,
"spa_wellness": handle_hotel_facilities,
# ํ˜ธํ…” ์–ด๋ฉ”๋‹ˆํ‹ฐ ์š”์•ฝ - hotel_facilities ํ•ธ๋“ค๋Ÿฌ๋กœ ํ†ตํ•ฉ ์ฒ˜๋ฆฌ
"hotel_amenities_summary": handle_hotel_facilities,
"amenities_summary": handle_hotel_facilities,
# ์›”๋ณ„ ๊ฐ€๊ฒฉ ์ƒ์„ธ (๊ณ ์œ  ๊ตฌ์กฐ๋กœ ๋ณ„๋„ ํ•ธ๋“ค๋Ÿฌ ์œ ์ง€)
"monthly_cash_prices_detailed": handle_monthly_prices_detailed,
"monthly_points_prices_detailed": handle_monthly_prices_detailed,
# ๊ฐ์‹ค ์œ ํ˜•๋ณ„ ์š”๊ธˆ (๊ณ ์œ  ๊ตฌ์กฐ๋กœ ๋ณ„๋„ ํ•ธ๋“ค๋Ÿฌ ์œ ์ง€)
"room_rates_by_type": handle_room_rates_by_type,
# ๊ฐ์‹ค ์œ ํ˜•
"room_types": handle_room_types,
# ๊ฐ์‹ค ์–ด๋ฉ”๋‹ˆํ‹ฐ
"room_common_amenities": handle_room_amenities,
"room_common_features": handle_room_amenities,
# ๋ ˆ์Šคํ† ๋ž‘/๋‹ค์ด๋‹
"restaurants": handle_restaurants,
"breakfast_options": handle_breakfast,
"breakfast_special_features": handle_breakfast,
"dining_programs": handle_dining_programs,
# ํ‰์ 
"ratings": handle_ratings,
# ๊ฐ€๊ฒฉ ๋ถ„์„ (Core ํ‚ค)
"price_analysis": handle_pricing_analysis,
# ํšŒ์› ์š”๊ธˆ
"member_rates": handle_member_rates,
# ์ •์ฑ… ๊ด€๋ จ (ํ˜œํƒ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ)
"blackout_periods": handle_benefits,
"excluded_rate_types": handle_benefits,
"excluded_rate_evidence": handle_benefits,
"excluded_properties": handle_benefits,
"regional_exceptions": handle_benefits,
"elite_qualifying_points_rules": handle_benefits,
"rate_classifications": handle_benefits,
"rollover_nights_policy": handle_benefits,
"sub_programs": handle_benefits,
"point_exclusions": handle_benefits,
"general_policies": handle_benefits,
"tier_upgrade_exclusion_rules": handle_benefits,
"overseas_usage_info": handle_benefits,
"performance_calculation_rules": handle_benefits,
"family_card_info": handle_benefits,
"customer_service": handle_benefits,
"common_benefits": handle_benefits,
"elite_night_credit_rules": handle_benefits,
"amex_common_benefits": handle_benefits,
"annual_choice_benefits": handle_benefits,
"partner_programs": handle_benefits,
"non_qualifying_rates": handle_benefits,
"key_policies": handle_benefits,
# ์นด๋“œ ํ˜œํƒ/ํ• ์ธ ๊ด€๋ จ (Phase 3 ํ™•์žฅ)
"club_lounge_services": handle_benefits, # ํด๋Ÿฝ ๋ผ์šด์ง€ ์„œ๋น„์Šค
"spending_exclusions": handle_benefits, # ์ ๋ฆฝ ์ œ์™ธ ํ•ญ๋ชฉ
"account_linking_guide": handle_benefits, # ๊ณ„์ • ์—ฐ๋™ ๊ฐ€์ด๋“œ
"hotel_discounts": handle_benefits, # ํ˜ธํ…” ํ• ์ธ ํ˜œํƒ
"spa_discounts": handle_benefits, # ์ŠคํŒŒ ํ• ์ธ ํ˜œํƒ
"airport_lounges": handle_benefits, # ๊ณตํ•ญ ๋ผ์šด์ง€ ํ˜œํƒ
# ์‹œ์„ค/ํ˜ธํ…” ์ •๋ณด ๊ด€๋ จ
"dining_venues": handle_restaurants,
"dining_details": handle_restaurants,
"room_service": handle_restaurants,
"policies": handle_hotel_facilities,
"facilities": handle_hotel_facilities,
# FHR/์•„๋ฉ•์Šค ๊ด€๋ จ (์ฑ„๋„ ๊ตฌํ˜„ ๋˜๋Š” ํ˜œํƒ์œผ๋กœ ์ฒ˜๋ฆฌ)
"partner_program": handle_benefits,
"channel_comparison": handle_benefits,
"booking_payment_rules": handle_benefits,
"eligible_cards": handle_benefits,
"korea_cards": handle_benefits,
"korea_booking": handle_benefits,
"price_comparison": handle_pricing,
"point_earning_rules": handle_benefits,
"korea_amex_usage": handle_benefits,
"korea_cardholder_guides": handle_benefits,
"terms_and_conditions": handle_benefits,
# --- ํ•ญ๊ณต ํ”„๋กœ๊ทธ๋žจ (Phase 2) ---
"airline_programs": handle_airline_programs,
"airline_program": handle_airline_programs,
"airline_partnership": handle_airline_partnership, # Flying Blue โ†’ Korean Air ๋“ฑ
"airline_tiers": handle_airline_tiers,
"airline_tier": handle_airline_tiers,
"award_charts": handle_award_charts,
"award_chart": handle_award_charts,
"airline_earning_rules": handle_airline_earning_rules,
# --- ํ•ญ๊ณต ์šด์ž„ (Airline Fares) ---
"airline_fare_tables": handle_airline_fare_tables,
"domestic_fare_rules": handle_domestic_fare_rules,
"domestic_peak_seasons": handle_domestic_peak_seasons,
"domestic_route_fares": handle_domestic_route_fares,
"codeshare_domestic_fares": handle_codeshare_domestic_fares,
"international_route_fares": handle_international_route_fares,
"fare_comparison_summary": handle_fare_comparison_summary,
"important_notes": handle_important_notes,
# --- ํ•ญ๊ณต ๋ถ€๊ฐ€ ์š”๊ธˆ ๊ทœ์ • (Extra Fee Rules) ---
"baggage_rules": handle_baggage_rules,
"baggage_general_rules": handle_baggage_general_rules,
"sports_equipment_rules": handle_sports_equipment_rules,
"pet_transport_rules": handle_pet_transport_rules,
"unaccompanied_minor_rules": handle_unaccompanied_minor_rules,
"refund_rules": handle_refund_rules,
"no_show_penalties": handle_no_show_penalties,
"lounge_penalties": handle_lounge_penalties,
"medical_oxygen_rules": handle_medical_oxygen_rules,
"codeshare_baggage_rules": handle_codeshare_baggage_rules,
"route_classifications": handle_route_classifications,
# --- ํ•ญ๊ณต ์šด์ž„ ์ถ”๊ฐ€ ์ •๋ณด (์‹œ์ฆŒ, ํด๋ž˜์Šค, ์„ธ๊ธˆ, ๊ณตํ•ญ, ์œ ๋ฅ˜ํ• ์ฆ๋ฃŒ, ํŒฉํŠธ) ---
"international_season_definitions": handle_international_season_definitions,
"booking_class_definitions": handle_booking_class_definitions,
"special_tax_structures": handle_special_tax_structures,
"multi_airport_routes": handle_multi_airport_routes,
"fuel_surcharge_tiers": handle_fuel_surcharge_tiers,
"facts": handle_facts_router, # ๋„๋ฉ”์ธ๋ณ„ ๋ผ์šฐํŒ… (ํ˜ธํ…” โ†’ NESTED, ํ•ญ๊ณต โ†’ handle_airline_facts)
# --- ๋”œ/๋‰ด์Šค (Phase 5) ---
"deal_alerts": handle_deal_alerts,
"deal_alert": handle_deal_alerts,
"news_updates": handle_news_updates,
"news_update": handle_news_updates,
# --- ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ (Mileage Efficiency) ---
"award_chart_entries": handle_award_chart_entries,
"mileage_efficiency_analysis": handle_mileage_efficiency_analysis,
"route_efficiency_data_lowest_fare": handle_route_efficiency_data,
"route_efficiency_data_highest_fare": handle_route_efficiency_data,
"efficiency_ranking_lowest_fare_low_season": handle_efficiency_ranking,
"efficiency_ranking_highest_fare_low_season": handle_efficiency_ranking,
"efficiency_ranking_lowest_fare_high_season": handle_efficiency_ranking,
"efficiency_ranking_highest_fare_high_season": handle_efficiency_ranking,
"fare_basis_comparison": handle_fare_basis_comparison,
"mileage_usage_strategy": handle_mileage_usage_strategy,
# --- ๋ˆ„๋ฝ๋œ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ๋ถ„์„ ํ‚ค ๋งคํ•‘ ์ถ”๊ฐ€ ---
"route_efficiency_data": handle_route_efficiency_data, # nested dict ๊ตฌ์กฐ ์ง€์›
"efficiency_ranking": handle_efficiency_ranking, # nested dict (top_20/worst_20) ์ง€์›
"key_insights": handle_key_insights, # ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ
"usage_recommendations": handle_usage_recommendations, # ์‚ฌ์šฉ ๊ถŒ์žฅ์‚ฌํ•ญ
"strategy_recommendations": handle_mileage_usage_strategy, # ๊ธฐ์กด ํ•ธ๋“ค๋Ÿฌ ์žฌ์‚ฌ์šฉ
"regional_efficiency_summary": handle_regional_efficiency_summary, # ์ง€์—ญ๋ณ„ ํšจ์œจ ์š”์•ฝ
"seasonal_efficiency_summary": handle_seasonal_efficiency_summary, # ์‹œ์ฆŒ๋ณ„ ํšจ์œจ ์š”์•ฝ
# --- ์ถ”๊ฐ€ ๋งˆ์ผ๋ฆฌ์ง€ ํšจ์œจ ํ‚ค ๋งคํ•‘ ---
"route_efficiency_evidence": handle_route_efficiency_data, # ๋…ธ์„ ๋ณ„ ํšจ์œจ ์ฆ๊ฑฐ
"business_vs_smartium_comparison": handle_fare_basis_comparison, # ๋น„์ฆˆ๋‹ˆ์Šค vs ์Šค๋งˆํ‹ฐ์›€ ๋น„๊ต
"weekday_weekend_comparison": handle_fare_basis_comparison, # ์ฃผ์ค‘/์ฃผ๋ง ํšจ์œจ ๋น„๊ต
"usage_recommendations_detailed": handle_usage_recommendations, # ์ƒ์„ธ ๊ถŒ์žฅ์‚ฌํ•ญ
# --- ๋งˆ์ผ๋ฆฌ์ง€ ๊ทœ์ • ๊ด€๋ จ ํ‚ค ๋งคํ•‘ (์‹ ๊ทœ ์ถ”๊ฐ€) ---
# ์„ฑ์ˆ˜๊ธฐ/์‹œ์ฆŒ ๊ทœ์ •
"peak_seasons": handle_peak_season_rules,
"peak_season_rules": handle_peak_season_rules,
"peak_season_exemptions": handle_peak_season_rules,
# ๋งˆ์ผ๋ฆฌ์ง€ ์ฐจ๊ฐ ๊ทœ์ •
"business_smartium_rules": handle_mileage_deduction_rules,
"child_mileage_deduction_rules": handle_mileage_deduction_rules,
# ๋…ธ์„ /ํ™˜์Šน ๊ทœ์ •
"route_restrictions": handle_route_transfer_rules,
"transfer_rules": handle_route_transfer_rules,
# ํ™˜๋ถˆ/๋ณ€๊ฒฝ ์ˆ˜์ˆ˜๋ฃŒ
"refund_fees": handle_refund_change_fees,
"change_fees": handle_refund_change_fees,
"fee_exemptions": handle_refund_change_fees,
"refund_channel_restrictions": handle_refund_change_fees,
"online_change_fee_rules": handle_refund_change_fees,
"mileage_refund_calculation": handle_refund_change_fees,
"mileage_validity_on_refund": handle_refund_change_fees,
"change_additional_costs": handle_refund_change_fees,
"fuel_surcharge_change_rules": handle_refund_change_fees,
# ๋…ธ์‡ผ ๊ทœ์ •
"no_show_definitions": handle_no_show_rules,
"no_show_post_processing": handle_no_show_rules,
# ์—ฌ์ • ๋ณ€๊ฒฝ ๊ทœ์ •
"itinerary_change_rules": handle_itinerary_change_rules,
"itinerary_change_examples": handle_itinerary_change_rules,
# ํ”„๋กœ๊ทธ๋žจ/์„ธ๊ธˆ ๊ทœ์ •
"mileage_now_program": handle_mileage_program_rules,
"taxes_and_fees": handle_mileage_program_rules,
# ์˜ˆ์•ฝ ๊ทœ์ •
"booking_process": handle_mileage_booking_rules,
"booking_notes": handle_mileage_booking_rules,
# ์ œ์™ธ ๋…ธ์„ 
"excluded_routes": handle_excluded_routes,
# --- ์ง€์—ญ๋ณ„ ํšจ์œจ ๋ฐ์ดํ„ฐ (Asiana ๋“ฑ) ---
"route_efficiency_data_domestic": handle_regional_efficiency_data,
"route_efficiency_data_japan": handle_regional_efficiency_data,
"route_efficiency_data_china_northeast_asia": handle_regional_efficiency_data,
"route_efficiency_data_southeast_asia": handle_regional_efficiency_data,
"route_efficiency_data_south_asia_cis": handle_regional_efficiency_data,
"route_efficiency_data_oceania": handle_regional_efficiency_data,
"route_efficiency_data_americas": handle_regional_efficiency_data,
"route_efficiency_data_europe": handle_regional_efficiency_data,
"efficiency_ranking_low_season": handle_efficiency_ranking,
# --- ๋งˆ์ผ๋ฆฌ์ง€ ๊ตฌ๋งค ํ”„๋กœ๊ทธ๋žจ (Mileage Purchase Programs) ---
"mileage_purchase_programs": handle_mileage_purchase_programs,
"price_summary": handle_mileage_price_summary,
"insights": handle_mileage_insights,
# --- ๋ฏธ์ฒ˜๋ฆฌ ํ‚ค๋“ค์„ ์ ์ ˆํ•œ ํ•ธ๋“ค๋Ÿฌ์— ๋งคํ•‘ ---
"raw_pricing_data": handle_pricing, # ClassicTravel ์›์‹œ ๊ฐ€๊ฒฉ ๋ฐ์ดํ„ฐ
"rate_descriptions": handle_benefits, # ClassicTravel ์š”๊ธˆ ์„ค๋ช…
"brunch_options": handle_breakfast, # ClassicTravel ๋ธŒ๋Ÿฐ์น˜ ์˜ต์…˜
"regional_info": handle_hotel_facilities, # ClassicTravel ์ง€์—ญ ์ •๋ณด
"breakfast_benefit_participating_brands": handle_benefits, # GHA Discovery ์กฐ์‹ ํ˜œํƒ ์ฐธ์—ฌ ๋ธŒ๋žœ๋“œ
"direct_channels": handle_benefits, # GHA Discovery ์ง์ ‘ ์ฑ„๋„
"irregular_prices_observed": handle_pricing, # ํ•ญ๊ณต์‚ฌ ๋ถˆ๊ทœ์น™ ๊ฐ€๊ฒฉ ๊ด€์ฐฐ
"route_availability_patterns": handle_airline_programs, # ํ•ญ๊ณต์‚ฌ ๋…ธ์„  ๊ฐ€์šฉ์„ฑ ํŒจํ„ด
"booking_policies": handle_benefits, # WhataHotel ์˜ˆ์•ฝ ์ •์ฑ… (์„œ๋น„์Šค ์ˆ˜์ˆ˜๋ฃŒ, ๊ฒฐ์ œ ์ •์ฑ…, ์ทจ์†Œ ์ •์ฑ…)
# --- AMEX Platinum ์นด๋“œ ์ „์šฉ ํ‚ค ๋งคํ•‘ ---
"travel_programs": handle_benefits, # FHR, THC ๋“ฑ ์—ฌํ–‰ ํ”„๋กœ๊ทธ๋žจ
"statement_credit_timing": handle_benefits, # ์Šคํ…Œ์ดํŠธ๋จผํŠธ ํฌ๋ ˆ๋”ง ์ฒ˜๋ฆฌ ์‹œ๊ฐ„
"statement_credit_troubleshooting": handle_benefits, # ํฌ๋ ˆ๋”ง ๋ฌธ์ œ ํ•ด๊ฒฐ ํŒ
"tier_eligibility_comparison": handle_benefits, # ๋“ฑ๊ธ‰ ์ž๊ฒฉ ๋น„๊ตํ‘œ
"benefit_exclusions": handle_benefits, # ํ˜œํƒ ์ œ์™ธ ํ•ญ๋ชฉ
"benefit_calendar": handle_benefits, # ํ˜œํƒ ์ผ์ • (์›”๊ฐ„/๋ถ„๊ธฐ/์—ฐ๊ฐ„)
"contact_information": handle_benefits, # ์—ฐ๋ฝ์ฒ˜ ์ •๋ณด
"important_notices": handle_benefits, # ์ค‘์š” ๊ณต์ง€์‚ฌํ•ญ
"benefit_value_summary": handle_benefits, # ํ˜œํƒ ๊ฐ€์น˜ ์š”์•ฝ
# --- Chase ์นด๋“œ ๊ด€๋ จ ํ‚ค ๋งคํ•‘ ---
"lounge_access_details": handle_benefits, # ๊ณตํ•ญ ๋ผ์šด์ง€ ์ ‘๊ทผ ์ •๋ณด
"spending_tier_benefits": handle_benefits, # ์ง€์ถœ ๋“ฑ๊ธ‰๋ณ„ ํ˜œํƒ
"balance_transfer_conditions": handle_benefits, # ์ž”์•ก ์ด์ฒด ์กฐ๊ฑด
"chase_pay_over_time": handle_benefits, # Chase ํ• ๋ถ€ ๊ฒฐ์ œ ์˜ต์…˜
"state_notices": handle_benefits, # ์ฃผ๋ณ„ ๊ณ ์ง€์‚ฌํ•ญ
"mileageplus_integration": handle_benefits, # MileagePlus ์—ฐ๋™ ์ •๋ณด
"privacy_legal": handle_benefits, # ๊ฐœ์ธ์ •๋ณด/๋ฒ•์  ๊ณ ์ง€
"concierge_services": handle_benefits, # ์ปจ์‹œ์–ด์ง€ ์„œ๋น„์Šค
"other_info": handle_benefits, # ๊ธฐํƒ€ ์ •๋ณด
# --- ์ถ”๊ฐ€ ๋ฏธ์ฒ˜๋ฆฌ ํ‚ค ๋งคํ•‘ (credit card ๊ด€๋ จ) ---
"ihg_membership_earning_rules": handle_benefits, # IHG ๋ฉค๋ฒ„์‹ญ ์ ๋ฆฝ ๊ทœ์น™
"points_expiration_policy": handle_benefits, # ํฌ์ธํŠธ ๋งŒ๋ฃŒ ์ •์ฑ…
"terms_summary": handle_benefits, # ์•ฝ๊ด€ ์š”์•ฝ
"hyatt_brands_reference": handle_hotel_brands, # Hyatt ๋ธŒ๋žœ๋“œ ๋ชฉ๋ก
"ihg_program_earning_info": handle_benefits, # IHG ํ”„๋กœ๊ทธ๋žจ ์ ๋ฆฝ ์ •๋ณด
"related_cards_comparison": handle_benefits, # ๊ด€๋ จ ์นด๋“œ ๋น„๊ต
"value_analysis": handle_benefits, # ๊ฐ€์น˜ ๋ถ„์„
# --- Disney ์นด๋“œ ์ „์šฉ ํ‚ค ๋งคํ•‘ ---
"dining_eligible_restaurants": handle_benefits, # Disney ์‹๋‹น ํ• ์ธ ๋Œ€์ƒ ๋ ˆ์Šคํ† ๋ž‘
"tour_eligible_experiences": handle_benefits, # Disney ํˆฌ์–ด/์ฒดํ—˜ ํ• ์ธ ๋Œ€์ƒ
"photo_opportunities": handle_benefits, # Disney ์นด๋“œ ํฌํ†  ๊ธฐํšŒ
# --- IHG/United ์นด๋“œ ๊ด€๋ จ ํ‚ค ๋งคํ•‘ ---
"airline_program_integration": handle_benefits, # ํ•ญ๊ณต์‚ฌ ํ”„๋กœ๊ทธ๋žจ ์—ฐ๋™
# --- ์ถ”๊ฐ€ ๋ฏธ์ฒ˜๋ฆฌ ํ‚ค ๋งคํ•‘ (Citi/Barclays/BoA ์นด๋“œ) ---
"aadvantage_program_tiers": handle_benefits, # AAdvantage ํ”„๋กœ๊ทธ๋žจ ๋“ฑ๊ธ‰
"american_eagle_operators": handle_benefits, # American Eagle ์šดํ•ญ์‚ฌ
"additional_benefits": handle_benefits, # ์ถ”๊ฐ€ ํ˜œํƒ
"insurance_benefits": handle_benefits, # ๋ณดํ—˜ ํ˜œํƒ
"security_features": handle_benefits, # ๋ณด์•ˆ ๊ธฐ๋Šฅ
"entertainment_program": handle_benefits, # ์—”ํ„ฐํ…Œ์ธ๋จผํŠธ ํ”„๋กœ๊ทธ๋žจ
"annual_benefit_value_summary": handle_benefits, # ์—ฐ๊ฐ„ ํ˜œํƒ ๊ฐ€์น˜ ์š”์•ฝ
"card_benefits": handle_benefits, # ์นด๋“œ ํ˜œํƒ (์ตœ์ƒ์œ„ ํ‚ค)
"eligibility_requirements": handle_benefits, # ์ž๊ฒฉ ์š”๊ฑด
"balance_transfer_terms": handle_benefits, # ์ž”์•ก ์ด์ฒด ์กฐ๊ฑด
"cash_equivalent_transactions": handle_benefits, # ํ˜„๊ธˆ ๋“ฑ๊ฐ€ ๊ฑฐ๋ž˜
"points_forfeiture_conditions": handle_benefits, # ํฌ์ธํŠธ ๋ชฐ์ˆ˜ ์กฐ๊ฑด
"trueblue_integration": handle_benefits, # TrueBlue ์—ฐ๋™
"trueblue_points_important_info": handle_benefits, # TrueBlue ํฌ์ธํŠธ ์ค‘์š” ์ •๋ณด
"additional_info": handle_benefits, # ์ถ”๊ฐ€ ์ •๋ณด
"partnership_programs": handle_benefits, # ํŒŒํŠธ๋„ˆ์‹ญ ํ”„๋กœ๊ทธ๋žจ
}
# ์ค‘์ฒฉ ํ‚ค ์ง€์› (์˜ˆ: facts.pricing_analysis)
NESTED_HANDLERS = {
# facts.* ํ‚ค๋“ค (๋ฒ”์šฉ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ)
"facts.pricing_analysis": handle_pricing_analysis,
"facts.pricing_insights": handle_pricing_analysis,
"facts.pricing_2025": handle_pricing, # ์—ฐ๋„๋ณ„ ๊ฐ€๊ฒฉ ์ •๋ณด
"facts.pricing_2026": handle_pricing,
"facts.pricing_2027": handle_pricing,
"facts.user_tips": None, # ๋ฒ”์šฉ ํ…์ŠคํŠธ๋กœ ์ฒ˜๋ฆฌ ์˜ˆ์ •
"facts.pros_cons": None,
"facts.dining_venues": handle_restaurants,
"facts.facilities": handle_hotel_facilities,
"facts.room_types": handle_room_types,
}
# ๋ฌด์‹œํ•  ํ‚ค (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋“ฑ - ๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ)
IGNORED_KEYS = {
# ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
"extraction_timestamp",
"extractor_model",
"schema_version",
"document_reference",
"identity",
"source",
"version",
"document_metadata",
"extraction_metadata",
"extraction_notes",
"analysis_metadata", # ๋ถ„์„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
# ๋‚ด๋ถ€/์ฐธ์กฐ์šฉ
"evidence",
"extra_attributes",
"data_sources",
"verification_needed",
"price_summary_evidence", # ๊ฐ€๊ฒฉ ์š”์•ฝ ์ฆ๊ฑฐ (๋ณ„๋„ ํ•ธ๋“ค๋Ÿฌ ๋ถˆํ•„์š”)
# ์ค‘์ฒฉ ํ•ธ๋“ค๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ (ํ˜ธํ…” facts๋Š” NESTED_HANDLERS์—์„œ ์ฒ˜๋ฆฌ)
# "facts" - ํ•ญ๊ณต ์šด์ž„์šฉ์€ handle_airline_facts์—์„œ ์ฒ˜๋ฆฌ
# Core ํ‚ค์— ํฌํ•จ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ฒ˜๋ฆฌ ๋ถˆํ•„์š”
# "hotel_brands", # ์ด์ œ handle_hotel_brands๋กœ ์ฒ˜๋ฆฌ
"nearby_attractions", # extra_attributes์— ํฌํ•จ
# ๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ
"loyalty_program_features", # loyalty_program์œผ๋กœ ์ปค๋ฒ„
"breakfast_special_features", # breakfast_options์œผ๋กœ ์ปค๋ฒ„
"unverified_items",
"unverified_items_evidence",
"pending_items",
"to_verify",
"sources",
"legal_provisions",
"external_references",
"general_terms",
"pros_cons",
"verification_needed", # ํ™•์ธ ํ•„์š” ์‚ฌํ•ญ (๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ)
"collection_date", # ์ˆ˜์ง‘์ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ
"related_documents", # ๊ด€๋ จ ๋ฌธ์„œ ์ฐธ์กฐ
# AMEX ์นด๋“œ ๊ด€๋ จ ์ฆ๊ฑฐ์ž๋ฃŒ ๋ฐ ๋ฒ•์  ์ •๋ณด (๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ)
"seller_of_travel", # ์—ฌํ–‰ ํŒ๋งค์ž ๋ฒ•์  ์ •๋ณด
"statement_credit_evidence", # ์Šคํ…Œ์ดํŠธ๋จผํŠธ ํฌ๋ ˆ๋”ง ์ฆ๊ฑฐ
"verification_needed_evidence", # ํ™•์ธ ํ•„์š” ์‚ฌํ•ญ ์ฆ๊ฑฐ
# Chase ์นด๋“œ ๊ด€๋ จ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ)
"related_links", # ๊ด€๋ จ ๋งํฌ (URL ์ฐธ์กฐ์šฉ)
"benefit_value_summary_total", # ํ˜œํƒ ๊ฐ€์น˜ ์ดํ•ฉ (๋ฉ”ํƒ€๋ฐ์ดํ„ฐ)
"spending_tier_120k_additional", # ์ถ”๊ฐ€ ์ง€์ถœ ๋“ฑ๊ธ‰ (์ƒ์„ธ ๋‚ด์šฉ ์—†์Œ)
"benefit_value_evidence", # ํ˜œํƒ ๊ฐ€์น˜ ์ฆ๊ฑฐ (๋‚ด๋ถ€์šฉ)
"legal_notices", # ๋ฒ•์  ๊ณ ์ง€์‚ฌํ•ญ
# Deprecated ํ‚ค (์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
# "loyalty_programs" (๋ณต์ˆ˜) - ๋‹จ, ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ„ํ•ด ํ•ธ๋“ค๋Ÿฌ๋Š” ์œ ์ง€
}
def get_handler(key: str):
"""ํ‚ค์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋ฐ˜ํ™˜"""
return CHUNK_HANDLERS.get(key)
def get_nested_handler(parent_key: str, child_key: str):
"""์ค‘์ฒฉ ํ‚ค์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋ฐ˜ํ™˜"""
nested_key = f"{parent_key}.{child_key}"
return NESTED_HANDLERS.get(nested_key)
def is_ignored(key: str) -> bool:
"""๋ฌด์‹œํ•ด์•ผ ํ•  ํ‚ค์ธ์ง€ ํ™•์ธ"""
return key in IGNORED_KEYS
def get_all_handler_keys() -> set:
"""์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ํ‚ค ๋ฐ˜ํ™˜"""
keys = set(CHUNK_HANDLERS.keys())
keys.update(IGNORED_KEYS)
return keys