# Cryptoments Partner Console — UI 구현 지침서

> **버전**: v1.0
> **작성일**: 2026-03-23
> **대상**: VS Code + AI 세션에서 partner-ui 프로젝트 구현 시 사용
> **범위**: 프로젝트 구조, 아키텍처, 코딩 규칙, 전체 화면 스펙 (28개 화면, 67+ endpoints)
> **기반**: CONSOLE_IA_V2.md (Part 2), CRYPTOMENTS_PARTNER_CONSOLE_SCREEN_DESIGN.md, PARTNER_UI_CLAUDE_MD.md

---

## 1. 프로젝트 개요

Cryptoments Partner Console은 멀티체인 스테이블코인 결제 게이트웨이의 **파트너(가맹점/총판) 전용 관리 도구**입니다.
10개 메뉴 그룹, 28개 화면, 약 67개 API 엔드포인트로 구성됩니다.

### 1.1 기술 스택

| 카테고리 | 기술 | 버전 | 비고 |
|----------|------|------|------|
| 프레임워크 | Vue 3 (Composition API) | 3.5+ | `<script setup lang="ts">` 필수 |
| 언어 | TypeScript | 5.5+ | strict mode, **`any` 사용 금지** |
| 빌드 | Vite | 6+ | |
| UI 컴포넌트 | shadcn/vue (Reka UI) | 최신 | New York 스타일 |
| 스타일링 | Tailwind CSS | 3.4+ | CSS Variables 기반 테마 |
| 상태 관리 | Pinia | 3.0+ | Composition API 스타일 |
| 라우터 | Vue Router | 4.6+ | |
| HTTP | Axios | 1.13+ | 서비스 레이어 추상화 필수 |
| 폼 검증 | Vee-Validate + Zod | 최신 | 모든 폼에 일관 적용 |
| 테이블 | @tanstack/vue-table | 8.x | |
| 차트 | Chart.js + vue-chartjs | 4.x | 대시보드/리포트 |
| 아이콘 | Lucide Vue Next | 최신 | |
| 날짜 | Day.js | 1.11+ | UTC→KST 변환 |
| 유틸리티 | @vueuse/core | 14+ | |

### 1.2 프로젝트 생성

```bash
npm create vite@latest partner-ui -- --template vue-ts
cd partner-ui
npm install

# UI Framework
npx shadcn-vue@latest init  # New York 스타일 선택

# Core dependencies
npm install vue-router pinia axios
npm install vee-validate @vee-validate/zod zod
npm install @tanstack/vue-table
npm install chart.js vue-chartjs
npm install lucide-vue-next dayjs @vueuse/core

# Styling
npm install -D tailwindcss postcss autoprefixer tailwindcss-animate
npm install tailwind-merge class-variance-authority clsx
```

---

## 2. 디렉토리 구조

```
partner-ui/
├── src/
│   ├── api/                        ← API 서비스 레이어 (핵심!)
│   │   ├── client.ts               ← Axios 인스턴스 + 인터셉터
│   │   ├── types/                  ← API 요청/응답 타입 정의
│   │   │   ├── common.ts           ← XPage<T>, Pagination, ApiError 등
│   │   │   ├── auth.ts             ← LoginRequest/Response, 2FA 등
│   │   │   ├── dashboard.ts
│   │   │   ├── deposit.ts
│   │   │   ├── withdrawal.ts
│   │   │   ├── balance.ts
│   │   │   ├── settlement.ts
│   │   │   ├── gas.ts
│   │   │   ├── sub-partner.ts      ← DISTRIBUTOR only
│   │   │   ├── report.ts           ← DISTRIBUTOR only
│   │   │   ├── integration.ts
│   │   │   └── account.ts
│   │   └── services/               ← 도메인별 API 함수
│   │       ├── auth.service.ts
│   │       ├── dashboard.service.ts
│   │       ├── deposit.service.ts
│   │       ├── withdrawal.service.ts
│   │       ├── balance.service.ts
│   │       ├── settlement.service.ts
│   │       ├── gas.service.ts
│   │       ├── sub-partner.service.ts
│   │       ├── report.service.ts
│   │       ├── integration.service.ts
│   │       └── account.service.ts
│   │
│   ├── components/
│   │   ├── ui/                     ← shadcn/vue 기본 컴포넌트 (자동 설치)
│   │   ├── common/                 ← 공통 비즈니스 컴포넌트
│   │   │   ├── StatusBadge.vue     ← 상태 뱃지 (전역 상태값 매핑)
│   │   │   ├── AddressDisplay.vue  ← 주소 축약 + 복사 + 익스플로러 링크
│   │   │   ├── TxHashDisplay.vue   ← TX Hash 축약 + 익스플로러 링크
│   │   │   ├── AmountDisplay.vue   ← 금액 포맷팅 (콤마, 소수점, 통화)
│   │   │   ├── DateDisplay.vue     ← 날짜 포맷 + 상대시간 hover
│   │   │   ├── NetworkIcon.vue     ← 네트워크 아이콘 + 이름
│   │   │   ├── SearchForm.vue      ← 공통 검색 폼 래퍼
│   │   │   ├── DataTable.vue       ← TanStack Table 래퍼 (정렬, 페이지네이션)
│   │   │   ├── SlideDrawer.vue     ← 우측 슬라이드 오버 (상세 보기)
│   │   │   ├── ConfirmDialog.vue   ← 확인 모달 (위험 액션용)
│   │   │   ├── PageHeader.vue      ← 페이지 제목 + 브레드크럼 + 액션 버튼
│   │   │   ├── SummaryCards.vue    ← 목록 상단 요약 카드
│   │   │   ├── EmptyState.vue      ← 빈 데이터 표시
│   │   │   ├── LoadingSkeleton.vue ← 스켈레톤 로더
│   │   │   ├── TwoFaBanner.vue    ← 2FA 미등록 경고 배너
│   │   │   └── UserIdLink.vue     ← partner_user_id 클릭 → UserContext
│   │   └── layout/
│   │       ├── PartnerLayout.vue   ← 전체 레이아웃 (사이드바 + 헤더 + 콘텐츠)
│   │       ├── Sidebar.vue         ← 사이드바 (partner_type별 메뉴 분기)
│   │       ├── Header.vue          ← 상단 헤더 (파트너명, 알림, 프로필)
│   │       └── NotificationCenter.vue ← 알림 센터 드롭다운
│   │
│   ├── composables/                ← 재사용 로직
│   │   ├── useDataTable.ts         ← 테이블 페이지네이션/정렬/검색 통합
│   │   ├── useConfirm.ts           ← 확인 모달 제어
│   │   ├── useToast.ts             ← 토스트 알림
│   │   ├── useClipboard.ts         ← 클립보드 복사
│   │   ├── useCurrencies.ts        ← 통화 목록 캐시
│   │   ├── useNetworks.ts          ← 네트워크 목록 캐시
│   │   └── useUserContext.ts       ← 사용자 컨텍스트 오버레이 제어
│   │
│   ├── stores/
│   │   ├── auth.ts                 ← 인증 상태 (token, partner 정보, partnerType)
│   │   └── app.ts                  ← 앱 전역 상태 (사이드바, 테마, 2FA 경고)
│   │
│   ├── router/
│   │   └── index.ts                ← 라우트 정의 + 가드 (인증 + partnerType 분기)
│   │
│   ├── utils/
│   │   ├── format.ts               ← 금액/날짜/주소 포맷 함수
│   │   ├── explorer.ts             ← 체인별 익스플로러 URL 생성
│   │   ├── constants.ts            ← 상태값 한글/색상 매핑, 메뉴 정의
│   │   └── validation.ts           ← Zod 스키마 공통 정의
│   │
│   ├── views/                      ← 페이지 컴포넌트 (라우트 1:1 매핑)
│   │   ├── auth/
│   │   │   ├── LoginView.vue                ← PCR-0100
│   │   │   ├── TwoFaView.vue               ← PCR-0110
│   │   │   ├── ForgotPasswordView.vue       ← PCR-0120
│   │   │   └── ResetPasswordView.vue        ← PCR-0130
│   │   ├── dashboard/
│   │   │   └── DashboardView.vue            ← PCR-1000
│   │   ├── deposits/
│   │   │   ├── DepositListView.vue          ← PCR-2000
│   │   │   ├── DepositSessionListView.vue   ← PCR-2010
│   │   │   ├── DepositSessionNewView.vue    ← PCR-2015
│   │   │   ├── PaymentLinkListView.vue      ← PCR-2020
│   │   │   ├── DepositAddressListView.vue   ← PCR-2030
│   │   │   ├── CollectionListView.vue       ← PCR-2040
│   │   │   └── AximPaymentListView.vue      ← PCR-2050
│   │   ├── withdrawals/
│   │   │   ├── WithdrawalListView.vue       ← PCR-3000
│   │   │   ├── WithdrawalNewView.vue        ← PCR-3010
│   │   │   └── WhitelistView.vue            ← PCR-3020
│   │   ├── balance/
│   │   │   ├── BalanceOverviewView.vue      ← PCR-4000
│   │   │   └── LedgerView.vue              ← PCR-4010
│   │   ├── settlement/
│   │   │   ├── DailyFeeView.vue            ← PCR-5000
│   │   │   ├── RealizationView.vue         ← PCR-5010
│   │   │   ├── SettlementBalanceView.vue   ← PCR-5020
│   │   │   └── SettlementWithdrawView.vue  ← PCR-5030
│   │   ├── gas/
│   │   │   ├── GasCostView.vue             ← PCR-6000
│   │   │   └── GasInvoiceView.vue          ← PCR-6010
│   │   ├── sub-partners/                    ← DISTRIBUTOR only
│   │   │   ├── SubPartnerListView.vue      ← PCR-7000
│   │   │   ├── SubPartnerNewView.vue       ← PCR-7010
│   │   │   ├── SubPartnerDetailView.vue    ← PCR-7020
│   │   │   └── SubPartnerOverviewView.vue  ← PCR-7030
│   │   ├── reports/                         ← DISTRIBUTOR only
│   │   │   ├── RevenueReportView.vue       ← PCR-8000
│   │   │   ├── CommissionReportView.vue    ← PCR-8010
│   │   │   └── PerformanceReportView.vue   ← PCR-8020
│   │   ├── settings/
│   │   │   ├── ApiKeyView.vue              ← PCR-9000
│   │   │   ├── WebhookView.vue             ← PCR-9010
│   │   │   ├── TelegramView.vue            ← PCR-9020
│   │   │   └── AximPayView.vue             ← PCR-9030
│   │   └── account/
│   │       ├── ProfileView.vue             ← PCR-A000
│   │       ├── PasswordView.vue            ← PCR-A010
│   │       └── TwoFaSetupView.vue          ← PCR-A020
│   │
│   ├── App.vue
│   ├── main.ts
│   └── style.css                   ← Tailwind + CSS Variables
│
├── .env                            ← VITE_PARTNER_API_URL
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.js
├── components.json                 ← shadcn/vue 설정
└── package.json
```

