Skip to content

Commit

Permalink
feat: #294 유저 상호 평가 api 추가 (#515)
Browse files Browse the repository at this point in the history
* feat: 사용자 평가 엔티티 추가

* feat: 사용자 평가 레포지토리 추가

* feat: 사용자 신뢰도 계산 기능 추가

* feat: 사용자 신뢰도 제출 및 조회 서비스 추가

* feat: 사용자 신뢰도 제출 및 조회 컨트롤러 추가

* refactor: 개행 및 메서드 분리를 통해 가독성 개선

* feat: 사용자 평가 관련 flyway 스크립트 추가

* docs: 유저 상호 평가 api 문서화 추가

* feat: 평가 등록 시 자격이 있는지 검증 로직 추가

* rename: request dto와 response dto 패키지 이동

* rename: 지정한 유저가 받은 모든 평가 목록을 가져오는 controller 메서드 이름 변경

* feat: 지정한 작성자가 지정한 경매에 제출한 평가를 조회하는 레포지토리 추가

* feat: 지정한 작성자가 지정한 경매에 제출한 평가를 조회하는 서비스 추가

* feat: 지정한 작성자가 지정한 경매에 제출한 평가를 조회하는 컨트롤러 추가

* docs: 사용자가 경매 거래 상대에게 작성한 평가 조회 부분 문서화

* test: 픽스처 객체 생성을 `@beforeEach`에서 하도록 변경

* test: 테스트에 Non-ASCII 경고 억제 어노테이션 추가

* refactor: null 데이터를 가진 dto를 매번 생성하지 않고 static 하게 갖고 있도록 변경

* refactor: 빌더를 사용한 생성자의 접근지정자를 private으로 변경

* style: 개행 수정

* feat: db에 평가 점수 필드에 null 불가 조건 추가

* test: 서비스 테스트에 픽스처 추가

* refactor: 사용하지 않는 `@EntityGraph` 제거

* feat: 지정한 평가 아이디로 평가를 조회할 수 있는 api 추가

* docs: 지정한 평가 아이디로 평가를 조회할 수 있는 api 문서화 추가

* feat: 평가 점수를 나타내는 VO 추가

* test: 픽스처 누락된 부분 추가

* feat: 사용자가 경매 거래에 작성한 평가 조회 uri 변경

* docs: 사용자가 경매 거래에 작성한 평가 조회 uri 변경에 따른 문서화 수정

* feat: 사용자 엔티티에 신뢰도 값객체 적용

* fix: 사용자 엔티티에 신뢰도 값객체 적용으로 인한 컴파일 에러 해결

* refactor: 사용자 엔티티 생성자 필드의 `@NotNull`을 `@NonNull`로 변경

* test: 실패하는 테스트 해결

* refactor: 초기 상태의 신뢰도를 나타내는 상수를 활용하도록 생성자 로직 수정

* style: 불필요한 개행 제거

* ci: 충돌 해결

* ci: 충돌 해결
  • Loading branch information
kwonyj1022 authored and swonny committed Oct 6, 2023
1 parent fea774e commit 904d22d
Show file tree
Hide file tree
Showing 80 changed files with 2,480 additions and 164 deletions.
48 changes: 48 additions & 0 deletions backend/ddang/src/docs/asciidoc/docs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,51 @@ include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_
==== 응답

include::{snippets}/device-token-controller-test/디바이스_토큰을_저장_또는_갱신한다/http-response.adoc[]

== 유저 상호 평가 API

=== 사용자 평가 등록

==== 요청

include::{snippets}/review-controller-test/평가를_등록한다/http-request.adoc[]
include::{snippets}/review-controller-test/평가를_등록한다/request-headers.adoc[]
include::{snippets}/review-controller-test/평가를_등록한다/request-fields.adoc[]

==== 응답

include::{snippets}/review-controller-test/평가를_등록한다/http-response.adoc[]

=== 지정한 평가 아이디에 해당하는 평가 조회

==== 요청

include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/http-request.adoc[]
include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/path-parameters.adoc[]

==== 응답
include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/http-response.adoc[]
include::{snippets}/review-controller-test/지정한_평가_아이디에_해당하는_평가를_조회한다/response-fields.adoc[]

=== 지정한 사용자가 받은 평가 목록 조회

==== 요청

include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/http-request.adoc[]
include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/path-parameters.adoc[]

==== 응답

include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/http-response.adoc[]
include::{snippets}/review-controller-test/주어진_사용자가_받은_평가_목록을_최신순으로_조회한다/response-fields.adoc[]

=== 사용자가_경매_거래_상대에게_작성한_평가를_조회한다

==== 요청
include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/http-request.adoc[]
include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/path-parameters.adoc[]
include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/request-headers.adoc[]

