lovelymango commited on
Commit
4263ca9
ยท
verified ยท
1 Parent(s): c5e0abc

Upload 19 files

Browse files
Files changed (2) hide show
  1. scripts/chunk_handlers.py +959 -150
  2. scripts/sync_to_supabase.py +44 -2
scripts/chunk_handlers.py CHANGED
@@ -39,6 +39,51 @@ def format_hotel_header(hotel_name: str, hotel_name_ko: Optional[str], chain: st
39
  return content
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  # ===========================================================================
43
  # ํ•ธ๋“ค๋Ÿฌ: ๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
44
  # ===========================================================================
@@ -90,7 +135,10 @@ def handle_membership_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str
90
  """membership_tiers ์ฒ˜๋ฆฌ"""
91
  chunks = []
92
  chain = context.get("chain", "UNKNOWN")
93
-
 
 
 
94
  tiers = data if isinstance(data, list) else [data]
95
 
96
  for tier in tiers[:10]:
@@ -101,29 +149,77 @@ def handle_membership_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str
101
  tier_level = tier.get("tier_level", "N/A")
102
  tier_chain = tier.get("chain", chain)
103
 
104
- upgrade_req = tier.get("upgrade_requirement", {})
105
- req_nights = upgrade_req.get("nights", "N/A")
106
- req_points = upgrade_req.get("points")
 
 
 
 
 
 
 
 
107
 
108
  content = f"""
109
  ๋“ฑ๊ธ‰: {tier_name} ({tier_level})
110
  ์ฒด์ธ: {tier_chain}
111
  ๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
112
  ์ž๊ฒฉ ์š”๊ฑด:
113
- - ์ˆ™๋ฐ•: {req_nights}๋ฐ•
114
- - ํฌ์ธํŠธ: {req_points if req_points else 'N/A'}
115
- ๋ณด๋„ˆ์Šค ํฌ์ธํŠธ: {tier.get("elite_bonus_points", "N/A")}
116
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  # ์ฃผ์š” ํ˜œํƒ
119
  benefits = tier.get("benefits", [])
120
  if benefits:
121
- content += "์ฃผ์š” ํ˜œํƒ:\n"
122
  for benefit in benefits[:8]:
123
  if isinstance(benefit, dict):
124
  ben_name = benefit.get("name", benefit.get("benefit_name", "N/A"))
125
- ben_desc = benefit.get("description", "")[:100]
126
- content += f"- {ben_name}: {ben_desc}\n"
 
 
 
 
 
 
 
 
127
 
128
  chunks.append({
129
  "content": content.strip(),
@@ -138,6 +234,147 @@ def handle_membership_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str
138
  return chunks
139
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  # ===========================================================================
142
  # ํ•ธ๋“ค๋Ÿฌ: ํฌ์ธํŠธ ์‹œ์Šคํ…œ
143
  # ===========================================================================
@@ -162,24 +399,88 @@ def handle_points_systems(data: Any, context: Dict[str, Any]) -> List[Dict[str,
162
  content = f"""
163
  ํฌ์ธํŠธ ์‹œ์Šคํ…œ: {point_name}
164
  ์ฒด์ธ: {system.get("chain", chain)}
165
-
166
- ์ ๋ฆฝ ๊ทœ์น™:
167
  """
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  # ์ ๋ฆฝ ๊ทœ์น™
 
170
  earning_rules = system.get("earning_rules", [])
171
  if earning_rules and isinstance(earning_rules, list):
172
  for rule in earning_rules[:6]:
173
  if isinstance(rule, dict):
174
- content += f"- {rule.get('name', 'N/A')}: {rule.get('rate', 'N/A')}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- # ๋งŒ๋ฃŒ
177
- expiry = system.get("expiry")
178
- if expiry:
179
- if isinstance(expiry, dict):
180
- content += f"\n๋งŒ๋ฃŒ: {expiry.get('has_expiry', '์žˆ์Œ')} ({expiry.get('expiry_period', 'N/A')})"
181
- else:
182
- content += f"\n๋งŒ๋ฃŒ: {expiry}"
183
 
184
  chunks.append({
185
  "content": content.strip(),
@@ -254,12 +555,69 @@ def handle_hotel_properties(data: Any, context: Dict[str, Any]) -> List[Dict[str
254
  return chunks
255
 
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  # ===========================================================================
258
  # ํ•ธ๋“ค๋Ÿฌ: ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
259
  # ===========================================================================
260
 
261
  def handle_tier_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
262
- """tier_implementations ์ฒ˜๋ฆฌ"""
263
  chunks = []
264
  chain = context.get("chain", "UNKNOWN")
265
 
@@ -273,10 +631,28 @@ def handle_tier_implementations(data: Any, context: Dict[str, Any]) -> List[Dict
273
  tier_level = impl.get("tier_level", "N/A")
274
  impl_chain = impl.get("chain", chain)
275
 
 
 
 
 
 
 
276
  content = f"""
277
  ํ˜ธํ…”๋ณ„ ๋ฉค๋ฒ„์‹ญ ํ˜œํƒ ๊ตฌํ˜„
278
- ํ˜ธํ…”: {hotel_id}
279
- ์ฒด์ธ: {impl_chain}
 
 
 
 
 
 
 
 
 
 
 
 
280
  ๋“ฑ๊ธ‰: {tier_level}
281
  ๋ผ์šด์ง€ ์ด์šฉ: {'๊ฐ€๋Šฅ' if impl.get('lounge_access') else '๋ถˆ๊ฐ€'}
282
  ์กฐ์‹ ํฌํ•จ: {'ํฌํ•จ' if impl.get('breakfast_included') else '๋ฏธํฌํ•จ'}
@@ -290,16 +666,28 @@ def handle_tier_implementations(data: Any, context: Dict[str, Any]) -> List[Dict
290
  for override in overrides[:5]:
291
  if isinstance(override, dict):
292
  category = override.get("benefit_category", "N/A")
293
- detail = override.get("implementation_detail", "")[:100]
294
  content += f"- {category}: {detail}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
  chunks.append({
297
  "content": content.strip(),
298
- "metadata": {
299
- "type": "tier_implementation",
300
- "hotel_id": hotel_id,
301
- "tier_level": tier_level
302
- }
303
  })
304
 
305
  return chunks
@@ -326,9 +714,29 @@ def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
326
  issuer = card.get("issuer", "N/A")
327
  issuer_code = card.get("issuer_code", "")
328
 
 
 
329
  annual_fee = card.get("annual_fee", {})
330
- fee_amount = annual_fee.get("amount", "N/A") if isinstance(annual_fee, dict) else annual_fee
331
- fee_currency = annual_fee.get("currency", "KRW") if isinstance(annual_fee, dict) else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
  content = f"""
334
  ์‹ ์šฉ์นด๋“œ: {card_name}
@@ -336,7 +744,22 @@ def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
336
 
337
  if issuer_code:
338
  content += f" ({issuer_code})"
339
- content += f"\n์—ฐํšŒ๋น„: {fee_amount:,} {fee_currency}\n" if isinstance(fee_amount, (int, float)) else f"\n์—ฐํšŒ๋น„: {fee_amount} {fee_currency}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  # ์นด๋“œ ์œ ํ˜• (Phase 3)
342
  if card_type:
@@ -441,6 +864,11 @@ def handle_credit_cards(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
441
  elif isinstance(benefit, str):
442
  content += f" - {benefit}\n"
443
 
 
 
 
 
 
444
  content += DISCLAIMER_SHORT
445
 
446
  # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ ์ •๋ณด ํฌํ•จ
@@ -486,17 +914,39 @@ def handle_subscription_programs(data: Any, context: Dict[str, Any]) -> List[Dic
486
  ๊ธฐ๋ณธ ํ”„๋กœ๊ทธ๋žจ: {program.get('base_program_name', 'N/A')}
487
  """
488
 
489
- # ํ”Œ๋žœ
490
  plans = program.get("plans", [])
491
  for plan in plans[:5]:
492
  if isinstance(plan, dict):
493
  plan_name = plan.get("plan_name", "N/A")
494
  prices = plan.get("prices", [])
495
  price_str = ""
496
- if prices and isinstance(prices[0], dict):
497
- price = prices[0].get("price", {})
498
- price_str = f"{price.get('amount', 'N/A')} {price.get('currency', '')}/๋…„"
499
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  content += f"\nํ”Œ๋žœ: {plan_name}\n๊ฐ€๊ฒฉ: {price_str}\n"
501
 
502
  # ํ”Œ๋žœ ํ˜œํƒ
@@ -811,45 +1261,235 @@ def handle_benefits(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
811
  # ===========================================================================
812
 
813
  def handle_best_rate_guarantee(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
814
- """best_rate_guarantee ์ฒ˜๋ฆฌ"""
 
 
 
 
 
815
  chunks = []
816
  chain = context.get("chain", "UNKNOWN")
817
 
818
  if not isinstance(data, dict):
819
  return chunks
820
 
821
- content = f"""
822
- ์ตœ์ €๊ฐ€ ๋ณด์žฅ (Best Rate Guarantee)
823
- ์ฒด์ธ: {chain}
824
- ํ”„๋กœ๊ทธ๋žจ๋ช…: {data.get('program_name', 'N/A')}
825
- ๋ณด์ƒ ์œ ํ˜•: {data.get('compensation_type', 'N/A')}
 
 
 
826
  """
827
 
828
- # ์ด์šฉ ์กฐ๊ฑด
829
- conditions = data.get("conditions", [])
830
- if conditions:
831
- content += "\n์ด์šฉ ์กฐ๊ฑด:\n"
832
- for cond in conditions[:5]:
833
- if isinstance(cond, dict):
834
- content += f"- {cond.get('description', cond)}\n"
835
- else:
836
- content += f"- {cond}\n"
 
 
 
 
 
 
 
 
837
 
838
- # ์ œ์™ธ ์‚ฌํ•ญ
839
- exclusions = data.get("exclusions", [])
840
- if exclusions:
841
- content += "\n์ œ์™ธ ์‚ฌํ•ญ:\n"
842
- for exc in exclusions[:3]:
843
- content += f"- {exc}\n" if isinstance(exc, str) else ""
844
 
845
  chunks.append({
846
- "content": content.strip(),
847
  "metadata": {
848
- "type": "best_rate_guarantee",
849
- "chain": chain
 
850
  }
851
  })
852
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
  return chunks
854
 
855
 
@@ -858,23 +1498,41 @@ def handle_best_rate_guarantee(data: Any, context: Dict[str, Any]) -> List[Dict[
858
  # ===========================================================================
859
 
860
  def handle_channel_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
861
- """channel_implementations ์ฒ˜๋ฆฌ"""
862
  chunks = []
863
  chain = context.get("chain", "UNKNOWN")
864
 
865
  impls = data if isinstance(data, list) else [data]
866
 
867
- for impl in impls[:5]:
868
  if not isinstance(impl, dict):
869
  continue
870
 
871
  program_name = impl.get("program_name", "N/A")
872
  hotel_id = impl.get("hotel_id", "N/A")
873
 
 
 
 
 
 
 
874
  content = f"""
875
  ์ฑ„๋„ ํ”„๋กœ๊ทธ๋žจ: {program_name}
876
- ํ˜ธํ…”: {hotel_id}
877
- ์ฑ„๋„: {impl.get('channel', 'DIRECT')}
 
 
 
 
 
 
 
 
 
 
 
 
878
  """
879
 
880
  # ํ˜œํƒ ์˜ค๋ฒ„๋ผ์ด๋“œ
@@ -884,16 +1542,28 @@ def handle_channel_implementations(data: Any, context: Dict[str, Any]) -> List[D
884
  for override in overrides[:5]:
885
  if isinstance(override, dict):
886
  category = override.get("benefit_category", "N/A")
887
- detail = override.get("implementation_detail", "")[:80]
888
  content += f"- {category}: {detail}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
 
890
  chunks.append({
891
  "content": content.strip(),
892
- "metadata": {
893
- "type": "channel_implementation",
894
- "program_name": program_name,
895
- "hotel_id": hotel_id
896
- }
897
  })
898
 
899
  return chunks
@@ -904,7 +1574,7 @@ def handle_channel_implementations(data: Any, context: Dict[str, Any]) -> List[D
904
  # ===========================================================================
905
 
906
  def handle_channel_benefit_packages(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
907
- """channel_benefit_packages ์ฒ˜๋ฆฌ (FHR, THC ๋“ฑ)"""
908
  chunks = []
909
 
910
  packages = data if isinstance(data, list) else [data]
@@ -921,15 +1591,47 @@ def handle_channel_benefit_packages(data: Any, context: Dict[str, Any]) -> List[
921
  ์ฑ„๋„: {channel}
922
  """
923
 
924
- # ํ˜œํƒ ๋ชฉ๋ก
925
- benefits = pkg.get("benefits", pkg.get("included_benefits", []))
 
 
 
 
 
 
 
 
926
  if benefits:
927
  content += "\nํฌํ•จ ํ˜œํƒ:\n"
928
- for benefit in benefits[:6]:
929
  if isinstance(benefit, dict):
930
- content += f"- {benefit.get('name', 'N/A')}: {benefit.get('description', '')[:60]}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
931
  elif isinstance(benefit, str):
932
- content += f"- {benefit}\n"
 
 
933
 
934
  chunks.append({
935
  "content": content.strip(),
@@ -948,31 +1650,90 @@ def handle_channel_benefit_packages(data: Any, context: Dict[str, Any]) -> List[
948
  # ===========================================================================
949
 
950
  def handle_member_rates(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
951
- """member_rates ์ฒ˜๋ฆฌ (ํšŒ์› ์ „์šฉ ์š”๊ธˆ)"""
952
  chunks = []
953
  chain = context.get("chain", "UNKNOWN")
954
-
955
  if data is None:
956
  return chunks
957
-
958
  rates = data if isinstance(data, list) else [data]
959
-
960
  for rate in rates[:5]:
961
  if not isinstance(rate, dict):
962
  continue
963
-
964
  rate_name = rate.get("rate_name", rate.get("name", "N/A"))
965
-
966
  content = f"""
967
  ํšŒ์› ์š”๊ธˆ: {rate_name}
968
  ์ฒด์ธ: {rate.get('chain', chain)}
969
- ํ• ์ธ์œจ: {rate.get('discount_percentage', rate.get('discount', 'N/A'))}%
970
- ์ ์šฉ ์กฐ๊ฑด: {rate.get('conditions', rate.get('eligibility', 'N/A'))}
971
  """
972
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
  if rate.get("description"):
974
  content += f"์„ค๋ช…: {rate.get('description')}\n"
975
-
976
  chunks.append({
977
  "content": content.strip(),
978
  "metadata": {
@@ -981,7 +1742,7 @@ def handle_member_rates(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
981
  "chain": rate.get("chain", chain)
982
  }
983
  })
984
-
985
  return chunks
986
 
987
 
@@ -1304,44 +2065,43 @@ def handle_dining_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str,
1304
  if min_spend:
1305
  content += f" - ์ตœ์†Œ ์†Œ๋น„ ๊ธˆ์•ก: ${min_spend}\n"
1306
 
1307
- # facts ์ •๋ณด ์ถ”๊ฐ€
1308
- facts = context.get("facts", {})
1309
- if isinstance(facts, dict):
1310
- # ํ”„๋กœ๊ทธ๋žจ ์„ธ๋ถ€์‚ฌํ•ญ
1311
- program_details = facts.get("program_details", {})
1312
- if program_details:
1313
- content += "\nํ”„๋กœ๊ทธ๋žจ ์„ธ๋ถ€์‚ฌํ•ญ:\n"
1314
- total_outlets = program_details.get("total_participating_outlets")
1315
- if total_outlets:
1316
- content += f" - ์ฐธ์—ฌ ๋งค์žฅ ์ˆ˜: {total_outlets}๊ฐœ\n"
1317
- coverage = program_details.get("coverage")
1318
- if coverage:
1319
- content += f" - ์ปค๋ฒ„๋ฆฌ์ง€: {coverage}\n"
1320
- outlet_types = program_details.get("outlet_types", [])
1321
- if outlet_types:
1322
- content += f" - ๋งค์žฅ ์œ ํ˜•: {', '.join(outlet_types)}\n"
1323
-
1324
- # ์ œํ•œ์‚ฌํ•ญ
1325
- restrictions = facts.get("restrictions", {})
1326
- if restrictions:
 
 
 
 
 
 
 
 
 
1327
  content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
1328
- max_group = restrictions.get("max_group_size")
1329
- if max_group:
1330
- content += f" - ์ตœ๋Œ€ ๊ทธ๋ฃน ํฌ๊ธฐ: {max_group}๋ช…\n"
1331
- split_bills = restrictions.get("split_bills_allowed")
1332
- if split_bills is not None:
1333
- content += f" - ๋ถ„ํ•  ๊ฒฐ์ œ: {'ํ—ˆ์šฉ' if split_bills else '๋ถˆํ—ˆ์šฉ'}\n"
1334
- member_present = restrictions.get("member_must_be_present")
1335
- if member_present is not None:
1336
- content += f" - ํšŒ์› ๋™์„: {'ํ•„์ˆ˜' if member_present else '๋ถˆํ•„์š”'}\n"
1337
-
1338
- # ์ œ์™ธ์‚ฌํ•ญ
1339
- exclusions = facts.get("exclusions", [])
1340
- if exclusions:
1341
- content += "\n์ œ์™ธ์‚ฌํ•ญ:\n"
1342
- for exclusion in exclusions:
1343
- if isinstance(exclusion, str):
1344
- content += f" - {exclusion.replace('_', ' ')}\n"
1345
 
1346
  content += DISCLAIMER_SHORT
1347
 
@@ -1434,7 +2194,10 @@ def handle_airline_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str
1434
  def handle_airline_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1435
  """airline_tiers ์ฒ˜๋ฆฌ"""
1436
  chunks = []
1437
-
 
 
 
1438
  tiers = data if isinstance(data, list) else [data]
1439
 
1440
  for tier in tiers[:10]:
@@ -1443,14 +2206,18 @@ def handle_airline_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, A
1443
 
1444
  tier_name = tier.get("tier_name", "N/A")
1445
  airline = tier.get("airline", context.get("chain", "UNKNOWN"))
1446
-
 
 
 
 
1447
  content = f"""
1448
  ํ•ญ๊ณต ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰: {tier_name}
1449
  ํ•ญ๊ณต์‚ฌ: {airline}
1450
  ๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
1451
 
1452
  ์ž๊ฒฉ ์š”๊ฑด:
1453
- - ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€: {tier.get("miles_required", "N/A"):,} if isinstance(tier.get("miles_required"), int) else "N/A"
1454
  - ํ•„์š” ๊ตฌ๊ฐ„: {tier.get("segments_required", "N/A")}
1455
  - ์‚ฐ์ • ๊ธฐ๊ฐ„: {tier.get("qualification_period", "calendar_year")}
1456
  """
@@ -1563,47 +2330,80 @@ def handle_award_charts(data: Any, context: Dict[str, Any]) -> List[Dict[str, An
1563
 
1564
 
1565
  def handle_airline_earning_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1566
- """airline_earning_rules ์ฒ˜๋ฆฌ"""
1567
  chunks = []
1568
-
1569
  rules = data if isinstance(data, list) else [data]
1570
  airline = context.get("chain", "UNKNOWN")
1571
-
1572
  # ๊ทœ์น™๋“ค์„ ํ•˜๋‚˜์˜ ์ฒญํฌ๋กœ ๋ฌถ๊ธฐ
1573
  if not rules:
1574
  return chunks
1575
-
1576
  content = f"""
1577
  ๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ ๊ทœ์น™
1578
  ํ•ญ๊ณต์‚ฌ: {airline}
1579
 
1580
  """
1581
-
1582
  for rule in rules[:15]:
1583
  if not isinstance(rule, dict):
1584
  continue
1585
-
1586
  rule_name = rule.get("rule_name", "N/A")
1587
  cabin = rule.get("cabin_class", "")
1588
- rate = rule.get("earning_rate_percent", "N/A")
1589
- route = rule.get("route_type", "")
1590
-
1591
  content += f"โ€ข {rule_name}\n"
1592
  if cabin:
1593
  content += f" - ๊ฐ์‹ค: {cabin}\n"
 
 
 
1594
  if route:
1595
  content += f" - ๋…ธ์„ : {route}\n"
1596
- content += f" - ์ ๋ฆฝ๋ฅ : {rate}%\n"
1597
-
1598
- if rule.get("booking_classes"):
1599
- content += f" - ์˜ˆ์•ฝ ํด๋ž˜์Šค: {', '.join(rule['booking_classes'])}\n"
1600
- if rule.get("bonus_miles"):
1601
- content += f" - ๋ณด๋„ˆ์Šค: +{rule['bonus_miles']:,} ๋งˆ์ผ\n"
1602
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  content += "\n"
1604
-
1605
  content += DISCLAIMER_SHORT
1606
-
1607
  if len(content) > 100:
1608
  chunks.append({
1609
  "content": content.strip(),
@@ -1612,7 +2412,7 @@ def handle_airline_earning_rules(data: Any, context: Dict[str, Any]) -> List[Dic
1612
  "airline": airline
1613
  }
1614
  })
1615
-
1616
  return chunks
1617
 
1618
 
@@ -1809,7 +2609,15 @@ CHUNK_HANDLERS = {
1809
 
1810
  # ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰
1811
  "membership_tiers": handle_membership_tiers,
1812
- "milestone_program": handle_membership_tiers, # ๋งˆ์ผ์Šคํ†ค์€ ๋ฉค๋ฒ„์‹ญ ๊ด€๋ จ์ด๋ฏ€๋กœ ๋™์ผ ํ•ธ๋“ค๋Ÿฌ ์‚ฌ์šฉ
 
 
 
 
 
 
 
 
1813
 
1814
  # ํฌ์ธํŠธ ์‹œ์Šคํ…œ
1815
  "points_systems": handle_points_systems,
@@ -1817,6 +2625,7 @@ CHUNK_HANDLERS = {
1817
 
1818
  # ํ˜ธํ…”
1819
  "hotel_properties": handle_hotel_properties,
 
1820
 
1821
  # ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
1822
  "tier_implementations": handle_tier_implementations,
@@ -1975,7 +2784,7 @@ IGNORED_KEYS = {
1975
  "facts",
1976
 
1977
  # Core ํ‚ค์— ํฌํ•จ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ฒ˜๋ฆฌ ๋ถˆํ•„์š”
1978
- "hotel_brands", # hotel_properties์— ํฌํ•จ
1979
  "nearby_attractions", # extra_attributes์— ํฌํ•จ
1980
 
1981
  # ๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ
 
39
  return content
40
 
41
 
42
+ def format_evidence(evidence_list: list, max_items: int = 2, max_quote_len: int = 80) -> str:
43
+ """Evidence ๋ฐฐ์—ด์„ ์ฒญํฌ์— ํฌํ•จํ•  ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜
44
+
45
+ Args:
46
+ evidence_list: evidence ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ
47
+ max_items: ํฌํ•จํ•  ์ตœ๋Œ€ evidence ์ˆ˜
48
+ max_quote_len: ์ธ์šฉ๋ฌธ ์ตœ๋Œ€ ๊ธธ์ด
49
+
50
+ Returns:
51
+ ํฌ๋งท๋œ ์ถœ์ฒ˜ ์ •๋ณด ๋ฌธ์ž์—ด (๋น„์–ด์žˆ์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด)
52
+ """
53
+ if not evidence_list or not isinstance(evidence_list, list):
54
+ return ""
55
+
56
+ content = "\n์ถœ์ฒ˜:\n"
57
+ added = 0
58
+
59
+ for ev in evidence_list:
60
+ if added >= max_items:
61
+ break
62
+ if not isinstance(ev, dict):
63
+ continue
64
+
65
+ section = ev.get("section_path", "")
66
+ quote = ev.get("quote", "")
67
+
68
+ if section or quote:
69
+ if section:
70
+ content += f" - [{section}] "
71
+ else:
72
+ content += " - "
73
+
74
+ if quote:
75
+ # ์ธ์šฉ๋ฌธ ๊ธธ์ด ์ œํ•œ
76
+ truncated = quote[:max_quote_len]
77
+ if len(quote) > max_quote_len:
78
+ truncated += "..."
79
+ content += f'"{truncated}"\n'
80
+ else:
81
+ content += "\n"
82
+ added += 1
83
+
84
+ return content if added > 0 else ""
85
+
86
+
87
  # ===========================================================================
88
  # ํ•ธ๋“ค๋Ÿฌ: ๋กœ์—ดํ‹ฐ ํ”„๋กœ๊ทธ๋žจ
89
  # ===========================================================================
 
135
  """membership_tiers ์ฒ˜๋ฆฌ"""
136
  chunks = []
137
  chain = context.get("chain", "UNKNOWN")
138
+
139
+ if data is None:
140
+ return chunks
141
+
142
  tiers = data if isinstance(data, list) else [data]
143
 
144
  for tier in tiers[:10]:
 
149
  tier_level = tier.get("tier_level", "N/A")
150
  tier_chain = tier.get("chain", chain)
151
 
152
+ # ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: qualification (TierQualification)
153
+ # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: upgrade_requirement๋„ ํด๋ฐฑ ์ง€์›
154
+ qualification = tier.get("qualification", tier.get("upgrade_requirement", {}))
155
+ req_nights = qualification.get("nights_required", qualification.get("nights", "N/A"))
156
+ req_stays = qualification.get("stays_required")
157
+ req_points = qualification.get("points_required", qualification.get("points"))
158
+ req_spending = qualification.get("spending_required")
159
+ qual_period = qualification.get("qualification_period", "calendar_year")
160
+
161
+ # ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: bonus_points_percentage
162
+ bonus_pct = tier.get("bonus_points_percentage", tier.get("elite_bonus_points"))
163
 
164
  content = f"""
165
  ๋“ฑ๊ธ‰: {tier_name} ({tier_level})
166
  ์ฒด์ธ: {tier_chain}
167
  ๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
168
  ์ž๊ฒฉ ์š”๊ฑด:
169
+ - ์ˆ™๋ฐ•: {req_nights}๋ฐ•"""
170
+
171
+ if req_stays:
172
+ content += f"\n - ์ฒด๋ฅ˜: {req_stays}ํšŒ"
173
+ if req_points:
174
+ content += f"\n - ํฌ์ธํŠธ: {req_points:,}P" if isinstance(req_points, int) else f"\n - ํฌ์ธํŠธ: {req_points}"
175
+ if req_spending and isinstance(req_spending, dict):
176
+ content += f"\n - ์ง€์ถœ: {req_spending.get('amount', 'N/A')} {req_spending.get('currency', '')}"
177
+
178
+ content += f"\n - ์‚ฐ์ • ๊ธฐ๊ฐ„: {qual_period}\n"
179
+
180
+ # ๋Œ€์ฒด ์ž๊ฒฉ ๊ฒฝ๋กœ
181
+ alt_paths = qualification.get("alternative_paths", [])
182
+ if alt_paths:
183
+ content += f" - ๋Œ€์ฒด ๊ฒฝ๋กœ: {', '.join(alt_paths[:3])}\n"
184
+
185
+ if bonus_pct:
186
+ content += f"๋ณด๋„ˆ์Šค ํฌ์ธํŠธ: {bonus_pct}%\n"
187
+
188
+ # ๋“ฑ๊ธ‰ ์œ ์ง€ ์กฐ๊ฑด: retention (TierRetention)
189
+ retention = tier.get("retention")
190
+ if retention and isinstance(retention, dict):
191
+ content += "\n๋“ฑ๊ธ‰ ์œ ์ง€ ์กฐ๊ฑด:\n"
192
+ ret_nights = retention.get("nights_required")
193
+ ret_points = retention.get("points_required")
194
+ if ret_nights:
195
+ content += f" - ์ˆ™๋ฐ•: {ret_nights}๋ฐ•\n"
196
+ if ret_points:
197
+ content += f" - ํฌ์ธํŠธ: {ret_points:,}P\n"
198
+
199
+ # ํ‰์ƒ ๋“ฑ๊ธ‰
200
+ if tier.get("lifetime_tier_available"):
201
+ content += f"\nํ‰์ƒ ๋“ฑ๊ธ‰: ๊ฐ€๋Šฅ"
202
+ if tier.get("lifetime_requirements"):
203
+ content += f" ({tier.get('lifetime_requirements')})"
204
+ content += "\n"
205
 
206
  # ์ฃผ์š” ํ˜œํƒ
207
  benefits = tier.get("benefits", [])
208
  if benefits:
209
+ content += "\n์ฃผ์š” ํ˜œํƒ:\n"
210
  for benefit in benefits[:8]:
211
  if isinstance(benefit, dict):
212
  ben_name = benefit.get("name", benefit.get("benefit_name", "N/A"))
213
+ ben_desc = benefit.get("description", "")[:80]
214
+ content += f" - {ben_name}"
215
+ if ben_desc:
216
+ content += f": {ben_desc}"
217
+ content += "\n"
218
+
219
+ # evidence ์ •๋ณด ํฌํ•จ (์„ ํƒ์ )
220
+ evidence = tier.get("evidence", [])
221
+ if evidence:
222
+ content += format_evidence(evidence, max_items=2)
223
 
224
  chunks.append({
225
  "content": content.strip(),
 
234
  return chunks
235
 
236
 
237
+ # ===========================================================================
238
+ # ํ•ธ๋“ค๋Ÿฌ: ๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ
239
+ # ===========================================================================
240
+
241
+ def handle_milestone_programs(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
242
+ """milestone_program ์ฒ˜๋ฆฌ (IHG Milestone Rewards ๋“ฑ)"""
243
+ chunks = []
244
+ chain = context.get("chain", "UNKNOWN")
245
+
246
+ programs = data if isinstance(data, list) else [data]
247
+
248
+ for program in programs[:3]:
249
+ if not isinstance(program, dict):
250
+ continue
251
+
252
+ program_name = program.get("program_name", "Milestone Rewards")
253
+ program_chain = program.get("chain", chain)
254
+ minimum_tier = program.get("minimum_tier", "N/A")
255
+ reset_period = program.get("reset_period", "calendar_year")
256
+
257
+ content = f"""
258
+ ๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ: {program_name}
259
+ ์ฒด์ธ: {program_chain}
260
+ ์ฐธ์—ฌ ์ตœ์†Œ ๋“ฑ๊ธ‰: {minimum_tier}
261
+ ๋ฆฌ์…‹ ์ฃผ๊ธฐ: {reset_period}
262
+ ๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ: {"์˜ˆ" if program.get("rollover_enabled") else "์•„๋‹ˆ์˜ค"}
263
+ """
264
+
265
+ # ๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ ๋“ฑ๊ธ‰
266
+ rollover_tiers = program.get("rollover_eligible_tiers", [])
267
+ if rollover_tiers:
268
+ content += f"๋กค์˜ค๋ฒ„ ๊ฐ€๋Šฅ ๋“ฑ๊ธ‰: {', '.join(str(t) for t in rollover_tiers)}\n"
269
+
270
+ # ๋งˆ์ผ์Šคํ†ค ๋ชฉ๋ก
271
+ milestones = program.get("milestones", [])
272
+ if milestones:
273
+ content += "\n๋งˆ์ผ์Šคํ†ค ๋ณด์ƒ:\n"
274
+ for milestone in milestones[:10]:
275
+ if not isinstance(milestone, dict):
276
+ continue
277
+
278
+ nights = milestone.get("nights_required", "?")
279
+ is_bonus = milestone.get("is_bonus_choice", False)
280
+ max_sel = milestone.get("max_selections", 1)
281
+
282
+ content += f"\n [{nights}๋ฐ• ๋‹ฌ์„ฑ]"
283
+ if is_bonus:
284
+ content += f" (Bonus Choice - {max_sel}๊ฐœ ์„ ํƒ)"
285
+ content += ":\n"
286
+
287
+ # ๋ณด์ƒ ์˜ต์…˜
288
+ options = milestone.get("reward_options", [])
289
+ for opt in options[:6]:
290
+ if isinstance(opt, dict):
291
+ opt_name = opt.get("name", "N/A")
292
+ bonus_pts = opt.get("bonus_points")
293
+ quantity = opt.get("quantity")
294
+
295
+ opt_str = f" - {opt_name}"
296
+ if bonus_pts:
297
+ opt_str += f" ({bonus_pts:,}P)"
298
+ elif quantity:
299
+ opt_str += f" ({quantity}๊ฐœ)"
300
+ content += opt_str + "\n"
301
+
302
+ content += DISCLAIMER_SHORT
303
+
304
+ chunks.append({
305
+ "content": content.strip(),
306
+ "metadata": {
307
+ "type": "milestone_program",
308
+ "program_name": program_name,
309
+ "chain": program_chain,
310
+ "minimum_tier": str(minimum_tier)
311
+ }
312
+ })
313
+
314
+ return chunks
315
+
316
+
317
+ # ===========================================================================
318
+ # ํ•ธ๋“ค๋Ÿฌ: ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ
319
+ # ===========================================================================
320
+
321
+ def handle_partner_status_matches(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
322
+ """partner_status_matches ์ฒ˜๋ฆฌ (ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ)"""
323
+ chunks = []
324
+ chain = context.get("chain", "UNKNOWN")
325
+
326
+ matches = data if isinstance(data, list) else [data]
327
+
328
+ # ์—ฌ๋Ÿฌ ํŒŒํŠธ๋„ˆ๋ฅผ ํ•˜๋‚˜์˜ ์ฒญํฌ๋กœ ๋ฌถ๊ธฐ
329
+ if not matches:
330
+ return chunks
331
+
332
+ content = f"""
333
+ ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ ํ”„๋กœ๊ทธ๋žจ
334
+ ์ฒด์ธ: {chain}
335
+
336
+ """
337
+
338
+ for match in matches[:10]:
339
+ if not isinstance(match, dict):
340
+ continue
341
+
342
+ partner_name = match.get("partner_name", "N/A")
343
+ partner_program = match.get("partner_program", "")
344
+ partner_tier = match.get("partner_tier", "N/A")
345
+ required_tier = match.get("required_hotel_tier", "N/A")
346
+ validity = match.get("validity", "")
347
+
348
+ content += f"โ€ข {partner_name}"
349
+ if partner_program:
350
+ content += f" ({partner_program})"
351
+ content += f"\n"
352
+ content += f" - ํ•„์š” ํ˜ธํ…” ๋“ฑ๊ธ‰: {required_tier}\n"
353
+ content += f" - ๋ถ€์—ฌ ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰: {partner_tier}\n"
354
+
355
+ if validity:
356
+ content += f" - ์œ ํšจ ๊ธฐ๊ฐ„: {validity}\n"
357
+
358
+ # ํŒŒํŠธ๋„ˆ ํ˜œํƒ
359
+ partner_benefits = match.get("partner_benefits", [])
360
+ if partner_benefits:
361
+ content += f" - ํ˜œํƒ: {', '.join(partner_benefits[:3])}\n"
362
+
363
+ content += "\n"
364
+
365
+ content += DISCLAIMER_SHORT
366
+
367
+ chunks.append({
368
+ "content": content.strip(),
369
+ "metadata": {
370
+ "type": "partner_status_match",
371
+ "chain": chain
372
+ }
373
+ })
374
+
375
+ return chunks
376
+
377
+
378
  # ===========================================================================
379
  # ํ•ธ๋“ค๋Ÿฌ: ํฌ์ธํŠธ ์‹œ์Šคํ…œ
380
  # ===========================================================================
 
399
  content = f"""
400
  ํฌ์ธํŠธ ์‹œ์Šคํ…œ: {point_name}
401
  ์ฒด์ธ: {system.get("chain", chain)}
 
 
402
  """
403
 
404
+ # ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: expiration_policy (PointExpirationPolicy)
405
+ # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: expiry๋„ ํด๋ฐฑ ์ง€์›
406
+ expiration = system.get("expiration_policy", system.get("expiry"))
407
+ if expiration:
408
+ if isinstance(expiration, dict):
409
+ expires = expiration.get("expires", expiration.get("has_expiry", True))
410
+ exp_months = expiration.get("expiration_months", expiration.get("expiry_period"))
411
+ can_extend = expiration.get("can_extend_with_activity", False)
412
+ activity_types = expiration.get("activity_types", [])
413
+
414
+ content += f"\n๋งŒ๋ฃŒ: {'์žˆ์Œ' if expires else '์—†์Œ'}"
415
+ if exp_months:
416
+ content += f" ({exp_months}๊ฐœ์›”)"
417
+ content += "\n"
418
+
419
+ if can_extend:
420
+ content += f"ํ™œ๋™์œผ๋กœ ์—ฐ์žฅ: ๊ฐ€๋Šฅ"
421
+ if activity_types:
422
+ content += f" ({', '.join(activity_types)})"
423
+ content += "\n"
424
+ else:
425
+ content += f"\n๋งŒ๋ฃŒ: {expiration}\n"
426
+
427
  # ์ ๋ฆฝ ๊ทœ์น™
428
+ content += "\n์ ๋ฆฝ ๊ทœ์น™:\n"
429
  earning_rules = system.get("earning_rules", [])
430
  if earning_rules and isinstance(earning_rules, list):
431
  for rule in earning_rules[:6]:
432
  if isinstance(rule, dict):
433
+ rule_name = rule.get("rule_name", rule.get("name", "N/A"))
434
+ category = rule.get("category", "")
435
+ points_per_unit = rule.get("points_per_unit")
436
+ spending_unit = rule.get("spending_unit", {})
437
+
438
+ content += f" - {rule_name}"
439
+ if category:
440
+ content += f" ({category})"
441
+ content += ": "
442
+
443
+ # ์Šคํ‚ค๋งˆ: points_per_unit, spending_unit (Money ๊ฐ์ฒด)
444
+ if points_per_unit and spending_unit:
445
+ # spending_unit์ด dict์ธ ๊ฒฝ์šฐ Money ๊ตฌ์กฐ ํ™•์ธ
446
+ if isinstance(spending_unit, dict):
447
+ unit_amount = spending_unit.get("amount", "N/A")
448
+ unit_currency = spending_unit.get("currency", "")
449
+ if unit_amount != "N/A" and unit_currency:
450
+ content += f"{unit_amount} {unit_currency}๋‹น {points_per_unit}P"
451
+ else:
452
+ content += f"{points_per_unit}P"
453
+ else:
454
+ # spending_unit์ด ๋‹จ์ˆœ ๊ฐ’์ธ ๊ฒฝ์šฐ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ)
455
+ content += f"{spending_unit}๋‹น {points_per_unit}P"
456
+ elif rule.get("rate"):
457
+ content += f"{rule.get('rate')}"
458
+ else:
459
+ content += "N/A"
460
+ content += "\n"
461
+
462
+ # ํ•œ๋„
463
+ monthly_cap = rule.get("monthly_cap")
464
+ yearly_cap = rule.get("yearly_cap")
465
+ if monthly_cap:
466
+ content += f" ์›” ํ•œ๋„: {monthly_cap:,}P\n"
467
+ if yearly_cap:
468
+ content += f" ์—ฐ ํ•œ๋„: {yearly_cap:,}P\n"
469
+
470
+ # ์‚ฌ์šฉ ๊ทœ์น™
471
+ redemption_rules = system.get("redemption_rules", [])
472
+ if redemption_rules:
473
+ content += "\n์‚ฌ์šฉ ๊ทœ์น™:\n"
474
+ for rule in redemption_rules[:3]:
475
+ if isinstance(rule, dict):
476
+ r_type = rule.get("redemption_type", "N/A")
477
+ min_points = rule.get("minimum_points", "N/A")
478
+ content += f" - {r_type}: ์ตœ์†Œ {min_points:,}P\n" if isinstance(min_points, int) else f" - {r_type}: ์ตœ์†Œ {min_points}P\n"
479
 
480
+ # ์ „ํ™˜ ํŒŒํŠธ๋„ˆ
481
+ transfer_partners = system.get("transfer_partners", [])
482
+ if transfer_partners:
483
+ content += f"\n์ „ํ™˜ ํŒŒํŠธ๋„ˆ: {', '.join(transfer_partners[:5])}\n"
 
 
 
484
 
485
  chunks.append({
486
  "content": content.strip(),
 
555
  return chunks
556
 
557
 
558
+ # ===========================================================================
559
+ # ํ•ธ๋“ค๋Ÿฌ: ํ˜ธํ…” ๋ธŒ๋žœ๋“œ
560
+ # ===========================================================================
561
+
562
+ def handle_hotel_brands(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
563
+ """hotel_brands ์ฒ˜๋ฆฌ"""
564
+ chunks = []
565
+ chain = context.get("chain", "UNKNOWN")
566
+
567
+ brands = data if isinstance(data, list) else [data]
568
+
569
+ for brand in brands[:10]:
570
+ if not isinstance(brand, dict):
571
+ continue
572
+
573
+ brand_code = brand.get("brand_code", "N/A")
574
+ brand_name = brand.get("brand_name", "N/A")
575
+ brand_chain = brand.get("chain", chain)
576
+ tier = brand.get("tier", "N/A")
577
+
578
+ # ํ•œ๊ตญ์–ด ์ด๋ฆ„
579
+ name_localized = brand.get("brand_name_localized", {})
580
+ name_ko = name_localized.get("ko") if isinstance(name_localized, dict) else None
581
+
582
+ content = f"""
583
+ ํ˜ธํ…” ๋ธŒ๋žœ๋“œ: {brand_name}
584
+ """
585
+ if name_ko and name_ko != brand_name:
586
+ content += f"๋ธŒ๋žœ๋“œ๋ช… (ํ•œ๊ตญ์–ด): {name_ko}\n"
587
+
588
+ content += f"""๋ธŒ๋žœ๋“œ ์ฝ”๋“œ: {brand_code}
589
+ ์ฒด์ธ: {brand_chain}
590
+ ํ‹ฐ์–ด: {tier}
591
+ """
592
+
593
+ # ๊ธฐ๋ณธ ํ˜œํƒ
594
+ default_benefits = brand.get("default_benefits", [])
595
+ if default_benefits:
596
+ content += "\n๋ธŒ๋žœ๋“œ ๊ธฐ๋ณธ ํ˜œํƒ:\n"
597
+ for benefit in default_benefits[:5]:
598
+ if isinstance(benefit, dict):
599
+ ben_name = benefit.get("name", "N/A")
600
+ content += f" - {ben_name}\n"
601
+
602
+ chunks.append({
603
+ "content": content.strip(),
604
+ "metadata": {
605
+ "type": "hotel_brand",
606
+ "brand_code": brand_code,
607
+ "brand_name": brand_name,
608
+ "tier": str(tier)
609
+ }
610
+ })
611
+
612
+ return chunks
613
+
614
+
615
  # ===========================================================================
616
  # ํ•ธ๋“ค๋Ÿฌ: ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
617
  # ===========================================================================
618
 
619
  def handle_tier_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
620
+ """tier_implementations ์ฒ˜๋ฆฌ - ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ •๋ณด ํฌํ•จ์œผ๋กœ ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ"""
621
  chunks = []
622
  chain = context.get("chain", "UNKNOWN")
623
 
 
631
  tier_level = impl.get("tier_level", "N/A")
632
  impl_chain = impl.get("chain", chain)
633
 
634
+ # extra_attributes์—์„œ ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ถ”์ถœ (๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ)
635
+ extra = impl.get("extra_attributes", {})
636
+ country = extra.get("country", "")
637
+ hotel_name = extra.get("hotel_name", "")
638
+ region = extra.get("region", "")
639
+
640
  content = f"""
641
  ํ˜ธํ…”๋ณ„ ๋ฉค๋ฒ„์‹ญ ํ˜œํƒ ๊ตฌํ˜„
642
+ """
643
+ # ์‹ค์ œ ํ˜ธํ…”๋ช…์ด ์žˆ์œผ๋ฉด ์šฐ์„  ํ‘œ์‹œ
644
+ if hotel_name:
645
+ content += f"ํ˜ธํ…”: {hotel_name}\n"
646
+ else:
647
+ content += f"ํ˜ธํ…” ID: {hotel_id}\n"
648
+
649
+ # ๊ตญ๊ฐ€/์ง€์—ญ ์ •๋ณด (๊ฒ€์ƒ‰ ๋งค์นญ์— ์ค‘์š”)
650
+ if country:
651
+ content += f"๊ตญ๊ฐ€: {country}\n"
652
+ if region:
653
+ content += f"์ง€์—ญ: {region}\n"
654
+
655
+ content += f"""์ฒด์ธ: {impl_chain}
656
  ๋“ฑ๊ธ‰: {tier_level}
657
  ๋ผ์šด์ง€ ์ด์šฉ: {'๊ฐ€๋Šฅ' if impl.get('lounge_access') else '๋ถˆ๊ฐ€'}
658
  ์กฐ์‹ ํฌํ•จ: {'ํฌํ•จ' if impl.get('breakfast_included') else '๋ฏธํฌํ•จ'}
 
666
  for override in overrides[:5]:
667
  if isinstance(override, dict):
668
  category = override.get("benefit_category", "N/A")
669
+ detail = override.get("implementation_detail", "")[:150]
670
  content += f"- {category}: {detail}\n"
671
+
672
+ # ํŒ ์ถ”๊ฐ€
673
+ tips = override.get("tips", [])
674
+ for tip in tips[:2]:
675
+ content += f" โ†’ {tip}\n"
676
+
677
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—๋„ ๊ตญ๊ฐ€ ์ •๋ณด ํฌํ•จ
678
+ metadata = {
679
+ "type": "tier_implementation",
680
+ "hotel_id": hotel_id,
681
+ "tier_level": tier_level
682
+ }
683
+ if country:
684
+ metadata["country"] = country
685
+ if hotel_name:
686
+ metadata["hotel_name"] = hotel_name
687
 
688
  chunks.append({
689
  "content": content.strip(),
690
+ "metadata": metadata
 
 
 
 
691
  })
692
 
693
  return chunks
 
714
  issuer = card.get("issuer", "N/A")
715
  issuer_code = card.get("issuer_code", "")
716
 
717
+ # ์Šคํ‚ค๋งˆ ๊ตฌ์กฐ: AnnualFee.total (Money ๊ฐ์ฒด)
718
+ # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๋ ˆ๊ฑฐ์‹œ annual_fee.amount๋„ ์ง€์›
719
  annual_fee = card.get("annual_fee", {})
720
+ if isinstance(annual_fee, dict):
721
+ # ์Šคํ‚ค๋งˆ ๊ตฌ์กฐ: annual_fee.total.amount
722
+ total_fee = annual_fee.get("total", annual_fee) # ํด๋ฐฑ: ์ง์ ‘ ์ ‘๊ทผ
723
+ if isinstance(total_fee, dict):
724
+ fee_amount = total_fee.get("amount", "N/A")
725
+ fee_currency = total_fee.get("currency", "KRW")
726
+ else:
727
+ fee_amount = annual_fee.get("amount", "N/A")
728
+ fee_currency = annual_fee.get("currency", "KRW")
729
+
730
+ # ์ƒ์„ธ ์—ฐํšŒ๋น„ ์ •๋ณด
731
+ base_fee = annual_fee.get("base_fee", {})
732
+ partner_fee = annual_fee.get("partner_fee", {})
733
+ family_fee = annual_fee.get("family_card_fee", {})
734
+ waiver_conds = annual_fee.get("waiver_conditions", [])
735
+ else:
736
+ fee_amount = annual_fee
737
+ fee_currency = "KRW"
738
+ base_fee = partner_fee = family_fee = {}
739
+ waiver_conds = []
740
 
741
  content = f"""
742
  ์‹ ์šฉ์นด๋“œ: {card_name}
 
744
 
745
  if issuer_code:
746
  content += f" ({issuer_code})"
747
+
748
+ # ์—ฐํšŒ๋น„ ํ‘œ์‹œ
749
+ if isinstance(fee_amount, (int, float)):
750
+ content += f"\n์—ฐํšŒ๋น„: {fee_amount:,} {fee_currency}\n"
751
+ else:
752
+ content += f"\n์—ฐํšŒ๋น„: {fee_amount} {fee_currency}\n"
753
+
754
+ # ์ƒ์„ธ ์—ฐํšŒ๋น„ (์Šคํ‚ค๋งˆ ๊ตฌ์กฐ ๋ฐ˜์˜)
755
+ if base_fee and isinstance(base_fee, dict) and base_fee.get("amount"):
756
+ content += f" - ๊ธฐ๋ณธ: {base_fee.get('amount'):,} {base_fee.get('currency', '')}\n"
757
+ if partner_fee and isinstance(partner_fee, dict) and partner_fee.get("amount"):
758
+ content += f" - ์ œํœด: {partner_fee.get('amount'):,} {partner_fee.get('currency', '')}\n"
759
+ if family_fee and isinstance(family_fee, dict) and family_fee.get("amount"):
760
+ content += f" - ๊ฐ€์กฑ์นด๋“œ: {family_fee.get('amount'):,} {family_fee.get('currency', '')}\n"
761
+ if waiver_conds:
762
+ content += f" - ๋ฉด์ œ ์กฐ๊ฑด: {', '.join(waiver_conds[:2])}\n"
763
 
764
  # ์นด๋“œ ์œ ํ˜• (Phase 3)
765
  if card_type:
 
864
  elif isinstance(benefit, str):
865
  content += f" - {benefit}\n"
866
 
867
+ # evidence ์ •๋ณด ํฌํ•จ (์„ ํƒ์ )
868
+ evidence = card.get("evidence", [])
869
+ if evidence:
870
+ content += format_evidence(evidence, max_items=2)
871
+
872
  content += DISCLAIMER_SHORT
873
 
874
  # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ถ”๊ฐ€ ์ •๋ณด ํฌํ•จ
 
914
  ๊ธฐ๋ณธ ํ”„๋กœ๊ทธ๋žจ: {program.get('base_program_name', 'N/A')}
915
  """
916
 
917
+ # ํ”Œ๋žœ (์Šคํ‚ค๋งˆ: SubscriptionPlan.prices -> SubscriptionPlanPrice)
918
  plans = program.get("plans", [])
919
  for plan in plans[:5]:
920
  if isinstance(plan, dict):
921
  plan_name = plan.get("plan_name", "N/A")
922
  prices = plan.get("prices", [])
923
  price_str = ""
924
+
925
+ # ๊ฐ€๊ฒฉ ์ฒ˜๋ฆฌ ๊ฐœ์„  (์Šคํ‚ค๋งˆ์— ๋งž์ถค)
926
+ if prices:
927
+ for p in prices[:1]: # ์ฒซ ๋ฒˆ์งธ ๊ฐ€๊ฒฉ๋งŒ
928
+ if isinstance(p, dict):
929
+ billing_cycle = p.get("billing_cycle", "YEARLY")
930
+ price_info = p.get("price", {})
931
+ if isinstance(price_info, dict):
932
+ amount = price_info.get("amount", "N/A")
933
+ currency = price_info.get("currency", "")
934
+
935
+ # ๊ฒฐ์ œ ์ฃผ๊ธฐ๋ฅผ ํ•œ๊ตญ์–ด๋กœ ๋ณ€ํ™˜
936
+ cycle_kr = {
937
+ "YEARLY": "๋…„",
938
+ "MONTHLY": "์›”",
939
+ "QUARTERLY": "๋ถ„๊ธฐ",
940
+ "WEEKLY": "์ฃผ"
941
+ }.get(billing_cycle, billing_cycle.lower())
942
+
943
+ if amount != "N/A" and currency:
944
+ price_str = f"{amount} {currency}/{cycle_kr}"
945
+ else:
946
+ price_str = "๊ฐ€๊ฒฉ ์ •๋ณด ์—†์Œ"
947
+ else:
948
+ price_str = str(price_info) if price_info else "๊ฐ€๊ฒฉ ์ •๋ณด ์—†์Œ"
949
+
950
  content += f"\nํ”Œ๋žœ: {plan_name}\n๊ฐ€๊ฒฉ: {price_str}\n"
951
 
952
  # ํ”Œ๋žœ ํ˜œํƒ
 
1261
  # ===========================================================================
1262
 
1263
  def handle_best_rate_guarantee(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1264
+ """best_rate_guarantee ์ฒ˜๋ฆฌ (์Šคํ‚ค๋งˆ: BestRateGuarantee, BRGRule, BRGRemedy)
1265
+
1266
+ ์ฒญํฌ ๋ถ„๋ฆฌ ์ „๋žต:
1267
+ 1. ํ”„๋กœ๊ทธ๋žจ ๊ฐœ์š” ์ฒญํฌ (๊ธฐ๋ณธ ์ •๋ณด, ์ œ์™ธ ์‚ฌํ•ญ)
1268
+ 2. ๊ทœ์น™๋ณ„ ์ฒญํฌ (๊ฐ BRGRule์„ ๊ฐœ๋ณ„ ์ฒญํฌ๋กœ)
1269
+ """
1270
  chunks = []
1271
  chain = context.get("chain", "UNKNOWN")
1272
 
1273
  if not isinstance(data, dict):
1274
  return chunks
1275
 
1276
+ program_name = data.get("program_name", "Best Rate Guarantee")
1277
+ brg_chain = data.get("chain", chain)
1278
+
1279
+ # === ์ฒญํฌ 1: ํ”„๋กœ๊ทธ๋žจ ๊ฐœ์š” ===
1280
+ overview_content = f"""
1281
+ ์ตœ์ €๊ฐ€ ๋ณด์žฅ ํ”„๋กœ๊ทธ๋žจ: {program_name}
1282
+ ์ฒด์ธ: {brg_chain}
1283
+ ๋ฉค๋ฒ„์‹ญ ํ•„์š”: {"์˜ˆ" if data.get("requires_membership") else "์•„๋‹ˆ์˜ค"}
1284
  """
1285
 
1286
+ # ์ œ์™ธ ์‚ฌํ•ญ (์Šคํ‚ค๋งˆ ํ•„๋“œ)
1287
+ excluded_regions = data.get("excluded_regions", [])
1288
+ excluded_brands = data.get("excluded_brands", [])
1289
+ excluded_hotels = data.get("excluded_hotels", [])
1290
+
1291
+ if excluded_regions:
1292
+ overview_content += f"์ œ์™ธ ์ง€์—ญ: {', '.join(excluded_regions[:5])}\n"
1293
+ if excluded_brands:
1294
+ overview_content += f"์ œ์™ธ ๋ธŒ๋žœ๋“œ: {', '.join(excluded_brands[:5])}\n"
1295
+ if excluded_hotels:
1296
+ overview_content += f"์ œ์™ธ ํ˜ธํ…”: {', '.join(excluded_hotels[:3])}\n"
1297
+
1298
+ # ์•ฝ๊ด€
1299
+ if data.get("governing_law"):
1300
+ overview_content += f"\n์ค€๊ฑฐ๋ฒ•: {data.get('governing_law')}\n"
1301
+ if data.get("arbitration_clause"):
1302
+ overview_content += "์ค‘์žฌ ์กฐํ•ญ: ํฌํ•จ\n"
1303
 
1304
+ # ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
1305
+ if data.get("last_updated"):
1306
+ overview_content += f"๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ: {data.get('last_updated')}\n"
1307
+
1308
+ overview_content += DISCLAIMER_SHORT
 
1309
 
1310
  chunks.append({
1311
+ "content": overview_content.strip(),
1312
  "metadata": {
1313
+ "type": "best_rate_guarantee_overview",
1314
+ "program_name": program_name,
1315
+ "chain": brg_chain
1316
  }
1317
  })
1318
 
1319
+ # === ์ฒญํฌ 2+: ๊ทœ์น™๋ณ„ ์ƒ์„ธ (์Šคํ‚ค๋งˆ: BRGRule) ===
1320
+ rules = data.get("rules", [])
1321
+ for rule in rules[:5]: # ์ตœ๋Œ€ 5๊ฐœ ๊ทœ์น™
1322
+ if not isinstance(rule, dict):
1323
+ continue
1324
+
1325
+ rule_name = rule.get("rule_name", "N/A")
1326
+ summary = rule.get("summary", "")
1327
+
1328
+ rule_content = f"""
1329
+ {program_name} - {rule_name}
1330
+ ์ฒด์ธ: {brg_chain}
1331
+
1332
+ """
1333
+ if summary:
1334
+ rule_content += f"์š”์•ฝ: {summary}\n\n"
1335
+
1336
+ # ํด๋ ˆ์ž„ ์กฐ๊ฑด (์Šคํ‚ค๋งˆ ํ•„๋“œ)
1337
+ claim_window = rule.get("claim_window")
1338
+ min_hours = rule.get("minimum_hours_before_checkin")
1339
+ rate_diff = rule.get("rate_difference_threshold_percent")
1340
+ max_rooms = rule.get("max_rooms_per_claim")
1341
+
1342
+ if claim_window or min_hours or rate_diff or max_rooms:
1343
+ rule_content += "ํด๋ ˆ์ž„ ์กฐ๊ฑด:\n"
1344
+ if claim_window:
1345
+ rule_content += f" - ํด๋ ˆ์ž„ ๊ฐ€๋Šฅ ๊ธฐ๊ฐ„: {claim_window}\n"
1346
+ if min_hours:
1347
+ rule_content += f" - ์ฒดํฌ์ธ ์ „ ์ตœ์†Œ: {min_hours}์‹œ๊ฐ„\n"
1348
+ if rate_diff:
1349
+ rule_content += f" - ์ตœ์†Œ ๊ฐ€๊ฒฉ ์ฐจ์ด: {rate_diff}%\n"
1350
+ if max_rooms:
1351
+ rule_content += f" - ํด๋ ˆ์ž„๋‹น ์ตœ๋Œ€ ๊ฐ์‹ค: {max_rooms}์‹ค\n"
1352
+
1353
+ # ๋น„๊ต ๊ธฐ์ค€
1354
+ comparison = rule.get("comparison_criteria", [])
1355
+ if comparison:
1356
+ rule_content += f"\n๋น„๊ต ๊ธฐ์ค€:\n"
1357
+ for c in comparison[:6]:
1358
+ rule_content += f" - {c}\n"
1359
+
1360
+ # ์ฑ„๋„ ์š”๊ตฌ์‚ฌํ•ญ
1361
+ channel_reqs = rule.get("channel_requirements", [])
1362
+ if channel_reqs:
1363
+ rule_content += f"\n์˜ˆ์•ฝ ์ฑ„๋„: {', '.join(channel_reqs)}\n"
1364
+
1365
+ # ์ ์šฉ/์ œ์™ธ ์š”๊ธˆ
1366
+ eligible_rates = rule.get("eligible_rates", [])
1367
+ ineligible_rates = rule.get("ineligible_rates", [])
1368
+ if eligible_rates:
1369
+ rule_content += f"\n์ ์šฉ ์š”๊ธˆ:\n"
1370
+ for r in eligible_rates[:5]:
1371
+ rule_content += f" - {r}\n"
1372
+ if ineligible_rates:
1373
+ rule_content += f"\n์ œ์™ธ ์š”๊ธˆ:\n"
1374
+ for r in ineligible_rates[:8]:
1375
+ rule_content += f" - {r}\n"
1376
+
1377
+ # ๋ณด์ƒ (์Šคํ‚ค๋งˆ: BRGRemedy)
1378
+ remedies = rule.get("remedies", [])
1379
+ if remedies:
1380
+ rule_content += "\n๋ณด์ƒ:\n"
1381
+ for remedy in remedies[:3]:
1382
+ if isinstance(remedy, dict):
1383
+ r_type = remedy.get("type", "N/A")
1384
+ r_desc = remedy.get("description", "")
1385
+ rule_content += f" [{r_type}] {r_desc}\n"
1386
+
1387
+ # ๊ฐ€์น˜ (BenefitValue ๊ตฌ์กฐ)
1388
+ value = remedy.get("value", {})
1389
+ if isinstance(value, dict):
1390
+ v_type = value.get("type")
1391
+ v_text = value.get("text", "")
1392
+ if v_text:
1393
+ rule_content += f" - ๊ฐ€์น˜: {v_text}\n"
1394
+ elif v_type == "PERCENT" and value.get("percent"):
1395
+ rule_content += f" - ๊ฐ€์น˜: {value.get('percent')}%\n"
1396
+ elif v_type == "POINTS" and value.get("points"):
1397
+ rule_content += f" - ๊ฐ€์น˜: {value.get('points'):,}P\n"
1398
+
1399
+ # ์ตœ๋Œ€ ๋ณด์ƒ (Money ๊ตฌ์กฐ)
1400
+ max_val = remedy.get("max_value", {})
1401
+ if isinstance(max_val, dict) and max_val.get("amount"):
1402
+ rule_content += f" - ์ตœ๋Œ€: {max_val.get('amount'):,} {max_val.get('currency', '')}\n"
1403
+
1404
+ # ์ถ”๊ฐ€ ์กฐ๊ฑด
1405
+ conditions = rule.get("conditions", [])
1406
+ if conditions:
1407
+ rule_content += "\n์ถ”๊ฐ€ ์กฐ๊ฑด:\n"
1408
+ for cond in conditions[:5]:
1409
+ if isinstance(cond, dict):
1410
+ cond_summary = cond.get("summary", "")
1411
+ if cond_summary:
1412
+ rule_content += f" - {cond_summary}\n"
1413
+
1414
+ rule_content += DISCLAIMER_SHORT
1415
+
1416
+ if len(rule_content) > 100:
1417
+ chunks.append({
1418
+ "content": rule_content.strip(),
1419
+ "metadata": {
1420
+ "type": "best_rate_guarantee_rule",
1421
+ "program_name": program_name,
1422
+ "rule_name": rule_name,
1423
+ "chain": brg_chain
1424
+ }
1425
+ })
1426
+
1427
+ # === ํ•˜์œ„ ํ˜ธํ™˜: ๊ธฐ์กด conditions ํ˜•์‹ ์ง€์› (rules๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) ===
1428
+ conditions = data.get("conditions", [])
1429
+ if conditions and not rules:
1430
+ cond_content = f"""
1431
+ {program_name} - ์ด์šฉ ์กฐ๊ฑด
1432
+ ์ฒด์ธ: {brg_chain}
1433
+
1434
+ ์ด์šฉ ์กฐ๊ฑด:
1435
+ """
1436
+ for cond in conditions[:5]:
1437
+ if isinstance(cond, dict):
1438
+ cond_content += f" - {cond.get('description', cond.get('summary', cond))}\n"
1439
+ else:
1440
+ cond_content += f" - {cond}\n"
1441
+
1442
+ exclusions = data.get("exclusions", [])
1443
+ if exclusions:
1444
+ cond_content += "\n์ œ์™ธ ์‚ฌํ•ญ:\n"
1445
+ for exc in exclusions[:5]:
1446
+ if isinstance(exc, str):
1447
+ cond_content += f" - {exc}\n"
1448
+
1449
+ cond_content += DISCLAIMER_SHORT
1450
+
1451
+ chunks.append({
1452
+ "content": cond_content.strip(),
1453
+ "metadata": {
1454
+ "type": "best_rate_guarantee_conditions",
1455
+ "program_name": program_name,
1456
+ "chain": brg_chain
1457
+ }
1458
+ })
1459
+
1460
+ # === ์ฒญํฌ: ํด๋ ˆ์ž„ ๊ฑฐ๋ถ€ ์‚ฌ์œ  (extra_attributes) ===
1461
+ extra_attrs = data.get("extra_attributes", {})
1462
+ if extra_attrs and isinstance(extra_attrs, dict):
1463
+ rejection_reasons = extra_attrs.get("claim_rejection_reasons", [])
1464
+ if rejection_reasons:
1465
+ rejection_content = f"""
1466
+ {program_name} - ํด๋ ˆ์ž„ ๊ฑฐ๋ถ€ ์‚ฌ์œ 
1467
+ ์ฒด์ธ: {brg_chain}
1468
+
1469
+ ํด๋ ˆ์ž„์ด ๊ฑฐ๋ถ€๋  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์œ :
1470
+ """
1471
+ for reason in rejection_reasons:
1472
+ rejection_content += f" - {reason}\n"
1473
+
1474
+ # ์ถ”๊ฐ€ ์†์„ฑ
1475
+ if extra_attrs.get("nj_resident_exception"):
1476
+ rejection_content += f"\n๋‰ด์ €์ง€ ๊ฑฐ์ฃผ์ž ์˜ˆ์™ธ: {extra_attrs.get('nj_resident_exception')}\n"
1477
+ if extra_attrs.get("modification_rights"):
1478
+ rejection_content += f"ํ”„๋กœ๊ทธ๋žจ ์ˆ˜์ • ๊ถŒํ•œ: {extra_attrs.get('modification_rights')}\n"
1479
+ if extra_attrs.get("employee_exclusion"):
1480
+ rejection_content += f"์ง์› ์ œ์™ธ: {extra_attrs.get('employee_exclusion')}\n"
1481
+
1482
+ rejection_content += DISCLAIMER_SHORT
1483
+
1484
+ chunks.append({
1485
+ "content": rejection_content.strip(),
1486
+ "metadata": {
1487
+ "type": "best_rate_guarantee_rejection",
1488
+ "program_name": program_name,
1489
+ "chain": brg_chain
1490
+ }
1491
+ })
1492
+
1493
  return chunks
1494
 
1495
 
 
1498
  # ===========================================================================
1499
 
1500
  def handle_channel_implementations(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1501
+ """channel_implementations ์ฒ˜๋ฆฌ - ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ •๋ณด ํฌํ•จ์œผ๋กœ ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ"""
1502
  chunks = []
1503
  chain = context.get("chain", "UNKNOWN")
1504
 
1505
  impls = data if isinstance(data, list) else [data]
1506
 
1507
+ for impl in impls[:10]:
1508
  if not isinstance(impl, dict):
1509
  continue
1510
 
1511
  program_name = impl.get("program_name", "N/A")
1512
  hotel_id = impl.get("hotel_id", "N/A")
1513
 
1514
+ # extra_attributes์—์„œ ๊ตญ๊ฐ€/ํ˜ธํ…”๋ช… ์ถ”์ถœ (๊ฒ€์ƒ‰ ์ •ํ™•๋„ ํ–ฅ์ƒ)
1515
+ extra = impl.get("extra_attributes", {})
1516
+ country = extra.get("country", "")
1517
+ hotel_name = extra.get("hotel_name", "")
1518
+ region = extra.get("region", "")
1519
+
1520
  content = f"""
1521
  ์ฑ„๋„ ํ”„๋กœ๊ทธ๋žจ: {program_name}
1522
+ """
1523
+ # ์‹ค์ œ ํ˜ธํ…”๋ช…์ด ์žˆ์œผ๋ฉด ์šฐ์„  ํ‘œ์‹œ
1524
+ if hotel_name:
1525
+ content += f"ํ˜ธํ…”: {hotel_name}\n"
1526
+ else:
1527
+ content += f"ํ˜ธํ…” ID: {hotel_id}\n"
1528
+
1529
+ # ๊ตญ๊ฐ€/์ง€์—ญ ์ •๋ณด (๊ฒ€์ƒ‰ ๋งค์นญ์— ์ค‘์š”)
1530
+ if country:
1531
+ content += f"๊ตญ๊ฐ€: {country}\n"
1532
+ if region:
1533
+ content += f"์ง€์—ญ: {region}\n"
1534
+
1535
+ content += f"""์ฑ„๋„: {impl.get('channel', 'DIRECT')}
1536
  """
1537
 
1538
  # ํ˜œํƒ ์˜ค๋ฒ„๋ผ์ด๋“œ
 
1542
  for override in overrides[:5]:
1543
  if isinstance(override, dict):
1544
  category = override.get("benefit_category", "N/A")
1545
+ detail = override.get("implementation_detail", "")[:100]
1546
  content += f"- {category}: {detail}\n"
1547
+
1548
+ # ํŒ ์ถ”๊ฐ€
1549
+ tips = override.get("tips", [])
1550
+ for tip in tips[:2]:
1551
+ content += f" โ†’ {tip}\n"
1552
+
1553
+ # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—๋„ ๊ตญ๊ฐ€ ์ •๋ณด ํฌํ•จ
1554
+ metadata = {
1555
+ "type": "channel_implementation",
1556
+ "program_name": program_name,
1557
+ "hotel_id": hotel_id
1558
+ }
1559
+ if country:
1560
+ metadata["country"] = country
1561
+ if hotel_name:
1562
+ metadata["hotel_name"] = hotel_name
1563
 
1564
  chunks.append({
1565
  "content": content.strip(),
1566
+ "metadata": metadata
 
 
 
 
1567
  })
1568
 
1569
  return chunks
 
1574
  # ===========================================================================
1575
 
1576
  def handle_channel_benefit_packages(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1577
+ """channel_benefit_packages ์ฒ˜๋ฆฌ (FHR, THC ๋“ฑ) - ์Šคํ‚ค๋งˆ: ChannelBenefitPackage"""
1578
  chunks = []
1579
 
1580
  packages = data if isinstance(data, list) else [data]
 
1591
  ์ฑ„๋„: {channel}
1592
  """
1593
 
1594
+ # ํŠน์ • ์š”๊ธˆ์ œ ํ•„์š” ์—ฌ๋ถ€
1595
+ if pkg.get("requires_specific_rate"):
1596
+ content += "ํŠน์ • ์š”๊ธˆ์ œ ํ•„์š”: ์˜ˆ\n"
1597
+ eligible_rates = pkg.get("eligible_rates", [])
1598
+ if eligible_rates:
1599
+ content += f"์ ์šฉ ๊ฐ€๋Šฅ ์š”๊ธˆ: {', '.join(eligible_rates)}\n"
1600
+
1601
+ # ์Šคํ‚ค๋งˆ ํ•„๋“œ๋ช…: standard_benefits
1602
+ # ํ•˜์œ„ ํ˜ธํ™˜: benefits, included_benefits๋„ ์ง€์›
1603
+ benefits = pkg.get("standard_benefits", pkg.get("benefits", pkg.get("included_benefits", [])))
1604
  if benefits:
1605
  content += "\nํฌํ•จ ํ˜œํƒ:\n"
1606
+ for benefit in benefits[:8]:
1607
  if isinstance(benefit, dict):
1608
+ ben_name = benefit.get("name", benefit.get("benefit_name", "N/A"))
1609
+ ben_desc = benefit.get("description", "")[:60]
1610
+ ben_category = benefit.get("category", "")
1611
+
1612
+ content += f" - {ben_name}"
1613
+ if ben_category:
1614
+ content += f" [{ben_category}]"
1615
+ if ben_desc:
1616
+ content += f": {ben_desc}"
1617
+ content += "\n"
1618
+
1619
+ # ๊ฐ€์น˜ ํ‘œ์‹œ (BenefitValue ๊ตฌ์กฐ)
1620
+ value = benefit.get("value", {})
1621
+ if isinstance(value, dict):
1622
+ v_type = value.get("type")
1623
+ if v_type == "MONEY" and value.get("money"):
1624
+ money = value.get("money", {})
1625
+ content += f" ๊ฐ€์น˜: {money.get('amount', 'N/A')} {money.get('currency', '')}\n"
1626
+ elif v_type == "TEXT" and value.get("text"):
1627
+ content += f" ๊ฐ€์น˜: {value.get('text')}\n"
1628
+ elif v_type == "PERCENT" and value.get("percent"):
1629
+ content += f" ๊ฐ€์น˜: {value.get('percent')}%\n"
1630
+
1631
  elif isinstance(benefit, str):
1632
+ content += f" - {benefit}\n"
1633
+
1634
+ content += DISCLAIMER_SHORT
1635
 
1636
  chunks.append({
1637
  "content": content.strip(),
 
1650
  # ===========================================================================
1651
 
1652
  def handle_member_rates(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
1653
+ """member_rates ์ฒ˜๋ฆฌ (ํšŒ์› ์ „์šฉ ์š”๊ธˆ) - ์Šคํ‚ค๋งˆ MemberRate ํ•„๋“œ ๊ตฌ์กฐ์— ๋งž์ถค"""
1654
  chunks = []
1655
  chain = context.get("chain", "UNKNOWN")
1656
+
1657
  if data is None:
1658
  return chunks
1659
+
1660
  rates = data if isinstance(data, list) else [data]
1661
+
1662
  for rate in rates[:5]:
1663
  if not isinstance(rate, dict):
1664
  continue
1665
+
1666
  rate_name = rate.get("rate_name", rate.get("name", "N/A"))
1667
+
1668
  content = f"""
1669
  ํšŒ์› ์š”๊ธˆ: {rate_name}
1670
  ์ฒด์ธ: {rate.get('chain', chain)}
 
 
1671
  """
1672
+
1673
+ # ํ• ์ธ์œจ (์Šคํ‚ค๋งˆ ๊ตฌ์กฐ ๋ฐ˜์˜)
1674
+ weekday_discount = rate.get("weekday_discount_percent")
1675
+ weekend_discount = rate.get("weekend_discount_percent")
1676
+ min_discount = rate.get("minimum_discount_percent")
1677
+ max_discount = rate.get("maximum_discount_percent")
1678
+
1679
+ # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๊ธฐ์กด ํ•„๋“œ๋„ ์ง€์›
1680
+ legacy_discount = rate.get("discount_percentage", rate.get("discount"))
1681
+
1682
+ if weekday_discount or weekend_discount:
1683
+ content += "ํ• ์ธ์œจ:\n"
1684
+ if weekday_discount:
1685
+ content += f" - ํ‰์ผ: {weekday_discount}%\n"
1686
+ if weekend_discount:
1687
+ content += f" - ์ฃผ๋ง: {weekend_discount}%\n"
1688
+ elif min_discount or max_discount:
1689
+ content += f"ํ• ์ธ์œจ: {min_discount or '?'}% ~ {max_discount or '?'}%\n"
1690
+ elif legacy_discount and legacy_discount != "N/A":
1691
+ content += f"ํ• ์ธ์œจ: {legacy_discount}%\n"
1692
+
1693
+ # ํ•„์ˆ˜ ๋ฉค๋ฒ„์‹ญ
1694
+ if rate.get("requires_membership", True):
1695
+ content += "๋ฉค๋ฒ„์‹ญ ํ•„์š”: ์˜ˆ\n"
1696
+ if rate.get("minimum_tier"):
1697
+ content += f"์ตœ์†Œ ๋“ฑ๊ธ‰: {rate.get('minimum_tier')}\n"
1698
+
1699
+ # ์ ์šฉ ๊ฐ์‹ค
1700
+ applies_to = rate.get("applies_to_room_types", [])
1701
+ excluded = rate.get("excluded_room_types", [])
1702
+ if applies_to:
1703
+ content += f"์ ์šฉ ๊ฐ์‹ค: {', '.join(applies_to)}\n"
1704
+ if excluded:
1705
+ content += f"์ œ์™ธ ๊ฐ์‹ค: {', '.join(excluded)}\n"
1706
+
1707
+ # ์˜ˆ์•ฝ ์ฑ„๋„
1708
+ channels = rate.get("eligible_channels", [])
1709
+ if channels:
1710
+ content += f"์˜ˆ์•ฝ ์ฑ„๋„: {', '.join(channels)}\n"
1711
+
1712
+ # ์ œํ•œ์‚ฌํ•ญ
1713
+ max_rooms = rate.get("max_rooms")
1714
+ if max_rooms:
1715
+ content += f"์ตœ๋Œ€ ๊ฐ์‹ค: {max_rooms}์‹ค\n"
1716
+
1717
+ excluded_rate_types = rate.get("excluded_rate_types", [])
1718
+ if excluded_rate_types:
1719
+ content += f"์ œ์™ธ ์š”๊ธˆ ์œ ํ˜•: {', '.join(excluded_rate_types)}\n"
1720
+
1721
+ excluded_hotels = rate.get("excluded_hotels", [])
1722
+ if excluded_hotels:
1723
+ content += f"์ œ์™ธ ํ˜ธํ…”: {', '.join(excluded_hotels[:3])}\n"
1724
+
1725
+ # ์ค‘๋ณต ์ ์šฉ
1726
+ if rate.get("combinable_with_promotions"):
1727
+ content += "๋‹ค๋ฅธ ํ”„๋กœ๋ชจ์…˜๊ณผ ์ค‘๋ณต ๊ฐ€๋Šฅ\n"
1728
+
1729
+ # ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: ๊ธฐ์กด ํ•„๋“œ๋“ค
1730
+ legacy_conditions = rate.get("conditions", rate.get("eligibility"))
1731
+ if legacy_conditions and legacy_conditions != "N/A":
1732
+ content += f"์ ์šฉ ์กฐ๊ฑด: {legacy_conditions}\n"
1733
+
1734
  if rate.get("description"):
1735
  content += f"์„ค๋ช…: {rate.get('description')}\n"
1736
+
1737
  chunks.append({
1738
  "content": content.strip(),
1739
  "metadata": {
 
1742
  "chain": rate.get("chain", chain)
1743
  }
1744
  })
1745
+
1746
  return chunks
1747
 
1748
 
 
2065
  if min_spend:
2066
  content += f" - ์ตœ์†Œ ์†Œ๋น„ ๊ธˆ์•ก: ${min_spend}\n"
2067
 
2068
+ # ํ”„๋กœ๊ทธ๋žจ ์ถ”๊ฐ€ ์ •๋ณด (์Šคํ‚ค๋งˆ ํ•„๋“œ ๊ธฐ๋ฐ˜)
2069
+ # ์ฐธ์—ฌ ๋งค์žฅ ์ •๋ณด
2070
+ participating_outlets = program.get("participating_outlets")
2071
+ if participating_outlets:
2072
+ content += f"\n์ฐธ์—ฌ ๋งค์žฅ ์ˆ˜: {participating_outlets}๊ฐœ\n"
2073
+
2074
+ outlet_types = program.get("outlet_types", [])
2075
+ if outlet_types:
2076
+ content += f"๋งค์žฅ ์œ ํ˜•: {', '.join(outlet_types)}\n"
2077
+
2078
+ # ์ œํ•œ์‚ฌํ•ญ
2079
+ restrictions_added = False
2080
+ max_group = program.get("max_group_size")
2081
+ if max_group:
2082
+ if not restrictions_added:
2083
+ content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
2084
+ restrictions_added = True
2085
+ content += f" - ์ตœ๋Œ€ ๊ทธ๋ฃน ํฌ๊ธฐ: {max_group}๋ช…\n"
2086
+
2087
+ split_bills = program.get("split_bills_allowed")
2088
+ if split_bills is not None:
2089
+ if not restrictions_added:
2090
+ content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
2091
+ restrictions_added = True
2092
+ content += f" - ๋ถ„ํ•  ๊ฒฐ์ œ: {'ํ—ˆ์šฉ' if split_bills else '๋ถˆํ—ˆ์šฉ'}\n"
2093
+
2094
+ member_present = program.get("member_must_be_present")
2095
+ if member_present is not None:
2096
+ if not restrictions_added:
2097
  content += "\n์ œํ•œ์‚ฌํ•ญ:\n"
2098
+ restrictions_added = True
2099
+ content += f" - ํšŒ์› ๋ณธ์ธ ์ฐธ์„: {'ํ•„์ˆ˜' if member_present else '๋ถˆํ•„์š”'}\n"
2100
+
2101
+ # ์ œ์™ธ์‚ฌํ•ญ
2102
+ exclusions = program.get("exclusions", [])
2103
+ if exclusions:
2104
+ content += f"\n์ œ์™ธ์‚ฌํ•ญ: {', '.join(exclusions[:3])}\n"
 
 
 
 
 
 
 
 
 
 
2105
 
2106
  content += DISCLAIMER_SHORT
2107
 
 
2194
  def handle_airline_tiers(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
2195
  """airline_tiers ์ฒ˜๋ฆฌ"""
2196
  chunks = []
2197
+
2198
+ if data is None:
2199
+ return chunks
2200
+
2201
  tiers = data if isinstance(data, list) else [data]
2202
 
2203
  for tier in tiers[:10]:
 
2206
 
2207
  tier_name = tier.get("tier_name", "N/A")
2208
  airline = tier.get("airline", context.get("chain", "UNKNOWN"))
2209
+
2210
+ # f-string ์˜ค๋ฅ˜ ์ˆ˜์ •: miles_required ์ฒ˜๋ฆฌ
2211
+ miles_req = tier.get("miles_required")
2212
+ miles_str = f"{miles_req:,}" if isinstance(miles_req, int) else "N/A"
2213
+
2214
  content = f"""
2215
  ํ•ญ๊ณต ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰: {tier_name}
2216
  ํ•ญ๊ณต์‚ฌ: {airline}
2217
  ๋“ฑ๊ธ‰ ์ˆœ์œ„: {tier.get("rank", "N/A")}
2218
 
2219
  ์ž๊ฒฉ ์š”๊ฑด:
2220
+ - ํ•„์š” ๋งˆ์ผ๋ฆฌ์ง€: {miles_str}
2221
  - ํ•„์š” ๊ตฌ๊ฐ„: {tier.get("segments_required", "N/A")}
2222
  - ์‚ฐ์ • ๊ธฐ๊ฐ„: {tier.get("qualification_period", "calendar_year")}
2223
  """
 
2330
 
2331
 
2332
  def handle_airline_earning_rules(data: Any, context: Dict[str, Any]) -> List[Dict[str, Any]]:
2333
+ """airline_earning_rules ์ฒ˜๋ฆฌ (์Šคํ‚ค๋งˆ AirlineEarningRule ํ•„๋“œ ๊ตฌ์กฐ์— ๋งž์ถค)"""
2334
  chunks = []
2335
+
2336
  rules = data if isinstance(data, list) else [data]
2337
  airline = context.get("chain", "UNKNOWN")
2338
+
2339
  # ๊ทœ์น™๋“ค์„ ํ•˜๋‚˜์˜ ์ฒญํฌ๋กœ ๋ฌถ๊ธฐ
2340
  if not rules:
2341
  return chunks
2342
+
2343
  content = f"""
2344
  ๋งˆ์ผ๋ฆฌ์ง€ ์ ๋ฆฝ ๊ทœ์น™
2345
  ํ•ญ๊ณต์‚ฌ: {airline}
2346
 
2347
  """
2348
+
2349
  for rule in rules[:15]:
2350
  if not isinstance(rule, dict):
2351
  continue
2352
+
2353
  rule_name = rule.get("rule_name", "N/A")
2354
  cabin = rule.get("cabin_class", "")
2355
+
 
 
2356
  content += f"โ€ข {rule_name}\n"
2357
  if cabin:
2358
  content += f" - ๊ฐ์‹ค: {cabin}\n"
2359
+
2360
+ # ๋…ธ์„  ์œ ํ˜•
2361
+ route = rule.get("route_type", "")
2362
  if route:
2363
  content += f" - ๋…ธ์„ : {route}\n"
2364
+
2365
+ # ์ ๋ฆฝ๋ฅ  (์Šคํ‚ค๋งˆ ํ•„๋“œ ๋ฐ˜์˜)
2366
+ earning_rate = rule.get("earning_rate_percent")
2367
+ base_miles_type = rule.get("base_miles_type", "FLOWN")
2368
+ fixed_miles = rule.get("fixed_miles")
2369
+
2370
+ if earning_rate:
2371
+ content += f" - ์ ๋ฆฝ๋ฅ : {earning_rate}%\n"
2372
+ elif base_miles_type == "FIXED" and fixed_miles:
2373
+ content += f" - ๊ณ ์ • ๋งˆ์ผ: {fixed_miles:,}๋งˆ์ผ\n"
2374
+
2375
+ # ์˜ˆ์•ฝ ํด๋ž˜์Šค
2376
+ booking_classes = rule.get("booking_classes", [])
2377
+ if booking_classes:
2378
+ content += f" - ์˜ˆ์•ฝ ํด๋ž˜์Šค: {', '.join(booking_classes)}\n"
2379
+
2380
+ # ๋ณด๋„ˆ์Šค ๋งˆ์ผ
2381
+ bonus_miles = rule.get("bonus_miles")
2382
+ if bonus_miles:
2383
+ content += f" - ๋ณด๋„ˆ์Šค: +{bonus_miles:,}๋งˆ์ผ\n"
2384
+
2385
+ # ํ•„์š” ๋“ฑ๊ธ‰
2386
+ req_tier = rule.get("requires_membership_tier")
2387
+ if req_tier:
2388
+ content += f" - ํ•„์š” ๋“ฑ๊ธ‰: {req_tier}\n"
2389
+
2390
+ # ์œ ํšจ ๊ธฐ๊ฐ„
2391
+ validity = rule.get("validity")
2392
+ if validity and isinstance(validity, dict):
2393
+ start = validity.get("start_date")
2394
+ end = validity.get("end_date")
2395
+ if start or end:
2396
+ content += f" - ์œ ํšจ ๊ธฐ๊ฐ„: {start or '?'} ~ {end or '?'}\n"
2397
+
2398
+ # ์ง€์—ญ
2399
+ region = rule.get("region")
2400
+ if region and region != "GLOBAL":
2401
+ content += f" - ์ ์šฉ ์ง€์—ญ: {region}\n"
2402
+
2403
  content += "\n"
2404
+
2405
  content += DISCLAIMER_SHORT
2406
+
2407
  if len(content) > 100:
2408
  chunks.append({
2409
  "content": content.strip(),
 
2412
  "airline": airline
2413
  }
2414
  })
2415
+
2416
  return chunks
2417
 
2418
 
 
2609
 
2610
  # ๋ฉค๋ฒ„์‹ญ ๋“ฑ๊ธ‰
2611
  "membership_tiers": handle_membership_tiers,
2612
+
2613
+ # ๋งˆ์ผ์Šคํ†ค ํ”„๋กœ๊ทธ๋žจ
2614
+ "milestone_program": handle_milestone_programs,
2615
+ "milestone_programs": handle_milestone_programs,
2616
+ "milestones": handle_milestone_programs,
2617
+
2618
+ # ํŒŒํŠธ๋„ˆ ๋“ฑ๊ธ‰ ๋งค์นญ
2619
+ "partner_status_matches": handle_partner_status_matches,
2620
+ "partner_status_match": handle_partner_status_matches,
2621
 
2622
  # ํฌ์ธํŠธ ์‹œ์Šคํ…œ
2623
  "points_systems": handle_points_systems,
 
2625
 
2626
  # ํ˜ธํ…”
2627
  "hotel_properties": handle_hotel_properties,
2628
+ "hotel_brands": handle_hotel_brands,
2629
 
2630
  # ๋“ฑ๊ธ‰๋ณ„ ๊ตฌํ˜„
2631
  "tier_implementations": handle_tier_implementations,
 
2784
  "facts",
2785
 
2786
  # Core ํ‚ค์— ํฌํ•จ๋˜๋ฏ€๋กœ ๋ณ„๋„ ์ฒ˜๋ฆฌ ๋ถˆํ•„์š”
2787
+ # "hotel_brands", # ์ด์ œ handle_hotel_brands๋กœ ์ฒ˜๋ฆฌ
2788
  "nearby_attractions", # extra_attributes์— ํฌํ•จ
2789
 
2790
  # ๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ
scripts/sync_to_supabase.py CHANGED
@@ -77,12 +77,24 @@ def create_chunks_from_knowledge(
77
  name_localized = first_hotel.get("name_localized", {})
78
  hotel_name_ko = name_localized.get("ko") if isinstance(name_localized, dict) else None
79
 
 
 
 
 
 
 
 
 
 
80
  # ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
81
  context = {
82
  "chain": chain,
83
  "hotel_name": hotel_name,
84
  "hotel_name_ko": hotel_name_ko,
85
  "doc_id": doc_id,
 
 
 
86
  }
87
 
88
  def add_chunk(content: str, metadata: Dict[str, Any]):
@@ -327,7 +339,23 @@ def main(chain: Optional[str], domain: str, dry_run: bool, skip_embeddings: bool
327
  extracted_knowledge = data.get("extracted_knowledge")
328
  if not extracted_knowledge or not isinstance(extracted_knowledge, dict):
329
  # extracted_knowledge๊ฐ€ ์—†์œผ๋ฉด data ์ž์ฒด๊ฐ€ knowledge์ผ ์ˆ˜ ์žˆ์Œ
330
- if "hotel_properties" in data or "loyalty_programs" in data or "membership_tiers" in data or "tier_implementations" in data:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  extracted_knowledge = data
332
  else:
333
  if verbose:
@@ -336,7 +364,21 @@ def main(chain: Optional[str], domain: str, dry_run: bool, skip_embeddings: bool
336
  continue
337
 
338
  # ๋ฌธ์„œ ID ์ƒ์„ฑ
339
- rel_path = str(md_file.relative_to(data_dir.parent.parent))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  doc_id = generate_doc_id(rel_path)
341
 
342
  # ์ฒญํฌ ์ƒ์„ฑ
 
77
  name_localized = first_hotel.get("name_localized", {})
78
  hotel_name_ko = name_localized.get("ko") if isinstance(name_localized, dict) else None
79
 
80
+ # identity ์„น์…˜์—์„œ ์ถ”๊ฐ€ ์ •๋ณด ์ถ”์ถœ (ํ”„๋กฌํ”„ํŠธ ์ถœ๋ ฅ ํ˜ธํ™˜์„ฑ)
81
+ identity = extracted_knowledge.get("identity", {})
82
+ if identity and isinstance(identity, dict):
83
+ # identity์—์„œ ์ •๋ณด ๋ณด๊ฐ•
84
+ if identity.get("title") and hotel_name == "Unknown Hotel":
85
+ hotel_name = identity.get("title")
86
+ if identity.get("chain"):
87
+ chain = identity.get("chain", chain)
88
+
89
  # ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
90
  context = {
91
  "chain": chain,
92
  "hotel_name": hotel_name,
93
  "hotel_name_ko": hotel_name_ko,
94
  "doc_id": doc_id,
95
+ # identity ์ถ”๊ฐ€ ์ •๋ณด
96
+ "document_category": identity.get("category") if identity else None,
97
+ "document_type": identity.get("document_type") if identity else None,
98
  }
99
 
100
  def add_chunk(content: str, metadata: Dict[str, Any]):
 
339
  extracted_knowledge = data.get("extracted_knowledge")
340
  if not extracted_knowledge or not isinstance(extracted_knowledge, dict):
341
  # extracted_knowledge๊ฐ€ ์—†์œผ๋ฉด data ์ž์ฒด๊ฐ€ knowledge์ผ ์ˆ˜ ์žˆ์Œ
342
+ # ๋‹ค์–‘ํ•œ ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ํ‚ค๋“ค์„ ์ฒดํฌ
343
+ core_keys = {
344
+ # ํ˜ธํ…”
345
+ "hotel_properties", "loyalty_programs", "loyalty_program",
346
+ "membership_tiers", "tier_implementations", "hotel_brands",
347
+ "best_rate_guarantee", "channel_benefit_packages",
348
+ # ํ•ญ๊ณต
349
+ "airline_programs", "airline_program", "airline_tiers",
350
+ "award_charts", "airline_earning_rules",
351
+ # ์นด๋“œ
352
+ "credit_cards",
353
+ # ํ”„๋กœ๋ชจ์…˜/๋‰ด์Šค
354
+ "deal_alerts", "news_updates", "promotions",
355
+ # ๊ธฐํƒ€
356
+ "points_systems", "member_rates", "dining_programs",
357
+ }
358
+ if any(key in data for key in core_keys):
359
  extracted_knowledge = data
360
  else:
361
  if verbose:
 
364
  continue
365
 
366
  # ๋ฌธ์„œ ID ์ƒ์„ฑ
367
+ # --file ์˜ต์…˜ ์‚ฌ์šฉ ์‹œ data_dir๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํŒŒ์ผ ๊ฒฝ๋กœ์—์„œ ์ง์ ‘ ๊ณ„์‚ฐ
368
+ try:
369
+ # data/raw ๊ธฐ์ค€์œผ๋กœ ์ƒ๋Œ€ ๊ฒฝ๋กœ ๊ณ„์‚ฐ
370
+ data_raw = Path("data/raw")
371
+ if md_file.is_relative_to(data_raw):
372
+ rel_path = str(md_file.relative_to(data_raw.parent))
373
+ elif "data/raw" in str(md_file):
374
+ # ์ ˆ๋Œ€ ๊ฒฝ๋กœ์ธ ๊ฒฝ์šฐ data/raw ์ดํ›„ ๋ถ€๋ถ„ ์ถ”์ถœ
375
+ path_str = str(md_file)
376
+ idx = path_str.find("data/raw")
377
+ rel_path = path_str[idx:] if idx >= 0 else str(md_file.name)
378
+ else:
379
+ rel_path = str(md_file)
380
+ except Exception:
381
+ rel_path = str(md_file)
382
  doc_id = generate_doc_id(rel_path)
383
 
384
  # ์ฒญํฌ ์ƒ์„ฑ