UX/UI 설계 감사 — 운영자 화면 (/admin + act-as /dashboard)
- 작성: PRODUCT_DESIGN (L1, 외부 설계 감사 레인)
- 일자: 2026-11-02 · 대상: https://staging.yetdapay.com · 로컬 기준:
8e2f0a6(main) - 방법: Playwright chromium, 뷰포트 1440×900 / 375×812 (+ 480/768/992/1024/1280 헤더 붕괴 탐지), DOM 지오메트리 + computed-style 측정, 라이브 스테이징 데이터
- 픽스처:
[email protected](admin); act-as 대상seller_nougat(Nougat Labs) - 증적:
/tmp/ux-audit/admin/shots/*.png,/tmp/ux-audit/admin/metrics.json - 범위 참고: 설계 검토 전용 — 제품 코드 변경 없음. 보안/기능 결함(예: act-as 스코핑 버그)은 INDEPENDENT_QA의 영역이며 여기서 다시 다루지 않음.
화면 목록
| # | 화면 | 뷰포트 | 증적 |
|---|---|---|---|
| S1 | /admin 헤더 (D29 랩핑 이후) | 375→1440 | d-admin-top.png, m-admin-top.png, m-dropdown-open.png |
| S2 | /admin KPI 행 | 양쪽 | d-admin-top.png, m-admin-top.png |
| S3 | /admin 판매자 심사 (verification 카드) | 양쪽 | d-admin-top.png, m-admin-top.png |
| S4 | /admin 사용자 계정 (users 테이블) | 양쪽 | d-admin-top.png |
| S5 | /admin 거래 · 정산 모니터링 | 양쪽 | d-admin-with-context.png |
| S6 | /admin 운영 로그 · 감사 | 양쪽 | d-admin-with-context.png |
| S7 | 판매자 컨텍스트 드롭다운 | 양쪽 | d-dropdown-open.png, d-dropdown-filtered.png, m-dropdown-open.png |
| S8 | act-as 상태의 /dashboard (배너 + 컨텍스트 UX) | 양쪽 | d-dashboard-actas.png, d-actas-banner.png, d-dashboard-actas-scrolled.png, m-dashboard-actas.png, probe2-dashboard-actas.png |
| S9 | /admin 다크 테마 스팟 체크 | 양쪽 | d-admin-dark.png, m-admin-dark.png |
확인된 정상 항목 (유지)
- D29 수정 유지: 탐지한 모든 너비(375–1440)에서
scrollWidth == innerWidth. 가로 오버플로 없음. (fe/layouts/admin.vue:57) - D31/D33 수정 유지: 모바일 드롭다운이 뷰포트 안에 들어감(우측 가장자리 206px < 375); 모바일 테마 토글이 44×44px.
- KPI 그리드가 4단 → 2×2로 올바르게 접힘; 두 데이터 테이블이 카드 안에서 스크롤됨(wrap 349px < table 620/712px).
- 컨텍스트 드롭다운: 검색 가능, 37개 항목이
max-h-320안에서 스크롤, 컨텍스트 없을 때전체 보기 (admin)가 올바르게 비활성, 선택 지속, clear-X 동작. - Verification 배지 톤 매핑이 의미적으로 정확: 승인됨=good, 반려됨=bad, 보완 요청=warn, 심사 대기=info, 미등록=neutral. (
fe/pages/admin/index.vue:24-30) - 가장(impersonation) 배너: sticky
top-0이 스크롤 후에도 유지(scrollY 2000에서 top=0), 로드 후 판매자명 표시,운영자 콘솔로 돌아가기와컨텍스트 해제모두 제공; 사이드바에도운영자 콘솔노출 — 중복 탈출 경로는 바람직함. - 가장 화면이
SANDBOX배지를 유지 — 운영자가 가장 상태와 mock 환경 경고를 동시에 확인.Mock 거래액/실제 admin JWT문구가 적절히 정직함. - 심사 액션 버튼이 모바일에서 44px 높이이고 깔끔하게 줄바꿈됨; 액션 순서(승인 primary → 보완 secondary → 반려 destructive)가 합리적.
발견 사항
심각도: HIGH = 운영자 워크플로를 차단/오도 · MEDIUM = 실질적 마찰 또는 오용 위험 · LOW = 마감/일관성.
S1 — /admin 헤더 (D29 이후)
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A1 | HIGH | hidden/sm:hidden/sm:inline-flex 유틸리티가 UiButton/UiBadge에서 무력화 — 컴포넌트의 .ng-btn{display:inline-flex}/.ng-badge{display:inline-flex} 스코프 스타일이 Tailwind display 유틸리티보다 캐스케이드에서 우선함. 결과: 모든 브레이크포인트에서 로그아웃 버튼 두 개 렌더 (1440에서 32px 아이콘 + 98px 라벨 나란히); 라벨 버튼이 375에서 자체 행으로 줄바꿈 → 3행 169px 헤더(뷰포트 ~21%) (2행 ~117px 대신); hidden sm:inline-flex ADMIN 배지가 모바일에 노출. D29가 오버플로를 고쳤으나, 앱 전체 hidden sm:* on Ui* 컴포넌트에 영향 가능성이 있는 컴포넌트 수준 버그로 접힘 품질이 저하됨. | DOM: ng-btn hidden sm:inline-flex이 375에서 display:flex 측정; ng-btn sm:hidden이 1440에서 display:flex 측정; 헤더 높이 65/105/117/169 | 컴포넌트 루트의 display 유틸리티에 의존하지 말 것: 조건부 렌더링(브레이크포인트 인식 v-if/래퍼), !hidden류 important 유틸리티 제공, 또는 UiButton/UiBadge에 hidden boolean prop 추가. 다른 hidden sm:*/sm:hidden on Ui* 컴포넌트 재점검(예: dashboard.vue:276 SANDBOX 배지도 같은 패턴). | fe/layouts/admin.vue:64,119-125; fe/components/ui/UiButton.vue:66; fe/components/ui/UiBadge.vue:16 |
| A2 | LOW | 올바르게 접혀도 모바일 헤더가 2행 ≈117px이고 브랜드 행이 비어 보임(워드마크 + 아이콘 2개). 수용 가능하나, 컨텍스트 그룹을 컴팩트 팝오버에 넣은 단일 행 헤더면 뷰포트 ~6% 회수 가능. | header@375 행 분석 | 선택적 통합; 운영자가 모바일 중심일 때만 재검토. | fe/layouts/admin.vue:57-126 |
S2 — KPI 행
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A3 | LOW | 심사 대기 KPI가 경고 삼각형 아이콘을 상시 표시 — 0건에서도 경고를 암시. 값을 나타내는 의미는 상태 기반이어야 함. | d-admin-top.png | pendingCount > 0일 때만 아이콘/경고 톤 표시. | fe/pages/admin/index.vue:112 |
S3 — 판매자 심사 (verification 카드)
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A4 | HIGH | 38개 verification 전부 무제한 렌더, 각각 메모 입력 + 액션 버튼 3개 — 미등록(미신청)과 이미 승인됨/반려됨 판매자까지 포함. 대기 우선 정렬도, 필터/탭도, 검색도 없음. 이 목록만으로 S5/S6이 **y≈16,548px(뷰포트 ~18개 아래)**로 밀려남 — 모니터링과 감사가 사실상 도달 불가. | 카드 목록 y 오프셋; li 38개; 모든 li에 승인/보완/반려 | 기본 뷰 = PENDING_REVIEW만(KPI가 이미 집계); 상태별 액션 세트(종결 상태는 없음 또는 재심사); 메모는 선택한 액션 뒤에 접기; 페이지네이션/"전체 보기"로 상한. | fe/pages/admin/index.vue:117-136 |
| A5 | MEDIUM | 반려에 확인 단계 없음, 보완/반려 시 메모가 선택 사항 — 운영자가 기록된 사유 없이 한 번의 오터치로 판매자를 반려 가능. S4의 비활성도 같은 패턴. | 코드: review() 즉시 실행 | UiModal 존재 — 파괴적/상태 회수 액션에 확인 적용; NEEDS_CORRECTION/REJECTED 시 메모 필수화(판매자 콘솔에 노출됨). | fe/pages/admin/index.vue:61-73,130-133 |
S4 — 사용자 계정 (users 테이블)
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A6 | HIGH | 123명 사용자 전체가 하나의 무제한 테이블에 렌더 — 페이지네이션, 검색, 정렬, 역할/상태 필터 없음. 16k px 페이지의 주요 원인. | tbody tr 수 = 123, 필터 컨트롤 0 | 페이지네이션 또는 고정 높이 스크롤; 이메일 검색 + 역할/상태 필터가 최소 운영 컨트롤. | fe/pages/admin/index.vue:138-156 |
| A7 | MEDIUM | 셀프 잠금 유도: 로그인한 관리자 본인 행([email protected], role admin, 활성)에 비활성 제공 — 가드 없음, 확인 없음, "본인" 표시 없음. API가 400을 반환하더라도 UI가 제시하면 안 됨. | DOM: 본인 행에 비활성 버튼 | u.id === me.id에서 액션 비활성/숨김 + 툴팁, 및/또는 모든 상태 전환에 확인 모달. | fe/pages/admin/index.vue:149-150 |
| A8 | LOW | role 열이 모든 역할에 tone="accent" mono 사용 — admin이 user/seller와 시각적으로 동일; 역할은 이 페이지에서 가장 위험도 높은 열. | accent 배지 123개 | 역할별 톤 매핑(admin=info/warn, seller=accent, user=neutral). | fe/pages/admin/index.vue:144 |
S5 — 거래 · 정산 모니터링
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A9 | MEDIUM | act-as 상태에서 스코프 혼합: 카드의 미니 KPI(주문, 상품, 지급 실패)는 전역 admin.overview()에서 오고, 주문 테이블 + 정산 건수는 act-as 스코프(X-Act-As-Seller). 판매자 컨텍스트 적용 중 배지가 경고하지만, 전역 수치가 스코프된 행 바로 위에 위치 — 수치가 맞지 않아 오도함. | 코드: Promise.all([admin.overview(), …, orders.list(), settlements.list()])가 스코프 혼합 | 미니 KPI도 컨텍스트로 스코프하거나, 명시적으로 전체 라벨 + 시각적 분리. | fe/pages/admin/index.vue:44-47,160-179 |
| A10 | LOW | UiKpi가 .card(1px 테두리, r16, bg-surface)를 렌더 — UiCard 안에 테두리 카드 3개 중첩으로 디자인 언어와 다른 카드-인-카드 노이즈. | 중첩 .card 3개 측정 | 카드 내 통계용 테두리 없는/plain UiKpi 변형 추가. | fe/components/ui/UiKpi.vue:17; fe/pages/admin/index.vue:164-168 |
| A11 | LOW | 주문 상태 열이 소문자 enum 원문(paid) 출력 — 한국어·배지 중심 UI에서 상태 언어 불일치. | 주문 행: "paid" 일반 텍스트 | 사용자 상태와 동일 패턴: UiBadge + 상태→톤/라벨 매핑. | fe/pages/admin/index.vue:173 |
S6 — 운영 로그 · 감사
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A12 | MEDIUM | 감사 행이 기계 문자열 원문(checkout · order + ord_… entityId); detailJson이 자주 비어 매달린 · 표시; actor 없음, 심각도 없음, 확장/복사 없음, "전체 보기" 없이 10건 제한. 읽거나 페이지할 수 없는 감사 화면은 장식 — 게다가 16k px 페이지 맨 아래에 위치. | 행: ord_bdc56c623674 ·(detail 비어 있음); 10행, 더보기 링크 없음 | 사람이 읽을 수 있는 액션 라벨 + actor + 엔티티 링크; 확장 가능한 detail(UiJson 존재); 페이지네이션 또는 전체 감사 뷰 라우트. | fe/pages/admin/index.vue:181-192 |
S7 — 판매자 컨텍스트 드롭다운
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A13 | MEDIUM | 선택 상태 없음: 현재 가장 중인 판매자 항목에 체크/aria-selected/활성 스타일이 없음 — 운영자가 메뉴 안에서 컨텍스트를 확인 불가. | 탐지: 활성 판매자 항목에 sel:null | DropItem에 active/selected 추가, 체크 아이콘 + aria-selected 렌더. | fe/components/ui/UiDropdown.vue:5-11,63-75; fe/layouts/admin.vue:17-26 |
| A14 | MEDIUM | ARIA 메뉴 의미 불완전: role="menu"/menuitem 선언됐으나 Escape로 닫히지 않음(검증됨 — Escape 후에도 메뉴 열림), 화살표 키 탐색 없음, 트리거로 포커스 복귀 없음. 메뉴 패턴을 지키거나 일반 목록으로 강등할 것. | menu open after Escape: 1 | UiDropdown에 Escape 닫기 + 화살표 탐색 + 포커스 관리 구현(앱 전체 사용 — 한 번 고치면 사용자 메뉴에도 도움). | fe/components/ui/UiDropdown.vue:36-40,47-75 |
| A15 | LOW | 판매자 표시명 중복(Admin Matrix Seller ×2, Commerce Seller ×4)으로 가장 대상이 모호 — 잘못 선택하면 지원/감사 위험. | 드롭다운 항목 목록 | name + 흐린 slug/id를 두 번째 줄로 표시. | fe/layouts/admin.vue:21-25 |
S8 — act-as 상태의 /dashboard (가장 UX)
| ID | 심각도 | 발견 | 증적 | 권고 | 파일 |
|---|---|---|---|---|---|
| A16 | MEDIUM | 가장 표시가 늦게 도착하고 원시 id를 표시할 수 있음: 배너는 fetchMe resolve 후에만 마운트(userRole==='admin' 게이트) — 초기 탐지에 배너 없음인데 대시보드는 이미 렌더(표시 없는 가장 상태가 잠시 노출); actAsLabel이 저장된 id로 폴백해 seller_nougat(Nougat Labs 아님)이 배너에 지속될 수 있음. | 초기 "(no banner yet)"; 후기 텍스트 — seller_nougat | 저장된 컨텍스트 + 토큰에서 배너를 동기적으로 렌더(반증 전까지 admin으로 간주 — 경고용 fail-open은 chrome이지 인가가 아니므로 허용 가능), id→이름 해석은 레이아웃 이동 없이(공간 예약/스켈레톤). | fe/layouts/dashboard.vue:49-53,228-237 |
| A17 | LOW | 배너 액션 링크가 12px 텍스트, 375에서 ~18px 히트 영역 — .ng-nav에 적용된 ≥44px 터치 대상 규칙(D36) 미달. | 측정 124×18 / 74×18 | 배너 액션에 min-h-11 터치 패딩(모바일만도 가능). | fe/layouts/dashboard.vue:230-236 |
| A18 | LOW | Sticky 오프셋 스택(top-[66px]/top-[33px]/top-0)이 배너 높이에 하드코딩 — 배너 텍스트 줄바꿈(i18n, 긴 판매자명)이나 세 번째 배너 등장 시 취약. | headerTop computed | 배너를 측정하거나 CSS var/ResizeObserver로 오프셋 구동. | fe/layouts/dashboard.vue:56-60,249 |
S9 — 다크 테마
- 스팟 체크 캡처(
d-admin-dark.png); 토큰은 테마 완비(--ng-warn,--ng-info등 모두 다크 변형 존재). 시각적 승인 NOT_VERIFIED — 다크에서 배지 배경/선 대비에 대한 사람 검토 권장.
교차 발견
- 전신적인 A1 근본 원인이 가장 레버리지 높은 수정:
UiButton/UiBadge(및 다른.ng-*display 루트)에 놓인hidden sm:*/sm:hidden클래스가 조용히 실패. admin 헤더가 유일한 피해자라고 가정하기 전에 사용처를 전수 점검(layouts/dashboard.vue도 네이티브 요소에 같은hidden md:inline-flex/hidden sm:block패턴 사용 — 이것들은 괜찮음; 위험은 자체 CSS가display를 설정하는 컴포넌트 루트에 한정). - 모바일 토스트 가시성(D32)은 페이지 오버플로의 증상이었음; 오버플로가 없어졌으므로 해결된 것으로 추정 — 이번 감사에서 재검증하지 않음.
/admin에 확인 대화상자가 전무(A5/A7) — 이 화면에 첫UiModal사용을 도입하게 됨; 제품 톤과 정렬(짧고 결과를 명시하는 문구).
상위 5개 우선순위
- 무제한 목록에 상한(A4+A6). 123명 사용자 + 38개 심사 카드가 모니터링/감사 행 전체를 ~16.5k px 아래로 밈.
PENDING_REVIEW필터, 사용자 페이지네이션, "전체 보기" 상한. 최고 워크플로 영향. - Ui 루트의
hidden유틸리티 무력화 수정(A1).* 컴포넌트 수준 수정 하나로 중복 로그아웃 제거, ADMIN 배지 누출 해소, 모바일 헤더 한 행(~52px) 축소. 전신적. - 파괴적 운영자 액션 가드(A5+A7).
반려/비활성확인 모달, 반려/보완 시 메모 필수, 관리자 본인 행의 셀프 잠금 방지. 오용/잠금 위험. - 모니터링 카드의 스코프 혼합 해소(A9). act-as 스코프 행 위의 전역 KPI가 상태를 잘못 보고; KPI를 스코프하거나
전체라벨. 데이터 무결성 인식. - 컨텍스트 드롭다운 완성(A13+A14+A15). Escape/화살표 키, 선택된 판매자 표시, slug 구분 — 가장 선택기가 이 화면에서 가장 결과가 큰 컨트롤. 접근성 + 대상 지정 안전.
한계
fe/assets/css/main.css의 디자인 토큰 대비 DOM 지오메트리/computed style + 코드 검토 기반; 스크린샷은 증적이며 픽셀 비교하지 않음.- 다크 테마 시각 미검증(S9).
- act-as 스코핑, 심사 API, 감사 커버리지의 기능/보안 정확성은 INDEPENDENT_QA 영역 —
fe/e2e/qa-sweep/*발견 참조.