--- { "schema": "gitea-agent.requirement/v1", "id": "REQ-002", "title": "Dashboard 메뉴 전환 시 공통 화면 연속성", "revision": "1.0", "status": "approved", "depends_on": ["REQ-001"], "applies_to": ["dashboard"], "language": "ko" } --- # REQ-002 — Dashboard 메뉴 전환 시 공통 화면 연속성 ## 1. 목적 인증된 Dashboard에서 메뉴를 바꿀 때 문서 전체를 이동시키지 않고 공통 shell과 배경을 유지하며 body만 교체한다. 현재 메뉴 선택, URL 상태, 느린 렌더링, 렌더링 실패의 동작을 예측 가능하게 만든다. ## 2. 배경 Issue #9의 Dashboard 메뉴 전환 계약이다. REQ-001의 인증 경계, cookie, logout, page dispatch와 기존 금융 페이지 계약을 유지한 채 Streamlit 메뉴 전환을 연속적인 단일 화면으로 제공한다. ## 3. 범위 ### 포함 - Streamlit `st.radio` 기반 메뉴의 정확한 10개 label/order와 selected state - 메뉴 클릭 시 document navigation 없이 공통 shell/background를 유지하고 body만 교체 - 느린 대상 page 렌더링 중 대상별 loading 표시 - renderer 예외의 local generic error와 retry - 빠른 연속 선택에서 latest serialized selection wins - 직접 `?page=` 접근, 누락/unknown page의 `요약` fallback, sibling query key 보존 - 기존 auth gate, cookie, logout, finance page, cache-clear 회귀 검증 ### 제외 - browser history/back-forward 동작, history API 계약, path URL 설계 - 인증 방식·cookie 속성·MCP transport·SQLite schema 변경 - page renderer의 금융 계산·gateway 의미 변경 - 별도 document route 또는 full-document navigation 구현 ## 4. 용어와 기준 - **공통 shell**: 인증 후 `dashboard/app.py`가 제공하는 sidebar, 메뉴, refresh control 및 앱 배경/CSS. - **body**: 선택된 page renderer가 공통 shell 안에서 채우는 주 콘텐츠 영역. - **latest serialized selection**: Streamlit rerun/콜백 직렬화 순서에서 마지막으로 반영된 `dashboard_page` 선택. - **AppTest**: Streamlit `streamlit.testing.v1.AppTest`의 synthetic runtime 검증. DOM, 실제 browser document navigation, 실제 paint/지연 체감은 검증하지 못한다. - **real-browser evidence**: 실제 browser에서 authenticated session으로 메뉴를 클릭하고 URL/document/body/shell을 관찰해 남기는 증거. AppTest를 대체하지 않으며 AppTest만으로 충족할 수 없는 browser-level 항목에 필수다. ## 5. 요구사항 ### REQ-002.01 — 메뉴 정체성 메뉴는 정확히 다음 순서와 label을 유지한다: `월정산`, `요약`, `자산/마감`, `거래내역`, `확인필요 큐`, `미분류`, `분류 규칙`, `카테고리`, `계좌`, `Import`. selected state는 현재 page와 일치한다. **Impact** - Interfaces: `dashboard_page` radio and `page` query parameter - Persistence: none - Consumers: dashboard page renderers - Migration: none - Compatibility: existing labels, order, and selected-state contract - Deployment prerequisites: none ### REQ-002.02 — URL과 직접 접근 메뉴 선택은 query의 `page`만 대상 label로 갱신하고 `month`, `account_id`, `filter` 등 sibling query key를 보존한다. 직접 `?page=`은 해당 page를 렌더링하고, missing/unknown 값은 `요약`으로 fallback한다. **Impact** - Interfaces: browser query parameters - Persistence: none - Consumers: direct links and page renderers - Migration: none - Compatibility: sibling query keys and direct page access - Deployment prerequisites: none ### REQ-002.03 — 연속 화면 메뉴 클릭은 document navigation을 일으키지 않는다. 공통 shell과 background는 유지하고 body만 선택된 renderer의 내용으로 교체한다. history/back-forward와 path URL은 이 요구사항의 판정 범위가 아니다. **Impact** - Interfaces: Streamlit shell/body rendering - Persistence: none - Consumers: authenticated dashboard users - Migration: none - Compatibility: existing shell, background, and path behavior - Deployment prerequisites: none ### REQ-002.04 — 전환 중 표시 느린 renderer가 실행되는 동안 target page label을 포함한 `불러오는 중…` loading을 대상 body 영역에 표시하고, renderer 완료 후 loading placeholder를 제거한다. **Impact** - Interfaces: target body loading state - Persistence: none - Consumers: dashboard users - Migration: none - Compatibility: existing renderer output after completion - Deployment prerequisites: none ### REQ-002.05 — local failure와 retry target renderer 예외는 공통 shell을 무너뜨리지 않고 body 영역을 generic error와 `다시 시도` control로 교체한다. 예외 상세·secret은 UI에 노출하지 않는다. **Impact** - Interfaces: body error and retry control - Persistence: none - Consumers: dashboard users and support operators - Migration: none - Compatibility: shell remains available; secrets remain redacted - Deployment prerequisites: none ### REQ-002.06 — 최신 선택 우선 연속 메뉴 선택은 serialized callback 순서의 마지막 선택을 query와 body에 반영하며 이전 선택이 마지막 상태를 덮어쓰지 않는다. **Impact** - Interfaces: serialized menu callback and query state - Persistence: none - Consumers: page selection state - Migration: none - Compatibility: latest-selection ordering contract - Deployment prerequisites: none ### REQ-002.07 — 인증 경계 보존 모든 menu/page dispatch는 REQ-001 gate와 cookie 검증 뒤에만 실행된다. no-cookie/invalid-cookie는 page body에 도달하지 않으며 valid cookie는 기존 Dashboard에 접근한다. **Impact** - Interfaces: REQ-001 authentication gate and cookie verification - Persistence: none - Consumers: authenticated dashboard dispatch - Migration: none - Compatibility: REQ-001 expiry, rotation, cleanup, and logout behavior - Deployment prerequisites: existing REQ-001 settings ### REQ-002.08 — 금융 page 회귀 없음 요약, 자산/마감, 분류 규칙, 확인필요 큐, 월정산의 기존 synthetic fixture DB 핵심 내용과 기존 page query 계약을 유지한다. **Impact** - Interfaces: existing page/query contracts - Persistence: existing synthetic fixture DB reads and writes - Consumers: financial page renderers - Migration: none - Compatibility: existing financial calculations and gateway semantics - Deployment prerequisites: existing fixture and page setup ### REQ-002.09 — 운영 control 회귀 없음 refresh control은 기존 `clear_cache` callable 계약을 유지하고, 메뉴 전환은 cache-clear 동작을 변경하지 않는다. **Impact** - Interfaces: `clear_cache` callable and refresh control - Persistence: none - Consumers: dashboard operators - Migration: none - Compatibility: existing cache-clear behavior - Deployment prerequisites: none ## 6. 제약과 불변조건 - `dashboard/app.py`의 auth gate와 page dispatch는 유지하며 인증 전에 DB path를 resolve하지 않는다. - 메뉴 클릭은 full document reload 또는 path navigation을 추가하지 않는다. - body 교체는 stable `st.empty` container 안에서 수행하며 공통 shell/background를 삭제하지 않는다. - loading/error/retry는 target body에 국소적으로 표시하고 generic error 외 renderer exception 내용을 공개하지 않는다. - query update는 `page` key만 변경하며 sibling page-specific key를 삭제하거나 정규화하지 않는다. - REQ-001의 auth/cookie/logout, financial gateway/page, cache-clear, MCP 및 SQLite semantics는 변경하지 않는다. ## 7. 인수 기준 각 기준은 하나의 명시된 test/evidence target으로 판정한다. AppTest는 synthetic runtime 판정이며 real-browser evidence가 필요한 기준을 대신하지 않는다. ### REQ-002.01-AC01 — label/order/selected **Cases**: positive, unchanged-state **State Invariants**: labels/order and selected state remain exact. **판정 기준** 정확한 10개 label과 순서가 radio options로 전달되고 `dashboard_page` selected state가 유지된다. **검증** `tests/test_dashboard_menu_transition.py::test_dashboard_menu_labels_and_order_are_preserved` **검증 분류** - 수준: unit - 백엔드: pytest monkeypatch - 증거: radio options, key, label visibility assertion ### REQ-002.02-AC01 — query와 fallback **Cases**: positive, boundary, unchanged-state **State Invariants**: only `page` changes; sibling query keys remain unchanged. **판정 기준** callback은 `page`만 갱신하고 sibling key를 보존하며, missing/unknown은 `요약`, known direct query는 요청 page를 선택한다. **검증** `tests/test_dashboard_menu_transition.py::test_sidebar_selection_updates_only_page_query_parameter`, `tests/test_dashboard_menu_transition.py::test_missing_or_unknown_page_falls_back_to_overview`, `tests/test_dashboard_menu_transition.py::test_direct_page_query_access_renders_requested_page` **검증 분류** - 수준: unit/integration - 백엔드: pytest monkeypatch - 증거: exact query mapping and selected renderer assertion ### REQ-002.03-AC01 — body-only transition **Cases**: positive, unchanged-state **State Invariants**: document, shell, and background remain stable; body changes only. **판정 기준** 실제 browser에서 authenticated menu click 후 document URL path와 full-document identity가 바뀌지 않고 공통 sidebar/background가 남으며 target body만 바뀐다. history/back-forward와 path URL은 판정하지 않는다. **검증** Required real-browser evidence: `docs/evidence/issue-9-dashboard-menu-transition/`의 authenticated `요약` → `분류 규칙` native click 연속 frame, screenshot, DOM, URL, document/sidebar identity와 background 관찰. `manifest.json`은 정확한 working-tree file hash, runtime viewport·interval·환경·제약, artifact hash 및 validator exit code를 기록한다. 이 실행은 local Chrome 부재 때문에 Playwright MCP browser runner로 수집했으며, 재현 가능한 local fail-closed harness는 `scripts/streamlit_browser_evidence.py --mode transition`과 `tests/test_streamlit_browser_evidence.py`로 유지한다. AppTest companion: `tests/test_dashboard_menu_transition.py::test_streamlit_menu_click_updates_body_and_preserves_page_query_siblings`. **검증 분류** - 수준: browser evidence + integration companion - 백엔드: real browser, Streamlit AppTest - 한계: AppTest는 실제 document navigation, browser paint, focus/scroll, network request, perceived continuity를 증명하지 못한다. ### REQ-002.04-AC01 — target loading **Cases**: positive, boundary **State Invariants**: loading appears before completion and is removed after rendering. **판정 기준** slow renderer 동안 target label loading이 먼저 표시되고 완료 후 placeholder가 제거된다. **검증** `tests/test_dashboard_menu_transition.py::test_slow_transition_clears_previous_body_and_shows_target_loading`; real-browser frames: `docs/evidence/issue-9-dashboard-menu-transition/browser-observations.json` **검증 분류** - 수준: unit + browser evidence - 백엔드: pytest fake Streamlit placeholder, authenticated Playwright browser - 증거: ordered event list와 50 ms 연속 frame에서 `loading=true`, `previous_body_visible=false`, 최종 `loading=false` - 한계: browser evidence는 실제 authenticated transition을 50 ms 간격으로 표본화했으며 loading은 2개 frame에서 관찰됐다. 표본 사이의 모든 순간을 보장하지 않는다. ### REQ-002.05-AC01 — local error/retry **Cases**: negative, positive **State Invariants**: shell remains intact; renderer details and secrets are not exposed. **판정 기준** renderer 예외가 generic error/retry body로 국소 대체되고 예외 상세가 노출되지 않는다. **검증** `tests/test_dashboard_menu_transition.py::test_body_error_is_local_and_retry_rerenders_target` **검증 분류** - 수준: unit - 백엔드: pytest monkeypatch - 증거: error text, retry event, secret-detail absence assertion ### REQ-002.06-AC01 — latest selection **Cases**: concurrency, unchanged-state **State Invariants**: latest serialized selection is final. **판정 기준** 연속 선택 `거래내역` → `자산/마감` → `Import` 후 query가 마지막 `Import`이고 sibling key가 보존된다. **검증** `tests/test_dashboard_menu_transition.py::test_rapid_menu_clicks_commit_latest_selection_only` **검증 분류** - 수준: unit - 백엔드: pytest monkeypatch - 증거: final query mapping ### REQ-002.07-AC01 — auth regression **Cases**: negative, positive **State Invariants**: auth gate precedes DB/page dispatch; cookie semantics remain REQ-001. **판정 기준** valid cookie가 기존 Dashboard를 열고 cookie가 session state에 저장되지 않으며 gate rejection은 DB/page dispatch 전에 종료된다. **검증** `tests/test_dashboard_authentication_app.py::test_valid_cookie_allows_dashboard_and_is_not_stored_in_session_state`, `tests/test_dashboard_authentication_app.py::test_main_does_not_resolve_database_or_dispatch_when_authentication_rejects` **검증 분류** - 수준: integration regression - 백엔드: Streamlit AppTest, synthetic fixture DB - 증거: authenticated body and pre-gate forbidden DB sentinel ### REQ-002.08-AC01 — finance page regression **Cases**: positive, unchanged-state **State Invariants**: existing financial page results and query contracts remain unchanged. **판정 기준** 기존 핵심 금융 page의 expected text와 synthetic fixture DB 결과가 유지된다. **검증** `tests/test_dashboard_app.py::test_streamlit_apptest_core_pages_use_synthetic_fixture_db` **검증 분류** - 수준: regression integration - 백엔드: Streamlit AppTest, synthetic SQLite - 증거: page expectation and metric assertions ### REQ-002.09-AC01 — cache-clear regression **Cases**: positive, unchanged-state **State Invariants**: `clear_cache` remains callable and menu transition does not alter it. **판정 기준** refresh control이 callable `clear_cache` 계약을 유지한다. **검증** `tests/test_dashboard_menu_transition.py::test_refresh_control_preserves_cache_clear_contract` **검증 분류** - 수준: regression unit - 백엔드: pytest - 증거: callable/name assertion ### Acceptance Criteria canonical fields Each Acceptance Criterion has explicit `Cases` and `State Invariants` in addition to the existing Korean verification text. | Acceptance Criterion | Cases | State Invariants | |---|---|---| | `REQ-002.01-AC01` | positive, unchanged-state | labels/order and selected state remain exact | | `REQ-002.02-AC01` | positive, boundary, unchanged-state | only `page` changes; sibling query keys remain unchanged | | `REQ-002.03-AC01` | positive, unchanged-state | document, shell, and background remain stable; body changes only | | `REQ-002.04-AC01` | positive, boundary | loading appears before completion and is removed after rendering | | `REQ-002.05-AC01` | negative, positive | shell remains intact; renderer details and secrets are not exposed | | `REQ-002.06-AC01` | concurrency, unchanged-state | latest serialized selection is final | | `REQ-002.07-AC01` | negative, positive | auth gate precedes DB/page dispatch; cookie semantics remain REQ-001 | | `REQ-002.08-AC01` | positive, unchanged-state | existing financial page results and query contracts remain unchanged | | `REQ-002.09-AC01` | positive, unchanged-state | `clear_cache` remains callable and menu transition does not alter it | ## 8. 오류 및 충돌 계약 인증 실패는 항상 REQ-001 gate가 우선하며 메뉴·query로 우회할 수 없다. renderer 실패는 해당 body만 generic error/retry로 대체한다. 빠른 선택은 Streamlit serialized callback 순서를 따르며 마지막 serialized selection이 최종 상태다. history/back-forward와 path URL은 요구하지 않는다. ## 9. 데이터 및 인터페이스 계약 - `dashboard_page` is the canonical selected menu field; `page` is the only query field written by menu selection. - `month`, `account_id`, `filter`, and other sibling query keys are preserved verbatim. - Body loading, error, retry, and renderer output are local to the stable body container; shell/background are not replaced. ## 10. 하위호환성 - Authentication expiry, rotation, invalid-cookie cleanup, and explicit logout remain governed by `REQ-001.05-AC01`, `REQ-001.07-AC01`, `REQ-001.08-AC01`, and `REQ-001.09-AC01`. - Existing exact tests remain the authority: `tests/test_dashboard_persistent_authentication.py::test_token_expiry_is_absolute_and_not_sliding`, `tests/test_dashboard_persistent_authentication.py::test_tampering_and_rotations_invalidate_token`, and `tests/test_dashboard_auth_server.py::test_logout_clears_cookie_with_matching_scope`. - Menu/query reruns/rendering do not initiate or replay financial writes or external calls. Financial writes and external calls remain behind explicit page user actions; this adds no new idempotency contract. ## 11. 검증 계획 - Menu transition tests and required browser evidence verify REQ-002 criteria; REQ-001 authentication tests above verify expiry, rotation, and logout regressions. ### Test Traceability Contract Test definitions reference the exact Acceptance Criterion IDs and execution evidence records revision, commit, environment, and result separately. ## 12. 추적성 - 근거 이슈: `#9` - 구현 PR: `#10` - 관련 설계 문서: `docs/project-structure.md`, `docs/runtime-flow.md` - 관련 requirement: `REQ-001` - 구현 진입점: `dashboard/app.py::main`, `dashboard/common.py::render_sidebar_nav`, `dashboard/common.py::validate_dashboard_page`, `dashboard/common.py::render_dashboard_body`, `dashboard/common.py::_sync_dashboard_page_query` - 검증 파일: `tests/test_dashboard_menu_transition.py`, `tests/test_streamlit_browser_evidence.py`, `tests/test_dashboard_app.py`, `tests/test_dashboard_authentication_app.py` - browser evidence 도구: `scripts/streamlit_browser_evidence.py --mode transition` ## 13. 확정된 결정 - Keep the authenticated shell and background stable while replacing only the body. - Preserve REQ-001 authentication semantics and keep financial writes/external calls behind explicit page actions. ## 14. Open Questions - 없음 ## 15. 승인 기록 - 상태: `approved` - 승인 주체: existing requirement approval - 승인 근거: `REQ-002` revision `1.0`