# Cryptoments Admin Console — UI 구현 지침서

> **버전**: v1.0
> **작성일**: 2026-03-17
> **대상**: VS Code + AI 세션에서 신규 프로젝트 생성 시 사용
> **범위**: 프로젝트 구조, 아키텍처, 코딩 규칙, 전체 화면 스펙

---

## 1. 프로젝트 개요

Cryptoments Admin Console은 멀티체인 스테이블코인 결제 게이트웨이의 **SUPER_ADMIN 전용 관리 도구**입니다.
9개 메뉴 그룹, 32개 화면, 약 121개 API 엔드포인트로 구성됩니다.

### 1.1 기술 스택

| 카테고리 | 기술 | 버전 | 비고 |
|----------|------|------|------|
| 프레임워크 | Vue 3 (Composition API) | 3.5+ | `<script setup lang="ts">` 필수 |
| 언어 | TypeScript | 5.5+ | strict mode, **`any` 사용 금지** |
| 빌드 | Vite | 5.4+ | |
| 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 | |
| 아이콘 | Lucide Vue Next | 최신 | |
| 날짜 | Day.js | 1.11+ | |
| 유틸리티 | @vueuse/core | 14+ | |

### 1.2 프로젝트 생성

```bash
npm create vite@latest admin-ui -- --template vue-ts
cd admin-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 lucide-vue-next dayjs @vueuse/core

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

---

## 2. 디렉토리 구조

```
admin-ui/
├── src/
│   ├── api/                        ← API 서비스 레이어 (핵심!)
│   │   ├── client.ts               ← Axios 인스턴스 + 인터셉터
│   │   ├── types/                  ← API 요청/응답 타입 정의
│   │   │   ├── common.ts           ← XPage<T>, Pagination, ApiError 등
│   │   │   ├── partner.ts
│   │   │   ├── deposit.ts
│   │   │   ├── withdrawal.ts
│   │   │   ├── collection.ts
│   │   │   ├── infra.ts
│   │   │   ├── settlement.ts
│   │   │   ├── gas.ts
│   │   │   ├── system.ts
│   │   │   └── dashboard.ts
│   │   └── services/               ← 도메인별 API 함수
│   │       ├── partner.service.ts
│   │       ├── deposit.service.ts
│   │       ├── withdrawal.service.ts
│   │       ├── collection.service.ts
│   │       ├── infra.service.ts
│   │       ├── settlement.service.ts
│   │       ├── gas.service.ts
│   │       ├── system.service.ts
│   │       └── dashboard.service.ts
│   │
│   ├── components/
│   │   ├── ui/                     ← shadcn/vue 기본 컴포넌트 (자동 설치)
│   │   ├── common/                 ← 공통 비즈니스 컴포넌트
│   │   │   ├── StatusBadge.vue     ← 상태 뱃지 (전역 상태값 매핑)
│   │   │   ├── AddressDisplay.vue  ← 주소 축약 + 복사 + 익스플로러 링크
│   │   │   ├── TxHashDisplay.vue   ← TX Hash 축약 + 익스플로러 링크
│   │   │   ├── AmountDisplay.vue   ← 금액 포맷팅 (콤마, 소수점, 통화)
│   │   │   ├── DateDisplay.vue     ← 날짜 포맷 + 상대시간 hover
│   │   │   ├── SearchForm.vue      ← 공통 검색 폼 래퍼
│   │   │   ├── DataTable.vue       ← TanStack Table 래퍼 (정렬, 페이지네이션)
│   │   │   ├── ConfirmDialog.vue   ← 확인 모달 (위험 액션용)
│   │   │   ├── PageHeader.vue      ← 페이지 제목 + 브레드크럼 + 액션 버튼
│   │   │   ├── SummaryCards.vue    ← 목록 상단 요약 카드 (대시보드/입금 등)
│   │   │   ├── EmptyState.vue      ← 빈 데이터 표시
│   │   │   └── LoadingSkeleton.vue ← 스켈레톤 로더
│   │   └── layout/
│   │       ├── AdminLayout.vue     ← 전체 레이아웃 (사이드바 + 헤더 + 콘텐츠)
│   │       ├── Sidebar.vue         ← 사이드바 네비게이션
│   │       └── Header.vue          ← 상단 헤더
│   │
│   ├── composables/                ← 재사용 로직 (Vue Composables)
│   │   ├── useDataTable.ts         ← 테이블 페이지네이션/정렬/검색 통합
│   │   ├── useConfirm.ts           ← 확인 모달 제어
│   │   ├── useToast.ts             ← 토스트 알림
│   │   ├── useClipboard.ts         ← 클립보드 복사
│   │   └── useNetworks.ts          ← 네트워크/통화 목록 캐시
│   │
│   ├── stores/
│   │   ├── auth.ts                 ← 인증 상태 (token, admin 정보)
│   │   └── app.ts                  ← 앱 전역 상태 (사이드바, 테마 등)
│   │
│   ├── router/
│   │   └── index.ts                ← 라우트 정의 + 가드
│   │
│   ├── utils/
│   │   ├── format.ts               ← 금액/날짜/주소 포맷 함수
│   │   ├── explorer.ts             ← 체인별 익스플로러 URL 생성
│   │   ├── constants.ts            ← 상태값, 색상 매핑 등 상수
│   │   └── validation.ts           ← Zod 스키마 공통 정의
│   │
│   ├── views/                      ← 페이지 컴포넌트 (라우트 1:1 매핑)
│   │   ├── auth/
│   │   │   └── LoginView.vue
│   │   ├── dashboard/
│   │   │   └── DashboardView.vue
│   │   ├── partners/
│   │   │   ├── PartnerListView.vue
│   │   │   ├── PartnerCreateView.vue
│   │   │   └── PartnerDetailView.vue
│   │   ├── deposits/
│   │   │   ├── DepositListView.vue
│   │   │   ├── DepositDetailView.vue
│   │   │   └── DepositSessionListView.vue
│   │   ├── withdrawals/
│   │   │   ├── WithdrawalListView.vue
│   │   │   ├── WithdrawalDetailView.vue
│   │   │   └── WithdrawalApprovalView.vue
│   │   ├── collections/
│   │   │   ├── CollectionListView.vue
│   │   │   └── CollectionDetailView.vue
│   │   ├── cs/
│   │   │   ├── UnidentifiedDepositView.vue
│   │   │   └── TxSearchView.vue
│   │   ├── infra/
│   │   │   ├── NetworkListView.vue
│   │   │   ├── CurrencyListView.vue
│   │   │   ├── HdWalletListView.vue
│   │   │   ├── InfraWalletListView.vue
│   │   │   ├── WalletApprovalListView.vue
│   │   │   ├── NonceTrackerView.vue
│   │   │   ├── GlobalWalletView.vue
│   │   │   ├── ContractListView.vue
│   │   │   └── RelayerListView.vue
│   │   ├── settlements/
│   │   │   ├── DailyFeeListView.vue
│   │   │   ├── RealizationListView.vue
│   │   │   └── BalanceListView.vue
│   │   ├── gas/
│   │   │   ├── GasRecordListView.vue
│   │   │   ├── GasInvoiceListView.vue
│   │   │   ├── LedgerListView.vue
│   │   │   └── ExchangeRateView.vue
│   │   ├── system/
│   │   │   ├── SystemSettingsView.vue
│   │   │   ├── AuditLogView.vue
│   │   │   └── MaintenanceView.vue
│   │   └── admins/
│   │       └── AdminListView.vue
│   │
│   ├── App.vue
│   ├── main.ts
│   └── style.css                   ← Tailwind + CSS Variables
│
├── .env                            ← VITE_API_URL
├── vite.config.ts
├── tsconfig.json
├── tailwind.config.js
├── components.json                 ← shadcn/vue 설정
└── package.json
```

---

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

### 3.1 API 서비스 레이어 (가장 중요)

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

#### `src/api/client.ts` — Axios 인스턴스

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

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

// Request: Access-Token 자동 첨부
apiClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const auth = useAuthStore()
  if (auth.token) {
    config.headers['Access-Token'] = auth.token
  }
  return config
})

// Response: 에러 통합 처리
apiClient.interceptors.response.use(
  (response) => {
    // 응답 헤더에서 새 토큰 갱신
    const newToken = response.headers['access-token']
    if (newToken) {
      const auth = useAuthStore()
      auth.setToken(newToken)
    }
    return 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)
  }
)

export default apiClient
```

