Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ awscliv2.zip

# Spring Boot local config
src/main/resources/application-local.yml
src/main/resources/application.yml

# copyPrivateConfig 태스크가 MR_config/local에서 복사해 오는 빌드 산출물 (실제 비밀키 포함 가능)
src/main/resources/application.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.mr.domain.backingTrack.controller;

import com.mr.domain.backingTrack.dto.req.BackingTrackSaveRequestDTO;
import com.mr.domain.backingTrack.dto.res.BackingTrackCreateResponseDTO;
import com.mr.domain.backingTrack.dto.res.BackingTrackUpdateResponseDTO;
import com.mr.domain.backingTrack.service.BackingTrackService;
import com.mr.global.apipayload.ApiResponse;
import com.mr.global.security.principal.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/backing-tracks")
public class BackingTrackController {

private final BackingTrackService backingTrackService;

@PostMapping
public ApiResponse<BackingTrackCreateResponseDTO.CreateResultDTO> createBackingTrack(
@AuthenticationPrincipal CustomUserDetails userDetails,
@Valid @RequestBody BackingTrackSaveRequestDTO.SaveDTO request
) {
BackingTrackCreateResponseDTO.CreateResultDTO result =
backingTrackService.createBackingTrack(userDetails.getUserId(), request);

return ApiResponse.onSuccess(result);
}

@PutMapping("/{backingTrackId}")
public ApiResponse<BackingTrackUpdateResponseDTO.UpdateResultDTO> updateBackingTrack(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long backingTrackId,
@Valid @RequestBody BackingTrackSaveRequestDTO.SaveDTO request
) {
Long userId = userDetails.getUserId();
BackingTrackUpdateResponseDTO.UpdateResultDTO result =
backingTrackService.updateBackingTrack(userId, backingTrackId, request);
return ApiResponse.onSuccess(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.mr.domain.backingTrack.dto.req;

import com.mr.domain.backingTrack.entity.enums.AccessLevel;
import com.mr.domain.backingTrack.entity.enums.Level;
import com.mr.domain.backingTrack.entity.enums.ScaleType;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import java.util.List;
import java.util.Map;

public class BackingTrackSaveRequestDTO {

public record SaveDTO(
@NotBlank(message = "백킹트랙 제목은 필수입니다.")
@Size(max = 50, message = "백킹트랙 제목은 50자 이내여야 합니다.")
String title,

@NotBlank(message = "장르는 필수입니다.")
@Size(max = 50, message = "장르는 50자 이내여야 합니다.")
String genre,

@NotBlank(message = "Key 정보는 필수입니다.")
@Size(max = 20, message = "Key 정보는 20자 이내여야 합니다.")
String keySignature,

@NotNull(message = "조성은 필수입니다.")
ScaleType scaleType,

@NotBlank(message = "박자는 필수입니다.")
@Pattern(regexp = "^([1-9]|1[0-6])/(2|4|8|16)$", message = "음악적으로 유효한 박자 형식이어야 합니다. (예: 4/4, 3/4, 6/8)")
String timeSignature,
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@NotNull(message = "BPM은 필수입니다.")
@Min(value = 50, message = "BPM은 50 이상이어야 합니다.")
@Max(value = 200, message = "BPM은 200 이하여야 합니다.")
Integer bpm,

@NotNull(message = "재생 시간은 필수입니다.")
@Min(value = 60, message = "재생 시간은 최소 60초(1분) 이상이어야 합니다.")
@Max(value = 600, message = "재생 시간은 최대 600초(10분) 이하여야 합니다.")
Integer playtimeSec,

@Size(max = 255, message = "오디오 파일 URL은 255자 이내여야 합니다.")
String audioFileUrl,

@NotNull(message = "공개 범위는 필수입니다.")
AccessLevel accessLevel,

@NotNull(message = "난이도는 필수입니다.")
Level level,

@Valid
@NotEmpty(message = "코드 진행 정보는 필수입니다.")
List<ChordProgressionDTO> chordProgression
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.
) {}

public record ChordProgressionDTO(
@NotNull(message = "마디 번호는 필수입니다.")
@Min(value = 1, message = "마디 번호는 1 이상이어야 합니다.")
Integer measureNo,

@NotNull(message = "코드 순서는 필수입니다.")
@Min(value = 1, message = "코드 순서는 1 이상이어야 합니다.")
Integer sequenceNo,
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.

@NotBlank(message = "코드명은 필수입니다.")
@Size(max = 30, message = "코드명은 30자 이내여야 합니다.")
String chordName
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mr.domain.backingTrack.dto.res;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

public class BackingTrackCreateResponseDTO {

public record CreateResultDTO(
Long backingTrackId,
String title,

// 날짜 포맷
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
LocalDateTime createdAt
) {
public static CreateResultDTO of(Long backingTrackId, String title, LocalDateTime createdAt) {
return new CreateResultDTO(backingTrackId, title, createdAt);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mr.domain.backingTrack.dto.res;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

public class BackingTrackUpdateResponseDTO {

public record UpdateResultDTO(
Long backingTrackId,
String title,

// 날짜 포맷
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss")
LocalDateTime updatedAt
) {
public static BackingTrackUpdateResponseDTO.UpdateResultDTO of(Long backingTrackId, String title, LocalDateTime updatedAt) {
return new BackingTrackUpdateResponseDTO.UpdateResultDTO(backingTrackId, title, updatedAt);
}
}
}
51 changes: 37 additions & 14 deletions src/main/java/com/mr/domain/backingTrack/entity/BackingTrack.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
package com.mr.domain.backingTrack.entity;

import com.fasterxml.jackson.databind.JsonNode;
import com.mr.domain.backingTrack.entity.enums.AccessLevel;
import com.mr.domain.backingTrack.entity.enums.Level;
import com.mr.domain.backingTrack.entity.enums.ScaleType;
import com.mr.domain.user.entity.User;
import com.mr.global.entity.BaseTimeDeletedEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.util.ArrayList;
import java.util.List;

@Getter
@Entity
Expand All @@ -36,11 +47,12 @@ public class BackingTrack extends BaseTimeDeletedEntity {
private Long id;

// 유저 아이디
@Column(name = "user_id", nullable = false)
private Long userId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

// 학원 아이디
@Column(name = "academy_id")
@Column(name = "academy_id", nullable = false)
private Long academyId;

// 트랙 이름
Expand Down Expand Up @@ -76,8 +88,9 @@ public class BackingTrack extends BaseTimeDeletedEntity {
@Column(name = "audio_file_url", length = 255)
private String audioFileUrl;

@Column(name = "midi_file_url", columnDefinition = "JSON")
private String midiFileUrl;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "midi_data", columnDefinition = "jsonb")
private JsonNode midiData;

// 재생 수
@Column(name = "play_count", nullable = false)
Expand All @@ -93,13 +106,17 @@ public class BackingTrack extends BaseTimeDeletedEntity {
@Column(name = "level", nullable = false)
private Level level;

// 양방향 매핑 추가, 영속성 전이랑 고아 객체 제거
@OneToMany(mappedBy = "backingTrack", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ChordProgression> chordProgressions = new ArrayList<>();

@Builder(access = lombok.AccessLevel.PRIVATE)
private BackingTrack(Long userId, Long academyId, String title, String genre,
private BackingTrack(User user, Long academyId, String title, String genre,
String keySignature, ScaleType scaleType, String timeSignature,
Integer bpm, Integer playtimeSec, String audioFileUrl,
String midiFileUrl, Integer playCount,
JsonNode midiData, Integer playCount,
AccessLevel accessLevel, Level level) {
this.userId = userId;
this.user = user;
this.academyId = academyId;
this.title = title;
this.genre = genre;
Expand All @@ -109,18 +126,18 @@ private BackingTrack(Long userId, Long academyId, String title, String genre,
this.bpm = bpm;
this.playtimeSec = playtimeSec;
this.audioFileUrl = audioFileUrl;
this.midiFileUrl = midiFileUrl;
this.midiData = midiData;
this.playCount = playCount != null ? playCount : 0;
this.accessLevel = accessLevel != null ? accessLevel : AccessLevel.PRIVATE;
this.level = level != null ? level : Level.BASIC;
}

public static BackingTrack create(Long userId, Long academyId, String title, String genre,
public static BackingTrack create(User user, Long academyId, String title, String genre,
String keySignature, ScaleType scaleType, String timeSignature,
Integer bpm, Integer playtimeSec, String audioFileUrl,
String midiFileUrl, AccessLevel accessLevel, Level level) {
JsonNode midiData, AccessLevel accessLevel, Level level) {
return BackingTrack.builder()
.userId(userId)
.user(user)
.academyId(academyId)
.title(title)
.genre(genre)
Expand All @@ -130,7 +147,7 @@ public static BackingTrack create(Long userId, Long academyId, String title, Str
.bpm(bpm)
.playtimeSec(playtimeSec)
.audioFileUrl(audioFileUrl)
.midiFileUrl(midiFileUrl)
.midiData(midiData)
.playCount(0)
.accessLevel(accessLevel)
.level(level)
Expand All @@ -139,14 +156,15 @@ public static BackingTrack create(Long userId, Long academyId, String title, Str

public void updateTrackInfo(String title, String genre, String keySignature,
ScaleType scaleType, String timeSignature, Integer bpm,
Integer playtimeSec, AccessLevel accessLevel, Level level) {
Integer playtimeSec, String audioFileUrl, AccessLevel accessLevel, Level level) {
this.title = title;
this.genre = genre;
this.keySignature = keySignature;
this.scaleType = scaleType;
this.timeSignature = timeSignature;
this.bpm = bpm;
this.playtimeSec = playtimeSec;
this.audioFileUrl = audioFileUrl;
if (accessLevel != null) this.accessLevel = accessLevel;
if (level != null) this.level = level;
}
Expand All @@ -157,4 +175,9 @@ public void changeAccessLevel(AccessLevel accessLevel) {
this.accessLevel = accessLevel;
}
}

// 코드 진행 추가
public void addChordProgression(ChordProgression chordProgression) {
this.chordProgressions.add(chordProgression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ChordProgression{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "chord_progression_id")
private Long chordProgressionId;
private Long id;

// 백킹트랙 아이디
@ManyToOne(fetch = FetchType.LAZY)
Expand Down Expand Up @@ -61,12 +61,17 @@ private ChordProgression(BackingTrack backingTrack, Integer sequenceNo, Integer
}

public static ChordProgression create(BackingTrack backingTrack, Integer sequenceNo, Integer measureNo, String chordName) {
return ChordProgression.builder()
ChordProgression chordProgression = ChordProgression.builder()
.backingTrack(backingTrack)
.sequenceNo(sequenceNo)
.measureNo(measureNo)
.chordName(chordName)
.build();
// 양방향 매핑 추가 하고 연결하게
if (backingTrack != null) {
backingTrack.addChordProgression(chordProgression);
}
return chordProgression;
}

public void updateChordInfo(Integer sequenceNo, Integer measureNo, String chordName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.mr.domain.backingTrack.exception;

import com.mr.global.apipayload.code.BaseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum BackingTrackErrorStatus implements BaseCode {

// [400] Validation Errors
Comment thread
rkdehdrbs7885-oss marked this conversation as resolved.
TITLE_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_01", "백킹트랙 제목은 필수입니다."),
TITLE_TOO_LONG(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_02", "백킹트랙 제목은 50자 이내여야 합니다."),
GENRE_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_03", "장르는 필수입니다."),
UNSUPPORTED_GENRE(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_04", "지원하지 않는 장르입니다."),
KEY_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_05", "Key 정보는 필수입니다."),
INVALID_KEY(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_06", "Key 정보가 올바르지 않습니다."),
SCALE_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_07", "조성은 필수입니다."),
INVALID_SCALE(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_08", "조성 값이 올바르지 않습니다."),
TIME_SIGNATURE_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_09", "박자는 필수입니다."),
INVALID_TIME_SIGNATURE(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_10", "박자 형식이 올바르지 않습니다."),
BPM_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_11", "BPM은 필수입니다."),
INVALID_BPM(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_12", "BPM 값이 올바르지 않습니다."),
PLAYTIME_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_13", "재생 시간은 필수입니다."),
INVALID_PLAYTIME(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_14", "재생 시간은 60초 이상 600초 이하여야 합니다."),
ACCESS_LEVEL_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_17", "공개 범위는 필수입니다."),
INVALID_ACCESS_LEVEL(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_18", "공개 범위 값이 올바르지 않습니다."),
LEVEL_REQUIRED(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_19", "난이도는 필수입니다."),
INVALID_LEVEL(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_20", "난이도 값이 올바르지 않습니다."),
DUPLICATE_CHORD_POSITION(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_21", "동일한 마디(measureNo)와 순서(sequenceNo)를 가진 코드 진행이 중복으로 존재합니다."),
INVALID_CHORD_SEQUENCE(HttpStatus.BAD_REQUEST, "BACKING_TRACK_400_22", "마디의 허용된 코드 순서 범위를 초과했습니다."),

// [403] 권한 에러
FORBIDDEN_UPDATE(HttpStatus.FORBIDDEN, "BACKING_TRACK_403_01", "백킹트랙 수정 권한이 없습니다."),

// [404] 리소스 없음
BACKING_TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "BACKING_TRACK_404_01", "존재하지 않는 백킹트랙입니다."),

// [500] 서버 에러
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "BACKING_TRACK_500_01", "백킹트랙 생성 중 서버 오류가 발생했습니다.");

private final HttpStatus status;
private final String code;
private final String message;
}
Loading
Loading