---

## 3. 핵심 아키텍처 패턴

### 3.1 API 클라이언트 (`src/api/client.ts`)

**원칙: 컴포넌트에서 직접 axios를 호출하지 않는다.**

```typescript
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/auth'
import { useToast } from '@/composables/useToast'
import type { ApiError } from '@/api/types/common'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_PARTNER_API_URL || '/api/partner',
  timeout: 60_000,
  headers: { 'Content-Type': 'application/json' }
})

// Request: Access-Token 자동 첨부
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const auth = useAuthStore()
  if (auth.token) {
    // ⚠ partner-api 토큰 헤더 — Bearer 없이 직접 전달
    config.headers['Access-Token'] = auth.token
  }
  return config
})

// Response: 에러 통합 처리
apiClient.interceptors.response.use(
  (response) => response,
  (error: AxiosError<ApiError>) => {
    const { toast } = useToast()

    if (error.response?.status === 401) {
      const auth = useAuthStore()
      auth.logout()
      window.location.href = '/login'
      return Promise.reject(error)
    }

    // 서버 에러 메시지 표시
    const apiError = error.response?.data
    if (apiError?.message) {
      toast({ title: '오류', description: apiError.message, variant: 'destructive' })
    } else {
      toast({ title: '오류', description: '서버 통신 중 오류가 발생했습니다.', variant: 'destructive' })
    }

    return Promise.reject(error)
  }
)

// Helper 함수
export const api = {
  get: <T>(url: string, params?: Record<string, any>) =>
    apiClient.get<T>(url, { params }).then(r => r.data),

  post: <T>(url: string, data?: any) =>
    apiClient.post<T>(url, data).then(r => r.data),

  put: <T>(url: string, data?: any) =>
    apiClient.put<T>(url, data).then(r => r.data),

  patch: <T>(url: string, data?: any) =>
    apiClient.patch<T>(url, data).then(r => r.data),

  delete: <T>(url: string) =>
    apiClient.delete<T>(url).then(r => r.data),
}

export default apiClient
```

### 3.2 응답 형식 규칙

**단건 응답** — 객체 직접 반환 (래퍼 없음):
```json
{ "id": 1, "partnerCode": "P001", "status": "ACTIVE" }
```

**목록 응답 (XPage)** — Axim 페이지네이션:
```json
{ "pageRows": [{ ... }], "page": 1, "size": 20, "offset": 0, "hasNext": true, "totalCount": 45, "sort": "", "orders": [] }
```

**에러 응답**:
```json
{ "code": "304", "message": "지갑을 찾을 수 없습니다." }
```

### 3.3 페이지네이션 파라미터

```
GET /api/partner/deposits?page=1&size=20&sort=id&order=desc
```
- `page`: 1-based (1부터 시작)
- `size`: 기본 20 (선택: 20/50/100)
- `sort`: 정렬 컬럼
- `order`: asc / desc

### 3.4 날짜 처리

- 서버: `yyyy-MM-dd HH:mm:ss` (UTC)
- 프론트: KST 변환 표시 → `dayjs.utc(date).tz('Asia/Seoul').format('YYYY-MM-DD HH:mm:ss')`
- 필터 파라미터: `from=2026-01-01&to=2026-01-31` (yyyy-MM-dd)

### 3.5 null 정책

- `spring.jackson.default-property-inclusion: non_null` → null 필드 응답에서 제외
- 프론트: optional chaining 필수 → `item?.txHash`

---

## 4. 공통 타입 정의

### 4.1 `src/api/types/common.ts`

```typescript
/** Axim 페이지네이션 응답 */
export interface XPage<T> {
  pageRows: T[]
  page: number
  size: number
  offset: number
  hasNext: boolean
  totalCount: number
  sort: string
  orders: string[]
}

/** 에러 응답 */
export interface ApiError {
  code: string
  message: string
  errors?: Record<string, string>
}

/** 페이지네이션 요청 파라미터 */
export interface PaginationParams {
  page?: number
  size?: number
  sort?: string
  order?: 'asc' | 'desc'
}

/** 기간 필터 */
export interface DateRangeParams {
  from?: string  // yyyy-MM-dd
  to?: string
}

/** 메시지 응답 */
export interface MessageResponse {
  message: string
}
```

### 4.2 `src/api/types/auth.ts`