#### `src/api/types/common.ts` — 공통 타입

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

/** 서버 에러 응답 (Axim ApiError) */
export interface ApiError {
  code: string
  message: string
  status: number
}

/** 페이지네이션 요청 파라미터 */
export interface PaginationParams {
  page: number    // 1-based
  size: number    // 기본 20
  sort?: string   // 예: "createdAt,DESC"
}

/** 날짜 범위 검색 */
export interface DateRangeParams {
  fromDate?: string  // YYYY-MM-DD
  toDate?: string    // YYYY-MM-DD
}
```

#### `src/api/types/partner.ts` — 도메인 타입 예시

```typescript
import type { DateRangeParams } from './common'

/** 파트너 목록 검색 파라미터 */
export interface PartnerSearchParams extends DateRangeParams {
  partnerCode?: string
  name?: string
  partnerType?: 'DISTRIBUTOR' | 'MERCHANT'
  status?: string[]             // ACTIVE, SUSPENDED, PENDING, TERMINATED
  email?: string
  parentPartnerId?: number
}

/** 파트너 목록 응답 아이템 */
export interface PartnerListItem {
  /** 파트너 ID */
  id: number
  /** 파트너 코드 (P000001) */
  partnerCode: string
  /** 파트너명 */
  name: string
  /** 파트너 유형 */
  partnerType: 'DISTRIBUTOR' | 'MERCHANT'
  /** 상태 */
  status: 'ACTIVE' | 'SUSPENDED' | 'PENDING' | 'TERMINATED'
  /** 상위 파트너명 (JOIN) */
  parentPartnerName: string | null
  /** 입금 수수료율 */
  depositFeeRate: string
  /** API Key (마스킹) */
  apiKey: string
  /** 담당 이메일 */
  contactEmail: string
  /** 등록일 */
  createdAt: string
}

/** 파트너 생성 요청 */
export interface PartnerCreateRequest {
  /** 파트너명 */
  name: string
  /** 사업자명 */
  businessName?: string
  /** 파트너 유형 */
  partnerType: 'DISTRIBUTOR' | 'MERCHANT'
  /** 상위 파트너 ID */
  parentPartnerId?: number
  /** 담당 이메일 */
  contactEmail?: string
  /** 담당 전화 */
  contactPhone?: string
  /** 로그인 이메일 */
  loginEmail: string
  /** 초기 비밀번호 */
  password: string
  /** 상위 수수료율 (%) */
  parentFeeRate: string
  /** 최대 수수료 캡 (%) */
  maxFeeCap: string
  /** 입금 수수료율 (%) — MERCHANT만 */
  depositFeeRate?: string
  /** 출금 수수료 (고정) */
  withdrawalFeeFixed?: string
  /** Webhook URL */
  webhookUrl?: string
  /** 타임존 */
  timezone?: string
  /** 로케일 */
  locale?: string
}
```

#### `src/api/services/partner.service.ts` — API 서비스

```typescript
import apiClient from '@/api/client'
import type { XPage, PaginationParams } from '@/api/types/common'
import type {
  PartnerSearchParams,
  PartnerListItem,
  PartnerCreateRequest,
  PartnerDetail,
  PartnerUpdateRequest
} from '@/api/types/partner'

const BASE = '/admin/partners'

export const partnerService = {
  /** 파트너 목록 조회 */
  getList(params: PaginationParams & PartnerSearchParams) {
    return apiClient.get<XPage<PartnerListItem>>(BASE, { params })
  },

  /** 파트너 상세 조회 */
  getDetail(id: number) {
    return apiClient.get<PartnerDetail>(`${BASE}/${id}`)
  },

  /** 파트너 등록 */
  create(data: PartnerCreateRequest) {
    return apiClient.post<PartnerDetail>(BASE, data)
  },

  /** 파트너 기본 정보 수정 */
  update(id: number, data: PartnerUpdateRequest) {
    return apiClient.put<PartnerDetail>(`${BASE}/${id}`, data)
  },

  /** 파트너 상태 변경 */
  changeStatus(id: number, data: { status: string; reason?: string }) {
    return apiClient.patch(`${BASE}/${id}/status`, data)
  },

  /** API Key 재발급 */
  regenerateApiKey(id: number) {
    return apiClient.post<{ apiKey: string; apiSecret: string }>(`${BASE}/${id}/regenerate-api-key`)
  },

  // ... 수수료, 체인설정, 출금정책, 텔레그램 등 하위 API
}
```

#### 뷰에서 사용

```vue
<script setup lang="ts">
import { partnerService } from '@/api/services/partner.service'
import { useDataTable } from '@/composables/useDataTable'
import type { PartnerListItem, PartnerSearchParams } from '@/api/types/partner'

