code-slicer commited on
Commit
7b0a7b6
·
verified ·
1 Parent(s): 907dbf8

Upload 16 files

Browse files
Files changed (17) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +45 -0
  3. README.md +20 -0
  4. app.py +1653 -0
  5. chat_a.py +1568 -0
  6. countries_cities.csv +3628 -0
  7. css.py +227 -0
  8. external_scores.csv +88 -0
  9. festivals.csv +0 -0
  10. gitattributes +1 -0
  11. packages.csv +0 -0
  12. packages.txt +3 -0
  13. requirements.txt +11 -0
  14. runtime.txt +1 -0
  15. theme_title_phrases.json +62 -0
  16. trip_emotions.csv +3 -0
  17. weather.csv +0 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ trip_emotions.csv filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. 파이썬 버전을 3.10으로 설정
2
+ FROM python:3.11-slim
3
+
4
+ # 2. 필요한 시스템 패키지 및 git-lfs 설치
5
+ RUN pip install --no-cache-dir hf_transfer>=0.1.6
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ git \
10
+ git-lfs \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # 3. 작업 디렉터리 설정
14
+ WORKDIR /app
15
+
16
+ # 4. 소스 코드 복사
17
+ COPY . /app
18
+
19
+ # 5. Git LFS 설정 및 대용량 파일 다운로드
20
+ RUN git lfs install && \
21
+ git lfs pull
22
+
23
+ # 6. 파이썬 라이브러리 설치
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # 7. 스트림릿이 사용할 설정 폴더를 미리 만들고 권한 부여 (PermissionError 해결)
27
+ # 권한/경로 고정
28
+ ENV HOME=/app
29
+ ENV STREAMLIT_HOME=/app/.streamlit
30
+ RUN mkdir -p /app/.streamlit && chmod -R 777 /app/.streamlit
31
+
32
+ # 권장: 캐시 경로를 환경변수로 고정
33
+ ENV HF_HOME=/tmp/hf-home \
34
+ TRANSFORMERS_CACHE=/tmp/hf-cache \
35
+ HUGGINGFACE_HUB_CACHE=/tmp/hf-cache \
36
+ TORCH_HOME=/tmp/torch-cache \
37
+ XDG_CACHE_HOME=/tmp/xdg-cache
38
+
39
+ RUN mkdir -p /tmp/hf-home /tmp/hf-cache /tmp/torch-cache /tmp/xdg-cache
40
+
41
+ # 8. 포트 개방
42
+ EXPOSE 8501
43
+
44
+ # 9. 앱 실행 (PermissionError 추가 방지)
45
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.fileWatcherType=none"]
README.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MO
3
+ emoji: 🚀
4
+ colorFrom: red
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 8501
8
+ tags:
9
+ - streamlit
10
+ pinned: false
11
+ short_description: Streamlit template space
12
+ license: mit
13
+ ---
14
+
15
+ # Welcome to Streamlit!
16
+
17
+ Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
+
19
+ If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
+ forums](https://discuss.streamlit.io).
app.py ADDED
@@ -0,0 +1,1653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # ──────────────────────────────── BOOTSTRAP (must be first) ────────────────────────────────
3
+ import os, pathlib, io, json, random
4
+
5
+ HOME = pathlib.Path.home() # ✅ 실행 사용자 홈 디렉터리 (쓰기 가능)
6
+ APP_DIR = pathlib.Path(__file__).parent.resolve()
7
+
8
+ # Streamlit 홈/설정
9
+ STREAMLIT_DIR = HOME / ".streamlit"
10
+ STREAMLIT_DIR.mkdir(parents=True, exist_ok=True)
11
+ os.environ["STREAMLIT_HOME"] = str(STREAMLIT_DIR)
12
+ os.environ["STREAMLIT_SERVER_HEADLESS"] = "true"
13
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
14
+
15
+ # ✅ HF/Transformers 캐시: 홈 밑의 .cache 사용 (필요 시 HF_CACHE_ROOT로 오버라이드 가능)
16
+ CACHE_ROOT = pathlib.Path(os.environ.get("HF_CACHE_ROOT", HOME / ".cache" / f"u{os.getuid()}"))
17
+ HF_HOME = CACHE_ROOT / "hf-home"
18
+ TRANSFORMERS_CACHE = CACHE_ROOT / "hf-cache"
19
+ HUB_CACHE = CACHE_ROOT / "hf-cache"
20
+ TORCH_HOME = CACHE_ROOT / "torch-cache"
21
+ XDG_CACHE_HOME = CACHE_ROOT / "xdg-cache"
22
+
23
+ # 폴더 생성 (권한 오류가 나면 /tmp로 자동 폴백)
24
+ try:
25
+ for p in [HF_HOME, TRANSFORMERS_CACHE, HUB_CACHE, TORCH_HOME, XDG_CACHE_HOME]:
26
+ p.mkdir(parents=True, exist_ok=True)
27
+ except PermissionError:
28
+ TMP_ROOT = pathlib.Path("/tmp") / f"hf-cache-u{os.getuid()}"
29
+ HF_HOME = TMP_ROOT / "hf-home"
30
+ TRANSFORMERS_CACHE = TMP_ROOT / "hf-cache"
31
+ HUB_CACHE = TMP_ROOT / "hf-cache"
32
+ TORCH_HOME = TMP_ROOT / "torch-cache"
33
+ XDG_CACHE_HOME = TMP_ROOT / "xdg-cache"
34
+ for p in [HF_HOME, TRANSFORMERS_CACHE, HUB_CACHE, TORCH_HOME, XDG_CACHE_HOME]:
35
+ p.mkdir(parents=True, exist_ok=True)
36
+
37
+ os.environ["HF_HOME"] = str(HF_HOME)
38
+ os.environ["TRANSFORMERS_CACHE"] = str(TRANSFORMERS_CACHE)
39
+ os.environ["HUGGINGFACE_HUB_CACHE"] = str(HUB_CACHE)
40
+ os.environ["TORCH_HOME"] = str(TORCH_HOME)
41
+ os.environ["XDG_CACHE_HOME"] = str(XDG_CACHE_HOME)
42
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
43
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
44
+
45
+ from huggingface_hub import hf_hub_download
46
+ import pandas as pd
47
+ import streamlit as st
48
+ from streamlit.components.v1 import html
49
+ from css import render_message, render_chip_buttons, log_and_render, replay_log, _get_colors
50
+
51
+ #st.success("🎉 앱이 성공적으로 시작되었습니다! 라이브러리 설치 성공!")
52
+
53
+ # ──────────────────────────────── Dataset Repo 설정 ────────────────────────────────
54
+ HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data")
55
+ HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main")
56
+
57
+ def _is_pointer_bytes(b: bytes) -> bool:
58
+ head = b[:2048].decode(errors="ignore").lower()
59
+ return (
60
+ "version https://git-lfs.github.com/spec/v1" in head
61
+ or "git-lfs" in head
62
+ or "xet" in head # e.g. xet 포인터
63
+ or "pointer size" in head
64
+ )
65
+
66
+ def _read_csv_bytes(b: bytes) -> pd.DataFrame:
67
+ try:
68
+ return pd.read_csv(io.BytesIO(b), encoding="utf-8")
69
+ except UnicodeDecodeError:
70
+ return pd.read_csv(io.BytesIO(b), encoding="cp949")
71
+
72
+ def load_csv_smart(local_path: str,
73
+ hub_filename: str | None = None,
74
+ repo_id: str = HF_DATASET_REPO,
75
+ repo_type: str = "dataset",
76
+ revision: str = HF_DATASET_REV) -> pd.DataFrame:
77
+ # hub_filename 생략 시 로컬 파일명 사용
78
+ if hub_filename is None:
79
+ hub_filename = os.path.basename(local_path)
80
+ # 1) 로컬 우선
81
+ if os.path.exists(local_path):
82
+ with open(local_path, "rb") as f:
83
+ data = f.read()
84
+ if not _is_pointer_bytes(data):
85
+ return _read_csv_bytes(data)
86
+ # 2) 허브 다운로드
87
+ cached = hf_hub_download(repo_id=repo_id, filename=hub_filename,
88
+ repo_type=repo_type, revision=revision)
89
+ try:
90
+ return pd.read_csv(cached, encoding="utf-8")
91
+ except UnicodeDecodeError:
92
+ return pd.read_csv(cached, encoding="cp949")
93
+
94
+ def load_json_smart(local_path: str,
95
+ hub_filename: str | None = None,
96
+ repo_id: str = HF_DATASET_REPO,
97
+ repo_type: str = "dataset",
98
+ revision: str = HF_DATASET_REV):
99
+ if hub_filename is None:
100
+ hub_filename = os.path.basename(local_path)
101
+ if os.path.exists(local_path):
102
+ with open(local_path, "rb") as f:
103
+ data = f.read()
104
+ if not _is_pointer_bytes(data):
105
+ return json.loads(data.decode("utf-8"))
106
+ cached = hf_hub_download(repo_id=repo_id, filename=hub_filename,
107
+ repo_type=repo_type, revision=revision)
108
+ with open(cached, "r", encoding="utf-8") as f:
109
+ return json.load(f)
110
+
111
+ # ──────────────────────────────── 데이터 로드 ────────────────────────────────
112
+ travel_df = load_csv_smart("trip_emotions.csv", "trip_emotions.csv")
113
+ external_score_df = load_csv_smart("external_scores.csv", "external_scores.csv")
114
+ festival_df = load_csv_smart("festivals.csv", "festivals.csv")
115
+ weather_df = load_csv_smart("weather.csv", "weather.csv")
116
+ package_df = load_csv_smart("packages.csv", "packages.csv")
117
+ master_df = load_csv_smart("countries_cities.csv", "countries_cities.csv")
118
+ theme_title_phrases = load_json_smart("theme_title_phrases.json", "theme_title_phrases.json")
119
+
120
+ # 필수 컬럼 가드
121
+ for col in ("여행나라", "여행도시", "여행지"):
122
+ if col not in travel_df.columns:
123
+ st.error(f"'travel_df'에 '{col}' 컬럼이 없습니다. 실제 컬럼: {travel_df.columns.tolist()}")
124
+ st.stop()
125
+
126
+ # ──────────────────────────────── chat_a import & 초기화 ────────────────────────────────
127
+ from chat_a import (
128
+ init_datasets, # ⬅️ 새로 추가된 지연 초기화 함수
129
+ analyze_emotion,
130
+ detect_intent,
131
+ extract_themes,
132
+ recommend_places_by_theme,
133
+ detect_location_filter,
134
+ generate_intro_message,
135
+ theme_ui_map,
136
+ ui_to_theme_map,
137
+ theme_opening_lines,
138
+ intent_opening_lines,
139
+ apply_weighted_score_filter,
140
+ get_highlight_message,
141
+ get_weather_message,
142
+ get_intent_intro_message,
143
+ recommend_packages,
144
+ handle_selected_place,
145
+ generate_region_intro,
146
+ parse_companion_and_age,
147
+ filter_packages_by_companion_age,
148
+ make_top2_description_custom,
149
+ format_summary_tags_custom,
150
+ make_companion_age_message
151
+ )
152
+
153
+ # 지연 초기화: import 시점에는 데이터 접근 금지, 여기서 한 번만 주입
154
+ init_datasets(
155
+ travel_df=travel_df,
156
+ festival_df=festival_df,
157
+ external_score_df=external_score_df,
158
+ weather_df=weather_df,
159
+ package_df=package_df,
160
+ master_df=master_df,
161
+ theme_title_phrases=theme_title_phrases,
162
+ )
163
+ # ───────────────────────────────────── streamlit용 함수
164
+ def init_session():
165
+ if "chat_log" not in st.session_state:
166
+ st.session_state.chat_log = []
167
+ if "mode" not in st.session_state:
168
+ st.session_state.mode = None
169
+ if "user_input" not in st.session_state:
170
+ st.session_state.user_input = ""
171
+ if "selected_theme" not in st.session_state:
172
+ st.session_state.selected_theme = None
173
+
174
+ def make_key(row) -> tuple[str, str]:
175
+ """prev 에 넣고 꺼낼 때 쓰는 고유키(여행지, 여행도시)"""
176
+ return (row["여행지"], row["여행도시"])
177
+
178
+ # ───────────────────────────────────── streamlit 영역 선언
179
+ st.set_page_config(page_title="여행은 모두투어 : 모아(MoAi)", layout="centered")
180
+ accent = _get_colors().get("accent", "#0B8A5A")
181
+ st.markdown(
182
+ f"""
183
+ <h3 style="color:{accent}; font-weight:1000; margin:0.25rem 0 1rem;">
184
+ 🅼 여행은 모두투어, 추천은 모아(MoAi)
185
+ </h3>
186
+ """,
187
+ unsafe_allow_html=True,
188
+ )
189
+
190
+ # 고정 이미지 URL
191
+ #BG_URL = "https://plus.unsplash.com/premium_photo-1679830513869-cd3648acb1db?q=80&w=2127&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
192
+
193
+ # === 배경 설정 UI (수정됨) ===
194
+ st.sidebar.subheader("🎨 배경 설정")
195
+ st.sidebar.toggle("배경 이미지 사용", key="bg_on", value=True)
196
+
197
+ # 1. '배경 이미지 사용'이 ON일 때만 이미지 관련 옵션 표시
198
+ if st.session_state.bg_on:
199
+ with st.sidebar.expander("이미지 배경 옵션", expanded=True):
200
+ st.text_input("배경 이미지 URL", key="bg_url", value="https://images.unsplash.com/photo-1506744038136-46273834b3fb")
201
+ st.slider("배경 이미지 오버레이 (%)", 0, 100, 85, key="bg_overlay_pct")
202
+ # 2. '배경 이미지 사용'이 OFF일 때만 단색 관련 옵션 표시
203
+ else:
204
+ with st.sidebar.expander("단색 배경 옵션", expanded=True):
205
+ # 추천 색상 팔레트를 버튼으로 구현
206
+ palette = {
207
+ "Light Gray": "#F1F1F1",
208
+ "Mint": "#E3E8E3",
209
+ "Sky Blue": "#D9E1E2",
210
+ "Beige": "#F0F0EC"
211
+ }
212
+ selected_color_name = st.radio(
213
+ "추천 색상",
214
+ options=palette.keys(),
215
+ key="selected_color_name",
216
+ horizontal=True # 버튼을 가로로 배열
217
+ )
218
+
219
+ #선택된 라디오 버튼의 색상 코드를 color_picker의 기본값으로 사용
220
+ st.color_picker(
221
+ "색상 직접 선택",
222
+ key="bg_color",
223
+ value=palette[selected_color_name]
224
+ )
225
+
226
+
227
+ def apply_background():
228
+ # 보호: 기존 ::before 배경이 있으면 끄기 (겹침/끊김 방지)
229
+ base_reset_css = """
230
+ <style>
231
+ .stApp::before, .block-container::before { content:none !important; }
232
+ /* 입력박스 아래 여백 */
233
+ div[data-testid="stTextInput"] { margin-bottom:18px !important; }
234
+ </style>
235
+ """
236
+ st.markdown(base_reset_css, unsafe_allow_html=True)
237
+
238
+ if st.session_state.get("bg_on") and st.session_state.get("bg_url"):
239
+ url = st.session_state["bg_url"]
240
+ overlay_alpha = float(st.session_state.get("bg_overlay_pct", 15)) / 100.0
241
+
242
+ # ✅ 이미지 배경 (메인 컨텐츠 영역에만 고정 배경 적용)
243
+ st.markdown(f"""
244
+ <style>
245
+ /* 상단·배경 투명 처리 */
246
+ header[data-testid="stHeader"],
247
+ main, section.main {{ background: transparent !important; }}
248
+
249
+ [data-testid="stAppViewContainer"] {{
250
+ background: url('{url}') center / cover no-repeat fixed;
251
+ position: relative;
252
+ z-index: 0;
253
+ }}
254
+
255
+ /* 오버레이: 이미지 위에 흰색 막을 얹어 가독성 확보 */
256
+ [data-testid="stAppViewContainer"]::after {{
257
+ content: "";
258
+ position: absolute;
259
+ inset: 0;
260
+ background: rgba(255, 255, 255, {overlay_alpha});
261
+ z-index: -1;
262
+ pointer-events: none;
263
+ }}
264
+
265
+ /* 컨텐츠와 사이드바가 배경보다 위에 오도록 */
266
+ .block-container, [data-testid="stSidebar"] {{
267
+ position: relative;
268
+ z-index: 1;
269
+ }}
270
+
271
+ /* 모바일은 fixed 이슈가 있어 고정 해제 */
272
+ @media (max-width: 768px) {{
273
+ [data-testid="stAppViewContainer"] {{
274
+ background-attachment: initial;
275
+ }}
276
+ }}
277
+ </style>
278
+ """, unsafe_allow_html=True)
279
+
280
+ else:
281
+ # ✅ 단색 배경 (메인 컨텐츠 영역에만 적용)
282
+ color = st.session_state.get("bg_color", "#F1F1F1")
283
+ st.markdown(f"""
284
+ <style>
285
+ [data-testid="stAppViewContainer"] {{
286
+ background-color: {color} !important;
287
+ }}
288
+ </style>
289
+ """, unsafe_allow_html=True)
290
+
291
+ # 함수 호출
292
+ apply_background()
293
+
294
+
295
+
296
+ # ── P 글꼴 크기 14 px ───────────────────────────────────
297
+ st.markdown("""
298
+ <style>
299
+ /* 기본 p 태그 글꼴 크기 */
300
+ html, body, p {
301
+ font-size: 14px !important; /* ← 14 px 고정 */
302
+ line-height: 1.5; /* (선택) 가독성을 위한 줄간격 */
303
+ }
304
+
305
+ /* Streamlit 기본 마진 제거로 불필요한 여백 방지 (선택) */
306
+ p {
307
+ margin-top: 0;
308
+ margin-bottom: 0.5rem;
309
+ }
310
+ </style>
311
+ """, unsafe_allow_html=True)
312
+
313
+ # ───────────────────────────────────── region mode
314
+ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
315
+ country_filter, city_filter, chat_container, log_and_render):
316
+ """region 모드(특정 나라, 도시를 직접 언급했을 경우) 전용 UI & 로직"""
317
+
318
+ # ────────────────── 세션 키 정의
319
+ region_key = "region_chip_selected"
320
+ prev_key = "region_prev_recommended"
321
+ step_key = "region_step"
322
+ sample_key = "region_sample_df"
323
+
324
+ # ────────────────── 0) 초기화
325
+ if step_key not in st.session_state:
326
+ st.session_state[step_key] = "recommend"
327
+ st.session_state[prev_key] = set()
328
+ st.session_state.pop(sample_key, None)
329
+
330
+
331
+ # ────────────────── 1) restart 상태면 인트로만 출력하고 종료
332
+ if st.session_state[step_key] == "restart":
333
+ log_and_render(
334
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가요?",
335
+ sender="bot",
336
+ chat_container=chat_container,
337
+ key="region_restart_intro"
338
+ )
339
+ return
340
+
341
+ # ────────────────── 2) 추천 단계
342
+ if st.session_state[step_key] == "recommend":
343
+
344
+ # 2.1) 추천 문구 출력 (도시 또는 국가 기준)
345
+ city_exists = bool(city_filter) and city_filter in travel_df["여행도시"].values
346
+ country_exists = bool(country_filter) and country_filter in travel_df["여행나라"].values
347
+
348
+ # 존재하지 않는 도시인 경우
349
+ if city_filter and not city_exists:
350
+ intro = generate_region_intro('', country_filter)
351
+ log_and_render(
352
+ f"죄송해요. {city_filter}의 여행지는 아직 미정이에요.<br>하지만, {intro}",
353
+ sender="bot",
354
+ chat_container=chat_container,
355
+ key="region_intro_invalid"
356
+ )
357
+ else:
358
+ # 정상적인 도시/국가일 경우
359
+ intro = generate_region_intro(city_filter, country_filter)
360
+ log_and_render(intro,
361
+ sender="bot",
362
+ chat_container=chat_container,
363
+ key="region_intro")
364
+
365
+ # 2.2) 여행지 후보 목록 필터링
366
+ df = travel_df.drop_duplicates(subset=["여행지"])
367
+ if city_exists:
368
+ df = df[df["여행도시"].str.contains(city_filter, na=False)]
369
+ elif country_exists:
370
+ df = df[df["여행나라"].str.contains(country_filter, na=False)]
371
+
372
+ # 2.3) 이전 추천 목록과 겹치지 않는 여행지만 남김
373
+ prev = st.session_state.setdefault(prev_key, set())
374
+ remaining = df[~df.apply(lambda r: make_key(r) in prev, axis=1)]
375
+
376
+ # 추천 가능한 여행지가 없다면 종료 단계로 전환
377
+ if remaining.empty and sample_key not in st.session_state:
378
+ st.session_state[step_key] = "recommand_end"
379
+ st.rerun()
380
+ return
381
+
382
+
383
+ # 2.4) 샘플링 (이전 샘플이 없거나 비어 있으면 새로 추출)
384
+ if sample_key not in st.session_state or st.session_state[sample_key].empty:
385
+ sampled = remaining.sample(
386
+ n=min(3, len(remaining)), #최대 3개
387
+ random_state=random.randint(1, 9999)
388
+ )
389
+ st.session_state[sample_key] = sampled
390
+
391
+ # tuple 형태로 한꺼번에 추가
392
+ prev.update([make_key(r) for _, r in sampled.iterrows()])
393
+ st.session_state[prev_key] = prev
394
+ else:
395
+ sampled = st.session_state[sample_key]
396
+
397
+ loc_df = st.session_state[sample_key]
398
+
399
+ # 2.5) 추천 리스트 출력 & 칩 UI
400
+ message = (
401
+ "📌 추천 여행지 목록<br>가장 가고 싶은 곳을 골라주세요!<br><br>" +
402
+ "<br>".join([
403
+ f"{i+1}. <strong>{row.여행지}</strong> "
404
+ f"({row.여행나라}, {row.여행도시}) "
405
+ f"{getattr(row, '한줄설명', '설명이 없습니다')}"
406
+ for i, row in enumerate(loc_df.itertuples())
407
+ ])
408
+ )
409
+ with chat_container:
410
+ log_and_render(message,
411
+ sender="bot",
412
+ chat_container=chat_container,
413
+ key=f"region_recommendation_{random.randint(1,999999)}"
414
+ )
415
+ # 칩 버튼으로 추천지 중 선택받기
416
+ prev_choice = st.session_state.get(region_key, None)
417
+ choice = render_chip_buttons(
418
+ loc_df["여행지"].tolist() + ["다른 여행지 보기 🔄"],
419
+ key_prefix="region_chip",
420
+ selected_value=prev_choice
421
+ )
422
+
423
+ # 2.7) 선택 결과 처리
424
+ if not choice or choice == prev_choice:
425
+ return
426
+
427
+ if choice == "다른 여행지 보기 🔄":
428
+ log_and_render("다른 여행지 보기 🔄",
429
+ sender="user",
430
+ chat_container=chat_container,
431
+ key=f"user_place_refresh_{random.randint(1,999999)}")
432
+
433
+ st.session_state.pop(sample_key, None)
434
+ st.rerun()
435
+ return
436
+
437
+ # 2.8) 여행지 선택 완료
438
+ st.session_state[region_key] = choice
439
+ st.session_state[step_key] = "detail"
440
+ st.session_state.chat_log.append(("user", choice))
441
+
442
+
443
+ # 실제로 선택된 여행지만 prev에 기록
444
+ match = sampled[sampled["여행지"] == choice]
445
+ if not match.empty:
446
+ prev.add(make_key(match.iloc[0]))
447
+ st.session_state[prev_key] = prev
448
+
449
+ # 샘플 폐기
450
+ st.session_state.pop(sample_key, None)
451
+ st.rerun()
452
+ return
453
+
454
+ # ────────────────── 3) 추천 종료 단계: 더 이상 추천할 여행지가 없을 때
455
+ elif st.session_state[step_key] == "recommand_end":
456
+ with chat_container:
457
+ # 3.1) 메시지 출력
458
+ log_and_render(
459
+ "⚠️ 더 이상 새로운 여행지가 없어요.<br>다시 질문하시겠어요?",
460
+ sender="bot",
461
+ chat_container=chat_container,
462
+ key="region_empty"
463
+ )
464
+ # 3.2) 재시작 여부 칩 버튼 출력
465
+ restart_done_key = "region_restart_done"
466
+ chip_ph = st.empty()
467
+
468
+ if not st.session_state.get(restart_done_key, False):
469
+ with chip_ph:
470
+ choice = render_chip_buttons(
471
+ ["예 🔄", "아니오 ❌"],
472
+ key_prefix="region_restart"
473
+ )
474
+ else:
475
+ choice = None
476
+
477
+ # 3.3) 아직 아무것도 선택하지 않은 경우
478
+ if choice is None:
479
+ return
480
+
481
+ chip_ph.empty()
482
+ st.session_state[restart_done_key] = True
483
+
484
+ # 3.4) 사용자 선택값 출력
485
+ log_and_render(
486
+ choice,
487
+ sender="user",
488
+ chat_container=chat_container,
489
+ key=f"user_restart_choice_{choice}"
490
+ )
491
+
492
+ # 3.5) 사용자가 재추천을 원하는 경우
493
+ if choice == "예 🔄":
494
+ # 여행 추천 상태 초기화
495
+ for k in [region_key, prev_key, sample_key, restart_done_key]:
496
+ st.session_state.pop(k, None)
497
+ chip_ph.empty()
498
+
499
+ # 다음 추천 단계로 초기화
500
+ st.session_state["user_input_rendered"] = False
501
+ st.session_state["region_step"] = "restart"
502
+
503
+ log_and_render(
504
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가요?",
505
+ sender="bot",
506
+ chat_container=chat_container,
507
+ key="region_restart_intro"
508
+ )
509
+ return
510
+
511
+ # 3.6) 사용자가 종료를 선택한 경우
512
+ else:
513
+ log_and_render("여행 추천을 종료할게요. 필요하실 때 언제든지 또 찾아주세요! ✈️",
514
+ sender="bot",
515
+ chat_container=chat_container,
516
+ key="region_exit")
517
+ st.stop()
518
+ return
519
+
520
+
521
+ # ────────────────── 4) 여행지 상세 단계
522
+ if st.session_state[step_key] == "detail":
523
+ chosen = st.session_state[region_key]
524
+ # city 이름 뽑아서 세션에 저장
525
+ row = travel_df[travel_df["여행지"] == chosen].iloc[0]
526
+ st.session_state["selected_city"] = row["여행도시"]
527
+ st.session_state["selected_place"] = chosen
528
+
529
+ log_and_render(chosen,
530
+ sender="user",
531
+ chat_container=chat_container,
532
+ key=f"user_place_{chosen}")
533
+ handle_selected_place(
534
+ chosen,
535
+ travel_df,
536
+ external_score_df,
537
+ festival_df,
538
+ weather_df,
539
+ chat_container=chat_container
540
+ )
541
+ st.session_state[step_key] = "companion"
542
+ st.rerun()
543
+ return
544
+
545
+
546
+ # ────────────────── 5) 동행·연령 받기 단계
547
+ elif st.session_state[step_key] == "companion":
548
+ with chat_container:
549
+ # 5.1) 안내 메시지 출력
550
+ log_and_render(
551
+ "함께 가는 분이나 연령대를 알려주시면 더 딱 맞는 상품을 골라드릴게요!<br>"
552
+ "1️⃣ 동행 여부 (혼자 / 친구 / 커플 / 가족 / 단체)<br>"
553
+ "2️⃣ 연령대 (20대 / 30대 / 40대 / 50대 / 60대 이상)",
554
+ sender="bot",
555
+ chat_container=chat_container,
556
+ key="ask_companion_age"
557
+ )
558
+
559
+ # 5.1.1) 동행 체크박스
560
+ st.markdown(
561
+ '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">👫 동행 선택</div>',
562
+ unsafe_allow_html=True
563
+ )
564
+ c_cols = st.columns(5)
565
+ comp_flags = {
566
+ "혼자": c_cols[0].checkbox("혼자"),
567
+ "친구": c_cols[1].checkbox("친구"),
568
+ "커플": c_cols[2].checkbox("커플"),
569
+ "가족": c_cols[3].checkbox("가족"),
570
+ "단체": c_cols[4].checkbox("단체"),
571
+ }
572
+ companions = [k for k, v in comp_flags.items() if v]
573
+
574
+ # 5.1.2) 연령 체크박스
575
+ st.markdown(
576
+ '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">🎂 연령 선택</div>',
577
+ unsafe_allow_html=True
578
+ )
579
+ a_cols = st.columns(5)
580
+ age_flags = {
581
+ "20대": a_cols[0].checkbox("20대"),
582
+ "30대": a_cols[1].checkbox("30대"),
583
+ "40대": a_cols[2].checkbox("40대"),
584
+ "50대": a_cols[3].checkbox("50대"),
585
+ "60대 이상": a_cols[4].checkbox("60대 이상"),
586
+ }
587
+ age_group = [k for k, v in age_flags.items() if v]
588
+
589
+ # 5.1.3) 확인 버튼
590
+ confirm = st.button(
591
+ "추천 받기",
592
+ key="btn_confirm_companion",
593
+ disabled=not (companions or age_group),
594
+ )
595
+
596
+ # 5.2) 메시지 출력
597
+ if confirm:
598
+ # 사용자 버블 출력
599
+ user_msg = " / ".join(companions + age_group)
600
+ log_and_render(
601
+ user_msg if user_msg else "선택 안 함",
602
+ sender="user",
603
+ chat_container=chat_container,
604
+ key=f"user_comp_age_{random.randint(1,999999)}"
605
+ )
606
+
607
+ # 세션 저장
608
+ st.session_state["companions"] = companions or None
609
+ st.session_state["age_group"] = age_group or None
610
+
611
+ # 다음 스텝
612
+ st.session_state[step_key] = "package"
613
+ st.rerun()
614
+ return
615
+
616
+
617
+ # ────────────────── 6) 동행·연령 필터링· 패키지 출력 단계
618
+ elif st.session_state[step_key] == "package":
619
+
620
+ # 패키지 버블을 이미 만들었으면 건너뜀
621
+ if st.session_state.get("package_rendered", False):
622
+ st.session_state[step_key] = "package_end"
623
+ return
624
+
625
+ companions = st.session_state.get("companions")
626
+ age_group = st.session_state.get("age_group")
627
+ city = st.session_state.get("selected_city")
628
+ place = st.session_state.get("selected_place")
629
+
630
+ filtered = filter_packages_by_companion_age(
631
+ package_df, companions, age_group, city=city, top_n=2
632
+ )
633
+
634
+ if filtered.empty:
635
+ log_and_render(
636
+ "⚠️ 아쉽지만 지금 조건에 맞는 패키지가 없어요.<br>"
637
+ "다른 조건으로 다시 찾아볼까요?",
638
+ sender="bot", chat_container=chat_container,
639
+ key="no_package"
640
+ )
641
+ st.session_state[step_key] = "companion" # 다시 입력 단계로
642
+ st.rerun()
643
+ return
644
+
645
+ combo_msg = make_companion_age_message(companions, age_group)
646
+ header = f"{combo_msg}"
647
+
648
+ # 패키지 카드 출력
649
+ used_phrases = set()
650
+ theme_row = travel_df[travel_df["여행지"] == place]
651
+ raw_theme = theme_row["통합테마명"].iloc[0] if not theme_row.empty else None
652
+ selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
653
+
654
+ title_candidates = theme_title_phrases.get(selected_ui_theme, ["추천"])
655
+ sampled_titles = random.sample(title_candidates,
656
+ k=min(2, len(title_candidates)))
657
+
658
+ # 메시지 생성
659
+ pkg_msgs = [header]
660
+
661
+ for i, (_, row) in enumerate(filtered.iterrows(), 1):
662
+ desc, used_phrases = make_top2_description_custom(
663
+ row.to_dict(), used_phrases
664
+ )
665
+ tags = format_summary_tags_custom(row["요약정보"])
666
+ title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
667
+ else random.choice(title_candidates))
668
+ title = f"{city} {title_phrase} 패키지"
669
+ url = row.URL
670
+
671
+ pkg_msgs.append(
672
+ f"{i}. <strong>{title}</strong><br>"
673
+ f"🅼 {desc}<br>{tags}<br>"
674
+ f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
675
+ 'style="text-decoration:none;font-weight:600;color:#009c75;">'
676
+ '💚 바로가기&nbsp;↗</a>'
677
+ )
678
+ # 메시지 출력
679
+ log_and_render(
680
+ "<br><br>".join(pkg_msgs),
681
+ sender="bot",
682
+ chat_container=chat_container,
683
+ key=f"pkg_bundle_{random.randint(1,999999)}"
684
+ )
685
+
686
+ # 세션 정리
687
+ st.session_state["package_rendered"] = True
688
+ st.session_state[step_key] = "package_end"
689
+ return
690
+
691
+ # ────────────────── 7) 종료 단계
692
+ elif st.session_state[step_key] == "package_end":
693
+ log_and_render("필요하실 때 언제든지 또 찾아주세요! ✈️",
694
+ sender="bot", chat_container=chat_container,
695
+ key="goodbye")
696
+
697
+ # ───────────────────────────────────── intent 모드
698
+ def intent_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
699
+ country_filter, city_filter, chat_container, intent, log_and_render):
700
+ """intent(의도를 입력했을 경우) 모드 전용 UI & 로직"""
701
+ # ────────────────── 세션 키 정의
702
+ sample_key = "intent_sample_df"
703
+ step_key = "intent_step"
704
+ prev_key = "intent_prev_places"
705
+ intent_key = "intent_chip_selected"
706
+
707
+ # ────────────────── 0) 초기화
708
+ if step_key not in st.session_state:
709
+ st.session_state[step_key] = "recommend_places"
710
+ st.session_state[prev_key] = set()
711
+ st.session_state.pop(sample_key, None)
712
+
713
+ # ────────────────── 1) restart 상태면 인트로만 출력하고 종료
714
+ if st.session_state[step_key] == "restart":
715
+ log_and_render(
716
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가��?",
717
+ sender="bot",
718
+ chat_container=chat_container,
719
+ key="region_restart_intro"
720
+ )
721
+ return
722
+
723
+ # ────────────────── 2) 여행지 추천 단계
724
+ if st.session_state[step_key] == "recommend_places":
725
+ selected_theme = intent
726
+ theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter)
727
+ theme_df = theme_df.drop_duplicates(subset=["여행도시"])
728
+ theme_df = theme_df.drop_duplicates(subset=["여행지"])
729
+
730
+ # 2.1) 이전 추천 기록 세팅
731
+ prev = st.session_state.setdefault(prev_key, set())
732
+
733
+ # 2.2) 이미 샘플이 있다면 result_df 재사용
734
+ if sample_key in st.session_state and not st.session_state[sample_key].empty:
735
+ result_df = st.session_state[sample_key]
736
+ else:
737
+ # 2.3) 새로운 추천 대상 필터링
738
+ candidates = theme_df[~theme_df["여행지"].isin(prev)]
739
+
740
+ # 2.4) 후보가 없다면 종료
741
+ if candidates.empty:
742
+ st.session_state[step_key] = "recommend_places_end"
743
+ st.rerun()
744
+ return
745
+
746
+ # 2.5) 새로운 추천 추출 및 저장
747
+ result_df = apply_weighted_score_filter(candidates)
748
+ st.session_state[sample_key] = result_df
749
+
750
+ # prev에 등록하여 중복 추천 방지
751
+ prev.update(result_df["여행지"])
752
+ st.session_state[prev_key] = prev
753
+
754
+ # 2.6) 오프닝 문장 생성
755
+ opening_line = intent_opening_lines.get(selected_theme, f"'{selected_theme}' 여행지를 소개할게요.")
756
+ opening_line = opening_line.format(len(result_df))
757
+
758
+ # 2.7) 추천 메시지 구성
759
+ message = "<br>".join([
760
+ f"{i+1}. <strong>{row.여행지}</strong> "
761
+ f"({row.여행나라}, {row.여행도시}) "
762
+ f"{getattr(row, '한줄설명', '설명이 없습니다')}"
763
+ for i, row in enumerate(result_df.itertuples())
764
+ ])
765
+
766
+ # 2.8) 챗봇 출력 + 칩 버튼 렌더링
767
+ with chat_container:
768
+ log_and_render(f"{opening_line}<br><br>{message}",
769
+ sender="bot",
770
+ chat_container=chat_container,
771
+ key=f"intent_recommendation_{random.randint(1,999999)}")
772
+
773
+ recommend_names = result_df["여행지"].tolist()
774
+ prev_choice = st.session_state.get(intent_key, None)
775
+ choice = render_chip_buttons(
776
+ recommend_names + ["다른 여행지 보기 🔄"],
777
+ key_prefix="intent_chip",
778
+ selected_value=prev_choice
779
+ )
780
+ # 2.9) 선택 없거나 중복 선택이면 대기
781
+ if not choice or choice == prev_choice:
782
+ return
783
+
784
+ # 선택 결과 처리
785
+ if choice:
786
+ if choice == "다른 여행지 보기 🔄":
787
+ log_and_render("다른 여행지 보기 🔄",
788
+ sender="user",
789
+ chat_container=chat_container,
790
+ key=f"user_place_refresh_{random.randint(1,999999)}")
791
+
792
+ st.session_state.pop(sample_key, None)
793
+ st.rerun()
794
+ return
795
+
796
+ # 정상 선택된 경우
797
+ st.session_state[intent_key] = choice
798
+ st.session_state[step_key] = "detail"
799
+ st.session_state.chat_log.append(("user", choice))
800
+
801
+ # 실제로 선택된 여행지만 prev에 기록
802
+ match = result_df[result_df["여행지"] == choice]
803
+ if not match.empty:
804
+ prev.add(choice)
805
+ st.session_state[prev_key] = prev
806
+
807
+ # 샘플 폐기
808
+ st.session_state.pop(sample_key, None)
809
+ st.rerun()
810
+ return
811
+
812
+ # ────────────────── 3) 추천 종료 단계
813
+ elif st.session_state[step_key] == "recommend_places_end":
814
+ # 3.1) 메시지 출력
815
+ with chat_container:
816
+ log_and_render(
817
+ "⚠️ 더 이상 새로운 여행지가 없어요.<br>다시 질문하시겠어요?",
818
+ sender="bot",
819
+ chat_container=chat_container,
820
+ key="intent_empty"
821
+ )
822
+
823
+ # 3.2) 재시작 여부 칩 버튼 출력
824
+ restart_done_key = "intent_restart_done"
825
+ chip_ph = st.empty()
826
+
827
+ if not st.session_state.get(restart_done_key, False):
828
+ with chip_ph:
829
+ choice = render_chip_buttons(
830
+ ["예 🔄", "아니오 ❌"],
831
+ key_prefix="intent_restart")
832
+ else:
833
+ choice = None
834
+
835
+ # 3.3) 아직 아무것도 선택하지 않은 경우
836
+ if choice is None:
837
+ return
838
+
839
+ chip_ph.empty()
840
+ st.session_state[restart_done_key] = True
841
+
842
+ # 3.4) 사용자 선택값 출력
843
+ log_and_render(choice,
844
+ sender="user",
845
+ chat_container=chat_container
846
+ )
847
+
848
+ # 3.5) 사용자가 재추천을 원하는 경우
849
+ if choice == "예 🔄":
850
+ for k in [sample_key, prev_key, intent_key, restart_done_key]:
851
+ st.session_state.pop(k, None)
852
+ chip_ph.empty()
853
+
854
+ # 다음 추천 단계로 초기화
855
+ st.session_state["user_input_rendered"] = False
856
+ st.session_state["intent_step"] = "restart"
857
+
858
+ log_and_render(
859
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가요?",
860
+ sender="bot",
861
+ chat_container=chat_container,
862
+ key="intent_restart_intro"
863
+ )
864
+ return
865
+
866
+ # 3.6) 사용자가 종료를 선택한 경우
867
+ else:
868
+ log_and_render("여행 추천을 종료할게요. 필요하실 때 언제든지 또 찾아주세요! ✈️",
869
+ sender="bot",
870
+ chat_container=chat_container,
871
+ key="intent_exit")
872
+ st.stop()
873
+ return
874
+
875
+ # ────────────────── 4) 여행지 상세 단계
876
+ if st.session_state[step_key] == "detail":
877
+ chosen = st.session_state[intent_key]
878
+ # city 이름 뽑아서 세션에 저장
879
+ row = travel_df[travel_df["여행지"] == chosen].iloc[0]
880
+ st.session_state["selected_city"] = row["여행도시"]
881
+ st.session_state["selected_place"] = chosen
882
+
883
+ log_and_render(chosen,
884
+ sender="user",
885
+ chat_container=chat_container,
886
+ key=f"user_place_{chosen}")
887
+ handle_selected_place(
888
+ chosen,
889
+ travel_df,
890
+ external_score_df,
891
+ festival_df,
892
+ weather_df,
893
+ chat_container=chat_container
894
+ )
895
+ st.session_state[step_key] = "companion"
896
+ st.rerun()
897
+ return
898
+
899
+ # ────────────────── 5) 동행·연령 받기 단계
900
+ elif st.session_state[step_key] == "companion":
901
+ with chat_container:
902
+ # 5.1) 안내 메시지 출력
903
+ log_and_render(
904
+ "함께 가는 분이나 연령대를 알려주시면 더 딱 맞는 상품을 골라드릴게요!<br>"
905
+ "1️⃣ 동행 여부 (혼자 / 친구 / 커플 / 가족 / 단체)<br>"
906
+ "2️⃣ 연령대 (20대 / 30대 / 40대 / 50대 / 60대 이상)",
907
+ sender="bot",
908
+ chat_container=chat_container,
909
+ key="ask_companion_age"
910
+ )
911
+
912
+ # 5.1.1) 동행 체크박스
913
+ st.markdown(
914
+ '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">👫 동행 선택</div>',
915
+ unsafe_allow_html=True
916
+ )
917
+ c_cols = st.columns(5)
918
+ comp_flags = {
919
+ "혼자": c_cols[0].checkbox("혼자"),
920
+ "친구": c_cols[1].checkbox("친구"),
921
+ "커플": c_cols[2].checkbox("커플"),
922
+ "가족": c_cols[3].checkbox("가족"),
923
+ "단체": c_cols[4].checkbox("단체"),
924
+ }
925
+ companions = [k for k, v in comp_flags.items() if v]
926
+
927
+ # 5.1.2) 연령 체크박스
928
+ st.markdown(
929
+ '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">🎂 연령 선택</div>',
930
+ unsafe_allow_html=True
931
+ )
932
+ a_cols = st.columns(5)
933
+ age_flags = {
934
+ "20대": a_cols[0].checkbox("20대"),
935
+ "30대": a_cols[1].checkbox("30대"),
936
+ "40대": a_cols[2].checkbox("40대"),
937
+ "50대": a_cols[3].checkbox("50대"),
938
+ "60대 이상": a_cols[4].checkbox("60대 이상"),
939
+ }
940
+ age_group = [k for k, v in age_flags.items() if v]
941
+
942
+ # 5.1.3) 확인 버튼
943
+ confirm = st.button(
944
+ "추천 받기",
945
+ key="btn_confirm_companion",
946
+ disabled=not (companions or age_group),
947
+ )
948
+
949
+ # 5.2) 메시지 출력
950
+ if confirm:
951
+ # 사용자 버블 출력
952
+ user_msg = " / ".join(companions + age_group)
953
+ log_and_render(
954
+ user_msg if user_msg else "선택 안 함",
955
+ sender="user",
956
+ chat_container=chat_container,
957
+ key=f"user_comp_age_{random.randint(1,999999)}"
958
+ )
959
+
960
+ # 세션 저장
961
+ st.session_state["companions"] = companions or None
962
+ st.session_state["age_group"] = age_group or None
963
+
964
+ # 다음 스텝
965
+ st.session_state[step_key] = "package"
966
+ st.rerun()
967
+ return
968
+
969
+ # ────────────────── 6) 동행·연령 필터링· 패키지 출력 단계
970
+ elif st.session_state[step_key] == "package":
971
+
972
+ # 패키지 버블을 이미 만들었으면 건너뜀
973
+ if st.session_state.get("package_rendered", False):
974
+ st.session_state[step_key] = "package_end"
975
+ return
976
+
977
+ companions = st.session_state.get("companions")
978
+ age_group = st.session_state.get("age_group")
979
+ city = st.session_state.get("selected_city")
980
+ place = st.session_state.get("selected_place")
981
+
982
+ filtered = filter_packages_by_companion_age(
983
+ package_df, companions, age_group, city=city, top_n=2
984
+ )
985
+
986
+ if filtered.empty:
987
+ log_and_render(
988
+ "⚠️ 아쉽지만 지금 조건에 맞는 패키지가 없어요.<br>"
989
+ "다른 조건으로 다시 찾아볼까요?",
990
+ sender="bot", chat_container=chat_container,
991
+ key="no_package"
992
+ )
993
+ st.session_state[step_key] = "companion" # 다시 입력 단계로
994
+ st.rerun()
995
+ return
996
+
997
+ combo_msg = make_companion_age_message(companions, age_group)
998
+ header = f"{combo_msg}"
999
+
1000
+ # 패키지 카드 출력
1001
+ used_phrases = set()
1002
+ theme_row = travel_df[travel_df["여행지"] == place]
1003
+ raw_theme = theme_row["통합테마명"].iloc[0] if not theme_row.empty else None
1004
+ selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
1005
+
1006
+ title_candidates = theme_title_phrases.get(selected_ui_theme, ["추천"])
1007
+ sampled_titles = random.sample(title_candidates,
1008
+ k=min(2, len(title_candidates)))
1009
+
1010
+ # 메시지 생성
1011
+ pkg_msgs = [header]
1012
+
1013
+ for i, (_, row) in enumerate(filtered.iterrows(), 1):
1014
+ desc, used_phrases = make_top2_description_custom(
1015
+ row.to_dict(), used_phrases
1016
+ )
1017
+ tags = format_summary_tags_custom(row["요약정보"])
1018
+ title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
1019
+ else random.choice(title_candidates))
1020
+ title = f"{city} {title_phrase} 패키지"
1021
+ url = row.URL
1022
+
1023
+ pkg_msgs.append(
1024
+ f"{i}. <strong>{title}</strong><br>"
1025
+ f"🅼 {desc}<br>{tags}<br>"
1026
+ f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
1027
+ 'style="text-decoration:none;font-weight:600;color:#009c75;">'
1028
+ '💚 바로가기&nbsp;↗</a>'
1029
+ )
1030
+ # 메시지 출력
1031
+ log_and_render(
1032
+ "<br><br>".join(pkg_msgs),
1033
+ sender="bot",
1034
+ chat_container=chat_container,
1035
+ key=f"pkg_bundle_{random.randint(1,999999)}"
1036
+ )
1037
+
1038
+ # 세션 정리
1039
+ st.session_state["package_rendered"] = True
1040
+ st.session_state[step_key] = "package_end"
1041
+ return
1042
+
1043
+ # ────────────────── 7) 종료 단계
1044
+ elif st.session_state[step_key] == "package_end":
1045
+ log_and_render("필요하실 때 언제든지 또 찾아주세요! ✈️",
1046
+ sender="bot", chat_container=chat_container,
1047
+ key="goodbye")
1048
+
1049
+ # ───────────────────────────────────── emotion 모드
1050
+ def emotion_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
1051
+ country_filter, city_filter, chat_container, candidate_themes,
1052
+ intent, emotion_groups, top_emotions, log_and_render):
1053
+ """emotion(감정을 입력했을 경우) 모드 전용 UI & 로직"""
1054
+
1055
+ # ────────────────── 세션 키 정의
1056
+ sample_key = "emotion_sample_df"
1057
+ step_key = "emotion_step"
1058
+ theme_key = "selected_theme"
1059
+ emotion_key = "emotion_chip_selected"
1060
+ prev_key = "emotion_prev_places"
1061
+
1062
+ # ────────────────── 0) 초기화
1063
+ if step_key not in st.session_state:
1064
+ st.session_state[step_key] = "theme_selection"
1065
+ st.session_state[prev_key] = set()
1066
+ st.session_state.pop(sample_key, None)
1067
+
1068
+
1069
+ # ────────────────── 1) restart 상태면 인트로만 출력하고 종료
1070
+ if st.session_state[step_key] == "restart":
1071
+ log_and_render(
1072
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가요?",
1073
+ sender="bot",
1074
+ chat_container=chat_container,
1075
+ key="region_restart_intro"
1076
+ )
1077
+ return
1078
+
1079
+ # ────────────────── 2) 테마 추천 단계
1080
+ if st.session_state[step_key] == "theme_selection":
1081
+ # 추천 테마 1개일 경우
1082
+ if len(candidate_themes) == 1:
1083
+ selected_theme = candidate_themes[0]
1084
+ st.session_state[theme_key] = selected_theme
1085
+ log_and_render(f"추천 가능한 테마가 1개이므로 '{selected_theme}'을 선택할게요.", sender="bot", chat_container=chat_container)
1086
+ st.session_state[step_key] = "recommend_places"
1087
+ st.rerun()
1088
+
1089
+ # 테마가 여러 개일 경우
1090
+ else:
1091
+ # 인트로 메시지
1092
+ intro_msg = generate_intro_message(intent=intent, emotion_groups=emotion_groups, emotion_scores=top_emotions)
1093
+ log_and_render(f"{intro_msg}<br>아래 중 마음이 끌리는 여행 스타일을 골라주세요 💫", sender="bot", chat_container=chat_container)
1094
+
1095
+ # 후보 테마 준비
1096
+ dfs = [recommend_places_by_theme(t, country_filter, city_filter) for t in candidate_themes]
1097
+ dfs = [df for df in dfs if not df.empty]
1098
+ all_theme_df = pd.concat(dfs) if dfs else pd.DataFrame(columns=travel_df.columns)
1099
+ all_theme_df = all_theme_df.drop_duplicates(subset=["여행지"])
1100
+ all_theme_names = all_theme_df["통합테마명"].dropna().tolist()
1101
+
1102
+ available_themes = []
1103
+ for t in candidate_themes:
1104
+ if t in all_theme_names and t not in available_themes:
1105
+ available_themes.append(t)
1106
+ for t in all_theme_names:
1107
+ if t not in available_themes:
1108
+ available_themes.append(t)
1109
+ available_themes = available_themes[:3] # 최대 3개
1110
+
1111
+ # 칩 UI 출력
1112
+ with chat_container:
1113
+ chip = render_chip_buttons(
1114
+ [theme_ui_map.get(t, (t, ""))[0] for t in available_themes],
1115
+ key_prefix="theme_chip"
1116
+ )
1117
+
1118
+ # 선택이 완료되면 다음 단계로 이동
1119
+ if chip:
1120
+ selected_theme = ui_to_theme_map.get(chip, chip)
1121
+ st.session_state[theme_key] = selected_theme
1122
+ st.session_state[step_key] = "recommend_places"
1123
+ st.session_state["emotion_all_theme_df"] = all_theme_df
1124
+ log_and_render(f"{chip}", sender="user",
1125
+ chat_container=chat_container)
1126
+
1127
+ st.rerun()
1128
+
1129
+ # ────────────────── 3) 여행지 추천 단계
1130
+ if st.session_state[step_key] == "recommend_places":
1131
+ all_theme_df = st.session_state.get("emotion_all_theme_df", pd.DataFrame())
1132
+ selected_theme = st.session_state.get(theme_key, "")
1133
+
1134
+ prev_key = "emotion_prev_places"
1135
+ prev = st.session_state.setdefault(prev_key, set())
1136
+
1137
+ # 예외 처리: 데이터 없을 경우
1138
+ if all_theme_df.empty or not selected_theme:
1139
+ log_and_render("추천 데이터를 불러오는 데 문제가 발생했어요. <br>다시 입력해 주세요.", sender="bot", chat_container=chat_container)
1140
+ return
1141
+
1142
+ if sample_key not in st.session_state:
1143
+ theme_df = all_theme_df[all_theme_df["통합테마명"] == selected_theme]
1144
+ theme_df = theme_df.drop_duplicates(subset=["여행도시"])
1145
+ theme_df = theme_df.drop_duplicates(subset=["여행지"])
1146
+ remaining = theme_df[~theme_df["여행지"].isin(prev)]
1147
+
1148
+ if remaining.empty:
1149
+ st.session_state[step_key] = "recommend_places_end"
1150
+ st.rerun()
1151
+ return
1152
+
1153
+ result_df = apply_weighted_score_filter(remaining)
1154
+ st.session_state[sample_key] = result_df
1155
+ else:
1156
+ result_df = st.session_state[sample_key]
1157
+
1158
+ # 추천 수 부족할 경우 Fallback 보완
1159
+ if len(result_df) < 3:
1160
+ fallback = travel_df[
1161
+ (travel_df["통합테마명"] == selected_theme) &
1162
+ (~travel_df["여행지"].isin(result_df["여행지"]))
1163
+ ].drop_duplicates(subset=["여행지"])
1164
+
1165
+ if not fallback.empty:
1166
+ fill_count = min(3 - len(result_df), len(fallback))
1167
+ fill = fallback.sample(n=fill_count, random_state=random.randint(1, 9999))
1168
+ result_df = pd.concat([result_df, fill], ignore_index=True)
1169
+
1170
+ # 샘플 저장
1171
+ st.session_state[sample_key] = result_df
1172
+
1173
+ # 2.1)첫 문장 출력
1174
+ ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0]
1175
+ opening_line_template = theme_opening_lines.get(ui_name)
1176
+ opening_line = opening_line_template.format(len(result_df)) if opening_line_template else ""
1177
+
1178
+ message = (
1179
+ "<br>".join([
1180
+ f"{i+1}. <strong>{row.여행지}</strong> "
1181
+ f"({row.여행나라}, {row.여행도시}) "
1182
+ f"{getattr(row, '한줄설명', '설명이 없습니다')}"
1183
+ for i, row in enumerate(result_df.itertuples())
1184
+ ])
1185
+ )
1186
+ if opening_line_template:
1187
+ message_combined = f"{opening_line}<br><br>{message}"
1188
+ with chat_container:
1189
+ log_and_render(message_combined,
1190
+ sender="bot",
1191
+ chat_container=chat_container,
1192
+ key=f"emotion_recommendation_{random.randint(1,999999)}"
1193
+ )
1194
+ # 2.2) 칩 버튼으로 추천지 중 선택받기
1195
+ recommend_names = result_df["여행지"].tolist()
1196
+ prev_choice = st.session_state.get(emotion_key, None)
1197
+ choice = render_chip_buttons(
1198
+ recommend_names + ["다른 여행지 보기 🔄"],
1199
+ key_prefix="emotion_chip",
1200
+ selected_value=prev_choice
1201
+ )
1202
+
1203
+ # 2.3) 선택 결과 처리
1204
+ if not choice or choice == prev_choice:
1205
+ return
1206
+
1207
+ if choice == "다른 여행지 보기 🔄":
1208
+ log_and_render("다른 여행지 보기 🔄",
1209
+ sender="user",
1210
+ chat_container=chat_container,
1211
+ key=f"user_place_refresh_{random.randint(1,999999)}")
1212
+
1213
+ st.session_state.pop(sample_key, None)
1214
+ st.rerun()
1215
+ return
1216
+
1217
+ # 실제 선택한 여행지 처리
1218
+ st.session_state[emotion_key] = choice
1219
+ st.session_state[step_key] = "detail"
1220
+ st.session_state.chat_log.append(("user", choice))
1221
+
1222
+ # 선택한 여행지를 prev 기록에 추가
1223
+ match = result_df[result_df["여행지"] == choice]
1224
+ if not match.empty:
1225
+ prev.add(choice)
1226
+ st.session_state[prev_key] = prev
1227
+
1228
+ # 샘플 폐기
1229
+ st.session_state.pop(sample_key, None)
1230
+ st.rerun()
1231
+ return
1232
+
1233
+ # ────────────────── 3) 추천 종료 단계: 더 이상 추천할 여행지가 없을 때
1234
+ elif st.session_state[step_key] == "recommend_place_end":
1235
+ with chat_container:
1236
+ # 3.1) 메시지 출력
1237
+ log_and_render(
1238
+ "⚠️ 더 이상 새로운 여행지가 없어요.<br>다시 질문하시겠어요?",
1239
+ sender="bot",
1240
+ chat_container=chat_container,
1241
+ key="emotion_empty"
1242
+ )
1243
+ # 3.2) 재시작 여부 칩 버튼 출력
1244
+ restart_done_key = "emotion_restart_done"
1245
+ chip_ph = st.empty()
1246
+
1247
+ if not st.session_state.get(restart_done_key, False):
1248
+ with chip_ph:
1249
+ choice = render_chip_buttons(
1250
+ ["예 🔄", "아니오 ❌"],
1251
+ key_prefix="emotion_restart"
1252
+ )
1253
+ else:
1254
+ choice = None
1255
+
1256
+ # 3.3) 아직 아무것도 선택하지 않은 경우
1257
+ if choice is None:
1258
+ return
1259
+
1260
+ chip_ph.empty()
1261
+ st.session_state[restart_done_key] = True
1262
+
1263
+ # 3.4) 사용자 선택값 출력
1264
+ log_and_render(
1265
+ choice,
1266
+ sender="user",
1267
+ chat_container=chat_container,
1268
+ key=f"user_restart_choice_{choice}"
1269
+ )
1270
+
1271
+ # 3.5) 사용자가 재추천을 원하는 경우
1272
+ if choice == "예 🔄":
1273
+ # 여행 추천 상태 초기화
1274
+ for k in [emotion_key, prev_key, sample_key, restart_done_key]:
1275
+ st.session_state.pop(k, None)
1276
+ chip_ph.empty()
1277
+
1278
+ # 다음 추천 단계로 초기화
1279
+ st.session_state["user_input_rendered"] = False
1280
+ st.session_state["emotion_step"] = "restart"
1281
+
1282
+ log_and_render(
1283
+ "다시 여행지를 추천해드릴게요!<br>요즘 떠오르는 여행이 있으신가요?",
1284
+ sender="bot",
1285
+ chat_container=chat_container,
1286
+ key="emotion_restart_intro"
1287
+ )
1288
+ return
1289
+
1290
+ # 3.6) 사용자가 종료를 선택한 경우
1291
+ else:
1292
+ log_and_render("여행 추천을 종료할게요. 필요하실 때 언제든지 또 찾아주세요! ✈️",
1293
+ sender="bot",
1294
+ chat_container=chat_container,
1295
+ key="emotion_exit")
1296
+ st.stop()
1297
+ return
1298
+
1299
+ # ────────────────── 4) 여행지 상세 단계
1300
+ if st.session_state[step_key] == "detail":
1301
+ chosen = st.session_state[emotion_key]
1302
+ # city 이름 뽑아서 세션에 저장
1303
+ row = travel_df[travel_df["여행지"] == chosen].iloc[0]
1304
+ st.session_state["selected_city"] = row["여행도시"]
1305
+ st.session_state["selected_place"] = chosen
1306
+
1307
+ log_and_render(chosen,
1308
+ sender="user",
1309
+ chat_container=chat_container,
1310
+ key=f"user_place_{chosen}")
1311
+ handle_selected_place(
1312
+ chosen,
1313
+ travel_df,
1314
+ external_score_df,
1315
+ festival_df,
1316
+ weather_df,
1317
+ chat_container=chat_container
1318
+ )
1319
+ st.session_state[step_key] = "companion"
1320
+ st.rerun()
1321
+ return
1322
+
1323
+ # ────────────────── 5) 동행·연령 받기 단계
1324
+ elif st.session_state[step_key] == "companion":
1325
+ with chat_container:
1326
+ # 5.1) 안내 메시지 출력
1327
+ log_and_render(
1328
+ "함께 가는 분이나 연령대를 알려주시면 더 딱 맞는 상품을 골라드릴게요!<br>"
1329
+ "1️⃣ 동행 여부 (혼자 / 친구 / 커플 / 가족 / 단체)<br>"
1330
+ "2️⃣ 연령대 (20대 / 30대 / 40대 / 50대 / 60대 이상)",
1331
+ sender="bot",
1332
+ chat_container=chat_container,
1333
+ key="ask_companion_age"
1334
+ )
1335
+
1336
+ # 5.1.1) 동행 체크박스
1337
+ st.markdown(
1338
+ '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">👫 동행 선택</div>',
1339
+ unsafe_allow_html=True
1340
+ )
1341
+ c_cols = st.columns(5)
1342
+ comp_flags = {
1343
+ "혼자": c_cols[0].checkbox("혼자"),
1344
+ "친구": c_cols[1].checkbox("친구"),
1345
+ "커플": c_cols[2].checkbox("커플"),
1346
+ "가족": c_cols[3].checkbox("가족"),
1347
+ "단체": c_cols[4].checkbox("단체"),
1348
+ }
1349
+ companions = [k for k, v in comp_flags.items() if v]
1350
+
1351
+ # 5.1.2) 연령 체크박스
1352
+ st.markdown(
1353
+ '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">🎂 연령 선택</div>',
1354
+ unsafe_allow_html=True
1355
+ )
1356
+ a_cols = st.columns(5)
1357
+ age_flags = {
1358
+ "20대": a_cols[0].checkbox("20대"),
1359
+ "30대": a_cols[1].checkbox("30대"),
1360
+ "40대": a_cols[2].checkbox("40대"),
1361
+ "50대": a_cols[3].checkbox("50대"),
1362
+ "60대 이상": a_cols[4].checkbox("60대 이상"),
1363
+ }
1364
+ age_group = [k for k, v in age_flags.items() if v]
1365
+
1366
+ # 5.1.3) 확인 버튼
1367
+ confirm = st.button(
1368
+ "추천 받기",
1369
+ key="btn_confirm_companion",
1370
+ disabled=not (companions or age_group),
1371
+ )
1372
+
1373
+ # 5.2) 메시지 출력
1374
+ if confirm:
1375
+ # 사용자 버블 출력
1376
+ user_msg = " / ".join(companions + age_group)
1377
+ log_and_render(
1378
+ user_msg if user_msg else "선택 안 함",
1379
+ sender="user",
1380
+ chat_container=chat_container,
1381
+ key=f"user_comp_age_{random.randint(1,999999)}"
1382
+ )
1383
+
1384
+ # 세션 저장
1385
+ st.session_state["companions"] = companions or None
1386
+ st.session_state["age_group"] = age_group or None
1387
+
1388
+ # 다음 스텝
1389
+ st.session_state[step_key] = "package"
1390
+ st.rerun()
1391
+ return
1392
+
1393
+ # ────────────────── 6) 동행·연령 필터링· 패키지 출력 단계
1394
+ elif st.session_state[step_key] == "package":
1395
+
1396
+ # 패키지 버블을 이미 만들었으면 건너뜀
1397
+ if st.session_state.get("package_rendered", False):
1398
+ st.session_state[step_key] = "package_end"
1399
+ return
1400
+
1401
+ companions = st.session_state.get("companions")
1402
+ age_group = st.session_state.get("age_group")
1403
+ city = st.session_state.get("selected_city")
1404
+ place = st.session_state.get("selected_place")
1405
+
1406
+ filtered = filter_packages_by_companion_age(
1407
+ package_df, companions, age_group, city=city, top_n=2
1408
+ )
1409
+
1410
+ if filtered.empty:
1411
+ log_and_render(
1412
+ "⚠️ 아쉽지만 지금 조건에 맞는 패키지가 없어요.<br>"
1413
+ "다른 조건으로 다시 찾아볼까요?",
1414
+ sender="bot", chat_container=chat_container,
1415
+ key="no_package"
1416
+ )
1417
+ st.session_state[step_key] = "companion"
1418
+ st.rerun()
1419
+ return
1420
+
1421
+ combo_msg = make_companion_age_message(companions, age_group)
1422
+ header = f"{combo_msg}"
1423
+
1424
+ # 패키지 카드 출력
1425
+ used_phrases = set()
1426
+ theme_row = travel_df[travel_df["여행지"] == place]
1427
+ raw_theme = theme_row["통합테마명"].iloc[0] if not theme_row.empty else None
1428
+ selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0]
1429
+
1430
+ title_candidates = theme_title_phrases.get(selected_ui_theme, ["추천"])
1431
+ sampled_titles = random.sample(title_candidates,
1432
+ k=min(2, len(title_candidates)))
1433
+
1434
+ # 메시지 생성
1435
+ pkg_msgs = [header]
1436
+
1437
+ for i, (_, row) in enumerate(filtered.iterrows(), 1):
1438
+ desc, used_phrases = make_top2_description_custom(
1439
+ row.to_dict(), used_phrases
1440
+ )
1441
+ tags = format_summary_tags_custom(row["요약정보"])
1442
+ title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles)
1443
+ else random.choice(title_candidates))
1444
+ title = f"{city} {title_phrase} 패키지"
1445
+ url = row.URL
1446
+
1447
+ pkg_msgs.append(
1448
+ f"{i}. <strong>{title}</strong><br>"
1449
+ f"🅼 {desc}<br>{tags}<br>"
1450
+ f'<a href="{url}" target="_blank" rel="noopener noreferrer" '
1451
+ 'style="text-decoration:none;font-weight:600;color:#009c75;">'
1452
+ '💚 바로가기&nbsp;↗</a>'
1453
+ )
1454
+ # 메시지 출력
1455
+ log_and_render(
1456
+ "<br><br>".join(pkg_msgs),
1457
+ sender="bot",
1458
+ chat_container=chat_container,
1459
+ key=f"pkg_bundle_{random.randint(1,999999)}"
1460
+ )
1461
+
1462
+ # 세션 정리
1463
+ st.session_state["package_rendered"] = True
1464
+ st.session_state[step_key] = "package_end"
1465
+ return
1466
+
1467
+ # ────────────────── 7) 종료 단계
1468
+ elif st.session_state[step_key] == "package_end":
1469
+ log_and_render("필요하실 때 언제든지 또 찾아주세요! ✈️",
1470
+ sender="bot", chat_container=chat_container,
1471
+ key="goodbye")
1472
+
1473
+ # ───────────────────────────────────── unknown 모드
1474
+ def unknown_ui(country, city, chat_container, log_and_render):
1475
+ """unknown 모드(아직 DB에 없는 나라·도시일 때 안내) 전용 UI & 로직"""
1476
+ # 안내 메시지
1477
+ if city:
1478
+ msg = (f"🔍 죄송해요. 해당 <strong>{city}</strong>의 여행지는 "
1479
+ "아직 준비 중이에요.<br> 빠른 시일 안에 업데이트할게요!")
1480
+ elif country:
1481
+ msg = (f"🔍 죄송해요. 해당 <strong>{country}</strong>의 여행지는 "
1482
+ "아직 준비 중이에요.<br> 빠른 시일 안에 업데이트할게요!")
1483
+ else:
1484
+ msg = "🔍 죄송해요. 해당 여행지는 아직 준비 중이에요."
1485
+
1486
+ with chat_container:
1487
+ log_and_render(
1488
+ f"{msg}",
1489
+ sender="bot",
1490
+ chat_container=chat_container,
1491
+ key="unknown_dest"
1492
+ )
1493
+
1494
+ # ───────────────────────────────────── 챗봇 호출
1495
+ def main():
1496
+
1497
+ init_session()
1498
+ chat_container = st.container()
1499
+
1500
+ # 🎛️ 말풍선/표시 옵션 (③, ④)
1501
+ st.sidebar.subheader("⚙️ 대화 표시")
1502
+ st.sidebar.selectbox("테마", ["피스타치오", "스카이블루", "크리미오트"], key="bubble_theme")
1503
+ st.sidebar.toggle("타임스탬프 표시", value=False, key="show_time")
1504
+
1505
+
1506
+ # ✅ 타자 효과 on/off 토글 (기본 ON)
1507
+ st.sidebar.toggle("타자 효과", value=False, key="typewriter_on")
1508
+
1509
+ if "chat_log" in st.session_state and st.session_state.chat_log:
1510
+ replay_log(chat_container)
1511
+
1512
+ # ───── greeting 메시지 출력
1513
+ if not st.session_state.get("greeting_rendered", False):
1514
+ greeting_message = (
1515
+ "안녕하세요. <strong>모아(MoAi)</strong>입니다.🤖<br><br>"
1516
+ "요즘 어떤 여행이 떠오르세요?<br>""모아가 딱 맞는 여행지를 찾아드릴게요."
1517
+ )
1518
+ log_and_render(
1519
+ greeting_message,
1520
+ sender="bot",
1521
+ chat_container=chat_container,
1522
+ key="greeting"
1523
+ )
1524
+ st.session_state["greeting_rendered"] = True
1525
+
1526
+
1527
+ # ───── 사용자 입력 & 추천 시작
1528
+ # 1) 사용자 입력
1529
+ user_input = st.text_input(
1530
+ "입력창", # 비어있지 않은 라벨(접근성 확보)
1531
+ placeholder="ex)'요즘 힐링이 필요해요', '가족 여행 어디가 좋을까요?'",
1532
+ key="user_input",
1533
+ label_visibility="collapsed", # 화면에선 숨김
1534
+ )
1535
+ user_input_key = "last_user_input"
1536
+ select_keys = ["intent_chip_selected", "region_chip_selected", "emotion_chip_selected", "theme_chip_selected"]
1537
+
1538
+ # 1-1) “진짜 새로 입력” 감지
1539
+ prev = st.session_state.get(user_input_key, "")
1540
+ if user_input and user_input != prev:
1541
+ for k in select_keys:
1542
+ st.session_state.pop(k, None)
1543
+ st.session_state[user_input_key] = user_input
1544
+ st.session_state["user_input_rendered"] = False
1545
+
1546
+ # step 초기화
1547
+ st.session_state["region_step"] = "recommend"
1548
+ st.rerun()
1549
+
1550
+ # 1-2) 사용자 메시지 한 번만 렌더링
1551
+ if user_input and not st.session_state.get("user_input_rendered", False):
1552
+ log_and_render(
1553
+ user_input,
1554
+ sender="user",
1555
+ chat_container = chat_container,
1556
+ key=f"user_input_{user_input}"
1557
+
1558
+ )
1559
+ st.session_state["user_input_rendered"] = True
1560
+
1561
+ if user_input:
1562
+ # 1) 저비용 단계: 위치/의도 먼저
1563
+ country_filter, city_filter, loc_mode = detect_location_filter(user_input)
1564
+ intent, intent_score = detect_intent(user_input)
1565
+
1566
+ # 사이드바에서 임계값을 쓸 수 있게 했다면, 없으면 0.70 기본
1567
+ threshold = st.session_state.get("intent_threshold", 0.70)
1568
+
1569
+ # 2) 모드 결정: 지역 확정 → intent 확정 → unknown → (그 외) emotion
1570
+ if loc_mode == "region":
1571
+ mode = "region"
1572
+ top_emotions, emotion_groups = [], []
1573
+ elif intent_score >= threshold:
1574
+ mode = "intent"
1575
+ top_emotions, emotion_groups = [], []
1576
+ elif loc_mode == "unknown":
1577
+ mode = "unknown"
1578
+ top_emotions, emotion_groups = [], []
1579
+ else:
1580
+ mode = "emotion"
1581
+ # 3) 고비용 단계: 정말 필요할 때만 감성(BERT) 실행
1582
+ # with st.spinner("감정 분석 중..."): # UX 원하시면 스피너 추가
1583
+ top_emotions, emotion_groups = analyze_emotion(user_input)
1584
+
1585
+ # 4) 모드별 분기 (필요한 계산만 수행)
1586
+ if mode == "region":
1587
+ region_ui(
1588
+ travel_df,
1589
+ external_score_df,
1590
+ festival_df,
1591
+ weather_df,
1592
+ package_df,
1593
+ country_filter,
1594
+ city_filter,
1595
+ chat_container,
1596
+ log_and_render
1597
+ )
1598
+ return
1599
+
1600
+ elif mode == "intent":
1601
+ intent_ui(
1602
+ travel_df,
1603
+ external_score_df,
1604
+ festival_df,
1605
+ weather_df,
1606
+ package_df,
1607
+ country_filter,
1608
+ city_filter,
1609
+ chat_container,
1610
+ intent,
1611
+ log_and_render
1612
+ )
1613
+ return
1614
+
1615
+ elif mode == "unknown":
1616
+ unknown_ui(
1617
+ country_filter,
1618
+ city_filter,
1619
+ chat_container,
1620
+ log_and_render
1621
+ )
1622
+ return
1623
+
1624
+ else: # emotion
1625
+ # emotion 모드에서만 테마 추출 (불필요한 계산 방지)
1626
+ candidate_themes = extract_themes(
1627
+ emotion_groups,
1628
+ intent,
1629
+ force_mode=False # intent 확정 케이스가 아니라면 False
1630
+ )
1631
+ emotion_ui(
1632
+ travel_df,
1633
+ external_score_df,
1634
+ festival_df,
1635
+ weather_df,
1636
+ package_df,
1637
+ country_filter,
1638
+ city_filter,
1639
+ chat_container,
1640
+ candidate_themes,
1641
+ intent,
1642
+ emotion_groups,
1643
+ top_emotions,
1644
+ log_and_render
1645
+ )
1646
+
1647
+ if __name__ == "__main__":
1648
+ main()
1649
+
1650
+
1651
+ #cmd 입력-> cd "파일 위치 경로 복붙"
1652
+ #ex(C:\Users\gayoung\Desktop\multi\0514 - project\06 - streamlit 테스트\test)
1653
+ #cmd 입력 -> streamlit run app.py
chat_a.py ADDED
@@ -0,0 +1,1568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os, io, json, pathlib, re, random
3
+ import pandas as pd
4
+ import streamlit as st
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from collections import defaultdict
8
+ from datetime import datetime
9
+ from huggingface_hub import hf_hub_download
10
+ from sentence_transformers import SentenceTransformer, util
11
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
12
+ from css import log_and_render
13
+
14
+ # ──────────────────────────────── 캐시/데이터셋 설정 ────────────────────────────────
15
+ HOME = pathlib.Path.home()
16
+
17
+ # ✅ ENV가 있으면 따르고, 없으면 홈 밑 .cache/hf-cache 사용
18
+ CACHE_DIR = os.getenv("TRANSFORMERS_CACHE") or os.path.expanduser("~/.cache/hf-cache")
19
+ os.makedirs(CACHE_DIR, exist_ok=True)
20
+
21
+ HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data")
22
+ HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main")
23
+
24
+ def _is_pointer_bytes(b: bytes) -> bool:
25
+ head = b[:2048].decode(errors="ignore").lower()
26
+ return (
27
+ "version https://git-lfs.github.com/spec/v1" in head
28
+ or "git-lfs" in head
29
+ or "xet" in head
30
+ or "pointer size" in head
31
+ )
32
+
33
+ def _read_csv_bytes(b: bytes) -> pd.DataFrame:
34
+ try:
35
+ return pd.read_csv(io.BytesIO(b), encoding="utf-8")
36
+ except UnicodeDecodeError:
37
+ return pd.read_csv(io.BytesIO(b), encoding="cp949")
38
+
39
+ def load_csv_smart(local_path: str,
40
+ hub_filename: str | None = None,
41
+ repo_id: str = HF_DATASET_REPO,
42
+ repo_type: str = "dataset",
43
+ revision: str = HF_DATASET_REV) -> pd.DataFrame:
44
+ if hub_filename is None:
45
+ hub_filename = os.path.basename(local_path)
46
+ if os.path.exists(local_path):
47
+ with open(local_path, "rb") as f:
48
+ data = f.read()
49
+ if not _is_pointer_bytes(data):
50
+ try:
51
+ return pd.read_csv(io.BytesIO(data), encoding="utf-8")
52
+ except UnicodeDecodeError:
53
+ return pd.read_csv(io.BytesIO(data), encoding="cp949")
54
+ cached = hf_hub_download(repo_id=repo_id, filename=hub_filename,
55
+ repo_type=repo_type, revision=revision)
56
+ try:
57
+ return pd.read_csv(cached, encoding="utf-8")
58
+ except UnicodeDecodeError:
59
+ return pd.read_csv(cached, encoding="cp949")
60
+
61
+ # ──────────────────────────────── 전역 데이터 컨테이너 (지연 초기화) ────────────────────────────────
62
+ travel_df = festival_df = external_score_df = weather_df = package_df = master_df = None
63
+ countries, cities = [], []
64
+ theme_title_phrases = {}
65
+
66
+ def _strip_columns(df: pd.DataFrame | None) -> pd.DataFrame | None:
67
+ if df is not None and hasattr(df, "columns"):
68
+ df.columns = df.columns.str.strip()
69
+ return df
70
+
71
+ def init_datasets(*,
72
+ travel_df: pd.DataFrame,
73
+ festival_df: pd.DataFrame,
74
+ external_score_df: pd.DataFrame,
75
+ weather_df: pd.DataFrame,
76
+ package_df: pd.DataFrame,
77
+ master_df: pd.DataFrame,
78
+ theme_title_phrases: dict | None = None):
79
+ """app.py에서 데이터 로드가 끝난 뒤 딱 한 번 호출"""
80
+ globals()["travel_df"] = _strip_columns(travel_df.copy())
81
+ globals()["festival_df"] = _strip_columns(festival_df.copy())
82
+ globals()["external_score_df"] = _strip_columns(external_score_df.copy())
83
+ globals()["weather_df"] = _strip_columns(weather_df.copy())
84
+ globals()["package_df"] = _strip_columns(package_df.copy())
85
+ globals()["master_df"] = _strip_columns(master_df.copy())
86
+ if theme_title_phrases is not None:
87
+ globals()["theme_title_phrases"] = theme_title_phrases
88
+
89
+ # 필수 컬럼 확인
90
+ req = ["여행나라", "여행도시", "여행지"]
91
+ miss = [c for c in req if c not in globals()["travel_df"].columns]
92
+ if miss:
93
+ raise KeyError(f"travel_df 필수 컬럼 누락: {miss} / 실제: {list(globals()['travel_df'].columns)}")
94
+
95
+ # 파생 목록
96
+ global countries, cities
97
+ countries = sorted(globals()["travel_df"]["여행나라"].dropna().unique().tolist())
98
+ cities = sorted(globals()["travel_df"]["여행도시"].dropna().unique().tolist())
99
+
100
+ def _assert_ready():
101
+ if globals()["travel_df"] is None:
102
+ raise RuntimeError("chat_a.init_datasets(...)를 먼저 호출해주세요.")
103
+
104
+ # ──────────────────────────────── 모델 로더 (캐시/권한 안전) ────────────────────────────────
105
+ @st.cache_resource(show_spinner=False)
106
+ def load_tokenizer():
107
+ return AutoTokenizer.from_pretrained("hun3359/klue-bert-base-sentiment",
108
+ cache_dir=CACHE_DIR)
109
+
110
+ @st.cache_resource(show_spinner=False)
111
+ def load_sentiment_model():
112
+ # 토크나이저는 load_tokenizer 함수가 담당하므로 여기서는 모델만 로드
113
+ model = AutoModelForSequenceClassification.from_pretrained(
114
+ "hun3359/klue-bert-base-sentiment", cache_dir=CACHE_DIR
115
+ )
116
+ model.eval()
117
+ return model # 모델만 반환하도록 수정!
118
+
119
+ @st.cache_resource(show_spinner=False)
120
+ def load_sbert_model():
121
+ return SentenceTransformer("jhgan/ko-sroberta-multitask", cache_folder=CACHE_DIR)
122
+
123
+ def detect_location_filter(text, intent_score=None):
124
+ def in_text_exact(word):
125
+ return word in text
126
+
127
+ master_cities_list = master_df["여행도시"].dropna().unique()
128
+ master_countries_list = master_df["여행나라"].dropna().unique()
129
+
130
+ found_city = next((c for c in master_cities_list if c in text), None)
131
+ found_country = next((c for c in master_countries_list if c in text), None)
132
+
133
+ # 1. 이름조차 찾지 못했다면 intent_score 확인
134
+ if not found_city and not found_country:
135
+ if intent_score is not None and intent_score >= 0.70:
136
+ return None, None, "intent"
137
+ else:
138
+ return None, None, "emotion"
139
+
140
+ # 2. 명확한 도시/나라가 있을 경우 → region
141
+ if found_city and not found_country:
142
+ match = master_df[master_df["여행도시"] == found_city]
143
+ if not match.empty:
144
+ found_country = match.iloc[0]["여행나라"]
145
+
146
+ is_city_recommendable = found_city in cities if found_city else False
147
+ is_country_recommendable = found_country in countries if found_country else False
148
+
149
+ if is_city_recommendable or is_country_recommendable:
150
+ return found_country, found_city, "region"
151
+ return found_country, found_city, "unknown"
152
+ # -------------------- 감정 키워드 설정 --------------------
153
+ klue_emotions = {
154
+ 0: "분노", 1: "툴툴대는", 2: "좌절한", 3: "짜증내는", 4: "방어적인", 5: "악의적인",
155
+ 6: "안달하는", 7: "구역질 나는", 8: "노여워하는", 9: "성가신", 10: "슬픔", 11: "실망한",
156
+ 12: "비통한", 13: "후회되는", 14: "우울한", 15: "마비된", 16: "염세적인", 17: "눈물이 나는",
157
+ 18: "낙담한", 19: "환멸을 느끼는", 20: "불안", 21: "두려운", 22: "스트레스 받는",
158
+ 23: "취약한", 24: "혼란스러운", 25: "당혹스러운", 26: "회의적인", 27: "걱정스러운",
159
+ 28: "조심스러운", 29: "초조한", 30: "상처", 31: "질투하는", 32: "배신당한", 33: "고립된",
160
+ 34: "충격 받은", 35: "가난한 불우한", 36: "희생된", 37: "억울한", 38: "괴로워하는",
161
+ 39: "버려진", 40: "당황", 41: "고립된(당황한)", 42: "남의 시선을 의식하는", 43: "외로운",
162
+ 44: "열등감", 45: "죄책감의", 46: "부끄러운", 47: "혐오스러운", 48: "한심한",
163
+ 49: "혼란스러운(당황한)", 50: "기쁨", 51: "감사하는", 52: "신뢰하는", 53: "편안한",
164
+ 54: "만족스러운", 55: "흥분", 56: "느긋", 57: "안도", 58: "신이 난", 59: "자신하는"
165
+ }
166
+
167
+ kote_emotion_groups = {
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
+ klue_label_to_group = {}
203
+ klue_to_general = {}
204
+
205
+ # label → group 매핑 준비
206
+ for group, keywords in kote_emotion_groups.items():
207
+ for word in keywords:
208
+ klue_label_to_group[word] = group
209
+
210
+ # klue_emotions에 있는 감정 키들을 그룹화
211
+ for klue_id, klue_label in klue_emotions.items():
212
+ group = klue_label_to_group.get(klue_label, "부정")
213
+ klue_to_general[klue_id] = group
214
+
215
+ emotion_override_dict = {
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
+ intent_keywords = {
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
+ intent_to_category = {k: [k] for k in intent_keywords.keys()}
362
+ category_mapping = {k: k for k in intent_keywords.keys()}
363
+
364
+ emotion_to_category_boost = {
365
+ "신남": ["테마파크", "해양체험", "미식", "문화거리", "야시장", "워터파크"],
366
+ "기대감": ["랜드마크","전망", "문화체험", "이동관광", "포토존", "공연"],
367
+ "편안함": ["자연", "산책", "공원", "해변", "호수", "식물원", "도심공원"],
368
+ "안정": ["휴양지", "호수", "성당", "미술관", "예술감상"],
369
+ "감탄": ["랜드마크", "전망", "야경", "예술감상", "역사", "종교", "포토존"],
370
+ "혼란회복": ["종교", "도심공원", "미술관", "산책", "트레킹"],
371
+ "감정전환": ["테마파크", "동물원", "수족관", "문화체험", "쇼핑", "미식"],
372
+ "위로": ["자연", "호수", "동물원", "휴양지", "성당"],
373
+ "부정": ["자연", "공원", "섬"]
374
+ }
375
+
376
+ # ---------------------안내문구 매핑 --------------------
377
+ theme_ui_map = {
378
+ "자연/풍경 감상형": ("힐링 여행지 🧘", "자연과 풍경 속에서 편안해지는 조용한 휴식"),
379
+ "가족/체험 투어형": ("체험 여행지 🎢", "이색적인 활동으로 즐기는 생생한 전환"),
380
+ "쇼핑/거리 체험형": ("쇼핑 여행지 🛍", "설렘 가득한 거리에서 현지의 색을 담은 즐거움"),
381
+ "박물관/문화 감상형": ("문화 여행지 🎨", "예술과 전통이 살아있는 공간에서 느끼는 감동"),
382
+ "랜드마크/종교 건축형": ("명소 여행지 🏛", "감탄을 부르는 풍경과 함께 도시의 상징을 만나다"),
383
+ }
384
+ ui_to_theme_map = {v[0]: k for k, v in theme_ui_map.items()}
385
+
386
+ theme_opening_lines = {
387
+ "힐링 여행지 🧘": "여유와 자연을 느낄 수 있는 ‘힐링 여행지’ {}곳을 추천드릴게요.",
388
+ "체험 여행지 🎢": "몸과 마음이 들뜨는 ‘체험 여행지’ {}곳을 추천드릴게요.",
389
+ "문화 여행지 🎨": "예술과 이야기가 있는 ‘문화 여행지’ {}곳을 추천드릴게요.",
390
+ "쇼핑 여행지 🛍": "현지 먹거리가 가득한 ‘쇼핑 여행지’ {}곳을 추천드릴게요.",
391
+ "명소 여행지 🏛": "문화와 상징이 깃든 ‘명소 여행지’ {}곳을 추천드릴게요.",
392
+ }
393
+
394
+ intent_opening_lines = {
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
+ theme_title_phrases = {
452
+ "힐링 여행지 🧘": [
453
+ "완전 휴식 힐링", "조용한 쉼표 감성", "마음 회복 여정", "여유 가득 치유", "재충전 슬로우 라이프",
454
+ "혼자만의 위로 여행", "스트레스 해소 힐링", "편안한 하루 쉼", "무리 없는 휴식 코스", "고요한 자연 속 여유"
455
+ ],
456
+ "체험 여행지 🎢": [
457
+ "액티비티 가득 체험", "전통+현지활동 즐기기", "오감만족 현지 체험", "생생한 투어 중심", "로컬 라이프 몰입형",
458
+ "이색 활동 탐험 여행", "다채로운 체험 나들이", "직접 해보는 체험 위주", "참여형 투어 여행", "이색적인 하루 체험"
459
+ ],
460
+ "문화 여행지 🎨": [
461
+ "감성 깊은 문화 산책", "예술+역사 감상 투어", "전통과 현대의 조화", "미술과 공연 탐방형", "고요한 박물관 여행",
462
+ "유산 따라 걷는 길", "명화 따라 가는 여행", "인문학 감성 문화 코스", "예술품과 함께하는 여정", "전시+예술 감상 중심"
463
+ ],
464
+ "쇼핑 여행지 🛍": [
465
+ "현지 감성 쇼핑", "트렌디 마켓 탐방", "먹거리+기념품 거리투어", "핫플 마켓 나들이", "로컬 브랜드 쇼핑",
466
+ "시장 골목 체험형 쇼핑", "감성 소품 수집 여행", "실속형 쇼핑 탐방", "득템 투어 여행", "즐거운 거리 탐방"
467
+ ],
468
+ "명소 여행지 🏛": [
469
+ "랜드마크 집중 투어", "유명 명소 핵심일정", "도시 상징 명소여행", "사진 맛집 스팟 투어", "대표 장소 완전정복",
470
+ "상징적 장소 따라가기", "유적+건축 핵심 코스", "도시 한눈에 보기 여행", "베스트 명소 몰아보기", "상징 명소 스탬프 투어"
471
+ ]
472
+ }
473
+
474
+ feature_phrase_map = {
475
+ frozenset(["숙소", "일정"]): [
476
+ "편안한 숙소와 알찬 일정이 돋보이는 여행이에요", "조용한 숙소에서 시작해 일정까지 여유롭게 즐겨보세요",
477
+ "숙소와 일정 모두 균형잡힌 완벽한 여행이 기다려요"
478
+ ],
479
+ frozenset(["숙소", "가이드"]): [
480
+ "친절한 가이드와 편안한 숙소가 여행의 품격을 높여줘요", "믿음직한 가이드와 푹 쉴 수 있는 숙소가 조화를 이뤄요",
481
+ "좋은 숙소와 세심한 가이드가 잊지 못할 추억을 만들어줘요"
482
+ ],
483
+ frozenset(["숙소", "식사"]): [
484
+ "맛있는 음식과 포근한 숙소가 하루를 완벽하게 마무리해줘요", "편안한 숙소에서 휴식하고, 입맛 돋우는 식사까지 즐겨보세요",
485
+ "숙소와 식사 모두 기대 이상! 하루가 즐거워지는 조합이에요"
486
+ ],
487
+ frozenset(["숙소", "가성비"]): [
488
+ "합리적인 가격에 숙소 퀄리티까지 만족스러운 여행이에요", "가성비 좋고 편한 숙소 덕분에 여유로운 여행이 가능해요",
489
+ "가성비와 숙소 퀄리티 모두 잡은 최고의 선택이에요"
490
+ ],
491
+ frozenset(["숙소", "이동수단"]): [
492
+ "숙소와 교통 모두 걱정 없는 편안한 여행 코스예요", "편한 숙소와 편리한 이동으로 피로 없이 즐겨요",
493
+ "숙소 위치도 좋고 이동도 편해서 스트레스 없는 일정이에요"
494
+ ],
495
+ frozenset(["일정", "가이드"]): [
496
+ "계획적인 일정과 세심한 가이드가 함께하는 만족도 높은 여행이에요", "친절한 가이드의 안내로 일정이 훨씬 알차고 편안해요",
497
+ "시간 낭비 없이 똑똑하게 즐기는 일정, 믿음직한 가이드까지 완벽해요"
498
+ ],
499
+ frozenset(["일정", "식사"]): [
500
+ "시간 알차고 식사까지 만족도 높은 구성이에요", "일정이 짜임새 있고, 식사도 군더더기 없어요",
501
+ "식사 시간이 기다려질 만큼 구성 좋은 일정이에요"
502
+ ],
503
+ frozenset(["일정", "가성비"]): [
504
+ "알찬 일정과 가성비 높은 구성으로 만족스러운 여행이에요", "시간도 돈도 아끼는 실속 있는 일정이에요",
505
+ "지루하지 않은 알찬 일정과 착한 가격, 가성비 최고에요"
506
+ ],
507
+ frozenset(["일정", "이동수단"]): [
508
+ "효율적인 일정과 편리한 이동으로 스트레스 없는 여행이에요", "이동이 편해서 일정이 더 즐겁고 여유로워요",
509
+ "부드러운 이동 루트와 알찬 일정의 조화가 인상 깊어요"
510
+ ],
511
+ frozenset(["가���드", "식사"]): [
512
+ "친절한 가이드와 맛있는 음식이 감동을 더해줘요", "가이드의 설명과 맛있는 음식으로 여행이 풍성해져요",
513
+ "입도 마음도 만족스러운 식사와 가이드 조합이에요"
514
+ ],
515
+ frozenset(["가이드", "가성비"]): [
516
+ "세심한 안내와 좋은 구성, 가격까지 잡은 실속 있는 여행이에요", "저렴한 가격에도 훌륭한 가이드를 만날 수 있어요",
517
+ "가격 대비 서비스 최고! 가이드 덕에 더 알차고 완벽해요"
518
+ ],
519
+ frozenset(["가이드", "이동수단"]): [
520
+ "믿음직한 가이드와 쾌적한 이동수단으로 편안한 여행이에요", "가이드의 동선 설계가 이동을 훨씬 효율적으로 만들어줘요",
521
+ "편안한 이동과 노련한 가이드의 조합으로 긴 여정도 든든해요"
522
+ ],
523
+ frozenset(["식사", "가성비"]): [
524
+ "만족스러운 식사와 가격까지 착한 여행 코스예요", "음식 퀄리티와 가성비까지 잡은 패키지 구성이에요",
525
+ "식사도 푸짐하고 가격도 합리적인 최고의 구성!"
526
+ ],
527
+ frozenset(["식사", "이동수단"]): [
528
+ "여유로운 이동과 든든한 식사로 여행이 더욱 즐거워요", "편안한 버스 타고 가는 길마다 맛집 투어 같은 경험이에요",
529
+ "친절한 이동기사님과 맛있는 식사가 조화로운 패키지에요"
530
+ ],
531
+ frozenset(["가성비", "이동수단"]): [
532
+ "교통 편의성과 가격 모두 만족하는 실속 여행이에요", "가격 착하고 이동도 편리해서 부담 없이 가기 좋은 패키지에요",
533
+ "가볍게 떠나기 좋은 가성비+교통 조합이에요"
534
+ ]
535
+ }
536
+ # -------------------- 챗봇 연동 --------------------
537
+ def get_intent_intro_message(intent: str) -> str:
538
+ intent_opening_texts = {
539
+ "쇼핑":"특별한 기념품과 현지의 매력을 느끼고 싶으시군요.",
540
+ "실내": "날씨와 상관없이 알차게 여행을 즐기고 싶으시군요.",
541
+ "가족": "가족과 함께 소중한 추억을 만들고 싶으시군요.",
542
+ "워터파크": "물놀이로 스트레스를 날리고 싶으신가요?",
543
+ "자연": "자연 속에서 힐링하고 싶은 마음이 느껴져요.",
544
+ "전망": "탁 트인 풍경을 바라보며 여유를 느끼고 싶으시군요.",
545
+ "포토존": "잊지 못할 순간을 사진으로 남기고 싶으시군요.",
546
+ "해양체험": "바다 속 세상을 가까이에서 느끼고 싶으신가요?",
547
+ "수족관": "해양 생물을 직접 보고 싶으신가요?",
548
+ "종교": "마음의 평화를 찾고 싶은 여행을 원하시나요?",
549
+ "성당": "아름다운 건축과 고요함을 느끼고 싶으시군요.",
550
+ "예식장": "특별한 순간을 로맨틱하게 남기고 싶으시군요.",
551
+ "커플": "둘만의 로맨틱한 시간을 보내고 싶으시군요.",
552
+ "랜드마크": "도시의 대표 명소에서 그 지역의 매력을 느끼고 싶으시군요.",
553
+ "역사": "과거의 흔적 속에서 깊은 이야기를 느끼고 싶으신가요?",
554
+ "궁전": "왕실의 화려함을 경험하고 싶으시군요.",
555
+ "문화체험": "전통 문화를 직접 체험하고 싶으신가요?",
556
+ "박물관": "새로운 지식과 흥미로운 전시를 경험하고 싶으시군요.",
557
+ "예술감상": "감성과 창의력을 자극하고 싶으시군요.",
558
+ "체험관": "직접 해보는 체험으로 생생한 여행을 원하시나요?",
559
+ "광장": "현지 분위기를 직접 느끼고 싶으시군요.",
560
+ "산책": "여유롭게 걸으며 생각 정리하고 싶으시군요.",
561
+ "미술관": "예술작품 속에서 여유와 영감을 느끼고 싶으시군요",
562
+ "공연": "생생한 무대와 감동을 직접 느끼고 싶으시군요.",
563
+ "유람선": "바다 위에서 낭만적인 시간을 보내고 싶으시군요.",
564
+ "야경": "낮보다 아름다운 밤 풍경을 감상하고 싶으시군요.",
565
+ "이동관광": "편하게 이동하면서 다양한 명소를 보고 싶으시군요.",
566
+ "공원": "잠시 일상을 벗어나 여유를 느끼고 싶으시군요.",
567
+ "도심공원": "도시 속에서 잠깐의 쉼을 원하시나요?",
568
+ "문화거리": "걷기만 해도 감성이 채워지는 골목을 원하시나요?",
569
+ "호수": "잔잔한 풍경 속에서 마음의 평화를 찾고 싶으시군요.",
570
+ "휴양지": "아무 생각 없이 쉬어가고 싶은 순간이시군요.",
571
+ "성": "중세 감성의 고풍스러움을 느끼고 싶으시군요.",
572
+ "관람차": "높은 곳에서 색다른 풍경을 보고 싶으시군요.",
573
+ "테마전시": "독특한 콘텐츠로 새로운 자극을 원하시나요?",
574
+ "강": "물소리를 들으며 여유를 느끼고 싶으시군요.",
575
+ "경기장": "현장의 열기와 박진감을 느끼고 싶으시군요.",
576
+ "사원": "고요한 공간에서 마음을 가라앉히고 싶으시군요.",
577
+ "시장": "현지의 진짜 일상을 경험하고 싶으시군요.",
578
+ "야시장": "밤이 더 매력적인 여행을 기대하고 계시군요.",
579
+ "동물원": "귀여운 친구들을 만나며 힐링하고 싶으시군요.",
580
+ "기차": "천천히 이동하며 풍경을 즐기고 싶으시군요.",
581
+ "항구": "바닷바람과 함께 낭만을 느끼고 싶으시군요.",
582
+ "겨울스포츠": "눈 위에서 짜릿한 활동을 원하시나요?",
583
+ "식물원": "피톤치드향이 가득한 공간에서 편안함을 느끼고 싶으시군요.",
584
+ "케이블카": "색다른 시야로 풍경을 바라보고 싶으시군요.",
585
+ "해변": "햇살과 바다를 함께 즐기고 싶으시군요.",
586
+ "테마파크": "하루종일 웃고 뛰어놀고 싶은 기분이신가요?",
587
+ "트레킹": "자연 속에서 몸과 마음을 걷고 싶으시군요.",
588
+ "섬": "복잡함에서 벗어나 고요함을 찾고 싶으시군요.",
589
+ "미식": "새로운 맛으로 여행의 즐거움을 더하고 싶으시군요.",
590
+ "버스": "편하게 둘러보며 여행하고 싶으시군요.",
591
+ "기념관": "그 시절을 다시 느끼고 싶으신가요?",
592
+ "신사": "신비롭고 조용한 장소를 찾고 계시군요.",
593
+ }
594
+ if intent in intent_opening_texts:
595
+ return intent_opening_texts[intent]
596
+ else:
597
+ raise ValueError(f"의도 '{intent}'에 맞는 문구가 정의되어 있지 않습니다.")
598
+
599
+ def determine_weather_description_official(row):
600
+ try:
601
+ rain = float(row["강수량"])
602
+ humidity = float(row["습도"])
603
+ except Exception:
604
+ return "날씨 정보 없음"
605
+
606
+ if rain >= 10:
607
+ return "비가 많이 오는 날씨예요."
608
+ elif rain >= 3:
609
+ return "비가 오는 날씨예요."
610
+ elif rain >= 0.5:
611
+ return "약한 비가 오는 날씨예요."
612
+ else:
613
+ if humidity >= 85:
614
+ return "흐린 날씨예요."
615
+ elif humidity >= 65:
616
+ return "구름이 많은 날씨예요."
617
+ else:
618
+ return "맑은 날씨예요."
619
+
620
+ def get_weather_message(city, weather_df, date="2025-06-01"):
621
+ date = pd.to_datetime(date).date()
622
+
623
+ # '날짜' 컬럼이 datetime으로 되어있으면 date로 변환
624
+ if pd.api.types.is_datetime64_any_dtype(weather_df["날짜"]):
625
+ weather_df["날짜_일자"] = weather_df["날짜"].dt.date
626
+ else:
627
+ weather_df["날짜_일자"] = pd.to_datetime(weather_df["날짜"], errors="coerce").dt.date
628
+
629
+ # 2. 정확 일치 시도
630
+ exact_match = weather_df[
631
+ (weather_df["여행도시"].str.strip() == city.strip())
632
+ & (weather_df["날짜_일자"] == date)
633
+ ]
634
+ if not exact_match.empty:
635
+ row = exact_match.iloc[0]
636
+ else:
637
+ # 3. 포함 검색 시도
638
+ partial_match = weather_df[
639
+ (weather_df["여행도시"].str.contains(city, na=False))
640
+ & (weather_df["날짜_일자"] == date)
641
+ ]
642
+ if not partial_match.empty:
643
+ row = partial_match.iloc[0]
644
+ else:
645
+ return f"📅 {city}의 {date} 날씨 정보가 없습니다."
646
+
647
+ # 최고 기온
648
+ try:
649
+ temp = f"{float(row['최고_기온']):.1f}"
650
+ temp_a = f"{float(row['최저_기온']):.1f}°C"
651
+ except Exception:
652
+ temp = "정보 없음"
653
+
654
+ # 설명
655
+ desc = determine_weather_description_official(row)
656
+
657
+ return f"📅 {row['여행도시']}의 날씨는 {temp}/{temp_a}, {desc}"
658
+
659
+
660
+
661
+ def generate_intro_message(intent=None, emotion_groups=None, emotion_scores=None, min_emotion_score=15.0):
662
+ from collections import defaultdict
663
+ import random
664
+
665
+ emotion_priority = [
666
+ "감정전환", "위로", "혼란회복", "안정", "편안함", "감탄", "신남", "부정"
667
+ ]
668
+
669
+ emotion_messages = {
670
+ "신남": "🎉 즐거움이 가득한 여행을 찾고 계시는군요.",
671
+ "편안함": "😌 고요하고 편안한 여행이 필요하시군요.",
672
+ "안정": "🕊️ 마음의 안정을 찾고 싶으신가 봐요.",
673
+ "감탄": "😍 감동과 놀라움을 느끼고 싶으시군요.",
674
+ "혼란회복": "🌀 마음을 정리할 시간이 필요하신가 봐요.",
675
+ "감정전환": "🔄 기분 전환이 필요한 순간이네요.",
676
+ "위로": "🤍 지친 마음에 작은 위로가 필요하시군요.",
677
+ "부정": "😮 잠시 멈춰 숨 돌릴 시간이 필요하시군요."
678
+ }
679
+
680
+ neutral_messages = [
681
+ "지금 이 순간, 어떤 여행이 어울릴지 함께 고민해봤어요.",
682
+ "기분 전환이 필요하신 것 같아요. 여러 스타일의 여행지를 추천드릴게요.",
683
+ "딱 떨어지는 목적은 없지만, 어딘가 떠나고 싶을 때가 있죠.",
684
+ "지금 마음에 맞을 수 있는 여행 스타일 몇 가지를 골라봤어요.",
685
+ "다양한 감정을 담을 수 있는 여행지를 준비했어요.",
686
+ "지금의 기분에 맞춰, 어울릴 만한 여행 스타일을 제안드릴게요."
687
+ ]
688
+
689
+ # 👉 사��� 오버라이드 결과 우선 적용
690
+ if emotion_groups:
691
+ # 우선순위에 따라 가장 먼저 매칭되는 감정 그룹 메시지를 리턴
692
+ for emo in emotion_priority:
693
+ if emo in emotion_groups:
694
+ return emotion_messages.get(emo, random.choice(neutral_messages))
695
+
696
+ # 👉 모델 감정 점수 기반 적용
697
+ group_scores = defaultdict(float)
698
+ if emotion_scores:
699
+ for klue_label, score in emotion_scores:
700
+ group = klue_label_to_group.get(klue_label)
701
+ if group:
702
+ group_scores[group] = max(group_scores[group], score)
703
+
704
+ for emo in emotion_priority:
705
+ if emo in group_scores:
706
+ return emotion_messages.get(emo, random.choice(neutral_messages))
707
+
708
+ # 👉 아무것도 없으면 중립 메시지
709
+ return random.choice(neutral_messages)
710
+
711
+ def generate_region_intro(city=None, country=None):# 추가 함############################
712
+ name = city if city else country
713
+ templates = [
714
+ f"✨ 낭만이 가득한 {name}의 매력적인 여행지로 여러분을 초대할게요!",
715
+ f"🌏 {name}에서만 느낄 수 있는 특별한 감성과 순간을 함께 찾아볼까요?",
716
+ f"📍 기억에 오래 남을 {name}의 아름다운 여행지들을 하나하나 소개해드릴게요.",
717
+ f"🌿 {name}에서만 만날수 있는 매력 가득한 여행지를 엄선했어요.",
718
+ f"🎒 일상 속 쉼표가 필요한 지금, {name}에서 설렘 가득한 여정을 떠나보세요."
719
+ ]
720
+ return random.choice(templates)
721
+
722
+ def parse_companion_and_age(text):
723
+ companions = None
724
+ age_group = None
725
+
726
+ # 사용자 입력 ➜ 실제 컬럼명 매핑
727
+ companion_map = {
728
+ "혼자": "나혼자",
729
+ "나혼자": "나혼자",
730
+ "친구": "친구들과",
731
+ "친구들": "친구들과",
732
+ "커플": "커플",
733
+ "연인": "커플",
734
+ "가족": "가족여행",
735
+ "단체": "단체여행"
736
+ }
737
+
738
+ # 나이대 매핑 (CSV 컬럼 그대로)
739
+ age_map = {
740
+ "20대": "20대",
741
+ "30대": "30대",
742
+ "40대": "40대",
743
+ "50대": "50대",
744
+ "60대": "60대 이상 ",
745
+ "60대 이상": "60대 이상 "
746
+ }
747
+
748
+ text = text.strip()
749
+
750
+ # 동행 파싱
751
+ for k, mapped in companion_map.items():
752
+ if k in text:
753
+ companions = mapped
754
+ break
755
+
756
+ # 나이대 파싱
757
+ for k, mapped in age_map.items():
758
+ if k in text:
759
+ age_group = mapped
760
+ break
761
+
762
+ # 숫자만 입력했을 때 처리
763
+ if age_group is None:
764
+ if "20" in text:
765
+ age_group = "20대"
766
+ elif "30" in text:
767
+ age_group = "30대"
768
+ elif "40" in text:
769
+ age_group = "40대"
770
+ elif "50" in text:
771
+ age_group = "50대"
772
+ elif "60" in text:
773
+ age_group = "60대 이상 "
774
+
775
+ return companions, age_group
776
+
777
+
778
+ #------------------------- 수정
779
+ def make_companion_age_message(companions, age_group):
780
+ companion_friendly = {
781
+ "혼자": "혼자",
782
+ "나혼자": "혼자",
783
+ "친구": "친구분들과",
784
+ "친구들과": "친구분들과",
785
+ "커플": "연인과",
786
+ "가족여행": "가족분들과",
787
+ "가족": "가족분들과",
788
+ "단체여행": "단체로",
789
+ "단체": "단체로"
790
+ }
791
+
792
+ age_friendly = {
793
+ "20대": "20대",
794
+ "30대": "30대",
795
+ "40대": "40대",
796
+ "50대": "50대",
797
+ "60대 이상": "60대 이상",
798
+ "60대": "60대 이상"
799
+ }
800
+
801
+ # ✔ 리스트 → 첫 항목(대표값) 또는 None
802
+ def to_friendly(terms, mapping):
803
+ if not terms:
804
+ return []
805
+ if not isinstance(terms, list):
806
+ terms = [terms]
807
+ return [mapping[t] for t in terms if t in mapping]
808
+
809
+ friendly_ages = to_friendly(age_group, age_friendly)
810
+ friendly_companions = to_friendly(companions, companion_friendly)
811
+
812
+ age_text = ", ".join(friendly_ages) + " 여행객" if friendly_ages else ""
813
+ companion_text = ", ".join(friendly_companions)
814
+
815
+ if friendly_ages and friendly_companions:
816
+ return f"💡{age_text} {companion_text} 여행하시는 분들께 특히 인기 있는 패키지예요."
817
+ elif friendly_ages:
818
+ return f"💡{age_text} 분들께 인기 있는 패키지예요."
819
+ elif friendly_companions:
820
+ return f"💡{companion_text} 여행하시는 분들께 특히 인기 있는 패키지예요."
821
+ else:
822
+ return ""
823
+
824
+ #------------------------- 같은 그룹에 2개이상 선택이 가능하도록 로직 수정
825
+ def filter_packages_by_companion_age(package_df, companions=None, age_group=None, city=None, top_n=5):
826
+ # 사용자 입력 ➜ 실제 컬럼명 매핑
827
+ companion_map = {
828
+ "혼자": "나혼자",
829
+ "나혼자": "나혼자",
830
+ "친구": "친구들과",
831
+ "친구들": "친구들과",
832
+ "커플": "커플",
833
+ "연인": "커플",
834
+ "가족": "가족여행",
835
+ "단체": "단체여행"
836
+ }
837
+
838
+ # 나이대 매핑 (CSV 컬럼 그대로)
839
+ age_map = {
840
+ "20대": "20대",
841
+ "30대": "30대",
842
+ "40대": "40대",
843
+ "50대": "50대",
844
+ "60대": "60대 이상 ",
845
+ "60대 이상": "60대 이상"
846
+ }
847
+
848
+ # companions, age_group → 리스트로 통일
849
+ comp_list = companions if isinstance(companions, list) else ([companions] if companions else [])
850
+ age_list = age_group if isinstance(age_group, list) else ([age_group] if age_group else [])
851
+
852
+ companions = [companion_map.get(c) for c in comp_list if companion_map.get(c) in package_df.columns]
853
+ age_group = [age_map.get(a) for a in age_list if age_map.get(a) in package_df.columns]
854
+
855
+ df = package_df.copy()
856
+
857
+ # city 컬럼 있으면 필터
858
+ if city and "여행도시" in df.columns:
859
+ df = df[df["여행도시"].str.contains(city, na=False)]
860
+
861
+ # 조건1: 동행+연령
862
+ if companions and age_group:
863
+ mask = (df[companions].sum(axis=1) > 0) & (df[age_group].sum(axis=1) > 0)
864
+ both = df[mask].copy()
865
+ if len(both) >= top_n:
866
+ both["점수합"] = both[companions + age_group].sum(axis=1)
867
+ return both.sort_values("점수합", ascending=False).head(top_n)
868
+
869
+ # 조건2: 연령만
870
+ if age_group:
871
+ age_only = df[df[age_group].sum(axis=1) > 0].copy()
872
+ if len(age_only) >= top_n:
873
+ age_only["점수"] = age_only[age_group].sum(axis=1)
874
+ return age_only.sort_values("점수", ascending=False).head(top_n)
875
+
876
+ # 조건3: 동행만
877
+ if companions:
878
+ comp_only = df[df[companions].sum(axis=1) > 0].copy()
879
+ if len(comp_only) >= top_n:
880
+ comp_only["점수"] = comp_only[companions].sum(axis=1)
881
+ return comp_only.sort_values("점수", ascending=False).head(top_n)
882
+
883
+ # 조건4: 아무 조건도 없거나 개수 부족
884
+ return df.sample(n=min(top_n, len(df)))
885
+
886
+ # -------------------- 핵심 함수 --------------------
887
+ def get_highlight_message(selected_place, travel_df, external_score_df, festival_df):
888
+ import random
889
+ from datetime import datetime
890
+
891
+ # 메시지 풀
892
+ messages_pool = {
893
+ "festival_score": [
894
+ "🎉 지금 {city}에서는 '{festival_name}'이 진행 중이에요!",
895
+ "🎊 '{festival_name}' 축제가 열리고 있어요! 놓치지 마세요."
896
+ ],
897
+ "cost_score": [
898
+ "💸 여행 비용이 저렴한 편이라 부담 없이 다녀올 수 있어요.",
899
+ "💰 예상 경비가 낮아서 가성비 좋은 여행이 가능합니다."
900
+ ],
901
+ "norm_fx": [
902
+ "💱 환율이 떨어져서 환전하기 좋은 시기예요.",
903
+ "💵 환율이 안정적이라 여행 경비가 절약됩니다.",
904
+ "1,000원으로 더 많은 금액을 환전할 수 있어요!"
905
+ ],
906
+ "norm_cpi": [
907
+ "🛍 현지 물가가 저렴해서 여행 경비가 합리적이에요.",
908
+ "☕ 카페, 식사, 쇼핑까지 부담이 덜해요.",
909
+ "평균보다 낮은 물가 덕분에 여유로운 여행이 가능해요."
910
+ ],
911
+ "트렌드급상승": [
912
+ "📈 최근 검색량이 급등했어요. 요즘 뜨는 여행지예요!",
913
+ "🔥 지금 많은 사람들이 이곳을 검색하고 있어요!"
914
+ ],
915
+ "trend_score": [
916
+ "⭐ 여행객들에게 꾸준히 사랑받고 있는 곳이에요.",
917
+ "✨ 언제 가도 만족도가 높은 인기 여행지예요.",
918
+ "💖 지금도 많은 사람들이 찾는 베스트셀러 여행지예요."
919
+ ]
920
+ }
921
+
922
+ # 🎯 축제명 가져오기 함수
923
+ def get_festival_name(city, festival_df):
924
+ today = datetime.today().date()
925
+ matches = festival_df[festival_df["여행도시"] == city]
926
+ if matches.empty:
927
+ return "현지 축제"
928
+ matches = matches.copy()
929
+ try:
930
+ matches["시작일"] = pd.to_datetime(matches["시작일"], errors="coerce").dt.date
931
+ matches["종료일"] = pd.to_datetime(matches["종료일"], errors="coerce").dt.date
932
+ except Exception:
933
+ return "현지 축제"
934
+ # 진행중
935
+ ongoing = matches[(matches["시작일"] <= today) & (matches["종료일"] >= today)]
936
+ if not ongoing.empty:
937
+ return random.choice(ongoing["축제명"].dropna().tolist())
938
+ # 다가오는
939
+ upcoming = matches[matches["시작일"] > today].sort_values("시작일")
940
+ if not upcoming.empty:
941
+ return upcoming.iloc[0]["축제명"]
942
+ return "현지 축제"
943
+
944
+ # 🌍 여행지에 해당하는 도시/나라 가져오기
945
+ place_row = travel_df[travel_df["여행지"] == selected_place]
946
+ if place_row.empty:
947
+ return None
948
+
949
+ place_row = place_row.iloc[0]
950
+ city = place_row["여행도시"]
951
+ country = place_row["여행나라"]
952
+
953
+ # 외부요인 데이터에서 해당 도시/나라 찾기
954
+ external_row = external_score_df[
955
+ (external_score_df["여행도시"] == city) &
956
+ (external_score_df["여행나라"] == country)
957
+ ]
958
+ if external_row.empty:
959
+ return None
960
+
961
+ external_row = external_row.iloc[0]
962
+
963
+ # 조건별 만족 여부 체크
964
+ highlight_candidates = []
965
+
966
+ if external_row.get("festival_score", 0) >= 2:
967
+ festival_name = get_festival_name(city, festival_df)
968
+ if festival_name != '현지 축제':
969
+ msg = random.choice(messages_pool["festival_score"]).format(city=city, festival_name=festival_name)
970
+ highlight_candidates.append(msg)
971
+
972
+ if external_row.get("cost_score", 0) == 10:
973
+ highlight_candidates.append(random.choice(messages_pool["cost_score"]))
974
+
975
+ if external_row.get("norm_fx", 99) < 1.0:
976
+ highlight_candidates.append(random.choice(messages_pool["norm_fx"]))
977
+
978
+ if external_row.get("norm_cpi", 99) < 1.0:
979
+ highlight_candidates.append(random.choice(messages_pool["norm_cpi"]))
980
+
981
+ if str(external_row.get("트렌드급상승", "")).strip() == "급상승":
982
+ highlight_candidates.append(random.choice(messages_pool["트렌드급상승"]))
983
+
984
+ if external_row.get("trend_score", 0) >= 6.0:
985
+ highlight_candidates.append(random.choice(messages_pool["trend_score"]))
986
+
987
+ if not highlight_candidates:
988
+ fallback_messages = [
989
+ "🌿 일상을 벗어나 새로운 경험을 만들어주는, {city}로 떠나보세요.",
990
+ "🎈 뚜렷한 목적 없이도 좋은 기억만 남게 해주는, {city}예요.",
991
+ "🌸 매력이 흘러넘치는 도시, {city}에서 행복한 시간을 보내보세요."
992
+ ]
993
+ return random.choice(fallback_messages).format(city=city)
994
+
995
+ # 랜덤 1개만 선택
996
+ return random.choice(highlight_candidates)
997
+
998
+ def apply_weighted_score_random_top(df, top_n=50, sample_k=3):
999
+ # 🛡 원본 백업: 외부 점수 없는 것도 포함된 전체 df
1000
+ original_df = df.drop_duplicates(subset=["여행지"]).copy()
1001
+
1002
+ # 외부 점수와 병합
1003
+ merged = pd.merge(df, external_score_df, on=["여행도시", "여행나라"], how="left")
1004
+ merged["종합점수"] = merged["종합점수"].fillna(0)
1005
+
1006
+ # 상위 점수 정렬
1007
+ ranked = merged.sort_values(by="종합점수", ascending=False).drop_duplicates(subset=["여행지"])
1008
+
1009
+ # 최상위 top_n 중 sample_k개 선택
1010
+ top_df = ranked.head(top_n)
1011
+ top_count = min(sample_k, len(top_df))
1012
+ sampled_top = top_df.sample(n=top_count, random_state=random.randint(1, 9999))
1013
+
1014
+ # 부족하면 bottom에서 채우기
1015
+ bottom_pool = ranked.iloc[top_n:]
1016
+ needed = max(0, 3 - len(sampled_top))
1017
+ if needed > 0 and not bottom_pool.empty:
1018
+ sampled_bottom = bottom_pool.sample(n=min(needed, len(bottom_pool)), random_state=random.randint(1, 9999))
1019
+ final_df = pd.concat([sampled_top, sampled_bottom], ignore_index=True)
1020
+ else:
1021
+ final_df = sampled_top
1022
+
1023
+ # 🔒 최종 보완: 외부 점수 없던 원본 df에서 무조건 3개 채우기
1024
+ if len(final_df) < 3:
1025
+ additional = original_df[~original_df["여행지"].isin(final_df["여행지"])]
1026
+ if not additional.empty:
1027
+ fill_df = additional.sample(n=min(3 - len(final_df), len(additional)), random_state=random.randint(1, 9999))
1028
+ final_df = pd.concat([final_df, fill_df], ignore_index=True)
1029
+
1030
+ return final_df
1031
+
1032
+ def apply_weighted_score_filter(df, top_n=50, sample_k=3):
1033
+ return apply_weighted_score_random_top(df, top_n=top_n, sample_k=sample_k)
1034
+
1035
+ def override_emotion_if_needed(text):
1036
+ for keyword, (emotion_label, emotion_group) in emotion_override_dict.items():
1037
+ if keyword in text:
1038
+ return [(emotion_label, 50.0)], [emotion_group]
1039
+ return None
1040
+
1041
+ def analyze_emotion(text: str):
1042
+ model = load_sentiment_model()
1043
+ tokenizer = load_tokenizer()
1044
+
1045
+ override = override_emotion_if_needed(text)
1046
+ if override:
1047
+ return override
1048
+
1049
+ inputs = tokenizer(text, return_tensors="pt", truncation=True)
1050
+ with torch.no_grad():
1051
+ logits = model(**inputs).logits # ← model은 '단일 객체'
1052
+ probs = F.softmax(logits, dim=1)[0]
1053
+
1054
+ top_indices = torch.topk(probs, k=5).indices.tolist()
1055
+ top_emotions = [(klue_emotions[i], float(probs[i]) * 100) for i in top_indices]
1056
+ top_emotion_groups = list(dict.fromkeys(
1057
+ [klue_to_general[i] for i in top_indices if probs[i] > 0.05]
1058
+ ))
1059
+ return top_emotions, top_emotion_groups
1060
+
1061
+ def detect_intent(user_input):
1062
+ force_map = {
1063
+ "수족관": "수족관", "아쿠아리움":"수족관", "워터파크": "워터파크", "쇼핑":"쇼핑", "커플":"커플", "실내":"실내", "가족":"가족", "산책":"산책",
1064
+ "전망":"전망", "해양 체험":"해양체험", "종교":"종교", "성당":"성당", "웨딩":"예���장", "역사":"역사", "자연":"자연", '부모님':'가족', '애들':'가족','아이들':'가족',
1065
+ "궁전":"궁전", "문화 체험": "문화체험", "박물관":"박물관", "예술 작품":"예술감상", "과학관":"체험관", "광장":"광장", "미술관":"미술관",
1066
+ "공연":"공연", "유람선":"유람선", "야경":"야경", "호수":"호수", "휴양지":"휴양지","관람차":"관람차", "강":"강",
1067
+ "경기장":"경기장", "사원":"사원", "시장":"시장", "야시장":"야시장", "동물원":"동물원", "기차":"기차", "항구":"항구", "겨울 스포츠":"겨울스포츠",
1068
+ "식물원":"식물원", "케이블카":"케이블카", "해변":"해변", "바다":"해변", "테마파크":"테마파크", "트레킹":"트레킹", "섬":"섬", "맛있는":"미식",
1069
+ "버스":"버스", "기념관":"기념관", "신사":"신사", '바다':'해변', '인생샷':'포토존','먹방':'미식','소품':'쇼핑','바닷가':'해변','서핑':'해양체험',
1070
+ '일몰':'전망','로맨틱':'커플','브랜드샵':'쇼핑','아울렛':'쇼핑','비치':'해변','고성':'성','고궁':'궁전','문화거리':'문화거리','전통마을':'문화체험',
1071
+ '곤돌라':'케이블카','스카이라인':'전망','힐링':'휴양지', '미식':'쇼핑', '문화':'문화체험', '걷기':'산책', '여자친구':'커플', '남자친구':'커플','연인':'커플'
1072
+ }
1073
+ for keyword, mapped_intent in force_map.items():
1074
+ if keyword in user_input:
1075
+ return mapped_intent, 1.0
1076
+ phrases, labels = [], []
1077
+ for intent, keywords in intent_keywords.items():
1078
+ for word in keywords:
1079
+ phrases.append(word)
1080
+ labels.append(intent)
1081
+
1082
+ sbert_model = load_sbert_model()
1083
+ input_emb = sbert_model.encode(user_input, convert_to_tensor=True)
1084
+ phrase_embs = sbert_model.encode(phrases, convert_to_tensor=True)
1085
+ sims = util.cos_sim(input_emb, phrase_embs)[0]
1086
+ max_idx = torch.argmax(sims).item()
1087
+ return labels[max_idx], float(sims[max_idx])
1088
+
1089
+ def extract_themes(emotion_groups, intent, force_mode=False):
1090
+ scores = defaultdict(float)
1091
+ if force_mode:
1092
+ mapped = category_mapping.get(intent)
1093
+ if mapped:
1094
+ scores[mapped] += 1.0
1095
+ return list(scores.keys())[:3]
1096
+ for group in emotion_groups:
1097
+ for cat in emotion_to_category_boost.get(group, []):
1098
+ mapped = category_mapping.get(cat)
1099
+ if mapped:
1100
+ scores[mapped] += 1.0
1101
+ mapped = category_mapping.get(intent)
1102
+ if mapped:
1103
+ scores[mapped] += 1.5
1104
+ ranked = sorted(scores.items(), key=lambda x: -x[1])
1105
+
1106
+ return [x[0] for x in ranked[:3]]
1107
+
1108
+
1109
+ def recommend_places_by_theme(theme, country_filter=None, city_filter=None):
1110
+ today = datetime.today().date()
1111
+
1112
+ # 1. 테마 필터링
1113
+ df = travel_df[travel_df['의도테마명'].str.contains(theme, na=False)].drop_duplicates(subset=["여행지"])
1114
+
1115
+ # 2. 국가/도시 필터링
1116
+ if city_filter:
1117
+ df = df[df["여행도시"].str.contains(city_filter)]
1118
+ if country_filter:
1119
+ df = df[df["여행나라"].str.contains(country_filter)]
1120
+
1121
+ # 3. 비어있으면 빈 DF 리턴
1122
+ if df.empty:
1123
+ df = pd.DataFrame(columns=travel_df.columns) # 빈 DF라도 컬럼 포함
1124
+
1125
+ # ✅ 최소 3개 수집 보장
1126
+ collected = df.copy()
1127
+
1128
+ extra_fill = travel_df[
1129
+ (travel_df['의도테마명'].str.contains(theme, na=False)) &
1130
+ (~travel_df['여행지'].isin(collected['여행지']))
1131
+ ].drop_duplicates(subset=["여행지"])
1132
+
1133
+ needed = 3 - len(collected)
1134
+ if needed > 0 and not extra_fill.empty:
1135
+ fill = extra_fill.sample(n=min(needed, len(extra_fill)), random_state=random.randint(1, 9999))
1136
+ collected = pd.concat([collected, fill], ignore_index=True)
1137
+
1138
+ # 여전히 부족하면 무작위로 travel_df에서 채우기
1139
+ if len(collected) < 3:
1140
+ fallback = travel_df[~travel_df['여행지'].isin(collected['여행지'])].drop_duplicates(subset=["여행지"])
1141
+ if not fallback.empty:
1142
+ fill = fallback.sample(n=min(3 - len(collected), len(fallback)), random_state=random.randint(1, 9999))
1143
+ collected = pd.concat([collected, fill], ignore_index=True)
1144
+
1145
+ # ✅ 필수 컬럼 보장
1146
+ for col in ["여행도시", "여행나라"]:
1147
+ if col not in collected.columns:
1148
+ collected[col] = None
1149
+
1150
+ # ✅ 통합테마명 보장
1151
+ if "통합테마명" not in collected.columns:
1152
+ collected["통합테마명"] = theme
1153
+ else:
1154
+ collected["통합테마명"] = collected["통합테마명"].fillna(theme)
1155
+
1156
+ return collected
1157
+
1158
+ def get_festival_info(city):
1159
+ match = festival_df[festival_df['여행도시'] == city]
1160
+ if match.empty:
1161
+ return "없음", None, None
1162
+ row = match.iloc[0]
1163
+ try:
1164
+ start = pd.to_datetime(row["시작일"]).date()
1165
+ end = pd.to_datetime(row["종료일"]).date()
1166
+ if end < today:
1167
+ return "없음", None, None
1168
+ return row["축제명"], start, end
1169
+ except:
1170
+ return "없음", None, None
1171
+ df[["추천축제", "축제시작", "축제종료"]] = df["여행도시"].apply(lambda x: pd.Series(get_festival_info(x)))
1172
+ return df
1173
+
1174
+ def make_top2_description_custom(row, used_phrases=set()):
1175
+ scores = {
1176
+ k: row.get(k, 0)
1177
+ for k in ["숙소", "일정", "가이드", "식사", "가성비", "이동수단"]
1178
+ }
1179
+ top2 = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:2]
1180
+ top2_keys = frozenset([k for k, _ in top2])
1181
+ phrases = feature_phrase_map.get(top2_keys, [])
1182
+
1183
+ if not phrases:
1184
+ return "", used_phrases
1185
+
1186
+ available = [p for p in phrases if p not in used_phrases]
1187
+ phrase = random.choice(available) if available else random.choice(phrases)
1188
+ used_phrases.add(phrase)
1189
+ return phrase, used_phrases
1190
+
1191
+ def format_summary_tags_custom(summary):
1192
+ if pd.isna(summary):
1193
+ return ""
1194
+ parts = [s.strip() for s in summary.split(",") if s.strip()]
1195
+ tags = []
1196
+
1197
+ i = 0
1198
+ while i < len(parts):
1199
+ part = parts[i]
1200
+ # 가이드 경비 블록 처리
1201
+ if "가이드 경비" in part or "가이드경비" in part:
1202
+ guide_block = [part]
1203
+ j = i + 1
1204
+ while j < len(parts):
1205
+ next_part = parts[j]
1206
+ guide_block.append(next_part)
1207
+ if "선택관광" in next_part:
1208
+ break
1209
+ j += 1
1210
+
1211
+ if len(guide_block) > 1:
1212
+ merged = "".join(guide_block[:-1]).replace(" ", "")
1213
+ tags.append(f"#{merged}")
1214
+ tags.append(f"#{guide_block[-1].strip()}")
1215
+ else:
1216
+ tags.extend(f"#{x.strip()}" for x in guide_block)
1217
+
1218
+ i = j + 1
1219
+ continue
1220
+
1221
+ # 일반 항목
1222
+ tags.append(f"#{part}")
1223
+ i += 1
1224
+
1225
+ return " ".join(tags)
1226
+
1227
+ def recommend_packages(
1228
+ selected_theme,
1229
+ selected_place,
1230
+ travel_df,
1231
+ package_df,
1232
+ theme_ui_map,
1233
+ chat_container=None
1234
+ ):
1235
+ import random
1236
+
1237
+ # ✅ 통합 테마명 추출
1238
+ if selected_theme in theme_ui_map:
1239
+ integrated_theme = selected_theme
1240
+ else:
1241
+ integrated_theme = (
1242
+ travel_df[
1243
+ travel_df["의도테마명"].str.contains(selected_theme, na=False)
1244
+ ]
1245
+ .drop_duplicates(subset=["여행지"])["통합테마명"]
1246
+ .mode()
1247
+ .iloc[0]
1248
+ )
1249
+
1250
+ # ✅ UI 이름 및 도시명
1251
+ selected_ui_name = theme_ui_map[integrated_theme][0]
1252
+ selected_city = travel_df.loc[
1253
+ travel_df["여행지"] == selected_place, "여행도시"
1254
+ ].values[0]
1255
+
1256
+ # ✅ 도시 필터
1257
+ filtered_package = package_df[
1258
+ package_df["여행도시"].str.contains(selected_city, na=False)
1259
+ ].copy()
1260
+
1261
+ # 📝 감성 문구
1262
+ def make_top2_description(row, used_phrases=set()):
1263
+ scores = {
1264
+ k: row.get(k, 0)
1265
+ for k in ["숙소", "일정", "가이드", "식사", "가성비", "이동수단"]
1266
+ }
1267
+ top2 = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:2]
1268
+ top2_keys = frozenset([k for k, _ in top2])
1269
+ phrases = feature_phrase_map.get(top2_keys, [])
1270
+
1271
+ if not phrases:
1272
+ return "", used_phrases
1273
+
1274
+ available = [p for p in phrases if p not in used_phrases]
1275
+ phrase = random.choice(available) if available else random.choice(phrases)
1276
+ used_phrases.add(phrase)
1277
+ return phrase, used_phrases
1278
+
1279
+ # 📝 요약정보를 해시태그로 변환
1280
+ def format_summary_tags(summary):
1281
+ if pd.isna(summary):
1282
+ return ""
1283
+ parts = [s.strip() for s in summary.split(",") if s.strip()]
1284
+ tags = []
1285
+
1286
+ i = 0
1287
+ while i < len(parts):
1288
+ part = parts[i]
1289
+ # 가이드 경비 블록 처리
1290
+ if "가이드 경비" in part or "가이드경비" in part:
1291
+ guide_block = [part]
1292
+ j = i + 1
1293
+ while j < len(parts):
1294
+ next_part = parts[j]
1295
+ guide_block.append(next_part)
1296
+ if "선택관광" in next_part:
1297
+ break
1298
+ j += 1
1299
+
1300
+ if len(guide_block) > 1:
1301
+ merged = "".join(guide_block[:-1]).replace(" ", "")
1302
+ tags.append(f"#{merged}")
1303
+ tags.append(f"#{guide_block[-1].strip()}")
1304
+ else:
1305
+ tags.extend(f"#{x.strip()}" for x in guide_block)
1306
+
1307
+ i = j + 1
1308
+ continue
1309
+
1310
+ # 일반 항목
1311
+ tags.append(f"#{part}")
1312
+ i += 1
1313
+
1314
+ return " ".join(tags)
1315
+
1316
+ # ✅ 샘플링
1317
+ recommend_package = filtered_package.sample(
1318
+ n=min(2, len(filtered_package)),
1319
+ random_state=42
1320
+ )
1321
+
1322
+ # ✅ 문구 생성
1323
+ recommend_texts = []
1324
+ title_candidates = theme_title_phrases.get(selected_ui_name, ["추천"])
1325
+ sampled_titles = random.sample(title_candidates, k=min(2, len(title_candidates)))
1326
+
1327
+ used_phrases = set()
1328
+ for idx, (_, row) in enumerate(recommend_package.iterrows(), 1):
1329
+ desc, used_phrases = make_top2_description(row.to_dict(), used_phrases)
1330
+ tags = format_summary_tags(row["요약정보"])
1331
+ title_phrase = sampled_titles[idx - 1] if idx <= len(sampled_titles) else random.choice(title_candidates)
1332
+ title = f"{selected_city} {title_phrase} 패키지"
1333
+
1334
+ recommend_texts.append(
1335
+ f"""{idx}. <strong>{title}</strong><br> 🅼 {desc}<br> {tags}<br> \
1336
+ <a href="{row.URL}" target="_blank" rel="noopener noreferrer"
1337
+ style="text-decoration:none;font-weight:600;color:#009c75;">
1338
+ 💚 바로가기&nbsp;↗
1339
+ </a>"""
1340
+ )
1341
+
1342
+ # ✅ 출력
1343
+ if recommend_texts:
1344
+ full_message = "🧳 이런 패키지를 추천드려요:<br><br>" + "<br><br>".join(recommend_texts)
1345
+ log_and_render(
1346
+ full_message,
1347
+ sender="bot",
1348
+ chat_container = chat_container,
1349
+ key="recommend_package_intro",
1350
+ )
1351
+
1352
+ else:
1353
+ log_and_render(
1354
+ "⚠️ 추천 가능한 패키지가 없습니다.",
1355
+ sender="bot",
1356
+ chat_container = chat_container,
1357
+ key="no_package_warning"
1358
+ )
1359
+ return
1360
+
1361
+ def handle_selected_place(selected_place, travel_df, external_score_df, festival_df, weather_df, selected_theme=None, chat_container=None):
1362
+ selected_row = travel_df[travel_df["여행지"] == selected_place].iloc[0]
1363
+ country = selected_row["여행나라"]
1364
+ city = selected_row["여행도시"]
1365
+
1366
+ message_lines = []
1367
+ message_lines.append(f"{selected_place}은(는) {city}에 위치해 있어요.")
1368
+ message_lines.append(get_weather_message(city, weather_df))
1369
+
1370
+ highlight = get_highlight_message(selected_place, travel_df, external_score_df, festival_df)
1371
+ if highlight:
1372
+ message_lines.append(highlight+"<br>")
1373
+
1374
+ ##수정##
1375
+ other = travel_df[(travel_df["여행도시"] == city) & (travel_df["여행지"] != selected_place)].drop_duplicates("여행지")
1376
+ if not other.empty:
1377
+ other_sample = other.sample(n=min(3, len(other)), random_state=42)
1378
+ sample_names = ", ".join(other_sample["여행지"].tolist())
1379
+ message_lines.append(f"함께 가보면 좋은 여행지: {sample_names}")
1380
+ else:
1381
+ message_lines.append("⚠️ 함께 가볼 다른 여행지가 없어요.")
1382
+
1383
+ # integrated_theme 추론 추가
1384
+ if selected_theme is None:
1385
+ theme_row = travel_df[travel_df["여행지"] == selected_place]
1386
+ if not theme_row.empty and pd.notna(theme_row.iloc[0]["통합테마명"]):
1387
+ selected_theme = theme_row.iloc[0]["통합테마명"]
1388
+
1389
+ full_message = "<br>".join(message_lines)
1390
+
1391
+ log_and_render(
1392
+ full_message,
1393
+ sender="bot",
1394
+ key=f"region_detail_{selected_place}",
1395
+ chat_container=chat_container
1396
+ )
1397
+
1398
+ recommend_packages(
1399
+ selected_theme=selected_theme,
1400
+ selected_place=selected_place,
1401
+ travel_df=travel_df,
1402
+ package_df=package_df,
1403
+ theme_ui_map=theme_ui_map,
1404
+ chat_container=chat_container
1405
+ )
1406
+
1407
+ def main():
1408
+ user_input = input("요즘, 어떤 여행이 떠오르시나요?")
1409
+
1410
+ # 1. 감정 및 의도 분석
1411
+ top_emotions, emotion_groups = analyze_emotion(user_input)
1412
+ intent, intent_score = detect_intent(user_input)
1413
+ country_filter, city_filter = detect_location_filter(user_input)
1414
+
1415
+ if country_filter or city_filter:
1416
+ loc_str = f"{country_filter or ''} {city_filter or ''}".strip()
1417
+ print(f"🔎 '{city_filter or country_filter}'에 해당하는 여행지들만 기반으로 추천드릴게요!")
1418
+
1419
+ candidate_themes = extract_themes(emotion_groups, intent, force_mode=(intent_score >= 0.7))
1420
+
1421
+ # 2. 출력
1422
+ print("\n[감정 분석 결과]")
1423
+ for emo, score in top_emotions:
1424
+ print(f"- {emo}: {score:.2f}%")
1425
+ print(f"\n[의도 판단 결과] → {intent} (유사도: {intent_score:.2f})")
1426
+
1427
+ # 3. 조건 분기
1428
+
1429
+ # ✅ case 1: intent 기반 추천
1430
+ if intent_score >= 0.70:
1431
+ selected_theme = intent
1432
+ print(f"\n[명확한 의도에 따라 자동 추천 테마 선택됨] → {selected_theme}")
1433
+
1434
+ # 의도 오프닝 문구 출력
1435
+ ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0]
1436
+ opening_line = (
1437
+ theme_opening_lines.get(ui_name)
1438
+ or intent_opening_lines.get(selected_theme)
1439
+ or None
1440
+ )
1441
+ if opening_line:
1442
+ print(f"\n{opening_line}")
1443
+
1444
+ theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter)
1445
+ theme_df = theme_df.drop_duplicates(subset=["여행지"])
1446
+ result_df = apply_weighted_score_filter(theme_df)
1447
+
1448
+ if len(result_df) < 3:
1449
+ fallback = travel_df[~travel_df['여행지'].isin(result_df['여행지'])].drop_duplicates(subset=["여행지"])
1450
+ if not fallback.empty:
1451
+ fill = fallback.sample(n=min(3 - len(result_df), len(fallback)), random_state=random.randint(1, 9999))
1452
+ result_df = pd.concat([result_df, fill], ignore_index=True)
1453
+
1454
+ # ✅ case 2: 후보 테마가 1개
1455
+ elif len(candidate_themes) == 1:
1456
+ selected_theme = candidate_themes[0]
1457
+ print(f"\n추천 가능한 테마가 1개이므로 자동 선택: {selected_theme}")
1458
+ theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter)
1459
+ theme_df = theme_df.drop_duplicates(subset=["여행지"])
1460
+ result_df = apply_weighted_score_filter(theme_df)
1461
+
1462
+ if len(result_df) < 3:
1463
+ fallback = travel_df[~travel_df['여행지'].isin(result_df['여행지'])].drop_duplicates(subset=["여행지"])
1464
+ if not fallback.empty:
1465
+ fill = fallback.sample(n=min(3 - len(result_df), len(fallback)), random_state=random.randint(1, 9999))
1466
+ result_df = pd.concat([result_df, fill], ignore_index=True)
1467
+
1468
+ # ✅ case 3: 복수 테마 → 사용자 선택
1469
+ else:
1470
+ # 복수 테마의 여행지 전체 수집
1471
+ all_theme_df = pd.concat([
1472
+ recommend_places_by_theme(t, country_filter, city_filter) for t in candidate_themes
1473
+ ])
1474
+ all_theme_df = all_theme_df.drop_duplicates(subset=["여행지"])
1475
+
1476
+ # 통합테마명 목록 추출
1477
+ # 5. 최종 병합
1478
+ filtered = pd.merge(
1479
+ all_theme_df,
1480
+ external_score_df[["여행나라", "여행도시"]],
1481
+ on=["여행나라", "여행도시"],
1482
+ how="inner"
1483
+ ).drop_duplicates(subset=["여행지"])
1484
+
1485
+ # 2) 중복 제거 후 최종 테마 목록
1486
+ filtered = filtered.drop_duplicates(subset=['여행지'])
1487
+ available_themes = filtered['통합테마명'].dropna().unique().tolist()[:3] ##[:3] 가가
1488
+
1489
+ # 💡 감성 UI 포맷으로 출력
1490
+ print("\n추천 가능한 여행 테마:")
1491
+ for idx, theme in enumerate(available_themes, 1):
1492
+ ui_name, ui_desc = theme_ui_map.get(theme, (theme, ""))
1493
+ print(f"{idx}. {ui_name} – {ui_desc}")
1494
+
1495
+ print("\n👉 어떤 테마가 끌리시나요?")
1496
+ print(" ".join(f"[{theme_ui_map.get(t, (t,))[0]}]" for t in available_themes))
1497
+
1498
+ # 자동 선택 or 사용자 입력
1499
+ if len(available_themes) == 1:
1500
+ selected_ui_name = theme_ui_map.get(available_themes[0], (available_themes[0], ""))[0]
1501
+ selected_theme = ui_to_theme_map[selected_ui_name]
1502
+ print(f"\n추천 가능한 테마가 1개이므로 자동 선택: {selected_ui_name}")
1503
+ else:
1504
+ sel = int(input("\n원하는 테마 번호를 선택하세요: ")) - 1
1505
+ selected_ui_name = theme_ui_map.get(available_themes[sel], (available_themes[sel], ""))[0]
1506
+ selected_theme = ui_to_theme_map[selected_ui_name]
1507
+ print(f"\n선택하신 테마: {selected_ui_name}")
1508
+
1509
+ # 해당 테마 기준 최종 추천
1510
+ theme_df = all_theme_df[all_theme_df["통합테마명"] == selected_theme]
1511
+ theme_df = theme_df.drop_duplicates(subset=["여행지"])
1512
+ result_df = apply_weighted_score_filter(theme_df)
1513
+
1514
+ ###추가###
1515
+ if len(result_df) < 3:
1516
+ fallback = travel_df[~travel_df['여행지'].isin(result_df['여행지'])].drop_duplicates(subset=["여행지"])
1517
+ if not fallback.empty:
1518
+ fill = fallback.sample(n=min(3 - len(result_df), len(fallback)), random_state=random.randint(1, 9999))
1519
+ result_df = pd.concat([result_df, fill], ignore_index=True)
1520
+
1521
+ # 4. 결과 출력
1522
+ if intent_score < 0.7: # 감정 기반인 경우에만 출력
1523
+ ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0]
1524
+ opening_line_template = theme_opening_lines.get(ui_name)
1525
+ if opening_line_template:
1526
+ print(f"\n{opening_line_template.format(len(result_df))}")
1527
+
1528
+ print("\n[최종 추천 여행지]")
1529
+ for idx, row in enumerate(result_df.itertuples(), 1):
1530
+ country = row.여행나라
1531
+ city = row.여행도시
1532
+ name = row.여행지
1533
+ desc = row.한줄설명 if hasattr(row, '한줄설명') else "설명이 없습니다"
1534
+
1535
+ if country == city:
1536
+ loc = f"{country}"
1537
+ else:
1538
+ loc = f"{country}, {city}"
1539
+
1540
+ print(f"{idx}. {name} ({loc}) - {desc}")
1541
+
1542
+ recommend_names = result_df["여행지"].tolist()
1543
+
1544
+ print("\n👉 마음에 드는 여행지를 골라주세요:")
1545
+ print(" ".join(f"[{name}]" for name in recommend_names))
1546
+
1547
+ try:
1548
+ sel = int(input("\n원하는 여행지 번호를 선택���세요:")) - 1
1549
+ if 0 <= sel < len(recommend_names):
1550
+ selected_place = recommend_names[sel]
1551
+ print(f"\n🎉 '{selected_place}'를 선택하셨습니다. 멋진 여행 되세요!")
1552
+ else:
1553
+ print("\n⚠️ 올바른 번호를 입력해주세요.")
1554
+ except ValueError:
1555
+ print("\n⚠️ 숫자로 된 번호를 입력해주세요.")
1556
+
1557
+ # In[12]:
1558
+
1559
+
1560
+ # if __name__ == "__main__":
1561
+ #main()
1562
+
1563
+
1564
+ # In[ ]:
1565
+
1566
+
1567
+
1568
+
countries_cities.csv ADDED
@@ -0,0 +1,3628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ 미국,워싱턴 D.C.
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
+ 말레이시아,고타
555
+ 말레이시아,젠타
556
+ 말레이시아,탄중말림
557
+ 말레이시아,페낭
558
+ 말레이시아,라왕
559
+ 필리핀,마닐라
560
+ 필리핀,케손시티
561
+ 필리핀,세부시티
562
+ 필리핀,다바오시티
563
+ 필리핀,칼람바
564
+ 필리핀,안티폴로
565
+ 필리핀,타귁
566
+ 필리핀,파사이
567
+ 필리핀,마카티
568
+ 필리핀,마리키나
569
+ 필리핀,파라냐케
570
+ 필리핀,라스피냐스
571
+ 필리핀,만달루용
572
+ 필리핀,발렌수엘라
573
+ 필리핀,몰롤로스
574
+ 필리핀,산페르난도
575
+ 필리핀,이로일로시티
576
+ 필리핀,바기오
577
+ 필리핀,바콜로드
578
+ 필리핀,카가얀데오로
579
+ 필리핀,일로일로
580
+ 필리핀,산타로사
581
+ 필리핀,앙겔레스
582
+ 필리핀,제너럴산토스
583
+ 필리핀,라푸라푸
584
+ 필리핀,일리간
585
+ 필리핀,산파블로
586
+ 필리핀,오르목
587
+ 필리핀,투게가라오
588
+ 필리핀,바탕가스시티
589
+ 필리핀,바탕가스
590
+ 필리핀,코타바토
591
+ 필리핀,타를락
592
+ 필리핀,로하스
593
+ 필리핀,카방칼란
594
+ 필리핀,루세나
595
+ 필리핀,도마스
596
+ 필리핀,라오아그
597
+ 필리핀,코타바토시티
598
+ 필리핀,산페르난도라유니온
599
+ 필리핀,산페르난도파파팡가
600
+ 필리핀,바이스리
601
+ 필리핀,키다파완
602
+ 필리핀,산후안
603
+ 필리핀,토로스
604
+ 필리핀,발레르
605
+ 필리핀,산호세델몬테
606
+ 필리핀,팔라얀
607
+ 대만,타이베이
608
+ 대만,신베이
609
+ 대만,타오위안
610
+ 대만,타이중
611
+ 대만,타이난
612
+ 대만,가오슝
613
+ 대만,기륭
614
+ 대만,신주
615
+ 대만,윈린
616
+ 대만,먀오리
617
+ 대만,난터우
618
+ 대만,푸리
619
+ 대만,지룽
620
+ 대만,화롄
621
+ 대만,타이둥
622
+ 대만,펑후
623
+ 대만,진먼
624
+ 대만,롄장
625
+ 대만,루강
626
+ 대만,신주시
627
+ 대만,신주현
628
+ 대만,장화현
629
+ 대만,윈린현
630
+ 대만,자이시
631
+ 대만,자이현
632
+ 대만,핑둥
633
+ 대만,핑둥현
634
+ 대만,난터우현
635
+ 대만,화롄현
636
+ 대만,타이둥현
637
+ 대만,둥강
638
+ 대만,루오동
639
+ 대만,관산
640
+ 대만,뤄둥
641
+ 대만,지아이
642
+ 대만,주난
643
+ 대만,토우펀
644
+ 대만,단수이
645
+ 대만,베이터우
646
+ 대만,중리
647
+ 대만,싼충
648
+ 대만,룽탄
649
+ 대만,뤼강
650
+ 대만,주베이
651
+ 대만,싼이
652
+ 대만,어롱
653
+ 대만,싼샤
654
+ 몽골,울란바토르
655
+ 몽골,에르데네트
656
+ 몽골,다르항
657
+ 몽골,초이발산
658
+ 몽골,무롱
659
+ 몽골,울기이
660
+ 몽골,알타이
661
+ 몽골,아르바이헤르
662
+ 몽골,달란자르갈란
663
+ 몽골,조론트
664
+ 몽골,바가노르
665
+ 몽골,자브항
666
+ 몽골,바얀홍고르
667
+ 몽골,바얀올기이
668
+ 몽골,헙드
669
+ 몽골,튀브
670
+ 몽골,바양호슈
671
+ 몽골,바가항가이
672
+ 몽골,발항
673
+ 몽골,셍게
674
+ 몽골,술데
675
+ 몽골,쇼빈
676
+ 몽골,아이막
677
+ 몽골,호브드
678
+ 몽골,호브스골
679
+ 몽골,도르노고비
680
+ 몽골,고비숨베르
681
+ 몽골,다르항올
682
+ 몽골,동고비
683
+ 몽골,동부르
684
+ 몽골,셍게르
685
+ 몽골,헨티
686
+ 몽골,중고비
687
+ 몽골,수흐바타르
688
+ 몽골,자브항솜
689
+ 몽골,달란자드가드
690
+ 몽골,오브르항가이
691
+ 몽골,자브항아이막
692
+ 몽골,아르항가이
693
+ 몽골,볼간
694
+ 몽골,오르홍
695
+ 몽골,고비알타이
696
+ 몽골,바가누르
697
+ 몽골,소험보
698
+ 몽골,하라호롬
699
+ 몽골,타반톨고이
700
+ 몽골,바트솜
701
+ 몽골,촐로트
702
+ 네팔,카트만두
703
+ 네팔,포카라
704
+ 네팔,랄릿푸르
705
+ 네팔,버크타푸르
706
+ 네팔,비라트나가르
707
+ 네팔,비라간지
708
+ 네팔,헤타우다
709
+ 네팔,담풀
710
+ 네팔,바네파
711
+ 네팔,이타하리
712
+ 네팔,장지
713
+ 네팔,버투왈
714
+ 네팔,바이래와
715
+ 네팔,도당
716
+ 네팔,니왈
717
+ 네팔,다란
718
+ 네팔,디락
719
+ 네팔,가이하트
720
+ 네팔,벨히야
721
+ 네팔,다당
722
+ 네팔,마누하리
723
+ 네팔,감수르
724
+ 네팔,찰리코트
725
+ 네팔,뉴아코트
726
+ 네팔,팔파
727
+ 네팔,굴미
728
+ 네팔,루판데히
729
+ 네팔,다르켈리
730
+ 네팔,다데일드후라
731
+ 네팔,바사르
732
+ 네팔,사페이
733
+ 네팔,수룩헤트
734
+ 네팔,피로트나
735
+ 네팔,바르디바스
736
+ 네팔,트리부반나가르
737
+ 네팔,라메창
738
+ 네팔,싱글리
739
+ 네팔,시라하
740
+ 네팔,다르추라
741
+ 네팔,루크라
742
+ 네팔,테라이
743
+ 네팔,바르타푸르
744
+ 네팔,라마푸르
745
+ 네팔,다두르
746
+ 네팔,보카라
747
+ 네팔,마히타리
748
+ 네팔,모랑
749
+ 네팔,키란
750
+ 네팔,가다카이
751
+ 포르투갈,리스본
752
+ 포르투갈,포르투
753
+ 포르투갈,코임브라
754
+ 포르투갈,브라가
755
+ 포르투갈,아베이루
756
+ 포르투갈,기마랑이스
757
+ 포르투갈,에보라
758
+ 포르투갈,파루
759
+ 포르투갈,피게이라다포스
760
+ 포르투갈,마투지뉴스
761
+ 포르투갈,세투발
762
+ 포르투갈,카스카이스
763
+ 포르투갈,케이루스
764
+ 포르투갈,비제우
765
+ 포르투갈,빌라노바드가이아
766
+ 포르투갈,신트라
767
+ 포르투갈,알마다
768
+ 포르투갈,베자
769
+ 포르투갈,비아도콘데
770
+ 포르투갈,포르탈레그레
771
+ 포르투갈,에스피뉴
772
+ 포르투갈,마데이라
773
+ 포르투갈,폰샬
774
+ 포르투갈,오디벨라스
775
+ 포르투갈,로우레
776
+ 포르투갈,오이란
777
+ 포르투갈,폰타델가다
778
+ 포르투갈,카르카벨로스
779
+ 포르투갈,타비라
780
+ 포르투갈,라구스
781
+ 포르투갈,칼다스다라이냐
782
+ 포르투갈,톰마르
783
+ 포르투갈,마르코데카나베세스
784
+ 포르투갈,바르셀루스
785
+ 포르투갈,론드라
786
+ 포르투갈,바르게이라
787
+ 포르투갈,팔마라
788
+ 포르투갈,엘바스
789
+ 포르투갈,아마도라
790
+ 포르투갈,세이샬
791
+ 포르투갈,페나피엘
792
+ 포르투갈,마프라
793
+ 포르투갈,파소스데페레이라
794
+ 포르투갈,샤베스
795
+ 포르투갈,올량
796
+ 포르투갈,카마라데로보
797
+ 포르투갈,알부페이라
798
+ 그리스,아테네
799
+ 그리스,테살로니키
800
+ 그리스,파트라스
801
+ 그리스,이라클리오
802
+ 그리스,라리사
803
+ 그리스,볼로스
804
+ 그리스,이오아니나
805
+ 그리스,트리칼라
806
+ 그리스,하니아
807
+ 그리스,카발라
808
+ 그리스,칼라마타
809
+ 그리스,코르푸
810
+ 그리스,로도스
811
+ 그리스,코모티니
812
+ 그리스,세레스
813
+ 그리스,카르디차
814
+ 그리스,베리아
815
+ 그리스,아그리니오
816
+ 그리스,카테리니
817
+ 그리스,아르타
818
+ 그리스,레스보스
819
+ 그리스,코자니
820
+ 그리스,피레아스
821
+ 그리스,플로리나
822
+ 그리스,라미아
823
+ 그리스,드라마
824
+ 그리스,스파르타
825
+ 그리스,메갈로폴리
826
+ 그리스,카스토리아
827
+ 그리스,킬키스
828
+ 그리스,피르고스
829
+ 그리스,알렉산드루폴리
830
+ 그리스,코르딜라
831
+ 그리스,자킨토스
832
+ 그리스,레프카다
833
+ 그리스,이타카
834
+ 그리스,칼키스
835
+ 그리스,마라톤
836
+ 그리스,메소롱기
837
+ 그리스,폴리치니
838
+ 그리스,로도피
839
+ 그리스,프로티
840
+ 그리스,사모스
841
+ 그리스,나플리오
842
+ 그리스,오레스티아다
843
+ 그리스,파르가
844
+ 그리스,페라
845
+ 그리스,로도스섬
846
+ 스위스,취리히
847
+ 스위스,제네바
848
+ 스위스,바젤
849
+ 스위스,베른
850
+ 스위스,로잔
851
+ 스위스,빈터투어
852
+ 스위스,루체른
853
+ 스위스,생갈렌
854
+ 스위스,루가노
855
+ 스위스,비엘
856
+ 스위스,투르가우
857
+ 스위스,티치노
858
+ 스위스,투린겐
859
+ 스위스,샤프하우젠
860
+ 스위스,슈비츠
861
+ 스위스,프리부르
862
+ 스위스,솔로투른
863
+ 스위스,노이엔부르크
864
+ 스위스,우스터
865
+ 스위스,브루크
866
+ 스위스,크리엔스
867
+ 스위스,티펜브루넨
868
+ 스위스,렌츠부르크
869
+ 스위스,아라우
870
+ 스위스,시옹
871
+ 스위스,비터스빌
872
+ 스위스,바덴
873
+ 스위스,레겐스도르프
874
+ 스위스,에멘
875
+ 스위스,올텐
876
+ 스위스,슈투르
877
+ 스위스,슈바이츠
878
+ 스위스,투네
879
+ 스위스,슈바넨
880
+ 스위스,라쇼드퐁
881
+ 스위스,이베르동
882
+ 스위스,브크스
883
+ 스위스,글라루스
884
+ 스위스,로잔느
885
+ 스위스,샤텔생드니
886
+ 스위스,쥐라
887
+ 스위스,크로이첸링겐
888
+ 스위스,스튜사흐
889
+ 스위스,아펜첼
890
+ 스위스,크리에스
891
+ 스위스,베크스
892
+ 싱가포���,센트럴
893
+ 싱가포르,부킷티마
894
+ 싱가포르,오차드
895
+ 싱가포르,마리나베이
896
+ 싱가포르,차이나타운
897
+ 싱가포르,리틀인디아
898
+ 싱가포르,부킷바톡
899
+ 싱가포르,부킷메라
900
+ 싱가포르,부킷판장
901
+ 싱가포르,칼랑
902
+ 싱가포르,토아파요
903
+ 싱가포르,탕린
904
+ 싱가포르,노벨나
905
+ 싱가포르,게일랑
906
+ 싱가포르,로칭
907
+ 싱가포르,탄종파가
908
+ 싱가포르,주롱이스트
909
+ 싱가포르,주롱웨스트
910
+ 싱가포르,크멘티
911
+ 싱가포르,추아추강
912
+ 싱가포르,파시르리스
913
+ 싱가포르,탐피니스
914
+ 싱가포르,호우강
915
+ 싱가포르,세랑군
916
+ 싱가포르,앙모키오
917
+ 싱가포르,비샨
918
+ 싱가포르,우드랜즈
919
+ 싱가포르,센가캉
920
+ 싱가포르,유추강
921
+ 싱가포르,포동파시르
922
+ 싱가포르,이슌
923
+ 싱가포르,시미
924
+ 싱가포르,도버
925
+ 싱가포르,퀸스타운
926
+ 싱가포르,베독
927
+ 싱가포르,마린파레이드
928
+ 싱가포르,부나비스타
929
+ 싱가포르,서던리지스
930
+ 싱가포르,이스턴
931
+ 싱가포르,파이오니어
932
+ 싱가포르,펑골
933
+ 싱가포르,셀레타
934
+ 싱가포르,알주니드
935
+ 싱가포르,바이야르르바루
936
+ 싱가포르,레드힐
937
+ 싱가포르,탐핀스노스
938
+ 싱가포르,신가포르리버
939
+ 싱가포르,오뜨람
940
+ 싱가포르,신가포르치프터운
941
+ 인도네시아,자카르타
942
+ 인도네시아,수라바야
943
+ 인도네시아,반둥
944
+ 인도네시아,메단
945
+ 인도네시아,베카시
946
+ 인도네시아,덴파사르
947
+ 인도네시아,탕에랑
948
+ 인도네시아,팔렘방
949
+ 인도네시아,마카사르
950
+ 인도네시아,바타미
951
+ 인도네시아,보고르
952
+ 인도네시아,파당
953
+ 인도네시아,삼린다
954
+ 인도네시아,슬라탄
955
+ 인도네시아,마나도
956
+ 인도네시아,발릭파판
957
+ 인도네시아,수카부미
958
+ 인도네시아,야카르타팀
959
+ 인도네시아,테르나테
960
+ 인도네시아,브카시
961
+ 인도네시아,시마랑
962
+ 인도네시아,페칸바루
963
+ 인도네시아,반자르마신
964
+ 인도네시아,폰티아낙
965
+ 인도네시아,자얍라
966
+ 인도네시아,쿠팡
967
+ 인도네시아,마타람
968
+ 인도네시아,암본
969
+ 인도네시아,참페
970
+ 인도네시아,비툰
971
+ 인도네시아,소로카르타
972
+ 인도네시아,자얀타라
973
+ 인도네시아,제파라
974
+ 인도네시아,보노
975
+ 인도네시아,포로볼링고
976
+ 인도네시아,란타르
977
+ 인도네시아,카리왕
978
+ 인도네시아,툴룽아궁
979
+ 인도네시아,마겔랑
980
+ 인도네시아,만도로
981
+ 인도네시아,바뉴왕이
982
+ 인도네시아,란퐁
983
+ 인도네시아,탄중피낭
984
+ 인도네시아,바투
985
+ 인도네시아,카리망
986
+ 인도네시아,프로볼링고
987
+ 인도네시아,시트레본
988
+ 인도네시아,마루스
989
+ 튀르키예,이스탄불
990
+ 튀르키예,앙카라
991
+ 튀르키예,이즈미르
992
+ 튀르키예,부르사
993
+ 튀르키예,안탈리아
994
+ 튀르키예,아다나
995
+ 튀르키예,가지안테프
996
+ 튀르키예,코냐
997
+ 튀르키예,카이세리
998
+ 튀르키예,메르신
999
+ 튀르키예,에스키셰히르
1000
+ 튀르키예,디야르바크르
1001
+ 튀르키예,삼순
1002
+ 튀르키예,데니즐리
1003
+ 튀르키예,샨르우르파
1004
+ 튀르키예,말라티아
1005
+ 튀르키예,카흐라만마라슈
1006
+ 튀르키예,에르주룸
1007
+ 튀르키예,엘라지
1008
+ 튀르키예,사카리야
1009
+ 튀르키예,트라브존
1010
+ 튀르키예,발리케시르
1011
+ 튀르키예,마르딘
1012
+ 튀르키예,오르두
1013
+ 튀르키예,카르스
1014
+ 튀르키예,아피온카라히사르
1015
+ 튀르키예,우샤크
1016
+ 튀르키예,비틀리스
1017
+ 튀르키예,아다파자르
1018
+ 튀르키예,네브셰히르
1019
+ 튀르키예,쉬르나크
1020
+ 튀르키예,카라만
1021
+ 튀르키예,아르다한
1022
+ 튀르키예,바트만
1023
+ 튀르키예,아그리
1024
+ 튀르키예,이체리시헤르
1025
+ 튀르키예,귰류크
1026
+ 튀르키예,에디르네
1027
+ 튀르키예,무글라
1028
+ 튀르키예,아마시아
1029
+ 튀르키예,니데
1030
+ 튀르키예,토카트
1031
+ 튀르키예,예니샤히르
1032
+ 튀르키예,듀즈제
1033
+ 튀르키예,예니마할레
1034
+ 튀르키예,질레
1035
+ 튀르키예,아크사라이
1036
+ 튀르키예,킬리스
1037
+ 튀르키예,바르틴
1038
+ 인도,뭄바이
1039
+ 인도,델리
1040
+ 인도,벵갈루루
1041
+ 인도,하이데라바드
1042
+ 인도,아메다바드
1043
+ 인도,첸나이
1044
+ 인도,콜카타
1045
+ 인도,수라트
1046
+ 인도,푸네
1047
+ 인도,자이푸르
1048
+ 인도,러크나우
1049
+ 인도,칸푸르
1050
+ 인도,나그푸르
1051
+ 인도,인도르
1052
+ 인도,보팔
1053
+ 인도,비사카파트남
1054
+ 인도,파트나
1055
+ 인도,부바네스와르
1056
+ 인도,루디아나
1057
+ 인도,아그라
1058
+ 인도,바도다라
1059
+ 인도,마두라이
1060
+ 인도,바랑가르
1061
+ 인도,나시크
1062
+ 인도,메루트
1063
+ 인도,파리드바드
1064
+ 인도,라지코트
1065
+ 인도,샤타르푸르
1066
+ 인도,스리나가르
1067
+ 인도,다바드
1068
+ 인도,구르가온
1069
+ 인도,암리차르
1070
+ 인도,자발푸르
1071
+ 인도,자운푸르
1072
+ 인도,코임바토르
1073
+ 인도,고르크푸르
1074
+ 인도,구왈리오르
1075
+ 인도,비자야와다
1076
+ 인도,벨가움
1077
+ 인도,비카네르
1078
+ 인도,알라하바드
1079
+ 인도,잠셰드푸르
1080
+ 인도,울하스나가르
1081
+ 인도,말라다
1082
+ 인도,실리구리
1083
+ 인도,탐바람
1084
+ 스리랑카,콜롬보
1085
+ 스리랑��,캔디
1086
+ 스리랑카,갈레
1087
+ 스리랑카,자프나
1088
+ 스리랑카,니곰보
1089
+ 스리랑카,라트나푸라
1090
+ 스리랑카,트린코말리
1091
+ 스리랑카,마하누와라
1092
+ 스리랑카,바두라
1093
+ 스리랑카,아누라다푸라
1094
+ 스리랑카,치라니카
1095
+ 스리랑카,마타라
1096
+ 스리랑카,쿠루네갈라
1097
+ 스리랑카,바티칼로아
1098
+ 스리랑카,킬리노치
1099
+ 스리랑카,함반토타
1100
+ 스리랑카,푸타람
1101
+ 스리랑카,비야니카
1102
+ 스리랑카,폴론나루와
1103
+ 스리랑카,암파라
1104
+ 스리랑카,칼루타라
1105
+ 스리랑카,엘라
1106
+ 스리랑카,웰라사라
1107
+ 스리랑카,단불라
1108
+ 스리랑카,웰라와야
1109
+ 스리랑카,아쿨라나
1110
+ 스리랑카,만나라
1111
+ 스리랑카,하푸탈레
1112
+ 스리랑카,알루타가마
1113
+ 스리랑카,모네라갈라
1114
+ 스리랑카,나왈라피티야
1115
+ 스리랑카,바라칼로아
1116
+ 스리랑카,마하라가마
1117
+ 스리랑카,타빌라마나
1118
+ 스리랑카,켈라니야
1119
+ 스리랑카,다감파
1120
+ 스리랑카,카투나야케
1121
+ 스리랑카,간가가마
1122
+ 스리랑카,아빈가무아
1123
+ 스리랑카,마운트라비니아
1124
+ 스리랑카,웰리마다
1125
+ 스리랑카,키리반다라
1126
+ 스리랑카,카라피타
1127
+ 스리랑카,카라니카
1128
+ 스리랑카,파나두라
1129
+ 스리랑카,왓타라
1130
+ 스리랑카,갈레포트
1131
+ 스리랑카,나타나가마
1132
+ 오스트리아,그라츠
1133
+ 오스트리아,린츠
1134
+ 오스트리아,잘츠부르크
1135
+ 오스트리아,인스브루크
1136
+ 오스트리아,클라겐푸르트
1137
+ 오스트리아,필라흐
1138
+ 오스트리아,웨너
1139
+ 오스트리아,장트푈텐
1140
+ 오스트리아,도른비른
1141
+ 오스트리아,브레겐츠
1142
+ 오스트리아,로이텐
1143
+ 오스트리아,볼프스베르크
1144
+ 오스트리아,슈타이어
1145
+ 오스트리아,펠트키르헨
1146
+ 오스트리아,크레무스안데어도나우
1147
+ 오스트리아,피싱
1148
+ 오스트리아,블루덴츠
1149
+ 오스트리아,암스트텐
1150
+ 오스트리아,쿠프슈타인
1151
+ 오스트리아,안스펠덴
1152
+ 오스트리아,트라운
1153
+ 오스트리아,하렌도르프
1154
+ 오스트리아,마우터
1155
+ 오스트리아,크로이츠슈타인
1156
+ 오스트리아,라벤스부르크
1157
+ 오스트리아,라르
1158
+ 오스트리아,가문덴
1159
+ 오스트리아,퓔스라우
1160
+ 오스트리아,브루크안데어라이타
1161
+ 오스트리아,로이벤
1162
+ 오스트리아,첼암제
1163
+ 오스트리아,슈바츠
1164
+ 오스트리아,로이터스
1165
+ 오스트리아,퓔커마르크트
1166
+ 오스트리아,하트베르크
1167
+ 오스트리아,그문덴
1168
+ 오스트리아,산트요한임퐁가우
1169
+ 오스트리아,크래머스
1170
+ 오스트리아,브루크무르
1171
+ 오스트리아,자르
1172
+ 오스트리아,슈타들라우
1173
+ 오스트리아,슈타이어마르크
1174
+ 오스트리아,아이젠슈타트
1175
+ 오스트리아,그라이스키르헨
1176
+ 오스트리아,트라우
1177
+ 오스트리아,칼스도르프
1178
+ 오스트리아,안델스부흐
1179
+ 핀란드,헬싱키
1180
+ 핀란드,에스포
1181
+ 핀란드,탐페레
1182
+ 핀란드,오울루
1183
+ 핀란드,투르쿠
1184
+ 핀란드,라흐티
1185
+ 핀란드,쿠오피오
1186
+ 핀란드,유바스큘라
1187
+ 핀란드,포리
1188
+ 핀란드,코콜라
1189
+ 핀란드,로바니에미
1190
+ 핀란드,로호라
1191
+ 핀란드,코우볼라
1192
+ 핀란드,케미
1193
+ 핀란드,요엔수
1194
+ 핀란드,살로
1195
+ 핀란드,미켈리
1196
+ 핀란드,로이마
1197
+ 핀란드,하메린나
1198
+ 핀란드,이마트라
1199
+ 핀란드,라펜란타
1200
+ 핀란드,세이나요키
1201
+ 핀란드,하미나
1202
+ 핀란드,바르카우스
1203
+ 핀란드,카야니
1204
+ 핀란드,칼레발라
1205
+ 핀란드,누르메스
1206
+ 핀란드,칼라요키
1207
+ 핀란드,오리마티라
1208
+ 핀란드,얘르벤파
1209
+ 핀란드,니보
1210
+ 핀란드,코르소
1211
+ 핀란드,피에타르사리
1212
+ 핀란드,시포오
1213
+ 핀란드,케라바
1214
+ 핀란드,킬피슬라미
1215
+ 핀란드,얌사
1216
+ 핀란드,빌라맨사
1217
+ 핀란드,발케아코스키
1218
+ 핀란드,로이히카리
1219
+ 핀란드,하우키프타
1220
+ 핀란드,칼라야르비
1221
+ 핀란드,노르마르쿠
1222
+ 핀란드,루오토
1223
+ 핀란드,인카라일라
1224
+ 핀란드,쿨타라
1225
+ 핀란드,파르가스
1226
+ 핀란드,요케이넨
1227
+ 네덜란드,암스테르담
1228
+ 네덜란드,로테르담
1229
+ 네덜란드,헤이그
1230
+ 네덜란드,위트레흐트
1231
+ 네덜란드,아인트호벤
1232
+ 네덜란드,틸뷔르흐
1233
+ 네덜란드,흐로닝언
1234
+ 네덜란드,알메러
1235
+ 네덜란드,브레다
1236
+ 네덜란드,나이메헌
1237
+ 네덜란드,엔스헤데
1238
+ 네덜란드,아펠도른
1239
+ 네덜란드,하를럼
1240
+ 네덜란드,즈볼러
1241
+ 네덜란드,덴보스
1242
+ 네덜란드,아른험
1243
+ 네덜란드,아머스포르트
1244
+ 네덜란드,마스트리흐트
1245
+ 네덜란드,레일라르덴
1246
+ 네덜란드,도르드레흐트
1247
+ 네덜란드,레이덴
1248
+ 네덜란드,즈위버르헨
1249
+ 네덜란드,알크마르
1250
+ 네덜란드,호른
1251
+ 네덜란드,헬몬트
1252
+ 네덜란드,훼이넨달
1253
+ 네덜란드,헤이르후고바르트
1254
+ 네덜란드,루르몬트
1255
+ 네덜란드,베이르트
1256
+ 네덜란드,스헤르토헨보스
1257
+ 네덜란드,로스말렌
1258
+ 네덜란드,에먼
1259
+ 네덜란드,스파이켄니서
1260
+ 네덜란드,펄레
1261
+ 네덜란드,펄메렌드
1262
+ 네덜란드,하를럼메르메이르
1263
+ 네덜란드,오스
1264
+ 네덜란드,베르헌오프좀
1265
+ 네덜란드,비넨달
1266
+ 네덜란드,우덴
1267
+ 네덜란드,비넨
1268
+ 네덜란드,카펠레인아인덴이셀
1269
+ 네덜란드,아우데바터
1270
+ 네덜란드,레크산트
1271
+ 네덜란드,자이스트
1272
+ 네덜란드,발리크
1273
+ 네덜란드,티엘
1274
+ 네덜란드,하우타인
1275
+ 네덜란드,누네
1276
+ 네덜란드,피안헨
1277
+ 노르웨이,오슬로
1278
+ 노르웨이,베르겐
1279
+ 노르웨이,트론헤임
1280
+ 노르웨이,스타방에르
1281
+ 노르웨이,프레드릭스타
1282
+ 노르웨이,드람멘
1283
+ 노르웨이,크리스티안산
1284
+ 노르웨이,샌네스
1285
+ 노르웨이,트롬쇠
1286
+ 노르웨이,아렌달
1287
+ 노르웨이,하마르
1288
+ 노르웨이,할덴
1289
+ 노르웨이,리고
1290
+ 노르웨이,포르스그룬
1291
+ 노르웨이,스키엔
1292
+ 노르웨이,예비크
1293
+ 노르웨이,호네포스
1294
+ 노르웨이,예스하임
1295
+ 노르웨이,엘베룸
1296
+ 노르웨이,라이르비크
1297
+ 노르웨이,크롱에르
1298
+ 노르웨이,나르빅
1299
+ 노르웨이,오렌달
1300
+ 노르웨이,리레스트롬
1301
+ 노르웨이,스테인헤르
1302
+ 노르웨이,렌네스
1303
+ 노르웨이,피엘라
1304
+ 노르웨이,플로
1305
+ 노르웨이,모이라나
1306
+ 노르웨이,킬데베르그
1307
+ 노르웨이,리스하임
1308
+ 노르웨이,홀메스트란
1309
+ 노르웨이,브뤼네
1310
+ 노르웨이,닐스트룀
1311
+ 노르웨이,트로스빅
1312
+ 노르웨이,피엘트마르크
1313
+ 노르웨이,피엘트스타
1314
+ 노르웨이,프레스트
1315
+ 노르웨이,브륀노이순드
1316
+ 노르웨이,피오르드
1317
+ 노르웨이,콜보텐
1318
+ 노르웨이,플로리오
1319
+ 노르웨이,바케로이
1320
+ 노르웨이,우타
1321
+ 노르웨이,란드비크
1322
+ 노르웨이,에이두스
1323
+ 노르웨이,피엘트포르스
1324
+ 덴마크,코펜하겐
1325
+ 덴마크,오르후스
1326
+ 덴마크,오덴세
1327
+ 덴마크,올보르
1328
+ 덴마크,에스비에르
1329
+ 덴마크,란데르스
1330
+ 덴마크,콜딩
1331
+ 덴마크,호르센스
1332
+ 덴마크,비보르
1333
+ 덴마크,로스킬레
1334
+ 덴마크,헤르닝
1335
+ 덴마크,실케보르
1336
+ 덴마크,나이스트베드
1337
+ 덴마크,프레데릭스하운
1338
+ 덴마크,스벤보르
1339
+ 덴마크,홀스테브로
1340
+ 덴마크,하딩
1341
+ 덴마크,니보르
1342
+ 덴마크,헤르레브
1343
+ 덴마크,쇠보르
1344
+ 덴마크,나스트베드
1345
+ 덴마크,슬라고르
1346
+ 덴마크,프레데릭스베르
1347
+ 덴마크,피엘러
1348
+ 덴마크,바일레
1349
+ 덴마크,토스트루프
1350
+ 덴마크,하데르슬레브
1351
+ 덴마크,소뵈르
1352
+ 덴마크,힐레뢰드
1353
+ 덴마크,넥쇠
1354
+ 덴마크,스케브
1355
+ 덴마크,타스트럽
1356
+ 덴마크,뢰드비
1357
+ 덴마크,그레브
1358
+ 덴마크,히빙
1359
+ 덴마크,프레데릭순
1360
+ 덴마크,브뢴비
1361
+ 덴마크,로스킬데
1362
+ 덴마크,토어스하운
1363
+ 덴마크,르비
1364
+ 덴마크,피온
1365
+ 덴마크,마리보
1366
+ 덴마크,알보르
1367
+ 덴마크,미델파르트
1368
+ 덴마크,비보르크
1369
+ 덴마크,파더보르크
1370
+ 덴마크,스킬스키
1371
+ 덴마크,엘신노어
1372
+ 덴마크,홋슈스
1373
+ 스웨덴,스톡홀름
1374
+ 스웨덴,예테보리
1375
+ 스웨덴,말뫼
1376
+ 스웨덴,웁살라
1377
+ 스웨덴,벡셰
1378
+ 스웨덴,외레브로
1379
+ 스웨덴,린셰핑
1380
+ 스웨덴,헬싱보리
1381
+ 스웨덴,예블레
1382
+ 스웨덴,예테보리서부
1383
+ 스웨덴,칼마르
1384
+ 스웨덴,할름스타드
1385
+ 스웨덴,보로스
1386
+ 스웨덴,베스테로스
1387
+ 스웨덴,우메오
1388
+ 스웨덴,순드스발
1389
+ 스웨덴,에스킬스투나
1390
+ 스웨덴,카를스타드
1391
+ 스웨덴,노르셰핑
1392
+ 스웨덴,팔룬
1393
+ 스웨덴,루레오
1394
+ 스웨덴,트롤하탄
1395
+ 스웨덴,외스터순드
1396
+ 스웨덴,스코브데
1397
+ 스웨덴,크리스티안스타드
1398
+ 스웨덴,난데르
1399
+ 스웨덴,칼스크로나
1400
+ 스웨덴,예르플라
1401
+ 스웨덴,에레브루
1402
+ 스웨덴,비크스요
1403
+ 스웨덴,호딩게
1404
+ 스웨덴,크로노베리
1405
+ 스웨덴,에데보
1406
+ 스웨덴,살라
1407
+ 스웨덴,티보
1408
+ 스웨덴,코포라
1409
+ 스웨덴,모타라
1410
+ 스웨덴,마리에스타드
1411
+ 스웨덴,앙홀트
1412
+ 스웨덴,소데르텔리에
1413
+ 스웨덴,비스뷔
1414
+ 스웨덴,칼릭스
1415
+ 스웨덴,우디발라
1416
+ 스웨덴,스타프손
1417
+ 스웨덴,알링스
1418
+ 스웨덴,티에르프
1419
+ 스웨덴,마리에프레드
1420
+ 스웨덴,보덴
1421
+ 스웨덴,슬리테
1422
+ 스웨덴,브롬마
1423
+ 벨기에,브뤼셀
1424
+ 벨기에,앤트워프
1425
+ 벨기에,겐트
1426
+ 벨기에,브뤼헤
1427
+ 벨기에,리에주
1428
+ 벨기에,나무르
1429
+ 벨기에,루벤
1430
+ 벨기에,메헬렌
1431
+ 벨기에,샤를루아
1432
+ 벨기에,하셀트
1433
+ 벨기에,코르트레이크
1434
+ 벨기에,신트닉라스
1435
+ 벨기에,아알스트
1436
+ 벨기에,로커렌
1437
+ 벨기에,오스텐더
1438
+ 벨기에,위클레
1439
+ 벨기에,뷔렌
1440
+ 벨기에,모르탈
1441
+ 벨기에,도른
1442
+ 벨기에,브라스차트
1443
+ 벨기에,리르
1444
+ 벨기에,발케나르
1445
+ 벨기에,앤더레흐트
1446
+ 벨기에,루아르
1447
+ 벨기에,안데르레흐트
1448
+ 벨기에,디스
1449
+ 벨기에,로슈포르
1450
+ 벨기에,비르통
1451
+ 벨기에,오데나르데
1452
+ 벨기에,헤이스덴
1453
+ 벨기에,스킬데
1454
+ 벨기에,유페
1455
+ 벨기에,루비오
1456
+ 벨기에,루엉
1457
+ 벨기에,베베르
1458
+ 벨기에,라루빌
1459
+ 벨기에,푸아스
1460
+ 벨기에,베링겐
1461
+ 벨기에,보크리크
1462
+ 벨기에,그림베르겐
1463
+ 벨기에,하렌
1464
+ 벨기에,메넨
1465
+ 벨기에,제렐
1466
+ 벨기에,지펜
1467
+ 벨기에,템세
1468
+ 벨기에,크노케헤이스트
1469
+ 벨기에,라루비에르
1470
+ 체코,프라하
1471
+ ���코,브르노
1472
+ 체코,오스트라바
1473
+ 체코,플젠
1474
+ 체코,올로모우츠
1475
+ 체코,리베레츠
1476
+ 체코,체스케부데요비체
1477
+ 체코,흐라데츠크랄로베
1478
+ 체코,우스티나트라벰
1479
+ 체코,파르두비체
1480
+ 체코,하블리츠쿠브브로트
1481
+ 체코,카라비나
1482
+ 체코,모스토프
1483
+ 체코,흘라바
1484
+ 체코,프셰로프
1485
+ 체코,예흘라바
1486
+ 체코,트르지네츠
1487
+ 체코,크로메르지시
1488
+ 체코,브제슬라브
1489
+ 체코,호도닌
1490
+ 체코,타보르
1491
+ 체코,츠르노프
1492
+ 체코,즈노이모
1493
+ 체코,쿠트나호라
1494
+ 체코,프리브람
1495
+ 체코,클라드노
1496
+ 체코,오파바
1497
+ 체코,지흘라바
1498
+ 체코,스비타비
1499
+ 체코,빌레모프
1500
+ 체코,콜린
1501
+ 체코,트루트노프
1502
+ 체코,프라흐라티체
1503
+ 체코,드브르크랄로베
1504
+ 체코,브룬탈
1505
+ 체코,라코프니크
1506
+ 체코,루드나
1507
+ 체코,로코브
1508
+ 체코,코플리브니체
1509
+ 체코,체스키크루몰로프
1510
+ 체코,흘루친
1511
+ 체코,세즈임
1512
+ 체코,체스케브로도
1513
+ 체코,체스카리파
1514
+ 체코,체스카트르제보바
1515
+ 체코,스트라코니체
1516
+ 체코,베로니
1517
+ 체코,슈텟케브
1518
+ 체코,소콜로프
1519
+ 체코,베네쇼프
1520
+ 헝가리,부다페스트
1521
+ 헝가리,데브레첸
1522
+ 헝가리,세게드
1523
+ 헝가리,미슈콜츠
1524
+ 헝가리,페치
1525
+ 헝가리,죠르
1526
+ 헝가리,니레지하저
1527
+ 헝가리,케치케메트
1528
+ 헝가리,섹슈페헤르바르
1529
+ 헝가리,솜버트헤이
1530
+ 헝가리,소르노크
1531
+ 헝가리,베케시차바
1532
+ 헝가리,자졸노크
1533
+ 헝가리,에스테르곰
1534
+ 헝가리,에게르
1535
+ 헝가리,호드메죄바샤르헤이
1536
+ 헝가리,카포슈바르
1537
+ 헝가리,자스베레니
1538
+ 헝가리,타타바녀
1539
+ 헝가리,잘레게르섹
1540
+ 헝가리,바사르헐리
1541
+ 헝가리,버치
1542
+ 헝가리,코마롬
1543
+ 헝가리,벨러스러이바르트
1544
+ 헝가리,도나우이바로시
1545
+ 헝가리,뉴기라드
1546
+ 헝가리,살고타르얀
1547
+ 헝가리,케스켐
1548
+ 헝가리,바츠
1549
+ 헝가리,하이두비허르마트
1550
+ 헝가리,시게트바르
1551
+ 헝가리,토로크발린트
1552
+ 헝가리,팔로타
1553
+ 헝가리,버러츠크
1554
+ 헝가리,마코
1555
+ 헝가리,하트반
1556
+ 헝가리,어위아르
1557
+ 헝가리,토타
1558
+ 헝가리,모하치
1559
+ 헝가리,메조토를로지
1560
+ 헝가리,버로츠크
1561
+ 헝가리,버츠케스크
1562
+ 헝가리,시오포크
1563
+ 헝가리,페치바라드
1564
+ 헝가리,실바쇼로슈
1565
+ 헝가리,칼로차
1566
+ 헝가리,토카이
1567
+ 헝가리,바이
1568
+ 헝가리,어이카
1569
+ 멕시코,멕시코시티
1570
+ 멕시코,몬테레이
1571
+ 멕시코,푸에블라
1572
+ 멕시코,톨루카
1573
+ 멕시코,티후아나
1574
+ 멕시코,시우다드후아레스
1575
+ 멕시코,토레온
1576
+ 멕시코,케레타로
1577
+ 멕시코,산루이스포토시
1578
+ 멕시코,아과스칼리엔테스
1579
+ 멕시코,쿠에르나바카
1580
+ 멕시코,모렐리아
1581
+ 멕시코,카수아레스
1582
+ 멕시코,살티요
1583
+ 멕시코,치와와
1584
+ 멕시코,에르모시요
1585
+ 멕시코,쿨리아칸
1586
+ 멕시코,마사틀란
1587
+ 멕시코,우루아판
1588
+ 멕시코,비야에르모사
1589
+ 멕시코,로스모치스
1590
+ 멕시코,카보산루카스
1591
+ 멕시코,엔세나다
1592
+ 멕시코,누에보라레도
1593
+ 멕시코,마타모로스
1594
+ 멕시코,아카풀코
1595
+ 멕시코,레리도
1596
+ 멕시코,피에드라스네그라스
1597
+ 멕시코,코아수일라
1598
+ 멕시코,오악사카
1599
+ 멕시코,사카테카스
1600
+ 멕시코,산크리스토발데라스카사스
1601
+ 멕시코,이스타팔루카
1602
+ 멕시코,타파출라
1603
+ 멕시코,셀라야
1604
+ 멕시코,만사니요
1605
+ 멕시코,카르데나스
1606
+ 멕시코,테피크
1607
+ 멕시코,카르멘
1608
+ 멕시코,이라푸아토
1609
+ 멕시코,아파세오
1610
+ 멕시코,코요아칸
1611
+ 멕시코,테카마크
1612
+ 멕시코,치말와칸
1613
+ 멕시코,산타카타리나
1614
+ 멕시코,사카테펙
1615
+ 브라질,상파울루
1616
+ 브라질,리우데자네이루
1617
+ 브라질,브라질리아
1618
+ 브라질,살바도르
1619
+ 브라질,포르투알레그리
1620
+ 브라질,포르탈레자
1621
+ 브라질,벨렝
1622
+ 브라질,쿠리치바
1623
+ 브라질,마나우스
1624
+ 브라질,레시페
1625
+ 브라질,고이아니아
1626
+ 브라질,캄피나스
1627
+ 브라질,상루이스
1628
+ 브라질,니테로이
1629
+ 브라질,블루메나우
1630
+ 브라질,테레지나
1631
+ 브라질,주앙페소아
1632
+ 브라질,마카파
1633
+ 브라질,마세이오
1634
+ 브라질,쿠이아바
1635
+ 브라질,비토리아
1636
+ 브라질,나탈
1637
+ 브라질,보아비스타
1638
+ 브라질,아라카주
1639
+ 브라질,소로카바
1640
+ 브라질,루젠드
1641
+ 브라질,필로타스
1642
+ 브라질,산토스
1643
+ 브라질,펠로타스
1644
+ 브라질,올린다
1645
+ 브라질,산호세두스캄푸스
1646
+ 브라질,타우바테
1647
+ 브라질,피라시카바
1648
+ 브라질,지우드
1649
+ 브라질,페트롤리나
1650
+ 브라질,카라피쿠이바
1651
+ 브라질,보타푸고
1652
+ 브라질,세아라
1653
+ 브라질,아파레시다디고이아니아
1654
+ 브라질,상곤살로
1655
+ 브라질,카라과타투바
1656
+ 브라질,프랑카
1657
+ 브라질,자우
1658
+ 브라질,주이즈지포라
1659
+ 브라질,브루스케
1660
+ 브라질,아라라콰라
1661
+ 브라질,안나폴리스
1662
+ 브라질,파소폰두
1663
+ 브라질,마우아
1664
+ 브라질,우베를란디아
1665
+ 뉴질랜드,웰링턴
1666
+ 뉴질랜드,크라이스트처치
1667
+ 뉴질랜드,타우랑가
1668
+ 뉴질랜드,더니든
1669
+ 뉴질랜드,네이피어
1670
+ 뉴질랜드,로토루아
1671
+ 뉴질랜드,넬슨
1672
+ 뉴질랜드,뉴플리머스
1673
+ 뉴질랜드,인버카길
1674
+ 뉴질랜드,휘앙가레이
1675
+ 뉴질랜드,팔머스턴노스
1676
+ 뉴질랜드,타라나키
1677
+ 뉴질랜드,와이라라파
1678
+ 뉴질랜드,호크스베이
1679
+ 뉴질랜드,블레넘
1680
+ 뉴질랜드,티마루
1681
+ 뉴질랜드,그레이마우스
1682
+ 뉴질랜드,웨스트포트
1683
+ 뉴질랜드,왕거누이
1684
+ 뉴질랜드,마스터턴
1685
+ 뉴질랜드,오아마루
1686
+ 뉴질랜드,알렉산드라
1687
+ 뉴질랜드,와이히
1688
+ 뉴질랜드,피오피오
1689
+ 뉴질랜드,카이코우라
1690
+ 뉴질랜드,모투에카
1691
+ 뉴질랜드,다니버크
1692
+ 뉴질랜드,마나이아
1693
+ 뉴질랜드,패러매타
1694
+ 뉴질랜드,카티카티
1695
+ 뉴질랜드,마타마타
1696
+ 뉴질랜드,테아로하
1697
+ 뉴질랜드,스트랫포드
1698
+ 뉴질랜드,카와카와
1699
+ 뉴질랜드,오포티키
1700
+ 뉴질랜드,토코로아
1701
+ 뉴질랜드,헌티
1702
+ 뉴질랜드,테쿠이티
1703
+ 뉴질랜드,도카로아
1704
+ 뉴질랜드,와이카토
1705
+ 뉴질랜드,리버헤드
1706
+ 뉴질랜드,오네항가
1707
+ 뉴질랜드,오네하우
1708
+ 뉴질랜드,롤스톤
1709
+ 뉴질랜드,헨더슨
1710
+ 호주,시드니
1711
+ 호주,멜버른
1712
+ 호주,브리즈번
1713
+ 호주,퍼스
1714
+ 호주,애들레이드
1715
+ 호주,캔버라
1716
+ 호주,골드코스트
1717
+ 호주,뉴캐슬
1718
+ 호주,울런공
1719
+ 호주,선샤인코스트
1720
+ 호주,지롱
1721
+ 호주,호바트
1722
+ 호주,타운즈빌
1723
+ 호주,케언스
1724
+ 호주,투움바
1725
+ 호주,달링턴
1726
+ 호주,밸러랫
1727
+ 호주,벤디고
1728
+ 호주,록햄프턴
1729
+ 호주,매키
1730
+ 호주,번버리
1731
+ 호주,터마라
1732
+ 호주,글래드스톤
1733
+ 호주,마운트갬비어
1734
+ 호주,와가와가
1735
+ 호주,포트맥쿼리
1736
+ 호주,셰퍼턴
1737
+ 호주,알버리
1738
+ 호주,칼굴리
1739
+ 호주,타무워스
1740
+ 호주,라이삼
1741
+ 호주,포트오거스타
1742
+ 호주,마운트이사
1743
+ 호주,애니스모어
1744
+ 호주,발리나
1745
+ 호주,브로큰힐
1746
+ 호주,베라
1747
+ 호주,오렌지
1748
+ 호주,카툼바
1749
+ 호주,지스본
1750
+ 호주,더보
1751
+ 호주,제럴턴
1752
+ 호주,암머데일
1753
+ 호주,킬모어
1754
+ 호주,브런즈윅
1755
+ 호주,에스펀스
1756
+ 호주,포스터
1757
+ 호주,애머데일
1758
+ 호주,알리스스프링스
1759
+ 호주,위트선데이
1760
+ 중국,북경
1761
+ 중국,상해
1762
+ 중국,칭다오
1763
+ 중국,장가계
1764
+ 베트남,호치민
1765
+ 베트남,나트랑
1766
+ 태국,크라비
1767
+ 필리핀,보라카이
1768
+ 필리핀,보홀
1769
+ 필리핀,세부
1770
+ 홍콩,홍콩
1771
+ 대만,타이페이
1772
+ 싱가포르,싱가포르
1773
+ 인도네시아,발리
1774
+ 몽골,테를지
1775
+ 몰디브,말레
1776
+ 네팔,룸비니
1777
+ 포르투갈,포르토
1778
+ 오스트리아,비엔나
1779
+ 이탈리아,베니스
1780
+ 세르비아,베오그라드
1781
+ 그리스,산토리니
1782
+ 스위스,체르마트
1783
+ 아이슬란드,레이캬비크
1784
+ 아일랜드,더블린
1785
+ 미국,로스엔젤레스
1786
+ 미국,올란도
1787
+ 미국,호놀룰루
1788
+ 미국,사이판
1789
+ 멕시코,칸쿤
1790
+ 가나,아크라
1791
+ 가나,쿠마시
1792
+ 가나,타말레
1793
+ 가나,세콘디타코라디
1794
+ 가나,오부아시
1795
+ 가나,타코라디
1796
+ 가나,타폴리
1797
+ 가나,카세나크루아
1798
+ 가나,케이프코스트
1799
+ 가나,테치만
1800
+ 가나,볼가탕가
1801
+ 가나,마코리디
1802
+ 가나,키부
1803
+ 가나,아샨티
1804
+ 가나,볼타
1805
+ 가나,카수아
1806
+ 가나,아심팡
1807
+ 가나,스니아니
1808
+ 가나,압렌크와
1809
+ 가나,수니아니
1810
+ 가나,카롤리부
1811
+ 가나,니아수니아
1812
+ 가나,코프리두아
1813
+ 가나,아파나
1814
+ 가나,엔다
1815
+ 가나,앙골라
1816
+ 가나,아만포
1817
+ 가나,다보야
1818
+ 가나,멘지아
1819
+ 가나,와지
1820
+ 가나,나비타
1821
+ 가나,아카치
1822
+ 가나,브롱아하포
1823
+ 가나,아심포
1824
+ 가나,노르테
1825
+ 가나,가라카
1826
+ 가나,카테
1827
+ 가나,탄그마
1828
+ 가나,세이키
1829
+ 가나,사벨라
1830
+ 가나,티파
1831
+ 가나,도고
1832
+ 가나,아부아
1833
+ 가나,레아
1834
+ 도미니카공화국,산토도밍고
1835
+ 도미니카공화국,산티아고데로스카바예로스
1836
+ 도미니카공화국,라로마나
1837
+ 도미니카공화국,산페드로데마코리스
1838
+ 도미니카공화국,히게이
1839
+ 도미니카공화국,푸에르토플라타
1840
+ 도미니카공화국,산프란시스코데마코리스
1841
+ 도미니카공화국,산후안데라마과나
1842
+ 도미니카공화국,마오
1843
+ 도미니카공화국,아수아
1844
+ 도미니카공화국,몬테크리스티
1845
+ 도미니카공화국,코투이
1846
+ 도미니카공화국,보나오
1847
+ 도미니카공화국,모카
1848
+ 도미니카공화국,엘세이보
1849
+ 도미니카공화국,산크리스토발
1850
+ 도미니카공화국,페랄타
1851
+ 도미니카공화국,누에바에스파냐
1852
+ 도미니카공화국,바라오나
1853
+ 도미니카공화국,라베가
1854
+ 도미니카공화국,네이바
1855
+ 도미니카공화국,나과보
1856
+ 도미니카공화국,하라바코아
1857
+ 도미니카공화국,콘스탄사
1858
+ 도미니카공화국,몬테플라타
1859
+ 도미니카공화국,하토마요르
1860
+ 도미니카공화국,살세도
1861
+ 도미니카공화국,바오루코
1862
+ 도미니카공화국,아르투로
1863
+ 도미니카공화국,코마예
1864
+ 도미니카공화국,산호세데오코아
1865
+ 도미니카공화국,예라마
1866
+ 도미니카공화국,아토마요르델레이
1867
+ 도미니카공화국,카브레라
1868
+ 도미니카���화국,가스파르에르난데스
1869
+ 도미니카공화국,나구아
1870
+ 도미니카공화국,산라파엘델유마
1871
+ 도미니카공화국,라마타
1872
+ 도미니카공화국,안토니오구즈만
1873
+ 도미니카공화국,라호야
1874
+ 도미니카공화국,엘바예
1875
+ 도미니카공화국,산안토니오
1876
+ 도미니카공화국,도밍고사비오
1877
+ 도미니카공화국,라이스라
1878
+ 도미니카공화국,모네야
1879
+ 도미니카공화국,에나그로
1880
+ 도미니카공화국,엘푸에르토
1881
+ 도미니카공화국,시우다드누에바
1882
+ 도미니카공화국,엔산체라페
1883
+ 러시아,모스크바
1884
+ 러시아,상트페테르부르크
1885
+ 러시아,노보시비르스크
1886
+ 러시아,예카테린부르크
1887
+ 러시아,니즈니노브고로드
1888
+ 러시아,카잔
1889
+ 러시아,첼랴빈스크
1890
+ 러시아,옴스크
1891
+ 러시아,사마라
1892
+ 러시아,로스토프나도누
1893
+ 러시아,크라스노야르스크
1894
+ 러시아,보로네시
1895
+ 러시아,페름
1896
+ 러시아,볼고그라드
1897
+ 러시아,크라스노다르
1898
+ 러시아,사라토프
1899
+ 러시아,톰스크
1900
+ 러시아,툴라
1901
+ 러시아,이르쿠츠크
1902
+ 러시아,하바롭스크
1903
+ 러시아,야로슬라블
1904
+ 러시아,마하치칼라
1905
+ 러시아,오렌부르크
1906
+ 러시아,노보쿠즈네츠크
1907
+ 러시아,케메로보
1908
+ 러시아,류벤스크
1909
+ 러시아,바르나울
1910
+ 러시아,울리야놉스크
1911
+ 러시아,이제브스크
1912
+ 러시아,브얀스크
1913
+ 러시아,울란우데
1914
+ 러시아,첼라빈스크
1915
+ 러시아,벨고로드
1916
+ 러시아,수르구트
1917
+ 러시아,칼리닌그라드
1918
+ 러시아,니즈네캄스크
1919
+ 러시아,스타브로폴
1920
+ 러시아,타간로그
1921
+ 러시아,세베로드빈스크
1922
+ 러시아,코스트로마
1923
+ 러시아,키로프
1924
+ 러시아,펜자
1925
+ 러시아,스몰렌스크
1926
+ 러시아,나베레즈니첼니
1927
+ 러시아,노보체르카스크
1928
+ 러시아,노보야쿠츠크
1929
+ 러시아,예실쿠르간
1930
+ 러시아,알렉산드로프
1931
+ 러시아,코페이스크
1932
+ 아르헨티나,부에노스아이레스
1933
+ 아르헨티나,로사리오
1934
+ 아르헨티나,멘도사
1935
+ 아르헨티나,라플라타
1936
+ 아르헨티나,투쿠만
1937
+ 아르헨티나,마르델플라타
1938
+ 아르헨티나,산타페
1939
+ 아르헨티나,레시스텐시아
1940
+ 아르헨티나,네우켄
1941
+ 아르헨티나,산루이스
1942
+ 아르헨티나,콤도로리바다비아
1943
+ 아르헨티나,바히아블랑카
1944
+ 아르헨티나,파라나
1945
+ 아르헨티나,포사다스
1946
+ 아르헨티나,콘셉시온델우루과이
1947
+ 아르헨티나,리오쿠아르토
1948
+ 아르헨티나,산살바도르데후후이
1949
+ 아르헨티나,리오갈레고스
1950
+ 아르헨티나,카타마르카
1951
+ 아르헨티나,라리오하
1952
+ 아르헨티나,산니콜라스데로스아로요스
1953
+ 아르헨티나,고도이크루즈
1954
+ 아르헨티나,하네라
1955
+ 아르헨티나,마르델투유
1956
+ 아르헨티나,콜론
1957
+ 아르헨티나,카르멘데파타고네스
1958
+ 아르헨티나,산라파엘
1959
+ 아르헨티나,에스켈
1960
+ 아르헨티나,사라테
1961
+ 아르헨티나,오베라
1962
+ 아르헨티나,찰라바레타
1963
+ 아르헨티나,벨그라노
1964
+ 아르헨티나,후닌
1965
+ 아르헨티나,바라데로
1966
+ 아르헨티나,칠리시토
1967
+ 아르헨티나,산마르틴
1968
+ 아르헨티나,아베야네다
1969
+ 아르헨티나,베나도투에르토
1970
+ 아르헨티나,마르데아호
1971
+ 아르헨티나,산로렌소
1972
+ 아르헨티나,마차가이
1973
+ 아르헨티나,알타그라시아
1974
+ 아르헨티나,엘칼라파테
1975
+ 아르헨티나,사우세비에호
1976
+ 아이슬란드,아쿠레이리
1977
+ 아이슬란드,하프나르피오르두르
1978
+ 아이슬란드,코파보귀르
1979
+ 아이슬란드,가르다바이르
1980
+ 아이슬란드,아쿠르네스
1981
+ 아이슬란드,셀포스
1982
+ 아이슬란드,에이일스타디르
1983
+ 아이슬란드,그린다빅
1984
+ 아이슬란드,비크
1985
+ 아이슬란드,셀타르나르네스
1986
+ 아이슬란드,모스펠스바이르
1987
+ 아이슬란드,케플라비크
1988
+ 아이슬란드,스코가포스
1989
+ 아이슬란드,후사빅
1990
+ 아이슬란드,소우더크뢰쿠르
1991
+ 아이슬란드,포르락스회픈
1992
+ 아이슬란드,블론두오스
1993
+ 아이슬란드,이스라피외르뒤르
1994
+ 아이슬란드,보르가르네스
1995
+ 아이슬란드,스티키스홀뮈르
1996
+ 아이슬란드,에이디스비크
1997
+ 아이슬란드,브레이다슬레이크
1998
+ 아이슬란드,스코우가르
1999
+ 아이슬란드,드리트빅
2000
+ 아이슬란드,라후트라르
2001
+ 아이슬란드,그룬다르피외르뒤르
2002
+ 아이슬란드,플리요틀슬루트르
2003
+ 아이슬란드,라가르플요트
2004
+ 아이슬란드,헬라
2005
+ 아이슬란드,헬리사비크
2006
+ 아이슬란드,클레이비크
2007
+ 아이슬란드,디우피비크
2008
+ 아이슬란드,리르퓌크
2009
+ 아이슬란드,오우라로스
2010
+ 아이슬란드,스토다바키
2011
+ 아이슬란드,호픈
2012
+ 아이슬란드,리다르홀라르
2013
+ 아이슬란드,비크이미르달
2014
+ 아이슬란드,칼다르셀
2015
+ 아이슬란드,바트나에이오쿠틀
2016
+ 아이슬란드,에이야피아틀라요쿨
2017
+ 아이슬란드,스카프타펠
2018
+ 아이슬란드,그루타르비크
2019
+ 아이슬란드,헤스트피외르뒤르
2020
+ 아이슬란드,스토르후볼슬레르
2021
+ 아이슬란드,호프나르피외르뒤르
2022
+ 아이��란드,크발피외르뒤르
2023
+ 아이슬란드,크라플라
2024
+ 아이슬란드,하우칼리르
2025
+ 우크라이나,키이우
2026
+ 우크라이나,하르키우
2027
+ 우크라이나,오데사
2028
+ 우크라이나,드니프로
2029
+ 우크라이나,도네츠크
2030
+ 우크라이나,자포리자
2031
+ 우크라이나,리비우
2032
+ 우크라이나,크리비리흐
2033
+ 우크라이나,마리우폴
2034
+ 우크라이나,빈니차
2035
+ 우크라이나,헤르손
2036
+ 우크라이나,폴타바
2037
+ 우크라이나,체르니히우
2038
+ 우크라이나,체르카시
2039
+ 우크라이나,호로드노
2040
+ 우크라이나,자카르파티아
2041
+ 우크라이나,이바노프란키우스크
2042
+ 우크라이나,루한스크
2043
+ 우크라이나,루츠크
2044
+ 우크라이나,우지호로드
2045
+ 우크라이나,테르노필
2046
+ 우크라이나,믈리누프
2047
+ 우크라이나,크레멘추크
2048
+ 우크라이나,니콜라이프
2049
+ 우크라이나,벨리카노보실카
2050
+ 우크라이나,슬로비얀스크
2051
+ 우크라이나,카미안스케
2052
+ 우크라이나,베르댠스크
2053
+ 우크라이나,브로바리
2054
+ 우크라이나,크라마토르스크
2055
+ 우크라이나,파블로흐라드
2056
+ 우크라이나,바흐무트
2057
+ 우크라이나,우만
2058
+ 우크라이나,하르치즈크
2059
+ 우크라이나,호로슈
2060
+ 우크라이나,코스티안티니우카
2061
+ 우크라이나,안드리이우카
2062
+ 우크라이나,스테파니브카
2063
+ 우크라이나,카흐로브카
2064
+ 우크라이나,우즈호로드
2065
+ 우크라이나,드루즈키우카
2066
+ 우크라이나,스비틀로보츠크
2067
+ 우크라이나,칼루시
2068
+ 우크라이나,호르니츠키
2069
+ 우크라이나,체르노모르스크
2070
+ 우크라이나,노보폴타브카
2071
+ 우크라이나,부차
2072
+ 우크라이나,보르디안시크
2073
+ 우크라이나,보리스필
2074
+ 이라크,바그다드
2075
+ 이라크,모술
2076
+ 이라크,바스라
2077
+ 이라크,에르빌
2078
+ 이라크,수레마니야
2079
+ 이라크,나자프
2080
+ 이라크,카르발라
2081
+ 이라크,카르코크
2082
+ 이라크,나시리야
2083
+ 이라크,아르라카
2084
+ 이라크,라마디
2085
+ 이라크,힐라
2086
+ 이라크,디와니야
2087
+ 이라크,카디시야
2088
+ 이라크,쿠트
2089
+ 이라크,도훅
2090
+ 이라크,마흐무디야
2091
+ 이라크,팔루자
2092
+ 이라크,바쿠바
2093
+ 이라크,티크리트
2094
+ 이라크,알카임
2095
+ 이라크,하딘
2096
+ 이라크,라시디야
2097
+ 이라크,칼라트
2098
+ 이라크,알하우자
2099
+ 이라크,알샤미야
2100
+ 이라크,알쿠트
2101
+ 이라크,자크후
2102
+ 이라크,알하라스
2103
+ 이라크,알수크
2104
+ 이라크,알쿠라
2105
+ 이라크,알바티
2106
+ 이라크,알카르마
2107
+ 이라크,알자브
2108
+ 이라크,알투자
2109
+ 이라크,알샤트라
2110
+ 이라크,알아지지야
2111
+ 이라크,알시니야
2112
+ 이라크,알마흐무디야
2113
+ 이라크,알타지
2114
+ 이라크,알도라
2115
+ 이라크,알하비야
2116
+ 이라크,알무스하다
2117
+ 이라크,알하딘
2118
+ 이라크,알타미야
2119
+ 이라크,알야르무크
2120
+ 이라크,알라쉬크
2121
+ 이라크,알살람
2122
+ 이라크,알후세이니야
2123
+ 이스라엘,텔아비브
2124
+ 이스라엘,예루살렘
2125
+ 이스라엘,하이파
2126
+ 이스라엘,리숀레지온
2127
+ 이스라엘,페타티크바
2128
+ 이스라엘,아슈도드
2129
+ 이스라엘,네타냐
2130
+ 이스라엘,베르셰바
2131
+ 이스라엘,홀론
2132
+ 이스라엘,브네이브라크
2133
+ 이스라엘,라맛간
2134
+ 이스라엘,바트얌
2135
+ 이스라엘,아슈켈론
2136
+ 이스라엘,헤르츨리야
2137
+ 이스라엘,호파
2138
+ 이스라엘,카파르사바
2139
+ 이스라엘,모디인
2140
+ 이스라엘,예후드
2141
+ 이스라엘,나사렛
2142
+ 이스라엘,에일랏
2143
+ 이스라엘,아풀라
2144
+ 이스라엘,람라
2145
+ 이스라엘,레호보트
2146
+ 이스라엘,크리엇모츠킨
2147
+ 이스라엘,크리엇아타
2148
+ 이스라엘,츠파트
2149
+ 이스라엘,테베리아
2150
+ 이스라엘,카르미엘
2151
+ 이스라엘,미그달하에멕
2152
+ 이스라엘,오르아키바
2153
+ 이스라엘,사케닌
2154
+ 이스라엘,타이베
2155
+ 이스라엘,크파르카나
2156
+ 이스라엘,아크레
2157
+ 이스라엘,라우트
2158
+ 이스라엘,마알로트타르쉬하
2159
+ 이스라엘,예코나암일릿
2160
+ 이스라엘,네세치오나
2161
+ 이스라엘,오파킴
2162
+ 이스라엘,슈파람
2163
+ 이스라엘,에일라트
2164
+ 이스라엘,에코
2165
+ 이스라엘,베이트셰안
2166
+ 이스라엘,디모나
2167
+ 이스라엘,카쉴론
2168
+ 이스라엘,아랄
2169
+ 이스라엘,미스게브
2170
+ 이스라엘,모트자
2171
+ 이스라엘,에프라트
2172
+ 이집트,카이로
2173
+ 이집트,알렉산드리아
2174
+ 이집트,기자
2175
+ 이집트,수에즈
2176
+ 이집트,포트사이드
2177
+ 이집트,루크소르
2178
+ 이집트,아스완
2179
+ 이집트,마르사마트루흐
2180
+ 이집트,이즈마일리아
2181
+ 이집트,아슈트
2182
+ 이집트,알미니야
2183
+ 이집트,담냐타
2184
+ 이집트,타타
2185
+ 이집트,베니수에프
2186
+ 이집트,카프르엘셰이크
2187
+ 이집트,알마할라알쿠브라
2188
+ 이집트,자그하지그
2189
+ 이집트,카나
2190
+ 이집트,다카할리야
2191
+ 이집트,수하그
2192
+ 이집트,팔류움
2193
+ 이집트,알마하라
2194
+ 이집트,마르사알람
2195
+ 이집트,엘알라메인
2196
+ 이집트,알오부르
2197
+ 이집트,아스유트
2198
+ 이집트,벨베이스
2199
+ 이집트,사피
2200
+ 이집트,타흐타
2201
+ 이집트,아부티지
2202
+ 이집트,만수라
2203
+ 이집트,이브라힘
2204
+ 이집트,카르카르
2205
+ 이집트,파이윰
2206
+ 이집트,신티
2207
+ 이집트,���디나
2208
+ 이집트,지르자
2209
+ 이집트,부하이라
2210
+ 이집트,마흐알라
2211
+ 이집트,메니아
2212
+ 이집트,하와
2213
+ 이집트,시디바라니
2214
+ 이집트,쿠사
2215
+ 이집트,에드푸
2216
+ 이집트,엘멘야
2217
+ 이집트,바흘라
2218
+ 이집트,알카나
2219
+ 이집트,엘하마물
2220
+ 이집트,사드라
2221
+ 칠레,산티아고
2222
+ 칠레,발파라이소
2223
+ 칠레,비냐델마르
2224
+ 칠레,콘셉시온
2225
+ 칠레,라세레나
2226
+ 칠레,안토파가스타
2227
+ 칠레,테무코
2228
+ 칠레,란카구아
2229
+ 칠레,이킬리케
2230
+ 칠레,푸에르토몬트
2231
+ 칠레,코피아포
2232
+ 칠레,탈카
2233
+ 칠레,아리카
2234
+ 칠레,오소르노
2235
+ 칠레,치얀
2236
+ 칠레,파라소
2237
+ 칠레,칼라마
2238
+ 칠레,키요타
2239
+ 칠레,리나레스
2240
+ 칠레,라칼레라
2241
+ 칠레,코로넬
2242
+ 칠레,산펠리페
2243
+ 칠레,바타과
2244
+ 칠레,푸콘
2245
+ 칠레,라우타로
2246
+ 칠레,앙가로아
2247
+ 칠레,비야알레만
2248
+ 칠레,푸에르토아리나스
2249
+ 칠레,코르디예라
2250
+ 칠레,카스트로
2251
+ 칠레,멜리피야
2252
+ 칠레,카우케네스
2253
+ 칠레,키야오
2254
+ 칠레,로스라구스
2255
+ 칠레,카우틴
2256
+ 칠레,쿠릴로스
2257
+ 칠레,톨텐
2258
+ 칠레,칼데라
2259
+ 칠레,빌라알레그레
2260
+ 칠레,로타
2261
+ 칠레,코야이케
2262
+ 칠레,푸에르토바라스
2263
+ 칠레,투투브
2264
+ 칠레,페라코스
2265
+ 칠레,로스안데스
2266
+ 칠레,마이푸
2267
+ 칠레,킬푸에
2268
+ 카자흐스탄,아스타나
2269
+ 카자흐스탄,알마티
2270
+ 카자흐스탄,카라간다
2271
+ 카자흐스탄,아티라우
2272
+ 카자흐스탄,아크토베
2273
+ 카자흐스탄,타라즈
2274
+ 카자흐스탄,우랄스크
2275
+ 카자흐스탄,파블로다르
2276
+ 카자흐스탄,코스타나이
2277
+ 카자흐스탄,템르타우
2278
+ 카자흐스탄,카플차가이
2279
+ 카자흐스탄,코클셰타우
2280
+ 카자흐스탄,크질오르다
2281
+ 카자흐스탄,우스티카메노고르스크
2282
+ 카자흐스탄,제즈카즈간
2283
+ 카자흐스탄,악타우
2284
+ 카자흐스탄,발하쉬
2285
+ 카자흐스탄,루데노예
2286
+ 카자흐스탄,에키바스토즈
2287
+ 카자흐스탄,페트로파블로프스크
2288
+ 카자흐스탄,사리아가쉬
2289
+ 카자흐스탄,탈디코르간
2290
+ 카자흐스탄,투르키스탄
2291
+ 카자흐스탄,카라타우
2292
+ 카자흐스탄,시마나이하
2293
+ 카자흐스탄,샤흐틴스크
2294
+ 카자흐스탄,카스켈렌
2295
+ 카자흐스탄,아이카우
2296
+ 카자흐스탄,에미
2297
+ 카자흐스탄,사랑쿨락
2298
+ 카자흐스탄,타이쇼베
2299
+ 카자흐스탄,크라스노보츠크
2300
+ 카자흐스탄,아크수
2301
+ 카자흐스탄,마카치칼라
2302
+ 카자흐스탄,아바이
2303
+ 카자흐스탄,루바니
2304
+ 카자흐스탄,코사니
2305
+ 카자흐스탄,아크지라
2306
+ 카자흐스탄,스베틀로고르스크
2307
+ 카자흐스탄,체르니고브카
2308
+ 카자흐스탄,사트파예프
2309
+ 카자흐스탄,차르스
2310
+ 카자흐스탄,자나오젠
2311
+ 카자흐스탄,아트바사르
2312
+ 카자흐스탄,아야고즈
2313
+ 카자흐스탄,아울
2314
+ 카자흐스탄,구르예프
2315
+ 카자흐스탄,베세로프카
2316
+ 짐바브웨,하라레
2317
+ 짐바브웨,불라와요
2318
+ 짐바브웨,치툰구이자
2319
+ 짐바브웨,구베로
2320
+ 짐바브웨,마스빙고
2321
+ 짐바브웨,마운트다르윈
2322
+ 짐바브웨,치노이
2323
+ 짐바브웨,마라운데라
2324
+ 짐바브웨,카라이바
2325
+ 짐바브웨,치푸리
2326
+ 짐바브웨,비쿠타
2327
+ 짐바브웨,고쿠웨
2328
+ 짐바브웨,치레지
2329
+ 짐바브웨,치망가니
2330
+ 짐바브웨,키웨레
2331
+ 짐바브웨,루시투
2332
+ 짐바브웨,무레와
2333
+ 짐바브웨,엡워스
2334
+ 짐바브웨,마주웨
2335
+ 짐바브웨,마추투
2336
+ 짐바브웨,우에멍
2337
+ 짐바브웨,카돈가
2338
+ 짐바브웨,노토
2339
+ 짐바브웨,음부야
2340
+ 짐바브웨,마자리
2341
+ 짐바브웨,에스카파
2342
+ 짐바브웨,무타레
2343
+ 짐바브웨,카웨
2344
+ 짐바브웨,고쿠베라
2345
+ 짐바브웨,음본데
2346
+ 짐바브웨,카돔베
2347
+ 짐바브웨,부어히
2348
+ 짐바브웨,마케도
2349
+ 짐바브웨,카비라
2350
+ 짐바브웨,구투
2351
+ 짐바브웨,응고마
2352
+ 짐바브웨,루포메로
2353
+ 짐바브웨,브롬리
2354
+ 짐바브웨,카사마
2355
+ 짐바브웨,차리라
2356
+ 짐바브웨,루피라
2357
+ 짐바브웨,카라레
2358
+ 짐바브웨,마잔가
2359
+ 짐바브웨,음카티
2360
+ 짐바브웨,우무진디
2361
+ 짐바브웨,치프움바
2362
+ 짐바브웨,무쿠바
2363
+ 짐바브웨,루타레
2364
+ 짐바브웨,시노야
2365
+ 캄보디아,프놈펜
2366
+ 캄보디아,씨엠립
2367
+ 캄보디아,시하누크빌
2368
+ 캄보디아,바탐방
2369
+ 캄보디아,캄퐁참
2370
+ 캄보디아,타케오
2371
+ 캄보디아,캄폿
2372
+ 캄보디아,코콩
2373
+ 캄보디아,스와이리엥
2374
+ 캄보디아,프레이벵
2375
+ 캄보디아,라타나끼리
2376
+ 캄보디아,스텅트렝
2377
+ 캄보디아,크라티에
2378
+ 캄보디아,프레아비히어
2379
+ 캄보디아,몬돌끼리
2380
+ 캄보디아,바라이
2381
+ 캄보디아,바웅
2382
+ 캄보디아,포이펫
2383
+ 캄보디아,수앙사이
2384
+ 캄보디아,깜퐁솜
2385
+ 캄보디아,프놈크롬
2386
+ 캄보디아,크로체
2387
+ 캄보디아,모니롬
2388
+ 캄보디아,체이르
2389
+ 캄보디아,로락
2390
+ 캄보디아,오우동크
2391
+ 캄보디아,라옹
2392
+ 캄보디아,드라노트
2393
+ 캄보디아,쩌로이
2394
+ 캄보디아,빠이린
2395
+ 캄보디아,시엠푸
2396
+ 캄보디아,바르카
2397
+ 캄보디아,껌퐁터
2398
+ 캄보디아,스반나깨트
2399
+ 캄보디아,세레이사폰
2400
+ 캄보디아,크다이
2401
+ 캄보디아,롬���
2402
+ 캄보디아,스라엠
2403
+ 캄보디아,트라빼앙
2404
+ 캄보디아,스라엠캄퐁
2405
+ 캄보디아,크라오
2406
+ 캄보디아,동훽
2407
+ 캄보디아,펜롬
2408
+ 캄보디아,카이퐁
2409
+ 캄보디아,프레이방
2410
+ 캄보디아,테프
2411
+ 케냐,나이로비
2412
+ 케냐,몸바사
2413
+ 케냐,키수무
2414
+ 케냐,나쿠루
2415
+ 케냐,엘도렛
2416
+ 케냐,키투이
2417
+ 케냐,키수리
2418
+ 케냐,카카메가
2419
+ 케냐,키사무
2420
+ 케냐,투르카나
2421
+ 케냐,타나리버
2422
+ 케냐,말린디
2423
+ 케냐,메루
2424
+ 케냐,키안부
2425
+ 케냐,가리사
2426
+ 케냐,벵게
2427
+ 케냐,카라티나
2428
+ 케냐,마차코스
2429
+ 케냐,움베
2430
+ 케냐,케리초
2431
+ 케냐,부시아
2432
+ 케냐,키리냐가
2433
+ 케냐,키수니
2434
+ 케냐,마린디
2435
+ 케냐,키암부
2436
+ 케냐,음구니
2437
+ 케냐,나이바샤
2438
+ 케냐,치아카나
2439
+ 케냐,믹키니
2440
+ 케냐,타보라
2441
+ 케냐,믈란디
2442
+ 케냐,타이트타베타
2443
+ 케냐,이라라
2444
+ 케냐,모하디
2445
+ 케냐,모야레
2446
+ 케냐,남디
2447
+ 케냐,올칼루
2448
+ 케냐,카자도
2449
+ 케냐,카캄가
2450
+ 케냐,타크
2451
+ 케냐,카우카
2452
+ 케냐,카파라
2453
+ 케냐,케무
2454
+ 케냐,카지아도
2455
+ 케냐,마루구
2456
+ 케냐,키시
2457
+ 케냐,로이암쿠
2458
+ 케냐,키투이웨스트
2459
+ 콜롬비아,보고타
2460
+ 콜롬비아,메데인
2461
+ 콜롬비아,칼리
2462
+ 콜롬비아,바랑키야
2463
+ 콜롬비아,쿠쿠타
2464
+ 콜롬비아,부카라망가
2465
+ 콜롬비아,산타마르타
2466
+ 콜롬비아,이바게
2467
+ 콜롬비아,빌라비센시오
2468
+ 콜롬비아,만리살레스
2469
+ 콜롬비아,페레이라
2470
+ 콜롬비아,몬테리아
2471
+ 콜롬비아,파스토
2472
+ 콜롬비아,팝아얀
2473
+ 콜롬비아,아르메니아
2474
+ 콜롬비아,바예두파르
2475
+ 콜롬비아,퀴바도
2476
+ 콜롬비아,리오하차
2477
+ 콜롬비아,플로리다블랑카
2478
+ 콜롬비아,소아차
2479
+ 콜롬비아,야팔
2480
+ 콜롬비아,바예도리드
2481
+ 콜롬비아,플로렌시아
2482
+ 콜롬비아,투마코
2483
+ 콜롬비아,시판쿨라
2484
+ 콜롬비아,말람보
2485
+ 콜롬비아,엘바그레
2486
+ 콜롬비아,마이카오
2487
+ 콜롬비아,차차가
2488
+ 콜롬비아,바르가스
2489
+ 콜롬비아,바라노아
2490
+ 콜롬비아,카사나레
2491
+ 콜롬비아,마그달레나
2492
+ 콜롬비아,자미아카
2493
+ 콜롬비아,이타구이
2494
+ 콜롬비아,시우다드볼리바르
2495
+ 콜롬비아,후앙데아코스타
2496
+ 콜롬비아,카마르고
2497
+ 콜롬비아,비야르메르
2498
+ 콜롬비아,키브도
2499
+ 콜롬비아,시우다드델리바노
2500
+ 콜롬비아,세로산안토니오
2501
+ 콜롬비아,바르랑카베르메하
2502
+ 콜롬비아,라세바
2503
+ 콜롬비아,마라카이보
2504
+ 콩고,킨샤사
2505
+ 콩고,루분바시
2506
+ 콩고,믈루디
2507
+ 콩고,마타디
2508
+ 콩고,카남바
2509
+ 콩고,무부지마이
2510
+ 콩고,부카부
2511
+ 콩고,마니에마
2512
+ 콩고,키푸쿠
2513
+ 콩고,콩골레자
2514
+ 콩고,음반다카
2515
+ 콩고,분디아
2516
+ 콩고,우비라
2517
+ 콩고,루지지
2518
+ 콩고,키상가니
2519
+ 콩고,카상가니
2520
+ 콩고,칼레미
2521
+ 콩고,이시로
2522
+ 콩고,카사이
2523
+ 콩고,카방가
2524
+ 콩고,카나가
2525
+ 콩고,부니아
2526
+ 콩고,키크윗
2527
+ 콩고,마쿰바
2528
+ 콩고,카사이오리엔탈
2529
+ 콩고,콩고센트럴
2530
+ 콩고,마이엔도
2531
+ 콩고,카콩고
2532
+ 콩고,키카두
2533
+ 콩고,마팔라
2534
+ 콩고,카사이서부
2535
+ 콩고,콩고타운
2536
+ 콩고,카빌라
2537
+ 콩고,카부토
2538
+ 콩고,카빌라웨스트
2539
+ 콩고,칼루미
2540
+ 콩고,카바레
2541
+ 콩고,카상가
2542
+ 콩고,콜웨지
2543
+ 콩고,콩고리버
2544
+ 콩고,마시시
2545
+ 콩고,카마나
2546
+ 콩고,부메키
2547
+ 콩고,바산쿠수
2548
+ 콩고,키사이니
2549
+ 콩고,카마리
2550
+ 콩고,마니마
2551
+ 콩고,우반다카
2552
+ 파키스탄,카라치
2553
+ 파키스탄,라호르
2554
+ 파키스탄,페샤와르
2555
+ 파키스탄,이슬라마바드
2556
+ 파키스탄,라왈핀디
2557
+ 파키스탄,쿼타
2558
+ 파키스탄,무르탄
2559
+ 파키스탄,페살라바드
2560
+ 파키스탄,구자란왈라
2561
+ 파키스탄,수쿠르
2562
+ 파키스탄,사르곳하
2563
+ 파키스탄,사히왈
2564
+ 파키스탄,바하왈푸르
2565
+ 파키스탄,카수르
2566
+ 파키스탄,시알코트
2567
+ 파키스탄,구즈라트
2568
+ 파키스탄,만세라
2569
+ 파키스탄,스와트
2570
+ 파키스탄,미르푸르
2571
+ 파키스탄,자코바바드
2572
+ 파키스탄,데라가지칸
2573
+ 파키스탄,코하트
2574
+ 파키스탄,바누
2575
+ 파키스탄,압보타바드
2576
+ 파키스탄,라르카나
2577
+ 파키스탄,카로르
2578
+ 파키스탄,코르라치
2579
+ 파키스탄,키라트푸르
2580
+ 파키스탄,차크왈
2581
+ 파키스탄,라호르칸트
2582
+ 파키스탄,지라니왈라
2583
+ 파키스탄,노샤라
2584
+ 파키스탄,자파라바드
2585
+ 파키스탄,데라이스마일칸
2586
+ 파키스탄,카이무르
2587
+ 파키스탄,투르밧
2588
+ 파키스탄,칠라스
2589
+ 파키스탄,길기트
2590
+ 파키스탄,훈자
2591
+ 파키스탄,티모르가라
2592
+ 파키스탄,차크제라
2593
+ 파키스탄,포르데라
2594
+ 파키스탄,카샤
2595
+ 파키스탄,카쉬모어
2596
+ 파키스탄,탄크
2597
+ 파키스탄,골라
2598
+ 파키스탄,타크시라
2599
+ 파키스탄,하라푸르
2600
+ 페루,리마
2601
+ 페루,아레키파
2602
+ 페루,트루히요
2603
+ 페루,치클라요
2604
+ 페루,피우라
2605
+ 페루,이카
2606
+ 페루,쿠스코
2607
+ 페루,칼라오
2608
+ 페루,추클라요
2609
+ 페루,푸노
2610
+ 페루,타크나
2611
+ 페루,아야쿠초
2612
+ 페루,후아누코
2613
+ 페루,모케과
2614
+ 페루,파카스마요
2615
+ 페루,살라베리
2616
+ 페루,차차포���스
2617
+ 페루,후안카요
2618
+ 페루,츄루칸
2619
+ 페루,수야나
2620
+ 페루,타라포토
2621
+ 페루,친차알타
2622
+ 페루,카하마르카
2623
+ 페루,나스카
2624
+ 페루,툼베스
2625
+ 페루,티라포토
2626
+ 페루,몰렌도
2627
+ 페루,아바니카
2628
+ 페루,타이보
2629
+ 페루,아루아나
2630
+ 페루,아투사
2631
+ 페루,엘카야오
2632
+ 페루,베야비스타
2633
+ 페루,카냐르
2634
+ 페루,파이타
2635
+ 페루,산이시드로
2636
+ 페루,미라플로레스
2637
+ 페루,자우자
2638
+ 페루,우아라스
2639
+ 페루,팔카스
2640
+ 페루,아마소나스
2641
+ 페루,라오로야
2642
+ 페루,안다후아일라스
2643
+ 페루,푸라에라
2644
+ 페루,유리아마구아스
2645
+ 페루,스야나
2646
+ 페루,마라논
2647
+ 폴란드,바르샤바
2648
+ 폴란드,크라쿠프
2649
+ 폴란드,우치
2650
+ 폴란드,브로츠와프
2651
+ 폴란드,포즈난
2652
+ 폴란드,그단스크
2653
+ 폴란드,셰치니
2654
+ 폴란드,비아위스토크
2655
+ 폴란드,카토비체
2656
+ 폴란드,루블린
2657
+ 폴란드,비엘스코비아와
2658
+ 폴란드,비드고슈치
2659
+ 폴란드,올슈틴
2660
+ 폴란드,토룬
2661
+ 폴란드,켈체
2662
+ 폴란드,글리비체
2663
+ 폴란드,차토비체
2664
+ 폴란드,그디니아
2665
+ 폴란드,소포트
2666
+ 폴란드,라돔
2667
+ 폴란드,지엘로나구라
2668
+ 폴란드,바이엘스코비아
2669
+ 폴란드,피와
2670
+ 폴란드,발브지흐
2671
+ 폴란드,루비니크
2672
+ 폴란드,엘블롱크
2673
+ 폴란드,레그니차
2674
+ 폴란드,슈체츠넥
2675
+ 폴란드,플로크
2676
+ 폴란드,코샬린
2677
+ 폴란드,토마슈프마조비에츠키
2678
+ 폴란드,시엘체
2679
+ 폴란드,고주프비엘코폴스키
2680
+ 폴란드,레슈노
2681
+ 폴란드,스와우키
2682
+ 폴란드,오스트로비에츠
2683
+ 폴란드,노비송츠
2684
+ 폴란드,카르코프
2685
+ 폴란드,프셰미실
2686
+ 폴란드,스타라하워
2687
+ 폴란드,야스트셈비에
2688
+ 폴란드,타르누프
2689
+ 폴란드,비에슈
2690
+ 폴란드,올레산
2691
+ 폴란드,크로스노
2692
+ 폴란드,비엘리츠카
2693
+ 폴란드,오포레
2694
+ 폴란드,브제크
2695
+ 폴란드,스카르지스코
2696
+ 푸에르토리코,폰세
2697
+ 푸에르토리코,마야구에스
2698
+ 푸에르토리코,카로리나
2699
+ 푸에르토리코,카과스
2700
+ 푸에르토리코,바야몬
2701
+ 푸에르토리코,과야니야
2702
+ 푸에르토리코,아레시보
2703
+ 푸에르토리코,우마카오
2704
+ 푸에르토리코,카보로호
2705
+ 푸에르토리코,바르셀로네타
2706
+ 푸에르토리코,라레스
2707
+ 푸에르토리코,과야마
2708
+ 푸에르토리코,이스라베야
2709
+ 푸에르토리코,보네토
2710
+ 푸에르토리코,나구아보
2711
+ 푸에르토리코,푸엔테스
2712
+ 푸에르토리코,마우나보
2713
+ 푸에르토리코,알보누시
2714
+ 푸에르토리코,아귈라르
2715
+ 푸에르토리코,야부코아
2716
+ 푸에르토리코,올메도
2717
+ 푸에르토리코,아과디야
2718
+ 푸에르토리코,아과다
2719
+ 푸에르토리코,아냐스코
2720
+ 푸에르토리코,코모
2721
+ 푸에르토리코,게랄도
2722
+ 푸에르토리코,하이우야
2723
+ 푸에르토리코,모로비스
2724
+ 푸에르토리코,코로잘
2725
+ 푸에르토리코,루키요
2726
+ 푸에르토리코,베가바하
2727
+ 푸에르토리코,마리스칼
2728
+ 푸에르토리코,케브라디야
2729
+ 푸에르토리코,라자스
2730
+ 푸에르토리코,아하이유야
2731
+ 푸에르토리코,나란히토
2732
+ 푸에르토리코,산로렌조
2733
+ 푸에르토리코,살리나스
2734
+ 푸에르토리코,세바야
2735
+ 푸에르토리코,베가알타
2736
+ 푸에르토리코,시드라
2737
+ 푸에르토리코,토아바하
2738
+ 푸에르토리코,트루히요알토
2739
+ 푸에르토리코,우투아도
2740
+ 푸에르토리코,야우코
2741
+ 푸에르토리코,리오그란데
2742
+ 푸에르토리코,쿠마오
2743
+ 푸에르토리코,도라도
2744
+ 가이아나,린덴
2745
+ 가이아나,뉴암스테르담
2746
+ 가이아나,바트리카
2747
+ 가이아나,스쿠넌
2748
+ 가이아나,로세그날
2749
+ 가이아나,말리아
2750
+ 가이아나,코리버톤
2751
+ 가이아나,앤나리젠
2752
+ 가이아나,마디아
2753
+ 가이아나,마하이카
2754
+ 가이아나,라그루
2755
+ 가이아나,포트카이툼
2756
+ 가이아나,마하이카버버리스
2757
+ 가이아나,캐너마
2758
+ 가이아나,웨이크나마
2759
+ 가이아나,팜그로브
2760
+ 가이아나,로스익스텐션
2761
+ 가이아나,로즈홀
2762
+ 가이아나,로스이그니아
2763
+ 가이아나,파라카이마
2764
+ 가이아나,마아이카
2765
+ 가이아나,탑보
2766
+ 가이아나,에세퀴보
2767
+ 가이아나,콜라크릭
2768
+ 가이아나,마카바
2769
+ 가이아나,와카나마
2770
+ 가이아나,포트모우리
2771
+ 가이아나,와이라크
2772
+ 가이아나,와카포
2773
+ 가이아나,포트마라
2774
+ 가이아나,하트필드
2775
+ 가이아나,코토파르
2776
+ 가이아나,아르카이마
2777
+ 가이아나,클론브룩
2778
+ 가이아나,웨일즈
2779
+ 가이아나,크레마크
2780
+ 가이아나,스프링가든
2781
+ 가이아나,로즈홀타운
2782
+ 가이아나,트리올로
2783
+ 가이아나,메하이크
2784
+ 가이아나,롱크릭
2785
+ 가이아나,헤롤드타운
2786
+ 가이아나,버버리스
2787
+ 가이아나,롱크릭힐
2788
+ 가이아나,와마크리크
2789
+ 나이지리아,라고스
2790
+ 나이지리아,아부자
2791
+ 나이지리아,카노
2792
+ 나이지리아,이바단
2793
+ 나이지리아,포트하커트
2794
+ 나이지리아,카두나
2795
+ 나이지리아,베닌시티
2796
+ 나이지리아,마이두구리
2797
+ 나이지리아,일로린
2798
+ 나이지리아,아베오쿠타
2799
+ 나이지리아,엔우구
2800
+ 나이지리아,오니차
2801
+ 나이지리아,우요
2802
+ 나이지리아,칼라바르
2803
+ 나이지리아,와리
2804
+ 나이지리아,오웨리
2805
+ 나이지리아,소코토
2806
+ 나이지리아,오쇼구보
2807
+ 나이지리아,에누구
2808
+ 나이지리아,자리아
2809
+ 나이지리아,에베도
2810
+ 나이지리아,아쿠레
2811
+ 나이지리아,에펜
2812
+ 나이지리아,구스
2813
+ 나이지리아,야올라
2814
+ 나이지리아,카치나
2815
+ 나이지리아,로코자
2816
+ 나이지리아,에누구이즈
2817
+ 나이지리아,우무아히아
2818
+ 나이지리아,카피리
2819
+ 나이지리아,이제부오데
2820
+ 나이지리아,바우치
2821
+ 나이지리아,오그보모쇼
2822
+ 나이지리아,미나
2823
+ 나이지리아,마쿠르디
2824
+ 나이지리아,아주바
2825
+ 나이지리아,아베도
2826
+ 나이지리아,우게리
2827
+ 나이지리아,오크리카
2828
+ 나이지리아,예니고아
2829
+ 나이지리아,오토포
2830
+ 나이지리아,아비라
2831
+ 나이지리아,에코이
2832
+ 나이지리아,자마파라
2833
+ 나이지리아,아구로
2834
+ 나이지리아,아바
2835
+ 남아프리카공화국,요하네스버그
2836
+ 남아프리카공화국,케이프타운
2837
+ 남아프리카공화국,더반
2838
+ 남아프리카공화국,프리토리아
2839
+ 남아프리카공화국,포트엘리자베스
2840
+ 남아프리카공화국,블룸폰테인
2841
+ 남아프리카공화국,이스트런던
2842
+ 남아프리카공화국,폴로콰네
2843
+ 남아프리카공화국,킴벌리
2844
+ 남아프리카공화국,넬스프루이트
2845
+ 남아프리카공화국,조지
2846
+ 남아프리카공화국,마푸말랑가
2847
+ 남아프리카공화국,리처즈베이
2848
+ 남아프리카공화국,웰콤
2849
+ 남아프리카공화국,크루거스도르프
2850
+ 남아프리카공화국,우텐헤그
2851
+ 남아프리카공화국,마운트에이리프
2852
+ 남아프리카공화국,벤더
2853
+ 남아프리카공화국,페치스톤
2854
+ 남아프리카공화국,칼튼빌
2855
+ 남아프리카공화국,울룬디
2856
+ 남아프리카공화국,하라리스미스
2857
+ 남아프리카공화국,쿠루만
2858
+ 남아프리카공화국,스텔렌보스
2859
+ 남아프리카공화국,워스터
2860
+ 남아프리카공화국,크라이스도르프
2861
+ 남아프리카공화국,하우텐
2862
+ 남아프리카공화국,스프링스
2863
+ 남아프리카공화국,마푸토
2864
+ 남아프리카공화국,비쇼
2865
+ 남아프리카공화국,무르파
2866
+ 남아프리카공화국,워터베르크
2867
+ 남아프리카공화국,오츠혼
2868
+ 남아프리카공화국,피터마리츠버그
2869
+ 남아프리카공화국,카세리
2870
+ 남아프리카공화국,베른하르드
2871
+ 남아프리카공화국,레디스미스
2872
+ 남아프리카공화국,무룸비
2873
+ 남아프리카공화국,블로버그
2874
+ 남아프리카공화국,클레어몬트
2875
+ 남아프리카공화국,코트만스호프
2876
+ 남아프리카공화국,암잘로
2877
+ 남아프리카공화국,모코파네
2878
+ 남아프리카공화국,마로카
2879
+ 남아프리카공화국,자크라
2880
+ 남아프리카공화국,안도
2881
+ 남아프리카공화국,루이스트리히트
2882
+ 남아프리카공화국,하트비스포르트
2883
+ 루마니아,부쿠레슈티
2884
+ 루마니아,클루지나포카
2885
+ 루마니아,티미쇼아라
2886
+ 루마니아,콘스탄차
2887
+ 루마니아,크라이오바
2888
+ 루마니아,브라쇼브
2889
+ 루마니아,갈라치
2890
+ 루마니아,플로이에슈티
2891
+ 루마니아,오라데아
2892
+ 루마니아,브라일라
2893
+ 루마니아,아라드
2894
+ 루마니아,피테슈티
2895
+ 루마니아,시에비우
2896
+ 루마니아,바커우
2897
+ 루마니아,트르구무레슈
2898
+ 루마니아,바이아마레
2899
+ 루마니아,비토라
2900
+ 루마니아,부르쿠
2901
+ 루마니아,브러일라
2902
+ 루마니아,알바이울리아
2903
+ 루마니아,라미쿠발체아
2904
+ 루마니아,바이아스프리에
2905
+ 루마니아,투르다
2906
+ 루마니아,피아트라네암츠
2907
+ 루마니아,푸크샤니
2908
+ 루마니아,브라쇼브노르드
2909
+ 루마니아,세베슈
2910
+ 루마니아,술리나
2911
+ 루마니아,루게쥘
2912
+ 루마니아,부조우
2913
+ 루마니아,르시타
2914
+ 루마니아,투르누세베린
2915
+ 루마니아,데바
2916
+ 루마니아,페트로사니
2917
+ 루마니아,사투마레
2918
+ 루마니아,호레주
2919
+ 루마니아,올테니타
2920
+ 루마니아,레흐치아
2921
+ 루마니아,코로이아
2922
+ 루마니아,드로베타투르누세베린
2923
+ 루마니아,순게오루데무레슈
2924
+ 루마니아,루고지
2925
+ 루마니아,비스트리차
2926
+ 루마니아,지우르지우
2927
+ 루마니아,카람세베시
2928
+ 루마니아,베케오아르
2929
+ 루마니아,칼라라시
2930
+ 루마니아,슬라티나
2931
+ 루마니아,판테레스티
2932
+ 모로코,카사블랑카
2933
+ 모로코,라바트
2934
+ 모로코,마라케시
2935
+ 모로코,페스
2936
+ 모로코,탕헤르
2937
+ 모로코,메크네스
2938
+ 모로코,우즈다
2939
+ 모로코,케니트라
2940
+ 모로코,아가디르
2941
+ 모로코,테투안
2942
+ 모로코,나도르
2943
+ 모로코,엘자디다
2944
+ 모로코,세타트
2945
+ 모로코,쿠리브가
2946
+ 모로코,타르단트
2947
+ 모로코,이르하운
2948
+ 모로코,티자니트
2949
+ 모로코,타운아트
2950
+ 모로코,베니멜랄
2951
+ 모로코,타오르트
2952
+ 모로코,엘하우즈
2953
+ 모로코,부자두르
2954
+ 모로코,라윤
2955
+ 모로코,타나그라
2956
+ 모로코,메디야
2957
+ 모로코,베르칸
2958
+ 모로코,타르파야
2959
+ 모로코,제르다
2960
+ 모로코,수케스
2961
+ 모로코,아르라��디아
2962
+ 모로코,타운기트
2963
+ 모로코,타우르트
2964
+ 모로코,틴기르
2965
+ 모로코,투네트
2966
+ 모로코,타르가
2967
+ 모로코,이프란
2968
+ 모로코,아시라
2969
+ 모로코,에르푸드
2970
+ 모로코,에사우이라
2971
+ 모로코,시디이프니
2972
+ 모로코,게르미스
2973
+ 모로코,케사르
2974
+ 모로코,메크네스타운
2975
+ 모로코,제딘
2976
+ 모로코,타르수트
2977
+ 모로코,미데트
2978
+ 모로코,젤라타
2979
+ 모로코,샤우엔
2980
+ 방글라데시,다카
2981
+ 방글라데시,치타공
2982
+ 방글라데시,쿨나
2983
+ 방글라데시,라지샤히
2984
+ 방글라데시,실헷
2985
+ 방글라데시,바리살
2986
+ 방글라데시,랑푸르
2987
+ 방글라데시,콕스바자르
2988
+ 방글라데시,나라얀간지
2989
+ 방글라데시,가제푸르
2990
+ 방글라데시,코밀라
2991
+ 방글라데시,보그라
2992
+ 방글라데시,마이멘싱
2993
+ 방글라데시,제소르
2994
+ 방글라데시,파브나
2995
+ 방글라데시,파투아카리
2996
+ 방글라데시,브라만바리아
2997
+ 방글라데시,파르디푸르
2998
+ 방글라데시,디나라이푸르
2999
+ 방글라데시,마다리푸르
3000
+ 방글라데시,통기
3001
+ 방글라데시,사트키라
3002
+ 방글라데시,시라즈간지
3003
+ 방글라데시,술라푸르
3004
+ 방글라데시,쿠스티아
3005
+ 방글라데시,노아카리
3006
+ 방글라데시,다이나지푸르
3007
+ 방글라데시,마울비바자르
3008
+ 방글라데시,메헤르푸르
3009
+ 방글라데시,조나이푸르
3010
+ 방글라데시,시드푸르
3011
+ 방글라데시,라크시미푸르
3012
+ 방글라데시,피로즈푸르
3013
+ 방글라데시,탄가일
3014
+ 방글라데시,피랍푸르
3015
+ 방글라데시,잘카티
3016
+ 방글라데시,키쇼레간지
3017
+ 방글라데시,보이랄
3018
+ 방글라데시,라호다
3019
+ 방글라데시,하비간지
3020
+ 방글라데시,푸티아
3021
+ 방글라데시,시파이푸르
3022
+ 방글라데시,반드라반
3023
+ 방글라데시,보라일
3024
+ 방글라데시,이샤르디
3025
+ 방글라데시,닐파마리
3026
+ 방글라데시,루홀
3027
+ 방글라데시,쿨나라지샤히
3028
+ 방글라데시,루하간지
3029
+ 베네수엘라,카라카스
3030
+ 베네수엘라,바르키시메토
3031
+ 베네수엘라,마라카이
3032
+ 베네수엘라,시우다드과야나
3033
+ 베네수엘라,마투린
3034
+ 베네수엘라,푸에르토라크루즈
3035
+ 베네수엘라,포르투게사
3036
+ 베네수엘라,카베요
3037
+ 베네수엘라,카라보보
3038
+ 베네수엘라,엘티그레
3039
+ 베네수엘라,엘비헤아
3040
+ 베네수엘라,과타이레
3041
+ 베네수엘라,바르길라
3042
+ 베네수엘라,아카리과
3043
+ 베네수엘라,투쿠피타
3044
+ 베네수엘라,산펠릭스
3045
+ 베네수엘라,사카파
3046
+ 베네수엘라,라과이라
3047
+ 베네수엘라,과나레
3048
+ 베네수엘라,산카를로스
3049
+ 베네수엘라,오코마레델투이
3050
+ 베네수엘라,알타그라시아데오리투코
3051
+ 베네수엘라,소코포
3052
+ 베네수엘라,구아이카이푸로
3053
+ 베네수엘라,바르네아
3054
+ 베네수엘라,사우아렐
3055
+ 베네수엘라,아라과
3056
+ 베네수엘라,바예델투이
3057
+ 베네수엘라,베나카로
3058
+ 베네수엘라,라프리아
3059
+ 베네수엘라,아푸레
3060
+ 베네수엘라,쿠마나
3061
+ 베네수엘라,과야나
3062
+ 베네수엘라,카리파
3063
+ 베네수엘라,칼라보조
3064
+ 베네수엘라,보카델토코로
3065
+ 베네수엘라,엘삼브르
3066
+ 베네수엘라,사우아레
3067
+ 베네수엘라,코리
3068
+ 볼리비아,라파스
3069
+ 볼리비아,산타크루즈데라시에라
3070
+ 볼리비아,수크레
3071
+ 볼리비아,코차밤바
3072
+ 볼리비아,엘알토
3073
+ 볼리비아,오루로
3074
+ 볼리비아,타리하
3075
+ 볼리비아,포토시
3076
+ 볼리비아,트리니다드
3077
+ 볼리비아,코비하
3078
+ 볼리비아,몬테로
3079
+ 볼리비아,케차바야
3080
+ 볼리비아,카미리
3081
+ 볼리비아,우유니
3082
+ 볼리비아,야쿠이바
3083
+ 볼리비아,로레토
3084
+ 볼리비아,과야라마린
3085
+ 볼리비아,산이그나시오데벨라스코
3086
+ 볼리비아,산호세데치키토스
3087
+ 볼리비아,라하
3088
+ 볼리비아,자빌라
3089
+ 볼리비아,라구나
3090
+ 볼리비아,마몬
3091
+ 볼리비아,토로토로
3092
+ 볼리비아,바루오
3093
+ 볼리비아,마카레나
3094
+ 볼리비아,소라타
3095
+ 볼리비아,빌라툰아리
3096
+ 볼리비아,산타아나
3097
+ 볼리비아,산호세
3098
+ 볼리비아,샤르아
3099
+ 볼리비아,자피타
3100
+ 볼리비아,사마이파타
3101
+ 볼리비아,엘토로
3102
+ 볼리비아,산마테오
3103
+ 볼리비아,라메르세드
3104
+ 볼리비아,타마린도
3105
+ 볼리비아,포르토수아레즈
3106
+ 볼리비아,포르토퀴조
3107
+ 볼리비아,포르토비에호
3108
+ 볼리비아,코로이코
3109
+ 볼리비아,루레나바케
3110
+ 볼리비아,레예스
3111
+ 볼리비아,과이라
3112
+ 볼리비아,루리베이카
3113
+ 볼리비아,차카리
3114
+ 볼리비아,크루세냐
3115
+ 부탄,팀부
3116
+ 부탄,파로
3117
+ 부탄,펀처링
3118
+ 부탄,왕두에포드랑
3119
+ 부탄,장카르
3120
+ 부탄,트롱사
3121
+ 부탄,밤당
3122
+ 부탄,가사
3123
+ 부탄,타시강
3124
+ 부탄,펨악체링
3125
+ 부탄,삼드럽종카르
3126
+ 부탄,삼치
3127
+ 부탄,다가나
3128
+ 부탄,셰름강
3129
+ 부탄,탐가
3130
+ 부탄,라야
3131
+ 부탄,루나나
3132
+ 부탄,메랑
3133
+ 부탄,쿠르토에
3134
+ 부탄,탁상
3135
+ 부탄,탕치
3136
+ 부탄,첸데비
3137
+ 부탄,몽가르
3138
+ 부탄,솜드루프
3139
+ 부탄,롱덴카르
3140
+ 부탄,톱카
3141
+ 부탄,우라
3142
+ 부탄,칸가르
3143
+ 부탄,신카르
3144
+ 부탄,자카
3145
+ 부탄,살라카
3146
+ 부탄,바르토
3147
+ 부탄,탄강
3148
+ 부탄,키차
3149
+ 부탄,바케이사
3150
+ 부탄,베베나
3151
+ 부탄,카르브지
3152
+ 부탄,젠카
3153
+ 부탄,트랑카
3154
+ 부탄,롱가르
3155
+ 부탄,리모카
3156
+ 부탄,타르마링
3157
+ 부탄,파링카
3158
+ 부탄,삼페카
3159
+ 부탄,첸토카
3160
+ 부탄,마루
3161
+ 부탄,캄지
3162
+ 부탄,렌덴카
3163
+ 불가리아,소피아
3164
+ 불가리아,플로브디프
3165
+ 불가리아,바르나
3166
+ 불가리아,부르가스
3167
+ 불가리아,루세
3168
+ 불가리아,스타라자가라
3169
+ 불가리아,플레벤
3170
+ 불가리아,슬리벤
3171
+ 불가리아,도브리치
3172
+ 불가리아,시미트리
3173
+ 불가리아,샤멘
3174
+ 불가리아,야보르
3175
+ 불가리아,하스코보
3176
+ 불가리아,파자르지크
3177
+ 불가리아,페르니크
3178
+ 불가리아,시리스키
3179
+ 불가리아,키르지알리
3180
+ 불가리아,라즈그라드
3181
+ 불가리아,실리스트라
3182
+ 불가리아,바이코보
3183
+ 불가리아,벨리코타르노보
3184
+ 불가리아,블라고에브그라드
3185
+ 불가리아,가브로보
3186
+ 불가리아,트르고비슈테
3187
+ 불가리아,모스타르
3188
+ 불가리아,쿠발코보
3189
+ 불가리아,카잔루크
3190
+ 불가리아,몽타나
3191
+ 불가리아,비딘
3192
+ 불가리아,카르조리
3193
+ 불가리아,아센오브그라드
3194
+ 불가리아,모믈로브
3195
+ 불가리아,카르로보
3196
+ 불가리아,노바자고라
3197
+ 불가리아,사모코브
3198
+ 불가리아,슬리브니차
3199
+ 불가리아,스베토브라
3200
+ 불가리아,야물크
3201
+ 불가리아,실바
3202
+ 불가리아,카브리차
3203
+ 불가리아,보리소보
3204
+ 불가리아,엘리노벨
3205
+ 불가리아,루코트
3206
+ 불가리아,트라키아
3207
+ 불가리아,이흐틸로프
3208
+ 사우디아라비아,리야드
3209
+ 사우디아라비아,제다
3210
+ 사우디아라비아,메카
3211
+ 사우디아라비아,메디나
3212
+ 사우디아라비아,담맘
3213
+ 사우디아라비아,코바르
3214
+ 사우디아라비아,타이프
3215
+ 사우디아라비아,타부크
3216
+ 사우디아라비아,부라이다
3217
+ 사우디아라비아,하일
3218
+ 사우디아라비아,알아하사
3219
+ 사우디아라비아,알호프
3220
+ 사우디아라비아,나즈란
3221
+ 사우디아라비아,카심
3222
+ 사우디아라비아,아브하
3223
+ 사우디아라비아,알주바일
3224
+ 사우디아라비아,알카르지
3225
+ 사우디아라비아,알카심
3226
+ 사우디아라비아,알바하
3227
+ 사우디아라비아,알쿠나
3228
+ 사우디아라비아,알마디나
3229
+ 사우디아라비아,알카트리프
3230
+ 사우디아라비아,알하프즈
3231
+ 사우디아라비아,알하프르
3232
+ 사우디아라비아,알무지마
3233
+ 사우디아라비아,알라이프
3234
+ 사우디아라비아,알모자히미야
3235
+ 사우디아라비아,알쿨라
3236
+ 사우디아라비아,알다와드미
3237
+ 사우디아라비아,알라마디
3238
+ 사우디아라비아,알카이사
3239
+ 사우디아라비아,알스미르
3240
+ 사우디아라비아,알와지흐
3241
+ 사우디아라비아,알로하야
3242
+ 사우디아라비아,알로메이야
3243
+ 사우디아라비아,알하루르
3244
+ 사우디아라비아,알타이프
3245
+ 사우디아라비아,알보라이다
3246
+ 사우디아라비아,알하마
3247
+ 사우디아라비아,알시하르
3248
+ 사우디아라비아,알하크라
3249
+ 사우디아라비아,알마자미
3250
+ 사우디아라비아,알하야
3251
+ 사우디아라비아,알타마이르
3252
+ 사우디아라비아,알아부르
3253
+ 사우디아라비아,알카이르
3254
+ 수리남,파라마리보
3255
+ 수리남,니커리
3256
+ 수리남,라트리엘
3257
+ 수리남,와게닝겐
3258
+ 수리남,리디
3259
+ 수리남,코라니
3260
+ 수리남,모엥고
3261
+ 수리남,브로코폰도
3262
+ 수리남,코트리크
3263
+ 수리남,자르조에
3264
+ 수리남,그로테크릭
3265
+ 수리남,다모
3266
+ 수리남,플란타게트
3267
+ 수리남,칼레토
3268
+ 수리남,아포라
3269
+ 수리남,마로에네
3270
+ 수리남,마토리
3271
+ 수리남,마포티
3272
+ 수리남,플로레스
3273
+ 수리남,카수마리
3274
+ 수리남,시파리파
3275
+ 수리남,모피
3276
+ 수리남,무로페
3277
+ 수리남,파타몬카
3278
+ 수리남,카이르바
3279
+ 수리남,자카리
3280
+ 수리남,미시오네스
3281
+ 수리남,빌레그
3282
+ 수리남,니콰리
3283
+ 수리남,콰타리
3284
+ 수리남,수쿠리
3285
+ 수리남,와니카
3286
+ 수리남,키리아바
3287
+ 수리남,마라카스
3288
+ 수리남,라즈베리
3289
+ 수리남,자쿠리
3290
+ 수리남,자콰리
3291
+ 수리남,사라마카
3292
+ 수리남,마로니
3293
+ 수리남,바코보
3294
+ 수리남,카포라
3295
+ 수리남,아카리바
3296
+ 수리남,포쿠사리
3297
+ 수리남,바타비스타
3298
+ 수리남,콜라코
3299
+ 수리남,사프리
3300
+ 아랍에미리트,두바이
3301
+ 아랍에미리트,아부다비
3302
+ 아랍에미리트,샤르자
3303
+ 아랍에미리트,알아인
3304
+ 아랍에미리트,아지만
3305
+ 아랍에미리트,라스알카이마
3306
+ 아랍에미리트,우므알카이와인
3307
+ 아랍에미리트,푸자이라
3308
+ 아랍에미리트,카르바
3309
+ 아랍에미리트,칼바
3310
+ 아랍에미리트,디바
3311
+ 아랍에미리트,쿠와이낫
3312
+ 아랍에미리트,알다이드
3313
+ 아랍에미리트,마드힌
3314
+ 아랍에미리트,자발알리
3315
+ 아랍에미리트,알하무라
3316
+ 아랍에미리트,하타
3317
+ 아랍에미리트,알마자즈
3318
+ 아랍에미리트,알부라이미
3319
+ 아랍에미리트,알리와
3320
+ 아랍에미리트,알자지라
3321
+ 아랍에미리트,알쿠자이
3322
+ 아랍에미리트,알바이야
3323
+ 아랍에미리트,알���르샤
3324
+ 아랍에미리트,무라카밧
3325
+ 아랍에미리트,알나흐다
3326
+ 아랍에미리트,알타와힌
3327
+ 아랍에미리트,메자르
3328
+ 아랍에미리트,알카라마
3329
+ 아랍에미리트,알미르파
3330
+ 아랍에미리트,알마크타
3331
+ 아랍에미리트,알자르
3332
+ 아랍에미리트,알라와
3333
+ 아랍에미리트,알마리나
3334
+ 아랍에미리트,알파라이즈
3335
+ 아랍에미리트,알파크라
3336
+ 아랍에미리트,알시람
3337
+ 아랍에미리트,알카스르
3338
+ 아랍에미리트,알아흐람
3339
+ 아랍에미리트,알카자이
3340
+ 아랍에미리트,알라이야
3341
+ 아랍에미리트,알자파라
3342
+ 아랍에미리트,알미나
3343
+ 아랍에미리트,알와르카
3344
+ 아랍에미리트,알모하라크
3345
+ 아랍에미리트,알슈라파
3346
+ 아랍에미리트,알하이르
3347
+ 아랍에미리트,알하마라
3348
+ 아랍에미리트,알와슬
3349
+ 알제리,알제
3350
+ 알제리,오란
3351
+ 알제리,콘스탄틴
3352
+ 알제리,아나바
3353
+ 알제리,바트나
3354
+ 알제리,블리다
3355
+ 알제리,세티프
3356
+ 알제리,바자아르
3357
+ 알제리,바야드
3358
+ 알제리,스키크다
3359
+ 알제리,바라키
3360
+ 알제리,티지우주
3361
+ 알제리,제젤
3362
+ 알제리,베자이아
3363
+ 알제리,알타르프
3364
+ 알제리,부메르데스
3365
+ 알제리,마스카라
3366
+ 알제리,미리아나
3367
+ 알제리,타르프
3368
+ 알제리,라그와트
3369
+ 알제리,소우카라스
3370
+ 알제리,티미문
3371
+ 알제리,아드라르
3372
+ 알제리,보우이라
3373
+ 알제리,우마부아기
3374
+ 알제리,메데아
3375
+ 알제리,티스미실트
3376
+ 알제리,티아르트
3377
+ 알제리,부이라
3378
+ 알제리,쏘우크아라스
3379
+ 알제리,카프이비스
3380
+ 알제리,카파
3381
+ 알제리,카파리나
3382
+ 알제리,엘바야드
3383
+ 알제리,일리지
3384
+ 알제리,티지우수
3385
+ 알제리,아인데프라
3386
+ 알제리,아인테무차
3387
+ 알제리,아인베니안
3388
+ 알제리,브메르데스
3389
+ 알제리,엘우에드
3390
+ 알제리,엘타르프
3391
+ 알제리,자르자이르
3392
+ 알제리,제르자우
3393
+ 알제리,타만라세트
3394
+ 알제리,나마
3395
+ 에콰도르,키토
3396
+ 에콰도르,과야킬
3397
+ 에콰도르,쿠엥카
3398
+ 에콰도르,암바토
3399
+ 에콰도르,마차라
3400
+ 에콰도르,에스메랄다스
3401
+ 에콰도르,로하
3402
+ 에콰도르,리오밤바
3403
+ 에콰도르,라타쿵가
3404
+ 에콰도르,과라anda
3405
+ 에콰도르,바바호요
3406
+ 에콰도르,푸요
3407
+ 에콰도르,오타발로
3408
+ 에콰도르,이바라
3409
+ 에콰도르,치모르조
3410
+ 에콰도르,투쿤
3411
+ 에콰도르,잠보라
3412
+ 에콰도르,팔렌시아
3413
+ 에콰도르,알라우시
3414
+ 에콰도르,페드레갈레스
3415
+ 에콰도르,카르치
3416
+ 에콰도르,술루아가
3417
+ 에콰도르,파스탈로
3418
+ 에콰도르,아타카메스
3419
+ 에콰도르,테나
3420
+ 에콰도르,플라야스
3421
+ 에콰도르,발디비아
3422
+ 에콰도르,엘로야알파로
3423
+ 에콰도르,푸에르토비에호
3424
+ 에콰도르,플라크리오
3425
+ 에콰도르,보야카
3426
+ 에콰도르,파브로
3427
+ 에콰도르,카를로스
3428
+ 에콰도르,페레스
3429
+ 에콰도르,코야코
3430
+ 에콰도르,피코타
3431
+ 에콰도르,밀라그로
3432
+ 에콰도르,파타테
3433
+ 에콰도르,핀야스
3434
+ 에콰도르,마차치
3435
+ 에콰도르,피사르
3436
+ 에콰도르,자코리
3437
+ 에콰도르,라만타
3438
+ 우루과이,몬테비데오
3439
+ 우루과이,살토
3440
+ 우루과이,시우다델라코스타
3441
+ 우루과이,파이산두
3442
+ 우루과이,라스피에드라스
3443
+ 우루과이,리베라
3444
+ 우루과이,말도나도
3445
+ 우루과이,타쿠아렘보
3446
+ 우루과이,멜로
3447
+ 우루과이,메르세데스
3448
+ 우루과이,아틀란티다
3449
+ 우루과이,두라스노
3450
+ 우루과이,플로리다
3451
+ 우루과이,로차
3452
+ 우루과이,산호세데마요
3453
+ 우루과이,벨라유니온
3454
+ 우루과이,리오브랑코
3455
+ 우루과이,카네로네스
3456
+ 우루과이,프라도
3457
+ 우루과이,콜로니아델사크라멘토
3458
+ 우루과이,파소데로스토로스
3459
+ 우루과이,피리포리스
3460
+ 우루과이,산타루시아
3461
+ 우루과이,차르케아다
3462
+ 우루과이,카라스코
3463
+ 우루과이,소리아노
3464
+ 우루과이,벨라비스타
3465
+ 우루과이,라팔로마
3466
+ 우루과이,콜로니아니콜리치
3467
+ 우루과이,팔마레스
3468
+ 우루과이,산그레고리오데폴라니
3469
+ 우루과이,아르티가스
3470
+ 우루과이,옹브레스
3471
+ 우루과이,팔미라
3472
+ 우루과이,피에드라솔라
3473
+ 우루과이,카라스코노르테
3474
+ 우루과이,마르티네스
3475
+ 우루과이,라테후엘라
3476
+ 우루과이,프라도노르테
3477
+ 우루과이,카우수바
3478
+ 카타르,도하
3479
+ 카타르,알와크라
3480
+ 카타르,알코르
3481
+ 카타르,알라이얀
3482
+ 카타르,움살랄
3483
+ 카타르,마디나트슈말
3484
+ 카타르,루와이스
3485
+ 카타르,두카한
3486
+ 카타르,라스라판
3487
+ 카타르,알가라파
3488
+ 카타르,무사이데
3489
+ 카타르,알타이얀
3490
+ 카타르,알힐랄
3491
+ 카타르,알다얀
3492
+ 카타르,알사드
3493
+ 카타르,알마르카
3494
+ 카타르,알하라
3495
+ 카타르,알마시라
3496
+ 카타르,알카부르
3497
+ 카타르,알자히야
3498
+ 카타르,알자푸르
3499
+ 카타르,알자말
3500
+ 카타르,알자다드
3501
+ 카타르,알타니야
3502
+ 카타르,알마샤프
3503
+ 카타르,알무하라크
3504
+ 카타르,알코트
3505
+ 카타르,알우크라이나
3506
+ 카타르,알니에자
3507
+ 카타르,알카라나
3508
+ 카타르,알오크라
3509
+ 카타르,알마타르
3510
+ 카타르,알와하
3511
+ 카타르,알까라르
3512
+ 카타르,알카사르
3513
+ 카타르,알수하일라
3514
+ 카타르,알주마일라
3515
+ 카타르,알아비야트
3516
+ 카타르,알무사일
3517
+ 카타르,알마드하드
3518
+ 카타르,알사미야
3519
+ 카타르,알구와이야
3520
+ 카타르,알타라푸스
3521
+ 카타르,알마르바
3522
+ 카타르,알파라스
3523
+ 카타르,알가프르
3524
+ 카타르,알카티야
3525
+ 카타르,알나스르
3526
+ 탄자니아,도도마
3527
+ 탄자니아,다르에스살람
3528
+ 탄자니아,아루샤
3529
+ 탄자니아,음베야
3530
+ 탄자니아,모로고로
3531
+ 탄자니아,모시
3532
+ 탄자니아,이링가
3533
+ 탄자니아,샤이냐
3534
+ 탄자니아,스움바완가
3535
+ 탄자니아,톤도마
3536
+ 탄자니아,마완자
3537
+ 탄자니아,무완자
3538
+ 탄자니아,키고마
3539
+ 탄자니아,모로골로
3540
+ 탄자니아,탕가
3541
+ 탄자니아,손게아
3542
+ 탄자니아,싱이다
3543
+ 탄자니아,무롱게
3544
+ 탄자니아,루쿼
3545
+ 탄자니아,마사시
3546
+ 탄자니아,모로곤고
3547
+ 탄자니아,코로고웨
3548
+ 탄자니아,바가모요
3549
+ 탄자니아,모로코
3550
+ 탄자니아,키리야
3551
+ 탄자니아,우카라
3552
+ 탄자니아,마쿰비
3553
+ 탄자니아,키고사
3554
+ 탄자니아,피피자
3555
+ 탄자니아,키세나
3556
+ 탄자니아,움베야
3557
+ 탄자니아,루비
3558
+ 탄자니아,카세레
3559
+ 탄자니아,키베하
3560
+ 탄자니아,음토와라
3561
+ 탄자니아,기토
3562
+ 탄자니아,루룽구
3563
+ 탄자니아,카라투
3564
+ 탄자니아,카호라마
3565
+ 탄자니아,음베야도마
3566
+ 탄자니아,음투쿠라
3567
+ 탄자니아,나침바
3568
+ 탄자니아,키쿰비
3569
+ 탄자니아,마케레
3570
+ 탄자니아,우루리
3571
+ 탄자니아,음판다
3572
+ 탄자니아,코로시
3573
+ 튀르키예,샨리우르파
3574
+ 튀르키예,엘라즈
3575
+ 튀르키예,시바스
3576
+ 튀르키예,테키르다
3577
+ 튀르키예,이즈미트
3578
+ 튀르키예,길레순
3579
+ 튀르키예,르즈
3580
+ 튀르키예,아르트빈
3581
+ 튀르키예,이질
3582
+ 튀르키예,무쉬
3583
+ 튀르키예,시르트
3584
+ 튀르키예,하카리
3585
+ 튀르키예,키리클라레리
3586
+ 튀르키예,바르딘
3587
+ 튀르키예,욜로바
3588
+ 파라과이,아순시온
3589
+ 파라과이,시우다드델에스테
3590
+ 파라과이,엔카르나시온
3591
+ 파라과이,루케
3592
+ 파라과이,람바레
3593
+ 파라과이,페르난데스
3594
+ 파라과이,카피아타
3595
+ 파라과이,피라이
3596
+ 파라과이,비야엘리사
3597
+ 파라과이,마리아노로케알론소
3598
+ 파라과이,프란시아
3599
+ 파라과이,이타과
3600
+ 파라과이,에스테팔마레스
3601
+ 파라과이,아레구아
3602
+ 파라과이,리마다비아
3603
+ 파라과이,아요라
3604
+ 파라과이,코로넬오비에도
3605
+ 파라과이,산페드로
3606
+ 파라과이,엔카르나시오네스
3607
+ 파라과이,빌라헤이즈
3608
+ 파라과이,카르멘델파라나
3609
+ 파라과이,코로넬보가도
3610
+ 파라과이,누에바콜롬비아
3611
+ 파라과이,벤자민아세
3612
+ 파라과이,우루타우
3613
+ 파라과이,아마리야
3614
+ 파라과이,페르난도데라모라
3615
+ 파라과이,이스파노
3616
+ 파라과이,프란시스코카바예로알베르티
3617
+ 파라과이,타바라레
3618
+ 파라과이,카아과수
3619
+ 파라과이,이타푸아
3620
+ 파라과이,비야프란카
3621
+ 파라과이,호세파라과요
3622
+ 파라과이,산이그나시오
3623
+ 파라과이,산후안바우티스타
3624
+ 파라과이,바예미
3625
+ 파라과이,호세파라
3626
+ 파라과이,카피타네미
3627
+ 파라과이,이바구아
3628
+ 파라과이,누에바호헨데
css.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit.components.v1 as components
3
+ import re
4
+ import uuid
5
+ import pandas as pd
6
+ import time
7
+ from zoneinfo import ZoneInfo
8
+ from datetime import datetime # 타임스탬프용
9
+
10
+ # ────────────────── 말풍선 생성 함수
11
+ # 색상 정의
12
+ PRIMARY_USER = "#e2f6e8"
13
+ PRIMARY_BOT = "#f6f6f6"
14
+ #f5f5f5
15
+ # ③ 말풍선 테마 팔레트 & 헬퍼
16
+ THEMES = {
17
+ "피스타치오": {"user": "#C6E0D6", "bot": "#FFFFFF", "accent": "#0B8A5A"},
18
+ "스카이블루": {"user": "#C8D9E6", "bot": "#FFFFFF", "accent": "#5D768B"},
19
+ "크리미오트": {"user": "#E6DAC8", "bot": "#FFFFFF", "accent": "#A48D78"},
20
+ }
21
+ def _get_colors():
22
+ theme = st.session_state.get("bubble_theme", "피스타치오")
23
+ return THEMES.get(theme, THEMES["피스타치오"])
24
+
25
+ def render_message(
26
+ message: str,
27
+ sender: str = "bot",
28
+ chips: list[str] | None = None,
29
+ key: str | None = None,
30
+ *,
31
+ animated: bool = False, # 타자 효과 ON/OFF
32
+ speed_cps: int = 40, # 초당 글자 수
33
+ by_word: bool = False, # 단어 단위 출력
34
+ ) -> str | None:
35
+ import re, time
36
+ # ③ 테마 값 읽기
37
+ palette = _get_colors()
38
+ # show_time = st.session_state.get("show_time", False) and sender == "bot"
39
+ show_time = bool(st.session_state.get("show_time", False))
40
+
41
+ color = palette["user"] if sender == "user" else palette["bot"]
42
+ align = "right" if sender == "user" else "left"
43
+ pad = "10px 14px"
44
+ fsz = "13px"
45
+
46
+ message = str(message).rstrip()
47
+ if show_time:
48
+ try:
49
+ tz = st.session_state.get("tz", "Asia/Seoul") # 기본 KST
50
+ ts_text = datetime.now(ZoneInfo(tz)).strftime("%H:%M")
51
+ except Exception:
52
+ ts_text = datetime.now().strftime("%H:%M")
53
+ else:
54
+ ts_text = "" # ⬅️ 토글 off면 시간 문자열 비우기
55
+
56
+ # 공통 풍선 래퍼
57
+ # ✅ 카톡 스타일: 시간은 말풍선 '밖' (왼쪽: 봇=시각+버블, 오른쪽: 유저=버블+시각)
58
+ def _wrap(html_inner: str, ts_text_local: str = ts_text):
59
+ bubble = (
60
+ f'''<span style="background:{color}; padding:{pad}; border-radius:12px;'''
61
+ f'''display:inline-block; max-width:80%; font-size:{fsz}; line-height:1.45;'''
62
+ f'''word-break:break-word;">{html_inner}</span>'''
63
+ )
64
+
65
+ if ts_text_local:
66
+ if sender == "user":
67
+ # 사용자: 시간(좌) + 버블
68
+ ts = (
69
+ f'''<span style="font-size:11px;color:#888;white-space:nowrap;'''
70
+ f'''align-self:flex-end;margin:0 2px 2px 0;">{ts_text_local}</span>'''
71
+ )
72
+ inner = ts + bubble
73
+ else:
74
+ # 봇: 버블 + 시간(우)
75
+ ts = (
76
+ f'''<span style="font-size:11px;color:#888;white-space:nowrap;'''
77
+ f'''align-self:flex-end;margin:0 0 2px 2px;">{ts_text_local}</span>'''
78
+ )
79
+ inner = bubble + ts
80
+ else:
81
+ inner = bubble
82
+
83
+ row_align = "flex-end" if sender == "user" else "flex-start"
84
+ return (
85
+ f'''<div style="display:flex;align-items:flex-end;justify-content:{row_align};'''
86
+ f'''gap:2px;margin:6px 0;">{inner}</div>'''
87
+ )
88
+
89
+ if not animated:
90
+ st.markdown(_wrap(message), unsafe_allow_html=True)
91
+ else:
92
+ ph = st.empty()
93
+ buf = ""
94
+ segments = re.split(r'(<[^>]+>)', message)
95
+ delay = max(0.005, 1.0 / max(1, speed_cps))
96
+ for seg in segments:
97
+ if not seg:
98
+ continue
99
+ if seg.startswith("<") and seg.endswith(">"):
100
+ buf += seg
101
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
102
+ else:
103
+ if by_word or st.session_state.get("type_by_word", False):
104
+ for w in seg.split(" "):
105
+ buf = (buf + " " + w).strip()
106
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
107
+ time.sleep(delay * 5)
108
+ else:
109
+ for ch in seg:
110
+ buf += ch
111
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
112
+ time.sleep(delay)
113
+
114
+ if chips:
115
+ prefix = f"{key or 'chips'}_{abs(hash(message))}"
116
+ clicked = render_chip_buttons(chips, key_prefix=prefix)
117
+ return clicked
118
+ return None
119
+
120
+ # ────────────────── 칩버튼 생성 함수
121
+ def render_chip_buttons(options, key_prefix="chip", selected_value=None):
122
+ def slugify(text):
123
+ return re.sub(r"[^a-zA-Z0-9]+", "-", str(text)).strip("-").lower() or "empty"
124
+ session_key = f"{key_prefix}_selected"
125
+ selected_value = st.session_state.get(session_key)
126
+
127
+ # 스타일 적용
128
+ st.markdown(f"""
129
+ <style>
130
+ div[data-testid="stHorizontalBlock"]{{
131
+ display:block !important;
132
+ }}
133
+ button[data-testid="stBaseButton-secondary"] {{
134
+ background-color: white;
135
+ border: 1px solid #e3e8e7;
136
+ border-radius: 20px;
137
+ padding: 6px 14px;
138
+ font-size: 14px;
139
+ cursor: pointer;
140
+ transition: 0.2s ease-in-out;
141
+ margin-bottom: -2px;
142
+ width: 230px;
143
+ text-align:center;
144
+ }}
145
+
146
+ button[data-testid="stBaseButton-secondary"]:hover {{
147
+ background-color: #e8f0ef;
148
+ border-color: #009c75;
149
+ color: #009c75;
150
+ }}
151
+ button[data-testid="baseButton-secondary"][disabled]{{
152
+ background-color: white;
153
+ border-color: #009c75; !important;
154
+ color: #009c75; !important;
155
+ }}
156
+ </style>
157
+ """, unsafe_allow_html=True)
158
+
159
+
160
+ clicked_val = None
161
+
162
+ #cols = st.columns(len(options))
163
+ for idx, opt in enumerate(options):
164
+ if opt is None or (isinstance(opt, float) and pd.isna(opt)) or str(opt).strip()=="":
165
+ continue
166
+
167
+ is_selected = (opt == selected_value)
168
+ is_refresh_btn = "다른 여행지 보기" in str(opt)
169
+ disabled = (opt == selected_value) and not is_refresh_btn
170
+
171
+ label = f"{opt}" if is_selected else opt
172
+
173
+ # stable key
174
+ safe_opt = slugify(opt)
175
+ stable_key = f"{key_prefix}_{idx}_{safe_opt}"
176
+
177
+ if st.button(label, key=stable_key, disabled=disabled):
178
+ clicked_val = opt
179
+
180
+ return clicked_val
181
+
182
+
183
+ # ────────────────── 메시지 리플레이 함수
184
+ def replay_log(chat_container=None):
185
+ with chat_container:
186
+ for sender, msg in st.session_state.chat_log:
187
+ render_message(msg, sender=sender)
188
+
189
+
190
+ # ────────────────── 메시지 로깅&생성 함수
191
+ def log_and_render(
192
+ msg,
193
+ sender,
194
+ chat_container=None,
195
+ key=None,
196
+ chips=None,
197
+ *,
198
+ animated: bool | None = None,
199
+ speed_cps: int = 45,
200
+ by_word: bool = False,
201
+ ):
202
+ # 중복 방지
203
+ sent_once = st.session_state.setdefault("sent_once", {})
204
+ if key and sent_once.get(key):
205
+ return
206
+ if key:
207
+ sent_once[key] = True
208
+ if st.session_state.chat_log and st.session_state.chat_log[-1] == (sender, msg):
209
+ return
210
+
211
+ # 로그 저장(리플레이는 정적표시)
212
+ st.session_state.chat_log.append((sender, msg))
213
+
214
+ # 기본 정책: 봇 메시지는 타자 효과, 유저 메시지는 즉시 표시
215
+ if animated is None:
216
+ animated = (sender == "bot") and st.session_state.get("typewriter_on", True)
217
+
218
+ with chat_container:
219
+ return render_message(
220
+ msg,
221
+ sender=sender,
222
+ chips=chips,
223
+ key=key,
224
+ animated=animated,
225
+ speed_cps=speed_cps,
226
+ by_word=by_word,
227
+ )
external_scores.csv ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 여행나라,여행도시,목표날짜,종합점수,트렌드급상승,trend_score,weather_score,cost_score,festival_score,safety_modifier
2
+ 중국,칭다오,2025-06-01,9.317722878625133,,7.338255977496484,8.0,10.0,2,10.0
3
+ 중국,광저우,2025-06-01,9.046974579305406,,6.638537271448665,8.0,10.0,2,10.0
4
+ 베트남,하노이,2025-06-01,8.453777300393842,,7.689873417721519,8.0,10.0,1,10.0
5
+ 캐나다,밴쿠버,2025-06-01,8.393197278911567,,6.962025316455697,10.0,6.0,2,10.0
6
+ 아일랜드,더블린,2025-06-01,8.048299319727892,,5.295358649789029,10.0,7.0,2,10.0
7
+ 베트남,다낭,2025-06-01,8.013677049767276,,7.640646976090014,6.0,10.0,1,10.0
8
+ 핀란드,헬싱키,2025-06-01,7.9693877551020424,급상승,5.09142053445851,10.0,7.0,2,10.0
9
+ 태국,방콕,2025-06-01,7.893949158610813,,4.746835443037974,6.0,10.0,2,10.0
10
+ 캐나다,몬트리올,2025-06-01,7.843537414965986,,5.541490857946555,10.0,6.0,2,10.0
11
+ 캐나다,오타와,2025-06-01,7.7673469387755105,,3.7939521800281293,10.0,8.0,2,10.0
12
+ 호주,멜버른,2025-06-01,7.749659863945579,,5.29887482419128,10.0,6.0,2,10.0
13
+ 태국,푸켓,2025-06-01,7.66265664160401,,6.733473980309423,6.0,10.0,1,10.0
14
+ 이탈리아,밀라노,2025-06-01,7.658539205155748,,6.926863572433193,8.0,5.0,2,10.0
15
+ 오스트리아,비엔나,2025-06-01,7.609559613319012,,6.80028129395218,8.0,5.0,2,10.0
16
+ 일본,도쿄,2025-06-01,7.591192266380238,,5.977496483825597,8.0,6.0,2,10.0
17
+ 이탈리아,베니스,2025-06-01,7.566022198353027,,6.6877637130801695,8.0,5.0,2,10.0
18
+ 일본,오사카,2025-06-01,7.430648048693163,,5.562587904360057,8.0,6.0,2,10.0
19
+ 홍콩,홍콩,2025-06-01,7.413641245972073,,4.743319268635725,8.0,7.0,2,10.0
20
+ 이탈리아,피렌체,2025-06-01,7.374185463659149,,6.19198312236287,8.0,5.0,2,10.0
21
+ 캐나다,토론토,2025-06-01,7.357823129251702,급상승,4.286216596343179,10.0,6.0,2,10.0
22
+ 싱가포르,싱가포르,2025-06-01,7.3299677765843185,,5.3023909985935305,8.0,6.0,2,10.0
23
+ 헝가리,부다페스트,2025-06-01,7.314321518080917,,6.037271448663855,8.0,5.0,2,10.0
24
+ 대만,타이중,2025-06-01,7.28983172216255,,5.457102672292546,8.0,9.0,1,10.0
25
+ 독일,프랑크푸르트,2025-06-01,7.242857142857142,,4.7644163150492265,10.0,5.0,2,10.0
26
+ 말레이시아,쿠알라룸푸르,2025-06-01,7.185682384359947,,2.9163991593727623,6.0,10.0,2,10.0
27
+ 호주,시드니,2025-06-01,7.178231292517008,,3.822081575246132,10.0,6.0,2,10.0
28
+ 일본,교토,2025-06-01,7.134049409237379,,4.79606188466948,8.0,6.0,2,10.0
29
+ 베트남,나트랑,2025-06-01,7.0347296813462235,,6.6068917018284115,8.0,10.0,0,10.0
30
+ 이탈리아,로마,2025-06-01,7.029967776584318,,3.751758087201125,8.0,7.0,2,10.0
31
+ 스웨덴,스톡홀름,2025-06-01,7.029931972789116,,6.5400843881856545,10.0,2.0,2,10.0
32
+ 베트남,호치민,2025-06-01,7.0245614035087725,,5.084388185654008,6.0,10.0,1,10.0
33
+ 스페인,마드리드,2025-06-01,6.9592194772645914,,5.119549929676511,8.0,5.0,2,10.0
34
+ 포르투갈,리스본,2025-06-01,6.8476548514142515,,4.831223628691983,8.0,5.0,2,10.0
35
+ 스위스,취리히,2025-06-01,6.825850340136055,,6.012658227848101,10.0,2.0,2,10.0
36
+ 필리핀,보라카이,2025-06-01,6.802076620121733,,6.005625879043601,8.0,10.0,0,10.0
37
+ 포르투갈,포르토,2025-06-01,6.783709273182959,,4.665963431786216,8.0,5.0,2,10.0
38
+ 필리핀,보홀,2025-06-01,6.684425349087003,,6.789732770745428,6.0,10.0,0,10.0
39
+ 태국,치앙마이,2025-06-01,6.6368063014679555,,6.666666666666666,6.0,10.0,0,10.0
40
+ 영국,런던,2025-06-01,6.55578231292517,,2.9887482419127984,10.0,5.0,2,10.0
41
+ 스페인,바르셀로나,2025-06-01,6.541532402434659,,4.0400843881856545,8.0,5.0,2,10.0
42
+ 몽골,울란바토르,2025-06-01,6.491836734693878,,4.890998593530239,10.0,9.0,0,10.0
43
+ 프랑스,파리,2025-06-01,6.439491586108128,,3.7763713080168775,8.0,5.0,2,10.0
44
+ 독일,뮌헨,2025-06-01,6.434693877551021,,2.6758087201125176,10.0,5.0,2,10.0
45
+ 체코,프라하,2025-06-01,6.237414965986394,,2.1659634317862166,10.0,5.0,2,10.0
46
+ 중국,장가계,2025-06-01,6.226566416040101,,4.518284106891702,8.0,10.0,0,10.0
47
+ 일본,삿포로,2025-06-01,6.216326530612246,,6.50492264416315,10.0,6.0,0,10.0
48
+ 중국,마카오,2025-06-01,6.15717866093806,,4.338959212376934,8.0,10.0,0,10.0
49
+ 미국,올란도,2025-06-01,6.142212674543503,,6.884669479606188,8.0,0.0,2,10.0
50
+ 독일,베를린,2025-06-01,6.106802721088435,,1.828410689170183,10.0,5.0,2,10.0
51
+ 중국,상해,2025-06-01,6.0,,8.01336146272855,0.5,10.0,0,10.0
52
+ 미국,시카고,2025-06-01,5.987755102040817,,5.3973277074542905,10.0,0.0,2,10.0
53
+ 뉴질랜드,오클랜드,2025-06-01,5.960544217687076,급상승,0.6751054852320675,10.0,6.0,2,10.0
54
+ 말레이시아,코타키나발루,2025-06-01,5.814321518080917,,3.452883263009845,8.0,10.0,0,10.0
55
+ 일본,후쿠오카,2025-06-01,5.75717866093806,,6.406469760900141,8.0,6.0,0,10.0
56
+ 필리핀,세부,2025-06-01,5.708915145005371,,4.2686357243319275,6.0,10.0,0,10.0
57
+ 베트남,푸꾸옥,2025-06-01,5.658575008950949,,4.138537271448664,6.0,10.0,0,10.0
58
+ 미국,샌프란시스코,2025-06-01,5.613605442176871,,4.430379746835443,10.0,0.0,2,10.0
59
+ 대만,타이난,2025-06-01,5.566022198353026,,3.586497890295359,8.0,9.0,0,10.0
60
+ 그리스,아테네,2025-06-01,5.511600429645544,,6.547116736990154,8.0,5.0,0,10.0
61
+ 중국,북경,2025-06-01,5.4517006802721095,,6.59634317862166,0.5,10.0,0,10.0
62
+ 튀르키예,이스탄불,2025-06-01,5.221654135338346,,5.748945147679326,8.0,10.0,2,6.0
63
+ 네팔,카트만두,2025-06-01,5.102756892230577,급상승,1.6139240506329113,8.0,10.0,0,10.0
64
+ 호주,브리즈번,2025-06-01,5.102756892230577,,4.715189873417722,8.0,6.0,0,10.0
65
+ 미국,뉴욕,2025-06-01,5.066022198353027,,4.10337552742616,8.0,0.0,2,10.0
66
+ 영국,에든버러,2025-06-01,5.063265306122449,,4.30028129395218,10.0,5.0,0,10.0
67
+ 스위스,제네바,2025-06-01,4.802076620121733,,4.4549929676511955,8.0,2.0,1,10.0
68
+ 스페인,그라나다,2025-06-01,4.691192266380238,,4.426863572433193,8.0,5.0,0,10.0
69
+ 프랑스,베르사유,2025-06-01,4.64421768707483,,3.217299578059072,10.0,5.0,0,10.0
70
+ 미국,로스엔젤레스,2025-06-01,4.599355531686359,,2.8973277074542896,8.0,0.0,2,10.0
71
+ 태국,크라비,2025-06-01,4.578947368421052,급상승,0.26019690576652604,8.0,10.0,0,10.0
72
+ 덴마크,코펜하겐,2025-06-01,4.57265306122449,,2.2749648382559773,10.0,2.0,2,8.5
73
+ 멕시코,멕시코시티,2025-06-01,4.5397243107769425,,4.813642756680731,8.0,0.0,2,8.5
74
+ 브라질,상파울루,2025-06-01,4.119928392409595,,3.5372714486638532,8.0,0.0,2,8.5
75
+ 그리스,산토리니,2025-06-01,4.036770497672753,,2.735583684950773,8.0,5.0,0,10.0
76
+ 뉴질랜드,퀸스타운,2025-06-01,4.002721088435375,,0.7841068917018283,10.0,6.0,0,10.0
77
+ 아이슬란드,레이캬비크,2025-06-01,3.9816326530612245,급상승,1.5049226441631505,10.0,5.0,0,10.0
78
+ 인도,뭄바이,2025-06-01,3.4093125671321163,,5.049226441631505,6.0,0.0,1,8.5
79
+ 멕시코,칸쿤,2025-06-01,3.4087039026136776,,6.543600562587905,8.0,0.0,0,8.5
80
+ 몰디브,말레,2025-06-01,3.399355531686359,,4.964838255977496,8.0,0.0,0,10.0
81
+ 브라질,리우데자네이루,2025-06-01,3.3381596849266026,급상승,1.160337552742616,8.0,0.0,2,8.5
82
+ 미국,호놀룰루,2025-06-01,3.238811313999284,,4.549929676511955,8.0,0.0,0,10.0
83
+ 세르비아,베오그라드,2025-06-01,3.0347296813462235,,1.438115330520394,8.0,0.0,1,10.0
84
+ 스리랑카,콜롬보,2025-06-01,2.8501324740422485,,2.260900140646976,8.0,0.0,1,8.5
85
+ 대만,타이페이,2025-06-01,0.8612280701754387,,6.290436005625879,8.0,9.0,2,1.0
86
+ 괌,괌,2025-06-01,0.2981023988542786,,4.971870604781997,6.0,0.0,0,1.0
87
+ 네덜란드,오슬로,2025-06-01,0.27265306122448985,,5.755977496483825,0.5,2.0,0,1.0
88
+ 벨기에,브뤼쉘,2025-06-01,0.10277876475178417,,2.9163991593727623,0.5,0.0,0,1.0
festivals.csv ADDED
The diff for this file is too large to render. See raw diff
 
gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ trip_emotions.csv filter=lfs diff=lfs merge=lfs -text
packages.csv ADDED
The diff for this file is too large to render. See raw diff
 
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ build-essential
2
+ curl
3
+ git
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cpu
2
+ streamlit
3
+ pandas
4
+ numpy
5
+ scikit-learn
6
+ transformers
7
+ sentence-transformers
8
+ requests
9
+ huggingface_hub>=0.23
10
+ hf_transfer>=0.1.6
11
+ sentencepiece # <- 마지막 열쇠 추가!
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.11
theme_title_phrases.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
trip_emotions.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9a712e15b58c5325874d601f69b7b5e222fc2679fcadca296254ec812bc250bf
3
+ size 279239905
weather.csv ADDED
The diff for this file is too large to render. See raw diff