Spaces:
Sleeping
Sleeping
| # app.py | |
| import streamlit as st | |
| import pandas as pd | |
| import logic | |
| import os | |
| from datetime import datetime, timedelta, timezone | |
| from wordcloud import WordCloud | |
| import matplotlib.pyplot as plt | |
| # [NEW] 3D ์๊ฐํ๋ฅผ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ | |
| import plotly.express as px | |
| from sklearn.decomposition import PCA | |
| import numpy as np | |
| # ------------------------------------------------------------------------- | |
| # 1. ํ์ด์ง ๊ธฐ๋ณธ ์ค์ & ์ธ์ ์ํ ์ด๊ธฐํ | |
| # ------------------------------------------------------------------------- | |
| st.set_page_config(page_title="AI ํ์ ์ฌ๋ฃ ์ถ์ฒ", layout="wide") | |
| st.title("๐ณ AI ์์ฌ๋ฃ ๋์ฒด ์ถ์ฒ ๋์๋ณด๋") | |
| # [NEW] ํฌํธํด๋ฆฌ์ค ์คํ์ผ ์ ์ฉ (Nanum Pen Script & Theme) | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&display=swap'); | |
| html, body, [class*="css"] { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.25rem; | |
| } | |
| /* ์ ๋ชฉ ํฐํธ ํฌ๊ธฐ ํค์ฐ๊ธฐ */ | |
| h1 { font-size: 3.5rem !important; color: #1e293b !important; } | |
| h2 { font-size: 2.8rem !important; color: #334155 !important; } | |
| h3 { font-size: 2.2rem !important; color: #475569 !important; } | |
| /* ๋ฐฐ๊ฒฝ ๊ทธ๋ผ๋ฐ์ด์ (ํฌํธํด๋ฆฌ์ค์ ์ ์ฌํ๊ฒ) */ | |
| .stApp { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; | |
| } | |
| /* ์ค๋ช ๋ฐ์ค ์คํ์ผ */ | |
| div[data-testid="stMarkdownContainer"] > div { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| } | |
| /* ๋ฒํผ ์คํ์ผ */ | |
| .stButton > button { | |
| background: #3b82f6 !important; | |
| color: white !important; | |
| border-radius: 12px !important; | |
| border: none !important; | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.4rem !important; | |
| padding: 0.5rem 1rem !important; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| transition: all 0.2s; | |
| } | |
| .stButton > button:hover { | |
| transform: scale(1.05); | |
| opacity: 0.9; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| /* ์ ๋ ฅ ํ๋ ์คํ์ผ */ | |
| .stTextInput > div > div > input, .stTextArea > div > div > textarea { | |
| background-color: rgba(255, 255, 255, 0.9) !important; | |
| border-radius: 10px !important; | |
| border: 1px solid #cbd5e1 !important; | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.2rem !important; | |
| color: #1e293b !important; | |
| } | |
| /* Expander ์คํ์ผ */ | |
| .streamlit-expanderHeader { | |
| font-family: 'Nanum Pen Script', cursive !important; | |
| font-size: 1.3rem !important; | |
| background-color: rgba(255, 255, 255, 0.5) !important; | |
| border-radius: 8px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| if 'voted_logs' not in st.session_state: st.session_state['voted_logs'] = set() | |
| if "stopword_input_field" not in st.session_state: st.session_state["stopword_input_field"] = "" | |
| if "board_nick_input" not in st.session_state: st.session_state["board_nick_input"] = "" | |
| if "board_msg_input" not in st.session_state: st.session_state["board_msg_input"] = "" | |
| if "feedback_input_field" not in st.session_state: st.session_state["feedback_input_field"] = "" | |
| # ------------------------------------------------------------------------- | |
| # 2. ํฌํผ ํจ์ ๋ฐ ๋ค์ด์ผ๋ก๊ทธ | |
| # ------------------------------------------------------------------------- | |
| def format_saving(score, is_multi=False): | |
| prefix = "์ด " if is_multi else "" | |
| if score > 0: return f"๐ข {prefix}+{score}๋จ๊ณ (์ ๊ฐ)" | |
| elif score < 0: return f"๐ด {prefix}{score}๋จ๊ณ (๋น์)" | |
| else: return "โช ๋์ผ ์์ค" | |
| def show_logic_dialog(): | |
| if os.path.exists("flowchart.png"): | |
| st.image("flowchart.png", use_container_width=True) | |
| try: | |
| with open("docs/logic_explanation.md", "r", encoding="utf-8") as f: | |
| markdown_text = f.read() | |
| st.markdown("---") | |
| st.markdown(markdown_text) | |
| except: | |
| st.error("์ค๋ช ํ์ผ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| def show_wordcloud_dialog(timeframe_text, text_data): | |
| st.subheader(f"{timeframe_text} ๋ง์ด ๊ฒ์๋ ํ๊ฒ ์ฌ๋ฃ") | |
| if not text_data: | |
| st.info("๋ฐ์ดํฐ๊ฐ ์ถฉ๋ถํ์ง ์์ต๋๋ค.") | |
| return | |
| font_path = "src/font.ttf" if os.path.exists("src/font.ttf") else None | |
| try: | |
| wordcloud = WordCloud(font_path=font_path, width=800, height=400, background_color='white', colormap='viridis', random_state=42).generate(text_data) | |
| fig, ax = plt.subplots(figsize=(10, 5)) | |
| ax.imshow(wordcloud, interpolation='bilinear'); ax.axis('off') | |
| st.pyplot(fig) | |
| if not font_path: st.caption("โ ๏ธ ํ๊ธ ํฐํธ ํ์ผ์ด ์์ด ๊ธ์๊ฐ ๊นจ์ง ์ ์์ต๋๋ค.") | |
| except Exception as e: st.error(f"์ค๋ฅ ๋ฐ์: {e}") | |
| # [NEW] 3D ๋ฒกํฐ ๊ณต๊ฐ ์๊ฐํ ํ์ | |
| def show_3d_space_dialog(): | |
| st.caption("AI๊ฐ ํ์ตํ ์ฌ๋ฃ๋ค์ ๊ด๊ณ๋ฅผ 3์ฐจ์ ๊ณต๊ฐ์์ ํ์ธํด๋ณด์ธ์. (์์ 300๊ฐ ์ฌ๋ฃ)") | |
| try: | |
| # logic.py์์ ๋ก๋๋ Word2Vec ๋ชจ๋ธ ๊ฐ์ ธ์ค๊ธฐ | |
| model = logic.w2v_model | |
| # ๋น๋์ ์์ 300๊ฐ ๋จ์ด ์ถ์ถ | |
| words = model.wv.index_to_key[:300] | |
| vectors = np.array([model.wv[word] for word in words]) | |
| # PCA๋ก 100์ฐจ์ -> 3์ฐจ์ ์ถ์ | |
| pca = PCA(n_components=3) | |
| projections = pca.fit_transform(vectors) | |
| # ๋ฐ์ดํฐํ๋ ์ ์์ฑ | |
| df_vis = pd.DataFrame(projections, columns=['x', 'y', 'z']) | |
| df_vis['word'] = words | |
| # Plotly 3D ์ฐ์ ๋ ๊ทธ๋ฆฌ๊ธฐ | |
| fig = px.scatter_3d( | |
| df_vis, x='x', y='y', z='z', | |
| text='word', | |
| hover_name='word', | |
| color='z', # ๋์ด์ ๋ฐ๋ผ ์์ ๋ณํ | |
| color_continuous_scale='Viridis' | |
| ) | |
| fig.update_traces( | |
| marker=dict(size=4, opacity=0.8), | |
| textposition='top center', | |
| textfont=dict(size=10, color='black') # ํ ์คํธ ์คํ์ผ | |
| ) | |
| fig.update_layout( | |
| height=600, | |
| scene=dict( | |
| xaxis=dict(showticklabels=False, title=''), | |
| yaxis=dict(showticklabels=False, title=''), | |
| zaxis=dict(showticklabels=False, title='') | |
| ), | |
| margin=dict(l=0, r=0, b=0, t=0) | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.info("๐ก **ํ:** ๋ง์ฐ์ค๋ก ๋๋๊ทธํ์ฌ ํ์ ํ๊ฑฐ๋ ํ ๋ก ํ๋/์ถ์ํ ์ ์์ต๋๋ค. ๊ฐ๊น์ด ์๋ ์ฌ๋ฃ๋ค์ AI๊ฐ '๋น์ทํ ์ฑ์ง'๋ก ์ธ์ํ ๊ฒ์ ๋๋ค.") | |
| except Exception as e: | |
| st.error(f"์๊ฐํ ์์ฑ ์คํจ: {e}") | |
| # [CALLBACK] ํจ์๋ค | |
| def handle_board_submission(): | |
| nick = st.session_state.get("board_nick_input", "") | |
| msg = st.session_state.get("board_msg_input", "") | |
| if nick and msg: | |
| if logic.save_board_message(nick, msg): | |
| st.toast("๊ฒ์๊ธ์ด ๋ฑ๋ก๋์์ต๋๋ค!", icon="โ ") | |
| st.session_state["board_nick_input"] = "" | |
| st.session_state["board_msg_input"] = "" | |
| else: st.toast("๊ฒ์๊ธ ๋ฑ๋ก์ ์คํจํ์ต๋๋ค.", icon="โ") | |
| else: st.toast("๋๋ค์๊ณผ ๋ด์ฉ์ ๋ชจ๋ ์ ๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ") | |
| def handle_stopword_submission(): | |
| current_input = st.session_state.get("stopword_input_field", "") | |
| if current_input: | |
| is_success, msg = logic.save_stopwords_to_db(current_input) | |
| if is_success: | |
| st.toast(msg, icon="โ ") | |
| st.session_state["stopword_input_field"] = "" | |
| else: st.toast(msg, icon="โ") | |
| else: st.toast("๋จ์ด๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ") | |
| def handle_feedback_submission(): | |
| content = st.session_state.get("feedback_input_field", "") | |
| if content: | |
| if logic.save_feedback_to_db(content): | |
| st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค!", icon="โ ") | |
| st.balloons() | |
| st.session_state["feedback_input_field"] = "" | |
| else: st.toast("์ ์ก ์คํจ", icon="โ") | |
| else: st.toast("๋ด์ฉ์ ์ ๋ ฅํด์ฃผ์ธ์.", icon="โ ๏ธ") | |
| # ------------------------------------------------------------------------- | |
| # 3. ์ฌ์ด๋๋ฐ UI | |
| # ------------------------------------------------------------------------- | |
| with st.sidebar: | |
| st.header("๐๏ธ ์ปจํธ๋กค ํจ๋") | |
| selected_mode = st.radio("๋ชจ๋ ์ ํ", ["๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์", "โจ Ver.2 ๋๋ง์ ์ฌ๋ฃ ์ ๋ ฅ (์ปค์คํ )"], index=0) | |
| st.divider() | |
| st.subheader("โ๏ธ ๊ฐ์ค์น ์ค์ ") | |
| is_v1 = selected_mode == "๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์" | |
| w_w2v = st.slider("๋งยท์ฑ์ง (Word2Vec)", 0.0, 5.0, 5.0, 0.5) | |
| w_d2v = st.slider("๋ฌธ๋งฅ (Doc2Vec)", 0.0, 5.0, 1.0, 0.5) | |
| w_method = st.slider("์กฐ๋ฆฌ๋ฒ ํต๊ณ (Ver.1 ์ ์ฉ)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1) | |
| w_cat = st.slider("์นดํ ๊ณ ๋ฆฌ ํต๊ณ (Ver.1 ์ ์ฉ)", 0.0, 5.0, 1.0, 0.5, disabled=not is_v1) | |
| if not is_v1: st.caption("๐ก ์ปค์คํ ๋ชจ๋์์๋ ํต๊ณ ๊ฐ์ค์น๊ฐ ์ ์ฉ๋์ง ์์ต๋๋ค.") | |
| excluded_ingredients = [] | |
| if not is_v1: | |
| st.divider() | |
| st.subheader("๐ซ ์ ์ธํ ์ฌ๋ฃ ์ค์ ") | |
| all_ing_options = sorted(list(logic.all_ingredients_set)) | |
| excluded_ingredients = st.multiselect("์ ์ธํ ์ฌ๋ฃ ์ ํ", all_ing_options, placeholder="์: ๋ ์ฝฉ, ์ค์ด") | |
| st.divider() | |
| # [NEW] 3D ์๊ฐํ ๋ฒํผ ์ถ๊ฐ | |
| if st.button("๐ ์ฌ๋ฃ ์ฐ์ฃผ(3D) ํํํ๊ธฐ", use_container_width=True): | |
| show_3d_space_dialog() | |
| if st.button("๐ค ์ด๋ค ๊ณผ์ ์ ๊ฑฐ์ณ ์ฌ๋ฃ๊ฐ ์ถ์ฒ๋๋์?", use_container_width=True): | |
| show_logic_dialog() | |
| st.divider() | |
| st.subheader("๐ ์ธ์ฌ์ดํธ ๋์๋ณด๋ (Beta)") | |
| kst = timezone(timedelta(hours=9)) | |
| today_date_string = datetime.now(kst).strftime("%Y๋ %m์ %d์ผ") | |
| stopwords_list = logic.load_global_stopwords() | |
| tab_today, tab_all = st.tabs(["๐ ์ค๋", "๐ ๋์ "]) | |
| wc_text_today = logic.get_wordcloud_text('today') | |
| wc_text_all = logic.get_wordcloud_text('all') | |
| today_count, today_dishes, today_targets = logic.get_usage_stats(timeframe='today') | |
| all_count, all_dishes, all_targets = logic.get_usage_stats(timeframe='all') | |
| with tab_today: | |
| st.caption(f"๊ธฐ์ค์ผ: {today_date_string} (KST)") | |
| col_m1, col_m2 = st.columns(2) | |
| col_m1.metric("์ค๋ ์ฌ์ฉ๋", f"{today_count}๊ฑด") | |
| col_m2.metric("๋์ ๋ถ์ฉ์ด", f"{len(stopwords_list)}๊ฐ") | |
| if today_count > 0: | |
| if st.button("โ๏ธ ์ค๋์ ์๋ํด๋ผ์ฐ๋", key="btn_wc_today", use_container_width=True): | |
| show_wordcloud_dialog("์ค๋", wc_text_today) | |
| st.caption("๐ฅ ์ค๋ ๋ง์ด ๋์ฒด๋ ์ฌ๋ฃ") | |
| if not today_targets.empty: st.bar_chart(today_targets, color="#FF6B6B", height=200) | |
| else: st.info("์ค๋์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| with tab_all: | |
| st.caption("์๋น์ค ์์ ์ดํ ์ ์ฒด ๋ฐ์ดํฐ") | |
| col_a1, col_a2 = st.columns(2) | |
| col_a1.metric("์ด ์ฌ์ฉ๋", f"{all_count}๊ฑด") | |
| col_a2.metric("๋์ ๋ถ์ฉ์ด", f"{len(stopwords_list)}๊ฐ") | |
| if all_count > 0: | |
| if st.button("โ๏ธ ๋์ ์๋ํด๋ผ์ฐ๋", key="btn_wc_all", use_container_width=True): | |
| show_wordcloud_dialog("๋์ ", wc_text_all) | |
| st.caption("๐ฅ ์ญ๋ ๋ง์ด ๋์ฒด๋ ์ฌ๋ฃ") | |
| if not all_targets.empty: st.bar_chart(all_targets, color="#FF6B6B", height=200) | |
| else: st.info("๋์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| with st.expander("๐ ์ ๊ณ ๋ ๋ถ์ฉ์ด ๋ชฉ๋ก ๋ณด๊ธฐ"): | |
| if stopwords_list: st.dataframe(pd.DataFrame(stopwords_list, columns=["๋ถ์ฉ์ด"]), use_container_width=True, hide_index=True) | |
| else: st.info("์ ๊ณ ๋ ๋ถ์ฉ์ด๊ฐ ์์ต๋๋ค.") | |
| st.divider() | |
| with st.expander("๐ฌ ์ต๋ช ๊ฒ์ํ (Beta)", expanded=True): | |
| with st.form("board_form"): | |
| st.text_input("๋๋ค์", placeholder="์ต๋ช ", key="board_nick_input") | |
| st.text_area("๋ด์ฉ", placeholder="์์ ๋กญ๊ฒ ์๊ฒฌ์ ๋จ๊ฒจ์ฃผ์ธ์", height=80, key="board_msg_input") | |
| st.form_submit_button("๋ฑ๋ก", on_click=handle_board_submission) | |
| st.markdown("---") | |
| messages = logic.get_board_messages() | |
| if messages: | |
| for m in messages: | |
| st.markdown(f"**{m['nickname']}** <span style='color:grey; font-size:0.8em;'>({m['display_time']})</span>", unsafe_allow_html=True) | |
| st.text(m['content']) | |
| st.divider() | |
| else: st.caption("์ฒซ ๋ฒ์งธ ๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!") | |
| # ------------------------------------------------------------------------- | |
| # 4. ๋ฉ์ธ UI (๊ธฐ์กด๊ณผ ๋์ผ) | |
| # ------------------------------------------------------------------------- | |
| col_main, _ = st.columns([0.9, 0.1]) | |
| with col_main: | |
| if selected_mode == "๐ Ver.1 ๊ธฐ์กด ๋ ์ํผ DB ๊ฒ์": | |
| st.markdown("""<div style="background-color: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#0066cc;">[Ver.1] ๋ ์ํผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ฒ์</h4><p style="margin:5px 0 0 0; font-size:14px;">ํ์ต๋ 12๋ง์ฌ ๊ฐ์ ๋ ์ํผ ์ค ํ๋๋ฅผ ์ ํํ์ฌ ๋ถ์ํฉ๋๋ค. ๋ชจ๋ ํต๊ณ ์ ์๊ฐ ํ์ฉ๋ฉ๋๋ค.</p></div>""", unsafe_allow_html=True) | |
| search_keyword = st.text_input("๐ฝ๏ธ ์๋ฆฌ๋ช ๊ฒ์ (ํค์๋ ์ ๋ ฅ ํ ์ํฐ)", placeholder="์: ๋์ฅ์ฐ๊ฐ") | |
| final_dish_name = None | |
| if search_keyword: | |
| exact_match = logic.df[logic.df['์๋ฆฌ๋ช '] == search_keyword] | |
| exact_name = exact_match['์๋ฆฌ๋ช '].iloc[0] if not exact_match.empty else None | |
| candidates = logic.df[logic.df['์๋ฆฌ๋ช '].str.contains(search_keyword, na=False, case=False)] | |
| if exact_name: candidates = candidates[candidates['์๋ฆฌ๋ช '] != exact_name] | |
| candidate_names = sorted(candidates['์๋ฆฌ๋ช '].unique().tolist())[:30] | |
| options = [] | |
| if exact_name: options.append(exact_name) | |
| options.extend(candidate_names) | |
| if not options: st.warning(f"๐ '{search_keyword}'๊ฐ ํฌํจ๋ ์๋ฆฌ๋ช ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| else: | |
| index_to_select = 0 if exact_name else None | |
| label_msg = f"๐ '{search_keyword}' ๊ฒ์ ๊ฒฐ๊ณผ ({len(options)}๊ฐ)" | |
| if exact_name: label_msg += " - ์ ํํ ์๋ฆฌ๋ช ์ด ๋ฐ๊ฒฌ๋์์ต๋๋ค!" | |
| selected_option = st.selectbox(label_msg, options, index=index_to_select) | |
| final_dish_name = selected_option | |
| if final_dish_name: | |
| st.success(f"โ ์ ํ๋ ์๋ฆฌ: **{final_dish_name}**") | |
| cands = logic.df[logic.df['์๋ฆฌ๋ช '] == final_dish_name] | |
| cands = cands.head(10).reset_index(drop=True) | |
| if cands.empty: st.error("โ ๋ ์ํผ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ฌ ์ ์์ต๋๋ค.") | |
| else: | |
| st.divider() | |
| options = {} | |
| for _, r in cands.iterrows(): | |
| preview = ', '.join(r['์ฌ๋ฃํ ํฐ']) | |
| options[f"[{r['์๋ฆฌ๋ฐฉ๋ฒ๋ณ๋ช ']}] {r['์๋ฆฌ๋ช ']} (ID:{r['๋ ์ํผ์ผ๋ จ๋ฒํธ']}) - {preview}"] = r['๋ ์ํผ์ผ๋ จ๋ฒํธ'] | |
| selected_label = st.selectbox("๐ ๋ถ์ํ ๋ ์ํผ๋ฅผ ์ ํํ์ธ์", list(options.keys())) | |
| recipe_id = options[selected_label] | |
| c1, c2 = st.columns(2) | |
| with c1: target_str = st.text_input("๐ฏ ๋ฐ๊ฟ ์ฌ๋ฃ", placeholder="๋ผ์ง๊ณ ๊ธฐ, ์ํ") | |
| with c2: stop_str = st.text_input("๐ซ ์ ๊ฑฐํ ๋ฌธ๊ตฌ", placeholder="์ฝ๊ฐ, ์ํ์ฉ") | |
| if target_str: | |
| targets = [t.strip() for t in target_str.split(',') if t.strip()] | |
| stops = [s.strip() for s in stop_str.split(',') if s.strip()] | |
| current_recipe_row = logic.df[logic.df['๋ ์ํผ์ผ๋ จ๋ฒํธ'] == recipe_id].iloc[0] | |
| recipe_ingredients = current_recipe_row['์ฌ๋ฃํ ํฐ'] | |
| invalid_targets = [t for t in targets if t not in recipe_ingredients] | |
| if not targets: st.warning("ํ๊ฒ ์ฌ๋ฃ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| elif invalid_targets: | |
| st.error(f"๐จ ๋ค์ ์ฌ๋ฃ๋ ์ ํํ ๋ ์ํผ์ ์์ต๋๋ค: {', '.join(invalid_targets)}") | |
| st.info("๐ก ํ: ๋ ์ํผ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ ์๋ ์ฌ๋ฃ๋ช ์ ์ ํํ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| else: | |
| st.divider() | |
| has_result = False | |
| final_recs = [] | |
| if len(targets) == 1: | |
| st.subheader("๐น ๋จ์ผ ์ฌ๋ฃ ๋์ฒด ์ถ์ฒ (DB ๊ธฐ๋ฐ)") | |
| t = targets[0] | |
| res = logic.substitute_single(recipe_id, t, stops, w_w2v, w_d2v, w_method, w_cat, topn=5) | |
| st.markdown(f"**{t}** ๋์ฒด ๊ฒฐ๊ณผ") | |
| if not res.empty: | |
| has_result = True | |
| final_recs = res['๋์ฒด์ฌ๋ฃ'].head(3).tolist() | |
| d_df = res[['๋์ฒด์ฌ๋ฃ', '์ต์ข ์ ์', 'saving_score']].copy() | |
| d_df['์์ ์๊ฐ๋ณ๋'] = d_df['saving_score'].apply(lambda x: format_saving(x)) | |
| d_df = d_df[['๋์ฒด์ฌ๋ฃ', '์ต์ข ์ ์', '์์ ์๊ฐ๋ณ๋']] | |
| d_df.columns = ['์ถ์ฒ์ฌ๋ฃ', '์ ํฉ๋', '์์ ์๊ฐ๋ณ๋'] | |
| st.dataframe(d_df.style.format("{:.1%}", subset=['์ ํฉ๋']).background_gradient(cmap='Greens', subset=['์ ํฉ๋']), use_container_width=True, hide_index=True) | |
| else: st.warning("๊ฒฐ๊ณผ ์์") | |
| elif len(targets) > 1: | |
| st.subheader("๐งฉ ์ต์ ์ ์ฌ๋ฃ ์กฐํฉ (DB ๊ธฐ๋ฐ ๋ค์ค ๋์ฒด)") | |
| multi_res = logic.substitute_multi(recipe_id, targets, stops, w_w2v, w_d2v, w_method, w_cat) | |
| if multi_res: | |
| has_result = True | |
| final_recs = [", ".join(subs) for subs, score, saving in multi_res] | |
| m_df = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res], columns=['์ถ์ฒ ์กฐํฉ', '์ข ํฉ ์ ์', '์์ ์๊ฐ๋ณ๋ ํฉ๊ณ']) | |
| st.dataframe(m_df.style.format("{:.1%}", subset=['์ข ํฉ ์ ์']).background_gradient(cmap='Blues', subset=['์ข ํฉ ์ ์']), use_container_width=True, hide_index=True) | |
| else: st.info("์กฐํฉ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| if has_result: | |
| current_state = f"DB_{final_dish_name}_{target_str}_{stop_str}_{w_w2v}_{w_d2v}_{w_method}_{w_cat}_{final_recs}" | |
| if 'last_log' not in st.session_state: st.session_state['last_log'] = "" | |
| if st.session_state['last_log'] != current_state: | |
| log_id = logic.save_log_to_db(final_dish_name, target_str, stops, w_w2v, w_d2v, w_method, w_cat, rec_list=final_recs, is_custom=False) | |
| st.session_state['current_log_id'] = log_id | |
| st.session_state['last_log'] = current_state | |
| if 'current_log_id' in st.session_state and st.session_state['current_log_id']: | |
| cl_id = st.session_state['current_log_id'] | |
| is_voted = cl_id in st.session_state['voted_logs'] | |
| st.write(""); b1, b2, _ = st.columns([0.2, 0.2, 0.6]) | |
| if is_voted: b1.success("โ ํ๊ฐ ์๋ฃ!"); b2.write("") | |
| else: | |
| b1.button("๐ ๋ง์กฑํด์", key="btn_sat_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "satisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("๊ฐ์ฌํฉ๋๋ค!"))) | |
| b2.button("๐ ์์ฌ์์", key="btn_dis_db", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id, "dissatisfy"), st.session_state['voted_logs'].add(cl_id), st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค."))) | |
| elif selected_mode == "โจ Ver.2 ๋๋ง์ ์ฌ๋ฃ ์ ๋ ฅ (์ปค์คํ )": | |
| st.markdown("""<div style="background-color: #fff5f0; padding: 15px; border-radius: 10px; margin-bottom: 20px;"><h4 style="margin:0; color:#cc5500;">[Ver.2] ๋๋ง์ ์ฌ๋ฃ ๋ฆฌ์คํธ ์ ๋ ฅ</h4><p style="margin:5px 0 0 0; font-size:14px;">๋์ฅ๊ณ ์ ์ฌ๋ฃ๋ค์ ์ง์ ์ ๋ ฅํ์ธ์. ๋ฌธ๋งฅ์ ์ค์๊ฐ์ผ๋ก ๋ถ์ํ์ฌ ์ถ์ฒํฉ๋๋ค. (ํต๊ณ ์ ์ ์ ์ธ)</p></div>""", unsafe_allow_html=True) | |
| st.markdown("##### ๐ท๏ธ ์๋ฆฌ๋ช ์ ๋ ฅ (์ฐธ๊ณ ์ฉ)") | |
| search_keyword_v2 = st.text_input("ํค์๋ ์ ๋ ฅ ํ ์ํฐ (์: ๋ณถ์๋ฐฅ) - ์ ํ์ฌํญ", key="v2_search") | |
| custom_dish_name = search_keyword_v2 | |
| if search_keyword_v2: | |
| exact_match_v2 = logic.df[logic.df['์๋ฆฌ๋ช '] == search_keyword_v2] | |
| exact_name_v2 = exact_match_v2['์๋ฆฌ๋ช '].iloc[0] if not exact_match_v2.empty else None | |
| candidates_v2 = logic.df[logic.df['์๋ฆฌ๋ช '].str.contains(search_keyword_v2, na=False, case=False)] | |
| if exact_name_v2: candidates_v2 = candidates_v2[candidates_v2['์๋ฆฌ๋ช '] != exact_name_v2] | |
| candidate_names_v2 = sorted(candidates_v2['์๋ฆฌ๋ช '].unique().tolist())[:30] | |
| options_v2 = [] | |
| if exact_name_v2: options_v2.append(exact_name_v2) | |
| options_v2.append("(์ง์ ์ ๋ ฅํ ์ด๋ฆ ์ฌ์ฉ)") | |
| options_v2.extend(candidate_names_v2) | |
| if options_v2: | |
| idx_v2 = 0 if exact_name_v2 else 0 | |
| label_v2 = f"๐ก ๊ด๋ จ ์๋ฆฌ๋ช ๋ฐ๊ฒฌ ({len(options_v2)-1}๊ฐ)" | |
| if exact_name_v2: label_v2 += " - ์ ํํ ์๋ฆฌ๋ช ๋ฐ๊ฒฌ!" | |
| sel_v2 = st.selectbox(label_v2, options_v2, index=idx_v2, key="v2_select") | |
| if sel_v2 != "(์ง์ ์ ๋ ฅํ ์ด๋ฆ ์ฌ์ฉ)": custom_dish_name = sel_v2 | |
| st.write("") | |
| context_str = st.text_area("๐ ์ ์ฒด ์ฌ๋ฃ ๋ฆฌ์คํธ (์ผํ๋ก ๊ตฌ๋ถ)", placeholder="์: ๋ฐฅ, ๊ณ๋, ๋ํ, ๊ฐ์ฅ, ์ฐธ๊ธฐ๋ฆ", height=100, key="v2_context") | |
| if context_str: | |
| context_ings_list = [ing.strip() for ing in context_str.split(',') if ing.strip()] | |
| if not context_ings_list: st.warning("์ฌ๋ฃ๋ฅผ ํ ๊ฐ ์ด์ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| else: | |
| st.caption(f"์ธ์๋ ์ฌ๋ฃ ({len(context_ings_list)}๊ฐ): {', '.join(context_ings_list)}") | |
| c1_c, c2_c = st.columns(2) | |
| with c1_c: target_str_c = st.text_input("๐ฏ ๋ฐ๊ฟ ์ฌ๋ฃ (์ ๋ฆฌ์คํธ ์ค)", placeholder="์: ๊ณ๋", key="v2_target") | |
| with c2_c: stop_str_c = st.text_input("๐ซ ์ ๊ฑฐํ ๋ฌธ๊ตฌ (์์)", placeholder="์: ์ฝ๊ฐ", key="v2_stop") | |
| if target_str_c: | |
| targets_c = [t.strip() for t in target_str_c.split(',') if t.strip()] | |
| stops_c = [s.strip() for s in stop_str_c.split(',') if s.strip()] | |
| invalid_targets = [t for t in targets_c if t not in context_ings_list] | |
| if invalid_targets: st.error(f"๐จ ๋ค์ ์ฌ๋ฃ๋ ์ ์ฒด ๋ฆฌ์คํธ์ ์์ต๋๋ค: {', '.join(invalid_targets)}") | |
| elif not targets_c: st.warning("๋ฐ๊ฟ ์ฌ๋ฃ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| else: | |
| st.divider() | |
| has_result_c = False | |
| final_recs_c = [] | |
| if len(targets_c) == 1: | |
| st.subheader("๐น ๋จ์ผ ์ฌ๋ฃ ๋์ฒด ์ถ์ฒ (์ปค์คํ )") | |
| t_c = targets_c[0] | |
| res_c = logic.substitute_single_custom(t_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients, topn=5) | |
| st.markdown(f"**{t_c}** ๋์ฒด ๊ฒฐ๊ณผ") | |
| if not res_c.empty: | |
| has_result_c = True | |
| final_recs_c = res_c['๋์ฒด์ฌ๋ฃ'].head(3).tolist() | |
| d_df_c = res_c[['๋์ฒด์ฌ๋ฃ', '์ต์ข ์ ์', 'saving_score']].copy() | |
| d_df_c['์์ ์๊ฐ๋ณ๋'] = d_df_c['saving_score'].apply(lambda x: format_saving(x)) | |
| d_df_c = d_df_c[['๋์ฒด์ฌ๋ฃ', '์ต์ข ์ ์', '์์ ์๊ฐ๋ณ๋']] | |
| d_df_c.columns = ['์ถ์ฒ์ฌ๋ฃ', '์ ํฉ๋', '์์ ์๊ฐ๋ณ๋'] | |
| st.dataframe(d_df_c.style.format("{:.1%}", subset=['์ ํฉ๋']).background_gradient(cmap='Greens', subset=['์ ํฉ๋']), use_container_width=True, hide_index=True) | |
| else: st.warning("๊ฒฐ๊ณผ ์์") | |
| elif len(targets_c) > 1: | |
| st.subheader("๐งฉ ์ต์ ์ ์ฌ๋ฃ ์กฐํฉ (์ปค์คํ ๋ค์ค ๋์ฒด)") | |
| multi_res_c = logic.substitute_multi_custom(targets_c, context_ings_list, stops_c, w_w2v, w_d2v, excluded_ings=excluded_ingredients) | |
| if multi_res_c: | |
| has_result_c = True | |
| final_recs_c = [", ".join(subs) for subs, score, saving in multi_res_c] | |
| m_df_c = pd.DataFrame([(f"{', '.join(subs)}", score, format_saving(saving, True)) for subs, score, saving in multi_res_c], columns=['์ถ์ฒ ์กฐํฉ', '์ข ํฉ ์ ์', '์์ ์๊ฐ๋ณ๋ ํฉ๊ณ']) | |
| st.dataframe(m_df_c.style.format("{:.1%}", subset=['์ข ํฉ ์ ์']).background_gradient(cmap='Blues', subset=['์ข ํฉ ์ ์']), use_container_width=True, hide_index=True) | |
| else: st.info("์กฐํฉ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| if has_result_c: | |
| current_state_c = f"Custom_{custom_dish_name}_{target_str_c}_{stop_str_c}_{w_w2v}_{w_d2v}_{final_recs_c}" | |
| if 'last_log_c' not in st.session_state: st.session_state['last_log_c'] = "" | |
| if st.session_state['last_log_c'] != current_state_c: | |
| log_id_c = logic.save_log_to_db(custom_dish_name, target_str_c, stops_c, w_w2v, w_d2v, 0, 0, rec_list=final_recs_c, is_custom=True) | |
| st.session_state['current_log_id_c'] = log_id_c | |
| st.session_state['last_log_c'] = current_state_c | |
| if 'current_log_id_c' in st.session_state and st.session_state['current_log_id_c']: | |
| cl_id_c = st.session_state['current_log_id_c'] | |
| is_voted_c = cl_id_c in st.session_state['voted_logs'] | |
| st.write(""); b1_c, b2_c, _ = st.columns([0.2, 0.2, 0.6]) | |
| if is_voted_c: b1_c.success("โ ํ๊ฐ ์๋ฃ!"); b2_c.write("") | |
| else: | |
| b1_c.button("๐ ๋ง์กฑํด์", key="btn_sat_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "satisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("๊ฐ์ฌํฉ๋๋ค!"))) | |
| b2_c.button("๐ ์์ฌ์์", key="btn_dis_custom", use_container_width=True, on_click=lambda: (logic.update_feedback_in_db(cl_id_c, "dissatisfy"), st.session_state['voted_logs'].add(cl_id_c), st.toast("์๊ฒฌ ๊ฐ์ฌํฉ๋๋ค."))) | |
| else: st.info("๐ ์ ์ฒด ์ฌ๋ฃ ๋ฆฌ์คํธ๋ฅผ ๋จผ์ ์ ๋ ฅํด์ฃผ์ธ์.") | |
| # ------------------------------------------------------------------------- | |
| # 5. ํ๋จ ํผ๋๋ฐฑ ๋ฐ ๋ถ์ฉ์ด ์ ๊ณ ์์ญ | |
| # ------------------------------------------------------------------------- | |
| st.divider() | |
| col_feedback, col_stopword = st.columns(2) | |
| with col_feedback: | |
| st.subheader("๐ข ์๋น์ค ์๊ฒฌ ๋ณด๋ด๊ธฐ") | |
| with st.form("feedback_form"): | |
| text = st.text_area("๊ฐ์ ํ ์ ์ด๋ ๋ฒ๊ทธ๊ฐ ์๋ค๋ฉด ์๋ ค์ฃผ์ธ์!", height=100, key="feedback_input_field") | |
| st.form_submit_button("์๊ฒฌ ๋ณด๋ด๊ธฐ", use_container_width=True, on_click=handle_feedback_submission) | |
| with col_stopword: | |
| st.subheader("๐ซ ๋ถ์ฉ์ด(์ด์ํ ๋จ์ด) ์ ๊ณ ํ๊ธฐ") | |
| st.caption( | |
| "์ถ์ฒ ๊ฒฐ๊ณผ์ ์ด์ํ ๋จ์ด๊ฐ ์๋์? ์ ๊ณ ํด์ฃผ์๋ฉด ๋ค์๋ถํฐ ์ ์ธ๋ฉ๋๋ค.", | |
| help="ํ์ฌ ํ์ต ๋ฐ์ดํฐ์ ํฌํจ๋ ๋ถ์ฉ์ด๊ฐ ๋๋ฌด ๋ง์ ์ผ์ผ์ด ์์์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ต์ต๋๋ค. ๐ฅ ์ฌ๋ฌ๋ถ์ ์ ๊ณ ๊ฐ ๋ชจ์ด๋ฉด ๋ฐ์ดํฐ์ ํ์ง์ด ๋์์ง๊ณ ์ถ์ฒ ๊ฒฐ๊ณผ๋ ๋ ์ ํํด์ง๋๋ค. ์์คํ ๊ธฐ์ฌ ๋ถํ๋๋ฆฝ๋๋ค! ๐" | |
| ) | |
| st.info("๐ก Tip: '๊ฐ์ฅor์ง๊ฐ์ฅ' ๊ฐ์ ๊ฒฝ์ฐ 'or'๋ฅผ ์ ๊ณ ํ๋ฉด '๊ฐ์ฅ์ง๊ฐ์ฅ'์ผ๋ก ํฉ์ณ์ ธ ์ถ์ฒ์์ ์ ์ธ๋ฉ๋๋ค.") | |
| with st.form("stopword_form"): | |
| st.text_input("์ ๊ณ ํ ๋จ์ด ์ ๋ ฅ (์ผํ๋ก ๊ตฌ๋ถ)", placeholder="์: ๋ฉดํฌ, ํฉ์์ด์ , ํ ๋ฐญ", key="stopword_input_field") | |
| st.form_submit_button("์ ๊ณ ํ๊ธฐ", use_container_width=True, on_click=handle_stopword_submission) | |