Skip to content

LOCATION_AUTOMATION_DESIGN

Joo edited this page Oct 10, 2026 · 21 revisions

장소 도착 자동화 (Location Automation) 설계

작성일: 2026-08-22 · 상태: 1차 구현 완료 (orchestration-server 6c0c7e0, 마이그레이션 085)

구현 범위: §3 테이블 3개 · §4 실행 엔진(internal/mqttx/arrival_automation.go) · Admin REST (/admin/{tasks|events|locations}/:id/automations, /admin/automations[/runs|/:id/fire]) · 백오피스(태스크/이벤트/장소 상세 편집기, 로봇 연동 → 규칙 목록·실행 로그). 미구현: §4.4 automation/fired 토픽(FE 수요 미확인), §6-5 세션 종료 시 대기 액션 취소, leave/dwell 트리거, §6-1 수동>자동 우선순위. 로봇팔·반려로봇은 스키마 확정 전까지 params.topic + payload JSON 자유 입력. 2026-08-22 추가: §4.3 디바이스 명령 큐(SQS FIFO, 완료 대기·타임아웃·액션 체인) 구현. 2026-09-12 추가: leave(장소 이탈) 트리거 구현 (마이그레이션 114, §4 참고). Vision이 세션 위치를 다른 장소로 바꿔 기록하는 순간 직전 장소의 규칙을 trigger='leave'로 평가한다. 규칙 모델·조건· 발화 정책·액션·실행 로그는 도착과 동일하며 모든 owner(태스크/이벤트/장소)에 붙일 수 있다. 후처리 연출이므로 액션을 user_task_commands로 추적하지 않는다(태스크 자동 완료 무관). dwell은 여전히 미구현. 2026-09-12 추가 (117): 태스크 실행 방식 tasks.execution_mode = parallel(기본, 동시 진행) | sequential(순차). 순차 태스크의 도착 규칙·자동 시작은 같은 장소의 앞 순서 태스크가 전부 종결될 때까지 보류(skip_reason=waiting_predecessor)되고, 앞 태스크 종결 시 OnUserTaskFinished가 사용자가 아직 그 장소에 있으면 대기가 풀린 태스크의 규칙만 재평가한다(RunArrivalParams.OnlyTaskIDs). 시험 발화/강제 실행은 통과. 2026-09-12 추가 (119): device_command_logs — 큐가 보낸 명령(pub)과 로봇 응답(sub: ack·plan·hub response)을 command_id 로 묶어 원문 저장. GET /admin/automations/runs/:runId/logs 로 실행 1건의 송수신 로그를 받아 파이프라인 실행 이력에서 단계별로 본다. 2026-09-12 추가: 태스크 상세 테스트 패널 — 세션을 골라 전제조건(세션 활성·코스 포함·배정·상태·현재 위치· 순차 앞 태스크·규칙·아이템·SPOT/로봇팔 가용성·타입별)을 점검하고 버튼으로 바로 조치한 뒤 도착 시뮬레이션(Vision 경로 동일) 또는 강제 실행. GET/POST /admin/tasks/:id/test/{preflight,fix} (internal/handlers/task_preflight.go).

사용자가 특정 장소에 도착한 것을 Vision 위치 정보로 감지했을 때, 그 사용자에게 그 장소에 배정된 태스크/이벤트와 보유 아이템에 따라 SPOT · 로봇팔 · 반려로봇 · Hub 등에 어드민이 미리 정의한 명령을 자동으로 내리는 기능의 데이터 모델 · 서버 실행 흐름 · 백오피스 화면 구조 제안.

목차

  1. 배경과 목표
  2. 현재 있는 것 (재활용)
  3. 데이터 모델
  4. 서버 실행 흐름
  5. 백오피스 화면 구조
  6. 정책 결정 사항
  7. 단계별 구현 순서
  8. 미결 사항

1. 배경과 목표

  • Vision(카메라 트래킹)은 vision/position/person으로 방문객 좌표·zone_id를 계속 발행한다.
  • 서버는 이를 장소(locations)로 매핑해 orchestration/{sessionId}/position으로 재발행하고, 장소가 바뀐 순간만 user_location_history에 기록한다.
  • 현재 "장소 도착 시 자동으로 일어나는 일"은 돌발 이벤트 발행 1가지뿐이다.
  • 목표: 도착 시 여러 디바이스(SPOT, 로봇팔, 반려로봇, Hub)에 명령을 순서대로 자동 발행하고, 그 규칙을 어드민이 백오피스에서 정의·확인·테스트할 수 있게 한다.
  • 핵심 전제: "무엇을 자동 수행할지"는 장소 자체가 아니라 (사용자, 장소, 태스크 or 이벤트) 매핑이 결정한다. 같은 "집"이라도 세션마다 배정된 태스크·이벤트가 다르고(user_tasks, user_locations.event_id), 거기에 보유 아이템 유무가 더해져 수행 여부가 갈린다.

