환자 리포트 조회 API
참고: 이 문서는
dta-wi-mono모노레포의dta-wi-medi-api애플리케이션에 구현되어 있습니다.
개요
환자 리포트 조회 API는 환자의 치료 진행 상황, 수면 기록, 설문 결과 등을 종합적으로 조회하는 기능을 제공합니다.
주요 기능
- 환자 리포트 조회: 인증된 사용자의 환자 리포트 조회
- 외부 토큰으로 환자 리포트 조회: 외부 액세스 토큰을 사용한 환자 리포트 조회 (공유 링크)
구현 방안
외부 공유 링크(토큰 기반)와 인증된 요청을 처리하는 두 가지 방안을 검토합니다. 구현 시 택 1.
| 항목 | 인증된 요청 | 외부 공유 |
|---|---|---|
| 엔드포인트 | GET /v1/user-management/app-users/{appUserId}/reports | GET /v1/user-management/app-users/{appUserId}/reports/external |
| 인증 | Authorization: Bearer {accessToken} | query param: ?token={external_md_access_token} |
| 사용처 | 대시보드 내부 | 공유 링크, 외부 시스템 |
[GET] /v1/user-management/app-users/{appUserId}/reports - 환자 리포트 조회
환자 리포트 조회
특정 환자의 종합 리포트를 조회합니다.
⚠️ 변경 사항
주의: 이전 버전에서 다음 사항이 변경되었습니다. 클라이언트 코드 수정이 필요합니다.
엔드포인트 변경
- asis:
GET /v1/patients/user-cycles/{userCycleId}/reports(Go API) - tobe:
GET /v1/user-management/app-users/{appUserId}/reports(TypeScript API)
변경된 path 파라미터
| 항목 | Legacy | Current |
|---|---|---|
| Path Parameter | userCycleId (number) | appUserId (string UUID) |
| 식별 방식 | UserCycle ID로 식별 | User ID로 식별 (활성 UserCycle 자동 조회) |
- HTTP Method:
GET - 인증: 액세스 토큰 (
accessToken) 필요
Headers
| Header | Type | Description | Required |
|---|---|---|---|
| Authorization | Bearer {accessToken} | 사용자 인증을 통해 발급받은 액세스 토큰 입니다. | ✔ |
Path Parameters
| 필드 | 타입 | 설명 | Required |
|---|---|---|---|
| appUserId | string | 사용자 UUID | ✔ |
Request 예시
# 활성 주기 리포트 조회 (항상 ACTIVE 주기만 조회)
GET /v1/user-management/app-users/abc-123/reports
Responses
| Http Status Code | 설명 | Error Code(s) |
|---|---|---|
200 OK | 조회 성공 | - |
400 Bad Request | 잘못된 요청 | VALIDATION_FAILED |
401 Unauthorized | 인증 실패 | UNAUTHORIZED |
404 Not Found | 환자 미발견 | USER_NOT_FOUND, USER_CYCLE_NOT_FOUND |
500 Internal Server Error | 서버 내부 오류 | INTERNAL_SERVER_ERROR |
200 OK - 성공
{
"period": {
"startDate": "2024-01-01",
"endDate": "2024-03-31"
},
"user": {
"name": "홍길동",
"email": "patient@example.com",
"phoneNumber": "01012345678",
"birthdate": "1990-01-01",
"siteId": "site-001",
"operationId": "account-001",
"operationName": "김의사",
"currentDayIndex": 15,
"status": "ACTIVE",
"lastLogin": "2024-01-20T10:00:00Z",
"startedAt": "2024-01-10T09:00:00Z",
"endAt": "2024-04-10T09:00:00Z",
"memo": "특이사항 없음",
"medicalRegistrationNumber": "MRN-001"
},
"sleeps": {
"2024-01-15": {
"diary": {
"dns": false,
"lot": 1380,
"ast": 1410,
"aet": 450,
"waso": 30,
"pill": false,
"nap": 0,
"sleepQuality": 4,
"tst": 420,
"se": 93.3,
"dse": 1,
"sol": 30,
"timezoneId": "Europe/Berlin",
"timezoneOffset": 60
},
"goal": {
"targetDate": "2024-01-15",
"durationInMinutes": 450,
"lot": 1380,
"aet": 450,
"timezoneId": "Europe/Berlin",
"timezoneOffset": 60,
"typeId": 1
}
}
},
"questionnaires": {
"ISI": [{ "completed": true, "score": 12, "maxScore": 28, "round": 1 }],
"WIQ": [],
"PHQ9": [{ "completed": true, "score": 5, "maxScore": 27, "round": 1 }],
"GAD7": [{ "completed": true, "score": 3, "maxScore": 21, "round": 1 }],
"ESS": [],
"DBAS": [{ "completed": true, "score": 5.5, "maxScore": 10, "round": 1 }]
},
"summary": {
"treatmentProgress": {
"currentDay": 15,
"totalUsageDays": 15
},
"sleepRecord": {
"count": 14,
"appUsageDays": 15,
"percentage": 93.3
},
"lotSuccess": {
"count": 12,
"appUsageDays": 15,
"percentage": 80.0
},
"aetSuccess": {
"count": 13,
"appUsageDays": 15,
"percentage": 86.7
}
}
}
Response 구조
period (조회 기간)
| 필드 | 타입 | 설명 |
|---|---|---|
| startDate | string | 시작 날짜 |
| endDate | string | 종료 날짜 |
user (환자 정보)
| 필드 | 타입 | 설명 |
|---|---|---|
| name | string | 환자 이름 |
| string | 이메일 | |
| phoneNumber | string | 전화번호 |
| birthdate | string | 생년월일 |
| siteId | string | 사이트 ID |
| operationId | string | 담당 의료진 계정 ID |
| operationName | string | 담당 의료진 이름 |
| currentDayIndes | number | 치료 일차 |
| status | string | 환자 상태 |
| lastLogin | string | 마지막 로그인 |
| startedAt | string | 치료 시작일 |
| endAt | string | 치료 종료일 |
| memo | string | 메모 |
| medicalRegistrationNumber | string | 의무 등록 번호 |
sleeps (수면 기록)
날짜를 키로 하는 맵 형태로 제공됩니다. 각 날짜별로 수면 일지(diary)와 수면 목표(goal) 정보를 포함합니다.
diary (수면 일지):
| 필드 | 타입 | 설명 |
|---|---|---|
| dns | boolean | Did Not Sleep (수면하지 않음) |
| lot | number | Lights Out Time (소등 시각, 분 단위) |
| ast | number | Actual Sleep Time (실제 수면 시각) |
| aet | number | Actual End Time (실제 기상 시각) |
| waso | number | Wake After Sleep Onset (중간 각성) |
| pill | boolean | 수면제 복용 여부 |
| nap | number | 낮잠 시간 (분) |
| sleepQuality | number | 수면 질 평가 (1-5) |
| tst | number | Total Sleep Time (총 수면 시간, 분) |
| se | number | Sleep Efficiency (수면 효율, %) |
| dse | number | Daily Sleep Efficiency |
| sol | number | Sleep Onset Latency (입면 잠복기) |
| timezoneId | string | 타임존 ID |
| timezoneOffset | number | 타임존 오프셋 (분) |
goal (수면 목표):
| 필드 | 타입 | 설명 |
|---|---|---|
| targetDate | string | 목표 날짜 |
| durationInMinutes | number | 목표 수면 시간 (분) |
| lot | number | 목표 소등 시각 |
| aet | number | 목표 기상 시각 |
| timezoneId | string | 타임존 ID |
| timezoneOffset | number | 타임존 오프셋 (분) |
| typeId | number | 수면 목표 타입 ID |
questionnaires (설문 결과)
각 설문 타입별로 배열 형태로 제공됩니다.
설문 타입:
- ISI: Insomnia Severity Index (불면증 심각도 지수)
- WIQ: Work and Health Interview Questionnaire
- PHQ9: Patient Health Questionnaire-9 (우울증)
- GAD7: Generalized Anxiety Disorder-7 (불안장애)
- ESS: Epworth Sleepiness Scale
- DBAS: Dysfunctional Beliefs and Attitudes about Sleep
| 필드 | 타입 | 설명 |
|---|---|---|
| completed | boolean | 완료 여부 |
| score | number | 점수 (optional) |
| maxScore | number | 최대 점수 |
| round | number | 회차 |
summary (요약 정보)
treatmentProgress (치료 진행):
| 필드 | 타입 | 설명 |
|---|---|---|
| currentDay | number | 현재 치료 일차 |
| totalUsageDays | number | 총 앱 사용 일수 |
sleepRecord (수면 기록 통계):
| 필드 | 타입 | 설명 |
|---|---|---|
| count | number | 수면 기록 횟수 |
| appUsageDays | number | 앱 사용 일수 |
| percentage | number | 수면 기록 비율 (%) |
lotSuccess (소등 시각 준수 통계):
| 필드 | 타입 | 설명 |
|---|---|---|
| count | number | 소등 시각 준수 횟수 |
| appUsageDays | number | 앱 사용 일수 |
| percentage | number | 소등 시각 준수 비율 (%) |
aetSuccess (기상 시각 준수 통계):
| 필드 | 타입 | 설명 |
|---|---|---|
| count | number | 기상 시각 준수 횟수 |
| appUsageDays | number | 앱 사용 일수 |
| percentage | number | 기상 시각 준수 비율 (%) |
400 Bad Request - 잘못된 요청
{
"statusCode": 400,
"code": 1001,
"message": "Bad Request",
"detail": "요청한 정보가 유효하지 않습니다."
}
발생 가능한 에러 메시지:
"Invalid appUserId format"- 유효하지 않은 UUID 형식
401 Unauthorized - 인증 실패
{
"statusCode": 401,
"code": 1080,
"message": "UNAUTHORIZED",
"detail": "인증되지 않은 요청입니다"
}
액세스 토큰이 유효하지 않거나 만료된 경우 발생합니다.
404 Not Found - 환자 미발견
{
"statusCode": 404,
"code": 1040,
"message": "NOT_FOUND",
"detail": "유효하지 않은 사용자입니다."
}
해당 appUserId에 해당하는 환자가 존재하지 않거나, 활성 UserCycle이 없는 경우 발생합니다.
500 Internal Server Error - 서버 내부 오류
{
"statusCode": 500,
"code": 1000,
"message": "Internal Server Error",
"detail": "서버 내부 오류가 발생했습니다."
}
[GET] /v1/user-management/app-users/{appUserId}/reports/external - 외부 토큰으로 환자 리포트 조회
외부 토큰으로 환자 리포트 조회
외부 액세스 토큰을 사용하여 환자 리포트를 조회합니다. 일반 인증 토큰이 아닌 별도의 외부 액세스 토큰(external_md_access)으로 인증합니다.
⚠️ 변경 사항
엔드포인트 변경
- asis:
GET /v1/patients/user-cycles/{userCycleId}/reports/external?token=xxx(Go API) - tobe:
GET /v1/user-management/app-users/{appUserId}/reports/external?token=xxx(TypeScript API)
변경된 파라미터
| 항목 | Legacy | Current |
|---|---|---|
| Path Parameter | userCycleId (number) | appUserId (string UUID) |
| 토큰 sub | userCycleId | userId |
- HTTP Method:
GET - 인증: 외부 액세스 토큰 (
token) 필요 (Authorization 헤더 불필요)
Path Parameters
| 필드 | 타입 | 설명 | Required |
|---|---|---|---|
| appUserId | string | 사용자 UUID | ✔ |
Query Parameters
| 필드 | 타입 | 설명 | Required |
|---|---|---|---|
| token | string | 외부 액세스 토큰 (external_md_access) | ✔ |
Request 예시
# 활성 주기 리포트 (외부 토큰)
GET /v1/user-management/app-users/abc-123/reports/external?token=eyJhbGci...
토큰 검증 로직
- 쿼리 파라미터에서
token값 추출 및 필수 체크 - 외부 액세스 토큰의 유효성 검증
- 토큰 타입이
external_md_access인지 확인 - 토큰의 JWT claims에서
sub(subject) 추출 sub값이 요청 path의appUserId와 일치하는지 확인- 검증 성공 시 활성(ACTIVE) UserCycle의 리포트 조회
Responses
| Http Status Code | 설명 | Error Code(s) |
|---|---|---|
200 OK | 조회 성공 | - |
400 Bad Request | 잘못된 요청 | VALIDATION_FAILED |
401 Unauthorized | 인증 실패 | UNAUTHORIZED |
404 Not Found | 환자 미발견 | USER_NOT_FOUND, USER_CYCLE_NOT_FOUND |
500 Internal Server Error | 서버 내부 오류 | INTERNAL_SERVER_ERROR |
200 OK - 성공
응답 형식은 일반 환자 리포트 조회와 동일합니다.
400 Bad Request - 잘못된 요청
{
"statusCode": 400,
"code": 1001,
"message": "Bad Request",
"detail": "잘못된 요청입니다."
}
발생 가능한 에러 메시지:
"잘못된 요청입니다."- 토큰 파라미터가 제공되지 않음"유효하지 않은 토큰 정보 입니다."- 토큰의 sub 클레임이 없거나 형식이 잘못됨"유효하지 않은 토큰 정보 입니다."- 토큰의 sub와 appUserId가 일치하지 않음
401 Unauthorized - 인증 실패
{
"statusCode": 401,
"code": 1080,
"message": "UNAUTHORIZED",
"detail": "인증되지 않은 요청입니다"
}
발생 시나리오:
- JWT 토큰이 만료됨
- 토큰 서명이 유효하지 않음
- 토큰 타입이
external_md_access가 아님
404 Not Found - 환자 미발견
{
"statusCode": 404,
"code": 1040,
"message": "NOT_FOUND",
"detail": "유효하지 않은 계정 정보입니다."
}
appUserId에 해당하는 환자 정보를 찾을 수 없는 경우 발생합니다.
500 Internal Server Error - 서버 내부 오류
{
"statusCode": 500,
"code": 1000,
"message": "Internal Server Error",
"detail": "서버 내부 오류가 발생했습니다."
}
토큰 정보
| 타입 | 설명 | sub |
|---|---|---|
| external_md_access | 의료진 대시보드 외부 액세스 토큰 | userId |
기존 대비 변경: 토큰의 sub가 userCycleId → userId로 변경됨. cycle이 변경/추가되어도 토큰 재발급 불필요.
비즈니스 로직
- 해당 user의 활성(ACTIVE) UserCycle 자동 조회
- 환자의 치료 시작일/종료일 계산 (UserCycle 기준)
- 해당 기간의 수면 다이어리 조회
- 수면 목표 로그 조회
- 설문지 결과 조회 (ISI, WIQ, PHQ9, GAD7, ESS, DBAS)
- LOT/AET 목표 달성 성공 횟수 계산
- 치료 진행률 및 성공률 통계 계산 (Plan 기반 총 일차)
특이사항:
- 항상 활성(ACTIVE) 상태의 UserCycle만 조회
- startDate부터 endDate까지 모든 날짜 슬롯 생성 (데이터 없어도 빈 객체 생성)
- endDate가 현재 날짜보다 미래인 경우 현재 날짜로 조정
- 타임존은 UserCycle의 timezoneId 기준으로 처리
- 총 치료 일차는 Plan 기반으로 계산 (기본: 90일)