Skip to content

FUTURE_CAR SPEC TABLET

ansunny1170 edited this page Aug 28, 2026 · 4 revisions

[tablet-fe 구현 스펙] 미래차 연동 — 2026-08-29판

  • 작성: 미래차 팀 · 2026-08-29 (2026-08-23판 전면 재작성 — 과거 내용은 폐기)
  • 대상 코드: tablet-fe 60d353c (main) · 미래차 서버 zer01ne-future-car-server main-2026 a1d3673 (조직 미러: zero1ne-26/future-car-server)
  • 계약 정본: 위키 MQTT_SCHEMA-FUTURE_CAR — 이 문서는 태블릿 개발자가 할 일만 다룬다. 토픽·필드 정의는 정본을 본다.
  • 미래차 서버 배포 상태: 2026-08-29 Mac Studio 배포 완료(서버 a1d3673·클라 51ba4a7, 둘 다 5cc571f/8e22f63 이후). §7 "배포 전후 차이"의 "배포 전" 동작은 더 이상 해당 없음 — 기록용으로만 남긴다.

요약: 태블릿이 미래차와 주고받는 코드는 이미 다 들어가 있다(enter/exit 발행, future-car/response 수신, 노선도). 2026-08-28 정책 변경으로 딱 하나가 새로 필요하다 — 차 안에 있는 동안 태블릿의 채팅 입력(텍스트·마이크·제안 칩)을 잠그는 것(§2-A). 나머지는 권장 사항과 확인 사항이다.


목차


0. 배경 — 미래차가 무엇이고 관람객에게 무슨 일이 일어나나

전시장에는 미래차(실물 차 + 차 안 디스플레이 + 마이크)가 한 대 있다. 관람객은 태블릿을 들고 여러 장소(집·공원·마트·차)를 돌며 태스크를 수행하는데, 그중 "차" 장소가 미래차다. 미래차 쪽 시스템은 둘이다.

구성 역할
미래차 서버 (Mac Studio :8100, FastAPI) MQTT 로 OC·태블릿과 통신. 관람객 발화를 재료로 LLM 이 "스텝"(차 안 연출 한 장면)을 4개 생성해 화면에 내려보내고, 태블릿 채팅에 질문을 띄우고, OC 에 태스크 시작·완료를 보고한다
미래차 화면 (/ambient, 차 안 디스플레이) 스텝을 재생하고, 재생이 끝나면 서버에 보고하고, 관람객 차례에 마이크를 열어 발화를 STT 해 서버로 보낸다

관람객이 겪는 순서 (태블릿 시점)

  1. 태블릿에서 "차로 이동"을 확정하면(TaskCompleteDialog) 태블릿이 enter 를 보낸다. 차 화면이 대기 화면으로 바뀌고, 태블릿 채팅에 "출발 할까요?" 와 답 후보 칩 2개가 뜬다.
  2. 관람객은 차에 대고 답한다("네, 출발해요"). 차 마이크가 받아 서버로 보내고, 서버가 스텝 1을 만든다(10~30초). 태블릿에는 이 구간(route)이 노선도에 그려진다.
  3. 차 화면이 스텝 1을 재생한다. 그동안 태블릿의 "차량 상태·주행 점검" 같은 basic 태스크 카드가 "진행 중" 이 된다.
  4. 재생이 끝나면 카드가 "완료" 로 바뀌고, 채팅에 다음 질문이 뜬다. 관람객은 다시 차에 대고 답한다.
  5. 2~4 가 스텝 4까지 반복된다. 스텝 4 재생이 끝나면 질문 없이 차 화면이 엔딩을 보여주고, 태블릿에는 하차 가능 신호(state.next="exit")가 온다.
  6. 태블릿에서 "다음 장소로 이동"을 확정하면 exit 를 보낸다. 차 화면은 작별 화면, 여정 종료.

2026-08-28 정책 변경 — 왜 태블릿 입력을 잠그나

