개발: http://localhost:8080/api/v1
운영: https://api.shadowfit.com/api/v1
// Request
{
"email": "user@example.com",
"password": "password123",
"nickname": "홈트초보"
}
// Response 201
{
"id": 1,
"email": "user@example.com",
"nickname": "홈트초보",
"token": "eyJhbGci..."
}// Request
{
"email": "user@example.com",
"password": "password123"
}
// Response 200
{
"token": "eyJhbGci...",
"user": {
"id": 1,
"nickname": "홈트초보",
"persona": "BEGINNER"
}
}// Request (Header: Authorization: Bearer {token})
{
"nickname": "홈트초보",
"persona": "BEGINNER",
"height": 175.0,
"weight": 70.5
}
// Response 200
{
"id": 1,
"nickname": "홈트초보",
"persona": "BEGINNER",
"height": 175.0,
"weight": 70.5
}// Response 200
[
{
"id": 1,
"name": "스쿼트",
"category": "LOWER",
"description": "하체 전체 운동",
"syncThresholdBeginner": 60.0,
"syncThresholdAdvanced": 85.0
}
]// Request
{
"exerciseId": 1,
"referenceSource": "youtube:https://youtu.be/xxx"
}
// Response 202 Accepted (비동기 - gRPC 호출이 백그라운드로 진행)
{
"sessionId": 42,
"exerciseId": 1,
"startTime": "2026-03-30T14:00:00",
"status": "IN_PROGRESS"
}내부 흐름: Spring 이 DB에 세션 생성 → 즉시 202 응답 →
@Async로 gRPCStartAnalysis송신 (AI 가 기준 좌표 받아 분석 시작). 결합 상세는architecture/ai-backend-integration.md.
POST /exercises/1/reference?youtubeUrl=https://youtu.be/xxx
// Response 202
"운동 ID [1]에 대한 기준 좌표 추출이 시작되었습니다."
유튜브 URL → AI 가 MediaPipe로 프레임마다 관절 좌표 추출 → Spring 콜백으로 exercise_references 테이블 영속화.
권장 종료 경로. 프론트가 종료 버튼을 누르면 호출. 멱등 — 이미 종료된 세션에 다시 호출해도 200.
PATCH /sessions/42/end
// Response 200 (본문 없음)
// 403 — 본인 세션 아님
SessionController.java:57 → SessionService.endSession. endTime 기록 + 아웃박스에 AI 통보 적재(직접 gRPC 호출 아님 — architecture/ai-backend-integration.md §4 중단).
🔴 2026-08-08 정정 — 이 절은 존재하지 않는 엔드포인트를 적고 있었다. 원래
PUT /exercises/sessions/{sessionId}/stop(2026-05-17143a2e4신설,202)로 되어 있었는데 지금 컨트롤러에stop매핑이 0건이다. 프론트도PATCH /sessions/{id}/end를 부른다(frontend/services/exercisesService.ts:23) — 즉 클라·서버는 일치하고 문서만 낡아 있었다.✅ 언제·왜 바뀌었는지는 결정 문서에 있다 —
decisions/session-end-trigger.md(2026-05-26): ET-H(단일 endpoint 분배자 패턴) 확정으로PUT …/stop을 의도적으로 삭제하고PATCH /sessions/{id}/end하나로 합쳤다. 클라가 종료를 한 번만 알리면 Spring 이 endTime 기록과 AI 통보를 분배한다(그 통보는 2026-07-29 아웃박스로 다시 바뀐다).🔴 즉 이 문서가 2.5개월 넘게 «폐기된 endpoint 를 권장 경로» 로 적고 있었다. 결정 문서는 맞았고 API 문서만 안 따라왔다 — 프론트가 이 문서를 계약으로 읽었다면 없는 경로를 불렀을 것이다.
⚠️ 응답 코드도202 Accepted(비동기 접수) →200 OK(멱등 종료)로 바뀌었다.📌
143a2e4를 기록한 문서들(architecture/ai-backend-changelog.md·commit-details·monthly-log)은 그 시점 사실이라 그대로 둔다. 내부 흐름:
- Spring 이
endTime을 기록하고 아웃박스에 AI 통보를 적재한 뒤200반환 (gRPC 직접 호출 아님) - 아웃박스 publisher 가 커밋 확정 후 gRPC
StopAnalysis(session_id=42)를 AI 에 송신 - AI 가 누적 통계 정리 후 gRPC
CompleteAnalysis로 콜백 (3회 재시도) - Spring 콜백 수신 시점에
status=COMPLETED,total_reps,avg_sync_rate등 DB에 영속화 - 프론트는 별도 조회 API 로 결과 폴링
AI = 운동 통계의 단일 진실 원천 원칙. (커밋 143a2e4)
프론트가 자체 카운트한 통계로 DB를 직접 갱신하던 옛 경로. AI 분석 결과와 권위가 충돌해 디프리케이트됐고, 엔드포인트는 23c8953(2026-07-11, 인증 없이 임의 세션을 강제 완료할 수 있던 결함)에서, 뒤에 남아 있던 서비스 계층·DTO 는 이슈 #179 에서 제거했다. 종료는 PATCH /sessions/{sessionId}/end 하나이고 완료 값의 출처는 AI gRPC 콜백이다.
// Response 200
[
{
"feedbackType": "KNEE_OVER",
"message": "무릎이 발끝을 넘었습니다",
"priority": 10
},
{
"feedbackType": "GOOD_FORM",
"message": "좋은 자세입니다",
"priority": 100
}
]세션 시작 시 클라이언트가 호출해 device TTS 재생용 멘트 매핑.
참고: 과거에 검토되었던
POST /exercises/sessions/{id}/pose-data(REST 배치 저장) 엔드포인트는 gRPCSavePoseDataBatch콜백으로 대체되어 제거됨 (커밋 8ac8248).
// Response 200
{
"year": 2026,
"month": 3,
"records": [
{
"date": "2026-03-15",
"totalExerciseTime": 45,
"totalCalories": 320.5,
"sessionCount": 2,
"mood": "GOOD"
}
]
}// Request
{
"logDate": "2026-03-30",
"memo": "오늘 스쿼트 자세가 많이 좋아졌다!",
"mood": "GREAT"
}// Response 200
{
"reportId": 10,
"sessionId": 42,
"exerciseName": "스쿼트",
"duration": "30분",
"avgSyncRate": 78.5,
"summary": "전체적으로 좋은 자세를 유지했습니다. 다만 후반부에 무릎이 발끝을...",
"improvementTips": "1. 무릎 위치를 더 신경써주세요\n2. 허리를 곧게 유지해주세요",
"comparisonWithPrevious": {
"syncRateChange": +5.2,
"repChange": +3
},
"syncRateTimeline": [82.5, 80.1, 75.0, ...]
}주간이 두 경로로 갈려 있었고 새 쪽은 부르는 곳이 없었다. 이제 하나다.
요청: 파라미터 없음. 인증 필요. 기준은 오늘이 속한 주(월 시작) 고정 — 기준일 파라미터를 안 받는 이유는 응답의 두 절반(활동 집계 · A층 요약)이 같은 주를 보게 하기 위해서다.
응답 WeeklyActivityResponseDto
| 필드 | 뜻 |
|---|---|
dateRange · totalWorkouts · totalMinutes · totalCalories |
활동 집계 |
dailyLogs[] · todayDetails[] |
요일별 막대 · 오늘 상세 |
summary |
A층 요약 — periodStart · periodEnd(미포함) · thisWeek · lastWeek 집계와 sentences[](규칙 문장) |
기록이 없어도 200 이다 — 「이번 주에 운동을 안 했다」는 정상 상태라 빈 집계와 그 사실을 말하는 문장을 돌려준다.
// Response 200
{
"ttsEnabled": true,
"ttsSpeed": 1.0
}// Request
{
"ttsEnabled": true,
"ttsSpeed": 1.5
}
// Response 200 — 갱신된 설정 반환ttsSpeed 는 0.5~2.0 범위. device TTS 재생 시 클라이언트가 이 값을 그대로 expo-speech 의 rate 로 전달. (11-tts-youtube-guide.md)
관리자 권한(ROLE_ADMIN) 필수. 신규 세션부터 적용.
// Request
{
"syncThresholdBeginner": 65.0,
"syncThresholdAdvanced": 88.0
}
// 제약: beginner < advanced
// Response 200
{
"exerciseId": 1,
"syncThresholdBeginner": 65.0,
"syncThresholdAdvanced": 88.0
}관리자 권한(ROLE_ADMIN) 필수. 필터 5종의 임의 조합(32가지) + 정렬 3종 + offset 페이징.
| 파라미터 | 기본값 | 설명 |
|---|---|---|
keyword |
— | username/email 부분일치. |
persona · workoutLevel · onboardingCompleted |
— | 등치 필터 |
joinedFrom · joinedTo |
— | 가입일 범위(joinedTo 는 그날 포함) |
sort |
CREATED_AT |
화이트리스트 enum |
asc |
false |
최신순 기본 |
page · size |
0 · 20 |
size 최대 100 |
// Response 200 — PageResponse<AdminMemberListItemDto>
{ "content": [ { "id": 1, "username": "...", "email": "...", "selectedPersona": "BEGINNER",
"workoutLevel": "STARTER", "onboardingCompleted": true, "createdAt": "..." } ],
"page": 0, "size": 20, "totalElements": 1234, "totalPages": 62 }관리자 권한 필수. exercise_sessions ⋈ users ⋈ exercises 조인. 필터 4종 + 정렬 3종.
| 파라미터 | 설명 |
|---|---|
status |
세션 상태 등치 |
exerciseId |
운동 종목 |
startedFrom · startedTo |
시작 시각 범위 |
keyword |
회원 username/email 부분일치 (조인 너머) |
sort |
START_TIME(기본) / AVG_SYNC_RATE / TOTAL_REPS — 2차 정렬로 id 고정 |
관리자 권한 필수. 위젯 5종을 실시간 집계로 반환(사전집계·캐시 없음).
// Response 200
{ "todaySessionCount": 2653, "sessionStatusDistribution": { "COMPLETED": 500000, "FAILED": 249554, "IN_PROGRESS": 250446 },
"averageSyncRate": 75.0, "newMemberCount": 548, "activeMemberCount": 19019 }📌 세 API 의 설계 근거·실측은
decisions/admin-page-scope.md. 인덱스 커버리지(§4-3), 드라이빙 테이블(§4-4-1), 집계 비용(§4-5) 이 거기 있다.🔶 총건수(
totalElements)는LIMIT이 없어 조건에 맞는 행을 전부 센다. 대부분의 필터 조합에서 전수 스캔이고, 이건 감수하기로 한 것(㉮)이다 — 페이지 번호 UI 를 그리려면 필요하고, 무한 스크롤로 정해지면 keyset 을 얹으면서 안 부르면 된다(§4-3 "2026-08-06").🔶 대시보드는 b(상태별 분포)·e(활성 회원) 둘이 비용의 대부분이다(실측 각각 ~357ms / ~307ms, 나머지 셋 합쳐 5ms 미만). 캐시·인덱스 도입은 미결(§4-5-1 ④).
2026-05-26 갱신: AI → Spring 내부 호출은 전부 gRPC 로 통일.
Authorization: Bearer {INTERNAL_API_TOKEN}(metadata) 로 인증. REST/internal/*endpoint 는 폐기됨 (기존POST /internal/feedback/batch→ExerciseService.ReportFeedbackBatch). proto 정의는backend/src/main/proto/exercise.proto. 박제:./decisions/tts-design.md상단 박스.
호출자: AI 서버 (FastAPI → Spring). BT-SET 모델 (분기 2.A.BT) — 세트 경계마다 mini-batch + 세션 종료 시 final batch. 매 rep 실시간 호출 금지.
인증: gRPC metadata Authorization: Bearer {INTERNAL_API_TOKEN} (InternalAuthInterceptor).
멱등성: (session_id, occurred_at, feedback_type) uniqueKey + INSERT IGNORE. 같은 events 재송신 안전.
// Request
message FeedbackBatchRequest {
int64 session_id = 1;
int32 set_no = 2; // 1-based. BT-NONE 호환 시 1 고정
bool is_final = 3; // 마지막 batch 여부
repeated FeedbackEvent events = 4;
}
message FeedbackEvent {
string feedback_type = 1; // 8종 enum 중 하나 (KNEE_OUT 등)
double sync_rate_at_trigger = 2;
google.protobuf.Timestamp occurred_at = 3;
}
// Response
message FeedbackBatchResponse {
int64 session_id = 1;
int32 saved_count = 2; // INSERT 된 row 수 (중복 흡수 제외)
}StartAnalysis (AnalyzeRequest) returns (AnalyzeResponse)— Spring → FastAPI 세션 시작StopAnalysis (StopRequest) returns (StopResponse)— Spring → FastAPI 강제 중단 (ET-H,SessionService.endSessionafterCommit 에서 호출)SavePoseDataBatch (PoseDataBatchRequest) returns (PoseDataResponse)— FastAPI → Spring 포즈 batchCompleteAnalysis (SessionCompleteRequest) returns (SessionCompleteResponse)— FastAPI → Spring 세션 최종 통계ExtractReferenceData (ExtractRequest) returns (ExtractResponse)— Spring → FastAPI YouTube 좌표 추출
{
"status": 200,
"data": { ... }
}{
"status": 400,
"error": "BAD_REQUEST",
"message": "유효하지 않은 이메일 형식입니다."
}- JWT Bearer Token
- Header:
Authorization: Bearer {token} - 토큰 만료: 24시간
/auth/*엔드포인트는 인증 불필요