Skip to content

Commit

Permalink
feat: #413 상위 입찰자 발생 시 기존 마지막 입찰자에게 알림을 전송하는 기능 추가 (#438)
Browse files Browse the repository at this point in the history
* feat: 상위 입찰 발생 시 기존 마지막 입찰자에게 알림 전송하는 기능 추가

* feat: 레포지토리에서 경매 조회 시 마지막 입찰과 경매 대표이미지를 함께 조회하는 기능 추가

* refactor: 경매 이미지 경로 계산 부분 유틸로 분리

* test: 이미지 url 처리 관련 테스트 추가

* rename: 잘못된 변수명 수정 및 return문 개행 추가

* style: todo 추가
  • Loading branch information
kwonyj1022 authored and swonny committed Oct 6, 2023
1 parent 3a697eb commit 43485e0
Show file tree
Hide file tree
Showing 16 changed files with 568 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,12 @@ public Optional<User> findWinner(final LocalDateTime targetTime) {
private boolean isWinnerExist(final LocalDateTime targetTime) {
return auctioneerCount != 0 && isClosed(targetTime);
}

public Optional<User> findLastBidder() {
if (lastBid == null) {
return Optional.empty();
}

return Optional.of(lastBid.getBidder());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import com.ddang.ddang.auction.domain.Auction;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaAuctionRepository extends JpaRepository<Auction, Long>, QuerydslAuctionRepository {
public interface JpaAuctionRepository extends JpaRepository<Auction, Long>, QuerydslAuctionRepository, QuerydslAuctionAndImageRepository {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;

import java.util.Optional;

public interface QuerydslAuctionAndImageRepository {

Optional<AuctionAndImageDto> findDtoByAuctionId(final Long auctionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;
import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageQueryProjectionDto;
import com.ddang.ddang.auction.infrastructure.persistence.dto.QAuctionAndImageQueryProjectionDto;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

import static com.ddang.ddang.auction.domain.QAuction.auction;
import static com.ddang.ddang.bid.domain.QBid.bid;
import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage;

@Repository
@RequiredArgsConstructor
public class QuerydslAuctionAndImageRepositoryImpl implements QuerydslAuctionAndImageRepository {

private final JPAQueryFactory queryFactory;

@Override
public Optional<AuctionAndImageDto> findDtoByAuctionId(final Long auctionId) {
final AuctionAndImageQueryProjectionDto auctionAndImageQueryProjectionDto =
queryFactory.select(new QAuctionAndImageQueryProjectionDto(auction, auctionImage))
.from(auction)
.leftJoin(auction.lastBid, bid).fetchJoin()
.leftJoin(bid.bidder).fetchJoin()
.leftJoin(auctionImage).on(auctionImage.id.eq(
JPAExpressions
.select(auctionImage.id.min())
.from(auctionImage)
.where(auctionImage.auction.id.eq(auction.id))
.groupBy(auctionImage.auction.id)
)).fetchJoin()
.where(auction.id.eq(auctionId))
.fetchOne();

if (auctionAndImageQueryProjectionDto == null) {
return Optional.empty();
}

return Optional.of(auctionAndImageQueryProjectionDto.toDto());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ddang.ddang.auction.infrastructure.persistence.dto;

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.image.domain.AuctionImage;

public record AuctionAndImageDto(Auction auction, AuctionImage auctionImage) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ddang.ddang.auction.infrastructure.persistence.dto;

import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.image.domain.AuctionImage;
import com.querydsl.core.annotations.QueryProjection;

public record AuctionAndImageQueryProjectionDto(Auction auction, AuctionImage auctionImage) {

@QueryProjection
public AuctionAndImageQueryProjectionDto {
}

// TODO: 2023/09/22 dto이름 정해지면 명확한 dto이름으로 바꾸기
public AuctionAndImageDto toDto() {
return new AuctionAndImageDto(this.auction, this.auctionImage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository;
import com.ddang.ddang.auction.infrastructure.persistence.dto.AuctionAndImageDto;
import com.ddang.ddang.bid.application.dto.CreateBidDto;
import com.ddang.ddang.bid.application.dto.ReadBidDto;
import com.ddang.ddang.bid.application.exception.InvalidAuctionToBidException;
Expand All @@ -11,35 +12,61 @@
import com.ddang.ddang.bid.domain.Bid;
import com.ddang.ddang.bid.domain.BidPrice;
import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.presentation.util.ImageUrlCalculator;
import com.ddang.ddang.notification.application.NotificationService;
import com.ddang.ddang.notification.application.dto.CreateNotificationDto;
import com.ddang.ddang.notification.domain.NotificationType;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class BidService {

private final NotificationService notificationService;
private final JpaAuctionRepository auctionRepository;
private final JpaUserRepository userRepository;
private final JpaBidRepository bidRepository;

@Transactional
public Long create(final CreateBidDto bidDto) {
public Long create(final CreateBidDto bidDto, final String baseUrl) {
final User bidder = userRepository.findById(bidDto.userId())
.orElseThrow(() -> new UserNotFoundException("해당 사용자를 찾을 수 없습니다."));
final Auction auction = auctionRepository.findById(bidDto.auctionId())
.orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."));
final AuctionAndImageDto auctionAndImageDto =
auctionRepository.findDtoByAuctionId(bidDto.auctionId())
.orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다."));

final Auction auction = auctionAndImageDto.auction();
checkInvalidAuction(auction);
checkInvalidBid(auction, bidder, bidDto);

final Optional<User> previousBidder = auction.findLastBidder();

final Bid saveBid = saveBid(bidDto, auction, bidder);

if (previousBidder.isEmpty()) {
return saveBid.getId();
}

try {
final String sendNotificationMessage = sendNotification(auctionAndImageDto, previousBidder.get(), baseUrl);
log.info(sendNotificationMessage);
} catch (Exception ex) {
log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex);
}

return saveBid.getId();
}

Expand Down Expand Up @@ -109,6 +136,29 @@ private Bid saveBid(final CreateBidDto bidDto, final Auction auction, final User
return saveBid;
}

private String sendNotification(
final AuctionAndImageDto auctionAndImageDto,
final User previousBidder,
final String baseUrl
) {
final Auction auction = auctionAndImageDto.auction();
final AuctionImage auctionImage = auctionAndImageDto.auctionImage();
final CreateNotificationDto dto = new CreateNotificationDto(
NotificationType.BID,
previousBidder.getId(),
auction.getTitle(),
String.valueOf(auction.getLastBid().getPrice()),
calculateRedirectUrl(auction.getId()),
ImageUrlCalculator.calculateAuctionImageUrl(auctionImage, baseUrl)
);

return notificationService.send(dto);
}

private String calculateRedirectUrl(final Long auctionId) {
return "/auctions/" + auctionId;
}

public List<ReadBidDto> readAllByAuctionId(final Long auctionId) {
if (auctionRepository.existsById(auctionId)) {
final List<Bid> bids = bidRepository.findByAuctionIdOrderByIdAsc(auctionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.ddang.ddang.bid.presentation.dto.request.CreateBidRequest;
import com.ddang.ddang.bid.presentation.dto.response.ReadBidResponse;
import com.ddang.ddang.bid.presentation.dto.response.ReadBidsResponse;
import com.ddang.ddang.image.presentation.util.ImageBaseUrl;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -33,7 +34,7 @@ public ResponseEntity<Void> create(
@AuthenticateUser AuthenticationUserInfo userInfo,
@RequestBody @Valid final CreateBidRequest bidRequest
) {
bidService.create(CreateBidDto.of(bidRequest, userInfo.userId()));
bidService.create(CreateBidDto.of(bidRequest, userInfo.userId()), ImageBaseUrl.AUCTION.getBaseUrl());

return ResponseEntity.created(URI.create("/auctions/" + bidRequest.auctionId()))
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ddang.ddang.image.application.util;

import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.domain.ProfileImage;

public final class ImageIdProcessor {
Expand All @@ -14,4 +15,12 @@ public static Long process(final ProfileImage profileImage) {

return profileImage.getId();
}

public static Long process(final AuctionImage auctionImage) {
if (auctionImage == null) {
return null;
}

return auctionImage.getId();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ddang.ddang.image.presentation.util;

import com.ddang.ddang.image.application.util.ImageIdProcessor;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.domain.ProfileImage;

public final class ImageUrlCalculator {
Expand All @@ -20,6 +21,13 @@ public static String calculate(final ImageBaseUrl imageBaseUrl, final Long id) {

public static String calculateProfileImageUrl(final ProfileImage profileImage, final String baseUrl) {
final Long profileImageId = ImageIdProcessor.process(profileImage);

return baseUrl.concat(String.valueOf(profileImageId));
}

public static String calculateAuctionImageUrl(final AuctionImage auctionImage, final String baseUrl) {
final Long auctionImageId = ImageIdProcessor.process(auctionImage);

return baseUrl.concat(String.valueOf(auctionImageId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.ddang.ddang.bid.domain.BidPrice;
import com.ddang.ddang.configuration.JpaConfiguration;
import com.ddang.ddang.configuration.QuerydslConfiguration;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.image.domain.AuctionImage;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.region.domain.AuctionRegion;
import com.ddang.ddang.region.domain.Region;
import com.ddang.ddang.user.domain.User;
Expand Down Expand Up @@ -489,4 +489,66 @@ class AuctionTest {
// then
assertThat(actual).isEmpty();
}

@Test
void 마지막_입찰자를_반환한다() {
// given
final User seller = User.builder()
.name("회원1")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();
final User bidder = User.builder()
.name("회원2")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12346")
.build();
userRepository.save(seller);
userRepository.save(bidder);

final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L);

final Auction auction = Auction.builder()
.title("경매")
.seller(seller)
.closingTime(pastTime)
.startPrice(new Price(1_000))
.bidUnit(new BidUnit(1_000))
.build();
auction.updateLastBid(new Bid(auction, bidder, new BidPrice(10_000)));

// when
final Optional<User> actual = auction.findLastBidder();

// then
assertThat(actual).contains(bidder);
}

@Test
void 마지막_입찰자가_없다면_빈_Optional을_반환한다() {
// given
final User seller = User.builder()
.name("회원1")
.profileImage(new ProfileImage("upload.png", "store.png"))
.reliability(4.7d)
.oauthId("12345")
.build();
userRepository.save(seller);

final LocalDateTime pastTime = LocalDateTime.now().minusDays(3L);

final Auction auction = Auction.builder()
.title("경매")
.seller(seller)
.closingTime(pastTime)
.build();

// when
final Optional<User> actual = auction.findLastBidder();

// then
assertThat(actual).isEmpty();
}
}
Loading

0 comments on commit 43485e0

Please sign in to comment.