import pandas as pd import json import os import random from chat_a import ( analyze_emotion, detect_intent, extract_themes, recommend_places_by_theme, detect_location_filter, generate_intro_message, theme_ui_map, ui_to_theme_map, theme_opening_lines, intent_opening_lines, apply_weighted_score_filter, get_highlight_message, get_weather_message, get_intent_intro_message, recommend_packages, handle_selected_place, generate_region_intro, parse_companion_and_age, filter_packages_by_companion_age, make_top2_description_custom, format_summary_tags_custom, make_companion_age_message ) import streamlit as st from streamlit.components.v1 import html from css import render_message, render_chip_buttons, log_and_render, replay_log import streamlit as st, pandas as pd, requests, json st.success("πŸŽ‰ 앱이 μ„±κ³΅μ μœΌλ‘œ μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€! 라이브러리 μ„€μΉ˜ 성곡!") @st.cache_data(show_spinner=False) def load_csv_any(p): return pd.read_csv(p) if str(p).startswith(("http://","https://")) else pd.read_csv(p) # 데이터 λ‘œλ”©μ„ μœ„ν•œ ν•¨μˆ˜ @st.cache_data def load_travel_data(file_path): print(f"Caching {file_path}...") # μΊμ‹œκ°€ μ–Έμ œ μ‹€ν–‰λ˜λŠ”μ§€ ν™•μΈμš© return pd.read_csv(file_path) @st.cache_data def load_json_data(file_path): print(f"Caching {file_path}...") with open(file_path, "r", encoding="utf-8") as f: return json.load(f) # ───────────────────────────────────── 데이터 λ‘œλ“œ # trip_url = st.secrets.get("TRIPDATA_URL") # if not trip_url: # st.error("TRIPDATA_URL λ―Έμ„€μ •: Streamlit Secrets에 URL을 λ„£μ–΄μ£Όμ„Έμš”.") # st.stop() travel_df = load_travel_data("νŠΈλ¦½λ‹·μ»΄_감정_ν…Œλ§ˆ_ν•œμ€„μ„€λͺ…_톡합_07_08.csv") external_score_df = load_travel_data("ν΄λŸ¬μŠ€ν„°_포함_μ™ΈλΆ€μš”μΈ_μ’…ν•©μ μˆ˜_κ²°κ³Ό_μ΅œμ’….csv") festival_df = load_travel_data("μ „μ²˜λ¦¬_ν†΅ν•©μ§€μ—­μΆ•μ œ.csv") weather_df = load_travel_data("μ „μ²˜λ¦¬_날씨_톡합_07_08.csv") package_df = load_travel_data("λͺ¨λ‘νˆ¬μ–΄_μ»¬λŸΌλ³„_개수_07_08.csv") master_df = load_travel_data("λ‚˜λΌ_λ„μ‹œ_리슀트.csv") theme_title_phrases = load_json_data("theme_title_phrases.json") # ───────────────────────────────────── streamlit용 ν•¨μˆ˜ def init_session(): if "chat_log" not in st.session_state: st.session_state.chat_log = [] if "mode" not in st.session_state: st.session_state.mode = None if "user_input" not in st.session_state: st.session_state.user_input = "" if "selected_theme" not in st.session_state: st.session_state.selected_theme = None def make_key(row) -> tuple[str, str]: """prev 에 λ„£κ³  κΊΌλ‚Ό λ•Œ μ“°λŠ” κ³ μœ ν‚€(μ—¬ν–‰μ§€, μ—¬ν–‰λ„μ‹œ)""" return (row["μ—¬ν–‰μ§€"], row["μ—¬ν–‰λ„μ‹œ"]) # ── P κΈ€κΌ΄ 크기 14 px ─────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ───────────────────────────────────── region mode def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, log_and_render): """region λͺ¨λ“œ(νŠΉμ • λ‚˜λΌ, λ„μ‹œλ₯Ό 직접 μ–ΈκΈ‰ν–ˆμ„ 경우) μ „μš© UI & 둜직""" # ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜ region_key = "region_chip_selected" prev_key = "region_prev_recommended" step_key = "region_step" sample_key = "region_sample_df" # ────────────────── 0) μ΄ˆκΈ°ν™” if step_key not in st.session_state: st.session_state[step_key] = "recommend" st.session_state[prev_key] = set() st.session_state.pop(sample_key, None) # ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ if st.session_state[step_key] == "restart": log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="region_restart_intro" ) return # ────────────────── 2) μΆ”μ²œ 단계 if st.session_state[step_key] == "recommend": # 2.1) μΆ”μ²œ 문ꡬ 좜λ ₯ (λ„μ‹œ λ˜λŠ” κ΅­κ°€ κΈ°μ€€) city_exists = bool(city_filter) and city_filter in travel_df["μ—¬ν–‰λ„μ‹œ"].values country_exists = bool(country_filter) and country_filter in travel_df["μ—¬ν–‰λ‚˜λΌ"].values # μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ„μ‹œμΈ 경우 if city_filter and not city_exists: intro = generate_region_intro('', country_filter) log_and_render( f"μ£„μ†‘ν•΄μš”. {city_filter}의 μ—¬ν–‰μ§€λŠ” 아직 λ―Έμ •μ΄μ—μš”.
ν•˜μ§€λ§Œ, {intro}", sender="bot", chat_container=chat_container, key="region_intro_invalid" ) else: # 정상적인 λ„μ‹œ/ꡭ가일 경우 intro = generate_region_intro(city_filter, country_filter) log_and_render(intro, sender="bot", chat_container=chat_container, key="region_intro") # 2.2) μ—¬ν–‰μ§€ 후보 λͺ©λ‘ 필터링 df = travel_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"]) if city_exists: df = df[df["μ—¬ν–‰λ„μ‹œ"].str.contains(city_filter, na=False)] elif country_exists: df = df[df["μ—¬ν–‰λ‚˜λΌ"].str.contains(country_filter, na=False)] # 2.3) 이전 μΆ”μ²œ λͺ©λ‘κ³Ό κ²ΉμΉ˜μ§€ μ•ŠλŠ” μ—¬ν–‰μ§€λ§Œ 남김 prev = st.session_state.setdefault(prev_key, set()) remaining = df[~df.apply(lambda r: make_key(r) in prev, axis=1)] # μΆ”μ²œ κ°€λŠ₯ν•œ μ—¬ν–‰μ§€κ°€ μ—†λ‹€λ©΄ μ’…λ£Œ λ‹¨κ³„λ‘œ μ „ν™˜ if remaining.empty and sample_key not in st.session_state: st.session_state[step_key] = "recommand_end" st.rerun() return # 2.4) μƒ˜ν”Œλ§ (이전 μƒ˜ν”Œμ΄ μ—†κ±°λ‚˜ λΉ„μ–΄ 있으면 μƒˆλ‘œ μΆ”μΆœ) if sample_key not in st.session_state or st.session_state[sample_key].empty: sampled = remaining.sample( n=min(3, len(remaining)), #μ΅œλŒ€ 3개 random_state=random.randint(1, 9999) ) st.session_state[sample_key] = sampled # tuple ν˜•νƒœλ‘œ ν•œκΊΌλ²ˆμ— μΆ”κ°€ prev.update([make_key(r) for _, r in sampled.iterrows()]) st.session_state[prev_key] = prev else: sampled = st.session_state[sample_key] loc_df = st.session_state[sample_key] # 2.5) μΆ”μ²œ 리슀트 좜λ ₯ & μΉ© UI message = ( "πŸ“Œ μΆ”μ²œ μ—¬ν–‰μ§€ λͺ©λ‘
κ°€μž₯ κ°€κ³  싢은 곳을 κ³¨λΌμ£Όμ„Έμš”!

