# Guide #56: Partner UI 버그 수정 + 기능 보완

> **대상**: IntelliJ (Spring Boot partner-api) + VS Code (partner-ui Vue)
> **선행**: Guide #55 (완료)
> **다음**: Guide #57 — 정산 및 총판 기능 검증

---

## 목차

| # | 항목 | 대상 | 난이도 |
|---|------|------|--------|
| 1 | 대시보드 통화별 잔액 — 네트워크 구분 | Backend + Vue | ★★☆ |
| 2 | 2FA QR 코드 이미지 렌더링 | Backend 또는 Vue | ★★☆ |
| 3 | 프로필 담당자 필드 제거 | Vue | ★☆☆ |
| 4 | 내 계정 → 내 정보 단일 페이지 | Vue | ★★★ |
| 5 | Axim Pay 설정 — siteId/결제내역 버튼 제거 | Vue | ★☆☆ |
| 6 | Telegram 설정 — 동작 테스트 | 테스트 | ★☆☆ |
| 7 | 출금 승인 관리 (승인/거부) | Backend + Vue | ★★★ |
| 8 | 출금 취소 버튼 | Vue | ★☆☆ |
| 9 | 입금 주소 메뉴 제거 | Vue | ★☆☆ |
| 10 | 입금 세션 — 입금 금액 컬럼 추가 | Vue | ★☆☆ |
| 11 | Axim Pay 결제 요청 — NOT_IMPLEMENTED 해제 | Backend + Vue | ★★☆ |

---

## 1. 대시보드 통화별 잔액 — 네트워크 구분

### 문제
통화별 잔액에 네트워크 정보가 없어서 USDT가 3줄(TRON/POLYGON/BSC) 중복 표시됨.
`BalanceResponse`에 `networkId`/`chainSymbol` 필드가 없음.

### 1-A. Backend — BalanceResponse에 네트워크 정보 추가

**파일**: `partner-api/.../dto/response/BalanceResponse.java`

```java
// 기존 필드에 추가
/** 네트워크 ID */
private Long networkId;

/** 체인 심볼 (BSC, TRON, POLYGON 등) */
private String chainSymbol;
```

### 1-B. Backend — PartnerBalanceMapper 수정

**파일**: `partner-api/.../mapper/PartnerBalanceMapper.java`

`findPartnerBalances` 쿼리에서 `blockchain_networks` JOIN 추가:

```sql
SELECT
    wb.currency_id AS currencyId,
    c.symbol AS currencyCode,
    wb.network_id AS networkId,
    bn.chain_symbol AS chainSymbol,
    SUM(wb.available_balance) AS totalBalance
FROM wallet_balances wb
JOIN wallet_assignments wa ON wb.wallet_address_id = wa.wallet_address_id
JOIN currencies c ON wb.currency_id = c.id
JOIN blockchain_networks bn ON wb.network_id = bn.id
WHERE wa.partner_id = #{partnerId}
  AND wa.wallet_type IN ('HOT', 'MASTER')
  AND c.is_active = 1
  AND c.currency_type = 'TOKEN'
GROUP BY wb.currency_id, wb.network_id, c.symbol, bn.chain_symbol
```

