# Cryptoments Node.js 코드 리뷰 결과

**검토일**: 2026-03-16
**대상**: `node-service/packages/` (5개 패키지, 62개 TypeScript 파일)
**기준 문서**: DDL v1.4, NODEJS_API_INTEGRATION_GUIDE.md v1.1

---

## 리뷰 범위

| 패키지 | 유형 | 파일 수 | 역할 |
|--------|------|---------|------|
| **common** | 공유 라이브러리 | 28 | DB Repository, Chain Provider, Crypto, Nonce, Types |
| **blockchain-api** | REST API (3001) | 14 | 지갑 파생, 잔액, TX 조회, 컨트랙트 관리 |
| **relayer-api** | REST API + Poller (3002) | 4 | Relayer 관리, 집금/출금 폴러 |
| **wallet-activator** | Worker | 3 | 가스 전송 → ERC-20 approve 실행 |
| **telegram-bot** | Worker | 2 | 텔레그램 알림 봇 |

---

## CRITICAL — DDL에 없는 팬텀 컬럼 (2건)

### PHANTOM-1. `partners.is_active` — DDL에 존재하지 않는 컬럼

**위치**: `common/db/repositories/PartnerRepo.ts:15,24,32`

DDL `partners` 테이블에는 `is_active` 컬럼이 없다. 상태 관리는 `status VARCHAR(20)` (PENDING/ACTIVE/SUSPENDED/TERMINATED)로 한다.

```typescript
// Line 24 — 존재하지 않는 컬럼 SELECT + WHERE
'SELECT id, partner_code, name, business_name, partner_type, is_active
 FROM partners WHERE partner_code = ? AND is_active = TRUE LIMIT 1'
//                                       ^^^^^^^^^ DDL에 없음
```

**수정 지침**:
- `SELECT`에서 `is_active` 제거, `status` 추가
- `WHERE is_active = TRUE` → `WHERE status = 'ACTIVE'`
- TypeScript 인터페이스의 `is_active: boolean` → `status: string`

### PHANTOM-2. `nonce_tracker.network_id` — DDL에 존재하지 않는 컬럼

**위치**: `common/src/types/tx.ts:149`

DDL `nonce_tracker` 테이블에는 `network_id` 컬럼이 없다. 네트워크는 `wallet_address_id` → `wallet_addresses.network_id` JOIN으로 추적한다.

```typescript
export interface NonceTracker {
  id: number;
  wallet_address_id: number;
  network_id: number;    // ❌ DDL에 없는 팬텀 필드
  next_nonce: number;
  // ...
}
```

**수정 지침**:
- 타입에서 `network_id` 제거
- 네트워크 정보가 필요한 곳은 `wallet_addresses` JOIN으로 조회

---

## CRITICAL — 런타임 크래시 (2건)

### BUG-1. `relayer_wallets.address` 컬럼 없음

**위치**: `relayer-api/routes/relayer.ts:188,219`, `telegram-bot/handlers/index.ts:217`

DDL에 `relayer_wallets`는 `wallet_address_id` (FK)만 있고 `address` 컬럼이 없다. 코드는 `relayer.address`를 직접 접근하고 있어 런타임에 `undefined` 크래시 발생.

```typescript
// relayer.ts:188 — removeRelayer 호출 시 크래시
const tx = await contract.removeRelayer(relayer.address);  // ❌ undefined

// handlers/index.ts:217 — /status 명령어 크래시
message += `  - ${r.address.slice(0, 10)}...`;  // ❌ undefined.slice()
```

**수정**: `relayer_wallets` 조회 시 `wallet_addresses` JOIN으로 주소를 가져와야 함.

```sql
SELECT rw.*, wa.address
FROM relayer_wallets rw
JOIN wallet_addresses wa ON rw.wallet_address_id = wa.id
WHERE rw.id = ?
```

### BUG-2. TRON Approve 에너지 추정 — 잘못된 컨트랙트 주소

**위치**: `wallet-activator/services/ApprovalProcessor.ts:228-230`

TRC-20 approve의 에너지 추정에서 `to_address`에 토큰 컨트랙트를 넣고 있다. approve는 CryptoRelayer(spender)에 대한 승인이므로 `to_address`는 CryptoRelayer 주소여야 한다.

