ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,82 @@
+package com.example.swapit.service;
+
+import java.util.List;
+
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+
+import com.example.swapit.common.exception.CustomException;
+import com.example.swapit.common.exception.ErrorCode;
+import com.example.swapit.domain.Reviews;
+import com.example.swapit.domain.TradeStatus;
+import com.example.swapit.domain.Trades;
+import com.example.swapit.domain.Users;
+import com.example.swapit.domain.dto.ReviewDto;
+import com.example.swapit.domain.dto.ReviewListDto;
+import com.example.swapit.domain.dto.ReviewRequestDto;
+import com.example.swapit.repository.ReviewRepository;
+import com.example.swapit.repository.TradesRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReviewServiceImpl implements ReviewService {
+
+	private final ReviewRepository reviewRepository;
+	private final TradesRepository tradesRepository;
+	private final CurrentUserService currentUserService;
+
+	@Override
+	public void createReview(ReviewRequestDto reviewRequestDto) {
+		Users writer = currentUserService.getCurrentUser();
+		Trades trade = tradesRepository.findById(reviewRequestDto.getTradesId())
+			.orElseThrow(() -> new CustomException(ErrorCode.TRADES_NOT_FOUND));
+
+		// 해당 거래가 COMPLETED 되었는지 검증.
+		if (!trade.getStatus().equals(TradeStatus.COMPLETED)) {
+			throw new CustomException(ErrorCode.REVIEW_TRADE_NOT_COMPLETED);
+		}
+
+		// 리뷰 대상자 설정
+		Users reviewee;
+		if (trade.getRequester().getUsersId().equals(writer.getUsersId())) {
+			reviewee = trade.getOwner(); // 요청자가 작성하는 경우 -> 리뷰 대상자는 소유자
+		} else if (trade.getOwner().getUsersId().equals(writer.getUsersId())) {
+			reviewee = trade.getRequester(); // 소유자가 작성하는 경우 -> 리뷰 대상자는 요청자
+		} else {
+			throw new CustomException(ErrorCode.REVIEW_UNAUTHORIZED_ACCESS);
+		}
+
+		// 리뷰 저장
+		Reviews review = Reviews.builder()
+			.writer(writer)
+			.reviewee(reviewee)
+			.trade(trade)
+			.content(reviewRequestDto.getContent())
+			.rating(reviewRequestDto.getRating())
+			.build();
+
+		try {
+			reviewRepository.save(review);
+		} catch (DataIntegrityViolationException e) {
+			log.error("[리뷰 에러] {}, writer ID : {}, trade ID :  {}", ErrorCode.REVIEW_DUPLICATE.getMessage(),
+				writer.getUsersId(), trade.getId());
+			throw new CustomException(ErrorCode.REVIEW_DUPLICATE);
+		}
+
+	}
+
+	@Override
+	public ReviewListDto getMyReviews() {
+		Users reviewee = currentUserService.getCurrentUser();
+		List<ReviewDto> reviewDtos = reviewRepository.findAllByRevieweeOrderByCreatedAtDesc(reviewee)
+			.stream()
+			.map(ReviewDto::of)
+			.toList();
+
+		return new ReviewListDto(reviewDtos.size(), reviewDtos);
+	}
+}
\ No newline at end of file