" + "
".join([ f"{i+1}. {row.μ—¬ν–‰μ§€} " f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) " f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}" for i, row in enumerate(loc_df.itertuples()) ]) ) with chat_container: log_and_render(message, sender="bot", chat_container=chat_container, key=f"region_recommendation_{random.randint(1,999999)}" ) # μΉ© λ²„νŠΌμœΌλ‘œ μΆ”μ²œμ§€ 쀑 선택받기 prev_choice = st.session_state.get(region_key, None) choice = render_chip_buttons( loc_df["μ—¬ν–‰μ§€"].tolist() + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"], key_prefix="region_chip", selected_value=prev_choice ) # 2.7) 선택 κ²°κ³Ό 처리 if not choice or choice == prev_choice: return if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„": log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„", sender="user", chat_container=chat_container, key=f"user_place_refresh_{random.randint(1,999999)}") st.session_state.pop(sample_key, None) st.rerun() return # 2.8) μ—¬ν–‰μ§€ 선택 μ™„λ£Œ st.session_state[region_key] = choice st.session_state[step_key] = "detail" st.session_state.chat_log.append(("user", choice)) # μ‹€μ œλ‘œ μ„ νƒλœ μ—¬ν–‰μ§€λ§Œ prev에 기둝 match = sampled[sampled["μ—¬ν–‰μ§€"] == choice] if not match.empty: prev.add(make_key(match.iloc[0])) st.session_state[prev_key] = prev # μƒ˜ν”Œ 폐기 st.session_state.pop(sample_key, None) st.rerun() return # ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계: 더 이상 μΆ”μ²œν•  μ—¬ν–‰μ§€κ°€ 없을 λ•Œ elif st.session_state[step_key] == "recommand_end": with chat_container: # 3.1) λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.
λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?", sender="bot", chat_container=chat_container, key="region_empty" ) # 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯ restart_done_key = "region_restart_done" chip_ph = st.empty() if not st.session_state.get(restart_done_key, False): with chip_ph: choice = render_chip_buttons( ["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"], key_prefix="region_restart" ) else: choice = None # 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우 if choice is None: return chip_ph.empty() st.session_state[restart_done_key] = True # 3.4) μ‚¬μš©μž 선택값 좜λ ₯ log_and_render( choice, sender="user", chat_container=chat_container, key=f"user_restart_choice_{choice}" ) # 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우 if choice == "예 πŸ”„": # μ—¬ν–‰ μΆ”μ²œ μƒνƒœ μ΄ˆκΈ°ν™” for k in [region_key, prev_key, sample_key, restart_done_key]: st.session_state.pop(k, None) chip_ph.empty() # λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™” st.session_state["user_input_rendered"] = False st.session_state["region_step"] = "restart" log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="region_restart_intro" ) return # 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우 else: log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="region_exit") st.stop() return # ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계 if st.session_state[step_key] == "detail": chosen = st.session_state[region_key] # city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯ row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0] st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"] st.session_state["selected_place"] = chosen log_and_render(chosen, sender="user", chat_container=chat_container, key=f"user_place_{chosen}") handle_selected_place( chosen, travel_df, external_score_df, festival_df, weather_df, chat_container=chat_container ) st.session_state[step_key] = "companion" st.rerun() return # ────────────────── 5) 동행·연령 λ°›κΈ° 단계 elif st.session_state[step_key] == "companion": with chat_container: # 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!
" "1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)
" "2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)", sender="bot", chat_container=chat_container, key="ask_companion_age" ) # 5.1.1) 동행 μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸ‘« 동행 선택
', unsafe_allow_html=True ) c_cols = st.columns(5) comp_flags = { "혼자": c_cols[0].checkbox("혼자"), "친ꡬ": c_cols[1].checkbox("친ꡬ"), "μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"), "κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"), "단체": c_cols[4].checkbox("단체"), } companions = [k for k, v in comp_flags.items() if v] # 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸŽ‚ μ—°λ Ή 선택
', unsafe_allow_html=True ) a_cols = st.columns(5) age_flags = { "20λŒ€": a_cols[0].checkbox("20λŒ€"), "30λŒ€": a_cols[1].checkbox("30λŒ€"), "40λŒ€": a_cols[2].checkbox("40λŒ€"), "50λŒ€": a_cols[3].checkbox("50λŒ€"), "60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"), } age_group = [k for k, v in age_flags.items() if v] # 5.1.3) 확인 λ²„νŠΌ confirm = st.button( "μΆ”μ²œ λ°›κΈ°", key="btn_confirm_companion", disabled=not (companions or age_group), ) # 5.2) λ©”μ‹œμ§€ 좜λ ₯ if confirm: # μ‚¬μš©μž 버블 좜λ ₯ user_msg = " / ".join(companions + age_group) log_and_render( user_msg if user_msg else "선택 μ•ˆ 함", sender="user", chat_container=chat_container, key=f"user_comp_age_{random.randint(1,999999)}" ) # μ„Έμ…˜ μ €μž₯ st.session_state["companions"] = companions or None st.session_state["age_group"] = age_group or None # λ‹€μŒ μŠ€ν… st.session_state[step_key] = "package" st.rerun() return # ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계 elif st.session_state[step_key] == "package": # νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€ if st.session_state.get("package_rendered", False): st.session_state[step_key] = "package_end" return companions = st.session_state.get("companions") age_group = st.session_state.get("age_group") city = st.session_state.get("selected_city") place = st.session_state.get("selected_place") filtered = filter_packages_by_companion_age( package_df, companions, age_group, city=city, top_n=2 ) if filtered.empty: log_and_render( "⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.
" "λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?", sender="bot", chat_container=chat_container, key="no_package" ) st.session_state[step_key] = "companion" # λ‹€μ‹œ μž…λ ₯ λ‹¨κ³„λ‘œ st.rerun() return combo_msg = make_companion_age_message(companions, age_group) header = f"{combo_msg}" # νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯ used_phrases = set() theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place] raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"]) sampled_titles = random.sample(title_candidates, k=min(2, len(title_candidates))) # λ©”μ‹œμ§€ 생성 pkg_msgs = [header] for i, (_, row) in enumerate(filtered.iterrows(), 1): desc, used_phrases = make_top2_description_custom( row.to_dict(), used_phrases ) tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"]) title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) else random.choice(title_candidates)) title = f"{city} {title_phrase} νŒ¨ν‚€μ§€" url = row.URL pkg_msgs.append( f"{i}. {title}
" f"πŸ…Ό {desc}
{tags}
" f'' 'πŸ’š λ°”λ‘œκ°€κΈ° β†—' ) # λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "

".join(pkg_msgs), sender="bot", chat_container=chat_container, key=f"pkg_bundle_{random.randint(1,999999)}" ) # μ„Έμ…˜ 정리 st.session_state["package_rendered"] = True st.session_state[step_key] = "package_end" return # ────────────────── 7) μ’…λ£Œ 단계 elif st.session_state[step_key] == "package_end": log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="goodbye") # ───────────────────────────────────── intent λͺ¨λ“œ def intent_ui(travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, intent, log_and_render): """intent(μ˜λ„λ₯Ό μž…λ ₯ν–ˆμ„ 경우) λͺ¨λ“œ μ „μš© UI & 둜직""" # ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜ sample_key = "intent_sample_df" step_key = "intent_step" prev_key = "intent_prev_places" intent_key = "intent_chip_selected" # ────────────────── 0) μ΄ˆκΈ°ν™” if step_key not in st.session_state: st.session_state[step_key] = "recommend_places" st.session_state[prev_key] = set() st.session_state.pop(sample_key, None) # ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ if st.session_state[step_key] == "restart": log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="region_restart_intro" ) return # ────────────────── 2) μ—¬ν–‰μ§€ μΆ”μ²œ 단계 if st.session_state[step_key] == "recommend_places": selected_theme = intent theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter) theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰λ„μ‹œ"]) theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"]) # 2.1) 이전 μΆ”μ²œ 기둝 μ„ΈνŒ… prev = st.session_state.setdefault(prev_key, set()) # 2.2) 이미 μƒ˜ν”Œμ΄ μžˆλ‹€λ©΄ result_df μž¬μ‚¬μš© if sample_key in st.session_state and not st.session_state[sample_key].empty: result_df = st.session_state[sample_key] else: # 2.3) μƒˆλ‘œμš΄ μΆ”μ²œ λŒ€μƒ 필터링 candidates = theme_df[~theme_df["μ—¬ν–‰μ§€"].isin(prev)] # 2.4) 후보가 μ—†λ‹€λ©΄ μ’…λ£Œ if candidates.empty: st.session_state[step_key] = "recommend_places_end" st.rerun() return # 2.5) μƒˆλ‘œμš΄ μΆ”μ²œ μΆ”μΆœ 및 μ €μž₯ result_df = apply_weighted_score_filter(candidates) st.session_state[sample_key] = result_df # prev에 λ“±λ‘ν•˜μ—¬ 쀑볡 μΆ”μ²œ λ°©μ§€ prev.update(result_df["μ—¬ν–‰μ§€"]) st.session_state[prev_key] = prev # 2.6) μ˜€ν”„λ‹ λ¬Έμž₯ 생성 opening_line = intent_opening_lines.get(selected_theme, f"'{selected_theme}' μ—¬ν–‰μ§€λ₯Ό μ†Œκ°œν• κ²Œμš”.") opening_line = opening_line.format(len(result_df)) # 2.7) μΆ”μ²œ λ©”μ‹œμ§€ ꡬ성 message = "
".join([ f"{i+1}. {row.μ—¬ν–‰μ§€} " f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) " f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}" for i, row in enumerate(result_df.itertuples()) ]) # 2.8) 챗봇 좜λ ₯ + μΉ© λ²„νŠΌ λ Œλ”λ§ with chat_container: log_and_render(f"{opening_line}

{message}", sender="bot", chat_container=chat_container, key=f"intent_recommendation_{random.randint(1,999999)}") recommend_names = result_df["μ—¬ν–‰μ§€"].tolist() prev_choice = st.session_state.get(intent_key, None) choice = render_chip_buttons( recommend_names + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"], key_prefix="intent_chip", selected_value=prev_choice ) # 2.9) 선택 μ—†κ±°λ‚˜ 쀑볡 선택이면 λŒ€κΈ° if not choice or choice == prev_choice: return # 선택 κ²°κ³Ό 처리 if choice: if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„": log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„", sender="user", chat_container=chat_container, key=f"user_place_refresh_{random.randint(1,999999)}") st.session_state.pop(sample_key, None) st.rerun() return # 정상 μ„ νƒλœ 경우 st.session_state[intent_key] = choice st.session_state[step_key] = "detail" st.session_state.chat_log.append(("user", choice)) # μ‹€μ œλ‘œ μ„ νƒλœ μ—¬ν–‰μ§€λ§Œ prev에 기둝 match = result_df[result_df["μ—¬ν–‰μ§€"] == choice] if not match.empty: prev.add(choice) st.session_state[prev_key] = prev # μƒ˜ν”Œ 폐기 st.session_state.pop(sample_key, None) st.rerun() return # ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계 elif st.session_state[step_key] == "recommend_places_end": # 3.1) λ©”μ‹œμ§€ 좜λ ₯ with chat_container: log_and_render( "⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.
λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?", sender="bot", chat_container=chat_container, key="intent_empty" ) # 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯ restart_done_key = "intent_restart_done" chip_ph = st.empty() if not st.session_state.get(restart_done_key, False): with chip_ph: choice = render_chip_buttons( ["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"], key_prefix="intent_restart") else: choice = None # 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우 if choice is None: return chip_ph.empty() st.session_state[restart_done_key] = True # 3.4) μ‚¬μš©μž 선택값 좜λ ₯ log_and_render(choice, sender="user", chat_container=chat_container ) # 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우 if choice == "예 πŸ”„": for k in [sample_key, prev_key, intent_key, restart_done_key]: st.session_state.pop(k, None) chip_ph.empty() # λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™” st.session_state["user_input_rendered"] = False st.session_state["intent_step"] = "restart" log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="intent_restart_intro" ) return # 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우 else: log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="intent_exit") st.stop() return # ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계 if st.session_state[step_key] == "detail": chosen = st.session_state[intent_key] # city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯ row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0] st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"] st.session_state["selected_place"] = chosen log_and_render(chosen, sender="user", chat_container=chat_container, key=f"user_place_{chosen}") handle_selected_place( chosen, travel_df, external_score_df, festival_df, weather_df, chat_container=chat_container ) st.session_state[step_key] = "companion" st.rerun() return # ────────────────── 5) 동행·연령 λ°›κΈ° 단계 elif st.session_state[step_key] == "companion": with chat_container: # 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!
" "1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)
" "2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)", sender="bot", chat_container=chat_container, key="ask_companion_age" ) # 5.1.1) 동행 μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸ‘« 동행 선택
', unsafe_allow_html=True ) c_cols = st.columns(5) comp_flags = { "혼자": c_cols[0].checkbox("혼자"), "친ꡬ": c_cols[1].checkbox("친ꡬ"), "μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"), "κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"), "단체": c_cols[4].checkbox("단체"), } companions = [k for k, v in comp_flags.items() if v] # 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸŽ‚ μ—°λ Ή 선택
', unsafe_allow_html=True ) a_cols = st.columns(5) age_flags = { "20λŒ€": a_cols[0].checkbox("20λŒ€"), "30λŒ€": a_cols[1].checkbox("30λŒ€"), "40λŒ€": a_cols[2].checkbox("40λŒ€"), "50λŒ€": a_cols[3].checkbox("50λŒ€"), "60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"), } age_group = [k for k, v in age_flags.items() if v] # 5.1.3) 확인 λ²„νŠΌ confirm = st.button( "μΆ”μ²œ λ°›κΈ°", key="btn_confirm_companion", disabled=not (companions or age_group), ) # 5.2) λ©”μ‹œμ§€ 좜λ ₯ if confirm: # μ‚¬μš©μž 버블 좜λ ₯ user_msg = " / ".join(companions + age_group) log_and_render( user_msg if user_msg else "선택 μ•ˆ 함", sender="user", chat_container=chat_container, key=f"user_comp_age_{random.randint(1,999999)}" ) # μ„Έμ…˜ μ €μž₯ st.session_state["companions"] = companions or None st.session_state["age_group"] = age_group or None # λ‹€μŒ μŠ€ν… st.session_state[step_key] = "package" st.rerun() return # ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계 elif st.session_state[step_key] == "package": # νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€ if st.session_state.get("package_rendered", False): st.session_state[step_key] = "package_end" return companions = st.session_state.get("companions") age_group = st.session_state.get("age_group") city = st.session_state.get("selected_city") place = st.session_state.get("selected_place") filtered = filter_packages_by_companion_age( package_df, companions, age_group, city=city, top_n=2 ) if filtered.empty: log_and_render( "⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.
" "λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?", sender="bot", chat_container=chat_container, key="no_package" ) st.session_state[step_key] = "companion" # λ‹€μ‹œ μž…λ ₯ λ‹¨κ³„λ‘œ st.rerun() return combo_msg = make_companion_age_message(companions, age_group) header = f"{combo_msg}" # νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯ used_phrases = set() theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place] raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"]) sampled_titles = random.sample(title_candidates, k=min(2, len(title_candidates))) # λ©”μ‹œμ§€ 생성 pkg_msgs = [header] for i, (_, row) in enumerate(filtered.iterrows(), 1): desc, used_phrases = make_top2_description_custom( row.to_dict(), used_phrases ) tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"]) title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) else random.choice(title_candidates)) title = f"{city} {title_phrase} νŒ¨ν‚€μ§€" url = row.URL pkg_msgs.append( f"{i}. {title}
" f"πŸ…Ό {desc}
{tags}
" f'' 'πŸ’š λ°”λ‘œκ°€κΈ° β†—' ) # λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "

".join(pkg_msgs), sender="bot", chat_container=chat_container, key=f"pkg_bundle_{random.randint(1,999999)}" ) # μ„Έμ…˜ 정리 st.session_state["package_rendered"] = True st.session_state[step_key] = "package_end" return # ────────────────── 7) μ’…λ£Œ 단계 elif st.session_state[step_key] == "package_end": log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="goodbye") # ───────────────────────────────────── emotion λͺ¨λ“œ def emotion_ui(travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, candidate_themes, intent, emotion_groups, top_emotions, log_and_render): """emotion(감정을 μž…λ ₯ν–ˆμ„ 경우) λͺ¨λ“œ μ „μš© UI & 둜직""" # ────────────────── μ„Έμ…˜ ν‚€ μ •μ˜ sample_key = "emotion_sample_df" step_key = "emotion_step" theme_key = "selected_theme" emotion_key = "emotion_chip_selected" prev_key = "emotion_prev_places" # ────────────────── 0) μ΄ˆκΈ°ν™” if step_key not in st.session_state: st.session_state[step_key] = "theme_selection" st.session_state[prev_key] = set() st.session_state.pop(sample_key, None) # ────────────────── 1) restart μƒνƒœλ©΄ 인트둜만 좜λ ₯ν•˜κ³  μ’…λ£Œ if st.session_state[step_key] == "restart": log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="region_restart_intro" ) return # ────────────────── 2) ν…Œλ§ˆ μΆ”μ²œ 단계 if st.session_state[step_key] == "theme_selection": # μΆ”μ²œ ν…Œλ§ˆ 1개일 경우 if len(candidate_themes) == 1: selected_theme = candidate_themes[0] st.session_state[theme_key] = selected_theme log_and_render(f"μΆ”μ²œ κ°€λŠ₯ν•œ ν…Œλ§ˆκ°€ 1κ°œμ΄λ―€λ‘œ '{selected_theme}'을 μ„ νƒν• κ²Œμš”.", sender="bot", chat_container=chat_container) st.session_state[step_key] = "recommend_places" st.rerun() # ν…Œλ§ˆκ°€ μ—¬λŸ¬ 개일 경우 else: # 인트둜 λ©”μ‹œμ§€ intro_msg = generate_intro_message(intent=intent, emotion_groups=emotion_groups, emotion_scores=top_emotions) log_and_render(f"{intro_msg}
μ•„λž˜ 쀑 마음이 λŒλ¦¬λŠ” μ—¬ν–‰ μŠ€νƒ€μΌμ„ κ³¨λΌμ£Όμ„Έμš” πŸ’«", sender="bot", chat_container=chat_container) # 후보 ν…Œλ§ˆ μ€€λΉ„ dfs = [recommend_places_by_theme(t, country_filter, city_filter) for t in candidate_themes] dfs = [df for df in dfs if not df.empty] all_theme_df = pd.concat(dfs) if dfs else pd.DataFrame(columns=travel_df.columns) all_theme_df = all_theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"]) all_theme_names = all_theme_df["ν†΅ν•©ν…Œλ§ˆλͺ…"].dropna().tolist() available_themes = [] for t in candidate_themes: if t in all_theme_names and t not in available_themes: available_themes.append(t) for t in all_theme_names: if t not in available_themes: available_themes.append(t) available_themes = available_themes[:3] # μ΅œλŒ€ 3개 # μΉ© UI 좜λ ₯ with chat_container: chip = render_chip_buttons( [theme_ui_map.get(t, (t, ""))[0] for t in available_themes], key_prefix="theme_chip" ) # 선택이 μ™„λ£Œλ˜λ©΄ λ‹€μŒ λ‹¨κ³„λ‘œ 이동 if chip: selected_theme = ui_to_theme_map.get(chip, chip) st.session_state[theme_key] = selected_theme st.session_state[step_key] = "recommend_places" st.session_state["emotion_all_theme_df"] = all_theme_df log_and_render(f"{chip}", sender="user", chat_container=chat_container) st.rerun() # ────────────────── 3) μ—¬ν–‰μ§€ μΆ”μ²œ 단계 if st.session_state[step_key] == "recommend_places": all_theme_df = st.session_state.get("emotion_all_theme_df", pd.DataFrame()) selected_theme = st.session_state.get(theme_key, "") prev_key = "emotion_prev_places" prev = st.session_state.setdefault(prev_key, set()) # μ˜ˆμ™Έ 처리: 데이터 없을 경우 if all_theme_df.empty or not selected_theme: log_and_render("μΆ”μ²œ 데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 데 λ¬Έμ œκ°€ λ°œμƒν–ˆμ–΄μš”.
λ‹€μ‹œ μž…λ ₯ν•΄ μ£Όμ„Έμš”.", sender="bot", chat_container=chat_container) return if sample_key not in st.session_state: theme_df = all_theme_df[all_theme_df["ν†΅ν•©ν…Œλ§ˆλͺ…"] == selected_theme] theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰λ„μ‹œ"]) theme_df = theme_df.drop_duplicates(subset=["μ—¬ν–‰μ§€"]) remaining = theme_df[~theme_df["μ—¬ν–‰μ§€"].isin(prev)] if remaining.empty: st.session_state[step_key] = "recommend_places_end" st.rerun() return result_df = apply_weighted_score_filter(remaining) st.session_state[sample_key] = result_df else: result_df = st.session_state[sample_key] # μΆ”μ²œ 수 λΆ€μ‘±ν•  경우 Fallback 보완 if len(result_df) < 3: fallback = travel_df[ (travel_df["ν†΅ν•©ν…Œλ§ˆλͺ…"] == selected_theme) & (~travel_df["μ—¬ν–‰μ§€"].isin(result_df["μ—¬ν–‰μ§€"])) ].drop_duplicates(subset=["μ—¬ν–‰μ§€"]) if not fallback.empty: fill_count = min(3 - len(result_df), len(fallback)) fill = fallback.sample(n=fill_count, random_state=random.randint(1, 9999)) result_df = pd.concat([result_df, fill], ignore_index=True) # μƒ˜ν”Œ μ €μž₯ st.session_state[sample_key] = result_df # 2.1)첫 λ¬Έμž₯ 좜λ ₯ ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0] opening_line_template = theme_opening_lines.get(ui_name) opening_line = opening_line_template.format(len(result_df)) if opening_line_template else "" message = ( "
".join([ f"{i+1}. {row.μ—¬ν–‰μ§€} " f"({row.μ—¬ν–‰λ‚˜λΌ}, {row.μ—¬ν–‰λ„μ‹œ}) " f"{getattr(row, 'ν•œμ€„μ„€λͺ…', 'μ„€λͺ…이 μ—†μŠ΅λ‹ˆλ‹€')}" for i, row in enumerate(result_df.itertuples()) ]) ) if opening_line_template: message_combined = f"{opening_line}

{message}" with chat_container: log_and_render(message_combined, sender="bot", chat_container=chat_container, key=f"emotion_recommendation_{random.randint(1,999999)}" ) # 2.2) μΉ© λ²„νŠΌμœΌλ‘œ μΆ”μ²œμ§€ 쀑 선택받기 recommend_names = result_df["μ—¬ν–‰μ§€"].tolist() prev_choice = st.session_state.get(emotion_key, None) choice = render_chip_buttons( recommend_names + ["λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„"], key_prefix="emotion_chip", selected_value=prev_choice ) # 2.3) 선택 κ²°κ³Ό 처리 if not choice or choice == prev_choice: return if choice == "λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„": log_and_render("λ‹€λ₯Έ μ—¬ν–‰μ§€ 보기 πŸ”„", sender="user", chat_container=chat_container, key=f"user_place_refresh_{random.randint(1,999999)}") st.session_state.pop(sample_key, None) st.rerun() return # μ‹€μ œ μ„ νƒν•œ μ—¬ν–‰μ§€ 처리 st.session_state[emotion_key] = choice st.session_state[step_key] = "detail" st.session_state.chat_log.append(("user", choice)) # μ„ νƒν•œ μ—¬ν–‰μ§€λ₯Ό prev 기둝에 μΆ”κ°€ match = result_df[result_df["μ—¬ν–‰μ§€"] == choice] if not match.empty: prev.add(choice) st.session_state[prev_key] = prev # μƒ˜ν”Œ 폐기 st.session_state.pop(sample_key, None) st.rerun() return # ────────────────── 3) μΆ”μ²œ μ’…λ£Œ 단계: 더 이상 μΆ”μ²œν•  μ—¬ν–‰μ§€κ°€ 없을 λ•Œ elif st.session_state[step_key] == "recommend_place_end": with chat_container: # 3.1) λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "⚠️ 더 이상 μƒˆλ‘œμš΄ μ—¬ν–‰μ§€κ°€ μ—†μ–΄μš”.
λ‹€μ‹œ μ§ˆλ¬Έν•˜μ‹œκ² μ–΄μš”?", sender="bot", chat_container=chat_container, key="emotion_empty" ) # 3.2) μž¬μ‹œμž‘ μ—¬λΆ€ μΉ© λ²„νŠΌ 좜λ ₯ restart_done_key = "emotion_restart_done" chip_ph = st.empty() if not st.session_state.get(restart_done_key, False): with chip_ph: choice = render_chip_buttons( ["예 πŸ”„", "μ•„λ‹ˆμ˜€ ❌"], key_prefix="emotion_restart" ) else: choice = None # 3.3) 아직 아무것도 μ„ νƒν•˜μ§€ μ•Šμ€ 경우 if choice is None: return chip_ph.empty() st.session_state[restart_done_key] = True # 3.4) μ‚¬μš©μž 선택값 좜λ ₯ log_and_render( choice, sender="user", chat_container=chat_container, key=f"user_restart_choice_{choice}" ) # 3.5) μ‚¬μš©μžκ°€ μž¬μΆ”μ²œμ„ μ›ν•˜λŠ” 경우 if choice == "예 πŸ”„": # μ—¬ν–‰ μΆ”μ²œ μƒνƒœ μ΄ˆκΈ°ν™” for k in [emotion_key, prev_key, sample_key, restart_done_key]: st.session_state.pop(k, None) chip_ph.empty() # λ‹€μŒ μΆ”μ²œ λ‹¨κ³„λ‘œ μ΄ˆκΈ°ν™” st.session_state["user_input_rendered"] = False st.session_state["emotion_step"] = "restart" log_and_render( "λ‹€μ‹œ μ—¬ν–‰μ§€λ₯Ό μΆ”μ²œν•΄λ“œλ¦΄κ²Œμš”!
μš”μ¦˜ λ– μ˜€λ₯΄λŠ” 여행이 μžˆμœΌμ‹ κ°€μš”?", sender="bot", chat_container=chat_container, key="emotion_restart_intro" ) return # 3.6) μ‚¬μš©μžκ°€ μ’…λ£Œλ₯Ό μ„ νƒν•œ 경우 else: log_and_render("μ—¬ν–‰ μΆ”μ²œμ„ μ’…λ£Œν• κ²Œμš”. ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="emotion_exit") st.stop() return # ────────────────── 4) μ—¬ν–‰μ§€ 상세 단계 if st.session_state[step_key] == "detail": chosen = st.session_state[emotion_key] # city 이름 λ½‘μ•„μ„œ μ„Έμ…˜μ— μ €μž₯ row = travel_df[travel_df["μ—¬ν–‰μ§€"] == chosen].iloc[0] st.session_state["selected_city"] = row["μ—¬ν–‰λ„μ‹œ"] st.session_state["selected_place"] = chosen log_and_render(chosen, sender="user", chat_container=chat_container, key=f"user_place_{chosen}") handle_selected_place( chosen, travel_df, external_score_df, festival_df, weather_df, chat_container=chat_container ) st.session_state[step_key] = "companion" st.rerun() return # ────────────────── 5) 동행·연령 λ°›κΈ° 단계 elif st.session_state[step_key] == "companion": with chat_container: # 5.1) μ•ˆλ‚΄ λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "ν•¨κ»˜ κ°€λŠ” λΆ„μ΄λ‚˜ μ—°λ ΉλŒ€λ₯Ό μ•Œλ €μ£Όμ‹œλ©΄ 더 λ”± λ§žλŠ” μƒν’ˆμ„ κ³¨λΌλ“œλ¦΄κ²Œμš”!
" "1️⃣ 동행 μ—¬λΆ€ (혼자 / 친ꡬ / μ»€ν”Œ / κ°€μ‘± / 단체)
" "2️⃣ μ—°λ ΉλŒ€ (20λŒ€ / 30λŒ€ / 40λŒ€ / 50λŒ€ / 60λŒ€ 이상)", sender="bot", chat_container=chat_container, key="ask_companion_age" ) # 5.1.1) 동행 μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸ‘« 동행 선택
', unsafe_allow_html=True ) c_cols = st.columns(5) comp_flags = { "혼자": c_cols[0].checkbox("혼자"), "친ꡬ": c_cols[1].checkbox("친ꡬ"), "μ»€ν”Œ": c_cols[2].checkbox("μ»€ν”Œ"), "κ°€μ‘±": c_cols[3].checkbox("κ°€μ‘±"), "단체": c_cols[4].checkbox("단체"), } companions = [k for k, v in comp_flags.items() if v] # 5.1.2) μ—°λ Ή μ²΄ν¬λ°•μŠ€ st.markdown( '
πŸŽ‚ μ—°λ Ή 선택
', unsafe_allow_html=True ) a_cols = st.columns(5) age_flags = { "20λŒ€": a_cols[0].checkbox("20λŒ€"), "30λŒ€": a_cols[1].checkbox("30λŒ€"), "40λŒ€": a_cols[2].checkbox("40λŒ€"), "50λŒ€": a_cols[3].checkbox("50λŒ€"), "60λŒ€ 이상": a_cols[4].checkbox("60λŒ€ 이상"), } age_group = [k for k, v in age_flags.items() if v] # 5.1.3) 확인 λ²„νŠΌ confirm = st.button( "μΆ”μ²œ λ°›κΈ°", key="btn_confirm_companion", disabled=not (companions or age_group), ) # 5.2) λ©”μ‹œμ§€ 좜λ ₯ if confirm: # μ‚¬μš©μž 버블 좜λ ₯ user_msg = " / ".join(companions + age_group) log_and_render( user_msg if user_msg else "선택 μ•ˆ 함", sender="user", chat_container=chat_container, key=f"user_comp_age_{random.randint(1,999999)}" ) # μ„Έμ…˜ μ €μž₯ st.session_state["companions"] = companions or None st.session_state["age_group"] = age_group or None # λ‹€μŒ μŠ€ν… st.session_state[step_key] = "package" st.rerun() return # ────────────────── 6) 동행·연령 필터링· νŒ¨ν‚€μ§€ 좜λ ₯ 단계 elif st.session_state[step_key] == "package": # νŒ¨ν‚€μ§€ 버블을 이미 λ§Œλ“€μ—ˆμœΌλ©΄ κ±΄λ„ˆλœ€ if st.session_state.get("package_rendered", False): st.session_state[step_key] = "package_end" return companions = st.session_state.get("companions") age_group = st.session_state.get("age_group") city = st.session_state.get("selected_city") place = st.session_state.get("selected_place") filtered = filter_packages_by_companion_age( package_df, companions, age_group, city=city, top_n=2 ) if filtered.empty: log_and_render( "⚠️ μ•„μ‰½μ§€λ§Œ μ§€κΈˆ 쑰건에 λ§žλŠ” νŒ¨ν‚€μ§€κ°€ μ—†μ–΄μš”.
" "λ‹€λ₯Έ 쑰건으둜 λ‹€μ‹œ μ°Ύμ•„λ³ΌκΉŒμš”?", sender="bot", chat_container=chat_container, key="no_package" ) st.session_state[step_key] = "companion" st.rerun() return combo_msg = make_companion_age_message(companions, age_group) header = f"{combo_msg}" # νŒ¨ν‚€μ§€ μΉ΄λ“œ 좜λ ₯ used_phrases = set() theme_row = travel_df[travel_df["μ—¬ν–‰μ§€"] == place] raw_theme = theme_row["ν†΅ν•©ν…Œλ§ˆλͺ…"].iloc[0] if not theme_row.empty else None selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆ”μ²œ"]) sampled_titles = random.sample(title_candidates, k=min(2, len(title_candidates))) # λ©”μ‹œμ§€ 생성 pkg_msgs = [header] for i, (_, row) in enumerate(filtered.iterrows(), 1): desc, used_phrases = make_top2_description_custom( row.to_dict(), used_phrases ) tags = format_summary_tags_custom(row["μš”μ•½μ •λ³΄"]) title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) else random.choice(title_candidates)) title = f"{city} {title_phrase} νŒ¨ν‚€μ§€" url = row.URL pkg_msgs.append( f"{i}. {title}
" f"πŸ…Ό {desc}
{tags}
" f'' 'πŸ’š λ°”λ‘œκ°€κΈ° β†—' ) # λ©”μ‹œμ§€ 좜λ ₯ log_and_render( "

".join(pkg_msgs), sender="bot", chat_container=chat_container, key=f"pkg_bundle_{random.randint(1,999999)}" ) # μ„Έμ…˜ 정리 st.session_state["package_rendered"] = True st.session_state[step_key] = "package_end" return # ────────────────── 7) μ’…λ£Œ 단계 elif st.session_state[step_key] == "package_end": log_and_render("ν•„μš”ν•˜μ‹€ λ•Œ μ–Έμ œλ“ μ§€ 또 μ°Ύμ•„μ£Όμ„Έμš”! ✈️", sender="bot", chat_container=chat_container, key="goodbye") # ───────────────────────────────────── unknown λͺ¨λ“œ def unknown_ui(country, city, chat_container, log_and_render): """unknown λͺ¨λ“œ(아직 DB에 μ—†λŠ” λ‚˜λΌΒ·λ„μ‹œμΌ λ•Œ μ•ˆλ‚΄) μ „μš© UI & 둜직""" # μ•ˆλ‚΄ λ©”μ‹œμ§€ if city: msg = (f"πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή {city}의 μ—¬ν–‰μ§€λŠ” " "아직 μ€€λΉ„ μ€‘μ΄μ—μš”.
λΉ λ₯Έ μ‹œμΌ μ•ˆμ— μ—…λ°μ΄νŠΈν• κ²Œμš”!") elif country: msg = (f"πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή {country}의 μ—¬ν–‰μ§€λŠ” " "아직 μ€€λΉ„ μ€‘μ΄μ—μš”.
λΉ λ₯Έ μ‹œμΌ μ•ˆμ— μ—…λ°μ΄νŠΈν• κ²Œμš”!") else: msg = "πŸ” μ£„μ†‘ν•΄μš”. ν•΄λ‹Ή μ—¬ν–‰μ§€λŠ” 아직 μ€€λΉ„ μ€‘μ΄μ—μš”." with chat_container: log_and_render( f"{msg}", sender="bot", chat_container=chat_container, key="unknown_dest" ) # ───────────────────────────────────── 챗봇 호좜 def main(): init_session() chat_container = st.container() if "chat_log" in st.session_state and st.session_state.chat_log: replay_log(chat_container) # ───── greeting λ©”μ‹œμ§€ 좜λ ₯ if not st.session_state.get("greeting_rendered", False): greeting_message = ( "μ•ˆλ…•ν•˜μ„Έμš”. λͺ¨μ•„(MoAi)μž…λ‹ˆλ‹€.πŸ€–

" "μš”μ¦˜ μ–΄λ–€ 여행이 λ– μ˜€λ₯΄μ„Έμš”?
""λͺ¨μ•„κ°€ λ”± λ§žλŠ” μ—¬ν–‰μ§€λ₯Ό μ°Ύμ•„λ“œλ¦΄κ²Œμš”." ) log_and_render( greeting_message, sender="bot", chat_container=chat_container, key="greeting" ) st.session_state["greeting_rendered"] = True # ───── μ‚¬μš©μž μž…λ ₯ & μΆ”μ²œ μ‹œμž‘ # 1) μ‚¬μš©μž μž…λ ₯ user_input = st.text_input( "μž…λ ₯μ°½", # λΉ„μ–΄μžˆμ§€ μ•Šμ€ 라벨(μ ‘κ·Όμ„± 확보) placeholder="ex)'μš”μ¦˜ 힐링이 ν•„μš”ν•΄μš”', 'κ°€μ‘± μ—¬ν–‰ μ–΄λ””κ°€ μ’‹μ„κΉŒμš”?'", key="user_input", label_visibility="collapsed", # 화면에선 μˆ¨κΉ€ ) user_input_key = "last_user_input" select_keys = ["intent_chip_selected", "region_chip_selected", "emotion_chip_selected", "theme_chip_selected"] # 1-1) β€œμ§„μ§œ μƒˆλ‘œ μž…λ ₯” 감지 prev = st.session_state.get(user_input_key, "") if user_input and user_input != prev: for k in select_keys: st.session_state.pop(k, None) st.session_state[user_input_key] = user_input st.session_state["user_input_rendered"] = False # step μ΄ˆκΈ°ν™” st.session_state["region_step"] = "recommend" st.rerun() # 1-2) μ‚¬μš©μž λ©”μ‹œμ§€ ν•œ 번만 λ Œλ”λ§ if user_input and not st.session_state.get("user_input_rendered", False): log_and_render( user_input, sender="user", chat_container = chat_container, key=f"user_input_{user_input}" ) st.session_state["user_input_rendered"] = True if user_input: # 2) mode 탐지 _, _, mode = detect_location_filter(user_input) top_emotions, emotion_groups = analyze_emotion(user_input) intent, intent_score = detect_intent(user_input) country_filter, city_filter, _ = detect_location_filter(user_input) candidate_themes = extract_themes( emotion_groups, intent, force_mode=(intent_score >= 0.70) ) if intent_score >= 0.70: mode = "intent" # 🌟 DEBUG ──────────────────────────────── # with st.expander("πŸ” DEBUG - λͺ¨λ“œ νŒμ •", expanded=True): # st.markdown(f""" # **μž…λ ₯ λ¬Έμž₯**: `{user_input}` # **detect_location_filter** πŸ‘‰ # β€’ country β†’ `{country_filter}` # β€’ city     β†’ `{city_filter}` # β€’ mode  β†’ `{mode}` # **intent_score**: `{intent_score:.3f}` # **top_emotions**: `{top_emotions}` # """) # ──────────────────────────────────────── # 3) λͺ¨λ“œλ³„ λΆ„κΈ° if mode == "region": region_ui( travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, log_and_render ) return elif mode == "intent": intent_ui( travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, intent, log_and_render) return elif mode == "unknown": unknown_ui( country_filter, city_filter, chat_container, log_and_render) return else: emotion_ui( travel_df, external_score_df, festival_df, weather_df, package_df, country_filter, city_filter, chat_container, candidate_themes, intent, emotion_groups, top_emotions, log_and_render ) if __name__ == "__main__": main() #cmd μž…λ ₯-> cd "파일 μœ„μΉ˜ 경둜 볡뢙" #ex(C:\Users\gayoung\Desktop\multi\0514 - project\06 - streamlit ν…ŒμŠ€νŠΈ\test) #cmd μž…λ ₯ -> streamlit run app.py