# 2026-03-18 차트/가격 불일치 에러 분석

## 문제 현상

1. **차트에 표시되는 가격과 게임 결과 가격이 다름**
   - 차트: DOWN으로 보임
   - 결과: UP으로 처리됨

2. **시작가/종료가 불일치**
   - BTC 서버 시작가: 73809.23
   - DB 시작가: 73835.85
   - 결과 가격도 불일치

3. **종료가격이 "로딩중..."으로 표시**

---

## 원인 분석

### 1. 두 개의 서로 다른 가격 소스

```
btc-server (포트 9090)
├── Binance에서 데이터 수신
├── 가격 계산 (noise 적용)
├── 자체 WebSocket 클라이언트에 브로드캐스트
└── /chart/start, /chart/result로 게임서버에 알림

socket/server.js (포트 8081)
├── Binance에서 별도로 데이터 수신 ← 문제!
├── 독자적으로 가격 계산
├── /price-ws로 차트에 브로드캐스트
└── 게임 결과는 btc-server 가격 사용
```

**결과**: 차트는 socket/server.js 가격, 게임 결과는 btc-server 가격 → 불일치

### 2. 라운드 ID 형식 불일치

- **btc-server**: `1, 2, 3, 4...` (서버 시작 기준 상대값)
- **socket/server.js**: `202603182068` (YYYYMMDD + 4자리)

btc-server가 보내는 라운드와 socket/server.js가 처리하는 라운드가 매칭 안됨

### 3. Apache 프록시 설정

```apache
# /price-ws → 9090 (btc-server) 로 설정되어 있음
ProxyPass /price-ws http://127.0.0.1:9090/
```

설정은 맞지만 socket/server.js가 자체 가격 계산을 하면서 충돌 발생

---

## 수정 내역

### 1. socket/server.js - /chart/start, /chart/result 엔드포인트 추가

```javascript
// /chart/start 엔드포인트 (BTC 차트서버에서 시작가 수신)
if (req.url === '/chart/start' && req.method === 'POST') {
    // timestamp 기반으로 정확한 라운드 ID 계산
    const gameRoundId = calculateRoundId(Math.floor(timestamp / 1000));
    await lockBettingRoundsWithChartPrice(gameRoundId, startPrice);
}

// /chart/result 엔드포인트 (BTC 차트서버에서 결과 수신)
if (req.url === '/chart/result' && req.method === 'POST') {
    // startTimestamp 기반으로 정산할 라운드 ID 계산
    const settleRoundId = calculateRoundId(Math.floor(startTimestamp / 1000));

    // btc-server의 result를 신뢰하지 않고 직접 계산
    let result;
    if (endPrice > startPrice) result = 'UP';
    else if (endPrice < startPrice) result = 'DOWN';
    else result = 'DRAW';

    await settleRoundWithChartResult(settleRoundId, endPrice, result);
}
```

### 2. 로컬 바이낸스 기반 lock/settle 비활성화

```javascript
// ⛔ 비활성화: btc-server에서 /chart/start로 전송받음
// lockBettingRoundsWithChartPrice(gameRoundId, noisyPrice)

// ⛔ 비활성화: btc-server에서 /chart/result로 전송받음
// settleRoundWithChartResult(resultGameRoundId, finalPrice, result)
```

### 3. fallbackSettleRound - 결과 재계산 로직 추가

```javascript
// DB start_price와 endPrice를 기준으로 결과 재계산
let correctedResult;
if (endPrice > dbStartPrice) correctedResult = 'UP';
else if (endPrice < dbStartPrice) correctedResult = 'DOWN';
else correctedResult = 'DRAW';

if (correctedResult !== result.toUpperCase()) {
    console.log(`[Round] ⚠️ Result mismatch! passed=${result}, corrected=${correctedResult}`);
}
```

### 4. fallbackLockRound - start_price 덮어쓰기

```javascript
// 변경 전: 기존 값 유지
start_price = COALESCE(NULLIF(start_price, 0), ?)

// 변경 후: 차트 가격으로 무조건 덮어씀
start_price = ?
```

### 5. btc/js/script.js - WebSocket 연결 대상

```javascript
// btc-server (9090)로 프록시됨 via /price-ws
socket = new WebSocket(`${wsProtocol}//${wsHost}/price-ws`);
```

---

## 아키텍처 정리

### 수정 전 (문제 상태)
```
Binance ─┬→ btc-server (9090) ─→ 게임 결과 계산
         │
         └→ socket/server.js (8081) ─→ 차트 표시

         ※ 서로 다른 가격 계산 → 불일치
```

### 수정 후 (정상 상태)
```
Binance → btc-server (9090) ─┬→ /price-ws → 차트 표시
                             │
                             └→ /chart/start, /chart/result → socket/server.js → 게임 결과

         ※ 단일 가격 소스 → 일치
