diff --git a/src/main/java/com/mr/domain/playing/controller/PlayingController.java b/src/main/java/com/mr/domain/playing/controller/PlayingController.java index 39797b14..518e2892 100644 --- a/src/main/java/com/mr/domain/playing/controller/PlayingController.java +++ b/src/main/java/com/mr/domain/playing/controller/PlayingController.java @@ -5,19 +5,17 @@ import com.mr.domain.playing.dto.req.RecordingUploadUrlRequest; import com.mr.domain.playing.dto.res.MidiEventSaveResponse; import com.mr.domain.playing.dto.res.AnalysisContextResponse; -import com.mr.domain.playing.dto.res.PlayingDeleteResponse; -import com.mr.domain.playing.dto.res.PlayingDetailResponse; import com.mr.domain.playing.dto.res.PlayingStartResponse; import com.mr.domain.playing.dto.res.RecordingUploadUrlResponse; import com.mr.domain.playing.service.PlayingService; import com.mr.global.apipayload.ApiResponse; import com.mr.global.security.principal.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -28,14 +26,14 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/playings") -@Tag(name = "연주", description = "연주 세션 생성, 파일 업로드, MIDI 저장, 연주 기록 조회 및 삭제 API") +@Tag(name = "연주", description = "연주 API") public class PlayingController { private final PlayingService playingService; @Operation( - summary = "연주 세션 시작", - description = "백킹트랙 기반 연주 세션을 생성하고 연주 시작 상태로 변경합니다." + summary = "연주 세션 시작 API", + description = "백킹트랙을 기반으로 새로운 연주 세션을 생성하고 연주를 시작합니다." ) @PostMapping public ApiResponse startPlaying( @@ -49,16 +47,14 @@ public ApiResponse startPlaying( } @Operation( - summary = "연주 녹음 파일 업로드 URL 발급", - description = """ - 연주 녹음 파일을 S3에 직접 업로드하기 위한 Presigned URL을 발급합니다. - 로그인한 사용자 본인의 IN_PROGRESS 상태 연주 세션에 대해서만 요청할 수 있습니다. - 발급받은 URL을 사용하여 클라이언트가 녹음 파일을 S3에 직접 업로드합니다. - """ + summary = "연주 녹음 파일 업로드 URL 발급 API", + description = "연주 녹음 파일을 S3에 직접 업로드하기 위한 Presigned URL을 발급합니다." ) @PostMapping("/{playingId}/recording-upload-url") public ApiResponse createRecordingUploadUrl( @AuthenticationPrincipal CustomUserDetails userDetails, + + @Parameter(description = "연주 ID", example = "128") @PathVariable Long playingId, @Valid @RequestBody RecordingUploadUrlRequest request ) { @@ -70,17 +66,14 @@ public ApiResponse createRecordingUploadUrl( } @Operation( - summary = "MIDI 이벤트 저장 및 연주 완료", - description = """ - 연주 중 수집한 MIDI 이벤트와 업로드된 녹음 파일 정보를 저장합니다. - 로그인한 사용자 본인의 IN_PROGRESS 상태 연주 세션에 대해서만 요청할 수 있습니다. - MIDI 이벤트와 녹음 파일 검증이 완료되면 연주 세션을 COMPLETED 상태로 변경합니다. - 연주 완료 트랜잭션이 정상적으로 커밋된 이후 사용자 연습 통계가 갱신됩니다. - """ + summary = "MIDI 이벤트 저장 및 연주 완료 API", + description = "연주 중 수집한 MIDI 이벤트와 녹음 파일 정보를 저장하고 연주를 완료 처리합니다." ) @PostMapping("/{playingId}/midi-events") public ApiResponse saveMidiEvents( @AuthenticationPrincipal CustomUserDetails userDetails, + + @Parameter(description = "연주 ID", example = "128") @PathVariable Long playingId, @Valid @RequestBody MidiEventSaveRequest request ){ @@ -96,32 +89,14 @@ public ApiResponse saveMidiEvents( } @Operation( - summary = "연주 세션 단건 조회", - description = """ - 완료된 연주 세션의 상세 정보를 조회합니다. - 로그인한 사용자 본인의 연주 세션만 조회할 수 있으며, - 삭제되지 않은 COMPLETED 상태의 연주 세션만 조회할 수 있습니다. - """ - ) - @GetMapping("/{playingId}") - public ApiResponse getPlayingDetail( - @AuthenticationPrincipal CustomUserDetails userDetails, - @PathVariable Long playingId - ){ - Long userId = userDetails.getUserId(); - - PlayingDetailResponse response = playingService.getPlayingDetail(userId, playingId); - - return ApiResponse.onSuccess(response); - } - - @Operation( - summary = "분석 마디 선택 정보 조회", + summary = "분석 마디 선택 정보 조회 API", description = "완료된 본인 연주의 분석 마디 선택 정보를 조회합니다." ) @GetMapping("/{playingId}/analysis-context") public ApiResponse getAnalysisContext( @AuthenticationPrincipal CustomUserDetails userDetails, + + @Parameter(description = "연주 ID", example = "128") @PathVariable Long playingId ) { AnalysisContextResponse response = playingService.getAnalysisContext( @@ -131,25 +106,4 @@ public ApiResponse getAnalysisContext( return ApiResponse.onSuccess(response); } - - @Operation( - summary = "연주 기록 삭제", - description = """ - 로그인한 사용자의 연주 기록을 삭제합니다. - 본인의 삭제되지 않은 연주 기록만 삭제할 수 있으며, - 실제 데이터는 제거하지 않고 삭제 일시를 기록하는 Soft Delete 방식으로 처리합니다. - """ - ) - @DeleteMapping("/{playingId}") - public ApiResponse deletePlaying( - @AuthenticationPrincipal CustomUserDetails userDetails, - @PathVariable Long playingId - ){ - - Long userId = userDetails.getUserId(); - - PlayingDeleteResponse response = playingService.deletePlaying(userId, playingId); - - return ApiResponse.onSuccess(response); - } } diff --git a/src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java b/src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java index 08055686..91326ee8 100644 --- a/src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java +++ b/src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java @@ -1,6 +1,7 @@ package com.mr.domain.playing.dto.req; import com.mr.domain.playing.entity.enums.MidiType; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -13,33 +14,44 @@ import static com.mr.domain.playing.constant.MidiEventConstants.MAX_MIDI_EVENT_COUNT; public record MidiEventSaveRequest ( + + @Schema(description = "연주 중 수집한 MIDI 이벤트 목록") @NotEmpty(message = "MIDI 이벤트 목록은 필수입니다") @Size(max = MAX_MIDI_EVENT_COUNT, message = "MIDI 이벤트는 최대 100,000개까지 저장할 수 있습니다.") List<@NotNull @Valid MidiEventRequest> events, + @Schema( + description = "업로드된 녹음 파일 Object Key", + example = "recordings/34/2026-08-12/161800_a1b2c3.webm" + ) @NotBlank(message = "녹음 파일 Object Key는 필수입니다.") String recordingObjectKey ) { public record MidiEventRequest ( + @Schema(description = "MIDI 이벤트 순서", example = "0") @NotNull(message = "MIDI 이벤트 순서 값은 필수입니다.") @Min(value = 0, message = "MIDI 이벤트 순서 값은 0 이상이어야 합니다.") Integer sequence, + @Schema(description = "MIDI 이벤트 타입", example = "NOTE_ON") @NotNull(message = "MIDI 이벤트 타입은 필수입니다.") MidiType type, + @Schema(description = "MIDI 피치 값", example = "60") @NotNull(message = "MIDI 피치 값은 필수입니다.") @Min(value = 0, message = "MIDI 피치 값은 0 이상이어야 합니다.") @Max(value = 127, message = "MIDI 피치 값은 127 이하여야 합니다.") Integer pitch, + @Schema(description = "MIDI 입력 강도", example = "100") @NotNull(message = "MIDI 입력 강도 값은 필수입니다.") @Min(value = 0, message = "MIDI 입력 강도 값은 0 이상이어야 합니다.") @Max(value = 127, message = "MIDI 입력 강도 값은 127 이하여야 합니다.") Integer velocity, + @Schema(description = "연주 시작 기준 MIDI 이벤트 발생 시간(ms)", example = "1250") @NotNull(message = "MIDI 이벤트 발생 시간은 필수입니다.") @Min(value = 0, message = "MIDI 이벤트 발생 시간은 0 이상이어야 합니다.") Long timestampMs diff --git a/src/main/java/com/mr/domain/playing/dto/req/PlayingStartRequest.java b/src/main/java/com/mr/domain/playing/dto/req/PlayingStartRequest.java index 7dc22dc5..9e51714a 100644 --- a/src/main/java/com/mr/domain/playing/dto/req/PlayingStartRequest.java +++ b/src/main/java/com/mr/domain/playing/dto/req/PlayingStartRequest.java @@ -1,10 +1,12 @@ package com.mr.domain.playing.dto.req; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; public record PlayingStartRequest( + @Schema(description = "연주에 사용할 백킹트랙 ID", example = "25") @NotNull(message = "백킹트랙 ID는 필수입니다.") @Min(value = 1, message = "백킹트랙 ID는 1 이상이어야 합니다.") Long backingTrackId diff --git a/src/main/java/com/mr/domain/playing/dto/req/RecordingUploadUrlRequest.java b/src/main/java/com/mr/domain/playing/dto/req/RecordingUploadUrlRequest.java index 320e058a..37336b3f 100644 --- a/src/main/java/com/mr/domain/playing/dto/req/RecordingUploadUrlRequest.java +++ b/src/main/java/com/mr/domain/playing/dto/req/RecordingUploadUrlRequest.java @@ -1,17 +1,22 @@ package com.mr.domain.playing.dto.req; import com.mr.global.file.s3.dto.FileUploadCommand; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; public record RecordingUploadUrlRequest( + + @Schema(description = "업로드할 녹음 파일명", example = "recording.webm") @NotBlank(message = "파일명은 필수입니다.") String fileName, + @Schema(description = "업로드할 녹음 파일의 Content-Type", example = "audio/webm") @NotBlank(message = "Content-Type은 필수입니다.") String contentType, + @Schema(description = "업로드할 녹음 파일 크기(byte)", example = "1048576") @NotNull(message = "파일 크기는 필수입니다.") @Positive(message = "파일 크기는 0보다 커야 합니다.") Long fileSize diff --git a/src/main/java/com/mr/domain/playing/dto/res/AnalysisContextResponse.java b/src/main/java/com/mr/domain/playing/dto/res/AnalysisContextResponse.java index 3977b334..1558223c 100644 --- a/src/main/java/com/mr/domain/playing/dto/res/AnalysisContextResponse.java +++ b/src/main/java/com/mr/domain/playing/dto/res/AnalysisContextResponse.java @@ -12,25 +12,59 @@ import java.util.List; public record AnalysisContextResponse( + @Schema(description = "연주 ID", example = "128") Long playingId, - @Schema(description = "재연주에 사용할 백킹트랙 ID", example = "11") Long backingTrackId, + + @Schema(description = "재연주에 사용할 백킹트랙 ID", example = "11") + Long backingTrackId, + + @Schema(description = "백킹트랙 제목", example = "Autumn Leaves") String title, + + @Schema(description = "장르", example = "JAZZ") String genre, + + @Schema(description = "조성", example = "Bb") String key, + + @Schema(description = "연주 BPM", example = "120") Integer bpm, + + @Schema(description = "박자표", example = "4/4") String timeSignature, + @Schema(description = "연주 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") Instant playedAt, + @Schema(description = "연주 시간(분)", example = "3") Integer durationMinutes, + + @Schema(description = "연주 시간(초)", example = "185") Integer durationSec, + + @Schema( + description = "연주 녹음 파일 접근 URL", + example = "https://example.com/recording.webm" + ) String recordingFileUrl, + + @Schema( + description = "백킹트랙 오디오 파일 접근 URL", + example = "https://example.com/backing-track.mp3" + ) String backingTrackAudioFileUrl, + + @Schema(description = "연주 중 수집된 MIDI 이벤트 목록") List midiEvents, + + @Schema(description = "백킹트랙 MIDI 데이터") JsonNode backingTrackMidiData, + + @Schema(description = "분석 가능한 전체 마디 수", example = "32") Integer totalBars + ) { public static AnalysisContextResponse from( @@ -62,10 +96,20 @@ private static Integer toDurationMinutes(Integer durationSec) { } public record MidiEvent( + + @Schema(description = "MIDI 이벤트 순서", example = "0") Integer sequence, + + @Schema(description = "MIDI 이벤트 타입", example = "NOTE_ON") MidiType type, + + @Schema(description = "MIDI 피치 값", example = "60") Integer pitch, + + @Schema(description = "MIDI 입력 강도", example = "100") Integer velocity, + + @Schema(description = "연주 시작 기준 MIDI 이벤트 발생 시간(ms)", example = "1250") Long timestampMs ) { diff --git a/src/main/java/com/mr/domain/playing/dto/res/MidiEventSaveResponse.java b/src/main/java/com/mr/domain/playing/dto/res/MidiEventSaveResponse.java index 57dd6bf8..81be5c3e 100644 --- a/src/main/java/com/mr/domain/playing/dto/res/MidiEventSaveResponse.java +++ b/src/main/java/com/mr/domain/playing/dto/res/MidiEventSaveResponse.java @@ -1,7 +1,13 @@ package com.mr.domain.playing.dto.res; +import io.swagger.v3.oas.annotations.media.Schema; + public record MidiEventSaveResponse( + + @Schema(description = "연주 ID", example = "128") Long playingId, + + @Schema(description = "저장된 MIDI 이벤트 개수", example = "256") int savedCount ) { diff --git a/src/main/java/com/mr/domain/playing/dto/res/PlayingStartResponse.java b/src/main/java/com/mr/domain/playing/dto/res/PlayingStartResponse.java index 6d717129..18329310 100644 --- a/src/main/java/com/mr/domain/playing/dto/res/PlayingStartResponse.java +++ b/src/main/java/com/mr/domain/playing/dto/res/PlayingStartResponse.java @@ -12,8 +12,14 @@ import java.util.List; public record PlayingStartResponse( + + @Schema(description = "생성된 연주 ID", example = "128") Long playingId, + + @Schema(description = "연주 상태", example = "IN_PROGRESS") PlayingStatus status, + + @Schema(description = "연주에 사용되는 백킹트랙 정보") BackingTrackResponse backingTrack, @Schema(description = "시작 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string") @@ -32,15 +38,38 @@ public static PlayingStartResponse from (Playing playing, String backingTrackAud } public record BackingTrackResponse( + + @Schema(description = "백킹트랙 ID", example = "25") Long backingTrackId, + + @Schema(description = "백킹트랙 제목", example = "Summer") String title, + + @Schema( + description = "백킹트랙 오디오 파일 접근 URL", + example = "https://example.com/backing-track.mp3" + ) String audioFileUrl, + + @Schema(description = "장르", example = "JAZZ") String genre, + + @Schema(description = "조성", example = "Bb") String keySignature, + + @Schema(description = "스케일 타입", example = "MAJOR") ScaleType scaleType, + + @Schema(description = "BPM", example = "120") Integer bpm, + + @Schema(description = "박자표", example = "4/4") String timeSignature, + + @Schema(description = "백킹트랙 재생 시간(초)", example = "180") Integer playtimeSec, + + @Schema(description = "코드 진행 목록") List chordProgression ) { @@ -64,8 +93,14 @@ public static BackingTrackResponse from(BackingTrack backingTrack, String audioF } public record ChordProgressionResponse( + + @Schema(description = "마디 번호", example = "1") Integer measureNo, + + @Schema(description = "마디 내 코드 순서", example = "1") Integer sequenceNo, + + @Schema(description = "코드명", example = "Cm7") String chordName ) { public static ChordProgressionResponse from(ChordProgression chordProgression) { diff --git a/src/main/java/com/mr/domain/playing/dto/res/RecordingUploadUrlResponse.java b/src/main/java/com/mr/domain/playing/dto/res/RecordingUploadUrlResponse.java index 7ad07ada..ecac6e14 100644 --- a/src/main/java/com/mr/domain/playing/dto/res/RecordingUploadUrlResponse.java +++ b/src/main/java/com/mr/domain/playing/dto/res/RecordingUploadUrlResponse.java @@ -8,13 +8,24 @@ import java.util.Map; public record RecordingUploadUrlResponse( + + @Schema( + description = "업로드할 녹음 파일의 S3 Object Key", + example = "recordings/34/2026-08-12/161800_a1b2c3.webm" + ) String objectKey, + + @Schema( + description = "녹음 파일 업로드용 Presigned URL", + example = "https://example.com/presigned-upload-url" + ) String uploadUrl, @Schema(description = "URL 만료 일시 (KST 기준 응답)", example = "2026-08-11T18:00:00", type = "string") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") Instant expiresAt, + @Schema(description = "S3 업로드 요청 시 포함해야 하는 필수 헤더") Map requiredHeaders ) { public static RecordingUploadUrlResponse from( diff --git a/src/main/java/com/mr/domain/playing/service/PlayingService.java b/src/main/java/com/mr/domain/playing/service/PlayingService.java index 23764655..c804d8c4 100644 --- a/src/main/java/com/mr/domain/playing/service/PlayingService.java +++ b/src/main/java/com/mr/domain/playing/service/PlayingService.java @@ -33,7 +33,6 @@ import java.time.Clock; import java.time.DayOfWeek; import java.time.Instant; -import java.time.LocalDate; import java.time.ZoneId; import java.util.List;