이전 설계는 관람객이 태블릿 채팅에 답하면 그 답(ai/response.transcript)을 미래차가 구독해 재료로 썼다. 지금은 차 마이크로만 받는다. 이유는 둘이다.

  • 태블릿에 답하면 OC 의 AI 동반자가 따로 답을 한다(차 안 연출과 태블릿 채팅이 "따로 노는" 문제). OC 가 ai/mode=car 를 구현해 음성 발화에는 답을 생략하지만, 텍스트(칩 탭·타이핑)에는 여전히 답한다(ai_chat.go car 분기가 type=voice 안에만 있음).
  • 미래차 서버는 아직 ai/response.transcript 를 구독하고 있어서, 태블릿에서 보낸 문장도 스텝 재료로 섞여 들어간다.

그래서 차 안에 있는 동안(enter~exit) 태블릿은 채팅 입력을 받지 않는다. 미래차가 띄우는 질문·칩은 보여주기만 한다. 관람객이 "무슨 말을 하면 되는지" 힌트로 쓰라는 뜻이다.

태스크 규칙 (알아만 두면 되는 것)

  • 차 장소 태스크 중 type=basic 만 미래차가 수행한다 — 태블릿에 조작 UI 가 없어 서버가 완료해 줘야 하는 타입. 스텝마다 하나씩 AI 가 골라 스텝 시작에 task/start, 렌더 완료에 task/complete 를 OC 에 보낸다.
  • 그 외 타입(vehicle_ready·detailed·quote_briefing … 돋보기 모달)은 미래차가 건드리지 않는다. 지금처럼 관람객이 태블릿 모달에서 완료한다.

1. 현재 구현 상태 (60d353c 대조)

미래차 팀이 tablet-fe 코드를 읽고 확인한 것. 아래 항목은 추가 작업이 없다.

항목 위치 상태
future-car/response 구독 · SSE 중계 · state replay 캐시 server-bus.ts SESSION_SUFFIX.futureCar, handleFutureCarResponse(1014), bus.futureCarState(280) ✅
zod 스키마 4종 (state·route·error·step strip) schema.ts 995~1053 ✅
future-car-store (phase·step·canExit·route·segments·lastError) store/future-car-store.ts ✅
enter/exit 발행 — BFF 라우트 · 어댑터 · 가드 app/api/orchestration/future-car/route.ts, future-car-client.ts, task-complete-dialog.tsx 101~135 ✅
노선도(지나온 구간 누적) components/main/cctv/vehicle-route-view.tsx ✅
미래차 질문·칩 표시 chat-store.receiveAiResponse(모르는 context_id → AI 버블) · receiveSuggestions ✅ 기존 로직 그대로
signout 시 store·BFF 캐시 초기화 end-session.ts 80, server-bus.ts 357 ✅
미래차 모드 입력 잠금 chat-input.tsx, chat-thread.tsx, use-ai-chat.ts ⬜ 없음 → §2-A
하차 유도 UI (useFutureCarCanExit) future-car-store.ts 120 — 정의만 있고 미사용 ⬜ 권장 → §2-B

2. 추가 구현

A. (필수) 미래차 모드 입력 잠금

무엇을

future-car-store.phase 가 waiting · driving · arrived 중 하나인 동안(= enter 이후 exit 이전):

잠글 것 지금 코드 잠근 뒤
텍스트 입력창 chat-input.tsx:43 disabled = pending || sessionUuid === null 조건에 inCar 추가 → Input disabled
마이크(press-and-hold) 같은 disabled 가 usePushToTalk 로 전달됨 자동으로 함께 잠김
전송 버튼 canSend ← disabled 자동
제안 칩 탭 chat-thread.tsx:76 onPickSuggestion → sendText 칩은 표시하되 disabled(탭 무시)
프로그램적 전송 use-ai-chat.ts dispatch (sendText·sendSystemText 공용) inCar 면 return false — 어떤 경로로도 ai/request 가 나가지 않게 하는 최종 방어선

표시: 입력창 placeholder 를 "차량 안에서는 차에 직접 말씀해 주세요" 로. 칩은 회색으로 보이되 눌리지 않게(관람객이 "이렇게 답하면 되는구나" 힌트로 읽는다).

