From 499787a5a10b1cbed1ea1aff07a94fc7e7846116 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Thu, 3 Jul 2025 15:09:26 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[feat]=20:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20use=20case=20=EA=B0=9C=EB=B0=9C=20(#?= =?UTF-8?q?31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/port/in/DummyUseCase.java | 5 ---- .../port/in/VoteCreateUseCase.java | 8 ++++++ .../application/port/in/dto/DummyCommand.java | 10 ------- .../port/in/dto/VoteCreateCommand.java | 26 +++++++++++++++++++ .../application/port/out/VoteCommandPort.java | 2 ++ .../vote/application/service/VoteService.java | 21 +++++++++++++-- .../java/konkuk/thip/vote/domain/Vote.java | 11 ++++++++ 7 files changed, 66 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java create mode 100644 src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java delete mode 100644 src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java create mode 100644 src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java diff --git a/src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java b/src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java deleted file mode 100644 index 4b8a8b301..000000000 --- a/src/main/java/konkuk/thip/vote/application/port/in/DummyUseCase.java +++ /dev/null @@ -1,5 +0,0 @@ -package konkuk.thip.vote.application.port.in; - -public interface DummyUseCase { - -} diff --git a/src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java b/src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java new file mode 100644 index 000000000..ea104ac4e --- /dev/null +++ b/src/main/java/konkuk/thip/vote/application/port/in/VoteCreateUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.vote.application.port.in; + +import konkuk.thip.vote.application.port.in.dto.VoteCreateCommand; + +public interface VoteCreateUseCase { + + Long createVote(VoteCreateCommand command); +} diff --git a/src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java b/src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java deleted file mode 100644 index f0ae41938..000000000 --- a/src/main/java/konkuk/thip/vote/application/port/in/dto/DummyCommand.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.vote.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyCommand { - -} diff --git a/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java b/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java new file mode 100644 index 000000000..cf1f09a91 --- /dev/null +++ b/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java @@ -0,0 +1,26 @@ +package konkuk.thip.vote.application.port.in.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Builder +@Getter +public record VoteCreateCommand( + Long userId, + + Long roomId, + + int page, + + boolean isOverview, + + String content, + + List voteItems +) { + @Getter + @Builder + public record VoteItem(String itemName) {} +} diff --git a/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java b/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java index 4c6511a58..16700377a 100644 --- a/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java +++ b/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java @@ -1,6 +1,8 @@ package konkuk.thip.vote.application.port.out; +import konkuk.thip.vote.domain.Vote; public interface VoteCommandPort { + Long save(Vote vote); } diff --git a/src/main/java/konkuk/thip/vote/application/service/VoteService.java b/src/main/java/konkuk/thip/vote/application/service/VoteService.java index 8a615b289..fde59fd09 100644 --- a/src/main/java/konkuk/thip/vote/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/vote/application/service/VoteService.java @@ -1,11 +1,28 @@ package konkuk.thip.vote.application.service; -import konkuk.thip.vote.application.port.in.DummyUseCase; +import konkuk.thip.vote.application.port.in.VoteCreateUseCase; +import konkuk.thip.vote.application.port.in.dto.VoteCreateCommand; +import konkuk.thip.vote.application.port.out.VoteCommandPort; +import konkuk.thip.vote.domain.Vote; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class VoteService implements DummyUseCase { +public class VoteService implements VoteCreateUseCase { + private final VoteCommandPort voteCommandPort; + + @Override + public Long createVote(VoteCreateCommand command) { + Vote vote = Vote.withoutId( + command.content(), + command.userId(), + command.page(), + command.isOverview(), + command.roomId() + ); + + return voteCommandPort.save(vote); + } } diff --git a/src/main/java/konkuk/thip/vote/domain/Vote.java b/src/main/java/konkuk/thip/vote/domain/Vote.java index 9bc9f563a..5008703fe 100644 --- a/src/main/java/konkuk/thip/vote/domain/Vote.java +++ b/src/main/java/konkuk/thip/vote/domain/Vote.java @@ -19,4 +19,15 @@ public class Vote extends BaseDomainEntity { private boolean isOverview; private Long roomId; + + public static Vote withoutId(String content, Long creatorId, Integer page, boolean isOverview, Long roomId) { + return Vote.builder() + .id(null) + .content(content) + .creatorId(creatorId) + .page(page) + .isOverview(isOverview) + .roomId(roomId) + .build(); + } } From 43fada51d7e3202868980493e2286d84e1da3d6f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:19:18 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[refactor]=20:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20use=20case=20=EC=88=98=EC=A0=95=20(#?= =?UTF-8?q?37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 투표 생성 시 페이지 범위, 총평 여부 유효성 검사 로직 추가 --- .../BookCommandPersistenceAdapter.java | 14 +++++++ .../application/port/out/BookCommandPort.java | 1 + .../thip/common/exception/code/ErrorCode.java | 13 +++++- .../RoomCommandPersistenceAdapter.java | 17 +++++++- .../application/port/out/RoomCommandPort.java | 2 + .../vote/application/service/VoteService.java | 42 ++++++++++++++++++- .../java/konkuk/thip/vote/domain/Vote.java | 26 ++++++++++++ .../konkuk/thip/vote/domain/VoteItem.java | 9 ++++ 8 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 0dc6015aa..2407924e2 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -1,13 +1,18 @@ package konkuk.thip.book.adapter.out.persistence; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.mapper.BookMapper; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.common.exception.code.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.util.Optional; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; + @Repository @RequiredArgsConstructor public class BookCommandPersistenceAdapter implements BookCommandPort { @@ -20,4 +25,13 @@ public Optional findByIsbn(String isbn) { return bookJpaRepository.findByIsbn(isbn) .map(bookMapper::toDomainEntity); } + + @Override + public Book findById(Long id) { + BookJpaEntity bookJpaEntity = bookJpaRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException(BOOK_NOT_FOUND) + ); + + return bookMapper.toDomainEntity(bookJpaEntity); + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index 3e70212bd..e842e09c4 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -9,4 +9,5 @@ public interface BookCommandPort { Optional findByIsbn(String isbn); + Book findById(Long id); } \ No newline at end of file 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 1d8d57230..013150729 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -48,12 +48,23 @@ public enum ErrorCode implements ResponseCode { BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), BOOK_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "ISBN으로 검색한 결과가 존재하지 않습니다."), - BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."); + BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."), + /** + * 100000 : room error + */ + ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 100000, "존재하지 않는 ROOM 입니다."), + /** + * 110000 : vote error + */ + VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."), + VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다. 총평은 진행률이 80% 이상이어야 가능합니다."), + INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다. 페이지 값은 1이상 ") + ; private final HttpStatus httpStatus; private final int code; diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java index b3b8c9163..c6a607187 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomCommandPersistenceAdapter.java @@ -1,15 +1,28 @@ package konkuk.thip.room.adapter.out.persistence; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.mapper.RoomMapper; import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_NOT_FOUND; + @Repository @RequiredArgsConstructor public class RoomCommandPersistenceAdapter implements RoomCommandPort { - private final RoomJpaRepository jpaRepository; - private final RoomMapper userMapper; + private final RoomJpaRepository roomJpaRepository; + private final RoomMapper roomMapper; + + @Override + public Room findById(Long id) { + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(id).orElseThrow( + () -> new EntityNotFoundException(ROOM_NOT_FOUND) + ); + return roomMapper.toDomainEntity(roomJpaEntity); + } } diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java index 585323dd8..aa4ca8d28 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomCommandPort.java @@ -1,6 +1,8 @@ package konkuk.thip.room.application.port.out; +import konkuk.thip.room.domain.Room; public interface RoomCommandPort { + Room findById(Long id); } diff --git a/src/main/java/konkuk/thip/vote/application/service/VoteService.java b/src/main/java/konkuk/thip/vote/application/service/VoteService.java index fde59fd09..6b2658863 100644 --- a/src/main/java/konkuk/thip/vote/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/vote/application/service/VoteService.java @@ -1,20 +1,34 @@ package konkuk.thip.vote.application.service; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.room.application.port.out.RoomCommandPort; +import konkuk.thip.room.domain.Room; import konkuk.thip.vote.application.port.in.VoteCreateUseCase; import konkuk.thip.vote.application.port.in.dto.VoteCreateCommand; import konkuk.thip.vote.application.port.out.VoteCommandPort; import konkuk.thip.vote.domain.Vote; +import konkuk.thip.vote.domain.VoteItem; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; @Service @RequiredArgsConstructor +@Slf4j public class VoteService implements VoteCreateUseCase { private final VoteCommandPort voteCommandPort; + private final RoomCommandPort roomCommandPort; + private final BookCommandPort bookCommandPort; + @Transactional @Override public Long createVote(VoteCreateCommand command) { + // 1. validate Vote vote = Vote.withoutId( command.content(), command.userId(), @@ -23,6 +37,32 @@ public Long createVote(VoteCreateCommand command) { command.roomId() ); - return voteCommandPort.save(vote); + validateVote(vote); + + // 2. vote 저장 + Long savedVoteId = voteCommandPort.saveVote(vote); + + // 3. vote item 저장 + List voteItems = command.voteItemCreateCommands().stream() + .map(itemCmd -> VoteItem.withoutId( + itemCmd.itemName(), + 0, + savedVoteId + )) + .toList(); + voteCommandPort.saveAllVoteItems(voteItems); + + return savedVoteId; + } + + private void validateVote(Vote vote) { + Room room = roomCommandPort.findById(vote.getRoomId()); + Book book = bookCommandPort.findById(room.getBookId()); + + // 페이지 유효성 검증 + vote.validatePage(book.getPageCount()); + + // 총평 유효성 검증 + vote.validateOverview(book.getPageCount()); } } diff --git a/src/main/java/konkuk/thip/vote/domain/Vote.java b/src/main/java/konkuk/thip/vote/domain/Vote.java index 5008703fe..6cde37e24 100644 --- a/src/main/java/konkuk/thip/vote/domain/Vote.java +++ b/src/main/java/konkuk/thip/vote/domain/Vote.java @@ -1,9 +1,12 @@ package konkuk.thip.vote.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 Vote extends BaseDomainEntity { @@ -30,4 +33,27 @@ public static Vote withoutId(String content, Long creatorId, Integer page, boole .roomId(roomId) .build(); } + + public void validateOverview(int totalPageCount) { + double ratio = (double) page / totalPageCount; + if (isOverview && ratio < 0.8) { + String message = String.format( + "총평(isOverview)은 진행률이 80%% 이상일 때만 가능합니다. 현재 진행률 = %.2f%% (%d/%d)", + ratio * 100, page, totalPageCount + ); + throw new InvalidStateException(VOTE_CANNOT_BE_OVERVIEW, new IllegalStateException(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_VOTE_PAGE_RANGE, + new IllegalArgumentException(message) + ); + } + } } diff --git a/src/main/java/konkuk/thip/vote/domain/VoteItem.java b/src/main/java/konkuk/thip/vote/domain/VoteItem.java index e10e2dbf2..184cc09fc 100644 --- a/src/main/java/konkuk/thip/vote/domain/VoteItem.java +++ b/src/main/java/konkuk/thip/vote/domain/VoteItem.java @@ -15,4 +15,13 @@ public class VoteItem extends BaseDomainEntity { private int count; private Long voteId; + + public static VoteItem withoutId(String itemName, int count, Long voteId) { + return VoteItem.builder() + .id(null) + .itemName(itemName) + .count(count) + .voteId(voteId) + .build(); + } } From f384961202d6132747588b3fe6c28b81fad4b9d6 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:19:57 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[refactor]=20:=20Business=20exception=20h?= =?UTF-8?q?andler=20=EC=88=98=EC=A0=95=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상세 error message 또한 다룰 수 있도록 코드 수정 --- .../common/exception/handler/GlobalExceptionHandler.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index 93dd6e739..9c8cef53d 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -91,9 +91,15 @@ public ResponseEntity authExceptionHandler(AuthException e) { @ExceptionHandler(BusinessException.class) public ResponseEntity businessExceptionHandler(BusinessException e) { log.error("[BusinessExceptionHandler] {}", e.getMessage()); + + // 1) cause 에 포함된 상세 메시지를 파싱, 없다면 빈 문자열로 설정 + String detail = Optional.ofNullable(e.getCause()) + .map(Throwable::getMessage) + .orElse(""); + return ResponseEntity .status(e.getErrorCode().getHttpStatus()) - .body(ErrorResponse.of(e.getErrorCode())); + .body(ErrorResponse.of(e.getErrorCode(), detail)); } // 서버 내부 오류 예외 처리 @@ -132,5 +138,4 @@ public ResponseEntity constraintViolationExceptionHandler(Constra .body(ErrorResponse.of(API_INVALID_PARAM, errorMessage)); } - } From 3d2f1513b39ebc2c81845891f49450c4b629fe60 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:22:26 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[feat]=20:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20=EC=98=81=EC=86=8D=EC=84=B1=20adapte?= =?UTF-8?q?r=20=EA=B0=9C=EB=B0=9C=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoteCommandPersistenceAdapter.java | 46 +++++++++++++++++++ .../persistence/VoteItemJpaRepository.java | 11 +++++ .../application/port/out/VoteCommandPort.java | 7 ++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 725610788..016eda89a 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -1,15 +1,61 @@ package konkuk.thip.vote.adapter.out.persistence; +import konkuk.thip.common.exception.EntityNotFoundException; +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 konkuk.thip.vote.adapter.out.jpa.VoteItemJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; +import konkuk.thip.vote.adapter.out.mapper.VoteItemMapper; import konkuk.thip.vote.adapter.out.mapper.VoteMapper; import konkuk.thip.vote.application.port.out.VoteCommandPort; +import konkuk.thip.vote.domain.Vote; +import konkuk.thip.vote.domain.VoteItem; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; + +import static konkuk.thip.common.exception.code.ErrorCode.*; + @Repository @RequiredArgsConstructor public class VoteCommandPersistenceAdapter implements VoteCommandPort { private final VoteJpaRepository voteJpaRepository; + private final VoteItemJpaRepository voteItemJpaRepository; + private final UserJpaRepository userJpaRepository; + private final RoomJpaRepository roomJpaRepository; + private final VoteMapper voteMapper; + private final VoteItemMapper voteItemMapper; + + @Override + public Long saveVote(Vote vote) { + UserJpaEntity userJpaEntity = userJpaRepository.findById(vote.getCreatorId()).orElseThrow( + () -> new EntityNotFoundException(USER_NOT_FOUND) + ); + + RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(vote.getRoomId()).orElseThrow( + () -> new EntityNotFoundException(ROOM_NOT_FOUND) + ); + + return voteJpaRepository.save(voteMapper.toJpaEntity(vote, userJpaEntity, roomJpaEntity)).getPostId(); + } + + @Override + public void saveAllVoteItems(List voteItems) { + List voteItemJpaEntities = voteItems.stream() + .map(voteItem -> { + VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteItem.getVoteId()).orElseThrow( + () -> new EntityNotFoundException(VOTE_NOT_FOUND) + ); + + return voteItemMapper.toJpaEntity(voteItem, voteJpaEntity); + }) + .toList(); + voteItemJpaRepository.saveAll(voteItemJpaEntities); + } } diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java new file mode 100644 index 000000000..3c2b86caf --- /dev/null +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteItemJpaRepository.java @@ -0,0 +1,11 @@ +package konkuk.thip.vote.adapter.out.persistence; + +import konkuk.thip.vote.adapter.out.jpa.VoteItemJpaEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface VoteItemJpaRepository extends JpaRepository { + + List findAllByVoteJpaEntity_PostId(Long voteId); +} diff --git a/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java b/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java index 16700377a..e3f6c525f 100644 --- a/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java +++ b/src/main/java/konkuk/thip/vote/application/port/out/VoteCommandPort.java @@ -1,8 +1,13 @@ package konkuk.thip.vote.application.port.out; import konkuk.thip.vote.domain.Vote; +import konkuk.thip.vote.domain.VoteItem; + +import java.util.List; public interface VoteCommandPort { - Long save(Vote vote); + Long saveVote(Vote vote); + + void saveAllVoteItems(List voteItems); } From 02a72a192e2db6069cba522ad71f194e3fc8cfed Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:22:41 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[feat]=20:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20controller=20=EA=B0=9C=EB=B0=9C=20(#?= =?UTF-8?q?37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/VoteCommandController.java | 21 ++++++++ .../adapter/in/web/request/DummyRequest.java | 7 --- .../in/web/request/VoteCreateRequest.java | 48 +++++++++++++++++++ .../in/web/response/VoteCreateResponse.java | 9 ++++ .../port/in/dto/VoteCreateCommand.java | 11 +---- 5 files changed, 80 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java create mode 100644 src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java create mode 100644 src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java index 8ebbbfaa9..2ed5025d5 100644 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/VoteCommandController.java @@ -1,10 +1,31 @@ package konkuk.thip.vote.adapter.in.web; +import jakarta.validation.Valid; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; +import konkuk.thip.vote.adapter.in.web.request.VoteCreateRequest; +import konkuk.thip.vote.adapter.in.web.response.VoteCreateResponse; +import konkuk.thip.vote.application.port.in.VoteCreateUseCase; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class VoteCommandController { + private final VoteCreateUseCase voteCreateUseCase; + + @PostMapping("/rooms/{roomId}/vote") + public BaseResponse createVote( + @UserId Long userId, + @PathVariable Long roomId, + @Valid @RequestBody VoteCreateRequest request) { + + return BaseResponse.ok(VoteCreateResponse.of( + voteCreateUseCase.createVote(request.toCommand(userId, roomId)) + )); + } } diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java b/src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java deleted file mode 100644 index c46b4260c..000000000 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/request/DummyRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.vote.adapter.in.web.request; - -import lombok.Getter; - -@Getter -public class DummyRequest { -} diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java new file mode 100644 index 000000000..6fd9c9e3e --- /dev/null +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java @@ -0,0 +1,48 @@ +package konkuk.thip.vote.adapter.in.web.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import konkuk.thip.vote.application.port.in.dto.VoteCreateCommand; + +import java.util.List; + +public record VoteCreateRequest( + @NotNull(message = "page는 필수입니다.") + Integer page, + + @NotNull(message = "isOverview(= 총평 여부)는 필수입니다.") + Boolean isOverview, + + @NotBlank(message = "투표 내용은 필수입니다.") + @Size(max = 20, message = "투표 내용은 최대 20자 입니다.") + String content, + + @NotNull(message = "투표 항목은 필수입니다.") + @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") + @Valid + List voteItemCreateRequests +) { + public record VoteItemCreateRequest( + @NotBlank(message = "투표 항목 이름은 필수입니다.") + @Size(max = 20, message = "투표 항목 이름은 최대 20자입니다.") + String itemName + ) {} + + public VoteCreateCommand toCommand(Long userId, Long roomId) { + List mappedItems = voteItemCreateRequests.stream() + .map(voteItem -> new VoteCreateCommand.VoteItemCreateCommand(voteItem.itemName)) + .toList(); + + return new VoteCreateCommand( + userId, + roomId, + page, + isOverview, + content, + mappedItems + ); + } + +} diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java b/src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java new file mode 100644 index 000000000..0f6989593 --- /dev/null +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/response/VoteCreateResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.vote.adapter.in.web.response; + +public record VoteCreateResponse( + Long voteId +) { + public static VoteCreateResponse of(Long voteId) { + return new VoteCreateResponse(voteId); + } +} diff --git a/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java b/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java index cf1f09a91..b0e5d8308 100644 --- a/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java +++ b/src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java @@ -1,12 +1,7 @@ package konkuk.thip.vote.application.port.in.dto; -import lombok.Builder; -import lombok.Getter; - import java.util.List; -@Builder -@Getter public record VoteCreateCommand( Long userId, @@ -18,9 +13,7 @@ public record VoteCreateCommand( String content, - List voteItems + List voteItemCreateCommands ) { - @Getter - @Builder - public record VoteItem(String itemName) {} + public record VoteItemCreateCommand(String itemName) {} } From d53fdb42b3d9dc28b31d0eb6ca45b9c2edf6a29d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:23:39 +0900 Subject: [PATCH 06/12] =?UTF-8?q?[refactor]=20:=20userCommandController=20?= =?UTF-8?q?request=20body=20bean=20validation=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=88=98=EC=A0=95=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @Validated -> @Valid 로 수정 --- .../thip/user/adapter/in/web/UserCommandController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index e95ab982f..55ca9a027 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -1,6 +1,7 @@ package konkuk.thip.user.adapter.in.web; import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.Oauth2Id; import konkuk.thip.common.security.util.JwtUtil; @@ -11,7 +12,6 @@ import konkuk.thip.user.application.port.in.UserSignupUseCase; import konkuk.thip.user.application.port.in.UserVerifyNicknameUseCase; import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -28,7 +28,7 @@ public class UserCommandController { private final JwtUtil jwtUtil; @PostMapping("/users/signup") - public BaseResponse signup(@Validated @RequestBody UserSignupRequest request, + public BaseResponse signup(@Valid @RequestBody UserSignupRequest request, @Oauth2Id String oauth2Id, HttpServletResponse response) { Long userId = userSignupUseCase.signup(request.toCommand(oauth2Id)); @@ -38,7 +38,7 @@ public BaseResponse signup(@Validated @RequestBody UserSignu } @PostMapping("/users/nickname") - public BaseResponse verifyNickname(@Validated @RequestBody UserVerifyNicknameRequest request) { + public BaseResponse verifyNickname(@Valid @RequestBody UserVerifyNicknameRequest request) { return BaseResponse.ok(UserVerifyNicknameResponse.of( userVerifyNicknameUseCase.isNicknameUnique(request.nickname())) ); From 6084ef8741fdfc079d071a034b78422341170b9a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:24:14 +0900 Subject: [PATCH 07/12] =?UTF-8?q?[test]=20:=20Vote=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/vote/domain/VoteTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/test/java/konkuk/thip/vote/domain/VoteTest.java diff --git a/src/test/java/konkuk/thip/vote/domain/VoteTest.java b/src/test/java/konkuk/thip/vote/domain/VoteTest.java new file mode 100644 index 000000000..49b0e1366 --- /dev/null +++ b/src/test/java/konkuk/thip/vote/domain/VoteTest.java @@ -0,0 +1,62 @@ +package konkuk.thip.vote.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.*; + +class VoteTest { + + @Test + @DisplayName("validatePage: 유효한 페이지 범위일 때, 예외가 발생하지 않는다.") + void validate_page_valid_range() { + Vote vote = Vote.withoutId("content", 1L, 10, false, 1L); + assertDoesNotThrow(() -> vote.validatePage(20)); + assertDoesNotThrow(() -> vote.validatePage(10)); // 경계값 + } + + @Test + @DisplayName("validatePage: page가 1보다 작을 때, InvalidStateException 발생한다.") + void validate_page_lower_than_zero() { + Vote vote = Vote.withoutId("content", 1L, 0, false, 1L); + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> vote.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() { + Vote vote = Vote.withoutId("content", 1L, 25, false, 1L); + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> vote.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() { + Vote vote = Vote.withoutId("content", 1L, 5, false, 1L); + assertDoesNotThrow(() -> vote.validateOverview(20)); + } + + @Test + @DisplayName("validateOverview: 진행률 80% 이상이고 isOverview=true 이면, 예외가 발생하지 않는다.") + void validate_overview_ratio_at_least_80_percent() { + Vote vote = Vote.withoutId("content", 1L, 16, true, 1L); + assertDoesNotThrow(() -> vote.validateOverview(20)); + } + + @Test + @DisplayName("validateOverview: 진행률 80% 미만이고 isOverview=true 이면, InvalidStateException 발생한다.") + void validate_overview_ratio_below_80_percent() { + Vote vote = Vote.withoutId("content", 1L, 15, true, 1L); // 15/20 = 0.75 + InvalidStateException ex = assertThrows(InvalidStateException.class, + () -> vote.validateOverview(20)); + assertInstanceOf(IllegalStateException.class, ex.getCause()); + assertTrue(ex.getCause().getMessage().contains("현재 진행률 = 75.00% (15/20)")); + } +} From d3aa352c4f0c83318fbb3800d1e1133a3b857f40 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:24:35 +0900 Subject: [PATCH 08/12] =?UTF-8?q?[test]=20:=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20controller=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/VoteCreateControllerTest.java | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java 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 new file mode 100644 index 000000000..6a87b3ed4 --- /dev/null +++ b/src/test/java/konkuk/thip/vote/adapter/in/web/VoteCreateControllerTest.java @@ -0,0 +1,353 @@ +package konkuk.thip.vote.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.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.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import konkuk.thip.vote.adapter.in.web.request.VoteCreateRequest; +import konkuk.thip.vote.adapter.out.jpa.VoteItemJpaEntity; +import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity; +import konkuk.thip.vote.adapter.out.persistence.VoteItemJpaRepository; +import konkuk.thip.vote.adapter.out.persistence.VoteJpaRepository; +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.time.LocalDate; +import java.util.List; +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) +class VoteCreateControllerTest { + + @Autowired + private 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 VoteJpaRepository voteJpaRepository; + + @Autowired + private VoteItemJpaRepository voteItemJpaRepository; + + @AfterEach + void tearDown() { + voteItemJpaRepository.deleteAll(); + voteJpaRepository.deleteAll(); + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + private void saveUserAndRoom() { + AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() + .value("책벌레") + .color("blue") + .imageUrl("http://image.url") + .build()); + + UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder() + .oauth2Id("kakao_432708231") + .nickname("User1") + .imageUrl("https://avatar1.jpg") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()); + + BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() + .title("작별하지 않는다") + .isbn("9788954682152") + .authorName("한강") + .bestSeller(false) + .publisher("문학동네") + .imageUrl("https://image1.jpg") + .pageCount(300) + .description("한강의 소설") + .build()); + + + CategoryJpaEntity category = categoryJpaRepository.save(CategoryJpaEntity.builder() + .value("소설") + .aliasForCategoryJpaEntity(alias) + .build()); + + RoomJpaEntity room = roomJpaRepository.save(RoomJpaEntity.builder() + .title("한강 독서모임") + .description("한강 작품 읽기 모임") + .isPublic(true) + .roomPercentage(0.0) + .startDate(LocalDate.now().plusDays(2)) + .endDate(LocalDate.now().plusDays(30)) + .recruitCount(10) + .bookJpaEntity(book) + .categoryJpaEntity(category) + .build()); + } + + @Test + @DisplayName("[페이지 넘버, 총평 여부, 투표 내용, List<투표 항목>] 을 받아, 투표를 생성한다.") + void vote_create_success() throws Exception { + //given : user, room, request 생성 + saveUserAndRoom(); + + int page = 10; + boolean isOverview = false; + String content = "투표 내용 입니다."; + List voteItems = List.of( + new VoteCreateRequest.VoteItemCreateRequest("찬성"), + new VoteCreateRequest.VoteItemCreateRequest("반대") + ); + + VoteCreateRequest request = new VoteCreateRequest( + page, isOverview, content, voteItems + ); + + Long userId = userJpaRepository.findAll().get(0).getUserId(); + Long roomId = roomJpaRepository.findAll().get(0).getRoomId(); + + //when : 투표 생성 api 호출 (filter 통과 없이) + ResultActions result = mockMvc.perform(post("/rooms/{roomId}/vote", roomId) + .requestAttr("userId", userId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request) + )); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.voteId").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + Long voteId = jsonNode.path("data").path("voteId").asLong(); + + VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteId).orElse(null); + List voteItemJpaEntityList = voteItemJpaRepository.findAllByVoteJpaEntity_PostId(voteJpaEntity.getPostId()); + + assertThat(voteJpaEntity.getUserJpaEntity().getUserId()).isEqualTo(userId); + assertThat(voteJpaEntity.getRoomJpaEntity().getRoomId()).isEqualTo(roomId); + assertThat(voteJpaEntity.getPage()).isEqualTo(page); + assertThat(voteJpaEntity.getContent()).isEqualTo(content); + + assertThat(voteItemJpaEntityList).hasSize(2) + .extracting(VoteItemJpaEntity::getItemName) + .containsExactlyInAnyOrder("찬성", "반대"); + } + + @Test + @DisplayName("[page]가 누락되었을 때 400 Bad Request 반환") + void vote_create_page_null() throws Exception { + // given: page 누락 + Map request = Map.of( + "isOverview", false, + "content", "내용", + "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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 vote_create_is_over_view_null() throws Exception { + // given: isOverview 누락 + Map request = Map.of( + "page", 1, + "content", "내용", + "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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 vote_create_content_blank() throws Exception { + // given + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "", + "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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]가 20자 초과일 때 400 Bad Request 반환") + void vote_create_content_too_long() throws Exception { + // given + String longContent = "가".repeat(21); + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", longContent, + "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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("투표 내용은 최대 20자 입니다."))); + } + + @Test + @DisplayName("[voteItemCreateRequests]가 누락되었을 때 400 Bad Request 반환") + void vote_create_vote_item_null() throws Exception { + // given + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "내용" + // voteItemCreateRequests 생략 + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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("[voteItemCreateRequests]가 5개 초과일 때 400 Bad Request 반환") + void vote_create_vote_item_too_many() throws Exception { + // given: 6개 아이템 + List> items = List.of( + Map.of("itemName","A"), Map.of("itemName","B"), + Map.of("itemName","C"), Map.of("itemName","D"), + Map.of("itemName","E"), Map.of("itemName","F") + ); + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "내용", + "voteItemCreateRequests", items + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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("투표 항목은 1개 이상, 최대 5개까지입니다."))); + } + + @Test + @DisplayName("[voteItemCreateRequests] 내 [itemName]이 빈 문자열일 때 400 Bad Request 반환") + void vote_create_vote_item_name_blank() throws Exception { + // given + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "내용", + "voteItemCreateRequests", List.of(Map.of("itemName","")) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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("[voteItemCreateRequests] 내 [itemName]이 20자 초과일 때 400 Bad Request 반환") + void vote_create_vote_item_name_too_long() throws Exception { + // given + String longName = "가".repeat(21); + Map request = Map.of( + "page", 1, + "isOverview", false, + "content", "내용", + "voteItemCreateRequests", List.of(Map.of("itemName", longName)) + ); + + // when & then + mockMvc.perform(post("/rooms/{roomId}/vote", 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("투표 항목 이름은 최대 20자입니다."))); + } +} \ No newline at end of file From 1f807d677e152f08c002f7038c4cd37aa98cd23f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Fri, 4 Jul 2025 02:58:07 +0900 Subject: [PATCH 09/12] =?UTF-8?q?[refactor]=20error=20code=20message=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f527053e3..930705b97 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -65,7 +65,7 @@ public enum ErrorCode implements ResponseCode { * 110000 : vote error */ VOTE_NOT_FOUND(HttpStatus.NOT_FOUND, 110000, "존재하지 않는 VOTE 입니다."), - VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다. 총평은 진행률이 80% 이상이어야 가능합니다."), + VOTE_CANNOT_BE_OVERVIEW(HttpStatus.BAD_REQUEST, 110001, "총평이 될 수 없는 VOTE 입니다."), INVALID_VOTE_PAGE_RANGE(HttpStatus.BAD_REQUEST, 110002, "VOTE의 page 값이 유효하지 않습니다.") ; From 06d220081ab284f966d527dc62872c65bbe2c384 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 5 Jul 2025 18:42:09 +0900 Subject: [PATCH 10/12] =?UTF-8?q?[refactor]=20Service=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/{VoteService.java => VoteCreateService.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/konkuk/thip/vote/application/service/{VoteService.java => VoteCreateService.java} (97%) diff --git a/src/main/java/konkuk/thip/vote/application/service/VoteService.java b/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java similarity index 97% rename from src/main/java/konkuk/thip/vote/application/service/VoteService.java rename to src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java index 6b2658863..22b7c38e2 100644 --- a/src/main/java/konkuk/thip/vote/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/vote/application/service/VoteCreateService.java @@ -19,7 +19,7 @@ @Service @RequiredArgsConstructor @Slf4j -public class VoteService implements VoteCreateUseCase { +public class VoteCreateService implements VoteCreateUseCase { private final VoteCommandPort voteCommandPort; private final RoomCommandPort roomCommandPort; From 6705b6d134a6424763ae3e6dd242dbf938d52bff Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 5 Jul 2025 18:52:48 +0900 Subject: [PATCH 11/12] =?UTF-8?q?[refactor]=20request=20dto=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20attribute=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api 명세서 반영해서 네이밍 수정 --- .../in/web/request/VoteCreateRequest.java | 4 ++-- .../in/web/VoteCreateControllerTest.java | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java index 6fd9c9e3e..425f51517 100644 --- a/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java +++ b/src/main/java/konkuk/thip/vote/adapter/in/web/request/VoteCreateRequest.java @@ -22,7 +22,7 @@ public record VoteCreateRequest( @NotNull(message = "투표 항목은 필수입니다.") @Size(min = 1, max = 5, message = "투표 항목은 1개 이상, 최대 5개까지입니다.") @Valid - List voteItemCreateRequests + List voteItemList ) { public record VoteItemCreateRequest( @NotBlank(message = "투표 항목 이름은 필수입니다.") @@ -31,7 +31,7 @@ public record VoteItemCreateRequest( ) {} public VoteCreateCommand toCommand(Long userId, Long roomId) { - List mappedItems = voteItemCreateRequests.stream() + List mappedItems = voteItemList.stream() .map(voteItem -> new VoteCreateCommand.VoteItemCreateCommand(voteItem.itemName)) .toList(); 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 6a87b3ed4..76d4c7648 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 @@ -185,7 +185,7 @@ void vote_create_page_null() throws Exception { Map request = Map.of( "isOverview", false, "content", "내용", - "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + "voteItemList", List.of(Map.of("itemName", "찬성")) ); // when & then @@ -205,7 +205,7 @@ void vote_create_is_over_view_null() throws Exception { Map request = Map.of( "page", 1, "content", "내용", - "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + "voteItemList", List.of(Map.of("itemName", "찬성")) ); // when & then @@ -226,7 +226,7 @@ void vote_create_content_blank() throws Exception { "page", 1, "isOverview", false, "content", "", - "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + "voteItemList", List.of(Map.of("itemName", "찬성")) ); // when & then @@ -248,7 +248,7 @@ void vote_create_content_too_long() throws Exception { "page", 1, "isOverview", false, "content", longContent, - "voteItemCreateRequests", List.of(Map.of("itemName", "찬성")) + "voteItemList", List.of(Map.of("itemName", "찬성")) ); // when & then @@ -262,14 +262,14 @@ void vote_create_content_too_long() throws Exception { } @Test - @DisplayName("[voteItemCreateRequests]가 누락되었을 때 400 Bad Request 반환") + @DisplayName("[voteItemList]가 누락되었을 때 400 Bad Request 반환") void vote_create_vote_item_null() throws Exception { // given Map request = Map.of( "page", 1, "isOverview", false, "content", "내용" - // voteItemCreateRequests 생략 + // voteItemList 생략 ); // when & then @@ -283,7 +283,7 @@ void vote_create_vote_item_null() throws Exception { } @Test - @DisplayName("[voteItemCreateRequests]가 5개 초과일 때 400 Bad Request 반환") + @DisplayName("[voteItemList]가 5개 초과일 때 400 Bad Request 반환") void vote_create_vote_item_too_many() throws Exception { // given: 6개 아이템 List> items = List.of( @@ -295,7 +295,7 @@ void vote_create_vote_item_too_many() throws Exception { "page", 1, "isOverview", false, "content", "내용", - "voteItemCreateRequests", items + "voteItemList", items ); // when & then @@ -309,14 +309,14 @@ void vote_create_vote_item_too_many() throws Exception { } @Test - @DisplayName("[voteItemCreateRequests] 내 [itemName]이 빈 문자열일 때 400 Bad Request 반환") + @DisplayName("[voteItemList] 내 [itemName]이 빈 문자열일 때 400 Bad Request 반환") void vote_create_vote_item_name_blank() throws Exception { // given Map request = Map.of( "page", 1, "isOverview", false, "content", "내용", - "voteItemCreateRequests", List.of(Map.of("itemName","")) + "voteItemList", List.of(Map.of("itemName","")) ); // when & then @@ -330,7 +330,7 @@ void vote_create_vote_item_name_blank() throws Exception { } @Test - @DisplayName("[voteItemCreateRequests] 내 [itemName]이 20자 초과일 때 400 Bad Request 반환") + @DisplayName("[voteItemList] 내 [itemName]이 20자 초과일 때 400 Bad Request 반환") void vote_create_vote_item_name_too_long() throws Exception { // given String longName = "가".repeat(21); @@ -338,7 +338,7 @@ void vote_create_vote_item_name_too_long() throws Exception { "page", 1, "isOverview", false, "content", "내용", - "voteItemCreateRequests", List.of(Map.of("itemName", longName)) + "voteItemList", List.of(Map.of("itemName", longName)) ); // when & then From 1a01f13f9c815d430498343e56a6320a55a8635a Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sat, 5 Jul 2025 18:56:22 +0900 Subject: [PATCH 12/12] =?UTF-8?q?[refactor]=20VoteItem=20List=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EC=8B=9C,=20Vote=20=ED=95=9C=EB=B2=88=EB=A7=8C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoteCommandPersistenceAdapter.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 016eda89a..f38b2c0bd 100644 --- a/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/vote/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -46,14 +46,15 @@ public Long saveVote(Vote vote) { @Override public void saveAllVoteItems(List voteItems) { - List voteItemJpaEntities = voteItems.stream() - .map(voteItem -> { - VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteItem.getVoteId()).orElseThrow( - () -> new EntityNotFoundException(VOTE_NOT_FOUND) - ); + if (voteItems.isEmpty()) return; + + Long voteId = voteItems.get(0).getVoteId(); + VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteId).orElseThrow( + () -> new EntityNotFoundException(VOTE_NOT_FOUND) + ); - return voteItemMapper.toJpaEntity(voteItem, voteJpaEntity); - }) + List voteItemJpaEntities = voteItems.stream() + .map(voteItem -> voteItemMapper.toJpaEntity(voteItem, voteJpaEntity)) .toList(); voteItemJpaRepository.saveAll(voteItemJpaEntities);