```typescript
export interface LoginRequest {
  loginEmail: string
  password: string
}

export interface LoginResponse {
  /** 2FA 필요 여부 */
  requires2fa: boolean
  /** 2FA 불필요 시 최종 토큰, 2FA 필요 시 null */
  accessToken?: string
  /** 2FA 필요 시 임시 토큰 */
  tempToken?: string
  /** 파트너 기본 정보 */
  partner?: PartnerInfo
}

export interface PartnerInfo {
  id: number
  partnerCode: string
  name: string
  partnerType: 'MERCHANT' | 'DISTRIBUTOR'
  status: 'ACTIVE' | 'SUSPENDED' | 'PENDING' | 'TERMINATED'
  twoFactorEnabled: boolean
  loginEmail: string
}

export interface TwoFaVerifyRequest {
  tempToken: string
  otpCode: string
}

export interface TwoFaVerifyResponse {
  accessToken: string
  partner: PartnerInfo
}
```

### 4.3 `src/api/types/deposit.ts`

```typescript
export interface DepositResponse {
  id: number
  depositCode: string
  partnerId: number
  partnerUserId?: string
  partnerReference?: string
  sessionId?: number
  sessionCode?: string
  networkId: number
  networkName?: string
  currencyId: number
  currencyCode?: string
  amount: string           // BigDecimal → string
  feeAmount: string
  netAmount: string
  depositMethod: 'HD_WALLET' | 'EXTERNAL_WALLET' | 'DECIMAL_MATCH'
  depositSource: 'SESSION' | 'DIRECT' | 'UNIDENTIFIED'
  status: DepositStatus
  txHash?: string
  blockNumber?: number
  confirmations?: number
  requiredConfirmations?: number
  fromAddress?: string
  toAddress?: string
  createdAt: string
  confirmedAt?: string
  settledAt?: string
}

export type DepositStatus =
  | 'DETECTED' | 'CONFIRMING' | 'CONFIRMED'
  | 'COLLECTING' | 'SETTLED' | 'FAILED'

export interface DepositSearchParams extends PaginationParams, DateRangeParams {
  status?: DepositStatus
  currencyId?: number
  networkId?: number
  depositSource?: string
  keyword?: string  // depositCode or txHash
}

export interface DepositSessionResponse {
  id: number
  sessionCode: string
  partnerId: number
  partnerUserId?: string
  partnerReference?: string
  networkId: number
  networkName?: string
  currencyId: number
  currencyCode?: string
  depositMethod: string
  requestedAmount: string
  fullAmount?: string      // 소수점 매칭 시 실제 입금 금액
  decimalKey?: string      // 소수점 키
  assignedAddress?: string
  status: 'ACTIVE' | 'COMPLETED' | 'EXPIRED' | 'CANCELLED'
  requestSource: string
  expiresAt: string
  createdAt: string
}

export interface CreateDepositSessionRequest {
  currencyId: number
  networkId: number
  depositMethod: 'HD_WALLET' | 'DECIMAL_MATCH' | 'DIRECT'
  requestedAmount: string
  requestedCurrency?: string  // 'KRW' or crypto
  partnerUserId?: string
  partnerReference?: string
  expiresInMinutes: number    // 15, 30, 60, 180, 360, 720, 1440
}
```

### 4.4 `src/api/types/withdrawal.ts`

```typescript
export interface WithdrawalResponse {
  id: number
  withdrawalCode: string
  partnerId: number
  partnerUserId?: string
  networkId: number
  networkName?: string
  currencyId: number
  currencyCode?: string
  amount: string
  feeAmount: string
  netAmount: string
  toAddress: string
  withdrawalType: 'API_WITHDRAWAL' | 'MANUAL_WITHDRAWAL' | 'SETTLEMENT_WITHDRAW'
  status: WithdrawalStatus
  txHash?: string
  blockNumber?: number
  note?: string              // 거부/취소 사유
  createdAt: string
  confirmedAt?: string
}

export type WithdrawalStatus =
  | 'REQUESTED' | 'PENDING_APPROVAL' | 'APPROVED' | 'REJECTED'
  | 'BALANCE_PENDING' | 'PROCESSING' | 'BROADCASTING'
  | 'CONFIRMED' | 'FAILED' | 'CANCELLED'

export interface CreateWithdrawalRequest {
  networkId: number
  currencyId: number
  toAddress: string
  amount: string
}

export interface WhitelistEntry {
  id: number
  networkId: number
  networkName?: string
  address: string
  label?: string
  verified: boolean
  isActive: boolean
  createdAt: string
}
```

### 4.5 `src/api/types/settlement.ts`

```typescript
export interface SettlementDailyFee {
  id: number
  feeDate: string
  currencyId: number
  currencyCode?: string
  role: 'OPERATOR' | 'DISTRIBUTOR' | 'SYSTEM'
  shareAmount: string
  txCount: number
  totalAmount: string
}

export interface SettlementRealization {
  id: number
  currencyId: number
  currencyCode?: string
  periodStart: string
  periodEnd: string
  realizedAmount: string
  realizationTxHash?: string
  status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'
  realizedAt?: string
}

export interface SettlementBalance {
  id: number
  currencyId: number
  currencyCode?: string
  unrealizedBalance: string
  realizedBalance: string
  withdrawnBalance: string
  withdrawableBalance: string  // realized - withdrawn
}
```

### 4.6 기타 타입 (`dashboard.ts`, `gas.ts`, `integration.ts`, `account.ts`, `sub-partner.ts`, `report.ts`)

> 같은 패턴으로 작성. 각 화면 스펙(섹션 8)의 API 응답 필드 참조.
> **핵심 규칙**: BigDecimal → `string`, camelCase JSON, optional chaining 필수.

---

## 5. API 서비스 레이어

### 5.1 `src/api/services/auth.service.ts`

```typescript
import { api } from '@/api/client'
import type { LoginRequest, LoginResponse, TwoFaVerifyRequest, TwoFaVerifyResponse } from '@/api/types/auth'

export const authService = {
  login: (data: LoginRequest) =>
    api.post<LoginResponse>('/auth/login', data),

  verify2fa: (data: TwoFaVerifyRequest) =>
    api.post<TwoFaVerifyResponse>('/auth/2fa/verify', data),

  logout: () =>
    api.post<void>('/auth/logout'),
}
```

### 5.2 `src/api/services/deposit.service.ts`

```typescript
import { api } from '@/api/client'
import type { XPage } from '@/api/types/common'
import type { DepositResponse, DepositSearchParams, DepositSessionResponse, CreateDepositSessionRequest } from '@/api/types/deposit'

export const depositService = {
  // 입금 내역
  getDeposits: (params: DepositSearchParams) =>
    api.get<XPage<DepositResponse>>('/deposits', params),

  getDeposit: (id: number) =>
    api.get<DepositResponse>(`/deposits/${id}`),

  // 입금 세션
  getDepositSessions: (params: DepositSearchParams) =>
    api.get<XPage<DepositSessionResponse>>('/deposit-sessions', params),

  getDepositSession: (id: number) =>
    api.get<DepositSessionResponse>(`/deposit-sessions/${id}`),

  createDepositSession: (data: CreateDepositSessionRequest) =>
    api.post<DepositSessionResponse>('/deposit-sessions', data),

  // 결제 링크
  getPaymentLinks: (params: any) =>
    api.get<XPage<any>>('/payment-links', params),

  // 입금 주소
  getDepositAddresses: (params: any) =>
    api.get<XPage<any>>('/deposit-addresses', params),

  getDepositAddress: (id: number) =>
    api.get<any>(`/deposit-addresses/${id}`),

  // 집금 현황
  getCollections: (params: any) =>
    api.get<XPage<any>>('/collections', params),

  getCollection: (id: number) =>
    api.get<any>(`/collections/${id}`),

  // Axim Pay
  getAximPayments: (params: any) =>
    api.get<XPage<any>>('/axim-payments', params),
}
```

### 5.3 나머지 서비스 — 동일 패턴

