Repository navigation
FUTURE_CAR SPEC TABLET
- 작성: 미래차 팀 · 2026-08-29 (2026-08-23판 전면 재작성 — 과거 내용은 폐기)
- 대상 코드:
tablet-fe60d353c(main) · 미래차 서버zer01ne-future-car-servermain-2026a1d3673(조직 미러: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. 배경 — 미래차가 무엇이고 관람객에게 무슨 일이 일어나나
- 1. 현재 구현 상태 (60d353c 대조)
- 2. 추가 구현
- 3. 미래차 → 태블릿: 언제 무엇이 오고 무엇을 보여주나
- 4. 태블릿 → 미래차:
enter·exit - 5. 채팅 토픽에서 일어나는 일
- 6. 검증 절차
- 7. 배포 전후 차이
- 8. 장애·예외 상황
- 9. 변경 이력 · 문의
전시장에는 미래차(실물 차 + 차 안 디스플레이 + 마이크)가 한 대 있다. 관람객은 태블릿을 들고 여러 장소(집·공원·마트·차)를 돌며 태스크를 수행하는데, 그중 "차" 장소가 미래차다. 미래차 쪽 시스템은 둘이다.
| 구성 | 역할 |
|---|---|
미래차 서버 (Mac Studio :8100, FastAPI) |
MQTT 로 OC·태블릿과 통신. 관람객 발화를 재료로 LLM 이 "스텝"(차 안 연출 한 장면)을 4개 생성해 화면에 내려보내고, 태블릿 채팅에 질문을 띄우고, OC 에 태스크 시작·완료를 보고한다 |
미래차 화면 (/ambient, 차 안 디스플레이) |
스텝을 재생하고, 재생이 끝나면 서버에 보고하고, 관람객 차례에 마이크를 열어 발화를 STT 해 서버로 보낸다 |
- 태블릿에서 "차로 이동"을 확정하면(
TaskCompleteDialog) 태블릿이enter를 보낸다. 차 화면이 대기 화면으로 바뀌고, 태블릿 채팅에 "출발 할까요?" 와 답 후보 칩 2개가 뜬다. - 관람객은 차에 대고 답한다("네, 출발해요"). 차 마이크가 받아 서버로 보내고, 서버가 스텝 1을 만든다(10~30초). 태블릿에는 이 구간(
route)이 노선도에 그려진다. - 차 화면이 스텝 1을 재생한다. 그동안 태블릿의 "차량 상태·주행 점검" 같은
basic태스크 카드가 "진행 중" 이 된다. - 재생이 끝나면 카드가 "완료" 로 바뀌고, 채팅에 다음 질문이 뜬다. 관람객은 다시 차에 대고 답한다.
- 2~4 가 스텝 4까지 반복된다. 스텝 4 재생이 끝나면 질문 없이 차 화면이 엔딩을 보여주고, 태블릿에는 하차 가능 신호(
state.next="exit")가 온다. - 태블릿에서 "다음 장소로 이동"을 확정하면
exit를 보낸다. 차 화면은 작별 화면, 여정 종료.
이전 설계는 관람객이 태블릿 채팅에 답하면 그 답(ai/response.transcript)을 미래차가 구독해 재료로 썼다. 지금은
차 마이크로만 받는다. 이유는 둘이다.
- 태블릿에 답하면 OC 의 AI 동반자가 따로 답을 한다(차 안 연출과 태블릿 채팅이 "따로 노는" 문제). OC 가
ai/mode=car를 구현해 음성 발화에는 답을 생략하지만, 텍스트(칩 탭·타이핑)에는 여전히 답한다(ai_chat.gocar 분기가type=voice안에만 있음). - 미래차 서버는 아직
ai/response.transcript를 구독하고 있어서, 태블릿에서 보낸 문장도 스텝 재료로 섞여 들어간다.
그래서 차 안에 있는 동안(enter~exit) 태블릿은 채팅 입력을 받지 않는다. 미래차가 띄우는 질문·칩은 보여주기만 한다.
관람객이 "무슨 말을 하면 되는지" 힌트로 쓰라는 뜻이다.
- 차 장소 태스크 중
type=basic만 미래차가 수행한다 — 태블릿에 조작 UI 가 없어 서버가 완료해 줘야 하는 타입. 스텝마다 하나씩 AI 가 골라 스텝 시작에task/start, 렌더 완료에task/complete를 OC 에 보낸다. - 그 외 타입(
vehicle_ready·detailed·quote_briefing… 돋보기 모달)은 미래차가 건드리지 않는다. 지금처럼 관람객이 태블릿 모달에서 완료한다.
미래차 팀이 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 |
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 미수신) 이면 잠그지 않는다.
- 잠금의 근거는 "미래차 서버가 발화 수집 창을 열어 두고 있는가"이고, 그건 정확히
state.phase ∈ {waiting, driving, arrived}다. -
state는 retained 라 새로고침·SSE 재연결 뒤에도 마지막 값이 즉시 복원된다(BFFfutureCarState캐시 + 브로커 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-opinCar 판정을 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 → 해제 |
state · phase=arrived · next="exit" (스텝 4 렌더 완료) 가 오면 canExit === true 가 된다. 지금은 이 값을 아무 UI 도 읽지 않아
관람객이 스스로 "다음 장소로 이동"을 눌러야 한다. 차 화면은 이때 "체험이 모두 끝났습니다 / 뒷쪽 출구로 퇴장해 주세요" 를
보여주고 있으므로, 태블릿에도 같은 맥락의 안내("차에서 내려 다음 장소로 이동해 주세요" + 이동 확정으로 연결)를 띄우면 관람객이
헤매지 않는다. useFutureCarCanExit() 가 이미 있다. 어디에 띄울지는 태블릿 UX 결정이다(노선도 캡션, 태스크 패널 상단 배너 등).
미래차 서버가 스텝마다 task/start(스텝 시작) → task/complete(렌더 완료) 를 OC 에 보내면 OC 가 dashboard 를
in_progress → completed 로 재발행한다. 태블릿의 basic 카드(task-panel.tsx STATUS_STYLE)는 이미 active(진행 중) 배지를
그리므로 코드 변경 없이 스텝 동안 "진행 중" → 끝나면 "완료" 로 보여야 한다. 실제로 그렇게 보이는지만 확인해 달라.
non-basic 카드(돋보기)는 배지가 없고 미래차가 건드리지 않으므로 변화 없음.
use-task-summary.ts 는 dashboard 첫 수신 때 하루 요약을 ai/request type=text 로 보낸다. 이건 세션 초입(= enter 전)이라
미래차가 창 밖(closed)으로 버리므로 문제없다. 앞으로 차 안(waiting~arrived)에서 자동으로 ai/request 를 보내는 기능을
추가하지 말 것 — 미래차가 관람객 발화로 오인해 스텝을 생성한다. §2-A 의 dispatch 가드가 이걸 막는 최종 방어선이다.
토픽 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는 스텝 4arrived에만 실린다. 키 존재 여부로 판단(구현됨:message.next === 'exit'). -
error.code:STEP_NOT_READY(생성 중 진행 요청) ·GENERATION_FAILED(LLM 실패 — 관람객이 다시 말하면 재시도) ·PLAN_MISSING(plan 없음) ·JOURNEY_ENDED(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 로 기록된다).
태블릿이 이미 구독하는 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 다.
미래차 서버 도구(전시망 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 |
- 태블릿에서 페르소나 생성 →
/mqtt에서 그 세션의state idle확인. 태블릿 채팅 입력 가능이어야 한다. - 태블릿에서 차로 이동 확정(또는
/mqtt수동enter) →state waiting수신 → 입력창·마이크·칩 잠김, placeholder 안내 문구, 채팅에 "출발 할까요?" + 칩 2개(비활성). -
POST /ambient/utterance로 "네, 출발해요" → 10~30초 뒤route·step→ 노선도 구간 표시. 여전히 잠김. -
/mqtt에서 그 세션의state arrived가 올 때까지 기다린다(차 화면이 없으면 렌더 완료 보고가 안 오므로POST …:8100/ambient/step-rendered {"session_id","step":1}로 흉내) → 채팅에 스텝 1 질문·칩. 여전히 잠김.basic카드가 "진행 중"→"완료"(§2-C). - 3~4 를 스텝 4까지 →
state arrived next=exit→ 하차 유도(§2-B) 표시. - 이동 확정(또는 수동
exit) →state done→ 잠금 해제, 채팅 입력 가능. - 새로고침해서 2 단계 상태를 다시 열어 봤을 때 잠금이 복원되는지(retained state).
차 화면 /ambient 가 열려 있으면 3~4 단계의 발화·렌더 완료가 자동으로 일어난다. 관람객이 차에 말했을 때 태블릿 채팅에 그
말이 사용자 버블로 뜨지 않는 것이 정상이다(차 마이크 발화는 태블릿 채팅과 무관한 HTTP 경로).
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 는 이미 나가고 있다.
| 상황 | 태블릿에 보이는 것 | 태블릿이 할 일 |
|---|---|---|
| 미래차 서버가 죽어 있음 |
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 경고 참고 |
| 날짜 | 내용 |
|---|---|
| 2026-08-23 | 초판 — 구독·store·enter/exit 발행 가이드 (tablet-fe 에 미래차 코드 0건 시점) |
| 2026-08-29 | 전면 재작성 — 구현 완료분 정리, 입력 잠금(§2-A) 신규 요청, 하차 유도·태스크 카드·타임라인·검증·예외 추가. 차 마이크 STT 정책 반영 |
문의: 미래차 팀. 서버 트래픽·로그는 위 §6 도구로 직접 볼 수 있다 — "왜 안 왔나"는 /mqtt 와 /logs?session_id= 에 남는다.
- Google ADK 검토 (프레임워크·모델 비교)
- 페르소나 + 상황 카드 모델 (2차 시나리오 구동 방식)
- Mac Studio 로컬 LLM + Claude Opus 하이브리드 라우팅 전략
- 개발 일정·할 일 점검표 (단계별 진행 점검)
- 로봇 Adapter 개발 온보딩 (HA·MQTT 계정 등록, 토픽 적용, 테스트 송수신, 상태 확인)
- LG ThinQ 가전 연동 (HA 통합 · REST 데이터 취득 · 제어)
- 장소 도착 자동화 (태스크/이벤트/장소 규칙 → SPOT·로봇팔·반려로봇·Hub 자동 명령)
- 엔딩 화면 데이터 출처 점검 (차량 3필드 · 세션 종료 엔딩 5화면)