Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,14 +8,13 @@ except:
|
|
| 8 |
token = ""
|
| 9 |
st.error("Settingsμμ 'HF_TOKEN'μ λ¨Όμ μ€μ ν΄μ£ΌμΈμ!")
|
| 10 |
|
| 11 |
-
# 2. νμ΄μ§ μ€μ
|
| 12 |
st.set_page_config(page_title="Drunken Plane", page_icon="π₯", layout="wide")
|
| 13 |
|
| 14 |
st.markdown("""
|
| 15 |
<style>
|
| 16 |
.block-container { padding: 0 !important; margin: 0 !important; max-width: 100% !important; }
|
| 17 |
-
header {
|
| 18 |
-
footer { visibility: hidden; }
|
| 19 |
iframe { height: 100vh !important; display: block; }
|
| 20 |
</style>
|
| 21 |
""", unsafe_allow_html=True)
|
|
@@ -33,126 +32,170 @@ html_code = f"""
|
|
| 33 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 34 |
|
| 35 |
<style>
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
/*
|
| 42 |
-
|
| 43 |
-
position: absolute; top:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
}}
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}}
|
| 53 |
-
.search-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
box-shadow: 0 4px 15px rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; font-size: 20px;
|
| 57 |
}}
|
| 58 |
|
| 59 |
/* νλ¨ μ»¨νΈλ‘€ */
|
| 60 |
-
.bottom-
|
| 61 |
-
position: absolute; bottom: 40px; left: 0; width: 100%;
|
| 62 |
-
|
| 63 |
-
|
| 64 |
}}
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
| 69 |
}}
|
| 70 |
.filter-chip {{
|
| 71 |
-
background: rgba(
|
| 72 |
-
padding:
|
| 73 |
-
font-size: 0.9rem; font-weight: 600; transition: all 0.2s;
|
| 74 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
| 75 |
-
}}
|
| 76 |
-
.filter-chip.active {{
|
| 77 |
-
background: var(--copper); color: #000; border-color: var(--copper);
|
| 78 |
}}
|
|
|
|
| 79 |
|
|
|
|
| 80 |
.gps-btn {{
|
| 81 |
-
width:
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
font-size: 28px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.5);
|
| 85 |
-
margin-left: 10px;
|
| 86 |
}}
|
| 87 |
|
| 88 |
/* 리μ€νΈ ν¨λ */
|
| 89 |
.list-panel {{
|
| 90 |
-
position: absolute; bottom: 0; left: 0; width: 100%; max-height:
|
| 91 |
-
background: rgba(
|
| 92 |
-
border-top-left-radius:
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
box-shadow: 0 -10px 30px rgba(0,0,0,0.7);
|
| 96 |
}}
|
| 97 |
-
.panel-
|
|
|
|
| 98 |
|
| 99 |
-
.place-item {{ padding: 15px 0; border-bottom: 1px solid #333;
|
| 100 |
-
.place-idx {{ font-size: 1.2rem; font-weight:
|
| 101 |
.place-info {{ flex: 1; }}
|
| 102 |
-
.place-name {{
|
| 103 |
-
.place-
|
| 104 |
-
.place-reason {{ font-size: 0.9rem;
|
| 105 |
-
|
| 106 |
-
/* μ¬κ²μ λ²νΌ */
|
| 107 |
-
.rescan-pill {{
|
| 108 |
-
position: absolute; top: 90px; left: 50%; transform: translateX(-50%);
|
| 109 |
-
z-index: 999; background: white; color: black; border: none;
|
| 110 |
-
padding: 10px 20px; border-radius: 30px; font-weight: bold; font-size: 0.9rem;
|
| 111 |
-
box-shadow: 0 4px 15px rgba(0,0,0,0.3); cursor: pointer; display: none;
|
| 112 |
-
}}
|
| 113 |
|
| 114 |
/* λ‘λ© */
|
| 115 |
.loader {{
|
| 116 |
-
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.
|
| 117 |
-
z-index:
|
| 118 |
}}
|
| 119 |
-
.spinner {{
|
| 120 |
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
| 121 |
|
| 122 |
-
.gmap-btn {{ display: block; margin-top: 8px; text-decoration: none;
|
| 123 |
</style>
|
| 124 |
</head>
|
| 125 |
<body>
|
| 126 |
|
| 127 |
-
<div id="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
|
| 134 |
-
<
|
|
|
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
<
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
<div class="filter-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
-
<div class="gps-btn" onclick="findMe()">π§</div>
|
| 145 |
-
</div>
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
| 151 |
</div>
|
| 152 |
|
| 153 |
<div id="loading" class="loader">
|
| 154 |
<div class="spinner"></div>
|
| 155 |
-
<div id="loading-text" style="margin-top:20px; font-weight:bold;">
|
| 156 |
</div>
|
| 157 |
|
| 158 |
<script>
|
|
@@ -166,85 +209,121 @@ html_code = f"""
|
|
| 166 |
let myLocMarker = null;
|
| 167 |
let currentCategory = 'All';
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
function
|
| 174 |
-
|
| 175 |
-
document.
|
| 176 |
-
|
| 177 |
-
scanCurrentView();
|
| 178 |
}}
|
| 179 |
|
|
|
|
| 180 |
function findMe() {{
|
| 181 |
-
if (!navigator.geolocation) return alert("GPS λ―Έμ§μ
|
| 182 |
-
showLoading("λ΄ μμΉ
|
| 183 |
|
| 184 |
navigator.geolocation.getCurrentPosition(
|
| 185 |
(pos) => {{
|
|
|
|
| 186 |
const lat = pos.coords.latitude;
|
| 187 |
const lng = pos.coords.longitude;
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}},
|
| 193 |
-
() => {{ hideLoading(); alert("μμΉ κΆν
|
| 194 |
);
|
| 195 |
}}
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
if(!q) return;
|
| 200 |
showLoading("μ§μ μ΄λ μ€...");
|
| 201 |
-
document.getElementById('rescan-btn').style.display = 'none';
|
| 202 |
-
closePanel();
|
| 203 |
-
|
| 204 |
try {{
|
| 205 |
-
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${{
|
| 206 |
const data = await res.json();
|
| 207 |
if(data.length > 0) {{
|
|
|
|
| 208 |
const b = data[0].boundingbox;
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
}} catch(e) {{ alert("κ²μ μ€λ₯"); hideLoading(); }}
|
| 213 |
}}
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
async function scanCurrentView() {{
|
| 216 |
showLoading(`'${{currentCategory}}' μ₯μ μ°Ύλ μ€...`);
|
| 217 |
-
|
| 218 |
-
|
| 219 |
const b = map.getBounds();
|
| 220 |
const bbox = `${{b.getSouth()}},${{b.getWest()}},${{b.getNorth()}},${{b.getEast()}}`;
|
| 221 |
|
| 222 |
let query = "";
|
| 223 |
if (currentCategory === 'Whiskey') {{
|
| 224 |
-
query = `node["amenity"
|
| 225 |
}} else if (currentCategory === 'Beer') {{
|
| 226 |
query = `node["amenity"~"pub|biergarten"](${{bbox}}); node["craft"="brewery"](${{bbox}});`;
|
| 227 |
}} else if (currentCategory === 'Wine') {{
|
| 228 |
query = `node["amenity"="wine_bar"](${{bbox}}); node["craft"="winery"](${{bbox}}); node["shop"="wine"](${{bbox}});`;
|
| 229 |
-
}} else if (currentCategory === '
|
| 230 |
-
query = `node["
|
| 231 |
}} else {{
|
|
|
|
| 232 |
query = `
|
| 233 |
node["craft"~"distillery|brewery|winery"](${{bbox}});
|
| 234 |
-
node["amenity"~"bar|pub|biergarten|wine_bar"](${{bbox}});
|
| 235 |
node["shop"~"alcohol|wine"](${{bbox}});
|
| 236 |
`;
|
| 237 |
}}
|
| 238 |
-
|
|
|
|
| 239 |
const finalQuery = `[out:json][timeout:25]; ( ${{query}} ); out center 40;`;
|
| 240 |
|
| 241 |
try {{
|
| 242 |
const res = await fetch('https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(finalQuery));
|
| 243 |
const data = await res.json();
|
| 244 |
|
| 245 |
-
const banned = ['sauna', 'massage', 'spa', 'mart', 'convenience', '7-eleven', 'cu', 'gs25', 'supermarket', 'gym', 'bath'];
|
| 246 |
|
| 247 |
-
|
| 248 |
id: el.id, name: el.tags.name || "Unknown",
|
| 249 |
type: el.tags.amenity || el.tags.craft || el.tags.shop || "Venue",
|
| 250 |
lat: el.lat || el.center.lat, lng: el.lon || el.center.lon
|
|
@@ -253,28 +332,31 @@ html_code = f"""
|
|
| 253 |
.filter(p => !banned.some(b => p.name.toLowerCase().includes(b)));
|
| 254 |
|
| 255 |
if(places.length === 0) {{
|
| 256 |
-
alert("μ΄ νλ©΄ μμ κ΄λ ¨λ
|
| 257 |
hideLoading(); return;
|
| 258 |
}}
|
| 259 |
|
| 260 |
await rankWithAI(places);
|
| 261 |
|
| 262 |
-
}} catch(e) {{
|
|
|
|
|
|
|
|
|
|
| 263 |
}}
|
| 264 |
|
|
|
|
| 265 |
async function rankWithAI(places) {{
|
| 266 |
-
|
| 267 |
-
showLoading(`AIκ° ${{places.length}}κ³³ μ€ Top 10 μμ μ€...`);
|
| 268 |
|
| 269 |
const listStr = places.map((p, i) => `${{i}}. ${{p.name}} (${{p.type}})`).join('\\n');
|
| 270 |
const prompt = `
|
| 271 |
-
User wants
|
| 272 |
Candidates:
|
| 273 |
${{listStr}}
|
| 274 |
|
| 275 |
Task:
|
| 276 |
-
1. Select Top 10
|
| 277 |
-
2.
|
| 278 |
3. Reason in Korean.
|
| 279 |
|
| 280 |
JSON ONLY: [ {{ "index": 0, "reason": "Reason" }} ]
|
|
@@ -289,7 +371,8 @@ html_code = f"""
|
|
| 289 |
|
| 290 |
const text = json[0].generated_text;
|
| 291 |
const start = text.indexOf('['); const end = text.lastIndexOf(']') + 1;
|
| 292 |
-
|
|
|
|
| 293 |
|
| 294 |
const selection = JSON.parse(text.substring(start, end));
|
| 295 |
const finalPlaces = selection.map(s => ({{ ...places[s.index], reason: s.reason }})).slice(0, 10);
|
|
@@ -297,9 +380,9 @@ html_code = f"""
|
|
| 297 |
renderResults(finalPlaces);
|
| 298 |
|
| 299 |
}} catch(e) {{
|
| 300 |
-
|
| 301 |
-
const
|
| 302 |
-
renderResults(
|
| 303 |
}}
|
| 304 |
hideLoading();
|
| 305 |
}}
|
|
@@ -317,18 +400,19 @@ html_code = f"""
|
|
| 317 |
iconSize: [30, 30], iconAnchor: [15, 15]
|
| 318 |
}});
|
| 319 |
const gmap = `https://www.google.com/maps/search/?api=1&query=$${{encodeURIComponent(p.name)}}&query_place_id=${{p.lat}},${{p.lng}}`;
|
| 320 |
-
const popup = `<b>${{idx+1}}. ${{p.name}}</b><br><small>${{p.type}}</small><br>${{p.reason}}<br><a href="${{gmap}}" target="_blank"
|
| 321 |
|
| 322 |
const m = L.marker([p.lat, p.lng], {{icon: icon}}).addTo(map).bindPopup(popup);
|
| 323 |
markers.push(m);
|
| 324 |
|
| 325 |
const item = document.createElement('div');
|
| 326 |
item.className = 'place-item';
|
| 327 |
-
item.onclick = () => {{ map.setView([p.lat, p.lng], 16); m.openPopup();
|
| 328 |
item.innerHTML = `
|
| 329 |
<div class="place-idx">${{idx+1}}</div>
|
| 330 |
<div class="place-info">
|
| 331 |
-
<div
|
|
|
|
| 332 |
<div class="place-reason">${{p.reason}}</div>
|
| 333 |
</div>
|
| 334 |
`;
|
|
@@ -338,16 +422,18 @@ html_code = f"""
|
|
| 338 |
document.getElementById('list-panel').style.transform = 'translateY(0%)';
|
| 339 |
}}
|
| 340 |
|
| 341 |
-
function closePanel(
|
| 342 |
-
|
| 343 |
-
document.getElementById('list-panel').style.transform = `translateY(${{val}})`;
|
| 344 |
}}
|
| 345 |
|
| 346 |
function showLoading(msg) {{ document.getElementById('loading').style.display = 'flex'; document.getElementById('loading-text').innerText = msg; }}
|
| 347 |
function hideLoading() {{ document.getElementById('loading').style.display = 'none'; }}
|
| 348 |
|
| 349 |
const style = document.createElement('style');
|
| 350 |
-
style.innerHTML = `
|
|
|
|
|
|
|
|
|
|
| 351 |
document.head.appendChild(style);
|
| 352 |
</script>
|
| 353 |
</body>
|
|
|
|
| 8 |
token = ""
|
| 9 |
st.error("Settingsμμ 'HF_TOKEN'μ λ¨Όμ μ€μ ν΄μ£ΌμΈμ!")
|
| 10 |
|
| 11 |
+
# 2. νμ΄μ§ μ€μ (μ¬λ°± μ κ±°)
|
| 12 |
st.set_page_config(page_title="Drunken Plane", page_icon="π₯", layout="wide")
|
| 13 |
|
| 14 |
st.markdown("""
|
| 15 |
<style>
|
| 16 |
.block-container { padding: 0 !important; margin: 0 !important; max-width: 100% !important; }
|
| 17 |
+
header, footer { display: none !important; }
|
|
|
|
| 18 |
iframe { height: 100vh !important; display: block; }
|
| 19 |
</style>
|
| 20 |
""", unsafe_allow_html=True)
|
|
|
|
| 32 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 33 |
|
| 34 |
<style>
|
| 35 |
+
/* κΈ°λ³Έ ν
λ§ */
|
| 36 |
+
:root {{ --copper: #c5a059; --dark: #121212; --bg: #000; --text: #e0e0e0; }}
|
| 37 |
+
body {{ margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: -apple-system, sans-serif; overflow: hidden; }}
|
| 38 |
+
|
| 39 |
+
/* π 1. λλ© νμ΄μ§ */
|
| 40 |
+
#landing-page {{
|
| 41 |
+
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 42 |
+
background: radial-gradient(circle at center, #1a1a1a 0%, #000 100%);
|
| 43 |
+
z-index: 3000; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 44 |
+
transition: transform 0.4s ease-in-out;
|
| 45 |
+
}}
|
| 46 |
+
.logo-area {{ text-align: center; margin-bottom: 30px; }}
|
| 47 |
+
.app-title {{ font-size: 28px; font-weight: 900; color: var(--copper); letter-spacing: 2px; margin: 0; }}
|
| 48 |
|
| 49 |
+
.landing-search-box {{ width: 80%; max-width: 400px; margin-bottom: 30px; }}
|
| 50 |
+
.landing-input {{
|
| 51 |
+
width: 100%; padding: 15px; border-radius: 30px; border: 1px solid #333;
|
| 52 |
+
background: rgba(255,255,255,0.1); color: white; font-size: 16px; text-align: center;
|
| 53 |
+
}}
|
| 54 |
+
.landing-input:focus {{ outline: 2px solid var(--copper); background: rgba(255,255,255,0.15); }}
|
| 55 |
+
|
| 56 |
+
.landing-btn-group {{ display: flex; gap: 20px; }}
|
| 57 |
+
.landing-btn {{
|
| 58 |
+
background: transparent; border: 1px solid #333; color: var(--copper);
|
| 59 |
+
width: 100px; height: 100px; border-radius: 15px; cursor: pointer;
|
| 60 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 61 |
+
transition: all 0.2s;
|
| 62 |
+
}}
|
| 63 |
+
.landing-btn:active {{ background: #222; transform: scale(0.95); }}
|
| 64 |
|
| 65 |
+
/* πΊοΈ 2. μ§λ νμ΄μ§ */
|
| 66 |
+
#map-page {{
|
| 67 |
+
position: absolute; top: 0; left: 0; width: 100%; height: 100vh;
|
| 68 |
+
transform: translateX(100%); transition: transform 0.4s ease-in-out;
|
| 69 |
+
z-index: 1000;
|
| 70 |
}}
|
| 71 |
+
#map {{ width: 100%; height: 100%; }}
|
| 72 |
+
|
| 73 |
+
/* μλ¨ μ»¨νΈλ‘€ */
|
| 74 |
+
.map-top-bar {{
|
| 75 |
+
position: absolute; top: 20px; left: 0; width: 100%;
|
| 76 |
+
display: flex; justify-content: space-between; padding: 0 15px;
|
| 77 |
+
z-index: 2000; pointer-events: none;
|
| 78 |
+
}}
|
| 79 |
+
.back-btn {{
|
| 80 |
+
background: rgba(0,0,0,0.8); color: var(--copper); border: 1px solid var(--copper);
|
| 81 |
+
padding: 8px 15px; border-radius: 20px; font-weight: bold; cursor: pointer; pointer-events: auto;
|
| 82 |
}}
|
| 83 |
+
.map-search-input {{
|
| 84 |
+
flex: 1; margin: 0 10px; background: rgba(0,0,0,0.8); border: 1px solid #444;
|
| 85 |
+
color: white; border-radius: 20px; padding: 0 15px; pointer-events: auto;
|
|
|
|
| 86 |
}}
|
| 87 |
|
| 88 |
/* νλ¨ μ»¨νΈλ‘€ */
|
| 89 |
+
.map-bottom-bar {{
|
| 90 |
+
position: absolute; bottom: 40px; left: 0; width: 100%;
|
| 91 |
+
display: flex; align-items: flex-end; justify-content: space-between;
|
| 92 |
+
padding: 0 15px; z-index: 2000; pointer-events: none;
|
| 93 |
}}
|
| 94 |
+
|
| 95 |
+
/* νν° μΉ© */
|
| 96 |
+
.filter-scroll {{
|
| 97 |
+
display: flex; gap: 8px; overflow-x: auto; max-width: 75%;
|
| 98 |
+
pointer-events: auto; padding-bottom: 5px; scrollbar-width: none;
|
| 99 |
}}
|
| 100 |
.filter-chip {{
|
| 101 |
+
background: rgba(20,20,20,0.95); color: #bbb; border: 1px solid #444;
|
| 102 |
+
padding: 8px 16px; border-radius: 20px; font-size: 14px; white-space: nowrap; cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}}
|
| 104 |
+
.filter-chip.active {{ background: var(--copper); color: #000; border-color: var(--copper); font-weight: bold; }}
|
| 105 |
|
| 106 |
+
/* GPS λ²νΌ */
|
| 107 |
.gps-btn {{
|
| 108 |
+
width: 50px; height: 50px; background: white; border-radius: 50%;
|
| 109 |
+
display: flex; justify-content: center; align-items: center; font-size: 24px;
|
| 110 |
+
cursor: pointer; pointer-events: auto; box-shadow: 0 4px 10px rgba(0,0,0,0.5);
|
|
|
|
|
|
|
| 111 |
}}
|
| 112 |
|
| 113 |
/* 리μ€νΈ ν¨λ */
|
| 114 |
.list-panel {{
|
| 115 |
+
position: absolute; bottom: 0; left: 0; width: 100%; max-height: 45vh;
|
| 116 |
+
background: rgba(18,18,18,0.98); z-index: 2001;
|
| 117 |
+
border-top-left-radius: 20px; border-top-right-radius: 20px;
|
| 118 |
+
transform: translateY(110%); transition: transform 0.3s;
|
| 119 |
+
display: flex; flex-direction: column;
|
|
|
|
| 120 |
}}
|
| 121 |
+
.panel-header {{ padding: 15px; text-align: center; border-bottom: 1px solid #333; }}
|
| 122 |
+
.panel-content {{ overflow-y: auto; padding: 0 15px 30px 15px; }}
|
| 123 |
|
| 124 |
+
.place-item {{ padding: 15px 0; border-bottom: 1px solid #333; display: flex; cursor: pointer; }}
|
| 125 |
+
.place-idx {{ font-size: 1.2rem; font-weight: bold; color: var(--copper); margin-right: 15px; }}
|
| 126 |
.place-info {{ flex: 1; }}
|
| 127 |
+
.place-name {{ color: white; font-weight: bold; }}
|
| 128 |
+
.place-sub {{ color: #888; font-size: 0.8rem; margin-top: 4px; }}
|
| 129 |
+
.place-reason {{ color: #ccc; font-size: 0.9rem; margin-top: 6px; line-height: 1.4; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
/* λ‘λ© */
|
| 132 |
.loader {{
|
| 133 |
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9);
|
| 134 |
+
z-index: 4000; display: none; justify-content: center; align-items: center; flex-direction: column; color: var(--copper);
|
| 135 |
}}
|
| 136 |
+
.spinner {{ border: 3px solid #333; border-top: 3px solid var(--copper); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; }}
|
| 137 |
@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
|
| 138 |
|
| 139 |
+
.gmap-btn {{ display: block; margin-top: 8px; color: #4285F4; text-decoration: none; font-size: 0.9rem; font-weight: bold; }}
|
| 140 |
</style>
|
| 141 |
</head>
|
| 142 |
<body>
|
| 143 |
|
| 144 |
+
<div id="landing-page">
|
| 145 |
+
<div class="logo-area">
|
| 146 |
+
<svg style="width:80px; height:80px; fill:#c5a059;" viewBox="0 0 24 24"><path d="M12,2A2,2 0 0,0 10,4V7H14V4A2,2 0 0,0 12,2M7,12V15C7,17.5 9,19.57 11.5,19.91V21H9V23H15V21H12.5V19.91C15,19.57 17,17.5 17,15V12H7M7,10H17V8H7V10Z"/></svg>
|
| 147 |
+
<h1 class="app-title">DRUNKEN PLANE</h1>
|
| 148 |
+
<p style="color:#666;">Global Alcohol Map</p>
|
| 149 |
+
</div>
|
| 150 |
|
| 151 |
+
<div class="landing-search-box">
|
| 152 |
+
<input type="text" id="landing-input" class="landing-input" placeholder="μ΄λλ‘ κ°κΉμ? (μ: London, Tokyo)" onkeypress="if(event.key==='Enter') searchFromLanding()">
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="landing-btn-group">
|
| 156 |
+
<button class="landing-btn" onclick="findMe()">
|
| 157 |
+
<div style="font-size:30px;">π§</div>
|
| 158 |
+
<div style="font-size:12px; font-weight:bold;">λ΄ μ£Όλ³</div>
|
| 159 |
+
</button>
|
| 160 |
+
<button class="landing-btn" onclick="searchFromLanding()">
|
| 161 |
+
<div style="font-size:30px;">π</div>
|
| 162 |
+
<div style="font-size:12px; font-weight:bold;">κ²μ</div>
|
| 163 |
+
</button>
|
| 164 |
+
</div>
|
| 165 |
</div>
|
| 166 |
|
| 167 |
+
<div id="map-page">
|
| 168 |
+
<div id="map"></div>
|
| 169 |
|
| 170 |
+
<div class="map-top-bar">
|
| 171 |
+
<button class="back-btn" onclick="goHome()">π HOME</button>
|
| 172 |
+
<input type="text" id="map-input" class="map-search-input" placeholder="λ€λ₯Έ μ§μ κ²μ..." onkeypress="if(event.key==='Enter') searchFromMap()">
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<div class="map-bottom-bar">
|
| 176 |
+
<div class="filter-scroll">
|
| 177 |
+
<div class="filter-chip active" onclick="setFilter('All', this)">All</div>
|
| 178 |
+
<div class="filter-chip" onclick="setFilter('Whiskey', this)">Whiskey</div>
|
| 179 |
+
<div class="filter-chip" onclick="setFilter('Beer', this)">Beer</div>
|
| 180 |
+
<div class="filter-chip" onclick="setFilter('Wine', this)">Wine</div>
|
| 181 |
+
<div class="filter-chip" onclick="setFilter('Traditional', this)">Traditional</div>
|
| 182 |
+
<div class="filter-chip" onclick="setFilter('Cocktail', this)">Cocktail</div>
|
| 183 |
+
</div>
|
| 184 |
+
<div class="gps-btn" onclick="findMe()">π§</div>
|
| 185 |
</div>
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
<div id="list-panel" class="list-panel">
|
| 188 |
+
<div class="panel-header" onclick="closePanel()">
|
| 189 |
+
<div style="width:40px; height:4px; background:#444; margin:0 auto; border-radius:2px;"></div>
|
| 190 |
+
<div style="margin-top:10px; font-weight:bold; color:var(--copper)">π AI μΆμ² 리μ€νΈ</div>
|
| 191 |
+
</div>
|
| 192 |
+
<div id="place-list" class="panel-content"></div>
|
| 193 |
+
</div>
|
| 194 |
</div>
|
| 195 |
|
| 196 |
<div id="loading" class="loader">
|
| 197 |
<div class="spinner"></div>
|
| 198 |
+
<div id="loading-text" style="margin-top:20px; font-weight:bold;">LOCATING...</div>
|
| 199 |
</div>
|
| 200 |
|
| 201 |
<script>
|
|
|
|
| 209 |
let myLocMarker = null;
|
| 210 |
let currentCategory = 'All';
|
| 211 |
|
| 212 |
+
// π νλ©΄ μ ν
|
| 213 |
+
function goMap() {{
|
| 214 |
+
document.getElementById('landing-page').style.transform = 'translateX(-100%)';
|
| 215 |
+
document.getElementById('map-page').style.transform = 'translateX(0%)';
|
| 216 |
+
// π₯ ν΅μ¬: νλ©΄ μ ν ν μ§λ 리μ¬μ΄μ¦λ₯Ό ν΄μΌ μ΄λμ΄ λ¨Ήν
|
| 217 |
+
setTimeout(() => {{ map.invalidateSize(); }}, 300);
|
| 218 |
+
}}
|
| 219 |
|
| 220 |
+
function goHome() {{
|
| 221 |
+
document.getElementById('landing-page').style.transform = 'translateX(0%)';
|
| 222 |
+
document.getElementById('map-page').style.transform = 'translateX(100%)';
|
| 223 |
+
closePanel();
|
|
|
|
| 224 |
}}
|
| 225 |
|
| 226 |
+
// π§ GPS λ΄ μμΉ μ°ΎκΈ° (μ§λ μ΄λ 보μ₯)
|
| 227 |
function findMe() {{
|
| 228 |
+
if (!navigator.geolocation) return alert("GPS λ―Έμ§μ");
|
| 229 |
+
showLoading("λ΄ μμΉ νμΈ μ€...");
|
| 230 |
|
| 231 |
navigator.geolocation.getCurrentPosition(
|
| 232 |
(pos) => {{
|
| 233 |
+
goMap(); // μ§λ νλ©΄μΌλ‘ λ¨Όμ μ ν
|
| 234 |
const lat = pos.coords.latitude;
|
| 235 |
const lng = pos.coords.longitude;
|
| 236 |
+
|
| 237 |
+
// π₯ μ§λ 리μ¬μ΄μ¦ ν μ΄λ (νμ€νκ²)
|
| 238 |
+
setTimeout(() => {{
|
| 239 |
+
map.invalidateSize();
|
| 240 |
+
map.setView([lat, lng], 15);
|
| 241 |
+
|
| 242 |
+
if(myLocMarker) map.removeLayer(myLocMarker);
|
| 243 |
+
myLocMarker = L.circleMarker([lat, lng], {{ radius: 8, color: '#fff', fillColor: '#4285F4', fillOpacity: 1 }}).addTo(map).bindPopup("ME").openPopup();
|
| 244 |
+
|
| 245 |
+
setTimeout(scanCurrentView, 500); // μ΄λ ν μ€μΊ
|
| 246 |
+
}}, 400);
|
| 247 |
}},
|
| 248 |
+
() => {{ hideLoading(); alert("μμΉ κΆνμ μΌμ£ΌμΈμ."); }}
|
| 249 |
);
|
| 250 |
}}
|
| 251 |
|
| 252 |
+
// π μ£Όμ κ²μ (μ§λ μ΄λ 보μ₯)
|
| 253 |
+
async function runSearch(query) {{
|
|
|
|
| 254 |
showLoading("μ§μ μ΄λ μ€...");
|
|
|
|
|
|
|
|
|
|
| 255 |
try {{
|
| 256 |
+
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${{query}}&limit=1`);
|
| 257 |
const data = await res.json();
|
| 258 |
if(data.length > 0) {{
|
| 259 |
+
goMap(); // μ§λ νλ©΄ μ ν
|
| 260 |
const b = data[0].boundingbox;
|
| 261 |
+
|
| 262 |
+
// π₯ μ§λ 리μ¬μ΄μ¦ ν μ΄λ
|
| 263 |
+
setTimeout(() => {{
|
| 264 |
+
map.invalidateSize();
|
| 265 |
+
// Nominatim bbox: [minLat, maxLat, minLon, maxLon]
|
| 266 |
+
// Leaflet fitBounds: [[minLat, minLon], [maxLat, maxLon]]
|
| 267 |
+
map.fitBounds([[b[0], b[2]], [b[1], b[3]]]);
|
| 268 |
+
setTimeout(scanCurrentView, 800);
|
| 269 |
+
}}, 400);
|
| 270 |
+
}} else {{ alert("μ₯μ μμ"); hideLoading(); }}
|
| 271 |
}} catch(e) {{ alert("κ²μ μ€λ₯"); hideLoading(); }}
|
| 272 |
}}
|
| 273 |
|
| 274 |
+
function searchFromLanding() {{
|
| 275 |
+
const q = document.getElementById('landing-input').value;
|
| 276 |
+
if(q) runSearch(q);
|
| 277 |
+
}}
|
| 278 |
+
function searchFromMap() {{
|
| 279 |
+
const q = document.getElementById('map-input').value;
|
| 280 |
+
if(q) runSearch(q);
|
| 281 |
+
}}
|
| 282 |
+
|
| 283 |
+
// ποΈ νν°
|
| 284 |
+
function setFilter(cat, el) {{
|
| 285 |
+
currentCategory = cat;
|
| 286 |
+
document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
|
| 287 |
+
el.classList.add('active');
|
| 288 |
+
scanCurrentView();
|
| 289 |
+
}}
|
| 290 |
+
|
| 291 |
+
// π‘ λ°μ΄ν° μ€μΊ
|
| 292 |
async function scanCurrentView() {{
|
| 293 |
showLoading(`'${{currentCategory}}' μ₯μ μ°Ύλ μ€...`);
|
| 294 |
+
closePanel();
|
| 295 |
+
|
| 296 |
const b = map.getBounds();
|
| 297 |
const bbox = `${{b.getSouth()}},${{b.getWest()}},${{b.getNorth()}},${{b.getEast()}}`;
|
| 298 |
|
| 299 |
let query = "";
|
| 300 |
if (currentCategory === 'Whiskey') {{
|
| 301 |
+
query = `node["amenity"="bar"](${{bbox}}); node["craft"="distillery"](${{bbox}}); node["shop"="alcohol"](${{bbox}});`;
|
| 302 |
}} else if (currentCategory === 'Beer') {{
|
| 303 |
query = `node["amenity"~"pub|biergarten"](${{bbox}}); node["craft"="brewery"](${{bbox}});`;
|
| 304 |
}} else if (currentCategory === 'Wine') {{
|
| 305 |
query = `node["amenity"="wine_bar"](${{bbox}}); node["craft"="winery"](${{bbox}}); node["shop"="wine"](${{bbox}});`;
|
| 306 |
+
}} else if (currentCategory === 'Traditional') {{
|
| 307 |
+
query = `node["shop"="alcohol"](${{bbox}}); node["craft"~"distillery|brewery"](${{bbox}});`;
|
| 308 |
}} else {{
|
| 309 |
+
// All: λ²μ λκ²
|
| 310 |
query = `
|
| 311 |
node["craft"~"distillery|brewery|winery"](${{bbox}});
|
| 312 |
+
node["amenity"~"bar|pub|biergarten|wine_bar|lounge"](${{bbox}});
|
| 313 |
node["shop"~"alcohol|wine"](${{bbox}});
|
| 314 |
`;
|
| 315 |
}}
|
| 316 |
+
|
| 317 |
+
// 40κ° μ ν
|
| 318 |
const finalQuery = `[out:json][timeout:25]; ( ${{query}} ); out center 40;`;
|
| 319 |
|
| 320 |
try {{
|
| 321 |
const res = await fetch('https://overpass-api.de/api/interpreter?data=' + encodeURIComponent(finalQuery));
|
| 322 |
const data = await res.json();
|
| 323 |
|
| 324 |
+
const banned = ['sauna', 'massage', 'spa', 'mart', 'convenience', '7-eleven', 'cu', 'gs25', 'supermarket', 'gym', 'bath', 'starbucks', 'coffee', 'cafe'];
|
| 325 |
|
| 326 |
+
let places = data.elements.map(el => ({{
|
| 327 |
id: el.id, name: el.tags.name || "Unknown",
|
| 328 |
type: el.tags.amenity || el.tags.craft || el.tags.shop || "Venue",
|
| 329 |
lat: el.lat || el.center.lat, lng: el.lon || el.center.lon
|
|
|
|
| 332 |
.filter(p => !banned.some(b => p.name.toLowerCase().includes(b)));
|
| 333 |
|
| 334 |
if(places.length === 0) {{
|
| 335 |
+
alert("μ΄ νλ©΄ μμ κ΄λ ¨λ μ μ§μ΄ μμ΅λλ€.");
|
| 336 |
hideLoading(); return;
|
| 337 |
}}
|
| 338 |
|
| 339 |
await rankWithAI(places);
|
| 340 |
|
| 341 |
+
}} catch(e) {{
|
| 342 |
+
console.error(e);
|
| 343 |
+
alert("λ°μ΄ν° λ‘λ© μ€ν¨"); hideLoading();
|
| 344 |
+
}}
|
| 345 |
}}
|
| 346 |
|
| 347 |
+
// π§ AI λνΉ (μμ μ₯μΉ ν¬ν¨)
|
| 348 |
async function rankWithAI(places) {{
|
| 349 |
+
showLoading(`AIκ° ${{places.length}}κ³³ μ€ Best 10 μ λ³ μ€...`);
|
|
|
|
| 350 |
|
| 351 |
const listStr = places.map((p, i) => `${{i}}. ${{p.name}} (${{p.type}})`).join('\\n');
|
| 352 |
const prompt = `
|
| 353 |
+
Context: User wants [${{currentCategory}}].
|
| 354 |
Candidates:
|
| 355 |
${{listStr}}
|
| 356 |
|
| 357 |
Task:
|
| 358 |
+
1. Select Top 10 best places.
|
| 359 |
+
2. REJECT NON-ALCOHOL places (like Sauna, Massage, Cafe) strictly.
|
| 360 |
3. Reason in Korean.
|
| 361 |
|
| 362 |
JSON ONLY: [ {{ "index": 0, "reason": "Reason" }} ]
|
|
|
|
| 371 |
|
| 372 |
const text = json[0].generated_text;
|
| 373 |
const start = text.indexOf('['); const end = text.lastIndexOf(']') + 1;
|
| 374 |
+
|
| 375 |
+
if(start === -1) throw new Error("JSON Parse Fail");
|
| 376 |
|
| 377 |
const selection = JSON.parse(text.substring(start, end));
|
| 378 |
const finalPlaces = selection.map(s => ({{ ...places[s.index], reason: s.reason }})).slice(0, 10);
|
|
|
|
| 380 |
renderResults(finalPlaces);
|
| 381 |
|
| 382 |
}} catch(e) {{
|
| 383 |
+
// AI μ€ν¨μ μμ μ₯μΉ: 거리μ 10κ° νμ
|
| 384 |
+
const fallback = places.slice(0, 10).map(p => ({{...p, reason: "AI μ°κ²° μ§μ°: κΈ°λ³Έ λ°μ΄ν° νμ"}}));
|
| 385 |
+
renderResults(fallback);
|
| 386 |
}}
|
| 387 |
hideLoading();
|
| 388 |
}}
|
|
|
|
| 400 |
iconSize: [30, 30], iconAnchor: [15, 15]
|
| 401 |
}});
|
| 402 |
const gmap = `https://www.google.com/maps/search/?api=1&query=$${{encodeURIComponent(p.name)}}&query_place_id=${{p.lat}},${{p.lng}}`;
|
| 403 |
+
const popup = `<b>${{idx+1}}. ${{p.name}}</b><br><small>${{p.type}}</small><br>${{p.reason}}<br><a href="${{gmap}}" target="_blank" class="gmap-btn">π ꡬκΈλ§΅</a>`;
|
| 404 |
|
| 405 |
const m = L.marker([p.lat, p.lng], {{icon: icon}}).addTo(map).bindPopup(popup);
|
| 406 |
markers.push(m);
|
| 407 |
|
| 408 |
const item = document.createElement('div');
|
| 409 |
item.className = 'place-item';
|
| 410 |
+
item.onclick = () => {{ map.setView([p.lat, p.lng], 16); m.openPopup(); }};
|
| 411 |
item.innerHTML = `
|
| 412 |
<div class="place-idx">${{idx+1}}</div>
|
| 413 |
<div class="place-info">
|
| 414 |
+
<div class="place-name">${{p.name}}</div>
|
| 415 |
+
<div class="place-sub">${{p.type}}</div>
|
| 416 |
<div class="place-reason">${{p.reason}}</div>
|
| 417 |
</div>
|
| 418 |
`;
|
|
|
|
| 422 |
document.getElementById('list-panel').style.transform = 'translateY(0%)';
|
| 423 |
}}
|
| 424 |
|
| 425 |
+
function closePanel() {{
|
| 426 |
+
document.getElementById('list-panel').style.transform = 'translateY(110%)';
|
|
|
|
| 427 |
}}
|
| 428 |
|
| 429 |
function showLoading(msg) {{ document.getElementById('loading').style.display = 'flex'; document.getElementById('loading-text').innerText = msg; }}
|
| 430 |
function hideLoading() {{ document.getElementById('loading').style.display = 'none'; }}
|
| 431 |
|
| 432 |
const style = document.createElement('style');
|
| 433 |
+
style.innerHTML = `
|
| 434 |
+
.leaflet-popup-content-wrapper {{ background: #222; color: #eee; border: 1px solid var(--copper); }}
|
| 435 |
+
.leaflet-popup-tip {{ background: var(--copper); }}
|
| 436 |
+
`;
|
| 437 |
document.head.appendChild(style);
|
| 438 |
</script>
|
| 439 |
</body>
|