해제: phase 가 done(exit 뒤) 또는 idle(새 plan) 또는 null(state 미수신) 이면 잠그지 않는다.

왜 phase 로 판단하나 (장소 이름이 아니라)

  • 잠금의 근거는 "미래차 서버가 발화 수집 창을 열어 두고 있는가"이고, 그건 정확히 state.phase ∈ {waiting, driving, arrived} 다.
  • state 는 retained 라 새로고침·SSE 재연결 뒤에도 마지막 값이 즉시 복원된다(BFF futureCarState 캐시 + 브로커 retained 이중). 장소(activePhase.zone)로 판단하면 태블릿 단계와 차 상태가 어긋나는 순간(이동 확정 직후 등)에 구멍이 생긴다.
  • null(아직 state 를 못 받음)은 잠그지 않는다 — 미래차 서버가 죽어 있어도 태블릿 채팅은 살아 있어야 한다.

구현 위치 제안

// use-ai-chat.ts — dispatch 맨 앞
const carPhase = useFutureCarStore.getState().phase
if (carPhase === 'waiting' || carPhase === 'driving' || carPhase === 'arrived') return false

// chat-input.tsx
const carPhase = useFutureCarPhase()
const inCar = carPhase === 'waiting' || carPhase === 'driving' || carPhase === 'arrived'
const disabled = pending || sessionUuid === null || inCar
// placeholder: inCar ? '차량 안에서는 차에 직접 말씀해 주세요' : …

// chat-thread.tsx — 칩 버튼에 disabled={inCar}, onPickSuggestion 은 inCar 면 no-op

inCar 판정을 store 셀렉터(useFutureCarInCar())로 하나 두고 세 곳이 같이 쓰면 조건이 흩어지지 않는다.

엣지 케이스

상황 기대 동작
enter 발행 직후 state waiting 이 오기 전 0.1~1초 아직 안 잠김. 이 틈에 보낸 문장은 미래차 창이 이미 열려 있어 재료로 들어갈 수 있다 — 허용 범위(관람객이 그 순간 타이핑할 가능성 낮음). 원하면 enter 발행 시점에 낙관적으로 잠가도 된다
pending 상태(태블릿이 답을 기다리는 중)에서 enter pending 이 먼저 잠그고 있으므로 충돌 없음. car 모드에서 OC 가 message 없이 transcript 만 돌려줘도 receiveAiResponse 가 pending 을 푼다(빈 버블 안 생김)
exit 발행 → state done 즉시 해제. done 뒤 OC ai/mode=chat 도 함께 가므로 OC AI 답변이 정상 복귀
새로고침 retained state 로 phase 복원 → 잠금 복원
미래차 서버 재시작 state 는 DB 복원 후 재발행된다. 그 사이엔 마지막 retained 값이 유지
signout useFutureCarStore.reset() → phase null → 해제

B. (권장) 하차 유도

state · phase=arrived · next="exit" (스텝 4 렌더 완료) 가 오면 canExit === true 가 된다. 지금은 이 값을 아무 UI 도 읽지 않아 관람객이 스스로 "다음 장소로 이동"을 눌러야 한다. 차 화면은 이때 "체험이 모두 끝났습니다 / 뒷쪽 출구로 퇴장해 주세요" 를 보여주고 있으므로, 태블릿에도 같은 맥락의 안내("차에서 내려 다음 장소로 이동해 주세요" + 이동 확정으로 연결)를 띄우면 관람객이 헤매지 않는다. useFutureCarCanExit() 가 이미 있다. 어디에 띄울지는 태블릿 UX 결정이다(노선도 캡션, 태스크 패널 상단 배너 등).

C. (확인) 차량 태스크 카드

미래차 서버가 스텝마다 task/start(스텝 시작) → task/complete(렌더 완료) 를 OC 에 보내면 OC 가 dashboard 를 in_progress → completed 로 재발행한다. 태블릿의 basic 카드(task-panel.tsx STATUS_STYLE)는 이미 active(진행 중) 배지를 그리므로 코드 변경 없이 스텝 동안 "진행 중" → 끝나면 "완료" 로 보여야 한다. 실제로 그렇게 보이는지만 확인해 달라. non-basic 카드(돋보기)는 배지가 없고 미래차가 건드리지 않으므로 변화 없음.

