studio-wallet.md 18 KB

Implementation Plan: Studio Wallet (지갑) Pages

Task Type

  • Frontend
  • Backend (API endpoints assumed to exist or will be created separately)
  • Fullstack

Research Summary

Korean Creator Platform Patterns (CHZZK, SOOP, Toonation, YouTube)

  • All major platforms use separate sub-pages under a "Studio" hub: 수익 요약 / 후원 내역 / 정산 내역 / 정산 정보
  • CHZZK: 수익창출 → 정산내역, 정산정보, 후원내역, 구독내역, 광고수익
  • SOOP: 별풍선 환전, 광고수익 환전, 구독수익 (각각 별도 플로우)
  • Toonation: 대시보드(잔액) + 정산등록 + 정산신청(별도)

Tax & Fee Structure (Korean regulations)

  • 원천징수 3.3% (소득세 3% + 지방소득세 0.3%) — 사업소득 기준
  • 부가세: 물적시설 없는 1인 크리에이터 = 면세사업자 (VAT 없음)
  • 플랫폼 수수료: 설정값으로 관리 (backend config, 추후 결정)

Design Decision: 3-Page Structure

현재 사이드바에 이미 정의된 3개 메뉴를 그대로 유지:

메뉴 경로 역할
잔액 /studio/wallet/balance 현재 잔액 요약 + 최근 내역
수익 /studio/wallet/revenue 후원 수입 상세 내역 (기간별)
출금 /studio/wallet/withdraw 출금 신청 + 출금 내역

Technical Solution

Architecture

  • 각 페이지는 'use client' 클라이언트 컴포넌트
  • fetchApi<T>()/api/studio/wallet/[...path] Route Handler → Backend
  • 기존 charge-logs 패턴 기반 (상태 관리, 기간 필터, 페이지네이션)
  • BEM 클래스 네이밍, studio-page 공통 스타일 활용
  • 반응형: PC 그리드 테이블 + 모바일 DL 레이아웃

Balance Data Model (DropdownData 확장)

기존 DropdownDataspendableBalance(P), withdrawableBalance(M) 활용. 각 페이지별 전용 API 응답 타입 신규 생성.


Implementation Steps

Step 1: Type Definitions

File: types/response/wallet/balance.ts (NEW)

export interface WalletBalanceResponse {
  spendableBalance: number;       // 포인트 (P) 잔액
  withdrawableBalance: number;    // 머니 (M) 잔액
  totalEarned: number;            // 누적 수익
  totalWithdrawn: number;         // 누적 출금
  pendingWithdrawal: number;      // 출금 대기 중 금액
  recentTransactions: {
    id: number;
    type: 'donation_received'|'withdrawal'|'fee'|'adjustment';
    amount: number;
    balance: number;
    description: string;
    createdAt: string;
  }[];
}

File: types/response/wallet/revenue.ts (NEW)

export interface WalletRevenueResponse {
  total: number;
  summary: {
    grossAmount: number;         // 총 후원 금액
    platformFee: number;         // 플랫폼 수수료
    netAmount: number;           // 순수익 (수수료 차감)
  };
  list: {
    id: number;
    donorName: string;
    donorSID: string|null;
    grossAmount: number;
    platformFee: number;
    netAmount: number;
    type: 'donation'|'crew_donation';
    crewName: string|null;
    createdAt: string;
  }[];
}

File: types/response/wallet/withdraw.ts (NEW)

export interface WalletWithdrawHistoryResponse {
  total: number;
  withdrawableBalance: number;
  hasBankAccount: boolean;
  list: {
    id: number;
    requestedAmount: number;     // 신청 금액
    withholdingTax: number;      // 원천징수 (3.3%)
    netAmount: number;           // 실수령액
    status: 'Pending'|'Processing'|'Completed'|'Rejected';
    bankName: string;
    accountNumber: string;       // 마스킹 처리 (뒤 4자리만)
    requestedAt: string;
    completedAt: string|null;
    rejectedReason: string|null;
  }[];
}

export interface WithdrawRequest {
  amount: number;
}

