fix(dashboard): keep shell during menu transitions

Replace document navigation with native Streamlit state transitions, preserve dashboard contracts, and add regression plus authenticated browser evidence for Issue #9.
This commit is contained in:
2026-10-05 00:49:28 +00:00
parent ffd943fe65
commit 96e11812ce
13 changed files with 2166 additions and 51 deletions
+42 -8
View File
@@ -4,7 +4,6 @@ import sqlite3
from datetime import datetime
from html import escape
from typing import Any
from urllib.parse import quote
import importlib
import inspect
from zoneinfo import ZoneInfo
@@ -314,6 +313,9 @@ def inject_app_css() -> None:
border-color: rgba(37, 99, 235, 0.48);
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10);
}
.st-key-dashboard_body:has(.st-key-dashboard_loading [data-testid="stAlert"]) .st-key-dashboard_content {
display: none;
}
.jarca-card {
border: 1px solid rgba(226, 232, 240, 0.86);
border-radius: 22px;
@@ -355,13 +357,45 @@ def render_section_label(icon: str, label: str) -> None:
st.markdown(f'<div class="jarca-section-label"><span>{escape(icon)}</span><span>{escape(label)}</span></div>', unsafe_allow_html=True)
def render_sidebar_nav(pages: list[str], current_page: str) -> None:
items = []
for name in pages:
active = " active" if name == current_page else ""
href = f"?page={quote(name)}"
items.append(f'<a class="jarca-nav-item{active}" href="{href}" target="_self">{escape(name)}</a>')
st.markdown(f'<div class="jarca-nav-list">{"".join(items)}</div>', unsafe_allow_html=True)
def validate_dashboard_page(page: str | None, pages: list[str]) -> str:
return page if page in pages else "요약"
def _sync_dashboard_page_query() -> None:
st.query_params["page"] = st.session_state["dashboard_page"]
def render_sidebar_nav(pages: list[str], current_page: str) -> str:
if "dashboard_page" not in st.session_state:
st.session_state["dashboard_page"] = current_page
return st.radio(
"메뉴",
pages,
key="dashboard_page",
label_visibility="collapsed",
on_change=_sync_dashboard_page_query,
)
def render_dashboard_body(page: str, renderer) -> None:
body = st.empty()
body.empty()
try:
with body.container():
with st.container(key="dashboard_body"):
loading = st.empty()
with loading.container():
with st.container(key="dashboard_loading"):
st.info(f"{page} 불러오는 중…")
with st.container(key="dashboard_content"):
renderer()
loading.empty()
except Exception:
body.empty()
with body.container():
st.error("페이지를 불러오지 못했습니다. 다시 시도해 주세요.")
if st.button("다시 시도", key=f"retry_{page}"):
st.rerun()
def parse_optional_int(value: str, field_name: str) -> int | None: