제로웍스 연동 API 문서

제로웍스 로봇이 비스캣 관제와 주고받는 채널·토픽·페이로드·인증 규약입니다. 동작 흐름은 개발 시나리오를 함께 참조하세요.

본 문서의 JSON 본문은 예시 값이며, 값 자체에 의미를 두지 않습니다.

1. 공통 규약

requestId — 텔레메트리는 생략 가능

상태(state)처럼 자발·고빈도로 발행되는 텔레메트리는 최신값만 의미가 있어 requestId를 생략할 수 있고, 관제는 occurredAt 비교로 더 최신일 때만 반영(last-write-wins)합니다. 반면 행위를 트리거하는 이벤트·명령(입찰 bid, 알람, PIN, 박스 닫힘 등)은 at-least-once 재전송에 대비해 requestId(또는 동등 식별자)를 유지해 멱등 처리합니다.

2. 인증 (REST)

등록·인증은 REST로 진행합니다. 키쌍은 로봇이 직접 생성하며, 개인키는 디바이스 밖으로 내보내지 않습니다(TPM·보안 칩 권장).

키 발급 절차

  1. 키쌍 생성 — 로봇이 공개키/개인키 쌍을 디바이스 내부에서 생성.
  2. 공개키 제출 — 사전 등록된 robotCode(예: R-001)와 함께 로봇의 공개키POST /v1/robot/key로 제출. 관제가 robotCode 로 로봇을 조회해 내부 식별자 robotId(UUID)를 회신하며, 로봇은 이후 인증에 robotId 를 사용한다.
  3. 비밀키 수령 — 서버가 HMAC 비밀키를 로봇 공개키로 암호화해 응답 → 로봇이 개인키로 복호화해 보관.
  4. 인증서 발급POST /v1/robot/cert로 X.509 인증서 번들(다운로드 URL 목록) 수령 → 설치 후 MQTT 연결.
  5. 요청 서명 — 이후 모든 REST 인증 요청에 HMAC-SHA256 서명 헤더 부착.

암호화·서명 규격 비스캣 제안 — 협의 확정 대상

로봇(Ubuntu·C++/OpenSSL)과 관제가 상호운용하도록 아래 규격을 제안합니다. 모든 문자열은 UTF-8, base64는 표준(RFC 4648, 개행 없음)입니다.

키쌍RSA-2048. 공개키(publicKey)는 DER SubjectPublicKeyInfo(SPKI)를 base64 인코딩(PEM 헤더·개행 없음). OpenSSL i2d_PUBKEY → base64.
비밀키 암호화
(secretKeyEncrypted)
RSA-OAEP — 해시 SHA-256 · MGF1 SHA-256 · label 없음. 서버가 로봇 공개키로 암호화한 base64 를 로봇이 개인키로 복호화.
OpenSSL: RSA_PKCS1_OAEP_PADDING + set_rsa_oaep_md(sha256) + set_rsa_mgf1_md(sha256).
HMAC 비밀키복호화 결과 문자열을 그대로(ASCII 바이트) HMAC 키로 사용 — base64 디코드 금지.
서명HMAC-SHA256, 출력은 소문자 hex. X-Robot-Signature = HMAC-SHA256(secret, 정규화문자열).
정규화 문자열
(서명 대상)
{METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)} — METHOD 대문자, 구분자 \n(0x0A), sha256hex(body) = 요청 본문 바이트의 SHA-256 소문자 hex.
인증 헤더X-Robot-ID(robotId UUID) · X-Robot-Ts(ISO-8601 +09:00) · X-Robot-Signature(위 HMAC hex)
타임스탬프ISO-8601 KST(예 2026-07-08T14:30:00+09:00). 서버 시각과 ±60초 이내(NTP 동기 필요) — 초과 시 ROBOT_TIMESTAMP_OUT_OF_WINDOW.

위 값은 비스캣 제안 규격이며 제로웍스 확인 후 확정합니다. 인증서 발급은 현재 서버 생성(원클릭) + 다운로드 URL 번들 방식이며, 필요 시 CSR 서명 방식(로봇이 mTLS 키 생성·CSR 제출, 개인키 미전송)으로 협의 가능합니다. 협의 확정 대상

POST /v1/robot/key — 비밀키 발급

인증없음 (사전 등록된 robotCode + 공개키)
요청
{ "robotCode": "R-001", "publicKey": "base64(SPKI DER)" }   // robotCode = 관리자 등록 식별자, publicKey = RSA-2048 공개키(DER SubjectPublicKeyInfo → base64)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "robotId": "uuid",                // 관제가 robotCode 로 조회해 반환하는 내부 식별자 — 이후 인증·MQTT 에 사용
    "secretKeyEncrypted": "base64",   // 로봇 공개키로 암호화된 HMAC 비밀키
    "issuedAt": "2026-05-21T10:00:00+09:00"
  }
}
재발급이미 발급(활성화)된 로봇이 다시 호출하면 409 ROBOT_ALREADY_ACTIVATED로 거절됩니다. 재발급이 필요하면 관제 관리자 초기화 후 다시 발급받습니다(이전 Thing·인증서는 폐기).
에러404 ROBOT_NOT_REGISTERED(robotCode 미등록), 422 ROBOT_PUBLIC_KEY_INVALID(공개키 형식 오류)

POST /v1/robot/cert — X.509 인증서 발급 (다운로드 URL 목록)

관제가 AWS IoT Core에 로봇 Thing(thingName = robotId)과 X.509 인증서를 발급하고, 인증서 번들을 저장한 뒤 파일별 다운로드 URL 목록을 회신합니다. 로봇은 각 URL로 파일을 내려받아 설치하고 mTLS로 MQTT에 연결합니다.

인증HMAC (로봇)
번들 파일certificate.pem.crt(디바이스 인증서) · private.pem.key(개인키) · public.pem.key(공개키) · AmazonRootCA1.pem(서버 검증 CA)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "files": [                          // 번들 파일별 다운로드 URL
      { "name": "certificate.pem.crt", "url": "https://.../certificate.pem.crt?..." },
      { "name": "private.pem.key",     "url": "https://..." },
      { "name": "public.pem.key",      "url": "https://..." },
      { "name": "AmazonRootCA1.pem",   "url": "https://..." }
    ],
    "expiresIn": 600                    // 다운로드 URL 유효기간(초)
  }
}