==== 응답
include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/http-response.adoc[]
include::{snippets}/auction-review-controller-test/사용자가_경매_거래에_작성한_평가를_조회한다/response-fields.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static ReadAuctionDto of(final Auction auction, final LocalDateTime targe
auction.getSeller().getId(),
ImageIdProcessor.process(auction.getSeller().getProfileImage()),
auction.getSeller().getName(),
auction.getSeller().getReliability(),
auction.getSeller().getReliability().getValue(),
auction.getSeller().isDeleted(),
auction.findAuctionStatus(targetTime)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package com.ddang.ddang.auction.infrastructure.persistence;

import static com.ddang.ddang.auction.domain.QAuction.auction;
import static com.ddang.ddang.bid.domain.QBid.bid;
import static com.ddang.ddang.category.domain.QCategory.category;
import static com.ddang.ddang.region.domain.QAuctionRegion.auctionRegion;
import static com.ddang.ddang.region.domain.QRegion.region;

import com.ddang.ddang.auction.configuration.util.AuctionSortConditionConsts;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.infrastructure.persistence.exception.UnsupportedSortConditionException;
Expand All @@ -16,18 +10,25 @@
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Repository;

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

import static com.ddang.ddang.auction.domain.QAuction.auction;
import static com.ddang.ddang.bid.domain.QBid.bid;
import static com.ddang.ddang.category.domain.QCategory.category;
import static com.ddang.ddang.region.domain.QAuctionRegion.auctionRegion;
import static com.ddang.ddang.region.domain.QRegion.region;

@Repository
@RequiredArgsConstructor
public class QuerydslAuctionRepositoryImpl implements QuerydslAuctionRepository {
Expand Down Expand Up @@ -74,7 +75,7 @@ private List<OrderSpecifier<?>> processOrderSpecifiers(final Pageable pageable)

private List<OrderSpecifier<?>> processOrderSpecifierByCondition(final Order order) {
if (AuctionSortConditionConsts.RELIABILITY.equals(order.getProperty())) {
return List.of(auction.seller.reliability.desc());
return List.of(auction.seller.reliability.value.desc());
}
if (AuctionSortConditionConsts.AUCTIONEER_COUNT.equals(order.getProperty())) {
return List.of(auction.auctioneerCount.desc());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.ddang.ddang.auction.presentation;

import com.ddang.ddang.auction.presentation.dto.response.ReadReviewDetailResponse;
import com.ddang.ddang.authentication.configuration.AuthenticateUser;
import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo;
import com.ddang.ddang.review.application.ReviewService;
import com.ddang.ddang.review.application.dto.ReadReviewDetailDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/auctions")
@RequiredArgsConstructor
public class AuctionReviewController {

private final ReviewService reviewService;

@GetMapping("/{auctionId}/reviews")
public ResponseEntity<ReadReviewDetailResponse> readByAuctionId(
@AuthenticateUser final AuthenticationUserInfo userInfo,
@PathVariable final Long auctionId
) {
final ReadReviewDetailDto readReviewDetailDto = reviewService.readByAuctionIdAndWriterId(userInfo.userId(), auctionId);
ReadReviewDetailResponse response = ReadReviewDetailResponse.from(readReviewDetailDto);

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ddang.ddang.auction.presentation.dto.response;

import com.ddang.ddang.review.application.dto.ReadReviewDetailDto;
import jakarta.annotation.Nullable;

public record ReadReviewDetailResponse(@Nullable Double score, @Nullable String content) {

public static ReadReviewDetailResponse from(final ReadReviewDetailDto readReviewDetailDto) {
return new ReadReviewDetailResponse(readReviewDetailDto.score(), readReviewDetailDto.content());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.ddang.ddang.image.application.exception.ImageNotFoundException;
import com.ddang.ddang.image.domain.ProfileImage;
import com.ddang.ddang.image.infrastructure.persistence.JpaProfileImageRepository;
import com.ddang.ddang.user.domain.Reliability;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -67,7 +68,7 @@ private User findOrPersistUser(final Oauth2Type oauth2Type, final UserInformatio
final User user = User.builder()
.name(oauth2Type.calculateNickname(calculateRandomNumber()))
.profileImage(findDefaultProfileImage())
.reliability(0.0d)
.reliability(new Reliability(0.0d))
.oauthId(userInformationDto.findUserId())
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static ReadUserInChatRoomDto from(final User user) {
user.getId(),
user.getName(),
ImageIdProcessor.process(user.getProfileImage()),
user.getReliability(),
user.getReliability().getValue(),
user.isDeleted()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,21 +24,21 @@ public void run(final ApplicationArguments args) {
final User seller1 = User.builder()
.name("판매자1")
.profileImage(new ProfileImage("upload.png", "updateImage.png"))
.reliability(4.7d)
.reliability(new Reliability(4.7d))
.oauthId("12345")
.build();

final User buyer1 = User.builder()
.name("구매자1")
.profileImage(new ProfileImage("upload.png", "updateImage.png"))
.reliability(3.0d)
.reliability(new Reliability(3.0d))
.oauthId("12346")
.build();

final User buyer2 = User.builder()
.name("구매자2")
.profileImage(new ProfileImage("upload.png", "updateImage.png"))
.reliability(0.8d)
.reliability(new Reliability(0.8d))
.oauthId("12347")
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
import com.ddang.ddang.report.application.exception.InvalidChatRoomReportException;
import com.ddang.ddang.report.application.exception.InvalidReportAuctionException;
import com.ddang.ddang.report.application.exception.InvalidReporterToAuctionException;
import com.ddang.ddang.review.application.exception.AlreadyReviewException;
import com.ddang.ddang.review.application.exception.ReviewNotFoundException;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import java.net.MalformedURLException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
Expand All @@ -41,6 +42,8 @@
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.net.MalformedURLException;

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

Expand Down Expand Up @@ -323,6 +326,26 @@ public ResponseEntity<ExceptionResponse> handleDeviceTokenNotFoundException(fina
.body(new ExceptionResponse(ex.getMessage()));
}

@ExceptionHandler(AlreadyReviewException.class)
public ResponseEntity<ExceptionResponse> handleAlreadyReviewException(
final AlreadyReviewException ex
) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

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

@ExceptionHandler(ReviewNotFoundException.class)
public ResponseEntity<ExceptionResponse> handleReviewNotFoundException(
final ReviewNotFoundException ex
) {
logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage()));

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

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
final MethodArgumentNotValidException ex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static ReadReporterDto from(final User reporter) {
reporter.getId(),
reporter.getName(),
ImageIdProcessor.process(reporter.getProfileImage()),
reporter.getReliability(),
reporter.getReliability().getValue(),
reporter.isDeleted()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static ReadUserInReportDto from(final User user) {
user.getId(),
user.getName(),
ImageIdProcessor.process(user.getProfileImage()),
user.getReliability(),
user.getReliability().getValue(),
user.getOauthId(),
user.isDeleted()
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.ddang.ddang.review.application;

import com.ddang.ddang.auction.application.exception.AuctionNotFoundException;
import com.ddang.ddang.auction.domain.Auction;
import com.ddang.ddang.auction.infrastructure.persistence.JpaAuctionRepository;
import com.ddang.ddang.review.application.dto.CreateReviewDto;
import com.ddang.ddang.review.application.dto.ReadReviewDetailDto;
import com.ddang.ddang.review.application.dto.ReadReviewDto;
import com.ddang.ddang.review.application.exception.AlreadyReviewException;
import com.ddang.ddang.review.application.exception.InvalidUserToReview;
import com.ddang.ddang.review.application.exception.ReviewNotFoundException;
import com.ddang.ddang.review.domain.Review;
import com.ddang.ddang.review.infrastructure.persistence.JpaReviewRepository;
import com.ddang.ddang.user.application.exception.UserNotFoundException;
import com.ddang.ddang.user.domain.User;
import com.ddang.ddang.user.infrastructure.persistence.JpaUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

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

private final JpaReviewRepository reviewRepository;
private final JpaAuctionRepository auctionRepository;
private final JpaUserRepository userRepository;

@Transactional
public Long create(final CreateReviewDto reviewDto) {
final Auction findAuction = auctionRepository.findById(reviewDto.auctionId())
.orElseThrow(() ->
new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")
);
final User writer = userRepository.findById(reviewDto.writerId())
.orElseThrow(() -> new UserNotFoundException("작성자 정보를 찾을 수 없습니다."));
final User target = userRepository.findById(reviewDto.targetId())
.orElseThrow(() -> new UserNotFoundException("평가 상대의 정보를 찾을 수 없습니다."));

validateWriterCanReview(findAuction, writer);

final Review review = reviewDto.toEntity(findAuction, writer, target);
final Review persistReview = saveReviewAndUpdateReliability(review, target);

return persistReview.getId();
}

private void validateWriterCanReview(final Auction auction, final User writer) {
if (!auction.isSellerOrWinner(writer, LocalDateTime.now())) {
throw new InvalidUserToReview("경매의 판매자 또는 최종 낙찰자만 평가가 가능합니다.");
}

validateAlreadyReviewed(auction, writer);
}

private void validateAlreadyReviewed(final Auction auction, final User writer) {
if (reviewRepository.existsByAuctionIdAndWriterId(auction.getId(), writer.getId())) {
throw new AlreadyReviewException("이미 평가하였습니다.");
}
}

private Review saveReviewAndUpdateReliability(final Review review, final User target) {
final Review persistReview = reviewRepository.save(review);

final List<Review> targetReviews = reviewRepository.findAllByTargetId(target.getId());
target.updateReliability(targetReviews);

return persistReview;
}

public ReadReviewDetailDto readByReviewId(final Long reviewId) {
final Review findReview = reviewRepository.findById(reviewId)
.orElseThrow(() -> new ReviewNotFoundException("해당 평가를 찾을 수 없습니다."));

return ReadReviewDetailDto.from(findReview);
}

public List<ReadReviewDto> readAllByTargetId(final Long targetId) {
final List<Review> targetReviews = reviewRepository.findAllByTargetId(targetId);

return targetReviews.stream()
.map(ReadReviewDto::from)
.toList();
}

public ReadReviewDetailDto readByAuctionIdAndWriterId(final Long writerId, final Long auctionId) {
return reviewRepository.findByAuctionIdAndWriterId(auctionId, writerId)
.map(ReadReviewDetailDto::from)
.orElse(ReadReviewDetailDto.EMPTY);
}
}
Loading

0 comments on commit 904d22d

Please sign in to comment.