딜러
라이브 테이블을 직접 운영하는 오퍼레이터가 소속 딜러 계정 · 사원증 · 얼굴 사진을 관리하고, 딜러툴 단말 현황과 설치 파일 주소, 딜러 카드 입력 기록을 조회하는 API 입니다. 라이브 테이블을 운영하지 않는 오퍼레이터는 쓰지 않아도 됩니다.
| 메서드 | 경로 | 용도 |
|---|---|---|
| GET | /operator/v1/dealers | 소속 딜러 목록 |
| POST | /operator/v1/dealers | 딜러 계정 만들기 |
| PATCH | /operator/v1/dealers/{dealer_id} | 이름 · 역할 · PIN · 사용 여부 변경 |
| POST | /operator/v1/dealers/{dealer_id}/badge | 사원증 발급 · 재발급 |
| DELETE | /operator/v1/dealers/{dealer_id}/badge | 사원증 회수 |
| PUT | /operator/v1/dealers/{dealer_id}/photo | 얼굴 사진 올리기 |
| DELETE | /operator/v1/dealers/{dealer_id}/photo | 얼굴 사진 지우기 |
| GET | /operator/v1/dealer-devices | 딜러툴 단말 현황 |
| GET | /operator/v1/dealer-tool | 딜러툴 최신 버전 · 설치 파일 주소 |
| GET | /operator/v1/dealer-card-log | 딜러 카드 입력 기록 (독립운영만) |
- 딜러 계정은 이 오퍼레이터 소속으로만 만들고 고칩니다. 소속 딜러는 이 오퍼레이터의 전용 라이브 테이블만 다룹니다.
- 다른 오퍼레이터 소속 딜러를 가리키면 존재하지 않는 딜러와 똑같이
404 DEALER_NOT_FOUND입니다. dealer_id는 접두어 없는 UUID 문자열입니다(예:0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6).- 단말 승인 · 차단 · 테이블 묶기 · 허용 단말 수는 본사가 정합니다. 이 API 로는 단말을 조회만 합니다.
- 쓰기 요청의 응답에는
X-Audit-Id헤더가 실립니다. 계정 변경 · 사진 변경은 변경 이력에서 되돌릴 수 있습니다.
GET /operator/v1/dealers
소속 딜러 목록입니다. 만든 순서로 정렬됩니다.
응답 예시
{
"dealers": [
{
"dealer_id": "0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6",
"login": "maria01",
"name": "Maria Santos",
"role": "dealer",
"status": "active",
"rounds": 1842,
"created_at": "2026-09-02T03:12:44.51Z",
"photo_url": "{API_BASE}/dealer/v1/photos/0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6?v=mgx3k2a1",
"badge_tail": "7QK2",
"badge_issued_at": "2026-09-02T03:15:02.1Z",
"badge_code": "DLR-8M3TZ0RW7QK2"
}
]
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
dealer_id | string | 딜러 ID (UUID) |
login | string | 딜러툴 로그인 아이디 |
name | string | 표시 이름 |
role | string | dealer(딜러) · pitboss(피트보스 — 딜러툴 화면에서 카드를 직접 골라 넣는 수동 입력 등 관리 권한) |
status | string | active · disabled(사용 중지 — 딜러툴에 로그인할 수 없음) |
rounds | int64 | 이 딜러가 진행한 판 수 |
created_at | 시각 | 계정 생성 시각 |
photo_url | string | 얼굴 사진 공개 주소. 없으면 "" |
badge_tail | string | null | 사원증 코드 끝 4자리 (발급 전 · 회수 뒤 null) |
badge_issued_at | 시각 | null | 사원증 발급 시각 |
badge_code | string | 사원증 코드 전체 (DLR- + 12자). 발급되지 않았으면 "" |
사원증 코드 취급
badge_code 는 딜러툴에서 근무를 시작할 때 스캔하는 값입니다. 목록 응답에 전체 코드가 실리므로 관리자 권한이 있는 화면에서만 보여 주세요. 분실하면 회수하거나 재발급하세요.
photo_url 의 사진은 인증 없이 받을 수 있는 공개 주소이며, 사진을 바꾸면 v= 값이 바뀝니다.
POST /operator/v1/dealers
딜러 계정을 만듭니다.
요청 필드
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
login | string | 예 | 소문자 · 숫자로 시작하고 소문자 · 숫자 · _ · - 로 된 2 ~ 32자 (^[a-z0-9][a-z0-9_-]{1,31}$). 대문자는 소문자로, 앞뒤 공백은 지워서 저장합니다. 모든 오퍼레이터가 함께 쓰는 공간이라 다른 오퍼레이터가 쓰는 아이디는 쓸 수 없습니다 |
name | string | 예 | 1 ~ 60바이트(UTF-8). 앞뒤 공백은 지웁니다 |
pin | string | 예 | 딜러툴 로그인 PIN, 4 ~ 32자 |
role | string | 아니오 | dealer(기본) · pitboss |
요청 예시
curl -X POST {API_BASE}/operator/v1/dealers \
-H 'Content-Type: application/json; charset=utf-8' \
-H 'X-Operator: acme' -H 'X-Api-Key: ak_live_…' \
-H 'X-Timestamp: 1760839200123' -H 'X-Nonce: 5d7f9a1c3e5b7d9f1a3c5e7b' -H 'X-Signature: 4c8a…' \
-d '{"login":"maria01","name":"Maria Santos","pin":"482913","role":"dealer"}'응답 예시
201 Created
{ "dealer_id": "0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6" }오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 400 | INVALID_REQUEST (message invalid json) | 본문이 JSON 이 아님 |
| 400 | INVALID_REQUEST (details.fields) | login · name · pin · role 중 형식이 틀린 필드 (여러 개면 모두) |
| 409 | DUPLICATE | 이미 있는 login |
| 500 | INTERNAL_ERROR | 저장 실패 |
PATCH /operator/v1/dealers/{dealer_id}
이름 · 역할 · PIN · 사용 여부를 고칩니다. 보낸 필드만 바꿉니다.
요청 필드
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
name | string | 아니오 | 1 ~ 60바이트 |
role | string | 아니오 | dealer · pitboss |
pin | string | 아니오 | 새 PIN, 4 ~ 32자. "" 이면 바꾸지 않습니다 |
status | string | 아니오 | active · disabled |
사용 중지(disabled)하거나 역할을 바꾸면 그 딜러가 접속해 있던 딜러툴 연결이 끊기고 딜러툴은 로그인 화면으로 돌아갑니다. login 은 바꿀 수 없습니다.
요청 예시
curl -X PATCH {API_BASE}/operator/v1/dealers/0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6 \
-H 'Content-Type: application/json; charset=utf-8' \
-H 'X-Operator: acme' -H 'X-Api-Key: ak_live_…' \
-H 'X-Timestamp: 1760839200123' -H 'X-Nonce: 1a3c5e7b9d1f3a5c7e9b1d3f' -H 'X-Signature: e09b…' \
-d '{"status":"disabled"}'응답
204 No Content (본문 없음)
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 404 | DEALER_NOT_FOUND | 형식이 틀린 ID, 없는 딜러, 다른 오퍼레이터 소속 |
| 400 | INVALID_REQUEST (message invalid json) | 본문이 JSON 이 아님 |
| 400 | INVALID_REQUEST (details.fields) | name · role · status · pin 중 형식이 틀린 필드 |
| 500 | INTERNAL_ERROR | 저장 실패 |
POST /operator/v1/dealers/{dealer_id}/badge
사원증을 발급합니다. 이미 있으면 새 코드로 재발급하며 이전 코드는 즉시 쓸 수 없게 됩니다. 본문은 없습니다(빈 본문 또는 {} 로 서명).
응답 예시
{ "code": "DLR-8M3TZ0RW7QK2", "tail": "7QK2", "login": "maria01", "name": "Maria Santos", "role": "dealer" }| 필드 | 설명 |
|---|---|
code | 사원증 코드 전체 — 바코드 · QR 로 인쇄해 딜러에게 지급합니다 |
tail | 코드 끝 4자리 (목록 badge_tail) |
login · name · role | 사원증에 함께 인쇄할 딜러 정보 |
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 404 | DEALER_NOT_FOUND | 없는 딜러 · 다른 오퍼레이터 소속 |
| 500 | INTERNAL_ERROR | 저장 실패 |
DELETE /operator/v1/dealers/{dealer_id}/badge
사원증을 회수합니다(분실 등). 다시 발급할 때까지 사원증으로 근무를 시작할 수 없습니다. 본문은 없습니다.
응답
204 No Content
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 404 | DEALER_NOT_FOUND | 없는 딜러 · 다른 오퍼레이터 소속 |
| 500 | INTERNAL_ERROR | 저장 실패 |
PUT /operator/v1/dealers/{dealer_id}/photo
딜러 얼굴 사진을 올립니다. 딜러툴 상단의 근무 딜러 카드와 관리 화면 목록에 쓰이며, 근무 중인 딜러툴에는 바로 반영됩니다.
요청 필드
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
image | string | 예 | 이미지 base64, 또는 data URL(data:image/jpeg;base64,…) |
이미지 조건: JPEG 또는 PNG, 가로 · 세로 각각 2048px 이하. 정사각 이미지를 권장합니다.
실제 크기 한도는 요청 본문 64KB 입니다
Operator API 는 요청 본문을 최대 64KB(65,536바이트) 까지만 읽습니다. 사진은 base64 로 실리므로(원본의 약 4/3 배) 원본 이미지가 약 48KB 를 넘으면 본문이 잘려 서명 검증에 실패하고 401 INVALID_SIGNATURE 가 납니다. 올리기 전에 정사각으로 잘라 약 400×400px JPEG 등으로 줄여 48KB 이하로 맞추세요. (사진 자체의 검사 기준은 512KB 이지만, 본문 한도가 먼저 걸립니다.)
요청 예시
IMG=$(base64 -w0 maria.jpg) # macOS: base64 -i maria.jpg
curl -X PUT {API_BASE}/operator/v1/dealers/0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6/photo \
-H 'Content-Type: application/json; charset=utf-8' \
-H 'X-Operator: acme' -H 'X-Api-Key: ak_live_…' \
-H 'X-Timestamp: 1760839200123' -H 'X-Nonce: 9b1d3f5a7c9e1b3d5f7a9c1e' -H 'X-Signature: 77d1…' \
-d "{\"image\":\"data:image/jpeg;base64,${IMG}\"}"서명은 실제로 보내는 본문 바이트 그대로 계산합니다. → 인증 · 서명
응답 예시
{ "photo_url": "{API_BASE}/dealer/v1/photos/0199e8a1-6b2c-7d3e-9f40-a1b2c3d4e5f6?v=mgx3q8z0" }오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 401 | INVALID_SIGNATURE | 본문이 64KB 를 넘어 잘림 (위 경고) — 또는 일반 서명 오류 |
| 404 | DEALER_NOT_FOUND | 없는 딜러 · 다른 오퍼레이터 소속 |
| 400 | INVALID_REQUEST | 본문이 JSON 이 아님 |
| 400 | BAD_PHOTO | base64 가 아님, 빈 이미지, JPEG · PNG 가 아님, 2048px 초과, 512KB 초과 |
| 500 | INTERNAL_ERROR | 저장 실패 |
DELETE /operator/v1/dealers/{dealer_id}/photo
얼굴 사진을 지웁니다. 딜러툴에는 기본 이미지가 보입니다. 본문은 없습니다.
응답
204 No Content
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 404 | DEALER_NOT_FOUND | 없는 딜러 · 다른 오퍼레이터 소속 |
| 500 | INTERNAL_ERROR | 삭제 실패 |
GET /operator/v1/dealer-devices
이 오퍼레이터에 묶인 딜러툴 단말(딜러 PC) 현황입니다. 원격 지원 접속 정보는 싣지 않습니다.
응답 예시
{
"active": 2,
"limit": 4,
"devices": [
{
"id": "0199e9c4-2f1a-7b3c-8d4e-5f6a7b8c9d0e",
"name": "STUDIO-PC-01",
"status": "active",
"online": true,
"logged_in": true,
"dealer": "maria01",
"on_duty": "Maria Santos",
"operator": "acme",
"operator_name": "Acme Casino",
"app_version": "0.2.28",
"os": "windows 11",
"last_ip": "203.0.113.24",
"last_seen_at": "2026-10-19T02:10:11.3Z",
"created_at": "2026-09-01T08:00:00Z",
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"open_tables": ["tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d"],
"setup_ready": true,
"live": {
"app_version": "0.2.28",
"os": "windows 11",
"scanner": "connected",
"stream": { "state": "live", "latency_ms": 180, "bitrate_kbps": 2500, "width": 1920, "height": 1080, "fps": 30 },
"cpu": 23.5,
"mem": 41.2
}
}
]
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
active | int | 승인된(status: active) 단말 수 |
limit | int | null | 본사가 정한 허용 단말 수. 제한이 없으면 null |
devices[].id | string | 단말 ID (UUID) |
devices[].name | string | 단말 이름 |
devices[].status | string | pending(승인 대기) · active(승인) · disabled(차단) |
devices[].online | bool | 지금 연결돼 있는지 |
devices[].logged_in | bool | 딜러가 로그인해 있는지 |
devices[].dealer · on_duty | string | 근무 중인 딜러 아이디 · 이름 (로그인 중일 때만) |
devices[].app_version · os | string | 딜러툴 버전 · 운영체제 |
devices[].last_ip · last_seen_at | 마지막 접속 IP · 시각 | |
devices[].table_id | string | 묶인 테이블 (없으면 생략) |
devices[].requested_table_id | string | 딜러툴이 묶어 달라고 요청한 테이블 (있을 때만) |
devices[].open_tables | string[] | 지금 딜러툴에서 열려 있는 테이블 |
devices[].setup_ready | bool | 연동 설정(게임 서버 · 영상 서버 · 테이블)이 모두 정해졌는지 |
devices[].live | object | 실시간 상태(온라인일 때 마지막 보고) — scanner(connected · disconnected · virtual · none), stream(state: live · connecting · off · error, 지연 · 비트레이트 · 해상도 · fps), cpu · mem(%), update(자동 업데이트 진행 — 있을 때만) |
devices[].last_status | object | 오프라인일 때 참고할 마지막 상태 요약 (있을 때만) |
이 밖에 operator · operator_name · game_server · media_server 같은 연동 설정 필드가 함께 실릴 수 있습니다. 값이 없는 필드는 생략됩니다.
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 500 | INTERNAL_ERROR | 일시적인 서버 오류 |
GET /operator/v1/dealer-tool
딜러 PC 에 딜러툴을 설치할 최신 버전과 내려받기 주소입니다. 관리 화면에 링크를 보여 주고, 딜러 PC 브라우저에서 열어 설치하게 합니다. 설치한 딜러툴은 처음 실행할 때 운영사 코드(operator)를 입력하고, 본사 승인을 받은 뒤에 쓸 수 있습니다.
응답 예시
{
"version": "0.2.28",
"notes": "영상 인코딩 개선",
"pub_date": "2026-10-18T10:00:00Z",
"files": [
{ "platform": "windows-x86_64", "alias": "windows", "file": "R-Live-Dealer_0.2.28_x64-setup.exe", "size": 9876543,
"url": "{API_BASE}/dealer/v1/download/windows" }
],
"operator": "acme"
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
version | string | 게시된 최신 버전. 게시본이 없으면 "" |
notes · pub_date | string | 버전 설명 · 게시 시각 (게시본이 없으면 생략) |
files[] | array | 플랫폼별 설치 파일. 게시본이 없으면 [] |
files[].platform | string | windows-x86_64 · darwin-aarch64(Apple 실리콘) · darwin-x86_64(Intel Mac) |
files[].alias | string | 짧은 이름 windows · mac · mac-intel |
files[].file · size | 파일 이름 · 크기(바이트) | |
files[].url | string | 고정 내려받기 주소 — 새 버전이 게시돼도 같은 주소가 최신 파일을 줍니다 |
operator | string | 딜러툴 첫 화면에 넣을 운영사 코드 (= X-Operator) |
내려받기 주소는 인증이 필요 없지만, 플랫폼이 허용한 스튜디오 네트워크에서만 열리도록 제한될 수 있습니다. 설치된 딜러툴은 새 버전이 게시되면 판이 끝난 뒤 스스로 업데이트합니다.
협의 사항
제공 플랫폼(Windows · macOS), 스튜디오 네트워크 허용 범위, 단말 승인 절차는 계약 시 협의합니다.
GET /operator/v1/dealer-card-log
자기 전용 테이블에 들어온 딜러 카드 입력(스캐너 · 수동 · 가상) 기록입니다. 딜러가 어떤 카드를 언제 어떤 방식으로 넣었고 서버가 받아들였는지 확인하는 감사용입니다.
독립운영 전용
독립운영(independent) 오퍼레이터만 쓸 수 있습니다. 알방식(prepaid) 오퍼레이터의 테이블은 본사가 운영하므로 403 CARD_LOG_FORBIDDEN 입니다. 공용 테이블 입력은 보이지 않습니다.
쿼리 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
from · to | 시각 | 아니오 | 서버가 입력을 받은 시각(received_at) 기준 [from, to). 기본 최근 24시간. 형식은 공통 조회 파라미터와 같습니다 |
table_id | string | 아니오 | 이 테이블만 (형식이 틀리면 빈 결과) |
round_id | string | 아니오 | 이 라운드만 (형식이 틀리면 빈 결과) |
dealer | string | 아니오 | 이 딜러 아이디(login)만 |
source | string | 아니오 | scanner · wedge · manual · virtual |
result | string | 아니오 | accepted · rejected · no_reply |
group | round | 아니오 | round 이면 입력 대신 판별 묶음을 돌려줍니다 |
no_round | 1 | 아니오 | 판에 속하지 않은 입력만 (입력 목록에서만) |
limit | 정수 | 아니오 | 기본 100, 최대 500 |
offset | 정수 | 아니오 | 건너뛸 행 수 |
응답 예시 — 입력 목록
{
"entries": [
{
"received_at": "2026-10-19T02:10:04.512Z",
"id": "0199f3c1-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
"dealer": "maria01",
"role": "dealer",
"device_id": "0199e9c4-2f1a-7b3c-8d4e-5f6a7b8c9d0e",
"device_name": "STUDIO-PC-01",
"ip": "203.0.113.24",
"source": "scanner",
"card": "AS",
"raw": "1S14",
"scanner_id": "HID-0A3F",
"scanned_at": "2026-10-19T02:10:04.43Z",
"lag_ms": 82,
"result": "accepted",
"position": "seat_3",
"reject_code": null,
"reject_reason": null,
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"table_name": "Live Hold'em 1",
"table_names": { "en": "Live Hold'em 1" },
"game_code": "live_holdem",
"round_id": "rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7",
"round_no": 66,
"round_status": "settled"
}
],
"has_more": false
}입력 필드
| 필드 | 타입 | 설명 |
|---|---|---|
received_at | 시각 | 서버가 받은 시각 (UTC) |
id | string | 입력 기록 ID |
dealer · role | string | 입력한 딜러 아이디 · 역할 |
device_id · device_name | string | 입력한 단말 |
ip | string | null | 단말 IP |
source | string | scanner · wedge(키보드 입력형 스캐너) · manual(피트보스 수동 입력) · virtual(가상 스캐너 — 테스트 환경) |
card | string | null | 카드 (AS 등) |
raw | string | null | 스캐너가 읽은 원본 값 |
scanner_id | string | null | 스캐너 식별값 |
scanned_at | 시각 | null | 단말이 읽은 시각 |
lag_ms | int | null | 단말이 읽은 뒤 서버가 받기까지 걸린 시간(ms). 단말 시계 차이가 포함됩니다 |
result | string | accepted(받아들임) · rejected(거절) · no_reply(응답 없음) |
position | string | null | 카드 자리 (예: seat_3) |
reject_code · reject_reason | string | null | 거절 코드 · 사유 |
hidden | bool | true 면 진행 중인 판이라 card · raw 를 가렸습니다 (있을 때만) |
table_id · table_name · table_names · game_code | 테이블 | |
round_id · round_no · round_status | 라운드 (판에 속하지 않으면 null) |
진행 중인 판(round_status: "open")의 입력은 카드 값(card · raw)을 null 로 가리고 hidden: true 를 붙입니다. 판이 끝나면 보입니다.
응답 예시 — 판별 묶음 (group=round)
{
"rounds": [
{
"table_id": "tbl_0198c1d2e3f47a5b8c9d0e1f2a3b4c5d",
"table_name": "Live Hold'em 1",
"table_names": { "en": "Live Hold'em 1" },
"game_code": "live_holdem",
"round_id": "rnd_0199f3a2c4e57b1d9a2e4f6081c3d5e7",
"round_no": 66,
"round_status": "settled",
"first_at": "2026-10-19T02:10:01.2Z",
"last_at": "2026-10-19T02:11:01.9Z",
"n": 19, "n_accepted": 19, "n_rejected": 0, "n_no_reply": 0,
"n_scanned": 19, "n_manual": 0, "n_virtual": 0, "n_late": 0, "n_pitboss": 0,
"max_lag_ms": 140,
"dealers": ["maria01"],
"devices": ["STUDIO-PC-01"]
}
],
"has_more": true
}| 필드 | 설명 |
|---|---|
first_at · last_at | 그 판의 첫 · 마지막 입력 시각 |
n · n_accepted · n_rejected · n_no_reply | 입력 수 · 결과별 수 |
n_scanned · n_manual · n_virtual | 입력 방식별 수 (n_scanned = scanner + wedge) |
n_late | 스캔부터 서버 수신까지 5초를 넘긴 입력 수 |
n_pitboss | 피트보스가 넣은 입력 수 |
max_lag_ms | 가장 긴 지연(ms). 스캔 시각이 하나도 없으면 null |
dealers · devices | 입력한 딜러 · 단말 |
판에 속하지 않은 입력(판 시작 전 거절 등)은 테이블별 한 행으로 묶이며 round_id 가 null 입니다. 판 행의 입력을 펼쳐 보려면 같은 조건에 round_id 를 붙여(판 없는 행은 table_id + no_round=1) 입력 목록을 다시 조회합니다. 묶음은 마지막 입력 최신순입니다.
라운드 하나의 입력은 라운드 상세의 card_log 로도 볼 수 있습니다.
페이지 넘기기
offset 방식입니다. has_more: true 면 offset 을 limit 만큼 늘려 다시 조회합니다. 입력 목록은 received_at 최신순입니다.
오류
| HTTP | 코드 | 상황 |
|---|---|---|
| 403 | CARD_LOG_FORBIDDEN | 알방식 오퍼레이터 |
| 500 | INTERNAL_ERROR | 일시적인 서버 오류 |