Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ public Long save(Book book) {
BookJpaEntity bookJpaEntity = bookMapper.toJpaEntity(book);
return bookJpaRepository.save(bookJpaEntity).getBookId();
}

@Override
public Book findById(Long id) {
BookJpaEntity bookJpaEntity = bookJpaRepository.findById(id).orElseThrow(
() -> new EntityNotFoundException(BOOK_NOT_FOUND)
);

return bookMapper.toDomainEntity(bookJpaEntity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import konkuk.thip.book.domain.Book;

import java.util.Optional;

public interface BookCommandPort {

Book findByIsbn(String isbn);

Long save(Book book);

Book findById(Long id);
}
16 changes: 14 additions & 2 deletions src/main/java/konkuk/thip/common/exception/code/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,25 @@ public enum ErrorCode implements ResponseCode {
BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."),
BOOK_NOT_SAVED_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."),

/**
* 90000 : recentSearch error
*/
INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, 900000,"알맞은 검색어 타입을 찾을 수 없습니다."),


/**
* 90000 : recentSearch error
* 100000 : room error
*/
ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 100000, "존재하지 않는 ROOM 입니다."),

/**
* 110000 : vote error
*/
INVALID_SEARCH_TYPE(HttpStatus.BAD_REQUEST, 900000,"알맞은 검색어 타입을 찾을 수 없습니다.");
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 값이 유효하지 않습니다.")

;

private final HttpStatus httpStatus;
private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,15 @@ public ResponseEntity<ErrorResponse> authExceptionHandler(AuthException e) {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> businessExceptionHandler(BusinessException e) {
log.error("[BusinessExceptionHandler] {}", e.getMessage());

// 1) cause 에 포함된 상세 메시지를 파싱, 없다면 빈 문자열로 설정
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤩

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));
}

// 서버 내부 오류 예외 처리
Expand Down Expand Up @@ -132,5 +138,4 @@ public ResponseEntity<ErrorResponse> constraintViolationExceptionHandler(Constra
.body(ErrorResponse.of(API_INVALID_PARAM, errorMessage));
}


}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package konkuk.thip.room.application.port.out;

import konkuk.thip.room.domain.Room;

