# Node.js 코드 리뷰 수정 보고서

**작업일**: 2026-03-16
**기준 문서**: `NODEJS_CODE_REVIEW_2026_03_16.md`
**대상**: `node-service/packages/` (5개 패키지)
**빌드 결과**: `pnpm run build` — 5개 패키지 모두 SUCCESS

---

## 수정 결과 요약

| 총 이슈 | 수정 완료 | 이미 해결 | 보류 |
|---------|----------|----------|------|
| 18건 | 16건 | 1건 (BUG-1) | 1건 (MED-5) |

---

## CRITICAL (4건 → 3건 수정, 1건 이미 해결)

### PHANTOM-1. `partners.is_active` → `status` 수정 ✅

**파일**: `common/src/db/repositories/PartnerRepo.ts`

| 항목 | Before | After |
|------|--------|-------|
| 인터페이스 | `is_active: boolean` | `status: string` |
| SELECT | `is_active` | `status` |
| WHERE | `is_active = TRUE` | `status = 'ACTIVE'` |

### PHANTOM-2. `NonceTracker.network_id` 제거 ✅

**파일**: `common/src/types/tx.ts:147`

- `NonceTracker` 인터페이스에서 `network_id: number` 필드 삭제
- DDL `nonce_tracker`에 해당 컬럼 없음 — 네트워크는 `wallet_addresses` JOIN으로 추적

### BUG-1. `relayer_wallets.address` — 이미 해결됨 ⏭️

**결과**: `RelayerWalletRepo.ts`의 `findAll()`, `findById()`, `findActiveRelayer()` 모두 이미 `wallet_addresses` JOIN으로 `address`를 가져오고 있음. 리뷰 문서의 오진.

### BUG-2. TRON Approve 에너지 추정 주소 수정 ✅

**파일**: `wallet-activator/src/services/ApprovalProcessor.ts:227-237`

```typescript
// Before — tokenContract를 to_address에 넣음 (잘못됨)
const estimate = await tronZap.estimateEnergy({
  from_address: walletAddress.address,
  to_address: tokenContract,           // ❌
  contract_address: tokenContract,
});

// After — CryptoRelayer spender 주소를 to_address에 넣음
const relayerContract = await relayerContractRepo.findActiveByNetwork(approval.network_id);
const estimate = await tronZap.estimateEnergy({
  from_address: walletAddress.address,
  to_address: relayerContract.contract_address,  // ✅
  contract_address: tokenContract,
});
```

---

## HIGH — 보안/데이터 (3건 → 3건 수정)

### SEC-1. SQL Injection ORDER BY 화이트리스트 ✅

**파일**: `common/src/db/repositories/WithdrawalRepo.ts:14-27`

```typescript
// Before — 외부 입력 문자열 직접 삽입
const orderBy = options.orderBy ?? 'created_at ASC';

// After — 화이트리스트 검증
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/src/handlers/index.ts`, `common/src/db/repositories/DepositRepo.ts`, `common/src/db/repositories/WalletBalanceRepo.ts`

변경 내용:
1. `formatAmount()` 헬퍼 함수 추가 — decimals 기반 소수점 변환 (예: `1000000000000000000` → `1`)
2. `DepositRepo.findRecent()` — `c.decimals` 컬럼 추가
3. `WalletBalanceRepo.findMasterBalances()` — `c.decimals` 컬럼 추가
4. `/balance`, `/recent` 핸들러에서 `formatAmount()` 적용

### SEC-3. 출금 내역 통화 표시 ✅

**파일**: `common/src/db/repositories/WithdrawalRepo.ts`, `telegram-bot/src/handlers/index.ts`

변경 내용:
1. `WithdrawalWithCurrency` 인터페이스 추가 (currency, decimals 포함)
2. `findRecentWithCurrency()` 메서드 추가 — `currencies` JOIN으로 통화 심볼 조회
3. `/recent` 핸들러에서 `findRecentWithCurrency()` 사용, `formatAmount()` 적용