1.1 판정 입력 요약

도착(세션 S, 장소 L)
  ├─ S에게 L에 배정된 태스크   : user_tasks ⋈ tasks(location_id = L)   — 0..N개
  ├─ S에게 L에 배정된 이벤트   : user_locations(S, L).event_id          — 0..1개
  ├─ (보조) L 자체 규칙        : 태스크·이벤트와 무관하게 장소에 오면 항상
  └─ S의 보유 아이템           : store_purchases(S)  (= orchestration/{S}/store/owned)
        ↓
  위 태스크/이벤트/장소에 붙은 자동화 규칙 중 조건(아이템 등)을 만족하는 것만 발화

2. 현재 있는 것 (재활용)

조각 위치 재활용 포인트
도착 감지 internal/mqttx/vision.go recordLocationChange() → maybePublishArrivalEvent() Vision의 연속 좌표를 "장소가 바뀐 순간 1회"로 이미 디바운스. 자동화 훅 포인트는 여기 한 곳
사용자 ↔ 장소 ↔ 태스크/이벤트 매핑 user_locations(visit_order, event_id, event_published_at), user_tasks(task_id, status) + tasks.location_id 세션 생성 시 이미 확정되는 매핑. 도착 시 "이 세션이 이 장소에서 할 태스크/이벤트"를 바로 조회 가능
SPOT 명령 정의 UI task_options / event_options의 spot_action, spot_destination_mode, spot_params + backoffice/.../components/SpotOptionFields.tsx, spotOptions.ts "선택지를 고르면 SPOT 명령"이 이미 구현됨. 트리거만 "선택 → 도착"으로 바꾸면 같은 모델. 태스크·이벤트 상세 화면에 이미 SPOT 연동 UI가 있으므로 도착 자동화도 같은 화면에 붙인다
SPOT 디스패치 internal/mqttx/spot.go DispatchSpotCommand(), SpotQueue 세션 → 배정 장소 → devices.type='spot' resolve + 큐잉 + ack/plan 릴레이 전부 재사용
디바이스 ↔ 장소 배정 devices + 장소 배정, SpotManagement(waypoint label 매핑) 로봇팔·반려로봇도 같은 방식으로 devices.type만 추가하면 resolve 가능
Hub 명령 포맷 MQTT_SCHEMA-HUB diorama/{device}/request {session_id, scenario_id, command, target, payload, timestamp} 범용 포맷이 이미 확정
보유 아이템 판정 task_options.required_product_id + 스토어 구매(store_purchase.go, orchestration/{sessionId}/store/owned) "이 상품을 가진 세션만" 조건이 선택지 단위로 이미 존재 → 규칙 조건으로 그대로 확장
테스트 경로 사용자 상세 "위치 테스트" 탭 (POST /admin/users/:id/position/test) recordLocationChange를 타므로 자동화 규칙도 함께 발화 → 별도 테스트 화면 불필요

⚠️ MQTT_SCHEMA-ROBOT_ARM, MQTT_SCHEMA-COMPANION_ROBOT은 아직 토픽명조차 비어 있는 템플릿이다. 이 두 디바이스는 스키마 확정 전까지 "토픽 + payload JSON 자유 입력"으로 받아둔다 (§5.3 참고).

3. 데이터 모델

3.1 arrival_automations — 규칙 (주인은 태스크 / 이벤트 / 장소)

컬럼 타입 설명
id TEXT PK UUID v7
owner_type TEXT task · event · location — 규칙이 붙는 대상 종류
owner_id TEXT tasks.id / events.id / locations.id
task_status_filter TEXT owner_type='task'일 때: pending(기본, 아직 안 한 태스크만) · any
name TEXT 어드민 표시용 이름 (예: "집 도착 — 반려로봇 인사 + SPOT 추종")
trigger TEXT arrive(도착, 기본) · leave(이탈, 114 — 다른 장소로 이동 기록 시 직전 장소 기준) · wrong_item/right_item(물건 집음, 103, 태스크 전용) · option_selected(파이프라인 선택지, 118, 이벤트 전용) · pending_entered(태스크가 pending 이 된 순간, 152, 태스크 전용) · dwell (향후)
dwell_seconds INTEGER trigger='dwell'일 때 체류 기준 초
condition_json TEXT(JSON) {"has_children": true|false|null, "first_visit_only": bool, "persona_ids": [], "required_product_ids": [], "required_products_mode": "all"|"any"} — null/빈 값은 "무관"
fire_policy TEXT once_per_session (기본) · every_arrival
cooldown_seconds INTEGER every_arrival일 때 재발화 최소 간격 (기본 30)
is_active INTEGER 0/1
display_order INTEGER 같은 owner 내 표시 순서
created_at, updated_at TIMESTAMP

장소는 owner_type으로 유도된다 — task → tasks.location_id, event → 그 세션의 user_locations.event_id가 가리키는 장소, location → 자기 자신. 규칙 테이블에 location_id를 중복 저장하지 않는다 (태스크가 다른 장소로 옮겨져도 규칙이 따라가도록).