| 서비스 파일 | prefix | 주요 메서드 |
|------------|--------|-----------|
| `withdrawal.service.ts` | `/withdrawals` | getWithdrawals, getWithdrawal, createWithdrawal, cancelWithdrawal, getWhitelist, addWhitelist, updateWhitelist |
| `balance.service.ts` | `/balances`, `/ledger` | getBalances, getLedger |
| `settlement.service.ts` | `/settlement` | getDailyFees, getRealizations, getBalance, requestWithdraw |
| `gas.service.ts` | `/gas-costs`, `/gas-invoices` | getGasCosts, getGasInvoices, getGasInvoice |
| `sub-partner.service.ts` | `/sub-partners` | getSubPartners, createSubPartner, getSubPartner, updateSubPartner, getOverview |
| `report.service.ts` | `/reports` | getRevenue, getFees, getPerformance |
| `integration.service.ts` | `/integration` | getApiKey, regenerateApiKey, updateWebhook, testWebhook, getWebhookLogs, getTelegram, updateTelegram, testTelegram, getAxim |
| `account.service.ts` | `/account` | getProfile, updateProfile, changePassword, get2faSetup, enable2fa, disable2fa |
| `dashboard.service.ts` | `/dashboard` | getSummary, getChart |

---

## 6. 라우팅 (`src/router/index.ts`)

```typescript
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ── 비인증 ──
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/views/auth/LoginView.vue'),
      meta: { requiresAuth: false }
    },
    {
      path: '/login/2fa',
      name: 'TwoFa',
      component: () => import('@/views/auth/TwoFaView.vue'),
      meta: { requiresAuth: false }
    },
    {
      path: '/forgot-password',
      name: 'ForgotPassword',
      component: () => import('@/views/auth/ForgotPasswordView.vue'),
      meta: { requiresAuth: false }
    },
    {
      path: '/reset-password',
      name: 'ResetPassword',
      component: () => import('@/views/auth/ResetPasswordView.vue'),
      meta: { requiresAuth: false }
    },

    // ── 인증 필요 (PartnerLayout) ──
    {
      path: '/',
      component: () => import('@/components/layout/PartnerLayout.vue'),
      meta: { requiresAuth: true },
      children: [
        // 1. 대시보드
        { path: '', redirect: '/dashboard' },
        { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },

        // 2. 입금 관리
        { path: 'deposits', name: 'DepositList', component: () => import('@/views/deposits/DepositListView.vue') },
        { path: 'deposit-sessions', name: 'DepositSessionList', component: () => import('@/views/deposits/DepositSessionListView.vue') },
        { path: 'deposit-sessions/new', name: 'DepositSessionNew', component: () => import('@/views/deposits/DepositSessionNewView.vue') },
        { path: 'payment-links', name: 'PaymentLinkList', component: () => import('@/views/deposits/PaymentLinkListView.vue') },
        { path: 'deposit-addresses', name: 'DepositAddressList', component: () => import('@/views/deposits/DepositAddressListView.vue') },
        { path: 'collections', name: 'CollectionList', component: () => import('@/views/deposits/CollectionListView.vue') },
        { path: 'axim-payments', name: 'AximPaymentList', component: () => import('@/views/deposits/AximPaymentListView.vue') },

        // 3. 출금 관리
        { path: 'withdrawals', name: 'WithdrawalList', component: () => import('@/views/withdrawals/WithdrawalListView.vue') },
        { path: 'withdrawals/new', name: 'WithdrawalNew', component: () => import('@/views/withdrawals/WithdrawalNewView.vue') },
        { path: 'withdrawals/whitelist', name: 'Whitelist', component: () => import('@/views/withdrawals/WhitelistView.vue') },

        // 4. 잔액/원장
        { path: 'balances', name: 'Balances', component: () => import('@/views/balance/BalanceOverviewView.vue') },
        { path: 'ledger', name: 'Ledger', component: () => import('@/views/balance/LedgerView.vue') },

        // 5. 정산
        { path: 'settlement/fees', name: 'DailyFees', component: () => import('@/views/settlement/DailyFeeView.vue') },
        { path: 'settlement/realizations', name: 'Realizations', component: () => import('@/views/settlement/RealizationView.vue') },
        { path: 'settlement/balance', name: 'SettlementBalance', component: () => import('@/views/settlement/SettlementBalanceView.vue') },
        { path: 'settlement/withdraw', name: 'SettlementWithdraw', component: () => import('@/views/settlement/SettlementWithdrawView.vue') },

        // 6. 가스비
        { path: 'gas-costs', name: 'GasCosts', component: () => import('@/views/gas/GasCostView.vue') },
        { path: 'gas-invoices', name: 'GasInvoices', component: () => import('@/views/gas/GasInvoiceView.vue') },

        // 7. 하위 파트너 (DISTRIBUTOR only)
        { path: 'sub-partners', name: 'SubPartnerList', component: () => import('@/views/sub-partners/SubPartnerListView.vue'), meta: { distributorOnly: true } },
        { path: 'sub-partners/new', name: 'SubPartnerNew', component: () => import('@/views/sub-partners/SubPartnerNewView.vue'), meta: { distributorOnly: true } },
        { path: 'sub-partners/:id', name: 'SubPartnerDetail', component: () => import('@/views/sub-partners/SubPartnerDetailView.vue'), meta: { distributorOnly: true } },
        { path: 'sub-partners/overview', name: 'SubPartnerOverview', component: () => import('@/views/sub-partners/SubPartnerOverviewView.vue'), meta: { distributorOnly: true } },

        // 8. 총판 리포트 (DISTRIBUTOR only)
        { path: 'reports/revenue', name: 'RevenueReport', component: () => import('@/views/reports/RevenueReportView.vue'), meta: { distributorOnly: true } },
        { path: 'reports/commission', name: 'CommissionReport', component: () => import('@/views/reports/CommissionReportView.vue'), meta: { distributorOnly: true } },
        { path: 'reports/performance', name: 'PerformanceReport', component: () => import('@/views/reports/PerformanceReportView.vue'), meta: { distributorOnly: true } },

        // 9. 연동 설정
        { path: 'settings/api', name: 'ApiKey', component: () => import('@/views/settings/ApiKeyView.vue') },
        { path: 'settings/webhook', name: 'Webhook', component: () => import('@/views/settings/WebhookView.vue') },
        { path: 'settings/telegram', name: 'Telegram', component: () => import('@/views/settings/TelegramView.vue') },
        { path: 'settings/axim', name: 'AximPay', component: () => import('@/views/settings/AximPayView.vue') },

        // 10. 내 계정
        { path: 'account/profile', name: 'Profile', component: () => import('@/views/account/ProfileView.vue') },
        { path: 'account/password', name: 'Password', component: () => import('@/views/account/PasswordView.vue') },
        { path: 'account/2fa', name: 'TwoFaSetup', component: () => import('@/views/account/TwoFaSetupView.vue') },
      ]
    },

    // 404
    { path: '/:pathMatch(.*)*', redirect: '/dashboard' }
  ]
})

// Navigation Guard
router.beforeEach((to) => {
  const auth = useAuthStore()

  // 인증 불필요 페이지
  if (to.meta.requiresAuth === false) {
    if (auth.isAuthenticated) return { path: '/dashboard' }
    return true
  }

  // 인증 필요
  if (!auth.isAuthenticated) {
    return { path: '/login', query: { redirect: to.fullPath } }
  }

  // DISTRIBUTOR only 페이지
  if (to.meta.distributorOnly && auth.partnerType !== 'DISTRIBUTOR') {
    return { path: '/dashboard' }
  }

  return true
})

export default router
```

---

## 7. Pinia 스토어

### 7.1 `src/stores/auth.ts`

```typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { PartnerInfo } from '@/api/types/auth'

export const useAuthStore = defineStore('auth', () => {
  const token = ref<string | null>(localStorage.getItem('accessToken'))
  const partner = ref<PartnerInfo | null>(
    JSON.parse(localStorage.getItem('partner') || 'null')
  )

  const isAuthenticated = computed(() => !!token.value)
  const partnerType = computed(() => partner.value?.partnerType)
  const isDistributor = computed(() => partnerType.value === 'DISTRIBUTOR')
  const twoFactorEnabled = computed(() => partner.value?.twoFactorEnabled ?? false)

  function setAuth(accessToken: string, partnerInfo: PartnerInfo) {
    token.value = accessToken
    partner.value = partnerInfo
    localStorage.setItem('accessToken', accessToken)
    localStorage.setItem('partner', JSON.stringify(partnerInfo))
  }

  function setToken(newToken: string) {
    token.value = newToken
    localStorage.setItem('accessToken', newToken)
  }

  function logout() {
    token.value = null
    partner.value = null
    localStorage.removeItem('accessToken')
    localStorage.removeItem('partner')
  }

  return { token, partner, isAuthenticated, partnerType, isDistributor, twoFactorEnabled, setAuth, setToken, logout }
})
```

### 7.2 `src/stores/app.ts`

```typescript
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useAppStore = defineStore('app', () => {
  const sidebarCollapsed = ref(false)
  const twofaBannerDismissed = ref(false)

  function toggleSidebar() {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }

  function dismissTwofaBanner() {
    twofaBannerDismissed.value = true
  }

  return { sidebarCollapsed, twofaBannerDismissed, toggleSidebar, dismissTwofaBanner }
})
```

---

## 8. 사이드바 메뉴 정의 (`src/utils/constants.ts`)

```typescript
import {
  LayoutDashboard, ArrowDownToLine, ArrowUpFromLine,
  Wallet, Calculator, Fuel, Users, BarChart3,
  Settings, User
} from 'lucide-vue-next'

export interface MenuItem {
  label: string
  icon: any
  path?: string
  distributorOnly?: boolean
  children?: { label: string; path: string }[]
}

export const MENU_ITEMS: MenuItem[] = [
  {
    label: '대시보드', icon: LayoutDashboard, path: '/dashboard'
  },
  {
    label: '입금 관리', icon: ArrowDownToLine,
    children: [
      { label: '입금 내역', path: '/deposits' },
      { label: '입금 세션', path: '/deposit-sessions' },
      { label: '결제 링크', path: '/payment-links' },
      { label: '입금 주소', path: '/deposit-addresses' },
      { label: '집금 현황', path: '/collections' },
      { label: 'Axim Pay', path: '/axim-payments' },
    ]
  },
  {
    label: '출금 관리', icon: ArrowUpFromLine,
    children: [
      { label: '출금 내역', path: '/withdrawals' },
      { label: '출금 요청', path: '/withdrawals/new' },
      { label: '화이트리스트', path: '/withdrawals/whitelist' },
    ]
  },
  {
    label: '잔액/원장', icon: Wallet,
    children: [
      { label: '잔액 현황', path: '/balances' },
      { label: '원장 조회', path: '/ledger' },
    ]
  },
  {
    label: '정산', icon: Calculator,
    children: [
      { label: '일별 수수료', path: '/settlement/fees' },
      { label: '실현 내역', path: '/settlement/realizations' },
      { label: '정산 잔액', path: '/settlement/balance' },
      { label: '쉐어 출금', path: '/settlement/withdraw' },
    ]
  },
  {
    label: '가스비', icon: Fuel,
    children: [
      { label: '가스비 기록', path: '/gas-costs' },
      { label: '인보이스', path: '/gas-invoices' },
    ]
  },
  // ── DISTRIBUTOR ONLY ──
  {
    label: '하위 파트너', icon: Users, distributorOnly: true,
    children: [
      { label: '파트너 목록', path: '/sub-partners' },
      { label: '파트너 등록', path: '/sub-partners/new' },
      { label: '거래 현황', path: '/sub-partners/overview' },
    ]
  },
  {
    label: '총판 리포트', icon: BarChart3, distributorOnly: true,
    children: [
      { label: '매출 리포트', path: '/reports/revenue' },
      { label: '수수료 수익', path: '/reports/commission' },
      { label: '성과 비교', path: '/reports/performance' },
    ]
  },
  // ── 공통 설정 ──
  {
    label: '연동 설정', icon: Settings,
    children: [
      { label: 'API 키', path: '/settings/api' },
      { label: 'Webhook', path: '/settings/webhook' },
      { label: 'Telegram', path: '/settings/telegram' },
      { label: 'Axim Pay', path: '/settings/axim' },
    ]
  },
  {
    label: '내 계정', icon: User,
    children: [
      { label: '프로필', path: '/account/profile' },
      { label: '비밀번호', path: '/account/password' },
      { label: '2FA 설정', path: '/account/2fa' },
    ]
  },
]
```

---

## 9. 공통 컴포넌트 상세

### 9.1 StatusBadge — 상태 뱃지 색상 매핑

```typescript
// src/utils/constants.ts
export const STATUS_COLORS: Record<string, string> = {
  // 🟢 초록 — 성공/완료
  ACTIVE: 'bg-green-100 text-green-800',
  CONFIRMED: 'bg-green-100 text-green-800',
  COMPLETED: 'bg-green-100 text-green-800',
  PAID: 'bg-green-100 text-green-800',
  SETTLED: 'bg-green-100 text-green-800',

  // 🔵 파랑 — 진행중
  CONFIRMING: 'bg-blue-100 text-blue-800',
  COLLECTING: 'bg-blue-100 text-blue-800',
  PROCESSING: 'bg-blue-100 text-blue-800',
  BROADCASTING: 'bg-blue-100 text-blue-800',

  // 🟡 노랑 — 대기
  PENDING: 'bg-yellow-100 text-yellow-800',
  QUEUED: 'bg-yellow-100 text-yellow-800',
  PENDING_APPROVAL: 'bg-yellow-100 text-yellow-800',
  REQUESTED: 'bg-yellow-100 text-yellow-800',
  DRAFT: 'bg-yellow-100 text-yellow-800',

  // 🟠 주황 — 경고
  SUSPENDED: 'bg-orange-100 text-orange-800',
  OVERDUE: 'bg-orange-100 text-orange-800',
  DEFERRED: 'bg-orange-100 text-orange-800',

  // 🔴 빨강 — 실패/거부
  FAILED: 'bg-red-100 text-red-800',
  REJECTED: 'bg-red-100 text-red-800',
  TERMINATED: 'bg-red-100 text-red-800',
  DENIED: 'bg-red-100 text-red-800',

  // ⚪ 회색 — 비활성
  DETECTED: 'bg-gray-100 text-gray-800',
  CANCELLED: 'bg-gray-100 text-gray-800',
  EXPIRED: 'bg-gray-100 text-gray-800',
  INACTIVE: 'bg-gray-100 text-gray-800',
  CANCELED: 'bg-gray-100 text-gray-800',
}

export const STATUS_LABELS_KO: Record<string, string> = {
  ACTIVE: '활성', CONFIRMED: '확인', COMPLETED: '완료',
  PAID: '납부', SETTLED: '정산완료',
  CONFIRMING: '확인중', COLLECTING: '집금중',
  PROCESSING: '처리중', BROADCASTING: '전송중',
  PENDING: '대기', QUEUED: '대기열', PENDING_APPROVAL: '승인대기',
  REQUESTED: '요청', DRAFT: '초안',
  SUSPENDED: '정지', OVERDUE: '연체', DEFERRED: '보류',
  FAILED: '실패', REJECTED: '거부', TERMINATED: '해지', DENIED: '거부',
  DETECTED: '감지', CANCELLED: '취소', EXPIRED: '만료',
  INACTIVE: '비활성', CANCELED: '취소',
  BALANCE_PENDING: '잔액대기', APPROVED: '승인',
  ISSUED: '발행',
}
```

### 9.2 AddressDisplay — 주소 표시 + 복사

**Props**: `address: string`, `network?: string`, `truncateLength?: number` (default 10)
**동작**: 앞 N자리...뒤 4자리, [복사] 아이콘, 클릭 시 체인 익스플로러 새 탭

