fix(web): 상담세션 라이브 연결 — start session_id를 turn에 사용(liveSessionId)

- 버그: handleStart가 sessionApi.start 결과 session_id 미저장, handleSend가
  URL :sessionId(임의값)로 turn 호출 → 백엔드 세션 불일치 → mock 폴백
- 수정: liveSessionId state에 start 결과 저장, turn/consume이 이걸 사용
- consumeClientReply: 동기 client_reply 있으면 SSE 스킵하고 타이핑 표시
- persona_code seoyeon→P1(시드 페르소나)
- puppeteer 실화면 검증: 회기시작→말걸기→서연 실응답 확인
This commit is contained in:
Yun Chan 2026-06-26 00:24:18 +09:00
parent c57c34ade6
commit 18b8ef3ef2
2 changed files with 18 additions and 6 deletions

1
.gitignore vendored
View file

@ -44,3 +44,4 @@ postgres-data/
*.out *.out
apps/api/gateway.restart.* apps/api/gateway.restart.*
apps/api/e2e_*.py apps/api/e2e_*.py
apps/web/_pptr_check.cjs

View file

@ -144,6 +144,8 @@ export default function Session() {
// ── 세션 진행 상태 ── // ── 세션 진행 상태 ──
const [started, setStarted] = useState(false); const [started, setStarted] = useState(false);
// start 가 백엔드에서 발급한 진짜 세션 id (turn/stream 은 이걸 써야 함. URL :sessionId 는 라우팅용)
const [liveSessionId, setLiveSessionId] = useState<string | null>(null);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [startError, setStartError] = useState<string | null>(null); const [startError, setStartError] = useState<string | null>(null);
@ -272,8 +274,9 @@ export default function Session() {
setStarting(true); setStarting(true);
setStartError(null); setStartError(null);
try { try {
// persona_code 는 데모 고정. 백엔드 연결 시 케이스 라우팅으로 대체. // persona_code 는 데모 고정(P1 시드 페르소나=서연). 백엔드 연결 시 케이스 라우팅으로 대체.
const res = await sessionApi.start("seoyeon", "humanistic"); const res = await sessionApi.start("P1", "humanistic");
setLiveSessionId(res.session_id); // ★ 진짜 세션 id 저장 — turn 이 이걸 써야 라이브
setStage(res.stage); setStage(res.stage);
} catch { } catch {
// 백엔드 미가동/스텁 → 데모 모드(에러로 막지 않음). 입력 가능한 데모 동작 유지. // 백엔드 미가동/스텁 → 데모 모드(에러로 막지 않음). 입력 가능한 데모 동작 유지.
@ -345,7 +348,13 @@ export default function Session() {
setOpenness((o) => clamp01(o + 0.04)); setOpenness((o) => clamp01(o + 0.04));
}; };
// SSE 시도 // 백엔드 turn 이 동기 client_reply(서연 실응답)를 줬으면 그걸 타이핑으로 표시(실서버 우선).
if (seedText) {
demoType(seedText, id, finalize);
return;
}
// (client_reply 없을 때만) SSE 시도 → 실패 시 mock 폴백
try { try {
streamRef.current?.close(); streamRef.current?.close();
streamRef.current = openSessionStream(sessionId, { streamRef.current = openSessionStream(sessionId, {
@ -398,7 +407,9 @@ export default function Session() {
let clientReply: string | null = null; let clientReply: string | null = null;
try { try {
const res = await sessionApi.turn(sessionId, text); // start 가 발급한 진짜 세션 id 로 turn (없으면 데모 폴백). URL :sessionId 아님.
if (!liveSessionId) throw new Error("no live session");
const res = await sessionApi.turn(liveSessionId, text);
setStage(res.stage); setStage(res.stage);
setOpenness(res.effective_openness); setOpenness(res.effective_openness);
clientReply = res.client_reply; clientReply = res.client_reply;
@ -406,7 +417,7 @@ export default function Session() {
setSafety("방금 발화에서 위기 신호가 감지됐어요. 안전을 우선해 머물러 주세요."); setSafety("방금 발화에서 위기 신호가 감지됐어요. 안전을 우선해 머물러 주세요.");
} }
} catch { } catch {
// 백엔드 미가동 → 데모 응답(끊김 방지) // 백엔드 미가동/세션없음 → 데모 응답(끊김 방지)
clientReply = null; clientReply = null;
} finally { } finally {
setSending(false); setSending(false);
@ -414,7 +425,7 @@ export default function Session() {
// 잠깐 'thinking' 후 내담자 응답 스트림 // 잠깐 'thinking' 후 내담자 응답 스트림
window.setTimeout(() => consumeClientReply(clientReply), 420); window.setTimeout(() => consumeClientReply(clientReply), 420);
}, [composeText, sending, paused, elapsed, sessionId, consumeClientReply]); }, [composeText, sending, paused, elapsed, liveSessionId, consumeClientReply]);
const onComposeKey = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => { const onComposeKey = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {