diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 085bae6..d29cb2b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -40,7 +40,7 @@ jobs: - name: make application.yml run: | mkdir -p src/main/resources - echo "$APPLICATION" > src/main/resources/application.properties + echo "$APPLICATION" > src/main/resources/application.yml env: APPLICATION: ${{ secrets.APPLICATION }} diff --git a/build.gradle b/build.gradle index 3b2513f..c4c5f8b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,66 +1,66 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.2.4' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.2.4' + id 'io.spring.dependency-management' version '1.1.4' } group = 'fairytale' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } // build 이름 변경 -jar{ - archiveBaseName = 'fairytale' - version = '0.0.1-SNAPSHOT' - enabled = false +jar { + archiveBaseName = 'fairytale' + version = '0.0.1-SNAPSHOT' + enabled = false } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' - //logging - implementation 'org.springframework.boot:spring-boot-starter-log4j2' - modules { - module("org.springframework.boot:spring-boot-starter-logging") { - replacedBy("org.springframework.boot:spring-boot-starter-log4j2") - } - } + //logging + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + modules { + module("org.springframework.boot:spring-boot-starter-logging") { + replacedBy("org.springframework.boot:spring-boot-starter-log4j2") + } + } - // ElevenLabs API - implementation ('net.andrewcpu:elevenlabs-api:2.7.8') + // ElevenLabs API + implementation('net.andrewcpu:elevenlabs-api:2.7.8') - // AWS - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + // AWS + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - // Spring Security OAUTH 2.1 - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.security:spring-security-oauth2-client' + // Spring Security OAUTH 2.1 + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.security:spring-security-oauth2-client' - // JWT token - implementation 'io.jsonwebtoken:jjwt:0.12.3' + // JWT token + implementation 'io.jsonwebtoken:jjwt:0.12.3' - compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'com.mysql:mysql-connector-j' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/fairytale/tbd/domain/fairytale/converter/Fairytaleconverter.java b/src/main/java/fairytale/tbd/domain/fairytale/converter/Fairytaleconverter.java new file mode 100644 index 0000000..8dcb252 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/converter/Fairytaleconverter.java @@ -0,0 +1,23 @@ +package fairytale.tbd.domain.fairytale.converter; + +import java.util.ArrayList; + +import fairytale.tbd.domain.fairytale.entity.Fairytale; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleRequestDTO; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleResponseDTO; + +public class Fairytaleconverter { + public static Fairytale toFairytale(FairytaleRequestDTO.AddFairytaleRequestDTO request) { + return Fairytale.builder() + .name(request.getName()) + .segmentList(new ArrayList<>()) + .build(); + } + + public static FairytaleResponseDTO.AddFairytaleResultDTO toAddFairytaleResultDTO(Fairytale fairytale) { + return FairytaleResponseDTO.AddFairytaleResultDTO.builder() + .id(fairytale.getId()) + .createdAt(fairytale.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/fairytale/tbd/domain/voice/entity/Fairytale.java b/src/main/java/fairytale/tbd/domain/fairytale/entity/Fairytale.java similarity index 91% rename from src/main/java/fairytale/tbd/domain/voice/entity/Fairytale.java rename to src/main/java/fairytale/tbd/domain/fairytale/entity/Fairytale.java index cf4ad7c..c39c3b0 100644 --- a/src/main/java/fairytale/tbd/domain/voice/entity/Fairytale.java +++ b/src/main/java/fairytale/tbd/domain/fairytale/entity/Fairytale.java @@ -1,8 +1,9 @@ -package fairytale.tbd.domain.voice.entity; +package fairytale.tbd.domain.fairytale.entity; import java.util.ArrayList; import java.util.List; +import fairytale.tbd.domain.voice.entity.Segment; import fairytale.tbd.global.entity.BaseEntity; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleExistExcption.java b/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleExistExcption.java new file mode 100644 index 0000000..a539c1f --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleExistExcption.java @@ -0,0 +1,10 @@ +package fairytale.tbd.domain.fairytale.exception; + +import fairytale.tbd.global.enums.statuscode.BaseCode; +import fairytale.tbd.global.exception.GeneralException; + +public class FairytaleExistExcption extends GeneralException { + public FairytaleExistExcption(BaseCode errorStatus) { + super(errorStatus); + } +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleNotFoundException.java b/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleNotFoundException.java new file mode 100644 index 0000000..62117fe --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/exception/FairytaleNotFoundException.java @@ -0,0 +1,10 @@ +package fairytale.tbd.domain.fairytale.exception; + +import fairytale.tbd.global.enums.statuscode.BaseCode; +import fairytale.tbd.global.exception.GeneralException; + +public class FairytaleNotFoundException extends GeneralException { + public FairytaleNotFoundException(BaseCode errorStatus) { + super(errorStatus); + } +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/repository/FairytaleRepository.java b/src/main/java/fairytale/tbd/domain/fairytale/repository/FairytaleRepository.java new file mode 100644 index 0000000..634e05c --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/repository/FairytaleRepository.java @@ -0,0 +1,11 @@ +package fairytale.tbd.domain.fairytale.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import fairytale.tbd.domain.fairytale.entity.Fairytale; + +@Repository +public interface FairytaleRepository extends JpaRepository { + boolean existsByName(String name); +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandService.java b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandService.java new file mode 100644 index 0000000..7998897 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandService.java @@ -0,0 +1,8 @@ +package fairytale.tbd.domain.fairytale.service; + +import fairytale.tbd.domain.fairytale.entity.Fairytale; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleRequestDTO; + +public interface FairytaleCommandService { + Fairytale saveFairytale(FairytaleRequestDTO.AddFairytaleRequestDTO request); +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandServiceImpl.java b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandServiceImpl.java new file mode 100644 index 0000000..942060a --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleCommandServiceImpl.java @@ -0,0 +1,30 @@ +package fairytale.tbd.domain.fairytale.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import fairytale.tbd.domain.fairytale.converter.Fairytaleconverter; +import fairytale.tbd.domain.fairytale.entity.Fairytale; +import fairytale.tbd.domain.fairytale.repository.FairytaleRepository; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleRequestDTO; +import fairytale.tbd.global.enums.statuscode.ErrorStatus; +import fairytale.tbd.global.exception.GeneralException; +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class FairytaleCommandServiceImpl implements FairytaleCommandService { + private final FairytaleRepository fairytaleRepository; + + @Override + @Transactional + public Fairytale saveFairytale(FairytaleRequestDTO.AddFairytaleRequestDTO request) { + if (fairytaleRepository.existsByName(request.getName())) { + throw new GeneralException(ErrorStatus._FAIRYTALE_EXIST_ERROR); + } + Fairytale fairytale = Fairytaleconverter.toFairytale(request); + return fairytaleRepository.save(fairytale); + } + +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryService.java b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryService.java new file mode 100644 index 0000000..0c33949 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryService.java @@ -0,0 +1,4 @@ +package fairytale.tbd.domain.fairytale.service; + +public interface FairytaleQueryService { +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryServiceImpl.java b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryServiceImpl.java new file mode 100644 index 0000000..cc41202 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/service/FairytaleQueryServiceImpl.java @@ -0,0 +1,4 @@ +package fairytale.tbd.domain.fairytale.service; + +public class FairytaleQueryServiceImpl { +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/web/controller/FairytaleRestController.java b/src/main/java/fairytale/tbd/domain/fairytale/web/controller/FairytaleRestController.java new file mode 100644 index 0000000..5a5269b --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/web/controller/FairytaleRestController.java @@ -0,0 +1,30 @@ +package fairytale.tbd.domain.fairytale.web.controller; + +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 fairytale.tbd.domain.fairytale.converter.Fairytaleconverter; +import fairytale.tbd.domain.fairytale.entity.Fairytale; +import fairytale.tbd.domain.fairytale.service.FairytaleCommandService; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleRequestDTO; +import fairytale.tbd.domain.fairytale.web.dto.FairytaleResponseDTO; +import fairytale.tbd.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/fairytale") +public class FairytaleRestController { + private final FairytaleCommandService fairytaleCommandService; + + @PostMapping("") + public ApiResponse addFairytale(@Valid @RequestBody + FairytaleRequestDTO.AddFairytaleRequestDTO request) { + Fairytale fairytale = fairytaleCommandService.saveFairytale(request); + return ApiResponse.onSuccess(Fairytaleconverter.toAddFairytaleResultDTO(fairytale)); + } + +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleRequestDTO.java b/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleRequestDTO.java new file mode 100644 index 0000000..698e9e9 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleRequestDTO.java @@ -0,0 +1,14 @@ +package fairytale.tbd.domain.fairytale.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +public class FairytaleRequestDTO { + @Getter + @Setter + public static class AddFairytaleRequestDTO { + @NotBlank + private String name; + } +} diff --git a/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleResponseDTO.java b/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleResponseDTO.java new file mode 100644 index 0000000..0f20fad --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/fairytale/web/dto/FairytaleResponseDTO.java @@ -0,0 +1,19 @@ +package fairytale.tbd.domain.fairytale.web.dto; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class FairytaleResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class AddFairytaleResultDTO { + private Long id; + private LocalDateTime createdAt; + } +} diff --git a/src/main/java/fairytale/tbd/domain/user/service/UserCommandServiceImpl.java b/src/main/java/fairytale/tbd/domain/user/service/UserCommandServiceImpl.java index 2456afa..6d65ca1 100644 --- a/src/main/java/fairytale/tbd/domain/user/service/UserCommandServiceImpl.java +++ b/src/main/java/fairytale/tbd/domain/user/service/UserCommandServiceImpl.java @@ -23,8 +23,10 @@ public class UserCommandServiceImpl implements UserCommandService { @Transactional @Override public User addUser(UserRequestDTO.AddUserDTO request) { + // 비밀번호 암호화 String encodedPassword = passwordEncoder.encode(request.getPassword()); User user = UserConverter.toUser(request, encodedPassword); + // 유저 권한 추가 Authority authority = Authority.builder() .role(Role.ROLE_USER) .build(); diff --git a/src/main/java/fairytale/tbd/domain/voice/converter/VoiceConverter.java b/src/main/java/fairytale/tbd/domain/voice/converter/VoiceConverter.java index 79a8afc..f27ff9e 100644 --- a/src/main/java/fairytale/tbd/domain/voice/converter/VoiceConverter.java +++ b/src/main/java/fairytale/tbd/domain/voice/converter/VoiceConverter.java @@ -1,20 +1,44 @@ package fairytale.tbd.domain.voice.converter; +import java.util.ArrayList; + +import fairytale.tbd.domain.voice.entity.Segment; +import fairytale.tbd.domain.voice.entity.TTSSegment; import fairytale.tbd.domain.voice.entity.Voice; +import fairytale.tbd.domain.voice.web.dto.VoiceRequestDTO; import fairytale.tbd.domain.voice.web.dto.VoiceResponseDTO; public class VoiceConverter { - public static VoiceResponseDTO.AddVoiceResultDTO toAddVoiceResult(Voice voice){ + public static VoiceResponseDTO.AddVoiceResultDTO toAddVoiceResult(Voice voice) { return VoiceResponseDTO.AddVoiceResultDTO.builder() .voiceId(voice.getId()) .createdAt(voice.getCreatedAt()) .build(); } - public static Voice toVoice(String keyId){ + public static Voice toVoice(String keyId) { return Voice.builder() + .voiceSampleList(new ArrayList<>()) .keyId(keyId) .build(); } + + public static Segment toSegment(VoiceRequestDTO.AddSegmentDTO request) { + return Segment.builder() + .context(request.getContext()) + .isMainCharacter(request.isMainCharacter()) + .voiceType(request.getVoiceType()) + .num(request.getSegmentNum()) + .build(); + } + + public static VoiceResponseDTO.AddTTSSegmentResultDTO toAddSegmentResultDTO(TTSSegment ttsSegment, Long segmentId) { + return VoiceResponseDTO.AddTTSSegmentResultDTO.builder() + .segmentId(segmentId) + .createdAt(ttsSegment.getCreatedAt()) + .ttsSegmentId(ttsSegment.getId()) + .url(ttsSegment.getUrl()) + .build(); + } } diff --git a/src/main/java/fairytale/tbd/domain/voice/entity/Segment.java b/src/main/java/fairytale/tbd/domain/voice/entity/Segment.java index 282fac2..6dd89ac 100644 --- a/src/main/java/fairytale/tbd/domain/voice/entity/Segment.java +++ b/src/main/java/fairytale/tbd/domain/voice/entity/Segment.java @@ -1,5 +1,5 @@ package fairytale.tbd.domain.voice.entity; - +import fairytale.tbd.domain.fairytale.entity.Fairytale; import fairytale.tbd.domain.voice.enums.VoiceType; import fairytale.tbd.global.entity.BaseEntity; import jakarta.persistence.CascadeType; @@ -27,7 +27,7 @@ public class Segment extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "fariytale_segment_id") + @Column(name = "fairytale_segment_id") private Long id; @Column(name = "segment_context", nullable = false) diff --git a/src/main/java/fairytale/tbd/domain/voice/enums/VoiceType.java b/src/main/java/fairytale/tbd/domain/voice/enums/VoiceType.java index c7b9892..dc12871 100644 --- a/src/main/java/fairytale/tbd/domain/voice/enums/VoiceType.java +++ b/src/main/java/fairytale/tbd/domain/voice/enums/VoiceType.java @@ -1,5 +1,6 @@ package fairytale.tbd.domain.voice.enums; public enum VoiceType { - OLD_WOMAN, OLD_MAN, YOUNG_WOMAN, YOUNG_MAN; + OLD_WOMAN, OLD_MAN, YOUNG_WOMAN, YOUNG_MAN, NARRATION, MIDDLEAGE_MAN, MIDDLEAGE_WOMAN; } + diff --git a/src/main/java/fairytale/tbd/domain/voice/exception/InvalidVoiceTypeException.java b/src/main/java/fairytale/tbd/domain/voice/exception/InvalidVoiceTypeException.java new file mode 100644 index 0000000..a6fa928 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/voice/exception/InvalidVoiceTypeException.java @@ -0,0 +1,10 @@ +package fairytale.tbd.domain.voice.exception; + +import fairytale.tbd.global.enums.statuscode.BaseCode; +import fairytale.tbd.global.exception.GeneralException; + +public class InvalidVoiceTypeException extends GeneralException { + public InvalidVoiceTypeException(BaseCode errorStatus) { + super(errorStatus); + } +} diff --git a/src/main/java/fairytale/tbd/domain/voice/repository/SegmentRepository.java b/src/main/java/fairytale/tbd/domain/voice/repository/SegmentRepository.java new file mode 100644 index 0000000..fee9b6b --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/voice/repository/SegmentRepository.java @@ -0,0 +1,10 @@ +package fairytale.tbd.domain.voice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import fairytale.tbd.domain.voice.entity.Segment; + +@Repository +public interface SegmentRepository extends JpaRepository { +} diff --git a/src/main/java/fairytale/tbd/domain/voice/repository/TTSSegmentRepository.java b/src/main/java/fairytale/tbd/domain/voice/repository/TTSSegmentRepository.java new file mode 100644 index 0000000..ceaf334 --- /dev/null +++ b/src/main/java/fairytale/tbd/domain/voice/repository/TTSSegmentRepository.java @@ -0,0 +1,10 @@ +package fairytale.tbd.domain.voice.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import fairytale.tbd.domain.voice.entity.TTSSegment; + +@Repository +public interface TTSSegmentRepository extends JpaRepository { +} diff --git a/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandService.java b/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandService.java index ff8d15a..45249f6 100644 --- a/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandService.java +++ b/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandService.java @@ -3,7 +3,10 @@ import fairytale.tbd.domain.user.entity.User; import fairytale.tbd.domain.voice.entity.Voice; import fairytale.tbd.domain.voice.web.dto.VoiceRequestDTO; +import fairytale.tbd.domain.voice.web.dto.VoiceResponseDTO; public interface VoiceCommandService { Voice uploadVoice(VoiceRequestDTO.AddVoiceDTO request, User user); + + VoiceResponseDTO.AddTTSSegmentResultDTO addTTSSegment(VoiceRequestDTO.AddSegmentDTO request); } diff --git a/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandServiceImpl.java b/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandServiceImpl.java index d1882ad..eaa6585 100644 --- a/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandServiceImpl.java +++ b/src/main/java/fairytale/tbd/domain/voice/service/VoiceCommandServiceImpl.java @@ -1,19 +1,35 @@ package fairytale.tbd.domain.voice.service; +import java.io.File; +import java.util.UUID; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import fairytale.tbd.domain.fairytale.entity.Fairytale; +import fairytale.tbd.domain.fairytale.exception.FairytaleNotFoundException; +import fairytale.tbd.domain.fairytale.repository.FairytaleRepository; import fairytale.tbd.domain.user.entity.User; import fairytale.tbd.domain.voice.converter.VoiceConverter; +import fairytale.tbd.domain.voice.entity.Segment; +import fairytale.tbd.domain.voice.entity.TTSSegment; import fairytale.tbd.domain.voice.entity.Voice; +import fairytale.tbd.domain.voice.entity.VoiceSample; import fairytale.tbd.domain.voice.exception.ExistVoiceException; import fairytale.tbd.domain.voice.exception.VoiceSaveErrorException; +import fairytale.tbd.domain.voice.repository.SegmentRepository; +import fairytale.tbd.domain.voice.repository.TTSSegmentRepository; import fairytale.tbd.domain.voice.repository.VoiceRepository; import fairytale.tbd.domain.voice.web.dto.VoiceRequestDTO; +import fairytale.tbd.domain.voice.web.dto.VoiceResponseDTO; +import fairytale.tbd.global.aws.s3.AmazonS3Manager; import fairytale.tbd.global.elevenlabs.ElevenlabsManager; import fairytale.tbd.global.enums.statuscode.ErrorStatus; +import fairytale.tbd.global.exception.GeneralException; +import fairytale.tbd.global.util.FileConverter; import lombok.RequiredArgsConstructor; @Service @@ -25,6 +41,10 @@ public class VoiceCommandServiceImpl implements VoiceCommandService { private final ElevenlabsManager elevenlabsManager; private final VoiceRepository voiceRepository; + private final AmazonS3Manager amazonS3Manager; + private final FairytaleRepository fairytaleRepository; + private final SegmentRepository segmentRepository; + private final TTSSegmentRepository ttsSegmentRepository; /** * ElevenLabs Voice 추가 @@ -36,7 +56,7 @@ public class VoiceCommandServiceImpl implements VoiceCommandService { public Voice uploadVoice(VoiceRequestDTO.AddVoiceDTO request, User user) { // 사용자의 목소리가 이미 저장되어 있으면 오류 if (voiceRepository.existsVoiceByUserId(user.getId())) { - LOGGER.error("이미 존재하는 목소리 === userId = {}", user.getId()); + LOGGER.error("=== 이미 존재하는 목소리 === userId = {}", user.getId()); throw new ExistVoiceException(ErrorStatus._EXIST_VOICE); } @@ -46,11 +66,56 @@ public Voice uploadVoice(VoiceRequestDTO.AddVoiceDTO request, User user) { throw new VoiceSaveErrorException(ErrorStatus._VOICE_SAVE_ERROR); } + String savePath = amazonS3Manager.uploadFile( + amazonS3Manager.generateS3SavePath(amazonS3Manager.VOICE_SAMPLE_PATH), + request.getSample()); + + VoiceSample voiceSample = VoiceSample.builder() + .url(savePath) + .build(); + Voice voice = VoiceConverter.toVoice(keyId); + voice.addVoiceSample(voiceSample); + user.setVoice(voice); voiceRepository.save(voice); return voice; } + @Transactional + @Override + public VoiceResponseDTO.AddTTSSegmentResultDTO addTTSSegment(VoiceRequestDTO.AddSegmentDTO request) { + Fairytale fairytale = fairytaleRepository.findById(request.getFairytaleId()) + .orElseThrow(() -> new FairytaleNotFoundException(ErrorStatus._FAIRYTALE_NOT_FOUND)); + + Segment segment = VoiceConverter.toSegment(request); + fairytale.addSegment(segment); + segmentRepository.save(segment); + + File file = elevenlabsManager.elevenLabsTTS(segment.getContext(), segment.getVoiceType()); + + String uuid = UUID.randomUUID().toString(); + + MultipartFile multipartFile; + try { + multipartFile = FileConverter.toMultipartFile(file, uuid); + } catch (Exception e) { + LOGGER.error("MultipartFile 변환 도중 에러 발생"); + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + + String savePath = amazonS3Manager.uploadFile( + amazonS3Manager.generateS3SavePath(amazonS3Manager.TTS_COMMON_VOICE_PATH, uuid), multipartFile); + + TTSSegment ttsSegment = TTSSegment.builder() + .url(savePath) + .segment(segment) + .build(); + + TTSSegment save = ttsSegmentRepository.save(ttsSegment); + return VoiceConverter.toAddSegmentResultDTO(save, segment.getId()); + } + + } diff --git a/src/main/java/fairytale/tbd/domain/voice/web/controller/VoiceRestController.java b/src/main/java/fairytale/tbd/domain/voice/web/controller/VoiceRestController.java index 0e74bfb..d1d32a3 100644 --- a/src/main/java/fairytale/tbd/domain/voice/web/controller/VoiceRestController.java +++ b/src/main/java/fairytale/tbd/domain/voice/web/controller/VoiceRestController.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.ModelAttribute; 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; @@ -34,4 +35,14 @@ public ApiResponse addVoice( return ApiResponse.onSuccess(VoiceConverter.toAddVoiceResult(voice)); } + @PostMapping("/segment") + public ApiResponse addSegment( + @Valid @RequestBody VoiceRequestDTO.AddSegmentDTO request) { + LOGGER.info("::: Segment 추가 요청 :::"); + VoiceResponseDTO.AddTTSSegmentResultDTO result = voiceCommandService.addTTSSegment(request); + LOGGER.info("::: Segment 추가 성공 SegmentId = {}:::", result.getSegmentId()); + return ApiResponse.onSuccess(result); + } + + } diff --git a/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceRequestDTO.java b/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceRequestDTO.java index e91e035..67db296 100644 --- a/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceRequestDTO.java +++ b/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceRequestDTO.java @@ -1,9 +1,8 @@ package fairytale.tbd.domain.voice.web.dto; -import java.util.List; - import org.springframework.web.multipart.MultipartFile; +import fairytale.tbd.domain.voice.enums.VoiceType; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; @@ -11,10 +10,20 @@ public class VoiceRequestDTO { @Getter @Setter - public static class AddVoiceDTO{ + public static class AddVoiceDTO { //파일 형식 검증 @NotNull MultipartFile sample; + } + @Getter + @Setter + public static class AddSegmentDTO { + private String context; + private boolean isMainCharacter; + private VoiceType voiceType; + private Long fairytaleId; + private Long segmentNum; } + } diff --git a/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceResponseDTO.java b/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceResponseDTO.java index 66ba627..31cb8f9 100644 --- a/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceResponseDTO.java +++ b/src/main/java/fairytale/tbd/domain/voice/web/dto/VoiceResponseDTO.java @@ -12,8 +12,19 @@ public class VoiceResponseDTO { @Getter @NoArgsConstructor @AllArgsConstructor - public static class AddVoiceResultDTO{ + public static class AddVoiceResultDTO { private Long voiceId; private LocalDateTime createdAt; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class AddTTSSegmentResultDTO { + private Long segmentId; + private Long ttsSegmentId; + private String url; + private LocalDateTime createdAt; + } } diff --git a/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java b/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java index dc3a7a0..bb1141b 100644 --- a/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java +++ b/src/main/java/fairytale/tbd/global/aws/s3/AmazonS3Manager.java @@ -1,7 +1,9 @@ package fairytale.tbd.global.aws.s3; import java.io.IOException; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +24,15 @@ public class AmazonS3Manager { private final AmazonS3 amazonS3; private final AmazonConfig amazonConfig; + @Value("${cloud.aws.s3.path.voice-sample}") + public String VOICE_SAMPLE_PATH; + + @Value("${cloud.aws.s3.path.common-voice}") + public String TTS_COMMON_VOICE_PATH; + + @Value("${cloud.aws.s3.path.user-voice}") + public String TTS_USER_VOICE_PATH; + public String uploadFile(String keyName, MultipartFile file) { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(file.getContentType()); @@ -37,4 +48,12 @@ public String uploadFile(String keyName, MultipartFile file) { return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString(); } + public String generateS3SavePath(String path) { + return path + '/' + UUID.randomUUID().toString(); + } + + public String generateS3SavePath(String path, String name) { + return path + '/' + name; + } + } diff --git a/src/main/java/fairytale/tbd/global/config/SecurityConfig.java b/src/main/java/fairytale/tbd/global/config/SecurityConfig.java index 65b3456..ad0be90 100644 --- a/src/main/java/fairytale/tbd/global/config/SecurityConfig.java +++ b/src/main/java/fairytale/tbd/global/config/SecurityConfig.java @@ -62,11 +62,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // CORS .cors(corsCustomizer -> corsCustomizer.configurationSource(request -> { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(Collections.singletonList("*")); + config.setAllowedOriginPatterns(Collections.singletonList("*")); config.setAllowedMethods(Collections.singletonList("*")); config.setAllowCredentials(true); config.setAllowedHeaders(Collections.singletonList("*")); - config.setExposedHeaders(Arrays.asList("Authorization")); + config.setExposedHeaders(Arrays.asList("Authorization", "Authorization-refresh")); config.setMaxAge(3600L); return config; })); diff --git a/src/main/java/fairytale/tbd/global/elevenlabs/ElevenlabsManager.java b/src/main/java/fairytale/tbd/global/elevenlabs/ElevenlabsManager.java index d0990c6..fe11080 100644 --- a/src/main/java/fairytale/tbd/global/elevenlabs/ElevenlabsManager.java +++ b/src/main/java/fairytale/tbd/global/elevenlabs/ElevenlabsManager.java @@ -1,8 +1,6 @@ package fairytale.tbd.global.elevenlabs; import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -18,13 +16,14 @@ import net.andrewcpu.elevenlabs.model.voice.VoiceBuilder; import net.andrewcpu.elevenlabs.model.voice.VoiceSettings; +import fairytale.tbd.domain.voice.enums.VoiceType; +import fairytale.tbd.domain.voice.exception.InvalidVoiceTypeException; import fairytale.tbd.global.elevenlabs.exception.FileConvertException; import fairytale.tbd.global.enums.statuscode.ErrorStatus; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - /** * https://github.com/Andrewcpu/elevenlabs-api */ @@ -45,23 +44,54 @@ public class ElevenlabsManager { private double stability; @Value("${voice.elevenlabs.voice_setting.style}") private double style; + + @Value("${voice.elevenlabs.narration-voice-setting.similarity_boost}") + private double Nsimilarity; + @Value("${voice.elevenlabs.narration-voice-setting.stability}") + private double Nstability; + @Value("${voice.elevenlabs.narration-voice-setting.style}") + private double Nstyle; + @Value("${voice.elevenlabs.narration-voice-setting.use-speaker-boost}") + private boolean NuseSpeakerBoost; + @Value("${voice.elevenlabs.voice_setting.use_speaker_boost}") private boolean useSpeakerBoost; + @Value("${voice.elevenlabs.common-voice-id.old-man}") + private String oldManKey; + + @Value("${voice.elevenlabs.common-voice-id.old-woman}") + private String oldWomanKey; + + @Value("${voice.elevenlabs.common-voice-id.middle-age-man}") + + private String middleAgeManKey; + + @Value("${voice.elevenlabs.common-voice-id.middle-age-woman}") + private String middleAgeWomanKey; + + @Value("${voice.elevenlabs.common-voice-id.young-man}") + private String youngManKey; + + @Value("${voice.elevenlabs.common-voice-id.young_woman}") + private String youngWomanKey; + + @Value("${voice.elevenlabs.common-voice-id.narration-woman}") + private String narrationWomanKey; + @PostConstruct - public void init(){ + public void init() { ElevenLabs.setApiKey(apiKey); ElevenLabs.setDefaultModel("eleven_multilingual_v2"); } - /** * EleventLabs TTS 변환 * @param text 음성 TTS 변환 할 내용 * @param voiceId voice 고유 값 * @return 생성된 .mpga 파일 */ - public File elevenLabsTTS(String text, String voiceId){ + public File elevenLabsTTS(String text, String voiceId) { return SpeechGenerationBuilder.textToSpeech() .file() .setText(text) @@ -72,6 +102,49 @@ public File elevenLabsTTS(String text, String voiceId){ .build(); } + public File elevenLabsTTS(String text, VoiceType voiceType) { + if (voiceType == VoiceType.NARRATION) { + return elevenLabsTTSNarration(text); + } + return elevenLabsTTS(text, getCommonVoiceId(voiceType)); + } + + private String getCommonVoiceId(VoiceType voiceType) { + switch (voiceType) { + case OLD_MAN: + return oldManKey; + case OLD_WOMAN: + return oldWomanKey; + case YOUNG_MAN: + return youngManKey; + case YOUNG_WOMAN: + return youngWomanKey; + case MIDDLEAGE_MAN: + return middleAgeManKey; + case MIDDLEAGE_WOMAN: + return middleAgeWomanKey; + default: + throw new InvalidVoiceTypeException(ErrorStatus._INVALID_VOICE_TYPE); + } + + } + + /** + * EleventLabs TTS 나레이션 변환 + * @param text 음성 TTS 변환 할 내용 + * @return 생성된 .mpga 파일 + */ + public File elevenLabsTTSNarration(String text) { + return SpeechGenerationBuilder.textToSpeech() + .file() + .setText(text) + .setGeneratedAudioOutputFormat(GeneratedAudioOutputFormat.MP3_44100_128) + .setVoiceId(narrationWomanKey) + .setVoiceSettings(new VoiceSettings(Nstability, Nsimilarity, Nstyle, NuseSpeakerBoost)) + .setLatencyOptimization(StreamLatencyOptimization.NONE) + .build(); + } + /** * Voice 생성 * @param userName 유저 이름 (voice에 저장될 이름) @@ -86,11 +159,12 @@ public String addVoice(String userName, MultipartFile sample) { builder.withDescription("the emotional voice of the main character of a children's book"); builder.withLabel("language", "ko"); Voice voice = builder.create(); + if (file != null && file.exists()) { + file.delete(); + } return voice.getVoiceId(); } - - /** * MultiPartFile -> File 변환 * @param multipartFile @@ -103,8 +177,7 @@ public static File convertMultipartFileToFile(MultipartFile multipartFile) { // MultipartFile의 내용을 파일에 쓰기 try { multipartFile.transferTo(file); - } - catch (Exception e){ + } catch (Exception e) { e.printStackTrace(); throw new FileConvertException(ErrorStatus._FILE_CONVERT_ERROR); } diff --git a/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java b/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java index 5239750..45a5cbe 100644 --- a/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java +++ b/src/main/java/fairytale/tbd/global/enums/statuscode/ErrorStatus.java @@ -9,13 +9,19 @@ public enum ErrorStatus implements BaseCode { _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // Fairytale + + _FAIRYTALE_NOT_FOUND(HttpStatus.BAD_REQUEST, "FAIRYTALE4001", "존재하지 않는 동화입니다."), + _FAIRYTALE_EXIST_ERROR(HttpStatus.BAD_REQUEST, "FAIRYTALE4002", "이미 존재하는 이름의 동화입니다."), + // ElevenLabs _FILE_CONVERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "FILE5001", "파일 변환에 실패했습니다."), // Voice _EXIST_VOICE(HttpStatus.BAD_REQUEST, "VOICE4001", "이미 존재하는 목소리입니다."), - _VOICE_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "VOICE4002", "목소리 저장에 실패했습니다."), + _VOICE_SAVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "VOICE5002", "목소리 저장에 실패했습니다."), + _INVALID_VOICE_TYPE(HttpStatus.BAD_REQUEST, "VOICE4002", "올바르지 않은 목소리 종류입니다."), // User _EXIST_USERNAME(HttpStatus.BAD_REQUEST, "USER4001", "이미 존재하는 닉네임입니다."), diff --git a/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginFailureHandler.java b/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginFailureHandler.java index 3c2776d..b2b6c0f 100644 --- a/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginFailureHandler.java +++ b/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginFailureHandler.java @@ -8,23 +8,30 @@ import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; +import com.fasterxml.jackson.databind.ObjectMapper; + +import fairytale.tbd.global.enums.statuscode.ErrorStatus; +import fairytale.tbd.global.response.ApiResponse; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; @Component +@RequiredArgsConstructor /** * JWT 로그인 필터 Failure Handler */ public class JwtLoginFailureHandler implements AuthenticationFailureHandler { private static final Logger LOGGER = LogManager.getLogger(JwtLoginSuccessHandler.class); - + private final ObjectMapper objectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.getWriter().write("AUTHENTICATION FAILED."); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onFailure(ErrorStatus._BAD_REQUEST.getCode(),ErrorStatus._BAD_REQUEST.getMessage(), "로그인에 실패했습니다."))); LOGGER.info("Jwt Login fail :: error = {}", exception.getMessage()); } } diff --git a/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginSuccessHandler.java b/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginSuccessHandler.java index d17f2a4..69b957d 100644 --- a/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginSuccessHandler.java +++ b/src/main/java/fairytale/tbd/global/security/jwt/handler/JwtLoginSuccessHandler.java @@ -9,6 +9,10 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; +import com.fasterxml.jackson.databind.ObjectMapper; + +import fairytale.tbd.global.enums.statuscode.SuccessStatus; +import fairytale.tbd.global.response.ApiResponse; import fairytale.tbd.global.security.jwt.JwtService; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -23,6 +27,7 @@ public class JwtLoginSuccessHandler implements AuthenticationSuccessHandler { private final JwtService jwtService; private static final Logger LOGGER = LogManager.getLogger(JwtLoginSuccessHandler.class); + private final ObjectMapper objectMapper; /** * 인증에 성공하면 Access Token과 Refresh Token을 생성한 후 반환 @@ -33,7 +38,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo UserDetails principal = (UserDetails)authentication.getPrincipal(); String loginId = principal.getUsername(); response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().write("AUTHENTICATION SUCCESS."); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.onSuccess("로그인에 성공했습니다."))); LOGGER.info("Jwt Login Success :: Login ID = {}", loginId); loginSuccess(response, loginId); } diff --git a/src/main/java/fairytale/tbd/global/util/FileConverter.java b/src/main/java/fairytale/tbd/global/util/FileConverter.java new file mode 100644 index 0000000..57221dd --- /dev/null +++ b/src/main/java/fairytale/tbd/global/util/FileConverter.java @@ -0,0 +1,19 @@ +package fairytale.tbd.global.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +public class FileConverter { + public static MultipartFile toMultipartFile(File file, String fileName) throws IOException { + FileInputStream input = new FileInputStream(file); + MultipartFile multipartFile = new MockMultipartFile("file", + file.getName(), + "audio/mpeg", + input); + return multipartFile; + } +}