const {
  data, loading, pagination, search, fetchData, handlePageChange, handleSearch
} = useDataTable<PartnerListItem, PartnerSearchParams>({
  fetchFn: (params) => partnerService.getList(params),
  defaultSort: 'createdAt,DESC'
})
</script>
```

### 3.2 useDataTable Composable (목록 화면 통합)

```typescript
// src/composables/useDataTable.ts
import { ref, reactive, onMounted } from 'vue'
import type { XPage, PaginationParams } from '@/api/types/common'
import type { AxiosResponse } from 'axios'

interface UseDataTableOptions<T, S> {
  fetchFn: (params: PaginationParams & S) => Promise<AxiosResponse<XPage<T>>>
  defaultSort?: string
  defaultSize?: number
  immediate?: boolean  // 기본 true — onMounted에서 자동 호출
}

export function useDataTable<T, S extends Record<string, unknown> = Record<string, never>>(
  options: UseDataTableOptions<T, S>
) {
  const { fetchFn, defaultSort = 'createdAt,DESC', defaultSize = 20, immediate = true } = options

  const data = ref<T[]>([]) as Ref<T[]>
  const loading = ref(false)
  const pagination = reactive({
    page: 1,
    size: defaultSize,
    totalCount: 0,
    hasNext: false,
    sort: defaultSort
  })
  const search = reactive({} as S)

  async function fetchData() {
    loading.value = true
    try {
      const params = {
        page: pagination.page,
        size: pagination.size,
        sort: pagination.sort,
        ...search
      } as PaginationParams & S
      const res = await fetchFn(params)
      data.value = res.data.pageRows
      pagination.totalCount = res.data.totalCount
      pagination.hasNext = res.data.hasNext
    } catch {
      data.value = []
    } finally {
      loading.value = false
    }
  }

  function handlePageChange(page: number) {
    pagination.page = page
    fetchData()
  }

  function handleSearch() {
    pagination.page = 1
    fetchData()
  }

  function handleReset() {
    Object.keys(search).forEach(key => {
      (search as Record<string, unknown>)[key] = undefined
    })
    handleSearch()
  }

  if (immediate) {
    onMounted(fetchData)
  }

  return { data, loading, pagination, search, fetchData, handlePageChange, handleSearch, handleReset }
}
```

### 3.3 에러 처리 전략

```
에러 발생 위치          처리 방법
──────────────────────────────────────────────
Axios 인터셉터         401 → logout + redirect
                       그 외 → toast로 서버 에러 메시지 표시
서비스 레이어           catch 없음 (인터셉터에 위임)
컴포넌트               try/catch로 UI 상태 제어 (loading, 폼 리셋 등)
                       에러 메시지 표시는 인터셉터가 처리했으므로 중복 표시 금지
```

**절대 하지 말 것:**
- catch에서 mock 데이터를 반환하지 않는다
- `any` 타입을 쓰지 않는다 (error: AxiosError<ApiError> 사용)
- console.log로 에러를 삼키지 않는다

### 3.4 인증 (Auth Store + Router Guard)

```typescript
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  const token = ref<string | null>(localStorage.getItem('access-token'))
  const adminName = ref<string | null>(localStorage.getItem('admin-name'))

  const isAuthenticated = computed(() => !!token.value)

  function setAuth(newToken: string, name: string) {
    token.value = newToken
    adminName.value = name
    localStorage.setItem('access-token', newToken)
    localStorage.setItem('admin-name', name)
  }

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

  function logout() {
    token.value = null
    adminName.value = null
    localStorage.removeItem('access-token')
    localStorage.removeItem('admin-name')
  }

  return { token, adminName, isAuthenticated, setAuth, setToken, logout }
})
```

```typescript
// src/router/index.ts — 가드
router.beforeEach((to, from, next) => {
  const auth = useAuthStore()

  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    next({ name: 'login', query: { redirect: to.fullPath } })
  } else if (to.name === 'login' && auth.isAuthenticated) {
    next({ name: 'dashboard' })
  } else {
    next()
  }
})
```

---

## 4. 공통 UI 규칙

> 이 섹션은 `CRYPTOMENTS_SCREEN_DESIGN_V1.md` 섹션 1의 규칙을 구현 수준으로 구체화한 것입니다.

### 4.1 레이아웃

```
┌──────────────────────────────────────────────────────────┐
│  [Logo] Cryptoments Admin         [관리자명]  [로그아웃]  │
├────────┬─────────────────────────────────────────────────┤
│        │  Breadcrumb: 홈 > 메뉴1 > 메뉴2               │
│ 사이드  ├─────────────────────────────────────────────────┤
│  바    │                                                 │
│  메뉴  │  [검색 영역]                                    │
│        ├─────────────────────────────────────────────────┤
│        │  [테이블/콘텐츠 영역]                            │
│        ├─────────────────────────────────────────────────┤
│        │  [페이지네이션]                                  │
└────────┴─────────────────────────────────────────────────┘
```

- 사이드바: 240px 고정, 모바일(md 미만)에서 Sheet 오버레이
- 콘텐츠: `max-w-screen-xl mx-auto px-6 py-4`
- 반응형: `md:` (768px)에서 사이드바 토글, `lg:` (1024px)에서 넓은 여백

### 4.2 데이터 표시 규칙 (공통 컴포넌트로 구현)

| 요소 | 컴포넌트 | 규칙 |
|------|----------|------|
| 상태 뱃지 | `StatusBadge.vue` | ACTIVE=초록, PENDING=노랑, SUSPENDED=주황, TERMINATED=빨강, FAILED=빨강, CONFIRMED=초록, PROCESSING=파랑 |
| 금액 | `AmountDisplay.vue` | 3자리 콤마, 소수점 최대 8자리 (trailing zero 제거), 통화 심볼 |
| 주소 | `AddressDisplay.vue` | 앞 6 + `...` + 뒤 4, 클릭 복사, 익스플로러 링크 |
| TX Hash | `TxHashDisplay.vue` | 앞 10 + `...` + 뒤 6, 클릭 시 익스플로러 새 탭 |
| 날짜 | `DateDisplay.vue` | `YYYY-MM-DD HH:mm:ss` (KST), hover 시 상대시간 |
| 빈 상태 | `EmptyState.vue` | "조회된 데이터가 없습니다" + 아이콘 |
| 로딩 | `LoadingSkeleton.vue` | 스켈레톤 UI (Spinner가 아닌 스켈레톤) |

### 4.3 페이지네이션

- 기본 20건/페이지, 선택: 20 / 50 / 100
- XPagination은 1-based
- 컬럼 헤더 클릭 시 ASC ↔ DESC 토글
- 기본 정렬: `created_at DESC`

### 4.4 검색

- 상단 검색 영역: [검색] / [초기화] 버튼
- DateRangePicker: 기본값 최근 7일, 최대 90일
- 검색 실행 시 page를 1로 리셋

### 4.5 폼 규칙

- 필수 필드: 라벨 옆 빨간 `*`
- 유효성: Zod 스키마로 정의, 입력 즉시 인라인 에러 메시지
- 위험 액션(상태 변경, 삭제): ConfirmDialog 필수
- 온체인 TX 포함 액션: 프로그레스 바 + 버튼 비활성화 + 60초 이상 타임아웃

### 4.6 체인 익스플로러 URL

```typescript
// src/utils/explorer.ts
const EXPLORERS: Record<string, string> = {
  ETH: 'https://etherscan.io',
  BSC: 'https://bscscan.com',
  POLYGON: 'https://polygonscan.com',
  TRON: 'https://tronscan.org'
}

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

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

---

## 5. 사이드바 메뉴 구조

```typescript
// src/utils/constants.ts — 네비게이션 정의
import {
  LayoutDashboard, Users, ArrowDownToLine, ArrowUpFromLine,
  Layers, Headphones, Server, Calculator, Fuel, Settings, UserCog
} from 'lucide-vue-next'

export const MENU_ITEMS = [
  {
    label: '1. 대시보드',
    icon: LayoutDashboard,
    to: '/dashboard'
  },
  {
    label: '2. 파트너 관리',
    icon: Users,
    to: '/partners'
  },
  {
    label: '3. 거래 관리',
    icon: ArrowDownToLine,
    children: [
      { label: '3.1 입금 조회', to: '/deposits' },
      { label: '3.2 입금 세션', to: '/deposit-sessions' },
      { label: '3.3 출금 조회', to: '/withdrawals' },
      { label: '3.4 출금 승인', to: '/withdrawals/approval' },
      { label: '3.5 집금 현황', to: '/collections' }
    ]
  },
  {
    label: '4. CS 도구',
    icon: Headphones,
    children: [
      { label: '4.1 미식별 입금', to: '/cs/unidentified' },
      { label: '4.2 TX 검색', to: '/cs/tx-search' }
    ]
  },
  {
    label: '5. 인프라 관리',
    icon: Server,
    children: [
      { label: '5.1 네트워크', to: '/infra/networks' },
      { label: '5.2 통화', to: '/infra/currencies' },
      { label: '5.3 HD Wallet', to: '/infra/hd-wallets' },
      { label: '5.4 인프라 지갑', to: '/infra/wallets' },
      { label: '5.5 Approve 현황', to: '/infra/approvals' },
      { label: '5.6 논스 관리', to: '/infra/nonce' },
      { label: '5.7 전체 지갑', to: '/infra/global-wallets' },
      { label: '5.8 컨트랙트', to: '/infra/contracts' },
      { label: '5.9 Relayer', to: '/infra/relayers' }
    ]
  },
  {
    label: '6. 정산 관리',
    icon: Calculator,
    children: [
      { label: '6.1 일별 수수료', to: '/settlements/daily-fees' },
      { label: '6.2 실현 현황', to: '/settlements/realizations' },
      { label: '6.3 참여자 잔액', to: '/settlements/balances' }
    ]
  },
  {
    label: '7. 가스비/원장',
    icon: Fuel,
    children: [
      { label: '7.1 가스비 기록', to: '/gas/records' },
      { label: '7.2 가스비 인보이스', to: '/gas/invoices' },
      { label: '7.3 원장', to: '/gas/ledger' },
      { label: '7.4 환율', to: '/gas/exchange-rates' }
    ]
  },
  {
    label: '8. 시스템 설정',
    icon: Settings,
    children: [
      { label: '8.1 런타임 설정', to: '/system/settings' },
      { label: '8.2 감사 로그', to: '/system/audit-logs' },
      { label: '8.3 유지보수', to: '/system/maintenance' }
    ]
  },
  {
    label: '9. 관리자 계정',
    icon: UserCog,
    to: '/admins'
  }
]
```

---

## 6. 라우트 정의

