Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

채팅방 생성 api 추가 #234

Merged
merged 25 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6fb3d89
feat: 경매 아이디를 포함하는 채팅방 조회하는 레포지토리 메서드 추가
kwonyj1022 Aug 8, 2023
21ade4c
feat: 낙찰자를 판단하는 메서드와 인자로 받은 사용자가 낙찰자인지 판단하는 메서드 추가
kwonyj1022 Aug 8, 2023
5dfba44
feat: 채팅방 생성 서비스 추가
kwonyj1022 Aug 8, 2023
537de24
feat: 채팅방 생성 컨트롤러 추가
kwonyj1022 Aug 8, 2023
1c8ba25
feat: 커스텀 예외 핸들러 추가
kwonyj1022 Aug 8, 2023
c72d0ae
feat: 서비스레이어에서 데이터 삽입 시 `@Transactional`의 readOnly 옵션을 false로 지정
kwonyj1022 Aug 8, 2023
db1f7e8
refactor: 낙찰자를 판단할 때 도메인에서 예외처리하지 않고 Optional로 반환하도록 변경
kwonyj1022 Aug 9, 2023
927a6c2
refactor: if문 안에 여러개의 조건 메서드 분리
kwonyj1022 Aug 9, 2023
9e23903
refactor: db에 존재하는지 파악할 때 find 대신 exists 쓰도록 변경
kwonyj1022 Aug 9, 2023
c8dabf1
refactor: service에 있던 domain -> dto로 변환하는 메서드 대신 dto의 정적 팩토리 메서드를 사용하…
kwonyj1022 Aug 10, 2023
0dcf9d8
test: 누락된 final 추가 및 double임을 표현할 수 있도록 소수 뒤에 d 추가
kwonyj1022 Aug 10, 2023
f8cab38
test: id만 비교할 시 id를 통한 비교 대신 equals 사용하도록 수정
kwonyj1022 Aug 10, 2023
6402af0
feat: 채팅방 생성 요청시 채팅방이 이미 있는 경우 그 채팅방을 반환하도록 기능 추가
kwonyj1022 Aug 10, 2023
8e8eaf1
test: 낙찰자를 찾을 수 없을 때 발생하는 예외 핸들링 테스트 추가
kwonyj1022 Aug 10, 2023
8099e62
refactor: 채팅방이 이미 존재하는 경우에 대한 불필요한 로직 및 예외 제거
kwonyj1022 Aug 10, 2023
6b953d2
test: 누락된 테스트 추가
kwonyj1022 Aug 10, 2023
ea2f171
refactor: merge로 인한 충돌 해결
kwonyj1022 Aug 10, 2023
2930458
refactor: 복잡하게 작성된 코드 간결하게 수정
kwonyj1022 Aug 10, 2023
ecc4f9d
refactor: 메서드명 수정 및 불필요한 개행 삭제
kwonyj1022 Aug 10, 2023
473695d
feat: querydsl을 사용해 채팅방 조회시 성능 개선 및 최신순 정렬 기능 추가
kwonyj1022 Aug 10, 2023
da6c18f
feat: 사용자 조회와 동시에 예외처리하도록 변경
kwonyj1022 Aug 10, 2023
8df6a77
Merge branch 'develop-be' into feature/228
kwonyj1022 Aug 10, 2023
b4fcd7e
fix: merge로 인한 충돌 해결
kwonyj1022 Aug 10, 2023
3dd1da0
Merge branch 'develop-be' into feature/228
kwonyj1022 Aug 10, 2023
acbb62f
refactor: 사용하지 않는 메서드 삭제
kwonyj1022 Aug 10, 2023
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 @@ -31,6 +31,7 @@
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down Expand Up @@ -147,4 +148,21 @@ private BidPrice calculateNextMinimumBidPrice() {
final int nextMinimumBidPrice = this.lastBid.getPrice().getValue() + this.bidUnit.getValue();
return new BidPrice(nextMinimumBidPrice);
}

public boolean isWinner(final User user, final LocalDateTime targetTime) {
return findWinner(targetTime).filter(user::equals)
.isPresent();
}

public Optional<User> findWinner(final LocalDateTime targetTime) {
if (isWinnerExist(targetTime)) {
return Optional.of(lastBid.getBidder());
}

return Optional.empty();
}

