콘텐츠로 이동

대화·메시지·분석·음성 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: 선택, 기본 1
  • size: 선택, 기본 20, 공통 상한 10,000
  • externalUserId: 선택, 현재 워크스페이스 최종 사용자 식별자
  • 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: UUID
  • sessionId: 문자열
  • 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
{
  "status": 200,
  "message": "success",
  "data": {
    "oneMinuteOrLongerCount": 42
  }
}
  • 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: 음성 stream
  • 400: 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을 해제하십시오.