리뷰: 유니크 제약조건 걸었으면 확인할 필요 없이 save할 때 try-catch 걸어서 발생하는 exception으로 처리 가능합니다! 미리 조회하는것보다 그게 낫다고 하더라구요..
@@ -0,0 +1,82 @@
+package com.example.swapit.service;
+
+import java.util.List;
+
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+
+import com.example.swapit.common.exception.CustomException;
+import com.example.swapit.common.exception.ErrorCode;
+import com.example.swapit.domain.Reviews;
+import com.example.swapit.domain.TradeStatus;
+import com.example.swapit.domain.Trades;
+import com.example.swapit.domain.Users;
+import com.example.swapit.domain.dto.ReviewDto;
+import com.example.swapit.domain.dto.ReviewListDto;
+import com.example.swapit.domain.dto.ReviewRequestDto;
+import com.example.swapit.repository.ReviewRepository;
+import com.example.swapit.repository.TradesRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReviewServiceImpl implements ReviewService {
+
+	private final ReviewRepository reviewRepository;
+	private final TradesRepository tradesRepository;
+	private final CurrentUserService currentUserService;
+
+	@Override
+	public void createReview(ReviewRequestDto reviewRequestDto) {
+		Users writer = currentUserService.getCurrentUser();
+		Trades trade = tradesRepository.findById(reviewRequestDto.getTradesId())
+			.orElseThrow(() -> new CustomException(ErrorCode.TRADES_NOT_FOUND));
+
+		// 해당 거래가 COMPLETED 되었는지 검증.
+		if (!trade.getStatus().equals(TradeStatus.COMPLETED)) {
+			throw new CustomException(ErrorCode.REVIEW_TRADE_NOT_COMPLETED);
+		}
+
+		// 리뷰 대상자 설정
+		Users reviewee;
+		if (trade.getRequester().getUsersId().equals(writer.getUsersId())) {
+			reviewee = trade.getOwner(); // 요청자가 작성하는 경우 -> 리뷰 대상자는 소유자
+		} else if (trade.getOwner().getUsersId().equals(writer.getUsersId())) {
+			reviewee = trade.getRequester(); // 소유자가 작성하는 경우 -> 리뷰 대상자는 요청자
+		} else {
+			throw new CustomException(ErrorCode.REVIEW_UNAUTHORIZED_ACCESS);
+		}
+
+		// 리뷰 저장
+		Reviews review = Reviews.builder()
+			.writer(writer)
+			.reviewee(reviewee)
+			.trade(trade)
+			.content(reviewRequestDto.getContent())
+			.rating(reviewRequestDto.getRating())
+			.build();
+
+		try {
+			reviewRepository.save(review);
+		} catch (DataIntegrityViolationException e) {
+			log.error("[리뷰 에러] {}, writer ID : {}, trade ID :  {}", ErrorCode.REVIEW_DUPLICATE.getMessage(),
+				writer.getUsersId(), trade.getId());
+			throw new CustomException(ErrorCode.REVIEW_DUPLICATE);
+		}
+
+	}
+
+	@Override
+	public ReviewListDto getMyReviews() {
+		Users reviewee = currentUserService.getCurrentUser();
+		List<ReviewDto> reviewDtos = reviewRepository.findAllByRevieweeOrderByCreatedAtDesc(reviewee)
+			.stream()
+			.map(ReviewDto::of)
+			.toList();
+
+		return new ReviewListDto(reviewDtos.size(), reviewDtos);
+	}
+}
\ No newline at end of file

리뷰: 오 완전 좋은 방법이네요!! 찾아보니까 성능 상으로 더 좋네요!! 배워갑니다 👍
@@ -13,7 +13,7 @@ const useReviewInfoData = ({ reviewRequestCode }: UseReviewInfoDataProps) => {
   };
 
   const { data } = useSuspenseQuery<ReviewInfoData>({
-    queryKey: [REVIEW_QUERY_KEY.reviewInfoData],
+    queryKey: [REVIEW_QUERY_KEY.reviewInfoData, reviewRequestCode],
     queryFn: () => fetchReviewInfoData(),
     staleTime: 60 * 60 * 1000,
   });

리뷰: 아예 staleTime을 지우는 것보다는, 리뷰 요청 코드를 Props로 받고 있으니 이를 쿼리 키에 포함시키는 것을 어떨까요? 리뷰 요청 코드는 유일한 코드이니, 중복 문제는 없을거고, 동일한 쿼리 키를 사용해서 쿼리 캐싱이 되는 것 같아서요. 

`useGetGroupedReviews`이 훅을 참고하면 좋을 것 같아요.
@@ -13,7 +13,7 @@ const useReviewInfoData = ({ reviewRequestCode }: UseReviewInfoDataProps) => {
   };
 
   const { data } = useSuspenseQuery<ReviewInfoData>({
-    queryKey: [REVIEW_QUERY_KEY.reviewInfoData],
+    queryKey: [REVIEW_QUERY_KEY.reviewInfoData, reviewRequestCode],
     queryFn: () => fetchReviewInfoData(),
     staleTime: 60 * 60 * 1000,
   });

