File size: 33,454 Bytes
f61e156
 
36b9b70
813e285
 
3ebf811
 
 
c072070
 
7f30a8f
c072070
9c4c7a3
 
4643984
 
 
 
 
 
 
 
 
b0d20fa
4643984
0d23016
4643984
 
4379084
4643984
4379084
4643984
4379084
4643984
1aef9d9
 
 
fae39e8
1aef9d9
 
 
 
3ebf811
357ec59
3ebf811
 
bbabc6c
 
0d23016
 
bbabc6c
 
a02ecf2
 
4643984
2912178
bbabc6c
3d3f989
0d23016
f86d6c4
2f889a4
7a319d1
3ebf811
4643984
3ebf811
4643984
 
bbabc6c
 
0d23016
bbabc6c
a02ecf2
813e285
 
bbabc6c
 
 
 
36b9b70
7b11e9f
 
813e285
fef278d
1b01ba6
fef278d
16f458a
f61e156
 
 
 
 
58982d9
 
f61e156
16f458a
f7a0a39
16f458a
879c3c9
0d23016
a0fa0db
4379084
 
b0d20fa
 
879c3c9
4643984
 
 
 
879c3c9
357ec59
 
 
 
 
 
f86d6c4
bbabc6c
d966714
bbabc6c
 
a0fa0db
 
f7a0a39
 
785226c
 
a8b52b7
8e01cf9
 
 
 
 
1b01ba6
8e01cf9
 
 
 
 
 
 
f7a0a39
 
 
e31c52c
8e01cf9
4379084
357ec59
 
785226c
 
 
8e01cf9
 
 
 
f61e156
 
 
08f05d6
879c3c9
 
a0fa0db
879c3c9
 
4379084
 
879c3c9
 
17401f6
 
bbabc6c
17401f6
e937907
879c3c9
36b9b70
f5fe057
 
 
785226c
8e01cf9
 
 
 
49f12e1
8e01cf9
 
 
 
785226c
f7a0a39
785226c
f7a0a39
 
785226c
f7a0a39
 
 
e31c52c
8e01cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f7a0a39
f61e156
4643984
16f458a
f7a0a39
785226c
d966714
8e01cf9
 
879c3c9
 
bbabc6c
4379084
2debcb5
4379084
879c3c9
5decb26
d966714
bbabc6c
 
 
 
 
 
d966714
 
 
17401f6
e31c52c
 
17401f6
 
16f458a
bbabc6c
 
 
 
 
 
 
 
4643984
 
bbabc6c
4643984
16f458a
36b9b70
b3f157b
 
 
 
 
 
 
 
879c3c9
 
e31c52c
 
 
879c3c9
 
 
b3f157b
16f458a
5decb26
16f458a
879c3c9
17401f6
 
d966714
17401f6
879c3c9
357ec59
 
 
 
 
 
785226c
e31c52c
d966714
 
 
 
 
879c3c9
357ec59
bbabc6c
785226c
a0fa0db
d966714
357ec59
879c3c9
5decb26
879c3c9
16f458a
24d1903
8e01cf9
0d23016
f7a0a39
b0d20fa
0d23016
 
 
 
 
 
 
 
 
 
 
 
b3f157b
8e01cf9
b3f157b
0d23016
 
37dbc4b
f7a0a39
 
 
 
 
 
17401f6
885b44d
b9f41cf
885b44d
0d23016
885b44d
 
0d23016
885b44d
0d23016
885b44d
31422b1
 
 
 
 
 
885b44d
31422b1
 
 
885b44d
d966714
 
2debcb5
 
8e01cf9
 
2debcb5
 
 
 
e31c52c
b9f41cf
17401f6
785226c
bbabc6c
e31c52c
 
17401f6
 
 
 
 
 
785226c
 
1b01ba6
 
785226c
17401f6
785226c
f7a0a39
 
 
17401f6
f7a0a39
785226c
17401f6
 
f7a0a39
 
8e01cf9
49f12e1
 
 
 
7a319d1
8e01cf9
357ec59
8e01cf9
 
 
49f12e1
 
 
 
 
 
357ec59
 
 
 
 
 
 
 
49f12e1
 
357ec59
 
 
 
 
 
49f12e1
 
 
e31c52c
8e01cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17401f6
8e01cf9
 
 
49f12e1
 
8e01cf9
357ec59
49f12e1
 
357ec59
49f12e1
 
8e01cf9
49f12e1
8e01cf9
 
 
 
785226c
16f458a
8e01cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7299771
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e01cf9
 
 
 
 
16f458a
f61e156
 
426caf1
cf54277
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
import streamlit as st
import streamlit.components.v1 as components
import json
import base64
import os
import pandas as pd
import requests
import io

