-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BSVR-118] 블록별 리뷰 조회 API 구현 #23
Changes from all commits
0adfcf7
c128167
1ac2fbb
709d2e6
e39e01b
b180a77
66b002e
ff72e1b
57a9601
2f69b01
4789fbe
81d40bd
81ac814
e3b0dbc
ba5e533
e90bd50
d651526
f9d25e4
effc1a9
faca081
9a1586b
bd4024e
f337af0
dfea456
4ba4f51
070e154
ae42f0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package org.depromeet.spot.application.review; | ||
|
||
import jakarta.validation.constraints.Max; | ||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Positive; | ||
import jakarta.validation.constraints.PositiveOrZero; | ||
|
||
import org.depromeet.spot.application.review.dto.response.ReviewListResponse; | ||
import org.depromeet.spot.domain.review.ReviewListResult; | ||
import org.depromeet.spot.usecase.port.in.review.ReviewReadUsecase; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.bind.annotation.*; | ||
|
||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.Parameter; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@RestController | ||
@Tag(name = "리뷰") | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/v1") | ||
public class ReviewReadController { | ||
|
||
private final ReviewReadUsecase reviewReadUsecase; | ||
|
||
@ResponseStatus(HttpStatus.OK) | ||
@GetMapping("/stadiums/{stadiumId}/blocks/{blockId}/reviews") | ||
@Operation(summary = "특정 야구장의 특정 블록에 대한 리뷰 목록을 조회한다.") | ||
public ReviewListResponse findReviewsByBlockId( | ||
@PathVariable("stadiumId") | ||
@NotNull | ||
@Positive | ||
@Parameter(description = "야구장 PK", required = true) | ||
Long stadiumId, | ||
@PathVariable("blockId") | ||
@NotNull | ||
@Positive | ||
@Parameter(description = "블록 PK", required = true) | ||
Long blockId, | ||
@RequestParam(required = false) @Parameter(description = "열 ID (필터링)") Long rowId, | ||
@RequestParam(required = false) @Parameter(description = "좌석 번호 (필터링)") Long seatNumber, | ||
@RequestParam(defaultValue = "0") | ||
@PositiveOrZero | ||
@Parameter(description = "시작 위치 (기본값: 0)") | ||
int offset, | ||
@RequestParam(defaultValue = "10") | ||
@Positive | ||
@Max(50) | ||
@Parameter(description = "조회할 리뷰 수 (기본값: 10, 최대: 50)") | ||
int limit) { | ||
|
||
ReviewListResult result = | ||
reviewReadUsecase.findReviewsByBlockId( | ||
stadiumId, blockId, rowId, seatNumber, offset, limit); | ||
return ReviewListResponse.from(result); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package org.depromeet.spot.application.review.dto.request; | ||
|
||
import jakarta.validation.constraints.Max; | ||
import jakarta.validation.constraints.PositiveOrZero; | ||
|
||
import io.swagger.v3.oas.annotations.Parameter; | ||
|
||
public record ReviewBlockRequest( | ||
@Parameter(description = "열 ID (필터링)") Long rowId, | ||
@Parameter(description = "좌석 번호 (필터링)") Long seatNumber, | ||
@PositiveOrZero @Parameter(description = "시작 위치 (기본값: 0)") int offset, | ||
@Max(50) @Parameter(description = "조회할 리뷰 수 (기본값: 10, 최대: 50)") int limit) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.depromeet.spot.application.review.dto.response; | ||
|
||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import org.depromeet.spot.domain.review.ReviewListResult; | ||
|
||
public record ReviewListResponse( | ||
List<KeywordCountResponse> keywords, | ||
List<ReviewResponse> reviews, | ||
Long totalCount, | ||
int filteredCount, | ||
int offset, | ||
int limit, | ||
boolean hasMore, | ||
FilterInfo filter) { | ||
public static ReviewListResponse from(ReviewListResult result) { | ||
List<ReviewResponse> reviewResponses = | ||
result.reviews().stream().map(ReviewResponse::from).collect(Collectors.toList()); | ||
|
||
List<KeywordCountResponse> keywordResponses = | ||
result.topKeywords().stream() | ||
.map(kc -> new KeywordCountResponse(kc.content(), kc.count())) | ||
.collect(Collectors.toList()); | ||
|
||
boolean hasMore = (result.offset() + result.reviews().size()) < result.totalCount(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JPA에서 제공하는 Page에 hasNext()가 있는데, 혹시 hasMore을 직접 계산한 이유가 있을까? |
||
FilterInfo filter = new FilterInfo(null, null); // Assuming no filter info for now | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 필터에는 항상 null 값이 들어가는 것 같은데, 이번 티켓에 포함된 이유가 있을까?! |
||
|
||
return new ReviewListResponse( | ||
keywordResponses, | ||
reviewResponses, | ||
result.totalCount(), | ||
result.reviews().size(), | ||
result.offset(), | ||
result.limit(), | ||
hasMore, | ||
filter); | ||
} | ||
|
||
record KeywordCountResponse(String content, Long count) {} | ||
|
||
record FilterInfo(Long rowId, Integer seatNumber) {} | ||
} | ||
Comment on lines
+40
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 얘들은 딱히 밖에서 사용할 것 같지 않아서 중첩 클래스?레코드?로 넣어놨어. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package org.depromeet.spot.application.review.dto.response; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import org.depromeet.spot.domain.review.Review; | ||
import org.depromeet.spot.domain.review.ReviewImage; | ||
import org.depromeet.spot.domain.review.ReviewKeyword; | ||
|
||
public record ReviewResponse( | ||
Long id, | ||
Long userId, | ||
Long blockId, | ||
Long seatId, | ||
Long rowId, | ||
Long seatNumber, | ||
LocalDateTime date, | ||
String content, | ||
LocalDateTime createdAt, | ||
LocalDateTime updatedAt, | ||
List<ImageResponse> images, | ||
List<KeywordResponse> keywords) { | ||
public static ReviewResponse from(Review review) { | ||
return new ReviewResponse( | ||
review.getId(), | ||
review.getUserId(), | ||
review.getBlockId(), | ||
review.getSeatId(), | ||
review.getRowId(), | ||
review.getSeatNumber(), | ||
review.getDateTime(), | ||
review.getContent(), | ||
review.getCreatedAt(), | ||
review.getUpdatedAt(), | ||
mapToImageResponses(review.getImages()), | ||
mapToKeywordResponses(review.getKeywords())); | ||
} | ||
|
||
private static List<ImageResponse> mapToImageResponses(List<ReviewImage> images) { | ||
return images.stream() | ||
.map(image -> new ImageResponse(image.getId(), image.getUrl())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private static List<KeywordResponse> mapToKeywordResponses(List<ReviewKeyword> keywords) { | ||
return keywords.stream() | ||
.map( | ||
keyword -> | ||
new KeywordResponse( | ||
keyword.getId(), | ||
keyword.getKeywordId(), | ||
keyword.getIsPositive())) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
record ImageResponse(Long id, String url) {} | ||
|
||
record KeywordResponse(Long id, Long keywordId, Boolean isPositive) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.depromeet.spot.common.exception.review; | ||
|
||
import org.depromeet.spot.common.exception.ErrorCode; | ||
import org.springframework.http.HttpStatus; | ||
|
||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum ReviewErrorCode implements ErrorCode { | ||
REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "RV001", "요청한 리뷰를 찾을 수 없습니다."), | ||
INVALID_REVIEW_DATA(HttpStatus.BAD_REQUEST, "RV002", "유효하지 않은 리뷰 데이터입니다."); | ||
|
||
private final HttpStatus status; | ||
private final String code; | ||
private String message; | ||
|
||
ReviewErrorCode(HttpStatus status, String code, String message) { | ||
this.status = status; | ||
this.code = code; | ||
this.message = message; | ||
} | ||
|
||
public ReviewErrorCode appended(Object o) { | ||
message = message + " {" + o.toString() + "}"; | ||
return this; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package org.depromeet.spot.common.exception.review; | ||
|
||
import org.depromeet.spot.common.exception.BusinessException; | ||
|
||
public abstract class ReviewException extends BusinessException { | ||
protected ReviewException(ReviewErrorCode errorCode) { | ||
super(errorCode); | ||
} | ||
|
||
public static class ReviewNotFoundException extends ReviewException { | ||
public ReviewNotFoundException() { | ||
super(ReviewErrorCode.REVIEW_NOT_FOUND); | ||
} | ||
|
||
public ReviewNotFoundException(String s) { | ||
super(ReviewErrorCode.REVIEW_NOT_FOUND.appended(s)); | ||
} | ||
} | ||
|
||
public static class InvalidReviewDataException extends ReviewException { | ||
public InvalidReviewDataException() { | ||
super(ReviewErrorCode.INVALID_REVIEW_DATA); | ||
} | ||
|
||
public InvalidReviewDataException(String str) { | ||
super(ReviewErrorCode.INVALID_REVIEW_DATA.appended(str)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,13 @@ | ||
package org.depromeet.spot.domain.block; | ||
|
||
import lombok.Builder; | ||
import lombok.Getter; | ||
|
||
@Builder | ||
@Getter | ||
public class BlockTopKeyword { | ||
private final Long id; | ||
private final Long blockId; | ||
private final Long keywordId; | ||
private final Long count; | ||
|
||
public BlockTopKeyword(Long id, Long blockId, Long keywordId, Long count) { | ||
this.id = id; | ||
this.blockId = blockId; | ||
this.keywordId = keywordId; | ||
this.count = count; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,12 @@ | ||
package org.depromeet.spot.domain.review; | ||
|
||
import lombok.Builder; | ||
import lombok.Getter; | ||
|
||
@Builder | ||
@Getter | ||
public class Keyword { | ||
|
||
private final Long id; | ||
private final String content; | ||
|
||
public Keyword(Long id, String content) { | ||
this.id = id; | ||
this.content = content; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package org.depromeet.spot.domain.review; | ||
|
||
public record KeywordCount(String content, Long count) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,28 @@ | ||
package org.depromeet.spot.domain.review; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
|
||
@Builder | ||
@Getter | ||
@AllArgsConstructor | ||
public class Review { | ||
|
||
private final Long id; | ||
private final Long userId; | ||
private final Long stadiumId; | ||
private final Long blockId; | ||
private final Long seatId; | ||
private final Long rowId; | ||
private final Long seatNumber; | ||
private final LocalDateTime dateTime; | ||
|
||
private final LocalDateTime dateTime; // 시간은 미표기 | ||
private final String content; | ||
private final LocalDateTime createdAt; | ||
private final LocalDateTime updatedAt; | ||
private final String status; | ||
private final LocalDateTime deletedAt; | ||
private final List<ReviewImage> images; | ||
private final List<ReviewKeyword> keywords; | ||
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 도메인 상에서 image와 keyword는 배열로 관리해줄 수 있도록 필드를 추가했어 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,14 @@ | ||
package org.depromeet.spot.domain.review; | ||
|
||
import lombok.Builder; | ||
import lombok.Getter; | ||
|
||
@Builder | ||
@Getter | ||
public class ReviewKeyword { | ||
|
||
private final Long id; | ||
private final Long reviewId; | ||
private final Long keywordId; | ||
private final Boolean isPositive; | ||
|
||
public ReviewKeyword(Long id, Long reviewId, Long keywordId, Boolean isPositive) { | ||
this.id = id; | ||
this.reviewId = reviewId; | ||
this.keywordId = keywordId; | ||
this.isPositive = isPositive; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.depromeet.spot.domain.review; | ||
|
||
import java.util.List; | ||
|
||
public record ReviewListResult( | ||
List<Review> reviews, | ||
List<KeywordCount> topKeywords, | ||
Long totalCount, | ||
int offset, | ||
int limit) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요건 단순 질문인데, offset = null로 들어오면
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null로 들어오면 0으로 세팅돼!