# 지갑 UI 설계 > 작성일: 2026-03-23 > 상태: 승인됨 --- ## 개요 웹 클라이언트(`a301_client`)에 블록체인 지갑 UI를 추가한다. 단일 `/wallet` 페이지에 4개 탭(지갑, 자산, 인벤토리, 마켓)으로 구성하고, HomePage에 잔액 요약 카드를 배치한다. --- ## 페이지 구조 ### 라우팅 | 경로 | 컴포넌트 | 인증 | |------|----------|------| | `/wallet` | WalletPage | 로그인 필수 | `/wallet` 라우트 보호: 기존 `AdminRoute` 패턴을 참고하여 `PrivateRoute` 컴포넌트를 새로 만든다. 로그인하지 않은 유저는 `/login`으로 리다이렉트. 기존 라우트는 변경하지 않는다. ### HomePage 변경 1. **헤더**: "지갑" 링크 추가 → `/wallet`로 이동 2. **지갑 요약 카드**: DownloadSection 위에 배치 - TOL 잔액 (큰 글씨) - 보유 자산 수, 장착 아이템 수 - 클릭 시 `/wallet`로 이동 - 로그인하지 않은 상태에서는 숨김 - 3개 API (`/balance`, `/assets`, `/inventory`) `Promise.all`로 병렬 호출 - 일부 API 실패 시: 실패한 항목만 "--"로 표시, 카드 자체는 숨기지 않음 --- ## 탭 1: 지갑 잔액, 지갑 주소, 키 내보내기. ### 잔액 카드 - TOL 잔액을 크게 표시 - API: `GET /api/chain/balance` ### 지갑 정보 - 공개키: 축약 표시 (`a3b4...e1f2`) + 복사 버튼 - 주소: 축약 표시 (`a3b4c5d6...e9f0a1b2`) + 복사 버튼 - 클릭 시 클립보드에 전체 값 복사, 토스트로 "복사됨" 알림 - API: `GET /api/chain/wallet` ### 키 내보내기 - 비밀번호 입력 필드 + "내보내기" 버튼 - 성공 시: 개인키 hex를 화면에 표시 (복사 버튼 포함) - 경고 텍스트: "개인키를 안전하게 보관하세요" - 탭 이탈 시(다른 탭 클릭): 개인키 표시 상태 초기화 (보안) - 에러 처리: - HTTP 401 → "비밀번호가 올바르지 않습니다" - 기타 에러 → 토스트로 서버 에러 메시지 표시 - API: `POST /api/chain/wallet/export` (body: `{"password": "..."}`) --- ## 탭 2: 자산 보유 NFT 목록 + 상세 정보. ### 자산 목록 - 아이템 이름, 템플릿 이름, 자산 ID 표시 - 거래 가능 여부 표시 ("거래 가능" / "거래 불가") - API 호출 전략: 1. `GET /api/chain/assets` → 자산 ID 배열 반환 2. 각 ID에 대해 `GET /api/chain/asset/:id`를 `Promise.all`로 **병렬 호출** 3. 개별 자산 로드 실패 시: 해당 자산만 "로드 실패" 표시, 나머지는 정상 표시 - 로딩 중: 스피너 표시 ### 자산 상세 (클릭 시 펼치기) - 속성(properties) 키-값 표시 - 거래 가능 여부 - 마켓 등록 상태 (등록됨 / 미등록) - **마켓 등록 버튼**: 미등록 + 거래 가능한 자산에만 표시 - 클릭 시: 가격 입력 인라인 UI (펼쳐진 상세 영역 내 숫자 input + "등록" 버튼) - `POST /api/chain/market/list` (body: `{"asset_id": "...", "price": N}`) - Idempotency-Key 헤더 필요 - 성공 시: 토스트 알림 + 해당 자산 상태 갱신 (마켓 등록됨으로 변경) --- ## 탭 3: 인벤토리 장착 슬롯 현황 (조회 전용). ### 슬롯 목록 - 슬롯 이름 + 장착된 아이템 이름/ID 표시 - 빈 슬롯은 "비어있음"으로 표시 (점선 테두리) - 장착/해제 버튼 없음 (게임 내에서만 조작) - API: `GET /api/chain/inventory` --- ## 탭 4: 마켓 NFT 마켓플레이스. 구매, 판매(자산 탭에서 연결), 취소. ### 리스팅 목록 - 전체 / 내 리스팅 필터 토글 - 각 리스팅: 아이템 이름, 판매자 (축약 주소), 가격 - "내 리스팅" 필터: 클라이언트에서 `/api/chain/wallet`으로 가져온 내 지갑 주소와 리스팅의 seller 필드를 비교하여 필터링 - API: `GET /api/chain/market` → 활성 리스팅 목록 - 미사용: `GET /api/chain/market/:id` — 리스팅 상세 모달이 없으므로 의도적으로 사용하지 않음 ### 구매 - 타인의 리스팅에 "구매" 버튼 - 확인 다이얼로그 (useConfirm 사용): "화염 활을 500 TOL에 구매하시겠습니까?" - `POST /api/chain/market/buy` (body: `{"listing_id": "..."}`) - Idempotency-Key 헤더 필요 - 성공 시: 토스트 알림 + 리스팅 목록 새로고침 + 잔액 갱신 - 실패 시: 서버 응답 에러 메시지를 토스트로 표시 (잔액 부족 등 포함) ### 내 리스팅 취소 - 내 리스팅에 "취소" 버튼 (빨간 테두리) - 확인 다이얼로그: "리스팅을 취소하시겠습니까?" - `POST /api/chain/market/cancel` (body: `{"listing_id": "..."}`) - Idempotency-Key 헤더 필요 - 성공 시: 토스트 알림 + 리스팅 목록 새로고침 --- ## API 호출 패턴 ### 기존 API 클라이언트 활용 - 모든 API 호출은 `src/api/client.js`의 `apiFetch()` 경유 - JWT 토큰 자동 첨부, 401 시 자동 refresh - 새 파일: `src/api/chain.js` — 체인 관련 API 래퍼 함수 모음 ### Idempotency-Key - 마켓 구매/등록/취소 등 트랜잭션 API에는 `Idempotency-Key` 헤더 필요 - `chain.js` 래퍼 내부에서 `crypto.randomUUID()`로 자동 생성하여 헤더에 추가 - 호출 측(컴포넌트)에서는 Idempotency-Key를 신경 쓸 필요 없음 ### 에러 처리 - API 에러 시 토스트로 에러 메시지 표시 - 네트워크 에러 시 기존 retry 로직 적용 (GET만) ### 로딩 상태 - 각 탭 진입 시: 중앙 스피너 표시 (기존 로딩 패턴과 동일) - WalletSummary: 인라인 스피너 또는 "--" placeholder --- ## 컴포넌트 구조 ``` src/ ├── api/ │ └── chain.js # 체인 API 래퍼 (신규) ├── pages/ │ ├── WalletPage.jsx # /wallet 페이지 (탭 컨테이너) (신규) │ └── WalletPage.css # 지갑 페이지 스타일 (신규) ├── components/ │ └── wallet/ # 지갑 관련 컴포넌트 (신규) │ ├── WalletTab.jsx # 탭 1: 잔액, 주소, 키 내보내기 │ ├── AssetsTab.jsx # 탭 2: 자산 목록 + 상세 │ ├── InventoryTab.jsx # 탭 3: 인벤토리 조회 │ ├── MarketTab.jsx # 탭 4: 마켓 │ └── WalletSummary.jsx # HomePage 요약 카드 ``` --- ## 스타일링 - 기존 프로젝트 패턴 그대로: plain CSS, dark 테마 - 색상: 기존 `#BACDB0` (sage green) accent 사용 - 탭 UI: 하단 보더로 활성 탭 표시 - 복사 버튼: 클릭 시 `navigator.clipboard.writeText()` + 토스트 - 반응형: `@media (max-width: 768px)` 대응 --- ## 변경 파일 요약 | 파일 | 변경 | |------|------| | `src/api/chain.js` | 신규: 체인 API 래퍼 (Idempotency-Key 자동 생성) | | `src/pages/WalletPage.jsx` | 신규: 지갑 페이지 (탭 컨테이너) | | `src/pages/WalletPage.css` | 신규: 지갑 페이지 스타일 | | `src/components/wallet/WalletTab.jsx` | 신규: 잔액/주소/키 내보내기 | | `src/components/wallet/AssetsTab.jsx` | 신규: 자산 목록 + 상세 + 마켓 등록 | | `src/components/wallet/InventoryTab.jsx` | 신규: 인벤토리 조회 | | `src/components/wallet/MarketTab.jsx` | 신규: 마켓 (구매/취소) | | `src/components/wallet/WalletSummary.jsx` | 신규: 홈 요약 카드 | | `src/App.jsx` | 수정: `/wallet` 라우트 + PrivateRoute 추가 | | `src/pages/HomePage.jsx` | 수정: 헤더 링크 + WalletSummary 추가 |