```typescript
// src/router/index.ts
const routes = [
  { path: '/login', name: 'login', component: () => import('@/views/auth/LoginView.vue') },
  {
    path: '/',
    component: () => import('@/components/layout/AdminLayout.vue'),
    meta: { requiresAuth: true },
    children: [
      { path: '', redirect: '/dashboard' },

      // 1. 대시보드
      { path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },

      // 2. 파트너 관리
      { path: 'partners', name: 'partner-list', component: () => import('@/views/partners/PartnerListView.vue') },
      { path: 'partners/new', name: 'partner-create', component: () => import('@/views/partners/PartnerCreateView.vue') },
      { path: 'partners/:id', name: 'partner-detail', component: () => import('@/views/partners/PartnerDetailView.vue') },

      // 3. 거래 관리
      { path: 'deposits', name: 'deposit-list', component: () => import('@/views/deposits/DepositListView.vue') },
      { path: 'deposits/:id', name: 'deposit-detail', component: () => import('@/views/deposits/DepositDetailView.vue') },
      { path: 'deposit-sessions', name: 'deposit-session-list', component: () => import('@/views/deposits/DepositSessionListView.vue') },
      { path: 'withdrawals', name: 'withdrawal-list', component: () => import('@/views/withdrawals/WithdrawalListView.vue') },
      { path: 'withdrawals/approval', name: 'withdrawal-approval', component: () => import('@/views/withdrawals/WithdrawalApprovalView.vue') },
      { path: 'withdrawals/:id', name: 'withdrawal-detail', component: () => import('@/views/withdrawals/WithdrawalDetailView.vue') },
      { path: 'collections', name: 'collection-list', component: () => import('@/views/collections/CollectionListView.vue') },
      { path: 'collections/:id', name: 'collection-detail', component: () => import('@/views/collections/CollectionDetailView.vue') },

      // 4. CS 도구
      { path: 'cs/unidentified', name: 'cs-unidentified', component: () => import('@/views/cs/UnidentifiedDepositView.vue') },
      { path: 'cs/tx-search', name: 'cs-tx-search', component: () => import('@/views/cs/TxSearchView.vue') },

      // 5. 인프라 관리
      { path: 'infra/networks', name: 'network-list', component: () => import('@/views/infra/NetworkListView.vue') },
      { path: 'infra/currencies', name: 'currency-list', component: () => import('@/views/infra/CurrencyListView.vue') },
      { path: 'infra/hd-wallets', name: 'hd-wallet-list', component: () => import('@/views/infra/HdWalletListView.vue') },
      { path: 'infra/wallets', name: 'infra-wallet-list', component: () => import('@/views/infra/InfraWalletListView.vue') },
      { path: 'infra/approvals', name: 'wallet-approval-list', component: () => import('@/views/infra/WalletApprovalListView.vue') },
      { path: 'infra/nonce', name: 'nonce-tracker', component: () => import('@/views/infra/NonceTrackerView.vue') },
      { path: 'infra/global-wallets', name: 'global-wallet', component: () => import('@/views/infra/GlobalWalletView.vue') },
      { path: 'infra/contracts', name: 'contract-list', component: () => import('@/views/infra/ContractListView.vue') },
      { path: 'infra/relayers', name: 'relayer-list', component: () => import('@/views/infra/RelayerListView.vue') },

      // 6. 정산 관리
      { path: 'settlements/daily-fees', name: 'daily-fee-list', component: () => import('@/views/settlements/DailyFeeListView.vue') },
      { path: 'settlements/realizations', name: 'realization-list', component: () => import('@/views/settlements/RealizationListView.vue') },
      { path: 'settlements/balances', name: 'balance-list', component: () => import('@/views/settlements/BalanceListView.vue') },

      // 7. 가스비/원장
      { path: 'gas/records', name: 'gas-record-list', component: () => import('@/views/gas/GasRecordListView.vue') },
      { path: 'gas/invoices', name: 'gas-invoice-list', component: () => import('@/views/gas/GasInvoiceListView.vue') },
      { path: 'gas/ledger', name: 'ledger-list', component: () => import('@/views/gas/LedgerListView.vue') },
      { path: 'gas/exchange-rates', name: 'exchange-rate', component: () => import('@/views/gas/ExchangeRateView.vue') },

      // 8. 시스템 설정
      { path: 'system/settings', name: 'system-settings', component: () => import('@/views/system/SystemSettingsView.vue') },
      { path: 'system/audit-logs', name: 'audit-log', component: () => import('@/views/system/AuditLogView.vue') },
      { path: 'system/maintenance', name: 'maintenance', component: () => import('@/views/system/MaintenanceView.vue') },

      // 9. 관리자 계정
      { path: 'admins', name: 'admin-list', component: () => import('@/views/admins/AdminListView.vue') },
    ]
  }
]
```

---

## 7. 화면별 구현 스펙

> 각 화면의 상세 필드/컬럼/검색조건은 `CRYPTOMENTS_SCREEN_DESIGN_V1.md`를 참조하세요.
> 아래는 API 엔드포인트와 화면 유형, 특수 주의사항 중심으로 정리합니다.
> `ADMIN_CONSOLE_UI_HANDOFF.md`의 변경사항은 ★ 표시로 명시합니다.

### 7.1 로그인 (LoginView)

| API | Method | Endpoint |
|-----|--------|----------|
| 로그인 | POST | `/admin/auth/login` |

- 이메일 + 비밀번호 폼
- 성공 시 `Access-Token` 헤더에서 토큰 추출 → authStore.setAuth()
- 실패 시 인라인 에러 메시지
- 2FA 지원 (선택적 OTP 입력 필드)

### 7.2 대시보드 (DashboardView)

| API | Method | Endpoint | 용도 |
|-----|--------|----------|------|
| 요약 | GET | `/admin/dashboard/summary` | 요약 카드 4개 |
| 알림 | GET | `/admin/dashboard/alerts` | 이상 알림 목록 |
| 미식별 | GET | `/admin/dashboard/unidentified` | 미식별 입금 |
| 정산 | GET | `/admin/dashboard/settlement` | 정산 현황 |

- 상단 4개 SummaryCards (오늘 입금/출금, 승인 대기, 미식별, 알림)
- 알림 목록: `alertType` 뱃지 + severity 색상 (WARNING=노랑, CRITICAL=빨강)
- 자동 새로고침 (30초 간격) — `@vueuse/core`의 `useIntervalFn`

### 7.3 파트너 관리

#### 7.3.1 파트너 목록 (PartnerListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/partners` |
| 상태변경 | PATCH | `/admin/partners/{id}/status` |

- 화면설계서: SCR-2100
- 검색 7개 필드 (partnerCode, name, partnerType, status, email, parentPartnerId, 등록일)
- 컬럼 10개 (ID, 코드, 이름, 유형, 상위, 수수료율, 상태, API Key, 이메일, 등록일)
- 행별 [상태변경] 드롭다운 → ConfirmDialog

#### 7.3.2 파트너 등록 (PartnerCreateView)

| API | Method | Endpoint |
|-----|--------|----------|
| 등록 | POST | `/admin/partners` |
| 상위검색 | GET | `/admin/partners?name={q}` |

- 화면설계서: SCR-2110
- 16개 폼 필드 (4개 섹션: 기본/연락처/계정/수수료)
- Zod 스키마로 유효성 검증 (수수료 cross-validation 포함)
- 등록 성공 시 API Secret 1회 노출 모달

