curl --request POST \
--url https://sajuapi.dev/api/daily-fortune-stream \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"saju": {},
"userName": "<string>",
"model": "<string>"
}
'Server-Sent Events를 사용한 실시간 운세 데이터 스트리밍
curl --request POST \
--url https://sajuapi.dev/api/daily-fortune-stream \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"saju": {},
"userName": "<string>",
"model": "<string>"
}
'haiku, sonnet, gpt4oevent: character
data: {"dayMaster":"병화","emoji":"☀️",...}
event: tendency
data: {"scores":{"riskTolerance":65,...},...}
event: fortune
data: {"score":78,"actionItems":[...],...}
event: complete
data: {"cached":false,"cost":0.013}
| 이벤트 | 설명 | 타이밍 |
|---|---|---|
character | 성격 분석 완료 | ~1초 |
tendency | 투자 성향 완료 | ~2초 |
fortune | 일일 운세 완료 | ~3초 |
complete | 모든 데이터 수신 완료 | ~3-5초 |
error | 에러 발생 | 상황에 따름 |
function streamFortune(saju, userName, onUpdate) {
return new Promise((resolve, reject) => {
const result = {};
// 참고: EventSource는 GET만 지원하므로 fetch와 ReadableStream 사용
fetch('https://api.sajuapi.dev/api/daily-fortune-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'bs_live_xxx',
'Accept': 'text/event-stream'
},
body: JSON.stringify({ saju, userName, model: 'haiku' })
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
resolve(result);
return;
}
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('event: ')) {
const eventType = line.slice(7);
// 다음 줄이 데이터
}
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
result[eventType] = data;
onUpdate(eventType, data);
}
}
read();
});
}
read();
}).catch(reject);
});
}
// 사용법
streamFortune(saju, '홍길동', (type, data) => {
switch (type) {
case 'character':
setCharacter(data);
break;
case 'tendency':
setTendency(data);
break;
case 'fortune':
setFortune(data);
break;
case 'complete':
setLoading(false);
break;
}
});
import { useState, useEffect } from 'react';
function useFortuneStream(saju: Saju, userName: string) {
const [character, setCharacter] = useState(null);
const [tendency, setTendency] = useState(null);
const [fortune, setFortune] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function stream() {
try {
const response = await fetch('/api/daily-fortune-stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream'
},
body: JSON.stringify({ saju, userName }),
signal: controller.signal
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let eventType = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split('\n')) {
if (line.startsWith('event: ')) {
eventType = line.slice(7);
} else if (line.startsWith('data: ') && eventType) {
const data = JSON.parse(line.slice(6));
switch (eventType) {
case 'character': setCharacter(data); break;
case 'tendency': setTendency(data); break;
case 'fortune': setFortune(data); break;
case 'complete': setIsLoading(false); break;
case 'error': setError(data.message); break;
}
}
}
}
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
}
}
stream();
return () => controller.abort();
}, [saju, userName]);
return { character, tendency, fortune, isLoading, error };
}
function FortuneCard() {
const { character, tendency, fortune, isLoading } = useFortuneStream(saju, userName);
return (
<div>
{/* 성격 섹션 - 먼저 표시 */}
<Section title="나의 투자 성격">
{character ? (
<CharacterDisplay data={character} />
) : (
<Skeleton height={100} />
)}
</Section>
{/* 성향 섹션 - 두 번째로 표시 */}
<Section title="투자 성향">
{tendency ? (
<TendencyChart data={tendency} />
) : (
<Skeleton height={150} />
)}
</Section>
{/* 운세 섹션 - 마지막으로 표시 */}
<Section title="오늘의 운세">
{fortune ? (
<FortuneDisplay data={fortune} />
) : (
<Skeleton height={200} />
)}
</Section>
</div>
);
}
event: error
data: {"code":"ai_provider_error","message":"AI 서비스 일시적 불가"}
if (eventType === 'error') {
const { code, message } = JSON.parse(data);
// 일반 엔드포인트로 폴백
const fortune = await fetchRegularFortune(saju, userName);
}
| 항목 | 일반 | 스트리밍 |
|---|---|---|
| 첫 바이트까지 시간 | ~3초 | ~1초 |
| 전체 응답 | ~3초 | ~3초 |
| 체감 속도 | 느림 | 빠름 |
| 복잡도 | 간단 | 중간 |
| 캐싱 | 전체 응답 | 섹션별 |
| 요청 제한 | 60회/분 | 30회/분 |