대화·메시지·분석·음성 API¶
App Backend는 Socket Backend가 공유 DB에 저장한 대화·메시지·분석 결과를 읽습니다. 실시간 세션 생성, STT/TTS, 메시지 write 또는 WebSocket 처리는 이 API의 책임이 아닙니다. 공유 데이터 경계는 공유 데이터 계약, 테넌트 규칙은 워크스페이스와 테넌시, 공통 shape는 스키마를 참고하십시오.
인증과 테넌트¶
모든 경로는 다음 중 하나로 인증합니다.
- access JWT:
ROLE_USER또는ROLE_ADMIN - 워크스페이스 API key:
SCOPE_WORKSPACE_READ
API key는 키에 귀속된 워크스페이스로 항상 격리됩니다. 헤더를 함께 보낼 때는 키의 워크스페이스와 같아야 합니다.
JWT 호출자는 모든 대화 요청에 X-Workspace-Id를 보내십시오. 통계와 음성 프록시는 헤더가
없으면 400입니다. 기준 커밋의 목록·dialogId 상세·sessionId 상세은 헤더가 없을 때
테넌트 컨텍스트 없이 진행될 수 있으므로, 이것을 헤더가 선택이라는 계약으로 해석하면 안 됩니다.
이 현재 격차는 보안과 현재 격차에 설명돼 있습니다.
대화 목록¶
GET /v1/dialogs¶
- 인증: access JWT + 헤더 권장/필수 계약, 또는 워크스페이스 API key
- query:
page: 선택, 기본 1size: 선택, 기본 20, 공통 상한 10,000externalUserId: 선택, 현재 워크스페이스 최종 사용자 식별자scenarioId: 선택, 시나리오 UUID- 결과:
turnCount > 0인 대화만 최신순 페이지 조회 - 부수 효과: 없음
GET /v1/dialogs?page=1&size=20&externalUserId=pilot-001&scenarioId=01900000-0000-7000-8000-000000000401
{
"status": 200,
"message": "success",
"page": {
"page": 1,
"size": 20,
"totalItems": 1,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false,
"first": true,
"last": true
},
"count": 1,
"list": [
{
"dialogId": "01900000-0000-7000-8000-000000000701",
"scenarioId": "01900000-0000-7000-8000-000000000401",
"sessionId": "socket-session-001",
"turnCount": 8,
"currentRiskLevel": "LOW",
"sourceType": "LIVE",
"sttProvider": "WHISPER",
"startedAt": "2026-09-23T10:00:00",
"endedAt": "2026-09-23T10:10:00",
"createdAt": "2026-09-23T10:00:00"
}
]
}
목록 항목의 scenarioId, currentRiskLevel, startedAt, endedAt은 null일 수 있습니다.
200: 페이지 반환400: query 형식, 헤더 UUID 형식 오류401: credential 누락·오류403: JWT 비멤버 또는 API key/헤더 워크스페이스 불일치
대화 상세¶
GET /v1/dialogs/{dialogId}¶
GET /v1/dialogs/sessions/{sessionId}¶
두 경로는 같은 응답을 반환합니다. 첫 번째는 내부 UUID, 두 번째는 Socket Backend 세션 식별자로 조회합니다.
dialogId: UUIDsessionId: 문자열detail: 선택 Boolean, 기본false- 성공:
200 OK - 부수 효과: 없음
detail=false에서도 대화 요약, 대화 단위 바이오마커 요약, 메시지 키워드, 대표 감정과
음성 메타데이터를 제공합니다. detail=true이면 감정 세부 점수, 치매·우울 결과,
바이오마커 metrics를 추가 조회합니다.
{
"status": 200,
"message": "success",
"data": {
"dialogId": "01900000-0000-7000-8000-000000000701",
"sessionId": "socket-session-001",
"summary": {
"dialogSummary": "오늘 건강 상태와 식사 여부를 확인했습니다.",
"dialogKeyword": "건강,식사",
"dialogEmotion": "NEUTRAL"
},
"biomarkerSummary": {
"summary": "발화 전반의 음성 특성을 요약한 결과입니다."
},
"messages": [
{
"messageId": "01900000-0000-7000-8000-000000000702",
"messageTurns": 1,
"speakerType": "USER",
"sttText": "안녕하세요",
"confidenceScore": 0.987,
"audio": {
"url": "https://api.example.com/v1/dialogs/01900000-0000-7000-8000-000000000701/messages/01900000-0000-7000-8000-000000000702/audio",
"format": "WAV",
"duration": 3.8,
"rate": 16000,
"size": 121644
},
"createdAt": "2026-09-23T10:00:03",
"keywords": [
{
"keyword": "어지러움",
"riskCategory": "PAIN",
"detectedAt": "2026-09-23T10:00:04"
}
],
"emotion": {
"bestEmotion": "NEUTRAL"
}
}
]
}
}
detail=true일 때 추가될 수 있는 message 하위 shape:
{
"emotion": {
"bestEmotion": "NEUTRAL",
"detail": {
"sadness": 5,
"neutral": 80,
"happiness": 10,
"angry": 3
}
},
"dementia": {
"control": 72,
"dementia": 28
},
"depression": {
"negative": 35,
"positive": 65
},
"biomarker": {
"metrics": [
{
"key": "energy:rms",
"name": "Speech energy",
"unit": "dB",
"level": "quiet",
"value": -24.5
}
]
}
}
분석 행이 없으면 summary, biomarkerSummary, emotion, dementia, depression,
biomarker 또는 audio가 null/생략될 수 있습니다. audio.url 외의 음성 필드도
개별적으로 null일 수 있습니다.
확정 발화(speechPhase=CONFIRMED)만 messages에 포함됩니다. 취소 발화와 그 하위 분석은
제외되므로 messageTurns가 1, 2, 4처럼 연속되지 않을 수 있습니다.
200: 상세 반환400:dialogId,detail또는 헤더 형식 오류401: credential 오류403: 멤버십 또는 API key/헤더 불일치404: 대화 또는 세션 없음. 테넌트 밖 대화도 존재를 숨기기 위해 404
사용자 그룹 대화 통계¶
통계 두 경로는 테넌트 컨텍스트를 실제로 요구합니다.
- JWT: access JWT + 헤더 + 해당 워크스페이스
OWNER - API key: 귀속 워크스페이스의
SCOPE_WORKSPACE_READ; OWNER 회원 검사는 적용하지 않음 - 집계 대상: 그룹
externalUserId와 현재 워크스페이스 사용자가 매칭되고,startedAt/endedAt이 모두 있으며 지속 시간이 60초 이상인 완료 대화 - soft-delete된 대화는 제외
- 날짜 기준: KST, 시작일·종료일 모두 포함. 내부적으로 대화
startedAt범위를 사용
startDate와 endDate는 각각 선택입니다. 둘 다 있으면 startDate <= endDate여야 하며,
한쪽만 보내면 열린 구간으로 조회합니다.
GET /v1/dialogs/statistics¶
| query | 필수 | 제약 |
|---|---|---|
groupId |
예 | 사용자 그룹 UUID |
startDate |
아니요 | ISO yyyy-MM-dd, KST 포함 시작일 |
endDate |
아니요 | ISO yyyy-MM-dd, KST 포함 종료일 |
GET /v1/dialogs/statistics?groupId=01900000-0000-7000-8000-000000000201&startDate=2026-09-01&endDate=2026-09-30
200: 집계 반환400: 헤더·UUID·날짜 형식 오류 또는 시작일이 종료일보다 늦음401: credential 오류403: JWT 회원이 OWNER가 아니거나 테넌트 인증 실패404: 그룹이 현재 워크스페이스에 없음- 부수 효과: 없음
GET /v1/dialogs/statistics/users¶
통계 query에 page, size를 추가합니다. 이 경로의 size는 최대 1,000으로 다시
제한됩니다. 그룹 구성원은 externalUserId 오름차순이며 대화가 없는 구성원도 0건으로
나옵니다. 최종 사용자와 아직 매칭되지 않았거나 이름이 없으면 userName=null입니다.
{
"status": 200,
"message": "success",
"page": {
"page": 1,
"size": 20,
"totalItems": 1,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false,
"first": true,
"last": true
},
"count": 1,
"list": [
{
"userId": "pilot-001",
"userName": "홍길동",
"oneMinuteOrLongerCount": 17
}
]
}
상태 코드는 전체 통계와 같고, 부수 효과는 없습니다.
메시지 음성 프록시¶
GET /v1/dialogs/{dialogId}/messages/{messageId}/audio¶
- 인증:
- JWT: access JWT + 헤더 필수
- API key: 귀속 워크스페이스 자동 적용
- path:
dialogId,messageId모두 UUID - 응답: JSON이 아닌 음성 byte stream
- 부수 효과: GCS 객체 읽기와 접근 감사 로그 기록. DB 데이터는 변경하지 않음
인가 순서는 테넌트의 대화 확인 → 해당 대화의 메시지 확인 → 최신 음성 행 확인 → GCS 객체 열기입니다. 다른 워크스페이스, 다른 대화의 messageId 끼워넣기, 리소스 부재는 존재 여부를 숨기기 위해 404로 처리합니다.
대표 응답 헤더:
HTTP/1.1 200 OK
Content-Type: audio/wav
Content-Disposition: inline; filename="<message-id>.wav"
Cache-Control: private, no-store
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
DB audioFormat이 WAV, MP3, OGG, WEBM이면 각각 알려진 audio MIME type으로
매핑합니다. 알 수 없으면 GCS metadata, 그것도 없으면 application/octet-stream입니다.
Content-Length는 GCS 또는 DB 크기를 알 수 있을 때만 설정됩니다. Range 요청은 지원하지 않습니다.
200: 음성 stream400: JWT 헤더 누락, 헤더·UUID 형식 오류401: credential 오류403: JWT 비멤버 또는 API key/헤더 불일치404: 대화/메시지/음성 행 부재, 테넌트 밖, 또는 DB 행은 있지만 GCS 객체 부재503: 버킷 미설정, 자격 조회 실패, GCS timeout/5xx 등 저장소 장애
messages[].audio.url은 credential이 포함된 서명 URL이 아니라 이 프록시의 절대 URL입니다.
브라우저의 <audio src>는 Authorization 헤더를 붙이지 못하므로 다음 흐름을 사용하십시오.
const response = await fetch(audio.url, {
headers: {
Authorization: "Bearer <credential>",
"X-Workspace-Id": "<workspace-uuid>"
}
});
const objectUrl = URL.createObjectURL(await response.blob());
API key 호출은 워크스페이스 헤더를 생략할 수 있습니다. 사용이 끝나면
URL.revokeObjectURL(objectUrl)로 object URL을 해제하십시오.