benjamin5607 commited on
Commit
08f05d6
·
verified ·
1 Parent(s): 3b6db63

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -52
app.py CHANGED
@@ -41,21 +41,31 @@ html_code = f"""
41
  :root {{ --gold: #d4af37; --dark: #0a0a0a; --card-bg: #1c1c1c; --text: #e0e0e0; --accent: #c5a059; }}
42
  body {{ margin: 0; padding: 0; background: var(--dark); color: var(--text); font-family: 'Segoe UI', sans-serif; overflow: hidden; }}
43
 
 
 
 
 
44
  #main-page {{
45
- position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
46
  background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.95)), url('https://images.unsplash.com/photo-1516981879613-9f5da904015f?q=80&w=2560&auto=format&fit=crop');
47
  background-size: cover; background-position: center;
48
- z-index: 5000; display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
49
- padding-top: 20px; box-sizing: border-box;
50
- transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
51
  }}
52
 
53
- .lang-switcher {{ position: absolute; top: 20px; right: 20px; z-index: 6000; display: flex; gap: 8px; }}
54
  .lang-btn {{ background: rgba(0,0,0,0.5); border: 1px solid #555; color: #aaa; padding: 5px 10px; border-radius: 15px; cursor: pointer; font-size: 12px; font-weight: bold; transition: all 0.2s; }}
55
  .lang-btn.active {{ background: var(--gold); color: black; border-color: var(--gold); }}
56
 
57
  .brand-title {{ font-size: 24px; font-weight: 900; color: var(--gold); letter-spacing: 4px; margin: 20px 0 0 0; text-transform: uppercase; }}
58
- .brand-subtitle {{ font-size: 11px; color: #ccc; letter-spacing: 2px; margin-bottom: 20px; }}
 
 
 
 
 
 
 
 
59
 
60
  .country-select-wrapper {{ width: 80%; max-width: 400px; margin-bottom: 15px; }}
61
  .country-select {{
@@ -77,18 +87,16 @@ html_code = f"""
77
  .pc-name {{ font-size: 16px; font-weight: bold; color: white; }}