### 9.3 TxHashDisplay — TX Hash + 익스플로러 링크

**Props**: `txHash: string`, `networkId?: number`
**동작**: 앞 8자리...뒤 4자리, 🔗 클릭 시 익스플로러 오픈

### 9.4 AmountDisplay — 금액 포맷

**Props**: `amount: string`, `currency?: string`, `decimals?: number` (default 6)
**동작**: 천 단위 콤마, 소수점 N자리, 통화 표시

### 9.5 DataTable — TanStack Table 래퍼

**Props**: `columns`, `data`, `pagination: XPage<T>`, `loading`, `onPageChange`, `onSortChange`
**기능**: 정렬 토글, 페이지네이션 (20/50/100), 빈 상태, 로딩 스켈레톤

### 9.6 SlideDrawer — 상세 보기 슬라이드 오버

**Props**: `open: boolean`, `title: string`, `width?: string`
**동작**: 우측에서 슬라이드 인, ESC/배경클릭으로 닫기

### 9.7 익스플로러 URL 유틸 (`src/utils/explorer.ts`)

```typescript
const EXPLORERS: Record<string, string> = {
  'Ethereum': 'https://etherscan.io',
  'BSC': 'https://bscscan.com',
  'Polygon': 'https://polygonscan.com',
  'Tron': 'https://tronscan.org',
}

export function getTxUrl(networkName: string, txHash: string): string {
  const base = EXPLORERS[networkName]
  if (!base) return '#'
  if (networkName === 'Tron') return `${base}/#/transaction/${txHash}`
  return `${base}/tx/${txHash}`
}

