UX/UI Design Audit — Public/Buyer Surface
- 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 screenshots (full-page) per route, plus a desktop dark-mode set (
prefers-color-scheme: dark); DOM geometry/computed-style measurement for wrap/overflow; live staging data; source inspection againstfe/assets/css/main.csstokens andUi*components. - Fixture: anonymous buyer; live store
s-e30305fda8e2(3 products), real mock purchase for the complete page. - Evidence:
/tmp/ux-audit/public/{desktop,mobile,dark}/*.png(48 shots),/tmp/ux-audit/public/meta.json - Scope note: Design review only — no product code changed. Functional/auth defects are INDEPENDENT_QA's lane. Severity scale per delegation brief: critical = blocks/misleads buyers on a money path · major = real friction, dead ends, or cross-screen inconsistency · polish = cosmetic/consistency.
- Side effects: two mock orders created on staging to reach the live complete page (
ord_0c3281f89334,ord_dded9f3f44f2) — designed sandbox flow, no real charge.
Screen inventory
| # | Route | Desktop | Mobile | Dark | Live state on staging |
|---|---|---|---|---|---|
| P1 | / | home | home | light-only | renders fully |
| P2 | /login | login | login | light-only | renders fully |
| P3 | /signup | signup | signup | light-only | renders fully |
| P4 | /legal/privacy, /legal/terms | legal-* | legal-* | light-only | placeholder docs |
| P5 | /store/s-e30305fda8e2 | store-index | store-index | ok | 3 products |
| P6 | /store/…/p/prod_661d517f1e50 | store-product | store-product | ok | one_time ₩1,000 |
| P7 | /store/nonexistent-slug-xyz | store-404 | store-404 | ok | 404 state |
| P8 | /donate/s-e30305fda8e2 | donate | donate | ok | donation product live |
| P9 | /checkout/prod_…?store=… | checkout-product-real / -filled / -sub | checkout-product-real | ok | real form (needs ?store=) |
| P10 | /checkout/prod_… (bare) | checkout-product | checkout-product | ok | "스토어 링크 필요" guard |
| P11 | /checkout/item/gi_dia100 | checkout-item | checkout-item | ok | always not-found (see F8) |
| P12 | /checkout/complete/ord_… | checkout-complete-real | checkout-complete-real | ok | session-only (see F5) |
| P13 | /license/verify | license-verify | license-verify | ok | dev tool page |
| P14 | /demo/proj_… ×2 | demo-a/b | demo-b | ok | always not-found (see F8) |
Verified OK (keep)
- Zero horizontal overflow at 375px on all 10 measured routes (
scrollWidth == innerWidth). Responsive grids collapse correctly (3→1 product grid, 2-col checkout → stacked, 4-up tiles → 2×2). - Token discipline is high: pages consistently use
.card,input-base,--ng-*colors,UiCard/UiField/UiInput/UiButton/UiBadge/UiEmpty/UiSkeleton/UiSegmented. No arbitrary hex in page markup except justified brand colors inPaymentMethodPicker(fe/components/payments/PaymentMethodPicker.vue:8-11).tabular-numson all prices. - Tap targets ≥44px on mobile —
min-h-11convention applied to links/nav/checkbox rows;.input-basemin-height 44px media rule (main.css:177-181); tiles h-11. - Loading skeletons mirror final layout on store/product/checkout/donate (
UiSkeletonshaped placeholders — e.g.[productId].vue:93-100,checkout/[productId].vue:166-176). - Mock-env honesty everywhere:
SANDBOX/MOCK 결제badges, "Mock 결제 · 실제 청구되지 않습니다" captions, PoC banner on store, warn banner on license-verify. Trustworthy labeling. - Dark mode renders correctly where enabled — inverted primary button, soft dark surfaces, readable badges on store/checkout/donate/license-verify (dark/* shots). Token coverage is complete.
- Error states mostly consistent —
UiEmpty+ recovery CTA on store-404, product-404, checkout no-store (스토어로 이동/홈으로 이동/다시 시도). - Korean copy tone: consistent 존댓말, concise helper text; form hints are useful ("라이선스 키가 이 이메일과 연결됩니다").
- Checkout order summary, license-verify request/result split, and donate preset+custom amount are well-shaped, single-purpose compositions.
Findings
S-P1 / — landing
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P1-1 | polish | Feature card icon fields (Package, Store, KeyRound…) are defined but never rendered — UiCard has no icon prop. Cards read text-only; dead data suggests an unfinished visual pass. | Render icons (small accent-tinted tile above card title) or drop the fields. | fe/pages/index.vue:3-10; fe/components/ui/UiCard.vue:2 |
| P1-2 | major | Page ignores dark preference entirely (see C2) — the landing is the brand front door and stays light while the store it links to goes dark. | Apply theme globally (C2). | fe/pages/index.vue:2 |
S-P2 /login — S-P3 /signup
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P2-1 | polish | No password-reset path on login. If reset isn't a product feature yet, fine — but no hint either. | Add 비밀번호 재설정 link when feature lands, or a support note. | fe/pages/login.vue:45-47 |
| P3-1 | polish | Password field shows placeholder="8자 이상" and hint="8자 이상" — duplicated microcopy visible under the input. | Keep hint, change placeholder to 비밀번호. | fe/pages/signup.vue:58-59 |
| P3-2 | polish | Consent link label 개인정보 처리 mismatches the doc title 개인정보 처리방침. | Label the link by the document's real name. | fe/pages/signup.vue:75 |
S-P4 /legal/privacy, /legal/terms — legal pages
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P4-1 | major | Dead-end chrome: wordmark→home is the only escape. No footer, no cross-link privacy↔terms, no contact route, no language of record. These pages are linked (mandatory, target=_blank) from signup consent — the surface most associated with trust looks abandoned. | Give legal pages a minimal shared public chrome: back/home affordance, cross-link between the two docs, contact channel, last-updated date. | fe/pages/legal/privacy.vue, terms.vue (13-line stubs) |
| P4-2 | major | Content is a 3-paragraph placeholder (검토 대상 badge). The page itself admits the policy isn't finalized — yet signup asks users to consent to it. (Copy/product gap more than visual.) | Escalate to PM: consent UI should not present an unfinalized document as a binding contract; interim copy should state scope plainly. | same |
| P4-3 | polish | UiBadge variant="warn" — invalid prop (component takes tone); badge silently renders neutral gray, losing the intended warn semantics. | tone="warn". Sweep for other variant= on UiBadge. | fe/pages/legal/privacy.vue:6, terms.vue equivalent; fe/components/ui/UiBadge.vue:3 |
S-P5 /store/[slug] — public storefront
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P5-1 | polish | Card CTA 자세히 → is opacity-0 until hover — invisible affordance on touch; cards look like static content on mobile. | Show a persistent affordance on touch (e.g. chevron always visible below sm), or rely on card-as-link styling cues (price + arrow hint). | fe/pages/store/[sellerSlug]/index.vue:136-138 |
| P5-2 | polish | SANDBOX badge (hidden sm:inline-flex on UiBadge) still leaks onto mobile on staging — measured display:flex at 375px, DOM class list lacks the template inline-flex utility. The A1 fix exists locally but is uncommitted worktree content (git diff fe/components/ui/UiBadge.vue), and the deployed bundle predates it. Harmless visually but proves deploy↔source skew. | Commit + deploy the A1 fix, then re-verify on staging. Flag to INDEPENDENT_QA for deploy-parity check. | fe/layouts/storefront.vue:24; fe/components/ui/UiBadge.vue:9; cf. docs/ux-audit-admin.md A1 |
| P5-3 | polish | Hero h1 renders seller.tagline while seller.name only appears in avatar letter + browser tab — a store with empty tagline produces an empty hero. Donation product cover (D) renders as a near-white tile vs gradient siblings — cover-less products degrade the grid. | Fallback h1 to seller.name; give products a default branded cover when coverGradient is empty. | fe/pages/store/[sellerSlug]/index.vue:89-92,122-125 |
| P5-4 | polish | Footer Powered by wordmark isn't a link (missed brand loop); 판매자 콘솔 → is a seller-audience CTA on a buyer page. | Link wordmark to /; consider de-emphasizing the console link. | fe/layouts/storefront.vue:33-34 |
S-P6 /store/[slug]/p/[id] — product detail
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P6-1 | polish | Stray hairline at top of purchase card: border-t border-line-soft pt-5 divider renders even when no plan-selector/donation block precedes it (visible as an orphan line on this product). | Render the divider conditionally (`v-if="prodPlans.length > 1 | |
| P6-2 | polish | Purchase card is sparse for licensed products — price + CTA + mock caption only. License policy sits far below in the content column; buyer can't see "what I get" at decision point. | Add a compact entitlement/license summary line in the card (key issuance, device count) when licensePolicy.enabled. | same file, aside block :169-228 |
| P6-3 | polish | Mobile: sticky aside becomes in-flow after all content — CTA is fine, but price context could repeat above fold if description is long. | Optional sticky bottom CTA on <lg. | same file :107,169 |
S-P7 /store/[bad] — storefront 404
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P7-1 | polish | Header renders an empty seller identity slot on 404 — invisible avatar + blank name on the left (screenshot: entire left header is dead space). | When headerSeller is empty, fall back to the YetdaPay wordmark → /. | fe/layouts/storefront.vue:9,16-22 |
S-P8 /donate/[slug] — donation page
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P8-1 | polish | No back-to-store affordance on the main form — error/done states have 스토어로 돌아가기 but the form itself doesn't; the only escape is the wordmark (which goes to /dashboard→login bounce, see C1). | Add a back link above the card (same pattern as product/checkout pages). | fe/pages/donate/[slug].vue:123-158 vs :99-110 |
| P8-2 | polish | h1 mid-word break at 375px: …후원하\n기 measured 2-line 72px on long seller names (C7 cluster). | word-break: keep-all on headings; truncate long seller names. | fe/pages/donate/[slug].vue:126 |
| P8-3 | polish | Payment method is hardcoded kakaopay in the call and never shown — a payment-shaped form with no method affordance differs from checkout's picker. | Show the method (read-only badge is enough for a mock) or reuse the picker. | fe/pages/donate/[slug].vue:62 |
S-P9 /checkout/[productId] — checkout form (real flow)
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P9-1 | critical | Header wordmark links to /dashboard → anonymous buyer clicking the logo mid-checkout is bounced to /login, losing checkout context. Every public page on default layout shares this (donate, license-verify, complete, demo). | Point the wordmark to / (or the current store slug when present). Highest-leverage one-line fix on the surface. | fe/layouts/default.vue:5 |
| P9-2 | major | Bare /checkout/[productId] (no ?store=) dead-ends at "스토어 링크가 필요합니다" with only 홈으로 이동 — but the productId is known; a shared/copied checkout link strands buyers one hop from purchase. | Resolve product→seller via public product fetch and auto-redirect, or offer 스토어로 이동 when resolvable. | fe/pages/checkout/[productId].vue:67,263-275 |
| P9-3 | major | Payment-tile labels break mid-word at 375px: 네이버페\n이, 카카오페\n이 — measured 41px (2 lines) vs 20px single-line for 4-char labels; the most critical interactive component on the buyer path looks broken. | whitespace-nowrap on the label span (or grid-cols-1 below sm). | fe/components/payments/PaymentMethodPicker.vue:34-36 |
| P9-4 | major | No footer on default layout — a payment page with zero legal/privacy/contact links (storefront layout has a footer; default has none). | Add the shared public footer (legal links + powered-by) to default layout. | fe/layouts/default.vue (absent) vs storefront.vue:31-36 |
| P9-5 | polish | Mobile: order summary + pay CTA sit below 3 stacked cards — buyers scroll ~2 viewports to reach the total. | Consider a compact sticky summary/CTA bar on <lg. | fe/pages/checkout/[productId].vue:183-260 |
| P9-6 | polish | 결제 시뮬레이션 segmented (success/failure/cancel) is exposed to buyers — appropriate for a PoC sandbox, but must be env-gated or removed for production UX. | Gate behind the same paidCheckoutEnabled/demo flag family; note for release checklist. | fe/pages/checkout/[productId].vue:201-216 |
S-P10 /checkout/item/[itemId] — game-item checkout
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P10-1 | major | Reads db.gameItemById/projectById from local state only — in API mode an anonymous session has no projects, so the route always renders "상품을 찾을 수 없습니다". Public item-checkout links can never work. | Wire a public item fetch (like usePublicStore for stores) or gate the route; don't publish it as a buyer surface. | fe/pages/checkout/item/[itemId].vue:13-14 |
| P10-2 | polish | The not-found UiEmpty has no CTA at all — a true dead end (inconsistent with store/product/checkout guards that offer recovery buttons). | Add 홈으로 이동/retry CTA. | fe/pages/checkout/item/[itemId].vue:82 |
S-P11 /checkout/complete/[orderId] — order confirmation
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P11-1 | major | Session-only receipt: page resolves order via db.orderById — in API mode business data is never persisted (persist.client.ts:30-34), so refresh/bookmark/shared-link shows "주문을 찾을 수 없습니다". Mock email means the buyer has no durable way back to their license key or receipt — the complete page effectively self-destructs after the session. The license-key block exists but only renders while session state lives. | Needs a product decision: public order lookup (order no + email), or an explicit "save your key now" affordance + copy/download; at minimum warn that the page is not re-openable. Coordinate with BACKEND_INFRA for any lookup endpoint. | fe/pages/checkout/complete/[orderId].vue:12-14; fe/plugins/persist.client.ts:8-9,30-34 |
| P11-2 | polish | Summary dl row breaks mid-word at 375px — 구매\n자 label wraps under long email values (C7 cluster). | dt → whitespace-nowrap flex-none; dd → min-w-0 break-all/truncate. | fe/pages/checkout/complete/[orderId].vue:78-83 |
| P11-3 | polish | Error-state CTA labeled 스토어로 이동 navigates to / when no store context — label/destination mismatch. | Label it 홈으로 이동 when fallback is /. | fe/pages/checkout/complete/[orderId].vue:98 |
S-P12 /license/verify — license verification tool
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P12-1 | polish | Strong page (request form, result panel with proper empty state, 데모 키 채우기, API example, honest Mock banner). Only nit: layout flips to dashboard for logged-in sellers — a public tool silently becomes console-chromed; check that the dual-layout switch doesn't surprise buyers who happen to hold a session. | Keep as-is or scope dashboard-chrome to a console-only route. | fe/pages/license/verify.vue:12-22 |
S-P13 /demo/[projectId] — game demo
| ID | Sev | Finding | Recommendation | Files |
|---|---|---|---|---|
| P13-1 | major | Same local-state dead route as P10-1 — db.projectById/itemsForProject never resolve for anonymous in API mode; both tested project ids render "프로젝트를 찾을 수 없습니다". The demo card UI (project header, player balance, shop, purchase modal) is effectively unshippable as a public surface. | Same fix family as P10-1: public fetch or gate. QA seed links (proj_2f071a8d5455, proj_space) are also stale — update fixtures after the data story is decided. | fe/pages/demo/[projectId].vue:14-15 |
| P13-2 | polish | Not-found UiEmpty again has no recovery CTA. | Add CTA (P10-2 pattern). | fe/pages/demo/[projectId].vue:130 |
Cross-cutting
| ID | Sev | Finding | Recommendation |
|---|---|---|---|
| C1 | critical | Three different header destinations/behaviors for the brand mark: landing header (local), storefront (seller identity), default layout wordmark→/dashboard (login bounce for anonymous). On a buyer path the logo must never route into an auth wall. | Point public wordmarks to /; keep console links labeled as such. (fe/layouts/default.vue:5) |
| C2 | major | Dark mode applies only via ThemeToggle → useTheme, mounted in default/storefront/admin/dashboard layouts. layout:false pages (index, login, signup, legal/*) never run it → .dark never applied → system-dark users get light landing → dark store → light checkout-adjacent pages: incoherent. | Apply theme app-wide (client plugin reading ng-theme/system at boot), then decide deliberately whether those pages get a toggle. (fe/composables/useTheme.ts:20-26; fe/layouts/default.vue:10; pages/index.vue:2, login.vue:5, signup.vue:2, legal/*.vue:1) |
| C3 | major | Footer exists only on landing + storefront. default layout pages (checkout, donate, license-verify, complete, demo) have no footer → no legal/contact links on the payment path. | Shared minimal public footer for default layout. |
| C4 | polish | Empty-state CTA coverage is inconsistent: store-404/product-404/checkout guards offer recovery buttons; /demo/* and /checkout/item/* offer nothing. | Make recovery CTA a required slot pattern for all public UiEmpty states. |
| C5 | polish | Every public page serves <title>YetdaPay</title> — no per-page title/description except /h/[publicId]. Hurts tab identity, share previews, and SEO on the one surface where SEO matters. | useHead/useSeoMeta per public page (e.g. {product.name} · {store} — YetdaPay). (fe/pages/h/[publicId].vue:77 is the precedent) |
| C6 | polish | Korean word-break hygiene: mid-word breaks verified at 375px in donate h1 (후원하/기), payment tiles (네이버페/이), complete dl (구매/자). Default overflow-wrap/break rules wrap CJK mid-word. | word-break: keep-all on headings/labels; whitespace-nowrap where space allows; break-all only on the value side. |
| C7 | polish | Deploy↔source skew confirmed: staging UiBadge DOM lacks the template inline-flex class and hidden suppression still reproduces (P5-2); the A1 fix is uncommitted local worktree content. Worktree also carries uncommitted edits to UiButton/UiDropdown/UiKpi/UiSegmented, layouts/admin.vue, pages/admin/index.vue (prior session's remediation — not this audit's). | Commit + deploy the pending remediation; INDEPENDENT_QA/④-C confirm deployed bundle matches source before re-testing UI fixes. |
Top-5 priorities
- Kill the logo→
/dashboardbounce (P9-1/C1). One-line fix infe/layouts/default.vue:5; today every public page's brand mark ejects anonymous buyers into login. Critical on the money path. - Give buyers a durable receipt (P11-1).
/checkout/complete/*is session-only; refresh = "주문을 찾을 수 없습니다" and the license key is unrecoverable (mock email). Decide: public order lookup (backend), or explicit "save this page / copy your key" affordance. Major — needs PM+BACKEND_INFRA. - Resurrect or gate the dead public routes (P10-1, P13-1).
/demo/*and/checkout/item/*can never render for anonymous visitors in API mode; either wire public fetches (parity withusePublicStore) or remove them from the public surface — and give every public empty state a recovery CTA (C4). Major. - Unify theme coverage (C2). Apply the theme at app boot so landing/login/signup/legal honor the user's dark preference — right now the brand's entry pages silently ignore it while the rest of the surface doesn't. Major consistency.
- Legal-page trust pass + mobile word-break batch (P4-1/2/3, C6, P9-3). Real chrome + cross-links + finalized-consent story for
/legal/*(linked from mandatory signup consent), and thekeep-all/nowrapsweep for mid-word Korean breaks — the payment-tile wrap being the worst instance. Major + polish cluster; also pick up the free fixes: stray purchase-card divider (P6-1), invalidvariantprop (P4-3), footer on default layout (P9-4/C3).
Limitations
- Dark-mode review limited to
default/storefrontpages (the only ones that can apply it); light-only pages were captured under dark preference to prove the gap, not to check dark rendering. /demo/*and/checkout/item/*could only be reviewed in their not-found states — the populated designs exist only in code (fe/pages/demo/[projectId].vue:62-129,fe/pages/checkout/item/[itemId].vue:52-84); recommend a follow-up visual pass once a public data path exists.- Checkout-complete success state reviewed via two real mock orders; donation success card and licensed-product license block not exercised live (conditional renders verified in source only).
- Staging may lag local source (C7) — findings were measured on the deployed bundle; a couple may already be fixed locally.
- No a11y tree/contrast measurement pass this round (geometry + visual review only); recommend INDEPENDENT_QA run axe on the public set.