# 2026-01-26 개발 로그

## 발견된 문제들

### 1. 라운드 결과 처리 안 되는 문제 ❌

**증상:**
```
[Manager] Round #12709 status changed: DISTRIBUTING -> COMPLETED
[Manager] Round #12709: COMPLETED, Countdown: 0s
[Manager] No active round found, starting new round...
```
- 라운드가 COMPLETED 상태로 변경됨
- `endRound()`가 실행되지 않아 결과 처리 안 됨

**원인:**
```php
// Game.class.php
public function calculateStatus($currentCount) {
    if ($currentCount >= 14) {
        return self::STATUS_BETTING;
    } elseif ($currentCount >= 4) {
        return self::STATUS_CALCULATING;
    } elseif ($currentCount >= 1) {
        return self::STATUS_DISTRIBUTING;
    } else {
        return self::STATUS_COMPLETED; // ← countdown 0에서 COMPLETED 반환
    }
}

// game-manager.php
if ($currentCount <= 0 && $round['status'] !== 'COMPLETED') {
    // ← status가 이미 COMPLETED이면 실행 안 됨!
    $result = $game->endRound($round['id'], $endPrice);
}
```

**해결:**
```php
// Game.class.php - calculateStatus() 수정
public function calculateStatus($currentCount) {
    if ($currentCount >= 14) {
        return self::STATUS_BETTING;
    } elseif ($currentCount >= 4) {
        return self::STATUS_CALCULATING;
    } else {
        // countdown 3~0: DISTRIBUTING 유지
        // COMPLETED는 endRound()에서만 설정
        return self::STATUS_DISTRIBUTING;
    }
}

// game-manager.php - 조건 수정
if ($currentCount <= 0 && $round['status'] === 'DISTRIBUTING') {
    // ← DISTRIBUTING 상태일 때만 실행
    $result = $game->endRound($round['id'], $endPrice);
}
```

---

### 2. 네트워크 지연으로 깃발 타이밍 안 맞는 문제 ⏱️

**증상:**
- 서버: 1ms 응답 시간
- 클라이언트: 43ms 응답 시간
- 각 클라이언트마다 다른 네트워크 지연 → 깃발 타이밍 다름

**원인:**
- `time-sync.ts`는 있지만 **실제로 사용하지 않음**
- `GameStateSyncService`에서 서버 timestamp를 받지만 `timeSync.sync()` 호출 안 함

**해결:**
```typescript
// GameStateSyncService.ts
private syncGameState(data: WSGameState): void {
    // 🔥 서버 시간 동기화 (네트워크 지연 보상)
    if (data.timestamp) {
        timeSync.sync(data.timestamp);
    }

    // ... rest of code
}

private handleBinancePrice(data: any): void {
    // 🔥 서버 시간 동기화 (네트워크 지연 보상)
    if (data.timestamp) {
        timeSync.sync(data.timestamp);
    }

    // ... rest of code
}
```

**결과:**
- 모든 클라이언트가 서버 시간 기준으로 동기화
- 네트워크 지연 자동 계산 및 보상

---

### 3. 소수점 4자리 표시 문제 🔢

**증상:**
```
시작가: $87465.22      ← 00 생략됨
종료가: $87465.2       ← 000 생략됨
```

**해결:**
```typescript
// format-price.ts (새로 생성)
export function formatPrice(price: number | null | undefined): string {
    if (price === null || price === undefined || isNaN(price)) {
        return '0.0000';
    }
    return price.toFixed(4); // 항상 4자리
}

export function formatPriceWithSymbol(price: number | null | undefined): string {
    return `$${formatPrice(price)}`;
}

// stastics.component.tsx
<span className="font-semibold text-gray-900 dark:text-white">
    {formatPriceWithSymbol(round.start_price)}
</span>
```

**서버 로그도 수정:**
```php
// game-manager.php
echo sprintf("[Manager] Round started! ID: %d, Price: %.4f\n", $roundId, $btcPrice);
echo sprintf("[Manager] Start: %.4f, End: %.4f, Result: %s\n",
    floatval($result['start_price']),
    floatval($result['end_price']),
    $result['result']);

// BinancePriceRelay.php
echo sprintf("[BinancePriceRelay] Price: %.4f, Broadcast to %d clients\n",
    $price,
    $this->gameWebSocket->getClientCount()
);
```

---

### 4. 차트가 Binance 직접 연결해서 다른 가격 표시되는 문제 🔥 (가장 중요!)

**증상:**
```
차트: 87841.9125 (Binance WebSocket 직접 연결)
서버: 87849.22 (서버 API 호출)
❌ 완전히 다른 가격!
```

**문제 분석:**
```
현재 상황:
├─ 게임 매니저 → getBTCPrice() → Binance API 직접 호출
├─ 차트 (클라이언트 A) → Binance WebSocket 직접 연결
├─ 차트 (클라이언트 B) → Binance WebSocket 직접 연결
└─ 모두 다른 시점, 다른 가격!

올바른 구조:
서버 BinancePriceRelay (0.5초마다)
    ↓
Binance에서 가격 가져옴
    ↓
파일에 저장 + WebSocket 브로드캐스트
    ↓
┌───────────┴────────────┐
│                        │
↓                        ↓
게임 매니저              모든 클라이언트 차트
(파일에서 읽음)          (WebSocket으로 받음)
```

