From 63f52f1580904e8aee44ac1b519c5d6e054f02c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=B0=AC?= Date: Sat, 18 Oct 2025 22:52:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,DTO=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 80 ++++++++++++++++++- .../domain/review/dto/ReviewUpdateDto.java | 5 ++ .../dto/request/ReviewReplyRequestDto.java | 3 + .../review/dto/request/ReviewRequestDto.java | 7 +- .../dto/response/ReviewReplyResponseDto.java | 16 ++++ .../dto/response/ReviewResponseDto.java | 22 +++++ 6 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sparta/tdd/domain/review/controller/ReviewController.java b/src/main/java/com/sparta/tdd/domain/review/controller/ReviewController.java index 4c475b57..0d59263d 100644 --- a/src/main/java/com/sparta/tdd/domain/review/controller/ReviewController.java +++ b/src/main/java/com/sparta/tdd/domain/review/controller/ReviewController.java @@ -8,6 +8,8 @@ import com.sparta.tdd.domain.review.dto.response.ReviewResponseDto; import com.sparta.tdd.domain.review.service.ReviewReplyService; import com.sparta.tdd.domain.review.service.ReviewService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -20,6 +22,7 @@ import java.net.URI; import java.util.UUID; +@Tag(name = "리뷰 API") @RestController @RequestMapping("/v1/reviews") @RequiredArgsConstructor @@ -28,6 +31,16 @@ public class ReviewController { private final ReviewService reviewService; private final ReviewReplyService reviewReplyService; + @Operation( + summary = "리뷰 생성", + description = """ + 주문 완료 후 고객이 가게에 대한 리뷰를 작성합니다.\n + orderId, storeId, rating(1~5점), content, photos를 받아 리뷰를 생성합니다.\n + 리뷰 생성 시 가게의 평균 평점이 자동으로 재계산됩니다.\n + 생성된 리뷰의 위치(Location 헤더)와 함께 201 Created 상태로 응답합니다.\n + 인증된 사용자만 접근 가능합니다. + """ + ) @PostMapping("/order/{orderId}") public ResponseEntity createReview( @PathVariable UUID orderId, @@ -43,6 +56,15 @@ public ResponseEntity createReview( } // 리뷰 수정 + @Operation( + summary = "리뷰 수정", + description = """ + 작성한 리뷰의 내용(content), 평점(rating), 사진(photos)을 수정합니다.\n + 본인이 작성한 리뷰만 수정할 수 있으며, 수정 시 가게의 평균 평점이 재계산됩니다.\n + 수정된 리뷰 정보를 200 OK 상태로 응답합니다.\n + 삭제되지 않은 리뷰만 수정 가능합니다. + """ + ) @PatchMapping("/{reviewId}") public ResponseEntity updateReview( @PathVariable UUID reviewId, @@ -58,6 +80,15 @@ public ResponseEntity updateReview( } // 리뷰 개별 조회 + @Operation( + summary = "리뷰 개별 조회", + description = """ + 특정 리뷰의 상세 정보를 조회합니다.\n + 리뷰 정보와 함께 답글이 있는 경우 답글 정보도 함께 반환됩니다.\n + 삭제되지 않은 리뷰만 조회 가능합니다.\n + 인증 없이 모든 사용자가 조회할 수 있습니다. + """ + ) @GetMapping("/{reviewId}") public ResponseEntity getReview(@PathVariable UUID reviewId) { ReviewResponseDto response = reviewService.getReview(reviewId); @@ -65,6 +96,15 @@ public ResponseEntity getReview(@PathVariable UUID reviewId) } // 리뷰 목록 조회 (가게별) + @Operation( + summary = "가게별 리뷰 목록 조회", + description = """ + 특정 가게의 모든 리뷰를 페이징하여 조회합니다.\n + 각 리뷰에 답글이 있는 경우 답글 정보도 함께 반환됩니다.\n + 삭제되지 않은 리뷰만 조회되며, 페이지네이션을 통해 대량의 리뷰를 효율적으로 조회할 수 있습니다.\n + 인증 없이 모든 사용자가 조회할 수 있습니다. + """ + ) @GetMapping("/store/{storeId}") public ResponseEntity> getReviewsByStore( @PathVariable UUID storeId, @@ -75,6 +115,16 @@ public ResponseEntity> getReviewsByStore( } // 리뷰 삭제 + @Operation( + summary = "리뷰 삭제", + description = """ + 작성한 리뷰를 논리적으로 삭제합니다.\n + 본인이 작성한 리뷰만 삭제할 수 있으며, deletedAt과 deletedBy 필드가 설정됩니다.\n + 리뷰 삭제 시 가게의 평균 평점이 자동으로 재계산됩니다.\n + 삭제 성공 시 204 No Content 상태로 응답합니다.\n + 물리적 삭제가 아닌 논리적 삭제(Soft Delete)로 데이터는 DB에 유지됩니다. + """ + ) @DeleteMapping("/{reviewId}") public ResponseEntity deleteReview( @PathVariable UUID reviewId, @@ -85,7 +135,16 @@ public ResponseEntity deleteReview( } // ========== 답글 관련 엔드포인트 ========== - + @Operation( + summary = "리뷰 답글 작성", + description = """ + 가게 사장님이 리뷰에 대한 답글을 작성합니다.\n + OWNER 권한이 필요하며, 본인 가게의 리뷰에만 답글을 작성할 수 있습니다.\n + 하나의 리뷰에는 하나의 답글만 작성 가능합니다.\n + 이미 답글이 존재하는 경우 에러가 발생합니다.\n + 생성된 답글의 위치(Location 헤더)와 함께 201 Created 상태로 응답합니다. + """ + ) @PostMapping("/{reviewId}/reply") public ResponseEntity createReply( @PathVariable UUID reviewId, @@ -102,7 +161,15 @@ public ResponseEntity createReply( URI location = URI.create("/v1/reviews/" + reviewId + "/reply"); return ResponseEntity.created(location).body(response); } - + @Operation( + summary = "리뷰 답글 수정", + description = """ + 작성한 답글의 내용을 수정합니다.\n + OWNER 권한이 필요하며, 본인 가게의 리뷰 답글만 수정할 수 있습니다.\n + 삭제되지 않은 답글만 수정 가능합니다.\n + 수정된 답글 정보를 200 OK 상태로 응답합니다. + """ + ) @PatchMapping("/{reviewId}/reply") public ResponseEntity updateReply( @PathVariable UUID reviewId, @@ -118,6 +185,15 @@ public ResponseEntity updateReply( return ResponseEntity.ok(response); } + @Operation( + summary = "리뷰 답글 삭제", + description = """ + 작성한 답글을 논리적으로 삭제합니다.\n + OWNER, MANAGER, MASTER 권한이 필요하며, 본인 가게의 리뷰 답글만 삭제할 수 있습니다.\n + deletedAt과 deletedBy 필드가 설정되며, 물리적 삭제가 아닌 논리적 삭제(Soft Delete)입니다.\n + 삭제 성공 시 204 No Content 상태로 응답합니다. + """ + ) @DeleteMapping("/{reviewId}/reply") public ResponseEntity deleteReply( @PathVariable UUID reviewId, diff --git a/src/main/java/com/sparta/tdd/domain/review/dto/ReviewUpdateDto.java b/src/main/java/com/sparta/tdd/domain/review/dto/ReviewUpdateDto.java index d257b75f..c32ad1ec 100644 --- a/src/main/java/com/sparta/tdd/domain/review/dto/ReviewUpdateDto.java +++ b/src/main/java/com/sparta/tdd/domain/review/dto/ReviewUpdateDto.java @@ -1,15 +1,20 @@ package com.sparta.tdd.domain.review.dto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; +@Schema(description = "리뷰 수정 요청 DTO") public record ReviewUpdateDto( + @Schema(description = "수정할 리뷰 내용", example = "정말 맛있었습니다!") String content, @Min(value = 1, message = "평점은 1점 이상이어야 합니다.") @Max(value = 5, message = "평점은 5점 이하여야 합니다.") + @Schema(description = "수정할 평점 (1~5점)", example = "4") Integer rating, + @Schema(description = "수정할 사진 URL", example = "https://example.com/new-photo.jpg") String photos ) { } diff --git a/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewReplyRequestDto.java b/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewReplyRequestDto.java index 0798e6ab..1e8c7097 100644 --- a/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewReplyRequestDto.java +++ b/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewReplyRequestDto.java @@ -3,10 +3,13 @@ import com.sparta.tdd.domain.review.entity.Review; import com.sparta.tdd.domain.review.entity.ReviewReply; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +@Schema(description = "리뷰 답글 요청 DTO") public record ReviewReplyRequestDto( @NotBlank(message = "답글 내용은 필수입니다.") + @Schema(description = "답글 내용", example = "좋은 리뷰 감사합니다! 다음에도 방문해주세요.") String content ) { public ReviewReply toEntity(Review review, Long ownerId) { diff --git a/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewRequestDto.java b/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewRequestDto.java index 8108acaf..8d2db8da 100644 --- a/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewRequestDto.java +++ b/src/main/java/com/sparta/tdd/domain/review/dto/request/ReviewRequestDto.java @@ -4,24 +4,29 @@ import com.sparta.tdd.domain.review.entity.Review; import com.sparta.tdd.domain.store.entity.Store; import com.sparta.tdd.domain.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import java.util.UUID; +@Schema(description = "리뷰 생성 요청 DTO") public record ReviewRequestDto( - + @Schema(description = "리뷰 내용", example = "음식이 정말 맛있었어요!") String content, @NotNull(message = "음식점 Id는 필수입니다.") + @Schema(description = "가게 ID", example = "550e8400-e29b-41d4-a716-446655440000") UUID storeId, @NotNull(message = "평점은 필수입니다") @Min(value = 1, message = "평점은 1점 이상이어야 합니다") @Max(value = 5, message = "평점은 5점 이하여야 합니다") + @Schema(description = "평점 (1~5점)", example = "5") Integer rating, + @Schema(description = "리뷰 사진 URL", example = "https://example.com/photo.jpg") String photos ) { public Review toEntity(User user, Store store, Order order) { diff --git a/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewReplyResponseDto.java b/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewReplyResponseDto.java index f0de112f..eb3aa2e3 100644 --- a/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewReplyResponseDto.java +++ b/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewReplyResponseDto.java @@ -2,17 +2,33 @@ package com.sparta.tdd.domain.review.dto.response; import com.sparta.tdd.domain.review.entity.ReviewReply; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.UUID; +@Schema(description = "리뷰 답글 응답 DTO") public record ReviewReplyResponseDto( + + @Schema(description = "답글 ID", example = "550e8400-e29b-41d4-a716-446655440003") UUID replyId, + + @Schema(description = "리뷰 ID", example = "550e8400-e29b-41d4-a716-446655440000") UUID reviewId, + + @Schema(description = "가게 ID", example = "550e8400-e29b-41d4-a716-446655440001") UUID storeId, + + @Schema(description = "사장님 ID", example = "2") Long ownerId, + + @Schema(description = "답글 내용", example = "좋은 리뷰 감사합니다!") String content, + + @Schema(description = "생성일시", example = "2025-01-15T10:30:00") LocalDateTime createdAt, + + @Schema(description = "수정일시", example = "2025-01-15T11:30:00") LocalDateTime modifiedAt ) { public static ReviewReplyResponseDto from(ReviewReply reply) { diff --git a/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewResponseDto.java b/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewResponseDto.java index d4b92999..9c34c4a4 100644 --- a/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewResponseDto.java +++ b/src/main/java/com/sparta/tdd/domain/review/dto/response/ReviewResponseDto.java @@ -1,20 +1,42 @@ package com.sparta.tdd.domain.review.dto.response; import com.sparta.tdd.domain.review.entity.Review; +import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDateTime; import java.util.UUID; +@Schema(description = "리뷰 응답 DTO") public record ReviewResponseDto( + + @Schema(description = "리뷰 ID", example = "550e8400-e29b-41d4-a716-446655440000") UUID reviewId, + + @Schema(description = "가게 ID", example = "550e8400-e29b-41d4-a716-446655440001") UUID storeId, + + @Schema(description = "주문 ID", example = "550e8400-e29b-41d4-a716-446655440002") UUID orderId, + + @Schema(description = "리뷰 내용", example = "음식이 정말 맛있었어요!") String content, + + @Schema(description = "평점 (1~5점)", example = "5") Integer rating, + + @Schema(description = "작성자 ID", example = "1") Long userId, + + @Schema(description = "리뷰 사진 URL", example = "https://example.com/photo.jpg") String photos, + + @Schema(description = "생성일시", example = "2025-01-15T10:30:00") LocalDateTime createdAt, + + @Schema(description = "수정일시", example = "2025-01-15T11:30:00") LocalDateTime modifiedAt, + + @Schema(description = "답글 정보") ReviewReplyInfo reply ) { public static ReviewResponseDto from(Review review) { From 9c0dc621318284b7f191dc1b92610b6fad4acb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A3=BC=EC=B0=AC?= Date: Sat, 18 Oct 2025 23:00:20 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(cart):=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,DTO=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cart/controller/CartController.java | 47 +++++++++++++++++++ .../cart/dto/request/CartItemRequestDto.java | 4 ++ .../dto/response/CartItemResponseDto.java | 23 ++++++--- .../cart/dto/response/CartResponseDto.java | 14 +++++- 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sparta/tdd/domain/cart/controller/CartController.java b/src/main/java/com/sparta/tdd/domain/cart/controller/CartController.java index dbf1ef77..7c530bb8 100644 --- a/src/main/java/com/sparta/tdd/domain/cart/controller/CartController.java +++ b/src/main/java/com/sparta/tdd/domain/cart/controller/CartController.java @@ -4,6 +4,8 @@ import com.sparta.tdd.domain.cart.dto.request.CartItemRequestDto; import com.sparta.tdd.domain.cart.dto.response.CartResponseDto; import com.sparta.tdd.domain.cart.service.CartService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -13,6 +15,7 @@ import java.util.UUID; +@Tag(name = "Cart", description = "장바구니 관리 API") @RestController @RequestMapping("/v1/cart") @RequiredArgsConstructor @@ -22,6 +25,15 @@ public class CartController { private final CartService cartService; // 장바구니 조회 + @Operation( + summary = "장바구니 조회", + description = """ + 사용자의 장바구니를 조회합니다.\n + 장바구니가 없으면 자동으로 생성됩니다.\n + 장바구니에 담긴 모든 아이템과 총 금액을 반환합니다.\n + CUSTOMER, MANAGER, MASTER 권한이 필요합니다. + """ + ) @GetMapping public ResponseEntity getCart( @AuthenticationPrincipal UserDetailsImpl userDetails @@ -31,6 +43,15 @@ public ResponseEntity getCart( } // 장바구니에 아이템 추가 + @Operation( + summary = "장바구니에 아이템 추가", + description = """ + 장바구니에 메뉴를 추가합니다.\n + 같은 메뉴가 이미 있으면 수량이 증가합니다.\n + 다른 가게의 메뉴를 추가하려면 기존 장바구니를 비워야 합니다.\n + 추가된 장바구니 정보를 반환합니다. + """ + ) @PostMapping("/items") public ResponseEntity addItemToCart( @AuthenticationPrincipal UserDetailsImpl userDetails, @@ -41,6 +62,15 @@ public ResponseEntity addItemToCart( } // 장바구니 아이템 수량 수정 + @Operation( + summary = "장바구니 아이템 수량 수정", + description = """ + 장바구니에 담긴 아이템의 수량을 변경합니다.\n + 수량은 1개 이상이어야 합니다.\n + 본인의 장바구니 아이템만 수정할 수 있습니다.\n + 수정된 장바구니 정보를 반환합니다. + """ + ) @PatchMapping("/items/{cartItemId}") public ResponseEntity updateCartItemQuantity( @AuthenticationPrincipal UserDetailsImpl userDetails, @@ -54,6 +84,15 @@ public ResponseEntity updateCartItemQuantity( } // 장바구니 아이템 삭제 + @Operation( + summary = "장바구니 아이템 삭제", + description = """ + 장바구니에서 특정 아이템을 삭제합니다.\n + 본인의 장바구니 아이템만 삭제할 수 있습니다.\n + 마지막 아이템을 삭제하면 가게 정보도 초기화됩니다.\n + 삭제 후 장바구니 정보를 반환합니다. + """ + ) @DeleteMapping("/items/{cartItemId}") public ResponseEntity removeCartItem( @AuthenticationPrincipal UserDetailsImpl userDetails, @@ -66,6 +105,14 @@ public ResponseEntity removeCartItem( } // 장바구니 전체 비우기 + @Operation( + summary = "장바구니 전체 비우기", + description = """ + 장바구니의 모든 아이템을 삭제합니다.\n + 가게 정보도 함께 초기화됩니다.\n + 삭제 성공 시 204 No Content 상태로 응답합니다. + """ + ) @DeleteMapping public ResponseEntity clearCart( @AuthenticationPrincipal UserDetailsImpl userDetails diff --git a/src/main/java/com/sparta/tdd/domain/cart/dto/request/CartItemRequestDto.java b/src/main/java/com/sparta/tdd/domain/cart/dto/request/CartItemRequestDto.java index 58470ec8..281d930e 100644 --- a/src/main/java/com/sparta/tdd/domain/cart/dto/request/CartItemRequestDto.java +++ b/src/main/java/com/sparta/tdd/domain/cart/dto/request/CartItemRequestDto.java @@ -1,14 +1,18 @@ package com.sparta.tdd.domain.cart.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.util.UUID; +@Schema(description = "장바구니 아이템 추가 요청 DTO") public record CartItemRequestDto( @NotNull(message = "메뉴 ID는 필수입니다.") + @Schema(description = "메뉴 ID", example = "550e8400-e29b-41d4-a716-446655440000") UUID menuId, @NotNull(message = "수량은 필수입니다.") @Positive(message = "수량은 1개 이상이어야 합니다.") + @Schema(description = "주문 수량", example = "2") Integer quantity ) {} \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartItemResponseDto.java b/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartItemResponseDto.java index 68960e2d..8a36aa20 100644 --- a/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartItemResponseDto.java +++ b/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartItemResponseDto.java @@ -1,17 +1,30 @@ package com.sparta.tdd.domain.cart.dto.response; import com.sparta.tdd.domain.cart.entity.CartItem; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.UUID; +@Schema(description = "장바구니 아이템 정보") public record CartItemResponseDto( + @Schema(description = "장바구니 아이템 ID", example = "550e8400-e29b-41d4-a716-446655440002") UUID cartItemId, + + @Schema(description = "메뉴 ID", example = "550e8400-e29b-41d4-a716-446655440003") UUID menuId, + + @Schema(description = "메뉴 이름", example = "후라이드 치킨") String menuName, + + @Schema(description = "메뉴 가격", example = "17000") Integer price, + + @Schema(description = "주문 수량", example = "2") Integer quantity, - Integer totalPrice, - UUID storeId, - String storeName + + @Schema(description = "총액 (가격 × 수량)", example = "34000") + Integer totalPrice + ) { public static CartItemResponseDto from(CartItem cartItem) { return new CartItemResponseDto( @@ -20,9 +33,7 @@ public static CartItemResponseDto from(CartItem cartItem) { cartItem.getMenu().getName(), cartItem.getPrice(), cartItem.getQuantity(), - cartItem.getPrice() * cartItem.getQuantity(), - cartItem.getStore().getId(), - cartItem.getStore().getName() + cartItem.getPrice() * cartItem.getQuantity() ); } } \ No newline at end of file diff --git a/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartResponseDto.java b/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartResponseDto.java index 0b177d7d..26a9d55d 100644 --- a/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartResponseDto.java +++ b/src/main/java/com/sparta/tdd/domain/cart/dto/response/CartResponseDto.java @@ -2,16 +2,28 @@ import com.sparta.tdd.domain.cart.entity.Cart; import com.sparta.tdd.domain.store.entity.Store; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import java.util.UUID; - +@Schema(description = "장바구니 응답 DTO") public record CartResponseDto( + @Schema(description = "장바구니 ID", example = "550e8400-e29b-41d4-a716-446655440000") UUID cartId, + + @Schema(description = "사용자 ID", example = "1") Long userId, + + @Schema(description = "장바구니 아이템 목록") List items, + + @Schema(description = "총 금액", example = "35000") Integer totalPrice, + + @Schema(description = "가게 ID (아이템이 없으면 null)", example = "550e8400-e29b-41d4-a716-446655440001") UUID storeId, + + @Schema(description = "가게 이름 (아이템이 없으면 null)", example = "맛있는 치킨집") String storeName ) { public static CartResponseDto from(Cart cart) {