# ==========================================
# πŸ› οΈ 1. μ„€μ • 및 μ‹œν¬λ¦Ώ (μœ μ§€)
# ==========================================
st.set_page_config(page_title="Drunken Plane", page_icon="✈️", layout="wide")

GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "").strip()
if not GROQ_API_KEY and "GROQ_API_KEY" in st.secrets:
    GROQ_API_KEY = st.secrets["GROQ_API_KEY"].strip()

SHEET_URL = os.environ.get("PRIVATE_SHEET_URL", "").strip()
if not SHEET_URL and "PRIVATE_SHEET_URL" in st.secrets:
    SHEET_URL = st.secrets["PRIVATE_SHEET_URL"].strip()

with st.sidebar:
    st.write("### πŸ”§ System Status")
    if GROQ_API_KEY and len(GROQ_API_KEY) > 10:
        st.success(f"🟒 API Key Ready")
    else:
        st.error("πŸ”΄ API Key Missing")
    
    if SHEET_URL:
        st.success("🟒 DB Connected")
    else:
        st.warning("🟠 Using Dummy Data")

st.markdown("""
    <style>
        .block-container { padding: 0 !important; margin: 0 !important; max-width: 100% !important; }
        header, footer { display: none !important; }
        iframe { height: 100vh !important; display: block; }
    </style>
""", unsafe_allow_html=True)

# ==========================================
# πŸ’Ύ 2. 데이터 λ‘œλ“œ (μœ μ§€)
# ==========================================
PLACE_DB = {}
DUMMY_DATA = {
    "Korea": [{"name": "Zest", "lat": 37.525, "lon": 127.040, "category": "Bar", "label": "🍸 World Best", "desc_ko": "μ„œμšΈ 졜고의 μΉ΅ν…ŒμΌ λ°”", "address": "μ„œμšΈ 강남ꡬ λ„μ‚°λŒ€λ‘œ55κΈΈ 26"}],
    "Malaysia": [{"name": "Bar Trigona", "lat": 3.159, "lon": 101.714, "category": "Bar", "label": "🐝 Sustainable", "desc_ko": "μΏ μ•ŒλΌλ£Έν‘Έλ₯΄μ˜ λŸ­μ…”λ¦¬ λ°”", "address": "Four Seasons Hotel, Kuala Lumpur"}],
    "Japan": [{"name": "Bar Benfiddich", "lat": 35.693, "lon": 139.699, "category": "Bar", "label": "🌿 Herbal", "desc_ko": "도쿄 μ‹ μ£ΌμΏ μ˜ μ•½μ΄ˆ μΉ΅ν…ŒμΌ λ°”", "address": "Tokyo, Shinjuku"}]
}

try:
    if SHEET_URL:
        response = requests.get(SHEET_URL, timeout=5)
        response.encoding = 'utf-8'
        response.raise_for_status()
        df = pd.read_csv(io.StringIO(response.text), on_bad_lines='skip')
        df = df.fillna("")
        df.columns = df.columns.str.strip()
        for col in df.select_dtypes(include=['object']).columns:
            df[col] = df[col].astype(str).str.replace(r'[\r\n\t]+', ' ', regex=True)
        if 'Country' in df.columns:
            df['Country'] = df['Country'].astype(str).str.strip()
            for country, group in df.groupby('Country'):
                if country and country.lower() != 'nan' and len(country) > 1:
                    PLACE_DB[country] = group.fillna("").to_dict(orient='records')
    else:
        PLACE_DB = DUMMY_DATA
except:
    PLACE_DB = DUMMY_DATA

emily_base64 = ""
if os.path.exists('emily.jpg'):
    try:
        with open("emily.jpg", "rb") as f:
            emily_base64 = base64.b64encode(f.read()).decode()
    except: pass

db_json_str = json.dumps(PLACE_DB, ensure_ascii=False)
db_base64 = base64.b64encode(db_json_str.encode('utf-8')).decode('utf-8')

