Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -49,8 +49,6 @@ import requests
|
|
| 49 |
from streamlit.components.v1 import html
|
| 50 |
from css import render_message, render_chip_buttons, log_and_render, replay_log, _get_colors
|
| 51 |
|
| 52 |
-
#st.success("🎉 앱이 성공적으로 시작되었습니다! 라이브러리 설치 성공!")
|
| 53 |
-
|
| 54 |
# ──────────────────────────────── Dataset Repo 설정 ────────────────────────────────
|
| 55 |
HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data")
|
| 56 |
HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main")
|
|
@@ -171,6 +169,7 @@ Return ONLY a valid JSON object:
|
|
| 171 |
}
|
| 172 |
If unknown, use "none" or "" and NEVER add extra text outside JSON.
|
| 173 |
"""
|
|
|
|
| 174 |
def to_llm_mode():
|
| 175 |
# 같은 렌더 사이클에서 여러 번 호출되어도 1회만 동작하게 가드
|
| 176 |
if not st.session_state.get("_llm_triggered"):
|
|
@@ -179,10 +178,11 @@ def to_llm_mode():
|
|
| 179 |
st.session_state["llm_intro_needed"] = True
|
| 180 |
st.rerun()
|
| 181 |
|
|
|
|
| 182 |
def _ensure_llm_state():
|
| 183 |
-
st.session_state.setdefault("llm_mode", False)
|
| 184 |
-
st.session_state.setdefault("llm_inline", False)
|
| 185 |
-
st.session_state.setdefault("llm_history", [])
|
| 186 |
st.session_state.setdefault("llm_intro_needed", False)
|
| 187 |
st.session_state.setdefault("llm_input", "")
|
| 188 |
|
|
@@ -245,13 +245,6 @@ def _llm_structured_extract(user_text: str):
|
|
| 245 |
data.setdefault("notes", "")
|
| 246 |
return data
|
| 247 |
|
| 248 |
-
# ──────────────────────────────── Streamlit용 LLM 모드 UI ────────────────────────────────
|
| 249 |
-
def _ensure_llm_state():
|
| 250 |
-
st.session_state.setdefault("llm_mode", False)
|
| 251 |
-
st.session_state.setdefault("llm_history", []) # [{'role':'user'|'assistant', 'content': str}, ...]
|
| 252 |
-
st.session_state.setdefault("llm_intro_needed", False)
|
| 253 |
-
st.session_state.setdefault("llm_input", "")
|
| 254 |
-
|
| 255 |
def render_llm_followup(chat_container, inline=False):
|
| 256 |
_ensure_llm_state()
|
| 257 |
MAX_TURNS = 6
|
|
@@ -313,6 +306,8 @@ def render_llm_followup(chat_container, inline=False):
|
|
| 313 |
log_and_render(q, sender="user", chat_container=chat_container,
|
| 314 |
key=f"llm_user_{random.randint(1,999999)}")
|
| 315 |
st.session_state.llm_history.append({"role": "user", "content": q})
|
|
|
|
|
|
|
| 316 |
|
| 317 |
msgs = st.session_state.llm_history[-(MAX_TURNS-1):]
|
| 318 |
a = _call_ollama_chat(
|
|
@@ -321,13 +316,19 @@ def render_llm_followup(chat_container, inline=False):
|
|
| 321 |
temperature=0.8, top_p=0.9, top_k=40, repeat_penalty=1.1
|
| 322 |
)
|
| 323 |
if not a:
|
| 324 |
-
|
| 325 |
-
|
| 326 |
key=f"llm_err_{random.randint(1,999999)}")
|
|
|
|
|
|
|
| 327 |
else:
|
|
|
|
| 328 |
log_and_render(a, sender="bot", chat_container=chat_container,
|
| 329 |
key=f"llm_bot_{random.randint(1,999999)}")
|
| 330 |
st.session_state.llm_history.append({"role": "assistant", "content": a})
|
|
|
|
|
|
|
|
|
|
| 331 |
st.session_state["llm_input"] = ""
|
| 332 |
|
| 333 |
# 하단 버튼: 인라인은 'LLM 패널 종료'만, 풀스크린은 'LLM 모드 종료'만
|
|
@@ -339,7 +340,6 @@ def render_llm_followup(chat_container, inline=False):
|
|
| 339 |
st.session_state["llm_mode"] = False
|
| 340 |
st.rerun()
|
| 341 |
|
| 342 |
-
|
| 343 |
# 지연 초기화: import 시점에는 데이터 접근 금지, 여기서 한 번만 주입
|
| 344 |
init_datasets(
|
| 345 |
travel_df=travel_df,
|
|
@@ -350,6 +350,7 @@ init_datasets(
|
|
| 350 |
master_df=master_df,
|
| 351 |
theme_title_phrases=theme_title_phrases,
|
| 352 |
)
|
|
|
|
| 353 |
# ───────────────────────────────────── streamlit용 함수
|
| 354 |
def init_session():
|
| 355 |
if "chat_log" not in st.session_state:
|
|
@@ -377,9 +378,6 @@ st.markdown(
|
|
| 377 |
unsafe_allow_html=True,
|
| 378 |
)
|
| 379 |
|
| 380 |
-
# 고정 이미지 URL
|
| 381 |
-
#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"
|
| 382 |
-
|
| 383 |
# === 배경 설정 UI (수정됨) ===
|
| 384 |
st.sidebar.subheader("🎨 배경 설정")
|
| 385 |
st.sidebar.toggle("배경 이미지 사용", key="bg_on", value=True)
|
|
@@ -413,7 +411,6 @@ else:
|
|
| 413 |
value=palette[selected_color_name]
|
| 414 |
)
|
| 415 |
|
| 416 |
-
|
| 417 |
def apply_background():
|
| 418 |
# 보호: 기존 ::before 배경이 있으면 끄기 (겹침/끊김 방지)
|
| 419 |
base_reset_css = """
|
|
@@ -481,8 +478,6 @@ def apply_background():
|
|
| 481 |
# 함수 호출
|
| 482 |
apply_background()
|
| 483 |
|
| 484 |
-
|
| 485 |
-
|
| 486 |
# ── P 글꼴 크기 14 px ───────────────────────────────────
|
| 487 |
st.markdown("""
|
| 488 |
<style>
|
|
@@ -517,7 +512,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 517 |
st.session_state[prev_key] = set()
|
| 518 |
st.session_state.pop(sample_key, None)
|
| 519 |
|
| 520 |
-
|
| 521 |
# ────────────────── 1) restart 상태면 인트로만 출력하고 종료
|
| 522 |
if st.session_state[step_key] == "restart":
|
| 523 |
log_and_render(
|
|
@@ -569,7 +563,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 569 |
st.rerun()
|
| 570 |
return
|
| 571 |
|
| 572 |
-
|
| 573 |
# 2.4) 샘플링 (이전 샘플이 없거나 비어 있으면 새로 추출)
|
| 574 |
if sample_key not in st.session_state or st.session_state[sample_key].empty:
|
| 575 |
sampled = remaining.sample(
|
|
@@ -629,7 +622,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 629 |
st.session_state[step_key] = "detail"
|
| 630 |
st.session_state.chat_log.append(("user", choice))
|
| 631 |
|
| 632 |
-
|
| 633 |
# 실제로 선택된 여행지만 prev에 기록
|
| 634 |
match = sampled[sampled["여행지"] == choice]
|
| 635 |
if not match.empty:
|
|
@@ -707,7 +699,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 707 |
st.stop()
|
| 708 |
return
|
| 709 |
|
| 710 |
-
|
| 711 |
# ────────────────── 4) 여행지 상세 단계
|
| 712 |
if st.session_state[step_key] == "detail":
|
| 713 |
chosen = st.session_state[region_key]
|
|
@@ -732,7 +723,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 732 |
st.rerun()
|
| 733 |
return
|
| 734 |
|
| 735 |
-
|
| 736 |
# ────────────────── 5) 동행·연령 받기 단계
|
| 737 |
elif st.session_state[step_key] == "companion":
|
| 738 |
with chat_container:
|
|
@@ -803,7 +793,6 @@ def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 803 |
st.rerun()
|
| 804 |
return
|
| 805 |
|
| 806 |
-
|
| 807 |
# ────────────────── 6) 동행·연령 필터링· 패키지 출력 단계
|
| 808 |
elif st.session_state[step_key] == "package":
|
| 809 |
|
|
@@ -1234,13 +1223,13 @@ def intent_ui(travel_df, external_score_df, festival_df, weather_df, package_df,
|
|
| 1234 |
st.session_state[step_key] = "package_end"
|
| 1235 |
show_llm_inline() # 플래그만 ON (rerun 없음)
|
| 1236 |
render_llm_followup(chat_container, inline=True) # 👈 같은 사이클에서 바로 아래에 LLM 박스 출력
|
| 1237 |
-
return
|
| 1238 |
|
| 1239 |
# ────────────────── 7) 종료 단계
|
| 1240 |
elif st.session_state[step_key] == "package_end":
|
| 1241 |
log_and_render("필요하실 때 언제든지 또 찾아주세요! ✈️",
|
| 1242 |
sender="bot", chat_container=chat_container,
|
| 1243 |
-
key="goodbye")
|
| 1244 |
|
| 1245 |
to_llm_mode()
|
| 1246 |
|
|
@@ -1263,7 +1252,6 @@ def emotion_ui(travel_df, external_score_df, festival_df, weather_df, package_df
|
|
| 1263 |
st.session_state[prev_key] = set()
|
| 1264 |
st.session_state.pop(sample_key, None)
|
| 1265 |
|
| 1266 |
-
|
| 1267 |
# ────────────────── 1) restart 상태면 인트로만 출력하고 종료
|
| 1268 |
if st.session_state[step_key] == "restart":
|
| 1269 |
log_and_render(
|
|
@@ -1692,19 +1680,7 @@ def unknown_ui(country, city, chat_container, log_and_render):
|
|
| 1692 |
chat_container=chat_container,
|
| 1693 |
key="unknown_dest"
|
| 1694 |
)
|
| 1695 |
-
|
| 1696 |
-
# def _get_active_step_key():
|
| 1697 |
-
# mode = st.session_state.get("mode", "unknown")
|
| 1698 |
-
# mapping = {
|
| 1699 |
-
# "region": "region_step",
|
| 1700 |
-
# "intent": "intent_step",
|
| 1701 |
-
# "emotion": "emotion_step",
|
| 1702 |
-
# "theme_selection": "theme_step",
|
| 1703 |
-
# "place_selection": "place_step",
|
| 1704 |
-
# "user_info_input": "user_info_step",
|
| 1705 |
-
# }
|
| 1706 |
-
# # 매핑에 없으면 공용 키로
|
| 1707 |
-
# return mapping.get(mode, "flow_step")
|
| 1708 |
# ───────────────────────────────────── 챗봇 호출
|
| 1709 |
def main():
|
| 1710 |
|
|
@@ -1721,12 +1697,6 @@ def main():
|
|
| 1721 |
st.sidebar.selectbox("테마", ["피스타치오", "스카이블루", "크리미오트"], key="bubble_theme")
|
| 1722 |
st.sidebar.toggle("타임스탬프 표시", value=False, key="show_time")
|
| 1723 |
|
| 1724 |
-
# with st.sidebar.expander("DEBUG steps", expanded=False):
|
| 1725 |
-
# st.write("mode:", st.session_state.get("mode"))
|
| 1726 |
-
# st.write("step_key:", cur_step_key)
|
| 1727 |
-
# st.write("state:", st.session_state.get(cur_step_key))
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
# ✅ 타자 효과 on/off 토글 (기본 ON)
|
| 1731 |
st.sidebar.toggle("타자 효과", value=False, key="typewriter_on")
|
| 1732 |
|
|
@@ -1747,7 +1717,6 @@ def main():
|
|
| 1747 |
)
|
| 1748 |
st.session_state["greeting_rendered"] = True
|
| 1749 |
|
| 1750 |
-
|
| 1751 |
# ───── 사용자 입력 & 추천 시작
|
| 1752 |
# 1) 사용자 입력
|
| 1753 |
user_input = st.text_input(
|
|
@@ -1778,7 +1747,6 @@ def main():
|
|
| 1778 |
sender="user",
|
| 1779 |
chat_container = chat_container,
|
| 1780 |
key=f"user_input_{user_input}"
|
| 1781 |
-
|
| 1782 |
)
|
| 1783 |
st.session_state["user_input_rendered"] = True
|
| 1784 |
|
|
@@ -1803,7 +1771,6 @@ def main():
|
|
| 1803 |
else:
|
| 1804 |
mode = "emotion"
|
| 1805 |
# 3) 고비용 단계: 정말 필요할 때만 감성(BERT) 실행
|
| 1806 |
-
# with st.spinner("감정 분석 중..."): # UX 원하시면 스피너 추가
|
| 1807 |
top_emotions, emotion_groups = analyze_emotion(user_input)
|
| 1808 |
|
| 1809 |
# 4) 모드별 분기 (필요한 계산만 수행)
|
|
|
|
| 49 |
from streamlit.components.v1 import html
|
| 50 |
from css import render_message, render_chip_buttons, log_and_render, replay_log, _get_colors
|
| 51 |
|
|
|
|
|
|
|
| 52 |
# ──────────────────────────────── Dataset Repo 설정 ────────────────────────────────
|
| 53 |
HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data")
|
| 54 |
HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main")
|
|
|
|
| 169 |
}
|
| 170 |
If unknown, use "none" or "" and NEVER add extra text outside JSON.
|
| 171 |
"""
|
| 172 |
+
|
| 173 |
def to_llm_mode():
|
| 174 |
# 같은 렌더 사이클에서 여러 번 호출되어도 1회만 동작하게 가드
|
| 175 |
if not st.session_state.get("_llm_triggered"):
|
|
|
|
| 178 |
st.session_state["llm_intro_needed"] = True
|
| 179 |
st.rerun()
|
| 180 |
|
| 181 |
+
# ──────────────────────────────── LLM 상태: 단일 정의 (중복 제거) ────────────────────────────────
|
| 182 |
def _ensure_llm_state():
|
| 183 |
+
st.session_state.setdefault("llm_mode", False) # 풀스크린 LLM 모드
|
| 184 |
+
st.session_state.setdefault("llm_inline", False) # 인라인 LLM 패널 표시 여부
|
| 185 |
+
st.session_state.setdefault("llm_history", []) # LLM 대화 원본
|
| 186 |
st.session_state.setdefault("llm_intro_needed", False)
|
| 187 |
st.session_state.setdefault("llm_input", "")
|
| 188 |
|
|
|
|
| 245 |
data.setdefault("notes", "")
|
| 246 |
return data
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
def render_llm_followup(chat_container, inline=False):
|
| 249 |
_ensure_llm_state()
|
| 250 |
MAX_TURNS = 6
|
|
|
|
| 306 |
log_and_render(q, sender="user", chat_container=chat_container,
|
| 307 |
key=f"llm_user_{random.randint(1,999999)}")
|
| 308 |
st.session_state.llm_history.append({"role": "user", "content": q})
|
| 309 |
+
# ✅ 재실행 후 복원을 위해 chat_log에도 저장
|
| 310 |
+
st.session_state.chat_log.append(("user", q))
|
| 311 |
|
| 312 |
msgs = st.session_state.llm_history[-(MAX_TURNS-1):]
|
| 313 |
a = _call_ollama_chat(
|
|
|
|
| 316 |
temperature=0.8, top_p=0.9, top_k=40, repeat_penalty=1.1
|
| 317 |
)
|
| 318 |
if not a:
|
| 319 |
+
err = "⚠️ LLM 응답을 받지 못했습니다. Ollama 서버를 확인해 주세요."
|
| 320 |
+
log_and_render(err, sender="bot", chat_container=chat_container,
|
| 321 |
key=f"llm_err_{random.randint(1,999999)}")
|
| 322 |
+
# ✅ 에러도 chat_log에 저장
|
| 323 |
+
st.session_state.chat_log.append(("bot", err))
|
| 324 |
else:
|
| 325 |
+
# 2) 봇 버블
|
| 326 |
log_and_render(a, sender="bot", chat_container=chat_container,
|
| 327 |
key=f"llm_bot_{random.randint(1,999999)}")
|
| 328 |
st.session_state.llm_history.append({"role": "assistant", "content": a})
|
| 329 |
+
# ✅ 답변도 chat_log에 저장
|
| 330 |
+
st.session_state.chat_log.append(("bot", a))
|
| 331 |
+
# 입력칸 초기화 (원하지 않으면 이 줄을 주석 처리)
|
| 332 |
st.session_state["llm_input"] = ""
|
| 333 |
|
| 334 |
# 하단 버튼: 인라인은 'LLM 패널 종료'만, 풀스크린은 'LLM 모드 종료'만
|
|
|
|
| 340 |
st.session_state["llm_mode"] = False
|
| 341 |
st.rerun()
|
| 342 |
|
|
|
|
| 343 |
# 지연 초기화: import 시점에는 데이터 접근 금지, 여기서 한 번만 주입
|
| 344 |
init_datasets(
|
| 345 |
travel_df=travel_df,
|
|
|
|
| 350 |
master_df=master_df,
|
| 351 |
theme_title_phrases=theme_title_phrases,
|
| 352 |
)
|
| 353 |
+
|
| 354 |
# ───────────────────────────────────── streamlit용 함수
|
| 355 |
def init_session():
|
| 356 |
if "chat_log" not in st.session_state:
|
|
|
|
| 378 |
unsafe_allow_html=True,
|
| 379 |
)
|
| 380 |
|
|
|
|
|
|
|
|
|
|
| 381 |
# === 배경 설정 UI (수정됨) ===
|
| 382 |
st.sidebar.subheader("🎨 배경 설정")
|
| 383 |
st.sidebar.toggle("배경 이미지 사용", key="bg_on", value=True)
|
|
|
|
| 411 |
value=palette[selected_color_name]
|
| 412 |
)
|
| 413 |
|
|
|
|
| 414 |
def apply_background():
|
| 415 |
# 보호: 기존 ::before 배경이 있으면 끄기 (겹침/끊김 방지)
|
| 416 |
base_reset_css = """
|
|
|
|
| 478 |
# 함수 호출
|
| 479 |
apply_background()
|
| 480 |
|
|
|
|
|
|
|
| 481 |
# ── P 글꼴 크기 14 px ───────────────────────────────────
|
| 482 |
st.markdown("""
|
| 483 |
<style>
|
|
|
|
| 512 |
st.session_state[prev_key] = set()
|
| 513 |
st.session_state.pop(sample_key, None)
|
| 514 |
|
|
|
|
| 515 |
# ────────────────── 1) restart 상태면 인트로만 출력하고 종료
|
| 516 |
if st.session_state[step_key] == "restart":
|
| 517 |
log_and_render(
|
|
|
|
| 563 |
st.rerun()
|
| 564 |
return
|
| 565 |
|
|
|
|
| 566 |
# 2.4) 샘플링 (이전 샘플이 없거나 비어 있으면 새로 추출)
|
| 567 |
if sample_key not in st.session_state or st.session_state[sample_key].empty:
|
| 568 |
sampled = remaining.sample(
|
|
|
|
| 622 |
st.session_state[step_key] = "detail"
|
| 623 |
st.session_state.chat_log.append(("user", choice))
|
| 624 |
|
|
|
|
| 625 |
# 실제로 선택된 여행지만 prev에 기록
|
| 626 |
match = sampled[sampled["여행지"] == choice]
|
| 627 |
if not match.empty:
|
|
|
|
| 699 |
st.stop()
|
| 700 |
return
|
| 701 |
|
|
|
|
| 702 |
# ────────────────── 4) 여행지 상세 단계
|
| 703 |
if st.session_state[step_key] == "detail":
|
| 704 |
chosen = st.session_state[region_key]
|
|
|
|
| 723 |
st.rerun()
|
| 724 |
return
|
| 725 |
|
|
|
|
| 726 |
# ────────────────── 5) 동행·연령 받기 단계
|
| 727 |
elif st.session_state[step_key] == "companion":
|
| 728 |
with chat_container:
|
|
|
|
| 793 |
st.rerun()
|
| 794 |
return
|
| 795 |
|
|
|
|
| 796 |
# ────────────────── 6) 동행·연령 필터링· 패키지 출력 단계
|
| 797 |
elif st.session_state[step_key] == "package":
|
| 798 |
|
|
|
|
| 1223 |
st.session_state[step_key] = "package_end"
|
| 1224 |
show_llm_inline() # 플래그만 ON (rerun 없음)
|
| 1225 |
render_llm_followup(chat_container, inline=True) # 👈 같은 사이클에서 바로 아래에 LLM 박스 출력
|
| 1226 |
+
return
|
| 1227 |
|
| 1228 |
# ────────────────── 7) 종료 단계
|
| 1229 |
elif st.session_state[step_key] == "package_end":
|
| 1230 |
log_and_render("필요하실 때 언제든지 또 찾아주세요! ✈️",
|
| 1231 |
sender="bot", chat_container=chat_container,
|
| 1232 |
+
key="goodbye")
|
| 1233 |
|
| 1234 |
to_llm_mode()
|
| 1235 |
|
|
|
|
| 1252 |
st.session_state[prev_key] = set()
|
| 1253 |
st.session_state.pop(sample_key, None)
|
| 1254 |
|
|
|
|
| 1255 |
# ────────────────── 1) restart 상태면 인트로만 출력하고 종료
|
| 1256 |
if st.session_state[step_key] == "restart":
|
| 1257 |
log_and_render(
|
|
|
|
| 1680 |
chat_container=chat_container,
|
| 1681 |
key="unknown_dest"
|
| 1682 |
)
|
| 1683 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1684 |
# ───────────────────────────────────── 챗봇 호출
|
| 1685 |
def main():
|
| 1686 |
|
|
|
|
| 1697 |
st.sidebar.selectbox("테마", ["피스타치오", "스카이블루", "크리미오트"], key="bubble_theme")
|
| 1698 |
st.sidebar.toggle("타임스탬프 표시", value=False, key="show_time")
|
| 1699 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1700 |
# ✅ 타자 효과 on/off 토글 (기본 ON)
|
| 1701 |
st.sidebar.toggle("타자 효과", value=False, key="typewriter_on")
|
| 1702 |
|
|
|
|
| 1717 |
)
|
| 1718 |
st.session_state["greeting_rendered"] = True
|
| 1719 |
|
|
|
|
| 1720 |
# ───── 사용자 입력 & 추천 시작
|
| 1721 |
# 1) 사용자 입력
|
| 1722 |
user_input = st.text_input(
|
|
|
|
| 1747 |
sender="user",
|
| 1748 |
chat_container = chat_container,
|
| 1749 |
key=f"user_input_{user_input}"
|
|
|
|
| 1750 |
)
|
| 1751 |
st.session_state["user_input_rendered"] = True
|
| 1752 |
|
|
|
|
| 1771 |
else:
|
| 1772 |
mode = "emotion"
|
| 1773 |
# 3) 고비용 단계: 정말 필요할 때만 감성(BERT) 실행
|
|
|
|
| 1774 |
top_emotions, emotion_groups = analyze_emotion(user_input)
|
| 1775 |
|
| 1776 |
# 4) 모드별 분기 (필요한 계산만 수행)
|