```typescript
// ❌ 현재 코드
const estimate = await tronZap.estimateEnergy({
  from_address: walletAddress.address,
  to_address: tokenContract,           // 잘못됨
  contract_address: tokenContract,
});

// ✅ 수정
const estimate = await tronZap.estimateEnergy({
  from_address: walletAddress.address,
  to_address: relayerContract.contract_address,  // CryptoRelayer 주소
  contract_address: tokenContract,
});
```

**영향**: TRON 네트워크의 모든 approve TX가 에너지 부족으로 실패.

---

## HIGH — 보안/데이터 문제 (3건)

### SEC-1. SQL Injection — WithdrawalRepo ORDER BY

**위치**: `common/db/repositories/WithdrawalRepo.ts:15`

```typescript
const orderBy = options.orderBy ?? 'created_at ASC';
return query<WithdrawalRow[]>(
  `SELECT * FROM withdrawals WHERE status = ? ORDER BY ${orderBy} LIMIT ?`,
  //                                                   ^^^^^^^^^ 문자열 삽입
  [status, limit],
);
```

**수정**: 허용된 컬럼 화이트리스트 검증 추가.

```typescript
const ALLOWED_ORDER = ['created_at ASC', 'created_at DESC', 'id ASC', 'id DESC'];
const orderBy = ALLOWED_ORDER.includes(options.orderBy) ? options.orderBy : 'created_at ASC';
```

### SEC-2. 텔레그램 봇 — 금액 표시 깨짐

**위치**: `telegram-bot/handlers/index.ts:126,158`

DECIMAL(36,18) 값이 원시 상태로 표시됨. `/balance`와 `/recent` 명령어가 사용 불가 수준.

```
// 현재 출력
BSC USDT: 1000000000000000000

// 기대 출력
BSC USDT: 1.000000
```

**수정**: decimals 기반 포맷팅 함수 추가. `currencies.decimals` 값으로 나누기.

### SEC-3. 텔레그램 봇 — 출금 내역에 통화 정보 없음

**위치**: `telegram-bot/handlers/index.ts:168`

입금은 통화를 표시하지만 출금은 금액만 표시.

```typescript
// 입금 — OK
message += `• ${d.amount} ${d.currency} [${d.status}] ${date}\n`;

// 출금 — 통화 없음
message += `• ${w.amount} (${w.status}) ${date}\n`;  // ❌ 어떤 토큰?
```

**수정**: 출금 쿼리에 currencies JOIN 추가.

---

## MEDIUM — 로직/성능 개선 (8건)

### MED-1. 입력 검증 — Number 변환 (4개 라우트 파일)

**위치**: `blockchain-api/routes/balance.ts:20`, `tx.ts:12`, `gas.ts:12`, `wallet.ts:12`

```typescript
const networkId = Number(req.query.networkId);
if (!networkId) { ... }  // networkId=0이면 falsy로 처리됨
```

**수정**: `if (isNaN(networkId) || networkId <= 0)`

### MED-2. N+1 쿼리 — BalanceService

**위치**: `blockchain-api/services/BalanceService.ts:54-91`

잔액 동기화 루프 내에서 `currencyRepo.findById()`를 매번 호출.

**수정**: 루프 전에 한 번에 조회해서 Map으로 캐싱.

### MED-3. 컨트랙트 교체 시 기존 컨트랙트 미정지

**위치**: `blockchain-api/services/ContractDeployService.ts:71-74`

새 컨트랙트 배포 시 기존 컨트랙트를 DEPRECATED로 변경하지만 PAUSED하지 않음. 기존 컨트랙트로 TX가 계속 실행될 수 있음.

**수정**: `deprecateByNetwork()` 전에 `updateStatus(existing.id, 'PAUSED')` 호출.

### MED-4. addRelayer TX 실패 시 DB 상태 미복구

**위치**: `blockchain-api/services/ContractDeployService.ts:194-218`

`tx.wait()` 실패 시 `relayer_wallets` 상태가 `REGISTERING`에 멈춤.

**수정**: try/catch로 감싸서 실패 시 상태를 `PENDING`으로 롤백.

### MED-5. 가스 추정 하드코딩

**위치**: `common/chain/EvmProvider.ts:118`

```typescript
const gasLimit = 60000n;  // 하드코딩
```

**수정**: `await provider.estimateGas({...approveCall...})`로 실제 추정.

