diff --git a/.github/workflows/backend_flyway.yml b/.github/workflows/backend_flyway.yml new file mode 100644 index 000000000..77b25804e --- /dev/null +++ b/.github/workflows/backend_flyway.yml @@ -0,0 +1,72 @@ +name: flyway 스크립트 검증 + +on: + pull_request: + paths: + - 'backend/ddang/src/main/resources/db/migration/**.sql' + types: [opened, reopened, synchronize] + branches: [develop-be] + +permissions: write-all +jobs: + build: + if: contains(github.event.pull_request.labels.*.name, 'backend') + runs-on: ubuntu-latest + services: + mysql: + image: mysql:latest + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testdb + MYSQL_USER: test + MYSQL_PASSWORD: password + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - uses: actions/checkout@v3 + + - name: settings java + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: cache gradle + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: chmod gradle + run: chmod +x backend/ddang/gradlew + + - name: Wait for MySQL to be ready + run: | + while [ -z "$DATABASE_URL" ]; do + echo "Waiting for MySQL to be ready..." + export DATABASE_URL=$(echo "SELECT 'ready';" | mysql -h127.0.0.1 -P3306 -utest -ppassword testdb --skip-column-names 2>/dev/null) + sleep 1 + done + + - name: Create flyway.conf + run: | + touch flyway.conf + echo "flyway.driver=com.mysql.cj.jdbc.Driver" >> flyway.conf + echo "flyway.url=jdbc:mysql://127.0.0.1:3306/testdb" >> flyway.conf + echo "flyway.user=test" >> flyway.conf + echo "flyway.password=password" >> flyway.conf + echo "flyway.encoding=UTF-8" >> flyway.conf + echo "flyway.locations=filesystem:src/main/resources/db/migration" >> flyway.conf + echo "flyway.validateOnMigrate=true" >> flyway.conf + working-directory: ./backend/ddang + + - name: flywayValidate + run: | + ./gradlew -Dflyway.configFiles=flyway.conf flywayMigrate --stacktrace + working-directory: ./backend/ddang diff --git a/.github/workflows/backend_pr_decorator.yml b/.github/workflows/backend_pr_decorator.yml index 246e81b8d..e63306202 100644 --- a/.github/workflows/backend_pr_decorator.yml +++ b/.github/workflows/backend_pr_decorator.yml @@ -61,31 +61,6 @@ jobs: echo "AUTHOR_NAME=${AUTHOR_NAME}" >> $GITHUB_OUTPUT echo "AUTHOR_ID=${AUTHOR_ID}" >> $GITHUB_OUTPUT - - name: set variables - id: variables - run: | - REVIEWERS_GIT_ID='${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' - reviewers=$(echo "$REVIEWERS_GIT_ID" | jq -r '.[]') - - REVIEWERS_SLACK_ID="" - - for reviewer in $reviewers; do - echo "Reviewer: $reviewer" - if [ "$reviewer" == "apptie" ]; then - REVIEWERS_SLACK_ID+="<@${{ secrets.apptie_slack_id }}> " - elif [ "$reviewer" == "swonny" ]; then - REVIEWERS_SLACK_ID+="<@${{ secrets.swonny_slack_id }}> " - elif [ "$reviewer" == "JJ503" ]; then - REVIEWERS_SLACK_ID+="<@${{ secrets.JJ503_slack_id }}> " - elif [ "$reviewer" == "kwonyj1022" ]; then - REVIEWERS_SLACK_ID+="<@${{ secrets.kwonyj1022_slack_id }}> " - fi - done - - echo "AUTHOR=${AUTHOR}" >> $GITHUB_OUTPUT - echo "REVIEWERS=${REVIEWERS}" >> $GITHUB_OUTPUT - echo "REVIEWERS_SLACK_ID=${REVIEWERS_SLACK_ID}" >> $GITHUB_OUTPUT - - name: slack notification if: github.event_name == 'pull_request' && github.event.action != 'synchronize' run: | @@ -97,8 +72,7 @@ jobs: SLACK_MESSAGE+="${{ github.event.pull_request.title }}" SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":white_check_mark:" - SLACK_MESSAGE+="\n>\n>*리뷰어*\n>" - SLACK_MESSAGE+="${{ steps.variables.outputs.REVIEWERS_SLACK_ID }}" + SLACK_MESSAGE+="\n\n리뷰 요청은 스레드로 직접 멘션해주세요." SLACK_MESSAGE+='"}}]}' curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" @@ -116,6 +90,7 @@ jobs: SLACK_MESSAGE+="${{ github.event.pull_request.title }}" SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":x:" + SLACK_MESSAGE+="\n\n리뷰 요청은 문제 해결 후 스레드로 직접 멘션해주세요." SLACK_MESSAGE+='"}}]}' curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" @@ -133,6 +108,7 @@ jobs: SLACK_MESSAGE+="${{ github.event.pull_request.title }}" SLACK_MESSAGE+="\n>\n>분석 결과\n>" SLACK_MESSAGE+=":black_square_for_stop:" + SLACK_MESSAGE+="\n\n리뷰 요청은 문제 해결 후 스레드로 직접 멘션해주세요." SLACK_MESSAGE+='"}}]}' curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "${SLACK_MESSAGE}" diff --git a/backend/ddang/build.gradle b/backend/ddang/build.gradle index ccbc8a1e9..14fd58159 100644 --- a/backend/ddang/build.gradle +++ b/backend/ddang/build.gradle @@ -1,9 +1,19 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.flywaydb:flyway-mysql:9.16.0' + } +} + plugins { id 'java' id 'org.springframework.boot' version '3.0.8' id 'io.spring.dependency-management' version '1.1.0' id 'jacoco' id 'org.asciidoctor.jvm.convert' version '3.3.2' + id 'org.flywaydb.flyway' version '9.16.0' } configurations { @@ -67,6 +77,11 @@ dependencies { implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'net.logstash.logback:logstash-logback-encoder:6.1' + // aws + implementation platform('software.amazon.awssdk:bom:2.20.56') + implementation 'software.amazon.awssdk:s3' + implementation 'software.amazon.awssdk:cloudfront' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/CreateInfoAuctionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/CreateInfoAuctionDto.java index a86d23dcf..f044074a1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/CreateInfoAuctionDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/CreateInfoAuctionDto.java @@ -1,11 +1,12 @@ package com.ddang.ddang.auction.application.dto; import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; public record CreateInfoAuctionDto( Long id, String title, - Long auctionImageId, + String auctionStoreName, int startPrice ) { @@ -13,7 +14,7 @@ public static CreateInfoAuctionDto from(final Auction auction) { return new CreateInfoAuctionDto( auction.getId(), auction.getTitle(), - auction.getAuctionImages().get(0).getId(), + ImageStoreNameProcessor.process(auction.getAuctionImages().get(0)), auction.getStartPrice().getValue() ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java index d5be76317..e56decd82 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/application/dto/ReadAuctionDto.java @@ -3,8 +3,7 @@ import com.ddang.ddang.auction.domain.Auction; import com.ddang.ddang.auction.domain.AuctionStatus; import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.image.application.util.ImageIdProcessor; -import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import java.time.LocalDateTime; import java.util.List; @@ -20,12 +19,12 @@ public record ReadAuctionDto( LocalDateTime registerTime, LocalDateTime closingTime, List auctionRegions, - List auctionImageIds, + List auctionImageStoreNames, int auctioneerCount, String mainCategory, String subCategory, Long sellerId, - Long sellerProfileId, + String sellerProfileImageStoreName, String sellerName, double sellerReliability, boolean isSellerDeleted, @@ -45,12 +44,12 @@ public static ReadAuctionDto of(final Auction auction, final LocalDateTime targe auction.getCreatedTime(), auction.getClosingTime(), convertReadRegionsDto(auction), - convertImageIds(auction), + convertImageStoreNames(auction), auction.getAuctioneerCount(), auction.getSubCategory().getMainCategory().getName(), auction.getSubCategory().getName(), auction.getSeller().getId(), - ImageIdProcessor.process(auction.getSeller().getProfileImage()), + ImageStoreNameProcessor.process(auction.getSeller().getProfileImage()), auction.getSeller().getName(), auction.getSeller().getReliability().getValue(), auction.getSeller().isDeleted(), @@ -59,10 +58,10 @@ public static ReadAuctionDto of(final Auction auction, final LocalDateTime targe ); } - private static List convertImageIds(final Auction auction) { + private static List convertImageStoreNames(final Auction auction) { return auction.getAuctionImages() .stream() - .map(AuctionImage::getId) + .map(ImageStoreNameProcessor::process) .toList(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java index 5d8025753..af22792ea 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/domain/Auction.java @@ -191,4 +191,8 @@ public Optional findLastBidder() { return Optional.of(lastBid.getBidder()); } + + public Optional findLastBid() { + return Optional.ofNullable(lastBid); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java index 8ccb8660f..b4a9271ee 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/JpaAuctionRepository.java @@ -19,7 +19,8 @@ public interface JpaAuctionRepository extends JpaRepository { LEFT JOIN FETCH a.lastBid JOIN FETCH a.subCategory sc JOIN FETCH sc.mainCategory - JOIN FETCH a.seller + JOIN FETCH a.seller seller + LEFT JOIN FETCH seller.profileImage WHERE a.deleted = false AND a.id = :id """) Optional findTotalAuctionById(final Long id); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java index 4ef4f2d15..b919e9f32 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/infrastructure/persistence/QuerydslAuctionRepository.java @@ -148,6 +148,7 @@ private List findAuctionsByIdsAndOrderSpecifiers( .join(auction.subCategory, category).fetchJoin() .join(category.mainCategory).fetchJoin() .join(auction.seller).fetchJoin() + .join(auction.seller.profileImage).fetchJoin() .where(auction.id.in(targetIds.toArray(Long[]::new))) .orderBy(orderSpecifiers.toArray(OrderSpecifier[]::new)) .fetch(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java index b4092b2e5..689c548e8 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/AuctionDetailResponse.java @@ -57,9 +57,9 @@ public static AuctionDetailResponse from(final ReadAuctionDto dto) { } private static List convertImageFullUrls(final ReadAuctionDto dto) { - return dto.auctionImageIds() + return dto.auctionImageStoreNames() .stream() - .map(id -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id)) + .map(imageStoreName -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, imageStoreName)) .toList(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java index 2f3c0c9d2..19d4d087e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/CreateAuctionResponse.java @@ -18,14 +18,14 @@ public static CreateAuctionResponse from(final CreateInfoAuctionDto dto) { return new CreateAuctionResponse( dto.id(), dto.title(), - convertAuctionImageUrl(dto.auctionImageId()), + convertAuctionImageUrl(dto.auctionStoreName()), dto.startPrice(), AuctionStatus.UNBIDDEN.name(), 0 ); } - private static String convertAuctionImageUrl(final Long id) { - return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id); + private static String convertAuctionImageUrl(final String storeName) { + return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, storeName); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java index 4eee887ff..035e1cd63 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadAuctionResponse.java @@ -17,7 +17,7 @@ public static ReadAuctionResponse from(final ReadAuctionDto dto) { return new ReadAuctionResponse( dto.id(), dto.title(), - ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.auctionImageIds().get(0)), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.auctionImageStoreNames().get(0)), processAuctionPrice(dto.startPrice(), dto.lastBidPrice()), dto.auctionStatus().name(), dto.auctioneerCount() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java index 336306eb2..e637071aa 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/ReadUserInAuctionQuestionResponse.java @@ -11,7 +11,7 @@ public static ReadUserInAuctionQuestionResponse from(final ReadUserInQnaDto writ return new ReadUserInAuctionQuestionResponse( writerDto.id(), NameProcessor.process(writerDto.isDeleted(), writerDto.name()), - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, writerDto.profileImageId()) + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, writerDto.profileImageStoreName()) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java index 3e6c432bd..7118ded9d 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/auction/presentation/dto/response/SellerResponse.java @@ -16,7 +16,7 @@ public record SellerResponse( public static SellerResponse from(final ReadAuctionDto auctionDto) { return new SellerResponse( auctionDto.sellerId(), - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, auctionDto.sellerProfileId()), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, auctionDto.sellerProfileImageStoreName()), NameProcessor.process(auctionDto.isSellerDeleted(), auctionDto.sellerName()), ReliabilityProcessor.process(auctionDto.sellerReliability()) ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java index d2c6a4e97..7f42911f7 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/BidService.java @@ -25,7 +25,6 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; @Service @Transactional(readOnly = true) @@ -47,37 +46,34 @@ public Long create(final CreateBidDto bidDto, final String auctionImageAbsoluteU .orElseThrow(() -> new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); final Auction auction = auctionAndImageDto.auction(); + checkInvalidAuction(auction); checkInvalidBid(auction, bidder, bidDto); - final Optional previousBidder = auction.findLastBidder(); - - final Bid saveBid = saveAndUpdateLastBid(bidDto, auction, bidder); - - publishBidNotificationEvent(auctionImageAbsoluteUrl, auctionAndImageDto, previousBidder); + auction.findLastBidder() + .ifPresent(previousBidder -> + publishBidNotificationEvent(auctionImageAbsoluteUrl, auctionAndImageDto, previousBidder)); - return saveBid.getId(); + return saveAndUpdateLastBid(bidDto, auction, bidder).getId(); } private void publishBidNotificationEvent( final String auctionImageAbsoluteUrl, final AuctionAndImageDto auctionAndImageDto, - final Optional previousBidder + final User previousBidder ) { - if (previousBidder.isEmpty()) { - return; - } - final BidDto bidDto = new BidDto( - previousBidder.get().getId(), + previousBidder.getId(), auctionAndImageDto, auctionImageAbsoluteUrl ); + bidEventPublisher.publishEvent(new BidNotificationEvent(bidDto)); } private void checkInvalidAuction(final Auction auction) { final LocalDateTime now = LocalDateTime.now(); + if (auction.isClosed(now)) { throw new InvalidAuctionToBidException("이미 종료된 경매입니다"); } @@ -87,18 +83,18 @@ private void checkInvalidAuction(final Auction auction) { } private void checkInvalidBid(final Auction auction, final User bidder, final CreateBidDto bidDto) { - final Optional lastBid = bidRepository.findLastBidByAuctionId(bidDto.auctionId()); - final BidPrice bidPrice = processBidPrice(bidDto.bidPrice()); - checkIsSeller(auction, bidder); - if (lastBid.isPresent()) { - checkIsNotLastBidder(lastBid.get(), bidder); - checkInvalidBidPrice(lastBid.get(), bidPrice); - return; - } + final BidPrice bidPrice = processBidPrice(bidDto.bidPrice()); - checkInvalidFirstBidPrice(auction, bidPrice); + auction.findLastBid() + .ifPresentOrElse( + lastBid -> { + checkIsNotLastBidder(lastBid, bidder); + checkInvalidBidPrice(lastBid, bidPrice); + }, + () -> checkInvalidFirstBidPrice(auction, bidPrice) + ); } private BidPrice processBidPrice(final int value) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java index d4cf89c82..ecb82854f 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/application/dto/ReadBidDto.java @@ -1,14 +1,14 @@ package com.ddang.ddang.bid.application.dto; import com.ddang.ddang.bid.domain.Bid; -import com.ddang.ddang.image.application.util.ImageIdProcessor; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; import java.time.LocalDateTime; public record ReadBidDto( String name, - Long profileImageId, + String profileImageStoreName, boolean isDeletedUser, int price, LocalDateTime bidTime @@ -19,7 +19,7 @@ public static ReadBidDto from(final Bid bid) { return new ReadBidDto( bidder.getName(), - ImageIdProcessor.process(bidder.getProfileImage()), + ImageStoreNameProcessor.process(bidder.getProfileImage()), bidder.isDeleted(), bid.getPrice().getValue(), bid.getCreatedTime() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/domain/repository/BidRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/domain/repository/BidRepository.java index 2005c8b69..7c262564a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/domain/repository/BidRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/domain/repository/BidRepository.java @@ -3,13 +3,10 @@ import com.ddang.ddang.bid.domain.Bid; import java.util.List; -import java.util.Optional; public interface BidRepository { Bid save(final Bid bid); List findAllByAuctionId(final Long auctionId); - - Optional findLastBidByAuctionId(final Long auctionId); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImpl.java index 4b1ca7073..6e0dfbbad 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImpl.java @@ -6,7 +6,6 @@ import org.springframework.stereotype.Repository; import java.util.List; -import java.util.Optional; @Repository @RequiredArgsConstructor @@ -23,9 +22,4 @@ public Bid save(final Bid bid) { public List findAllByAuctionId(final Long auctionId) { return jpaBidRepository.findAllByAuctionIdOrderByIdAsc(auctionId); } - - @Override - public Optional findLastBidByAuctionId(final Long auctionId) { - return jpaBidRepository.findLastBidByAuctionId(auctionId); - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java index 9c3a2e690..bf5f9824d 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/infrastructure/persistence/JpaBidRepository.java @@ -10,6 +10,13 @@ public interface JpaBidRepository extends JpaRepository { + @Query(""" + SELECT bid + FROM Bid bid + JOIN FETCH bid.bidder bidder + LEFT JOIN FETCH bidder.profileImage + WHERE bid.auction.id = :auctionId ORDER BY bid.id ASC + """) List findAllByAuctionIdOrderByIdAsc(final Long auctionId); @Query("SELECT b FROM Bid b WHERE b.auction.id = :auctionId ORDER BY b.id DESC LIMIT 1") diff --git a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java index 6f75d741f..2edc112b9 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/bid/presentation/dto/response/ReadBidResponse.java @@ -23,7 +23,7 @@ public static ReadBidResponse from(final ReadBidDto dto) { final String name = NameProcessor.process(dto.isDeletedUser(), dto.name()); return new ReadBidResponse( name, - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageId()), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageStoreName()), dto.price(), dto.bidTime() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java index efb3d78d6..8e91c6c0a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/ChatRoomService.java @@ -9,6 +9,7 @@ import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; @@ -22,6 +23,7 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +37,7 @@ public class ChatRoomService { private static final Long DEFAULT_CHAT_ROOM_ID = null; + private final ApplicationEventPublisher messageLogEventPublisher; private final ChatRoomRepository chatRoomRepository; private final ChatRoomAndImageRepository chatRoomAndImageRepository; private final ChatRoomAndMessageAndImageRepository chatRoomAndMessageAndImageRepository; @@ -51,9 +54,15 @@ public Long create(final Long userId, final CreateChatRoomDto chatRoomDto) { ); return chatRoomRepository.findChatRoomIdByAuctionId(findAuction.getId()) - .orElseGet(() -> - persistChatRoom(findUser, findAuction).getId() - ); + .orElseGet(() -> createChatRoom(findUser, findAuction)); + } + + private Long createChatRoom(final User findUser, final Auction findAuction) { + final ChatRoom persistChatRoom = persistChatRoom(findUser, findAuction); + + messageLogEventPublisher.publishEvent(new CreateReadMessageLogEvent(persistChatRoom)); + + return persistChatRoom.getId(); } private ChatRoom persistChatRoom(final User user, final Auction auction) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java new file mode 100644 index 000000000..5c67a1089 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java @@ -0,0 +1,39 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LastReadMessageLogEventListener { + + private final LastReadMessageLogService lastReadMessageLogService; + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { + try { + lastReadMessageLogService.create(createReadMessageLogEvent); + } catch (final IllegalArgumentException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } + + @TransactionalEventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { + try { + lastReadMessageLogService.update(updateReadMessageLogEvent); + } catch (final ReadMessageLogNotFoundException ex) { + log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java new file mode 100644 index 000000000..abba0d301 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LastReadMessageLogService { + + private final ReadMessageLogRepository readMessageLogRepository; + + @Transactional + public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { + final ChatRoom chatRoom = createReadMessageLogEvent.chatRoom(); + final User buyer = chatRoom.getBuyer(); + final User seller = chatRoom.getAuction().getSeller(); + final ReadMessageLog buyerReadMessageLog = new ReadMessageLog(chatRoom, buyer); + final ReadMessageLog sellerReadMessageLog = new ReadMessageLog(chatRoom, seller); + + readMessageLogRepository.saveAll(List.of(buyerReadMessageLog, sellerReadMessageLog)); + } + + @Transactional + public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { + final User reader = updateReadMessageLogEvent.reader(); + final ChatRoom chatRoom = updateReadMessageLogEvent.chatRoom(); + final ReadMessageLog messageLog = readMessageLogRepository.findBy(reader.getId(), chatRoom.getId()) + .orElseThrow(() -> + new ReadMessageLogNotFoundException( + "메시지 조회 로그가 존재하지 않습니다." + )); + + messageLog.updateLastReadMessage(updateReadMessageLogEvent.lastReadMessage().getId()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java index 35493d0f5..39e1498db 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java @@ -3,6 +3,7 @@ import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; @@ -15,7 +16,6 @@ import com.ddang.ddang.user.domain.User; import com.ddang.ddang.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,10 +25,10 @@ @Service @Transactional(readOnly = true) @RequiredArgsConstructor -@Slf4j public class MessageService { - private final ApplicationEventPublisher messageEventPublisher; + private final ApplicationEventPublisher messageLogEventPublisher; + private final ApplicationEventPublisher messageNotificationEventPublisher; private final MessageRepository messageRepository; private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; @@ -38,7 +38,7 @@ public Long create(final CreateMessageDto dto, final String profileImageAbsolute final ChatRoom chatRoom = chatRoomRepository.findById(dto.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); - final User writer = userRepository.findById(dto.writerId()) + final User writer = userRepository.findByIdWithProfileImage(dto.writerId()) .orElseThrow(() -> new UserNotFoundException( "지정한 아이디에 대한 발신자를 찾을 수 없습니다.")); final User receiver = userRepository.findById(dto.receiverId()) @@ -53,16 +53,14 @@ public Long create(final CreateMessageDto dto, final String profileImageAbsolute final Message persistMessage = messageRepository.save(message); - messageEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); + messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); return persistMessage.getId(); } public List readAllByLastMessageId(final ReadMessageRequest request) { - if (!userRepository.existsById(request.messageReaderId())) { - throw new UserNotFoundException("지정한 아이디에 대한 사용자를 찾을 수 없습니다."); - } - + final User reader = userRepository.findById(request.messageReaderId()) + .orElseThrow(() -> new UserNotFoundException("지정한 아이디에 대한 사용자를 찾을 수 없습니다.")); final ChatRoom chatRoom = chatRoomRepository.findById(request.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); @@ -77,6 +75,12 @@ public List readAllByLastMessageId(final ReadMessageRequest requ request.lastMessageId() ); + if (!readMessages.isEmpty()) { + final Message lastReadMessage = readMessages.get(readMessages.size() - 1); + + messageLogEventPublisher.publishEvent(new UpdateReadMessageLogEvent(reader, chatRoom, lastReadMessage)); + } + return readMessages.stream() .map(message -> ReadMessageDto.from(message, chatRoom)) .toList(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java index 233215c83..b39489800 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadAuctionInChatRoomDto.java @@ -2,13 +2,14 @@ import com.ddang.ddang.auction.domain.Auction; import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.image.domain.AuctionImage; public record ReadAuctionInChatRoomDto( Long id, String title, Integer lastBidPrice, - Long thumbnailImageId + String thumbnailImageStoreName ) { public static ReadAuctionInChatRoomDto of(final Auction auction, final AuctionImage thumbnailImage) { @@ -16,7 +17,7 @@ public static ReadAuctionInChatRoomDto of(final Auction auction, final AuctionIm auction.getId(), auction.getTitle(), convertPrice(auction.getLastBid()), - thumbnailImage.getId() + ImageStoreNameProcessor.process(thumbnailImage) ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java index 36e3d0382..b52e55a9e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadChatRoomWithLastMessageDto.java @@ -11,6 +11,7 @@ public record ReadChatRoomWithLastMessageDto( ReadAuctionInChatRoomDto auctionDto, ReadUserInChatRoomDto partnerDto, ReadLastMessageDto lastMessageDto, + Long unreadMessageCount, boolean isChatAvailable ) { @@ -22,12 +23,14 @@ public static ReadChatRoomWithLastMessageDto of( final User partner = chatRoom.calculateChatPartnerOf(findUser); final Message lastMessage = chatRoomAndMessageAndImageDto.message(); final AuctionImage thumbnailImage = chatRoomAndMessageAndImageDto.thumbnailImage(); + final Long unreadMessages = chatRoomAndMessageAndImageDto.unreadMessageCount(); return new ReadChatRoomWithLastMessageDto( chatRoom.getId(), ReadAuctionInChatRoomDto.of(chatRoom.getAuction(), thumbnailImage), ReadUserInChatRoomDto.from(partner), ReadLastMessageDto.from(lastMessage), + unreadMessages, chatRoom.isChatAvailablePartner(partner) ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java index 604827146..40c76de30 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadUserInChatRoomDto.java @@ -1,15 +1,15 @@ package com.ddang.ddang.chat.application.dto; -import com.ddang.ddang.image.application.util.ImageIdProcessor; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; -public record ReadUserInChatRoomDto(Long id, String name, Long profileImageId, double reliability, boolean isDeleted) { +public record ReadUserInChatRoomDto(Long id, String name, String profileImageStoreName, double reliability, boolean isDeleted) { public static ReadUserInChatRoomDto from(final User user) { return new ReadUserInChatRoomDto( user.getId(), user.getName(), - ImageIdProcessor.process(user.getProfileImage()), + ImageStoreNameProcessor.process(user.getProfileImage()), user.getReliability().getValue(), user.isDeleted() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java new file mode 100644 index 000000000..fec66ac3a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/CreateReadMessageLogEvent.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.chat.application.event; + +import com.ddang.ddang.chat.domain.ChatRoom; + +public record CreateReadMessageLogEvent(ChatRoom chatRoom) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java new file mode 100644 index 000000000..8fd1cde98 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.application.event; + +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.user.domain.User; + +public record UpdateReadMessageLogEvent(User reader, ChatRoom chatRoom, Message lastReadMessage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java new file mode 100644 index 000000000..45eb4623f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/exception/ReadMessageLogNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.application.exception; + +public class ReadMessageLogNotFoundException extends IllegalArgumentException { + + public ReadMessageLogNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java new file mode 100644 index 000000000..c741228a4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/ReadMessageLog.java @@ -0,0 +1,49 @@ +package com.ddang.ddang.chat.domain; + +import com.ddang.ddang.user.domain.User; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id", callSuper = false) +@ToString(of = {"id", "lastReadMessageId"}) +public class ReadMessageLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE}) + @JoinColumn(name = "chat_room_id", nullable = false, foreignKey = @ForeignKey(name = "fk_read_message_log_chat_room")) + private ChatRoom chatRoom; + + @OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE}) + @JoinColumn(name = "reader_id", nullable = false, foreignKey = @ForeignKey(name = "fk_read_message_log_reader")) + private User reader; + + private Long lastReadMessageId = 0L; + + public ReadMessageLog(final ChatRoom chatRoom, final User reader) { + this.chatRoom = chatRoom; + this.reader = reader; + } + + public void updateLastReadMessage(final Long lastReadMessageId) { + this.lastReadMessageId = lastReadMessageId; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java index 2e7e6ea6b..b31168c91 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/dto/ChatRoomAndMessageAndImageDto.java @@ -4,5 +4,10 @@ import com.ddang.ddang.chat.domain.Message; import com.ddang.ddang.image.domain.AuctionImage; -public record ChatRoomAndMessageAndImageDto(ChatRoom chatRoom, Message message, AuctionImage thumbnailImage) { +public record ChatRoomAndMessageAndImageDto( + ChatRoom chatRoom, + Message message, + AuctionImage thumbnailImage, + Long unreadMessageCount +) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java new file mode 100644 index 000000000..0da187e3a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/repository/ReadMessageLogRepository.java @@ -0,0 +1,13 @@ +package com.ddang.ddang.chat.domain.repository; + +import com.ddang.ddang.chat.domain.ReadMessageLog; + +import java.util.List; +import java.util.Optional; + +public interface ReadMessageLogRepository { + + Optional findBy(final Long readerId, final Long chatRoomId); + + List saveAll(List readMessageLogs); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java new file mode 100644 index 000000000..8ceceeb93 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/JpaReadMessageLogRepository.java @@ -0,0 +1,17 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface JpaReadMessageLogRepository extends JpaRepository { + + @Query(""" + SELECT rml + FROM ReadMessageLog rml + WHERE rml.chatRoom.id = :chatRoomId AND rml.reader.id = :readerId + """) + Optional findLastReadMessageByUserIdAndChatRoomId(final Long readerId, final Long chatRoomId); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java index cfdd83e9e..6c74526ed 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndImageRepository.java @@ -25,9 +25,10 @@ public Optional findChatRoomById(final Long chatRoomId) { final ChatRoomAndImageQueryProjectionDto chatRoomAndImageQueryProjectionDto = queryFactory.select(new QChatRoomAndImageQueryProjectionDto(chatRoom, auctionImage)) .from(chatRoom) - .leftJoin(chatRoom.buyer).fetchJoin() - .leftJoin(chatRoom.auction, auction).fetchJoin() - .leftJoin(auction.seller).fetchJoin() + .join(chatRoom.buyer).fetchJoin() + .join(chatRoom.auction, auction).fetchJoin() + .join(auction.seller).fetchJoin() + .leftJoin(auction.seller.profileImage).fetchJoin() .leftJoin(auctionImage).on(auctionImage.id.eq( JPAExpressions .select(auctionImage.id.min()) @@ -35,7 +36,7 @@ public Optional findChatRoomById(final Long chatRoomId) { .where(auctionImage.auction.id.eq(auction.id)) .groupBy(auctionImage.auction.id) )).fetchJoin() - .leftJoin(auction.lastBid).fetchJoin() + .join(auction.lastBid).fetchJoin() .where(chatRoom.id.eq(chatRoomId)) .fetchOne(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java index 07e7bc5f7..10a720230 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepository.java @@ -4,7 +4,9 @@ import com.ddang.ddang.chat.infrastructure.persistence.dto.ChatRoomAndMessageAndImageQueryProjectionDto; import com.ddang.ddang.chat.infrastructure.persistence.dto.QChatRoomAndMessageAndImageQueryProjectionDto; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.NumberPath; import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -15,6 +17,7 @@ import static com.ddang.ddang.auction.domain.QAuction.auction; import static com.ddang.ddang.chat.domain.QChatRoom.chatRoom; import static com.ddang.ddang.chat.domain.QMessage.message; +import static com.ddang.ddang.chat.domain.QReadMessageLog.readMessageLog; import static com.ddang.ddang.image.domain.QAuctionImage.auctionImage; import static java.util.Comparator.comparing; @@ -26,25 +29,28 @@ public class QuerydslChatRoomAndMessageAndImageRepository { public List findAllChatRoomInfoByUserIdOrderByLastMessage(final Long userId) { final List unsortedDtos = - queryFactory.select(new QChatRoomAndMessageAndImageQueryProjectionDto(chatRoom, message, auctionImage)) - .from(chatRoom) - .leftJoin(chatRoom.buyer).fetchJoin() - .leftJoin(chatRoom.auction, auction).fetchJoin() - .leftJoin(auction.seller).fetchJoin() + queryFactory.select(new QChatRoomAndMessageAndImageQueryProjectionDto( + chatRoom, + message, + auctionImage, + countUnreadMessages(userId, chatRoom.id) + )).from(chatRoom) + .join(chatRoom.buyer).fetchJoin() + .join(chatRoom.auction, auction).fetchJoin() + .join(auction.seller).fetchJoin() + .leftJoin(auction.seller.profileImage).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() - .leftJoin(auction.lastBid).fetchJoin() + .join(auction.lastBid).fetchJoin() .leftJoin(message).on(message.id.eq( JPAExpressions .select(message.id.max()) .from(message) .where(message.chatRoom.id.eq(chatRoom.id)) - .groupBy(message.chatRoom.id) )).fetchJoin() .where(isSellerOrWinner(userId)) .fetch(); @@ -52,6 +58,21 @@ public List findAllChatRoomInfoByUserIdOrderByLas return sortByLastMessageIdDesc(unsortedDtos); } + private static JPQLQuery countUnreadMessages(final Long userId, final NumberPath chatRoomId) { + return JPAExpressions.select(message.count()) + .from(message) + .where( + message.chatRoom.id.eq(chatRoomId), + message.writer.id.ne(userId), + message.id.gt( + JPAExpressions + .select(readMessageLog.lastReadMessageId) + .from(readMessageLog) + .where(readMessageLog.reader.id.eq(userId), readMessageLog.chatRoom.id.eq(chatRoomId)) + ) + ); + } + private List sortByLastMessageIdDesc( final List unsortedDtos ) { @@ -59,10 +80,10 @@ private List sortByLastMessageIdDesc( .filter((ChatRoomAndMessageAndImageQueryProjectionDto unsortedDto) -> Objects.nonNull(unsortedDto.message()) ).sorted(comparing( - (ChatRoomAndMessageAndImageQueryProjectionDto unsortedDto) -> - unsortedDto.message().getId() - ).reversed() - ).map(ChatRoomAndMessageAndImageQueryProjectionDto::toSortedDto) + (ChatRoomAndMessageAndImageQueryProjectionDto unsortedDto) -> + unsortedDto.message().getId() + ).reversed() + ).map(ChatRoomAndMessageAndImageQueryProjectionDto::toSortedDto) .toList(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java new file mode 100644 index 000000000..dceecf3ed --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImpl.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class ReadMessageLogRepositoryImpl implements ReadMessageLogRepository { + + private final JpaReadMessageLogRepository jpaReadMessageLogRepository; + + @Override + public Optional findBy(final Long readerId, final Long chatRoomId) { + return jpaReadMessageLogRepository.findLastReadMessageByUserIdAndChatRoomId(readerId, chatRoomId); + } + + @Override + public List saveAll(final List readMessageLogs) { + return jpaReadMessageLogRepository.saveAll(readMessageLogs); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java index c4369d8f3..f4a2b49a3 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/infrastructure/persistence/dto/ChatRoomAndMessageAndImageQueryProjectionDto.java @@ -6,7 +6,12 @@ import com.ddang.ddang.image.domain.AuctionImage; import com.querydsl.core.annotations.QueryProjection; -public record ChatRoomAndMessageAndImageQueryProjectionDto(ChatRoom chatRoom, Message message, AuctionImage auctionImage) { +public record ChatRoomAndMessageAndImageQueryProjectionDto( + ChatRoom chatRoom, + Message message, + AuctionImage auctionImage, + Long unreadMessage +) { @QueryProjection public ChatRoomAndMessageAndImageQueryProjectionDto { @@ -16,7 +21,8 @@ public ChatRoomAndMessageAndImageDto toSortedDto() { return new ChatRoomAndMessageAndImageDto( this.chatRoom, this.message, - this.auctionImage + this.auctionImage, + this.unreadMessage ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java index be45b0034..3f5d27c00 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadAuctionInChatRoomResponse.java @@ -7,7 +7,7 @@ public record ReadAuctionInChatRoomResponse(Long id, String title, String image, int price) { public static ReadAuctionInChatRoomResponse from(final ReadAuctionInChatRoomDto dto) { - final String thumbNailImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.thumbnailImageId()); + final String thumbNailImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, dto.thumbnailImageStoreName()); return new ReadAuctionInChatRoomResponse(dto.id(), dto.title(), thumbNailImageUrl, dto.lastBidPrice()); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java index 2a2daab62..95b540d68 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatPartnerResponse.java @@ -13,7 +13,7 @@ public static ReadChatPartnerResponse from(final ReadUserInChatRoomDto dto) { return new ReadChatPartnerResponse( dto.id(), name, - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageId()) + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.profileImageStoreName()) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java index 7aa99edc0..4c35179ea 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/dto/response/ReadChatRoomWithLastMessageResponse.java @@ -7,6 +7,7 @@ public record ReadChatRoomWithLastMessageResponse( ReadChatPartnerResponse chatPartner, ReadAuctionInChatRoomResponse auction, ReadLastMessageResponse lastMessage, + Long unreadMessageCount, boolean isChatAvailable ) { @@ -16,6 +17,7 @@ public static ReadChatRoomWithLastMessageResponse from(final ReadChatRoomWithLas ReadChatPartnerResponse.from(dto.partnerDto()), ReadAuctionInChatRoomResponse.from(dto.auctionDto()), ReadLastMessageResponse.from(dto.lastMessageDto()), + dto.unreadMessageCount(), dto.isChatAvailable() ); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/AwsConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/AwsConfiguration.java new file mode 100644 index 000000000..0098d27e4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/AwsConfiguration.java @@ -0,0 +1,24 @@ +package com.ddang.ddang.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@ProductProfile +@Configuration +public class AwsConfiguration { + + @Value("${aws.s3.region}") + private String s3Region; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.of(s3Region)) + .credentialsProvider(InstanceProfileCredentialsProvider.create()) + .build(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/ProductProfile.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/ProductProfile.java new file mode 100644 index 000000000..99ffc02a4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/ProductProfile.java @@ -0,0 +1,14 @@ +package com.ddang.ddang.configuration; + +import org.springframework.context.annotation.Profile; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Profile("!local && !test") +public @interface ProductProfile { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java index ac0848a4b..89e69f731 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/configuration/fcm/ProdFcmConfiguration.java @@ -1,5 +1,6 @@ package com.ddang.ddang.configuration.fcm; +import com.ddang.ddang.configuration.ProductProfile; import com.ddang.ddang.configuration.fcm.exception.FcmNotFoundException; import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; @@ -13,10 +14,9 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; -import org.springframework.context.annotation.Profile; @Configuration -@Profile("!test && !local") +@ProductProfile public class ProdFcmConfiguration { @Value("${fcm.key.path}") diff --git a/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java deleted file mode 100644 index f89bfc8ce..000000000 --- a/backend/ddang/src/main/java/com/ddang/ddang/configuration/initialization/InitializationUserConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.ddang.ddang.configuration.initialization; - -import com.ddang.ddang.image.domain.ProfileImage; -import com.ddang.ddang.user.domain.Reliability; -import com.ddang.ddang.user.domain.User; -import com.ddang.ddang.user.domain.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Configuration; -import org.springframework.transaction.annotation.Transactional; - -@Configuration -@ConditionalOnProperty(name = "data.init.user.enabled", havingValue = "true") -@RequiredArgsConstructor -public class InitializationUserConfiguration implements ApplicationRunner { - - private final UserRepository userRepository; - - @Override - @Transactional - public void run(final ApplicationArguments args) { - final User seller1 = User.builder() - .name("판매자1") - .profileImage(new ProfileImage("upload.png", "updateImage.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); - - final User buyer1 = User.builder() - .name("구매자1") - .profileImage(new ProfileImage("upload.png", "updateImage.png")) - .reliability(new Reliability(3.0d)) - .oauthId("12346") - .build(); - - final User buyer2 = User.builder() - .name("구매자2") - .profileImage(new ProfileImage("upload.png", "updateImage.png")) - .reliability(new Reliability(0.8d)) - .oauthId("12347") - .build(); - - userRepository.save(seller1); - userRepository.save(buyer1); - userRepository.save(buyer2); - } -} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index e3c7b2ea0..5831faf10 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -16,6 +16,7 @@ import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; import com.ddang.ddang.chat.application.exception.UnableToChatException; import com.ddang.ddang.device.application.exception.DeviceTokenNotFoundException; import com.ddang.ddang.exception.dto.ExceptionResponse; @@ -111,6 +112,14 @@ public ResponseEntity handleMessageNotFoundException(final Me .body(new ExceptionResponse(ex.getMessage())); } + @ExceptionHandler(ReadMessageLogNotFoundException.class) + public ResponseEntity handleReadMessageNotFoundException(final ReadMessageLogNotFoundException ex) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } + @ExceptionHandler(UnableToChatException.class) public ResponseEntity handleUnableToChatException(final UnableToChatException ex) { logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java b/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java index 7b9f240e3..8b2a11c3a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/application/ImageService.java @@ -1,7 +1,5 @@ package com.ddang.ddang.image.application; -import com.ddang.ddang.image.domain.AuctionImage; -import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.domain.repository.AuctionImageRepository; import com.ddang.ddang.image.domain.repository.ProfileImageRepository; import lombok.RequiredArgsConstructor; @@ -25,28 +23,22 @@ public class ImageService { private final ProfileImageRepository profileImageRepository; private final AuctionImageRepository auctionImageRepository; - public Resource readProfileImage(final Long id) throws MalformedURLException { - final ProfileImage profileImage = profileImageRepository.findById(id) - .orElse(null); - - if (profileImage == null) { + public Resource readProfileImage(final String storeName) throws MalformedURLException { + if (!profileImageRepository.existsByStoreName(storeName)) { return null; } - final String fullPath = findFullPath(profileImage.getImage().getStoreName()); + final String fullPath = findFullPath(storeName); return new UrlResource(FILE_PROTOCOL_PREFIX + fullPath); } - public Resource readAuctionImage(final Long id) throws MalformedURLException { - final AuctionImage auctionImage = auctionImageRepository.findById(id) - .orElse(null); - - if (auctionImage == null) { + public Resource readAuctionImage(final String storeName) throws MalformedURLException { + if (!auctionImageRepository.existsByStoreName(storeName)) { return null; } - final String fullPath = findFullPath(auctionImage.getImage().getStoreName()); + final String fullPath = findFullPath(storeName); return new UrlResource(FILE_PROTOCOL_PREFIX + fullPath); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java deleted file mode 100644 index 4492fb6f2..000000000 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageIdProcessor.java +++ /dev/null @@ -1,26 +0,0 @@ -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 { - - private ImageIdProcessor() { - } - - public static Long process(final ProfileImage profileImage) { - if (profileImage == null) { - return null; - } - - return profileImage.getId(); - } - - public static Long process(final AuctionImage auctionImage) { - if (auctionImage == null) { - return null; - } - - return auctionImage.getId(); - } -} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessor.java new file mode 100644 index 000000000..1b59b36ea --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessor.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.image.application.util; + +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; + +public final class ImageStoreNameProcessor { + + private ImageStoreNameProcessor() { + } + + public static String process(final ProfileImage profileImage) { + if (profileImage == null) { + return null; + } + + return profileImage.getImage().getStoreName(); + } + + public static String process(final AuctionImage auctionImage) { + if (auctionImage == null) { + return null; + } + + return auctionImage.getImage().getStoreName(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java index d85c01c1e..e45936a8c 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/ProfileImage.java @@ -19,8 +19,6 @@ public class ProfileImage { public static final String DEFAULT_PROFILE_IMAGE_STORE_NAME = "default_profile_image.png"; - // TODO: 10/13/23 앞으로 id가 아닌 store name으로 진행하기로 했는데, 임시로 해둡니다. 추후 삭제해주시면 감사하겠습니다. - public static final String DEFAULT_PROFILE_IMAGE_ID = "1"; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java index 19158bfd1..5485586f2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/StoreImageProcessor.java @@ -7,7 +7,10 @@ public interface StoreImageProcessor { - StoreImageDto storeImageFile(MultipartFile imageFile); + List WHITE_IMAGE_EXTENSION = List.of("jpg", "jpeg", "png"); + String EXTENSION_FILE_CHARACTER = "."; - List storeImageFiles(List imageFiles); + StoreImageDto storeImageFile(final MultipartFile imageFile); + + List storeImageFiles(final List imageFiles); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/AuctionImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/AuctionImageRepository.java index 586bca166..8fb1333c1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/AuctionImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/AuctionImageRepository.java @@ -1,10 +1,6 @@ package com.ddang.ddang.image.domain.repository; -import com.ddang.ddang.image.domain.AuctionImage; - -import java.util.Optional; - public interface AuctionImageRepository { - Optional findById(final Long id); + boolean existsByStoreName(final String storeName); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/ProfileImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/ProfileImageRepository.java index b0491a5d6..5fde626b0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/ProfileImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/domain/repository/ProfileImageRepository.java @@ -1,12 +1,6 @@ package com.ddang.ddang.image.domain.repository; -import com.ddang.ddang.image.domain.ProfileImage; - -import java.util.Optional; - public interface ProfileImageRepository { - Optional findById(final Long id); - - Optional findByStoreName(final String storeName); + boolean existsByStoreName(final String storeName); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java index 136b348a4..75a4de328 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/local/LocalStoreImageProcessor.java @@ -5,21 +5,21 @@ import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; @Component +@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "false") public class LocalStoreImageProcessor implements StoreImageProcessor { - private static final List WHITE_IMAGE_EXTENSION = List.of("jpg", "jpeg", "png"); - private static final String EXTENSION_FILE_CHARACTER = "."; - @Value("${image.store.dir}") private String imageStoreDir; @@ -27,7 +27,7 @@ public class LocalStoreImageProcessor implements StoreImageProcessor { public List storeImageFiles(final List imageFiles) { final List storeImageDtos = new ArrayList<>(); - for (MultipartFile imageFile : imageFiles) { + for (final MultipartFile imageFile : imageFiles) { if (imageFile.isEmpty()) { throw new EmptyImageException("이미지 파일의 데이터가 비어 있습니다."); } @@ -38,7 +38,8 @@ public List storeImageFiles(final List imageFiles) return storeImageDtos; } - public StoreImageDto storeImageFile(MultipartFile imageFile) { + @Override + public StoreImageDto storeImageFile(final MultipartFile imageFile) { try { final String originalImageFileName = imageFile.getOriginalFilename(); final String storeImageFileName = createStoreImageFileName(originalImageFileName); @@ -47,16 +48,16 @@ public StoreImageDto storeImageFile(MultipartFile imageFile) { imageFile.transferTo(new File(fullPath)); return new StoreImageDto(originalImageFileName, storeImageFileName); - } catch (IOException ex) { + } catch (final IOException ex) { throw new StoreImageFailureException("이미지 저장에 실패했습니다.", ex); } } - private String findFullPath(String storeImageFileName) { + private String findFullPath(final String storeImageFileName) { return imageStoreDir + storeImageFileName; } - private String createStoreImageFileName(String originalFilename) { + private String createStoreImageFileName(final String originalFilename) { final String extension = extractExtension(originalFilename); validateImageFileExtension(extension); @@ -66,7 +67,7 @@ private String createStoreImageFileName(String originalFilename) { return uuid + EXTENSION_FILE_CHARACTER + extension; } - private String extractExtension(String originalFilename) { + private String extractExtension(final String originalFilename) { int position = originalFilename.lastIndexOf(EXTENSION_FILE_CHARACTER); return originalFilename.substring(position + 1); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImpl.java index c24799bca..913ac97d2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImpl.java @@ -1,12 +1,9 @@ package com.ddang.ddang.image.infrastructure.persistence; -import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.domain.repository.AuctionImageRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository @RequiredArgsConstructor public class AuctionImageRepositoryImpl implements AuctionImageRepository { @@ -14,7 +11,7 @@ public class AuctionImageRepositoryImpl implements AuctionImageRepository { private final JpaAuctionImageRepository jpaAuctionImageRepository; @Override - public Optional findById(final Long id) { - return jpaAuctionImageRepository.findById(id); + public boolean existsByStoreName(final String storeName) { + return jpaAuctionImageRepository.existsByStoreName(storeName); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepository.java index 87f345983..fe0210243 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepository.java @@ -2,6 +2,14 @@ import com.ddang.ddang.image.domain.AuctionImage; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface JpaAuctionImageRepository extends JpaRepository { + + @Query(""" + SELECT COUNT(auction_image) > 0 + FROM AuctionImage auction_image + WHERE auction_image.image.storeName = :storeName + """) + boolean existsByStoreName(final String storeName); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java index 39b321bad..21f4e73e5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepository.java @@ -4,10 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; - public interface JpaProfileImageRepository extends JpaRepository { - @Query("SELECT i FROM ProfileImage i WHERE i.image.storeName = :storeName") - Optional findByStoreName(final String storeName); + @Query(""" + SELECT COUNT(profile_image) > 0 + FROM ProfileImage profile_image + WHERE profile_image.image.storeName = :storeName + """) + boolean existsByStoreName(final String storeName); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImpl.java index 12fa367f7..69fb884c4 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImpl.java @@ -1,12 +1,9 @@ package com.ddang.ddang.image.infrastructure.persistence; -import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.domain.repository.ProfileImageRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; - @Repository @RequiredArgsConstructor public class ProfileImageRepositoryImpl implements ProfileImageRepository { @@ -14,12 +11,7 @@ public class ProfileImageRepositoryImpl implements ProfileImageRepository { private final JpaProfileImageRepository jpaProfileImageRepository; @Override - public Optional findById(final Long id) { - return jpaProfileImageRepository.findById(id); - } - - @Override - public Optional findByStoreName(final String storeName) { - return jpaProfileImageRepository.findByStoreName(storeName); + public boolean existsByStoreName(final String storeName) { + return jpaProfileImageRepository.existsByStoreName(storeName); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessor.java b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessor.java new file mode 100644 index 000000000..a7e4eaffe --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessor.java @@ -0,0 +1,103 @@ +package com.ddang.ddang.image.infrastructure.s3; + +import com.ddang.ddang.configuration.ProductProfile; +import com.ddang.ddang.image.domain.StoreImageProcessor; +import com.ddang.ddang.image.domain.dto.StoreImageDto; +import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; +import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; +import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +@ProductProfile +@ConditionalOnProperty(name = "aws.s3.enabled", havingValue = "true") +@RequiredArgsConstructor +public class S3StoreImageProcessor implements StoreImageProcessor { + + @Value("${aws.s3.bucket-name}") + private String bucketName; + + @Value("${aws.s3.image-path}") + private String path; + + private final S3Client s3Client; + + @Override + public List storeImageFiles(final List imageFiles) { + final List storeImageDtos = new ArrayList<>(); + + for (final MultipartFile imageFile : imageFiles) { + if (imageFile.isEmpty()) { + throw new EmptyImageException("이미지 파일의 데이터가 비어 있습니다."); + } + + storeImageDtos.add(storeImageFile(imageFile)); + } + + return storeImageDtos; + } + + @Override + public StoreImageDto storeImageFile(final MultipartFile imageFile) { + try { + final String originalImageFileName = imageFile.getOriginalFilename(); + final String storeImageFileName = createStoreImageFileName(originalImageFileName); + final String fullPath = findFullPath(storeImageFileName); + final PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .key(fullPath) + .bucket(bucketName) + .contentType(imageFile.getContentType()) + .build(); + + s3Client.putObject( + putObjectRequest, + RequestBody.fromInputStream(imageFile.getInputStream(), imageFile.getSize()) + ); + + return new StoreImageDto(originalImageFileName, storeImageFileName); + } catch (final IOException ex) { + throw new StoreImageFailureException("이미지 저장에 실패했습니다.", ex); + } catch (final SdkException ex) { + throw new StoreImageFailureException("AWS 이미지 저장에 실패했습니다.", ex); + } + } + + private String findFullPath(final String storeImageFileName) { + return path + storeImageFileName; + } + + private String createStoreImageFileName(final String originalFilename) { + final String extension = extractExtension(originalFilename); + + validateImageFileExtension(extension); + + final String uuid = UUID.randomUUID().toString(); + + return uuid + EXTENSION_FILE_CHARACTER + extension; + } + + private String extractExtension(final String originalFilename) { + int position = originalFilename.lastIndexOf(EXTENSION_FILE_CHARACTER); + + return originalFilename.substring(position + 1); + } + + private void validateImageFileExtension(final String extension) { + if (!WHITE_IMAGE_EXTENSION.contains(extension)) { + throw new UnsupportedImageFileExtensionException("지원하지 않는 확장자입니다. : " + extension); + } + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java index 16ace8049..12a12e401 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/ImageController.java @@ -1,7 +1,6 @@ package com.ddang.ddang.image.presentation; import com.ddang.ddang.image.application.ImageService; -import java.net.MalformedURLException; import lombok.RequiredArgsConstructor; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -12,27 +11,29 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; +import java.net.MalformedURLException; + @RestController @RequiredArgsConstructor public class ImageController { private final ImageService imageService; - @GetMapping("/users/images/{id}") - public ResponseEntity downloadProfileImage(@PathVariable Long id) throws MalformedURLException { - final Resource resource = imageService.readProfileImage(id); + @GetMapping("/users/images/{storeName}") + public ResponseEntity downloadProfileImage(@PathVariable final String storeName) throws MalformedURLException { + final Resource resource = imageService.readProfileImage(storeName); - HttpHeaders headers = new HttpHeaders(); + final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.IMAGE_JPEG); return new ResponseEntity<>(resource, headers, HttpStatus.OK); } - @GetMapping("/auctions/images/{id}") - public ResponseEntity downloadAuctionImage(@PathVariable Long id) throws MalformedURLException { - final Resource resource = imageService.readAuctionImage(id); + @GetMapping("/auctions/images/{storeName}") + public ResponseEntity downloadAuctionImage(@PathVariable final String storeName) throws MalformedURLException { + final Resource resource = imageService.readAuctionImage(storeName); - HttpHeaders headers = new HttpHeaders(); + final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.IMAGE_JPEG); return new ResponseEntity<>(resource, headers, HttpStatus.OK); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java index 374a03459..bc1019357 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculator.java @@ -7,21 +7,21 @@ public final class ImageUrlCalculator { private ImageUrlCalculator() { } - public static String calculateBy(final ImageRelativeUrl imageRelativeUrl, final Long id) { + public static String calculateBy(final ImageRelativeUrl imageRelativeUrl, final String storeName) { final String absoluteUrl = imageRelativeUrl.calculateAbsoluteUrl(); - if (id == null && imageRelativeUrl == ImageRelativeUrl.USER) { - return absoluteUrl + ProfileImage.DEFAULT_PROFILE_IMAGE_ID; + if (storeName == null && imageRelativeUrl == ImageRelativeUrl.USER) { + return absoluteUrl + ProfileImage.DEFAULT_PROFILE_IMAGE_STORE_NAME; } - return absoluteUrl + id; + return absoluteUrl + storeName; } - public static String calculateBy(final String imageAbsoluteUrl, final Long id) { - if (id == null && imageAbsoluteUrl.contains(ImageRelativeUrl.USER.getValue())) { - return imageAbsoluteUrl + ProfileImage.DEFAULT_PROFILE_IMAGE_ID; + public static String calculateBy(final String imageAbsoluteUrl, final String storeName) { + if (storeName == null && imageAbsoluteUrl.contains(ImageRelativeUrl.USER.getValue())) { + return imageAbsoluteUrl + ProfileImage.DEFAULT_PROFILE_IMAGE_STORE_NAME; } - return imageAbsoluteUrl + id; + return imageAbsoluteUrl + storeName; } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java index 1a06f1125..2839f60cd 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java @@ -5,6 +5,7 @@ import com.ddang.ddang.bid.application.event.BidNotificationEvent; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.presentation.util.ImageUrlCalculator; @@ -44,7 +45,7 @@ public void sendMessageNotification(final MessageNotificationEvent messageNotifi message.getWriter().getName(), message.getContents(), calculateRedirectUrl(MESSAGE_NOTIFICATION_REDIRECT_URI, message.getChatRoom().getId()), - ImageUrlCalculator.calculateBy(messageNotificationEvent.profileImageAbsoluteUrl(), profileImage.getId()) + ImageUrlCalculator.calculateBy(messageNotificationEvent.profileImageAbsoluteUrl(), ImageStoreNameProcessor.process(profileImage)) ); notificationService.send(createNotificationDto); } catch (final FirebaseMessagingException ex) { @@ -64,7 +65,7 @@ public void sendBidNotification(final BidNotificationEvent bidNotificationEvent) auction.getTitle(), BID_NOTIFICATION_MESSAGE_FORMAT, calculateRedirectUrl(AUCTION_DETAIL_URI, auction.getId()), - ImageUrlCalculator.calculateBy(bidDto.auctionImageAbsoluteUrl(), auctionImage.getId()) + ImageUrlCalculator.calculateBy(bidDto.auctionImageAbsoluteUrl(), ImageStoreNameProcessor.process(auctionImage)) ); notificationService.send(createNotificationDto); } catch (final FirebaseMessagingException ex) { @@ -84,7 +85,7 @@ public void sendQuestionNotification(final QuestionNotificationEvent questionNot auction.getTitle(), question.getContent(), calculateRedirectUrl(AUCTION_DETAIL_URI, auction.getId()), - ImageUrlCalculator.calculateBy(questionNotificationEvent.absoluteImageUrl(), auctionImage.getId()) + ImageUrlCalculator.calculateBy(questionNotificationEvent.absoluteImageUrl(), ImageStoreNameProcessor.process(auctionImage)) ); notificationService.send(createNotificationDto); } catch (final FirebaseMessagingException ex) { @@ -105,7 +106,7 @@ public void sendAnswerNotification(final AnswerNotificationEvent answerNotificat question.getContent(), answer.getContent(), calculateRedirectUrl(AUCTION_DETAIL_URI, auction.getId()), - ImageUrlCalculator.calculateBy(answerNotificationEvent.absoluteImageUrl(), auctionImage.getId()) + ImageUrlCalculator.calculateBy(answerNotificationEvent.absoluteImageUrl(), ImageStoreNameProcessor.process(auctionImage)) ); notificationService.send(createNotificationDto); } catch (final FirebaseMessagingException ex) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java index a75d44558..c3d73e9cf 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/application/dto/ReadUserInQnaDto.java @@ -1,11 +1,12 @@ package com.ddang.ddang.qna.application.dto; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; public record ReadUserInQnaDto( Long id, String name, - Long profileImageId, + String profileImageStoreName, double reliability, String oauthId, boolean isDeleted @@ -14,7 +15,7 @@ public static ReadUserInQnaDto from(final User writer) { return new ReadUserInQnaDto( writer.getId(), writer.getName(), - writer.getProfileImage().getId(), + ImageStoreNameProcessor.process(writer.getProfileImage()), writer.getReliability().getValue(), writer.getOauthInformation().getOauthId(), writer.isDeleted() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java index 11de78514..ae6585356 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/qna/infrastructure/JpaQuestionRepository.java @@ -14,10 +14,12 @@ public interface JpaQuestionRepository extends JpaRepository { @Query(""" SELECT q FROM Question q - JOIN FETCH q.writer + JOIN FETCH q.writer w + LEFT JOIN FETCH w.profileImage LEFT JOIN FETCH q.answer JOIN FETCH q.auction a - JOIN FETCH a.seller + JOIN FETCH a.seller s + JOIN FETCH s.profileImage WHERE q.deleted = false AND a.id = :auctionId """) List findAllByAuctionId(final Long auctionId); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java index 09e21af9e..5e5847b29 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadReporterDto.java @@ -1,15 +1,15 @@ package com.ddang.ddang.report.application.dto; -import com.ddang.ddang.image.application.util.ImageIdProcessor; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; -public record ReadReporterDto(Long id, String name, Long profileImageId, double reliability, boolean isDeleted) { +public record ReadReporterDto(Long id, String name, String profileImageStoreName, double reliability, boolean isDeleted) { public static ReadReporterDto from(final User reporter) { return new ReadReporterDto( reporter.getId(), reporter.getName(), - ImageIdProcessor.process(reporter.getProfileImage()), + ImageStoreNameProcessor.process(reporter.getProfileImage()), reporter.getReliability().getValue(), reporter.isDeleted() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java index cd4fbd220..119fe432a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/application/dto/ReadUserInReportDto.java @@ -1,12 +1,12 @@ package com.ddang.ddang.report.application.dto; -import com.ddang.ddang.image.application.util.ImageIdProcessor; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; public record ReadUserInReportDto( Long id, String name, - Long profileImageId, + String profileImageStoreName, double reliability, String oauthId, boolean isSellerDeleted @@ -16,7 +16,7 @@ public static ReadUserInReportDto from(final User user) { return new ReadUserInReportDto( user.getId(), user.getName(), - ImageIdProcessor.process(user.getProfileImage()), + ImageStoreNameProcessor.process(user.getProfileImage()), user.getReliability().getValue(), user.getOauthInformation().getOauthId(), user.isDeleted() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java index 2be18d491..b4614a8fa 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAnswerReportRepository.java @@ -13,11 +13,11 @@ public interface JpaAnswerReportRepository extends JpaRepository findAll(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java index 5fa4e4186..dfff156f8 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaAuctionReportRepository.java @@ -13,9 +13,11 @@ public interface JpaAuctionReportRepository extends JpaRepository findAll(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java index c41258456..6f1bd65f4 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaChatRoomReportRepository.java @@ -13,11 +13,14 @@ public interface JpaChatRoomReportRepository extends JpaRepository findAll(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java index deaf6e349..caad0fd2e 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/report/infrastructure/persistence/JpaQuestionReportRepository.java @@ -13,8 +13,11 @@ public interface JpaQuestionReportRepository extends JpaRepository findAll(); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java index bb0fa0927..25c446c0f 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/application/dto/ReadUserInReviewDto.java @@ -1,14 +1,15 @@ package com.ddang.ddang.review.application.dto; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; -public record ReadUserInReviewDto(Long id, String name, Long profileImageId, double reliability, String oauthId) { +public record ReadUserInReviewDto(Long id, String name, String profileImageStoreName, double reliability, String oauthId) { public static ReadUserInReviewDto from(final User user) { return new ReadUserInReviewDto( user.getId(), user.getName(), - user.getProfileImage().getId(), + ImageStoreNameProcessor.process(user.getProfileImage()), user.getReliability().getValue(), user.getOauthInformation().getOauthId() ); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java index 5462534d6..c1be24a3d 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/infrastructure/persistence/JpaReviewRepository.java @@ -14,6 +14,7 @@ public interface JpaReviewRepository extends JpaRepository { @Query(""" SELECT r FROM Review r JOIN FETCH r.writer w + LEFT JOIN FETCH w.profileImage JOIN FETCH r.target t WHERE t.id = :targetId ORDER BY r.id DESC diff --git a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java index 328ed847c..720a923d0 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/review/presentation/dto/response/ReadUserInReviewResponse.java @@ -10,7 +10,7 @@ public static ReadUserInReviewResponse from(final ReadUserInReviewDto userDto) { return new ReadUserInReviewResponse( userDto.id(), userDto.name(), - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, userDto.profileImageId()) + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, userDto.profileImageStoreName()) ); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java b/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java index c0cc24705..9b299d407 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/application/UserService.java @@ -21,7 +21,7 @@ public class UserService { private final StoreImageProcessor imageProcessor; public ReadUserDto readById(final Long userId) { - final User user = userRepository.findById(userId) + final User user = userRepository.findByIdWithProfileImage(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 사용할 수 없습니다.")); return ReadUserDto.from(user); @@ -29,7 +29,7 @@ public ReadUserDto readById(final Long userId) { @Transactional public ReadUserDto updateById(final Long userId, final UpdateUserDto userDto) { - final User user = userRepository.findById(userId) + final User user = userRepository.findByIdWithProfileImage(userId) .orElseThrow(() -> new UserNotFoundException("사용자 정보를 사용할 수 없습니다.")); updateUserByRequest(userDto, user); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java index 015310258..282c580eb 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/application/dto/ReadUserDto.java @@ -1,12 +1,12 @@ package com.ddang.ddang.user.application.dto; -import com.ddang.ddang.image.application.util.ImageIdProcessor; +import com.ddang.ddang.image.application.util.ImageStoreNameProcessor; import com.ddang.ddang.user.domain.User; public record ReadUserDto( Long id, String name, - Long profileImageId, + String profileImageStoreName, double reliability, String oauthId, boolean isDeleted @@ -16,7 +16,7 @@ public static ReadUserDto from(final User user) { return new ReadUserDto( user.getId(), user.getName(), - ImageIdProcessor.process(user.getProfileImage()), + ImageStoreNameProcessor.process(user.getProfileImage()), user.getReliability().getValue(), user.getOauthInformation().getOauthId(), user.isDeleted() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java index 6f1b7fdfc..b4bc93c92 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/domain/repository/UserRepository.java @@ -10,7 +10,7 @@ public interface UserRepository { Optional findById(final Long id); - boolean existsById(final Long id); + Optional findByIdWithProfileImage(final Long id); Optional findByOauthId(final String oauthId); diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java index a703d934b..809eead49 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepository.java @@ -15,6 +15,14 @@ public interface JpaUserRepository extends JpaRepository { """) Optional findById(final Long id); + @Query(""" + SELECT u + FROM User u + LEFT JOIN FETCH u.profileImage + WHERE u.deleted = false AND u.id = :id + """) + Optional findByIdWithProfileImage(final Long id); + @Query(""" SELECT u FROM User u diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java index 5a5e3fac6..9f42c37b7 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImpl.java @@ -24,8 +24,8 @@ public Optional findById(final Long id) { } @Override - public boolean existsById(final Long id) { - return jpaUserRepository.existsById(id); + public Optional findByIdWithProfileImage(final Long id) { + return jpaUserRepository.findByIdWithProfileImage(id); } @Override diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/ReadUserResponse.java new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java index d7fa2befd..8b8b0d1e8 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/AuctionDetailResponse.java @@ -56,9 +56,9 @@ public static AuctionDetailResponse from(final ReadAuctionDto dto) { } private static List convertImageFullUrls(final ReadAuctionDto dto) { - return dto.auctionImageIds() + return dto.auctionImageStoreNames() .stream() - .map(id -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, id)) + .map(storeName -> ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, storeName)) .toList(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java index 05b142a7b..3595e11a3 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionDetailResponse.java @@ -18,7 +18,10 @@ public static ReadAuctionDetailResponse of( final AuthenticationUserInfo userInfo ) { final AuctionDetailResponse auctionDetailResponse = AuctionDetailResponse.from(dto.auctionDto()); - final String profileImageUrl = ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, dto.auctionDto().sellerId()); + final String profileImageUrl = ImageUrlCalculator.calculateBy( + ImageRelativeUrl.USER, + dto.auctionDto().sellerProfileImageStoreName() + ); final Float reliability = ReliabilityProcessor.process(dto.auctionDto().sellerReliability()); final SellerResponse sellerResponse = new SellerResponse( diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java index 4db39f7f5..0cca8926d 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadAuctionResponse.java @@ -25,9 +25,9 @@ public static ReadAuctionResponse from(final ReadAuctionDto dto) { } private static String calculateThumbnailImageUrl(final ReadAuctionDto dto) { - final Long thumbnailAuctionImage = dto.auctionImageIds().get(0); + final String thumbnailAuctionImageStoreName = dto.auctionImageStoreNames().get(0); - return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, thumbnailAuctionImage); + return ImageUrlCalculator.calculateBy(ImageRelativeUrl.AUCTION, thumbnailAuctionImageStoreName); } private static int processAuctionPrice(final Integer startPrice, final Integer lastBidPrice) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java index 9294e5024..e9be32709 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/user/presentation/dto/response/ReadUserResponse.java @@ -11,7 +11,7 @@ public record ReadUserResponse(String name, String profileImage, Float reliabili public static ReadUserResponse from(final ReadUserDto readUserDto) { return new ReadUserResponse( NameProcessor.process(readUserDto.isDeleted(), readUserDto.name()), - ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, readUserDto.profileImageId()), + ImageUrlCalculator.calculateBy(ImageRelativeUrl.USER, readUserDto.profileImageStoreName()), ReliabilityProcessor.process(readUserDto.reliability()) ); } diff --git a/backend/ddang/src/main/resources/application-local.yml b/backend/ddang/src/main/resources/application-local.yml index 2db9e094b..e6c8bb7d1 100644 --- a/backend/ddang/src/main/resources/application-local.yml +++ b/backend/ddang/src/main/resources/application-local.yml @@ -29,10 +29,6 @@ data: init: region: enabled: false - auction: - enabled: false - user: - enabled: false image: store: @@ -57,3 +53,10 @@ fcm: enabled: false key: path: firebase/private-key.json + +aws: + s3: + enabled: false + region: region + bucket-name: awsbucketname + image-path: image/path diff --git a/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql b/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql new file mode 100644 index 000000000..0cc6063d2 --- /dev/null +++ b/backend/ddang/src/main/resources/db/migration/V21__create_read_message_log_tables.sql @@ -0,0 +1,10 @@ +create table read_message_log ( + id bigint not null auto_increment, + chat_room_id bigint, + reader_id bigint, + last_read_message_id bigint, + primary key (id) +); + +alter table read_message_log add constraint fk_read_message_log_chat_room foreign key (chat_room_id) references chat_room (id); +alter table read_message_log add constraint fk_read_message_reader foreign key (reader_id) references users (id); diff --git a/backend/ddang/src/main/resources/static/docs/docs.html b/backend/ddang/src/main/resources/static/docs/docs.html index 4f254be2b..7b5494805 100644 --- a/backend/ddang/src/main/resources/static/docs/docs.html +++ b/backend/ddang/src/main/resources/static/docs/docs.html @@ -516,9 +516,9 @@

땅땅땅 API 문서

  • 채팅방 신고 등록
  • 채팅방 신고 조회
  • 질문 신고 등록
  • -
  • 질문 신고 조회
  • +
  • 채팅방 신고 조회
  • 답변 신고 등록
  • -
  • 답변 신고 조회
  • +
  • 채팅방 신고 조회
  • 디바이스 토큰 API @@ -531,7 +531,7 @@

    땅땅땅 API 문서

  • 사용자 평가 등록
  • 지정한 평가 아이디에 해당하는 평가 조회
  • 지정한 사용자가 받은 평가 목록 조회
  • -
  • 사용자가 경매 거래 상대에게 작성한 평가를 조회한다
  • +
  • 사용자가_경매_거래_상대에게_작성한_평가를_조회한다
  • @@ -914,9 +914,28 @@

    요청

    -

    Unresolved directive in docs.adoc - include::/Users/jamie/Documents/IntelliJ_Project/2023-3-ddang/backend/ddang/build/generated-snippets/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/path-parameters.adoc[]

    +

    Unresolved directive in docs.adoc - include::/Users/kwon-yejin/GitHub/2023-3-ddang/backend/ddang/build/generated-snippets/authentication-controller-test/ouath2-type과_access-token과_refresh-token을_전달하면_탈퇴한다/path-parameters.adoc[]

    + ++++ + + + + + + + + + + + + +
    Table 2. /oauth2/withdrawal/{oauth2Type}
    ParameterDescription

    oauth2Type

    소셜 로그인을 할 서비스 선택(kakao로 고정)

    +@@ -990,7 +1009,7 @@

    응답

    { "name" : "사용자1", - "profileImage" : "http://localhost:8080/users/images/1", + "profileImage" : "http://localhost:8080/users/images/profile_image.png", "reliability" : 4.6 } @@ -1049,7 +1068,7 @@

    응답

    { "name" : "updateName", - "profileImage" : "http://localhost:8080/users/images/1", + "profileImage" : "http://localhost:8080/users/images/profile_image.png", "reliability" : 4.6 } @@ -1278,7 +1297,7 @@

    요청

    - +@@ -1357,7 +1376,7 @@

    요청

    Table 2. /regions/{firstId}Table 3. /regions/{firstId}
    - +@@ -1453,7 +1472,7 @@

    요청

    Content-Disposition: form-data; name=request; filename=request Content-Type: application/json -{"title":"제목","description":"내용","bidUnit":1000,"startPrice":1000,"closingTime":"2023-10-22T13:47:37.536222","subCategoryId":2,"thirdRegionIds":[3]} +{"title":"제목","description":"내용","bidUnit":1000,"startPrice":1000,"closingTime":"2023-11-03T16:59:34.062594","subCategoryId":2,"thirdRegionIds":[3]} --6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm-- @@ -1509,7 +1528,7 @@

    응답

    { "id" : 1, "title" : "제목", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 0 @@ -1627,14 +1646,14 @@

    응답

    "auctions" : [ { "id" : 2, "title" : "경매 상품 1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 }, { "id" : 1, "title" : "경매 상품 1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 @@ -1769,14 +1788,14 @@

    응답

    "auctions" : [ { "id" : 2, "title" : "경매 상품 2", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 }, { "id" : 1, "title" : "경매 상품 1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 @@ -1911,14 +1930,14 @@

    응답

    "auctions" : [ { "id" : 2, "title" : "경매 상품 2", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 }, { "id" : 1, "title" : "경매 상품 1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "auctionPrice" : 1000, "status" : "UNBIDDEN", "auctioneerCount" : 2 @@ -2002,7 +2021,7 @@

    요청

    Table 3. /regions/{firstId}/{secondId}Table 4. /regions/{firstId}/{secondId}
    - +@@ -2049,7 +2068,7 @@

    응답

    { "auction" : { "id" : 1, - "images" : [ "http://localhost:8080/auctions/images/1" ], + "images" : [ "http://localhost:8080/auctions/images/auction_image.png" ], "title" : "경매 상품 1", "category" : { "main" : "main", @@ -2060,8 +2079,8 @@

    응답

    "lastBidPrice" : null, "status" : "UNBIDDEN", "bidUnit" : 1000, - "registerTime" : "2023-10-19T13:47:37", - "closingTime" : "2023-10-19T13:47:37", + "registerTime" : "2023-10-31T16:59:34", + "closingTime" : "2023-10-31T16:59:34", "directRegions" : [ { "first" : "서울특별시", "second" : "강서구", @@ -2071,7 +2090,7 @@

    응답

    }, "seller" : { "id" : 1, - "image" : "http://localhost:8080/users/images/1", + "image" : "http://localhost:8080/users/images/profile_image.png", "nickname" : "판매자", "reliability" : 3.5 }, @@ -2254,7 +2273,7 @@

    요청

    Table 4. /auctions/{auctionId}Table 5. /auctions/{auctionId}
    - +@@ -2405,7 +2424,7 @@

    요청

    Table 5. /auctions/{auctionId}Table 6. /auctions/{auctionId}
    - +@@ -2533,7 +2552,7 @@

    요청

    Table 6. /questions/{questionId}Table 7. /questions/{questionId}
    - +@@ -2587,9 +2606,9 @@

    응답

    "writer" : { "id" : 1, "name" : "질문자", - "image" : "http://localhost:8080/users/images/1" + "image" : "http://localhost:8080/users/images/profile_image1.png" }, - "createdTime" : "2023-10-19T13:47:37", + "createdTime" : "2023-10-31T16:59:34", "content" : "질문1", "isQuestioner" : false }, @@ -2598,9 +2617,9 @@

    응답

    "writer" : { "id" : 2, "name" : "판매자", - "image" : "http://localhost:8080/users/images/2" + "image" : "http://localhost:8080/users/images/profile_image2.png" }, - "createdTime" : "2023-10-19T13:47:37", + "createdTime" : "2023-10-31T16:59:34", "content" : "답변1" } }, { @@ -2609,9 +2628,9 @@

    응답

    "writer" : { "id" : 1, "name" : "질문자", - "image" : "http://localhost:8080/users/images/1" + "image" : "http://localhost:8080/users/images/profile_image1.png" }, - "createdTime" : "2023-10-19T13:47:37", + "createdTime" : "2023-10-31T16:59:34", "content" : "질문2", "isQuestioner" : false }, @@ -2620,9 +2639,9 @@

    응답

    "writer" : { "id" : 2, "name" : "판매자", - "image" : "http://localhost:8080/users/images/2" + "image" : "http://localhost:8080/users/images/profile_image2.png" }, - "createdTime" : "2023-10-19T13:47:37", + "createdTime" : "2023-10-31T16:59:34", "content" : "답변1" } } ] @@ -2819,7 +2838,7 @@

    입찰 조회

    요청

    -
    GET /bids/1 HTTP/1.1
    +
    GET /bids/-999 HTTP/1.1
     Content-Type: application/json
    @@ -2833,14 +2852,14 @@

    응답

    [ { "name" : "사용자1", - "profileImage" : "http://localhost:8080/users/images/1", + "profileImage" : "http://localhost:8080/users/images/profile_image1.png", "price" : 10000, - "bidTime" : "2023-10-19T13:47:42" + "bidTime" : "2023-10-31T16:59:40" }, { "name" : "사용자2", - "profileImage" : "http://localhost:8080/users/images/2", + "profileImage" : "http://localhost:8080/users/images/profile_image2.png", "price" : 12000, - "bidTime" : "2023-10-19T13:47:42" + "bidTime" : "2023-10-31T16:59:40" } ]
    @@ -3025,16 +3044,16 @@

    응답

    "chatPartner" : { "id" : 2, "name" : "구매자1", - "profileImage" : "http://localhost:8080/users/images/2" + "profileImage" : "http://localhost:8080/users/images/profile_image.png" }, "auction" : { "id" : 1, "title" : "경매1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "price" : 10000 }, "lastMessage" : { - "createdAt" : "2023-10-19T13:47:44", + "createdAt" : "2023-10-31T16:59:45", "contents" : "메시지1" }, "isChatAvailable" : true @@ -3043,16 +3062,16 @@

    응답

    "chatPartner" : { "id" : 3, "name" : "구매자2", - "profileImage" : "http://localhost:8080/users/images/3" + "profileImage" : "http://localhost:8080/users/images/profile_image.png" }, "auction" : { "id" : 2, "title" : "경매2", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "price" : 20000 }, "lastMessage" : { - "createdAt" : "2023-10-19T13:47:44", + "createdAt" : "2023-10-31T16:59:45", "contents" : "메시지2" }, "isChatAvailable" : true @@ -3164,7 +3183,7 @@

    요청

    Table 7. /questions/answers/{answerId}Table 8. /questions/answers/{answerId}
    - +@@ -3213,12 +3232,12 @@

    응답

    "chatPartner" : { "id" : 1, "name" : "판매자", - "profileImage" : "http://localhost:8080/users/images/1" + "profileImage" : "http://localhost:8080/users/images/profile_image.png" }, "auction" : { "id" : 1, "title" : "경매1", - "image" : "http://localhost:8080/auctions/images/1", + "image" : "http://localhost:8080/auctions/images/auction_image.png", "price" : 10000 }, "isChatAvailable" : true @@ -3315,7 +3334,7 @@

    요청

    Table 8. /chattings/{chatRoomId}Table 9. /chattings/{chatRoomId}
    - +@@ -3426,7 +3445,7 @@

    요청

    Table 9. /chattings/{chatRoomId}/messagesTable 10. /chattings/{chatRoomId}/messages
    - +@@ -3490,7 +3509,7 @@

    응답

    [ { "id" : 1, - "createdAt" : "2023-10-19T13:47:44", + "createdAt" : "2023-10-31T16:59:44", "isMyMessage" : true, "contents" : "메시지내용" } ] @@ -3640,7 +3659,7 @@

    응답

    "id" : 2, "name" : "회원1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3652,7 +3671,7 @@

    응답

    "id" : 3, "name" : "회원2" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3664,7 +3683,7 @@

    응답

    "id" : 4, "name" : "회원3" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "auction" : { "id" : 1, "title" : "제목" @@ -3828,7 +3847,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3839,7 +3858,7 @@

    응답

    "id" : 3, "name" : "구매자2" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3850,7 +3869,7 @@

    응답

    "id" : 3, "name" : "구매자2" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:54", "chatRoom" : { "id" : 1 }, @@ -3990,7 +4009,7 @@

    응답

    -

    질문 신고 조회

    +

    채팅방 신고 조회

    요청

    @@ -4014,7 +4033,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 1 }, @@ -4025,7 +4044,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 2 }, @@ -4036,7 +4055,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "question" : { "id" : 3 }, @@ -4176,7 +4195,7 @@

    응답

    -

    답변 신고 조회

    +

    채팅방 신고 조회

    요청

    @@ -4200,7 +4219,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 1 }, @@ -4211,7 +4230,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 2 }, @@ -4222,7 +4241,7 @@

    응답

    "id" : 2, "name" : "구매자1" }, - "createdTime" : "2023-10-19T13:47:52", + "createdTime" : "2023-10-31T16:59:55", "answer" : { "id" : 3 }, @@ -4452,7 +4471,7 @@

    요청

    Table 10. /chattings/{chatRoomId}/messagesTable 11. /chattings/{chatRoomId}/messages
    - +@@ -4523,7 +4542,7 @@

    요청

    Table 11. /reviews/{reviewId}Table 12. /reviews/{reviewId}
    - +@@ -4554,21 +4573,21 @@

    응답

    "writer" : { "id" : 2, "name" : "판매자2", - "profileImage" : "http://localhost:8080/users/images/2" + "profileImage" : "http://localhost:8080/users/images/profile_image2.png" }, "content" : "친절하다.", "score" : 5.0, - "createdTime" : "2023-10-19T13:47:53" + "createdTime" : "2023-10-31T16:59:56" }, { "id" : 2, "writer" : { "id" : 1, "name" : "판매자1", - "profileImage" : "http://localhost:8080/users/images/1" + "profileImage" : "http://localhost:8080/users/images/profile_image1.png" }, "content" : "친절하다.", "score" : 5.0, - "createdTime" : "2023-10-19T13:47:53" + "createdTime" : "2023-10-31T16:59:56" } ] @@ -4631,7 +4650,7 @@

    응답

    Table 12. /reviews/users/{userId}Table 13. /reviews/users/{userId}
    - +@@ -4726,7 +4745,7 @@

    응답

    @@ -4737,4 +4756,4 @@

    응답

    } - \ No newline at end of file + diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java index e58fe81f8..3848b1a3e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/domain/AuctionTest.java @@ -1,18 +1,19 @@ package com.ddang.ddang.auction.domain; -import static org.assertj.core.api.Assertions.assertThat; - import com.ddang.ddang.auction.domain.fixture.AuctionFixture; import com.ddang.ddang.bid.domain.Bid; import com.ddang.ddang.user.domain.User; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class AuctionTest extends AuctionFixture { @@ -352,6 +353,43 @@ class AuctionTest extends AuctionFixture { assertThat(actual).isEmpty(); } + @Test + void 마지막_입찰을_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) + .startPrice(new Price(1_000)) + .bidUnit(new BidUnit(1_000)) + .build(); + final Bid bid = new Bid(auction, 구매자, 유효한_입찰_금액); + + auction.updateLastBid(bid); + + // when + final Optional actual = auction.findLastBid(); + + // then + assertThat(actual).contains(bid); + } + + @Test + void 마지막_입찰이_없다면_빈_Optional을_반환한다() { + // given + final Auction auction = Auction.builder() + .title("제목") + .seller(판매자) + .closingTime(LocalDateTime.now().minusDays(3L)) + .build(); + + // when + final Optional actual = auction.findLastBid(); + + // then + assertThat(actual).isEmpty(); + } + @Test void 경매를_진행중이며_입찰자가_없는_경우_UNBIDDEN을_반환한다() { // given diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java index b92c77e17..d5951dfc4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionControllerFixture.java @@ -11,12 +11,13 @@ import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; import com.ddang.ddang.configuration.CommonControllerSliceTest; import com.fasterxml.jackson.core.JsonProcessingException; -import java.time.LocalDateTime; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import java.time.LocalDateTime; +import java.util.List; + @SuppressWarnings("NonAsciiCharacters") public class AuctionControllerFixture extends CommonControllerSliceTest { @@ -39,7 +40,7 @@ public class AuctionControllerFixture extends CommonControllerSliceTest { protected CreateInfoAuctionDto 경매_등록_결과_dto = new CreateInfoAuctionDto( 1L, "제목", - 1L, + "auction_image.png", 1_000 ); protected ReadChatRoomDto 쪽지방_dto = new ReadChatRoomDto(1L, true); @@ -148,12 +149,12 @@ void fixtureSetUp() throws JsonProcessingException { LocalDateTime.now(), LocalDateTime.now(), List.of(서울특별시_강서구_역삼동), - List.of(1L), + List.of("auction_image.png"), 2, "main", "sub", 1L, - 1L, + "profile_image.png", "판매자", 3.5d, false, @@ -172,12 +173,12 @@ void fixtureSetUp() throws JsonProcessingException { LocalDateTime.now(), LocalDateTime.now(), List.of(서울특별시_강서구_역삼동), - List.of(1L), + List.of("auction_image.png"), 2, "main", "sub", 1L, - 1L, + "profile_image.png", "판매자", 3.5d, false, @@ -196,12 +197,12 @@ void fixtureSetUp() throws JsonProcessingException { LocalDateTime.now(), LocalDateTime.now(), List.of(서울특별시_강서구_역삼동), - List.of(1L), + List.of("auction_image.png"), 2, "main", "sub", 1L, - 1L, + "profile_image.png", "판매자", 3.5d, false, diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java index 588ae7036..538d8f08c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionQuestionControllerFixture.java @@ -21,7 +21,7 @@ public class AuctionQuestionControllerFixture extends CommonControllerSliceTest protected ReadUserInQnaDto 질문자_정보_dto = new ReadUserInQnaDto( 1L, "질문자", - 1L, + "profile_image1.png", 4.5d, "12345", false @@ -29,7 +29,7 @@ public class AuctionQuestionControllerFixture extends CommonControllerSliceTest protected ReadUserInQnaDto 판매자_정보_dto = new ReadUserInQnaDto( 2L, "판매자", - 2L, + "profile_image2.png", 4.5d, "12346", false diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java index b0fbbf4f8..142df2be0 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/fixture/AuctionReviewControllerFixture.java @@ -15,8 +15,8 @@ public class AuctionReviewControllerFixture extends CommonControllerSliceTest { protected String 액세스_토큰 = "Bearer accessToken"; protected PrivateClaims 유효한_작성자_비공개_클레임 = new PrivateClaims(유효한_평가_작성자_아이디); protected Long 유효한_경매_아이디 = 1L; - protected Long 판매자_프로필_이미지_아이디 = 1L; - protected ReadUserInReviewDto 판매자 = new ReadUserInReviewDto(1L, "판매자", 판매자_프로필_이미지_아이디, 5.0d, "12347"); + protected String 판매자_프로필_이미지_저장_이름 = "profile_image.png"; + protected ReadUserInReviewDto 판매자 = new ReadUserInReviewDto(1L, "판매자", 판매자_프로필_이미지_저장_이름, 5.0d, "12347"); protected Long 구매자가_판매자에게_받은_평가_아이디 = 1L; protected ReadReviewDto 구매자가_판매자1에게_받은_평가 = new ReadReviewDto(구매자가_판매자에게_받은_평가_아이디, 판매자, "친절하다.", 5.0d, LocalDateTime.now()); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImplTest.java index d49dc0faa..3dd38ecd4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/infrastructure/persistence/BidRepositoryImplTest.java @@ -15,7 +15,6 @@ import org.springframework.context.annotation.Import; import java.util.List; -import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -56,13 +55,4 @@ void setUp(@Autowired final JpaBidRepository jpaBidRepository) { softAssertions.assertThat(actual.get(1).getId()).isEqualTo(경매1의_입찰2겸_마지막_입찰.getId()); }); } - - @Test - void 특정_경매의_마지막_입찰을_조회한다() { - // when - final Optional actual = bidRepository.findLastBidByAuctionId(경매1.getId()); - - // then - assertThat(actual.get().getId()).isEqualTo(경매1의_입찰2겸_마지막_입찰.getId()); - } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java index 1e28b17b7..0219c3ba7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/fixture/BidControllerFixture.java @@ -26,6 +26,6 @@ public class BidControllerFixture extends CommonControllerSliceTest { protected static CreateBidRequest 입찰액이_양수가_아닌_입찰_request1 = new CreateBidRequest(1L, -1); protected static CreateBidRequest 입찰액이_양수가_아닌_입찰_request2 = new CreateBidRequest(1L, 0); - protected ReadBidDto 입찰_정보_dto1 = new ReadBidDto("사용자1", 1L, false, 10_000, LocalDateTime.now()); - protected ReadBidDto 입찰_정보_dto2 = new ReadBidDto("사용자2", 2L, false, 12_000, LocalDateTime.now()); + protected ReadBidDto 입찰_정보_dto1 = new ReadBidDto("사용자1", "profile_image1.png", false, 10_000, LocalDateTime.now()); + protected ReadBidDto 입찰_정보_dto2 = new ReadBidDto("사용자2", "profile_image2.png", false, 12_000, LocalDateTime.now()); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java index de0cccb21..eeb42b1fb 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/ChatRoomServiceTest.java @@ -5,10 +5,13 @@ import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.fixture.ChatRoomServiceFixture; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.user.application.exception.UserNotFoundException; import org.assertj.core.api.SoftAssertions; @@ -16,6 +19,8 @@ import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; import java.util.List; @@ -23,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @IsolateDatabase +@RecordApplicationEvents @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") class ChatRoomServiceTest extends ChatRoomServiceFixture { @@ -30,6 +36,15 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { @Autowired ChatRoomService chatRoomService; + @Autowired + ReadMessageLogRepository readMessageLogRepository; + + @Autowired + MessageRepository messageRepository; + + @Autowired + ApplicationEvents events; + @Test void 채팅방을_생성한다() { // when @@ -39,6 +54,16 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { assertThat(actual).isPositive(); } + @Test + void 채팅방_생성시_메시지_로그_생성_이벤트가_호출된다() { + // when + chatRoomService.create(구매자.getId(), 채팅방_생성을_위한_DTO); + final long actual = events.stream(CreateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + @Test void 채팅방_생성시_요청한_사용자_정보를_찾을_수_없다면_예외가_발생한다() { // when & then @@ -96,8 +121,21 @@ class ChatRoomServiceTest extends ChatRoomServiceFixture { // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual).hasSize(2); - softAssertions.assertThat(actual.get(0)).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보); - softAssertions.assertThat(actual.get(1)).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보); + softAssertions.assertThat(actual.get(0).id()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.id()); + softAssertions.assertThat(actual.get(0).auctionDto()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.auctionDto()); + softAssertions.assertThat(actual.get(0).partnerDto()).isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.partnerDto()); + softAssertions.assertThat(actual.get(0).lastMessageDto()) + .isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.lastMessageDto()); + softAssertions.assertThat(actual.get(0).isChatAvailable()) + .isEqualTo(엔초_채팅_목록의_제이미_엔초_채팅방_정보.isChatAvailable()); + + softAssertions.assertThat(actual.get(1).id()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.id()); + softAssertions.assertThat(actual.get(1).auctionDto()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.auctionDto()); + softAssertions.assertThat(actual.get(1).partnerDto()).isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.partnerDto()); + softAssertions.assertThat(actual.get(1).lastMessageDto()) + .isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.lastMessageDto()); + softAssertions.assertThat(actual.get(1).isChatAvailable()) + .isEqualTo(엔초_채팅_목록의_엔초_지토_채팅방_정보.isChatAvailable()); }); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java new file mode 100644 index 000000000..5e0041468 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListenerTest.java @@ -0,0 +1,76 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.application.fixture.LastReadMessageLogEventListenerFixture; +import com.ddang.ddang.configuration.IsolateDatabase; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.verify; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class LastReadMessageLogEventListenerTest extends LastReadMessageLogEventListenerFixture { + + @Autowired + ApplicationEvents events; + + @Autowired + LastReadMessageLogEventListener lastReadMessageLogEventListener; + + @MockBean + LastReadMessageLogService lastReadMessageLogService; + + @Test + void 이벤트가_호출되면_메시지_로그를_저장한다() { + // given + willDoNothing().given(lastReadMessageLogService).create(any()); + + // when + lastReadMessageLogEventListener.create(생성용_메시지_조회_로그); + + // then + verify(lastReadMessageLogService).create(any()); + } + + @Test + void 메시지_로그_저장에_실패한_경우_예외가_발생하지_않는다() { + // given + willThrow(IllegalArgumentException.class).given(lastReadMessageLogService).create(any()); + + // when & then + assertDoesNotThrow(() -> lastReadMessageLogEventListener.create(생성용_메시지_조회_로그)); + } + + @Test + void 이벤트가_호출되면_메시지_로그를_업데이트한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + + // when + lastReadMessageLogEventListener.update(업데이트용_메시지_조회_로그); + + // then + verify(lastReadMessageLogService).update(any()); + } + + @Test + void 메시지_로그_업데이트에_실패한_경우_예외가_발생하지_않는다() { + // given + willThrow(ReadMessageLogNotFoundException.class).given(lastReadMessageLogService).update(any()); + + // when & then + assertDoesNotThrow(() -> lastReadMessageLogEventListener.update(업데이트용_메시지_조회_로그)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java new file mode 100644 index 000000000..5512fabe3 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/LastReadMessageLogServiceTest.java @@ -0,0 +1,75 @@ +package com.ddang.ddang.chat.application; + +import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; +import com.ddang.ddang.chat.application.fixture.LastReadMessageLogServiceFixture; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.configuration.IsolateDatabase; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.event.RecordApplicationEvents; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class LastReadMessageLogServiceTest extends LastReadMessageLogServiceFixture { + + @Autowired + LastReadMessageLogService lastReadMessageLogService; + + @Autowired + ReadMessageLogRepository readMessageLogRepository; + + @Test + void 메시지_로그를_생성한다() { + // given & when + lastReadMessageLogService.create(메시지_로그_생성용_이벤트); + final Optional actual_판매자 = readMessageLogRepository.findBy(메시지_로그_생성용_발신자_겸_판매자.getId(), 메시지_로그_생성용_채팅방.getId()); + final Optional actual_구매자 = readMessageLogRepository.findBy(메시지_로그_생성용_입찰자_구매자.getId(), 메시지_로그_생성용_채팅방.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_판매자).isPresent(); + softAssertions.assertThat(actual_구매자).isPresent(); + }); + } + + @Test + void 메시지_로그를_업데이트한다() { + // given & when + lastReadMessageLogService.update(메시지_로그_업데이트용_이벤트); + + final Optional actual_발신자 = readMessageLogRepository.findBy( + 메시지_로그_업데이트용_발신자_겸_판매자.getId(), + 메시지_로그_업데이트용_채팅방.getId() + ); + final Optional actual_입찰자 = readMessageLogRepository.findBy( + 메시지_로그_업데이트용_입찰자.getId(), + 메시지_로그_업데이트용_채팅방.getId() + ); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_발신자).isPresent(); + softAssertions.assertThat(actual_발신자.get().getLastReadMessageId()) + .isEqualTo(메시지_로그_업데이트용_마지막_조회_메시지.getId()); + softAssertions.assertThat(actual_입찰자.get().getLastReadMessageId()) + .isNotEqualTo(메시지_로그_업데이트용_마지막_조회_메시지.getId()); + }); + } + + @Test + void 메시지_로그_업데이트시_메시지_조회_로그가_존재하지_않으면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> lastReadMessageLogService.update(유효하지_않는_메시지_조회_로그)) + .isInstanceOf(ReadMessageLogNotFoundException.class); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java index a65cadb92..7794679c5 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java @@ -2,9 +2,11 @@ import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.fixture.MessageServiceFixture; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.notification.application.NotificationService; import com.ddang.ddang.notification.application.dto.CreateNotificationDto; @@ -25,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; @IsolateDatabase @RecordApplicationEvents @@ -35,9 +38,15 @@ class MessageServiceTest extends MessageServiceFixture { @Autowired MessageService messageService; + @Autowired + ReadMessageLogRepository readMessageLogRepository; + @MockBean NotificationService notificationService; + @MockBean + LastReadMessageLogService lastReadMessageLogService; + @Autowired ApplicationEvents events; @@ -101,6 +110,9 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 마지막_조회_메시지가_없는_경우_모든_메시지를_조회한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + // when final List actual = messageService.readAllByLastMessageId(마지막_조회_메시지_아이디가_없는_메시지_조회용_request); @@ -110,6 +122,9 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 첫_번째_메시지_이후에_생성된_모든_메시지를_조회한다() { + // given + willDoNothing().given(lastReadMessageLogService).update(any()); + // when final List actual = messageService.readAllByLastMessageId(두_번째_메시지부터_모든_메시지_조회용_request); @@ -126,6 +141,16 @@ class MessageServiceTest extends MessageServiceFixture { assertThat(readMessageDtos).isEmpty(); } + @Test + void 메시지를_조회할_경우_마지막으로_읽은_메시지_업데이트_이벤트를_호출한다() { + // when + messageService.readAllByLastMessageId(조회한_마지막_메시지가_5인_메시지_조회용_request); + final long actual = events.stream(UpdateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + @Test void 잘못된_사용자가_메시지를_조회할_경우_예외가_발생한다() { // when & then diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java index 234913411..e53afed2b 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/ChatRoomServiceFixture.java @@ -59,6 +59,8 @@ public class ChatRoomServiceFixture { protected User 엔초; protected User 제이미; protected User 지토; + protected User 채팅방을_생성하는_메리; + protected User 메리_경매_낙찰자_지토; protected User 경매에_참여한_적_없는_사용자; protected Auction 채팅방이_없는_경매; protected Auction 판매자_엔초_구매자_지토_경매; @@ -78,6 +80,7 @@ public class ChatRoomServiceFixture { protected CreateChatRoomDto 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO; protected CreateChatRoomDto 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO; protected CreateChatRoomDto 엔초_지토_채팅방_생성을_위한_DTO; + protected CreateChatRoomDto 메리가_생성하려는_채팅방; protected ReadParticipatingChatRoomDto 엔초가_조회한_엔초_지토_채팅방_정보_조회_결과; protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_가능; protected ReadChatRoomDto 엔초_지토_채팅방_정보_및_참여_불가능; @@ -120,32 +123,46 @@ void setUp() { .name("엔초") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12346") + .oauthId("12347") .build(); 제이미 = User.builder() .name("제이미") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12347") + .oauthId("12348") .build(); 지토 = User.builder() .name("지토") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12348") + .oauthId("12349") .build(); 경매에_참여한_적_없는_사용자 = User.builder() .name("외부인") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12349") + .oauthId("12340") .build(); + 채팅방을_생성하는_메리 = User.builder() + .name("채팅방을_생성하는_메리") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("09876") + .build(); + 메리_경매_낙찰자_지토 = User.builder() + .name("메리_경매_낙찰자_지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("10293") + .build(); userRepository.save(판매자); userRepository.save(구매자); userRepository.save(엔초); userRepository.save(제이미); userRepository.save(지토); userRepository.save(경매에_참여한_적_없는_사용자); + userRepository.save(채팅방을_생성하는_메리); + userRepository.save(메리_경매_낙찰자_지토); 채팅방이_없는_경매 = Auction.builder() .seller(판매자) @@ -192,25 +209,39 @@ void setUp() { .bidUnit(new BidUnit(1_000)) .closingTime(LocalDateTime.now()) .build(); + final Auction 판매자_메리_구매자_지토_경매 = Auction.builder() + .seller(채팅방을_생성하는_메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); 채팅방이_없는_경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); 판매자_엔초_구매자_지토_경매.addAuctionImages(List.of(엔초의_경매_대표_이미지, 엔초의_대표_이미지가_아닌_경매_이미지)); 판매자_제이미_구매자_엔초_경매.addAuctionImages(List.of(제이미의_경매_대표_이미지, 제이미의_대표_이미지가_아닌_경매_이미지)); + 판매자_메리_구매자_지토_경매.addAuctionImages(List.of(경매_대표_이미지, 대표_이미지가_아닌_경매_이미지)); auctionRepository.save(채팅방이_없는_경매); auctionRepository.save(종료되지_않은_경매); auctionRepository.save(낙찰자가_없는_경매); auctionRepository.save(판매자_엔초_구매자_지토_경매); auctionRepository.save(판매자_제이미_구매자_엔초_경매); + auctionRepository.save(판매자_메리_구매자_지토_경매); final Bid 채팅방_없는_경매_입찰 = new Bid(채팅방이_없는_경매, 구매자, new BidPrice(15_000)); final Bid 지토가_엔초_경매에_입찰 = new Bid(판매자_엔초_구매자_지토_경매, 지토, new BidPrice(15_000)); final Bid 엔초가_제이미_경매에_입찰 = new Bid(판매자_제이미_구매자_엔초_경매, 엔초, new BidPrice(15_000)); + final Bid 지토가_메리_경매에_입찰 = new Bid(판매자_메리_구매자_지토_경매, 메리_경매_낙찰자_지토, new BidPrice(15_000)); bidRepository.save(채팅방_없는_경매_입찰); bidRepository.save(지토가_엔초_경매에_입찰); bidRepository.save(엔초가_제이미_경매에_입찰); + bidRepository.save(지토가_메리_경매에_입찰); 채팅방이_없는_경매.updateLastBid(채팅방_없는_경매_입찰); 판매자_엔초_구매자_지토_경매.updateLastBid(지토가_엔초_경매에_입찰); 판매자_제이미_구매자_엔초_경매.updateLastBid(엔초가_제이미_경매에_입찰); + 판매자_메리_구매자_지토_경매.updateLastBid(지토가_메리_경매에_입찰); 엔초_지토_채팅방 = new ChatRoom(판매자_엔초_구매자_지토_경매, 지토); 제이미_엔초_채팅방 = new ChatRoom(판매자_제이미_구매자_엔초_경매, 엔초); @@ -239,6 +270,7 @@ void setUp() { 경매에_참여한_적_없는_사용자_정보 = new AuthenticationUserInfo(경매에_참여한_적_없는_사용자.getId()); 존재하지_않는_사용자_정보 = new AuthenticationUserInfo(존재하지_않는_사용자_아이디); 채팅방_생성을_위한_DTO = new CreateChatRoomDto(채팅방이_없는_경매.getId()); + 메리가_생성하려는_채팅방 = new CreateChatRoomDto(판매자_메리_구매자_지토_경매.getId()); 경매_정보가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(존재하지_않는_경매_아이디); 경매가_진행중이라서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(종료되지_않은_경매.getId()); 낙찰자가_없어서_채팅방을_생성할_수_없는_DTO = new CreateChatRoomDto(낙찰자가_없는_경매.getId()); @@ -253,6 +285,7 @@ void setUp() { ReadAuctionInChatRoomDto.of(판매자_제이미_구매자_엔초_경매, 제이미의_경매_대표_이미지), ReadUserInChatRoomDto.from(제이미), ReadLastMessageDto.from(제이미가_엔초에게_2시에_보낸_쪽지), + 1L, true ); 엔초_채팅_목록의_엔초_지토_채팅방_정보 = new ReadChatRoomWithLastMessageDto( @@ -260,6 +293,7 @@ void setUp() { ReadAuctionInChatRoomDto.of(판매자_엔초_구매자_지토_경매, 엔초의_경매_대표_이미지), ReadUserInChatRoomDto.from(지토), ReadLastMessageDto.from(엔초가_지토에게_1시에_보낸_쪽지), + 1L, true ); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java new file mode 100644 index 000000000..dc0bd5c35 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java @@ -0,0 +1,63 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import org.junit.jupiter.api.BeforeEach; + +@SuppressWarnings("NonAsciiCharacters") +public class LastReadMessageLogEventListenerFixture { + + protected CreateReadMessageLogEvent 생성용_메시지_조회_로그; + protected UpdateReadMessageLogEvent 업데이트용_메시지_조회_로그; + protected User 메시지_로그_생성용_발신자_겸_판매자; + protected User 메시지_로그_생성용_입찰자_구매자; + protected User 메시지_로그_업데이트용_발신자_겸_판매자; + protected User 메시지_로그_업데이트용_입찰자; + protected ChatRoom 메시지_로그_생성용_채팅방; + protected Auction 메시지_로그_생성용_경매; + + @BeforeEach + void setUp() { + 메시지_로그_생성용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_생성용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 메시지_로그_생성용_입찰자_구매자 = User.builder() + .name("메시지_로그_생성용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 메시지_로그_업데이트용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_업데이트용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 메시지_로그_업데이트용_입찰자 = User.builder() + .name("메시지_로그_업데이트용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + + 메시지_로그_생성용_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + + final Message 메시지 = Message.builder() + .chatRoom(메시지_로그_생성용_채팅방) + .writer(메시지_로그_생성용_발신자_겸_판매자) + .contents("메시지") + .build(); + + 업데이트용_메시지_조회_로그 = new UpdateReadMessageLogEvent(메시지_로그_생성용_발신자_겸_판매자, 메시지_로그_생성용_채팅방, 메시지); + 생성용_메시지_조회_로그 = new CreateReadMessageLogEvent(메시지_로그_생성용_채팅방); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java new file mode 100644 index 000000000..737969fd0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java @@ -0,0 +1,184 @@ +package com.ddang.ddang.chat.application.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.domain.repository.AuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.AuctionRepositoryImpl; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.QuerydslAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.infrastructure.persistence.BidRepositoryImpl; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ChatRoomRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ReadMessageLogRepositoryImpl; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.domain.repository.UserRepository; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.user.infrastructure.persistence.UserRepositoryImpl; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class LastReadMessageLogServiceFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + private AuctionRepository auctionRepository; + + private UserRepository userRepository; + + private ChatRoomRepository chatRoomRepository; + + private BidRepositoryImpl bidRepository; + + private ReadMessageLogRepository readMessageLogRepository; + + protected CreateReadMessageLogEvent 메시지_로그_생성용_이벤트; + protected UpdateReadMessageLogEvent 메시지_로그_업데이트용_이벤트; + protected UpdateReadMessageLogEvent 유효하지_않는_메시지_조회_로그; + protected User 메시지_로그_생성용_발신자_겸_판매자; + protected User 메시지_로그_생성용_입찰자_구매자; + protected User 메시지_로그_업데이트용_발신자_겸_판매자; + protected User 메시지_로그_업데이트용_입찰자; + protected User 저장되지_않은_사용자; + protected Auction 메시지_로그_생성용_경매; + protected Message 메시지_로그_생성용_마지막_조회_메시지; + protected Message 메시지_로그_업데이트용_마지막_조회_메시지; + protected Auction 메시지_로그_업데이트용_경매; + protected ChatRoom 메시지_로그_생성용_채팅방; + protected ChatRoom 저장되지_않은_채팅방; + protected ChatRoom 메시지_로그_업데이트용_채팅방; + + @BeforeEach + void fixtureSetUp( + @Autowired final JPAQueryFactory jpaQueryFactory, + @Autowired final JpaAuctionRepository jpaAuctionRepository, + @Autowired final JpaUserRepository jpaUserRepository, + @Autowired final JpaChatRoomRepository jpaChatRoomRepository, + @Autowired final JpaBidRepository jpaBidRepository, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository + ) { + auctionRepository = new AuctionRepositoryImpl(jpaAuctionRepository, new QuerydslAuctionRepository(jpaQueryFactory)); + userRepository = new UserRepositoryImpl(jpaUserRepository); + chatRoomRepository = new ChatRoomRepositoryImpl(jpaChatRoomRepository); + bidRepository = new BidRepositoryImpl(jpaBidRepository); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + + final Category 전자기기 = new Category("전자기기"); + final Category 전자기기_하위_노트북 = new Category("노트북"); + 전자기기.addSubCategory(전자기기_하위_노트북); + categoryRepository.save(전자기기); + + 메시지_로그_생성용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_생성용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 메시지_로그_생성용_입찰자_구매자 = User.builder() + .name("메시지_로그_생성용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + 메시지_로그_업데이트용_발신자_겸_판매자 = User.builder() + .name("메시지_로그_업데이트용_발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 메시지_로그_업데이트용_입찰자 = User.builder() + .name("메시지_로그_업데이트용_입찰자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); + 저장되지_않은_사용자 = User.builder() + .name("저장되지_않은_사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12349") + .build(); + userRepository.save(메시지_로그_생성용_발신자_겸_판매자); + userRepository.save(메시지_로그_생성용_입찰자_구매자); + userRepository.save(메시지_로그_업데이트용_발신자_겸_판매자); + userRepository.save(메시지_로그_업데이트용_입찰자); + + 메시지_로그_생성용_경매 = Auction.builder() + .title("경매") + .seller(메시지_로그_생성용_발신자_겸_판매자) + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + 메시지_로그_업데이트용_경매 = Auction.builder() + .seller(메시지_로그_업데이트용_발신자_겸_판매자) + .title("메시지_로그_업데이트용_경매") + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(메시지_로그_생성용_경매); + auctionRepository.save(메시지_로그_업데이트용_경매); + + final Bid 메시지_로그_생성용_입찰 = new Bid(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자, new BidPrice(15_000)); + final Bid 메시지_로그_업데이트용_입찰 = new Bid(메시지_로그_업데이트용_경매, 메시지_로그_업데이트용_입찰자, new BidPrice(15_000)); + bidRepository.save(메시지_로그_생성용_입찰); + bidRepository.save(메시지_로그_업데이트용_입찰); + 메시지_로그_생성용_경매.updateLastBid(메시지_로그_생성용_입찰); + 메시지_로그_업데이트용_경매.updateLastBid(메시지_로그_업데이트용_입찰); + + 메시지_로그_생성용_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + 저장되지_않은_채팅방 = new ChatRoom(메시지_로그_생성용_경매, 메시지_로그_생성용_입찰자_구매자); + 메시지_로그_업데이트용_채팅방 = new ChatRoom(메시지_로그_업데이트용_경매, 메시지_로그_업데이트용_발신자_겸_판매자); + chatRoomRepository.save(메시지_로그_생성용_채팅방); + chatRoomRepository.save(메시지_로그_업데이트용_채팅방); + + 메시지_로그_생성용_이벤트 = new CreateReadMessageLogEvent(메시지_로그_생성용_채팅방); + + 메시지_로그_생성용_마지막_조회_메시지 = Message.builder() + .writer(메시지_로그_생성용_발신자_겸_판매자) + .receiver(메시지_로그_생성용_입찰자_구매자) + .contents("메시지") + .build(); + 메시지_로그_업데이트용_마지막_조회_메시지 = Message.builder() + .writer(메시지_로그_업데이트용_발신자_겸_판매자) + .receiver(메시지_로그_업데이트용_입찰자) + .contents("메시지") + .build(); + final Message 저장되지_않은_메시지 = Message.builder() + .writer(저장되지_않은_사용자) + .contents("저장되지 않은 메시지") + .build(); + + final ReadMessageLog 메시지_로그_업데이트용_로그_판매자 = new ReadMessageLog(메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_발신자_겸_판매자); + final ReadMessageLog 메시지_로그_업데이트용_로그_구매자 = new ReadMessageLog(메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_입찰자); + readMessageLogRepository.saveAll(List.of(메시지_로그_업데이트용_로그_판매자, 메시지_로그_업데이트용_로그_구매자)); + + 메시지_로그_업데이트용_이벤트 = new UpdateReadMessageLogEvent(메시지_로그_업데이트용_발신자_겸_판매자, 메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_마지막_조회_메시지); + + 유효하지_않는_메시지_조회_로그 = new UpdateReadMessageLogEvent(저장되지_않은_사용자, 저장되지_않은_채팅방, 저장되지_않은_메시지); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java index 892695e39..01765bf70 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java @@ -49,9 +49,13 @@ public class MessageServiceFixture { protected ReadMessageRequest 마지막_조회_메시지_아이디가_없는_메시지_조회용_request; protected ReadMessageRequest 두_번째_메시지부터_모든_메시지_조회용_request; protected ReadMessageRequest 조회할_메시지가_더이상_없는_메시지_조회용_request; + protected ReadMessageRequest 조회한_마지막_메시지가_5인_메시지_조회용_request; protected ReadMessageRequest 유효하지_않은_사용자의_메시지_조회용_request; protected ReadMessageRequest 유효하지_않은_채팅방의_메시지_조회용_request; protected ReadMessageRequest 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request; + protected User 발신자; + protected ChatRoom 메시지가_5개인_채팅방; + protected Message 메시지가_5개인_채팅방_메시지의_마지막_메시지; protected String 이미지_절대_경로 = "/imageUrl"; protected int 메시지_총_개수 = 10; @@ -72,12 +76,12 @@ void setUp() { .build(); auctionRepository.save(경매); - final User 발신자 = User.builder() - .name("발신자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + 발신자 = User.builder() + .name("발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); final User 수신자 = User.builder() .name("수신자") .profileImage(new ProfileImage("upload.png", "store.png")) @@ -97,9 +101,11 @@ void setUp() { final ChatRoom 채팅방 = new ChatRoom(경매, 발신자); final ChatRoom 탈퇴한_사용자와의_채팅방 = new ChatRoom(경매, 탈퇴한_사용자); + 메시지가_5개인_채팅방 = new ChatRoom(경매, 발신자); chatRoomRepository.save(채팅방); chatRoomRepository.save(탈퇴한_사용자와의_채팅방); + chatRoomRepository.save(메시지가_5개인_채팅방); 메시지_생성_DTO = new CreateMessageDto( 채팅방.getId(), @@ -144,6 +150,19 @@ void setUp() { messageRepository.save(메시지); } + final List 메시지가_5개인_채팅방_메시지들 = new ArrayList<>(); + for (int count = 0; count < 5; count++) { + final Message 메시지 = Message.builder() + .writer(발신자) + .receiver(수신자) + .chatRoom(메시지가_5개인_채팅방) + .contents("메시지 내용") + .build(); + 메시지가_5개인_채팅방_메시지들.add(메시지); + messageRepository.save(메시지); + } + 메시지가_5개인_채팅방_메시지의_마지막_메시지 = 메시지가_5개인_채팅방_메시지들.get(4); + 마지막_조회_메시지_아이디가_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), null); 두_번째_메시지부터_모든_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(0).getId()); 조회할_메시지가_더이상_없는_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), 메시지들.get(메시지_총_개수 - 1) @@ -151,5 +170,6 @@ void setUp() { 유효하지_않은_사용자의_메시지_조회용_request = new ReadMessageRequest(-999L, 채팅방.getId(), null); 유효하지_않은_채팅방의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), -999L, null); 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 채팅방.getId(), -999L); + 조회한_마지막_메시지가_5인_메시지_조회용_request = new ReadMessageRequest(발신자.getId(), 메시지가_5개인_채팅방.getId(), null); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java index 0cb97b3e3..c12aa23f4 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/JpaMessageRepositoryTest.java @@ -4,8 +4,6 @@ import com.ddang.ddang.chat.infrastructure.persistence.fixture.JpaMessageRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java index 694cb6999..fc55b97c7 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/QuerydslChatRoomAndMessageAndImageRepositoryTest.java @@ -1,6 +1,9 @@ package com.ddang.ddang.chat.infrastructure.persistence; +import com.ddang.ddang.chat.domain.ReadMessageLog; import com.ddang.ddang.chat.domain.dto.ChatRoomAndMessageAndImageDto; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.chat.infrastructure.persistence.fixture.QuerydslChatRoomAndMessageAndImageRepositoryFixture; import com.ddang.ddang.configuration.JpaConfiguration; import com.ddang.ddang.configuration.QuerydslConfiguration; @@ -24,9 +27,41 @@ class QuerydslChatRoomAndMessageAndImageRepositoryTest extends QuerydslChatRoomA QuerydslChatRoomAndMessageAndImageRepository querydslChatRoomAndMessageAndImageRepository; + ReadMessageLogRepository readMessageLogRepository; + + MessageRepository messageRepository; + @BeforeEach - void setUp(@Autowired final JPAQueryFactory queryFactory) { + void setUp( + @Autowired final JPAQueryFactory queryFactory, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository, + @Autowired final JpaMessageRepository jpaMessageRepository + ) { querydslChatRoomAndMessageAndImageRepository = new QuerydslChatRoomAndMessageAndImageRepository(queryFactory); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + messageRepository = new MessageRepositoryImpl(jpaMessageRepository, new QuerydslMessageRepository(queryFactory)); + } + + @Test + void 사용자가_읽지_않은_메시지_개수를_반환한다() { + // given + readMessageLogRepository.saveAll(List.of(new ReadMessageLog(메리_엔초_채팅방, 메리), new ReadMessageLog(메리_엔초_채팅방, 엔초))); + + // when + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지1); + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지2); + messageRepository.save(메리가_엔초에게_3시에_보낸_쪽지3); + messageRepository.save(엔초가_메리에게_3시에_보낸_쪽지); + final List actual_엔초 = querydslChatRoomAndMessageAndImageRepository + .findAllChatRoomInfoByUserIdOrderByLastMessage(엔초.getId()); + final List actual_메리 = querydslChatRoomAndMessageAndImageRepository + .findAllChatRoomInfoByUserIdOrderByLastMessage(메리.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual_엔초.get(0).unreadMessageCount()).isEqualTo(3); + softAssertions.assertThat(actual_메리.get(0).unreadMessageCount()).isEqualTo(1); + }); } @Test @@ -37,16 +72,13 @@ void setUp(@Autowired final JPAQueryFactory queryFactory) { // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(actual).hasSize(3); + softAssertions.assertThat(actual).hasSize(2); softAssertions.assertThat(actual.get(0).chatRoom()).isEqualTo(엔초_지토_채팅방); softAssertions.assertThat(actual.get(0).message()).isEqualTo(엔초가_지토에게_5시에_보낸_쪽지); softAssertions.assertThat(actual.get(0).thumbnailImage()).isEqualTo(엔초의_경매_대표_이미지); softAssertions.assertThat(actual.get(1).chatRoom()).isEqualTo(제이미_엔초_채팅방); softAssertions.assertThat(actual.get(1).message()).isEqualTo(제이미가_엔초에게_4시에_보낸_쪽지); softAssertions.assertThat(actual.get(1).thumbnailImage()).isEqualTo(제이미의_경매_대표_이미지); - softAssertions.assertThat(actual.get(2).chatRoom()).isEqualTo(메리_엔초_채팅방); - softAssertions.assertThat(actual.get(2).message()).isEqualTo(메리가_엔초에게_3시에_보낸_쪽지); - softAssertions.assertThat(actual.get(2).thumbnailImage()).isEqualTo(메리의_경매_대표_이미지); }); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java new file mode 100644 index 000000000..ad254efd8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/ReadMessageLogRepositoryImplTest.java @@ -0,0 +1,55 @@ +package com.ddang.ddang.chat.infrastructure.persistence; + +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.fixture.ReadMessageLogRepositoryFixture; +import com.ddang.ddang.configuration.JpaConfiguration; +import com.ddang.ddang.configuration.QuerydslConfiguration; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({JpaConfiguration.class, QuerydslConfiguration.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ReadMessageLogRepositoryImplTest extends ReadMessageLogRepositoryFixture { + + ReadMessageLogRepository readMessageLogRepository; + + @BeforeEach + void setUp(@Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository) { + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + } + + @Test + void 마지막_읽은_메시지를_저장한다() { + // given + final ReadMessageLog actual = readMessageLogRepository.saveAll(List.of(다섯_번째_메시지까지_읽은_메시지_로그)).get(0); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + void 메시지_조회자_아이디와_채팅방_아이디에_해당하는_조회_메시지_로그를_반환한다() { + // given + final Optional actual = readMessageLogRepository.findBy(메리.getId(), 메리_엔초_채팅방.getId()); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).isPresent(); + softAssertions.assertThat(actual.get().getLastReadMessageId()).isEqualTo(다섯_번째_메시지.getId()); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java index 49bdf7e18..357cbf052 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/QuerydslChatRoomAndMessageAndImageRepositoryFixture.java @@ -58,15 +58,20 @@ public class QuerydslChatRoomAndMessageAndImageRepositoryFixture { private JpaMessageRepository messageRepository; protected User 엔초; + protected User 메리; protected AuctionImage 메리의_경매_대표_이미지; protected AuctionImage 엔초의_경매_대표_이미지; protected AuctionImage 제이미의_경매_대표_이미지; protected ChatRoom 메리_엔초_채팅방; protected ChatRoom 엔초_지토_채팅방; protected ChatRoom 제이미_엔초_채팅방; - protected Message 메리가_엔초에게_3시에_보낸_쪽지; protected Message 제이미가_엔초에게_4시에_보낸_쪽지; protected Message 엔초가_지토에게_5시에_보낸_쪽지; + protected Message 엔초가_지토에게_추가로_보낸_쪽지; + protected Message 메리가_엔초에게_3시에_보낸_쪽지1; + protected Message 메리가_엔초에게_3시에_보낸_쪽지2; + protected Message 메리가_엔초에게_3시에_보낸_쪽지3; + protected Message 엔초가_메리에게_3시에_보낸_쪽지; @BeforeEach void setUp() { @@ -80,24 +85,24 @@ void setUp() { .reliability(new Reliability(4.7d)) .oauthId("12346") .build(); - final User 메리 = User.builder() - .name("메리") - .profileImage(프로필_이미지) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + 메리 = User.builder() + .name("메리") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); final User 제이미 = User.builder() - .name("제이미") - .profileImage(프로필_이미지) - .reliability(new Reliability(4.7d)) - .oauthId("12347") - .build(); - final User 지토 = User.builder() - .name("지토") + .name("제이미") .profileImage(프로필_이미지) .reliability(new Reliability(4.7d)) - .oauthId("12348") + .oauthId("12347") .build(); + final User 지토 = User.builder() + .name("지토") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12348") + .build(); 메리의_경매_대표_이미지 = new AuctionImage("메리의_경매_대표_이미지.png", "메리의_경매_대표_이미지.png"); final AuctionImage 메리의_대표_이미지가_아닌_경매_이미지 = @@ -110,32 +115,32 @@ void setUp() { new AuctionImage("제이미의_대표 이미지가_아닌_경매_이미지.png", "제이미의_대표 이미지가_아닌_경매_이미지.png"); final Auction 메리의_경매 = Auction.builder() - .seller(메리) - .title("메리 맥북") - .description("메리 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Auction 엔초의_경매 = Auction.builder() - .seller(엔초) - .title("엔초 맥북") - .description("엔초 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(엔초) + .title("엔초 맥북") + .description("엔초 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Auction 제이미의_경매 = Auction.builder() - .seller(제이미) - .title("제이미 맥북") - .description("제이미 맥북 팔아요") - .subCategory(전자기기_서브_노트북_카테고리) - .startPrice(new Price(10_000)) - .bidUnit(new BidUnit(1_000)) - .closingTime(LocalDateTime.now()) - .build(); + .seller(제이미) + .title("제이미 맥북") + .description("제이미 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); final Bid 엔초가_메리_경매에_입찰 = new Bid(메리의_경매, 엔초, new BidPrice(15_000)); final Bid 지토가_엔초_경매에_입찰 = new Bid(엔초의_경매, 지토, new BidPrice(15_000)); @@ -146,36 +151,65 @@ void setUp() { 제이미_엔초_채팅방 = new ChatRoom(제이미의_경매, 엔초); final Message 제이미가_엔초에게_1시에_보낸_쪽지 = Message.builder() - .chatRoom(제이미_엔초_채팅방) - .contents("제이미가 엔초에게 1시애 보낸 쪽지") - .writer(제이미) - .receiver(엔초) - .build(); + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 1시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); final Message 엔초가_지토에게_2시에_보낸_쪽지 = Message.builder() - .chatRoom(엔초_지토_채팅방) - .contents("엔초가 지토에게 2시애 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); - 메리가_엔초에게_3시에_보낸_쪽지 = Message.builder() - .chatRoom(메리_엔초_채팅방) - .contents("메리가 엔초에게 3시에 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 2시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); 제이미가_엔초에게_4시에_보낸_쪽지 = Message.builder() - .chatRoom(제이미_엔초_채팅방) - .contents("제이미가 엔초에게 4시애 보낸 쪽지") - .writer(제이미) - .receiver(엔초) - .build(); + .chatRoom(제이미_엔초_채팅방) + .contents("제이미가 엔초에게 4시애 보낸 쪽지") + .writer(제이미) + .receiver(엔초) + .build(); 엔초가_지토에게_5시에_보낸_쪽지 = Message.builder() - .chatRoom(엔초_지토_채팅방) - .contents("엔초가 지토에게 5시애 보낸 쪽지") - .writer(엔초) - .receiver(지토) - .build(); - + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 5시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 엔초가_지토에게_추가로_보낸_쪽지 = Message.builder() + .chatRoom(엔초_지토_채팅방) + .contents("엔초가 지토에게 6시애 보낸 쪽지") + .writer(엔초) + .receiver(지토) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지1 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지1 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지2") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지2 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지3") + .writer(메리) + .receiver(엔초) + .build(); + 메리가_엔초에게_3시에_보낸_쪽지3 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("메리가 엔초에게 3시에 보낸 쪽지3") + .writer(메리) + .receiver(엔초) + .build(); + 엔초가_메리에게_3시에_보낸_쪽지 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .contents("엔초가 메리에게 3시에 보낸 쪽지3") + .writer(엔초) + .receiver(메리) + .build(); 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); categoryRepository.save(전자기기_카테고리); @@ -206,7 +240,6 @@ void setUp() { List.of( 제이미가_엔초에게_1시에_보낸_쪽지, 엔초가_지토에게_2시에_보낸_쪽지, - 메리가_엔초에게_3시에_보낸_쪽지, 제이미가_엔초에게_4시에_보낸_쪽지, 엔초가_지토에게_5시에_보낸_쪽지 ) diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java new file mode 100644 index 000000000..02c48e537 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/infrastructure/persistence/fixture/ReadMessageLogRepositoryFixture.java @@ -0,0 +1,149 @@ +package com.ddang.ddang.chat.infrastructure.persistence.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.domain.repository.AuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.AuctionRepositoryImpl; +import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository; +import com.ddang.ddang.auction.infrastructure.persistence.QuerydslAuctionRepository; +import com.ddang.ddang.bid.domain.Bid; +import com.ddang.ddang.bid.domain.BidPrice; +import com.ddang.ddang.bid.domain.repository.BidRepository; +import com.ddang.ddang.bid.infrastructure.persistence.BidRepositoryImpl; +import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.domain.ReadMessageLog; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ChatRoomRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.JpaChatRoomRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.JpaReadMessageLogRepository; +import com.ddang.ddang.chat.infrastructure.persistence.MessageRepositoryImpl; +import com.ddang.ddang.chat.infrastructure.persistence.QuerydslMessageRepository; +import com.ddang.ddang.chat.infrastructure.persistence.ReadMessageLogRepositoryImpl; +import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.domain.repository.UserRepository; +import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository; +import com.ddang.ddang.user.infrastructure.persistence.UserRepositoryImpl; +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("NonAsciiCharacters") +public class ReadMessageLogRepositoryFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + private AuctionRepository auctionRepository; + + private UserRepository userRepository; + + private BidRepository bidRepository; + + private ChatRoomRepository chatRoomRepository; + + private MessageRepository messageRepository; + + private ReadMessageLogRepositoryImpl readMessageLogRepository; + + protected ChatRoom 메리_엔초_채팅방; + protected User 메리; + protected User 엔초; + protected ReadMessageLog 다섯_번째_메시지까지_읽은_메시지_로그; + protected Message 다섯_번째_메시지; + + protected AuctionImage 메리의_경매_대표_이미지 = new AuctionImage("메리_경매_대표_이미지.png", "메리의_경매_대표_이미지.png"); + protected AuctionImage 메리의_대표_이미지가_아닌_경매_이미지 = new AuctionImage("메리의_대표 이미지가_아닌_경매_이미지.png", "메리의_대표 이미지가_아닌_경매_이미지.png"); + protected ProfileImage 프로필_이미지 = new ProfileImage("upload.png", "store.png"); + + @BeforeEach + void fixtureSetUp( + @Autowired final JPAQueryFactory jpaQueryFactory, + @Autowired final JpaAuctionRepository jpaAuctionRepository, + @Autowired final JpaUserRepository jpaUserRepository, + @Autowired final JpaBidRepository jpaBidRepository, + @Autowired final JpaChatRoomRepository jpaChatRoomRepository, + @Autowired final JpaMessageRepository jpaMessageRepository, + @Autowired final JpaReadMessageLogRepository jpaReadMessageLogRepository + ) { + auctionRepository = new AuctionRepositoryImpl(jpaAuctionRepository, new QuerydslAuctionRepository(jpaQueryFactory)); + userRepository = new UserRepositoryImpl(jpaUserRepository); + bidRepository = new BidRepositoryImpl(jpaBidRepository); + chatRoomRepository = new ChatRoomRepositoryImpl(jpaChatRoomRepository); + messageRepository = new MessageRepositoryImpl(jpaMessageRepository, new QuerydslMessageRepository(jpaQueryFactory)); + readMessageLogRepository = new ReadMessageLogRepositoryImpl(jpaReadMessageLogRepository); + + final Category 전자기기_카테고리 = new Category("전자기기"); + final Category 전자기기_서브_노트북_카테고리 = new Category("노트북 카테고리"); + 전자기기_카테고리.addSubCategory(전자기기_서브_노트북_카테고리); + categoryRepository.save(전자기기_카테고리); + + 메리 = User.builder() + .name("메리_판매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 엔초 = User.builder() + .name("엔초_구매자") + .profileImage(프로필_이미지) + .reliability(new Reliability(4.7d)) + .oauthId("14567") + .build(); + userRepository.save(메리); + userRepository.save(엔초); + + final Auction 판매자_메리_구매자_엔초_경매 = Auction.builder() + .seller(메리) + .title("메리 맥북") + .description("메리 맥북 팔아요") + .subCategory(전자기기_서브_노트북_카테고리) + .startPrice(new Price(10_000)) + .bidUnit(new BidUnit(1_000)) + .closingTime(LocalDateTime.now()) + .build(); + 판매자_메리_구매자_엔초_경매.addAuctionImages(List.of(메리의_경매_대표_이미지, 메리의_대표_이미지가_아닌_경매_이미지)); + auctionRepository.save(판매자_메리_구매자_엔초_경매); + + + final Bid 엔초가_메리_경매에_입찰 = new Bid(판매자_메리_구매자_엔초_경매, 엔초, new BidPrice(15_000)); + bidRepository.save(엔초가_메리_경매에_입찰); + + 판매자_메리_구매자_엔초_경매.updateLastBid(엔초가_메리_경매에_입찰); + 메리_엔초_채팅방 = new ChatRoom(판매자_메리_구매자_엔초_경매, 엔초); + chatRoomRepository.save(메리_엔초_채팅방); + + List 메리_엔초_메시지들 = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + final Message 메시지 = Message.builder() + .chatRoom(메리_엔초_채팅방) + .writer(메리) + .receiver(엔초) + .contents("안녕하세요") + .build(); + messageRepository.save(메시지); + 메리_엔초_메시지들.add(메시지); + } + + 다섯_번째_메시지 = 메리_엔초_메시지들.get(4); + 다섯_번째_메시지까지_읽은_메시지_로그 = new ReadMessageLog(메리_엔초_채팅방, 메리); + 다섯_번째_메시지까지_읽은_메시지_로그.updateLastReadMessage(다섯_번째_메시지.getId()); + + final ReadMessageLog 메리_엔초_채팅방의_메리_메시지_조회_로그 = new ReadMessageLog(메리_엔초_채팅방, 메리); + 메리_엔초_채팅방의_메리_메시지_조회_로그.updateLastReadMessage(다섯_번째_메시지.getId()); + readMessageLogRepository.saveAll(List.of(메리_엔초_채팅방의_메리_메시지_조회_로그)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java index 76b29dda0..5d9f61dcb 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java @@ -241,19 +241,15 @@ void setUp() { .andExpectAll( status().isOk(), jsonPath("$.[0].id", is(조회용_채팅방1.id()), Long.class), - jsonPath("$.[0].chatPartner.name", is(조회용_채팅방1.partnerDto() - .name())), - jsonPath("$.[0].auction.title", is(조회용_채팅방1.auctionDto() - .title())), - jsonPath("$.[0].lastMessage.contents", is(조회용_채팅방1.lastMessageDto() - .contents())), + jsonPath("$.[0].chatPartner.name", is(조회용_채팅방1.partnerDto().name())), + jsonPath("$.[0].auction.title", is(조회용_채팅방1.auctionDto().title())), + jsonPath("$.[0].lastMessage.contents", is(조회용_채팅방1.lastMessageDto().contents())), + jsonPath("$.[0].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class), jsonPath("$.[1].id", is(조회용_채팅방2.id()), Long.class), - jsonPath("$.[1].chatPartner.name", is(조회용_채팅방2.partnerDto() - .name())), - jsonPath("$.[1].auction.title", is(조회용_채팅방2.auctionDto() - .title())), - jsonPath("$.[1].lastMessage.contents", is(조회용_채팅방2.lastMessageDto() - .contents())) + jsonPath("$.[1].chatPartner.name", is(조회용_채팅방2.partnerDto().name())), + jsonPath("$.[1].auction.title", is(조회용_채팅방2.auctionDto().title())), + jsonPath("$.[1].lastMessage.contents", is(조회용_채팅방2.lastMessageDto().contents())), + jsonPath("$.[1].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class) ); readAllParticipatingChatRooms_문서화(resultActions); } @@ -286,10 +282,8 @@ void setUp() { .andExpectAll( status().isOk(), jsonPath("$.id", is(조회용_참가중인_채팅방.id()), Long.class), - jsonPath("$.chatPartner.name", is(조회용_참가중인_채팅방.partnerDto() - .name())), - jsonPath("$.auction.title", is(조회용_참가중인_채팅방.auctionDto() - .title())) + jsonPath("$.chatPartner.name", is(조회용_참가중인_채팅방.partnerDto().name())), + jsonPath("$.auction.title", is(조회용_참가중인_채팅방.auctionDto().title())) ); readChatRoom_문서화(resultActions); } @@ -539,6 +533,8 @@ void setUp() { .description("메시지를 보낸 시간"), fieldWithPath("[].lastMessage.contents").type(JsonFieldType.STRING) .description("메시지 내용"), + fieldWithPath("[].unreadMessageCount").type(JsonFieldType.NUMBER) + .description("안 읽은 메시지 개수"), fieldWithPath("[].isChatAvailable").type(JsonFieldType.BOOLEAN) .description("채팅 가능 여부") ) diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java index 8f7b5ddaa..39e081602 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/fixture/ChatRoomControllerFixture.java @@ -18,13 +18,13 @@ public class ChatRoomControllerFixture extends CommonControllerSliceTest { private Long 탈퇴한_사용자_아이디 = 5L; protected PrivateClaims 사용자_ID_클레임 = new PrivateClaims(1L); - protected ReadUserInChatRoomDto 판매자 = new ReadUserInChatRoomDto(1L, "판매자", 1L, 5.0d, false); - private ReadUserInChatRoomDto 구매자1 = new ReadUserInChatRoomDto(2L, "구매자1", 2L, 5.0d, false); - private ReadUserInChatRoomDto 구매자2 = new ReadUserInChatRoomDto(3L, "구매자2", 3L, 5.0d, false); - private ReadAuctionInChatRoomDto 조회용_경매1 = new ReadAuctionInChatRoomDto(1L, "경매1", 10_000, 1L); - private ReadAuctionInChatRoomDto 조회용_경매2 = new ReadAuctionInChatRoomDto(2L, "경매2", 20_000, 1L); - protected ReadChatRoomWithLastMessageDto 조회용_채팅방1 = new ReadChatRoomWithLastMessageDto(1L, 조회용_경매1, 구매자1, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자1, "메시지1"), true); - protected ReadChatRoomWithLastMessageDto 조회용_채팅방2 = new ReadChatRoomWithLastMessageDto(2L, 조회용_경매2, 구매자2, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자2, "메시지2"), true); + protected ReadUserInChatRoomDto 판매자 = new ReadUserInChatRoomDto(1L, "판매자", "profileImage1.png", 5.0d, false); + private ReadUserInChatRoomDto 구매자1 = new ReadUserInChatRoomDto(2L, "구매자1", "profileImage2.png", 5.0d, false); + private ReadUserInChatRoomDto 구매자2 = new ReadUserInChatRoomDto(3L, "구매자2", "profileImage3.png", 5.0d, false); + private ReadAuctionInChatRoomDto 조회용_경매1 = new ReadAuctionInChatRoomDto(1L, "경매1", 10_000, "auctionImage1.png"); + private ReadAuctionInChatRoomDto 조회용_경매2 = new ReadAuctionInChatRoomDto(2L, "경매2", 20_000, "auctionImage2.png"); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방1 = new ReadChatRoomWithLastMessageDto(1L, 조회용_경매1, 구매자1, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자1, "메시지1"), 1L, true); + protected ReadChatRoomWithLastMessageDto 조회용_채팅방2 = new ReadChatRoomWithLastMessageDto(2L, 조회용_경매2, 구매자2, new ReadLastMessageDto(1L, LocalDateTime.now(), 판매자, 구매자2, "메시지2"), 1L, true); protected CreateMessageRequest 메시지_생성_요청 = new CreateMessageRequest(1L, "메시지 내용"); protected CreateMessageRequest 유효하지_않은_발신자의_메시지_생성_요청 = new CreateMessageRequest(-999L, "메시지 내용"); protected CreateMessageRequest 탈퇴한_사용자와의_메시지_생성_요청 = new CreateMessageRequest(탈퇴한_사용자_아이디, "메시지 내용"); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java index e2044c85c..b86351be2 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/ImageServiceTest.java @@ -24,7 +24,7 @@ class ImageServiceTest extends ImageServiceFixture { @Test void 지정한_아이디에_해당하는_프로필_이미지를_조회한다() throws Exception { // when - final Resource actual = imageService.readProfileImage(프로필_이미지.getId()); + final Resource actual = imageService.readProfileImage(프로필_이미지.getImage().getStoreName()); // then assertThat(actual.getFilename()).isEqualTo(프로필_이미지_파일명); @@ -33,7 +33,7 @@ class ImageServiceTest extends ImageServiceFixture { @Test void 지정한_아이디에_해당하는_프로필_이미지가_없는_경우_null을_반환한다() throws MalformedURLException { // when - final Resource actual = imageService.readProfileImage(존재하지_않는_프로필_이미지_아이디); + final Resource actual = imageService.readProfileImage(존재하지_않는_프로필_이미지_이름); // then assertThat(actual).isNull(); @@ -42,7 +42,7 @@ class ImageServiceTest extends ImageServiceFixture { @Test void 지정한_아이디에_해당하는_경매_이미지를_조회한다() throws Exception { // when - final Resource actual = imageService.readAuctionImage(경매_이미지.getId()); + final Resource actual = imageService.readAuctionImage(경매_이미지.getImage().getStoreName()); // then assertThat(actual.getFilename()).isEqualTo(경매_이미지_파일명); @@ -51,7 +51,7 @@ class ImageServiceTest extends ImageServiceFixture { @Test void 지정한_아이디에_해당하는_경매_이미지가_없는_경우_null을_반환한다() throws MalformedURLException { // when - final Resource actual = imageService.readAuctionImage(존재하지_않는_경매_이미지_아이디); + final Resource actual = imageService.readAuctionImage(존재하지_않는_경매_이미지_이름); // then assertThat(actual).isNull(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java index c70725205..02232306a 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/fixture/ImageServiceFixture.java @@ -16,8 +16,8 @@ public class ImageServiceFixture { @Autowired private JpaAuctionImageRepository auctionImageRepository; - protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; - protected Long 존재하지_않는_경매_이미지_아이디 = -999L; + protected String 존재하지_않는_프로필_이미지_이름 = "invalid_profile.png"; + protected String 존재하지_않는_경매_이미지_이름 = "invalid_auction.png"; protected ProfileImage 프로필_이미지; protected String 프로필_이미지_파일명; diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessorTest.java similarity index 57% rename from backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java rename to backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessorTest.java index a016206cd..6f3b80589 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageIdProcessorTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/ImageStoreNameProcessorTest.java @@ -1,6 +1,6 @@ package com.ddang.ddang.image.application.util; -import com.ddang.ddang.image.application.util.fixture.ImageIdProcessorFixture; +import com.ddang.ddang.image.application.util.fixture.ImageStoreNameProcessorFixture; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -10,24 +10,24 @@ @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") -class ImageIdProcessorTest extends ImageIdProcessorFixture { +class ImageStoreNameProcessorTest extends ImageStoreNameProcessorFixture { @Test void 경매_이미지가_null이_아니라면_경매_이미지_아이디를_반환한다() { // given - given(경매_이미지.getId()).willReturn(경매_이미지_아이디); + given(경매_이미지.getImage()).willReturn(경매_이미지_데이터_VO); // when - final Long actual = ImageIdProcessor.process(경매_이미지); + final String actual = ImageStoreNameProcessor.process(경매_이미지); // then - assertThat(actual).isEqualTo(경매_이미지_아이디); + assertThat(actual).isEqualTo(경매_이미지_저장_이름); } @Test void 경매_이미지가_null이면_경매_이미지_아이디를_null로_반환한다() { // when - final Long actual = ImageIdProcessor.process(null인_경매_이미지); + final String actual = ImageStoreNameProcessor.process(null인_경매_이미지); // then assertThat(actual).isNull(); @@ -36,19 +36,19 @@ class ImageIdProcessorTest extends ImageIdProcessorFixture { @Test void 프로필_이미지가_null이_아니라면_프로필_이미지_아이디를_반환한다() { // given - given(프로필_이미지.getId()).willReturn(프로필_이미지_아이디); + given(프로필_이미지.getImage()).willReturn(프로필_이미지_데이터_VO); // when - final Long actual = ImageIdProcessor.process(프로필_이미지); + final String actual = ImageStoreNameProcessor.process(프로필_이미지); // then - assertThat(actual).isEqualTo(프로필_이미지_아이디); + assertThat(actual).isEqualTo(프로필_이미지_저장_이름); } @Test void 프로필_이미지가_null이면_프로필_이미지_아이디를_null로_반환한다() { // when - final Long actual = ImageIdProcessor.process(null인_프로필_이미지); + final String actual = ImageStoreNameProcessor.process(null인_프로필_이미지); // then assertThat(actual).isNull(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageStoreNameProcessorFixture.java similarity index 52% rename from backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java rename to backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageStoreNameProcessorFixture.java index 3fa95dbd4..d4c67a8f8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageIdProcessorFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/application/util/fixture/ImageStoreNameProcessorFixture.java @@ -1,18 +1,20 @@ package com.ddang.ddang.image.application.util.fixture; import com.ddang.ddang.image.domain.AuctionImage; +import com.ddang.ddang.image.domain.Image; import com.ddang.ddang.image.domain.ProfileImage; import static org.mockito.Mockito.mock; @SuppressWarnings("NonAsciiCharacters") -public class ImageIdProcessorFixture { +public class ImageStoreNameProcessorFixture { protected AuctionImage 경매_이미지 = mock(AuctionImage.class); protected AuctionImage null인_경매_이미지 = null; - protected Long 경매_이미지_아이디 = 1L; - protected ProfileImage 프로필_이미지 = mock(ProfileImage.class); protected ProfileImage null인_프로필_이미지 = null; - protected Long 프로필_이미지_아이디 = 1L; + protected String 경매_이미지_저장_이름 = "auction_image.png"; + protected String 프로필_이미지_저장_이름 = "profile_image.png"; + protected Image 프로필_이미지_데이터_VO = new Image("upload.png", 프로필_이미지_저장_이름); + protected Image 경매_이미지_데이터_VO = new Image("upload.png", 경매_이미지_저장_이름); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImplTest.java index a77fea90e..bb97c2993 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/AuctionImageRepositoryImplTest.java @@ -1,7 +1,6 @@ package com.ddang.ddang.image.infrastructure.persistence; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.domain.repository.AuctionImageRepository; import com.ddang.ddang.image.infrastructure.persistence.fixture.AuctionImageRepositoryImplFixture; import org.junit.jupiter.api.BeforeEach; @@ -12,8 +11,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -30,11 +27,20 @@ void setUp(@Autowired final JpaAuctionImageRepository jpaAuctionImageRepository) } @Test - void 경매_이미지를_아이디를_통해_조회한다() { + void 경매_이미지_이름에_해당하는_경매_이미지가_존재하면_참을_반환한다() { + // when + final boolean actual = auctionImageRepository.existsByStoreName(존재하는_경매_이미지_이름); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 경매_이미지_이름에_해당하는_경매_이미지가_존재하지_않으면_거짓을_반환한다() { // when - final Optional actual = auctionImageRepository.findById(경매_이미지.getId()); + final boolean actual = auctionImageRepository.existsByStoreName(존재하지_않는_경매_이미지_이름); // then - assertThat(actual).contains(경매_이미지); + assertThat(actual).isFalse(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java index 3689fd8be..809345b48 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaAuctionImageRepositoryTest.java @@ -3,8 +3,6 @@ import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.infrastructure.persistence.fixture.JpaAuctionImageRepositoryFixture; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -12,8 +10,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -22,9 +18,6 @@ @SuppressWarnings("NonAsciiCharacters") class JpaAuctionImageRepositoryTest extends JpaAuctionImageRepositoryFixture { - @PersistenceContext - EntityManager em; - @Autowired JpaAuctionImageRepository auctionImageRepository; @@ -37,27 +30,24 @@ class JpaAuctionImageRepositoryTest extends JpaAuctionImageRepositoryFixture { final AuctionImage actual = auctionImageRepository.save(auctionImage); // then - em.flush(); - em.clear(); - assertThat(actual.getId()).isPositive(); } @Test - void 지정한_아이디에_해당하는_경매_이미지를_조회한다() { + void 경매_이미지_이름에_해당하는_경매_이미지가_존재하면_참을_반환한다() { // when - final Optional actual = auctionImageRepository.findById(경매_이미지.getId()); + final boolean actual = auctionImageRepository.existsByStoreName(존재하는_경매_이미지_이름); // then - assertThat(actual).contains(경매_이미지); + assertThat(actual).isTrue(); } @Test - void 지정한_아이디에_해당하는_경매_이미지가_없는_경우_빈_Optional을_반환한다() { + void 경매_이미지_이름에_해당하는_경매_이미지가_존재하지_않으면_거짓을_반환한다() { // when - final Optional actual = auctionImageRepository.findById(존재하지_않는_경매_이미지_아이디); + final boolean actual = auctionImageRepository.existsByStoreName(존재하지_않는_경매_이미지_이름); // then - assertThat(actual).isEmpty(); + assertThat(actual).isFalse(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java index 59db38093..72a9e6fc6 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/JpaProfileImageRepositoryTest.java @@ -4,8 +4,6 @@ import com.ddang.ddang.configuration.QuerydslConfiguration; import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.infrastructure.persistence.fixture.JpaProfileImageRepositoryFixture; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -13,8 +11,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -23,9 +19,6 @@ @SuppressWarnings("NonAsciiCharacters") class JpaProfileImageRepositoryTest extends JpaProfileImageRepositoryFixture { - @PersistenceContext - EntityManager em; - @Autowired JpaProfileImageRepository profileImageRepository; @@ -37,37 +30,25 @@ class JpaProfileImageRepositoryTest extends JpaProfileImageRepositoryFixture { // when final ProfileImage actual = profileImageRepository.save(profileImage); - em.flush(); - em.clear(); - // then assertThat(actual.getId()).isPositive(); } @Test - void 지정한_아이디에_해당하는_이미지를_조회한다() { - // when - final Optional actual = profileImageRepository.findById(프로필_이미지.getId()); - - // then - assertThat(actual).contains(프로필_이미지); - } - - @Test - void 지정한_아이디에_해당하는_이미지가_없는_경우_빈_Optional을_반환한다() { + void 프로필을_이미지_이름에_해당하는_프로필_이미지가_존재하면_참을_반환한다() { // when - final Optional actual = profileImageRepository.findById(존재하지_않는_프로필_이미지_아이디); + final boolean actual = profileImageRepository.existsByStoreName(존재하는_프로필_이미지_이름); // then - assertThat(actual).isEmpty(); + assertThat(actual).isTrue(); } @Test - void 저장된_이름에_해당하는_이미지를_반환한다() { + void 프로필을_이미지_이름에_해당하는_프로필_이미지가_존재하지_않으면_참을_반환한다() { // when - final Optional actual = profileImageRepository.findByStoreName(저장된_이미지_파일명); + final boolean actual = profileImageRepository.existsByStoreName(존재하지_않는_프로필_이미지_이름); // then - assertThat(actual).contains(프로필_이미지); + assertThat(actual).isFalse(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImplTest.java index 7f733ae97..804c98876 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/ProfileImageRepositoryImplTest.java @@ -1,7 +1,6 @@ package com.ddang.ddang.image.infrastructure.persistence; import com.ddang.ddang.configuration.QuerydslConfiguration; -import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.domain.repository.ProfileImageRepository; import com.ddang.ddang.image.infrastructure.persistence.fixture.ProfileImageRepositoryImplFixture; import org.junit.jupiter.api.BeforeEach; @@ -12,8 +11,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import java.util.Optional; - import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @@ -30,20 +27,20 @@ void setUp(@Autowired final JpaProfileImageRepository jpaProfileImageRepository) } @Test - void 프로필을_이미지를_아이디를_통해_조회한다() { + void 프로필을_이미지_이름에_해당하는_프로필_이미지가_존재하면_참을_반환한다() { // when - final Optional actual = profileImageRepository.findById(프로필_이미지.getId()); + final boolean actual = profileImageRepository.existsByStoreName(존재하는_프로필_이미지_이름); // then - assertThat(actual).contains(프로필_이미지); + assertThat(actual).isTrue(); } @Test - void 프로필을_이미지를_저장_이미지를_통해_조회한다() { + void 프로필을_이미지_이름에_해당하는_프로필_이미지가_존재하지_않으면_참을_반환한다() { // when - final Optional actual = profileImageRepository.findByStoreName(프로필_이미지.getImage().getStoreName()); + final boolean actual = profileImageRepository.existsByStoreName(존재하지_않는_프로필_이미지_이름); // then - assertThat(actual).contains(프로필_이미지); + assertThat(actual).isFalse(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/AuctionImageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/AuctionImageRepositoryImplFixture.java index a3a88d949..e04980bc3 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/AuctionImageRepositoryImplFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/AuctionImageRepositoryImplFixture.java @@ -2,29 +2,22 @@ import com.ddang.ddang.image.domain.AuctionImage; import com.ddang.ddang.image.infrastructure.persistence.JpaAuctionImageRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @SuppressWarnings("NonAsciiCharacters") public class AuctionImageRepositoryImplFixture { - @PersistenceContext - private EntityManager em; - @Autowired private JpaAuctionImageRepository jpaAuctionImageRepository; - protected AuctionImage 경매_이미지; + protected String 존재하는_경매_이미지_이름 = "경매이미지.png"; + protected String 존재하지_않는_경매_이미지_이름 = "invalid.png"; @BeforeEach void fixtureSetUp() { - 경매_이미지 = new AuctionImage("경매이미지.png", "경매이미지.png"); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.png", 존재하는_경매_이미지_이름); jpaAuctionImageRepository.save(경매_이미지); - - em.flush(); - em.clear(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java index a92e8ec51..2f4a7d66d 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaAuctionImageRepositoryFixture.java @@ -18,13 +18,13 @@ public class JpaAuctionImageRepositoryFixture { protected String 업로드_이미지_파일명 = "uploadName"; protected String 저장된_이미지_파일명 = "storeName"; - protected Long 존재하지_않는_경매_이미지_아이디 = -999L; - protected AuctionImage 경매_이미지; + protected String 존재하는_경매_이미지_이름 = "경매이미지.png"; + protected String 존재하지_않는_경매_이미지_이름 = "invalid.png"; @BeforeEach void setUp() { - 경매_이미지 = new AuctionImage(업로드_이미지_파일명, 저장된_이미지_파일명); + final AuctionImage 경매_이미지 = new AuctionImage("경매이미지.png", 존재하는_경매_이미지_이름); auctionImageRepository.save(경매_이미지); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java index 1bf175301..2a855e0ed 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/JpaProfileImageRepositoryFixture.java @@ -18,13 +18,12 @@ public class JpaProfileImageRepositoryFixture { protected String 업로드_이미지_파일명 = "uploadName"; protected String 저장된_이미지_파일명 = "storeName"; - protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; - - protected ProfileImage 프로필_이미지; + protected String 존재하는_프로필_이미지_이름 = "프로필이미지.png"; + protected String 존재하지_않는_프로필_이미지_이름 = "invalid.png"; @BeforeEach void setUp() { - 프로필_이미지 = new ProfileImage("uploadName", "storeName"); + final ProfileImage 프로필_이미지 = new ProfileImage("프로필이미지.png", 존재하는_프로필_이미지_이름); profileImageRepository.save(프로필_이미지); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/ProfileImageRepositoryImplFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/ProfileImageRepositoryImplFixture.java index 719737f47..04a5ce2cd 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/ProfileImageRepositoryImplFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/persistence/fixture/ProfileImageRepositoryImplFixture.java @@ -2,29 +2,22 @@ import com.ddang.ddang.image.domain.ProfileImage; import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; @SuppressWarnings("NonAsciiCharacters") public class ProfileImageRepositoryImplFixture { - @PersistenceContext - private EntityManager em; - @Autowired private JpaProfileImageRepository profileImageRepository; - protected ProfileImage 프로필_이미지; + protected String 존재하는_프로필_이미지_이름 = "프로필이미지.png"; + protected String 존재하지_않는_프로필_이미지_이름 = "invalid.png"; @BeforeEach void fixtureSetUp() { - 프로필_이미지 = new ProfileImage("프로필이미지.png", "프로필이미지.png"); + final ProfileImage 프로필_이미지 = new ProfileImage("프로필이미지.png", 존재하는_프로필_이미지_이름); profileImageRepository.save(프로필_이미지); - - em.flush(); - em.clear(); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessorTest.java new file mode 100644 index 000000000..635ad022b --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/S3StoreImageProcessorTest.java @@ -0,0 +1,106 @@ +package com.ddang.ddang.image.infrastructure.s3; + +import com.ddang.ddang.image.domain.dto.StoreImageDto; +import com.ddang.ddang.image.infrastructure.local.exception.EmptyImageException; +import com.ddang.ddang.image.infrastructure.local.exception.StoreImageFailureException; +import com.ddang.ddang.image.infrastructure.local.exception.UnsupportedImageFileExtensionException; +import com.ddang.ddang.image.infrastructure.s3.fixture.S3StoreImageProcessorFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith({MockitoExtension.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class S3StoreImageProcessorTest extends S3StoreImageProcessorFixture { + + @InjectMocks + S3StoreImageProcessor imageProcessor; + + @Mock + S3Client s3Client; + + @Test + void 이미지_파일이_비어_있는_경우_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(빈_이미지_파일))) + .isInstanceOf(EmptyImageException.class) + .hasMessage("이미지 파일의 데이터가 비어 있습니다."); + } + + @Test + void 허용되지_않은_확장자의_이미지_파일인_경우_예외가_발생한다() { + // given + given(이미지_파일.getOriginalFilename()).willReturn(지원하지_않는_확장자를_가진_이미지_파일명); + + // when & then + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) + .isInstanceOf(UnsupportedImageFileExtensionException.class) + .hasMessageContaining("지원하지 않는 확장자입니다."); + } + + @Test + void 이미지_저장에_실패한_경우_예외가_발생한다() throws IOException { + // given + given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); + given(이미지_파일.getInputStream()).willThrow(new IOException()); + + // when & then + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) + .isInstanceOf(StoreImageFailureException.class) + .hasMessage("이미지 저장에 실패했습니다."); + } + + @Test + void AWS_이미지_저장에_실패한_경우_예외가_발생한다() throws IOException { + // given + final ByteArrayInputStream fakeInputStream = new ByteArrayInputStream("가짜 이미지 데이터".getBytes()); + given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); + given(이미지_파일.getInputStream()).willReturn(fakeInputStream); + given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) + .willThrow(SdkException.class); + + // when & then + assertThatThrownBy(() -> imageProcessor.storeImageFiles(List.of(이미지_파일))) + .isInstanceOf(StoreImageFailureException.class) + .hasMessage("AWS 이미지 저장에 실패했습니다."); + } + + @Test + void 유효한_이미지_파일인_경우_이미지_파일을_저장한다() throws Exception { + // given + final ByteArrayInputStream fakeInputStream = new ByteArrayInputStream("가짜 이미지 데이터".getBytes()); + given(이미지_파일.getOriginalFilename()).willReturn(기존_이미지_파일명); + given(이미지_파일.getInputStream()).willReturn(fakeInputStream); + given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))) + .willReturn(PutObjectResponse.builder().build()); + + // when + final List actual = imageProcessor.storeImageFiles(List.of(이미지_파일)); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual).hasSize(1); + softAssertions.assertThat(actual.get(0).storeName()).isNotBlank(); + softAssertions.assertThat(actual.get(0).uploadName()).isEqualTo(기존_이미지_파일명); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/fixture/S3StoreImageProcessorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/fixture/S3StoreImageProcessorFixture.java new file mode 100644 index 000000000..662463161 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/infrastructure/s3/fixture/S3StoreImageProcessorFixture.java @@ -0,0 +1,14 @@ +package com.ddang.ddang.image.infrastructure.s3.fixture; + +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import static org.mockito.Mockito.mock; + +public class S3StoreImageProcessorFixture { + + protected MockMultipartFile 빈_이미지_파일 = new MockMultipartFile("image.png", new byte[0]); + protected MultipartFile 이미지_파일 = mock(MultipartFile.class); + protected String 기존_이미지_파일명 = "image.png"; + protected String 지원하지_않는_확장자를_가진_이미지_파일명 = "image.gif"; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java index 2b5a9978e..178ac5a21 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/ImageControllerTest.java @@ -12,7 +12,7 @@ import java.net.MalformedURLException; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -35,10 +35,10 @@ void setUp() { @Test void 지정한_사용자_아이디에_대한_사용자_이미지를_조회한다() throws Exception { // given - given(imageService.readProfileImage(anyLong())).willReturn(이미지_파일_리소스); + given(imageService.readProfileImage(anyString())).willReturn(이미지_파일_리소스); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 프로필_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{storeName}", 프로필_이미지_이름)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.IMAGE_JPEG)) .andExpect(content().bytes(이미지_파일_바이트)); @@ -47,10 +47,10 @@ void setUp() { @Test void 사용자_이미지_조회시_지정한_아이디에_대한_이미지가_없는_경우_404를_반환한다() throws Exception { // given - given(imageService.readProfileImage(anyLong())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); + given(imageService.readProfileImage(anyString())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 존재하지_않는_프로필_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{storeName}", 존재하지_않는_프로필_이미지_이름)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").exists()); } @@ -58,10 +58,10 @@ void setUp() { @Test void 사용자_이미지_조회시_유효한_프로토콜이나_URL이_아닌_경우_500을_반환한다() throws Exception { // given - given(imageService.readProfileImage(anyLong())).willThrow(new MalformedURLException()); + given(imageService.readProfileImage(anyString())).willThrow(new MalformedURLException()); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{id}", 프로필_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/users/images/{storeName}", 프로필_이미지_이름)) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.message").exists()); } @@ -69,10 +69,10 @@ void setUp() { @Test void 지정한_아이디에_대한_경매_이미지를_조회한다() throws Exception { // given - given(imageService.readAuctionImage(anyLong())).willReturn(이미지_파일_리소스); + given(imageService.readAuctionImage(anyString())).willReturn(이미지_파일_리소스); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 경매_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{storeName}", 경매_이미지_이름)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.IMAGE_JPEG)) .andExpect(content().bytes(이미지_파일_바이트)); @@ -81,10 +81,10 @@ void setUp() { @Test void 경매_이미지_조회시_지정한_아이디에_대한_이미지가_없는_경우_404를_반환한다() throws Exception { // given - given(imageService.readAuctionImage(anyLong())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); + given(imageService.readAuctionImage(anyString())).willThrow(new ImageNotFoundException("지정한 이미지를 찾을 수 없습니다.")); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 존재하지_않는_경매_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{storeName}", 존재하지_않는_경매_이미지_이름)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").exists()); } @@ -92,10 +92,10 @@ void setUp() { @Test void 경매_이미지_조회시_유효한_프로토콜이나_URL이_아닌_경우_500을_반환한다() throws Exception { // given - given(imageService.readAuctionImage(anyLong())).willThrow(new MalformedURLException()); + given(imageService.readAuctionImage(anyString())).willThrow(new MalformedURLException()); // when & then - mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{id}", 경매_이미지_아이디)) + mockMvc.perform(RestDocumentationRequestBuilders.get("/auctions/images/{storeName}", 경매_이미지_이름)) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.message").exists()); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java index dbac712ed..9fe6ed49e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/fixture/ImageControllerFixture.java @@ -9,8 +9,8 @@ public class ImageControllerFixture extends CommonControllerSliceTest { protected byte[] 이미지_파일_바이트 = "이것은 이미지 파일의 바이트 코드입니다.".getBytes(); protected Resource 이미지_파일_리소스 = new ByteArrayResource(이미지_파일_바이트); - protected Long 프로필_이미지_아이디 = 1L; - protected Long 존재하지_않는_프로필_이미지_아이디 = -999L; - protected Long 경매_이미지_아이디 = 1L; - protected Long 존재하지_않는_경매_이미지_아이디 = -999L; + protected String 프로필_이미지_이름 = "profile_image.png"; + protected String 존재하지_않는_프로필_이미지_이름 = "invalid_profile_image.png"; + protected String 경매_이미지_이름 = "auction_image.png"; + protected String 존재하지_않는_경매_이미지_이름 = "invalid_auction_image.png"; } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java index 491761182..13f22b1f9 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/ImageUrlCalculatorTest.java @@ -16,43 +16,43 @@ class ImageUrlCalculatorTest extends ImageUrlCalculatorFixture { @Test void 프로필_사진의_상대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_상대_URL, 프로필_이미지_아이디); + final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_상대_URL, 프로필_이미지_저장_이름); // then - assertThat(actual).contains(프로필_이미지_전체_URL); + assertThat(actual).contains(프로필_이미지_저장_이름_기반_전체_URL); } @Test void 프로필_사진의_아이디가_null인_경우_기본_이미지의_상대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_상대_URL, 프로필_이미지_아이디가_null); + final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_상대_URL, 프로필_이미지_저장_이름이_null); // then - assertThat(actual).contains(프로필_기본_이미지_전체_URL); + assertThat(actual).contains(이미지_저장_이름_기반_프로필_기본_이미지_전체_URL); } @Test void 경매_대표_이미지의_상대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(경매_이미지_상대_URL, 경매_이미지_아이디); + final String actual = ImageUrlCalculator.calculateBy(경매_이미지_상대_URL, 경매_이미지_저장_이름); // then - assertThat(actual).contains(경매_이미지_전체_URL); + assertThat(actual).contains(경매_이미지_저장_이름_기반_전체_URL); } @Test void 프로필_사진의_절대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_절대_URL, 프로필_이미지_아이디); + final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_절대_URL, 프로필_이미지_저장_이름); // then - assertThat(actual).isEqualTo(프로필_이미지_전체_URL); + assertThat(actual).isEqualTo(프로필_이미지_저장_이름_기반_전체_URL); } @Test void 프로필_사진의_아이디가_null인_경우_기본_이미지의_절대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_절대_URL, 프로필_이미지_아이디가_null); + final String actual = ImageUrlCalculator.calculateBy(프로필_이미지_절대_URL, 프로필_이미지_저장_이름이_null); // then assertThat(actual).isEqualTo(프로필_기본_이미지_전체_URL); @@ -61,9 +61,9 @@ class ImageUrlCalculatorTest extends ImageUrlCalculatorFixture { @Test void 경매_대표_이미지의_절대_URL로_절대_경로를_계산한다() { // when - final String actual = ImageUrlCalculator.calculateBy(경매_이미지_절대_URL, 경매_이미지_아이디); + final String actual = ImageUrlCalculator.calculateBy(경매_이미지_절대_URL, 경매_이미지_저장_이름); // then - assertThat(actual).isEqualTo(경매_이미지_전체_URL); + assertThat(actual).isEqualTo(경매_이미지_저장_이름_기반_전체_URL); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java index abdeaf933..89db85cee 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/image/presentation/util/fixture/ImageUrlCalculatorFixture.java @@ -9,11 +9,12 @@ public class ImageUrlCalculatorFixture { protected ImageRelativeUrl 경매_이미지_상대_URL = ImageRelativeUrl.AUCTION; protected String 프로필_이미지_절대_URL = "/users/images/"; - protected Long 프로필_이미지_아이디 = 2L; - protected Long 프로필_이미지_아이디가_null = null; - protected String 프로필_이미지_전체_URL = 프로필_이미지_절대_URL + 프로필_이미지_아이디; - protected String 프로필_기본_이미지_전체_URL = 프로필_이미지_절대_URL + "1"; + protected String 프로필_기본_이미지_전체_URL = 프로필_이미지_절대_URL + "default_profile_image.png"; + protected String 프로필_이미지_저장_이름 = "profile_image_store_name.png"; + protected String 프로필_이미지_저장_이름이_null = null; + protected String 프로필_이미지_저장_이름_기반_전체_URL = 프로필_이미지_절대_URL + 프로필_이미지_저장_이름; + protected String 이미지_저장_이름_기반_프로필_기본_이미지_전체_URL = 프로필_이미지_절대_URL + "default_profile_image.png"; protected String 경매_이미지_절대_URL = "/auctions/images/"; - protected Long 경매_이미지_아이디 = 1L; - protected String 경매_이미지_전체_URL = 경매_이미지_절대_URL + 경매_이미지_아이디; + protected String 경매_이미지_저장_이름 = "auction_image_store_name.png"; + protected String 경매_이미지_저장_이름_기반_전체_URL = 경매_이미지_절대_URL + 경매_이미지_저장_이름; } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java index ae67ccf80..c69f9dbf8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/fixture/ReportControllerFixture.java @@ -35,7 +35,7 @@ public class ReportControllerFixture extends CommonControllerSliceTest { protected CreateAuctionReportRequest 경매_아이디가_음수인_신고_request = new CreateAuctionReportRequest(-999L, "신고합니다"); protected static CreateAuctionReportRequest 신고_내용이_null인_경매_신고_request = new CreateAuctionReportRequest(1L, null); protected static CreateAuctionReportRequest 신고_내용이_빈값인_경매_신고_request = new CreateAuctionReportRequest(1L, ""); - private ReadUserInReportDto 판매자_정보_dto = new ReadUserInReportDto(1L, "판매자", 1L, 4.0d, "12345", false); + private ReadUserInReportDto 판매자_정보_dto = new ReadUserInReportDto(1L, "판매자", "profile_image.png", 4.0d, "12345", false); private ReadAuctionInReportDto 신고할_경매_정보_dto = new ReadAuctionInReportDto( 1L, 판매자_정보_dto, @@ -49,21 +49,21 @@ public class ReportControllerFixture extends CommonControllerSliceTest { ); protected ReadAuctionReportDto 경매_신고_dto1 = new ReadAuctionReportDto( 1L, - new ReadReporterDto(2L, "회원1", 2L, 5.0, false), + new ReadReporterDto(2L, "회원1", "profile_image.png", 5.0, false), LocalDateTime.now(), 신고할_경매_정보_dto, "신고합니다." ); protected ReadAuctionReportDto 경매_신고_dto2 = new ReadAuctionReportDto( 2L, - new ReadReporterDto(3L, "회원2", 3L, 5.0, false), + new ReadReporterDto(3L, "회원2", "profile_image.png", 5.0, false), LocalDateTime.now(), 신고할_경매_정보_dto, "신고합니다." ); protected ReadAuctionReportDto 경매_신고_dto3 = new ReadAuctionReportDto( 3L, - new ReadReporterDto(4L, "회원3", 4L, 5.0, false), + new ReadReporterDto(4L, "회원3", "profile_image.png", 5.0, false), LocalDateTime.now(), 신고할_경매_정보_dto, "신고합니다." @@ -86,8 +86,8 @@ public class ReportControllerFixture extends CommonControllerSliceTest { LocalDateTime.now().plusDays(2), 2 ); - private ReadUserInReportDto 구매자_정보_dto1 = new ReadUserInReportDto(2L, "구매자1", 2L, 4.0d, "12346", false); - private ReadReporterDto 신고자_정보_dto1 = new ReadReporterDto(2L, "구매자1", 2L, 4.0d, false); + private ReadUserInReportDto 구매자_정보_dto1 = new ReadUserInReportDto(2L, "구매자1", "profile_image.png", 4.0d, "12346", false); + private ReadReporterDto 신고자_정보_dto1 = new ReadReporterDto(2L, "구매자1", "profile_image.png", 4.0d, false); protected ReadChatRoomReportDto 채팅방_신고_dto1 = new ReadChatRoomReportDto( 1L, 신고자_정보_dto1, @@ -106,8 +106,8 @@ public class ReportControllerFixture extends CommonControllerSliceTest { LocalDateTime.now().plusDays(2), 2 ); - private ReadUserInReportDto 구매자_정보_dto2 = new ReadUserInReportDto(3L, "구매자2", 3L, 4.0d, "12347", false); - private ReadReporterDto 신고자_정보_dto2 = new ReadReporterDto(3L, "구매자2", 3L, 4.0d, false); + private ReadUserInReportDto 구매자_정보_dto2 = new ReadUserInReportDto(3L, "구매자2", "profile_image.png", 4.0d, "12347", false); + private ReadReporterDto 신고자_정보_dto2 = new ReadReporterDto(3L, "구매자2", "profile_image.png", 4.0d, false); protected ReadChatRoomReportDto 채팅방_신고_dto2 = new ReadChatRoomReportDto( 2L, 신고자_정보_dto2, @@ -126,8 +126,8 @@ public class ReportControllerFixture extends CommonControllerSliceTest { LocalDateTime.now().plusDays(2), 2 ); - private ReadUserInReportDto 구매자_정보_dto3 = new ReadUserInReportDto(3L, "구매자2", 3L, 4.0d, "12347", false); - private ReadReporterDto 신고자_정보_dto3 = new ReadReporterDto(3L, "구매자2", 3L, 4.0d, false); + private ReadUserInReportDto 구매자_정보_dto3 = new ReadUserInReportDto(3L, "구매자2", "profile_image.png", 4.0d, "12347", false); + private ReadReporterDto 신고자_정보_dto3 = new ReadReporterDto(3L, "구매자2", "profile_image.png", 4.0d, false); protected ReadChatRoomReportDto 채팅방_신고_dto3 = new ReadChatRoomReportDto( 3L, 신고자_정보_dto3, @@ -147,7 +147,7 @@ public class ReportControllerFixture extends CommonControllerSliceTest { protected static CreateQuestionReportRequest 신고_내용이_null인_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, null); protected static CreateQuestionReportRequest 신고_내용이_빈값인_질문_신고_request = new CreateQuestionReportRequest(1L, 1L, ""); - private ReadUserInReportDto 질문자_dto = new ReadUserInReportDto(1L, "사용자", 1L, 5.0d, "12345", false); + private ReadUserInReportDto 질문자_dto = new ReadUserInReportDto(1L, "사용자", "profile_image.png", 5.0d, "12345", false); private ReadQuestionInReportDto 질문_dto1 = new ReadQuestionInReportDto(1L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); private ReadQuestionInReportDto 질문_dto2 = new ReadQuestionInReportDto(2L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); private ReadQuestionInReportDto 질문_dto3 = new ReadQuestionInReportDto(3L, 질문자_dto, "질문드립니다.", LocalDateTime.now()); @@ -165,12 +165,11 @@ public class ReportControllerFixture extends CommonControllerSliceTest { protected CreateAnswerReportRequest 답변_아이디가_음수인_질문_신고_request = new CreateAnswerReportRequest(1L, -1L, "신고합니다."); protected static CreateAnswerReportRequest 신고_내용이_null인_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, null); protected static CreateAnswerReportRequest 신고_내용이_빈값인_답변_신고_request = new CreateAnswerReportRequest(1L, 1L, ""); - private ReadUserInReportDto 답변자_dto = new ReadUserInReportDto(1L, "사용자", 1L, 5.0d, "12345", false); + private ReadUserInReportDto 답변자_dto = new ReadUserInReportDto(1L, "사용자", "profile_image.png", 5.0d, "12345", false); private ReadAnswerInReportDto 답변_dto1 = new ReadAnswerInReportDto(1L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); private ReadAnswerInReportDto 답변_dto2 = new ReadAnswerInReportDto(2L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); private ReadAnswerInReportDto 답변_dto3 = new ReadAnswerInReportDto(3L, 답변자_dto, "답변드립니다.", LocalDateTime.now()); protected ReadAnswerReportDto 답변_신고_dto1 = new ReadAnswerReportDto(1L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto1, "신고합니다."); protected ReadAnswerReportDto 답변_신고_dto2 = new ReadAnswerReportDto(2L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto2, "신고합니다."); protected ReadAnswerReportDto 답변_신고_dto3 = new ReadAnswerReportDto(3L, 신고자_정보_dto1, LocalDateTime.now(), 답변_dto3, "신고합니다."); - } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java index d34dce198..89427b623 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/fixture/ReviewControllerFixture.java @@ -16,12 +16,12 @@ public class ReviewControllerFixture extends CommonControllerSliceTest { private Long 유효한_평가_대상_아이디 = 2L; private Long 유효한_경매_아이디 = 1L; private Long 사용자가_이미_평가한_경매_아이디 = 2L; - private Long 판매자1_프로필_이미지_아이디 = 1L; - private Long 판매자2_프로필_이미지_아이디 = 2L; - private Long 구매자_프로필_이미지_아이디 = 3L; - private ReadUserInReviewDto 판매자1 = new ReadUserInReviewDto(1L, "판매자1", 판매자1_프로필_이미지_아이디, 5.0d, "12347"); - private ReadUserInReviewDto 판매자2 = new ReadUserInReviewDto(2L, "판매자2", 판매자2_프로필_이미지_아이디, 5.0d, "12348"); - protected ReadUserInReviewDto 구매자 = new ReadUserInReviewDto(3L, "구매자", 구매자_프로필_이미지_아이디, 5.0d, "12349"); + private String 판매자1_프로필_이미지_저장_이름 = "profile_image1.png"; + private String 판매자2_프로필_이미지_저장_이름 = "profile_image2.png"; + private String 구매자_프로필_이미지_저장_이름 = "profile_image3.png"; + private ReadUserInReviewDto 판매자1 = new ReadUserInReviewDto(1L, "판매자1", 판매자1_프로필_이미지_저장_이름, 5.0d, "12347"); + private ReadUserInReviewDto 판매자2 = new ReadUserInReviewDto(2L, "판매자2", 판매자2_프로필_이미지_저장_이름, 5.0d, "12348"); + protected ReadUserInReviewDto 구매자 = new ReadUserInReviewDto(3L, "구매자", 구매자_프로필_이미지_저장_이름, 5.0d, "12349"); protected String 액세스_토큰 = "Bearer accessToken"; protected PrivateClaims 유효한_작성자_비공개_클레임 = new PrivateClaims(유효한_평가_작성자_아이디); protected CreateReviewRequest 사용자_평가_등록_요청 = diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java index c68ffc576..11db5551e 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/application/UserServiceTest.java @@ -36,7 +36,7 @@ class UserServiceTest extends UserServiceFixture { // then SoftAssertions.assertSoftly(softAssertions -> { softAssertions.assertThat(actual.name()).isEqualTo(사용자.getName()); - softAssertions.assertThat(actual.profileImageId()).isEqualTo(사용자.getProfileImage().getId()); + softAssertions.assertThat(actual.profileImageStoreName()).isEqualTo(사용자.getProfileImage().getImage().getStoreName()); softAssertions.assertThat(actual.reliability()).isEqualTo(사용자.getReliability().getValue()); }); } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java index d907a04fd..7ce64a559 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/JpaUserRepositoryTest.java @@ -60,6 +60,24 @@ class JpaUserRepositoryTest extends JpaUserRepositoryFixture { assertThat(actual).isEmpty(); } + @Test + void 존재하는_사용자_아이디를_전달하면_해당_사용자를_Optional로_감싸_반환하고_프로필_이미지도_한번에_조회한다() { + // when + final Optional actual = userRepository.findByIdWithProfileImage(사용자.getId()); + + // then + assertThat(actual).contains(사용자); + } + + @Test + void 존재하지_않는_사용자_아이디를_전달하면_빈_Optional을_반환하고_프로필_이미지도_한번에_조회한다() { + // when + final Optional actual = userRepository.findByIdWithProfileImage(존재하지_않는_사용자_아이디); + + // then + assertThat(actual).isEmpty(); + } + @Test void 회원탈퇴한_사용자의_id를_전달하면_빈_Optional을_반환한다() { // when diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImplTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImplTest.java index 775126096..2e8e2c5df 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImplTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/infrastructure/persistence/UserRepositoryImplTest.java @@ -66,6 +66,24 @@ void setUp(@Autowired final JpaUserRepository jpaUserRepository) { assertThat(actual).isEmpty(); } + @Test + void 존재하는_사용자_아이디를_전달하면_해당_사용자를_Optional로_감싸_반환하고_프로필_이미지도_한번에_조회한다() { + // when + final Optional actual = userRepository.findByIdWithProfileImage(사용자.getId()); + + // then + assertThat(actual).contains(사용자); + } + + @Test + void 존재하지_않는_사용자_아이디를_전달하면_빈_Optional을_반환하고_프로필_이미지도_한번에_조회한다() { + // when + final Optional actual = userRepository.findByIdWithProfileImage(존재하지_않는_사용자_아이디); + + // then + assertThat(actual).isEmpty(); + } + @Test void 회원탈퇴한_사용자의_id를_전달하면_빈_Optional을_반환한다() { // when diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java index eb2731dc4..75f1ceef6 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserAuctionControllerFixture.java @@ -35,12 +35,12 @@ public class UserAuctionControllerFixture extends CommonControllerSliceTest { LocalDateTime.now(), LocalDateTime.now(), List.of(직거래_지역_정보_dto), - List.of(1L), + List.of("auction_image.png"), 2, "main1", "sub1", 1L, - 1L, + "profile_image.png", "판매자", 3.5d, false, @@ -58,12 +58,12 @@ public class UserAuctionControllerFixture extends CommonControllerSliceTest { LocalDateTime.now(), LocalDateTime.now(), List.of(직거래_지역_정보_dto), - List.of(1L), + List.of("auction_image.png"), 2, "main2", "sub2", 1L, - 1L, + "profile_image.png", "판매자", 3.5d, false, diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java index fa46afbd5..0976a80e1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/fixture/UserControllerFixture.java @@ -17,10 +17,10 @@ public class UserControllerFixture extends CommonControllerSliceTest { protected PrivateClaims 존재하지_않는_사용자_ID_클레임 = new PrivateClaims(999L); protected String 탈퇴한_사용자_이름 = "알 수 없음"; - protected ReadUserDto 사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", 1L, 4.6d, "12345", false); - protected ReadUserDto 탈퇴한_사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", 1L, 4.6d, "12345", true); + protected ReadUserDto 사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", "profile_image.png", 4.6d, "12345", false); + protected ReadUserDto 탈퇴한_사용자_정보_조회_dto = new ReadUserDto(1L, "사용자1", "profile_image.png", 4.6d, "12345", true); protected UpdateUserRequest 수정할_이름_request = new UpdateUserRequest("updateName"); - protected ReadUserDto 수정후_사용자_정보_조회_dto = new ReadUserDto(1L, 수정할_이름_request.name(), 1L, 4.6d, "12345", false); + protected ReadUserDto 수정후_사용자_정보_조회_dto = new ReadUserDto(1L, 수정할_이름_request.name(), "profile_image.png", 4.6d, "12345", false); private String json = "{\"name\":\"" + 수정할_이름_request.name() + "\"}"; private byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8); protected MockMultipartFile 수정할_이름 = new MockMultipartFile( diff --git a/backend/ddang/src/test/resources/application.yml b/backend/ddang/src/test/resources/application.yml index 1b0aeb253..875006e98 100644 --- a/backend/ddang/src/test/resources/application.yml +++ b/backend/ddang/src/test/resources/application.yml @@ -49,3 +49,10 @@ fcm: enabled: false key: path: firebase/private-key.json + +aws: + s3: + enabled: false + region: region + bucket-name: awsbucketname + image-path: image/path
    Table 13. /auctions/{auctionId}/reviewsTable 14. /auctions/{auctionId}/reviews