# Implementation Plan: Studio Wallet (지갑) Pages ## Task Type - [x] 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()` → `/api/studio/wallet/[...path]` Route Handler → Backend - 기존 `charge-logs` 패턴 기반 (상태 관리, 기간 필터, 페이지네이션) - BEM 클래스 네이밍, studio-page 공통 스타일 활용 - 반응형: PC 그리드 테이블 + 모바일 DL 레이아웃 ### Balance Data Model (DropdownData 확장) 기존 `DropdownData`의 `spendableBalance`(P), `withdrawableBalance`(M) 활용. 각 페이지별 전용 API 응답 타입 신규 생성. --- ## Implementation Steps ### Step 1: Type Definitions **File: `types/response/wallet/balance.ts`** (NEW) ```typescript 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) ```typescript 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) ```typescript 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) ```typescript // 단순 children 패스스루 + style import import './style.scss'; export default function WalletLayout({ children }) { return children; } ``` **File: `app/studio/wallet/constants.ts`** (NEW) ```typescript 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 = { 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 = { 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 > │ └───────────────────────────────────────────────────┘ ``` **차트 구현:** - `recharts`의 `AreaChart` + `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`):** ```typescript 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`** ```scss // ── 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` 프록시가 있으면 별도 생성 불필요. 없으면 동일한 프록시 패턴으로 생성: ```typescript // 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 미사용)