리뷰: 쿼리 키로 포함하는 것이 나을 것 같네요. 동일한 reviewRequestCode에 대해서는 캐싱하는 것이 맞는 것 같습니다
@@ -0,0 +1,18 @@
+const GATHERING_TYPE = ["DALLAEMFIT", "OFFICE_STRETCHING", "MINDFULNESS", "WORKATION"] as const;
+
+export type ScoresQuery = {
+  gatheringId?: number;
+  type?: (typeof GATHERING_TYPE)[number];
+};
+
+export type ScoresResponse = {
+  teamId: number;
+  gatheringId: number;
+  type?: (typeof GATHERING_TYPE)[number];
+  averageScore: number;
+  oneStar: number;
+  twoStars: number;
+  threeStars: number;
+  fourStars: number;
+  fiveStars: number;
+};

리뷰: _:warning: Potential issue_

**ScoresQuery의 type 필드 타입이 잘못 정의되어 있습니다.**

현재 `type?: typeof GATHERING_TYPE`는 배열 타입을 의미하며, 이는 의도한 타입이 아닐 것 같습니다.

다음과 같이 수정하는 것을 제안드립니다:

```diff
 export type ScoresQuery = {
   gatheringId?: number;
-  type?: typeof GATHERING_TYPE;
+  type?: (typeof GATHERING_TYPE)[number];
 };
```

또는 더 명시적으로:

```typescript
export const GATHERING_TYPE = {
  DALLAEMFIT: "DALLAEMFIT",
  OFFICE_STRETCHING: "OFFICE_STRETCHING",
  MINDFULNESS: "MINDFULNESS",
  WORKATION: "WORKATION",
} as const;

export type GatheringType = (typeof GATHERING_TYPE)[keyof typeof GATHERING_TYPE];

export type ScoresQuery = {
  gatheringId?: number;
  type?: GatheringType;
};
```

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,18 @@
+const GATHERING_TYPE = ["DALLAEMFIT", "OFFICE_STRETCHING", "MINDFULNESS", "WORKATION"] as const;
+
+export type ScoresQuery = {
+  gatheringId?: number;
+  type?: (typeof GATHERING_TYPE)[number];
+};
+
+export type ScoresResponse = {
+  teamId: number;
+  gatheringId: number;
+  type?: (typeof GATHERING_TYPE)[number];
+  averageScore: number;
+  oneStar: number;
+  twoStars: number;
+  threeStars: number;
+  fourStars: number;
+  fiveStars: number;
+};

리뷰: _:warning: Potential issue_

**ScoresResponse에도 동일한 타입 이슈가 있습니다.**

ScoresResponse의 type 필드도 같은 문제를 가지고 있습니다.

다음과 같이 수정해 주세요:

```diff
 export type ScoresResponse = {
   teamId: number;
   gatheringId: number;
-  type?: typeof GATHERING_TYPE;
+  type?: (typeof GATHERING_TYPE)[number];
   averageScore: number;
   oneStar: number;
   twoStars: number;
   threeStars: number;
   fourStars: number;
   fiveStars: number;
 };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export type ScoresResponse = {
  teamId: number;
  gatheringId: number;
  type?: (typeof GATHERING_TYPE)[number];
  averageScore: number;
  oneStar: number;
  twoStars: number;
  threeStars: number;
  fourStars: number;
  fiveStars: number;
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,31 @@
+import { fetchReviewsScores } from "../_utils/api";
+import ScoreProgressBar from "./ScoreProgressBar";
+import Score from "@/components/Score";
+
+export default async function ReviewsAverage() {
+  const [scores] = await fetchReviewsScores();
+
+  const { averageScore, oneStar, twoStars, threeStars, fourStars, fiveStars } = scores;
+  const total = oneStar + twoStars + threeStars + fourStars + fiveStars;
+  const stars = [fiveStars, fourStars, threeStars, twoStars, oneStar];
+
+  return (
+    <div className="flex w-full max-w-[995px] justify-center border-y-2 border-gray-200 bg-white px-6 py-8">
+      <div className="flex w-full max-w-[610px] justify-between">
+        <div className="flex flex-col items-center justify-center gap-2 pr-5">
+          <div className="flex items-center justify-center text-xl font-semibold md:text-2xl">
+            <span>{averageScore}</span>
+            <span className="text-gray-400">/5</span>
+          </div>
+          <Score score={averageScore} />
+        </div>
+
+        <div className="flex max-w-[302px] flex-1 flex-col gap-1">
+          {stars.map((count, index) => (
+            <ScoreProgressBar key={index} score={5 - index} stars={count} total={total} />
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

리뷰: _:warning: Potential issue_

**map 함수에 key prop이 필요합니다.**

React 배열 렌더링 시 각 요소를 식별하기 위한 key prop이 누락되었습니다.


다음과 같이 수정하는 것을 제안드립니다:

```diff
 {stars.map((count, index) => (
-  <ScoreProgressBar score={5 - index} stars={count} total={total} />
+  <ScoreProgressBar
+    key={5 - index}
+    score={5 - index}
+    stars={count}
+    total={total}
+  />
 ))}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          {stars.map((count, index) => (
            <ScoreProgressBar
              key={5 - index}
              score={5 - index}
              stars={count}
              total={total}
            />
          ))}
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 25-25: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation. 

