-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ [FEAT] 평가(리뷰) API 구현
- Loading branch information
Showing
18 changed files
with
710 additions
and
0 deletions.
There are no files selected for viewing
122 changes: 122 additions & 0 deletions
122
src/main/java/com/example/template/domain/review/controller/ReviewController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package com.example.template.domain.review.controller; | ||
|
||
import com.example.template.domain.member.entity.Member; | ||
import com.example.template.domain.review.dto.request.ReviewRequestDTO; | ||
import com.example.template.domain.review.dto.response.ReviewResponseDTO; | ||
import com.example.template.domain.review.entity.Keyword; | ||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.review.entity.ReviewImg; | ||
import com.example.template.domain.review.service.KeywordQueryService; | ||
import com.example.template.domain.review.service.ReviewCommandService; | ||
import com.example.template.domain.review.service.ReviewQueryService; | ||
import com.example.template.global.annotation.AuthenticatedMember; | ||
import com.example.template.global.apiPayload.ApiResponse; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.Parameters; | ||
import jakarta.validation.Valid; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.web.bind.annotation.*; | ||
import org.springframework.web.multipart.MultipartFile; | ||
|
||
import java.util.List; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/v1/reviews") | ||
public class ReviewController { | ||
|
||
private final ReviewQueryService reviewQueryService; | ||
private final ReviewCommandService reviewCommandService; | ||
private final KeywordQueryService keywordQueryService; | ||
|
||
@PostMapping | ||
@Operation(summary = "평가 생성 API", description = "Request Body를 이용하여 새로운 평가를 생성합니다. keywords를 제외하고는 모두 필요합니다.") | ||
public ApiResponse<ReviewResponseDTO.CreateReviewResponseDTO> createReview(@AuthenticatedMember Member member, | ||
@Valid @RequestBody ReviewRequestDTO.CreateReviewRequestDTO request) { | ||
Review review = reviewCommandService.createReview(member, request); | ||
return ApiResponse.onSuccess(HttpStatus.CREATED, ReviewResponseDTO.CreateReviewResponseDTO.from(review)); | ||
} | ||
|
||
@GetMapping("/stations/{stationId}") | ||
@Operation(summary = "충전소 평가 가져오는 API", description = "특정 충전소의 모든 평가를 정렬하고 무한 스크롤 방식으로 페이지네이션하여 반환") | ||
@Parameters({ | ||
@Parameter(name = "stationId", description = "평가를 가져올 충전소의 ID"), | ||
@Parameter(name = "query", description = "SCORE: 별점순, RECENT: 최신순"), | ||
@Parameter(name = "lastId", description = "마지막으로 받은 평가의 id, 처음 가져올 때 -> 0") | ||
}) | ||
public ApiResponse<List<ReviewResponseDTO.ReviewPreviewDTO>> getReviewsOfStations(@AuthenticatedMember Member member, | ||
@PathVariable Long stationId, | ||
@RequestParam(defaultValue = "0") Long lastId, | ||
@RequestParam(defaultValue = "SCORE") String query, | ||
@RequestParam(defaultValue = "10") int offset) { | ||
List<Review> reviewList = reviewQueryService.getReviewsOfStations(stationId, lastId, query, offset); | ||
return ApiResponse.onSuccess(reviewList | ||
.stream() | ||
.map(review -> ReviewResponseDTO.ReviewPreviewDTO.of(review, reviewCommandService.isRecommended(review.getId(), member))) | ||
.toList() | ||
); | ||
} | ||
|
||
@GetMapping("/users") | ||
@Operation(summary = "본인 평가 목록 가져오는 API", description = "로그인된 유저가 작성한 평가 목록 전체 조회") | ||
public ApiResponse<List<ReviewResponseDTO.ReviewPreviewDTO>> getReviewsOfUsers(@AuthenticatedMember Member member) { | ||
List<Review> reviewList = reviewQueryService.getReviewsOfUsers(member); | ||
return ApiResponse.onSuccess(reviewList | ||
.stream() | ||
.map(review -> ReviewResponseDTO.ReviewPreviewDTO.of(review, reviewCommandService.isRecommended(review.getId(), member))) | ||
.toList() | ||
); | ||
} | ||
|
||
@GetMapping("/{reviewId}") | ||
@Operation(summary = "평가 하나의 정보 가져오는 API", description = "특정 평가 하나 조회") | ||
public ApiResponse<ReviewResponseDTO.ReviewPreviewDTO> getReview(@AuthenticatedMember Member member, | ||
@PathVariable Long reviewId) { | ||
Review review = reviewQueryService.getReview(reviewId); | ||
return ApiResponse.onSuccess(ReviewResponseDTO.ReviewPreviewDTO.of(review, reviewCommandService.isRecommended(review.getId(), member))); | ||
} | ||
|
||
@GetMapping("/keywords") | ||
@Operation(summary = "평가의 키워드 가져오는 API", description = "평가 키워드 전부 가져오기") | ||
public ApiResponse<List<ReviewResponseDTO.KeywordDTO>> getKeywords() { | ||
List<Keyword> keywords = keywordQueryService.getKeywords(); | ||
return ApiResponse.onSuccess(keywords.stream().map(ReviewResponseDTO.KeywordDTO::from).toList()); | ||
} | ||
|
||
@PatchMapping("/{reviewId}") | ||
@Operation(summary = "평가 수정 API", description = "평가 하나 수정하기") | ||
public ApiResponse<ReviewResponseDTO.ReviewPreviewDTO> updateReview(@AuthenticatedMember Member member, | ||
@PathVariable Long reviewId, | ||
@RequestBody ReviewRequestDTO.UpdateReviewRequestDTO request) { | ||
Review review = reviewCommandService.updateReview(reviewId, request); | ||
return ApiResponse.onSuccess( | ||
ReviewResponseDTO.ReviewPreviewDTO.of(review, reviewCommandService.isRecommended(review.getId(), member)) | ||
); | ||
} | ||
|
||
@DeleteMapping("/{reviewId}") | ||
@Operation(summary = "평가 삭제 API", description = "평가 하나 삭제하기") | ||
public ApiResponse<Long> deleteReview(@PathVariable Long reviewId) { | ||
Long id = reviewCommandService.deleteReview(reviewId); | ||
return ApiResponse.onSuccess(id); | ||
} | ||
|
||
@PostMapping("/{reviewId}") | ||
@Operation(summary = "평가 추천 API", description = "평가 추천 및 추천 취소 API") | ||
public ApiResponse<Boolean> recommendReview(@AuthenticatedMember Member member, @PathVariable Long reviewId) { | ||
return ApiResponse.onSuccess(reviewCommandService.recommendReview(member, reviewId)); | ||
} | ||
|
||
@Operation(summary = "이미지 업로드", description = "평가에 첨부할 이미지를 미리 업로드") | ||
@PostMapping(value = "/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||
public ApiResponse<ReviewResponseDTO.ReviewImgDTO> uploadImages( | ||
@RequestPart("images") List<MultipartFile> images) { | ||
List<ReviewImg> reviewImgList = reviewCommandService.uploadImg(images); | ||
return ApiResponse.onSuccess(ReviewResponseDTO.ReviewImgDTO.builder(). | ||
images(reviewImgList.stream().map(ReviewImg::getImgUrl).toList()) | ||
.build()); | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
src/main/java/com/example/template/domain/review/dto/request/ReviewRequestDTO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.example.template.domain.review.dto.request; | ||
|
||
import com.example.template.domain.member.entity.Member; | ||
import com.example.template.domain.review.entity.Keyword; | ||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.station.entity.Station; | ||
import io.swagger.v3.oas.annotations.media.Schema; | ||
import jakarta.validation.constraints.Max; | ||
import jakarta.validation.constraints.Min; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Getter; | ||
|
||
import java.util.List; | ||
|
||
public class ReviewRequestDTO { | ||
|
||
@Getter | ||
public static class CreateReviewRequestDTO { | ||
@NotBlank(message = "내용이 비어있습니다.") | ||
@Schema(name = "content", description = "내용", example = "충전소 이용이 편리해요") | ||
private String content; | ||
// TODO: null 인 경우 무시하고 0으로 처리, 추후에 validator 추가 필요 | ||
@Min(0) | ||
@Max(5) | ||
@NotNull | ||
@Schema(name = "score", description = "별점", example = "4.5") | ||
private double score; | ||
private List<Keyword> keywords; | ||
@Schema(name = "stationId", description = "리뷰를 달 충전소의 id", example = "1") | ||
@NotNull | ||
private Long stationId; | ||
private List<String> imgUrls; | ||
|
||
public Review toReview(Member member, Station station) { | ||
return Review.builder() | ||
.content(this.content) | ||
.score(this.score) | ||
.recommendationNum(0) | ||
.member(member) | ||
.station(station) | ||
.build(); | ||
} | ||
} | ||
|
||
@Getter | ||
public static class UpdateReviewRequestDTO { | ||
@NotBlank(message = "내용이 비어있습니다.") | ||
@Schema(name = "content", description = "수정할 내용", example = "충전 속도가 빨라요 (수정)") | ||
private String content; | ||
// TODO: null 인 경우 무시하고 0으로 처리, 추후에 validator 추가 필요 | ||
@Min(0) | ||
@Max(5) | ||
@NotNull | ||
@Schema(name = "score", description = "수정할 점수", example = "4.7") | ||
private double score; | ||
private List<Keyword> keywords; | ||
private List<String> imgUrls; | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
src/main/java/com/example/template/domain/review/dto/response/ReviewResponseDTO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package com.example.template.domain.review.dto.response; | ||
|
||
import com.example.template.domain.member.dto.ProfileResponseDTO; | ||
import com.example.template.domain.review.entity.Keyword; | ||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.review.entity.ReviewImg; | ||
import com.example.template.domain.review.entity.ReviewKeyword; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
|
||
public class ReviewResponseDTO { | ||
|
||
@Getter | ||
@Builder | ||
public static class CreateReviewResponseDTO{ | ||
private Long id; | ||
private LocalDateTime createdAt; | ||
|
||
public static CreateReviewResponseDTO from(Review review) { | ||
return CreateReviewResponseDTO.builder() | ||
.id(review.getId()) | ||
.createdAt(review.getCreatedAt()) | ||
.build(); | ||
} | ||
} | ||
|
||
@Getter | ||
@Builder | ||
public static class ReviewPreviewDTO { | ||
private Long id; | ||
private String content; | ||
private Integer recommendationNum; | ||
private List<KeywordDTO> keywords; | ||
private List<String> images; | ||
private double score; | ||
// TODO: 공용 유저 응답으로 변경 예정 | ||
private ProfileResponseDTO.ProfileDTO member; | ||
private boolean isRecommended; | ||
private String username; | ||
|
||
public static ReviewPreviewDTO of(Review review, boolean isRecommended) { | ||
return ReviewPreviewDTO.builder() | ||
.id(review.getId()) | ||
.content(review.getContent()) | ||
.recommendationNum(review.getRecommendationNum()) | ||
.keywords(review.getKeywords().stream().map(ReviewKeyword::getKeyword).map(KeywordDTO::from).toList()) | ||
.images(review.getImgList().stream().map(ReviewImg::getImgUrl).toList()) | ||
.score(review.getScore()) | ||
.member(ProfileResponseDTO.from(review.getMember())) | ||
.isRecommended(isRecommended) | ||
.build(); | ||
} | ||
} | ||
|
||
@Getter | ||
@Builder | ||
public static class KeywordDTO { | ||
private String name; | ||
private String content; | ||
public static KeywordDTO from(Keyword keyword) { | ||
return KeywordDTO.builder() | ||
.name(keyword.name()) | ||
.content(keyword.getDescription()) | ||
.build(); | ||
} | ||
} | ||
|
||
@Getter | ||
@Builder | ||
public static class ReviewImgDTO { | ||
private List<String> images; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
src/main/java/com/example/template/domain/review/enums/SortType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.example.template.domain.review.enums; | ||
|
||
public enum SortType { | ||
RECENT, SCORE | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/example/template/domain/review/exception/ReviewErrorCode.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.example.template.domain.review.exception; | ||
|
||
import com.example.template.global.apiPayload.ApiResponse; | ||
import com.example.template.global.apiPayload.code.status.BaseErrorCode; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import org.springframework.http.HttpStatus; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public enum ReviewErrorCode implements BaseErrorCode { | ||
|
||
NOT_FOUND(HttpStatus.NOT_FOUND, "REVIEW404", "평가를 찾지 못했습니다."), | ||
QUERY_BAD_REQUEST(HttpStatus.BAD_REQUEST, "REVIEW400", "평가의 쿼리가 잘못되었습니다."), | ||
SCORE_RANGE_ERROR(HttpStatus.BAD_REQUEST, "REVIEW404", "평가 점수가 범위 밖입니다."), | ||
INVALID_IMG_URL(HttpStatus.BAD_REQUEST, "REVIEW404", "잘못된 이미지 경로입니다."), | ||
; | ||
|
||
private final HttpStatus httpStatus; | ||
private final String code; | ||
private final String message; | ||
|
||
@Override | ||
public ApiResponse<Void> getErrorResponse() { | ||
return ApiResponse.onFailure(code, message); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/example/template/domain/review/exception/ReviewException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.example.template.domain.review.exception; | ||
|
||
import com.example.template.global.apiPayload.exception.GeneralException; | ||
|
||
public class ReviewException extends GeneralException { | ||
public ReviewException(ReviewErrorCode code) { | ||
super(code); | ||
} | ||
|
||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/com/example/template/domain/review/repository/ReviewImgRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.example.template.domain.review.repository; | ||
|
||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.review.entity.ReviewImg; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.List; | ||
|
||
public interface ReviewImgRepository extends JpaRepository<ReviewImg, Long> { | ||
List<ReviewImg> findAllByReviewIs(Review review); | ||
List<ReviewImg> findAllByImgUrlIn(List<String> imgUrls); | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/com/example/template/domain/review/repository/ReviewKeywordRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.example.template.domain.review.repository; | ||
|
||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.review.entity.ReviewKeyword; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.List; | ||
|
||
public interface ReviewKeywordRepository extends JpaRepository<ReviewKeyword, Long> { | ||
List<ReviewKeyword> findAllByReviewIs(Review review); | ||
void deleteAllByReviewIs(Review review); | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/example/template/domain/review/repository/ReviewRecommendRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.example.template.domain.review.repository; | ||
|
||
import com.example.template.domain.member.entity.Member; | ||
import com.example.template.domain.review.entity.Review; | ||
import com.example.template.domain.review.entity.ReviewRecommend; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
import java.util.Optional; | ||
|
||
public interface ReviewRecommendRepository extends JpaRepository<ReviewRecommend, Long> { | ||
Optional<ReviewRecommend> findByMemberIsAndReviewIs(Member member, Review review); | ||
boolean existsByReviewIsAndMemberIs(Review review, Member member); | ||
void deleteAllByMemberIs(Member member); | ||
void deleteAllByReviewIs(Review review); | ||
} |
Oops, something went wrong.