Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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:
|
| 49 |
-
padding-top: 20px;
|
| 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:
|
| 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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="
|
| 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
|
| 161 |
-
let
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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": "🗺️ 길찾기"
|
| 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"
|
| 177 |
-
"JP": {{ "subtitle": "世界のお酒ヘリテージツアー", "filters": {{ "All": "すべて", "Whiskey": "ウイスキー", "Spirits": "蒸留酒", "Beer": "ビール", "Wine": "ワイン", "Traditional": "伝統酒", "Bar": "バー" }}, "report_header": "旅行レポート", "gmap": "Googleマップ", "nmap": "Naverマップ", "cta_ai": "🤖 AIレビュー", "cta_map": "🗺️ 地図"
|
| 178 |
-
"CN": {{ "subtitle": "全球酒类遗产之旅", "filters": {{ "All": "全部", "Whiskey": "威士忌", "Spirits": "烈酒", "Beer": "啤酒", "Wine": "葡萄酒", "Traditional": "传统酒", "Bar": "酒吧" }}, "report_header": "旅游报告", "gmap": "谷歌地图", "nmap": "Naver地图", "cta_ai": "🤖 AI 评论", "cta_map": "🗺️ 地图"
|
| 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 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}} • ${{
|
| 298 |
|
| 299 |
-
//
|
| 300 |
const btn = document.getElementById('gmap-link');
|
| 301 |
-
const isKorea =
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
if(isKorea
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
document.getElementById('repo-content').innerHTML = "";
|
| 307 |
|
| 308 |
-
if (!
|
| 309 |
-
|
| 310 |
-
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: '© OpenStreetMap' }}).addTo(
|
| 311 |
}}
|
| 312 |
setTimeout(() => {{
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
if (
|
| 316 |
-
|
| 317 |
}}, 300);
|
| 318 |
|
| 319 |
-
//
|
| 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 ${{
|
| 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]
|
| 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
|
| 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 |
}}
|