feat: 책 생성 api 구현
This commit is contained in:
36
src/main/java/com/audio/common/entity/BaseEntity.java
Normal file
36
src/main/java/com/audio/common/entity/BaseEntity.java
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package com.audio.common.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.EntityListeners;
|
||||||
|
import jakarta.persistence.MappedSuperclass;
|
||||||
|
import java.time.Instant;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@MappedSuperclass
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@SuperBuilder
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
protected Instant createdAt;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
protected Instant updatedAt;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
protected boolean deleted;
|
||||||
|
|
||||||
|
protected Instant deletedAt;
|
||||||
|
|
||||||
|
public void delete() {
|
||||||
|
this.deleted = true;
|
||||||
|
this.deletedAt = Instant.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import com.audio.core.auth.application.dto.result.LoginResult;
|
|||||||
import com.audio.core.auth.application.dto.result.RegisterUseCaseResult;
|
import com.audio.core.auth.application.dto.result.RegisterUseCaseResult;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ public class AccountService {
|
|||||||
|
|
||||||
String hashedPassword = passwordEncoder.encode(password);
|
String hashedPassword = passwordEncoder.encode(password);
|
||||||
|
|
||||||
Account account = Account.builder()
|
Account account = Account.builder().loginId(loginId).name("임시 이름").passwordHash(hashedPassword).build();
|
||||||
.loginId(loginId)
|
|
||||||
.name("임시 이름")
|
|
||||||
.passwordHash(hashedPassword)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
log.info("account: {}", account);
|
log.info("account: {}", account);
|
||||||
|
|
||||||
@@ -58,4 +54,9 @@ public class AccountService {
|
|||||||
return account.getId();
|
return account.getId();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Account getAccountById(Long accountId) {
|
||||||
|
return accountRepository.findById(accountId)
|
||||||
|
.orElseThrow(() -> new NotExistEntityException(Account.class.getName()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.audio.core.book.api.controller;
|
||||||
|
|
||||||
|
import com.audio.core.book.api.dto.req.BookCreateRequest;
|
||||||
|
import com.audio.core.book.api.dto.res.BookCreateResponse;
|
||||||
|
import com.audio.core.book.application.usecase.CreateBookUseCase;
|
||||||
|
import com.audio.core.auth.application.userDetail.User;
|
||||||
|
import com.audio.core.book.application.usecase.dto.command.CreateBookCommand;
|
||||||
|
import com.audio.core.book.application.usecase.dto.result.CreateBookResult;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api/v1/book")
|
||||||
|
public class BookController {
|
||||||
|
|
||||||
|
private final CreateBookUseCase createBookUseCase;
|
||||||
|
|
||||||
|
@PostMapping("/create")
|
||||||
|
public ResponseEntity<BookCreateResponse> create(
|
||||||
|
@AuthenticationPrincipal User user,
|
||||||
|
@RequestBody BookCreateRequest bookCreateRequest
|
||||||
|
) {
|
||||||
|
CreateBookCommand command = CreateBookCommand.builder()
|
||||||
|
.accountId(user.getAccountId())
|
||||||
|
.subject(bookCreateRequest.getSubject())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
CreateBookResult result = createBookUseCase.execute(command);
|
||||||
|
|
||||||
|
BookCreateResponse response = BookCreateResponse.builder().build();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.audio.core.book.api.dto.req;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class BookCreateRequest {
|
||||||
|
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.audio.core.book.api.dto.res;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class BookCreateResponse {
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private Integer durationSec;
|
||||||
|
|
||||||
|
private String contentPath;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.audio.core.book.application.event;
|
||||||
|
|
||||||
|
public record BookGenerationRequestedEvent(Long accountId, String subject, String genre) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.audio.core.book.application.listener;
|
||||||
|
|
||||||
|
import com.audio.core.book.application.event.BookGenerationRequestedEvent;
|
||||||
|
import com.audio.core.book.application.workflow.BookGenerationWorkflow;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BookGenerationEventListener {
|
||||||
|
|
||||||
|
private final BookGenerationWorkflow bookGenerationWorkflow;
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void on(BookGenerationRequestedEvent event) {
|
||||||
|
bookGenerationWorkflow.run(event.accountId(), event.subject(), event.genre());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.audio.core.book.application.usecase;
|
||||||
|
|
||||||
|
import com.audio.core.book.application.event.BookGenerationRequestedEvent;
|
||||||
|
import com.audio.core.book.application.usecase.dto.command.CreateBookCommand;
|
||||||
|
import com.audio.core.book.application.usecase.dto.result.CreateBookResult;
|
||||||
|
import com.audio.core.book.domain.entity.Book;
|
||||||
|
import com.audio.core.book.domain.service.BookService;
|
||||||
|
import com.audio.core.auth.domain.entity.Account;
|
||||||
|
import com.audio.core.auth.domain.service.AccountService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class CreateBookUseCase {
|
||||||
|
|
||||||
|
private final AccountService accountService;
|
||||||
|
private final BookService bookService;
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public CreateBookResult execute(CreateBookCommand command) {
|
||||||
|
Account account = accountService.getAccountById(command.getAccountId());
|
||||||
|
Book createdBook = bookService.createBookForAccountId(account.getId(), command.getSubject());
|
||||||
|
|
||||||
|
applicationEventPublisher.publishEvent(new BookGenerationRequestedEvent(
|
||||||
|
account.getId(),
|
||||||
|
command.getSubject(),
|
||||||
|
"다른 필요한 값"
|
||||||
|
));
|
||||||
|
|
||||||
|
return CreateBookResult.builder().title(createdBook.getTitle()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.audio.core.book.application.usecase.dto.command;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public class CreateBookCommand {
|
||||||
|
|
||||||
|
private Long accountId;
|
||||||
|
private String subject;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.audio.core.book.application.usecase.dto.result;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class CreateBookResult {
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.audio.core.book.application.workflow;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class BookGenerationWorkflow {
|
||||||
|
|
||||||
|
public void run(Long accountId, String subject, String genre) {
|
||||||
|
log.info("1. llm 으로 책 생성");
|
||||||
|
log.info("2. 성공/실패 응답으로 책 상태 업데이트");
|
||||||
|
// log.info("2-1. 오디오북 생성");
|
||||||
|
// log.info("2-2.");
|
||||||
|
log.info("3. S3 저장");
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/main/java/com/audio/core/book/domain/entity/Book.java
Normal file
61
src/main/java/com/audio/core/book/domain/entity/Book.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package com.audio.core.book.domain.entity;
|
||||||
|
|
||||||
|
import com.audio.common.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Builder.Default;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@SuperBuilder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class Book extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long ownerId;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 200)
|
||||||
|
private String subject;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 200)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@OneToOne
|
||||||
|
@JoinColumn(name = "active_version_id")
|
||||||
|
private BookVersion activeVersion;
|
||||||
|
|
||||||
|
@Default
|
||||||
|
@OneToMany(fetch = FetchType.LAZY, mappedBy = "book")
|
||||||
|
private List<BookVersion> bookVersions = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addVersion(BookVersion version) {
|
||||||
|
bookVersions.add(version);
|
||||||
|
this.updatedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveVersion(BookVersion version) {
|
||||||
|
this.activeVersion = version;
|
||||||
|
this.updatedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.audio.core.book.domain.entity;
|
||||||
|
|
||||||
|
import com.audio.common.entity.BaseEntity;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@SuperBuilder
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
@Table(name = "book_version")
|
||||||
|
public class BookVersion extends BaseEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "book_id")
|
||||||
|
private Book book;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false, length = 20)
|
||||||
|
private Status status = Status.GENERATING;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private LengthPreset lengthPreset;
|
||||||
|
|
||||||
|
private String bookS3Key;
|
||||||
|
|
||||||
|
private String audioS3Key;
|
||||||
|
|
||||||
|
private String language = "en-Us";
|
||||||
|
|
||||||
|
private Integer audioDurationSec;
|
||||||
|
|
||||||
|
public enum Status { GENERATING, READY, FAILED, DELETED }
|
||||||
|
public enum LengthPreset { SHORT, NORMAL, LONG }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.audio.core.book.domain.repository;
|
||||||
|
|
||||||
|
import com.audio.core.book.domain.entity.Book;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface BookRepository extends JpaRepository<Book, Long> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.audio.core.book.domain.service;
|
||||||
|
|
||||||
|
import com.audio.core.book.domain.entity.Book;
|
||||||
|
import com.audio.core.book.domain.entity.BookVersion;
|
||||||
|
import com.audio.core.book.domain.repository.BookRepository;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class BookService {
|
||||||
|
|
||||||
|
private final BookRepository bookRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Book createBookForAccountId(Long ownerId, String subject) {
|
||||||
|
Book book = Book.builder()
|
||||||
|
.ownerId(ownerId)
|
||||||
|
.subject(subject)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
BookVersion bookVersion = BookVersion.builder().build();
|
||||||
|
|
||||||
|
book.addVersion(bookVersion);
|
||||||
|
book.setActiveVersion(bookVersion);
|
||||||
|
|
||||||
|
return bookRepository.save(book);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
databaseChangeLog:
|
databaseChangeLog:
|
||||||
- include:
|
- includeAll:
|
||||||
file: db/sql/account/251203/1_create_account.sql
|
path: db/sql/account/251203
|
||||||
|
- includeAll:
|
||||||
|
path: db/sql/book/251215
|
||||||
17
src/main/resources/db/sql/book/251215/1_create_book.sql
Normal file
17
src/main/resources/db/sql/book/251215/1_create_book.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- liquibase formatted sql
|
||||||
|
-- changeset ijeongmin:1_create_book.sql
|
||||||
|
create table book (
|
||||||
|
id bigint not null auto_increment primary key,
|
||||||
|
owner_id bigint null,
|
||||||
|
subject varchar(200) not null,
|
||||||
|
title varchar(200) not null,
|
||||||
|
active_version_id bigint null,
|
||||||
|
|
||||||
|
created_at timestamp null,
|
||||||
|
updated_at timestamp null,
|
||||||
|
deleted boolean not null default false,
|
||||||
|
deleted_at timestamp null,
|
||||||
|
|
||||||
|
key index_active_version_id (active_version_id)
|
||||||
|
) engine = InnoDB
|
||||||
|
default charset = utf8mb4;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- liquibase formatted sql
|
||||||
|
-- changeset ijeongmin:2_create_book_version.sql
|
||||||
|
create table book_version (
|
||||||
|
id bigint not null auto_increment primary key,
|
||||||
|
book_id bigint null,
|
||||||
|
status varchar(20) not null,
|
||||||
|
length_preset varchar(10) null,
|
||||||
|
book_s3_key varchar(500) null,
|
||||||
|
audio_s3_key varchar(500) null,
|
||||||
|
language varchar(10) null,
|
||||||
|
audio_duration_sec int null,
|
||||||
|
|
||||||
|
created_at timestamp null,
|
||||||
|
updated_at timestamp null,
|
||||||
|
deleted boolean not null default false,
|
||||||
|
deleted_at timestamp null,
|
||||||
|
|
||||||
|
key idx_book_version_book_id (book_id),
|
||||||
|
key idx_book_version_status (status)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user