public interface RoomCommandPort {

Room findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -28,7 +28,7 @@ public class UserCommandController {
private final JwtUtil jwtUtil;

@PostMapping("/users/signup")
public BaseResponse<UserSignupResponse> signup(@Validated @RequestBody UserSignupRequest request,
public BaseResponse<UserSignupResponse> signup(@Valid @RequestBody UserSignupRequest request,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 👍🏻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

붐붐따리붐붐따 붐업

@Oauth2Id String oauth2Id,
HttpServletResponse response) {
Long userId = userSignupUseCase.signup(request.toCommand(oauth2Id));
Expand All @@ -38,7 +38,7 @@ public BaseResponse<UserSignupResponse> signup(@Validated @RequestBody UserSignu
}

@PostMapping("/users/nickname")
public BaseResponse<UserVerifyNicknameResponse> verifyNickname(@Validated @RequestBody UserVerifyNicknameRequest request) {
public BaseResponse<UserVerifyNicknameResponse> verifyNickname(@Valid @RequestBody UserVerifyNicknameRequest request) {
return BaseResponse.ok(UserVerifyNicknameResponse.of(
userVerifyNicknameUseCase.isNicknameUnique(request.nickname()))
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VoteCreateResponse> createVote(
@UserId Long userId,
@PathVariable Long roomId,
@Valid @RequestBody VoteCreateRequest request) {

return BaseResponse.ok(VoteCreateResponse.of(
voteCreateUseCase.createVote(request.toCommand(userId, roomId))
));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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개까지입니다.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 "투표 항목은 1개 이상, 최대 5개까지이다"라는 요구사항은 도메인 규칙에 해당하는 것 같긴한데, 어떻게 생각하시나요..? 변동될 수 있는 규칙이라 도메인에서 검증하는 것이 더 유지보수하는게 좋을 것 같긴한데 따지고보면 다른 파라미터들도 다 비슷한 거 같아서.. 애매하네요 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 현준님이 리뷰 남겨주신 것처럼 "투표 항목은 1개 이상, 5개 이하만 가능하다" 라는 요구사항을 투표 생성 api 구현시에 지켜야 하는 도메인 규칙이라고 생각합니다!
하지만 투표 생성 api 의 도메인 계층까지 가지않고, request body 에서도 충분히 검증할 수 있는 validation 이라 생각해서 request body 내부에서 validation 을 수행하도록 구현하였습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 확인했습니다! 뭐 투표를 생성하는 로직이 다른 api에 사용하는 것은 아니니 상관없을 것 같네요! 만약 추후에 투표를 생성하는 로직이 이 api가 아닌 다른 api에서도 필요해진다면 중복 코드를 방지하기 위해 도메인 계층으로 옮기는 것도 좋을 것 같아요~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 동의합니다!

@Valid
List<VoteItemCreateRequest> voteItemList
) {
public record VoteItemCreateRequest(
@NotBlank(message = "투표 항목 이름은 필수입니다.")
@Size(max = 20, message = "투표 항목 이름은 최대 20자입니다.")
String itemName
) {}

public VoteCreateCommand toCommand(Long userId, Long roomId) {
List<VoteCreateCommand.VoteItemCreateCommand> mappedItems = voteItemList.stream()
.map(voteItem -> new VoteCreateCommand.VoteItemCreateCommand(voteItem.itemName))
.toList();

return new VoteCreateCommand(
userId,
roomId,
page,
isOverview,
content,
mappedItems
);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,62 @@
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<VoteItem> voteItems) {
if (voteItems.isEmpty()) return;

Long voteId = voteItems.get(0).getVoteId();
VoteJpaEntity voteJpaEntity = voteJpaRepository.findById(voteId).orElseThrow(
() -> new EntityNotFoundException(VOTE_NOT_FOUND)
);

List<VoteItemJpaEntity> voteItemJpaEntities = voteItems.stream()
.map(voteItem -> voteItemMapper.toJpaEntity(voteItem, voteJpaEntity))
.toList();

voteItemJpaRepository.saveAll(voteItemJpaEntities);
}
}
Original file line number Diff line number Diff line change
@@ -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<VoteItemJpaEntity, Long> {

List<VoteItemJpaEntity> findAllByVoteJpaEntity_PostId(Long voteId);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package konkuk.thip.vote.application.port.in.dto;

import java.util.List;

public record VoteCreateCommand(
Long userId,

Long roomId,

int page,

boolean isOverview,

String content,

List<VoteItemCreateCommand> voteItemCreateCommands
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

null safety 고려 필요

voteItemCreateCommands 리스트가 null일 경우를 대비한 방어 로직이 필요할 수 있습니다. 서비스 레이어에서 이를 처리하고 있는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# voteItemCreateCommands null 체크가 서비스 레이어에서 이루어지는지 확인
ast-grep --pattern 'command.voteItemCreateCommands()' -A 5

Length of output: 741


#!/bin/bash
# VoteCreateCommand 클래스 내용 확인
cat src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java

다음으로 VoteCreateCommand DTO에서 voteItemCreateCommands 필드 정의와 null 처리 여부를 확인합니다:


🏁 Script executed:

#!/bin/bash
# VoteCreateCommand 클래스 내용 확인
cat src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java

Length of output: 433


null 안전성 강화 필요

VoteCreateCommandvoteItemCreateCommands가 null일 경우 서비스 레이어에서 바로 .stream()을 호출해 NPE가 발생할 수 있으므로, 다음 파일에 방어 로직을 추가해야 합니다.

  • src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java
    레코드 정의에 Objects.requireNonNull 검증 추가

     public record VoteCreateCommand(
             Long userId,
             Long roomId,
             int page,
             boolean isOverview,
             String content,
             List<VoteItemCreateCommand> voteItemCreateCommands
     ) {
    +    public VoteCreateCommand {
    +        Objects.requireNonNull(voteItemCreateCommands, "voteItemCreateCommands must not be null");
    +    }
    
         public record VoteItemCreateCommand(String itemName) {}
     }
  • src/main/java/konkuk/thip/vote/application/service/VoteService.java
    서비스 호출부에서 null을 빈 리스트로 대체

     List<VoteItem> voteItems = 
    -    command.voteItemCreateCommands().stream()
    +    Optional.ofNullable(command.voteItemCreateCommands())
    +            .orElseGet(List::of)
    +            .stream()
             .map(itemCmd -> VoteItem.withoutId(
                 itemCmd.itemName(),
                 0,
                 savedVoteId
             ))

위 두 군데에 null 체크를 추가해 NPE를 방지하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<VoteItemCreateCommand> voteItemCreateCommands
// File: src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java
public record VoteCreateCommand(
Long userId,
Long roomId,
int page,
boolean isOverview,
String content,
List<VoteItemCreateCommand> voteItemCreateCommands
) {
public VoteCreateCommand {
Objects.requireNonNull(voteItemCreateCommands, "voteItemCreateCommands must not be null");
}
public record VoteItemCreateCommand(String itemName) {}
}
Suggested change
List<VoteItemCreateCommand> voteItemCreateCommands
// File: src/main/java/konkuk/thip/vote/application/service/VoteService.java
List<VoteItem> voteItems =
Optional.ofNullable(command.voteItemCreateCommands())
.orElseGet(List::of)
.stream()
.map(itemCmd -> VoteItem.withoutId(
itemCmd.itemName(),
0,
savedVoteId
))
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/vote/application/port/in/dto/VoteCreateCommand.java
at line 16, add a null check using Objects.requireNonNull for the
voteItemCreateCommands field in the record constructor to prevent null values.
Additionally, in
src/main/java/konkuk/thip/vote/application/service/VoteService.java, update the
service method that calls VoteCreateCommand to replace any null
voteItemCreateCommands with an empty list before processing, ensuring no
NullPointerException occurs when calling stream().

) {
public record VoteItemCreateCommand(String itemName) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +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 saveVote(Vote vote);

void saveAllVoteItems(List<VoteItem> voteItems);
}
Loading