발급 인증서에는 로봇 정책(zws-iot-policy-fms, Thing 변수 기반)이 부착되어 로봇은 자기 토픽만 pub/sub 합니다. 번들은 암호화 저장되며, 다운로드 URL은 expiresIn 경과 시 만료됩니다.

재발급 정책 — 로봇은 등록·인증으로 비밀키·인증서를 발급받으며, 이미 발급(활성화)된 로봇이 /key·/cert를 다시 호출하면 409 ROBOT_ALREADY_ACTIVATED로 거절됩니다. 재발급(인증서 회전)이 필요하면 관제 관리자 초기화 후 다시 발급받으며, 이때 이전 Thing·인증서·번들은 폐기됩니다.

HMAC 서명 방식

발급받은 비밀키로 요청을 서명합니다. X-Robot-Signature = HMAC-SHA256(secret, 정규화문자열)(소문자 hex), 정규화 문자열은 {METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)}입니다. X-Robot-Ts는 ±60초 윈도우를 벗어나면 ROBOT_TIMESTAMP_OUT_OF_WINDOW로 거절(replay 방지)됩니다.

예) POST /v1/robot/cert · body {} → 정규화 문자열:

POST
/v1/robot/cert
2026-07-08T14:30:00+09:00
44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a

마지막 줄은 sha256hex("{}"). 정규화 문자열의 줄 구분자는 \n(LF)입니다.

C++/OpenSSL 레퍼런스 구현 비스캣 제안

로봇(Ubuntu · C++)에서 위 규격을 구현하는 최소 예제입니다. OpenSSL 3.x 기준(빌드: g++ auth.cpp -lssl -lcrypto)이며, 지면상 에러 처리는 생략했습니다. HTTP 호출·JSON 파싱은 libcurl·nlohmann/json 등 임의 라이브러리를 사용합니다.

① 공통 헬퍼 (base64 · hex · SHA-256 · HMAC)

#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <string>
#include <vector>
#include <stdexcept>

static std::string toHex(const unsigned char* d, size_t n) {
    static const char* h = "0123456789abcdef";
    std::string s; s.reserve(n * 2);
    for (size_t i = 0; i < n; ++i) { s += h[d[i] >> 4]; s += h[d[i] & 0xf]; }
    return s;
}

// 표준 base64(RFC 4648, 개행 없음)
static std::string b64encode(const unsigned char* d, int n) {
    std::string out(4 * ((n + 2) / 3), '\0');
    int len = EVP_EncodeBlock((unsigned char*)out.data(), d, n);
    out.resize(len);
    return out;
}
static std::vector<unsigned char> b64decode(const std::string& in) {
    std::vector<unsigned char> out(3 * (in.size() / 4) + 3);
    int len = EVP_DecodeBlock(out.data(), (const unsigned char*)in.data(), (int)in.size());
    if (len < 0) throw std::runtime_error("base64 decode 실패");
    if (!in.empty() && in.back() == '=') len--;            // 패딩 보정
    if (in.size() >= 2 && in[in.size() - 2] == '=') len--;
    out.resize(len);
    return out;
}

static std::string sha256Hex(const std::string& data) {
    unsigned char md[EVP_MAX_MD_SIZE]; unsigned int n = 0;
    EVP_Digest(data.data(), data.size(), md, &n, EVP_sha256(), nullptr);
    return toHex(md, n);
}
static std::string hmacSha256Hex(const std::string& key, const std::string& msg) {
    unsigned char mac[EVP_MAX_MD_SIZE]; unsigned int n = 0;
    HMAC(EVP_sha256(), key.data(), (int)key.size(),
         (const unsigned char*)msg.data(), msg.size(), mac, &n);
    return toHex(mac, n);
}

② 키쌍 생성 · 공개키 인코딩

#include <openssl/rsa.h>
#include <openssl/x509.h>   // i2d_PUBKEY

// RSA-2048 키쌍 생성. 개인키는 디바이스에 안전 저장(예: PEM_write_PrivateKey_ex), 외부 반출 금지.
EVP_PKEY* generateRobotKeyPair() { return EVP_RSA_gen(2048); }   // OpenSSL 3.0+

// 공개키 → base64(SPKI DER). POST /v1/robot/key 의 publicKey 필드에 그대로 사용.
std::string publicKeyB64(EVP_PKEY* pkey) {
    unsigned char* der = nullptr;
    int len = i2d_PUBKEY(pkey, &der);        // SubjectPublicKeyInfo(DER)
    if (len <= 0) throw std::runtime_error("i2d_PUBKEY 실패");
    std::string b64 = b64encode(der, len);
    OPENSSL_free(der);
    return b64;
}

③ 비밀키 복호화 (RSA-OAEP · SHA-256/MGF1 SHA-256)

// secretKeyEncrypted(base64) → HMAC secret 문자열. OAEP 해시 SHA-256 / MGF1 SHA-256.
std::string decryptSecret(EVP_PKEY* priv, const std::string& secretKeyEncryptedB64) {
    std::vector<unsigned char> ct = b64decode(secretKeyEncryptedB64);
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(priv, nullptr);
    EVP_PKEY_decrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256());
    EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256());   // ← SHA-256 로 명시(서버와 일치)
    size_t outLen = 0;
    EVP_PKEY_decrypt(ctx, nullptr, &outLen, ct.data(), ct.size());
    std::string secret(outLen, '\0');
    EVP_PKEY_decrypt(ctx, (unsigned char*)secret.data(), &outLen, ct.data(), ct.size());
    secret.resize(outLen);
    EVP_PKEY_CTX_free(ctx);
    return secret;   // 그대로 HMAC 키로 사용 — base64 디코드 금지
}

④ 타임스탬프 · HMAC 서명 헤더

#include <ctime>

// ISO-8601 KST (예: 2026-07-08T14:30:00+09:00). NTP 로 시각 동기 필요(±60초).
std::string nowKstIso8601() {
    std::time_t t = std::time(nullptr) + 9 * 3600;   // UTC+9
    std::tm tm{}; gmtime_r(&t, &tm);
    char buf[40];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S+09:00", &tm);
    return buf;
}

