benjamin5607 commited on
Commit
fae39e8
Β·
verified Β·
1 Parent(s): 6aa73a9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -135
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 { visibility: hidden; }
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
- :root {{ --copper: #c5a059; --dark: #121212; --panel: #1e1e1e; --text: #e0e0e0; --bg: #000; }}
37
- body {{ margin: 0; padding: 0; font-family: -apple-system, sans-serif; background-color: var(--bg); color: var(--text); overflow: hidden; }}
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- #map {{ width: 100%; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- /* 상단 검색바 */
42
- .top-bar {{
43
- position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
44
- width: 90%; max-width: 500px; z-index: 1000;
45
- display: flex; gap: 10px;
46
  }}
47
- .search-input {{
48
- flex: 1; padding: 12px 20px; border-radius: 30px; border: none;
49
- background: rgba(20, 20, 20, 0.95); color: white;
50
- box-shadow: 0 4px 15px rgba(0,0,0,0.5); font-size: 16px;
51
- backdrop-filter: blur(5px); border: 1px solid #333;
 
 
 
 
 
 
52
  }}
53
- .search-btn {{
54
- width: 48px; height: 48px; border-radius: 50%; border: none;
55
- background: var(--copper); color: #000; font-weight: bold; cursor: pointer;
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-controls {{
61
- position: absolute; bottom: 40px; left: 0; width: 100%; z-index: 1000;
62
- padding: 0 15px; box-sizing: border-box;
63
- display: flex; align-items: flex-end; justify-content: space-between; pointer-events: none;
64
  }}
65
-
66
- .filter-scroller {{
67
- display: flex; gap: 8px; overflow-x: auto; padding-bottom: 5px;
68
- pointer-events: auto; max-width: 80%; scrollbar-width: none;
 
69
  }}
70
  .filter-chip {{
71
- background: rgba(30, 30, 30, 0.95); color: #ccc; border: 1px solid #444;
72
- padding: 10px 18px; border-radius: 25px; white-space: nowrap; cursor: pointer;
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: 56px; height: 56px; border-radius: 50%;
82
- background: white; color: black; pointer-events: auto;
83
- display: flex; justify-content: center; align-items: center;
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: 50vh;
91
- background: rgba(20, 20, 20, 0.98); z-index: 1001;
92
- border-top-left-radius: 25px; border-top-right-radius: 25px;
93
- overflow-y: auto; padding: 25px; box-sizing: border-box;
94
- transform: translateY(110%); transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
95
- box-shadow: 0 -10px 30px rgba(0,0,0,0.7);
96
  }}
97
- .panel-drag-handle {{ width: 40px; height: 5px; background: #444; margin: 0 auto 20px auto; border-radius: 10px; }}
 
98
 
99
- .place-item {{ padding: 15px 0; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: flex-start; }}
100
- .place-idx {{ font-size: 1.2rem; font-weight: 800; color: var(--copper); margin-right: 15px; min-width: 25px; }}
101
  .place-info {{ flex: 1; }}
102
- .place-name {{ font-size: 1.05rem; font-weight: bold; color: #fff; }}
103
- .place-type {{ font-size: 0.75rem; color: #000; background: var(--copper); padding: 2px 8px; border-radius: 4px; font-weight: bold; margin-left: 8px; vertical-align: middle; }}
104
- .place-reason {{ font-size: 0.9rem; color: #aaa; margin-top: 6px; line-height: 1.4; }}
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.8);
117
- z-index: 2000; display: none; flex-direction: column; justify-content: center; align-items: center; color: var(--copper);
118
  }}
119
- .spinner {{ width: 40px; height: 40px; border: 4px solid #333; border-top: 4px solid var(--copper); border-radius: 50%; animation: spin 1s linear infinite; }}
120
  @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
121
 
122
- .gmap-btn {{ display: block; margin-top: 8px; text-decoration: none; color: #4285F4; font-size: 0.9rem; font-weight: bold; }}
123
  </style>
124
  </head>
125
  <body>
126
 
127
- <div id="map"></div>
 
 
 
 
 
128
 
129
- <div class="top-bar">
130
- <input type="text" id="search-input" class="search-input" placeholder="μ§€μ—­ 검색 (예: 강남, Shinjuku)" onkeypress="if(event.key==='Enter') searchLocation()">
131
- <button class="search-btn" onclick="searchLocation()">πŸ”</button>
 
 
 
 
 
 
 
 
 
 
 
132
  </div>
133
 
134
- <button id="rescan-btn" class="rescan-pill" onclick="scanCurrentView()">πŸ”„ 이 μ§€μ—­μ—μ„œ λ‹€μ‹œ μ°ΎκΈ°</button>
 
135
 
136
- <div class="bottom-controls">
137
- <div class="filter-scroller">
138
- <div class="filter-chip active" onclick="setFilter('All', this)">All</div>
139
- <div class="filter-chip" onclick="setFilter('Whiskey', this)">Whiskey</div>
140
- <div class="filter-chip" onclick="setFilter('Beer', this)">Beer</div>
141
- <div class="filter-chip" onclick="setFilter('Wine', this)">Wine</div>
142
- <div class="filter-chip" onclick="setFilter('Cocktail', this)">Cocktail</div>
 
 
 
 
 
 
 
 
143
  </div>
144
- <div class="gps-btn" onclick="findMe()">🧭</div>
145
- </div>
146
 
147
- <div id="list-panel" class="list-panel">
148
- <div class="panel-drag-handle" onclick="closePanel()"></div>
149
- <div style="font-size:1.1rem; font-weight:bold; color:var(--copper); margin-bottom:15px;">πŸ† Top Recommendations</div>
150
- <div id="place-list"></div>
 
 
 
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;">SEARCHING...</div>
156
  </div>
157
 
158
  <script>
@@ -166,85 +209,121 @@ html_code = f"""
166
  let myLocMarker = null;
167
  let currentCategory = 'All';
168
 
169
- map.on('moveend', () => {{
170
- document.getElementById('rescan-btn').style.display = 'block';
171
- }});
 
 
 
 
172
 
173
- function setFilter(cat, el) {{
174
- currentCategory = cat;
175
- document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
176
- el.classList.add('active');
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
- map.setView([lat, lng], 15);
189
- if (myLocMarker) map.removeLayer(myLocMarker);
190
- myLocMarker = L.circleMarker([lat, lng], {{ radius: 8, color: '#fff', fillColor: '#4285F4', fillOpacity: 1 }}).addTo(map).bindPopup("ME").openPopup();
191
- setTimeout(scanCurrentView, 500);
 
 
 
 
 
 
 
192
  }},
193
- () => {{ hideLoading(); alert("μœ„μΉ˜ κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€."); }}
194
  );
195
  }}
196
 
197
- async function searchLocation() {{
198
- const q = document.getElementById('search-input').value;
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=${{q}}&limit=1`);
206
  const data = await res.json();
207
  if(data.length > 0) {{
 
208
  const b = data[0].boundingbox;
209
- map.fitBounds([[b[0], b[2]], [b[1], b[3]]]);
210
- setTimeout(scanCurrentView, 1000);
211
- }} else {{ alert("μž₯μ†Œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); hideLoading(); }}
 
 
 
 
 
 
 
212
  }} catch(e) {{ alert("검색 였λ₯˜"); hideLoading(); }}
213
  }}
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  async function scanCurrentView() {{
216
  showLoading(`'${{currentCategory}}' μž₯μ†Œ μ°ΎλŠ” 쀑...`);
217
- document.getElementById('rescan-btn').style.display = 'none';
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"~"bar|pub|lounge"](${{bbox}}); node["craft"="distillery"](${{bbox}}); node["shop"="alcohol"](${{bbox}});`;
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 === 'Cocktail') {{
230
- query = `node["amenity"~"bar|lounge"](${{bbox}});`;
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
- const places = data.elements.map(el => ({{
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) {{ alert("Scan Error"); hideLoading(); }}
 
 
 
263
  }}
264
 
 
265
  async function rankWithAI(places) {{
266
- // πŸ”₯ [μˆ˜μ • μ™„λ£Œ] μžλ°”μŠ€ν¬λ¦½νŠΈ λ³€μˆ˜ ${{places.length}} 둜 ν‘œκΈ°
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: [${{currentCategory}}].
272
  Candidates:
273
  ${{listStr}}
274
 
275
  Task:
276
- 1. Select Top 10 places matching the category.
277
- 2. Strict filtering: Remove generic restaurants unless they are famous for alcohol.
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
- if(start === -1) throw new Error("AI JSON Fail");
 
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
- console.log("AI Fallback");
301
- const fallbackPlaces = places.slice(0, 10).map(p => ({{...p, reason: "AI μ—°κ²° μ§€μ—°: κΈ°λ³Έ μΆ”μ²œ"}}));
302
- renderResults(fallbackPlaces);
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" style="color:#4285F4; font-weight:bold;">πŸ”— ꡬ글맡</a>`;
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(); closePanel(false); }};
328
  item.innerHTML = `
329
  <div class="place-idx">${{idx+1}}</div>
330
  <div class="place-info">
331
- <div><span class="place-name">${{p.name}}</span> <span class="place-type">${{p.type}}</span></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(fullClose = true) {{
342
- const val = fullClose ? '110%' : '60%';
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 = `@keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }} .leaflet-popup-content-wrapper {{ background: #222; color: #eee; border: 1px solid var(--copper); }}`;
 
 
 
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>