diff --git a/docs/design/488-ux-mobile-responsive/README.md b/docs/design/488-ux-mobile-responsive/README.md new file mode 100644 index 0000000..30eb9f8 --- /dev/null +++ b/docs/design/488-ux-mobile-responsive/README.md @@ -0,0 +1,43 @@ +# Redmine #488 - 모바일 VPD/권한 화면 반응형 개선 설계 + +## 프로젝트 개요 + +VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이블로 제어하기 위한 Spring Boot 관리 도구다. 사용자는 사용자, 그룹, 역할, TABLE/VIEW 권한, VPD policy/filter, Bearer Token, ORDS 검증, MCP Reasoning을 한 화면 흐름에서 운영한다. + +## 목표 + +390px 모바일 viewport에서 `VPD 설정`과 `권한 관리` 화면이 가로로 밀리지 않고, 아키텍처 설명 카드와 테이블/폼이 읽을 수 있는 형태로 표시되게 한다. + +## 문제 + +캡처 기준: + +- `/tmp/vpd-ux-audit/screenshots/mobile-vpd-policies.png` +- `/tmp/vpd-ux-audit/screenshots/mobile-permissions.png` + +확인된 문제: + +- `architecture-strip` 모바일 override가 기본 grid 선언보다 앞에 있어 390px에서도 5열 grid가 유지된다. +- 일부 grid/table item이 `min-width:auto`로 계산되어 `.table-responsive` 바깥의 body 폭을 밀어낸다. +- 행 규칙 grid가 모바일에서도 4열을 유지해 입력 컨트롤이 좁아진다. + +## 설계 + +- 공통 CSS에서 architecture strip의 모바일 override를 실제 선언 뒤쪽에 다시 배치한다. +- card, form, table wrapper에 `min-width: 0`을 부여해 내부 overflow가 body 폭을 밀지 않게 한다. +- 모바일에서 큰 table은 body가 아니라 wrapper 내부에서만 horizontal scroll 되게 한다. +- 모바일에서 `form-grid`, `rule-row`, `section-heading`은 1열 또는 wrap 형태로 전환한다. + +## 완료 기준 + +- 390px viewport에서 `document.documentElement.scrollWidth <= window.innerWidth`가 성립한다. +- `VPD 설정`, `권한 관리` 모바일 캡처에서 architecture strip이 1열로 표시된다. +- 테이블은 화면 밖으로 body를 밀지 않고 wrapper 내부에서 스크롤된다. +- 1440px 데스크톱 화면의 정보 밀도는 유지된다. + +## 검증 + +- `mvn test` +- Playwright 캡처 또는 DOM 검사: + - `/vpd-policies` + - `/permissions` diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index 339844c..b4faad1 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -215,12 +215,14 @@ body { gap: .5rem; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); margin-bottom: 1rem; + min-width: 0; } .architecture-step { background: var(--rw-surface); border: 1px solid var(--rw-border); border-radius: 8px; + min-width: 0; padding: .85rem; } @@ -247,6 +249,7 @@ body { color: var(--rw-muted); font-size: .82rem; margin: .25rem 0 0; + overflow-wrap: anywhere; } .architecture-arrow { @@ -298,6 +301,7 @@ body { .content-band { margin-bottom: 1rem; + min-width: 0; padding: 1rem; } @@ -311,6 +315,7 @@ body { .section-heading { align-items: center; display: flex; + flex-wrap: wrap; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; @@ -325,8 +330,9 @@ body { .form-grid { align-items: end; display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: 1rem; + min-width: 0; } .form-grid label { @@ -394,12 +400,14 @@ body { .rule-row { display: grid; gap: .5rem; - grid-template-columns: 1fr 1fr 1fr auto; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) auto; + min-width: 0; } .permission-wizard { display: grid; gap: 1rem; + min-width: 0; } .wizard-progress { @@ -428,7 +436,8 @@ body { .wizard-summary { display: grid; gap: .75rem; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr)); + min-width: 0; } .wizard-summary > div, @@ -458,6 +467,7 @@ body { border: 1px solid var(--rw-border); border-radius: 8px; display: none; + min-width: 0; padding: 1rem; } @@ -1042,18 +1052,119 @@ body { font-weight: 800; } +.table-responsive { + max-width: 100%; + min-width: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.table-responsive > .table { + margin-bottom: 0; +} + +.table th, +.table td { + min-width: 0; + vertical-align: top; +} + +.table-responsive > .table th, +.table-responsive > .table td { + white-space: nowrap; +} + +.table code, +.table-responsive > .table code { + overflow-wrap: normal; + white-space: nowrap; + word-break: normal; +} + +.table-pre { + max-height: 8rem; + overflow: auto; + white-space: pre-wrap; +} + +@media (max-width: 920px) { + .architecture-strip { + grid-template-columns: 1fr; + } + + .architecture-arrow { + display: none; + } +} + @media (max-width: 640px) { + main.container { + max-width: 100%; + min-width: 0; + padding-left: .75rem; + padding-right: .75rem; + } + + .page-title { + margin-bottom: 1rem; + } + + .page-title h1 { + font-size: 1.45rem; + } + + .content-band { + padding: .85rem; + } + + .section-heading { + align-items: flex-start; + gap: .5rem; + } + .form-grid .span-2 { grid-column: span 1; } + .rule-row { + grid-template-columns: 1fr; + } + + .rule-row .btn { + width: 100%; + } + + .wizard-panel { + padding: .85rem; + } + + .wizard-actions { + flex-wrap: wrap; + } + + .wizard-actions .btn { + flex: 1 1 auto; + } + + .table-responsive > .table { + min-width: 960px; + } + .navbar .container { align-items: flex-start; gap: .75rem; } - .navbar-nav { - flex-wrap: wrap; - gap: .25rem; + .rw-menu { + width: 100%; + } + + .rw-menu-group { + min-width: 0; + } + + .rw-menu-trigger { + padding-left: .65rem; + padding-right: .65rem; } }