(lint/correctness/useJsxKeyInIterable)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,31 @@
+import { fetchReviewsScores } from "../_utils/api";
+import ScoreProgressBar from "./ScoreProgressBar";
+import Score from "@/components/Score";
+
+export default async function ReviewsAverage() {
+  const [scores] = await fetchReviewsScores();
+
+  const { averageScore, oneStar, twoStars, threeStars, fourStars, fiveStars } = scores;
+  const total = oneStar + twoStars + threeStars + fourStars + fiveStars;
+  const stars = [fiveStars, fourStars, threeStars, twoStars, oneStar];
+
+  return (
+    <div className="flex w-full max-w-[995px] justify-center border-y-2 border-gray-200 bg-white px-6 py-8">
+      <div className="flex w-full max-w-[610px] justify-between">
+        <div className="flex flex-col items-center justify-center gap-2 pr-5">
+          <div className="flex items-center justify-center text-xl font-semibold md:text-2xl">
+            <span>{averageScore}</span>
+            <span className="text-gray-400">/5</span>
+          </div>
+          <Score score={averageScore} />
+        </div>
+
+        <div className="flex max-w-[302px] flex-1 flex-col gap-1">
+          {stars.map((count, index) => (
+            <ScoreProgressBar key={index} score={5 - index} stars={count} total={total} />
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

리뷰: _:warning: Potential issue_

**데이터 로딩 및 에러 상태 처리가 필요합니다.**

비동기 데이터 페칭 시 발생할 수 있는 에러와 로딩 상태에 대한 처리가 누락되었습니다.


다음과 같이 에러 처리를 추가하는 것을 제안드립니다:

```diff
 export default async function ReviewsAverage() {
-  const [scores] = await fetchReviewsScores();
+  try {
+    const [scores] = await fetchReviewsScores();
+  } catch (error) {
+    console.error('리뷰 점수를 불러오는데 실패했습니다:', error);
+    return <div>리뷰 점수를 불러오는데 실패했습니다. 다시 시도해 주세요.</div>;
+  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export default async function ReviewsAverage() {
  try {
    const [scores] = await fetchReviewsScores();
  } catch (error) {
    console.error('리뷰 점수를 불러오는데 실패했습니다:', error);
    return <div>리뷰 점수를 불러오는데 실패했습니다. 다시 시도해 주세요.</div>;
  }
  // ... rest of the component logic
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +1,18 @@
+const GATHERING_TYPE = ["DALLAEMFIT", "OFFICE_STRETCHING", "MINDFULNESS", "WORKATION"] as const;
+
+export type ScoresQuery = {
+  gatheringId?: number;
+  type?: (typeof GATHERING_TYPE)[number];
+};
+
+export type ScoresResponse = {
+  teamId: number;
+  gatheringId: number;
+  type?: (typeof GATHERING_TYPE)[number];
+  averageScore: number;
+  oneStar: number;
+  twoStars: number;
+  threeStars: number;
+  fourStars: number;
+  fiveStars: number;
+};

리뷰: 코드래빗 말대로 type이 (typeof GATHERING_TYPE)[number]; 가 되어야 할 것 같습니다.
@@ -0,0 +1,31 @@
+import { fetchReviewsScores } from "../_utils/api";
+import ScoreProgressBar from "./ScoreProgressBar";
+import Score from "@/components/Score";
+
+export default async function ReviewsAverage() {
+  const [scores] = await fetchReviewsScores();
+
+  const { averageScore, oneStar, twoStars, threeStars, fourStars, fiveStars } = scores;
+  const total = oneStar + twoStars + threeStars + fourStars + fiveStars;
+  const stars = [fiveStars, fourStars, threeStars, twoStars, oneStar];
+
+  return (
+    <div className="flex w-full max-w-[995px] justify-center border-y-2 border-gray-200 bg-white px-6 py-8">
+      <div className="flex w-full max-w-[610px] justify-between">
+        <div className="flex flex-col items-center justify-center gap-2 pr-5">
+          <div className="flex items-center justify-center text-xl font-semibold md:text-2xl">
+            <span>{averageScore}</span>
+            <span className="text-gray-400">/5</span>
+          </div>
+          <Score score={averageScore} />
+        </div>
+
+        <div className="flex max-w-[302px] flex-1 flex-col gap-1">
+          {stars.map((count, index) => (
+            <ScoreProgressBar key={index} score={5 - index} stars={count} total={total} />
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}

리뷰: 여기 key prop이 필요할 것 같습니다... 근데 코드래빗이 이미 리뷰해줬네요
@@ -0,0 +1,19 @@
+package com.fourseason.delivery.domain.review.dto.request;
+
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.PositiveOrZero;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+public record ReviewRequestDto(
+        String content,
+
+        @NotNull(message = "평점은 필수 입력 값입니다.")
+        @PositiveOrZero(message = "평점은 0 이상이어야 합니다.")
+        int rating,
+
+        List<MultipartFile> images
+) {
+}

리뷰: dto의 경우 record로 작성하기로 했던걸로 기억합니당.
@@ -0,0 +1,17 @@
+package com.fourseason.delivery.domain.review.repository;
+
+import com.fourseason.delivery.domain.order.entity.Order;
+import com.fourseason.delivery.domain.review.entity.Review;
+import com.fourseason.delivery.domain.shop.entity.Shop;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface ReviewRepository extends JpaRepository<Review, UUID> {
+    Optional<Review> findByIdAndOrderId(UUID reviewId, UUID orderId);
+
+    List<Review> findByShop(Shop shop);
+
+}

리뷰: 데이터들을 실제로 삭제하지 않고 논리적 삭제 처리 하기로 했던 부분이라
findByIdAndDeletedAtIsNotNull 과 같은 형태로 조회해주셔야... 
저도 이 부분 처음에 까먹었습니다.
@@ -0,0 +1,62 @@
+package com.fourseason.delivery.domain.review.controller;
+
+import com.fourseason.delivery.domain.review.dto.request.ReviewRequestDto;
+import com.fourseason.delivery.domain.review.dto.response.ReviewResponseDto;
+import com.fourseason.delivery.domain.review.service.ReviewService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/orders/{order_id}/reviews")
+@RequiredArgsConstructor
+public class ReviewController {
+
+    private final ReviewService reviewService;
+
+    /**
+     * 리뷰 등록
+     */
+    @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
+    public ResponseEntity<Void> createReview(@PathVariable String orderId,
+                                             @RequestPart("review") ReviewRequestDto reviewRequestDto) {
+        reviewService.createReview(orderId, reviewRequestDto);
+        return ResponseEntity.ok().build();
+    }
+
+
+    /**
+     * 특정 리뷰 조회
+     */
+    @GetMapping("/{review_id}")
+    public ResponseEntity<ReviewResponseDto> getReview(@PathVariable String orderId,
+                                                       @PathVariable String reviewId) {
+        return ResponseEntity.ok(reviewService.getReview(orderId, reviewId));
+    }
+
+    /**
+     * 리뷰 수정
+     */
+    @PostMapping("/{review_id}")
+    public ResponseEntity<ReviewResponseDto> updateReview(@PathVariable String orderId,
+                                                          @PathVariable String reviewId,
+                                                          @RequestBody ReviewRequestDto reviewRequestDto) {
+        return ResponseEntity.ok(reviewService.updateReview(orderId, reviewId, reviewRequestDto));
+    }
+
+
+    /**
+     * 리뷰 삭제
+     */
+    @DeleteMapping("/{review_id}")
+    public ResponseEntity<Void> deleteReview(@PathVariable String orderId,
+                                             @PathVariable String reviewId) {
+        reviewService.deleteReview(orderId, reviewId);
+        return ResponseEntity.ok().build();
+    }
+
+}

리뷰: `String order_id` 는 아직 `Order`를 푸시 안했기 때문에 `UUID` 타입으로 바꾸지 않으신걸까요?
@@ -0,0 +1,62 @@
+package com.fourseason.delivery.domain.review.controller;
+
+import com.fourseason.delivery.domain.review.dto.request.ReviewRequestDto;
+import com.fourseason.delivery.domain.review.dto.response.ReviewResponseDto;
+import com.fourseason.delivery.domain.review.service.ReviewService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/orders/{order_id}/reviews")
+@RequiredArgsConstructor
+public class ReviewController {
+
+    private final ReviewService reviewService;
+
+    /**
+     * 리뷰 등록
+     */
+    @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
+    public ResponseEntity<Void> createReview(@PathVariable String orderId,
+                                             @RequestPart("review") ReviewRequestDto reviewRequestDto) {
+        reviewService.createReview(orderId, reviewRequestDto);
+        return ResponseEntity.ok().build();
+    }
+
+
+    /**
+     * 특정 리뷰 조회
+     */
+    @GetMapping("/{review_id}")
+    public ResponseEntity<ReviewResponseDto> getReview(@PathVariable String orderId,
+                                                       @PathVariable String reviewId) {
+        return ResponseEntity.ok(reviewService.getReview(orderId, reviewId));
+    }
+
+    /**
+     * 리뷰 수정
+     */
+    @PostMapping("/{review_id}")
+    public ResponseEntity<ReviewResponseDto> updateReview(@PathVariable String orderId,
+                                                          @PathVariable String reviewId,
+                                                          @RequestBody ReviewRequestDto reviewRequestDto) {
+        return ResponseEntity.ok(reviewService.updateReview(orderId, reviewId, reviewRequestDto));
+    }
+
+
+    /**
+     * 리뷰 삭제
+     */
+    @DeleteMapping("/{review_id}")
+    public ResponseEntity<Void> deleteReview(@PathVariable String orderId,
+                                             @PathVariable String reviewId) {
+        reviewService.deleteReview(orderId, reviewId);
+        return ResponseEntity.ok().build();
+    }
+
+}

리뷰: 이 부분은 정확히 잘 모르겠지만, `reviewId` 처럼 카멜케이스로 작성되는 것이 더 좋을 것 같다는 생각이 들어요.
그렇게 생각하는 이유는 `@PathVariable` 에서도 `String review_id` 와 같이 사용되기 때문이에요.
@@ -0,0 +1,28 @@
+package com.fourseason.delivery.domain.review.controller;
+
+import com.fourseason.delivery.domain.review.dto.response.ReviewResponseDto;
+import com.fourseason.delivery.domain.review.repository.ReviewRepository;
+import com.fourseason.delivery.domain.review.service.ReviewService;
+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.RestController;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+public class ShopReviewController {
+
+    private final ReviewRepository reviewRepository;
+    private final ReviewService reviewService;
+
+    /**
+     * 특정 가게 리뷰 리스트 조회
+     */
+    @GetMapping("/api/shops/{shop_id}/reviews")
+    public ResponseEntity<List<ReviewResponseDto>> getReviewList(@PathVariable String shopId) {
+        return ResponseEntity.ok(reviewService.getReviewList(shopId));
+    }
+}

리뷰: `/api/shops`  부분은 공통으로 해도 될 것 같아요!

아니면, `ShopController`와 겹치기 때문일까요?