### MED-6. DepositRepo — 비활성 통화 필터 없음

**위치**: `common/db/repositories/DepositRepo.ts:18`

입금 조회 시 `currencies.is_active = TRUE` 조건 없음.

### MED-7. Relayer 조회 O(n)

**위치**: `relayer-api/routes/relayer.ts:139-142`

```typescript
const relayers = await relayerWalletRepo.findAll();
const relayer = relayers.find((r) => r.id === relayerId);  // O(n) 선형 탐색
```

**수정**: `relayerWalletRepo.findById(relayerId)` 직접 조회.

### MED-8. chainId=0 — EVM Provider 초기화 오류

**위치**: `blockchain-api/app.ts:57`

```typescript
chainId: Number(row.chain_id) || 0,  // TRON은 chain_id NULL → 0
```

EVM Provider가 chainId=0으로 초기화되면 오류 발생.

**수정**: `chainId: row.network_type === 'EVM' ? Number(row.chain_id) : undefined`

---

## LOW — 운영 개선 (3건)

### LOW-1. NonceManager Redis Lock ID 불일치

**위치**: `common/nonce/NonceManager.ts:50`

`releaseLock()` 시 빈 문자열 전달. 원본 lockId를 저장해서 전달해야 함.

### LOW-2. 폴링 에러 시 백오프 없음

**위치**: `relayer-api/services/CollectionPoller.ts:284`

DB 연결 실패 등 지속적 에러 시 3초마다 재시도. 지수 백오프 필요.

### LOW-3. Graceful Shutdown 타임아웃 하드코딩

**위치**: `relayer-api/app.ts:179`

60초 하드코딩. 환경변수로 설정 가능하게 변경 권장.

---

## 정상 확인된 항목

| 항목 | 상태 | 비고 |
|------|------|------|
| Nonce FOR UPDATE 잠금 | ✅ | CLAUDE.md 요구사항 준수 |
| CryptoRelayer ABI 호출 | ✅ | executeTransfer 정상 |
| gas_cost_records 기록 | ✅ | 5가지 TX 타입 모두 기록 |
| HD 지갑 파생 (BIP-44) | ✅ | EVM/TRON 경로 분리 |
| AES-256-GCM 키 암호화 | ✅ | IV + AuthTag 정상 처리 |
| 상태 전이 (approval/collection/withdrawal) | ✅ | 문서와 일치 |
| API 엔드포인트 (22개) | ✅ | INTEGRATION_GUIDE와 100% 일치 |
| Telegram Long Polling | ✅ | 명령어 8개 구현 |
| TronZap 에너지 위임 | ✅ | (BUG-2 제외) 정상 |
| BigInt JSON 직렬화 | ✅ | toString() 변환 |
| Graceful Shutdown | ✅ | SIGINT/SIGTERM 핸들링 |

---

## 수정 우선순위

| 순위 | ID | 심각도 | 예상 작업량 | 설명 |
|------|-----|--------|-----------|------|
| 1 | PHANTOM-1 | CRITICAL | 20분 | partners.is_active → status 수정 (쿼리+타입) |
| 2 | PHANTOM-2 | CRITICAL | 10분 | nonce_tracker.network_id 타입에서 제거 |
| 3 | BUG-1 | CRITICAL | 30분 | relayer_wallets JOIN 수정 |
| 4 | BUG-2 | CRITICAL | 15분 | TRON approve 에너지 추정 주소 수정 |
| 5 | SEC-1 | HIGH | 15분 | SQL Injection ORDER BY 화이트리스트 |
| 4 | SEC-2 | HIGH | 30분 | 텔레그램 금액 포맷팅 |
| 5 | SEC-3 | HIGH | 15분 | 출금 내역 통화 표시 |
| 6 | MED-3 | MEDIUM | 15분 | 컨트랙트 교체 시 기존 PAUSE |
| 7 | MED-4 | MEDIUM | 20분 | addRelayer 실패 롤백 |
| 8 | MED-1 | MEDIUM | 15분 | 입력 검증 일괄 수정 |
| 9 | MED-2 | MEDIUM | 15분 | N+1 쿼리 제거 |
| 10 | MED-5~8 | MEDIUM | 30분 | 기타 로직 수정 |
| 11 | LOW-1~3 | LOW | 20분 | 운영 개선 |

**총 예상 작업량**: 약 3~4시간
