UX/UI Design Audit — Seller Console
- Author: PRODUCT_DESIGN (L1, external design-audit lane)
- Date: 2026-11-02 · Target: https://staging.yetdapay.com · Local ref:
8e2f0a6(main) - Method: Playwright chromium, desktop 1440×900 + mobile 375×812 full-page screenshots per route; DOM geometry/computed-style measurement (overflow, tap targets, font/color/radius inventory); dark-mode pass on 8 screens; interaction probes (drawer, sidebar collapse, ⌘K palette, refund/issue/delete modals, empty-form validation); onboarding exercised end-to-end on two fresh accounts. Live staging data; source inspection against
fe/assets/css/main.csstokens andUi*components. - Fixture:
seller_7b6b0bef65da(qa3-seller-…@e2e.invalid) — 1 project, 1 product, 2 orders (paid/refunded), 2 licenses (active/revoked), 0 subscriptions, empty feedback/widgets/settlements. Freshux-audit-{desktop,mobile}-…@e2e.invalidaccounts for onboarding. - Evidence:
/tmp/ux-audit/seller/shots/*.png(80 shots),metrics-{desktop,mobile}.json,sweep-results.json,text/*.txtbody-text captures,probe-*.mjsscripts. - Scope note: Design review only — no product code changed. Functional defects are INDEPENDENT_QA's lane; where a UX finding has a functional cause it is marked (cross-lane: ④/⑤). Severity: critical = blocks/misleads sellers on money/license paths · major = real friction, dead UI, inconsistency · polish = cosmetic/density.
- Side effects: 2 fresh staging accounts created for onboarding capture; no writes on the audited seller (modals opened and cancelled only).
Screen inventory
| # | Route | File | Desktop | Mobile | Dark | Live state |
|---|---|---|---|---|---|---|
| S1 | /onboarding | pages/onboarding.vue | step0/1/done + error shots | same | — | fresh acct: pass-through (F4) |
| S2 | /dashboard | pages/dashboard.vue | ok + collapse + ⌘K | ok + drawer | ok | full KPIs/chart/tables |
| S3 | /projects | projects/index.vue | ok | ok | — | 1 project card |
| S4 | /projects/new | projects/new.vue | ok | ok | — | form |
| S5 | /projects/[id] + /items + /fulfillments | projects/[id]/*.vue | ok | ok | ok | tabs + item cards + empty fulfillments |
| S6 | /products | products/index.vue | ok + row menu + delete confirm | ok | — | 1 row |
| S7 | /products/new · /products/[id] | products/new.vue, [id].vue | ok + validation toast | ok + validation | ok (detail) | shared ProductsForm |
| S8 | /orders | orders/index.vue | ok | ok | ok | 2 rows |
| S9 | /orders/[id] ×2 | orders/[id].vue | ok + refund modal | ok + refund modal | ok | paid + refunded |
| S10 | /licenses | licenses/index.vue | ok + issue modal | ok | ok | 2 rows |
| S11 | /licenses/[id] | licenses/[id].vue | ok | ok | — | active license |
| S12 | /subscriptions | subscriptions/index.vue | ok | ok | — | empty |
| S13 | /settlements | settlements/index.vue | ok | ok | — | KPIs + 1 unsettled |
| S14 | /settings | settings/index.vue | ok + apikey reveal | ok | ok | 8 sections, scroll-spy nav |
| S15 | /settings/settlement | settings/settlement.vue | ok | ok | — | 미등록 + apply form |
| S16 | /feedback | feedback/index.vue | ok | ok | — | empty inbox |
| S17 | /feedback/widgets | feedback/widgets.vue | ok | ok | ok | empty + 생성 폼 |
| S18 | /donations/widgets (+/mcp) | donations/widgets/*.vue | ok | ok (title truncate) | ok | live generator + MCP placeholder |
| S19 | /integrations/ai | integrations/ai.vue | ok | ok | ok | real code+prompt output |
Verified OK (keep)
- Token discipline is excellent: zero non-token Tailwind palette classes and ~zero hardcoded hex in seller pages (one justified preview canvas
#f7f7f7/#161618); all measured text colors map to--ng-ink/ink-2/muted/faint/accent/bad/warn; typography scale consistent (11–15.5px body, 22px h1, 26px KPI); radii consistent (16px cards / 8–12px controls / pills). - Component reuse is real, not nominal:
UiCard/UiField/UiInput/UiSelect/UiSegmented/UiTable/UiBadge/UiEmpty/UiKpi/UiModal/UiDropdown/UiSwitch/UiAvatarused throughout;CommonPageHeader+ breadcrumb on nearly all pages. - Mobile tap targets ≥44px on all measured interactive elements (nav 44px, inputs min-h-44, icon buttons w-11 h-11,
min-h-11 md:min-h-0convention on breadcrumbs/links). Only misses found: see C6/F12. - Dark mode is correct end-to-end — computed-surface probe found no off-theme light backgrounds or dark text; inverted primary buttons, switch thumbs, soft dark fills all token-derived.
- Destructive confirm on the money path: order refund uses
ConfirmModal(취소/환불 처리, destructive variant, description includes consequence); product delete uses confirm modal. - Sandbox honesty:
SANDBOXheader badge,Sandbox · Mock 환경sidebar footer, mock disclaimers on settlements ("실제 계좌이체 없이… 수수료는 예시 값"), donation MCP ("현재 실제 서버에 연결되지 않습니다"), order detail ("이 라이선스는 Mock 환경에서 발급"), settings provider cards (준비 중). - No console errors, page errors, or failed requests across all 23 routes × 2 viewports. Zero page-level horizontal overflow at 375px (all table overflow is contained in
.ng-table-scroll). - Status readability:
useLabels.tstone map is sensible (paid=good, refunded=bad, suspended=warn…); subscription period-end showsN일 후warn when ≤3 days; per-status filter chips carry counts. - Integration surfaces are genuinely functional:
/integrations/aiemits real embed code + AI prompts bound to the seller's project/items;/donations/widgetsis a complete working generator (style/size/icon/color presets, PC-mobile/light-dark preview, 4 output formats). - Loading-failure design is fail-closed:
serverBlockedoverlay blocks stale data, has retry + login escape — good instinct (but see F1 for the cost).
Cross-cutting findings
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| F1 | critical | Every full page load gates the entire console behind a full-screen "서버에 연결 중…" overlay for ~4.0–5.5s on staging (measured TTR 4.0–5.5s × 46 loads). pullAll fires ~15 serial-ish requests (/me → 10-entity batch → per-project items/players/fulfillments + settlements/settings); until commit, all content is v-show-hidden — no shell, no skeletons, no progressive render. A slow Worker/D1 response turns every direct visit/refresh into a 5s blank-loader; the overlay is also the first thing a new seller sees after signup→dashboard. Compounding: 4× GET /api/v1/me per load (api-sync role check, dashboard onMounted, CommandMenu onMounted, +1) — redundant roundtrips. | (a) Render the layout chrome (sidebar/header) + UiSkeleton content immediately; gate only data regions on first pull. (b) Dedupe /me calls (share one role fetch via store). (c) If staging latency is the driver, batch or trim pullAll (settings/settlements are lazy-loadable). | fe/layouts/dashboard.vue:204-226; fe/plugins/api-sync.client.ts:60-114; fe/lib/api/sync.ts:54-78; fe/components/common/CommandMenu.vue:29-40 (cross-lane: ④ perf) |
| F2 | major | 결제 이벤트 timeline card on order detail is permanently empty in API mode — paymentEvents is never populated (no events endpoint in client.ts, not in pullAll), so every paid order shows "기록된 이벤트가 없습니다" under a card titled like a real ledger. Looks broken on the most trust-sensitive screen in the console. | Either expose events on GET /orders/:id (BE records them — events_impl.rs writes audit/outbox) or hide the card when apiMode && !events.length. | fe/pages/orders/[id].vue:86-105; fe/stores/db.ts:98-99; fe/lib/api/sync.ts:54-66 (cross-lane: ④ no read path) |
| F3 | major | Dead "키" columns everywhere: licenses list first column "라이선스 키" renders — for every row (list API omits keys by design, SEL-05); same dead column in products/[id] 발급된 라이선스 table; subscriptions license cell renders an empty-text accent link (.key is undefined). Prime table real estate shows a permanent em-dash — reads as a bug, and the subscriptions cell is a click target with no visible label. | Replace key column with 주문번호 or lic_… id (mono, linked); keep the security note. In subscriptions, link by id when key is absent. | fe/pages/licenses/index.vue:115-116; fe/pages/products/[id].vue (licenses table); fe/pages/subscriptions/index.vue:90-97 |
| F4 | major | Onboarding validation is bypassed in apiMode + flow is vestigial. finish() never validates name/brand — empty submit fires POST /seller/onboarding immediately (the step-1 validation in next() is unreachable from the submit button). Separately, /signup already provisions role=seller (verified: fresh acct → seller_f46c0009f712), so the POST returns 409 ALREADY_SELLER → the flow short-circuits to a "가입 완료" screen claiming registration happened. Observed: only 1 POST needed; step-1 errors render only as required * marks when bypassed this way. | Run the same field validation inside finish() before posting; on ALREADY_SELLER skip steps 0–1 and show the completion card directly (or skip /onboarding for accounts already provisioned at signup). | fe/pages/onboarding.vue:33-61 (cross-lane: ⑤ functional) |
| F5 | major | 결제수단 column in /orders is always — — list API omits method, pullAll never fetches payments, so the column is dead in list view while order detail correctly shows card · mock. Same column class as F3: permanent placeholder cells that look like missing data. | Include method/provider in the orders list payload, or drop the column to a badge on detail only. | fe/pages/orders/index.vue:76; fe/lib/api/sync.ts (cross-lane: ④ payload) |
| F6 | major | Destructive/irreversible actions lack confirms, inconsistently: 구독 취소 (revokes license, no re-enable path in the dropdown) runs on a single menu tap; 라이선스 회수 in the row dropdown runs instantly (refund on the same data model requires a ConfirmModal). Command palette 시연 데이터 초기화 executes db.reset() with no confirm and a misleading toast — in apiMode it swaps live server data for mock seed until the next pull, presented as "초기화 완료". | Confirm 구독 취소; confirm or add undo-toast for license status flips. Gate 시연 데이터 초기화 to !apiMode, or relabel "화면 데이터만 초기화 (서버는 그대로)" + confirm. | fe/pages/subscriptions/index.vue:46-49,100-108; fe/pages/licenses/index.vue:131-136; fe/components/common/CommandMenu.vue:63-66; fe/stores/db.ts:1056-1059 |
| F7 | major | Form-validation UX is inconsistent: products/new reports a missing 상품명 only via an ephemeral bottom-right toast ("입력 확인") — no inline error, no aria-invalid, no focus move, and the failing field can be a full viewport above the toast. onboarding, settings/settlement, donations/widgets (Base URL :invalid + inline :error) show the correct inline pattern. | Render UiField/UiInput :error/:invalid on the 상품명 field + focus first invalid input; keep toast for server-phase failures (it already distinguishes phase==='validation'). | fe/pages/products/new.vue:52-71; fe/components/products/Form.vue:88-100 |
| F8 | polish | --ng-faint fails WCAG AA — 2.52:1 on --ng-surface light, 2.34:1 on dark. It's used for real content at 11.5–12.5px (row dates, buyer emails' second line, hints, "전월 데이터 없음"), not just decorative placeholders. | Raise faint to ≈#8f8f8f (light) / ≈#777 (dark) — or reserve text-faint for icons/placeholders and promote metadata to text-muted. | fe/assets/css/main.css:20,74 |
| F9 | polish | Table UX on 375px relies entirely on horizontal scroll (orders 920px / licenses 960px / products 860px min-width inside 349px — ~62% of columns hidden with no scroll-affordance cue like edge fade/shadow). Acceptable containment, but sellers can't see 주문번호+금액+상태 in one glance. Considered alongside: segmented filter tracks scroll too (365–445px in ~300–341px) — works, slightly buried. | Short-term: add scroll-edge fade or "N more columns →" hint. Mid-term: a row-card layout below sm for the primary columns. | fe/components/ui/UiTable.vue; pages/{orders,licenses,products,subscriptions}/index.vue |
| F10 | polish | /settings/settlement collects no payout fields — the "정산 정보 신청" form asks only 개발자 유형 + 브랜드명, while 정산 은행/계좌번호 live in /settings → 판매자 정보 with no cross-reference. A seller can reach APPROVED with no account on file. Also: hand-rolled header (no CommonPageHeader/breadcrumb) and a bespoke fixed toast instead of UiToaster; 심사 신청 button has no :loading. | Add bank fields to the application (or a "은행 정보는 설정에서 관리" link + incomplete-state warning when empty); adopt PageHeader + useToast; add loading. | fe/pages/settings/settlement.vue:36-82; cf. settings/index.vue:154-155 |
| F11 | polish | Command palette gaps: 피드백, 피드백 위젯, 정산 등록, 구독 취소/issue actions absent — top-level nav items that are present (후원 버튼, AI 연동 센터) make the omissions look accidental. Overlay also lacks role=dialog/aria-modal and listbox/option semantics that UiModal implements. | Add the missing nav entries (cheap, same list shape); add role="dialog" aria-modal + role=listbox/option + aria-activedescendant. | fe/components/common/CommandMenu.vue:42-58,118-152 |
| F12 | polish | Density nits on desktop: breadcrumb links measure 19–20px tall (min-h-11 md:min-h-0 collapses them); API 키 보기 eye button is 15×15px; donations-widgets UiCard header truncates its title ("실시간…") at 375px because two segmented controls in the header slot don't wrap (header lacks flex-wrap). | Acceptable density overall — bump the eye button to ≥24px; add flex-wrap (or min-[0px] on the header slot) to UiCard header. | fe/components/ui/UiCard.vue:7-17; pages/settings/index.vue:189; donations/widgets/index.vue:173-177 |
| F13 | polish | Status labels mix languages: orders/feedback use Korean (결제 완료·환불됨·대기) while licenses/subscriptions render English caps (ACTIVE/REVOKED/SUSPENDED/PAST_DUE) — same badge component, two conventions on adjacent screens. | Pick one convention (Korean labels, mono style for license/serial contexts if the English technical register is intentional). | fe/composables/useLabels.ts:11-22 |
| F14 | polish | Empty-state CTAs uneven: products empty → "상품 등록" CTA (good); subscriptions empty → title only, no description/CTA (could deep-link products/new with subscription type preselected); feedback empty → copy says "위젯 미리보기에서 테스트 제출" but doesn't link /feedback/widgets — the actual unblock action sits only in the header. | Give every empty state a primary action where one exists. | pages/subscriptions/index.vue:58; pages/feedback/index.vue:91 |
| F15 | polish | Chrome persistence: sidebar collapse state resets every load (no localStorage); dashboard's 5-tile "AI로 쉽게 만들기" hero is permanent even for a seller with revenue — eats first fold. /dashboard→/dashboard/ 308 redirect adds a hop on direct loads. | Persist collapsed; collapse/dismiss the hero once `products.length |
Per-screen notes
| Screen | VP | Findings (evidence) | Sev | Recommendation / files |
|---|---|---|---|---|
S1 /onboarding | both | Step progress (3 dots) + type-select cards + UiField errors read well; BUT empty "가입 완료" posts unvalidated → server decides (F4); account already seller → pass-through 완료 screen congratulates a registration that didn't happen; completion CTAs (정산 등록하기 / 첫 프로젝트 등록) are good exits. Centered 560px column scales fine to 375px. | major | F4. pages/onboarding.vue |
S2 /dashboard | d | Quick-action hero + 8 KPIs + 연동상태 table + chart + 2 recent tables — dense but scannable; two side-by-side .ng-table-scroll tables overflow their half-columns (sw 648 > cw 562) at 1440px — recent-orders/licenses need in-card scroll even on desktop; 전월 데이터 없음 chip uses text-faint (F8). | major | Reduce table cols or stack the two recents vertically below xl; dashboard.vue:230-256 |
S2 /dashboard | m | Stacks cleanly (KPI 2-col, hero 2-col→1, tables scroll — F9); drawer nav complete with active states. | ok | — |
S3 /projects | both | Single-card list scales; status badge + 아이템/플레이어/매출 meta line is a good summary row; "프로젝트 등록" primary CTA clear. | ok | — |
S4 /projects/new | both | Type chips + color swatches + icon input — clean form; footer has 취소 ghost + 등록 primary. | ok | — |
S5 /projects/[id]* | d | Tab row (개요/상품·아이템/지급 내역/AI 연동/데모 게임) — AI 연동 and 데모 게임 are outbound links styled as in-page tabs (leave the project context); KPI row + 수익화 연동 상태 checklist (연동됨/미설정 badges) is the best status-summary pattern in the console; 최근 거래 empty ok. | polish | Style outbound tabs with an icon/target affordance or move them into the checklist card. projects/[id]/index.vue:19-25,57-61 |
S5 …/items | both | Item cards (name, 유형 badge, price, 지급 spec, 수정/비활성화/구매 테스트) — good card list; back link ← QA3 Space Game is a 20px-tall target on desktop (F12). | polish | — |
S5 …/fulfillments | both | Filter chips 전체/완료/실패/취소 have no counts unlike orders/subscriptions chips (minor inconsistency); empty state copy explains the flow well. | polish | Add counts to match orders/subscriptions chips. projects/[id]/fulfillments.vue |
S6 /products | d | Table + type segmented + search + per-row ⋯ menu (수정/미리보기/삭제) + delete ConfirmModal — complete pattern; mobile table scrolls (F9). | ok | — |
S7 /products/new,/[id] | both | lg:[1fr_320px] form+sticky-preview layout is excellent; type/plan/entitlement editors are the deepest UI in the console and hold together; validation is toast-only (F7); the same form serves new+edit consistently. | major | F7 |
S8 /orders | d | Status chips w/ counts + search + 8-col table; 결제수단 column dead (F5), 지급 column — for all product-type orders (semi-justified but noisy). | major | F5; consider hiding 지급 column for non-game sellers |
S9 /orders/[id] | d | 3-col→2-col detail (주문 정보 dl + 이벤트 + 연결 리소스); refund ConfirmModal proper; 결제 이벤트 card always empty (F2); 멱등성 키 shown mono — good. Breadcrumb + badge + destructive button header wraps correctly. | major | F2 |
S10 /licenses | d | Status chips w/ counts + 검증 도구 + 수동 발급 modal (required fields, 취소/발급) — good; 키 column dead (F3); row-click nav works; ⋯ menu lacks confirm on 회수 (F6). | major | F3, F6 |
S11 /licenses/[id] | d | Status summary grid (상태/발급일/만료일/기기) + expiry inline-edit + activation list + entitlements + 연결 정보 — well-composed; title falls back to generic "라이선스" since key is never populated (F3 side-effect); security note copy is exemplary. | polish | Title could use lic_… id or buyer email; licenses/[id].vue:55 |
S12 /subscriptions | d | Filter chips w/ counts + MRR in header desc; table design (avatar buyer, plan+interval, warn-dated renewal, license link, status, 시뮬레이션 menu) is strong — but empty state has no CTA (F14), license cell renders empty link (F3), 구독 취소 unconfirmed (F6). | major | F3/F6/F14 |
S13 /settlements | d | KPI quartet (미정산/수수료×2/예상 정산액) + 지급 예정 badge + 대기 주문 table + history empty — clearest mock-disclaimer copy in the app; 정산 실행 (시뮬레이션) disabled-when-empty is correct. | ok | — |
S14 /settings | d | Scroll-spy side nav + 8 sections is the right pattern for this density; per-card 저장 is fine; provider cards (Mock available + 3 준비 중) honest; "시연 데이터 — Mock 데이터 초기화" is correctly labeled here (unlike its ⌘K twin, F6). | polish | F12 eye-button size; consider linking 정산 fields ↔ S15 (F10) |
S14 /settings | m | All 8 sections stack; nav hidden (acceptable — page is short enough); inputs stack 1-col correctly. | ok | — |
S15 /settings/settlement | both | Status card (미등록 badge + desc + operator memo slot) reads well; form thin — no payout fields (F10); no breadcrumb; bespoke toast; no button loading. | major | F10 |
S16 /feedback | d | 5-filter row renders even over empty inbox (harmless); header CTAs (AI 연동 센터/위젯 관리) compensate for missing empty-state CTA (F14); item design (type+status+donation badges, reply thread, per-item status select, URL-state detail modal) is well-built — but the detail modal exposes developer copy "이 화면은 URL 상태로 표현됩니다…". | polish | Drop the meta sentence or move to a tooltip; feedback/index.vue:124 |
S17 /feedback/widgets | d | Empty "위젯이 없습니다" + 새 위젯 CTA — correct; creation form (name/type/project/domain) standard. | ok | — |
S18 /donations/widgets | both | Full-featured generator (preset grid, live PC/mobile + light/dark preview, 4 code formats, install steps, 연동 방법 explainer) — the most polished surface in the console; mobile: card-header title truncates (F12), input 34×44/PC 37×44 sub-40px targets in preview chrome (minor — preview props). | polish | F12; widen PC/모바일 toggle to ≥40px on mobile |
S18 …/mcp | d | Entire page is "준비 중" API docs — honest, well-structured, with a clear not-connected disclaimer; whether a placeholder page merits a top-level nav slot is a product call, not a design defect. | polish | Consider folding under the donations page as a tab until live. |
S19 /integrations/ai | d | Config form → 구매 URL → code tabs → AI prompt → related links; selects for project/type/item/framework/mode; output bound to real entities — genuinely useful; 준비 중 badge only on the MCP link card. | ok | — |
Top-5 priority list
- F1 — Kill the 5-second white wall. First contentful interaction on every console load is a full-screen "서버에 연결 중…" for 4–5.5s on staging; render chrome + skeletons immediately, gate only data regions, and dedupe the 4×
/mecalls. Highest-impact UX fix in the console. (needs ④ for request batching) - F2 + F3 + F5 — Eliminate permanently-dead data surfaces. The payment-event timeline (always empty), license-key columns (always
—), subscriptions license link (empty text), and orders 결제수단 column (always—) all present as broken to a seller. Either wire real data (BE has the events/audit records) or remove/replace the columns — this is the biggest "polish-vs-trust" gap on the money screens. (needs ④ for payloads; ⑤ to verify) - F4 + F7 — Make validation real where users type.
products/new(the console's main write path) shows toast-only errors with no inline state or focus; onboarding's submit bypasses its own client validation entirely and congratulates anALREADY_SELLERno-op. Unify onUiField :error+ focus-first-invalid. - F6 — Uniform destructive affordance. Refund/product-delete confirm; subscription-cancel, license-revoke, and ⌘K
시연 데이터 초기화don't — and the last silently swaps live data for mock seed. Apply the ConfirmModal pattern (or undo) consistently and gate/relable the demo reset in apiMode. - F8 — Fix
--ng-faintcontrast. 2.52:1/2.34:1 fails AA on real metadata text used in every table and KPI hint. One-token change; verify the raised value still reads "secondary".
Appendix — measured data
- TTR (time-to-ready,
mainvisible): desktop mean ≈4.27s, max 5.56s (product-detail); mobile mean ≈4.52s, max 5.52s — all gated by the samepullAllbarrier (F1). - Overflow: page-level horizontal overflow = 0px on all 46 loads. Contained scrollers:
ng-table-scroll×7 (orders 928px, licenses 968px, products 868px, settlements 448px, product-detail 488px ×2, dashboard recents 648px ×3 on mobile / ×2 desktop),ng-seg__track×5 (365–445px), misc 38-in-32px and 108-in-62px truncations (F12). - Tap targets <40px @375px: only inside donations-widgets preview chrome (
input34×44,PC37×44, preview anchor 120×37) — F12. <24px @1440px: breadcrumb links 19–20px,API 키 보기15×15 — F12. - Network/console: 0 errors, 0 failed requests across the sweep; API 200s throughout. Redundant calls observed: 4×
/api/v1/meper load (F1). - Dark mode: computed-style audit found zero off-theme surfaces/text; all exceptions verified intentional (switch thumb white, preview canvas
#f7f7f7, inverted primary buttons with#121213labels). - Screenshot index:
desktop-*.png×23,mobile-*.png×23,dark-*.png×8,onb-{desktop,mobile}-*.png×12,ix-*.png×12 (drawer, collapse, ⌘K, refund/issue/delete modals, validation toasts, apikey reveal),probe-*.png×2 →/tmp/ux-audit/seller/shots/(80 files).