// POST /v1/robot/cert 서명 헤더 구성.
void buildCertHeaders(const std::string& robotId, const std::string& secret,
                      std::string& xId, std::string& xTs, std::string& xSig, std::string& body) {
    body = "{}";
    xId  = robotId;
    xTs  = nowKstIso8601();
    // 정규화: {METHOD}\n{path}\n{X-Robot-Ts}\n{sha256hex(body)}
    std::string canonical = "POST\n/v1/robot/cert\n" + xTs + "\n" + sha256Hex(body);
    xSig = hmacSha256Hex(secret, canonical);
}

⑤ 호출 순서

int main() {
    EVP_PKEY* priv = generateRobotKeyPair();
    std::string pub = publicKeyB64(priv);

    // 1) POST /v1/robot/key   body: {"robotCode":"R-001","publicKey":pub}
    //    응답(data): robotId, secretKeyEncrypted        (HTTP·JSON 은 libcurl·nlohmann/json 등)
    std::string robotId   = /* 응답 파싱 */ "";
    std::string secretEnc = /* 응답 파싱 */ "";

    std::string secret = decryptSecret(priv, secretEnc);   // HMAC 키 확보

    // 2) POST /v1/robot/cert  (서명 헤더 + body {})
    std::string xId, xTs, xSig, body;
    buildCertHeaders(robotId, secret, xId, xTs, xSig, body);
    //   헤더:  X-Robot-ID: xId  ·  X-Robot-Ts: xTs  ·  X-Robot-Signature: xSig
    //   응답(data.files[]) 의 다운로드 URL 을 일반 HTTPS GET 으로 각각 다운로드 →
    //   certificate.pem.crt · private.pem.key · AmazonRootCA1.pem 으로 mTLS MQTT 접속(clientId=robotId).
    return 0;
}

3. MQTT 채널·토픽 네임스페이스 협의 대상

인증서 설치 후 로봇은 MQTT 브로커에 연결합니다. 토픽은 방향 프리픽스를 앞에 둔 {방향}/{robot_id}/{항목}/{명령} 형태입니다 — s2r(server→robot, 로봇 subscribe), r2s(robot→server, 로봇 publish). 브로커 정책으로 로봇은 r2s/{자기id}/#만 publish, s2r/{자기id}/#만 subscribe할 수 있어 디바이스 토픽이 격리됩니다.

토픽방향용도QoS
s2r/{robot_id}/state/get관제 → 로봇상태 질의 (조회 필드 지정)1
s2r/{robot_id}/state/set관제 → 로봇상태 설정1
r2s/{robot_id}/state로봇 → 관제상태 발행 (변동 필드 이벤트, retain)0
r2s/{robot_id}/alarm로봇 → 관제알람 발행1
s2r/{robot_id}/alarm관제 → 로봇알람 리셋1
s2r/{robot_id}/goals/get관제 → 로봇goal 질의1
s2r/{robot_id}/goals/set관제 → 로봇goal 설정 (입찰 없이 즉시 수행)1
r2s/{robot_id}/goals로봇 → 관제goal 수행 상태 발행1
s2r/{robot_id}/goals/offer관제 → 로봇입찰 요청1
r2s/{robot_id}/goals/bid로봇 → 관제입찰 응답 (예상 도착시간)1
s2r/{robot_id}/goals/result관제 → 로봇입찰 결과 (낙찰 통보)1
s2r/{robot_id}/quote/offer관제 → 로봇배송 가능여부 견적 질의 (가용성·ETA, 비구속)1
r2s/{robot_id}/quote/bid로봇 → 관제배송 가능여부 견적 응답 (available·estArrival)1
r2s/{robot_id}/resource로봇 → 관제인프라(엘베/로비폰) 요청 — 로비폰은 orderId+lobbyId, 엘베는 orderId+elevatorCode+탑승층·목적층1
s2r/{robot_id}/resource/ready관제 → 로봇요청 처리 완료 → 진행 신호1
s2r/{robot_id}/resource/status관제 → 로봇엘베 중간 상태(serviceStatus) + 배정 호기(elId) 중계1
s2r/{robot_id}/box/open관제 → 로봇박스 개방 지시 (오토메타 웹 개방·상차/하차) 제안 §6.11
r2s/{robot_id}/box/loaded로봇 → 관제상차 완료 보고 (닫힘 = 출발 트리거) 제안 §6.11
r2s/{robot_id}/box/unloaded로봇 → 관제하차 완료 보고 (수령 완료) 제안 §6.11

네임스페이스 확정 항목

4. robot info — 상태·알람

4.1 상태 질의 — s2r/{robot_id}/state/get

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": { "fields": ["mode"] }     // 조회할 상태 필드 목록
}

상태 설정 — s2r/{robot_id}/state/set

관제가 일부 상태 필드를 설정합니다. 본문은 상태 발행과 동일 구조의 부분 집합을 사용합니다.

상태 발행 — r2s/{robot_id}/state

robot 상태가 변하는 필드값을 이벤트 단위로 로봇이 발행합니다. retain=true 권장, 최소 발행 간격(초 단위) 설정 가능. 자발·고빈도 텔레메트리이므로 requestId는 생략 가능하며 occurredAt으로 순서를 판단합니다.

{
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "online": true,
    "mode": "bringup",
    "status": "doing",
    "ems": ["button", "door", "LiDAR", "bumper"],
    "location_score": 0,
    "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
    "battery": {
      "voltage": 0, "current": 0, "soc": 0,
      "est_charge_complete": 0, "est_discharge_complete": 0,
      "soh": 0, "temperature": 0
    }
  }
}
필드타입단위설명
payload.onlineboolean-로봇 접속 여부 — true(온라인) / false(오프라인). 접속 시 true, 연결 끊김 시 LWT로 false. 관제는 이 값으로 활성(배차 대상) 로봇을 판별합니다(아래 접속 상태 callout).
payload.modeenum-bringup · ready · navi · cruise · delivery · trash_pickup · mapping
payload.statusenum-doing · done · paused
payload.emsstring[]-EMS 트리거 소스 (button · door · LiDAR · bumper)
payload.location_scorenumber0~100위치 추정 신뢰도
payload.location.x / y / thnumber-위치 좌표 및 방향
payload.location.map_idstring-맵 식별자
payload.battery.voltagenumberV전압
payload.battery.currentnumbermA전류
payload.battery.socnumber% (0~100)충전 상태
payload.battery.est_charge_completenumbermin충전 완료 예상 시간
payload.battery.est_discharge_completenumbermin방전 완료 예상 시간
payload.battery.sohnumber% (0~100)배터리 수명
payload.battery.temperaturenumber°C배터리 온도

