Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PlayingStartResponse> startPlaying(
Expand All @@ -49,16 +47,14 @@ public ApiResponse<PlayingStartResponse> 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<RecordingUploadUrlResponse> createRecordingUploadUrl(
@AuthenticationPrincipal CustomUserDetails userDetails,

@Parameter(description = "연주 ID", example = "128")
@PathVariable Long playingId,
@Valid @RequestBody RecordingUploadUrlRequest request
) {
Expand All @@ -70,17 +66,14 @@ public ApiResponse<RecordingUploadUrlResponse> createRecordingUploadUrl(
}

@Operation(
summary = "MIDI 이벤트 저장 및 연주 완료",
description = """
연주 중 수집한 MIDI 이벤트와 업로드된 녹음 파일 정보를 저장합니다.
로그인한 사용자 본인의 IN_PROGRESS 상태 연주 세션에 대해서만 요청할 수 있습니다.
MIDI 이벤트와 녹음 파일 검증이 완료되면 연주 세션을 COMPLETED 상태로 변경합니다.
연주 완료 트랜잭션이 정상적으로 커밋된 이후 사용자 연습 통계가 갱신됩니다.
"""
summary = "MIDI 이벤트 저장 및 연주 완료 API",
description = "연주 중 수집한 MIDI 이벤트와 녹음 파일 정보를 저장하고 연주를 완료 처리합니다."
)
@PostMapping("/{playingId}/midi-events")
public ApiResponse<MidiEventSaveResponse> saveMidiEvents(
@AuthenticationPrincipal CustomUserDetails userDetails,

@Parameter(description = "연주 ID", example = "128")
@PathVariable Long playingId,
@Valid @RequestBody MidiEventSaveRequest request
){
Expand All @@ -96,32 +89,14 @@ public ApiResponse<MidiEventSaveResponse> saveMidiEvents(
}

@Operation(
summary = "연주 세션 단건 조회",
description = """
완료된 연주 세션의 상세 정보를 조회합니다.
로그인한 사용자 본인의 연주 세션만 조회할 수 있으며,
삭제되지 않은 COMPLETED 상태의 연주 세션만 조회할 수 있습니다.
"""
)
@GetMapping("/{playingId}")
public ApiResponse<PlayingDetailResponse> 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<AnalysisContextResponse> getAnalysisContext(
@AuthenticationPrincipal CustomUserDetails userDetails,

@Parameter(description = "연주 ID", example = "128")
@PathVariable Long playingId
) {
AnalysisContextResponse response = playingService.getAnalysisContext(
Expand All @@ -131,25 +106,4 @@ public ApiResponse<AnalysisContextResponse> getAnalysisContext(

return ApiResponse.onSuccess(response);
}

@Operation(
summary = "연주 기록 삭제",
description = """
로그인한 사용자의 연주 기록을 삭제합니다.
본인의 삭제되지 않은 연주 기록만 삭제할 수 있으며,
실제 데이터는 제거하지 않고 삭제 일시를 기록하는 Soft Delete 방식으로 처리합니다.
"""
)
@DeleteMapping("/{playingId}")
public ApiResponse<PlayingDeleteResponse> deletePlaying(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long playingId
){

Long userId = userDetails.getUserId();

PlayingDeleteResponse response = playingService.deletePlaying(userId, playingId);

return ApiResponse.onSuccess(response);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MidiEvent> midiEvents,

@Schema(description = "백킹트랙 MIDI 데이터")
JsonNode backingTrackMidiData,

@Schema(description = "분석 가능한 전체 마디 수", example = "32")
Integer totalBars

) {

public static AnalysisContextResponse from(
Expand Down Expand Up @@ -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
) {

Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<ChordProgressionResponse> chordProgression

) {
Expand All @@ -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) {
Expand Down
Loading