D. (주의) 차 안에서 프로그램이 보내는 ai/request

use-task-summary.ts 는 dashboard 첫 수신 때 하루 요약을 ai/request type=text 로 보낸다. 이건 세션 초입(= enter 전)이라 미래차가 창 밖(closed)으로 버리므로 문제없다. 앞으로 차 안(waiting~arrived)에서 자동으로 ai/request 를 보내는 기능을 추가하지 말 것 — 미래차가 관람객 발화로 오인해 스텝을 생성한다. §2-A 의 dispatch 가드가 이걸 막는 최종 방어선이다.


3. 미래차 → 태블릿: 언제 무엇이 오고 무엇을 보여주나

토픽 orchestration/{sessionId}/future-car/response (QoS 1). 필드 정의는 정본 §5. 여기서는 시간 순으로 무엇이 오는지를 적는다.

시점 관람객 · 차 화면 도착하는 메시지 태블릿
페르소나 생성 직후 차와 무관 state {step:0, phase:"idle", prev_place?, next_place?} (retained) 새 여정. 구간·에러 비우기(구현됨). 잠금 아님
태블릿이 enter 발행 차 화면 대기. 마이크 열림 state {phase:"waiting"} → 이어서 채팅 토픽에 ai/response {message:"출발 할까요?"} + ai/suggestions ["네, 출발해요!","잠깐만요, 준비 좀 할게요"] 잠금 시작. 채팅에 질문·칩 표시(칩 비활성)
관람객이 차에 말함 → 생성 중 대기 화면 유지. "잠시만요" (없음, 10~30초) 변화 없음
생성 완료 스텝 재생 시작. 마이크 닫힘 route {step:1, from_stop_id?, from_place?, to_stop_id?, to_place?} → step {step:1, phase:"driving", data:{…}} 노선도 구간 표시. step.data 는 무시(스키마가 strip). basic 카드 "진행 중"(OC dashboard 경유)
재생 완료 마지막 장면 유지. 마이크 열림 state {step:1, phase:"arrived"} (next 없음) → 채팅에 ai/response {message:<질문>} + ai/suggestions 카드 "완료". 채팅에 다음 질문·칩(비활성)
스텝 2·3 위 반복
스텝 4 재생 완료 엔딩 화면. 마이크 안 열림 state {step:4, phase:"arrived", next:"exit"} — 질문 없음 canExit=true → 하차 유도(§2-B)
태블릿이 exit 발행 작별 화면 state {phase:"done"} 잠금 해제. 노선도 종료 상태
다음 관람객 state {phase:"idle"} 초기화
생성 실패 등 차 화면이 마이크를 다시 연다 error {step?, code, message} JOURNEY_ENDED 는 표시 안 함(정상 종료). 그 외는 안내

알아둘 것:

  • 생성 중에는 아무 메시지도 안 온다. route 가 올 때까지 "이동 준비 중" 정도로 두면 된다. 진행 표시를 태블릿이 만들 필요 없다.
  • state 만 retained. route·step·error 는 놓치면 다시 안 온다 — 그래서 segments 를 클라이언트가 누적한다(구현됨).
  • next 는 스텝 4 arrived 에만 실린다. 키 존재 여부로 판단(구현됨: message.next === 'exit').
  • error.code: STEP_NOT_READY(생성 중 진행 요청) · GENERATION_FAILED(LLM 실패 — 관람객이 다시 말하면 재시도) · PLAN_MISSING(plan 없음) · JOURNEY_ENDED(exit 뒤/스텝 초과, 정상).

4. 태블릿 → 미래차: enter · exit

토픽 orchestration/{sessionId}/future-car/request (QoS 1, retain=false). 페이로드 {"type":"enter"} / {"type":"exit"}. 구현돼 있다.