78
  .pc-desc {{ font-size: 12px; color: #bbb; line-height: 1.4; margin-top: 5px; margin-bottom: 15px; }}
79
 
80
- /* 🔥 Twin Action Buttons */
81
  .card-actions {{ display: flex; gap: 8px; margin-top: auto; }}
82
  .btn-action {{ flex: 1; padding: 10px 0; border-radius: 8px; font-size: 12px; font-weight: bold; cursor: pointer; text-align: center; transition: all 0.2s; border: none; }}
83
  .btn-ai {{ background: var(--gold); color: black; }}
84
- .btn-ai:hover {{ background: #b89530; }}
85
  .btn-map {{ background: #333; color: white; border: 1px solid #555; }}
86
- .btn-map:hover {{ background: #444; border-color: white; }}
87
 
88
- #detail-page {{ position: absolute; top: 0; left: 0; width: 100%; height: 100vh; transform: translateX(100%); transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); z-index: 6000; display: flex; flex-direction: column; background: #111; }}
 
89
  .nav-bar {{ height: 50px; background: rgba(0,0,0,0.9); border-bottom: 1px solid #333; display: flex; align-items: center; padding: 0 15px; justify-content: space-between; }}
90
  .back-btn {{ background: none; border: none; color: var(--gold); font-size: 24px; cursor: pointer; }}
91
- #map {{ width: 100%; height: 35%; border-bottom: 2px solid var(--gold); }}
92
  .report-container {{ flex: 1; overflow-y: auto; padding: 25px 20px 80px 20px; }}
93
  .report-title {{ font-size: 24px; color: var(--gold); font-weight: bold; margin-bottom: 5px; }}
94
  .report-meta {{ font-size: 12px; color: #666; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }}
@@ -96,6 +104,18 @@ html_code = f"""
96
  .gmap-btn {{ display: block; width: 90%; margin: 0 auto; padding: 15px 0; background: var(--gold); color: black; text-align: center; font-weight: bold; text-decoration: none; border-radius: 30px; position: fixed; bottom: 20px; left: 5%; z-index: 7000; }}
97
  .gmap-btn.naver {{ background: #03C75A !important; color: white !important; }}
98
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  .loading-screen {{ position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 8000; display: none; flex-direction: column; justify-content: center; align-items: center; }}
100
  .loader-ring {{ width: 40px; height: 40px; border: 3px solid #333; border-top: 3px solid var(--gold); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }}
101
  .loader-text {{ color: var(--gold); font-family: monospace; font-size: 13px; text-align: center; }}
@@ -104,7 +124,7 @@ html_code = f"""
104
  </head>
105
  <body>
106
 
107
- <div id="main-page">
108
  <div class="lang-switcher">
109
  <div class="lang-btn active" onclick="setLanguage('KO', this)">🇰🇷</div>
110
  <div class="lang-btn" onclick="setLanguage('EN', this)">🇺🇸</div>
@@ -115,6 +135,7 @@ html_code = f"""
115
  <div style="text-align:center;">
116
  <div class="brand-title">DRUNKEN PLANE</div>
117
  <div id="app-subtitle" class="brand-subtitle">GLOBAL ALCOHOL HERITAGE TOUR</div>
 
118
  </div>
119
 
120
  <div class="country-select-wrapper">
@@ -134,13 +155,13 @@ html_code = f"""
134
  <div class="place-list-container" id="place-list"></div>
135
  </div>
136
 
137
- <div id="detail-page">
138
  <div class="nav-bar">
139
- <button class="back-btn" onclick="goBack()">←</button>
140
  <div id="report-header" style="color:white; font-weight:bold;">TRAVEL REPORT</div>
141
  <div style="width:24px;"></div>
142
  </div>
143
- <div id="map"></div>
144
  <div class="report-container">
145
  <div id="repo-title" class="report-title"></div>
146
  <div id="repo-meta" class="report-meta"></div>
@@ -149,6 +170,16 @@ html_code = f"""
149
  <a id="gmap-link" href="#" target="_blank" class="gmap-btn">🗺️ Google Maps</a>
150
  </div>
151
 
 
 
 
 
 
 
 
 
 
 
152
  <div id="loader" class="loading-screen">
153
  <div class="loader-ring"></div>
154
  <div id="loader-msg" class="loader-text"></div>
@@ -157,25 +188,28 @@ html_code = f"""
157
  <script>
158
  const PLACE_DB = {db_json};
159
 
160
- let map = null;
161
- let currentMarker = null;
 
 
 
162
  let selectedCountry = "";
163
  let selectedType = "All";
164
  let currentLang = "KO";
165
  let loadingInterval = null;
166
 
167
  const LOADING_MSGS = {{
168
- "KO": ["AI가 비행기 타고 이동 중... ✈️", "가게 문을 열고 들어갑니다... 🚪", "주변 분위기를 스캔하는 중... 👀", "시그니처 메뉴를 시음 중... 🍷", "사장님과 인터뷰 중... 🗣️", "취하기 전에 리포트 작성 중... 📝"],
169
- "EN": ["Flying to location... ✈️", "Entering the venue... 🚪", "Scanning vibes... 👀", "Tasting signature drinks... 🍷", "Interviewing staff... 🗣️", "Writing report... 📝"],
170
- "JP": ["現地へ移動中... ✈️", "入店しています... 🚪", "雰囲気をスキャン中... 👀", "試飲中... 🍷", "レポート作成中... 📝"],
171
  "CN": ["正在前往... ✈️", "正在进店... 🚪", "扫描氛围... 👀", "品尝美酒... 🍷", "撰写报告... 📝"]
172
  }};
173
 
174
  const TRANSLATIONS = {{
175
- "KO": {{ "subtitle": "글로벌 주류 헤리티지 투어", "filters": {{ "All": "전체", "Whiskey": "위스키", "Spirits": "증류주", "Beer": "맥주", "Wine": "와인", "Traditional": "전통주/토속주", "Bar": "바/펍" }}, "report_header": "여행 리포트", "gmap": "구글맵", "nmap": "네이버 지도", "cta_ai": "🤖 AI 리뷰", "cta_map": "🗺️ 길찾기", "ai_error": "오프라인 정보로 대체합니다." }},
176
- "EN": {{ "subtitle": "GLOBAL ALCOHOL HERITAGE TOUR", "filters": {{ "All": "All", "Whiskey": "Whiskey", "Spirits": "Spirits", "Beer": "Beer", "Wine": "Wine", "Traditional": "Traditional", "Bar": "Bar" }}, "report_header": "TRAVEL REPORT", "gmap": "Google Maps", "nmap": "Naver Map", "cta_ai": "🤖 AI Review", "cta_map": "🗺️ Map", "ai_error": "Using offline mode." }},
177
- "JP": {{ "subtitle": "世界のお酒ヘリテージツアー", "filters": {{ "All": "すべて", "Whiskey": "ウイスキー", "Spirits": "蒸留酒", "Beer": "ビール", "Wine": "ワイン", "Traditional": "伝統酒", "Bar": "バー" }}, "report_header": "旅行レポート", "gmap": "Googleマップ", "nmap": "Naverマップ", "cta_ai": "🤖 AIレビュー", "cta_map": "🗺️ 地図", "ai_error": "オフラインモードで実行します。" }},
178
- "CN": {{ "subtitle": "全球酒类遗产之旅", "filters": {{ "All": "全部", "Whiskey": "威士忌", "Spirits": "烈酒", "Beer": "啤酒", "Wine": "葡萄酒", "Traditional": "传统酒", "Bar": "酒吧" }}, "report_header": "旅游报告", "gmap": "谷歌地图", "nmap": "Naver地图", "cta_ai": "🤖 AI 评论", "cta_map": "🗺️ 地图", "ai_error": "以离线模式运行。" }}
179
  }};
180
 
181
  window.onload = function() {{
@@ -199,7 +233,6 @@ html_code = f"""
199
  const t = TRANSLATIONS[currentLang];
200
  document.getElementById('app-subtitle').innerText = t.subtitle;
201
  document.getElementById('report-header').innerText = t.report_header;
202
-
203
  document.getElementById('filter-all').innerText = `🌐 ${{t.filters.All}}`;
204
  document.getElementById('filter-whiskey').innerText = `🥃 ${{t.filters.Whiskey}}`;
205
  document.getElementById('filter-spirits').innerText = `🍸 ${{t.filters.Spirits}}`;
@@ -232,9 +265,8 @@ html_code = f"""
232
  renderList();
233
  }}
234
 
235
- // 🔥 Helper: Get Map URL based on location
236
  function getMapUrl(place) {{
237
- const isKorea = selectedCountry.includes("Korea");
238
  let query = "";
239
  if (isKorea && place.real_addr) {{
240
  query = encodeURIComponent(place.real_addr);
@@ -260,15 +292,12 @@ html_code = f"""
260
  }}
261
 
262
  const t = TRANSLATIONS[currentLang];
263
-
264
  filtered.forEach(place => {{
265
  const card = document.createElement('div');
266
  card.className = 'place-card';
267
-
268
  let desc = currentLang === 'KO' ? place.desc_ko : place.desc_en;
269
  if(currentLang === 'JP' || currentLang === 'CN') desc = place.desc_en;
270
 
271
- // 🔥 Twin Action Logic (AI vs Map)
272
  card.innerHTML = `
273
  <div class="card-header">
274
  <div class="pc-name">${{place.name}}</div>
@@ -276,7 +305,7 @@ html_code = f"""
276
  </div>
277
  <div class="pc-desc">${{desc}}</div>
278
  <div class="card-actions">
279
- <button class="btn-action btn-ai" onclick="openDetail('${{place.name}}')">${{t.cta_ai}}</button>
280
  <button class="btn-action btn-map" onclick="window.open('${{getMapUrl(place)}}', '_blank')">${{t.cta_map}}</button>
281
  </div>
282
  `;
@@ -284,8 +313,85 @@ html_code = f"""
284
  }});
285
  }}
286
 
287
- async function openDetail(placeName) {{
288
- const place = PLACE_DB[selectedCountry].find(p => p.name === placeName);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  if(!place) return;
290
 
291
  document.getElementById('main-page').style.transform = 'translateX(-30%)';
@@ -294,29 +400,44 @@ html_code = f"""
294
  startLoadingAnimation();
295
 
296
  document.getElementById('repo-title').innerText = place.name;
297
- document.getElementById('repo-meta').innerText = `${{place.label}} • ${{selectedCountry}}`;
298
 
299
- // Set Map Button in Detail View
300
  const btn = document.getElementById('gmap-link');
301
- const isKorea = selectedCountry.includes("Korea");
302
- btn.href = getMapUrl(place);
303
- btn.innerText = isKorea ? TRANSLATIONS[currentLang].nmap : TRANSLATIONS[currentLang].gmap;
304
- if(isKorea) btn.classList.add("naver"); else btn.classList.remove("naver");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
  document.getElementById('repo-content').innerHTML = "";
307
 
308
- if (!map) {{
309
- map = L.map('map');
310
- L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: '© OpenStreetMap' }}).addTo(map);
311
  }}
312
  setTimeout(() => {{
313
- map.invalidateSize();
314
- map.setView([place.lat, place.lon], 15);
315
- if (currentMarker) map.removeLayer(currentMarker);
316
- currentMarker = L.marker([place.lat, place.lon]).addTo(map).bindPopup(place.name).openPopup();
317
  }}, 300);
318
 
319
- // 🔥 RAG Logic
320
  let contextDesc = "";
321
  if (place.real_addr) {{
322
  contextDesc = `Address: ${{place.real_addr}}, Vibe: ${{place.real_vibe}}, Drink: ${{place.real_drink}}, Tip: ${{place.real_tip}}`;
@@ -324,7 +445,7 @@ html_code = f"""
324
  contextDesc = currentLang === 'KO' ? place.desc_ko : place.desc_en;
325
  }}
326
 
327
- await generatePuterReport(place, contextDesc);
328
  }}
329
 
330
  function startLoadingAnimation() {{
@@ -345,14 +466,14 @@ html_code = f"""
345
  document.getElementById('loader').style.display = 'none';
346
  }}
347
 
348
- async function generatePuterReport(place, contextDesc) {{
349
  let langFull = "Korean";
350
  if(currentLang === "EN") langFull = "English";
351
  if(currentLang === "JP") langFull = "Japanese";
352
  if(currentLang === "CN") langFull = "Chinese";
353
 
354
  const prompt = `
355
- Act as a travel expert. Write a detailed review about "${{place.name}}" in ${{selectedCountry}}.
356
 
357
  [Context Info]
358
  ${{contextDesc}}
@@ -396,7 +517,7 @@ html_code = f"""
396
  function generateLocalReport(place, dbDesc) {{
397
  stopLoadingAnimation();
398
  const report = `
399
- ✨ **[Offline Info] ${{TRANSLATIONS[currentLang].ai_error}}**
400
 
401
  **${{place.name}}**
402
 
@@ -407,7 +528,7 @@ html_code = f"""
407
  document.getElementById('repo-content').innerHTML = report.replace(/\\n/g, '<br>');
408
  }}
409
 
410
- function goBack() {{
411
  document.getElementById('detail-page').style.transform = 'translateX(100%)';
412
  document.getElementById('main-page').style.transform = 'translateX(0%)';
413
  }}
 
41
  :root {{ --gold: #d4af37; --dark: #0a0a0a; --card-bg: #1c1c1c; --text: #e0e0e0; --accent: #c5a059; }}
42
  body {{ margin: 0; padding: 0; background: var(--dark); color: var(--text); font-family: 'Segoe UI', sans-serif; overflow: hidden; }}
43
 
44
+ /* --- Common --- */
45
+ .page {{ position: absolute; top: 0; left: 0; width: 100%; height: 100vh; transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1); box-sizing: border-box; }}
46
+
47
+ /* --- Main Page --- */
48
  #main-page {{
 
49
  background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.95)), url('https://images.unsplash.com/photo-1516981879613-9f5da904015f?q=80&w=2560&auto=format&fit=crop');
50
  background-size: cover; background-position: center;
51
+ z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
52
+ padding-top: 20px;
 
53
  }}
54
 
55
+ .lang-switcher {{ position: absolute; top: 20px; right: 20px; z-index: 200; display: flex; gap: 8px; }}
56
  .lang-btn {{ background: rgba(0,0,0,0.5); border: 1px solid #555; color: #aaa; padding: 5px 10px; border-radius: 15px; cursor: pointer; font-size: 12px; font-weight: bold; transition: all 0.2s; }}
57
  .lang-btn.active {{ background: var(--gold); color: black; border-color: var(--gold); }}
58
 
59
  .brand-title {{ font-size: 24px; font-weight: 900; color: var(--gold); letter-spacing: 4px; margin: 20px 0 0 0; text-transform: uppercase; }}
60
+ .brand-subtitle {{ font-size: 11px; color: #ccc; letter-spacing: 2px; margin-bottom: 10px; }}
61
+
62
+ /* World Map Entry Button */
63
+ .world-map-btn {{
64
+ background: rgba(212, 175, 55, 0.2); border: 1px solid var(--gold); color: var(--gold);
65
+ padding: 10px 20px; border-radius: 30px; font-weight: bold; cursor: pointer; margin-bottom: 15px;
66
+ display: flex; align-items: center; gap: 8px; backdrop-filter: blur(5px);
67
+ }}
68
+ .world-map-btn:hover {{ background: var(--gold); color: black; }}
69
 
70
  .country-select-wrapper {{ width: 80%; max-width: 400px; margin-bottom: 15px; }}
71
  .country-select {{
 
87
  .pc-name {{ font-size: 16px; font-weight: bold; color: white; }}
88
  .pc-desc {{ font-size: 12px; color: #bbb; line-height: 1.4; margin-top: 5px; margin-bottom: 15px; }}
89
 
 
90
  .card-actions {{ display: flex; gap: 8px; margin-top: auto; }}
91
  .btn-action {{ flex: 1; padding: 10px 0; border-radius: 8px; font-size: 12px; font-weight: bold; cursor: pointer; text-align: center; transition: all 0.2s; border: none; }}
92
  .btn-ai {{ background: var(--gold); color: black; }}
 
93
  .btn-map {{ background: #333; color: white; border: 1px solid #555; }}
 
94
 
95
+ /* --- Detail Page --- */
96
+ #detail-page {{ background: #111; z-index: 300; transform: translateX(100%); display: flex; flex-direction: column; }}
97
  .nav-bar {{ height: 50px; background: rgba(0,0,0,0.9); border-bottom: 1px solid #333; display: flex; align-items: center; padding: 0 15px; justify-content: space-between; }}
98
  .back-btn {{ background: none; border: none; color: var(--gold); font-size: 24px; cursor: pointer; }}
99
+ #mini-map {{ width: 100%; height: 35%; border-bottom: 2px solid var(--gold); }}
100
  .report-container {{ flex: 1; overflow-y: auto; padding: 25px 20px 80px 20px; }}
101
  .report-title {{ font-size: 24px; color: var(--gold); font-weight: bold; margin-bottom: 5px; }}
102
  .report-meta {{ font-size: 12px; color: #666; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }}
 
104
  .gmap-btn {{ display: block; width: 90%; margin: 0 auto; padding: 15px 0; background: var(--gold); color: black; text-align: center; font-weight: bold; text-decoration: none; border-radius: 30px; position: fixed; bottom: 20px; left: 5%; z-index: 7000; }}
105
  .gmap-btn.naver {{ background: #03C75A !important; color: white !important; }}
106
 
107
+ /* --- World Map Page --- */
108
+ #world-map-page {{ background: #111; z-index: 200; transform: translateY(100%); display: flex; flex-direction: column; }}
109
+ .map-search-bar {{
110
+ position: absolute; top: 20px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 500px;
111
+ z-index: 1000; display: flex; gap: 10px; background: rgba(0,0,0,0.8); padding: 10px; border-radius: 12px; border: 1px solid #444;
112
+ }}
113
+ .map-input {{ flex: 1; background: transparent; border: none; color: white; font-size: 16px; outline: none; }}
114
+ .map-icon-btn {{ background: none; border: none; font-size: 20px; cursor: pointer; color: var(--gold); }}
115
+ .home-btn {{ background: #333; color: white; border: 1px solid #555; padding: 0 15px; border-radius: 8px; font-weight: bold; cursor: pointer; }}
116
+ #full-map {{ width: 100%; height: 100%; }}
117
+
118
+ /* --- Loader --- */
119
  .loading-screen {{ position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 8000; display: none; flex-direction: column; justify-content: center; align-items: center; }}
120
  .loader-ring {{ width: 40px; height: 40px; border: 3px solid #333; border-top: 3px solid var(--gold); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }}
121
  .loader-text {{ color: var(--gold); font-family: monospace; font-size: 13px; text-align: center; }}
 
124
  </head>
125
  <body>
126
 
127
+ <div id="main-page" class="page">
128
  <div class="lang-switcher">
129
  <div class="lang-btn active" onclick="setLanguage('KO', this)">🇰🇷</div>
130
  <div class="lang-btn" onclick="setLanguage('EN', this)">🇺🇸</div>
 
135
  <div style="text-align:center;">
136
  <div class="brand-title">DRUNKEN PLANE</div>
137
  <div id="app-subtitle" class="brand-subtitle">GLOBAL ALCOHOL HERITAGE TOUR</div>
138
+ <button class="world-map-btn" onclick="openWorldMapPage()">🗺️ WORLD MAP EXPLORER</button>
139
  </div>
140
 
141
  <div class="country-select-wrapper">
 
155
  <div class="place-list-container" id="place-list"></div>
156
  </div>
157
 
158
+ <div id="detail-page" class="page">
159
  <div class="nav-bar">
160
+ <button class="back-btn" onclick="closeDetail()">←</button>
161
  <div id="report-header" style="color:white; font-weight:bold;">TRAVEL REPORT</div>
162
  <div style="width:24px;"></div>
163
  </div>
164
+ <div id="mini-map"></div>
165
  <div class="report-container">
166
  <div id="repo-title" class="report-title"></div>
167
  <div id="repo-meta" class="report-meta"></div>
 
170
  <a id="gmap-link" href="#" target="_blank" class="gmap-btn">🗺️ Google Maps</a>
171
  </div>
172
 
173
+ <div id="world-map-page" class="page">
174
+ <div class="map-search-bar">
175
+ <button class="home-btn" onclick="closeWorldMapPage()">🏠 HOME</button>
176
+ <input type="text" id="map-search-input" class="map-input" placeholder="Search City (e.g. Gangnam, London)..." onkeydown="if(event.key === 'Enter') searchMapLocation()">
177
+ <button class="map-icon-btn" onclick="searchMapLocation()">🔍</button>
178
+ <button class="map-icon-btn" onclick="getMyLocation()">📍</button>
179
+ </div>
180
+ <div id="full-map"></div>
181
+ </div>
182
+
183
  <div id="loader" class="loading-screen">
184
  <div class="loader-ring"></div>
185
  <div id="loader-msg" class="loader-text"></div>
 
188
  <script>
189
  const PLACE_DB = {db_json};
190
 
191
+ let miniMap = null;
192
+ let miniMarker = null;
193
+ let worldMap = null;
194
+ let worldMarkers = L.layerGroup();
195
+
196
  let selectedCountry = "";
197
  let selectedType = "All";
198
  let currentLang = "KO";
199
  let loadingInterval = null;
200
 
201
  const LOADING_MSGS = {{
202
+ "KO": ["AI가 비행기 타고 이동 중... ✈️", "가게 문을 열고 들어갑니다... 🚪", "분위기 스캔 중... 👀", "시그니처 시음 중... 🍷", "사장님 인터뷰 중... 🗣️", "리포트 작성 중... 📝"],
203
+ "EN": ["Flying to location... ✈️", "Entering venue... 🚪", "Scanning vibes... 👀", "Tasting drinks... 🍷", "Interviewing staff... 🗣️", "Writing report... 📝"],
204
+ "JP": ["現地へ移動中... ✈️", "入店しています... 🚪", "雰囲気スキャン中... 👀", "試飲中... 🍷", "レポート作成中... 📝"],
205
  "CN": ["正在前往... ✈️", "正在进店... 🚪", "扫描氛围... 👀", "品尝美酒... 🍷", "撰写报告... 📝"]
206
  }};
207
 
208
  const TRANSLATIONS = {{
209
+ "KO": {{ "subtitle": "글로벌 주류 헤리티지 투어", "filters": {{ "All": "전체", "Whiskey": "위스키", "Spirits": "증류주", "Beer": "맥주", "Wine": "와인", "Traditional": "전통주/토속주", "Bar": "바/펍" }}, "report_header": "여행 리포트", "gmap": "구글맵", "nmap": "네이버 지도", "cta_ai": "🤖 AI 리뷰", "cta_map": "🗺️ 길찾기" }},
210
+ "EN": {{ "subtitle": "GLOBAL ALCOHOL HERITAGE TOUR", "filters": {{ "All": "All", "Whiskey": "Whiskey", "Spirits": "Spirits", "Beer": "Beer", "Wine": "Wine", "Traditional": "Traditional", "Bar": "Bar" }}, "report_header": "TRAVEL REPORT", "gmap": "Google Maps", "nmap": "Naver Map", "cta_ai": "🤖 AI Review", "cta_map": "🗺️ Map" }},
211
+ "JP": {{ "subtitle": "世界のお酒ヘリテージツアー", "filters": {{ "All": "すべて", "Whiskey": "ウイスキー", "Spirits": "蒸留酒", "Beer": "ビール", "Wine": "ワイン", "Traditional": "伝統酒", "Bar": "バー" }}, "report_header": "旅行レポート", "gmap": "Googleマップ", "nmap": "Naverマップ", "cta_ai": "🤖 AIレビュー", "cta_map": "🗺️ 地図" }},
212
+ "CN": {{ "subtitle": "全球酒类遗产之旅", "filters": {{ "All": "全部", "Whiskey": "威士忌", "Spirits": "烈酒", "Beer": "啤酒", "Wine": "葡萄酒", "Traditional": "传统酒", "Bar": "酒吧" }}, "report_header": "旅游报告", "gmap": "谷歌地图", "nmap": "Naver地图", "cta_ai": "🤖 AI 评论", "cta_map": "🗺️ 地图" }}
213
  }};
214
 
215
  window.onload = function() {{
 
233
  const t = TRANSLATIONS[currentLang];
234
  document.getElementById('app-subtitle').innerText = t.subtitle;
235
  document.getElementById('report-header').innerText = t.report_header;
 
236
  document.getElementById('filter-all').innerText = `🌐 ${{t.filters.All}}`;
237
  document.getElementById('filter-whiskey').innerText = `🥃 ${{t.filters.Whiskey}}`;
238
  document.getElementById('filter-spirits').innerText = `🍸 ${{t.filters.Spirits}}`;
 
265
  renderList();
266
  }}
267
 
 
268
  function getMapUrl(place) {{
269
+ const isKorea = selectedCountry.includes("Korea") || (place.name && place.desc_ko && place.real_addr); // Check if place belongs to Korea logic
270
  let query = "";
271
  if (isKorea && place.real_addr) {{
272
  query = encodeURIComponent(place.real_addr);
 
292
  }}
293
 
294
  const t = TRANSLATIONS[currentLang];
 
295
  filtered.forEach(place => {{
296
  const card = document.createElement('div');
297
  card.className = 'place-card';
 
298
  let desc = currentLang === 'KO' ? place.desc_ko : place.desc_en;
299
  if(currentLang === 'JP' || currentLang === 'CN') desc = place.desc_en;
300
 
 
301
  card.innerHTML = `
302
  <div class="card-header">
303
  <div class="pc-name">${{place.name}}</div>
 
305
  </div>
306
  <div class="pc-desc">${{desc}}</div>
307
  <div class="card-actions">
308
+ <button class="btn-action btn-ai" onclick="openDetail('${{place.name}}', '${{selectedCountry}}')">${{t.cta_ai}}</button>
309
  <button class="btn-action btn-map" onclick="window.open('${{getMapUrl(place)}}', '_blank')">${{t.cta_map}}</button>
310
  </div>
311
  `;
 
313
  }});
314
  }}
315
 
316
+ /* --- World Map Logic --- */
317
+ function openWorldMapPage() {{
318
+ document.getElementById('world-map-page').style.transform = 'translateY(0%)';
319
+ if (!worldMap) {{
320
+ worldMap = L.map('full-map').setView([20, 0], 2);
321
+ L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: '© OpenStreetMap' }}).addTo(worldMap);
322
+ loadAllMarkers();
323
+ }}
324
+ }}
325
+
326
+ function closeWorldMapPage() {{
327
+ document.getElementById('world-map-page').style.transform = 'translateY(100%)';
328
+ }}
329
+
330
+ function loadAllMarkers() {{
331
+ worldMarkers.clearLayers();
332
+ Object.keys(PLACE_DB).forEach(country => {{
333
+ PLACE_DB[country].forEach(place => {{
334
+ // Create marker
335
+ const marker = L.marker([place.lat, place.lon]).bindPopup(`
336
+ <b>${{place.name}}</b><br>${{place.label}}<br>
337
+ <button onclick="openDetailFromMap('${{place.name}}', '${{country}}')" style="margin-top:5px; background:black; color:gold; border:none; padding:5px 10px; border-radius:5px; cursor:pointer;">
338
+ View Details
339
+ </button>
340
+ `);
341
+ worldMarkers.addLayer(marker);
342
+ }});
343
+ }});
344
+ worldMarkers.addTo(worldMap);
345
+ }}
346
+
347
+ async function searchMapLocation() {{
348
+ const query = document.getElementById('map-search-input').value;
349
+ if(!query) return;
350
+
351
+ // Simple Nominatim Geocoding
352
+ try {{
353
+ const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${{query}}`);
354
+ const data = await res.json();
355
+ if(data && data.length > 0) {{
356
+ const lat = data[0].lat;
357
+ const lon = data[0].lon;
358
+ worldMap.setView([lat, lon], 12);
359
+ }} else {{
360
+ alert("Location not found!");
361
+ }}
362
+ }} catch(e) {{
363
+ console.error(e);
364
+ }}
365
+ }}
366
+
367
+ function getMyLocation() {{
368
+ if (navigator.geolocation) {{
369
+ navigator.geolocation.getCurrentPosition(position => {{
370
+ const lat = position.coords.latitude;
371
+ const lon = position.coords.longitude;
372
+ worldMap.setView([lat, lon], 12);
373
+ L.marker([lat, lon], {{icon: L.divIcon({{html: '📍', className: 'my-loc-icon', iconSize: [20, 20]}})}}).addTo(worldMap).bindPopup("You are here").openPopup();
374
+ }});
375
+ }} else {{
376
+ alert("Geolocation is not supported by this browser.");
377
+ }}
378
+ }}
379
+
380
+ /* --- Detail & AI Logic --- */
381
+ async function openDetailFromMap(placeName, countryName) {{
382
+ // Close World Map slightly to show transition
383
+ // But actually we are jumping to detail view.
384
+ // Need to set selectedCountry so getMapUrl works correctly inside openDetail
385
+ selectedCountry = countryName;
386
+ closeWorldMapPage();
387
+ openDetail(placeName, countryName);
388
+ }}
389
+
390
+ async function openDetail(placeName, countryName) {{
391
+ // Find Place Object
392
+ // countryName arg is optional if coming from list (uses selectedCountry)
393
+ const cName = countryName || selectedCountry;
394
+ const place = PLACE_DB[cName].find(p => p.name === placeName);
395
  if(!place) return;
396
 
397
  document.getElementById('main-page').style.transform = 'translateX(-30%)';
 
400
  startLoadingAnimation();
401
 
402
  document.getElementById('repo-title').innerText = place.name;
403
+ document.getElementById('repo-meta').innerText = `${{place.label}} • ${{cName}}`;
404
 
405
+ // Detail Page Map Button
406
  const btn = document.getElementById('gmap-link');
407
+ const isKorea = cName.includes("Korea");
408
+
409
+ let query = "";
410
+ if (isKorea && place.real_addr) {{
411
+ query = encodeURIComponent(place.real_addr);
412
+ btn.innerText = TRANSLATIONS[currentLang].nmap;
413
+ btn.classList.add("naver");
414
+ btn.href = `https://map.naver.com/v5/search/${{query}}`;
415
+ }} else if (isKorea) {{
416
+ query = encodeURIComponent(place.name);
417
+ btn.innerText = TRANSLATIONS[currentLang].nmap;
418
+ btn.classList.add("naver");
419
+ btn.href = `https://map.naver.com/v5/search/${{query}}`;
420
+ }} else {{
421
+ query = encodeURIComponent(place.name + " " + cName);
422
+ btn.innerText = TRANSLATIONS[currentLang].gmap;
423
+ btn.classList.remove("naver");
424
+ btn.href = `https://www.google.com/maps/search/?api=1&query=$${{query}}`;
425
+ }}
426
 
427
  document.getElementById('repo-content').innerHTML = "";
428
 
429
+ if (!miniMap) {{
430
+ miniMap = L.map('mini-map');
431
+ L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: '© OpenStreetMap' }}).addTo(miniMap);
432
  }}
433
  setTimeout(() => {{
434
+ miniMap.invalidateSize();
435
+ miniMap.setView([place.lat, place.lon], 15);
436
+ if (miniMarker) miniMap.removeLayer(miniMarker);
437
+ miniMarker = L.marker([place.lat, place.lon]).addTo(miniMap).bindPopup(place.name).openPopup();
438
  }}, 300);
439
 
440
+ // RAG Logic
441
  let contextDesc = "";
442
  if (place.real_addr) {{
443
  contextDesc = `Address: ${{place.real_addr}}, Vibe: ${{place.real_vibe}}, Drink: ${{place.real_drink}}, Tip: ${{place.real_tip}}`;
 
445
  contextDesc = currentLang === 'KO' ? place.desc_ko : place.desc_en;
446
  }}
447
 
448
+ await generatePuterReport(place, cName, contextDesc);
449
  }}
450
 
451
  function startLoadingAnimation() {{
 
466
  document.getElementById('loader').style.display = 'none';
467
  }}
468
 
469
+ async function generatePuterReport(place, countryName, contextDesc) {{
470
  let langFull = "Korean";
471
  if(currentLang === "EN") langFull = "English";
472
  if(currentLang === "JP") langFull = "Japanese";
473
  if(currentLang === "CN") langFull = "Chinese";
474
 
475
  const prompt = `
476
+ Act as a travel expert. Write a detailed review about "${{place.name}}" in ${{countryName}}.
477
 
478
  [Context Info]
479
  ${{contextDesc}}
 
517
  function generateLocalReport(place, dbDesc) {{
518
  stopLoadingAnimation();
519
  const report = `
520
+ ✨ **[Offline Info]**
521
 
522
  **${{place.name}}**
523
 
 
528
  document.getElementById('repo-content').innerHTML = report.replace(/\\n/g, '<br>');
529
  }}
530
 
531
+ function closeDetail() {{
532
  document.getElementById('detail-page').style.transform = 'translateX(100%)';
533
  document.getElementById('main-page').style.transform = 'translateX(0%)';
534
  }}