Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a800a7d
Docs: Spring에서 S3를 사용하기 위해 build.gradle에 의존성 명시
Joonseok-Lee May 11, 2026
7151c13
Docs: org.springframework...의 S3 의존성은 레거시 라이브러리임을 주석으로 간단히 명시
Joonseok-Lee May 11, 2026
73946d3
Feat: 컨트롤러 메소드 파라미터 내에 User를 Security Context Holder에서 주입, wavFile을 검…
Joonseok-Lee May 12, 2026
8980a0a
Feat: Voice 도메인에서 사용할 예외 코드를 관리하는 이넘 객체 작성, 잘못된 파일 양식으로 넘어왔을 때 던질 예외 …
Joonseok-Lee May 12, 2026
4a1cbac
Feat: 음성 모델링 시, 요청 바디에 .wav 파일이 아닌 경우, 빈 경우 등 검증 실패에 적용할 예외 객체 Invali…
Joonseok-Lee May 12, 2026
bd77924
Fix: VoiceErrorCode 내에 잘못 작성된 code 타입, 누락된 세미콜론을 추가
Joonseok-Lee May 12, 2026
e203714
Feat: S3에 파일 업로드를 전담하는 클래스를 S3Uploader라는 이름으로 선언
Joonseok-Lee May 12, 2026
8dd6458
Feat: 파일 업로드에 실패했을 경우의 예외 코드를 VoiceErrorCode에 작성
Joonseok-Lee May 12, 2026
c79390d
Feat: 파일 업로드에 실패했을 때 던질 예외 객체를 작성
Joonseok-Lee May 12, 2026
bac8b38
Feat: userId와 TTS 모델의 이름을 매핑하기 위한 관계 테이블을 1:1 매핑으로 작성
Joonseok-Lee May 12, 2026
dcda66f
Feat: userId와 TTS 모델 이름을 매핑한 엔터티를 초기화하기 위한 Repository를 Spring Data JP…
Joonseok-Lee May 12, 2026
0d299f1
Feat: Kafka 메시지의 바디 부분에 사용할 MessageInterface 구현체를 불변 DTO 클래스로 작성
Joonseok-Lee May 12, 2026
0772900
Feat: VoiceService 인터페이스에 음성 녹음 파일을 저장하는 메소드를 작성
Joonseok-Lee May 12, 2026
67ecead
Feat: EventService 내에 S3에 업로드된 음성 파일과 userId를 메시지에 작성하여 TTS를 만들어달라는 토…
Joonseok-Lee May 12, 2026
03122b3
Feat: 음성 녹음 파일을 S3에 저장하고, Entity를 빌드 한 후 Kafka에 메시지를 전송하는 EventServic…
Joonseok-Lee May 12, 2026
b1e8688
Feat: /api/users/voice에 Post로 매핑되어 wav 파일을 form-data 형식으로 받고, 그 파일을 S…
Joonseok-Lee May 12, 2026
8243594
Feat: Voice 서비스 인터페이스와 컨트롤러에 전반적으로 saveMp3 메소드의 반환타입이었던 TtsModelingRe…
Joonseok-Lee May 12, 2026
aedd52e
Fix: VoiceController에 wav 파일이 아니라면 던져야 할 예외를 wav 파일일 때 던지는 오탈자 수정
Joonseok-Lee May 12, 2026
d6f5075
Fix: VoiceController에 form-data 형식의 .wav 파일 형식 검증 로직 수정
Joonseok-Lee May 12, 2026
1fa8c74
Fix: Slf4j의 log.error 함수에 대해, e가 마지막 인자가 아니라 스택 트레이스가 출력되지 않던 문제를 수정
Joonseok-Lee May 12, 2026
1cf0257
Fix: 기타 오탈자와 기대대로 동작하지 않던 코드들을 수정
Joonseok-Lee May 12, 2026
f69d01d
Fix: 잘못된 파일로 요청했을 때의 message를 더 자세하게 수정
Joonseok-Lee May 12, 2026
cc444f9
Refactor: voice 패키지가 fairytale 하위에 있던 문제 해결
Joonseok-Lee May 12, 2026
4b7ffff
Feat: 문장 단위로 TTS 파일과 User를 매핑할 수 있는 중간 테이블 선언
Joonseok-Lee May 12, 2026
7eaac15
Feat: TtsHistory의 Spring Data JPA interface declaration
Joonseok-Lee May 12, 2026
c875c4a
Feat: 202 ACCEPTED 응답 코드 추가
Joonseok-Lee May 12, 2026
1c295ac
Feat: 202 ACCEPTED를 사용한 정적 팩토리 메소드 추가
Joonseok-Lee May 12, 2026
fc299ba
Refactor: Kafka Message의 Body interface를 더 폭 넓게 사용하기 위해 MessageInterf…
Joonseok-Lee May 12, 2026
25ffdc7
Feat: TTS를 사용한 음성 녹음 파일이 없을 때 던질 예외에 대한 코드 작성
Joonseok-Lee May 12, 2026
630fc78
Feat: Paragraph 조회에 실패했을 때 사용할 에러 코드 작성
Joonseok-Lee May 12, 2026
5ad2f09
Feat: Paragraph를 찾지 못했을 때 던질 커스텀 예외 객체 작성
Joonseok-Lee May 12, 2026
fd9fd4a
Feat: TTS 음성 파일을 찾지 못했을 때 던질 커스텀 예외 객체 작성
Joonseok-Lee May 12, 2026
3f8f42e
Feat: TTS 요청, 응답에 대한 DTO 작성
Joonseok-Lee May 12, 2026
e4c65ce
Feat: TTS 음성 파일이 존재하는지 확인, 없다면 생성을 요청하는 비즈니스 로직 메소드를 추가
Joonseok-Lee May 12, 2026
315fcf6
Feat: paragraphId와 userId를 기반으로 TTS 음성 파일을 생성했는지 이력을 조회하는 메소드 작성
Joonseok-Lee May 12, 2026
8ac4359
Feat: PagraphId가 유효한 값으로 들어왔는지 검증하는 책임을 fairytale 패키지에 위임하기 위한 객체 작성
Joonseok-Lee May 12, 2026
ec99362
Feat: TTS 파일 생성 요청 Kafka Message를 전송하는 메소드 작성
Joonseok-Lee May 12, 2026
4f395a0
Feat: TTS 음성 파일이 생성되어 있는지 체크하는 메소드, 생성을 요청하는 메소드 작성
Joonseok-Lee May 12, 2026
dd958d6
Feat: TTS 음성 파일이 생성되어 있는지 체크하도록 하는 컨트롤러 메소드, 생성을 요청하는 컨트롤러 메소드 진입점을 각…
Joonseok-Lee May 12, 2026
ca19ed6
Docs: import optimize
Joonseok-Lee May 12, 2026
0ab92f7
Docs: optimize all import
Joonseok-Lee May 12, 2026
6fdf2ff
Fix: S3에 저장되는 파일 이름을 디렉토리 구조화
Joonseok-Lee May 13, 2026
a5fffba
Fix: 문단 단위 TTS 생성에서 전체 동화에 대한 생성으로 변경
Joonseok-Lee May 13, 2026
33520c4
Fix: 코드 리뷰 기반 런타임 버그 수정
Joonseok-Lee May 14, 2026
636395f
Fix: Controller의 기본 경로인 "/api"를 RequestMapping에 포함하도록 수정
Joonseok-Lee May 14, 2026
31ad995
Merge pull request #50 from R-Goodday/feat/#48
Joonseok-Lee May 14, 2026
48617af
Merge branch 'develop' into feat/#46
Joonseok-Lee May 14, 2026
6b5d645
Merge pull request #47 from R-Goodday/feat/#46
Joonseok-Lee May 14, 2026
d759da9
hotfix: CORS 설정 추가
zzuhannn May 14, 2026
ae725b9
hotfix:SecurityConfig에 CORS 활성화 및 OPTIONS 허용 추가
zzuhannn May 14, 2026
0272a5e
Fix: SecurityConfig 내에 requestMatchers가 /api로 시작하지 않아 모든 요청이 Unauthor…
Joonseok-Lee May 14, 2026
241dcb1
Merge branch 'main' into develop
Joonseok-Lee May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {

// .env 파일 로딩
implementation 'me.paulschwarz:spring-dotenv:4.0.0'

// S3 (latest dependency)
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.4.2'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auth")
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
@AllArgsConstructor
public enum FairytaleErrorCode implements BaseResponseCode {

FAIRYTALE_NOT_FOUND("FAIRYTALE_404_1", NOT_FOUND, "동화를 찾을 수 없습니다.");
FAIRYTALE_NOT_FOUND("FAIRYTALE_404_1", NOT_FOUND, "동화를 찾을 수 없습니다."),
PARAGRAPH_NOT_FOUND("FAIRYTALE_404_2", NOT_FOUND, "요청한 ID에 해당하는 Paragraph를 찾을 수 없습니다.");

private final String code;
private final int httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.capstone.kkumteul.domain.fairytale.exception;

import com.capstone.kkumteul.global.exception.BaseException;

public class ParagraphNotFoundException extends BaseException {
public ParagraphNotFoundException() {
super(FairytaleErrorCode.PARAGRAPH_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public interface ParagraphRepository extends JpaRepository<Paragraph, Long> {

/** 특정 페이지의 문장들 조회 — 단어장 추출 시 페이지 단위 본문 로드 */
List<Paragraph> findByFairytaleIdAndPage(Long fairytaleId, int page);

boolean existsById(Long paragraphId);

boolean existsByFairytaleId(Long fairytaleId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.capstone.kkumteul.domain.fairytale.validator;

import com.capstone.kkumteul.domain.fairytale.repository.ParagraphRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class ParagraphValidator {

private final ParagraphRepository paragraphRepository;

public boolean paragraphIdIsValid(Long paragraphId) {
return paragraphRepository.existsById(paragraphId);
}

public boolean fairytaleIdIsValid(Long fairytaleId) {
return paragraphRepository.existsByFairytaleId(fairytaleId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

import com.capstone.kkumteul.domain.fairytale.entity.Island;
import com.capstone.kkumteul.domain.fairytale.service.FairytaleService;
import com.capstone.kkumteul.domain.fairytale.service.sse.SseService;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleDetailRes;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleGenerateReq;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleListRes;
import com.capstone.kkumteul.domain.kafka.service.EventService;
import com.capstone.kkumteul.domain.user.entity.User;
import com.capstone.kkumteul.global.response.SuccessResponse;
import com.capstone.kkumteul.global.security.AuthUser;

import com.capstone.kkumteul.domain.fairytale.service.sse.SseService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -24,7 +23,7 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/fairytales")
@RequestMapping("/api/fairytales")
public class FairytaleController {

private final FairytaleService fairytaleService;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package com.capstone.kkumteul.domain.kafka.dto;

public interface MessageInterface {

Long getUserId();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.capstone.kkumteul.domain.kafka.dto.FairytaleGenerateMessage;
import com.capstone.kkumteul.domain.kafka.dto.MessageInterface;
import com.capstone.kkumteul.domain.user.entity.User;
import com.capstone.kkumteul.domain.voice.web.dto.TtsFileRequest;
import com.capstone.kkumteul.domain.voice.web.dto.TtsModelingRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -28,6 +30,9 @@ public class EventService {
@Value("${FAIRYTALE_GENERATION}")
private String FAIRYTALE_GENERATION;

@Value(("${TTS_MODELING}"))
private String TTS_MODELING;

@Transactional
public Long createFairytaleMessageSend(User user, FairytaleGenerateReq request) {

Expand Down Expand Up @@ -55,10 +60,30 @@ public Long createFairytaleMessageSend(User user, FairytaleGenerateReq request)
kafkaTemplate.send(FAIRYTALE_GENERATION, message)
.whenComplete((result, e) -> {
if (e != null) {
log.error("fairytale_generate failed", e);
log.error("fairytale_generate failed. userId={}, message={}", user.getId(), message, e);
}
});

return saved.getId();
}

public void sendTtsModelingRequest(TtsModelingRequest message) {

kafkaTemplate.send(TTS_MODELING, message)
.whenComplete((result, e) -> {
if(e != null) {
log.error("tts_modeling_request failed. userId={}, message={}", message.getUserId(), message.getUploadedUrl(), e);
}
});
}

public void sendTtsFileRequest(TtsFileRequest message) {

kafkaTemplate.send(TTS_MODELING, message)
.whenComplete((result, e) -> {
if( e != null) {
log.error("tts_file_request occurred error. userId={}, paragraphId={}", message.getUserId(), message.getFairytaleId(), e);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.capstone.kkumteul.domain.voice.entity;

import com.capstone.kkumteul.domain.fairytale.entity.Paragraph;
import com.capstone.kkumteul.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class TtsHistory {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "paragraph_id")
private Paragraph paragraph;

private String ttsUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.capstone.kkumteul.domain.voice.entity;

import com.capstone.kkumteul.domain.user.entity.User;
import com.capstone.kkumteul.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class VoiceModel extends BaseEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

// extracted tts model name
@Column(unique = true, nullable = true)
private String modelName;

@Column(nullable = false, unique = true)
private String wavFileUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.capstone.kkumteul.domain.voice.exception;

import com.capstone.kkumteul.global.exception.BaseException;

public class FileUploadFailException extends BaseException {
public FileUploadFailException() {
super(VoiceErrorCode.FILE_UPLOAD_FAIL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.capstone.kkumteul.domain.voice.exception;

import com.capstone.kkumteul.global.exception.BaseException;

public class InvalidFileException extends BaseException {
public InvalidFileException() {
super(VoiceErrorCode.INVALID_FILE_EXCEPTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.capstone.kkumteul.domain.voice.exception;

import com.capstone.kkumteul.global.response.code.BaseResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum VoiceErrorCode implements BaseResponseCode {

INVALID_FILE_EXCEPTION("INVALID_FILE_400", 400, "요청값으로 전달한 파일의 양식이 잘못었습니다."),
FILE_NOT_CREATED("FILE_NOT_FOUND_404", 404, "TTS 음성 파일을 찾을 수 없습니다."),
FILE_UPLOAD_FAIL("FILE_UPLOAD_FAIL_500", 500, "파일을 변환하고, S3에 업로드하는 것에 실패했습니다.");

private final String code;
private final int httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.capstone.kkumteul.domain.voice.exception;

import com.capstone.kkumteul.global.exception.BaseException;

public class VoiceFileNotFoundException extends BaseException {
public VoiceFileNotFoundException() {
super(VoiceErrorCode.FILE_NOT_CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.capstone.kkumteul.domain.voice.repository;

import com.capstone.kkumteul.domain.voice.entity.TtsHistory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface TtsHistoryRepository extends CrudRepository<TtsHistory, Long> {

@Query("""
select th
from TtsHistory th join fetch
th.paragraph p
where p.id = :paragraphId
and th.user.id = :userId
"""
)
Optional<TtsHistory> findByParagraphIdAndUserId(
@Param("paragraphId") Long paragraphId,
@Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.capstone.kkumteul.domain.voice.repository;

import com.capstone.kkumteul.domain.voice.entity.VoiceModel;
import org.springframework.data.jpa.repository.JpaRepository;

public interface VoiceModelRepository extends JpaRepository<VoiceModel, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.capstone.kkumteul.domain.voice.service;

import com.capstone.kkumteul.domain.user.entity.User;
import io.awspring.cloud.s3.ObjectMetadata;
import io.awspring.cloud.s3.S3Operations;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

@Slf4j
@Component
public class S3Uploader {

private final S3Operations s3Operations;
private final String AWS_S3_BUCKET_NAME;

// for dependency injection with environment variable
public S3Uploader(
S3Operations s3Operations,
@Value("${AWS_S3_BUCKET_NAME}")
String AWS_S3_BUCKET_NAME
) {
this.s3Operations = s3Operations;
this.AWS_S3_BUCKET_NAME = AWS_S3_BUCKET_NAME;
}

// upload S3 bucket
public String upload(MultipartFile wavFile, User user) throws IOException {
return putS3(
convert(wavFile),
createFilename(
wavFile.getOriginalFilename(),
user.getId()
),
wavFile.getContentType()
);
}

private InputStream convert(MultipartFile wavFile) throws IOException {
return wavFile.getInputStream();
}

// with UUID
private String createFilename(String originalFilename, Long userId) {
String uuid = UUID.randomUUID().toString();
String uniqueFilename = uuid + "-" + originalFilename.replaceAll("\\s", "-");
return "tts/" + userId.toString() + "/train/" + uniqueFilename;
}

private String putS3(InputStream inputStream,String filename, String contentType) throws IOException {
return s3Operations.upload(
AWS_S3_BUCKET_NAME,
filename,
inputStream,
ObjectMetadata.builder()
.contentType(contentType)
.build()
).getURL().toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.capstone.kkumteul.domain.voice.service;

import com.capstone.kkumteul.domain.user.entity.User;
import com.capstone.kkumteul.domain.voice.web.dto.TtsFileResponse;
import org.springframework.web.multipart.MultipartFile;

public interface VoiceService {
Void saveWav(MultipartFile wavFile, User user);
Boolean hasTtsHistory(Long userId, Long paragraphId);
TtsFileResponse getTtsFile(Long userId, Long paragraphId);
Void createTtsFile(Long userId, Long paragraphId);
}
Loading
Loading