발행 시점 (구현됨) 가드 (구현됨) 미래차에서 일어나는 일
다음 목적지가 차량이고 이동을 확정할 때 phase ∈ {null, idle} 일 때만 발화 창 열림 · state waiting · ai/mode car · 채팅에 "출발 할까요?" · 차 화면 대기 + 마이크
지금 장소가 차량이고 다음 장소로 이동을 확정할 때 phase ∈ {null, waiting, driving, arrived} 발화 창 닫힘 · state done · ai/mode chat · 차 화면 작별

⚠️ exit 는 언제 보내도 여정을 그 자리에서 끝낸다. 스텝 2 재생 중에 관람객이 태블릿에서 "이동 확정"을 눌러 버리면 차 화면은 즉시 작별 화면으로 가고 남은 스텝은 버려진다. 현재 가드는 이를 막지 않는다(계약상 유효). 운영에서 "스텝 4 끝나기 전엔 이동 확정을 누르지 않게" 안내하거나, §2-B 처럼 canExit 가 참일 때만 차량 단계의 이동 확정을 열어 두는 것을 권한다 — 후자는 태블릿 UX 결정이라 요청이 아니라 제안이다.

advance·start 는 보내지 않는다(폐기. 보내면 발화 없이 강제 진행돼 deprecated 로 기록된다).


5. 채팅 토픽에서 일어나는 일

태블릿이 이미 구독하는 ai/response·ai/suggestions 에 미래차가 직접 발행한다. OC 를 거치지 않는다.

토픽 미래차 발행 내용 태블릿 동작 (기존 로직)
orchestration/{sid}/ai/response {"ok":true, "context_id":"<미래차 UUID>", "message":"출발 할까요?"} context_id 가 태블릿 것이 아니므로 isCurrent=false → AI 버블로 표시
orchestration/{sid}/ai/suggestions {"context_id":"<같은 값>", "suggestions":["네, 출발해요!","잠깐만요, 준비 좀 할게요"]} (최대 3개) receiveSuggestions → 칩 표시. §2-A 뒤에는 탭 불가

발행 시점: enter 직후 1회, 스텝 1~3 렌더 완료 직후 각 1회. 스텝 4 뒤에는 없다. retain=false.

OC 쪽 동작(참고): enter 때 미래차가 ai/mode {car} 를 retained 로 올리면 OC 는 그 세션의 음성 ai/request 에 LLM 답변·제안을 생략하고 transcript 만 돌려준다(message 없음 → 태블릿엔 빈 버블 없이 pending 만 풀림). exit 때 {chat} 으로 복귀. 이건 잠금이 뚫렸을 때의 2차 안전장치이고, 1차는 §2-A 다.


6. 검증 절차

미래차 서버 도구(전시망 Tailscale 안에서만):

주소 용도
http://creator-mac-studio.tail69930a.ts.net:8100/mqtt 주고받은 MQTT 원문 시간순 + 수동 발행(enter/exit/advance) + DB 이력 토글
POST …:8100/ambient/utterance {"session_id":"…","transcript":"네, 출발해요","source":"debug"} 차 마이크 없이 관람객 발화 흉내 → 스텝 생성
GET …:8100/ambient/session/{session_id} 세션 스냅샷(phase·step·완료 태스크·수집 발화)
GET …:8100/logs?session_id=… 서버 진행 로그(DB, 재시작 뒤에도 남음)
…:8100/docs Swagger

6-1. 잠금 검증 (차 화면 없이)

  1. 태블릿에서 페르소나 생성 → /mqtt 에서 그 세션의 state idle 확인. 태블릿 채팅 입력 가능이어야 한다.
  2. 태블릿에서 차로 이동 확정(또는 /mqtt 수동 enter) → state waiting 수신 → 입력창·마이크·칩 잠김, placeholder 안내 문구, 채팅에 "출발 할까요?" + 칩 2개(비활성).
  3. POST /ambient/utterance 로 "네, 출발해요" → 10~30초 뒤 route·step → 노선도 구간 표시. 여전히 잠김.
  4. /mqtt 에서 그 세션의 state arrived 가 올 때까지 기다린다(차 화면이 없으면 렌더 완료 보고가 안 오므로 POST …:8100/ambient/step-rendered {"session_id","step":1} 로 흉내) → 채팅에 스텝 1 질문·칩. 여전히 잠김. basic 카드가 "진행 중"→"완료"(§2-C).
  5. 3~4 를 스텝 4까지 → state arrived next=exit → 하차 유도(§2-B) 표시.
  6. 이동 확정(또는 수동 exit) → state done → 잠금 해제, 채팅 입력 가능.
  7. 새로고침해서 2 단계 상태를 다시 열어 봤을 때 잠금이 복원되는지(retained state).