도어 상태는 미션 컨텍스트(어느 오더의 어느 칸인지)에서 의미가 있으므로 state가 아니라 goals[].mission[].door[]로 발행합니다(§5). ems의 트리거 소스 door(도어로 인한 EMS)와는 별개입니다.

변동 주기가 다른 필드(고빈도 위치 vs 저빈도 battery soh/temperature vs 희소 mode/status 전이)는 토픽을 더 분리해 각자 cadence·retain을 따로 줄 수 있습니다. 다만 과분할 시 스냅샷 일관성(동일 시점 상태 재구성)이 어려워지므로 분리 경계는 함께 확정합니다. 분리 경계 협의

접속 상태(online)와 LWT

online은 로봇의 MQTT 접속 여부를 나타내며, 관제가 배차 대상(활성) 로봇을 추리는 기준입니다. 로봇은 다음과 같이 관리합니다.

오프라인 감지 지연은 브로커 keepalive 주기에 좌우됩니다(LWT는 keepalive 만료 후 발동). 배차에서 오래된 online: true가 남는 창을 줄이려면 keepalive를 짧게 두는 편이 유리합니다. keepalive 값 협의

4.2 알람 발행 — r2s/{robot_id}/alarm

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "alarm": [
      { "code": "0000", "level": "warn", "msg": "" },
      { "code": "1000", "level": "warn", "msg": "" }
    ]
  }
}
필드타입설명
payload.alarm[].codestring알람 코드
payload.alarm[].levelenumwarn · error · fatal
payload.alarm[].msgstring알람 메시지

알람 리셋 — s2r/{robot_id}/alarm

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": { "alarm": "reset" }      // 알람 리셋 명령
}

알람 코드·레벨별 관제 대응 협의 대기

레벨별 관제 동작 방향(예시)은 아래와 같으며, 실제 코드·레벨 매핑은 양사 시나리오 협의로 확정합니다. 정해지는 대로 관제 대응에 반영합니다.

level관제 대응(예시)리셋
warn로그 저장 및 대시보드 표시자동
error홈 복귀 및 배차 제외 → 운영자 알림자동
fatal즉시 중단 및 배차 제외 → 운영자 확인수동

5. goals info — 미션·입찰

로봇 서비스는 mode → goal → mission 3계층으로 정의합니다. mode는 로봇이 현재 무슨 서비스를 수행 중인지(robot 레벨), goal은 어디로 가는지(목적지 단위), mission은 그 지점에서 무엇을 하는지(동작)를 나타냅니다. 각 계층의 어휘는 서로 겹치지 않게 분리합니다 — 장소 성격은 goal.type, 동작은 mission.type에만 둡니다.

하나의 mission은 하나의 고객 오더 단위이며, 동일 mission.id가 여러 goal에 걸쳐 존재할 수 있습니다 — 예: 픽업 goal과 하차 goal이 같은 오더(mission_12)로 묶입니다. 미션을 로봇에 부여하는 경로는 입찰(offer → bid → result) 또는 즉시 설정(set) 두 가지이며, 부여 후 로봇은 r2s/{robot_id}/goals로 수행 상태를 발행합니다. 멀티스톱 배송은 goal 배열로 표현합니다.

계층위치의미값(enum)
modestate.mode로봇 서비스 종류 / 동작 모드bringup · ready · navi · cruise · delivery · trash_pickup · mapping
goal.typegoals[].type목적지(지점)의 성격customer(고객) · store(매장) · unloading(하역장) · charging(충전소) · waiting(대기장소) · none
mission.typegoals[].mission[].type지점에서 수행하는 동작pickup · dropoff · wait · charge

mode 전이 — 충전 복귀는 navi

배송 로봇은 배차 전 대기·이동, 그리고 다음 배송으로의 연속 배차까지 mode=delivery를 유지합니다(일감 유무·진행은 state.status·goal.status로 표현). 단 미션 완료 후 충전소 복귀delivery가 아니라 mode=navi + goal.type=charging(mission.type=charge)으로 전이합니다 — 관제 지시 또는 다음 배차가 없을 때 로봇이 스스로 복귀하는 timeout 자동 회차. 동작 흐름은 시나리오 §4 참조. timeout 값 협의

5.1 goal 설정 — s2r/{robot_id}/goals/set 입찰 없이 즉시 수행

goal은 mission[] 배열을 품습니다. 아래 예시는 한 오더(mission_12)의 픽업(매장 goal)·하차(고객 goal)가 같은 mission.id로 두 goal에 걸친 모습입니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "goal_1126",
        "type": "store",
        "status": "going",
        "arrivalOrder": 1,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          {
            "id": "mission_12",
            "type": "pickup",
            "status": "ready",
            "atomic": true,
            "PIN": null,
            "door": [ { "name": "door2", "status": "closed" } ],
            "orderId": "",
            "address": "",
            "addressDetail": "",
            "phoneNumber": "",
            "message": ""
          }
        ]
      },
      {
        "id": "goal_1127",
        "type": "customer",
        "status": "ready",
        "arrivalOrder": 2,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          {
            "id": "mission_12",
            "type": "dropoff",
            "status": "ready",
            "atomic": true,
            "PIN": "1234",
            "door": [ { "name": "door2", "status": "closed" } ],
            "orderId": "",
            "address": "",
            "addressDetail": "",
            "phoneNumber": "",
            "message": ""
          }
        ]
      }
    ]
  }
}
필드타입설명
goals[].idstringgoal 식별자
goals[].typeenum지점 성격 — customer · store · unloading · charging · waiting · none
goals[].statusenumready · going · arrived · canceled
goals[].arrivalOrdernumber도착 순서 (미지정 시 가장 마지막)
goals[].location.x / y / thnumber위치 좌표 및 방향
goals[].location.map_idstring맵 식별자
goals[].mission[].idstring미션 식별자 (= 고객 오더 단위, 여러 goal에 걸쳐 동일 id 가능)
goals[].mission[].typeenum동작 — pickup · dropoff · wait · charge
goals[].mission[].statusenumready · doing · done · canceled
goals[].mission[].atomicboolean미션 원자성 — 묶음 순서 보존 여부 (아래 설명)
goals[].mission[].PINstring | null적재함 인증 PIN (로봇 PIN 입력 방식 시 값, 그 외 null — §6)
goals[].mission[].door[].namestring대상 도어 이름 (관제가 지정)
goals[].mission[].door[].statusenum도어 상태 — opening · opened · closing · closed (로봇이 발행)
goals[].mission[].orderIdstring주문 식별자
goals[].mission[].address / addressDetailstring주소 · 상세 주소
goals[].mission[].phoneNumberstring연락처
goals[].mission[].messagestring메시지
goals[].mission[].autoDropOffboolean비대면 자동 하차 여부. 배차 로봇의 robot.auto_drop_off=true일 때만 true(하차 mode auto). 그 외 false(하차 mode attended, PIN 선전달) — §6

