Portfolio Case Study

Seoul Skin Archive

A K-beauty subscription commerce demo engineered to a production TypeScript standard: eight typed domain interfaces, a three-layer demo infrastructure, a scoped CSS token system, and a quiz-to-cart state machine — all isolated from the host application and every external service.

May 11, 20266 min readDemo
Typed domain interfaces
8
CSS design tokens
20+
i18n locales
5 (ko / en / ja / de / fr)

Project brief

Client
Seoul Skin Archive
Role
Frontend architecture, TypeScript domain modeling, commerce state design, scoped design system
Stack
Next.js App RouterTypeScript strict modeReact client islandsCSS custom propertieslocalStorage state machinei18n dictionary system (5 locales)Null Supabase client stubDeterministic fixture graph

Highlights

Defined eight typed domain interfaces — SSAUser, Product, ProductImage, Subscription, Shipment, Address, Profile, BeautyBoxCartItem — matching column names, nullability, and relationship IDs to what a production Supabase schema would expose.
Built a three-layer demo infrastructure: a full SSAClient interface with generic SSAQueryBuilder<T> that returns null in the demo, a synchronous auth hook delivering a fixed persona, and a deterministic fixture graph with consistent cross-referenced IDs across every domain object.
Engineered a CSS custom property design system (20+ tokens) surfaced through a typed useTheme() hook, keeping all visual logic in tokens and inline expressions — no CSS class generation, no build-time processing, no style leakage into the host application.
Implemented a quiz → sessionStorage → buildBeautyBoxCartItem() → localStorage → checkout state machine where the skin profile from the quiz attaches to the cart item and persists across page boundaries as a typed field.

Domain Model

Production-grade TypeScript interfaces across the full commerce schema

Eight TypeScript interfaces define the commerce domain: SSAUser, Product, ProductImage, Subscription, Shipment, Address, Profile, and BeautyBoxCartItem. Field names, nullability, and relationship IDs match the conventions a Supabase-backed schema would expose — nullable recipient phones, optional address lines, is_primary and sort_order on product images, and enum-constrained status fields on subscriptions and shipments.

The product model is the most layered: a sorted image array (is_primary + sort_order), multi-valued skin_types and skin_concern string arrays, and nullable cost and retail fields that reflect real sourcing data quality. The structural match means the demo objects are a direct lift into production code, not a simplified placeholder that would require a rewrite.

Demo Infrastructure

Supabase interface stub, synchronous auth, and a deterministic fixture graph

Three production dependencies are replaced at the interface boundary. The Supabase client (supabase.ts) defines the full SSAAuthClient and generic SSAQueryBuilder<T> interfaces — with correct method signatures for select, eq, order, limit, single, and upsert — but getSupabaseClient() returns null. Component code that imports supabase compiles against the real interface without bundling the library or making any network call.

The auth hook (useRequireAuth) returns DEMO_USER synchronously, eliminating async auth resolution. The fixture graph in demo-data.ts covers every domain object with consistent cross-referenced IDs: DEMO_USER → DEMO_PROFILE → DEMO_ADDRESSES → DEMO_SUBSCRIPTIONS → DEMO_SHIPMENTS → DEMO_PRODUCTS. Relationship fields (shipping_address_id, subscription_id, user_id) resolve correctly across the my/ account pages without a backend.

Design System

CSS custom properties consumed through a typed runtime hook

The visual language is expressed in 20+ CSS custom properties scoped to the SSA context: --ssa-sage, --ssa-espresso, --ssa-cream, --ssa-forest, --ssa-forest-deep, --ssa-card-green, --ssa-sand, and more. The useTheme() hook reads these values at runtime and returns a typed ThemeTokens object used directly in inline style expressions across every component. No CSS class generation, no build-time processing, and no stylesheet interference with the host application.

Typography is centralized in keepBrandTerms() and keepBrandTermsDeep() utilities that protect brand strings from automatic capitalization or translation transforms. The i18n system loads server-side typed dictionary files for five locales (ko, en, ja, de, fr) in server components and passes them to client islands via props, requiring no client-side locale resolution at runtime.

State Machine

Skin profile from the quiz persists as a typed field into the cart item

The purchase flow is a typed hand-off chain: the skin quiz writes the result to sessionStorage; SubscribeClient reads it on mount and passes it to buildBeautyBoxCartItem(), which embeds it in BeautyBoxCartItem.skinType; the cart serializes the item to localStorage; CartClient deserializes it and forwards skinType to the checkout success URL. Each boundary is a typed transition.

Plan selection is driven by BEAUTY_BOX_PLANS — a typed config array with price, numericPrice, and showKit flags — and normalizeBeautyBoxPlanId() coerces arbitrary URL param input (including the legacy onetime variant) to the canonical plan ID. The success page reads from the URL param rather than from upstream component state, making it stateless with respect to the subscription flow.

Interactive build
Seoul Skin Archive

This case study explains the implementation intent and technical structure. The demo lets you review product discovery, skin profile, cart, box composition, account, order, and shipment states without connecting production data or server writes.

See demo

Yurasis

상호: 유라시스 랩

대표: 이규형

사업자등록번호: 842-05-03371

주소: 서울특별시 강남구 선릉로69길 19, 101동 401호(역삼동, 역삼래미안)

업태: 정보통신업 · 종목: 응용 소프트웨어 개발 및 공급업, 컴퓨터 프로그래밍 서비스업

TEL: 010-2709-9846

Email: siholee@yurasis.com

Seoul Skin Archive Case study | YURASIS Portfolio