|
|
@@ -0,0 +1,422 @@
|
|
|
+# 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<T>()` → `/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<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 > │
|
|
|
+└───────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+**차트 구현:**
|
|
|
+- `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 미사용)
|