mission.atomic — 묶음 순서 보존

atomic은 한 미션(고객 오더)에 속한 goal들의 순서가 깨지지 않고 묶여서 유지됨을 보장하는 옵션입니다. 이후 다른 미션이 추가되어 거리 최적화로 재정렬되더라도, atomic 미션에 속한 goal들의 상대 순서는 보존되며 다른 미션의 goal이 그 사이에 끼어들지 않습니다.

확정 필요: 입찰·배차 단위(goal 단위 vs 미션 묶음 단위), goal.type: waiting / none 의미, arrivalOrder 동률·미지정 처리, trash_pickup의 goal/mission 매핑, 한 미션에 goal이 다수(예: 5개 이상)일 때 배차(복수 로봇 동시 호출 등) 정책. 협의 대기

5.2 goal 수행 발행 — r2s/{robot_id}/goals

수행 중 goal 상태(진행률·도착예상·상태)를 로봇이 발행합니다. retain=true. 관제는 필요 시 goals/get으로 on-demand 조회합니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      {
        "id": "goal_1126",
        "type": "store",
        "status": "going",
        "arrivalOrder": 1,
        "estArrival": 0,
        "progress": 0,
        "location": { "x": 0.0, "y": 0.0, "th": 0.0, "map_id": "" },
        "mission": [
          { "id": "mission_12", "type": "pickup", "status": "ready", "atomic": true, "PIN": null, "door": [ { "name": "door2", "status": "closed" } ], "orderId": "" }
        ]
      }
    ]
  }
}
필드타입단위설명
goals[].estArrivalnumbermin도착 예상 시간
goals[].progressnumber0~100진행률
goals[].statusenum-ready · going · arrived · canceled
goals[].mission[].*--goal 설정 토픽(§5.1)의 mission 필드와 동일

5.3 입찰 (offer → bid → result)

입찰은 3단계입니다. 관제가 후보 로봇에 offer로 제시 → 각 로봇이 bid로 goal별 예상 도착시간 응답 → 관제가 전체 ETA(각 goal estArrival 합) 최소 로봇으로 배차를 결정(추후 적재공간 기준 포함)해 result로 낙찰 여부를 통보합니다. 어느 로봇이 수행할지의 최종 배차 결정은 관제가 단독으로 가집니다(배차 확정·충돌 방지).

입찰 라운드 마감

입찰·낙찰은 goalId 단위로 식별합니다(별도 오퍼 식별자 없음). 라운드는 단발이며 재오퍼하지 않습니다 — bid 타임아웃 시점에 관제가 모인 입찰로 배차를 결정하고, 마감 후 도착한 지각 bid는 배제합니다. (로봇, goal)당 in-flight 오퍼는 1건입니다. 입찰 가능한 로봇이 없으면 해당 미션은 수행 불가로 종료됩니다. 타임아웃 값 협의

offer 대상과 타임아웃

① 입찰 요청 — s2r/{robot_id}/goals/offer

goals/set동일한 본문 구조(goal 전체 정의)를 사용합니다. 입찰 요청 시 전달할 필드 범위(전체 goal vs 요약)는 확정 대상입니다. 필드 범위 협의

② 입찰 응답 — r2s/{robot_id}/goals/bid

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      { "id": "goal_1124", "arrivalOrder": 0, "estArrival": 0 }
    ]
  }
}

③ 입찰 결과 — s2r/{robot_id}/goals/result

{
  "requestId": "uuid",
  "occurredAt": "2026-06-04T10:00:00+09:00",
  "payload": {
    "goals": [
      { "id": "goal_1124", "claimed": true, "reason": null }
    ]
  }
}
필드타입설명
goals[].estArrivalnumber도착 예상 시간(min) bid
goals[].claimedboolean낙찰 여부 result
goals[].reasonstring | null미낙찰 사유 result

5.4 배송 가능여부 견적 (quote) 비스캣 제안 — 협의 확정 대상