export function getAddressUrl(networkName: string, address: string): string {
  const base = EXPLORERS[networkName]
  if (!base) return '#'
  if (networkName === 'Tron') return `${base}/#/address/${address}`
  return `${base}/address/${address}`
}
```

---

## 10. Vite 설정

### 10.1 `vite.config.ts`

```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  server: {
    port: 5173,
    proxy: {
      '/api/partner': {
        target: 'http://localhost:8081',
        changeOrigin: true,
      },
    },
  },
})
```

### 10.2 `.env`

```
VITE_PARTNER_API_URL=/api/partner
```

> Vite 프록시를 사용하므로 baseURL은 상대 경로. 프로덕션은 nginx reverse proxy.

---

## 11. 화면별 구현 스펙 (28개)

> 각 화면에 화면코드(PCR-xxxx), 라우트, API, 주요 컴포넌트, 구현 포인트를 정리.
> 상세 와이어프레임은 `CRYPTOMENTS_PARTNER_CONSOLE_SCREEN_DESIGN.md` 참조.

---

### 11.0 인증 (4개 화면)

#### PCR-0100 로그인 (`/login`)

| 항목 | 상세 |
|------|------|
| API | `POST /auth/login` → LoginResponse |
| 분기 | `requires2fa=true` → `/login/2fa` (tempToken 전달) / `false` → `setAuth()` → `/dashboard` |
| 검증 | 이메일 형식, 비밀번호 8자 이상 |
| 에러 | 5회 연속 실패 → 15분 잠금 카운트다운 표시 |
| UI | 센터 카드, 로고, 이메일/비밀번호 입력, "로그인 유지" 체크, "비밀번호 잊음" 링크 |

#### PCR-0110 2FA 인증 (`/login/2fa`)

| 항목 | 상세 |
|------|------|
| API | `POST /auth/2fa/verify` → TwoFaVerifyResponse |
| 입력 | 6자리 OTP 코드 (개별 입력칸) |
| 에러 | 3회 실패 → `/login` 리다이렉트 |
| 전달 | `tempToken`은 route query 또는 store 경유 |

#### PCR-0120 비밀번호 재설정 요청 (`/forgot-password`)

| 항목 | 상세 |
|------|------|
| API | `POST /auth/password/forgot` |
| UI | 이메일 입력 → "재설정 링크가 발송되었습니다" (존재 여부 무관 동일 메시지) |

#### PCR-0130 새 비밀번호 설정 (`/reset-password?token=...`)

| 항목 | 상세 |
|------|------|
| API | `POST /auth/password/reset` |
| 검증 | 8자 이상, 영문+숫자+특수문자, 비밀번호 확인 일치 |

---

### 11.1 대시보드 (PCR-1000, `/dashboard`)

| 항목 | 상세 |
|------|------|
| API 1 | `GET /dashboard/summary` → DashboardSummaryResponse |
| API 2 | `GET /dashboard/chart?days=7` → DailyChartResponse[] |
| 자동갱신 | 60초 인터벌 + [새로고침] 버튼 |

**KPI 카드 (4개)**:
| 카드 | 필드 | 비교 |
|------|------|------|
| 오늘 입금 | todayDepositCount, todayDepositAmount | 전일 대비 % |
| 오늘 출금 | todayWithdrawalCount, todayWithdrawalAmount | 전일 대비 % |
| 가용 잔액 | 통화별 합산 | — |
| 미납 인보이스 | pendingInvoiceCount | 건수 |

**차트**: 라인 차트 (입금/출금 7일 추이), vue-chartjs

**DISTRIBUTOR 추가**: 하위 파트너 현황 카드 (전체/활성/금주신규 + 금일 하위 입금/수수료 수익)

**2FA 미등록 배너**: `twoFactorEnabled=false` → 상단 경고 배너 표시

---

### 11.2 입금 관리 (7개 화면)

#### PCR-2000 입금 내역 (`/deposits`)

| 항목 | 상세 |
|------|------|
| API | `GET /deposits` → XPage\<DepositResponse\> |
| 필터 | 기간(DateRange), 상태(MultiSelect), 통화, 네트워크, 입금유형, 키워드 |
| 테이블 컬럼 | 입금코드, 일시↕, 통화, 네트워크, 금액↕, 수수료, 순입금↕, 상태(뱃지), TX Hash(🔗) |
| 상세 | 입금코드 클릭 → SlideDrawer (상세 정보 + 상태 이력 타임라인) |
| 포인트 | partner_user_id → UserIdLink (클릭 시 UserContext 오버레이) |

#### PCR-2010 입금 세션 (`/deposit-sessions`)

| 항목 | 상세 |
|------|------|
| API | `GET /deposit-sessions` → XPage\<DepositSessionResponse\> |
| 테이블 | 세션코드, 할당주소, 통화, 네트워크, 기대금액, 상태, 생성일, 만료일(잔여시간), 연결입금 건수 |
| 액션 | [+ 입금 세션 생성] → `/deposit-sessions/new` |

#### PCR-2015 입금 세션 생성 (`/deposit-sessions/new`)

| 항목 | 상세 |
|------|------|
| API | `POST /deposit-sessions` → DepositSessionResponse |
| 필드 | 통화*, 네트워크*, 입금방식*(HD_WALLET/DECIMAL_MATCH/DIRECT), 금액*(Crypto 또는 KRW), 고객ID, 파트너참조, 만료시간* |
| KRW 환산 | PriceService 시세로 실시간 변환 표시 |
| 완료 모달 | 세션코드, 입금주소, 정확한 금액(소수점 키 포함), 만료시간, [주소 복사] [금액 복사] [안내 URL 복사] |

#### PCR-2020 결제 링크 (`/payment-links`)

| 항목 | 상세 |
|------|------|
| API | `GET /payment-links` → XPage |
| 테이블 | 코드, 금액+통화, 네트워크, 설명, 상태, 생성일, 만료일, URL([복사]) |
| 생성 | `POST /payment-links` → ⚠️ P2 스텁 (501) → "준비 중" 버튼 비활성화 |

#### PCR-2030 입금 주소 (`/deposit-addresses`)

| 항목 | 상세 |
|------|------|
| API | `GET /deposit-addresses` → XPage |
| 요약 카드 | 전체/활성/비활성 주소 수 |
| 테이블 | 주소(+복사), 네트워크, 통화, 소수점자릿수, 최대동시세션, 현재활성세션, 여유율(프로그레스바), 활성여부, 등록일 |
| 상세 | 행 클릭 → SlideDrawer (주소 상세 + 최근 입금 세션) |
| 안내 | "입금 주소는 관리자(Admin)에 의해 할당됩니다" |

#### PCR-2040 집금 현황 (`/collections`)

| 항목 | 상세 |
|------|------|
| API | `GET /collections` → XPage |
| 필터 | 기간, 상태(QUEUED/COLLECTING/COMPLETED/FAILED/DEFERRED), 통화 |
| 테이블 | 집금코드, 대상주소, 통화, 네트워크, 금액, 모드, 상태, TX Hash, 생성일 |
| 상세 | 클릭 → SlideDrawer (집금 상세 + 연결된 입금 건) |

#### PCR-2050 Axim Pay (`/axim-payments`)

| 항목 | 상세 |
|------|------|
| API | `GET /axim-payments` → XPage |
| 조건 | partner_axim_settings 존재 시에만 메뉴 노출 |
| 필터 | 기간, 상태(REQUESTED/PENDING/CONFIRMED/CANCELED/DENIED/FAILED/EXPIRED), 통화 |
| 테이블 | 결제코드, Axim ID, 고객ID, 금액, 네트워크, 수수료, 상태, 파트너참조, 연결입금, 생성일 |
| 요청 | `POST /axim-payments/request` → ⚠️ P2 스텁 |

---

### 11.3 출금 관리 (3개 화면)

#### PCR-3000 출금 내역 (`/withdrawals`)

| 항목 | 상세 |
|------|------|
| API | `GET /withdrawals` → XPage\<WithdrawalResponse\> |
| 필터 | 기간, 상태(MultiSelect), 통화, 출금유형 |
| 테이블 | 출금코드, 일시↕, 통화, 금액↕, 수수료, 수신주소, 상태(뱃지), TX Hash |
| 상세 | 클릭 → SlideDrawer (기본정보 + TX정보 + 상태이력 + 거부사유) |
| 액션 | REQUESTED/PENDING_APPROVAL → [취소] 버튼 |

#### PCR-3010 수동 출금 요청 (`/withdrawals/new`)

| 항목 | 상세 |
|------|------|
| API | `POST /withdrawals` → WithdrawalResponse |
| 필드 | 통화*, 네트워크*, 수신주소*(화이트리스트 셀렉트 or 직접입력), 금액* |
| 계산 | 가용잔액 실시간 표시, 수수료 계산, 실수령액 자동 계산 |
| 검증 | 잔액 부족 방지, 네트워크별 주소 형식 (ETH:0x42자, TRON:T34자), 최소출금액 |
| 확인 | 2단계: ConfirmDialog (통화, 주소, 금액, 수수료, 실수령) → API 호출 |
| 승인안내 | 일정 금액 이상 시 "관리자 승인이 필요합니다" 안내 |

#### PCR-3020 화이트리스트 (`/withdrawals/whitelist`)

| 항목 | 상세 |
|------|------|
| API | `GET/POST/PUT /withdrawals/whitelist` |
| 테이블 | 라벨, 네트워크, 주소(+복사), 검증, 활성(토글), 등록일 |
| 추가 | [+ 주소 추가] → 모달 (네트워크*, 주소*, 라벨) |
| 수정 | 라벨 수정, 활성/비활성 토글 |

---

### 11.4 잔액/원장 (2개 화면)

#### PCR-4000 잔액 현황 (`/balances`)

| 항목 | 상세 |
|------|------|
| API | `GET /balances` → BalanceResponse[] |
| UI | 통화별 카드: 가용잔액, 동결잔액, 미실현정산, 합계 |
| 안내 | 가용=입금정산-출금-동결, 동결=처리중 출금, 미실현=settlement_balances.unrealized |

#### PCR-4010 원장 조회 (`/ledger`)

| 항목 | 상세 |
|------|------|
| API | `GET /ledger` → XPage |
| 필터 | 기간, 통화, 유형(DEPOSIT_IN/WITHDRAWAL_OUT/FEE_DEDUCT/SETTLEMENT_IN/...) |
| 테이블 | 일시, 유형(뱃지), 방향(DR🔴/CR🔵), 금액, 잔액(running balance), 참조(클릭→거래), 설명, TX Hash |

---

### 11.5 정산 (4개 화면)

#### PCR-5000 일별 수수료 (`/settlement/fees`)

| API | `GET /settlement/daily-fees` → XPage\<SettlementDailyFee\> |
| 필터 | 기간, 통화 |
| 테이블 | 날짜, 통화, 역할, 수수료수익, 건수, 총거래금액 |

#### PCR-5010 실현 내역 (`/settlement/realizations`)

| API | `GET /settlement/realizations` → XPage\<SettlementRealization\> |
| 필터 | 기간, 통화, 상태 |
| 테이블 | 기간, 통화, 미실현→실현, TX Hash(🔗), 상태, 실현일 |

#### PCR-5020 정산 잔액 (`/settlement/balance`)

| API | `GET /settlement/balance` → SettlementBalance[] |
| UI | 통화별 카드: 미실현잔액, 실현잔액, 출금완료, 출금가능 + [쉐어 출금] 버튼 |

#### PCR-5030 쉐어 출금 (`/settlement/withdraw`)

| API | `POST /settlement/withdraw` |
| UI | PCR-3010과 동일 패턴, withdrawalType=SETTLEMENT_WITHDRAW 고정, 가용잔액=출금가능 |

---

### 11.6 가스비 (2개 화면)

#### PCR-6000 가스비 기록 (`/gas-costs`)

| API | `GET /gas-costs` → XPage |
| 필터 | 기간, 네트워크, TX유형 |
| 테이블 | 일시, TX유형, 네트워크, 가스비(원본), 가스비(USD), 청구상태, TX Hash |

#### PCR-6010 인보이스 (`/gas-invoices`)

| API (목록) | `GET /gas-invoices` → XPage |
| API (상세) | `GET /gas-invoices/{id}` |
| 테이블 | 인보이스번호, 청구월, 총가스비, 상태, 발행일, 납부기한(OVERDUE→빨간) |
| 상세 | 클릭 → 상세 페이지/드로어 (내역 테이블 + 납부 안내) |

---

### 11.7 하위 파트너 관리 — DISTRIBUTOR ONLY (4개 화면)

#### PCR-7000 목록 (`/sub-partners`)

| API | `GET /sub-partners` → XPage |
| 필터 | 상태, 유형, 키워드 |
| 테이블 | 파트너코드(→상세), 이름, 유형(뱃지), 수수료율, 상태, 생성일, 금월입금액, 하위수 |
| 토글 | [목록] / [트리 뷰] 전환 (재귀 트리 렌더링) |

#### PCR-7010 등록 (`/sub-partners/new`)

| API | `POST /sub-partners` |
| 섹션 | 기본정보(유형, 이름, 사업자명, 이메일, 전화), 로그인정보(이메일), 수수료(MERCHANT: deposit_fee_rate / DISTRIBUTOR: parent_fee_rate + max_fee_cap), 체인설정(네트워크+통화 체크박스) |
| 검증 | fee_rate 범위: min_fee_rate ≤ 값 ≤ max_fee_cap, 최소 1개 체인, 이메일 UNIQUE |

#### PCR-7020 상세 (`/sub-partners/:id`)

| API | `GET /sub-partners/{id}`, `PATCH /sub-partners/{id}` |
| 탭 | 기본정보, 수수료, 체인설정, 출금정책, 거래현황(30일) |
| 상태변경 | [정지] ACTIVE→SUSPENDED (사유), [재활성] SUSPENDED→ACTIVE |

#### PCR-7030 거래 현황 (`/sub-partners/overview`)

| API | `GET /sub-partners/overview` → SubPartnerOverviewResponse[] |
| UI | 기간 필터 + 테이블 (파트너, 유형, 입금건수, 입금액, 출금액, 수수료) + 합계 행 |

---

### 11.8 총판 리포트 — DISTRIBUTOR ONLY (3개 화면)

#### PCR-8000 매출 리포트 (`/reports/revenue`)

| API | `GET /reports/revenue` → RevenueReportResponse |
| 필터 | 기간유형(일별/주별/월별), 날짜범위, 통화 |
| UI | 라인 차트 (입금/출금/순수입 추이) + 데이터 테이블 + [CSV 내보내기] |

#### PCR-8010 수수료 수익 (`/reports/commission`)

| API | `GET /reports/fees` → FeeReportResponse |
| UI | 바 차트 (일별/주별/월별 수익) + 테이블 (기간, 하위파트너, 거래액, 수수료수익, 건수) + 합계 |

#### PCR-8020 성과 비교 (`/reports/performance`)

| API | `GET /reports/performance` → PerformanceReportResponse |
| UI | 수평 바 차트 (Top 10) + 테이블 (순위, 파트너, 유형, 입금건수, 입금액, 수수료) |
| 정렬 | [입금액/수수료/건수] 선택 |

---

### 11.9 연동 설정 (4개 화면)

#### PCR-9000 API 키 (`/settings/api`)

| API | `GET /integration/api-key`, `POST /integration/api-key/regenerate` |
| UI | API Key 마스킹 표시, Secret 표시 불가 안내 |
| 재발급 | ConfirmDialog → 재발급 결과 모달 (Key + Secret 1회 표시, [복사] 버튼) |

#### PCR-9010 Webhook (`/settings/webhook`)

| API | `PUT /integration/webhook`, `POST /integration/webhook/test`, `GET /integration/webhook/logs` |
| UI | URL 입력 + [저장], 이벤트 6종 선택, 이벤트별 샘플 JSON 편집 + [테스트 전송], 최근 전달 로그 (이벤트, 상태, 응답코드, 재시도 횟수) |

#### PCR-9020 Telegram (`/settings/telegram`)

| API | `GET/PUT /integration/telegram`, `POST /integration/telegram/test` |
| UI | Bot Token + Chat ID 입력, [저장] [테스트], 이벤트 구독 체크박스 |

#### PCR-9030 Axim Pay (`/settings/axim`)

| API | `GET /integration/axim` |
| 조건 | partner_axim_settings 존재 시에만 메뉴/화면 표시 |
| UI | Merchant ID, Secret Key(마스킹), Callback URL, 상태, 최근 결제 목록 (→PCR-2050 링크) |

---

### 11.10 내 계정 (3개 화면)

#### PCR-A000 프로필 (`/account/profile`)

| API | `GET/PUT /account/profile` |
| 읽기전용 | 파트너코드, 유형, 로그인이메일, 생성일, 상위파트너 |
| 수정가능 | 파트너명, 사업자명, 담당자이메일, 전화, 타임존, 언어 |

#### PCR-A010 비밀번호 변경 (`/account/password`)

| API | `POST /account/password` |
| 검증 | 현재비밀번호, 새비밀번호(8자+영문+숫자+특수문자), 확인 일치 |
| 실시간 | 비밀번호 조건 체크리스트 (✅/☐) |

#### PCR-A020 2FA 설정 (`/account/2fa`)

| API | `GET /account/2fa/setup` → QR코드+secret, `POST /account/2fa/enable`, `DELETE /account/2fa` |
| 미등록 | QR코드 표시 + OTP 6자리 입력 → 활성화 |
| 등록됨 | "2FA가 활성화되어 있습니다" + [비활성화] (현재 OTP 검증 후) |

---

## 12. 구현 우선순위

### Phase 1 — 핵심 플로우 (1주)

| # | 화면 | API 수 | 난이도 |
|---|------|--------|--------|
| 1 | LoginView + TwoFaView | 2 | ★★ |
| 2 | DashboardView | 2 | ★★★ (차트) |
| 3 | DepositListView | 2 | ★★ |
| 4 | WithdrawalListView + WithdrawalNewView | 3 | ★★★ (폼 검증) |
| 5 | BalanceOverviewView + LedgerView | 2 | ★★ |

> 공통 컴포넌트 (StatusBadge, DataTable, SlideDrawer, AddressDisplay, TxHashDisplay, AmountDisplay) 선행 구현 필수.

### Phase 2 — 설정/부가 (1주)

| # | 화면 | API 수 |
|---|------|--------|
| 6 | ApiKeyView + WebhookView + TelegramView | 7 |
| 7 | ProfileView + PasswordView + TwoFaSetupView | 5 |
| 8 | DepositSessionListView + DepositSessionNewView | 3 |
| 9 | DailyFeeView + RealizationView + SettlementBalanceView | 3 |
| 10 | GasCostView + GasInvoiceView | 3 |

### Phase 3 — 총판/고급 (3일)

| # | 화면 | API 수 |
|---|------|--------|
| 11 | SubPartnerListView + NewView + DetailView + OverviewView | 5 |
| 12 | RevenueReportView + CommissionReportView + PerformanceReportView | 3 |
| 13 | CollectionListView + DepositAddressListView | 4 |
| 14 | WhitelistView | 3 |
| 15 | PaymentLinkListView + AximPaymentListView (P2 스텁) | 2 |

---

## 13. 코딩 규칙 요약

| 규칙 | 상세 |
|------|------|
| `<script setup lang="ts">` | 모든 .vue 파일에 필수 |
| `any` 금지 | 모든 데이터에 타입 정의 |
| 서비스 레이어 | 컴포넌트에서 직접 axios 호출 금지 |
| 상태 관리 | Pinia composition API 스타일 |
| 폼 검증 | Vee-Validate + Zod, 서버 검증과 일치 |
| 에러 처리 | 인터셉터 통합 + 화면별 세부 처리 |
| 날짜 | `dayjs.utc().tz('Asia/Seoul')` |
| 금액 | `string` 타입, 표시 시 소수점 6자리 + 콤마 |
| 주소 | 앞10자리...뒤4자리 + 복사 + 익스플로러 링크 |
| 빈 상태 | EmptyState 컴포넌트 + 안내 문구 |
| 로딩 | LoadingSkeleton (페이지 로드), Spinner (버튼 액션) |
| 반응형 | 사이드바 접힘/펼침, 모바일 햄버거 메뉴 |
| 토큰 헤더 | `Access-Token` (Bearer 없이 직접 전달) |

---

## 14. P2 스텁 (서버 미구현) 처리

| API | 대응 |
|-----|------|
| `POST /payment-links` | [결제 링크 생성] 버튼 비활성화 + "준비 중" 툴팁 |
| `POST /axim-payments/request` | [Axim Pay 요청] 버튼 비활성화 + "준비 중" 툴팁 |
| `POST /settlement/withdraw` | [쉐어 출금] 버튼 비활성화 + "준비 중" 툴팁 |

> 서버가 501 반환 시 인터셉터에서 "이 기능은 현재 준비 중입니다" 토스트 표시.

---

## 15. 참조 문서 인덱스

| 문서 | 용도 |
|------|------|
| `CONSOLE_IA_V2.md` (Part 2) | 최신 메뉴 트리 + API 매핑 (v2.0) |
| `CRYPTOMENTS_PARTNER_CONSOLE_SCREEN_DESIGN.md` | 전체 화면 와이어프레임 (ASCII) |
| `PARTNER_UI_INTEGRATION_MAP.md` | v1 화면 → v2 API 1:1 매핑 + Gap 분석 |
| `PARTNER_UI_CLAUDE_MD.md` | partner-ui 프로젝트 CLAUDE.md (API 컨벤션) |
| `CRYPTOMENTS_PARTNER_API_SPEC.md` | Partner API 전체 스펙 |
| `CRYPTOMENTS_V2_DDL.sql` | DB 스키마 (37개 테이블) |