6-2. 실차 검증 (차 화면 있음)

차 화면 /ambient 가 열려 있으면 3~4 단계의 발화·렌더 완료가 자동으로 일어난다. 관람객이 차에 말했을 때 태블릿 채팅에 그 말이 사용자 버블로 뜨지 않는 것이 정상이다(차 마이크 발화는 태블릿 채팅과 무관한 HTTP 경로).


7. 배포 전후 차이

2026-08-29 배포 완료로 아래는 기록용이다.

미래차 서버 5cc571f 가 Mac Studio 에 배포되기 전(10321ac)에는:

  • task/start 가 나가지 않는다 → 카드가 "대기"에서 바로 "완료"로 간다(§2-C 는 배포 후 확인).
  • 모달형(non-basic) 태스크도 미래차가 task/complete 를 보낼 수 있다 → 관람객이 열어보지 않은 돋보기 태스크가 완료로 바뀔 수 있다(배포 후 사라짐).
  • 차 마이크 경로(/ambient/utterance 정식 트리거)가 없다 → 실차 검증(§6-2)은 배포 후. §6-1 은 지금도 가능.

§2-A 잠금은 배포 전후와 무관하게 지금 넣어도 된다 — state.phase 는 이미 나가고 있다.


8. 장애·예외 상황

상황 태블릿에 보이는 것 태블릿이 할 일
미래차 서버가 죽어 있음 enter 뒤 state waiting 이 안 옴 잠금 안 걸림(phase null/idle). 채팅은 OC 와 평소처럼. 관람객은 차 체험 없이 다음 장소로 — exit 는 phase null 에서도 발행되므로 문제없음
스텝 생성 실패 error {code:"GENERATION_FAILED"} 안내만. 차 화면이 마이크를 다시 열고 관람객이 다시 말하면 재시도된다. 태블릿은 아무것도 보내지 않는다
관람객이 차에 말하지 않음 arrived 에서 멈춤 진행 불가(설계 전제). 현장 스태프가 차에 말하도록 안내. 비상시 서버 /mqtt 의 advance 로 강제 진행
차 마이크 권한 거부·STT 오류 위와 같음 위와 같음(차 화면 하단에 붉은 표시가 뜬다 — 운영자 확인용)
SSE 재연결 BFF 캐시 replay 로 state 복원 없음(구현됨)
미래차 서버 재시작 state 는 DB 복원 후 재발행. 대화 히스토리는 복원 안 됨(다음 스텝 품질↓) 없음
OC 이벤트 팝업이 차 안에서 뜸 event 팝업(장소 무관) 지금처럼 표시·닫기. 차가 이벤트에 반응할지는 미정 — 태블릿 변경 요청 없음
exit 를 일찍 보냄 state done, 차 작별 화면 §4 경고 참고

9. 변경 이력 · 문의

날짜 내용
2026-08-23 초판 — 구독·store·enter/exit 발행 가이드 (tablet-fe 에 미래차 코드 0건 시점)
2026-08-29 전면 재작성 — 구현 완료분 정리, 입력 잠금(§2-A) 신규 요청, 하차 유도·태스크 카드·타임라인·검증·예외 추가. 차 마이크 STT 정책 반영

문의: 미래차 팀. 서버 트래픽·로그는 위 §6 도구로 직접 볼 수 있다 — "왜 안 왔나"는 /mqtt 와 /logs?session_id= 에 남는다.

Clone this wiki locally