3.2 arrival_automation_actions — 규칙 1개 → 순서 있는 액션 N개

컬럼 타입 설명
id TEXT PK
automation_id TEXT FK → arrival_automations (ON DELETE CASCADE)
device_type TEXT spot · robot_arm · companion · hub
action TEXT SPOT: navigate_to/run_scenario/follow/return_home/stop (기존 SPOT_ACTIONS). Hub: command 값. 로봇팔/반려로봇: 자유 문자열
params_json TEXT(JSON) SPOT: 기존 spot_params와 동일 구조 (destination은 실행 시 동적 계산). Hub: {target, scenario_id, payload}. 로봇팔/반려로봇: {topic, payload}
delay_ms INTEGER 이전 액션 디스패치 후 대기 시간 (기본 0)
display_order INTEGER 실행 순서

3.3 automation_runs — 실행 로그

컬럼 타입 설명
id TEXT PK
automation_id TEXT FK
user_id TEXT 세션 ID (users.id)
location_id TEXT 발화 장소
matched_owner_type, matched_owner_id TEXT 어떤 태스크/이벤트 매핑 때문에 발화(또는 스킵)됐는지
fired_at TIMESTAMP Vision 메시지 timestamp 기준
status TEXT fired · skipped · partial · failed
skip_reason TEXT condition_unmet · missing_item · task_completed · already_fired · cooldown · inactive · no_device
action_results_json TEXT(JSON) [{device_type, action, status, command_id, error, dispatched_at}] — ack 릴레이 수신 시 갱신

3.4 ER 요약

