> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajuapi.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 이벤트 스트림 (SSE)

<Info>
  **v1 Enterprise API (Coming Soon)**

  이 엔드포인트는 Enterprise 버전에서 제공될 예정입니다.
  현재는 [v0 API](/api-reference/v0/overview)를 사용하세요.
</Info>

Server-Sent Events(SSE)를 통해 실시간 이벤트를 수신합니다. 웹훅의 대안으로, 클라이언트에서 직접 이벤트를 구독할 수 있습니다.

<Note>
  SSE는 단방향 스트리밍입니다. 양방향 통신이 필요하면 웹훅을 사용하세요.
</Note>

***

## Query 파라미터

<ParamField query="events" type="string">
  수신할 이벤트 유형입니다. 쉼표로 구분합니다. 생략하면 모든 이벤트를 수신합니다.
</ParamField>

<ParamField query="profile_id" type="string">
  특정 프로필의 이벤트만 수신합니다.
</ParamField>

<ParamField query="last_event_id" type="string">
  마지막으로 수신한 이벤트 ID입니다. 연결이 끊어진 후 재연결 시 사용합니다.
</ParamField>

***

## Response

### 성공

연결에 성공하면 SSE 스트림이 시작됩니다. 이벤트는 `text/event-stream` 형식으로 전송됩니다.

### 실패

| 상태 코드 | 에러 타입                  | 설명             |
| ----- | ---------------------- | -------------- |
| 401   | `authentication_error` | API 키가 유효하지 않음 |
| 429   | `rate_limited`         | 동시 연결 수 초과     |

***

## 요청 예시

<CodeGroup>
  ```bash cURL theme={null}
  # 모든 이벤트 수신
  curl -N -H "X-API-Key: bs_live_xxx" \
    "https://api.sajuapi.dev/v1/events"

  # 특정 이벤트만 수신
  curl -N -H "X-API-Key: bs_live_xxx" \
    "https://api.sajuapi.dev/v1/events?events=fortune.generated,fortune.cached"

  # 특정 프로필 이벤트만 수신
  curl -N -H "X-API-Key: bs_live_xxx" \
    "https://api.sajuapi.dev/v1/events?profile_id=prf_abc123def456"
  ```

  ```javascript JavaScript theme={null}
  // EventSource 사용
  const eventSource = new EventSource(
    'https://api.sajuapi.dev/v1/events?events=fortune.generated',
    {
      headers: {
        'X-API-Key': 'bs_live_xxx'
      }
    }
  );

  eventSource.onopen = () => {
    console.log('연결됨');
  };

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('이벤트 수신:', data);
  };

  eventSource.addEventListener('fortune.generated', (event) => {
    const fortune = JSON.parse(event.data);
    console.log('운세 생성됨:', fortune.fortune_id);
  });

  eventSource.onerror = (error) => {
    console.error('SSE 에러:', error);
    // 자동 재연결 시도됨
  };

  // 연결 종료
  // eventSource.close();
  ```

  ```python Python theme={null}
  import requests
  import json

  def stream_events(api_key, events=None):
      params = {}
      if events:
          params['events'] = ','.join(events)

      response = requests.get(
          'https://api.sajuapi.dev/v1/events',
          headers={'X-API-Key': api_key},
          params=params,
          stream=True
      )

      for line in response.iter_lines():
          if line:
              line = line.decode('utf-8')
              if line.startswith('data:'):
                  data = json.loads(line[5:].strip())
                  yield data

  # 사용 예시
  for event in stream_events('bs_live_xxx', ['fortune.generated']):
      print(f"이벤트: {event['type']}")
  ```
</CodeGroup>

***

## 이벤트 형식

SSE 이벤트는 다음 형식으로 전송됩니다.

```
event: fortune.generated
id: evt_abc123def456
data: {"type":"fortune.generated","fortune_id":"ftn_xyz789","profile_id":"prf_abc123","score":85,"timestamp":"2025-01-16T09:00:00Z"}

event: heartbeat
id: hb_1705395600
data: {"type":"heartbeat","timestamp":"2025-01-16T09:00:00Z"}
```

***

## 이벤트 유형