```

---

## 확인 사항

1. **Apache 재시작 필요** - 프록시 설정 적용
2. **socket/server.js 재시작 필요** - 엔드포인트 추가 적용
3. **btc-server 재시작 필요** - 종료가격 캡처 로직 적용
4. **브라우저 캐시 삭제 (Ctrl+F5)** - script.js 변경사항 적용

---

## 관련 파일

- `socket/server.js` - 게임 서버 (lock/settle 처리)
- `btc-server/server.js` - BTC 차트 서버 (가격 계산, 브로드캐스트)
- `btc/js/script.js` - 차트 클라이언트 (WebSocket 연결)
- `includes/Betting.php` - PHP 배팅 시스템
- `C:/laragon/etc/apache2/sites-enabled/bitupgo.conf` - Apache 프록시 설정

---

## 추가 수정 (2026-03-18 오후)

### 6. btc/js/script.js - 종료가격 표시 수정

**문제**: 종료가격이 고정값(73996.54)으로 표시됨

**원인**: `displayEndPrice = markerPrice` (애니메이션 값 사용)

```javascript
// 변경 전
displayEndPrice = markerPrice;

// 변경 후 - 서버 결과값 우선 사용
displayEndPrice = syncedResultPrice ?? prevResultPrice ?? markerPrice;
```

### 7. btc/js/script.js - 시작가격 표시 수정

**문제**: 시작점 마커가 잘못된 위치에 표시됨

```javascript
// 변경 전
displayStartPrice = markerPrice;

// 변경 후 - 서버에서 받은 시작가 사용
displayStartPrice = committedStartPrice ?? markerPrice;
```

### 8. btc/js/script.js - 마커 위치 차트 데이터 기반으로 수정

**문제**: 시작점/종료점 마커가 실제 차트 선(stroke) 위에 있지 않음

**원인**: 서버 가격과 차트에 그려진 데이터 포인트 간 불일치

```javascript
// 헬퍼 함수 추가 - 차트 데이터에서 특정 시간의 Y값 찾기
function findYAtTime(targetTime) {
    const data = chart.data.datasets[1]?.data;
    if (!data || data.length < 2) return null;

    // 이진 탐색으로 가장 가까운 포인트 찾기
    let left = 0, right = data.length - 1;
    while (left < right - 1) {
        const mid = Math.floor((left + right) / 2);
        if (data[mid].x < targetTime) left = mid;
        else right = mid;
    }

    // 두 포인트 사이 선형 보간
    const p1 = data[left], p2 = data[right];
    const t = (targetTime - p1.x) / (p2.x - p1.x);
    return p1.y + (p2.y - p1.y) * Math.max(0, Math.min(1, t));
}

// 마커 위치 계산 시 차트 데이터 우선 사용
const chartYAtStart = findYAtTime(window.startPriceTime);
const actualY = chartYAtStart ?? displayStartPrice;

const chartYAtEnd = findYAtTime(window.endPriceTime);
const actualEndY = chartYAtEnd ?? displayEndPrice;
```

### 9. btc-server/server.js - 종료가격 정확한 시점 캡처

**문제**: 종료가격이 정확한 시점(0초/30초)이 아닌 다른 시점의 값 사용

**원인**: `RESULT_SECONDS: [0, 1, 2, 30, 31, 32]` 범위 내 아무 때나 `lastNoisyPrice` 사용

```javascript
// 새 Map 추가
const capturedEndPrices = new Map();  // 정확히 0/30초에 캡처된 종료가격

// 정확히 0초/30초 (종료 깃발 시점)에 종료가격 캡처
const isExactEndSecond = (seconds === 0 || seconds === 30);
if (isExactEndSecond && !capturedEndPrices.has(resultRoundId) && noisyValid) {
    capturedEndPrices.set(resultRoundId, lastNoisyPrice);
    console.log(`[캡처] ${resultRoundId}회차 종료가격: ${lastNoisyPrice} (${seconds}초)`);
}

// 결과 계산에서 캡처된 가격 사용 (없으면 현재 가격 폴백)
const capturedPrice = capturedEndPrices.get(resultRoundId) ?? lastNoisyPrice;
let finalPrice = preventDraw(start, capturedPrice, resultRoundId);
```

**메모리 정리**: `cleanupOldRounds()`에 `capturedEndPrices` 정리 추가

---

## 게임 타이밍 정리

| 초 | 이벤트 | 관련 상수 |
|----|--------|-----------|
| 25초 | 시작 깃발 (시작가 캡처) | `COMMIT_SECONDS` |
| 30초 | 종료 깃발 (종료가 캡처) | `RESULT_SECONDS` |
| 55초 | 시작 깃발 (시작가 캡처) | `COMMIT_SECONDS` |
| 00초 | 종료 깃발 (종료가 캡처) | `RESULT_SECONDS` |

- 게임 시간: 5초 (25→30, 55→00)
- 시작가: `COMMIT_SECONDS: [25, 55]`
- 종료가: 0초, 30초 (정확히 이 시점에 캡처)
- 결과 처리: `RESULT_SECONDS: [0, 1, 2, 30, 31, 32]` (캡처 후 처리 여유)

---

## 추가 수정 필요 사항 (미완료)

1. socket/server.js의 자체 Binance 연결 완전 제거 검토
2. btc-server와 socket/server.js 간 통신 안정성 강화
3. 에러 발생 시 폴백 로직 점검