erDiagram
  tasks ||--o{ arrival_automations : "owner_type=task"
  events ||--o{ arrival_automations : "owner_type=event"
  locations ||--o{ arrival_automations : "owner_type=location"
  users ||--o{ user_tasks : assigned
  users ||--o{ user_locations : assigned
  user_locations }o--o| events : event_id
  tasks }o--|| locations : location_id
  arrival_automations ||--o{ arrival_automation_actions : contains
  arrival_automations ||--o{ automation_runs : fired
  users ||--o{ automation_runs : session
  devices }o--o{ locations : assigned
Loading

4. 서버 실행 흐름

vision/position/person 수신
  └─ PublishSessionPosition()
       └─ resolveSessionLocationForZone()           (기존)
            └─ recordLocationChange() == (prevLoc, true)   (기존: 장소가 바뀐 순간만 true; 직전 장소도 반환)
                 ├─ maybePublishArrivalEvent()      (기존: 돌발 이벤트)
                 ├─ onLeave(session, prevLoc, ts)   ← 114: 직전 장소의 trigger='leave' 규칙을 아래와 같은
                 │                                     흐름으로 평가 (LocationID=prevLoc, 도착보다 먼저 적재,
                 │                                     태스크 자동 시작·이벤트 타이밍 등 부수 효과 없음)
                 └─ runArrivalAutomations(session, loc, ts)   ← 신규
                      1. 이 세션·장소의 매핑을 수집
                           tasks  : user_tasks ⋈ tasks WHERE tasks.location_id = loc (status 필터 적용)
                           event  : user_locations(user, loc).event_id
                           location: loc 자체
                         → (owner_type, owner_id) 목록으로 arrival_automations 활성 규칙 로드
                           (정렬: location 규칙 → event 규칙 → task 규칙(visit/display 순))
                         ⚠️ 장소 규칙 우선(2026-10-06, Run() 내 게이트) — arrive 트리거에 location/
                           event 규칙과 task 규칙이 섞여 있으면, location/event 규칙을 먼저 평가·
                           디스패치하고 그 액션이 전부 종결(completed/failed/timeout/skipped)될
                           때까지 task 규칙 평가 자체를 미룬다(폴링, 최대 45초 — 넘으면 포기하고
                           진행). 단순 "정렬 순서"가 아니라 "장소가 먼저 끝나야 태스크가 시작"으로
                           강화된 것. leave·물건 집음·option_selected 트리거는 애초에 location과
                           task가 섞이지 않으므로 이 게이트가 안 걸린다
                         ⚠️ 세션 단위 게이트(2026-10-07, location_gate.go) — 첫 도착은 보통 태스크
                           시딩보다 먼저 와서 그 평가엔 task 규칙이 없다. 그래서 도착에서 돈
                           location/event run 을 세션별로 기억하고, 뒤늦게 task 규칙만 도는 평가
                           (시딩 후 재평가·pending_entered·순차 후속)도 그 run 이 전부 종결될 때까지
                           (최대 45초) 기다린다. 수동 실행·leave·물건 집음·option_selected 는 안 기다림
                      2. 규칙별 조건 평가 (has_children / first_visit_only / persona / 보유 아이템)
                           보유 아이템: 세션의 구매 내역(store_purchases)에 required_product_ids가
                           전부(all) 또는 하나라도(any) 있어야 발화. 없으면 skipped: missing_item
                      3. fire_policy · cooldown 검사 (automation_runs 최근 행 조회)
                      4. 액션을 display_order 순으로 goroutine에서 delay_ms 두고 디스패치
                           spot      → DispatchSpotCommand() 재사용 (세션 배정 장소의 spot 디바이스 resolve)
                           hub       → diorama/{device}/request publish
                           lg        → HA 전원/모드. params._lg_condition(2026-10-07)이 있으면 규칙
                                       시작 때 가전 상태를 한 번 읽어(스냅샷) 맞을 때만 보내고, 안 맞으면
                                       skipped: lg_condition_not_met. 모양은 발화 _lg_branches 한 줄과 같다
                                       ({appliance_id, feature(기본 power), op: is|is_not|unknown|any, value})
                           companion say + _lg_branches[i].lg_command(2026-10-07) → 발화 분기마다 가전 명령.
                                       발화가 속한 동시 묶음 뒤에 "분기 가전 명령" 단계(action_id={say}#lg)를
                                       끼우고, 발화 직전 고른 분기의 명령만 발화가 끝난 뒤 보낸다(없으면 skipped).
                                       실행 로그에 branch·lg_command 기록, 재시도는 그 명령만 재발송
                           robot_arm / companion → params_json.topic 으로 payload publish (스키마 확정 전 임시)
                      5. automation_runs 기록 (skipped 포함), ack/response 릴레이 수신 시 결과 갱신

4.0 매핑 기반 선택 예시

세션 장소 "집"에 배정된 것 보유 아이템 발화되는 규칙
지호 태스크 "조명 켜기" + 이벤트 "택배 도착" 없음 이벤트 "택배 도착"의 규칙(로봇팔 전달)은 아이템 필요 → missing_item 스킵(= 즉시 종결로 간주). 이벤트 규칙이 종결된 뒤에야 태스크 규칙(반려로봇 인사) 평가
수아 태스크 "조명 켜기" 간식 세트 이벤트가 배정 안 됐으므로 이벤트 규칙은 후보에도 안 들어감 → 장소 규칙이 없으면 태스크 규칙이 바로 평가(게이트 대상이 없음)
민준 태스크 "조명 켜기"(이미 완료) 간식 세트 task_status_filter=pending이면 후보 제외 → 아무것도 발화 안 함 (장소 규칙이 있으면 그것부터 전부 끝난 뒤에 평가하지만, 이 경우 애초에 태스크 후보가 없음)

4.1 보유 아이템 조건 평가

  • 판정 소스는 세션의 스토어 구매 내역 = orchestration/{sessionId}/store/owned와 동일한 집합.
  • 규칙 단위(condition_json.required_product_ids)와 액션 단위(params_json.required_product_id, 선택) 두 층으로 둔다. 규칙 단위는 "아이템 없으면 규칙 전체 스킵", 액션 단위는 "이 액션만 건너뛰고 나머지는 진행" (예: 간식 아이템 있을 때만 로봇팔 전달, 반려로봇 인사는 항상).
  • 구매는 세션 중 언제든 일어나므로 발화 시점에 조회한다 (캐시 X). once_per_session 규칙이 아이템 부재로 missing_item 스킵된 경우는 "발화한 것"으로 치지 않아, 이후 아이템을 사고 다시 도착하면 발화된다.

4.2 디바이스 resolve 규칙

  • 세션에 배정된 장소(user_locations)에 매핑된 devices(해당 type, is_active=1) 중 첫 번째.
  • 없으면 액션을 skipped: no device로 로그에 남기고 다음 액션은 계속 진행한다 (한 디바이스 부재가 전체 규칙을 막지 않도록).

4.3 디바이스 명령 큐 (SQS FIFO) — 2026-08-22 구현

모든 디바이스 명령(SPOT·로봇팔·반려로봇·Hub, 자동화/태블릿/선택지 출처 불문)은 internal/mqttx/device_queue.go의 디바이스 명령 큐를 거친다. 백엔드는 internal/devicequeue/ — SQS_ENDPOINT가 있으면 ministack SQS FIFO(device-commands.fifo

  • -dlq.fifo), 없으면 프로세스 내 메모리 큐(같은 계약).
Enqueue(job)  → SQS FIFO (MessageGroupId = 디바이스 키: spot:{robotId} | hub | robot_arm:{topic} | companion:{topic})
consumer      → Receive → delay_ms 대기 → MQTT 발행 → 완료 신호 대기(timeout_ms) → DeleteMessage → job.next 적재
  • 디바이스 단위 직렬화: 같은 그룹은 앞 잡을 Delete하기 전까지 다음 잡이 나오지 않는다. 세션이 달라도 같은 SPOT이면 순서대로. (SQS는 한 Receive 배치 안에서 같은 그룹을 여러 개 내줄 수 있어 소비자도 그룹별 락으로 한 번 더 직렬화한다.)
  • 규칙 안 액션 체인: 첫 액션만 큐에 넣고 job.next로 다음 액션을 실어, 앞 액션 완료 후 소비자가 다음을 적재한다 → 디바이스가 달라도 액션은 순차. delay_ms = "앞 액션 완료 후 대기".
  • 완료 신호: SPOT robot/{id}/ack 종결 status · Hub diorama/{device}/response.result · 로봇팔/반려로봇은 스키마 확정 전까지 params.duration_ms(기본 3초) 경과를 완료로 간주.
  • 타임아웃: arrival_automation_actions.timeout_ms(기본 30초). 넘기면 timeout으로 기록하고 다음으로 진행. 가시성 타임아웃을 delay+timeout+여유로 늘려 재전달을 막고, 소비자가 두 번 죽으면 DLQ.
  • 액션 상태: queued → dispatched → completed | timeout | failed (+ device_status에 ack 원문)이 automation_runs.action_results_json에 갱신되고 백오피스 실행 로그·ministack 화면에서 보인다.
  • 우선순위(수동 > 자동)는 미구현 — 모두 같은 FIFO.

4.4 MQTT 토픽 (신규)

Topic 방향 QoS Retained 설명
orchestration/{sessionId}/automation/fired S→C 1 X 규칙 발화 알림 {automation_id, name, location_id, actions:[{device_type, action, command_id, status}]} — 태블릿이 "로봇이 움직입니다" 같은 안내를 띄우는 용도(선택)

5. 백오피스 화면 구조

사이드바
├─ 태스크 관리 → 태스크 상세 (/tasks/:id)
│       ├─ 기본 정보 / 선택지(옵션별 SPOT 연동)    (기존)
│       └─ [NEW] 도착 자동화 섹션                  ← 규칙의 "주 편집 화면" ①
├─ 이벤트 관리 → 이벤트 상세 (/events/:id)
│       ├─ 기본 정보 / 선택지(옵션별 SPOT 연동)    (기존)
│       └─ [NEW] 도착 자동화 섹션                  ← 규칙의 "주 편집 화면" ②
├─ 장소 관리
│   └─ 장소 상세 (/locations/:id)
│       ├─ 기본 정보 / 태스크 / 콘텐츠           (기존)
│       └─ [NEW] 도착 자동화 탭
│             ├─ 장소 자체 규칙 (owner_type=location) 편집
│             └─ 이 장소의 태스크·이벤트에 붙은 규칙 **읽기 전용 집계** (클릭 → 해당 상세로)
│
└─ 로봇 연동   ← 기존 "SPOT 관리" 네비 트리를 이름 바꿔 확장
    ├─ SPOT 장소 관리 (/spot-management)          (기존, waypoint 매핑)
    ├─ [NEW] 자동화 규칙 전체 (/automations)       ← 장소·디바이스 축 read-mostly 목록
    ├─ [NEW] 자동화 실행 로그 (/automations/runs)  ← automation_runs 타임라인
    └─ 테스트 (/spot-test)                         (기존 + SPOT 위치 테스트)

왜 태스크/이벤트 상세를 주 편집 화면으로 두나 — 수행 여부를 가르는 건 "이 세션이 이 장소에서 무슨 태스크/이벤트를 하느냐"이므로, 규칙은 그 태스크/이벤트의 속성이다. 이미 태스크·이벤트 상세에 "선택지를 고르면 SPOT 명령"(SpotOptionFields)이 있어서, 바로 옆에 "도착하면 자동으로" 섹션이 붙으면 어드민이 한 화면에서 선택 시 동작 vs 도착 시 동작을 같이 설계할 수 있다. 장소 상세 탭은 "이 장소에서 일어날 수 있는 모든 자동화"를 한눈에 보는 집계 + 장소 공통 규칙 편집용. 전체 목록은 디바이스 축 역조회용 보조 뷰.

5.1 태스크/이벤트 상세 → 도착 자동화 섹션 (장소 상세의 장소 규칙도 동일 컴포넌트)

┌─ 도착 자동화  (이 태스크가 배정된 세션이 "집"에 도착하면) ──── [+ 규칙 추가] ─┐
│                                                                           │
│ ▾ 반려로봇 인사 + SPOT 추종                        [활성 ●]  [시험 발화 ▾] │
│   트리거: 도착 · 대상: 미완료 태스크만 · 조건: 아이 동반 무관 · 세션당 1회   │
│   필요 아이템: 없음                                                        │
│   ┌────┬──────────┬──────────────┬───────────────────────┬────────┬──────┐ │
│   │ 순서│ 디바이스  │ 명령         │ 파라미터               │ 지연   │      │ │
│   ├────┼──────────┼──────────────┼───────────────────────┼────────┼──────┤ │
│   │ 1  │ 반려로봇  │ greet        │ {"voice":"hello"}      │ 0 ms   │ ↑↓ 🗑 │ │
│   │ 2  │ SPOT     │ follow       │ mode=start             │ 1500ms │ ↑↓ 🗑 │ │
│   └────┴──────────┴──────────────┴───────────────────────┴────────┴──────┘ │
│   [+ 액션 추가]                                                            │
│                                                                           │
│ ▸ (아이 동반) 로봇팔 간식 전달                     [활성 ○]  [시험 발화 ▾] │
│   필요 아이템: 🛒 간식 세트 (all)                                          │
└───────────────────────────────────────────────────────────────────────────┘
  • 섹션 헤더에 규칙이 발화되는 장소를 명시 (tasks.location_id / 이벤트는 "배정된 장소")
  • 규칙 카드: 이름 · 트리거 · (태스크) 상태 필터 · 조건 요약 · 필요 아이템 칩(스토어 상품 다중 선택, all/any) · 발화 정책 · 활성 토글 · 시험 발화(이 태스크/이벤트가 배정된 활성 세션만 목록에 뜸 → 선택 시 즉시 실행, automation_runs에 manual=true로 기록)
  • 같은 컴포넌트(ArrivalAutomationEditor)를 장소 상세 탭의 "장소 규칙"에도 재사용
  • 액션 테이블: 순서 변경(↑↓), 삭제, 행 클릭 시 인라인/모달 편집
  • 마지막 실행 결과 배지 (최근 run의 status) — 로그 화면으로 링크

5.2 자동화 규칙 전체 (/automations)

컬럼 설명
주인 태스크 · 조명 켜기 / 이벤트 · 택배 도착 / 장소 · 집 칩 — 클릭 → 해당 상세
장소 유도된 장소
규칙명
트리거 / 조건 요약 (필요 아이템 포함)
액션 디바이스 칩 나열 (SPOT 반려로봇 …)
활성 토글 (여기서도 바로 on/off)
최근 발화 시각 + 상태 배지

필터: 주인 종류(태스크/이벤트/장소) · 장소 · 디바이스 타입 · 활성 여부.

5.3 액션 편집 폼 (디바이스 타입별 분기)

디바이스 입력 비고
SPOT SpotOptionFields 그대로 재사용 (spot_action / spot_destination_mode / scenario·variables·follow mode) navigate_to의 destination은 세션 장소 → waypoint label로 동적 계산(own/next). 2026-09-16부터 destination_mode=waypoint이면 편집기에서 SPOT의 retained waypoints 목록(+장소 매핑)에서 고른 params.destination을 그대로 사용 — SPOT 테스트 화면과 동일
Hub command, target, scenario_id, payload(JSON) diorama/{device}/request 포맷 확정됨
로봇팔 · 반려로봇 토픽 문자열 + payload JSON 자유 입력 + ⚠️ "스키마 미확정" 배지 스키마 확정되면 드롭다운/타입드 폼으로 교체. session_id는 서버가 자동 주입

공통: delay_ms 숫자 입력, 순서, 필요 아이템(선택) — 이 액션만 해당 상품 보유 시 실행.

5.4 자동화 실행 로그 (/automations/runs)

시각        세션(페르소나)   장소    주인                 규칙                  상태      액션 결과
13:36:23    지호 (abc123)    집      태스크·조명 켜기      반려로봇+SPOT         fired     반려로봇 ✓ · SPOT ACCEPTED→SUCCESS
13:36:23    지호 (abc123)    집      이벤트·택배 도착      로봇팔 간식 전달      skipped   missing_item (간식 세트)
13:35:50    지호 (abc123)    마트    태스크·장보기         로봇팔 상품 전달      skipped   no device (robot_arm)
13:20:01    민준 (ghi789)    집      태스크·조명 켜기      반려로봇+SPOT        skipped   task_completed (status filter)
13:20:01    수아 (def456)    집      집 도착 — 반려로봇+SPOT   skipped   already_fired (once_per_session)
  • 필터: 세션 · 장소 · 규칙 · 상태 · 기간
  • 행 펼치면 action_results_json 원문 + 발행한 MQTT payload
  • 사용자 상세(/task-completions/:sessionUuid)에도 "자동화" 탭으로 같은 데이터를 세션 필터 고정으로 노출

6. 정책 결정 사항

# 이슈 제안
1 수동 vs 자동 SPOT 명령 충돌 — 도착 규칙이 follow를 보낸 직후 사용자가 태블릿에서 navigate_to를 고르면? 수동(사용자 선택) > 자동. 자동 명령은 stop 외엔 진행 중 명령을 끊지 않고 큐 뒤에 붙는다. 역으로 수동 명령은 대기 중인 자동 명령을 건너뛸 수 있게 SpotQueue에 source 필드 추가
2 every_arrival 폭주 — Vision이 집↔마트 경계에서 깜빡이면 도착이 연달아 찍힘 규칙별 cooldown_seconds 기본 30초. once_per_session을 기본값으로
3 디바이스 부재 — 세션 장소에 해당 타입 디바이스가 없을 때 조용히 스킵하지 않고 automation_runs에 skipped: no device로 남김. 규칙 카드에 경고 배지
3b 태스크 완료 후 재도착 — 태스크를 마친 뒤 같은 장소에 다시 오면? task_status_filter=pending(기본)이면 후보에서 빠짐 → 완료 후엔 발화 안 함. 완료 후에도 돌아가길 원하면 any. 이벤트는 event_published_at과 무관하게 도착마다 후보(fire_policy로 제어)
3c 한 도착에 규칙 여러 개 — 태스크 2개 + 이벤트 1개가 각각 SPOT 명령을 갖고 있으면? 전부 발화하되 순서는 location → event → task(visit/display 순). SPOT은 SpotQueue가 직렬화. 상충(follow vs navigate_to)은 어드민 책임으로 두고, 규칙 전체 목록에서 같은 장소·같은 디바이스에 규칙이 여러 개면 ⚠️ 배지
4 zone_id='home' 인스턴스 선택 — 첫 방문은 STEP이 가장 앞선 집, 두 번째부터는 첫 인스턴스로 돌아가지 않고 나머지 안에서 순환 (2026-09-27) 자동화도 동일 인스턴스 기준으로 first_visit_only 판단. 집-아침/집-저녁이 같은 규칙을 공유하려면 장소 인스턴스마다 규칙을 따로 두거나, 향후 "zone 단위 규칙" 옵션 검토
5 세션 종료 시 세션 종료(end-session) 시 해당 세션의 대기 중 자동 액션(delay 중인 goroutine) 취소
6 아이템 조건과 once_per_session missing_item 스킵은 발화로 치지 않음 → 구매 후 재도착 시 발화. 시험 발화는 아이템 조건을 무시할지 토글 제공
7 서버 재기동 delay 중이던 액션은 유실 허용 (전시 현장 특성상 수 초 단위라 영속화 불필요)
9 액션 병렬 실행 (2026-08-30, 반려로봇 채널 모델) arrival_automation_actions.parallel=1 이면 앞 액션의 완료를 기다리지 않고 앞 액션과 동시에 적재. 연속된 병렬 액션은 한 단계로 묶이고 단계 전체가 종결돼야 다음 단계로. 반려로봇 명령은 큐 그룹이 companion:{robot}:{채널}이라 locomotion(하체)만 직렬, audio/led/gaze/projection은 실제로 동시에 나간다(예: 제스처 ∥ 발화). 첫 액션의 parallel은 무시. 병렬 그룹 카운트는 프로세스 메모리(재기동 시 그 단계의 다음 적재 유실 — 7번과 동일 수준)
9-1 기다리지 않는 단계 (2026-10-04) params_json._no_wait=true 면 같이 출발하되 아무도 그 단계의 완료를 기다리지 않는다. 드론 배송처럼 오래 걸리는 연출이 뒤 단계(로봇팔·SPOT)를 붙잡지 않게 하려고 넣었다 — 전에는 Hub(드론) ∥ SPOT 묶음이 전부 끝나야 로봇팔이 나갔고, 드론 ack 가 타임아웃이면 체인이 멈췄다. 서버는 액션을 buildChainStages 로 "한꺼번에 적재할 묶음"으로 나누고, 그 묶음의 기다리는 잡(= _no_wait 아닌 것)이 전부 종결될 때만 다음 묶음을 적재한다. 묶음이 통째로 _no_wait 면 다음 묶음을 같이 적재한다. 기다리지 않는 단계는 실패해도 체인을 멈추지 않고(내부적으로 _continue_on_failure 취급), 수동 재시도는 그 단계만 다시 보낸다(RetryRunFrom). 같은 로봇에 뒤 단계가 또 명령을 넣으면 그 로봇 큐에서 차례를 기다린다 — 병렬 금지 규칙(§같은 로봇 동시 금지)은 묶음 안에서만 검사한다. 어드민: 단계 카드 "뒤 단계가 기다리지 않음" 체크, 다이어그램은 점선 가지로 빠져나가게 그린다. 마이그레이션 없음 — parallel(100) 과 달리 컬럼이 아니라 params_json 예약 키다(_continue_on_failure 와 같은 자리)
8 태스크 자동 완료 (2026-08-30) — 태스크는 태블릿 선택 팝업 없이 도착 자동화로만 실행된다 owner_type=task 규칙이 실제 발화(시험 발화 제외)하면 각 액션이 user_task_commands에 queued로 기록되고, 큐가 액션을 종결(ack/timeout/failed)할 때마다 갱신 → 전부 종결되면 user_tasks를 자동 completed 하고 task/complete와 같은 후속 발행. 실패·타임아웃도 종결로 센다. 백오피스 세션 상세(위치별 태스크)에 명령별 상태 표시. 자동 완료 후 태블릿 task/complete가 또 와도 포인트 재지급 없음
10 pending_entered 트리거 (2026-10-06, 152) — 기존 트리거(arrive/leave/물건 집음/선택지)와 동일한 규칙 모델(UI·조건·발화 정책·액션·실행 로그·시험 발화)로, 트리거 값만 다르다. 태스크 전용(owner_type='task') — 장소·이벤트에는 pending 상태 개념이 없다 "내 차례" = 관람객이 그 장소에 있고 앞 태스크에 막히지 않음 (2026-10-10). 발화 지점 세 곳: ① 세션 시딩 직후 — 관람객이 지금 있는 장소의 pending 이면서 앞 태스크에 막히지 않은 태스크만(seedPendingEnteredTargets). 예전엔 코스 전체 장소를 훑어, 공원 첫 태스크의 pending_entered 가 관람객이 집에 있을 때 돌아 태스크가 자동 완료되고 공원 이벤트까지 발행됐다(세션 01a125b1). ② 장소 도착(onArrival → fireReadyPendingEntered, 도착 평가 뒤라 장소 연출 게이트를 탄다). ③ 순차 실행(117) 체인에서 앞 태스크가 종결돼 대기가 풀리는 순간(OnUserTaskFinished, 관람객이 그 장소에 있을 때). 중복 발화는 규칙의 fire_policy 가 막는다. 용도: detailed 처럼 태블릿의 명시적 task/start 없이 자동화로만 시작되는 태스크 타입이, 장소 도착(arrive)을 다시 기다리지 않고도 "내 차례가 된 순간" 바로 로봇 명령을 받을 수 있다 — 관람객이 이미 그 장소에 서 있는 채로 순차 체인의 두 번째 이후 태스크가 시작되는 경우, 그 사람은 다시 "도착"하지 않으므로 arrive 만으로는 영영 발화하지 않았다. 구체적으로 언제 쓸지는 운영자가 자동화 규칙을 만들며 정한다. 추적: arrive 와 동일하게 user_task_commands 로 추적해 액션이 전부 종결되면 태스크를 자동 completed 한다(isTrackableTaskTrigger) — leave·물건 집음처럼 "연출·후처리"가 아니라 "이 태스크를 수행하라"는 명령이기 때문

7. 단계별 구현 순서

  1. 마이그레이션 + sqlc — §3 테이블 3개 (-- +goose Up 지시자 필수). 매핑 수집용 쿼리: ListUserTasksAtLocation(user, location), 기존 GetUserLocationByUserAndLocation 재사용
  2. 서버 실행 엔진 — internal/mqttx/automation.go: runArrivalAutomations() + recordLocationChange 훅. 매핑 수집 → 규칙 로드 → 아이템/상태 조건 → 디스패치. SPOT만 먼저, 로봇팔/반려로봇/Hub는 generic publish
  3. Admin REST — /admin/{tasks|events|locations}/:id/automations CRUD(공통 핸들러, owner_type만 다름), /admin/automations, /admin/automations/runs, POST /admin/automations/:id/fire?user_id= (시험 발화)
  4. 백오피스 — ArrivalAutomationEditor 공용 컴포넌트 → 태스크 상세 → 이벤트 상세 → 장소 상세 탭(장소 규칙 + 집계) → 전체 목록 → 실행 로그
  5. 문서 — MQTT_SCHEMA-ORCHESTRATION_SERVER에 automation/fired 토픽, 이 문서 상태를 "확정"으로
  6. (스키마 확정 후) 로봇팔·반려로봇 타입드 폼 교체

8. 미결 사항

  • 로봇팔 · 반려로봇 MQTT 스키마 (각 팀 작성 대기) — 확정 전까지 JSON 자유 입력
  • leave 트리거 — 2026-09-12 구현(114). 타임아웃 기반이 아니라 다른 장소 도착 = 직전 장소 이탈로 판정한다. 따라서 마지막 장소(보통 home)는 세션 종료로는 이탈이 발화하지 않음 — 세션 종료 시 발화가 필요하면 §6-5(종료 시 대기 액션 취소)와 함께 결정
  • dwell 트리거 실제 수요 확인
  • orchestration/{sessionId}/automation/fired를 태블릿이 실제로 쓸지 (FE 팀 확인)
  • 장소 인스턴스 단위 규칙 vs zone 단위 규칙 (§6-4)
  • 태스크 선택지(option)별 도착 자동화가 필요한지 — 현재는 태스크 단위. "이 선택지를 이미 고른 세션이 도착하면"은 task_option_selections 조건으로 확장 가능
  • 이벤트 규칙의 발화 시점 — 도착 즉시 vs 돌발 이벤트 팝업(maybePublishArrivalEvent) 직후. 로봇팔 전달처럼 "이벤트 연출"인 경우 팝업과 타이밍을 맞춰야 할 수 있음
  • 아이템 "소모" 여부 — 로봇팔이 간식을 전달하면 보유 목록에서 차감해야 하는지 (현재 store/owned는 누적 보유만 표현)

Clone this wiki locally