| 이벤트                 | 설명            | 데이터                                          |
| ------------------- | ------------- | -------------------------------------------- |
| `fortune.generated` | 운세가 생성됨       | `fortune_id`, `profile_id`, `score`, `model` |
| `fortune.cached`    | 캐시된 운세 반환     | `fortune_id`, `profile_id`                   |
| `profile.created`   | 프로필이 생성됨      | `profile_id`, `day_master`                   |
| `profile.updated`   | 프로필이 수정됨      | `profile_id`                                 |
| `profile.deleted`   | 프로필이 삭제됨      | `profile_id`                                 |
| `daily.reset`       | 일일 운세 리셋 (자정) | `date`, `profiles_count`                     |
| `heartbeat`         | 연결 유지 확인      | `timestamp`                                  |

***

## 응답 예시

### fortune.generated

```json theme={null}
{
  "type": "fortune.generated",
  "fortune_id": "ftn_abc123def456",
  "profile_id": "prf_xyz789abc123",
  "fortune_type": "daily",
  "fortune_date": "2025-01-16",
  "score": 85,
  "model": "haiku",
  "cached": false,
  "timestamp": "2025-01-16T09:00:00Z"
}
```

### daily.reset

```json theme={null}
{
  "type": "daily.reset",
  "date": "2025-01-17",
  "profiles_count": 5420,
  "cache_cleared": true,
  "timestamp": "2025-01-17T00:00:00+09:00"
}
```

### heartbeat

```json theme={null}
{
  "type": "heartbeat",
  "timestamp": "2025-01-16T09:05:00Z"
}
```

***

## React 통합 예시

```jsx theme={null}
import { useEffect, useState } from 'react';

function useEventStream(apiKey, events = []) {
  const [lastEvent, setLastEvent] = useState(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    const params = new URLSearchParams();
    if (events.length > 0) {
      params.set('events', events.join(','));
    }

    const eventSource = new EventSource(
      `https://api.sajuapi.dev/v1/events?${params}`,
      { headers: { 'X-API-Key': apiKey } }
    );

    eventSource.onopen = () => setConnected(true);
    eventSource.onerror = () => setConnected(false);

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type !== 'heartbeat') {
        setLastEvent(data);
      }
    };

    return () => eventSource.close();
  }, [apiKey, events.join(',')]);

  return { lastEvent, connected };
}

// 사용 예시
function FortuneMonitor() {
  const { lastEvent, connected } = useEventStream(
    'bs_live_xxx',
    ['fortune.generated']
  );

  return (
    <div>
      <div>상태: {connected ? '연결됨' : '연결 끊김'}</div>
      {lastEvent && (
        <div>
          최근 이벤트: {lastEvent.type} - {lastEvent.fortune_id}
        </div>
      )}
    </div>
  );
}
```

***

## 재연결 처리

연결이 끊어지면 `Last-Event-ID` 헤더를 사용하여 누락된 이벤트를 복구할 수 있습니다.

```javascript theme={null}
let lastEventId = null;

function connect() {
  const url = new URL('https://api.sajuapi.dev/v1/events');
  if (lastEventId) {
    url.searchParams.set('last_event_id', lastEventId);
  }

  const eventSource = new EventSource(url, {
    headers: { 'X-API-Key': 'bs_live_xxx' }
  });

  eventSource.onmessage = (event) => {
    lastEventId = event.lastEventId;
    // 이벤트 처리
  };

  eventSource.onerror = () => {
    eventSource.close();
    // 5초 후 재연결
    setTimeout(connect, 5000);
  };
}
```

<Warning>
  동시 SSE 연결 수는 API 키당 5개로 제한됩니다. 초과 시 가장 오래된 연결이 종료됩니다.
</Warning>

***

## Heartbeat

서버는 30초마다 `heartbeat` 이벤트를 전송하여 연결을 유지합니다. 60초 이상 이벤트가 없으면 연결이 끊어진 것으로 간주하고 재연결하세요.

<Note>
  이벤트는 최대 5분간 버퍼링됩니다. 5분 이상 연결이 끊어지면 일부 이벤트가 손실될 수 있습니다. 중요한 이벤트는 웹훅을 사용하세요.
</Note>