```typescript
// Before
message += `• ${w.amount} (${w.status}) ${date}\n`;                     // 통화 없음

// After
message += `• ${formatAmount(w.amount, w.decimals)} ${w.currency} (${w.status}) ${date}\n`;
```

---

## MEDIUM — 로직/성능 (8건 → 7건 수정, 1건 보류)

### MED-1. 입력 검증 `Number()` falsy 문제 ✅

**파일**: `blockchain-api/src/routes/{balance,tx,gas,wallet}.ts` (4개 파일)

```typescript
// Before — networkId=0이면 falsy로 처리됨
if (!networkId) { ... }

// After
if (isNaN(networkId) || networkId <= 0) { ... }
```

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

**파일**: `blockchain-api/src/services/BalanceService.ts:54-56`

```typescript
// Before — 루프 내에서 매번 DB 호출
const currency = await currencyRepo.findById(bal.currency_id);

// After — 루프 전에 한 번에 조회 후 Map 캐싱
const allCurrencies = await currencyRepo.findAll();
const currencyMap = new Map(allCurrencies.map((c) => [c.id, c]));
// ...
const currency = currencyMap.get(bal.currency_id);
```

### MED-3. 컨트랙트 교체 시 기존 PAUSE ✅

**파일**: `blockchain-api/src/services/ContractDeployService.ts:77-84`

```typescript
// Before — deprecate만 수행, 기존 컨트랙트로 TX 유입 가능
await relayerContractRepo.deprecateByNetwork(networkId);

// After — PAUSE 먼저 수행하여 TX 유입 차단 후 deprecate
try {
  await pauseContract(networkId, 'Replaced by new deployment');
} catch (pauseError) {
  logger.warn('Failed to pause (may already be paused)');
}
await relayerContractRepo.deprecateByNetwork(networkId);
```

### MED-4. addRelayer TX 실패 시 상태 롤백 ✅

**파일**: `blockchain-api/src/services/ContractDeployService.ts:170-190`

```typescript
// Before — tx.wait() 실패 시 REGISTERING에 멈춤
await tx.wait();

// After — try/catch로 실패 시 INACTIVE 롤백
try {
  await tx.wait();
} catch (txError) {
  await relayerWalletRepo.updateStatus(relayerWalletId, 'INACTIVE');
  throw txError;
}
```

### MED-5. 가스 추정 하드코딩 — 보류 ⏸️

**파일**: `common/src/chain/EvmProvider.ts:126-133`

**보류 사유**: `estimateApproveGas()`는 가스비 전송량 계산용으로, 실제 approve TX 실행 시점이 아닌 사전 계산 단계에서 호출됨. 실제 `estimateGas()`를 호출하려면 토큰 컨트랙트와 calldata가 필요한데, 이 시점에서는 사용 불가. 고정값(60,000) + 1.5배 여유분이 실무에서 더 안정적.

### MED-6. DepositRepo 비활성 통화 필터 ✅

**파일**: `common/src/db/repositories/DepositRepo.ts:34`

```sql
-- Before
JOIN currencies c ON c.id = d.currency_id

-- After
JOIN currencies c ON c.id = d.currency_id AND c.is_active = TRUE
```

### MED-7. Relayer 조회 O(n) → 직접 조회 ✅

**파일**: `relayer-api/src/routes/relayer.ts`, `common/src/db/repositories/RelayerWalletRepo.ts`

변경 내용:
1. `findByWalletAddressId()` 메서드 추가 (RelayerWalletRepo)
2. `POST /unregister` — `findAll() + find()` → `findById()` 직접 조회
3. `GET /status/:walletId` — `findAll() + find()` → `findByWalletAddressId()` 직접 조회

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

**파일**: `blockchain-api/src/app.ts:63`, `relayer-api/src/app.ts:60`