주문이 확정되기 전, 관제가 후보 로봇에게 "지금 이 배송을 수행할 수 있는지"를 미리 물어 가용성·예상 도착시간을 받아 두는 사전 견적입니다. 미션(goal)을 생성하지 않는 비구속 질의이므로, 배차 입찰(§5.3 goals/*)과 토픽을 분리해 quote/* 네임스페이스로 운영합니다. 낙찰(result) 단계가 없어 입찰의 3단계를 2단계(질의 → 견적)로 축약합니다. 관제는 모든 goal 이 available인 로봇 중 전체 ETA(각 goal estArrival 합) 최소를 가용 ETA로 집계합니다(배차 낙찰과 동일 방식).

입찰(goals)과의 차이

① 견적 질의 — s2r/{robot_id}/quote/offer

입찰 요청(goals/offer)과 마찬가지로 goals/set과 동일한 본문 구조(goal 전체 정의 — §5.1)를 사용합니다. 별도 축약 스키마를 두지 않으며, 미션을 생성하지 않는 비구속 질의라는 점만 다릅니다. 배차 입찰과는 quote/* 토픽으로 구분되며, 견적에는 별도 식별자(견적ID·goalId 바인딩)가 없어 응답 상관은 토픽의 robot_id로 판별합니다.

② 견적 응답 — r2s/{robot_id}/quote/bid

입찰 응답(goals/bid)과 동일한 goals[] 배열로 회신하되, 예상 도착시간(estArrival)에 수행 가능 여부(available)를 더합니다. 불가 시 available=false·estArrival=null·reason(사유)로 응답합니다. 관제는 모든 goal 이 available일 때만 가용으로 보고 각 goal estArrival을 합산해 예상 도착시간을 산출합니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-05T10:00:03+09:00",
  "payload": {
    "goals": [
      { "id": "goal_1124", "arrivalOrder": 1, "available": true, "estArrival": 7, "reason": null }
    ]
  }
}
필드타입단위설명
goals[].idstring-goal 식별자(참고용, 빈 문자열 가능) — 견적 상관은 토픽 robot_id로 하며 이 값에 의존하지 않음
goals[].arrivalOrdernumber-도착 순서
goals[].availableboolean-수행 가능 여부 (입찰 bid 대비 추가 필드)
goals[].estArrivalnumber | nullmin예상 도착시간 (불가 시 null)
goals[].reasonstring | null-불가 사유 (available=false 시)

라운드 마감 · 무응답 처리

입찰과 동일하게 단발 라운드(재질의 없음)이며, bid 타임아웃도 배차와 동일(초기값 3초)입니다. 타임아웃 내 무응답 로봇은 available=false로 간주하고 지각 응답은 배제합니다. 후보 전원이 unavailable·무응답이면 가용성 결과는 수행 가능 로봇 없음으로 집계됩니다. 상관(어느 라운드 응답인지)은 토픽의 robot_id로 판별합니다 — 견적 응답에는 별도 견적 식별자가 없습니다. 운용 중 조정

5.5 인프라 자원 요청 — 엘베·로비폰 제안

로봇이 미션 수행 중 엘리베이터·로비폰(공동현관)이 필요한 시점에 관제로 요청합니다. 주소 정보는 관제가 orderId 로 보유합니다. 단, 엘리베이터는 로봇이 도착한 엘베 코드(elevatorCode)와 탑승층·목적층(sourceFloor·destinationFloor)을 함께 전달합니다 — 관제가 elevatorCode로 해당 엘베 라인(홀)의 호출 식별자를 조회해 MIRI 콜에 사용합니다. 실제 탑승 호기는 로봇이 고르지 않고 엘베 그룹제어가 배정하며, 관제는 배정된 호기(elId)를 resource/status 로 로봇에 전달해 로봇이 그 호기 문 앞으로 이동하도록 안내합니다. 로비폰은 로봇이 개방 대상 lobbyId(예: GOCHEOK-101)를 함께 전달하면, 관제가 lobbyId로 DB(lobbyphone 레지스트리)를 조회해 해당 로비의 siteId·IP·개방시간을 얻어 자동 개방(inBase 호출)합니다 — 로봇은 inBase·IP·토큰을 몰라도 됩니다. 처리 완료 시 관제가 진행 신호를 보내고, 로봇은 다음 단계로 이동합니다.

// 로봇 → 관제 : r2s/{robot_id}/resource  (엘베)
{ "orderId": "ORD-...", "resource": "elevator",
  "elevatorCode": "EL-...", "sourceFloor": "3", "destinationFloor": "15" }

// 로봇 → 관제 : r2s/{robot_id}/resource  (로비폰)
{ "orderId": "ORD-...", "resource": "lobbyphone", "lobbyId": "GOCHEOK-101" }

// 관제 → 로봇 : s2r/{robot_id}/resource/ready
//   result(로비폰): opened | failed   /  result(엘베): ok
{ "orderId": "ORD-...", "resource": "lobbyphone", "result": "opened" }
{ "orderId": "ORD-...", "resource": "elevator",   "result": "ok" }

요청 순서는 로비폰(건물 진입) → 엘베(상층 이동)입니다. 엘베는 로봇이 전달한 elevatorCode 로 관제가 호출 식별자를 조회하고, 로봇이 준 sourceFloor·destinationFloor 로 호출·탑승·하차를 조율하며, 하차 완료 후 진행 신호를 보냅니다. 로비폰은 자동 개방(입주민 호출 없음)이며, 로봇이 보낸 lobbyId 는 관제 DB(lobbyphone.lobby_id, status=active)의 값과 글자까지 정확히 일치해야 합니다 — 불일치·미등록이면 관제가 failed 를 회신합니다(신규 동/단지는 관제 DB에 로비 레코드를 먼저 등록하고 lobbyId 를 공유). 관제가 실제 개방 상태(opened/failed)를 result 로 회신하며, 로봇은 opened 일 때만 진입하고 그 외엔 대기·재시도합니다. 문 열림 시간은 관제 설정값(레코드별 durationSec)이며 로봇이 지정하지 않습니다.

엘베 중간 상태(탑승·하차)

엘베는 호출 후 탑승·하차 단계가 진행됩니다. 관제는 MIRI serviceStatus(호출확정→탑승→하차완료, 24종)를 받아 로봇에 중계(resource/status)하고, 로봇은 실제 탑승·하차 시점에 보고하면 관제가 이를 MIRI thing/status 로 중계합니다. 배정 호기는 콜 배정 확정(sourceFloorCallConfirmed) 시점부터 elId 로 함께 실려 오므로, 로봇은 그 호기 앞에서 탑승을 준비합니다.

// 관제 → 로봇 : s2r/{robot_id}/resource/status  (중간 상태 중계)
{ "resource": "elevator",
  "serviceStatus": "sourceFloorCallConfirmed" | "sourceFloorGetOn" | "destinationFloorGetOff" | …,
  "elId": "EL-..." }

// 로봇 → 관제 : r2s/{robot_id}/resource  (탑승/하차 보고 → 관제가 MIRI thing/status 중계)
{ "orderId": "ORD-...", "resource": "elevator", "status": "boarded" | "gotoff" }

관제는 콜 배정 확정(sourceFloorCallConfirmed) 시점에 배정 호기(elId)를 실어 보내며, 로봇은 그 호기 앞에서 대기합니다. 이어 로봇은 sourceFloorGetOn(탑승 안내) 시 탑승 후 status:"boarded", destinationFloorGetOff(하차 안내) 시 하차 후 status:"gotoff" 를 보고합니다. serviceStatus 어휘와 배정 호기 식별자(elId)는 MIRI 표준값을 그대로 사용하며, 호기 식별자→실제 문 위치 매핑은 제로웍스와 협의해 확정합니다.

6. PIN·박스 인증

고객 수령(하차) 방식은 로봇 H/W의 비대면 지원 여부(robot.auto_drop_off)로 결정되며, 배차/입찰 시점에 goal.mission으로 전달됩니다. 비스캣은 대면 배송에서 항상 userPin(4자리)을 발행하고 오토메타 배차 응답에 동봉합니다.

하차 modePINautoDropOff동작
비대면 (auto) — 로봇 auto_drop_off=true H/W 한정nulltrue도착 즉시 박스 자동 개방·하차 후 복귀 (고객 조작 불필요)
대면 (attended) — 기본랜덤(4자리)false① 로봇 키패드 PIN 입력(로컬 검증) 또는 ② 오토메타 웹 개방(관제→로봇 문열림 지시)

오토메타 웹 개방용 문열림 지시(s2r)와 상차·하차 완료 보고(r2s)의 구체 토픽·페이로드를 아래 6.1비스캣 제안으로 정리했습니다(협의 확정 대상). 로봇 키패드 PIN 입력 경로의 PIN 감사 이벤트(r2s) 토픽·페이로드는 여전히 별도 확정 대상입니다. 토픽·페이로드 협의

6.1 박스 개폐 (open · loaded · unloaded) 비스캣 제안 — 협의 확정 대상

박스 상호작용은 개방 지시(관제→로봇)상차·하차 완료 보고(로봇→관제)로 이뤄집니다. 이를 box/* 네임스페이스로 제안합니다 — 개방 지시 s2r/{robot_id}/box/open, 완료 보고 r2s/{robot_id}/box/loaded·r2s/{robot_id}/box/unloaded. 세 메시지는 모두 행위를 트리거하는 명령·이벤트이므로 §1 규약대로 requestId(멱등)를 유지하고 QoS 1로 전송합니다. 로봇은 주문번호(orderId)를 관장하지 않습니다 — 관제가 주문번호에 대응하는 박스 번호(boxSlots, 1~4)를 지정해 지시하고, 지시·보고는 모두 박스 번호 기준으로 처리합니다. 개방 지시는 로봇이 해당 지점에 도착한 뒤(점포=상차, 세대=하차)에만 유효합니다.

개방 세 경로와의 관계 (§6)

box/open(개방 지시)은 웹 개방(대면 ②)에서만 발행됩니다. 반면 상차·하차 완료 보고(box/loaded·box/unloaded)는 세 경로 공통으로, 개방 방식과 무관하게 닫힘 시점에 항상 발행됩니다.

① 개방 지시 — s2r/{robot_id}/box/open

관제가 열 박스 번호(boxSlots)를 지정해 지시합니다. 관제가 주문번호에 매핑한 번호를 내려주며, 로봇은 지정된 물리 박스 칸만 개방하고 주문번호는 해석하지 않습니다. 상차(점포)·하차(세대) 공용입니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-19T14:00:00+09:00",
  "payload": {
    "boxSlots": [1, 3]                   // 관제가 지정한 개방 박스 번호(1~4)
  }
}
필드타입설명
boxSlotsnumber[]개방할 박스 번호 배열(1~4). 관제가 주문번호에 매핑해 지정 — 로봇은 해당 물리 칸만 개방

② 상차 완료 — r2s/{robot_id}/box/loaded

점포에서 개방된 박스에 적재하고 닫으면 로봇이 보고합니다. 이 닫힘 보고가 배송(세대 이동) 출발 트리거이며, 보고도 박스 번호 기준입니다.

{
  "requestId": "uuid",
  "occurredAt": "2026-07-19T14:03:10+09:00",
  "payload": {
    "boxSlots": [1, 3],                  // 닫힌 박스 번호
    "result": "success"                  // success | fail
  }
}

③ 하차 완료 — r2s/{robot_id}/box/unloaded

세대에서 고객이 수령하고 닫으면 로봇이 보고합니다. 이 보고로 수령 완료·미션 종료를 처리합니다. 본문 구조는 box/loaded와 동일합니다.

필드타입설명
boxSlotsnumber[]대상 박스 번호(개방 지시와 동일 번호)
resultenumsuccess · fail(기구 오류 등)

개방 완료 확인 방식

개방 지시(box/open)의 완료 확인은 현재 별도 응답 토픽 없이 로봇의 goal 수행 발행(r2s/{robot_id}/goals, §5.2)에 실린 박스 개방 상태로 인지하도록 제안합니다. 필요 시 개방 결과 전용 응답 토픽(예: r2s/{robot_id}/box/openedboxSlots·result)으로 승격할 수 있으며, 이 선택은 협의로 확정합니다.

확정 필요 항목

7. 맵 등록 (REST)

로봇이 보유한 지도와 의미 거점 노드(관제가 goal을 내릴 때 참조하는 명명된 지점)를 siteId 기준으로 관제에 등록합니다. 로봇 UI의 업로드 버튼을 누르면 로봇이 보유 맵을 자동 업로드합니다. 흐름은 등록 개시 → 파일 업로드 → 등록 확정 3단계로, 등록 개시 시 siteId·제시 mapId업로드할 파일 목록을 선언하면 관제가 파일별 업로드 URL을 발급하고, 로봇은 지도 이미지·메타(yml)·노드(json) 파일을 각각 해당 URL로 전송합니다(압축하지 않음).

맵 버전 정책

mapId는 로봇(제로웍스)이 제시하며, MQTT 상태·goal의 location.map_id와 동일 값입니다. (siteId, mapId) 조합당 mapVersion이 증가하며 이력을 보관합니다. 등록 확정 시 최신본이 활성 맵이 되고 이전 버전은 비활성으로 전환됩니다(롤백·감사 가능). 관제는 등록된 노드 좌표를 goal의 location으로 사용합니다.

① 등록 개시 — POST /v1/robot/maps

인증: HMAC(로봇). siteId·제시 mapId와 함께 업로드할 파일 목록(files)을 선언하면, 관제가 mapVersion선언한 파일별 업로드 URL을 발급합니다. 지도 메타·노드는 본문에 싣지 않고 ②에서 각 파일로 업로드합니다.

인증HMAC (로봇)
요청
{
  "siteId": "APT-1023",
  "mapId": "zw-map-001",        // 로봇(제로웍스)이 제시하는 맵 식별자
  "files": [                    // 업로드할 파일 목록 선언
    { "name": "map.pgm",    "type": "image" },
    { "name": "map.yml",    "type": "meta"  },
    { "name": "nodes.json", "type": "nodes" }
  ]
}
200 응답
{
  "result": "SUCCESS",
  "data": {
    "mapId": "zw-map-001",
    "mapVersion": 3,
    "uploadExpiresIn": 600,
    "uploads": [                 // 선언한 파일별 업로드 URL
      { "name": "map.pgm",    "uploadUrl": "https://...(map.pgm 업로드 URL)..." },
      { "name": "map.yml",    "uploadUrl": "https://...(map.yml 업로드 URL)..." },
      { "name": "nodes.json", "uploadUrl": "https://...(nodes.json 업로드 URL)..." }
    ]
  }
}
에러404 SITE_NOT_REGISTERED
필드타입설명
siteIdstring단지·현장 식별자 (맵 매핑 기준)
mapIdstring로봇(제로웍스)이 제시하는 맵 식별자. MQTT location.map_id와 동일 값을 사용
files[].namestring업로드할 파일명
files[].typeenumimage(지도 이미지) · meta(yml) · nodes(json)

② 파일 업로드 — {uploadUrl}

로봇이 ① 응답의 uploads[]에 따라 각 파일을 해당 업로드 URL에 그대로 PUT합니다(압축하지 않음). uploadExpiresIn 내에 완료해야 합니다.

파일(type)내용
imageoccupancy grid 등 지도 이미지 (pgm/png 등)
meta (yml)지도 메타데이터 — 해상도(m/px)·원점(x/y/th)·크기 등
nodes (json)의미 거점 노드 목록 — 이름·type(goal.type과 동일 어휘 — store·unloading·charging·waiting 등)·좌표(x/y/th)

선언 목록(files)에 없는 파일은 DB에 반영되지 않고 삭제됩니다. 파일 구성·각 포맷·파일명 규칙은 협의로 확정합니다. 파일 포맷 협의

③ 등록 확정 — POST /v1/robot/maps/{mapId}/commit

업로드 완료를 통보합니다. 관제가 ① 선언 목록의 모든 파일이 업로드됐는지 확인하고, 각 파일을 검증(파싱·필수 항목 확인)해 통과하면 활성화(이전 버전 비활성)합니다. 선언 파일이 하나라도 누락됐거나 검증에 실패하면 거절하며, 선언에 없는 파일은 반영하지 않고 삭제합니다.

인증HMAC (로봇)
200 응답
{
  "result": "SUCCESS",
  "data": {
    "mapId": "zw-map-001",
    "mapVersion": 3,
    "isActive": true,
    "activatedAt": "2026-06-12T10:00:00+09:00"
  }
}
에러422 MAP_UPLOAD_INCOMPLETE(선언 파일 누락), 422 MAP_VALIDATION_FAILED(파일 검증 실패), 409 MAP_UPLOAD_EXPIRED(업로드 URL 만료 후 commit)

8. 추후 범위 (setting · remote) 추후 추가 예정

아래 항목은 1.5차 PoC 범위 밖이며 토픽 자리만 예약합니다. (지도 파일 등록은 §7 REST로 정의되며, 아래 map MQTT 토픽은 추후 실시간 조회·갱신용으로 별도 검토합니다.)

항목토픽
maps info (실시간 조회·갱신)s2r/{robot_id}/map/get · s2r/{robot_id}/map/set · r2s/{robot_id}/map
setting infos2r/{robot_id}/setting/get · s2r/{robot_id}/setting/set · r2s/{robot_id}/setting
remote controls2r/{robot_id}/stream/get · r2s/{robot_id}/stream · s2r/{robot_id}/action/set

9. 에러·거절 코드 (로봇 관련 발췌)

REST 인증 에러 (HTTP status)

코드HTTP의미
ROBOT_SIGNATURE_INVALID401HMAC 서명 검증 실패
ROBOT_TIMESTAMP_OUT_OF_WINDOW401timestamp ±60s 윈도우 초과 (replay 의심)
ROBOT_CERT_REVOKED401인증서 폐기됨
ROBOT_PUBLIC_KEY_INVALID422로봇 공개키(publicKey) 형식 오류 — POST /v1/robot/key
ROBOT_NOT_REGISTERED404사전 등록되지 않은 robotCode
ROBOT_ALREADY_ACTIVATED409이미 활성화된 로봇의 /key·/cert 재발급 시도 (재발급은 관제 관리자 초기화 후)

REST 맵 등록 에러 (§7)

코드HTTP의미
SITE_NOT_REGISTERED404등록되지 않은 siteId
MAP_UPLOAD_INCOMPLETE422commit 시 ① 선언 목록(files)의 파일이 일부 업로드되지 않음
MAP_VALIDATION_FAILED422commit 시 업로드 파일(이미지·yml·json) 검증 실패(파싱·필수 항목 누락 등)
MAP_UPLOAD_EXPIRED409업로드 URL 만료 후 commit 시도

MQTT 입찰 미낙찰 사유 (goals/resultreason)

입찰 단계의 미낙찰은 HTTP가 아니라 goals/result 메시지 goals[].reason 값으로 전달됩니다(claimed=false). 구체 사유 코드 집합은 bid 타임아웃 값과 함께 확정합니다. 사유 코드 협의

전체 에러 카탈로그·유니크 키(재시도 안전)·버전 정책은 비스캣 API 공통 규약을 따릅니다. 규약 상세는 비스캣 담당자를 통해 제공됩니다.