#### 7.3.3 파트너 상세 (PartnerDetailView)

| API | Method | Endpoint |
|-----|--------|----------|
| 상세 | GET | `/admin/partners/{id}` |
| 수정 | PUT | `/admin/partners/{id}` |
| 상태변경 | PATCH | `/admin/partners/{id}/status` |
| API Key 재발급 | POST | `/admin/partners/{id}/regenerate-api-key` |
| 수수료 수정 | PUT | `/admin/partners/{id}/fees` |
| 체인설정 | GET/PUT | `/admin/partners/{id}/chain-configs` |
| 출금정책 | GET/PUT | `/admin/partners/{id}/withdrawal-policy` |
| 화이트리스트 | GET/POST/DELETE | `/admin/partners/{id}/whitelist` |
| 텔레그램설정 | PATCH | `/admin/partners/{id}/telegram/config` |
| 텔레그램구독 | PUT | `/admin/partners/{id}/telegram/subscriptions` |
| Axim설정 | PUT | `/admin/partners/{id}/axim-settings` |
| 하위파트너 | GET | `/admin/partners/{id}/children` |
| 활동로그 | GET | `/admin/audit-logs?targetType=PARTNER&targetId={id}` |

- 화면설계서: SCR-2200
- **6개 탭**: 기본정보, 체인/통화, 출금정책, 연동, 하위파트너, 활동로그
- Tab2 체인/통화: 매트릭스 그리드 (네트워크×통화)
- Tab3 출금정책: 자동승인 임계값 + 화이트리스트 관리
- Tab5 하위파트너: DISTRIBUTOR일 때만 표시

### 7.4 거래 관리 — 입금

#### 7.4.1 입금 목록 (DepositListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/deposits` |

- 화면설계서: SCR-3100
- 검색 11개 필드
- 상단 SummaryCards 4개 (오늘 입금, 대기, 확인, 미식별)
- 컬럼 15개 (TX Hash, 금액 등 포함)

#### 7.4.2 입금 상세 (DepositDetailView)

| API | Method | Endpoint |
|-----|--------|----------|
| 상세 | GET | `/admin/deposits/{id}` |
| ★ TX 상태 | GET | `/admin/tx/status/{txHash}?networkId=` |

- 화면설계서: SCR-3110
- 4개 섹션: 기본정보, 금액, 블록체인, 집금이력
- ★ "온체인 확인" 버튼 → TX 상태 조회 (5~60초 응답, 로딩 필수)

#### 7.4.3 입금 세션 목록 (DepositSessionListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/deposit-sessions` |

- 화면설계서: SCR-3150

### 7.5 거래 관리 — 출금

#### 7.5.1 출금 목록 (WithdrawalListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/withdrawals` |

- 화면설계서: SCR-3200
- 10단계 출금 상태 뱃지: REQUESTED, PENDING_APPROVAL, APPROVED, BALANCE_PENDING, PROCESSING, BROADCASTING, CONFIRMED, SETTLED, REJECTED, FAILED, ★CANCELLED (L 2개)

#### 7.5.2 출금 상세 (WithdrawalDetailView)

| API | Method | Endpoint |
|-----|--------|----------|
| 상세 | GET | `/admin/withdrawals/{id}` |
| 승인/거부 | POST | `/admin/withdrawals/{id}/approve` or `reject` |
| ★ TX 상태 | GET | `/admin/tx/status/{txHash}?networkId=` |

- 화면설계서: SCR-3210
- 승인 이력 테이블 (approval_logs)
- ★ "온체인 확인" 버튼

#### 7.5.3 출금 승인 (WithdrawalApprovalView)

| API | Method | Endpoint |
|-----|--------|----------|
| 대기 목록 | GET | `/admin/withdrawals?status=PENDING_APPROVAL` |
| 단건 승인 | POST | `/admin/withdrawals/{id}/approve` |
| 단건 거부 | POST | `/admin/withdrawals/{id}/reject` |
| ★ 일괄 승인 | POST | `/admin/withdrawals/batch-approve` |
| ★ 일괄 거부 | POST | `/admin/withdrawals/batch-reject` |

- ★ 체크박스 선택 → [일괄 승인] / [일괄 거부] 버튼
- ★ 일괄 승인 확인 모달: 선택 건수 + 총 금액 표시 (위험 액션)
- ★ 일괄 거부: reason 입력 필수

### 7.6 집금 관리

#### 7.6.1 집금 목록 (CollectionListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/collections` |
| 재시도 | POST | `/admin/collections/{id}/retry` |
| 취소 | POST | `/admin/collections/{id}/cancel` |

- 상태: QUEUED → COLLECTING → BROADCASTING → ★CONFIRMED / FAILED (COMPLETED→CONFIRMED 변경)

### 7.7 CS 도구

#### 7.7.1 미식별 입금 (UnidentifiedDepositView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/cs/unidentified-deposits` |
| 매칭 | POST | `/admin/cs/unidentified-deposits/{id}/match` |
| 환불 | POST | `/admin/cs/unidentified-deposits/{id}/refund` |

- 화면설계서: SCR-4100
- 매칭: 파트너 + 유저 ID 선택 → ConfirmDialog
- 환불: TO 주소 입력 → ConfirmDialog (이중 확인)

#### 7.7.2 TX 검색 (TxSearchView)

| API | Method | Endpoint |
|-----|--------|----------|
| 검색 | GET | `/admin/cs/tx-search?txHash={hash}` |
| ★ 온체인 | GET | `/admin/tx/status/{txHash}?networkId=` |

- 화면설계서: SCR-4200
- TX Hash 입력 → DB 검색 결과 (입금/출금/집금) + ★ 온체인 상태 조회 버튼

### 7.8 인프라 관리

#### 5.1 네트워크 (NetworkListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/networks` |
| 수정 | PUT | `/admin/networks/{id}` |
| 상태변경 | PATCH | `/admin/networks/{id}/status` |

- CRUD 인라인 편집 또는 모달

#### 5.2 통화 (CurrencyListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/currencies` |
| 등록/수정 | POST/PUT | `/admin/currencies` |
| 네트워크 매핑 | GET/PUT | `/admin/currencies/{id}/networks` |

#### 5.3 HD Wallet (HdWalletListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/hd-wallets` |
| 생성 | POST | `/admin/hd-wallets` |