```typescript
// Before — TRON은 chain_id NULL → 0 → EVM Provider 에러
chainId: Number(row.chain_id) || 0

// After — TVM은 chainId undefined (EVM만 필요)
chainId: row.network_type === 'EVM' ? Number(row.chain_id) : undefined
```

---

## LOW — 운영 개선 (3건 → 3건 수정)

### LOW-1. NonceManager Redis Lock ID 정확한 전달 ✅

**파일**: `common/src/nonce/NonceManager.ts`, `relayer-api/src/services/{CollectionPoller,WithdrawalPoller}.ts`

변경 내용:
1. `confirmNonce()`, `releaseNonce()` 시그니처에 `lockId: string` 파라미터 추가
2. `releaseLock()`에 실제 `lockId` 전달 (기존: 빈 문자열)
3. 호출자 4곳 (`CollectionPoller`, `WithdrawalPoller`)에서 `acquired.lockId` 전달

```typescript
// Before — 빈 문자열로 락 해제 (다른 프로세스 락을 잘못 해제할 수 있음)
await releaseLock(`nonce:lock:${walletAddressId}`, '');

// After — 원본 lockId로 원자적 해제
await releaseLock(`nonce:lock:${walletAddressId}`, lockId);
```

### LOW-2. 폴링 에러 시 지수 백오프 ✅

**파일**: `relayer-api/src/services/{CollectionPoller,WithdrawalPoller}.ts`

```typescript
// 연속 에러 시: 3s → 6s → 12s → 24s → ... → 최대 60s
const backoff = this.consecutiveErrors > 0
  ? Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors - 1), this.maxBackoffMs)
  : this.pollIntervalMs;
```

- 성공 시 `consecutiveErrors = 0`으로 리셋
- DB 장애 등 지속적 에러 시 시스템 부하 방지

### LOW-3. Graceful Shutdown 타임아웃 환경변수 ✅

**파일**: `relayer-api/src/app.ts:176-179`

```typescript
// Before
setTimeout(() => { ... }, 60_000);

// After — 환경변수로 설정 가능
const shutdownTimeoutMs = Number(process.env.SHUTDOWN_TIMEOUT_MS) || 60_000;
setTimeout(() => { ... }, shutdownTimeoutMs);
```

---

## 변경 파일 목록 (16개)

| 패키지 | 파일 | 변경 유형 |
|--------|------|----------|
| common | `src/db/repositories/PartnerRepo.ts` | PHANTOM-1 |
| common | `src/types/tx.ts` | PHANTOM-2 |
| common | `src/db/repositories/WithdrawalRepo.ts` | SEC-1, SEC-3 |
| common | `src/db/repositories/DepositRepo.ts` | SEC-2, MED-6 |
| common | `src/db/repositories/WalletBalanceRepo.ts` | SEC-2 |
| common | `src/db/repositories/RelayerWalletRepo.ts` | MED-7 |
| common | `src/chain/EvmProvider.ts` | MED-5 (주석만) |
| common | `src/nonce/NonceManager.ts` | LOW-1 |
| blockchain-api | `src/routes/balance.ts` | MED-1 |
| blockchain-api | `src/routes/tx.ts` | MED-1 |
| blockchain-api | `src/routes/gas.ts` | MED-1 |
| blockchain-api | `src/routes/wallet.ts` | MED-1 |
| blockchain-api | `src/services/BalanceService.ts` | MED-2 |
| blockchain-api | `src/services/ContractDeployService.ts` | MED-3, MED-4 |
| blockchain-api | `src/app.ts` | MED-8 |
| relayer-api | `src/routes/relayer.ts` | MED-7 |
| relayer-api | `src/services/CollectionPoller.ts` | LOW-1, LOW-2 |
| relayer-api | `src/services/WithdrawalPoller.ts` | LOW-1, LOW-2 |
| relayer-api | `src/app.ts` | MED-8, LOW-3 |
| wallet-activator | `src/services/ApprovalProcessor.ts` | BUG-2 |
| telegram-bot | `src/handlers/index.ts` | SEC-2, SEC-3 |