**시도한 해결 방법들:**

#### 시도 1: protocol 제거
```typescript
// app.tsx
<Runtime config={{
    symbol: 'BTCUSDT',
    // protocol 제거
    autoStartGame: false,
}}>
```
**결과:** 차트 초기화 실패

#### 시도 2: protocol을 'websocket'으로 변경
```typescript
<Runtime config={{
    symbol: 'BTCUSDT',
    protocol: 'websocket',
}}>
```
**결과:** Binance API 404 에러

#### 시도 3: Binance Interceptor (Mock 응답)
```typescript
// binance-interceptor.ts
window.fetch = function(input, init) {
    if (url.includes('binance.com')) {
        // Mock 응답 반환
        if (url.includes('/klines')) return [];
        if (url.includes('/ping')) return {};
        if (url.includes('/price')) return {symbol: 'BTCUSDT', price: '87000.0000'};
    }
    return originalFetch(input, init);
};
```
**결과:** 초기화는 성공하지만 여전히 차트가 다르게 그려짐

#### 시도 4: 서버 가격 캐시 파일 시스템
```php
// BinancePriceRelay.php
private function savePriceToFile($price, $timestamp) {
    $data = [
        'price' => $price,
        'timestamp' => $timestamp,
        'updated_at' => date('Y-m-d H:i:s')
    ];
    file_put_contents($this->priceFile, json_encode($data), LOCK_EX);
}

public static function getLastPriceFromFile() {
    $priceFile = sys_get_temp_dir() . '/btc_price_cache.json';
    // 5초 이내 데이터만 유효
    $age = (microtime(true) * 1000) - $data['timestamp'];
    if ($age > 5000) return null;
    return floatval($data['price']);
}

// game-manager.php
function getBTCPrice() {
    // 먼저 캐시에서 가져오기
    $price = BinancePriceRelay::getLastPriceFromFile();
    if ($price !== null) return $price;

    // Fallback: 직접 호출
    // ...
}
```

#### 시도 5: Binance Proxy API
```php
// public/api/binance-proxy.php
if (stripos($action, 'price') !== false) {
    $price = BinancePriceRelay::getLastPriceFromFile();
    if ($price !== null) {
        echo json_encode([
            'symbol' => 'BTCUSDT',
            'price' => number_format($price, 4, '.', '')
        ]);
    }
}
```
**결과:** 404 에러 (서버 설정 문제)

#### 현재 시도 6: 개선된 Interceptor
```typescript
// binance-interceptor.ts
window.fetch = function(input, init) {
    if (url.includes('binance.com')) {
        if (url.includes('/klines')) {
            return Promise.resolve(new Response(JSON.stringify([]), {
                status: 200,
                headers: { 'Content-Type': 'application/json' }
            }));
        }
        if (url.includes('/exchangeInfo')) {
            return Promise.resolve(new Response(JSON.stringify({
                timezone: 'UTC',
                serverTime: Date.now(),
                symbols: [{
                    symbol: 'BTCUSDT',
                    status: 'TRADING',
                    baseAsset: 'BTC',
                    quoteAsset: 'USDT'
                }]
            }), {
                status: 200,
                headers: { 'Content-Type': 'application/json' }
            }));
        }
        // ... 기타 응답
    }
    return originalFetch(input, init);
};
```

**아직 해결 안 됨!** 차트 라이브러리가 여전히 다르게 그려짐.

---

### 5. WebSocket Ping Timeout 문제 🔌

**증상:**
```
[BackendWS] Ping timeout, reconnecting...
[BackendWS] Disconnected
[BackendWS] Reconnecting (1/5)...
```

**원인:**
- 클라이언트가 **pong만** 기다림
- 서버가 pong을 보내지만 타이밍이 안 맞음

**해결:**
```typescript
// BackendWebSocket.ts
private lastMessageTime: number = Date.now(); // pong뿐만 아니라 모든 메시지
private pingTimeout = 60000; // 60초 (매우 여유있게)

private handleMessage(data: any) {
    // 모든 메시지 수신 시 타임스탬프 업데이트
    this.lastMessageTime = Date.now();
    // ...
}

private startPing() {
    this.pingInterval = setInterval(() => {
        // 모든 메시지 체크 (game_state는 1초마다 오므로)
        const timeSinceLastMessage = Date.now() - this.lastMessageTime;

        if (timeSinceLastMessage > this.pingTimeout) {
            console.warn('[BackendWS] No server messages for 60s, reconnecting...');
            this.ws?.close();
            return;
        }

        this.send({ type: 'ping' });
    }, 20000); // 20초마다
}
```

**결과:** 연결 안정화

---

## 수정된 파일 목록

### Backend
1. `backend/includes/Game.class.php`
   - `calculateStatus()`: countdown 0에서 DISTRIBUTING 유지
   - 소수점 4자리 처리