- 생성 시 mnemonic 1회 노출 모달 (복사 + 확인 체크박스)

#### 5.4 인프라 지갑 (InfraWalletListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/infra-wallets` |
| ★ ADMIN 생성 | POST | `/admin/infra-wallets/admin` |
| FEE 생성 | POST | `/admin/infra-wallets/fee` |
| SETTLEMENT 생성 | POST | `/admin/infra-wallets/settlement` |

- ★ ADMIN 지갑: 네트워크당 1개, 409 시 "이미 존재" 에러 표시
- 유형별 생성 버튼 분리 또는 드롭다운

#### 5.5 Approve 현황 (WalletApprovalListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/wallet-approvals` |
| 재시도 | POST | `/admin/wallet-approvals/{id}/retry` |

#### 5.6 논스 관리 (NonceTrackerView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/nonce-trackers` |
| 리셋 | POST | `/admin/nonce-trackers/{id}/reset` |
| 동기화 | POST | `/admin/nonce-trackers/{id}/sync` |

#### 5.7 전체 지갑 (GlobalWalletView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/wallets` |
| 잔액 | GET | `/admin/wallets/{id}/balance` |

- 지갑 타입 필터: HOT, MASTER, FEE, ADMIN, SETTLEMENT, POOL

#### ★ 5.8 컨트랙트 (ContractListView) — 신규

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/contracts` |
| 상태 조회 | GET | `/admin/contracts/{networkId}/status` |
| 등록 | POST | `/admin/contracts/register` |
| Relayer 추가 | POST | `/admin/contracts/add-relayer` |
| Relayer 제거 | POST | `/admin/contracts/remove-relayer` |
| 긴급 정지 | POST | `/admin/contracts/pause` |
| 정지 해제 | POST | `/admin/contracts/{networkId}/unpause` |

- **모든 액션이 온체인 TX 포함**: 프로그레스 바 + 버튼 비활성화 + 60초 타임아웃
- 긴급 정지: reason 필수, 이중 확인 모달
- 등록 폼: 네트워크, 컨트랙트 주소, 배포 TX Hash, ADMIN 지갑, ABI 버전
- 상세: 온체인 실시간 정보 (Owner, Paused, Relayer 목록)

#### ★ 5.9 Relayer (RelayerListView) — 대폭 변경

| API | Method | Endpoint |
|-----|--------|----------|
| DB 목록 | GET | `/admin/relayer-wallets` |
| 상세 | GET | `/admin/relayer-wallets/{id}` |
| 등록 | POST | `/admin/relayer-wallets` |
| 상태변경 | PATCH | `/admin/relayer-wallets/{id}/status` |
| ★ 해제 | POST | `/admin/relayer-wallets/{id}/deactivate` |
| ★ 실시간 | GET | `/admin/relayer-wallets/live` |
| ★ 헬스 | GET | `/admin/relayer-wallets/health` |

- ★ 상태 분리: `status` (ACTIVE/PAUSED) + `registrationStatus` (PENDING/REGISTERING/REGISTERED/DEREGISTERING/REVOKED)
- ★ INACTIVE 제거
- ★ 탭 또는 토글: "DB 목록" / "실시간 상태"
- ★ 헬스 집계: 상단 카드 (네트워크별/상태별 집계)
- ★ 해제: 확인 모달 + pending TX 경고 메시지
- ★ 등록 시 `relayerRole` 필드 추가 (COLLECTION/WITHDRAWAL)

### 7.9 정산 관리

#### 6.1 일별 수수료 (DailyFeeListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/settlements/daily-fees` |

#### 6.2 실현 현황 (RealizationListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/settlements/realizations` |
| 실현 처리 | POST | `/admin/settlements/realize` |

#### 6.3 참여자 잔액 (BalanceListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/settlements/balances` |

### 7.10 가스비/원장

#### 7.1 가스비 기록 (GasRecordListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/gas-costs` |

#### 7.2 가스비 인보이스 (GasInvoiceListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/gas-invoices` |
| 생성 | POST | `/admin/gas-invoices` |
| 입금확인 | PATCH | `/admin/gas-invoices/{id}/confirm-payment` |

- 월별 발행, 상태: DRAFT → SENT → PAID / OVERDUE

#### 7.3 원장 (LedgerListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/ledgers` |
| 잔액 | GET | `/admin/ledgers/balance` |

#### 7.4 환율 (ExchangeRateView)

| API | Method | Endpoint |
|-----|--------|----------|
| 현재가 | GET | `/admin/exchange-rates/current` |
| 이력 | GET | `/admin/exchange-rates/history` |

### 7.11 시스템 설정

#### 8.1 런타임 설정 (SystemSettingsView)

| API | Method | Endpoint |
|-----|--------|----------|
| 전체 | GET | `/admin/system-settings` |
| 수정 | PUT | `/admin/system-settings/{key}` |

- Key-Value 테이블 + 인라인 편집

#### ★ 8.2 감사 로그 (AuditLogView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/audit-logs` |

- ★ action 필드: AuditAction enum 기반 47개 → Select 드롭다운 필터
- 검색: adminId, action (Select), targetType (Select), targetId, 기간

#### 8.3 유지보수 (MaintenanceView)

| API | Method | Endpoint |
|-----|--------|----------|
| 상태 조회 | GET | `/admin/maintenance` |
| 활성화 | POST | `/admin/maintenance/enable` |
| 비활성화 | POST | `/admin/maintenance/disable` |

- 토글 스위치 + 활성화 시 사유 입력 + 확인 모달

### 7.12 관리자 계정

#### 9.1 관리자 목록 (AdminListView)

| API | Method | Endpoint |
|-----|--------|----------|
| 목록 | GET | `/admin/admins` |
| 등록 | POST | `/admin/admins` |
| 수정 | PUT | `/admin/admins/{id}` |
| 상태변경 | PATCH | `/admin/admins/{id}/status` |
| 비밀번호 초기화 | POST | `/admin/admins/{id}/reset-password` |
| 2FA 초기화 | POST | `/admin/admins/{id}/reset-2fa` |
| 내 비밀번호 변경 | PUT | `/admin/admins/me/password` |
| 내 정보 | GET | `/admin/admins/me` |

- 등록/수정 모달 (Dialog)
- 비밀번호 초기화: 임시 비밀번호 1회 노출