# ==========================================
# πŸ–₯️ 3. 톡합 ν”„λ‘ νŠΈμ—”λ“œ (λ²„νŠΌ 클릭 μ—λŸ¬ ν•€ν¬μΈνŠΈ μˆ˜μ •)
# ==========================================
html_template = """
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <style>
        :root { --gold: #d4af37; --dark: #0a0a0a; --text: #e0e0e0; }
        body { margin: 0; padding: 0; background: var(--dark); color: var(--text); font-family: 'Segoe UI', sans-serif; overflow: hidden; }
        .page { position: absolute; top: 0; left: 0; width: 100%; height: 100vh; transition: transform 0.5s ease; display: flex; flex-direction: column; }
        
        .top-nav { height: 60px; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; z-index: 500; background: rgba(0,0,0,0.8); }
        .passport-btn { font-size: 26px; cursor: pointer; background: rgba(255,215,0,0.1); padding: 8px; border-radius: 15px; border: 1px solid rgba(212,175,55,0.3); }
        
        .lang-switcher { display: flex; gap: 8px; }
        .lang-btn { background: rgba(0,0,0,0.6); border: 1px solid #444; color: #888; padding: 5px 10px; border-radius: 20px; cursor: pointer; font-size: 16px; transition: 0.3s; }
        .lang-btn.active { background: var(--gold); color: black; border-color: var(--gold); transform: scale(1.1); font-weight: bold; }

        .type-filter-container { width: 100%; padding: 10px 0; overflow-x: auto; display: flex; gap: 8px; justify-content: center; scrollbar-width: none; }
        .type-btn { padding: 8px 16px; border-radius: 20px; font-size: 11px; font-weight: bold; background: #1a1a1a; color: #777; border: 1px solid #333; white-space: nowrap; cursor: pointer; }
        .type-btn.selected { background: var(--gold); color: black; border-color: var(--gold); }

        .place-list-container { flex: 1; overflow-y: auto; padding: 0 20px 120px 20px; }
        .place-card { background: #161616; border: 1px solid #2a2a2a; border-radius: 18px; padding: 20px; margin-bottom: 15px; position: relative; transition: all 0.3s; }
        
        .place-card.visited { border: 2px solid var(--gold); box-shadow: 0 0 15px rgba(212, 175, 55, 0.2); }
        .visited-badge { position: absolute; top: -10px; right: -10px; background: var(--gold); color: black; font-weight: bold; padding: 5px 10px; border-radius: 15px; font-size: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.5); z-index: 10; animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
        @keyframes popIn { from { transform: scale(0); } to { transform: scale(1); } }

        .pc-header { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
        .pc-name { font-size: 18px; font-weight: bold; color: var(--gold); flex: 1; }
        .pc-dist { font-size: 12px; color: #888; font-weight: normal; margin-left: 5px; }
        .pc-label { font-size: 11px; padding: 3px 8px; border-radius: 8px; background: rgba(212,175,55,0.15); color: var(--gold); border: 1px solid var(--gold); white-space: nowrap; }
        .pc-desc { font-size: 13px; color: #aaa; margin: 10px 0; line-height: 1.5; }
        .btn-action { flex: 1; padding: 12px; border-radius: 10px; border: none; font-weight: bold; cursor: pointer; font-size: 12px; }

        .bartender-btn { position: fixed; bottom: 30%; right: 20px; width: 85px; height: 85px; border-radius: 50%; background-image: url('IMAGE_DATA'); background-size: cover; border: 3px solid var(--gold); z-index: 999; box-shadow: 0 0 20px rgba(212, 175, 55, 0.5); cursor: pointer; }
        
        #world-map-page, #passport-page, #chat-page { background: #000; z-index: 2000; transform: translateY(100%); }
        #passport-page { transform: translateX(-100%); background: #0f0f1b; }
        
        #detail-page { background: #0f0f12; z-index: 4000; transform: translateX(100%); display:flex; flex-direction:column; }
        #detail-map { width: 100%; height: 40vh; border-bottom: 2px solid var(--gold); }
        .review-content-box { flex: 1; overflow-y: auto; padding: 20px; }
        .review-header { text-align: center; margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 15px; }
        .review-title { font-size: 24px; font-weight: bold; color: var(--gold); margin-bottom: 5px; }
        .review-body { font-size: 16px; line-height: 1.8; color: #e0e0e0; font-family: 'Georgia', serif; font-style: italic; text-align: justify; }
        .review-signature { text-align: right; margin-top: 20px; color: var(--gold); font-weight: bold; }
        .detail-actions { display: flex; gap: 10px; margin-top: 20px; }
        .detail-btn { flex: 1; padding: 12px; border-radius: 10px; border: none; font-weight: bold; cursor: pointer; font-size: 14px; text-transform: uppercase; }
        .btn-map { background: #333; color: white; border: 1px solid #555; }
        .btn-stamp { background: var(--gold); color: black; }

        .nav-bar { height: 60px; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid #222; background: #111; }
        .msg { max-width: 85%; padding: 12px 18px; border-radius: 15px; font-size: 14px; line-height: 1.6; margin-bottom: 12px; }
        .msg.bot { align-self: flex-start; background: #222; color: #eee; border: 1px solid #333; }
        .msg.user { align-self: flex-end; background: var(--gold); color: black; font-weight: bold; }
        .reco-btn { display: block; margin-top: 8px; padding: 10px; background: var(--gold); color: black !important; border-radius: 10px; font-weight: bold; text-decoration: none; font-size: 12px; cursor: pointer; border:none; width: 100%; text-align: center; }
        .popup-btn { background: var(--gold); color: black; border: none; padding: 6px 12px; border-radius: 5px; font-weight: bold; font-size: 11px; cursor: pointer; width: 100%; margin-top: 5px; }
        .leaflet-popup-content-wrapper { background: #1a1a1a; color: white; border: 1px solid var(--gold); }
        .leaflet-popup-content { margin: 10px; }
        
        .stamp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; padding: 20px; }
        .stamp { height: 90px; border-radius: 15px; border: 2px dashed #333; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 10px; color: #444; background: #161625; }
        .stamp.active { border: 2px solid var(--gold); color: var(--gold); background: rgba(212,175,55,0.05); }
        
        .map-search-container { position: absolute; top: 80px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 500px; z-index: 3000; display: flex; gap: 10px; }
        .map-search-input { flex: 1; padding: 12px 15px; border-radius: 25px; border: 2px solid var(--gold); background: rgba(0,0,0,0.85); color: white; outline: none; font-size: 14px; }
        .map-loc-btn { width: 45px; height: 45px; border-radius: 50%; background: var(--gold); border: none; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 10px rgba(0,0,0,0.5); }
    </style>
</head>
<body>
    <div id="main-page" class="page">
        <div class="top-nav">
            <div class="passport-btn" onclick="openPassport()">🎫</div>
            <div class="lang-switcher">
                <div class="lang-btn active" id="btn-KO" onclick="setLanguage('KO', this)">πŸ‡°πŸ‡·</div>
                <div class="lang-btn" id="btn-EN" onclick="setLanguage('EN', this)">πŸ‡ΊπŸ‡Έ</div>
                <div class="lang-btn" id="btn-JP" onclick="setLanguage('JP', this)">πŸ‡―πŸ‡΅</div>
                <div class="lang-btn" id="btn-CN" onclick="setLanguage('CN', this)">πŸ‡¨πŸ‡³</div>
            </div>
        </div>
        <div style="text-align:center; padding: 5px 0 15px 0;">
            <div style="color:var(--gold); font-size: 26px; font-weight: 900; letter-spacing: 4px;">DRUNKEN PLANE</div>
            <button onclick="openMap()" style="margin-top:10px; padding:10px 25px; background:var(--gold); border:none; border-radius:30px; font-weight:bold; cursor:pointer;">πŸ—ΊοΈ EXPLORE WORLD</button>
        </div>
        <center><select id="country-select" style="width:85%; max-width:400px; padding:12px; background:#111; border:1px solid var(--gold); color:white; border-radius:10px;" onchange="selectCountry(this.value)"></select></center>
        <div class="type-filter-container" id="filter-box"></div>
        <div class="place-list-container" id="place-list"></div>
        <div class="bartender-btn" onclick="openChat()"></div>
    </div>

    <div id="passport-page" class="page"><div class="nav-bar"><button onclick="closePassport()">←</button> MY PASSPORT</div><div id="stamp-grid" class="stamp-grid" style="overflow-y:auto;"></div></div>
    
    <div id="world-map-page" class="page">
        <div class="nav-bar"><button onclick="closeMap()">🏠 HOME</button></div>
        <div class="map-search-container">
            <input id="map-input" class="map-search-input" type="text" placeholder="Search City, Address or Place..." onkeydown="if(event.key==='Enter') searchMap()">
            <button class="map-loc-btn" onclick="panToUser()">β—Ž</button>
        </div>
        <div id="full-map" style="width:100%; height:100%;"></div>
    </div>
    
    <div id="chat-page" class="page">
        <div class="nav-bar"><button onclick="closeChat()">←</button> Emily</div>
        <div id="chat-messages" style="flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column;"></div>
        <div style="padding:15px; display:flex; gap:10px; background:#111;">
            <input id="chat-input" type="text" style="flex:1; padding:14px; border-radius:10px; border:1px solid #333; background:#222; color:#fff;" placeholder="Ask Emily..." onkeydown="if(event.key==='Enter') sendChat()">
            <button onclick="sendChat()" style="padding:10px 25px; background:var(--gold); border:none; border-radius:10px; font-weight:bold;">SEND</button>
        </div>
    </div>
    
    <div id="detail-page" class="page">
        <div class="nav-bar"><button onclick="closeDetail()">←</button> Bartender's Note</div>
        <div id="detail-map"></div>
        <div class="review-content-box">
            <div class="review-header">
                <div class="review-title" id="review-title">Loading...</div>
                <div style="color:#888;" id="review-subtitle"></div>
            </div>
            <div id="repo-content" class="review-body">
                Please wait while Emily recalls this place... 🍸
            </div>
            <div class="detail-actions">
                <button id="btn-detail-map" class="detail-btn btn-map">πŸ—ΊοΈ OPEN MAP</button>
                <button id="btn-detail-stamp" class="detail-btn btn-stamp">✈️ STAMP</button>
            </div>
        </div>
    </div>

    <script>
        function b64DecodeUnicode(str) { try { return decodeURIComponent(atob(str).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } catch(e) { return "{}"; } }
        const PLACE_DB = JSON.parse(b64DecodeUnicode("DB_DATA_BASE64"));
        let currentLang = "KO", selectedCountry = "", selectedType = "All", chatHistory = [];
        let visitedPlaces = JSON.parse(localStorage.getItem('visitedPlaces') || '[]');
        let userLat = null, userLon = null;
        let worldMap = null; 
        let detailMap = null;

        const TRANSLATIONS = {
            "KO": { "ai": "Korean", "greet": "μ•ˆλ…•? μ˜€λŠ˜μ€ μ–΄λ””μ„œ ν•œμž”ν• κΉŒ?", "aiBtn": "AI 리뷰", "mapBtn": "지도", "filters": ["🌐 전체", "🏺 전톡주", "πŸ₯ƒ μœ„μŠ€ν‚€", "🍺 λ§₯μ£Ό", "🍷 와인", "🍹 λ°”"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] },
            "EN": { "ai": "English", "greet": "Hey! Where to drink tonight?", "aiBtn": "AI REVIEW", "mapBtn": "MAP", "filters": ["🌐 All", "🏺 Trad.", "πŸ₯ƒ Whiskey", "🍺 Beer", "🍷 Wine", "🍹 Bar"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] },
            "JP": { "ai": "Japanese", "greet": "あら、ハニー. δ½•γ«γ™γ‚‹οΌŸ", "aiBtn": "AI レビγƒ₯γƒΌ", "mapBtn": "εœ°ε›³", "filters": ["🌐 全部", "🏺 伝硱酒", "πŸ₯ƒ ウむス킀", "🍺 ビール", "🍷 ワむン", "🍹 バー"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] },
            "CN": { "ai": "Chinese", "greet": "δΊ²ηˆ±ηš„, ζƒ³ε–η‚Ήδ»€δΉˆ?", "aiBtn": "AI η‚Ήθ―„", "mapBtn": "εœ°ε›Ύ", "filters": ["🌐 全部", "🏺 δΌ η»Ÿι…’", "πŸ₯ƒ 威士忌", "🍺 ε•€ι…’", "🍷 葑萄酒", "🍹 酒吧"], "types": ["All", "Traditional", "Whiskey", "Beer", "Wine", "Bar"] }
        };

        function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
            try {
                var R = 6371; var dLat = deg2rad(lat2-lat1); var dLon = deg2rad(lon2-lon1); 
                var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon/2) * Math.sin(dLon/2); 
                var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
                return (R * c).toFixed(1);
            } catch(e) { return "?"; }
        }
        function deg2rad(deg) { return deg * (Math.PI/180); }

        function getMapUrl(p, country) {
            if (country.includes("Korea") || country.includes("ν•œκ΅­")) return "https://map.naver.com/p/search/" + encodeURIComponent(p.address || p.name);
            return "https://www.google.com/maps/search/" + encodeURIComponent(p.name) + "/@" + p.lat + "," + p.lon + ",17z";
        }

        window.onload = function() {
            try {
                const keys = Object.keys(PLACE_DB).sort();
                const select = document.getElementById('country-select');
                keys.forEach(k => { const opt = document.createElement('option'); opt.value = k; opt.text = k; select.appendChild(opt); });
                if(keys.length > 0) selectCountry(keys[0]);
                renderFilters();
                if(navigator.geolocation) {
                    navigator.geolocation.getCurrentPosition(function(p) { 
                        userLat = p.coords.latitude; userLon = p.coords.longitude; renderList(); 
                    });
                }
            } catch(e) { console.error(e); }
        };

        function setLanguage(l, el) { 
            currentLang = l; 
            document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active')); 
            el.classList.add('active'); 
            renderFilters(); 
            renderList(); 
        }
        
        function renderFilters() {
            const box = document.getElementById('filter-box'); box.innerHTML = "";
            TRANSLATIONS[currentLang].filters.forEach((f, i) => {
                const btn = document.createElement('div'); btn.className = 'type-btn' + (selectedType === TRANSLATIONS[currentLang].types[i] ? ' selected' : '');
                btn.innerText = f; btn.onclick = () => { selectedType = TRANSLATIONS[currentLang].types[i]; renderFilters(); renderList(); };
                box.appendChild(btn);
            });
        }
        
        function selectCountry(c) { selectedCountry = c; renderList(); }

        function renderList() {
            const list = document.getElementById('place-list'); list.innerHTML = "";
            const countryPlaces = PLACE_DB[selectedCountry] || [];
            const filtered = countryPlaces.filter(p => selectedType === "All" || (p.category && p.category.includes(selectedType)));
            const t = TRANSLATIONS[currentLang];
            
            filtered.forEach(p => {
                const isVisited = visitedPlaces.includes(p.name);
                const visitedClass = isVisited ? ' visited' : '';
                const visitedBadge = isVisited ? '<div class="visited-badge">πŸ† VISITED</div>' : '';

                const card = document.createElement('div'); 
                card.className = 'place-card' + visitedClass;
                const displayLabel = p.label || p.category || "";
                const desc = p['desc_'+currentLang.toLowerCase()] || p.desc_ko || "";
                let distHtml = "";
                if(userLat && userLon && p.lat && p.lon) {
                    const dist = getDistanceFromLatLonInKm(userLat, userLon, p.lat, p.lon);
                    distHtml = `<span class="pc-dist">(πŸ“ ${dist}km)</span>`;
                }
                card.innerHTML = `
                    ${visitedBadge}
                    <div class="pc-header"><div class="pc-name">${p.name} ${distHtml}</div><div class="pc-label">${displayLabel}</div></div>
                    <div class="pc-desc">${desc}</div>
                    <div style="display:flex; gap:10px; margin-top:15px;">
                        <button class="btn-action" style="background:var(--gold); color:black;" onclick="openReview('${p.name.replace(/'/g, "\\\\'")}', '${selectedCountry}')">${t.aiBtn}</button>
                        <button class="btn-action" style="background:#333; color:white;" onclick="window.open('${getMapUrl(p, selectedCountry)}', '_blank')">${t.mapBtn}</button>
                    </div>`;
                list.appendChild(card);
            });
        }

        const ZOMBIE_MODELS = ["llama-3.1-8b-instant", "llama-3.3-70b-versatile", "mixtral-8x7b-32768"];

        async function callGroq(msgs) {
            let apiKey = "GROQ_KEY_INJECTED".trim();
            if(!apiKey || apiKey.includes("INJECTED") || apiKey.length < 10) return "Error: API Key Missing";

            for (let model of ZOMBIE_MODELS) {
                try {
                    const r = await fetch("https://api.groq.com/openai/v1/chat/completions", {
                        method: "POST", 
                        headers: { "Content-Type": "application/json", "Authorization": "Bearer " + apiKey },
                        body: JSON.stringify({ model: model, messages: msgs, temperature: 0.7 })
                    });
                    
                    if (r.ok) {
                        const d = await r.json(); return d.choices[0].message.content;
                    } 
                    if (r.status === 429) { await new Promise(r => setTimeout(r, 1000)); continue; }
                    if (r.status === 401) return "Error 401: Invalid API Key.";
                } catch(e) { continue; }
            }
            return "Emily is sleeping (Network Issue). Please try again.";
        }

        async function sendChat() {
            const inp = document.getElementById('chat-input'); const msg = inp.value.trim(); if(!msg) return;
            addChatMessage(msg, 'user'); inp.value = "";
            chatHistory.push({ role: "user", content: msg });
            
            const recentHistory = chatHistory.slice(-4);

            let fullDbContext = "CITIES:\\n";
            for(let c in PLACE_DB) {
                let pNames = PLACE_DB[c].map(p => p.name).join(",");
                fullDbContext += c + ": " + pNames + "\\n";
            }
            if(fullDbContext.length > 15000) fullDbContext = fullDbContext.substring(0, 15000) + "...(truncated)";

            let gpsContext = "No GPS.";
            if(userLat && userLon) {
                let nearbyPlaces = [];
                for(let c in PLACE_DB) {
                    PLACE_DB[c].forEach(p => {
                        if(p.lat && p.lon) {
                            const d = getDistanceFromLatLonInKm(userLat, userLon, p.lat, p.lon);
                            if(parseFloat(d) <= 20) nearbyPlaces.push(p.name);
                        }
                    });
                }
                if(nearbyPlaces.length > 0) gpsContext = "GPS Nearby(20km): " + nearbyPlaces.join(", ");
            }

            const sysPrompt = `CRITICAL RULE: OUTPUT PLACE NAMES IN [BRACKETS]. Example: [Zest].
            You are Emily, a bartender. Speak ONLY in ${TRANSLATIONS[currentLang].ai}.
            [DB] ${fullDbContext}
            [USER GPS] ${gpsContext}
            [INSTRUCTIONS]
            1. Specific city mentioned? Use [DB].
            2. 'Nearby' mentioned? Use [USER GPS].
            REMEMBER: WRAP PLACE NAMES IN [ ] OR I WILL BE FIRED.`;
            
            const botMsg = await callGroq([{ role: "system", content: sysPrompt }, ...recentHistory]);
            
            let finalHtml = botMsg;
            const regex = /\[(.*?)\]/g; 
            let match;
            while ((match = regex.exec(botMsg)) !== null) {
                const pName = match[1];
                let found = null; let fCountry = "";
                for(let c in PLACE_DB) {
                    found = PLACE_DB[c].find(p => p.name.includes(pName));
                    if(found) { fCountry = c; break; }
                }
                if(found) {
                    const mapUrl = getMapUrl(found, fCountry);
                    // [μˆ˜μ •] μ±—λ΄‡μ˜ μΆ”μ²œ λ²„νŠΌμ—μ„œλ„ markVisited κΈ°λŠ₯ 제거 (일관성 μœ μ§€)
                    finalHtml += `<br><button class="reco-btn" onclick="openReview('${found.name.replace(/'/g, "\\\\'")}', '${fCountry}')">πŸ“ ${TRANSLATIONS[currentLang].mapBtn}: ${pName}</button>`;
                }
            }
            addChatMessage(finalHtml, 'bot', true);
            chatHistory.push({ role: "assistant", content: botMsg });
        }

        function addChatMessage(t, s, isHtml=false) {
            const box = document.getElementById('chat-messages'); const div = document.createElement('div');
            div.className = "msg " + s; 
            if(isHtml) div.innerHTML = t; else div.innerText = t;
            box.appendChild(div); box.scrollTop = box.scrollHeight;
        }

        function markVisited(name) { 
            let vp = JSON.parse(localStorage.getItem('visitedPlaces') || '[]');
            if(!vp.includes(name)) { 
                vp.push(name); 
                localStorage.setItem('visitedPlaces', JSON.stringify(vp)); 
                visitedPlaces = vp; 
                alert("✈️ Stamped on Passport!"); 
                renderList(); 
            } else { alert("You already visited here!"); }
        }
        
        function openPassport() { 
            document.getElementById('passport-page').style.transform = 'translateX(0%)'; 
            visitedPlaces = JSON.parse(localStorage.getItem('visitedPlaces') || '[]');
            const grid = document.getElementById('stamp-grid'); 
            grid.innerHTML = ""; 
            
            const reversedVp = [...visitedPlaces].reverse();
            reversedVp.forEach(name => {
                const div = document.createElement('div'); 
                div.className = 'stamp active'; 
                div.innerHTML = `🍷<br><b>${name}</b>`; 
                grid.appendChild(div);
            });

            for(let c in PLACE_DB) { 
                PLACE_DB[c].forEach(p => { 
                    if(!visitedPlaces.includes(p.name)) {
                        const div = document.createElement('div'); 
                        div.className = 'stamp'; 
                        div.innerHTML = `✈️`; 
                        grid.appendChild(div); 
                    }
                }); 
            } 
        }
        function closePassport() { document.getElementById('passport-page').style.transform = 'translateX(-100%)'; }
        
        function openMap() { 
            document.getElementById('world-map-page').style.transform = 'translateY(0%)'; 
            if(!worldMap) { 
                worldMap = L.map('full-map').setView([20,0],2); 
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(worldMap); 
                for(let c in PLACE_DB) PLACE_DB[c].forEach(p => { 
                    if(p.lat && p.lon) { 
                        const lbl = p.label || p.category || ""; 
                        const desc = p['desc_'+currentLang.toLowerCase()] || p.desc_ko || "";
                        const shortDesc = desc.length > 50 ? desc.substring(0, 50) + "..." : desc;
                        const popupContent = `
                            <div style="text-align:left;">
                                <b style="color:#d4af37; font-size:14px;">${p.name}</b><br>
                                <span style="font-size:10px; color:#aaa;">${lbl}</span><br>
                                <div style="font-size:11px; margin:5px 0; color:#eee;">${shortDesc}</div>
                                <div style="display:flex; gap:5px;">
                                    <button class="popup-btn" style="background:#d4af37; color:black;" onclick="openReview('${p.name.replace(/'/g, "\\\\'")}', '${c}')">AI REVIEW</button>
                                    <button class="popup-btn" style="background:#333; color:white; border:1px solid #555;" onclick="window.open('${getMapUrl(p,c)}','_blank')">MAP</button>
                                </div>
                            </div>
                        `;
                        L.marker([p.lat,p.lon]).addTo(worldMap).bindPopup(popupContent); 
                    } 
                }); 
            } 
        }
        
        function closeMap() { document.getElementById('world-map-page').style.transform = 'translateY(100%)'; }
        
        function searchMap() {
            const q = document.getElementById('map-input').value.toLowerCase();
            if(!q) return;
            let found = false;
            for(let c in PLACE_DB) {
                if(c.toLowerCase().includes(q)) { if(PLACE_DB[c].length>0 && PLACE_DB[c][0].lat) { worldMap.setView([PLACE_DB[c][0].lat, PLACE_DB[c][0].lon], 12); found=true; break; } }
                for(let p of PLACE_DB[c]) {
                    const content = (p.name + " " + (p.address||"") + " " + (p.desc_ko||"")).toLowerCase();
                    if(content.includes(q)) { if(p.lat && p.lon) { worldMap.setView([p.lat, p.lon], 16); L.popup().setLatLng([p.lat, p.lon]).setContent(`<b>${p.name}</b>`).openOn(worldMap); found=true; break; } }
                }
                if(found) break;
            }
            if(!found) alert("Not Found");
        }
        
        function panToUser() { if(userLat && userLon) worldMap.setView([userLat, userLon], 14); else alert("GPS missing."); }

        function openChat() { document.getElementById('chat-page').style.transform = 'translateY(0%)'; if(chatHistory.length===0) addChatMessage(TRANSLATIONS[currentLang].greet, 'bot'); }
        function closeChat() { document.getElementById('chat-page').style.transform = 'translateY(100%)'; }
        
        async function openReview(n, c) { 
            document.getElementById('detail-page').style.transform = 'translateX(0%)'; 
            document.getElementById('review-title').innerText = n;
            document.getElementById('review-subtitle').innerText = c;
            document.getElementById('repo-content').innerText = "Bartender Emily is preparing your review... πŸ₯ƒ"; 
            
            let placeData = null;
            if(PLACE_DB[c]) placeData = PLACE_DB[c].find(p => p.name === n);
            
            if(placeData && placeData.lat && placeData.lon) {
                if(detailMap) { detailMap.remove(); }
                detailMap = L.map('detail-map').setView([placeData.lat, placeData.lon], 15);
                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(detailMap);
                L.marker([placeData.lat, placeData.lon]).addTo(detailMap);
            }

            document.getElementById('btn-detail-map').onclick = function() { window.open(getMapUrl(placeData, c), '_blank'); };
            document.getElementById('btn-detail-stamp').onclick = function() { markVisited(n); };

            const contextDesc = placeData ? (placeData['desc_'+currentLang.toLowerCase()] || placeData.desc_ko) : "";
            const addressInfo = placeData.address || "Address not available";

            let searchStrategy = "";
            if (c.includes("Korea") || c.includes("ν•œκ΅­")) {
                searchStrategy = "SOURCE: Strictly base your review on the provided [CONTEXT] and your general knowledge of Korean geography. Do not hallucinate.";
            } else {
                searchStrategy = "SOURCE: Use your internal knowledge base (simulating online reviews, blogs, news) to enrich the details. The [CONTEXT] is just a starting point.";
            }

            const reviewPrompt = `
            ROLE: You are Emily, a professional and charming bartender.
            TASK: Write a detailed review for the bar "${n}" located in "${c}".
            ${searchStrategy}
            [CONTEXT]: ${contextDesc}
            [ADDRESS]: ${addressInfo}

            REQUIRED SECTIONS (Translate headers to target language):
            1. πŸ“ Address & Access (How to get there easily)
            2. πŸ“– Story & Atmosphere (Vibe check)
            3. πŸ‘₯ Who goes there? (Demographics, age, style)
            4. πŸ₯ƒ Recommended Menu (Signature drinks)
            5. 🀫 Secret Tip (Best seat, best time, etc.)
            6. 🏁 Bartender's Note (Final Verdict)

            LANGUAGE: Respond STRICTLY in ${TRANSLATIONS[currentLang].ai}.
            TONE: Insightful, witty, and warm. Like giving advice to a close friend.
            `;

            const res = await callGroq([{role:"system",content: reviewPrompt}]);
            document.getElementById('repo-content').innerHTML = res + "<div class='review-signature'>- Yours, Emily πŸ’‹</div>"; 
        }
        
        function closeDetail() { document.getElementById('detail-page').style.transform = 'translateX(100%)'; }
    </script>
</body>
</html>
"""

html_code = html_template.replace("IMAGE_DATA", f"data:image/jpeg;base64,{emily_base64}")
html_code = html_code.replace("DB_DATA_BASE64", db_base64)
html_code = html_code.replace("GROQ_KEY_INJECTED", GROQ_API_KEY)

components.html(html_code, height=1000, scrolling=True)