2. `backend/websocket/game-manager.php`
   - `endRound()` 조건: `status === 'DISTRIBUTING'`
   - `getBTCPrice()`: 캐시 파일 우선 사용
   - 모든 가격 `sprintf("%.4f")` 포맷

3. `backend/websocket/BinancePriceRelay.php`
   - 가격을 파일에 저장: `savePriceToFile()`
   - 정적 메서드 추가: `getLastPriceFromFile()`
   - 가격 반올림: `round($price, 4)`

4. `backend/websocket/GameWebSocket.php`
   - ping/pong 핸들러 (이미 있었음)

5. `backend/api/binance-proxy.php` (새로 생성, 404 에러)
   - 캐시 가격 반환 API

### Frontend
1. `frontend/apps/up-down/src/app/app.tsx`
   - Binance interceptor 초기화
   - Chart data monitor 초기화
   - `protocol: 'websocket'` 설정

2. `frontend/apps/up-down/src/services/GameStateSyncService.ts`
   - `timeSync.sync()` 호출 추가
   - 네트워크 지연 로그

3. `frontend/apps/up-down/src/services/BackendWebSocket.ts`
   - `lastMessageTime` 추가
   - `pingTimeout` 60초로 증가
   - 모든 메시지로 활성 체크

4. `frontend/apps/up-down/src/utils/binance-interceptor.ts` (새로 생성)
   - Fetch API 가로채기
   - WebSocket Mock
   - Mock 응답 (exchangeInfo, klines, price, ping)

5. `frontend/apps/up-down/src/utils/chart-data-monitor.ts` (새로 생성)
   - 차트 데이터 소스 모니터링
   - 서버 데이터 수신 로그

6. `frontend/apps/up-down/src/utils/format-price.ts` (새로 생성)
   - `formatPrice()`: 항상 4자리
   - `formatPriceWithSymbol()`: $ 포함 4자리

7. `frontend/apps/up-down/src/components/stastics.component.tsx`
   - `formatPriceWithSymbol()` 사용

---

## 현재 상태

### ✅ 해결됨
1. 라운드 결과 처리 안 되는 문제
2. 네트워크 지연으로 깃발 타이밍 안 맞는 문제
3. 소수점 4자리 표시 문제
4. WebSocket ping timeout 문제

### ❌ 아직 해결 안 됨
1. **차트가 Binance 직접 연결해서 다른 가격 표시되는 문제 (가장 중요!)**
   - 여러 클라이언트에서 차트가 다르게 그려짐
   - Interceptor로 차단했지만 여전히 문제 발생

---

## 다음 시도할 방법

### 옵션 1: 차트 라이브러리 교체
- 현재 `@bc-game/chart` 라이브러리가 Binance 하드코딩
- Lightweight Charts 등 다른 차트 라이브러리로 교체
- 서버 WebSocket 데이터만 사용하도록 직접 구현

### 옵션 2: 프록시 서버 제대로 구현
- Nginx/Apache 설정으로 `/api/binance/*` → 프록시
- 모든 클라이언트가 프록시를 통해 동일한 캐시 데이터 받도록

### 옵션 3: 차트 컴포넌트 직접 구현
- `@bc-game/chart` 사용 중지
- Canvas 또는 SVG로 직접 차트 그리기
- 서버 WebSocket 데이터만 사용

---

## 로그 출력

### 서버 (일본)
```
[GameStateSync] 💰 Server Price: 87876.8100 | Time offset: -8ms
[GameStateSync] 💰 Server Price: 87868.0000 | Time offset: -8ms
[ChartDataMonitor] 📊 Chart receiving SERVER data: 87876.8100 | Updates: 1
```

### 클라이언트 (한국)
```
[GameStateSync] 💰 Server Price: 87876.8100 | Time offset: 15ms
[GameStateSync] 💰 Server Price: 87868.0100 | Time offset: 15ms
[ChartDataMonitor] 📊 Chart receiving SERVER data: 87876.8100 | Updates: 1
```

### 관찰
- 두 클라이언트 모두 서버 데이터 수신 ✅
- 네트워크 지연 보상 작동 ✅
- 하지만 차트는 여전히 다르게 그려짐 ❌

---

## 핵심 문제 요약

**사용자의 요구:**
> "그냥 바이낸스값을 내 서버에서 하나로 받아서 여러 클라이언트에 뿌려주면 되는거 아니냐고"

**현재 상황:**
- 서버는 이미 BinancePriceRelay로 0.5초마다 가격을 가져와서 브로드캐스트 중 ✅
- 클라이언트는 WebSocket으로 서버 가격을 받고 있음 ✅
- **하지만 차트 라이브러리가 내부적으로 Binance에 직접 연결** ❌

**근본 원인:**
- `@bc-game/chart` 라이브러리가 `protocol: 'binance'` 사용 시 Binance WebSocket에 직접 연결
- Interceptor로 차단해도 차트가 제대로 그려지지 않거나 여전히 다른 가격 표시

**필요한 것:**
- 차트가 서버 WebSocket 데이터(**exchange:tradeData** 이벤트)만 사용하도록 강제
- 또는 차트 라이브러리 교체