---

## 8. 코딩 규칙

### 8.1 TypeScript

```typescript
// ✅ 정확한 타입 정의
const partner = ref<PartnerDetail | null>(null)
const loading = ref(false)

// ❌ any 사용 금지
const data: any = {}          // NEVER
(error: any) => {}            // NEVER
params: any = {}              // NEVER

// ✅ 에러 타입
catch (error) {
  // 에러 메시지 표시는 인터셉터가 처리
  // 여기서는 UI 상태만 제어
}
```

### 8.2 Vue 컴포넌트

```vue
<!-- ✅ 항상 <script setup lang="ts"> -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { partnerService } from '@/api/services/partner.service'
import type { PartnerDetail } from '@/api/types/partner'

const props = defineProps<{
  id: number
}>()

const partner = ref<PartnerDetail | null>(null)
const loading = ref(false)

async function fetchPartner() {
  loading.value = true
  try {
    const res = await partnerService.getDetail(props.id)
    partner.value = res.data
  } finally {
    loading.value = false
  }
}

onMounted(fetchPartner)
</script>

<template>
  <LoadingSkeleton v-if="loading" />
  <div v-else-if="partner">
    <!-- 콘텐츠 -->
  </div>
  <EmptyState v-else />
</template>
```

### 8.3 폼 (Vee-Validate + Zod)

```vue
<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(z.object({
  name: z.string().min(2, '2자 이상 입력해주세요').max(200),
  loginEmail: z.string().email('올바른 이메일 형식이 아닙니다'),
  partnerType: z.enum(['DISTRIBUTOR', 'MERCHANT']),
  parentFeeRate: z.string().refine(
    (v) => { const n = Number(v); return n >= 0 && n <= 100 },
    '0~100 사이의 값을 입력해주세요'
  ),
}))

const { handleSubmit, errors, defineField } = useForm({
  validationSchema: schema,
})

const [name, nameAttrs] = defineField('name')
const [loginEmail, emailAttrs] = defineField('loginEmail')

const onSubmit = handleSubmit(async (values) => {
  // API 호출
})
</script>
```

### 8.4 네이밍 규칙

| 대상 | 규칙 | 예시 |
|------|------|------|
| 뷰 파일 | PascalCase + View | `PartnerListView.vue` |
| 컴포넌트 파일 | PascalCase | `StatusBadge.vue` |
| Composable 파일 | camelCase (use 접두사) | `useDataTable.ts` |
| 서비스 파일 | camelCase (service 접미사) | `partner.service.ts` |
| 타입 파일 | camelCase | `partner.ts` |
| CSS 클래스 | Tailwind 유틸리티만 | `class="flex items-center gap-2"` |
| 라우트 name | kebab-case | `partner-detail` |

---

## 9. 구현 우선순위 (Phase)

### Phase 1: 기반 + 핵심 화면 (약 60%)

```
1. 프로젝트 셋업 (Vite + shadcn/vue + Tailwind + 라우터 + Pinia)
2. API 서비스 레이어 (client.ts + types/common.ts + 1개 서비스)
3. 공통 컴포넌트 (StatusBadge, DataTable, PageHeader, ConfirmDialog 등)
4. 레이아웃 (AdminLayout + Sidebar + Header)
5. 로그인
6. 대시보드
7. 파트너 관리 (목록 + 등록 + 상세 6탭)
8. 입금 관리 (목록 + 상세)
9. 출금 관리 (목록 + 상세 + 승인)
```

### Phase 2: 인프라 + CS (약 25%)

```
10. CS 도구 (미식별 입금 + TX 검색)
11. 집금 관리
12. 네트워크/통화 관리
13. 지갑 관리 (HD, 인프라, 전체, Approve)
14. 컨트랙트 관리 (★ 신규)
15. Relayer 관리 (★ 대폭 변경)
16. 논스 관리
```

### Phase 3: 정산 + 시스템 (약 15%)

```
17. 정산 관리 (일별수수료, 실현, 잔액)
18. 가스비/원장 (기록, 인보이스, 원장, 환율)
19. 시스템 설정 + 감사 로그 + 유지보수
20. 관리자 계정
```

---

## 10. 참조 문서

이 지침서와 함께 아래 문서를 반드시 참조하세요:

| 문서 | 용도 |
|------|------|
| `CRYPTOMENTS_SCREEN_DESIGN_V1.md` | 화면별 상세 필드, 컬럼, 검색조건, 폼 명세 (**v3.1**, HANDOFF 통합 완료) |
| `CRYPTOMENTS_V2_API_DESIGN.md` | 화면별 API 매핑, 요청/응답 상세 (**v1.2**, HANDOFF 통합 완료, 121 APIs) |
| `CRYPTOMENTS_SUPER_ADMIN_IA.md` | 메뉴 트리, 역할, 권한 (IA v6.2) |
| `admin-api/build/docs/openapi.json` | 실제 API 스펙 (OpenAPI 3.0.3, 121 endpoints) |

> **참고**: `ADMIN_CONSOLE_UI_HANDOFF.md`의 변경사항은 위 두 문서(v3.1, v1.2)에 모두 통합되었으므로, 더 이상 별도로 참조할 필요 없습니다.

---

## 11. 기존 코드에서 배운 교훈 (Anti-patterns)

이전 구현에서 발견된 문제점과 해결 방법:

| 문제 (Anti-pattern) | 해결 (이 지침서 방식) |
|---------------------|----------------------|
| 컴포넌트에서 직접 `axios.get()` 호출 | `api/services/` 레이어 경유 |
| catch에서 mock 데이터 반환 | 인터셉터에서 toast 표시, mock 금지 |
| `params: any = {}` | 도메인별 SearchParams 인터페이스 |
| `error: any` | AxiosError<ApiError> 또는 생략 |
| 네트워크 목록 하드코딩 (TODO 3건) | `useNetworks` composable로 API에서 fetch + 캐시 |
| 401 처리 주석 상태 | 인터셉터에서 logout + redirect 구현 |
| Spinner로 로딩 표시 | LoadingSkeleton 컴포넌트 |
| API 서비스 레이어 없음 | `api/services/*.service.ts` 패턴 |
| 폼 검증 일관성 없음 | 모든 폼에 Zod 스키마 적용 |