private boolean isWinnerExist(final LocalDateTime targetTime) {
return auctioneerCount != 0 && isClosed(targetTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ddang.ddang.auction.domain.exception;

public class WinnerNotFoundException extends IllegalArgumentException {

public WinnerNotFoundException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.ddang.ddang.chat.application;

import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException;
import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository;
import com.ddang.ddang.chat.application.dto.CreateChatRoomDto;
import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto;
import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException;
import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException;
import com.ddang.ddang.chat.application.exception.UserNotAccessibleException;
import com.ddang.ddang.chat.domain.ChatRoom;
import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository;
Expand All @@ -22,23 +28,69 @@ public class ChatRoomService {

private final JpaChatRoomRepository chatRoomRepository;
private final JpaUserRepository userRepository;
private final JpaAuctionRepository auctionRepository;

@Transactional
public Long create(final Long userId, final CreateChatRoomDto chatRoomDto) {
final User findUser = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다."));
final Auction findAuction = auctionRepository.findById(chatRoomDto.auctionId())
.orElseThrow(() ->
new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."));

final ChatRoom persistChatRoom = findOrCreateChatRoomByAuction(findUser, findAuction);

return persistChatRoom.getId();
}

private ChatRoom findOrCreateChatRoomByAuction(final User user, final Auction auction) {
return chatRoomRepository.findByAuctionId(auction.getId())
.orElseGet(() -> createAndSaveChatRoom(user, auction));
}

private ChatRoom createAndSaveChatRoom(final User user, final Auction auction) {
checkAuctionStatus(auction);
final User winner = auction.findWinner(LocalDateTime.now())
.orElseThrow(() -> new WinnerNotFoundException("낙찰자가 존재하지 않습니다"));
checkUserCanParticipate(user, auction);

final ChatRoom chatRoom = new ChatRoom(auction, winner);

return chatRoomRepository.save(chatRoom);
}

private void checkAuctionStatus(final Auction findAuction) {
if (!findAuction.isClosed(LocalDateTime.now())) {
throw new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다.");
}
if (findAuction.isDeleted()) {
throw new InvalidAuctionToChatException("삭제된 경매입니다.");
Comment on lines +62 to +67
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기에서는 서비스에서 예외를 처리해주고 있었네요

Auction.checkWinnerExist()랑 이거랑 통일성을 지켜주는 방향은 어떨까 건의드려봅니다

}
}

private void checkUserCanParticipate(final User findUser, final Auction findAuction) {
if (!isSellerOrWinner(findUser, findAuction)) {
throw new UserNotAccessibleException("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다.");
}
}

private boolean isSellerOrWinner(final User findUser, final Auction findAuction) {
return findAuction.isOwner(findUser) || findAuction.isWinner(findUser, LocalDateTime.now());
}

public List<ReadParticipatingChatRoomDto> readAllByUserId(final Long userId) {
final User findUser = findUser(userId);
final User findUser = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다."));
final List<ChatRoom> chatRooms = chatRoomRepository.findAllByUserId(findUser.getId());

return chatRooms.stream()
.map(chatRoom -> toDto(findUser, chatRoom))
.map(chatRoom -> ReadParticipatingChatRoomDto.of(findUser, chatRoom, LocalDateTime.now()))
.toList();
}

private User findUser(final Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다."));
}

public ReadParticipatingChatRoomDto readByChatRoomId(final Long chatRoomId, final Long userId) {
final User findUser = findUser(userId);
final User findUser = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자 정보를 찾을 수 없습니다."));
final ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
Copy link
Member

Choose a reason for hiding this comment

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

해당 부분과 관련해 갑자기 궁금해진 부분이 있어 여쭤봅니다.
채팅룸을 조회한 후 바로 판매자와 구매자인지에 대해 확인하는 메서드가 존재합니다.
이 경우 판매자는 auction에 존재하기에 auction을 조회하는 쿼리를 날려야 됩니다.
그래서 차라리 fetch join을 통해 채팅룸을 조회할 때 auction도 함께 가져오도록 하면 어떨지 의견을 묻고 싶습니다.

.orElseThrow(() ->
new ChatRoomNotFoundException(
Expand All @@ -47,19 +99,12 @@ public ReadParticipatingChatRoomDto readByChatRoomId(final Long chatRoomId, fina
);
checkAccessible(findUser, chatRoom);

return toDto(findUser, chatRoom);
return ReadParticipatingChatRoomDto.of(findUser, chatRoom, LocalDateTime.now());
}

private void checkAccessible(final User findUser, final ChatRoom chatRoom) {
if (!chatRoom.isParticipant(findUser)) {
throw new UserNotAccessibleException("해당 채팅방에 접근할 권한이 없습니다.");
}
}

private ReadParticipatingChatRoomDto toDto(final User findUser, final ChatRoom chatRoom) {
return ReadParticipatingChatRoomDto.of(
chatRoom.calculateChatPartnerOf(findUser),
chatRoom,
chatRoom.isChatAvailableTime(LocalDateTime.now()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ddang.ddang.chat.application.dto;

import com.ddang.ddang.chat.presentation.dto.request.CreateChatRoomRequest;

public record CreateChatRoomDto(Long auctionId) {

public static CreateChatRoomDto from(final CreateChatRoomRequest chatRoomRequest) {
return new CreateChatRoomDto(chatRoomRequest.auctionId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.ddang.ddang.chat.domain.ChatRoom;
import com.ddang.ddang.user.domain.User;

import java.time.LocalDateTime;

public record ReadParticipatingChatRoomDto(
Long id,
ReadAuctionDto auctionDto,
Expand All @@ -11,15 +13,17 @@ public record ReadParticipatingChatRoomDto(
) {

public static ReadParticipatingChatRoomDto of(
final User partner,
final User findUser,
final ChatRoom chatRoom,
final boolean isChatAvailable
final LocalDateTime targetTime
) {
final User partner = chatRoom.calculateChatPartnerOf(findUser);

return new ReadParticipatingChatRoomDto(
chatRoom.getId(),
ReadAuctionDto.from(chatRoom.getAuction()),
ReadUserDto.from(partner),
isChatAvailable
chatRoom.isChatAvailableTime(targetTime)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ddang.ddang.chat.application.exception;

public class InvalidAuctionToChatException extends IllegalArgumentException {

public InvalidAuctionToChatException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@

import com.ddang.ddang.chat.domain.ChatRoom;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
public interface JpaChatRoomRepository extends JpaRepository<ChatRoom, Long>, QuerydslChatRoomRepository {

public interface JpaChatRoomRepository extends JpaRepository<ChatRoom, Long> {

@Query("select c from ChatRoom c where c.auction.seller.id = :userId or c.buyer.id = :userId")
List<ChatRoom> findAllByUserId(final Long userId);
boolean existsByAuctionId(final Long auctionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ddang.ddang.chat.infrastructure.persistence;

import com.ddang.ddang.chat.domain.ChatRoom;

import java.util.List;
import java.util.Optional;

public interface QuerydslChatRoomRepository {

List<ChatRoom> findAllByUserId(final Long userId);

Optional<ChatRoom> findByAuctionId(final Long auctionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.ddang.ddang.chat.infrastructure.persistence;

import com.ddang.ddang.chat.domain.ChatRoom;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

import static com.ddang.ddang.auction.domain.QAuction.auction;
import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom;

@Repository
@RequiredArgsConstructor
public class QuerydslChatRoomRepositoryImpl implements QuerydslChatRoomRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<ChatRoom> findAllByUserId(final Long userId) {
return queryFactory.selectFrom(chatRoom)
.leftJoin(chatRoom.auction, auction).fetchJoin()
.where(isSellerOrWinner(userId))
.orderBy(chatRoom.id.desc())
.fetch();
}
Comment on lines +22 to +28
Copy link
Member

Choose a reason for hiding this comment

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

목록을 조회할 때도 auction관련 검증이나 필요한 정보가 있는지 궁금합니다.
api만 봤을 때는 필요가 없어 보이는데 fetch join한 이유가 궁금합니다.
제가 놓친 코드가 있는 걸까요?

Copy link
Member

Choose a reason for hiding this comment

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

아! 동일한 dto를 사용하기 위함일까요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

chatRoom에는 seller가 없고 buyer만 존재하기 때문에 auction에 있는 seller까지 함께 가져오기 위해 fetchJoin을 사용했습니다!

Copy link
Member

Choose a reason for hiding this comment

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

앗 제가 chatRoom에 seller가 없는 것을 잠시 간과했네요..!
확인했습니다. 답변 감사합니다!


private BooleanExpression isSellerOrWinner(final Long userId) {
return (auction.seller.id.eq(userId))
.or(chatRoom.buyer.id.eq(userId));
}

@Override
public Optional<ChatRoom> findByAuctionId(final Long auctionId) {
final ChatRoom findChatRoom = queryFactory.selectFrom(chatRoom)
.leftJoin(chatRoom.auction, auction).fetchJoin()
.where(auction.id.eq(auctionId))
.fetchOne();

return Optional.ofNullable(findChatRoom);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import com.ddang.ddang.chat.application.ChatRoomService;
import com.ddang.ddang.chat.application.MessageService;
import com.ddang.ddang.chat.application.dto.CreateChatRoomDto;
import com.ddang.ddang.chat.application.dto.CreateMessageDto;
import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto;
import com.ddang.ddang.chat.presentation.auth.AuthenticateUser;
import com.ddang.ddang.chat.presentation.auth.AuthenticateUserInfo;
import com.ddang.ddang.chat.presentation.dto.request.CreateChatRoomRequest;
import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest;
import com.ddang.ddang.chat.presentation.dto.response.CreateMessageResponse;
import com.ddang.ddang.chat.presentation.dto.response.ReadChatRoomResponse;
Expand Down Expand Up @@ -33,6 +35,17 @@ public class ChatRoomController {
private final ChatRoomService chatRoomService;
private final MessageService messageService;

@PostMapping
public ResponseEntity<Void> create(
@AuthenticateUser final AuthenticateUserInfo userInfo,
@RequestBody @Valid final CreateChatRoomRequest chatRoomRequest
) {
final Long chatRoomId = chatRoomService.create(userInfo.id(), CreateChatRoomDto.from(chatRoomRequest));

return ResponseEntity.created(URI.create("/chattings/" + chatRoomId))
.build();
}

@GetMapping
public ResponseEntity<List<ReadChatRoomResponse>> readAllParticipatingChatRooms(
@AuthenticateUser final AuthenticateUserInfo userInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ddang.ddang.chat.presentation.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record CreateChatRoomRequest(
@NotNull(message = "경매 아이디가 입력되지 않았습니다.")
@Positive(message = "경매 아이디는 양수입니다.")
Long auctionId
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.application.exception.UserForbiddenException;
import com.ddang.ddang.auction.domain.exception.InvalidPriceValueException;
import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException;
import com.ddang.ddang.authentication.configuration.exception.UserUnauthorizedException;
import com.ddang.ddang.authentication.domain.exception.InvalidTokenException;
import com.ddang.ddang.authentication.domain.exception.UnsupportedSocialLoginException;
import com.ddang.ddang.bid.application.exception.InvalidBidException;
import com.ddang.ddang.category.application.exception.CategoryNotFoundException;
import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException;
import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException;
import com.ddang.ddang.chat.application.exception.UserNotAccessibleException;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.ddang.ddang.exception.dto.ExceptionResponse;
import com.ddang.ddang.image.application.exception.ImageNotFoundException;
import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException;
Expand All @@ -20,8 +21,7 @@
import com.ddang.ddang.report.application.exception.AlreadyReportAuctionException;
import com.ddang.ddang.report.application.exception.InvalidReportAuctionException;
import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException;
import java.net.MalformedURLException;
import java.util.stream.Collectors;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
Expand All @@ -33,6 +33,9 @@
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.net.MalformedURLException;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

Expand Down Expand Up @@ -139,8 +142,7 @@ public ResponseEntity<ExceptionResponse> handleStoreImageFailureException(
}

@ExceptionHandler(UnsupportedImageFileExtensionException.class)
public ResponseEntity<ExceptionResponse> handleUnsupportedImageFileExtensionException(
final UnsupportedImageFileExtensionException ex) {
public ResponseEntity<ExceptionResponse> handleUnsupportedImageFileExtensionException(final UnsupportedImageFileExtensionException ex) {
logger.warn(String.format(EXCEPTION_FORMAT, UnsupportedImageFileExtensionException.class), ex);

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
Expand Down Expand Up @@ -173,6 +175,26 @@ public ResponseEntity<ExceptionResponse> handleUserNotAccessibleException(
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(WinnerNotFoundException.class)
public ResponseEntity<ExceptionResponse> handleWinnerNotFoundException(
final WinnerNotFoundException ex
) {
logger.warn(String.format(EXCEPTION_FORMAT, WinnerNotFoundException.class), ex);

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidAuctionToChatException.class)
public ResponseEntity<ExceptionResponse> handleInvalidAuctionToChatException(
final InvalidAuctionToChatException ex
) {
logger.warn(String.format(EXCEPTION_FORMAT, InvalidAuctionToChatException.class), ex);

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(InvalidReporterToAuctionException.class)
public ResponseEntity<ExceptionResponse> handleInvalidReporterToAuctionException(final InvalidReporterToAuctionException ex) {
logger.warn(String.format(EXCEPTION_FORMAT, InvalidReporterToAuctionException.class), ex);
Expand Down
Loading
Loading