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
@@ -1,7 +1,9 @@
package com.mr.domain.playing.controller;

import com.mr.domain.playing.dto.req.MidiEventSaveRequest;
import com.mr.domain.playing.dto.req.PlayingStartRequest;
import com.mr.domain.playing.dto.res.MidiEventSaveResponse;
import com.mr.domain.playing.dto.res.PlayingStartResponse;
import com.mr.domain.playing.service.PlayingService;
import com.mr.global.apipayload.ApiResponse;
import com.mr.global.security.principal.CustomUserDetails;
Expand All @@ -22,6 +24,21 @@ public class PlayingController {

private final PlayingService playingService;

@Operation(
summary = "연주 세션 시작",
description = "백킹트랙 기반 연주 세션을 생성하고 연주 시작 상태로 변경합니다."
)
@PostMapping
public ApiResponse<PlayingStartResponse> startPlaying(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @RequestBody PlayingStartRequest request
){
Long userId = userDetails.getUserId();

PlayingStartResponse response = playingService.startPlaying(userId, request);
return ApiResponse.onSuccess(response);
}

@Operation(
summary = "MIDI 이벤트 저장",
description = "연주 세션에 대한 MIDI 이벤트를 저장하고 연주를 완료 상태로 변경합니다."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
public record MidiEventSaveRequest (
@NotEmpty(message = "MIDI 이벤트 목록은 필수입니다")
@Size(max = MAX_MIDI_EVENT_COUNT, message = "MIDI 이벤트는 최대 100,000개까지 저장할 수 있습니다.")
List<@Valid MidiEventRequest> events
List<@NotNull @Valid MidiEventRequest> events
) {

public record MidiEventRequest (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mr.domain.playing.dto.req;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

public record PlayingStartRequest(

@NotNull(message = "백킹트랙 ID는 필수입니다.")
@Min(value = 1, message = "백킹트랙 ID는 1 이상이어야 합니다.")
Long backingTrackId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.mr.domain.playing.dto.res;

import com.mr.domain.backingTrack.entity.BackingTrack;
import com.mr.domain.backingTrack.entity.ChordProgression;
import com.mr.domain.backingTrack.entity.enums.ScaleType;
import com.mr.domain.playing.entity.Playing;
import com.mr.domain.playing.entity.enums.PlayingStatus;

import java.time.LocalDateTime;
import java.util.List;

public record PlayingStartResponse(
Long playingId,
PlayingStatus status,
BackingTrackResponse backingTrack,
LocalDateTime startedAt
) {

public static PlayingStartResponse from (Playing playing) {
return new PlayingStartResponse(
playing.getId(),
playing.getStatus(),
BackingTrackResponse.from(playing.getBackingTrack()),
playing.getStartedAt()
);
}

public record BackingTrackResponse(
Long backingTrackId,
String title,
String audioFileUrl,
String genre,
String keySignature,
ScaleType scaleType,
Integer bpm,
String timeSignature,
Integer playtimeSec,
List<ChordProgressionResponse> chordProgression

) {
public static BackingTrackResponse from(BackingTrack backingTrack) {
return new BackingTrackResponse(
backingTrack.getId(),
backingTrack.getTitle(),
backingTrack.getAudioFileUrl(),
backingTrack.getGenre(),
backingTrack.getKeySignature(),
backingTrack.getScaleType(),
backingTrack.getBpm(),
backingTrack.getTimeSignature(),
backingTrack.getPlaytimeSec(),
backingTrack.getChordProgressions()
.stream()
.map(ChordProgressionResponse::from)
.toList()
);
}
}

public record ChordProgressionResponse(
Integer measureNo,
Integer sequenceNo,
String chordName
) {
public static ChordProgressionResponse from(ChordProgression chordProgression) {

return new ChordProgressionResponse(
chordProgression.getMeasureNo(),
chordProgression.getSequenceNo(),
chordProgression.getChordName()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public enum MidiEventErrorStatus implements BaseCode {
DUPLICATE_MIDI_SEQUENCE(HttpStatus.BAD_REQUEST, "MIDI_400_10", "동일한 시간에 중복된 MIDI sequence 값이 존재합니다."),

PLAYING_NOT_IN_PROGRESS(HttpStatus.CONFLICT, "MIDI_409_01", "진행 중인 연주 세션에만 MIDI 이벤트를 저장할 수 있습니다."),
MIDI_SAVE_REQUEST_TOO_FREQUENT(HttpStatus.TOO_MANY_REQUESTS, "MIDI_429_01", "연주 완료 요청은 1분에 한 번만 가능합니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ public enum PlayingErrorStatus implements BaseCode {
MISSING_PLAYING_MODE(HttpStatus.BAD_REQUEST, "PLAYING_400_11", "연주 모드는 필수 입력값입니다."),
MISSING_PLAYING_STATUS(HttpStatus.BAD_REQUEST, "PLAYING_400_12", "연주 상태는 필수 입력값입니다."),
UNSUPPORTED_PLAYING_MODE(HttpStatus.BAD_REQUEST, "PLAYING_400_13", "현재 지원하지 않는 연주 모드입니다."),
PLAYING_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PLAYING_403_01", "해당 연주 기록에 접근할 수 없습니다."),
PLAYING_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAYING_404_01", "연주 기록을 찾을 수 없습니다."),
PLAYING_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PLAYING_403_01", "해당 연주에 대한 접근 권한이 없습니다."),
BACKING_TRACK_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, "PLAYING_403_02", "해당 백킹트랙으로 연주를 시작할 수 없습니다."),
PLAYING_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAYING_404_01", "연주 세션을 찾을 수 없습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAYING_404_02", "유저를 찾을 수 없습니다."),
BACKING_TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "PLAYING_404_03", "백킹트랙을 찾을 수 없습니다."),
INVALID_PLAYING_STATUS(HttpStatus.CONFLICT, "PLAYING_409_01", "현재 연주 상태에서는 요청한 작업을 수행할 수 없습니다."),
MISSING_PLAYING_START_TIME(HttpStatus.CONFLICT, "PLAYING_409_02", "연주 시작 시간이 기록되지 않았습니다."),
INVALID_PLAYING_DURATION(HttpStatus.CONFLICT, "PLAYING_409_03", "연주 종료 시간이 시작 시간보다 이전일 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,4 @@ interface PracticeTotals {
}

Optional<Playing> findByIdAndDeletedAtIsNull(Long playingId);

boolean existsByUser_UserIdAndStatusAndEndedAtAfterAndDeletedAtIsNull(
Long userId,
PlayingStatus status,
LocalDateTime endedAt
);
}
76 changes: 54 additions & 22 deletions src/main/java/com/mr/domain/playing/service/PlayingService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.mr.domain.playing.service;

import com.mr.domain.backingTrack.entity.BackingTrack;
import com.mr.domain.backingTrack.repository.BackingTrackRepository;
import com.mr.domain.playing.dto.req.MidiEventSaveRequest;
import com.mr.domain.playing.dto.req.PlayingStartRequest;
import com.mr.domain.playing.dto.res.MidiEventSaveResponse;
import com.mr.domain.playing.dto.res.PlayingStartResponse;
import com.mr.domain.playing.entity.MidiEventData;
import com.mr.domain.playing.entity.Playing;
import com.mr.domain.playing.entity.enums.PlayingStatus;
import com.mr.domain.playing.exception.MidiEventErrorStatus;
import com.mr.domain.playing.exception.PlayingErrorStatus;
import com.mr.domain.playing.repository.PlayingRepository;
import com.mr.domain.user.entity.User;
import com.mr.domain.user.repository.UserRepository;
import com.mr.global.apipayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -16,13 +22,42 @@
import java.time.LocalDateTime;
import java.util.List;

import static com.mr.domain.backingTrack.entity.enums.AccessLevel.PUBLIC;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PlayingService {

private static final long MIDI_SAVE_REQUEST_INTERVAL_MINUTES = 1L;
private final PlayingRepository playingRepository;
private final UserRepository userRepository;
private final BackingTrackRepository backingTrackRepository;

@Transactional
public PlayingStartResponse startPlaying(
Long userId, PlayingStartRequest request
) {
validateUserId(userId);

User user = userRepository.findById(userId)
.orElseThrow(() -> new GeneralException(PlayingErrorStatus.USER_NOT_FOUND));

BackingTrack backingTrack = backingTrackRepository.findByIdAndDeletedAtIsNull(request.backingTrackId())
.orElseThrow(() -> new GeneralException(PlayingErrorStatus.BACKING_TRACK_NOT_FOUND));

validateBackingTrackAccessible(userId, backingTrack);

Playing playing = Playing.createBackingTrack(
user, backingTrack, backingTrack.getBpm()
);

playing.start();
Playing savedPlaying = playingRepository.save(playing);

return PlayingStartResponse.from(savedPlaying);
Comment thread
on1yoneprivate marked this conversation as resolved.


}

@Transactional
public MidiEventSaveResponse saveMidiEvents(
Expand All @@ -34,7 +69,6 @@ public MidiEventSaveResponse saveMidiEvents(
.orElseThrow(() -> new GeneralException(PlayingErrorStatus.PLAYING_NOT_FOUND));

playing.validatePlayingOwner(userId);
validateMidiSaveRequestInterval(userId);

List<MidiEventData> midiEvents = request.events()
.stream()
Expand All @@ -54,32 +88,30 @@ public MidiEventSaveResponse saveMidiEvents(
);
}

private void validateUserId(Long userId) {
if (userId == null || userId < 1) {
throw new GeneralException(PlayingErrorStatus.MISSING_USER_ID);
}
}

private void validatePlayingId(Long playingId) {
if (playingId == null || playingId < 1 ) {
throw new GeneralException(MidiEventErrorStatus.INVALID_PLAYING_ID);
}
}

private void validateMidiSaveRequestInterval(Long userId) {
LocalDateTime oneMinuteAgo =
LocalDateTime.now()
.minusMinutes(
MIDI_SAVE_REQUEST_INTERVAL_MINUTES
);

boolean recentlyCompleted =
playingRepository
.existsByUser_UserIdAndStatusAndEndedAtAfterAndDeletedAtIsNull(
userId,
PlayingStatus.COMPLETED,
oneMinuteAgo
);

if (recentlyCompleted) {
throw new GeneralException(
MidiEventErrorStatus
.MIDI_SAVE_REQUEST_TOO_FREQUENT
);
private void validateBackingTrackAccessible(Long userId, BackingTrack backingTrack) {
if (backingTrack.getAccessLevel() == PUBLIC){
return;
}

// TODO:
// ACADEMY 접근 정책은 User-학원 관계가 추가되면
// 같은 학원 사용자에게 접근을 허용하도록 수정한다.
// 현재는 PRIVATE와 동일하게 생성자만 접근 가능하도록 처리한다.

if (backingTrack.getUser() == null || !backingTrack.getUser().getUserId().equals(userId)){
throw new GeneralException(PlayingErrorStatus.BACKING_TRACK_ACCESS_FORBIDDEN);
}
}
Comment thread
on1yoneprivate marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ void setUp() {

CustomUserDetails userDetails =
new CustomUserDetails(
1L,
USER_ID,
UserRole.ROLE_STUDENT
);
Comment thread
on1yoneprivate marked this conversation as resolved.

Expand Down
Loading
Loading