refactor(poc4): modularize HMM interface

This commit is contained in:
devmrko
2026-07-22 11:01:59 +09:00
parent 8e44a59d0a
commit 8b86d58d95
4 changed files with 173 additions and 36 deletions

View File

@@ -6,3 +6,4 @@
- 외부 KB 로고·전용 글꼴 의존성을 제거하고, 애플리케이션 내부 SVG 워드마크와 해양 청색 계열로 표시한다.
- 기존 MCP, VPD, 데이터베이스 스키마 및 도구 계약은 변경하지 않는다.
- 로그인 유지 기능은 서버 비밀키로 서명한 7일 만료 토큰을 사용하며, 로그아웃 시 즉시 폐기한다.
- 화면 수정의 단일 진입점은 `src/poc4/hmm_ui.py`다. 색상·레이아웃·로그인/헤더 브랜드는 이 모듈에서만 관리한다.

View File

@@ -52,6 +52,11 @@ from src.oci_genai_sdk import (
)
from src.poc3.model_registry import load_model_registry, resolve_model_profile
from src.poc3.questions import COMMON_DEMO_QUESTIONS
from src.poc4.hmm_ui import (
apply_hmm_theme,
render_hmm_header,
render_hmm_login_brand,
)
LOG = logging.getLogger(__name__)
@@ -1359,6 +1364,89 @@ html, body, [data-testid="stAppViewContainer"], .stApp {
</style>
"""
HMM_CLEAN_THEME_CSS = """
<style>
:root {
--hmm-navy: #003b70;
--hmm-ink: #172b3a;
--hmm-muted: #667785;
--hmm-line: #dfe7ed;
--hmm-surface: #ffffff;
}
html, body, [data-testid="stAppViewContainer"], .stApp {
background: var(--hmm-surface) !important;
color: var(--hmm-ink) !important;
font-family: "Noto Sans KR", "Malgun Gothic", sans-serif;
}
#MainMenu, header [data-testid="stToolbar"] { display: none !important; }
.block-container { max-width: 1180px; padding: 2rem 3rem 4rem; }
section[data-testid="stSidebar"] {
background: #ffffff !important;
border-right: 1px solid var(--hmm-line);
}
section[data-testid="stSidebar"] > div { background: #ffffff !important; }
section[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p { color: var(--hmm-ink); }
input, textarea, [data-baseweb="select"] > div {
border-radius: 4px !important;
border-color: #cfdbe4 !important;
box-shadow: none !important;
}
div[data-testid="stButton"] > button,
div[data-testid="stFormSubmitButton"] > button {
border-radius: 4px !important;
box-shadow: none !important;
}
div[data-testid="stButton"] > button[kind="primary"],
div[data-testid="stFormSubmitButton"] > button[data-testid="stBaseButton-primaryFormSubmit"] {
background: var(--hmm-navy) !important;
border-color: var(--hmm-navy) !important;
color: #ffffff !important;
}
.hmm-app-header {
display: flex;
align-items: baseline;
gap: 18px;
margin: 0 0 28px;
padding: 0 0 22px;
border-bottom: 1px solid var(--hmm-line);
}
.hmm-app-wordmark, .hmm-login-wordmark {
color: var(--hmm-navy);
font-size: 1.35rem;
font-weight: 800;
letter-spacing: 0.08em;
}
.hmm-app-header h1 { margin: 0; color: var(--hmm-ink); font-size: 1.7rem; }
.hmm-app-header p { margin: 0; color: var(--hmm-muted); font-size: 0.92rem; }
.st-key-kb_login_container { max-width: 440px; margin: 12vh auto 0; }
.kb-login-brand { text-align: left; }
.kb-login-kicker { margin-top: 12px; color: var(--hmm-muted); font-size: 0.76rem; font-weight: 700; }
.kb-login-brand h1 { margin: 12px 0 8px; color: var(--hmm-ink); font-size: 1.7rem; }
.kb-login-brand p { margin: 0 0 22px; color: var(--hmm-muted); }
.st-key-kb_login_container [data-testid="stForm"] {
padding: 0; border: 0; background: transparent; box-shadow: none;
}
.st-key-kb_login_container [data-testid="stFormSubmitButton"] button {
min-height: 44px; background: var(--hmm-navy); border-color: var(--hmm-navy);
color: #ffffff; box-shadow: none;
}
.kb-login-note { color: var(--hmm-muted); }
@media (max-width: 760px) {
.block-container { padding: 1.25rem 1.25rem 3rem; }
.hmm-app-header { display: block; }
.hmm-app-header h1 { margin-top: 10px; }
.st-key-kb_login_container { margin-top: 8vh; }
}
</style>
"""
@dataclass(frozen=True)
class McpServer:
@@ -1799,8 +1887,8 @@ def _question_label(question: object) -> str:
return f"{question_id} · {category} · {text}"
def _apply_kb_theme() -> None:
st.markdown(HMM_MINIMAL_THEME_CSS, unsafe_allow_html=True)
def _apply_hmm_theme() -> None:
apply_hmm_theme(st)
def _portal_auth_value(name: str) -> str:
@@ -1900,17 +1988,7 @@ def _clear_portal_remembered_session() -> None:
def _render_portal_login() -> None:
with st.container(key="kb_login_container"):
st.markdown(
f"""
<section class="kb-login-brand">
<div class="hmm-login-wordmark" aria-label="HMM">HMM</div>
<div class="kb-login-kicker">HMM SHIPPING &amp; LOGISTICS DEMO</div>
<h1>HMM AI 업무 에이전트</h1>
<p>사용자 인증 후 해운·물류 AI 질의와 보안 관리 기능을 이용할 수 있습니다.</p>
</section>
""",
unsafe_allow_html=True,
)
render_hmm_login_brand(st)
if not _portal_auth_configured():
st.error("포털 로그인 설정을 확인해 주세요.")
return
@@ -1963,27 +2041,8 @@ def _logout_portal() -> None:
st.rerun()
def _render_kb_header() -> None:
st.markdown(
f"""
<section class="kb-hero">
<div class="kb-hero-main">
<div class="kb-brand-row">
<div>
<div class="kb-brand-name">HMM Digital Operations Demo</div>
<div class="kb-brand-sub">Connect Values · Navigate Growth · AI 업무 질의</div>
</div>
</div>
<h1>HMM AI 업무 에이전트</h1>
<p>
사용자 권한을 기준으로 해운·물류 업무의 AI 질의 응답과 보안 관리를 수행하는
DEMO 화면입니다.
</p>
</div>
</section>
""",
unsafe_allow_html=True,
)
def _render_hmm_header() -> None:
render_hmm_header(st)
def _render_vpd_user_card(preset: VpdTokenPreset) -> None:
@@ -6859,7 +6918,7 @@ def main() -> None:
st.set_page_config(
page_title="HMM AI Operations Console", page_icon="⛴️", layout="wide"
)
_apply_kb_theme()
_apply_hmm_theme()
_restore_portal_remembered_session()
if not st.session_state.get(PORTAL_AUTHENTICATED_KEY, False):
_render_portal_login()
@@ -6923,7 +6982,7 @@ def main() -> None:
)
if st.session_state.get(query_model_profile_key) not in model_profile_keys:
st.session_state[query_model_profile_key] = default_query_model_profile
_render_kb_header()
_render_hmm_header()
with st.sidebar:
st.caption(
f"포털 사용자 · {st.session_state.get(PORTAL_AUTH_USER_KEY, '')}"

View File

@@ -0,0 +1 @@
"""PoC4 UI support modules."""

View File

@@ -0,0 +1,76 @@
"""Small, self-contained HMM presentation layer for the PoC4 Streamlit app."""
HMM_CLEAN_THEME_CSS = """
<style>
:root { --hmm-navy:#003b70; --hmm-ink:#172b3a; --hmm-muted:#667785; --hmm-line:#dfe7ed; }
html, body, [data-testid="stAppViewContainer"], .stApp {
background:#fff !important; color:var(--hmm-ink) !important;
font-family:"Noto Sans KR","Malgun Gothic",sans-serif;
}
#MainMenu, header [data-testid="stToolbar"] { display:none !important; }
.block-container { max-width:1180px; padding:2rem 3rem 4rem; }
section[data-testid="stSidebar"], section[data-testid="stSidebar"] > div {
background:#fff !important;
}
section[data-testid="stSidebar"] { border-right:1px solid var(--hmm-line); }
input, textarea, [data-baseweb="select"] > div {
border-radius:4px !important; border-color:#cfdbe4 !important; box-shadow:none !important;
}
div[data-testid="stButton"] > button, div[data-testid="stFormSubmitButton"] > button {
border-radius:4px !important; box-shadow:none !important;
}
div[data-testid="stButton"] > button[kind="primary"],
div[data-testid="stFormSubmitButton"] > button[data-testid="stBaseButton-primaryFormSubmit"] {
background:var(--hmm-navy) !important; border-color:var(--hmm-navy) !important; color:#fff !important;
}
.hmm-app-header { margin:0 0 28px; padding:0 0 22px; border-bottom:1px solid var(--hmm-line); }
.hmm-app-wordmark, .hmm-login-wordmark { color:var(--hmm-navy); font-size:1.35rem; font-weight:800; letter-spacing:.08em; }
.hmm-app-header h1 { margin:10px 0 8px; color:var(--hmm-ink); font-size:1.7rem; }
.hmm-app-header p { margin:0; color:var(--hmm-muted); font-size:.92rem; }
.st-key-kb_login_container { max-width:440px; margin:12vh auto 0; }
.kb-login-brand { text-align:left; }
.kb-login-kicker { margin-top:12px; color:var(--hmm-muted); font-size:.76rem; font-weight:700; }
.kb-login-brand h1 { margin:12px 0 8px; color:var(--hmm-ink); font-size:1.7rem; }
.kb-login-brand p { margin:0 0 22px; color:var(--hmm-muted); }
.st-key-kb_login_container [data-testid="stForm"] { padding:0; border:0; background:transparent; box-shadow:none; }
.st-key-kb_login_container [data-testid="stFormSubmitButton"] button {
min-height:44px; background:var(--hmm-navy); border-color:var(--hmm-navy); color:#fff; box-shadow:none;
}
.kb-login-note { color:var(--hmm-muted); }
@media (max-width:760px) {
.block-container { padding:1.25rem 1.25rem 3rem; }
.st-key-kb_login_container { margin-top:8vh; }
}
</style>
"""
def apply_hmm_theme(st: object) -> None:
st.markdown(HMM_CLEAN_THEME_CSS, unsafe_allow_html=True)
def render_hmm_login_brand(st: object) -> None:
st.markdown(
"""
<section class="kb-login-brand">
<div class="hmm-login-wordmark" aria-label="HMM">HMM</div>
<div class="kb-login-kicker">HMM SHIPPING &amp; LOGISTICS DEMO</div>
<h1>HMM AI 업무 에이전트</h1>
<p>사용자 인증 후 해운·물류 AI 질의와 보안 관리 기능을 이용할 수 있습니다.</p>
</section>
""",
unsafe_allow_html=True,
)
def render_hmm_header(st: object) -> None:
st.markdown(
"""
<section class="hmm-app-header">
<div class="hmm-app-wordmark">HMM</div>
<h1>AI 업무 에이전트</h1>
<p>사용자 권한에 맞는 업무 질의와 보안 관리 기능을 제공합니다.</p>
</section>
""",
unsafe_allow_html=True,
)