Step 2: Shared Layout & Constants

File: app/studio/wallet/layout.tsx (NEW)

// 단순 children 패스스루 + style import
import './style.scss';
export default function WalletLayout({ children }) {
  return children;
}

File: app/studio/wallet/constants.ts (NEW)

export const PERIOD_TABS = [
  { label: '오늘', value: 0 },
  { label: '1주일', value: 1 },
  { label: '1개월', value: 2 },
  { label: '3개월', value: 3 },
  { label: '6개월', value: 4 },
];

export const WITHDRAW_STATUS_MAP: Record<string, { label: string; cls: string }> = {
  Pending:    { label: '대기',   cls: 'status--pending' },
  Processing: { label: '처리중', cls: 'status--processing' },
  Completed:  { label: '완료',   cls: 'status--completed' },
  Rejected:   { label: '거절',   cls: 'status--rejected' },
};

export const REVENUE_TYPE_MAP: Record<string, string> = {
  donation: '후원',
  crew_donation: '크루 후원',
};

export const WITHHOLDING_TAX_RATE = 0.033; // 3.3%
export const MIN_WITHDRAW_AMOUNT = 40000;  // 최소 출금 금액

Step 3: Balance Page (잔액)

File: app/studio/wallet/balance/page.tsx (NEW)

Layout:

┌─────────────────────────────────────────────┐
│  잔액 현황                                   │
├───────────┬───────────┬────────────────────────┤
│  머니(M)  │ 누적수익  │ 누적출금               │
│  80,000   │ 350,000   │ 270,000                │
├───────────┴───────────┴────────────────────────┤
│                           [출금하기]            │
├─────────────────────────────────────────────────┤
│  최근 거래 내역                                  │
│  일시 | 유형 | 내용 | 금액 | 잔액                │
│  ...                                            │
│  ...                                            │
└─────────────────────────────────────────────────┘

구현 내용:

  • 3개 Summary Card: M잔액(출금 가능), 누적수익, 누적출금
  • 빠른 액션: 출금하기 (/studio/wallet/withdraw 이동)
  • 최근 거래 10건 (페이지네이션 없음, 간략 리스트)
  • API: GET /api/studio/wallet/balance

Step 4: Revenue Page (수익)

의존성 추가: npm install recharts (React 친화적 차트 라이브러리, ~45KB gzip)

File: app/studio/wallet/revenue/page.tsx (NEW)

Layout:

┌─────────────────────────────────────────────────┐
│  수익 내역                                       │
├────────────────┬────────────────┬─────────────────┤
│ 총 후원 금액   │ 플랫폼 수수료  │ 순수익          │
│ 500,000원      │ -50,000원      │ 450,000원       │
├───────────────────────────────────────────────────┤
│  [오늘] [1주일] [1개월] [3개월] [6개월]            │
├───────────────────────────────────────────────────┤
│  수익 추이 (AreaChart)                            │
│  ┌─────────────────────────────────────────────┐ │
│  │    ╱\                                       │ │
│  │   ╱  \      ╱\   ╱\                        │ │
│  │  ╱    \    ╱  ╱    \                       │ │
│  │ ╱      \  ╱  ╱      ───                    │ │
│  │╱        ╲╱  ╱                               │ │
│  └──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┘ │
│     4/1 4/2 4/3 4/4 ...                         │
│  ■ 순수익  ■ 수수료                              │
├───────────────────────────────────────────────────┤
│  합계: 128                                        │
├───────────────────────────────────────────────────┤
│  일시 | 후원자 | 유형 | 후원금액 | 수수료 | 순수익  │
│  04.14 | 유저A | 후원 | 10,000  | -1,000 | 9,000  │
│  ...                                              │
├───────────────────────────────────────────────────┤
│  < 1 2 3 4 5 >                                    │
└───────────────────────────────────────────────────┘

차트 구현:

  • rechartsAreaChart + ResponsiveContainer 사용
  • X축: 날짜 (기간별 자동 그룹핑 — 일간/주간/월간)
  • Y축: 금액 (toLocaleString 포맷)
  • Area 2개: 순수익 (primary color, fill opacity 0.3), 수수료 (muted color, fill opacity 0.15)
  • Tooltip: 날짜 + 순수익 + 수수료 금액 표시
  • 높이 고정 280px, 반응형 너비
  • API: GET /api/studio/wallet/revenue/chart?type={period} (차트용 집계 데이터)

차트 응답 타입 추가 (types/response/wallet/revenue.ts):

export interface RevenueChartItem {
  date: string;           // '2026-04-01'
  grossAmount: number;
  platformFee: number;
  netAmount: number;
}

구현 내용:

  • Summary 3칸: 총 후원 금액, 플랫폼 수수료, 순수익 (선택 기간 기준)
  • 기간 필터 탭 (charge-logs와 동일 패턴)
  • 수익 추이 차트 (AreaChart — 기간 필터 연동)
  • 테이블: 일시, 후원자, 유형(후원/크루후원), 후원금액, 수수료, 순수익
  • 페이지네이션 (20건/페이지)
  • 반응형: PC grid + Mobile dl (차트는 모바일에서 높이 200px로 축소)
  • API: GET /api/studio/wallet/revenue?type={period}&page={n}&perPage=20

Step 5: Withdrawal Page (출금)

File: app/studio/wallet/withdraw/page.tsx (NEW)

Layout:

┌─────────────────────────────────────────────────┐
│  출금                                            │
├─────────────────────────────────────────────────┤
│  출금 가능 잔액: 80,000원 (M)                    │
│                                                  │
│  출금 금액  [_______________] 원                  │
│                                                  │
│  ┌─ 예상 차감 ──────────────────┐               │
│  │ 신청 금액      80,000원     │               │
│  │ 원천징수(3.3%) -2,640원     │               │
│  │ 실수령액       77,360원     │               │
│  └─────────────────────────────┘               │
│                                                  │
│  입금 계좌: 국민은행 ****5678 (홍길동)           │
│  [계좌 변경 →]                                   │
│                                                  │
│  ※ 최소 출금 금액: 40,000원                      │
│  ※ 원천징수 3.3% (소득세 3% + 지방소득세 0.3%)   │
│  ※ 매월 10일까지 신청 → 당월 말 입금              │
│                                                  │
│  [출금 신청]                                     │
├─────────────────────────────────────────────────┤
│  출금 내역                                       │
│  합계: 15     [오늘] [1주일] [1개월] [3개월] [6개월] │
├─────────────────────────────────────────────────┤
│  신청일 | 금액 | 원천징수 | 실수령 | 계좌 | 상태  │
│  ...                                             │
├─────────────────────────────────────────────────┤
│  < 1 2 3 4 5 >                                   │
└─────────────────────────────────────────────────┘

구현 내용:

  • 상단: 출금 가능 잔액 강조 표시
  • 출금 신청 폼: 금액 입력 → 실시간 원천징수/실수령액 계산 프리뷰
  • 계좌 정보 표시 (마스킹) + 정산 페이지 링크
  • 안내 문구 (최소 금액, 세금, 일정)
  • 계좌 미등록 시: 경고 + 계좌 등록 유도
  • 하단: 출금 내역 테이블 (기간 필터 + 페이지네이션)
  • API: GET /api/studio/wallet/withdraw?type={period}&page={n}&perPage=20
  • API: POST /api/studio/wallet/withdraw (출금 신청)

Step 6: SCSS Styling

File: app/studio/wallet/style.scss (NEW)

BEM Block: .wallet

// ── Summary Cards ──
.wallet__cards            // display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px;
.wallet__card             // border, radius 8px, padding 20px
.wallet__card-label       // 0.8125rem, muted color
.wallet__card-value       // 1.5rem, font-weight 700
.wallet__card-value--point   // P color
.wallet__card-value--money   // M color (brand)
.wallet__card-value--danger  // negative amounts

// ── Actions ──
.wallet__actions          // flex, gap 8px, justify center
.wallet__action-btn       // studio-page__btn 스타일 재사용

// ── Filter ──
.wallet__header           // flex, space-between (charge-logs__header 패턴)
.wallet__summary          // 합계 텍스트
.wallet__tabs             // flex, button 탭 (charge-logs__tabs 패턴)

// ── Table ──
.wallet__table-wrap       // studio-page__table-wrap 재사용
.wallet__table            // studio-page__table 재사용 + 커스텀 컬럼
.wallet__amount--plus     // color success
.wallet__amount--minus    // color danger

// ── Withdraw Form ──
.wallet__withdraw-form    // border, radius 8px, padding 24px, max-width 560px
.wallet__withdraw-balance // 잔액 강조 (1.25rem, font-weight 600)
.wallet__withdraw-field   // label + input 그룹
.wallet__withdraw-input   // crew-widget-form__input 패턴
.wallet__withdraw-preview // 차감 프리뷰 박스 (muted bg, border)
.wallet__withdraw-row     // flex, justify space-between
.wallet__withdraw-total   // font-weight 700, border-top
.wallet__withdraw-info    // 안내문 (0.8125rem, muted)
.wallet__withdraw-account // 계좌 정보 표시
.wallet__withdraw-submit  // studio-page__btn--primary

// ── Status Badges ──
.wallet__status--pending     // color warning
.wallet__status--processing  // color primary
.wallet__status--completed   // color success
.wallet__status--rejected    // color danger

// ── Responsive ──
@media (max-width: 768px)
  .wallet__cards: grid 2 columns
  Table: hide ol, show dl

Step 7: API Route Handler

File: app/api/studio/wallet/[...path]/route.ts (NEW — 또는 기존 /api/studio/[...path] 활용)

기존 app/api/studio/[...path]/route.ts 프록시가 있으면 별도 생성 불필요. 없으면 동일한 프록시 패턴으로 생성:

// GET/POST → fetchJson('/api/studio/wallet/{path}')

Key Files

File Operation Description
types/response/wallet/balance.ts Create 잔액 API 응답 타입
types/response/wallet/revenue.ts Create 수익 API 응답 타입
types/response/wallet/withdraw.ts Create 출금 요청/응답 타입
app/studio/wallet/layout.tsx Create style.scss import용 레이아웃
app/studio/wallet/constants.ts Create 공통 상수 (탭, 상태맵, 세율)
app/studio/wallet/style.scss Create 전체 wallet 스타일
app/studio/wallet/balance/page.tsx Create 잔액 현황 페이지
app/studio/wallet/revenue/page.tsx Create 수익 내역 페이지
app/studio/wallet/withdraw/page.tsx Create 출금 신청 + 내역 페이지
app/api/studio/wallet/[...path]/route.ts Create (필요 시) API 프록시
app/studio/Sidebar.tsx None 이미 3개 메뉴 정의됨

Risks and Mitigation

Risk Mitigation
Backend API 미존재 Mock 데이터로 UI 먼저 구현, API 연동은 후속 작업
플랫폼 수수료율 미확정 constants.ts에 변수화, backend config에서 받아오도록 설계
원천징수 계산 정확성 Frontend는 프리뷰만 표시, 실제 계산은 Backend에서 수행
계좌 등록 미구현 출금 페이지에서 정산 → 계좌관리 페이지로 유도
충전 팝업 연동 기존 /charge 페이지 팝업 패턴 그대로 활용

Implementation Order

  1. Types → constants 먼저 (의존성 없음)
  2. layout.tsx + style.scss (공통 기반)
  3. balance/page.tsx (가장 단순, 전체 구조 검증)
  4. revenue/page.tsx (charge-logs 패턴 복사 + 커스터마이즈)
  5. withdraw/page.tsx (폼 + 리스트 복합, 가장 복잡)
  6. API route handler (필요 시)

Notes

  • Backend API 스펙은 별도 협의 필요 (이 계획은 Frontend만 다룸)
  • 정산 메뉴 (계좌관리, 세금계산서)는 별도 계획으로 분리
  • 출금 신청 시 confirm dialog 필수 (금액 + 실수령액 재확인)
  • SESSION_ID: N/A (external model wrapper 미사용)