Skip to content

Commit d3c0f33

Browse files
authored
Merge pull request #8 from UMC-9th-Spring-Boot/feat/chapter7
Feat : Chapter 7. API 응답 통일 & 에러 핸들러
2 parents 906eaa9 + f41f7d1 commit d3c0f33

25 files changed

+492
-83
lines changed
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
package com.example.umc9th.domain.review.controller;
22

3+
import com.example.umc9th.domain.review.converter.ReviewConverter;
4+
import com.example.umc9th.domain.review.dto.res.ReviewResDTO;
35
import com.example.umc9th.domain.review.entity.Review;
4-
import com.example.umc9th.domain.review.service.ReviewQueryService;
6+
import com.example.umc9th.domain.review.service.query.ReviewQueryServiceImpl;
7+
import com.example.umc9th.global.apiPayload.ApiResponse;
8+
import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode;
59
import lombok.RequiredArgsConstructor;
6-
import org.springframework.web.bind.annotation.GetMapping;
7-
import org.springframework.web.bind.annotation.RequestMapping;
8-
import org.springframework.web.bind.annotation.RequestParam;
9-
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.web.bind.annotation.*;
1011

1112
import java.util.List;
1213

1314
@RestController
1415
@RequiredArgsConstructor
1516
@RequestMapping("/api/reviews")
1617
public class ReviewController {
17-
private final ReviewQueryService reviewQueryService;
18+
private final ReviewQueryServiceImpl reviewQueryService;
19+
1820

1921
// /api/reviews/me?memberId=7
2022
// /api/reviews/me?memberId=7&type=restaurant&query=반이학생마라탕마라반
2123
// /api/reviews/me?memberId=7&type=rating&query=4
2224
// /api/reviews/me?memberId=7&type=both&query=반이학생마라탕마라반&4
2325
@GetMapping("/me")
24-
public List<Review> myReviews(
26+
public ApiResponse<ReviewResDTO.MyReviews> myReviews(
2527
@RequestParam Long memberId,
2628
@RequestParam(required = false) String type,
2729
@RequestParam(required = false) String query
2830
) {
29-
return reviewQueryService.searchMyReviews(memberId, type, query);
31+
List<Review> list = reviewQueryService.searchMyReviews(memberId, type, query);
32+
ReviewResDTO.MyReviews dto = ReviewConverter.toMyReviews(list);
33+
return ApiResponse.onSuccess(GeneralSuccessCode.OK, dto);
3034
}
35+
36+
37+
3138
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.example.umc9th.domain.review.converter;
2+
3+
import com.example.umc9th.domain.review.dto.res.ReviewResDTO;
4+
import com.example.umc9th.domain.review.entity.Review;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
public class ReviewConverter {
10+
public static ReviewResDTO.MyReview toMyReview(Review review) {
11+
return ReviewResDTO.MyReview.builder()
12+
.reviewId(review.getId())
13+
.restaurantName(review.getRestaurant().getRestaurantName())
14+
.content(review.getReviewContent())
15+
.rating(review.getRating())
16+
.createdAt(review.getCreatedAt())
17+
.build();
18+
}
19+
20+
public static ReviewResDTO.MyReviews toMyReviews(List<Review> list) {
21+
List<ReviewResDTO.MyReview> reviews = new ArrayList<>();
22+
if (list != null) {
23+
for (Review review : list) {
24+
if (review != null) {
25+
reviews.add(toMyReview(review));
26+
}
27+
}
28+
}
29+
return ReviewResDTO.MyReviews.builder()
30+
.reviews(reviews)
31+
.totalCount(reviews.size())
32+
.build();
33+
}
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.example.umc9th.domain.review.dto.res;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
import java.time.LocalDateTime;
6+
import java.util.List;
7+
8+
public class ReviewResDTO {
9+
10+
@Builder
11+
@Getter
12+
public static class MyReview {
13+
private Long reviewId;
14+
private String restaurantName;
15+
private String content;
16+
private Double rating;
17+
private LocalDateTime createdAt;
18+
}
19+
20+
@Builder
21+
@Getter
22+
public static class MyReviews {
23+
private List<MyReview> reviews;
24+
private Integer totalCount;
25+
}
26+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.umc9th.domain.review.exception;
2+
3+
import com.example.umc9th.global.apiPayload.code.BaseErrorCode;
4+
import com.example.umc9th.global.apiPayload.exception.GeneralException;
5+
6+
public class ReviewException extends GeneralException {
7+
public ReviewException(BaseErrorCode code) {
8+
super(code);
9+
}
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.example.umc9th.domain.review.exception.code;
2+
3+
import com.example.umc9th.global.apiPayload.code.BaseErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import org.springframework.http.HttpStatus;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public enum ReviewErrorCode implements BaseErrorCode {
11+
INVALID_TYPE(HttpStatus.BAD_REQUEST, "REVIEW400_1", "type 값이 올바르지 않습니다. [restaurant|rating|both|all]"),
12+
MISSING_QUERY(HttpStatus.BAD_REQUEST, "REVIEW400_2", "해당 type에 필요한 query 값이 없습니다."),
13+
INVALID_BOTH_QUERY(HttpStatus.BAD_REQUEST, "REVIEW400_3", "both 타입의 query는 '이름&숫자' 형식이어야 합니다."),
14+
INVALID_RATING(HttpStatus.BAD_REQUEST, "REVIEW400_4", "rating은 0.0 ~ 5.0 사이의 숫자여야 합니다."),;
15+
16+
private final HttpStatus status;
17+
private final String code;
18+
private final String message;
19+
}

src/main/java/com/example/umc9th/domain/review/service/ReviewQueryService.java

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.umc9th.domain.review.service.query;
2+
3+
import com.example.umc9th.domain.review.entity.Review;
4+
5+
import java.util.List;
6+
7+
public interface ReviewQueryService {
8+
List<Review> searchMyReviews(Long memberId, String type, String query);
9+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.example.umc9th.domain.review.service.query;
2+
3+
import com.example.umc9th.domain.review.entity.QReview;
4+
import com.example.umc9th.domain.review.entity.Review;
5+
import com.example.umc9th.domain.review.exception.ReviewException;
6+
import com.example.umc9th.domain.review.exception.code.ReviewErrorCode;
7+
import com.example.umc9th.domain.review.repository.ReviewRepository;
8+
import com.querydsl.core.BooleanBuilder;
9+
import jakarta.transaction.Transactional;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.util.List;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
@Transactional
18+
public class ReviewQueryServiceImpl implements ReviewQueryService {
19+
private final ReviewRepository reviewRepository;
20+
21+
22+
public List<Review> searchMyReviews(Long memberId, String type, String query){
23+
String safeType = (type == null) ? "all" : type.trim();
24+
String safeQuery = (query == null) ? "" : query.trim();
25+
26+
QReview review = QReview.review;
27+
28+
// BooleanBuilder 선언
29+
BooleanBuilder builder = new BooleanBuilder();
30+
31+
// BooleanBuilder 사용
32+
33+
// 1) 내가 작성한 리뷰
34+
builder.and(review.member.id.eq(memberId));
35+
36+
// 동적 쿼리 : 조회 조건
37+
switch (type) {
38+
case "all" -> {
39+
// 추가 필터 없음
40+
}
41+
case "restaurant" -> {
42+
if (query.isEmpty()) throw new ReviewException(ReviewErrorCode.MISSING_QUERY);
43+
builder.and(review.restaurant.restaurantName.contains(query));
44+
}
45+
case "rating" -> {
46+
if (query.isEmpty()) throw new ReviewException(ReviewErrorCode.MISSING_QUERY);
47+
applyRatingFilterOrThrow(builder, review, query);
48+
}
49+
case "both" -> {
50+
if (query.isEmpty() || !query.contains("&")) {
51+
throw new ReviewException(ReviewErrorCode.INVALID_BOTH_QUERY);
52+
}
53+
String[] parts = query.split("&", 2);
54+
String name = parts[0].trim();
55+
String ratingStr = parts[1].trim();
56+
if (name.isEmpty() || ratingStr.isEmpty()) {
57+
throw new ReviewException(ReviewErrorCode.INVALID_BOTH_QUERY);
58+
}
59+
builder.and(review.restaurant.restaurantName.contains(name));
60+
applyRatingFilterOrThrow(builder, review, ratingStr);
61+
}
62+
default -> throw new ReviewException(ReviewErrorCode.INVALID_TYPE);
63+
}
64+
65+
66+
List<Review> reviewList = reviewRepository.searchReview(builder);
67+
68+
return reviewList;
69+
}
70+
71+
private void applyRatingFilterOrThrow(BooleanBuilder builder, QReview review, String ratingText) {
72+
if (ratingText == null || ratingText.isEmpty()) return;
73+
74+
double base;
75+
try {
76+
base = Double.parseDouble(ratingText);
77+
} catch (NumberFormatException e) {
78+
throw new ReviewException(ReviewErrorCode.INVALID_RATING);
79+
}
80+
81+
if (base < 0.0 || base > 5.0) {
82+
throw new ReviewException(ReviewErrorCode.INVALID_RATING);
83+
}
84+
85+
if (base >= 5.0) {
86+
builder.and(review.rating.eq(5.0));
87+
} else if (base >= 0.0) {
88+
double lower = base;
89+
double upper = Math.min(5.0, base + 1.0);
90+
builder.and(review.rating.goe(lower));
91+
builder.and(review.rating.lt(upper));
92+
}
93+
}
94+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.example.umc9th.domain.test.controller;
2+
3+
import com.example.umc9th.domain.test.converter.TestConverter;
4+
import com.example.umc9th.domain.test.dto.res.TestResDTO;
5+
import com.example.umc9th.domain.test.service.query.TestQueryService;
6+
import com.example.umc9th.global.apiPayload.ApiResponse;
7+
import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequiredArgsConstructor
16+
@RequestMapping("/temp")
17+
public class TestController {
18+
private final TestQueryService testQueryService;
19+
20+
@GetMapping("/test")
21+
public ApiResponse<TestResDTO.Testing> test() throws Exception {
22+
// 응답 코드 정의
23+
GeneralSuccessCode code = GeneralSuccessCode.OK;
24+
return ApiResponse.onSuccess(
25+
code,
26+
TestConverter.toTestingDTO("This is Test!")
27+
);
28+
}
29+
30+
// 예외 상황
31+
@GetMapping("/exception")
32+
public ApiResponse<TestResDTO.Exception> exception(
33+
@RequestParam Long flag
34+
) {
35+
testQueryService.checkFlag(flag);
36+
37+
// 응답 코드 정의
38+
GeneralSuccessCode code = GeneralSuccessCode.OK;
39+
return ApiResponse.onSuccess(code, TestConverter.toExceptionDTO("This is Test!"));
40+
}
41+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.example.umc9th.domain.test.converter;
2+
3+
import com.example.umc9th.domain.test.dto.res.TestResDTO;
4+
5+
public class TestConverter {
6+
// 객체 -> DTO
7+
public static TestResDTO.Testing toTestingDTO(
8+
String testing
9+
) {
10+
return TestResDTO.Testing.builder()
11+
.testString(testing)
12+
.build();
13+
}
14+
15+
16+
// 객체 -> DTO
17+
public static TestResDTO.Exception toExceptionDTO(
18+
String testing
19+
){
20+
return TestResDTO.Exception.builder()
21+
.testString(testing)
22+
.build();
23+
}
24+
}

0 commit comments

Comments
 (0)