diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 42da90c96..289a5d5ce 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -72,7 +72,20 @@ public enum ErrorCode implements ResponseCode { */ VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."), VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다."), - INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다.") + INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다."), + + /** + * 120000 : record error + */ + RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, 120000, "존재하지 않는 RECORD 입니다."), + RECORD_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 120001, "총평이 될 수 없는 RECORD 입니다."), + INVALID_RECORD_PAGE_RANGE(HttpStatus.BAD_REQUEST, 120002, "RECORD의 page 값이 유효하지 않습니다."), + RECORD_CANNOT_WRITE_IN_EXPIRED_ROOM(HttpStatus.BAD_REQUEST, 120003, "만료된 방에는 기록을 남길 수 없습니다."), + + /** + * 130000 : userRoom error + */ + USER_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 130000, "존재하지 않는 USER_ROOM (방과 사용자 관계) 입니다."), ; diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java b/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java index 4102d9811..c041b3737 100644 --- a/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java +++ b/src/main/java/konkuk/thip/record/adapter/in/web/RecordCommandController.java @@ -1,10 +1,28 @@ package konkuk.thip.record.adapter.in.web; +import jakarta.validation.Valid; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.record.adapter.in.web.request.RecordCreateRequest; +import konkuk.thip.record.adapter.in.web.response.RecordCreateResponse; +import konkuk.thip.record.application.port.in.RecordCreateUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor public class RecordCommandController { + private final RecordCreateUseCase recordCreateUseCase; + + @PostMapping("/rooms/{roomId}/record") + public BaseResponse createRecord( + @UserId final Long userId, + @PathVariable final Long roomId, + @Valid @RequestBody final RecordCreateRequest recordCreateRequest) { + return BaseResponse.ok( + RecordCreateResponse.of( + recordCreateUseCase.createRecord(recordCreateRequest.toCommand(roomId, userId)) + )); + } } diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/request/DummyRequest.java b/src/main/java/konkuk/thip/record/adapter/in/web/request/DummyRequest.java deleted file mode 100644 index ad0881008..000000000 --- a/src/main/java/konkuk/thip/record/adapter/in/web/request/DummyRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.record.adapter.in.web.request; - -import lombok.Getter; - -@Getter -public class DummyRequest { -} diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java b/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java new file mode 100644 index 000000000..856061bd0 --- /dev/null +++ b/src/main/java/konkuk/thip/record/adapter/in/web/request/RecordCreateRequest.java @@ -0,0 +1,28 @@ +package konkuk.thip.record.adapter.in.web.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import konkuk.thip.record.application.port.in.dto.RecordCreateCommand; + +public record RecordCreateRequest ( + @NotNull(message = "page는 필수입니다.") + Integer page, + + @NotNull(message = "isOverview(= 총평 여부)는 필수입니다.") + Boolean isOverview, + + @NotBlank(message = "기록 내용은 필수입니다.") + @Size(max = 500, message = "기록 내용은 최대 500자 입니다.") + String content +) { + public RecordCreateCommand toCommand(Long roomId, Long creatorId) { + return new RecordCreateCommand( + creatorId, + roomId, + page, + isOverview, + content + ); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/response/DummyResponse.java b/src/main/java/konkuk/thip/record/adapter/in/web/response/DummyResponse.java deleted file mode 100644 index 1a676ce2e..000000000 --- a/src/main/java/konkuk/thip/record/adapter/in/web/response/DummyResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.record.adapter.in.web.response; - -import lombok.Getter; - -@Getter -public class DummyResponse { -} diff --git a/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordCreateResponse.java b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordCreateResponse.java new file mode 100644 index 000000000..f871a07fd --- /dev/null +++ b/src/main/java/konkuk/thip/record/adapter/in/web/response/RecordCreateResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.record.adapter.in.web.response; + +public record RecordCreateResponse( + Long recordId +) { + public static RecordCreateResponse of(Long recordId) { + return new RecordCreateResponse(recordId); + } +} diff --git a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java index 33e98dfc4..5cce52852 100644 --- a/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/record/adapter/out/persistence/RecordCommandPersistenceAdapter.java @@ -1,15 +1,41 @@ package konkuk.thip.record.adapter.out.persistence; +import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.record.adapter.out.mapper.RecordMapper; import konkuk.thip.record.application.port.out.RecordCommandPort; +import konkuk.thip.record.domain.Record; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; + @Repository @RequiredArgsConstructor public class RecordCommandPersistenceAdapter implements RecordCommandPort { private final RecordJpaRepository recordJpaRepository; + private final UserJpaRepository userJpaRepository; + private final RoomJpaRepository roomJpaRepository; private final RecordMapper recordMapper; + @Override + public Long saveRecord(Record record) { + UserJpaEntity userJpaEntity = userJpaRepository.findById(record.getCreatorId()).orElseThrow( + () -> new EntityNotFoundException(USER_NOT_FOUND) + ); + + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(record.getRoomId()).orElseThrow( + () -> new EntityNotFoundException(ROOM_NOT_FOUND) + ); + + return recordJpaRepository.save( + recordMapper.toJpaEntity(record, userJpaEntity, roomJpaEntity) + ).getPostId(); + } + } diff --git a/src/main/java/konkuk/thip/record/application/port/in/DummyUseCase.java b/src/main/java/konkuk/thip/record/application/port/in/DummyUseCase.java deleted file mode 100644 index 190e4992c..000000000 --- a/src/main/java/konkuk/thip/record/application/port/in/DummyUseCase.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.record.application.port.in; - -public interface DummyUseCase { - -} diff --git a/src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java b/src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java new file mode 100644 index 000000000..957f58484 --- /dev/null +++ b/src/main/java/konkuk/thip/record/application/port/in/RecordCreateUseCase.java @@ -0,0 +1,9 @@ +package konkuk.thip.record.application.port.in; + +import konkuk.thip.record.application.port.in.dto.RecordCreateCommand; + +public interface RecordCreateUseCase { + + Long createRecord(RecordCreateCommand command); + +} diff --git a/src/main/java/konkuk/thip/record/application/port/in/dto/DummyCommand.java b/src/main/java/konkuk/thip/record/application/port/in/dto/DummyCommand.java deleted file mode 100644 index 9e48b6a52..000000000 --- a/src/main/java/konkuk/thip/record/application/port/in/dto/DummyCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.record.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyCommand { - -} diff --git a/src/main/java/konkuk/thip/record/application/port/in/dto/RecordCreateCommand.java b/src/main/java/konkuk/thip/record/application/port/in/dto/RecordCreateCommand.java new file mode 100644 index 000000000..c9e5c526a --- /dev/null +++ b/src/main/java/konkuk/thip/record/application/port/in/dto/RecordCreateCommand.java @@ -0,0 +1,15 @@ +package konkuk.thip.record.application.port.in.dto; + +public record RecordCreateCommand( + Long userId, + + Long roomId, + + int page, + + boolean isOverview, + + String content +) { + +} diff --git a/src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java b/src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java index 8719b6fb1..d57a5d6c3 100644 --- a/src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java +++ b/src/main/java/konkuk/thip/record/application/port/out/RecordCommandPort.java @@ -1,6 +1,10 @@ package konkuk.thip.record.application.port.out; +import konkuk.thip.record.domain.Record; + public interface RecordCommandPort { + Long saveRecord(Record record); + } diff --git a/src/main/java/konkuk/thip/record/application/service/RecordCreateService.java b/src/main/java/konkuk/thip/record/application/service/RecordCreateService.java new file mode 100644 index 000000000..63adae25a --- /dev/null +++ b/src/main/java/konkuk/thip/record/application/service/RecordCreateService.java @@ -0,0 +1,98 @@ +package konkuk.thip.record.application.service; + +import jakarta.transaction.Transactional; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.record.application.port.in.RecordCreateUseCase; +import konkuk.thip.record.application.port.in.dto.RecordCreateCommand; +import konkuk.thip.record.application.port.out.RecordCommandPort; +import konkuk.thip.record.domain.Record; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; +import konkuk.thip.user.application.port.out.UserRoomCommandPort; +import konkuk.thip.user.domain.UserRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static konkuk.thip.common.exception.code.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class RecordCreateService implements RecordCreateUseCase { + + private final RecordCommandPort recordCommandPort; + private final RoomCommandPort roomCommandPort; + private final BookCommandPort bookCommandPort; + private final UserRoomCommandPort userRoomCommandPort; + + @Transactional + @Override + //todo updateRoomPercentage 스케줄러로 책임을 분리할지 논의 + public Long createRecord(RecordCreateCommand command) { + // 1. Record 생성 + Record record = Record.withoutId( + command.content(), + command.userId(), + command.page(), + command.isOverview(), + command.roomId() + ); + + // 2. UserRoom, Room, Book 조회 + UserRoom userRoom = userRoomCommandPort.findByUserIdAndRoomId(command.userId(), command.roomId()); + Room room = roomCommandPort.findById(record.getRoomId()); + Book book = bookCommandPort.findById(room.getBookId()); + + // 3. 유효성 검증 + validateRoom(room); + validateUserRoom(userRoom); + validateRecord(record, book); + + // 4. UserRoom의 currentPage, userPercentage 업데이트 + updateRoomProgress(userRoom, record, book, room); + + // 5. Record 저장 + return recordCommandPort.saveRecord(record); + } + + private void updateRoomProgress(UserRoom userRoom, Record record, Book book, Room room) { + if(userRoom.updateUserProgress(record.getPage(), book.getPageCount())) { + // userPercentage가 업데이트되었으면 Room의 roomPercentage 업데이트 + List userRoomList = userRoomCommandPort.findAllByRoomId(record.getRoomId()); + Double totalUserPercentage = userRoomList.stream() + .map(UserRoom::getUserPercentage) + .reduce(0.0, Double::sum); + room.updateRoomPercentage(totalUserPercentage / userRoomList.size()); + } + } + + private void validateUserRoom(UserRoom userRoom) { + // UserRoom의 총평 작성 가능 여부 검증 + if (!userRoom.canWriteOverview()) { + String message = String.format( + "총평(isOverview)은 사용자 진행률이 80%% 이상일 때만 가능합니다. 현재 사용자 진행률 = %.2f%%", + userRoom.getUserPercentage() + ); + throw new InvalidStateException(RECORD_CANNOT_BE_OVERVIEW, new IllegalStateException(message)); + } + } + + private void validateRoom(Room room) { + // 방이 만료되었는지 검증 + if (room.isExpired()) { + throw new BusinessException(RECORD_CANNOT_WRITE_IN_EXPIRED_ROOM); + } + } + + private void validateRecord(Record record, Book book) { + // 페이지 유효성 검증 + record.validatePage(book.getPageCount()); + + // 총평 유효성 검증 + record.validateOverview(book.getPageCount()); + } +} diff --git a/src/main/java/konkuk/thip/record/application/service/RecordService.java b/src/main/java/konkuk/thip/record/application/service/RecordService.java deleted file mode 100644 index 123c0adbe..000000000 --- a/src/main/java/konkuk/thip/record/application/service/RecordService.java +++ /dev/null @@ -1,11 +0,0 @@ -package konkuk.thip.record.application.service; - -import konkuk.thip.record.application.port.in.DummyUseCase; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class RecordService implements DummyUseCase { - -} diff --git a/src/main/java/konkuk/thip/record/domain/Record.java b/src/main/java/konkuk/thip/record/domain/Record.java index 39734cd08..ae46fa255 100644 --- a/src/main/java/konkuk/thip/record/domain/Record.java +++ b/src/main/java/konkuk/thip/record/domain/Record.java @@ -1,9 +1,12 @@ package konkuk.thip.record.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.exception.InvalidStateException; import lombok.Getter; import lombok.experimental.SuperBuilder; +import static konkuk.thip.common.exception.code.ErrorCode.*; + @Getter @SuperBuilder public class Record extends BaseDomainEntity { @@ -19,4 +22,44 @@ public class Record extends BaseDomainEntity { private boolean isOverview; private Long roomId; + + public static Record withoutId( + String content, + Long creatorId, + Integer page, + boolean isOverview, + Long roomId + ) { + return Record.builder() + .content(content) + .creatorId(creatorId) + .page(page) + .isOverview(isOverview) + .roomId(roomId) + .build(); + } + + + public void validateOverview(int totalPageCount) { + // 총평 기록 생성 요청인데 page가 책의 전체 페이지 수가 아니라면 에러 + if (isOverview && page != totalPageCount) { + String message = String.format( + "총평(isOverview)은 책의 전체 페이지 수(%d)와 동일한 페이지에서만 작성할 수 있습니다. 현재 페이지 = %d", + totalPageCount, page + ); + throw new InvalidStateException(RECORD_CANNOT_BE_OVERVIEW, new IllegalArgumentException(message)); + } + } + + public void validatePage(int totalPageCount) { + if (page < 1 || page > totalPageCount) { + String message = String.format( + "페이지 범위가 잘못되었습니다. 현재 기록할 page = %d, 책 전체 page = %d", + page, totalPageCount + ); + throw new InvalidStateException(INVALID_RECORD_PAGE_RANGE, + new IllegalArgumentException(message) + ); + } + } } diff --git a/src/main/java/konkuk/thip/room/domain/Room.java b/src/main/java/konkuk/thip/room/domain/Room.java index f3cf40a04..f4bb359b1 100644 --- a/src/main/java/konkuk/thip/room/domain/Room.java +++ b/src/main/java/konkuk/thip/room/domain/Room.java @@ -1,6 +1,7 @@ package konkuk.thip.room.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.entity.StatusType; import lombok.Getter; import lombok.experimental.SuperBuilder; @@ -31,4 +32,12 @@ public class Room extends BaseDomainEntity { private Long bookId; private Long categoryId; + + public boolean isExpired() { + return this.getStatus() == StatusType.EXPIRED; + } + + public void updateRoomPercentage(double roomPercentage) { + this.roomPercentage = roomPercentage; + } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java new file mode 100644 index 000000000..cd412215a --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomCommandPersistenceAdapter.java @@ -0,0 +1,36 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.user.adapter.out.jpa.UserRoomJpaEntity; +import konkuk.thip.user.adapter.out.mapper.UserRoomMapper; +import konkuk.thip.user.application.port.out.UserRoomCommandPort; +import konkuk.thip.user.domain.UserRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class UserRoomCommandPersistenceAdapter implements UserRoomCommandPort { + + private final UserRoomJpaRepository userRoomJpaRepository; + private final UserRoomMapper userRoomMapper; + + @Override + public UserRoom findByUserIdAndRoomId(Long userId, Long roomId) { + UserRoomJpaEntity userRoomJpaEntity = userRoomJpaRepository.findByUserJpaEntity_UserIdAndRoomJpaEntity_RoomId(userId, roomId).orElseThrow( + () -> new EntityNotFoundException(ErrorCode.USER_ROOM_NOT_FOUND) + ); + + return userRoomMapper.toDomainEntity(userRoomJpaEntity); + } + + @Override + public List findAllByRoomId(Long roomId) { + return userRoomJpaRepository.findAllByRoomJpaEntity_RoomId(roomId).stream() + .map(userRoomMapper::toDomainEntity) + .toList(); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java index c073b4238..c77aadf5d 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserRoomJpaRepository.java @@ -3,5 +3,11 @@ import konkuk.thip.user.adapter.out.jpa.UserRoomJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + public interface UserRoomJpaRepository extends JpaRepository{ + + Optional findByUserJpaEntity_UserIdAndRoomJpaEntity_RoomId(Long userId, Long roomId); + List findAllByRoomJpaEntity_RoomId(Long roomId); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java new file mode 100644 index 000000000..6d3ced065 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/out/UserRoomCommandPort.java @@ -0,0 +1,12 @@ +package konkuk.thip.user.application.port.out; + +import konkuk.thip.user.domain.UserRoom; + +import java.util.List; + +public interface UserRoomCommandPort { + + UserRoom findByUserIdAndRoomId(Long userId, Long roomId); + List findAllByRoomId(Long roomId); + +} diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserRoomQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserRoomQueryPort.java new file mode 100644 index 000000000..c75d09f35 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/out/UserRoomQueryPort.java @@ -0,0 +1,4 @@ +package konkuk.thip.user.application.port.out; + +public interface UserRoomQueryPort { +} diff --git a/src/main/java/konkuk/thip/user/domain/UserRoom.java b/src/main/java/konkuk/thip/user/domain/UserRoom.java index 675a73cfc..fa1d233b0 100644 --- a/src/main/java/konkuk/thip/user/domain/UserRoom.java +++ b/src/main/java/konkuk/thip/user/domain/UserRoom.java @@ -19,4 +19,19 @@ public class UserRoom extends BaseDomainEntity { private Long userId; private Long roomId; + + public boolean canWriteOverview() { + return userPercentage >= 80; + } + + // 기록(투표) 요청 페이지와 책 전체 페이지 + public boolean updateUserProgress(int requestPage, int totalPageCount) { + if (currentPage < requestPage) { + currentPage = requestPage; + userPercentage = Math.min(((double) currentPage / totalPageCount) * 100, 100.0); + return true; + } + + return false; + } } diff --git a/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java b/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java index 22b7c38e2..96cf49e75 100644 --- a/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java +++ b/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java @@ -27,6 +27,7 @@ public class VoteCreateService implements VoteCreateUseCase { @Transactional @Override + //todo UserRoom 업데이트 로직 추가 필요!! public Long createVote(VoteCreateCommand command) { // 1. validate Vote vote = Vote.withoutId( diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java index 871ded7f7..204ac4cde 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java @@ -35,6 +35,7 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@DisplayName("[통합] BookChangeSavedController 테스트") class BookChangeSavedControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java index 4c7910c28..b86063d35 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookDetailSearchControllerTest.java @@ -31,6 +31,7 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @ActiveProfiles("test") +@DisplayName("[통합] BookDetailSearchController 테스트") class BookDetailSearchControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java index dddbbfe96..6aba36187 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java @@ -29,6 +29,7 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") +@DisplayName("[통합] BookQueryController 테스트") class BookQueryControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java b/src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java index b7ad7ae63..6b62f1178 100644 --- a/src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java +++ b/src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java @@ -10,6 +10,7 @@ import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NAVER_API_REQUEST_ERROR; import static org.assertj.core.api.Assertions.*; +@DisplayName("[단위] NaverApiUtil 테스트") class NaverApiUtilTest { private NaverApiUtil createTestUtil() { diff --git a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java index d956cf296..03d6484a1 100644 --- a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java @@ -20,6 +20,7 @@ @DataJpaTest @ActiveProfiles("test") +@DisplayName("[JPA] FeedJpaEntity 테스트") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class FeedJpaEntityTest { diff --git a/src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java b/src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java new file mode 100644 index 000000000..04279b9ce --- /dev/null +++ b/src/test/java/konkuk/thip/record/adapter/in/web/RecordCreateControllerTest.java @@ -0,0 +1,228 @@ +package konkuk.thip.record.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.record.adapter.in.web.request.RecordCreateRequest; +import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.record.adapter.out.persistence.RecordJpaRepository; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.*; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserRoomJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.Map; + +import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] RecordCommandController 테스트") +class RecordCreateControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @Autowired + private RecordJpaRepository recordJpaRepository; + + @Autowired + private UserRoomJpaRepository userRoomJpaRepository; + + @AfterEach + void tearDown() { + recordJpaRepository.deleteAll(); + userRoomJpaRepository.deleteAll(); + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + private void saveUserAndRoom() { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createAlias()); + + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + + CategoryJpaEntity category = categoryJpaRepository.save(TestEntityFactory.createCategory(alias)); + + RoomJpaEntity room = roomJpaRepository.save(TestEntityFactory.createRoom(book, category)); + + //UserRoomJpaEntity 생성 및 저장 + UserRoomJpaEntity userRoom = UserRoomJpaEntity.builder() + .currentPage(10) + .userPercentage(80.0) + .userRoomRole(UserRoomRole.HOST) + .userJpaEntity(user) + .roomJpaEntity(room) + .build(); + + userRoomJpaRepository.save(userRoom); + } + + @Test + @DisplayName("[페이지 넘버, 총평 여부, 기록 내용]을 받아, 기록을 생성한다.") + void record_create_success() throws Exception { + //given + saveUserAndRoom(); + + int page = 10; + boolean isOverview = false; + String content = "기록 내용"; + + RecordCreateRequest request = new RecordCreateRequest( + page, + isOverview, + content + ); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + Long roomId = roomJpaRepository.findAll().get(0).getRoomId(); + + //when + ResultActions result = mockMvc.perform(post("/rooms/{roomId}/record", roomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request) + )); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.recordId").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + Long recordId = jsonNode.path("data").path("recordId").asLong(); + + RecordJpaEntity recordJpaEntity = recordJpaRepository.findById(recordId).orElse(null); + + assertThat(recordJpaEntity).isNotNull(); + assertThat(recordJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId); + assertThat(recordJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(roomId); + assertThat(recordJpaEntity.getPage()).isEqualTo(page); + assertThat(recordJpaEntity.getContent()).isEqualTo(content); + } + + @Test + @DisplayName("[page]가 누락되었을 때 400 Bad Request 반환") + void record_create_page_null() throws Exception { + // given: page 누락 + Map request = Map.of( + "isOverview", false, + "content", "내용" + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/record", 1L) + .requestAttr("userId", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("page는 필수입니다."))); + } + + @Test + @DisplayName("[isOverview]가 누락되었을 때 400 Bad Request 반환") + void record_create_is_over_view_null() throws Exception { + // given: isOverview 누락 + Map request = Map.of( + "page", 1, + "content", "내용" + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/record", 1L) + .requestAttr("userId", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("isOverview(= 총평 여부)는 필수입니다."))); + } + + @Test + @DisplayName("[content]가 빈 문자열일 때 400 Bad Request 반환") + void record_create_content_blank() throws Exception { + // given + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "" + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/record", 1L) + .requestAttr("userId", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("기록 내용은 필수입니다."))); + } + + @Test + @DisplayName("[content]가 500자 초과일 때 400 Bad Request 반환") + void record_create_content_too_long() throws Exception { + // given + String longContent = "가".repeat(501); + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", longContent + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/record", 1L) + .requestAttr("userId", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("기록 내용은 최대 500자 입니다."))); + } + +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/record/domain/RecordTest.java b/src/test/java/konkuk/thip/record/domain/RecordTest.java new file mode 100644 index 000000000..75b14fdf2 --- /dev/null +++ b/src/test/java/konkuk/thip/record/domain/RecordTest.java @@ -0,0 +1,63 @@ +package konkuk.thip.record.domain; + +import konkuk.thip.common.exception.InvalidStateException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("[단위] Record 도메인 테스트") +class RecordTest { + + @Test + @DisplayName("validatePage: 유효한 페이지 범위일 때, 예외가 발생하지 않는다.") + void validate_page_valid_range() { + Record record = Record.withoutId("content", 1L, 10, false, 1L); + assertDoesNotThrow(() -> record.validatePage(20)); + assertDoesNotThrow(() -> record.validatePage(10)); + } + + @Test + @DisplayName("validatePage: page가 1보다 작을 때, InvalidStateException 발생한다.") + void validate_page_lower_than_zero() { + Record record = Record.withoutId("content", 1L, 0, false, 1L); + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> record.validatePage(20)); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("현재 기록할 page = 0, 책 전체 page = 20")); + } + + @Test + @DisplayName("validatePage: page가 전체 페이지 수를 초과할 때, InvalidStateException 발생한다.") + void validate_page_bigger_than_total() { + Record record = Record.withoutId("content", 1L, 25, false, 1L); + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> record.validatePage(20)); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("현재 기록할 page = 25, 책 전체 page = 20")); + } + + @Test + @DisplayName("validateOverview: isOverview=false 이면, 예외가 발생하지 않는다.") + void validate_overview_not_overview_no_exception() { + Record record = Record.withoutId("content", 1L, 5, false, 1L); + assertDoesNotThrow(() -> record.validateOverview(20)); + } + + @Test + @DisplayName("validateOverview: isOverview=true 이고 page가 전체 페이지 수와 같으면, 예외가 발생하지 않는다.") + void validate_overview_page_is_book_page_count() { + Record record = Record.withoutId("content", 1L, 100, true, 1L); + assertDoesNotThrow(() -> record.validateOverview(100)); + } + + @Test + @DisplayName("validateOverview: isOverview=true 이고 page가 전체 페이지 수와 다르면, InvalidStateException 발생한다.") + void validate_overview_page_is_not_book_page_count() { + Record record = Record.withoutId("content", 1L, 15, true, 1L); // 15/20 = 0.75 + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> record.validateOverview(20)); + assertInstanceOf(IllegalArgumentException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("현재 페이지 = 15")); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java index 4a807fe95..0d60b0088 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java @@ -24,6 +24,7 @@ @DataJpaTest @ActiveProfiles("test") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@DisplayName("[JPA] RecordJpaEntity 테스트") class RecordJpaEntityTest { @Autowired diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java index 97610553e..3d18de653 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java @@ -21,6 +21,7 @@ @DataJpaTest @ActiveProfiles("test") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@DisplayName("[JPA] RoomJpaEntity 테스트") class RoomJpaEntityTest { @PersistenceContext diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java index 692dfe4ef..79f3aea6a 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java @@ -24,6 +24,7 @@ @DataJpaTest @ActiveProfiles("test") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@DisplayName("[JPA] VoteJpaEntity 테스트") class VoteJpaEntityTest { @Autowired diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java index 9947800a1..899d914ac 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java @@ -30,6 +30,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc +@DisplayName("[통합] UserSignupController 테스트") class UserSignupControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java index e56c806cc..8afcd9bf8 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java @@ -29,6 +29,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] UserVerifyNicknameController 테스트") class UserVerifyNicknameControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java index 2514b10d7..15c999bf6 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java @@ -29,6 +29,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] UserViewAliasChoiceController 테스트") class UserViewAliasChoiceControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java index f303de831..ffd06ca78 100644 --- a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java +++ b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java @@ -17,6 +17,7 @@ @DataJpaTest @ActiveProfiles("test") @Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import +@DisplayName("[JPA] UserJpaEntity 테스트") class UserJpaEntityTest { @PersistenceContext diff --git a/src/test/java/konkuk/thip/user/domain/UserRoomTest.java b/src/test/java/konkuk/thip/user/domain/UserRoomTest.java new file mode 100644 index 000000000..517e4a8d1 --- /dev/null +++ b/src/test/java/konkuk/thip/user/domain/UserRoomTest.java @@ -0,0 +1,84 @@ +package konkuk.thip.user.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("[단위] UserRoom 도메인 테스트") +class UserRoomTest { + + @Test + @DisplayName("canWriteOverview: 사용자 퍼센트가 80 이상일 때, true를 반환한다.") + void can_writeOverview() { + //given + UserRoom userRoom = UserRoom.builder() + .userPercentage(80.0) + .build(); + + //when + boolean canWrite = userRoom.canWriteOverview(); + + //then + assertThat(canWrite).isTrue(); + } + + @Test + @DisplayName("canWriteOverview: 사용자 퍼센트가 80 미만일 때, false를 반환한다.") + void cannot_writeOverview() { + //given + UserRoom userRoom = UserRoom.builder() + .userPercentage(79.9) + .build(); + + //when + boolean canWrite = userRoom.canWriteOverview(); + + //then + assertThat(canWrite).isFalse(); + } + + @Test + @DisplayName("updateUserProgress: 요청 페이지가 현재 페이지보다 클 때, 현재 페이지와 사용자 퍼센트가 업데이트된다.") + void update_userProgress_success() throws Exception { + //given + UserRoom userRoom = UserRoom.builder() + .currentPage(1) + .userPercentage(5.0) + .userId(1L) + .roomId(1L) + .build(); + + //when + int totalPageCount = 20; + boolean isUpdated = userRoom.updateUserProgress(5, totalPageCount); + + //then + assertThat(isUpdated).isTrue(); + assertThat(userRoom.getCurrentPage()).isEqualTo(5); + double ratio = (double) userRoom.getCurrentPage() / totalPageCount; + assertThat(userRoom.getUserPercentage()).isEqualTo(ratio * 100); + } + + @Test + @DisplayName("updateUserProgress: 요청 페이지가 현재 페이지보다 작을 때, 현재 페이지와 사용자 퍼센트가 업데이트되지 않는다.") + void update_userProgress_when_request_isLower_than_bookPage() throws Exception { + //given + UserRoom userRoom = UserRoom.builder() + .currentPage(5) + .userPercentage(25.0) + .userId(1L) + .roomId(1L) + .build(); + + //when + int totalPageCount = 20; + boolean isUpdated = userRoom.updateUserProgress(3, totalPageCount); + + //then + assertThat(isUpdated).isFalse(); + assertThat(userRoom.getCurrentPage()).isEqualTo(5); + double ratio = (double) userRoom.getCurrentPage() / totalPageCount; + assertThat(userRoom.getUserPercentage()).isEqualTo(ratio * 100); + } +} \ No newline at end of file diff --git a/src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java b/src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java index 76d4c7648..b103037ef 100644 --- a/src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java +++ b/src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java @@ -44,6 +44,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] VoteCreateController 테스트") class VoteCreateControllerTest { @Autowired diff --git a/src/test/java/konkuk/thip/vote/domain/VoteTest.java b/src/test/java/konkuk/thip/vote/domain/VoteTest.java index 49b0e1366..8992a8dd5 100644 --- a/src/test/java/konkuk/thip/vote/domain/VoteTest.java +++ b/src/test/java/konkuk/thip/vote/domain/VoteTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.*; +@DisplayName("[단위] Vote 도메인 테스트") class VoteTest { @Test