From 33ab80134ee03ea8bfd3b921df0b67070420f991 Mon Sep 17 00:00:00 2001 From: chan0831 <116000778+chan0831@users.noreply.github.com> Date: Tue, 11 Feb 2025 19:37:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Feat]=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8?= =?UTF-8?q?=20=EC=9E=AC=EC=84=A4=EC=A0=95=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이찬우 --- .../apiPayLoad/code/status/ErrorStatus.java | 3 +++ .../codeplay/controller/AuthController.java | 27 +++++++++++++++++++ .../codeplay/controller/LikeController.java | 3 +++ .../umc/codeplay/dto/MemberRequestDTO.java | 6 +++++ .../umc/codeplay/service/EmailService.java | 20 +++++++++++++- .../umc/codeplay/service/MemberService.java | 15 +++++++++++ 6 files changed, 73 insertions(+), 1 deletion(-) 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..83aac7f 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -38,6 +38,9 @@ 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", "해당 화성 분석 결과를 찾을 수 없습니다."), 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/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index 3623637..71631a1 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -42,6 +42,12 @@ public static class CheckVerificationCodeDTO { String code; } + @Getter + public static class ChangePasswordDTO { + String email; + String newPassword; + } + @Getter @Setter public static class UpdateMemberDTO { 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; + } } From e44894053d38e7ce36599700fb1bc4ecb2fe77c1 Mon Sep 17 00:00:00 2001 From: Minhyung Kim Date: Wed, 12 Feb 2025 18:09:39 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat]=20=EC=9E=91=EC=97=85=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20-=20SQS=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20/=20=EC=9E=91=EC=97=85=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EC=83=81=ED=99=A9=20=EC=A1=B0=ED=9A=8C=20/=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=A7=84=ED=96=89=20=EC=83=81=ED=99=A9=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8(=ED=8C=8C=EC=9D=B4=EC=8D=AC)=20(#14?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sqsandall * final fix * git actions fix --- .github/workflows/build-test.yml | 1 + build.gradle | 3 + .../apiPayLoad/code/status/ErrorStatus.java | 8 +- .../umc/codeplay/config/SecurityConfig.java | 3 +- ...ltController.java => ModelController.java} | 21 ++- .../codeplay/controller/TaskController.java | 74 +++++++++++ .../codeplay/converter/MemberConverter.java | 10 ++ src/main/java/umc/codeplay/domain/Remix.java | 1 + src/main/java/umc/codeplay/domain/Task.java | 1 + .../codeplay/domain/enums/ProcessStatus.java | 1 - .../umc/codeplay/dto/MemberRequestDTO.java | 46 ++++++- .../umc/codeplay/dto/MemberResponseDTO.java | 11 ++ .../umc/codeplay/dto/ModelRequestDTO.java | 37 +++++- .../umc/codeplay/dto/ModelResponseDTO.java | 8 ++ .../java/umc/codeplay/dto/SQSMessageDTO.java | 125 ++++++++++++++++++ .../umc/codeplay/service/ModelService.java | 125 ++++++++++++++++-- .../umc/codeplay/service/MusicService.java | 6 + .../umc/codeplay/service/TaskService.java | 76 +++++++++++ src/main/resources/application-prod.yml | 4 + src/main/resources/application.yml | 5 +- 20 files changed, 546 insertions(+), 20 deletions(-) rename src/main/java/umc/codeplay/controller/{ModelResultController.java => ModelController.java} (72%) create mode 100644 src/main/java/umc/codeplay/controller/TaskController.java create mode 100644 src/main/java/umc/codeplay/dto/SQSMessageDTO.java create mode 100644 src/main/java/umc/codeplay/service/TaskService.java 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 83aac7f..cfd08af 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -45,7 +45,13 @@ public enum ErrorStatus implements BaseErrorCode { 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/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 71631a1..84040f6 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; @@ -65,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 = "음원 id 는 -12 이상의 값이어야 합니다.") + @Max(value = 12, message = "음원 id 는 12 이하의 값이어야 합니다.") + private Integer scaleModulation; + + @DecimalMin(value = "0.1", message = "음원 id 는 0.1 이상의 값이어야 합니다.") + @DecimalMax(value = "4.0", message = "음원 id 는 4.0 이하의 값이어야 합니다.") + private Double tempoRatio; + + @DecimalMin(value = "0.0", message = "음원 id 는 0.0 이상의 값이어야 합니다.") + @DecimalMax(value = "1.0", message = "음원 id 는 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/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: "" - From 43f5187702b4127f0673fef71131e01d59022d59 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:27:50 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=20=EB=B3=80=EA=B2=BD=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/codeplay/dto/MemberRequestDTO.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index 84040f6..7c42be1 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -68,7 +68,7 @@ public static class SearchByMusicTitleDTO { @Getter public static class StemTaskDTO { - @NotNull(message = "음원 id 는 필수 입력값입니다.") Long musicId; + @NotNull(message = "음원 id는 필수 입력값입니다.") Long musicId; @NotBlank(message = "빈칸이라도 아무거나 보내주세요.") String twoStemConfig; @@ -77,26 +77,26 @@ public static class StemTaskDTO { @Getter public static class HarmonyTaskDTO { - @NotNull(message = "음원 id 는 필수 입력값입니다.") private Long musicId; + @NotNull(message = "음원 id는 필수 입력값입니다.") private Long musicId; } @Getter public static class RemixTaskDTO { - @NotNull(message = "음원 id 는 필수 입력값입니다.") private Long musicId; + @NotNull(message = "음원 id는 필수 입력값입니다.") private Long musicId; private Long parentRemixId; - @Min(value = -12, message = "음원 id 는 -12 이상의 값이어야 합니다.") - @Max(value = 12, message = "음원 id 는 12 이하의 값이어야 합니다.") + @Min(value = -12, message = "스케일 변경값은 -12 이상의 값이어야 합니다.") + @Max(value = 12, message = "스케일 변경값은 12 이하의 값이어야 합니다.") private Integer scaleModulation; - @DecimalMin(value = "0.1", message = "음원 id 는 0.1 이상의 값이어야 합니다.") - @DecimalMax(value = "4.0", message = "음원 id 는 4.0 이하의 값이어야 합니다.") + @DecimalMin(value = "0.1", message = "템포 배속값은 0.1 이상의 값이어야 합니다.") + @DecimalMax(value = "4.0", message = "템포 배속값은 4.0 이하의 값이어야 합니다.") private Double tempoRatio; - @DecimalMin(value = "0.0", message = "음원 id 는 0.0 이상의 값이어야 합니다.") - @DecimalMax(value = "1.0", message = "음원 id 는 1.0 이하의 값이어야 합니다.") + @DecimalMin(value = "0.0", message = "리버브 값은 0.0 이상의 값이어야 합니다.") + @DecimalMax(value = "1.0", message = "리버브 값은 1.0 이하의 값이어야 합니다.") private Double reverbAmount; private Boolean isChorusOn; @@ -105,6 +105,6 @@ public static class RemixTaskDTO { @Getter public static class getTaskDTO { - @NotNull(message = "task id 는 필수 입력값입니다.") Long taskId; + @NotNull(message = "task id는 필수 입력값입니다.") Long taskId; } }