Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import streamlit.components.v1 as components
|
|
|
|
| 3 |
|
| 4 |
# 1. ์ํฌ๋ฆฟ ํ ํฐ ํ์ธ
|
| 5 |
try:
|
| 6 |
token = st.secrets["HF_TOKEN"]
|
| 7 |
except:
|
| 8 |
token = ""
|
| 9 |
-
|
| 10 |
-
st.error("๐จ HF_TOKEN์ด ์ค์ ๋์ง ์์์ต๋๋ค! Space Settings์์ ํ ํฐ์ ์ถ๊ฐํด์ฃผ์ธ์.")
|
| 11 |
|
| 12 |
# 2. ํ์ด์ง ์ค์
|
| 13 |
st.set_page_config(page_title="Drunken Plane", page_icon="โ๏ธ", layout="wide")
|
|
@@ -20,7 +20,75 @@ st.markdown("""
|
|
| 20 |
</style>
|
| 21 |
""", unsafe_allow_html=True)
|
| 22 |
|
| 23 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
html_code = f"""
|
| 25 |
<!DOCTYPE html>
|
| 26 |
<html>
|
|
@@ -36,101 +104,99 @@ html_code = f"""
|
|
| 36 |
:root {{ --gold: #d4af37; --dark: #0a0a0a; --card-bg: #1c1c1c; --text: #e0e0e0; --accent: #c5a059; }}
|
| 37 |
body {{ margin: 0; padding: 0; background: var(--dark); color: var(--text); font-family: 'Segoe UI', serif; overflow: hidden; }}
|
| 38 |
|
| 39 |
-
/* ๐
|
| 40 |
#main-page {{
|
| 41 |
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 42 |
-
background: linear-gradient(rgba(0,0,0,0.
|
| 43 |
background-size: cover; background-position: center;
|
| 44 |
-
z-index: 5000; display: flex; flex-direction: column; align-items: center; justify-content:
|
|
|
|
| 45 |
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
| 46 |
}}
|
| 47 |
|
| 48 |
-
.brand-
|
| 49 |
-
.brand-title {{ font-size:
|
| 50 |
-
.brand-subtitle {{ font-size:
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
.search-container {{ width: 80%; max-width: 500px; position: relative; margin-bottom: 20px; }}
|
| 54 |
-
.country-input {{
|
| 55 |
-
width: 100%; padding: 15px 20px; background: rgba(0,0,0,0.6);
|
| 56 |
-
border: 1px solid var(--gold); border-radius: 30px; color: white;
|
| 57 |
-
font-size: 16px; text-align: center; backdrop-filter: blur(5px);
|
| 58 |
-
transition: all 0.3s;
|
| 59 |
-
}}
|
| 60 |
-
.country-input:focus {{ outline: none; background: rgba(0,0,0,0.8); box-shadow: 0 0 15px rgba(212, 175, 55, 0.3); }}
|
| 61 |
-
|
| 62 |
-
/* ์์น ๊ฐ์ง ๋ฑ์ง */
|
| 63 |
-
.location-detect {{
|
| 64 |
-
background: rgba(255,255,255,0.1); backdrop-filter: blur(10px);
|
| 65 |
-
padding: 8px 20px; border-radius: 30px; border: 1px solid rgba(255,255,255,0.2);
|
| 66 |
-
color: #aaa; font-size: 13px; display: flex; align-items: center; gap: 8px; cursor: pointer;
|
| 67 |
-
margin-bottom: 40px; transition: 0.3s;
|
| 68 |
-
}}
|
| 69 |
-
.location-detect:hover {{ background: rgba(255,255,255,0.2); color: white; }}
|
| 70 |
|
| 71 |
-
/*
|
| 72 |
-
.
|
| 73 |
-
.
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
.
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
}}
|
| 78 |
-
.
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}}
|
| 85 |
-
.
|
| 86 |
-
.
|
| 87 |
-
.
|
| 88 |
-
|
| 89 |
-
.
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 94 |
-
transform: translateX(100%); transition: transform 0.
|
| 95 |
-
z-index:
|
| 96 |
}}
|
| 97 |
|
| 98 |
.nav-bar {{
|
| 99 |
-
height:
|
| 100 |
-
display: flex; align-items: center; padding: 0
|
| 101 |
}}
|
| 102 |
-
.back-btn {{ background: none; border: none; color:
|
| 103 |
-
.nav-title {{ color:
|
| 104 |
|
| 105 |
-
#map {{ width: 100%; height:
|
| 106 |
|
| 107 |
-
.
|
| 108 |
-
flex: 1;
|
| 109 |
-
box-sizing: border-box; position: relative;
|
| 110 |
}}
|
| 111 |
|
| 112 |
-
.
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
}}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
border-radius: 25px; font-size: 14px;
|
| 125 |
}}
|
|
|
|
| 126 |
|
| 127 |
-
/* ๋ก๋ฉ
|
| 128 |
.loading-screen {{
|
| 129 |
-
position:
|
| 130 |
-
|
|
|
|
| 131 |
}}
|
| 132 |
-
.
|
| 133 |
-
.loader-
|
| 134 |
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
| 135 |
|
| 136 |
</style>
|
|
@@ -138,308 +204,187 @@ html_code = f"""
|
|
| 138 |
<body>
|
| 139 |
|
| 140 |
<div id="main-page">
|
| 141 |
-
<
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
<div class="search-container">
|
| 146 |
-
<input type="text" id="country-search" class="country-input" placeholder="์ด๋ ๋๋ผ๋ก ๋ ๋ ๊น์? (์: Malaysia)" onkeypress="if(event.key==='Enter') searchCountry()">
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
<div
|
| 150 |
-
<
|
| 151 |
-
<span id="loc-text">๐ ๋ด ์์น(GPS) ๊ธฐ์ค์ผ๋ก ์ฐพ๊ธฐ</span>
|
| 152 |
</div>
|
| 153 |
|
| 154 |
-
<div class="
|
| 155 |
-
<div id="curation-header" class="curation-title">LOCAL HERITAGE</div>
|
| 156 |
-
<div id="card-list" class="card-scroll">
|
| 157 |
-
<div style="color:#666; padding:20px; font-size:0.9em;">
|
| 158 |
-
๊ตญ๊ฐ๋ฅผ ๊ฒ์ํ๊ฑฐ๋ ๋ด ์์น๋ฅผ ๋๋ฌ์ฃผ์ธ์.<br>
|
| 159 |
-
Qwen AI๊ฐ ํด๋น ๊ตญ๊ฐ์ ๋ํ ๋ช
์ฃผ์ ํฌ์ด ์ฝ์ค๋ฅผ ์ค๊ณํด๋๋ฆฝ๋๋ค.
|
| 160 |
-
</div>
|
| 161 |
-
</div>
|
| 162 |
-
</div>
|
| 163 |
</div>
|
| 164 |
|
| 165 |
-
<div id="
|
| 166 |
<div class="nav-bar">
|
| 167 |
-
<button class="back-btn" onclick="goBack()">โ</button>
|
| 168 |
-
<div
|
| 169 |
-
<div style="width:
|
| 170 |
</div>
|
|
|
|
| 171 |
<div id="map"></div>
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
</div>
|
| 174 |
|
| 175 |
-
<div id="
|
| 176 |
<div class="loader-ring"></div>
|
| 177 |
-
<div id="loader-msg" class="
|
| 178 |
</div>
|
| 179 |
|
| 180 |
<script>
|
| 181 |
const HF_TOKEN = "{token}";
|
| 182 |
-
// ๐ฅ [์ค์ ์์ ] 72B -> 32B ๋ชจ๋ธ๋ก ๋ณ๊ฒฝ (์์ ์ฑ ํ๋ณด)
|
| 183 |
const MODEL_ID = "Qwen/Qwen2.5-32B-Instruct";
|
| 184 |
|
|
|
|
|
|
|
|
|
|
| 185 |
let map = null;
|
| 186 |
-
let
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${{currentLat}}&lon=${{currentLng}}`)
|
| 208 |
-
.then(res => res.json())
|
| 209 |
-
.then(data => {{
|
| 210 |
-
targetCity = data.address.city || data.address.town || "";
|
| 211 |
-
targetCountry = data.address.country || "Unknown";
|
| 212 |
-
|
| 213 |
-
document.getElementById('loc-spinner').style.display = 'none';
|
| 214 |
-
document.getElementById('loc-text').innerText = `๐ ${{targetCity}}, ${{targetCountry}}`;
|
| 215 |
-
document.getElementById('curation-header').innerText = `HERITAGE OF ${{targetCountry.toUpperCase()}}`;
|
| 216 |
-
|
| 217 |
-
getRecommendationCards(targetCity, targetCountry);
|
| 218 |
-
}});
|
| 219 |
-
}}
|
| 220 |
-
|
| 221 |
-
function failGPS() {{
|
| 222 |
-
document.getElementById('loc-spinner').style.display = 'none';
|
| 223 |
-
document.getElementById('loc-text').innerText = "์์น ํ์ธ ๋ถ๊ฐ";
|
| 224 |
-
alert("์์น ๊ถํ์ ํ์ธํด์ฃผ์ธ์.");
|
| 225 |
-
}}
|
| 226 |
-
|
| 227 |
-
// 2. ๊ตญ๊ฐ ๊ฒ์
|
| 228 |
-
async function searchCountry() {{
|
| 229 |
-
const query = document.getElementById('country-search').value;
|
| 230 |
-
if(!query) return;
|
| 231 |
-
|
| 232 |
-
document.getElementById('loc-text').innerText = "๐ ๋ด ์์น(GPS) ๊ธฐ์ค์ผ๋ก ์ฐพ๊ธฐ";
|
| 233 |
-
|
| 234 |
-
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${{query}}&limit=1`);
|
| 235 |
-
const data = await res.json();
|
| 236 |
-
|
| 237 |
-
if(data.length > 0) {{
|
| 238 |
-
targetCountry = query;
|
| 239 |
-
targetCity = "";
|
| 240 |
-
document.getElementById('curation-header').innerText = `HERITAGE OF ${{targetCountry.toUpperCase()}}`;
|
| 241 |
-
getRecommendationCards("", targetCountry);
|
| 242 |
-
}} else {{
|
| 243 |
-
alert("๊ตญ๊ฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
|
| 244 |
-
}}
|
| 245 |
}}
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
You are a luxury travel curator.
|
| 256 |
-
Context: ${{context}}.
|
| 257 |
-
Recommend 5 famous alcoholic beverages representing this region/country.
|
| 258 |
-
Focus on Tradition, Craft, and Heritage.
|
| 259 |
|
| 260 |
-
|
| 261 |
-
[
|
| 262 |
-
{{
|
| 263 |
-
"name_kr": "Name in Korean",
|
| 264 |
-
"name_en": "Name in English",
|
| 265 |
-
"desc": "Short description (2 sentences)",
|
| 266 |
-
"search_query": "Keyword to find production sites"
|
| 267 |
-
}}
|
| 268 |
-
]
|
| 269 |
-
`;
|
| 270 |
-
|
| 271 |
-
try {{
|
| 272 |
-
const data = await callQwen(prompt);
|
| 273 |
-
renderCards(data);
|
| 274 |
-
}} catch(e) {{
|
| 275 |
-
console.error("AI Card Error:", e);
|
| 276 |
-
// ์๋ฌ ๋ฉ์์ง๋ฅผ ํ๋ฉด์ ๋ณด์ฌ์ค (๋๋ฒ๊น
์ฉ)
|
| 277 |
-
container.innerHTML = `<div style='color:#ff5555; padding:20px;'>AI ์ฐ๊ฒฐ ์ค๋ฅ: ${{e.message}}<br>์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.</div>`;
|
| 278 |
-
}}
|
| 279 |
-
}}
|
| 280 |
-
|
| 281 |
-
function renderCards(data) {{
|
| 282 |
-
const container = document.getElementById('card-list');
|
| 283 |
-
container.innerHTML = "";
|
| 284 |
-
|
| 285 |
-
data.forEach(item => {{
|
| 286 |
const card = document.createElement('div');
|
| 287 |
-
card.className = '
|
| 288 |
-
card.onclick = () =>
|
| 289 |
|
| 290 |
card.innerHTML = `
|
| 291 |
-
<div class="
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
<
|
|
|
|
|
|
|
| 295 |
`;
|
| 296 |
-
|
| 297 |
}});
|
| 298 |
}}
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
document.getElementById('
|
| 303 |
-
document.getElementById('
|
| 304 |
-
document.getElementById('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
Strictly NO generic bars.
|
| 313 |
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
]
|
| 318 |
`;
|
| 319 |
|
| 320 |
try {{
|
| 321 |
-
const
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
const coords = await getCoords(searchQuery);
|
| 328 |
-
if (coords) verifiedPlaces.push({{ ...place, ...coords }});
|
| 329 |
-
}}
|
| 330 |
-
|
| 331 |
-
if (verifiedPlaces.length === 0) {{
|
| 332 |
-
alert("์ ํํ ์์น๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
|
| 333 |
-
document.getElementById('global-loader').style.display = 'none';
|
| 334 |
-
return;
|
| 335 |
-
}}
|
| 336 |
-
|
| 337 |
-
const blogPrompt = `
|
| 338 |
-
Write a travel blog post (Korean) for these places:
|
| 339 |
-
${{JSON.stringify(verifiedPlaces.map(p => p.name))}}
|
| 340 |
-
|
| 341 |
-
For each, write: Meaning, Experience, Tips.
|
| 342 |
|
| 343 |
-
|
| 344 |
-
[
|
| 345 |
-
{{ "name": "Place Name", "content": "Long text content..." }}
|
| 346 |
-
]
|
| 347 |
-
`;
|
| 348 |
|
| 349 |
-
document.getElementById('loader
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
const finalData = verifiedPlaces.map(place => {{
|
| 353 |
-
const blog = blogContent.find(b => b.name === place.name);
|
| 354 |
-
return {{ ...place, content: blog ? blog.content : "์ ๋ณด ์์" }};
|
| 355 |
-
}});
|
| 356 |
-
|
| 357 |
-
showMapView(finalData);
|
| 358 |
|
| 359 |
}} catch(e) {{
|
| 360 |
-
console.error(
|
| 361 |
-
|
| 362 |
-
document.getElementById('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}}
|
| 364 |
}}
|
| 365 |
|
| 366 |
-
function
|
| 367 |
-
document.getElementById('
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
if (!map) {{
|
| 372 |
-
map = L.map('map');
|
| 373 |
-
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: 'ยฉ OpenStreetMap' }}).addTo(map);
|
| 374 |
-
}}
|
| 375 |
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
L.marker(latlng).addTo(map).bindPopup(`<b>${{p.name}}</b><br>${{p.type}}`).openPopup();
|
| 385 |
-
bounds.push(latlng);
|
| 386 |
-
|
| 387 |
-
const gmapUrl = `https://www.google.com/maps/search/?api=1&query=${{encodeURIComponent(p.name)}}`;
|
| 388 |
-
const article = document.createElement('div');
|
| 389 |
-
article.className = 'place-article';
|
| 390 |
-
article.innerHTML = `
|
| 391 |
-
<div class="place-title">${{idx+1}}. ${{p.name}}</div>
|
| 392 |
-
<div class="place-type">${{p.type}}</div>
|
| 393 |
-
<div class="place-content">${{p.content}}</div>
|
| 394 |
-
<a href="${{gmapUrl}}" target="_blank" class="gmap-link">Google Maps์์ ๊ธธ์ฐพ๊ธฐ</a>
|
| 395 |
-
`;
|
| 396 |
-
blogContainer.appendChild(article);
|
| 397 |
-
}});
|
| 398 |
-
|
| 399 |
-
if (bounds.length > 0) map.fitBounds(bounds, {{ padding: [50, 50] }});
|
| 400 |
}}
|
| 401 |
|
| 402 |
function goBack() {{
|
|
|
|
| 403 |
document.getElementById('main-page').style.transform = 'translateX(0%)';
|
| 404 |
-
document.getElementById('map-view').style.transform = 'translateX(100%)';
|
| 405 |
-
}}
|
| 406 |
-
|
| 407 |
-
// ๐ฅ [JSON ํ์ฑ ๋ก์ง ๊ฐํ]
|
| 408 |
-
async function callQwen(prompt, maxTokens = 1000) {{
|
| 409 |
-
const res = await fetch(`https://api-inference.huggingface.co/models/${{MODEL_ID}}`, {{
|
| 410 |
-
method: "POST",
|
| 411 |
-
headers: {{ "Authorization": `Bearer ${{HF_TOKEN}}`, "Content-Type": "application/json" }},
|
| 412 |
-
body: JSON.stringify({{ inputs: prompt, parameters: {{ max_new_tokens: maxTokens, return_full_text: false }} }})
|
| 413 |
-
}});
|
| 414 |
-
|
| 415 |
-
// ์๋ต ์ฝ๋ ์ฒดํฌ
|
| 416 |
-
if (!res.ok) {{
|
| 417 |
-
throw new Error(`HTTP Error ${{res.status}}: ${{res.statusText}}`);
|
| 418 |
-
}}
|
| 419 |
-
|
| 420 |
-
const json = await res.json();
|
| 421 |
-
|
| 422 |
-
if (json.error) throw new Error(json.error);
|
| 423 |
-
let text = json[0].generated_text;
|
| 424 |
-
|
| 425 |
-
// ๋งํฌ๋ค์ด ์ ๊ฑฐ
|
| 426 |
-
text = text.replace(/```json/g, '').replace(/```/g, '').trim();
|
| 427 |
-
|
| 428 |
-
const start = text.indexOf('[');
|
| 429 |
-
const end = text.lastIndexOf(']') + 1;
|
| 430 |
-
|
| 431 |
-
if (start === -1) throw new Error("No Valid JSON found in AI response");
|
| 432 |
-
|
| 433 |
-
return JSON.parse(text.substring(start, end));
|
| 434 |
-
}}
|
| 435 |
-
|
| 436 |
-
async function getCoords(query) {{
|
| 437 |
-
try {{
|
| 438 |
-
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${{query}}&limit=1`);
|
| 439 |
-
const data = await res.json();
|
| 440 |
-
if (data.length > 0) return {{ lat: data[0].lat, lon: data[0].lon }};
|
| 441 |
-
}} catch(e) {{ return null; }}
|
| 442 |
-
return null;
|
| 443 |
}}
|
| 444 |
|
| 445 |
</script>
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import streamlit.components.v1 as components
|
| 3 |
+
import json
|
| 4 |
|
| 5 |
# 1. ์ํฌ๋ฆฟ ํ ํฐ ํ์ธ
|
| 6 |
try:
|
| 7 |
token = st.secrets["HF_TOKEN"]
|
| 8 |
except:
|
| 9 |
token = ""
|
| 10 |
+
st.error("Settings์์ 'HF_TOKEN'์ ๋จผ์ ์ค์ ํด์ฃผ์ธ์!")
|
|
|
|
| 11 |
|
| 12 |
# 2. ํ์ด์ง ์ค์
|
| 13 |
st.set_page_config(page_title="Drunken Plane", page_icon="โ๏ธ", layout="wide")
|
|
|
|
| 20 |
</style>
|
| 21 |
""", unsafe_allow_html=True)
|
| 22 |
|
| 23 |
+
# ---------------------------------------------------------
|
| 24 |
+
# ๐พ GLOBAL ALCOHOL HERITAGE DATABASE (์ ์ธ๊ณ ๋ช
์)
|
| 25 |
+
# ---------------------------------------------------------
|
| 26 |
+
PLACE_DB = {
|
| 27 |
+
"Malaysia ๐ฒ๐พ": [
|
| 28 |
+
{"name": "The Old Tuak House", "type": "Traditional Rice Wine", "lat": 3.147, "lon": 101.708, "desc": "๋ณด๋ฅด๋ค์ค ์์ฃผ๋ฏผ์ ์ํผ์ด ๋ด๊ธด ์ ํต์ฃผ 'ํฌ์
' ์ ๋ฌธ์ "},
|
| 29 |
+
{"name": "Heineken Malaysia Berhad", "type": "Brewery Tour", "lat": 3.085, "lon": 101.609, "desc": "ํ์ด๊ฑฐ ๋งฅ์ฃผ์ ๊ธฐ๋ค์ค๊ฐ ์์ฐ๋๋ ๊ฑฐ๋ํ ์์กฐ ํ์ฅ"},
|
| 30 |
+
{"name": "Bar Trigona", "type": "World's 50 Best Bars", "lat": 3.155, "lon": 101.714, "desc": "๋ง๋ ์ด์์ ํ ์ข
๊ฟ์ ์ฌ์ฉํ๋ ๋ญ์
๋ฆฌ ์นตํ
์ผ ๋ฐ"},
|
| 31 |
+
{"name": "JungleBird", "type": "Rum Bar", "lat": 3.153, "lon": 101.621, "desc": "์์์ ์ต๊ณ ์ ๋ผ(Rum) ์ฑ์ง, ํธ๋กํผ์ปฌ ๋ถ์๊ธฐ"},
|
| 32 |
+
{"name": "Pahit", "type": "Gin Bar", "lat": 3.146, "lon": 101.708, "desc": "์ค๋๋ ์ตํ์ฐ์ค๋ฅผ ๊ฐ์กฐํ ๊ฐ์ฑ์ ์ธ ์ง(Gin) ์ ๋ฌธ ๋ฐ"}
|
| 33 |
+
],
|
| 34 |
+
"South Korea ๐ฐ๐ท": [
|
| 35 |
+
{"name": "Hwayo Distillery (Yeoju)", "type": "Soju Distillery", "lat": 37.294, "lon": 127.632, "desc": "ํ๊ตญ ํ๋ฆฌ๋ฏธ์ ์ฆ๋ฅ์ ์์ฃผ์ ํ์ค, ํ์ ์์กฐ์ฅ"},
|
| 36 |
+
{"name": "Sansawon (Kooksoondang)", "type": "Liquor Museum", "lat": 37.899, "lon": 127.279, "desc": "์ ํต์ฃผ ๊ฐค๋ฌ๋ฆฌ์ ์ ์์ด ์ด์ฐ๋ฌ์ง ์ฐ์ฌ์"},
|
| 37 |
+
{"name": "Jeju Beer Company", "type": "Craft Brewery", "lat": 33.364, "lon": 126.242, "desc": "์ ์ฃผ์ ๋ง์ ๋ฌผ๋ก ๋ง๋๋ ํ๋์ ์ธ ์์ ๋งฅ์ฃผ ํฌ์ด"},
|
| 38 |
+
{"name": "Boksoondoga", "type": "Makgeolli Brewery", "lat": 35.539, "lon": 129.106, "desc": "์ดํ์ธ ๊ฐ์ ํ์ฐ ๋ง๊ฑธ๋ฆฌ, ๋ณต์๋๊ฐ์ ๊ณ ํฅ"}
|
| 39 |
+
],
|
| 40 |
+
"Japan ๐ฏ๐ต": [
|
| 41 |
+
{"name": "Suntory Yamazaki Distillery", "type": "Whisky Distillery", "lat": 34.892, "lon": 135.674, "desc": "์ผ๋ณธ ์์คํค์ ๋ฐ์์ง์ด์ ์ ์ค์ ์ธ ์ผ๋ง์ํค ์ฆ๋ฅ์"},
|
| 42 |
+
{"name": "Sapporo Beer Museum", "type": "Beer Museum", "lat": 43.071, "lon": 141.369, "desc": "ํ์นด์ด๋ ๋ถ์ ๋ฒฝ๋ ๊ฑด๋ฌผ์์ ์ฆ๊ธฐ๋ ์ฟํฌ๋ก ๋งฅ์ฃผ"},
|
| 43 |
+
{"name": "Sake Brewery District (Fushimi)", "type": "Sake District", "lat": 34.929, "lon": 135.761, "desc": "๊ตํ ์ ๋ง์ ๋ฌผ๊ณผ ์๋ก ๋น๋ ์ฌ์ผ ์์กฐ์ฅ ๊ฑฐ๋ฆฌ"},
|
| 44 |
+
{"name": "Nikka Yoichi Distillery", "type": "Whisky Distillery", "lat": 43.186, "lon": 140.798, "desc": "์ค์ฝํ๋๋๋ฅผ ๊ฟ๊ฟจ๋ ํ์ผ์ธ ๋ฃจ์ ํผ์ด ๋ด๊ธด ์์ด์น"}
|
| 45 |
+
],
|
| 46 |
+
"Scotland ๐ด๓ ง๓ ข๓ ณ๓ ฃ๓ ด๓ ฟ": [
|
| 47 |
+
{"name": "The Macallan Distillery", "type": "Whisky Distillery", "lat": 57.484, "lon": -3.206, "desc": "์์คํค์ ๋กค์ค๋ก์ด์ค, ์์ ์ํ ๊ฐ์ ์ฆ๋ฅ์"},
|
| 48 |
+
{"name": "Laphroaig Distillery", "type": "Islay Whisky", "lat": 55.629, "lon": -6.153, "desc": "์์ผ๋ผ ์ฌ์ ๊ฑฐ์น ๋ฐ๋ค์ ํผํธ ํฅ์ ํ์ ๊ณณ"},
|
| 49 |
+
{"name": "Glenfiddich Distillery", "type": "Speyside Whisky", "lat": 57.454, "lon": -3.128, "desc": "์ธ๊ณ์์ ๊ฐ์ฅ ๋ง์ด ํ๋ฆฌ๋ ์ฑ๊ธ๋ชฐํธ์ ๊ณ ํฅ"},
|
| 50 |
+
{"name": "The Scotch Whisky Experience", "type": "Museum", "lat": 55.948, "lon": -3.195, "desc": "์๋ ๋ฒ๋ฌ ์ฑ ์์์ ์ฆ๊ธฐ๋ ์์คํค ์ธํฐ๋ํฐ๋ธ ํฌ์ด"}
|
| 51 |
+
],
|
| 52 |
+
"Ireland ๐ฎ๐ช": [
|
| 53 |
+
{"name": "Guinness Storehouse", "type": "Stout Experience", "lat": 53.341, "lon": -6.286, "desc": "๋๋ธ๋ฆฐ ์ฌํ์ ํ์ ์ฝ์ค, ๊ธฐ๋ค์ค์ ๋ชจ๋ ๊ฒ"},
|
| 54 |
+
{"name": "Jameson Distillery Bow St.", "type": "Irish Whiskey", "lat": 53.348, "lon": -6.277, "desc": "์์ด๋ฆฌ์ฌ ์์คํค์ ์ญ์ฌ์ ์์์ ํ ๋ฒ์"},
|
| 55 |
+
{"name": "The Temple Bar", "type": "Historic Pub", "lat": 53.345, "lon": -6.264, "desc": "๋๋ธ๋ฆฐ์์ ๊ฐ์ฅ ์์ง์ ์ด๊ณ ํ๊ธฐ์ฐฌ ํ"}
|
| 56 |
+
],
|
| 57 |
+
"France ๐ซ๐ท": [
|
| 58 |
+
{"name": "Moรซt & Chandon", "type": "Champagne House", "lat": 49.043, "lon": 3.957, "desc": "์นํ๋ด ์ง์ญ ์งํ ๋๊ตด์์ ์ฆ๊ธฐ๋ ๋ ํ๋ฆฌ๋ฝ"},
|
| 59 |
+
{"name": "Hennessy Maison", "type": "Cognac House", "lat": 45.694, "lon": -0.327, "desc": "์ธ๊ณ ์ต๊ณ ์ ์ฝ๋, ํค๋ค์์ ์ฌ์ฅ๋ถ"},
|
| 60 |
+
{"name": "Citรฉ du Vin", "type": "Wine Museum", "lat": 44.862, "lon": -0.550, "desc": "๋ณด๋ฅด๋์ ์์นํ ์๋์ ๊ท๋ชจ์ ์์ธ ํ
๋งํํฌ"}
|
| 61 |
+
],
|
| 62 |
+
"Germany ๐ฉ๐ช": [
|
| 63 |
+
{"name": "Hofbrรคuhaus Mรผnchen", "type": "Historic Beer Hall", "lat": 48.137, "lon": 11.580, "desc": "๋ฎํจ ์ฅํ ๋ฒํ์คํธ์ ์ ์ ์ด ์ผ๋
๋ด๋ด ํ๋ฅด๋ ๊ณณ"},
|
| 64 |
+
{"name": "Weihenstephan Brewery", "type": "Oldest Brewery", "lat": 48.395, "lon": 11.729, "desc": "์๊ธฐ 1040๋
์ ์์๋ ์ธ๊ณ์์ ๊ฐ์ฅ ์ค๋๋ ์์กฐ์ฅ"}
|
| 65 |
+
],
|
| 66 |
+
"Czech Republic ๐จ๐ฟ": [
|
| 67 |
+
{"name": "Pilsner Urquell Brewery", "type": "Pilsner Origin", "lat": 49.747, "lon": 13.388, "desc": "์ ์ธ๊ณ ๋ผ๊ฑฐ ๋งฅ์ฃผ์ ์์กฐ, ํ์ค๋ ์ฐ๋ฅด์ผ ์์กฐ์ฅ"},
|
| 68 |
+
{"name": "U Flekลฏ", "type": "Dark Beer Brewery", "lat": 50.078, "lon": 14.417, "desc": "ํ๋ผํ์์ 500๋
๋๊ฒ ํ๋งฅ์ฃผ๋ฅผ ๋ง๋ค์ด์จ ํ"}
|
| 69 |
+
],
|
| 70 |
+
"USA ๐บ๐ธ": [
|
| 71 |
+
{"name": "Jack Daniel's Distillery", "type": "Tennessee Whiskey", "lat": 35.284, "lon": -86.368, "desc": "๋ฏธ๊ตญ ์์คํค์ ์์กด์ฌ, ํ
๋ค์ ๋ฆฐ์น๋ฒ๊ทธ ํฌ์ด"},
|
| 72 |
+
{"name": "Buffalo Trace Distillery", "type": "Bourbon Distillery", "lat": 38.217, "lon": -84.870, "desc": "๋ฒ๋ฒ ์์คํค์ ์ด์์๋ ์ญ์ฌ์ ์ ์ค"},
|
| 73 |
+
{"name": "Robert Mondavi Winery", "type": "Napa Valley Wine", "lat": 38.427, "lon": -122.404, "desc": "์บ๋ฆฌํฌ๋์ ๋ํ๋ฐธ๋ฆฌ ์์ธ์ ์ ๊ตฌ์"}
|
| 74 |
+
],
|
| 75 |
+
"Mexico ๐ฒ๐ฝ": [
|
| 76 |
+
{"name": "Mundo Cuervo (Jose Cuervo)", "type": "Tequila Distillery", "lat": 20.878, "lon": -103.836, "desc": "๋ฐํฌ๋ผ ๋ง์์์ ๊ฒฝํํ๋ ํธ์ธ ์ฟ ์๋ณด์ ๋ชจ๋ ๊ฒ"},
|
| 77 |
+
{"name": "La Capilla", "type": "Historic Bar", "lat": 20.876, "lon": -103.837, "desc": "์ธ๊ณ 50๋ ๋ฐ, ์นตํ
์ผ '๋ฐํ๊ฐ'์ ๋ฐ์์ง"}
|
| 78 |
+
],
|
| 79 |
+
"Singapore ๐ธ๐ฌ": [
|
| 80 |
+
{"name": "Long Bar (Raffles Hotel)", "type": "Historic Bar", "lat": 1.294, "lon": 103.854, "desc": "์ ์ค์ ์ธ ์นตํ
์ผ '์ฑ๊ฐํฌ๋ฅด ์ฌ๋ง'์ด ํ์ํ ๊ณณ"},
|
| 81 |
+
{"name": "Atlas Bar", "type": "Gin Bar", "lat": 1.300, "lon": 103.858, "desc": "์ธ๊ณ ์ต๋ ๊ท๋ชจ์ ์ง ์ปฌ๋ ์
์ ๋ณด์ ํ ํ๋ คํ ๋ฐ"}
|
| 82 |
+
],
|
| 83 |
+
"Taiwan ๐น๐ผ": [
|
| 84 |
+
{"name": "Kavalan Distillery", "type": "Whisky Distillery", "lat": 24.711, "lon": 121.691, "desc": "์ธ๊ณ ์์คํค ์์ฅ์ ๋๋ผ๊ฒ ํ ๋๋ง์ ์์กด์ฌ, ์นด๋ฐ๋"},
|
| 85 |
+
{"name": "Draft Land", "type": "Cocktail Tap Bar", "lat": 25.042, "lon": 121.548, "desc": "๋งฅ์ฃผ์ฒ๋ผ ํญ์์ ๋ฝ์ ๋ง์๋ ํ์ ์ ์ธ ์นตํ
์ผ ๋ฐ"}
|
| 86 |
+
]
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
db_json = json.dumps(PLACE_DB)
|
| 90 |
+
|
| 91 |
+
# 4. HTML/JS ์ ์ฒด ์ฝ๋
|
| 92 |
html_code = f"""
|
| 93 |
<!DOCTYPE html>
|
| 94 |
<html>
|
|
|
|
| 104 |
:root {{ --gold: #d4af37; --dark: #0a0a0a; --card-bg: #1c1c1c; --text: #e0e0e0; --accent: #c5a059; }}
|
| 105 |
body {{ margin: 0; padding: 0; background: var(--dark); color: var(--text); font-family: 'Segoe UI', serif; overflow: hidden; }}
|
| 106 |
|
| 107 |
+
/* ๐ ๋ฉ์ธ ํ๋ฉด */
|
| 108 |
#main-page {{
|
| 109 |
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 110 |
+
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');
|
| 111 |
background-size: cover; background-position: center;
|
| 112 |
+
z-index: 5000; display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
|
| 113 |
+
padding-top: 50px; box-sizing: border-box;
|
| 114 |
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
| 115 |
}}
|
| 116 |
|
| 117 |
+
.brand-header {{ text-align: center; margin-bottom: 20px; animation: fadeInDown 1s; }}
|
| 118 |
+
.brand-title {{ font-size: 28px; font-weight: 900; color: var(--gold); letter-spacing: 3px; margin: 0; text-transform: uppercase; text-shadow: 0 4px 20px rgba(0,0,0,0.8); }}
|
| 119 |
+
.brand-subtitle {{ font-size: 12px; color: #ccc; letter-spacing: 2px; margin-top: 5px; font-weight: 300; }}
|
| 120 |
+
|
| 121 |
+
@keyframes fadeInDown {{ from {{ opacity: 0; transform: translateY(-20px); }} to {{ opacity: 1; transform: translateY(0); }} }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
/* ๊ตญ๊ฐ ์ ํ๊ธฐ (๊ฐ๋ก ์คํฌ๋กค) */
|
| 124 |
+
.country-selector-wrapper {{ width: 100%; overflow: hidden; padding: 10px 0; background: rgba(0,0,0,0.3); backdrop-filter: blur(5px); }}
|
| 125 |
+
.country-selector {{
|
| 126 |
+
display: flex; gap: 10px; overflow-x: auto; padding: 0 20px; scrollbar-width: none;
|
| 127 |
+
}}
|
| 128 |
+
.country-selector::-webkit-scrollbar {{ display: none; }}
|
| 129 |
|
| 130 |
+
.country-btn {{
|
| 131 |
+
background: rgba(50,50,50,0.6); border: 1px solid #444; color: #aaa;
|
| 132 |
+
padding: 8px 16px; border-radius: 20px; cursor: pointer; white-space: nowrap; font-size: 14px;
|
| 133 |
+
transition: 0.3s;
|
| 134 |
}}
|
| 135 |
+
.country-btn.active {{ background: var(--gold); color: black; border-color: var(--gold); font-weight: 800; transform: scale(1.05); box-shadow: 0 0 15px rgba(212, 175, 55, 0.4); }}
|
| 136 |
|
| 137 |
+
/* ์ฅ์ ๋ฆฌ์คํธ (์นด๋ํ) */
|
| 138 |
+
.place-list-container {{
|
| 139 |
+
width: 100%; max-width: 600px; flex: 1; overflow-y: auto; padding: 20px; box-sizing: border-box;
|
| 140 |
+
}}
|
| 141 |
+
.place-card {{
|
| 142 |
+
background: rgba(30,30,30,0.6); backdrop-filter: blur(10px);
|
| 143 |
+
border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 20px;
|
| 144 |
+
margin-bottom: 15px; cursor: pointer; transition: all 0.3s;
|
| 145 |
+
display: flex; flex-direction: column; position: relative; overflow: hidden;
|
| 146 |
}}
|
| 147 |
+
.place-card::before {{ content:''; position: absolute; left: 0; top: 0; height: 100%; width: 4px; background: var(--gold); opacity: 0; transition: 0.3s; }}
|
| 148 |
+
.place-card:hover {{ transform: translateX(5px); background: rgba(40,40,40,0.8); border-color: var(--gold); }}
|
| 149 |
+
.place-card:hover::before {{ opacity: 1; }}
|
| 150 |
+
|
| 151 |
+
.pc-header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }}
|
| 152 |
+
.pc-name {{ font-size: 18px; font-weight: bold; color: white; }}
|
| 153 |
+
.pc-type {{ font-size: 11px; background: var(--gold); color: black; padding: 2px 6px; border-radius: 4px; font-weight: bold; text-transform: uppercase; }}
|
| 154 |
+
.pc-desc {{ font-size: 13px; color: #ccc; line-height: 1.4; }}
|
| 155 |
+
.pc-cta {{ margin-top: 15px; font-size: 12px; color: var(--gold); text-align: right; font-weight: bold; letter-spacing: 1px; }}
|
| 156 |
+
|
| 157 |
+
/* ๐บ๏ธ ์์ธ ํ๋ฉด */
|
| 158 |
+
#detail-page {{
|
| 159 |
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 160 |
+
transform: translateX(100%); transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
| 161 |
+
z-index: 6000; display: flex; flex-direction: column; background: #111;
|
| 162 |
}}
|
| 163 |
|
| 164 |
.nav-bar {{
|
| 165 |
+
height: 50px; background: rgba(0,0,0,0.9); border-bottom: 1px solid #333;
|
| 166 |
+
display: flex; align-items: center; padding: 0 15px; justify-content: space-between;
|
| 167 |
}}
|
| 168 |
+
.back-btn {{ background: none; border: none; color: var(--gold); font-size: 24px; cursor: pointer; padding: 0; }}
|
| 169 |
+
.nav-title {{ color: white; font-weight: bold; font-size: 16px; }}
|
| 170 |
|
| 171 |
+
#map {{ width: 100%; height: 35%; border-bottom: 2px solid var(--gold); }}
|
| 172 |
|
| 173 |
+
.report-container {{
|
| 174 |
+
flex: 1; overflow-y: auto; padding: 25px 20px 80px 20px; box-sizing: border-box;
|
|
|
|
| 175 |
}}
|
| 176 |
|
| 177 |
+
.report-title {{ font-size: 26px; color: var(--gold); font-weight: bold; margin-bottom: 5px; font-family: serif; }}
|
| 178 |
+
.report-meta {{ font-size: 13px; color: #666; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }}
|
| 179 |
+
|
| 180 |
+
.ai-content {{ font-size: 15px; color: #ddd; line-height: 1.8; white-space: pre-wrap; text-align: justify; }}
|
| 181 |
+
|
| 182 |
+
.gmap-btn {{
|
| 183 |
+
display: block; width: 90%; margin: 0 auto; padding: 15px 0;
|
| 184 |
+
background: var(--gold); color: black; text-align: center;
|
| 185 |
+
font-weight: bold; text-decoration: none; border-radius: 30px;
|
| 186 |
+
position: fixed; bottom: 20px; left: 5%; z-index: 7000;
|
| 187 |
+
box-shadow: 0 5px 20px rgba(212, 175, 55, 0.4);
|
| 188 |
+
transition: transform 0.2s;
|
|
|
|
| 189 |
}}
|
| 190 |
+
.gmap-btn:active {{ transform: scale(0.95); }}
|
| 191 |
|
| 192 |
+
/* ๋ก๋ฉ */
|
| 193 |
.loading-screen {{
|
| 194 |
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
| 195 |
+
background: rgba(0,0,0,0.9); backdrop-filter: blur(5px);
|
| 196 |
+
z-index: 8000; display: none; flex-direction: column; justify-content: center; align-items: center;
|
| 197 |
}}
|
| 198 |
+
.loader-ring {{ width: 50px; height: 50px; border: 3px solid #333; border-top: 3px solid var(--gold); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; }}
|
| 199 |
+
.loader-text {{ color: var(--gold); font-family: monospace; font-size: 14px; text-align: center; line-height: 1.5; }}
|
| 200 |
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
| 201 |
|
| 202 |
</style>
|
|
|
|
| 204 |
<body>
|
| 205 |
|
| 206 |
<div id="main-page">
|
| 207 |
+
<div class="brand-header">
|
| 208 |
+
<h1 class="brand-title">DRUNKEN PLANE</h1>
|
| 209 |
+
<div class="brand-subtitle">GLOBAL ALCOHOL HERITAGE TOUR</div>
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
|
| 212 |
+
<div class="country-selector-wrapper">
|
| 213 |
+
<div class="country-selector" id="country-tabs"></div>
|
|
|
|
| 214 |
</div>
|
| 215 |
|
| 216 |
+
<div class="place-list-container" id="place-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
</div>
|
| 218 |
|
| 219 |
+
<div id="detail-page">
|
| 220 |
<div class="nav-bar">
|
| 221 |
+
<button class="back-btn" onclick="goBack()">โ BACK</button>
|
| 222 |
+
<div class="nav-title">TOUR GUIDE</div>
|
| 223 |
+
<div style="width:40px;"></div>
|
| 224 |
</div>
|
| 225 |
+
|
| 226 |
<div id="map"></div>
|
| 227 |
+
|
| 228 |
+
<div class="report-container">
|
| 229 |
+
<div id="repo-title" class="report-title"></div>
|
| 230 |
+
<div id="repo-meta" class="report-meta"></div>
|
| 231 |
+
<div id="repo-content" class="ai-content"></div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<a id="gmap-link" href="#" target="_blank" class="gmap-btn">๐บ๏ธ Google Maps์์ ์์น ํ์ธ</a>
|
| 235 |
</div>
|
| 236 |
|
| 237 |
+
<div id="loader" class="loading-screen">
|
| 238 |
<div class="loader-ring"></div>
|
| 239 |
+
<div id="loader-msg" class="loader-text">AI ์ฌํ์๊ฐ๊ฐ<br>ํ์ง ๋ฆฌํฌํธ๋ฅผ ์์ฑ ์ค์
๋๋ค...</div>
|
| 240 |
</div>
|
| 241 |
|
| 242 |
<script>
|
| 243 |
const HF_TOKEN = "{token}";
|
|
|
|
| 244 |
const MODEL_ID = "Qwen/Qwen2.5-32B-Instruct";
|
| 245 |
|
| 246 |
+
// ํ์ด์ฌ์์ ์์ฑํ ๊ฑฐ๋ DB ์ฃผ์
|
| 247 |
+
const PLACE_DB = {db_json};
|
| 248 |
+
|
| 249 |
let map = null;
|
| 250 |
+
let currentMarker = null;
|
| 251 |
+
|
| 252 |
+
window.onload = function() {{
|
| 253 |
+
renderCountryTabs();
|
| 254 |
+
// GPS๋ก ๊ฐ๊น์ด ๊ตญ๊ฐ ์ฐพ๊ธฐ ๊ธฐ๋ฅ ๋์ , ์ผ๋จ ์ฒซ๋ฒ์งธ ๊ตญ๊ฐ ์๋ ์ ํ (๋ง๋ ์ด์์)
|
| 255 |
+
const firstCountry = Object.keys(PLACE_DB)[0];
|
| 256 |
+
selectCountry(firstCountry);
|
| 257 |
+
}};
|
| 258 |
+
|
| 259 |
+
function renderCountryTabs() {{
|
| 260 |
+
const tabs = document.getElementById('country-tabs');
|
| 261 |
+
tabs.innerHTML = "";
|
| 262 |
+
Object.keys(PLACE_DB).forEach(country => {{
|
| 263 |
+
const btn = document.createElement('div');
|
| 264 |
+
btn.className = 'country-btn';
|
| 265 |
+
btn.innerText = country;
|
| 266 |
+
btn.id = `tab-${{country.replace(/ /g, "_")}}`; // ID ๊ณต๋ฐฑ ์ ๊ฑฐ
|
| 267 |
+
btn.onclick = () => selectCountry(country);
|
| 268 |
+
tabs.appendChild(btn);
|
| 269 |
+
}});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
}}
|
| 271 |
|
| 272 |
+
function selectCountry(country) {{
|
| 273 |
+
// ํญ ์คํ์ผ
|
| 274 |
+
document.querySelectorAll('.country-btn').forEach(b => b.classList.remove('active'));
|
| 275 |
+
document.getElementById(`tab-${{country.replace(/ /g, "_")}}`).classList.add('active');
|
| 276 |
|
| 277 |
+
// ๋ฆฌ์คํธ ๋ ๋๋ง
|
| 278 |
+
const list = document.getElementById('place-list');
|
| 279 |
+
list.innerHTML = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
+
PLACE_DB[country].forEach(place => {{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
const card = document.createElement('div');
|
| 283 |
+
card.className = 'place-card';
|
| 284 |
+
card.onclick = () => openDetail(place, country);
|
| 285 |
|
| 286 |
card.innerHTML = `
|
| 287 |
+
<div class="pc-header">
|
| 288 |
+
<span class="pc-name">${{place.name}}</span>
|
| 289 |
+
<span class="pc-type">${{place.type}}</span>
|
| 290 |
+
</div>
|
| 291 |
+
<div class="pc-desc">${{place.desc}}</div>
|
| 292 |
+
<div class="pc-cta">READ AI REPORT โ</div>
|
| 293 |
`;
|
| 294 |
+
list.appendChild(card);
|
| 295 |
}});
|
| 296 |
}}
|
| 297 |
|
| 298 |
+
async function openDetail(place, country) {{
|
| 299 |
+
// ํ๋ฉด ์ ํ
|
| 300 |
+
document.getElementById('main-page').style.transform = 'translateX(-30%)';
|
| 301 |
+
document.getElementById('detail-page').style.transform = 'translateX(0%)';
|
| 302 |
+
document.getElementById('loader').style.display = 'flex';
|
| 303 |
+
|
| 304 |
+
// ๊ธฐ๋ณธ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ
|
| 305 |
+
document.getElementById('repo-title').innerText = place.name;
|
| 306 |
+
document.getElementById('repo-meta').innerText = `${{place.type}} โข ${{country}}`;
|
| 307 |
+
document.getElementById('gmap-link').href = `https://www.google.com/maps/search/?api=1&query=${{place.lat}},${{place.lon}}`;
|
| 308 |
+
document.getElementById('repo-content').innerHTML = "";
|
| 309 |
|
| 310 |
+
// ์ง๋ ์ค์ (100% ๋ณด์ฅ๋ ์ขํ ์ฌ์ฉ)
|
| 311 |
+
if (!map) {{
|
| 312 |
+
map = L.map('map');
|
| 313 |
+
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{ attribution: 'ยฉ OpenStreetMap' }}).addTo(map);
|
| 314 |
+
}}
|
| 315 |
+
// ์ง๋ ๋ฆฌ์ฌ์ด์ฆ ํ์ (ํ๋ฉด ์ ํ ํ ๊นจ์ง ๋ฐฉ์ง)
|
| 316 |
+
setTimeout(() => {{
|
| 317 |
+
map.invalidateSize();
|
| 318 |
+
map.setView([place.lat, place.lon], 15);
|
| 319 |
+
if (currentMarker) map.removeLayer(currentMarker);
|
| 320 |
+
currentMarker = L.marker([place.lat, place.lon]).addTo(map).bindPopup(place.name).openPopup();
|
| 321 |
+
}}, 300);
|
| 322 |
+
|
| 323 |
+
// AI ๋ฆฌํฌํธ ์์ฑ
|
| 324 |
+
await generateAIReport(place, country);
|
| 325 |
+
}}
|
| 326 |
+
|
| 327 |
+
async function generateAIReport(place, country) {{
|
| 328 |
+
const prompt = `
|
| 329 |
+
Act as a professional travel writer for 'Lonely Planet'.
|
| 330 |
+
Write a fascinating, detailed article about "${{place.name}}" in ${{country}}.
|
| 331 |
|
| 332 |
+
Structure:
|
| 333 |
+
1. **History & Story**: Why is this place legendary? (Origin story)
|
| 334 |
+
2. **The Experience**: What exactly can a visitor drink, see, or smell here?
|
| 335 |
+
3. **Sommelier's Tip**: Best menu/drink to order or buy.
|
|
|
|
| 336 |
|
| 337 |
+
Tone: Sophisticated, immersive, and informative.
|
| 338 |
+
Language: Korean (ํ๊ตญ์ด).
|
| 339 |
+
Format: Clean text with paragraphs. No markdown bolding needed.
|
|
|
|
| 340 |
`;
|
| 341 |
|
| 342 |
try {{
|
| 343 |
+
const res = await fetch(`https://api-inference.huggingface.co/models/${{MODEL_ID}}`, {{
|
| 344 |
+
method: "POST",
|
| 345 |
+
headers: {{ "Authorization": `Bearer ${{HF_TOKEN}}`, "Content-Type": "application/json" }},
|
| 346 |
+
body: JSON.stringify({{ inputs: prompt, parameters: {{ max_new_tokens: 1500, return_full_text: false }} }})
|
| 347 |
+
}});
|
| 348 |
+
const json = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
|
| 350 |
+
if(json.error) throw new Error(json.error);
|
| 351 |
+
const text = json[0].generated_text;
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
+
document.getElementById('loader').style.display = 'none';
|
| 354 |
+
typeWriter(text);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
}} catch(e) {{
|
| 357 |
+
console.error(e);
|
| 358 |
+
document.getElementById('loader').style.display = 'none';
|
| 359 |
+
document.getElementById('repo-content').innerHTML = `
|
| 360 |
+
<div style="color:#aaa; padding:20px;">
|
| 361 |
+
AI ๋ฆฌํฌํฐ ์ฐ๊ฒฐ์ด ์ง์ฐ๋๊ณ ์์ต๋๋ค.<br>
|
| 362 |
+
ํ์ง๋ง ์ด๊ณณ์ <b>${{place.desc}}</b>๋ก ์ ๋ช
ํ ๋ช
์์
๋๋ค.<br>
|
| 363 |
+
์๋ ๊ตฌ๊ธ๋งต ๋ฒํผ์ ๋๋ฌ ์ง์ ํ์ธํด๋ณด์ธ์!
|
| 364 |
+
</div>
|
| 365 |
+
`;
|
| 366 |
}}
|
| 367 |
}}
|
| 368 |
|
| 369 |
+
function typeWriter(text) {{
|
| 370 |
+
const el = document.getElementById('repo-content');
|
| 371 |
+
let i = 0;
|
| 372 |
+
el.innerHTML = "";
|
| 373 |
+
const speed = 5;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
+
function type() {{
|
| 376 |
+
if (i < text.length) {{
|
| 377 |
+
el.innerHTML += text.charAt(i);
|
| 378 |
+
i++;
|
| 379 |
+
setTimeout(type, speed);
|
| 380 |
+
}}
|
| 381 |
+
}}
|
| 382 |
+
type();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
}}
|
| 384 |
|
| 385 |
function goBack() {{
|
| 386 |
+
document.getElementById('detail-page').style.transform = 'translateX(100%)';
|
| 387 |
document.getElementById('main-page').style.transform = 'translateX(0%)';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
}}
|
| 389 |
|
| 390 |
</script>
|