diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f7ca5d8..b1cfc53 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,6 +14,7 @@ jobs: AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} S3_BUCKET: ${{ secrets.S3_BUCKET }} JWT_SECRET: ${{ secrets.JWT_SECRET }} + SQS_QUEUE_NAME: ${{ secrets.SQS_QUEUE_NAME }} # GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} # GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} # GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} diff --git a/build.gradle b/build.gradle index 0173f77..541141e 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,9 @@ dependencies { // mail implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework:spring-context-support' + + // sqs + implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs' } tasks.named('test') { diff --git a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java index c1d32e5..cfd08af 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -38,11 +38,20 @@ public enum ErrorStatus implements BaseErrorCode { EMAIL_SEND_ERROR(HttpStatus.BAD_REQUEST, "EMAIL400", "메일 발송에 실패하였습니다."), EMAIL_CODE_ERROR(HttpStatus.BAD_REQUEST, "EMAIL401", "유효한 코드가 아닙니다."), + EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "EMAIL402", "인증되지 않은 이메일입니다."), + + PASSWORD_CHANGE_FAILED(HttpStatus.BAD_REQUEST, "PASSWORD400", "비밀번호 재설정이 실패하였습니다."), TASK_NOT_FOUND(HttpStatus.BAD_REQUEST, "TASK400", "해당 task를 찾을 수 없습니다."), HARMONY_NOT_FOUND(HttpStatus.BAD_REQUEST, "HARMONY400", "해당 화성 분석 결과를 찾을 수 없습니다."), TRACK_NOT_FOUND(HttpStatus.BAD_REQUEST, "TRACK400", "해당 트랙 분리 결과를 찾을 수 없습니다."), - REMIX_NOT_FOUND(HttpStatus.BAD_REQUEST, "REMIX400", "해당 리믹스 결과를 찾을 수 없습니다."); + REMIX_NOT_FOUND(HttpStatus.BAD_REQUEST, "REMIX400", "해당 리믹스 결과를 찾을 수 없습니다."), + REMIX_NO_CHANGE(HttpStatus.BAD_REQUEST, "REMIX401", "변경 사항이 없습니다."), + JOB_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "JOB400", "해당 작업 타입을 찾을 수 없습니다."), + INVALID_CONFIG(HttpStatus.BAD_REQUEST, "TRACK400", "유효하지 않은 설정입니다."), + INVALID_PARENT_REMIX(HttpStatus.BAD_REQUEST, "REMIX402", "유효하지 않은 부모 리믹스입니다."), + + SQS_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SQS500", "SQS 메시지 전송에 실패하였습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/umc/codeplay/config/SecurityConfig.java b/src/main/java/umc/codeplay/config/SecurityConfig.java index 604c77e..a44f71b 100644 --- a/src/main/java/umc/codeplay/config/SecurityConfig.java +++ b/src/main/java/umc/codeplay/config/SecurityConfig.java @@ -60,11 +60,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti auth // 로그인, 회원가입 등 토큰 없이 접근해야 하는 API 허용 .requestMatchers( + "/python-model/**", "/oauth/**", "/health", "/health/s3", "/auth/**", - "/member/**", + "/member/**", // ?? "/v2/api-docs", "/v3/api-docs", "/v3/api-docs/**", diff --git a/src/main/java/umc/codeplay/controller/AuthController.java b/src/main/java/umc/codeplay/controller/AuthController.java index eeb9020..c9eade1 100644 --- a/src/main/java/umc/codeplay/controller/AuthController.java +++ b/src/main/java/umc/codeplay/controller/AuthController.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.stream.Collectors; import jakarta.mail.MessagingException; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -20,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import umc.codeplay.apiPayLoad.ApiResponse; import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; import umc.codeplay.converter.MemberConverter; import umc.codeplay.domain.Member; @@ -120,6 +122,9 @@ public ApiResponse refresh( } // 비밀번호 찾기 및 변경. 이메일 인증 + @Operation( + summary = "비밀번호 찾기 -> 이메일 요청", + description = "이메일을 통해 인증번호 전송 요청 api, 인증번호는 5분간 유효합니다.") @PostMapping("/password/reset/request") public ApiResponse resetPasswordRequest( @RequestBody MemberRequestDTO.ResetPasswordDTO request) throws MessagingException { @@ -128,15 +133,37 @@ public ApiResponse resetPasswordRequest( } // 비밀번호 찾기 및 변경. 인증 코드 확인 + @Operation(summary = "비밀번호 찾기 -> 이메일 인증하기", description = "인증번호 인증하는 api") @PostMapping("/password/reset/verify") public ApiResponse resetPasswordVerify( @RequestBody MemberRequestDTO.CheckVerificationCodeDTO request) { boolean isValid = emailService.verifyCode(request.getEmail(), request.getCode()); if (isValid) { + emailService.markVerified(request.getEmail()); return ApiResponse.onSuccess("인증에 성공하였습니다."); // 이후에 비밀번호 변경 페이지 연결해 주어야 함. } else { throw new GeneralHandler(ErrorStatus.EMAIL_CODE_ERROR); } } + + // 비밀번호 잊었을 때 -> 변경 + @Operation(summary = "비밀번호 찾기 -> 재설정", description = "비밀번호 찾기에서 비밀번호 재설정 하는 api") + @PostMapping("/password/reset/change") + public ApiResponse changePassword( + @RequestBody @Valid MemberRequestDTO.ChangePasswordDTO request) { + String email = request.getEmail(); + + // 인증 확인 + if (!emailService.isVerified(email)) { + throw new GeneralException(ErrorStatus.EMAIL_NOT_VERIFIED); + } + boolean isChanged = memberService.newPassword(email, request.getNewPassword()); + if (isChanged) { + emailService.invalidateVerificationCode(email); + return ApiResponse.onSuccess("비밀번호 변경이 완료되었습니다."); + } else { + throw new GeneralException(ErrorStatus.PASSWORD_CHANGE_FAILED); + } + } } diff --git a/src/main/java/umc/codeplay/controller/LikeController.java b/src/main/java/umc/codeplay/controller/LikeController.java index 5fda8c8..05f0834 100644 --- a/src/main/java/umc/codeplay/controller/LikeController.java +++ b/src/main/java/umc/codeplay/controller/LikeController.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; +import io.swagger.v3.oas.annotations.Operation; import umc.codeplay.apiPayLoad.ApiResponse; import umc.codeplay.converter.MusicLikeConverter; import umc.codeplay.domain.Music; @@ -23,6 +24,7 @@ public class LikeController { private final LikeService likeService; + @Operation(summary = "좋아요 추가") @PostMapping("/like/add") public ApiResponse addLike( @RequestBody @Valid LikeRequestDTO.addLikeRequestDTO request) { @@ -34,6 +36,7 @@ public ApiResponse addLike( return ApiResponse.onSuccess(MusicLikeConverter.toLikeResponseDTO(like)); } + @Operation(summary = "좋아요 취소") @PostMapping("/like/remove") public ApiResponse removeLike( @RequestBody @Valid LikeRequestDTO.removeLikeRequestDTO request) { diff --git a/src/main/java/umc/codeplay/controller/ModelResultController.java b/src/main/java/umc/codeplay/controller/ModelController.java similarity index 72% rename from src/main/java/umc/codeplay/controller/ModelResultController.java rename to src/main/java/umc/codeplay/controller/ModelController.java index 802dc62..01e089a 100644 --- a/src/main/java/umc/codeplay/controller/ModelResultController.java +++ b/src/main/java/umc/codeplay/controller/ModelController.java @@ -10,17 +10,20 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import umc.codeplay.apiPayLoad.ApiResponse; +import umc.codeplay.domain.enums.ProcessStatus; import umc.codeplay.dto.ModelRequestDTO; import umc.codeplay.dto.ModelResponseDTO; import umc.codeplay.service.ModelService; +import umc.codeplay.service.TaskService; @RestController @RequiredArgsConstructor @RequestMapping("/python-model") @Tag(name = "py-model-controller", description = "Python 로컬 서버의 분석 결과를 저장하는 API (프론트엔드 사용 X)") -public class ModelResultController { +public class ModelController { private final ModelService modelService; + private final TaskService taskService; @Operation( summary = "화성 분석 결과 저장", @@ -58,4 +61,20 @@ public ApiResponse updateTrack( .remixId(modelService.setRemix(remixRequestDTO)) .build()); } + + @Operation( + summary = "작업 실패 업데이트", + description = "Python 서버의 작업 실패 결과를 저장합니다. 프론트엔드에서 사용하지 않습니다.") + @PostMapping("/fail") + public ApiResponse updateTaskFail( + @RequestBody ModelRequestDTO.TaskFailDTO request) { + // TODO: 작업 등록 시 생성되었던 엔티티 제거 로직 추가. + return ApiResponse.onSuccess( + ModelResponseDTO.TaskFailDTO.builder() + .taskId( + taskService + .updateTaskStatus(request.getTaskId(), ProcessStatus.FAILED) + .getId()) + .build()); + } } diff --git a/src/main/java/umc/codeplay/controller/TaskController.java b/src/main/java/umc/codeplay/controller/TaskController.java new file mode 100644 index 0000000..6cb7da1 --- /dev/null +++ b/src/main/java/umc/codeplay/controller/TaskController.java @@ -0,0 +1,74 @@ +package umc.codeplay.controller; + +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import umc.codeplay.apiPayLoad.ApiResponse; +import umc.codeplay.converter.MemberConverter; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.Task; +import umc.codeplay.dto.MemberRequestDTO; +import umc.codeplay.dto.MemberResponseDTO; +import umc.codeplay.service.ModelService; +import umc.codeplay.service.MusicService; +import umc.codeplay.service.TaskService; + +@RestController +@RequestMapping("/task") +@RequiredArgsConstructor +@Validated +@Tag(name = "task-controller", description = "화성분석/스템분리/리믹스 작업 요청 API") +public class TaskController { + + private final MusicService musicService; + private final ModelService modelService; + private final TaskService taskService; + + @Operation(summary = "화성분석 작업 요청", description = "음악 ID를 받아 화성분석 작업을 요청합니다.") + @PostMapping("/harmony") + public ApiResponse requestHarmonyTask( + @RequestBody @Validated MemberRequestDTO.HarmonyTaskDTO request) { + Music music = musicService.findById(request.getMusicId()); + + Task task = modelService.sendHarmonyMessage(music); + + return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); + } + + @Operation(summary = "스템분리 작업 요청", description = "음악 ID와 스템분리 설정을 받아 스템분리 작업을 요청합니다.") + @PostMapping("/stem") + public ApiResponse requestStemTask( + @RequestBody @Validated MemberRequestDTO.StemTaskDTO request) { + Music music = musicService.findById(request.getMusicId()); + + Task task = modelService.sendTrackMessage(music, request.getTwoStemConfig()); + + return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); + } + + @Operation(summary = "리믹스 작업 요청", description = "음악 ID와 리믹스 설정을 받아 리믹스 작업을 요청합니다.") + @PostMapping("/remix") + public ApiResponse requestRemixTask( + @RequestBody @Validated MemberRequestDTO.RemixTaskDTO request) { + Music music = musicService.findById(request.getMusicId()); + + Task task = modelService.sendRemixMessage(music, request); + + return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); + } + + @Operation(summary = "작업 조회", description = "작업 ID를 받아 작업 상태를 조회합니다.") + @PostMapping("/get-task") + public ApiResponse getTask( + @RequestBody @Validated MemberRequestDTO.getTaskDTO request) { + Task task = taskService.findById(request.getTaskId()); + return ApiResponse.onSuccess(MemberConverter.toTaskProgressDTO(task)); + } +} diff --git a/src/main/java/umc/codeplay/converter/MemberConverter.java b/src/main/java/umc/codeplay/converter/MemberConverter.java index f9fdc27..b205e0b 100644 --- a/src/main/java/umc/codeplay/converter/MemberConverter.java +++ b/src/main/java/umc/codeplay/converter/MemberConverter.java @@ -6,6 +6,7 @@ import umc.codeplay.domain.Harmony; import umc.codeplay.domain.Member; +import umc.codeplay.domain.Task; import umc.codeplay.domain.Track; import umc.codeplay.domain.enums.Role; import umc.codeplay.domain.enums.SocialStatus; @@ -81,4 +82,13 @@ public MemberResponseDTO.GetMyTrackDTO toGetMyTrackDTO(Track track, Member membe member, track.getMusic())) // LikeService에서 좋아요 여부 확인 .build(); } + + public static MemberResponseDTO.TaskProgressDTO toTaskProgressDTO(Task task) { + return MemberResponseDTO.TaskProgressDTO.builder() + .taskId(task.getId()) + .processStatus(task.getStatus().toString()) + .jobType(task.getJobType().toString()) + .jobId(task.getJobId()) + .build(); + } } diff --git a/src/main/java/umc/codeplay/domain/Remix.java b/src/main/java/umc/codeplay/domain/Remix.java index 9afe704..9c10ffa 100644 --- a/src/main/java/umc/codeplay/domain/Remix.java +++ b/src/main/java/umc/codeplay/domain/Remix.java @@ -50,6 +50,7 @@ public class Remix extends BaseEntity { @JoinColumn(name = "music_id", nullable = false) private Music music; + @Setter @ManyToOne(fetch = FetchType.LAZY) @Comment("이전 단계 리믹스 ID") @JoinColumn(name = "parent_remix_id") diff --git a/src/main/java/umc/codeplay/domain/Task.java b/src/main/java/umc/codeplay/domain/Task.java index 713700b..89b4b27 100644 --- a/src/main/java/umc/codeplay/domain/Task.java +++ b/src/main/java/umc/codeplay/domain/Task.java @@ -21,6 +21,7 @@ public class Task extends BaseEntity { // TODO: 추후 BigInteger로 변환 private Long id; + @Setter @Enumerated(EnumType.STRING) @Comment("task 진행 상태") @Builder.Default diff --git a/src/main/java/umc/codeplay/domain/enums/ProcessStatus.java b/src/main/java/umc/codeplay/domain/enums/ProcessStatus.java index e842122..430b1bf 100644 --- a/src/main/java/umc/codeplay/domain/enums/ProcessStatus.java +++ b/src/main/java/umc/codeplay/domain/enums/ProcessStatus.java @@ -3,7 +3,6 @@ public enum ProcessStatus { // todo: 그냥 예시로 적어둠 변경 필요 REQUESTED, - ONGOING, COMPLETED, FAILED; } diff --git a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index 3623637..7c42be1 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -1,7 +1,6 @@ package umc.codeplay.dto; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; @@ -42,6 +41,12 @@ public static class CheckVerificationCodeDTO { String code; } + @Getter + public static class ChangePasswordDTO { + String email; + String newPassword; + } + @Getter @Setter public static class UpdateMemberDTO { @@ -59,4 +64,47 @@ public static class SearchByMusicTitleDTO { @NotBlank(message = "음원 제목은 필수 입력값입니다.") String musicTitle; } + + @Getter + public static class StemTaskDTO { + + @NotNull(message = "음원 id는 필수 입력값입니다.") Long musicId; + + @NotBlank(message = "빈칸이라도 아무거나 보내주세요.") + String twoStemConfig; + } + + @Getter + public static class HarmonyTaskDTO { + + @NotNull(message = "음원 id는 필수 입력값입니다.") private Long musicId; + } + + @Getter + public static class RemixTaskDTO { + + @NotNull(message = "음원 id는 필수 입력값입니다.") private Long musicId; + + private Long parentRemixId; + + @Min(value = -12, message = "스케일 변경값은 -12 이상의 값이어야 합니다.") + @Max(value = 12, message = "스케일 변경값은 12 이하의 값이어야 합니다.") + private Integer scaleModulation; + + @DecimalMin(value = "0.1", message = "템포 배속값은 0.1 이상의 값이어야 합니다.") + @DecimalMax(value = "4.0", message = "템포 배속값은 4.0 이하의 값이어야 합니다.") + private Double tempoRatio; + + @DecimalMin(value = "0.0", message = "리버브 값은 0.0 이상의 값이어야 합니다.") + @DecimalMax(value = "1.0", message = "리버브 값은 1.0 이하의 값이어야 합니다.") + private Double reverbAmount; + + private Boolean isChorusOn; + } + + @Getter + public static class getTaskDTO { + + @NotNull(message = "task id는 필수 입력값입니다.") Long taskId; + } } diff --git a/src/main/java/umc/codeplay/dto/MemberResponseDTO.java b/src/main/java/umc/codeplay/dto/MemberResponseDTO.java index 51100c3..4388b35 100644 --- a/src/main/java/umc/codeplay/dto/MemberResponseDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberResponseDTO.java @@ -80,4 +80,15 @@ public static class GetAllByMusicTitleDTO { private List harmonies; private List tracks; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TaskProgressDTO { + Long taskId; + String processStatus; + String jobType; + Long jobId; + } } diff --git a/src/main/java/umc/codeplay/dto/ModelRequestDTO.java b/src/main/java/umc/codeplay/dto/ModelRequestDTO.java index eabf3d0..9d4ce04 100644 --- a/src/main/java/umc/codeplay/dto/ModelRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/ModelRequestDTO.java @@ -1,5 +1,8 @@ package umc.codeplay.dto; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,10 +13,18 @@ public class ModelRequestDTO { @NoArgsConstructor @AllArgsConstructor public static class HarmonyRequestDTO { - private Long taskId; + + @NotNull(message = "taskId 는 필수 입력 값입니다.") private Long taskId; + + @NotBlank(message = "scale 는 필수 입력 값입니다.") private String scale; - private Integer bpm; + + @NotNull(message = "bpm 는 필수 입력 값입니다.") private Integer bpm; + + @NotBlank(message = "genre 는 필수 입력 값입니다.") private String genre; + + @NotBlank(message = "voiceColor 는 필수 입력 값입니다.") private String voiceColor; } @@ -21,7 +32,11 @@ public static class HarmonyRequestDTO { @NoArgsConstructor @AllArgsConstructor public static class TrackRequestDTO { - private Long taskId; + + @NotNull(message = "taskId 는 필수 입력 값입니다.") private Long taskId; + + @NotNull(message = "isTwoStem 는 필수 입력 값입니다.") private Boolean isTwoStem; + private String vocalUrl; private String instrumentalUrl; private String bassUrl; @@ -32,7 +47,21 @@ public static class TrackRequestDTO { @NoArgsConstructor @AllArgsConstructor public static class RemixRequestDTO { - private Long taskId; + + @NotNull(message = "taskId 는 필수 입력 값입니다.") private Long taskId; + + @NotBlank(message = "resultMusicUrl 는 필수 입력 값입니다.") private String resultMusicUrl; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TaskFailDTO { + + @NotNull(message = "taskId 는 필수 입력 값입니다.") private Long taskId; + + @NotBlank(message = "failMessage 는 필수 입력 값입니다.") + private String failMessage; + } } diff --git a/src/main/java/umc/codeplay/dto/ModelResponseDTO.java b/src/main/java/umc/codeplay/dto/ModelResponseDTO.java index 518d9e2..d35b7a9 100644 --- a/src/main/java/umc/codeplay/dto/ModelResponseDTO.java +++ b/src/main/java/umc/codeplay/dto/ModelResponseDTO.java @@ -30,4 +30,12 @@ public static class TrackResponseDTO { public static class RemixResponseDTO { Long remixId; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TaskFailDTO { + Long taskId; + } } diff --git a/src/main/java/umc/codeplay/dto/SQSMessageDTO.java b/src/main/java/umc/codeplay/dto/SQSMessageDTO.java new file mode 100644 index 0000000..6fc358d --- /dev/null +++ b/src/main/java/umc/codeplay/dto/SQSMessageDTO.java @@ -0,0 +1,125 @@ +package umc.codeplay.dto; + +import lombok.*; + +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; +import umc.codeplay.domain.Remix; + +public class SQSMessageDTO { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class HarmonyMessageDTO { + String key; + Long taskId; + String jobType; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TrackMessageDTO { + String key; + Long taskId; + String jobType; + + @Setter String twoStemConfig; + } + + @Getter + @Setter + public static class RemixMessageDTO { + String key; + Long taskId; + String jobType; + + Integer scaleModulation; + Double tempoRatio; + Double reverbAmount; + Boolean isChorusOn; + + @Builder + public RemixMessageDTO() { + this.scaleModulation = 0; + this.tempoRatio = 1.0; + this.reverbAmount = 0.0; + this.isChorusOn = false; + } + + public void setRemix(Remix parentRemix, MemberRequestDTO.RemixTaskDTO request) { + boolean change = false; + + if (request.getScaleModulation() == null) { + scaleModulation = parentRemix.getScaleModulation(); + } else { + scaleModulation = request.getScaleModulation(); + change = (!scaleModulation.equals(parentRemix.getScaleModulation())); + } + + if (request.getTempoRatio() == null) { + tempoRatio = parentRemix.getTempoRatio(); + } else { + tempoRatio = request.getTempoRatio(); + change = change || (!tempoRatio.equals(parentRemix.getTempoRatio())); + } + + if (request.getReverbAmount() == null) { + reverbAmount = parentRemix.getReverbAmount(); + } else { + reverbAmount = request.getReverbAmount(); + change = change || (!reverbAmount.equals(parentRemix.getReverbAmount())); + } + + if (request.getIsChorusOn() == null) { + isChorusOn = parentRemix.getIsChorusOn(); + } else { + isChorusOn = request.getIsChorusOn(); + change = change || (!isChorusOn.equals(parentRemix.getIsChorusOn())); + } + + if (!change) { + throw new GeneralException(ErrorStatus.REMIX_NO_CHANGE); + } + } + + public void setRemix(MemberRequestDTO.RemixTaskDTO request) { + boolean change = false; + + if (request.getScaleModulation() == null) { + scaleModulation = 0; + } else { + scaleModulation = request.getScaleModulation(); + change = (!scaleModulation.equals(0)); + } + + if (request.getTempoRatio() == null) { + tempoRatio = 1.0; + } else { + tempoRatio = request.getTempoRatio(); + change = change || (!tempoRatio.equals(1.0)); + } + + if (request.getReverbAmount() == null) { + reverbAmount = 0.0; + } else { + reverbAmount = request.getReverbAmount(); + change = change || (!reverbAmount.equals(0.0)); + } + + if (request.getIsChorusOn() == null) { + isChorusOn = false; + } else { + isChorusOn = request.getIsChorusOn(); + change = change || (!isChorusOn.equals(false)); + } + + if (!change) { + throw new GeneralException(ErrorStatus.REMIX_NO_CHANGE); + } + } + } +} diff --git a/src/main/java/umc/codeplay/service/EmailService.java b/src/main/java/umc/codeplay/service/EmailService.java index 971ebdb..dcab35b 100644 --- a/src/main/java/umc/codeplay/service/EmailService.java +++ b/src/main/java/umc/codeplay/service/EmailService.java @@ -80,9 +80,27 @@ public boolean verifyCode(String email, String code) { return false; } if (verificationCode.getExpires().isBefore(LocalDateTime.now())) { - verificationCodes.remove(email); // 만료 코드 삭제 + verificationCodes.remove(email); // 만료된 코드 삭제 return false; } return verificationCode.getCode().equals(code); } + + // 인증상태 저장 + private final Map verifiedEmails = new ConcurrentHashMap<>(); + + // 인증 성공 시, 인증상태 변경 + public void markVerified(String email) { + verifiedEmails.put(email, true); + } + + // 인증 상태인지 확인 + public boolean isVerified(String email) { + return verifiedEmails.getOrDefault(email, false); + } + + // 변경 이후 인증 상태 초기화 + public void invalidateVerificationCode(String email) { + verifiedEmails.remove(email); + } } diff --git a/src/main/java/umc/codeplay/service/MemberService.java b/src/main/java/umc/codeplay/service/MemberService.java index 8675514..1518ff2 100644 --- a/src/main/java/umc/codeplay/service/MemberService.java +++ b/src/main/java/umc/codeplay/service/MemberService.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; import umc.codeplay.converter.MemberConverter; import umc.codeplay.domain.Harmony; @@ -156,4 +157,18 @@ public List getTrackByMusicTitle(Member member, .map(track -> memberConverter.toGetMyTrackDTO(track, member)) .collect(Collectors.toList()); } + + // 비밀번호 찾기 이후 변경 + @Transactional + public boolean newPassword(String email, String newPassword) { + + Member member = + memberRepository + .findByEmail(email) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + + member.encodePassword(passwordEncoder.encode(newPassword)); + memberRepository.save(member); + return true; + } } diff --git a/src/main/java/umc/codeplay/service/ModelService.java b/src/main/java/umc/codeplay/service/ModelService.java index fd797a3..751fabc 100644 --- a/src/main/java/umc/codeplay/service/ModelService.java +++ b/src/main/java/umc/codeplay/service/ModelService.java @@ -1,30 +1,40 @@ package umc.codeplay.service; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import umc.codeplay.domain.Harmony; -import umc.codeplay.domain.Remix; -import umc.codeplay.domain.Task; -import umc.codeplay.domain.Track; +import io.awspring.cloud.sqs.operations.SqsTemplate; +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; +import umc.codeplay.domain.*; +import umc.codeplay.domain.enums.JobType; +import umc.codeplay.domain.enums.ProcessStatus; +import umc.codeplay.dto.MemberRequestDTO; import umc.codeplay.dto.ModelRequestDTO; +import umc.codeplay.dto.SQSMessageDTO; import umc.codeplay.repository.HarmonyRepository; import umc.codeplay.repository.RemixRepository; -import umc.codeplay.repository.TaskRepository; import umc.codeplay.repository.TrackRepository; @Service @RequiredArgsConstructor public class ModelService { - private final TaskRepository taskRepository; + private final SqsTemplate sqsTemplate; private final HarmonyRepository harmonyRepository; private final TrackRepository trackRepository; private final RemixRepository remixRepository; + private final TaskService taskService; + + @Value("${sqs.queue.name}") + private String queueName; public Long setHarmony(ModelRequestDTO.HarmonyRequestDTO harmonyResult) { - Task task = taskRepository.findByIdOrThrow(harmonyResult.getTaskId()); + Task task = + taskService.updateTaskStatus(harmonyResult.getTaskId(), ProcessStatus.COMPLETED); Harmony harmony = harmonyRepository.findByIdOrThrow(task.getJobId()); harmony.updateHarmonyResult( @@ -36,7 +46,7 @@ public Long setHarmony(ModelRequestDTO.HarmonyRequestDTO harmonyResult) { } public Long setTrack(ModelRequestDTO.TrackRequestDTO trackResult) { - Task task = taskRepository.findByIdOrThrow(trackResult.getTaskId()); + Task task = taskService.updateTaskStatus(trackResult.getTaskId(), ProcessStatus.COMPLETED); Track track = trackRepository.findByIdOrThrow(task.getJobId()); track.updateTrackResult( @@ -48,10 +58,107 @@ public Long setTrack(ModelRequestDTO.TrackRequestDTO trackResult) { } public Long setRemix(ModelRequestDTO.RemixRequestDTO remixResult) { - Task task = taskRepository.findByIdOrThrow(remixResult.getTaskId()); + Task task = taskService.updateTaskStatus(remixResult.getTaskId(), ProcessStatus.COMPLETED); Remix remix = remixRepository.findByIdOrThrow(task.getJobId()); remix.setResultMusicUrl(remixResult.getResultMusicUrl()); return remixRepository.save(remix).getId(); } + + @Transactional + public Task sendTrackMessage(Music music, String config) { + + Task task = taskService.addTask(music, JobType.TRACK); + + if (config == null) { + config = "none"; + } + + switch (config) { + case "vocals", "bass", "drums", "none": // TODO: 6-stems guitar, piano 테스트 후 추가 + break; + default: + throw new GeneralException(ErrorStatus.INVALID_CONFIG); + } + + try { + sqsTemplate.send( + queueName, + SQSMessageDTO.TrackMessageDTO.builder() + .key(music.getTitle()) + .taskId(task.getId()) + .jobType(JobType.TRACK.toString()) + .twoStemConfig(config) + .build()); + } catch (Exception e) { + throw new GeneralException(ErrorStatus.SQS_SEND_ERROR); + } + return task; + } + + @Transactional + public Task sendHarmonyMessage(Music music) { + + Task task = taskService.addTask(music, JobType.HARMONY); + + try { + sqsTemplate.send( + queueName, + SQSMessageDTO.HarmonyMessageDTO.builder() + .key(music.getTitle()) + .taskId(task.getId()) + .jobType(JobType.HARMONY.toString()) + .build()); + } catch (Exception e) { + throw new GeneralException(ErrorStatus.SQS_SEND_ERROR); + } + return task; + } + + @Transactional + public Task sendRemixMessage(Music music, MemberRequestDTO.RemixTaskDTO request) { + + Remix parentRemix = null; + SQSMessageDTO.RemixMessageDTO remixPayLoad = + SQSMessageDTO.RemixMessageDTO.builder().build(); + + if (request.getParentRemixId() != null) { + parentRemix = remixRepository.findByIdOrThrow(request.getParentRemixId()); + if (!parentRemix.getMusic().getId().equals(music.getId())) { + throw new GeneralException(ErrorStatus.INVALID_PARENT_REMIX); + } + remixPayLoad.setRemix(parentRemix, request); + } else { + remixPayLoad.setRemix(request); + } + + Remix newRemix = + remixRepository.save( + Remix.builder() + .music(music) + .scaleModulation(remixPayLoad.getScaleModulation()) + .tempoRatio(remixPayLoad.getTempoRatio()) + .reverbAmount(remixPayLoad.getReverbAmount()) + .isChorusOn(remixPayLoad.getIsChorusOn()) + .build()); + + if (parentRemix != null) { + newRemix.setParentRemix(parentRemix); + parentRemix.getChildRemixList().add(newRemix); + remixRepository.save(parentRemix); + } + + Task task = taskService.addTask(newRemix); + + remixPayLoad.setKey(music.getTitle()); + remixPayLoad.setTaskId(task.getId()); + remixPayLoad.setJobType(JobType.REMIX.toString()); + + try { + sqsTemplate.send(queueName, remixPayLoad); + } catch (Exception e) { + throw new GeneralException(ErrorStatus.SQS_SEND_ERROR); + } + return task; + } } diff --git a/src/main/java/umc/codeplay/service/MusicService.java b/src/main/java/umc/codeplay/service/MusicService.java index 59101d5..ba04ae4 100644 --- a/src/main/java/umc/codeplay/service/MusicService.java +++ b/src/main/java/umc/codeplay/service/MusicService.java @@ -26,4 +26,10 @@ public void deleteMusic(Long id) { musicRepository.deleteById(id); } + + public Music findById(Long id) { + return musicRepository + .findById(id) + .orElseThrow(() -> new GeneralException(ErrorStatus.MUSIC_NOT_FOUND)); + } } diff --git a/src/main/java/umc/codeplay/service/TaskService.java b/src/main/java/umc/codeplay/service/TaskService.java new file mode 100644 index 0000000..833498e --- /dev/null +++ b/src/main/java/umc/codeplay/service/TaskService.java @@ -0,0 +1,76 @@ +package umc.codeplay.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; +import umc.codeplay.domain.*; +import umc.codeplay.domain.enums.JobType; +import umc.codeplay.domain.enums.ProcessStatus; +import umc.codeplay.repository.HarmonyRepository; +import umc.codeplay.repository.RemixRepository; +import umc.codeplay.repository.TaskRepository; +import umc.codeplay.repository.TrackRepository; + +@Service +@RequiredArgsConstructor +public class TaskService { + + private final TaskRepository taskRepository; + private final RemixRepository remixRepository; + private final TrackRepository trackRepository; + private final HarmonyRepository harmonyRepository; + + public Task findById(Long id) { + return taskRepository + .findById(id) + .orElseThrow(() -> new GeneralException(ErrorStatus.TASK_NOT_FOUND)); + } + + public Task removeTask(Long id) { + Task task = findById(id); + // TODO: 정크 엔티티 삭제 + taskRepository.delete(task); + return task; + } + + public Task updateTaskStatus(Long id, ProcessStatus status) { + Task task = findById(id); + task.setStatus(status); + return taskRepository.save(task); + } + + public Task addTask(Music music, JobType jobType) { + Long jobId = + switch (jobType) { + case HARMONY -> harmonyRepository + .save(Harmony.builder().music(music).build()) + .getId(); + case TRACK -> trackRepository + .save(Track.builder().music(music).build()) + .getId(); + default -> throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + }; + + Task task = + Task.builder() + .status(ProcessStatus.REQUESTED) + .jobType(jobType) + .jobId(jobId) + .build(); + return taskRepository.save(task); + } + + public Task addTask(Remix newRemix) { + Task task = + Task.builder() + .status(ProcessStatus.REQUESTED) + .jobType(JobType.REMIX) + .jobId(newRemix.getId()) + .build(); + + return taskRepository.save(task); + } +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 064e089..bccfc04 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -42,6 +42,10 @@ s3: jwt: secret: ${JWT_SECRET} +sqs: + queue: + name: ${SQS_QUEUE_NAME} + google: oauth2: client-id: ${GOOGLE_CLIENT_ID} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8699fdf..b8d907a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,6 +50,10 @@ s3: jwt: secret: ${JWT_SECRET} +sqs: + queue: + name: ${SQS_QUEUE_NAME} + google: oauth2: client-id: ${GOOGLE_CLIENT_ID} @@ -71,4 +75,3 @@ kakao: token-uri: "https://kauth.kakao.com/oauth/token" user-info-uri: "https://kapi.kakao.com/v2/user/me" additional-parameters: "" -