> **주의**: 기존 쿼리에 `c.currency_type = 'TOKEN'` 필터도 함께 추가 (Guide #55 Native 배제 일관성).

### 1-C. Vue — DashboardView 통화별 잔액 표시 수정

**파일**: `partner-ui/src/views/partner/DashboardView.vue`

```vue
<!-- 변경 전 -->
<span>{{ balance.currencyCode }}</span>

<!-- 변경 후 -->
<span>{{ balance.currencyCode }} ({{ balance.chainSymbol }})</span>
```

**타입 수정** (`api/types/balance.ts`):
```typescript
export interface BalanceResponse {
  currencyId: number
  currencyCode: string
  networkId: number       // 추가
  chainSymbol: string     // 추가
  totalBalance: number
}
```

---

## 2. 2FA QR 코드 이미지 렌더링

### 문제
Backend가 `otpauth://totp/...` URL을 반환하고, Vue에서 `<img :src="qrUrl">` 로 렌더링하고 있음.
`otpauth://` 프로토콜은 브라우저 이미지로 렌더링 불가 → QR 코드 이미지가 깨짐.

### 방안 선택

| 방안 | 위치 | 설명 |
|------|------|------|
| A | Backend | QR 이미지를 Base64 data URI로 변환하여 반환 |
| B | Vue | `qrcode` 라이브러리로 otpauth URL을 QR 캔버스로 렌더링 |

**B안 권장** — Backend 변경 없이 Vue만 수정.

### 2-A. Vue — qrcode 라이브러리 설치

```bash
cd partner-ui
npm install qrcode
npm install -D @types/qrcode
```

### 2-B. Vue — TwoFaView.vue 수정

**파일**: `partner-ui/src/views/partner/account/TwoFaView.vue`

```vue
<script setup>
import QRCode from 'qrcode'
import { ref, watch } from 'vue'

const qrDataUrl = ref<string>('')

// setup.qrUrl이 설정되면 QR 이미지 생성
watch(() => setup.value?.qrUrl, async (url) => {
  if (url) {
    qrDataUrl.value = await QRCode.toDataURL(url, {
      width: 200,
      margin: 2,
      color: { dark: '#000000', light: '#ffffff' }
    })
  }
})
</script>

<template>
  <!-- 변경 전 -->
  <!-- <img v-if="setup.qrUrl" :src="setup.qrUrl" alt="2FA QR" /> -->

  <!-- 변경 후 -->
  <img v-if="qrDataUrl" :src="qrDataUrl" alt="2FA QR Code" class="w-48 h-48" />
</template>
```

### 검증
- 2FA 설정 페이지 진입 시 QR 코드가 정상 표시되는지 확인
- Google Authenticator 앱에서 QR 스캔 후 인증 코드 입력 동작 확인

---

## 3. 프로필 담당자 필드 제거

### 문제
프로필 화면에 담당자명, 담당자 이메일, 담당자 전화번호 필드가 있으나 불필요.

### 3-A. Vue — ProfileView.vue 수정

**파일**: `partner-ui/src/views/partner/account/ProfileView.vue`

아래 필드/입력란 제거:
- 담당자명 (`contactName` 또는 유사 필드)
- 담당자 이메일 (`contactEmail`)
- 담당자 전화번호 (`contactPhone`)

로그인 계정 정보만 남김:
- 파트너명 (읽기 전용)
- 파트너 코드 (읽기 전용)
- 로그인 이메일 (읽기 전용 또는 수정 가능)

---

## 4. 내 계정 → 내 정보 단일 페이지

### 배경
현재 4개 메뉴(프로필, 비밀번호, 2FA 설정, 로그인 이력)가 별도 페이지.
하나의 "내 정보" 페이지에서 탭으로 전환하도록 통합.

### 4-A. Vue — MyAccountView.vue 생성 (탭 기반)

**파일 생성**: `partner-ui/src/views/partner/account/MyAccountView.vue`

| 탭 | 내용 |
|---|------|
| 기본 정보 | 파트너명, 코드, 로그인 이메일 (기존 ProfileView 내용) |
| 비밀번호 변경 | 현재 비밀번호, 새 비밀번호, 확인 (기존 PasswordView 내용) |
| 2FA 설정 | QR 코드 + OTP 입력 또는 해제 (기존 TwoFaView 내용) |
| 로그인 이력 | 로그인 기록 테이블 (기존 LoginHistoryView 내용) |

**구현 패턴**: 정산 단일 페이지와 동일한 탭 UI.

```vue
<template>
  <PageHeader title="내 정보" />
  <div class="tabs">
    <button v-for="tab in tabs" :key="tab.key"
            :class="{ active: activeTab === tab.key }"
            @click="activeTab = tab.key">
      {{ tab.label }}
    </button>
  </div>

  <ProfileSection v-if="activeTab === 'profile'" />
  <PasswordSection v-if="activeTab === 'password'" />
  <TwoFaSection v-if="activeTab === '2fa'" />
  <LoginHistorySection v-if="activeTab === 'history'" />
</template>
```

> 기존 개별 View 파일은 섹션 컴포넌트로 리팩토링하거나 인라인 삽입.

### 4-B. Vue — 사이드바 + 라우터 변경

**파일**: `partner-ui/src/utils/constants.ts`

```typescript
// 변경 전
{
  label: '내 계정', icon: User,
  children: [
    { label: '프로필', path: '/partner/account/profile' },
    { label: '비밀번호', path: '/partner/account/password' },
    { label: '2FA 설정', path: '/partner/account/2fa' },
    { label: '로그인 이력', path: '/partner/account/login-history' },
  ],
},

// 변경 후
{
  label: '내 정보', icon: User, path: '/partner/account',
},
```

**파일**: `partner-ui/src/router/index.ts`

```typescript
// 기존 4개 라우트 → 1개로 통합
{
  path: 'account',
  name: 'my-account',
  component: () => import('@/views/partner/account/MyAccountView.vue'),
  meta: { requiresAuth: true, title: '내 정보' },
},
// 기존 라우트는 리다이렉트
{ path: 'account/profile', redirect: '/partner/account' },
{ path: 'account/password', redirect: '/partner/account' },
{ path: 'account/2fa', redirect: '/partner/account' },
{ path: 'account/login-history', redirect: '/partner/account' },
```

---

## 5. Axim Pay 설정 — siteId/결제내역 버튼 제거

### 5-A. Vue — AximView.vue 수정

**파일**: `partner-ui/src/views/partner/settings/AximView.vue`

삭제할 요소:
1. **사이트 ID** 입력 필드 (`<input>` + 라벨) 제거
2. **"Axim Pay 결제 내역 보기"** 버튼 (`<button>` 또는 `<router-link>`) 제거

남길 요소:
- Axim Pay 활성화 토글
- API Key 입력
- API Secret 입력
- 설정 저장 버튼

### 5-B. Backend — AximSettings 관련 (선택)

`siteId` 필드가 Entity/DTO에 있다면 Backend에서도 제거하거나 무시 처리.
DB 컬럼은 유지해도 되지만 API 응답에서 제외.

---

## 6. Telegram 설정 — 동작 테스트

### 확인 사항

Backend 코드상 완전 구현:
- `GET /api/partner/integration/telegram` — 설정 조회 ✅
- `PUT /api/partner/integration/telegram` — 설정 수정 ✅
- `POST /api/partner/integration/telegram/test` — 테스트 전송 ✅

**테스트 절차**:
1. Telegram에서 `@CryptomentsBot` 검색 → `/start` 전송
2. 봇이 chatId를 반환하는지 확인
3. 파트너 UI에서 "ON" 토글 → 상태가 "연결됨"으로 변경되는지 확인
4. "테스트 전송" 버튼 클릭 → Telegram 메시지 수신 확인

**결과에 따른 조치**:
- 봇이 응답하지 않으면: `node-service/telegram-bot` 서비스 미실행 → 가이드 범위 밖 (Node.js 서비스 기동 필요)
- 봇은 응답하나 UI 연결 안 되면: chatId 등록 로직 확인 필요
- 모두 정상이면: Guide #56에서 별도 조치 불필요

---

## 7. 출금 승인 관리 (승인/거부)

### 배경
파트너가 `PENDING_APPROVAL` 상태 출금 건을 승인/거부할 수 있어야 함.
현재 Admin API에만 승인/거부 엔드포인트가 있고, Partner API에는 없음.

### 7-A. Backend — PartnerWithdrawalService 메서드 추가

**파일**: `partner-api/.../service/PartnerWithdrawalService.java`

```java
/**
 * 출금 승인.
 */
public WithdrawalResponse approveWithdrawal(Long partnerId, Long withdrawalId) {
    Withdrawal withdrawal = withdrawalRepository.findOne(withdrawalId);
    if (withdrawal == null || !partnerId.equals(withdrawal.getPartnerId())) {
        throw new NotFoundException(ErrorCodes.WITHDRAWAL_NOT_FOUND);
    }
    if (withdrawal.getStatus() != WithdrawalStatus.PENDING_APPROVAL) {
        throw new BadRequestException(ErrorCodes.INVALID_STATUS_TRANSITION);
    }

    Withdrawal updated = withdrawal.toBuilder()
            .status(WithdrawalStatus.APPROVED)
            .approvedAt(LocalDateTime.now())
            .build();
    withdrawalRepository.modify(updated);

    // 승인 로그 기록
    withdrawalApprovalLogRepository.save(WithdrawalApprovalLog.builder()
            .withdrawalId(withdrawalId)
            .action("APPROVE")
            .actorType("PARTNER")
            .actorId(partnerId)
            .build());

    return toResponse(updated);
}

/**
 * 출금 거부.
 */
public WithdrawalResponse rejectWithdrawal(Long partnerId, Long withdrawalId, String reason) {
    Withdrawal withdrawal = withdrawalRepository.findOne(withdrawalId);
    if (withdrawal == null || !partnerId.equals(withdrawal.getPartnerId())) {
        throw new NotFoundException(ErrorCodes.WITHDRAWAL_NOT_FOUND);
    }
    if (withdrawal.getStatus() != WithdrawalStatus.PENDING_APPROVAL) {
        throw new BadRequestException(ErrorCodes.INVALID_STATUS_TRANSITION);
    }

    Withdrawal updated = withdrawal.toBuilder()
            .status(WithdrawalStatus.REJECTED)
            .rejectedAt(LocalDateTime.now())
            .rejectedReason(reason)
            .build();
    withdrawalRepository.modify(updated);

    // 거부 로그 기록
    withdrawalApprovalLogRepository.save(WithdrawalApprovalLog.builder()
            .withdrawalId(withdrawalId)
            .action("REJECT")
            .actorType("PARTNER")
            .actorId(partnerId)
            .memo(reason)
            .build());

    return toResponse(updated);
}
```

> **주의**: `WithdrawalApprovalLog` Entity/Repository가 common에 있는지 확인.
> `approvedAt`, `rejectedAt`, `rejectedReason` 필드가 `Withdrawal` Entity에 있는지 DDL 확인.

### 7-B. Backend — PartnerWithdrawalController 엔드포인트 추가

**파일**: `partner-api/.../controller/PartnerWithdrawalController.java`

```java
/**
 * 출금 승인.
 *
 * @param withdrawalId 출금 ID
 * @return 승인된 출금
 * @response 200 승인 성공
 * @response 400 상태 전이 불가
 * @response 404 출금 건 없음
 * @group 출금 관리
 * @auth true
 */
@PostMapping(name = "출금 승인", value = "/{withdrawalId}/approve")
public WithdrawalResponse approveWithdrawal(@PathVariable Long withdrawalId) {
    Long partnerId = getSession().getPartnerId();
    return partnerWithdrawalService.approveWithdrawal(partnerId, withdrawalId);
}

/**
 * 출금 거부.
 *
 * @param withdrawalId 출금 ID
 * @param request 거부 사유
 * @return 거부된 출금
 * @response 200 거부 성공
 * @response 400 상태 전이 불가
 * @response 404 출금 건 없음
 * @group 출금 관리
 * @auth true
 */
@PostMapping(name = "출금 거부", value = "/{withdrawalId}/reject")
public WithdrawalResponse rejectWithdrawal(@PathVariable Long withdrawalId,
                                            @RequestBody(required = false) RejectWithdrawalRequest request) {
    Long partnerId = getSession().getPartnerId();
    String reason = request != null ? request.getReason() : null;
    return partnerWithdrawalService.rejectWithdrawal(partnerId, withdrawalId, reason);
}
```

### 7-C. Backend — RejectWithdrawalRequest DTO 생성

**파일 생성**: `partner-api/.../dto/request/RejectWithdrawalRequest.java`

```java
package com.cryptoments.partnerapi.dto.request;

import lombok.*;

@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
public class RejectWithdrawalRequest {
    /** 거부 사유 */
    private String reason;
}
```

### 7-D. Vue — 출금 내역에 승인/거부 버튼 추가

**파일**: `partner-ui/src/views/partner/withdrawals/WithdrawalListView.vue`

출금 상세 Drawer 또는 목록 행에서 `PENDING_APPROVAL` 상태인 건에 버튼 표시:

```vue
<template v-if="selectedWithdrawal?.status === 'PENDING_APPROVAL'">
  <div class="flex gap-2 mt-4">
    <button @click="approveWithdrawal(selectedWithdrawal.id)"
            class="bg-green-600 text-white px-4 py-2 rounded">
      승인
    </button>
    <button @click="openRejectModal(selectedWithdrawal.id)"
            class="bg-red-600 text-white px-4 py-2 rounded">
      거부
    </button>
  </div>
</template>
```

거부 시 사유 입력 모달 추가:
```vue
<SlideDrawer v-model="showRejectModal" title="출금 거부">
  <label>거부 사유</label>
  <textarea v-model="rejectReason" rows="3" />
  <button @click="rejectWithdrawal">거부 확인</button>
</SlideDrawer>
```

**API 서비스** (`withdrawal.service.ts` 추가):
```typescript
approveWithdrawal(id: number): Promise<WithdrawalResponse>
  → POST /api/partner/withdrawals/{id}/approve

rejectWithdrawal(id: number, reason?: string): Promise<WithdrawalResponse>
  → POST /api/partner/withdrawals/{id}/reject
```

---

## 8. 출금 취소 버튼

### 배경
Backend에 `POST /api/partner/withdrawals/{id}/cancel` 이미 존재.
Vue에서 버튼만 추가하면 됨.

### 8-A. Vue — 출금 내역에 취소 버튼 추가

**파일**: `partner-ui/src/views/partner/withdrawals/WithdrawalListView.vue`

`REQUESTED` 또는 `PENDING_APPROVAL` 상태인 건에 취소 버튼 표시:

```vue
<template v-if="['REQUESTED', 'PENDING_APPROVAL'].includes(selectedWithdrawal?.status)">
  <button @click="cancelWithdrawal(selectedWithdrawal.id)"
          class="text-gray-600 border px-4 py-2 rounded">
    취소
  </button>
</template>
```

**API 서비스** (`withdrawal.service.ts`):
```typescript
cancelWithdrawal(id: number): Promise<void>
  → POST /api/partner/withdrawals/{id}/cancel
```

확인 다이얼로그 필수: "정말 취소하시겠습니까?" → 확인 → API 호출 → 목록 새로고침.

---

## 9. 입금 주소 메뉴 제거

### 배경
입금 주소 풀은 파트너가 직접 관리하는 기능이 없고 조회만 가능.
지갑 목록에서 동일 정보를 확인할 수 있으므로 중복 메뉴 제거.

### 9-A. Vue — 사이드바 메뉴 제거

**파일**: `partner-ui/src/utils/constants.ts`

```typescript
// 입금 관리 children에서 제거
{ label: '입금 주소', path: '/partner/deposit-addresses' },  // ← 이 줄 삭제
```

### 9-B. Vue — 라우터 리다이렉트

**파일**: `partner-ui/src/router/index.ts`

```typescript
// 기존 라우트 → 리다이렉트
{ path: 'deposit-addresses', redirect: '/partner/wallets' },
{ path: 'deposit-addresses/:id', redirect: '/partner/wallets' },
```

> `DepositAddressesView.vue` 파일은 삭제하거나 유지 (라우트만 리다이렉트하면 접근 불가).

---

## 10. 입금 세션 — 입금 금액 컬럼 추가

### 배경
`DepositSession` Entity에 `receivedAmount` (실제 수신 금액) 필드가 있으나
Vue 화면에서 `요청금액`만 표시하고 있음.

### 10-A. Vue — DepositSessionsView.vue 수정

**파일**: `partner-ui/src/views/partner/deposits/DepositSessionsView.vue`

테이블 컬럼에 추가:

| 기존 컬럼 | 추가 |
|-----------|------|
| 세션코드, 할당주소, 통화, 네트워크, 요청금액, 입금방식, 상태, 생성일, 만료일 | **입금 금액** |

```vue
<!-- 요청금액 컬럼 뒤에 추가 -->
<th>입금 금액</th>

<!-- 행 -->
<td>
  <AmountDisplay
    v-if="row.receivedAmount && row.receivedAmount > 0"
    :amount="row.receivedAmount"
    :currency="row.currencyCode" />
  <span v-else class="text-gray-400">-</span>
</td>
```

> 0이면 아직 입금되지 않은 건이므로 `-` 표시.

---

## 11. Axim Pay 결제 요청 — NOT_IMPLEMENTED 해제

### 배경
`POST /api/partner/axim-payments/request` 엔드포인트가 `NOT_IMPLEMENTED` stub 상태.
core 모듈의 `AximService`가 구현되어 있으므로 연결만 하면 됨.

### 11-A. Backend — PartnerDepositService에 결제 요청 메서드 추가

**파일**: `partner-api/.../service/PartnerDepositService.java`

```java
// ── import 추가 ──
import com.cryptoments.core.axim.AximService;

// ── 생성자에 AximService 주입 ──
private final AximService aximService;

/**
 * Axim Pay 결제 요청 생성.
 */
public AximPayment requestAximPayment(Long partnerId, RequestAximPaymentRequest request) {
    return aximService.createPayment(
            partnerId,
            request.getCurrencyId(),
            request.getNetworkId(),
            request.getAmount(),
            request.getPartnerUserId(),
            request.getPartnerReference()
    );
}
```

> **주의**: `AximService.createPayment()` 메서드 시그니처를 확인하고 파라미터 조정 필요.
> core 모듈에서 `AximService`의 실제 메서드명과 파라미터를 확인할 것.

### 11-B. Backend — Controller에서 stub 제거

**파일**: `partner-api/.../controller/PartnerDepositController.java`

```java
// ── 변경 전 ──
@PostMapping(name = "Axim Pay 결제 요청 생성", value = "/axim-payments/request")
@ResponseStatus(HttpStatus.CREATED)
public AximPayment requestAximPayment(@Valid @RequestBody RequestAximPaymentRequest request) {
    // TODO: AximService / AximPayClient 연동 구현.
    throw new BadRequestException(
            new ErrorCode("NOT_IMPLEMENTED", "Axim Pay 결제 요청 기능은 현재 준비 중입니다."));
}

// ── 변경 후 ──
@PostMapping(name = "Axim Pay 결제 요청 생성", value = "/axim-payments/request")
@ResponseStatus(HttpStatus.CREATED)
public AximPayment requestAximPayment(@Valid @RequestBody RequestAximPaymentRequest request) {
    Long partnerId = getSession().getPartnerId();
    return partnerDepositService.requestAximPayment(partnerId, request);
}
```

### 11-C. Vue — 준비 중 배너 + 버튼 비활성화 해제

**파일**: `partner-ui/src/views/partner/deposits/AximPaymentsView.vue`

```vue
<!-- 삭제: 준비 중 배너 -->
<!-- <div class="warning">Axim Pay 결제 요청 기능은 현재 준비 중입니다...</div> -->

<!-- 변경: 버튼 활성화 -->
<!-- 변경 전: <button disabled>결제 요청 (준비 중)</button> -->
<!-- 변경 후: -->
<button @click="openPaymentModal">결제 요청</button>
```

---

## 체크리스트

### Backend (IntelliJ)

| # | 항목 | 파일 |
|---|------|------|
| 1 | BalanceResponse에 networkId/chainSymbol 추가 | `dto/response/BalanceResponse.java` |
| 2 | PartnerBalanceMapper JOIN + GROUP BY 수정 | `mapper/PartnerBalanceMapper.java` |
| 3 | 출금 승인 메서드 (approveWithdrawal) | `service/PartnerWithdrawalService.java` |
| 4 | 출금 거부 메서드 (rejectWithdrawal) | `service/PartnerWithdrawalService.java` |
| 5 | 출금 승인/거부 엔드포인트 | `controller/PartnerWithdrawalController.java` |
| 6 | RejectWithdrawalRequest DTO 생성 | `dto/request/RejectWithdrawalRequest.java` |
| 7 | Axim Pay 결제 요청 — stub 해제 + AximService 연동 | `service/PartnerDepositService.java` + `controller/PartnerDepositController.java` |

### Vue (VS Code)

| # | 항목 | 파일 |
|---|------|------|
| 8 | 대시보드 통화별 잔액 — chainSymbol 표시 | `DashboardView.vue` + `balance.ts` |
| 9 | 2FA QR — qrcode 라이브러리 + 렌더링 | `TwoFaView.vue` |
| 10 | 프로필 담당자 필드 제거 | `ProfileView.vue` |
| 11 | 내 계정 → 내 정보 단일 페이지 (탭) | `MyAccountView.vue` (신규) + 라우터 + 사이드바 |
| 12 | Axim Pay 설정 — siteId/결제내역 버튼 제거 | `AximView.vue` |
| 13 | 출금 승인/거부 버튼 + 거부 사유 모달 | `WithdrawalListView.vue` |
| 14 | 출금 취소 버튼 | `WithdrawalListView.vue` |
| 15 | 입금 주소 메뉴 제거 + 라우트 리다이렉트 | `constants.ts` + `router/index.ts` |
| 16 | 입금 세션 — receivedAmount 컬럼 추가 | `DepositSessionsView.vue` |
| 17 | Axim Pay — 준비 중 배너/버튼 해제 | `AximPaymentsView.vue` |

### 테스트

| # | 항목 |
|---|------|
| 18 | Telegram 봇 `/start` → chatId 등록 → 테스트 전송 확인 |

### 작업 순서 (권장)

```
Phase A: Backend (IntelliJ)
  1 → 2           대시보드 잔액 네트워크
  3 → 4 → 5 → 6   출금 승인/거부
  7               Axim Pay stub 해제

Phase B: Vue (VS Code)
  8               대시보드 잔액 표시
  9               2FA QR
  10 → 11         프로필 + 내 정보 통합
  12              Axim Pay 설정
  13 → 14         출금 승인/거부/취소
  15              입금 주소 제거
  16              입금 세션 컬럼
  17              Axim Pay 활성화

Phase C: 테스트
  18              Telegram 동작 확인
```
