Base URL: /api/v1
모든 응답은 ApiResponse<T> 래퍼로 감싸져 반환됩니다.
{
"success": true|false,
"message": "...", // error 시에만
"code": "ERROR_CODE", // error 시에만 (ErrorCode enum 이름)
"data": <T>, // success 시 페이로드
"fieldErrors": { // 유효성 오류일 때만 (필드명 → 메시지)
"name": "이름은 필수입니다."
},
"timestamp": "2026-04-25T12:00:00"
}인증이 필요한 API는 Authorization: Bearer {accessToken} 헤더를 사용합니다.
bandage-fe/.taskmaster/reports/ 의 5개 검증 리포트 + API_ISSUE_REPORT.md (mvp-1-fix-v3 Task 8) 에서 제기된 이슈에 대한 처리 현황입니다.
| # | 이슈 | 출처 | 상태 | 비고 |
|---|---|---|---|---|
| 1 | BandCreateRequest.profileImg non-null → optional |
MVP 통합 §4 | ✅ 해소 | DTO를 String?로 변경 (3-1 참조) |
| 2 | PerformanceUpdateRequest 부분 업데이트 불가 |
Performance §3-A | ✅ 해소 | 모든 필드 nullable + 서비스 partial update (6-4 참조) |
| 3 | GET /api/v1/practices 미구현 (405) |
MVP 통합 §6, Practice §3-B | ✅ 해소 | 신규 엔드포인트 추가 (4-1-2 참조) |
| 4 | API_SPEC 인증 표기 vs 실제 인증 요구 불일치 | Band §3-A, Practice §3-C | ✅ 해소 | 본 문서의 모든 보호 엔드포인트는 "인증 필요"로 정렬됨 (/auth/login, /auth/refresh, /members/join만 공개) |
| 5 | CursorResponse.nextCursor 일관성 (hasNext=false일 때 null) |
Band §3-C, Performance §3-C | ✅ 해소 | 모든 RepositoryImpl에서 hasNext=false 시 nextCursor=null |
| 6 | 인증(401) / 인가(403) 응답 코드 분리 | Auth §C, Band §3-B, Performance §3-B | ✅ 해소 (검증 완료) | NOT_A_LEADER, NOT_A_PERFORMANCE_MANAGER 모두 HttpStatus.FORBIDDEN로 매핑됨. JWT 무효/누락은 JwtAuthenticationEntryPoint가 401 + WWW-Authenticate 헤더 반환 |
| 7 | ApiResponse에 code / fieldErrors 추가 |
Auth §D | ✅ 해소 | 본 문서 상단 응답 스키마 참조. code는 ErrorCode enum 이름. 유효성 오류 시 fieldErrors 맵 동봉 |
| 8 | PerformanceDetailResponse 요약 필드 (밴드명/합주 제목) |
Performance §3-D | ✅ 해소 | bandIds, practiceIds → bands: [{bandId, bandName}], practices: [{practiceId, title, startAt}]로 확장 (6-3 참조) |
| 9 | GET /bands/me 응답에 본인 역할 (myRole) 포함 |
Band §3-D | ✅ 해소 (옵션 9-C) | 응답 항목 타입을 MyBandInfoResponse로 변경, myRole: BandRole 필드 추가 (3-3-1 참조) |
| 10 | PerformanceCreateRequest.bandIds non-null 차단 (T6) |
API_ISSUE_REPORT §4-1 | ✅ 해소 | bandIds: List<UUID>? = null 로 변경, 미전달 시 빈 배열로 보정 (6-1 참조) |
| 11 | Practice ↔ PracticeSong 닭-달걀 (FE-API-020) | API_ISSUE_REPORT §4-2 | ✅ 해소 (§5-2 안 채택) | PracticeSong 생성 API의 practiceId 옵셔널화 → FE는 PracticeSong 선행 생성 후 응답 songId 를 /practices 에 전달 (5-2/5-3/4-1 참조) |
| 12 | 입력 검증 메시지 정제 (T3, T6) | API_ISSUE_REPORT §4-3 | ✅ 해소 | HttpMessageNotReadableException 핸들러가 KotlinInvalidNullException/InvalidFormatException 을 한국어 메시지 + fieldErrors 로 매핑 |
| 13 | 과거 시각 startAt 검증 (T17) | API_ISSUE_REPORT §4-4 | ✅ 해소 | Practice/Performance 생성·수정·합주 일정 변경 DTO 의 startAt 에 @Future 적용 (4-1/4-8/6-1/6-4 참조) |
| # | 이슈 | 출처 | 상태 | 비고 |
|---|---|---|---|---|
| 14 | MemberInfoResponse.id alias / profileImg 노출 |
MVP 통합 §3 | ✅ 해소 | id 와 memberId 동시 노출 (alias), profileImg 필드 추가 (2-2 참조) |
| 15 | BandMemberInfoResponse 에 회원 이름/프로필 이미지 포함 |
FE-API-012 | ✅ 해소 | name, profileImg 추가 (3-4/3-5 참조) |
| 16 | BandApplicationInfoResponse 에 신청자 정보 포함 |
FE-API-013 | ✅ 해소 | applicantName, applicantProfileImg, appliedAt 추가 (3-7 참조) |
| 17 | 회원 메트릭 (밴드 수 / 다가오는 합주·공연 수 / 세션 수) | FE-API-014 | ✅ 해소 | GET /members/me/metrics 신규 (2-5 참조). 네이밍은 Stat → Metrics 로 통일 |
| 18 | 회원 검색 (이름/이메일 부분 일치) | FE-API-032 | ✅ 해소 | GET /members/search?q= 신규, 본인 제외, 최대 20건 (2-6 참조) |
| 19 | 밴드 정보 수정 / 삭제 / 멤버 강퇴 / 역할 변경 | FE-API-022/023, §8-2 | ✅ 해소 | 3-12 ~ 3-15 참조 |
| 20 | 공연 참여 밴드 일괄 추가/단건 제거 | FE-API-017 | ✅ 해소 | 6-9 / 6-10 참조 |
- POST
/api/v1/auth/login - 인증 불필요
- Request Body
{ "email": "member@google.com", "password": "pw1234" } - Response
{ "accessToken": "ey1234..." } - 비고: refresh 토큰은
Set-Cookie헤더로 반환
- DELETE
/api/v1/auth/logout - 인증 필요
- 비고: Redis와 쿠키에 저장된 refresh 토큰을 만료 처리
- POST
/api/v1/auth/refresh - 인증 불필요
- Request: Cookie에
refreshToken포함 - Response
{ "accessToken": "ey1234..." } - 비고: 새로운 refresh 토큰은
Set-Cookie헤더로 반환
- PATCH
/api/v1/auth/password - 인증 필요
- Request Body
{ "originalPassword": "originalPW123!", "newPassword": "newPW123!" } - 비고: 변경 후 refresh 토큰(쿠키) 만료 처리
- POST
/api/v1/members/join - 인증 불필요
- Request Body
{ "email": "member@google.com", "password": "pw1234", "name": "홍길동", "contact": "010-1234-5678" } - Response
{ "id": 1, "email": "member@gmail.com" }
- GET
/api/v1/members/me - 인증 필요
- Response:
MemberInfoResponse{ "id": 1, "memberId": 1, "email": "member@google.com", "name": "홍길동", "contact": "010-1234-5678", "profileImg": "https://cdn/...jpg" } - 비고:
id는memberId의 프론트 호환 alias 필드.profileImg는 미설정 시null.
- PATCH
/api/v1/members/me - 인증 필요
- Request Body
{ "name": "홍길동", "contact": "010-1234-5678" } - 비고: 각 필드는 nullable, 전달된 필드만 업데이트
- DELETE
/api/v1/members/me - 인증 필요
- 비고: 회원 정보 삭제(soft delete) 및 로그아웃 처리 (쿠키 만료)
- GET
/api/v1/members/me/metrics - 인증 필요
- Response:
MemberMetricsResponse{ "bandCount": 3, "upcomingPracticeCount": 2, "upcomingPerformanceCount": 1, "sessionCount": 5 } - 비고: 본인이 소속된 밴드 수, 본인이 참여한 다가오는 합주 수(
startAt > now), 본인 소속 밴드의 다가오는 공연 수(startAt > now), 본인이 참여 중인 합주 세션 수. 도메인 간 의존(Band/Practice/Performance)이 있어MemberMetricsFacade에서 집계.
- GET
/api/v1/members/search - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 qString N ""검색 키워드 (이름 또는 이메일) - Response:
List<MemberSearchItemResponse>[ { "memberId": 1, "name": "홍길동", "email": "user@bandage.test", "profileImg": "https://cdn/...jpg" } ] - 비고: 이름/이메일 부분 일치 (대소문자 구분 X). 본인은 결과에서 제외. 최대 20건.
- POST
/api/v1/bands - 인증 필요
- Request Body
{ "name": "TuNA", "description": "성균관대학교 문과대 락밴드 TuNA 입니다.", "profileImg": "url" }profileImg: optional
- Response
{ "bandId": "550e8400-e29b-41d4-a716-446655440000", "bandName": "TuNA" } - 비고: 생성자는 자동으로 LEADER 역할로 등록
- GET
/api/v1/bands/{bandId} - 인증 필요
- Path Variable:
bandId(UUID) - Response
{ "bandId": "550e8400-e29b-41d4-a716-446655440000", "bandName": "TuNA", "description": "성균관대학교 문과대 락밴드 TuNA 입니다.", "profileImg": "image_url" }
- GET
/api/v1/bands - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 lastIdUUID N - 이전 페이지 마지막 밴드 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<BandInfoResponse, UUID>
- GET
/api/v1/bands/me - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 lastIdUUID N - 이전 페이지 마지막 밴드 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<MyBandInfoResponse, UUID>{ "content": [ { "bandId": "550e8400-e29b-41d4-a716-446655440000", "bandName": "TuNA", "description": "...", "profileImg": "url", "myRole": "LEADER" } ], "nextCursor": null, "hasNext": false } - 비고: 현재 로그인한 회원이 소속된 밴드만 조회. 응답 항목에 본인의 밴드 내 역할(
myRole)이 포함되어 별도 멤버 목록 조회 없이도 권한 판정 가능.myRoleenum —LEADER|ADMIN|MEMBER
- GET
/api/v1/bands/search - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 keywordString Y - 검색 키워드 (밴드 이름) lastIdUUID N - 이전 페이지 마지막 밴드 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<BandInfoResponse, UUID> - 비고: 밴드 이름에
keyword가 포함된 밴드를 대소문자 구분 없이 부분 매칭 조회
- GET
/api/v1/bands/{bandId}/members/{bandMemberId} - 인증 필요
- Path Variables:
bandId(UUID),bandMemberId(UUID) - Response:
BandMemberInfoResponse{ "bandMemberId": "550e8400-e29b-41d4-a716-446655440000", "memberId": 1, "role": "MEMBER", "name": "홍길동", "profileImg": "https://cdn/...jpg" } - 비고:
roleenum —LEADER|ADMIN|MEMBER.name/profileImg는 회원 조회 후 매핑 (FE-API-012).
- GET
/api/v1/bands/{bandId}/members - 인증 필요
- Path Variable:
bandId(UUID) - Query Parameters:
lastId(UUID, optional),pageSize(Int 1~100, 기본값 10) - Response:
CursorResponse<BandMemberInfoResponse, UUID>
- POST
/api/v1/bands/{bandId}/applications - 인증 필요
- Path Variable:
bandId(UUID)
- GET
/api/v1/bands/{bandId}/applications - 인증 필요 (리더/관리자)
- Path Variable:
bandId(UUID) - Query Parameters
파라미터 타입 필수 기본값 설명 lastIdUUID N - 이전 페이지 마지막 신청 ID pageSizeInt (1~100) N 10 페이지 크기 statusApplicationStatus N PENDING신청 상태 필터 - 비고:
statusenum —PENDING|APPROVED|REJECTED|WITHDRAWN|LEAVED - Response:
CursorResponse<BandApplicationInfoResponse, UUID>{ "bandApplicationId": "550e8400-...", "memberId": 1, "status": "PENDING", "applicantName": "홍길동", "applicantProfileImg": "https://cdn/...jpg", "appliedAt": "2026-04-26T12:34:56" } - 비고:
applicantName/applicantProfileImg는 신청자 회원 조회 후 매핑 (FE-API-013).appliedAt은 신청 생성 시각.
- PATCH
/api/v1/bands/{bandId}/applications/me - 인증 필요
- Path Variable:
bandId(UUID) - 비고:
PENDING상태의 본인 신청만 철회 가능
- PATCH
/api/v1/bands/{bandId}/applications/{bandApplicationId} - 인증 필요 (리더)
- Path Variables:
bandId(UUID),bandApplicationId(UUID) - Query Parameter:
status(APPROVED|REJECTED)
- PATCH
/api/v1/bands/{bandId}/members/{bandMemberId}/role - 인증 필요 (리더)
- Path Variables:
bandId(UUID),bandMemberId(UUID) - Request Body (optional)
{ "role": "ADMIN" }roleenum —LEADER|ADMIN|MEMBER
- 비고:
- body 미제공 또는
role=LEADER면 리더 권한 위임 동작 (현재 리더가 지정 멤버에게 권한 양도) role=ADMIN/role=MEMBER면 해당 역할로 변경
- body 미제공 또는
- DELETE
/api/v1/bands/{bandId}/members/me - 인증 필요
- Path Variable:
bandId(UUID)
- PATCH
/api/v1/bands/{bandId} - 인증 필요 (리더)
- Path Variable:
bandId(UUID) - Request Body (모든 필드 optional, 전달된 필드만 갱신)
{ "name": "TuNA", "description": "성균관대학교 락밴드입니다.", "profileImg": "https://cdn/...jpg" } - Response:
BandResponse{ "bandId": "550e8400-e29b-41d4-a716-446655440000", "bandName": "TuNA" }
- DELETE
/api/v1/bands/{bandId} - 인증 필요 (리더)
- Path Variable:
bandId(UUID) - 비고: 밴드 및 소속 멤버를 cascade soft-delete.
- DELETE
/api/v1/bands/{bandId}/members/{bandMemberId} - 인증 필요 (리더)
- Path Variables:
bandId(UUID),bandMemberId(UUID) - 비고: 리더 자신은 강퇴 불가.
- POST
/api/v1/practices - 인증 필요
- Request Body
{ "title": "TuNA 정기공연 1주차 합주", "song": "550e8400-e29b-41d4-a716-446655440000", "venue": "홍대 스튜디오", "startAt": "2026-03-15 18:00", "durationMinutes": 60 }title: optionalvenue: optionalsong: UUID 만 허용 (PracticeSong ID). 검색 결과의 텍스트/임시 식별자는 받지 않음.startAt형식:yyyy-MM-dd HH:mm(Asia/Seoul 기준). 현재 이후만 허용 (@Future); 과거 시각은 400 +fieldErrors.startAt.
- Response
{ "practiceId": "550e8400-e29b-41d4-a716-446655440000", "practiceTitle": "TuNA 정기공연 1주차 합주" } - 합주 시작하기 마법사 호출 시퀀스 (FE-API-020): PracticeSong 을 먼저 생성한 후 응답의
songId를song필드로 전달. Practice 와 PracticeSong 의 1:1 바인딩은 PracticeSong 생성 시점에practiceId를 함께 전달하거나 (선바인딩), 본 API 의song으로 후바인딩.- 외부 검색 결과 사용:
POST /practice-songs/from-song(practiceId 생략) →songId획득 →POST /practices의song으로 전달 - 자작곡 입력:
POST /practice-songs(practiceId 생략) →songId획득 →POST /practices의song으로 전달
- 외부 검색 결과 사용:
- GET
/api/v1/practices - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 bandIdUUID N - 특정 밴드 멤버가 참여 중인 합주만 조회 lastIdUUID N - 이전 페이지 마지막 합주 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PracticeListResponse, UUID> - 비고:
bandId미제공 시 모든 합주 조회.bandId제공 시 해당 밴드 소속 멤버 중 1명 이상이 참여하는 합주만 반환 (밴드에 멤버가 없으면 빈 목록).
- GET
/api/v1/practices/me - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 lastIdUUID N - 이전 페이지 마지막 합주 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PracticeListResponse, UUID>{ "practiceId": "550e8400-e29b-41d4-a716-446655440000", "title": "TuNA 정기공연 1주차 합주", "startAt": "2026-03-15 18:00", "durationMinutes": 60, "venue": "홍대 스튜디오" } - 비고: 현재 로그인한 회원이 참여 중인 합주만 조회
- GET
/api/v1/practices/me/search - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 keywordString Y - 검색 키워드 (합주 타이틀 또는 곡 제목) lastIdUUID N - 이전 페이지 마지막 합주 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PracticeListResponse, UUID> - 비고: 본인이 참여 중인 합주 중 합주 타이틀 또는 합주곡 제목에
keyword가 포함된 합주만 대소문자 구분 없이 조회
- GET
/api/v1/practices/{practiceId} - 인증 필요
- Path Variable:
practiceId(UUID) - Response
{ "practiceId": "550e8400-e29b-41d4-a716-446655440000", "title": "TuNA 정기공연 1주차 합주", "venue": "홍대 스튜디오", "startAt": "2026-03-15 18:00", "durationMinutes": 60, "song": { "songId": "550e8400-e29b-41d4-a716-446655440000", "title": "Stairway to Heaven", "artist": "Led Zeppelin" }, "sessions": [ { "sessionId": "550e8400-e29b-41d4-a716-446655440000", "label": "Guitar 2", "type": "GUITAR", "participant": null } ], "participants": [ { "participantId": "550e8400-e29b-41d4-a716-446655440000", "memberId": 1 } ] }
- POST
/api/v1/practices/{practiceId}/sessions - 인증 필요
- Path Variable:
practiceId(UUID) - Request Body
{ "label": "Guitar 2", "type": "GUITAR" }typeenum —VOCAL|CHORUS|GUITAR|BASS|DRUM|PERCUSSION|SYNTH|ETC(기본값:ETC)
- Response
{ "sessionId": "550e8400-e29b-41d4-a716-446655440000", "label": "Guitar 2", "type": "GUITAR", "participant": null }
- DELETE
/api/v1/practices/{practiceId}/sessions/{sessionId} - 인증 필요
- Path Variables:
practiceId(UUID),sessionId(UUID)
- POST
/api/v1/practices/{practiceId}/participants - 인증 필요
- Path Variable:
practiceId(UUID) - Request Body
{ "memberId": 1 } - Response
{ "participantId": "550e8400-e29b-41d4-a716-446655440000", "memberId": 1 }
- PATCH
/api/v1/practices/{practiceId}/sessions/{sessionId}/assignment - 인증 필요
- Path Variables:
practiceId(UUID),sessionId(UUID) - 비고: 본인을 해당 세션에 배정
- DELETE
/api/v1/practices/{practiceId}/sessions/{sessionId}/assignment - 인증 필요
- Path Variables:
practiceId(UUID),sessionId(UUID) - 비고: 본인의 세션 배정 취소
- PATCH
/api/v1/practices/{practiceId}/schedule - 인증 필요
- Path Variable:
practiceId(UUID) - Request Body
{ "startAt": "2026-03-15 18:00", "durationMinutes": 90 }startAt형식:yyyy-MM-dd HH:mm(Asia/Seoul 기준). 현재 이후만 허용 (@Future).
- PATCH
/api/v1/practices/{practiceId}/venue - 인증 필요
- Path Variable:
practiceId(UUID) - Request Body
{ "venue": "Club FF" }
- DELETE
/api/v1/practices/{practiceId} - 인증 필요
- Path Variable:
practiceId(UUID)
- GET
/api/v1/practice-songs/search - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 keywordString Y - 검색 키워드 (곡 제목 또는 아티스트) - Response:
List<Song>— 외부 시스템 곡 정보 (DB 엔티티로 관리되지 않는 전송용 DTO)[ { "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": 426, "refLink": null } ] - 비고:
- POST
/api/v1/practice-songs - 인증 필요
- Request Body
{ "practiceId": "550e8400-e29b-41d4-a716-446655440000", "title": "자작곡 No.1", "artist": "TuNA", "album": "TuNA Demo", "duration": 300, "refLink": null }practiceId: optional. 미제공 시 PracticeSong 만 생성하고 Practice 바인딩은 수행하지 않음 (마법사 시나리오에서 응답songId를POST /practices의song으로 전달).refLink: optional
- Response:
PracticeSongResponse{ "songId": "550e8400-e29b-41d4-a716-446655440001", "title": "자작곡 No.1", "artist": "TuNA", "album": "TuNA Demo", "duration": 300, "refLink": null } - 비고: 자작곡 등 외부 API 연동이 필요 없는 곡을 사용자가 각 필드를 직접 입력하여 합주곡으로 생성. 생성된 합주곡은
practiceId의 합주에 1:1 바인딩.
- POST
/api/v1/practice-songs/from-song - 인증 필요
- Request Body
{ "practiceId": "550e8400-e29b-41d4-a716-446655440000", "song": { "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": 426, "refLink": null } }practiceId: optional. 미제공 시 PracticeSong 만 생성 (5-2와 동일 마법사 시나리오 지원).
- Response:
PracticeSongResponse(5-2와 동일 구조) - 비고: 외부 시스템에서 받아온 Song 객체를 그대로 사용하여 합주곡을 생성.
practiceId가 있으면 해당 합주에 1:1 바인딩, 없으면 응답songId를 4-1 의song으로 전달하여 후바인딩.song필드는 5-1 응답의 항목을 재가공 없이 그대로 사용 가능.
- PATCH
/api/v1/practice-songs/{songId} - 인증 필요
- Path Variable:
songId(UUID) - Request Body (모든 필드 optional, 전달된 필드만 업데이트)
{ "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": 426, "refLink": "https://www.youtube.com/watch?v=example" } - Response:
PracticeSongResponse
- PUT
/api/v1/practice-songs/{songId} - 인증 필요
- Path Variable:
songId(UUID) - Request Body:
Song(전체 필드){ "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": 426, "refLink": "https://www.youtube.com/watch?v=example" } - Response:
PracticeSongResponse - 비고: Song 객체의 값으로 모든 필드를 갱신. Request Body는 5-1 응답의 항목을 재가공 없이 그대로 사용 가능.
- PUT
/api/v1/practice-songs/{songId}/ref-link - 인증 필요
- Path Variable:
songId(UUID) - Request Body
{ "refLink": "https://www.youtube.com/watch?v=example" }
- DELETE
/api/v1/practice-songs/{songId}/ref-link - 인증 필요
- Path Variable:
songId(UUID)
- POST
/api/v1/performances - 인증 필요
- Request Body
{ "title": "TuNA 정기공연", "bandIds": ["550e8400-e29b-41d4-a716-446655440000"], "startAt": "2026-06-15 18:00", "durationMinutes": 120, "venue": "Club FF" }bandIds: optional (미전달/null/빈 배열 모두 허용 — 빈 배열로 보정)venue: optionalstartAt형식:yyyy-MM-dd HH:mm(Asia/Seoul 기준). 현재 이후만 허용 (@Future); 과거 시각은 400 +fieldErrors.startAt.
- Response
{ "performanceId": "550e8400-e29b-41d4-a716-446655440000", "title": "TuNA 정기공연" } - 비고: 생성자는 자동으로 PerformanceManager로 등록
- GET
/api/v1/performances - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 bandIdUUID N - 특정 밴드 소속 공연만 조회 lastIdUUID N - 이전 페이지 마지막 공연 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PerformanceListResponse, UUID>{ "performanceId": "550e8400-e29b-41d4-a716-446655440000", "title": "TuNA 정기공연", "startAt": "2026-06-15 18:00", "durationMinutes": 120, "venue": "Club FF" }
- GET
/api/v1/performances/me - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 lastIdUUID N - 이전 페이지 마지막 공연 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PerformanceListResponse, UUID> - 비고: 현재 로그인한 회원이 속한 밴드가 참여하는 모든 공연 조회 (소속 밴드가 없으면 빈 목록 반환)
- GET
/api/v1/performances/search - 인증 필요
- Query Parameters
파라미터 타입 필수 기본값 설명 keywordString Y - 검색 키워드 (공연 이름) lastIdUUID N - 이전 페이지 마지막 공연 ID pageSizeInt (1~100) N 10 페이지 크기 - Response:
CursorResponse<PerformanceListResponse, UUID> - 비고: 공연 제목에
keyword가 포함된 공연을 대소문자 구분 없이 부분 매칭 조회
- GET
/api/v1/performances/{performanceId} - 인증 필요
- Path Variable:
performanceId(UUID) - Response:
PerformanceDetailResponse{ "performanceId": "550e8400-e29b-41d4-a716-446655440000", "title": "TuNA 정기공연", "startAt": "2026-06-15 18:00", "durationMinutes": 120, "venue": "Club FF", "bands": [ { "bandId": "550e8400-e29b-41d4-a716-446655440000", "bandName": "TuNA" } ], "managerIds": [1], "practices": [ { "practiceId": "550e8400-e29b-41d4-a716-446655440001", "title": "TuNA 정기공연 1주차 합주", "startAt": "2026-06-01 18:00" } ] } - 비고: 기존
bandIds/practiceIds평면 배열은 각각bands/practices요약 객체 배열로 확장됨. 프론트가 별도 호출 없이 밴드 이름 / 합주 제목 / 합주 시작시각을 표시 가능.
- PATCH
/api/v1/performances/{performanceId} - 인증 필요 (PerformanceManager)
- Path Variable:
performanceId(UUID) - Request Body (모든 필드 optional, 전달된 필드만 갱신)
{ "title": "TuNA 정기공연 (수정)", "startAt": "2026-06-15 19:00", "durationMinutes": 90, "venue": "Club FF" }- 모든 필드 optional. 미전달 필드는 기존 값 유지.
startAt: 전달 시 현재 이후만 허용 (@Future).
- POST
/api/v1/performances/{performanceId}/practices - 인증 필요 (PerformanceManager)
- Path Variable:
performanceId(UUID) - Request Body
{ "title": "TuNA 정기공연 합주", "songId": "550e8400-e29b-41d4-a716-446655440000", "startAt": "2026-06-01 18:00", "durationMinutes": 60, "venue": "홍대 스튜디오" }title: optional (미입력 시 곡 제목으로 대체)venue: optionalstartAt: 현재 이후만 허용 (@Future).
- Response
{ "performancePracticeId": "550e8400-e29b-41d4-a716-446655440000", "practiceId": "550e8400-e29b-41d4-a716-446655440001" } - 비고: 빈 합주를 즉시 생성하여 공연에 연결
- POST
/api/v1/performances/{performanceId}/practices/batch - 인증 필요 (PerformanceManager)
- Path Variable:
performanceId(UUID) - Request Body
{ "practiceIds": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440001" ] } - Response:
List<PerformancePracticeResponse>[ { "performancePracticeId": "550e8400-e29b-41d4-a716-446655440000", "practiceId": "550e8400-e29b-41d4-a716-446655440001" } ]
- DELETE
/api/v1/performances/{performanceId}/practices/{practiceId} - 인증 필요 (PerformanceManager)
- Path Variables:
performanceId(UUID),practiceId(UUID) - 비고: 공연과 합주의 연결을 제거 (합주 자체는 삭제되지 않음)
- DELETE
/api/v1/performances/{performanceId} - 인증 필요 (PerformanceManager)
- Path Variable:
performanceId(UUID) - 비고: 연관된 합주도 함께 삭제
- POST
/api/v1/performances/{performanceId}/bands/batch - 인증 필요 (PerformanceManager)
- Path Variable:
performanceId(UUID) - Request Body
{ "bandIds": [ "550e8400-e29b-41d4-a716-446655440000", "550e8400-e29b-41d4-a716-446655440001" ] }bandIds: not empty
- Response:
List<PerformanceBandResponse>[ { "performanceBandId": "550e8400-e29b-41d4-a716-446655440010", "bandId": "550e8400-e29b-41d4-a716-446655440000" } ] - 비고: append 시맨틱. 이미 등록된 밴드는 무시하고 응답에서도 제외.
- DELETE
/api/v1/performances/{performanceId}/bands/{bandId} - 인증 필요 (PerformanceManager)
- Path Variables:
performanceId(UUID),bandId(UUID) - 비고: 공연에서 특정 참여 밴드 매핑을 제거 (밴드 자체는 삭제되지 않음).
Base path:
/api/v1/setlist-meetings. 모든 엔드포인트 인증 필요.권한 모델
- 참여 멤버:
SetlistMeetingMember에 등록된 사용자 또는 매니저- 매니저:
SetlistMeeting.managerId와 일치하는 사용자- 잠금 상태(
lockedAt != null) 제약: 곡 생성/수정/삭제는 차단 (SETLIST_MEETING_LOCKED).공통 응답 스키마
SetlistMeetingResponse{ "meetingId": "uuid", "bandId": "uuid", "title": "여름 페스티벌 셋리스트 회의", "purpose": "PERFORMANCE", // PERFORMANCE | GENERAL "performanceId": "uuid|null", "managerId": 1, "lockedAt": "2026-04-26T12:30:45|null", "createdAt": "...", "updatedAt": "..." }
SetlistItemResponse{ "setlistItemId": "uuid", "meetingId": "uuid", "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": "07:06", "proposerId": 1, "note": "폴리리듬 도입부…", "practiceSongId": "uuid|null", // 매니저 잠금 시 매핑되는 합주곡 ID (cross-domain TODO) "sessions": [ { "sessionId": "G", "label": "기타", "short": "G", "need": 1, "custom": false, "applicants": [2, 3], "confirmed": [2] } ], "createdAt": "...", "updatedAt": "..." }
- POST
/api/v1/setlist-meetings - 인증 필요
- Request Body
{ "title": "여름 페스티벌 셋리스트 회의", "purpose": "PERFORMANCE", "performanceId": "550e8400-e29b-41d4-a716-446655440000", "bandId": "550e8400-e29b-41d4-a716-446655440001", "managerId": 1, "participantUserIds": [1, 2, 3] }title,purpose,bandId,managerId필수purpose=PERFORMANCE일 때performanceId필수managerId는participantUserIds또는 호출자 본인에 포함되어야 함 (자동 추가됨)
- Response:
SetlistMeetingResponse - 검증
purpose=PERFORMANCE+ 동일performanceId활성 회의(lockedAt=null) 존재 → 409SETLIST_PERFORMANCE_HAS_ACTIVE_MEETING- 매니저 미참여 → 400
SETLIST_MANAGER_NOT_PARTICIPANT - PERFORMANCE 인데
performanceId누락 → 400SETLIST_PERFORMANCE_REQUIRED
- GET
/api/v1/setlist-meetings/me?lastId=&pageSize=20 - 인증 필요
- Query Parameters
lastId(UUID, optional): 직전 페이지 마지막 회의 IDpageSize(Int, default20, 1~100)
- Response:
CursorResponse<SetlistMeetingResponse, UUID> - 비고: 본인이 참여(
SetlistMeetingMember) 중인 회의만 조회.
- GET
/api/v1/setlist-meetings/{meetingId} - 인증 필요 (참여 멤버 또는 매니저)
- Response:
SetlistMeetingDetailResponse{ "meetingId": "uuid", "bandId": "uuid", "title": "...", "purpose": "GENERAL", "performanceId": null, "managerId": 1, "participantUserIds": [1, 2, 3], "lockedAt": null, "createdAt": "...", "updatedAt": "..." } - 에러: 404
SETLIST_MEETING_NOT_FOUND, 403SETLIST_MEETING_FORBIDDEN
- PATCH
/api/v1/setlist-meetings/{meetingId} - 인증 필요 (Manager)
- Request Body
{ "title": "...", "managerId": 2 }- 모든 필드 nullable.
managerId변경 시 새 매니저는 참여자에 포함되어야 함.
- 모든 필드 nullable.
- Response:
SetlistMeetingResponse - 에러: 403
SETLIST_MEETING_NOT_MANAGER, 400SETLIST_MANAGER_NOT_PARTICIPANT
- DELETE
/api/v1/setlist-meetings/{meetingId} - 인증 필요 (Manager)
- Response: 204 (
ApiResponse.success(), data=null) - 비고: soft-delete (
deleted_at세팅).
- GET
/api/v1/setlist-meetings/{meetingId}/items?lastId=&pageSize=50 - 인증 필요 (참여 멤버)
- Query Parameters:
lastId(UUID),pageSize(1~200, default 50) - Response:
CursorResponse<SetlistItemResponse, UUID>— 응답 항목에 sessions/applicants/confirmed 포함.
- GET
/api/v1/setlist-meetings/{meetingId}/items/{itemId} - 인증 필요 (참여 멤버)
- Response:
SetlistItemResponse
- POST
/api/v1/setlist-meetings/{meetingId}/items - 인증 필요 (참여 멤버, 회의 진행 중)
- Request Body
{ "title": "Vicarious", "artist": "Tool", "album": "10,000 Days", "duration": "07:06", "note": "폴리리듬 도입부…", "sessions": [ { "sessionId": "V", "label": "보컬", "short": "V", "need": 1, "custom": false }, { "sessionId": "G", "label": "기타", "short": "G", "need": 2, "custom": false } ] } - Response: 생성된
SetlistItemResponse(applicants/confirmed 빈 버킷) - 에러: 409
SETLIST_MEETING_LOCKED(잠금 상태에서는 생성 불가) - 비고:
proposerId는 토큰의 사용자 ID로 자동 매핑.
- PATCH
/api/v1/setlist-meetings/{meetingId}/items/{itemId} - 인증 필요 (곡 제안자 또는 매니저)
- Request Body
{ "title": "...", "artist": "...", "album": "...", "duration": "...", "note": "...", "sessions": [ /* 제공 시 새 세션 list 그대로 교체 */ ] } - Response: 갱신된
SetlistItemResponse - 비고:
sessions변경 시 제거된 세션의 applicants/confirmed 는 cascade 정리. - 에러: 409
SETLIST_MEETING_LOCKED, 403SETLIST_ITEM_FORBIDDEN
- DELETE
/api/v1/setlist-meetings/{meetingId}/items/{itemId} - 인증 필요 (곡 제안자 또는 매니저)
- 에러: 409
SETLIST_MEETING_LOCKED, 403SETLIST_ITEM_FORBIDDEN
- POST
/api/v1/setlist-meetings/{meetingId}/items/{itemId}/sessions/{sessionId}/applicants - 인증 필요 (참여 멤버 본인)
- 비고: 본인을 해당 세션 지원자로 등록. 중복 지원은 멱등 200.
- 에러: 404
SETLIST_ITEM_SESSION_NOT_FOUND
- DELETE
/api/v1/setlist-meetings/{meetingId}/items/{itemId}/sessions/{sessionId}/applicants/{userId} - 인증 필요 (본인 —
userId가 토큰 사용자와 일치해야 함) - 비고: cascade — 본인이 해당 세션 confirmed 에 있으면 함께 제거.
- 에러: 403
SETLIST_ITEM_FORBIDDEN(본인 외 철회 시도)
- PATCH
/api/v1/setlist-meetings/{meetingId}/items/{itemId}/sessions/{sessionId}/confirmations - 인증 필요 (Manager)
- Request Body
{ "confirm": [2, 3], "unconfirm": [4] } - Response: 갱신된
SetlistItemResponse - 에러: 400
SETLIST_ITEM_SESSION_FULL(정원need초과), 404SETLIST_ITEM_SESSION_NOT_FOUND, 403SETLIST_MEETING_NOT_MANAGER
- GET
/api/v1/setlist-meetings/{meetingId}/items/{itemId}/chat?lastId=&pageSize=50 - 인증 필요 (참여 멤버)
- Query Parameters:
lastId(UUID),pageSize(1~200, default 50) - Response:
CursorResponse<SetlistChatMessageResponse, UUID>— 최신순.{ "content": [ { "messageId": "uuid", "setlistItemId": "uuid", "memberId": 1, "message": "보컬 음역대 빡셈", "createdAt": "..." } ], "nextCursor": "uuid|null", "hasNext": true }
- POST
/api/v1/setlist-meetings/{meetingId}/items/{itemId}/chat - 인증 필요 (참여 멤버)
- Request Body
{ "message": "보컬 음역대 빡셈" }message: 1~500자
- Response: 생성된
SetlistChatMessageResponse
- POST
/api/v1/setlist-meetings/{meetingId}/lock - 인증 필요 (Manager)
- Response:
SetlistLockResponse{ "lockedAt": "2026-04-26T12:30:45", "songs": [ { "setlistItemId": "uuid", "practiceSongId": "uuid|null" } ] } - 에러: 409
SETLIST_MEETING_LOCKED(이미 잠금 상태), 403SETLIST_MEETING_NOT_MANAGER - 비고: 합주곡 벌크 생성 + Performance.setlist 자동 등록은 cross-domain 후속(TODO) — 현재는 잠금 상태 전이만 수행하며
practiceSongId는 매핑 전이면 null.
- POST
/api/v1/setlist-meetings/{meetingId}/unlock - 인증 필요 (Manager)
- Response:
SetlistMeetingResponse(lockedAt=null) - 에러: 409
SETLIST_MEETING_NOT_LOCKED, 403SETLIST_MEETING_NOT_MANAGER
Base path:
/api/v1/setlist-meetings/{meetingId}하위. 모든 엔드포인트 인증 필요.권한 모델
- 참여 멤버:
SetlistMeetingMember등록자 또는 매니저 (읽기/본인 가용 시간 입력)- 매니저:
SetlistMeeting.managerId일치 사용자 (시간표 시안 / 블록 / 확정 모든 쓰기)시간 슬롯 모델
- 하루는 30분 단위 48 슬롯(0~47). 슬롯 0 = 00:00, 슬롯 18 = 09:00, 슬롯 44 = 22:00.
- 블록 길이는
durationSlots(>=1),startSlot + durationSlots <= 48.공통 응답 스키마
MemberScheduleResponse{ "userId": 1, "availableDates": ["2026-06-01", "2026-06-03"], "unavailableDates": ["2026-06-02"], "blocks": { "2026-06-01": "ffe000000000" }, // Map<LocalDate, 12-hex 비트맵> "note": "토요일 오전만 가능", "completed": true, "updatedAt": "2026-05-03T10:00:00|null" // 미입력 시 null }
ScheduleBoardResponse{ "boardId": "uuid", "meetingId": "uuid", "name": "시안 A — 주말 위주", "paletteSeed": 7, "confirmed": false, "constraints": { "workingHoursStart": 18, "workingHoursEnd": 44, "excludeLateNight": true, "maxConsecutiveMinutes": 240 }, "blocks": [ /* ScheduleBlockResponse[] */ ], "version": 0, "createdAt": "2026-05-03T10:00:00" }
ScheduleBlockResponse{ "blockId": "uuid", "songId": "uuid", // SetlistItem.id 참조 "date": "2026-06-01", "startSlot": 18, "durationSlots": 4, // 30분 × 4 = 120분 "pinned": false, "paletteIndex": 2, "songTitleOverride": null, "note": null }
- GET
/api/v1/setlist-meetings/{meetingId}/schedules/me - 인증 필요 (참여 멤버)
- Response:
MemberScheduleResponse. 미등록 상태면availableDates/unavailableDates/blocks빈 컬렉션,note=null,completed=false,updatedAt=null. - 에러: 403
SETLIST_MEETING_FORBIDDEN, 404SETLIST_MEETING_NOT_FOUND
- PUT
/api/v1/setlist-meetings/{meetingId}/schedules/me - 인증 필요 (참여 멤버 — 본인만)
- Request Body
{ "availableDates": ["2026-06-01"], "unavailableDates": ["2026-06-02"], "blocks": { "2026-06-01": "ffe000000000" }, "note": "토요일 오전만 가능", "completed": true }- 모든 필드 nullable. 부분 갱신 가능 — 단 컬렉션 필드는 전체 교체.
note최대 500자.
- Response: 갱신된
MemberScheduleResponse - 검증
availableDates ∩ unavailableDates ≠ ∅→ 400SCHEDULE_DATES_OVERLAP- 임의 날짜 ∉
practiceWindow[from..to]→ 400SCHEDULE_DATE_OUT_OF_WINDOW
- GET
/api/v1/setlist-meetings/{meetingId}/schedules - 인증 필요 (참여 멤버)
- Response:
List<MemberScheduleResponse>— 응답을 등록한 참여자만 포함 (미등록자 미포함) - 에러: 403
SETLIST_MEETING_FORBIDDEN
- GET
/api/v1/setlist-meetings/{meetingId}/schedules/aggregate - 인증 필요 (참여 멤버)
- Response
{ "dateAvailability": { "2026-06-01": { "available": 3, "unavailable": 1, "pending": 1 } }, "totalParticipants": 5, "completedCount": 4 }dateAvailability는practiceWindow의 모든 일자를 포함.pending = totalParticipants − available − unavailable(>=0).
- GET
/api/v1/setlist-meetings/{meetingId}/schedule-boards - 인증 필요 (참여 멤버)
- Response:
List<ScheduleBoardResponse>— 각 board 의blocks[]가 함께 채워짐
- POST
/api/v1/setlist-meetings/{meetingId}/schedule-boards - 인증 필요 (Manager)
- Request Body
{ "name": "시안 A — 주말 위주", "paletteSeed": 7, "constraints": { "workingHoursStart": 18, "workingHoursEnd": 44, "excludeLateNight": true, "maxConsecutiveMinutes": 240 } }name필수 (최대 50자), 나머지 optional.constraints미지정 시 기본값 적용.
- Response:
ScheduleBoardResponse(blocks=[],confirmed=false,version=0) - 에러
- 회의당 6번째 생성 → 400
SCHEDULE_BOARD_LIMIT_EXCEEDED - 403
SETLIST_MEETING_NOT_MANAGER
- 회의당 6번째 생성 → 400
- PATCH
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId} - 인증 필요 (Manager)
- Request Body — 모든 필드 nullable, 부분 갱신
{ "name": "시안 A 개정", "paletteSeed": 8, "constraints": { ... } } - Response: 갱신된
ScheduleBoardResponse - 에러
- 404
SCHEDULE_BOARD_NOT_FOUND(boardId ↔ meetingId 불일치 포함) - 409
SCHEDULE_BOARD_ALREADY_CONFIRMED(확정된 시안) - 409
SCHEDULE_BOARD_VERSION_CONFLICT(@Version동시 수정 충돌)
- 404
- DELETE
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId} - 인증 필요 (Manager)
- Response: 빈
ApiResponse.success - 비고: 소속
ScheduleBlock은 DB-level CASCADE 로 함께 삭제. - 에러: 404
SCHEDULE_BOARD_NOT_FOUND, 409SCHEDULE_BOARD_ALREADY_CONFIRMED
- PUT
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId}/blocks/{blockId} - 인증 필요 (Manager)
- Request Body
{ "songId": "uuid", "date": "2026-06-01", "startSlot": 18, "durationSlots": 4, "pinned": false, "paletteIndex": 2, "songTitleOverride": null, "note": null }songId/date/startSlot/durationSlots필수, 나머지 nullable.note최대 200자.
- PUT 의미:
blockId가 존재하지 않으면 신규 생성, 존재하면 갱신 (FE 가 UUIDv7 을 client-side 로 생성하여 path 에 전달). - Response:
ScheduleBlockResponse - 에러
- 400
SCHEDULE_SLOT_INVALID(startSlot < 0/durationSlots < 1/startSlot + durationSlots > 48) - 400
SCHEDULE_DATE_OUT_OF_WINDOW - 404
SCHEDULE_BLOCK_NOT_FOUND(blockId ↔ boardId 불일치) - 409
SCHEDULE_BOARD_ALREADY_CONFIRMED
- 400
- DELETE
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId}/blocks/{blockId} - 인증 필요 (Manager)
- Response: 빈
ApiResponse.success - 에러: 404
SCHEDULE_BLOCK_NOT_FOUND, 409SCHEDULE_BOARD_ALREADY_CONFIRMED
- PATCH
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId}/blocks/{blockId}/pin - 인증 필요 (Manager)
- Request Body
{ "pinned": true } - Response:
ScheduleBlockResponse - 에러: 404
SCHEDULE_BLOCK_NOT_FOUND, 409SCHEDULE_BOARD_ALREADY_CONFIRMED
- POST
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId}/confirm - 인증 필요 (Manager)
- 사이드 이펙트
- board 의 모든
ScheduleBlock을Practice로 일괄 생성 (single transaction, 부분 실패 시 전체 롤백)title = block.songTitleOverride ?? PracticeSong.titlestartAt = block.date.atStartOfDay() + (startSlot × 30분)durationMinutes = durationSlots × 30- 참여자:
SetlistMeetingMember전원 자동 추가
meeting.purpose=PERFORMANCE이고performanceId != null이면 각 Practice 에 대해PerformancePractice링크 생성board.confirmed = true
- board 의 모든
- Response:
ScheduleConfirmResponse{ "confirmedAt": "2026-05-03T11:00:00", "practicesCreated": [ { "practiceId": "uuid", "title": "Vicarious", "startAt": "2026-06-01T09:00:00", "durationMinutes": 120 } ], "performancePracticesLinked": [ { "performancePracticeId": "uuid", "performanceId": "uuid", "practiceId": "uuid" } ] } - 에러
- 400
SETLIST_NOT_LOCKED(회의 미잠금) - 409
SCHEDULE_BOARD_ALREADY_CONFIRMED(같은 회의의 다른 시안이 이미 confirmed) - 404
SCHEDULE_BOARD_NOT_FOUND/SETLIST_ITEM_NOT_FOUND/PRACTICE_SONG_NOT_FOUND/PERFORMANCE_NOT_FOUND - 403
SETLIST_MEETING_NOT_MANAGER
- 400
- POST
/api/v1/setlist-meetings/{meetingId}/schedule-boards/{boardId}/unconfirm - 인증 필요 (Manager)
- Response
{ "unconfirmedAt": "2026-05-03T12:00:00" } - 비고: 8-12 에서 생성된
Practice/PerformancePractice는 그대로 유지된다 (board.confirmed 토글만). - 에러: 400
SCHEDULE_BOARD_NOT_CONFIRMED, 404SCHEDULE_BOARD_NOT_FOUND, 403SETLIST_MEETING_NOT_MANAGER