Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +15,7 @@

import java.util.UUID;

@Tag(name = "Cart", description = "장바구니 관리 API")
@RestController
@RequestMapping("/v1/cart")
@RequiredArgsConstructor
Expand All @@ -22,6 +25,15 @@ public class CartController {
private final CartService cartService;

// 장바구니 조회
@Operation(
summary = "장바구니 조회",
description = """
사용자의 장바구니를 조회합니다.\n
장바구니가 없으면 자동으로 생성됩니다.\n
장바구니에 담긴 모든 아이템과 총 금액을 반환합니다.\n
CUSTOMER, MANAGER, MASTER 권한이 필요합니다.
"""
)
@GetMapping
public ResponseEntity<CartResponseDto> getCart(
@AuthenticationPrincipal UserDetailsImpl userDetails
Expand All @@ -31,6 +43,15 @@ public ResponseEntity<CartResponseDto> getCart(
}

// 장바구니에 아이템 추가
@Operation(
summary = "장바구니에 아이템 추가",
description = """
장바구니에 메뉴를 추가합니다.\n
같은 메뉴가 이미 있으면 수량이 증가합니다.\n
다른 가게의 메뉴를 추가하려면 기존 장바구니를 비워야 합니다.\n
추가된 장바구니 정보를 반환합니다.
"""
)
@PostMapping("/items")
public ResponseEntity<CartResponseDto> addItemToCart(
@AuthenticationPrincipal UserDetailsImpl userDetails,
Expand All @@ -41,6 +62,15 @@ public ResponseEntity<CartResponseDto> addItemToCart(
}

// 장바구니 아이템 수량 수정
@Operation(
summary = "장바구니 아이템 수량 수정",
description = """
장바구니에 담긴 아이템의 수량을 변경합니다.\n
수량은 1개 이상이어야 합니다.\n
본인의 장바구니 아이템만 수정할 수 있습니다.\n
수정된 장바구니 정보를 반환합니다.
"""
)
@PatchMapping("/items/{cartItemId}")
public ResponseEntity<CartResponseDto> updateCartItemQuantity(
@AuthenticationPrincipal UserDetailsImpl userDetails,
Expand All @@ -54,6 +84,15 @@ public ResponseEntity<CartResponseDto> updateCartItemQuantity(
}

// 장바구니 아이템 삭제
@Operation(
summary = "장바구니 아이템 삭제",
description = """
장바구니에서 특정 아이템을 삭제합니다.\n
본인의 장바구니 아이템만 삭제할 수 있습니다.\n
마지막 아이템을 삭제하면 가게 정보도 초기화됩니다.\n
삭제 후 장바구니 정보를 반환합니다.
"""
)
@DeleteMapping("/items/{cartItemId}")
public ResponseEntity<CartResponseDto> removeCartItem(
@AuthenticationPrincipal UserDetailsImpl userDetails,
Expand All @@ -66,6 +105,14 @@ public ResponseEntity<CartResponseDto> removeCartItem(
}

// 장바구니 전체 비우기
@Operation(
summary = "장바구니 전체 비우기",
description = """
장바구니의 모든 아이템을 삭제합니다.\n
가게 정보도 함께 초기화됩니다.\n
삭제 성공 시 204 No Content 상태로 응답합니다.
"""
)
@DeleteMapping
public ResponseEntity<Void> clearCart(
@AuthenticationPrincipal UserDetailsImpl userDetails
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {}
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CartItemResponseDto> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +22,7 @@
import java.net.URI;
import java.util.UUID;

@Tag(name = "리뷰 API")
@RestController
@RequestMapping("/v1/reviews")
@RequiredArgsConstructor
Expand All @@ -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<ReviewResponseDto> createReview(
@PathVariable UUID orderId,
Expand All @@ -43,6 +56,15 @@ public ResponseEntity<ReviewResponseDto> createReview(
}

// 리뷰 수정
@Operation(
summary = "리뷰 수정",
description = """
작성한 리뷰의 내용(content), 평점(rating), 사진(photos)을 수정합니다.\n
본인이 작성한 리뷰만 수정할 수 있으며, 수정 시 가게의 평균 평점이 재계산됩니다.\n
수정된 리뷰 정보를 200 OK 상태로 응답합니다.\n
삭제되지 않은 리뷰만 수정 가능합니다.
"""
)
@PatchMapping("/{reviewId}")
public ResponseEntity<ReviewResponseDto> updateReview(
@PathVariable UUID reviewId,
Expand All @@ -58,13 +80,31 @@ public ResponseEntity<ReviewResponseDto> updateReview(
}

// 리뷰 개별 조회
@Operation(
summary = "리뷰 개별 조회",
description = """
특정 리뷰의 상세 정보를 조회합니다.\n
리뷰 정보와 함께 답글이 있는 경우 답글 정보도 함께 반환됩니다.\n
삭제되지 않은 리뷰만 조회 가능합니다.\n
인증 없이 모든 사용자가 조회할 수 있습니다.
"""
)
@GetMapping("/{reviewId}")
public ResponseEntity<ReviewResponseDto> getReview(@PathVariable UUID reviewId) {
ReviewResponseDto response = reviewService.getReview(reviewId);
return ResponseEntity.ok(response);
}

// 리뷰 목록 조회 (가게별)
@Operation(
summary = "가게별 리뷰 목록 조회",
description = """
특정 가게의 모든 리뷰를 페이징하여 조회합니다.\n
각 리뷰에 답글이 있는 경우 답글 정보도 함께 반환됩니다.\n
삭제되지 않은 리뷰만 조회되며, 페이지네이션을 통해 대량의 리뷰를 효율적으로 조회할 수 있습니다.\n
인증 없이 모든 사용자가 조회할 수 있습니다.
"""
)
@GetMapping("/store/{storeId}")
public ResponseEntity<Page<ReviewResponseDto>> getReviewsByStore(
@PathVariable UUID storeId,
Expand All @@ -75,6 +115,16 @@ public ResponseEntity<Page<ReviewResponseDto>> getReviewsByStore(
}

// 리뷰 삭제
@Operation(
summary = "리뷰 삭제",
description = """
작성한 리뷰를 논리적으로 삭제합니다.\n
본인이 작성한 리뷰만 삭제할 수 있으며, deletedAt과 deletedBy 필드가 설정됩니다.\n
리뷰 삭제 시 가게의 평균 평점이 자동으로 재계산됩니다.\n
삭제 성공 시 204 No Content 상태로 응답합니다.\n
물리적 삭제가 아닌 논리적 삭제(Soft Delete)로 데이터는 DB에 유지됩니다.
"""
)
@DeleteMapping("/{reviewId}")
public ResponseEntity<Void> deleteReview(
@PathVariable UUID reviewId,
Expand All @@ -85,7 +135,16 @@ public ResponseEntity<Void> deleteReview(
}

// ========== 답글 관련 엔드포인트 ==========

@Operation(
summary = "리뷰 답글 작성",
description = """
가게 사장님이 리뷰에 대한 답글을 작성합니다.\n
OWNER 권한이 필요하며, 본인 가게의 리뷰에만 답글을 작성할 수 있습니다.\n
하나의 리뷰에는 하나의 답글만 작성 가능합니다.\n
이미 답글이 존재하는 경우 에러가 발생합니다.\n
생성된 답글의 위치(Location 헤더)와 함께 201 Created 상태로 응답합니다.
"""
)
@PostMapping("/{reviewId}/reply")
public ResponseEntity<ReviewReplyResponseDto> createReply(
@PathVariable UUID reviewId,
Expand All @@ -102,7 +161,15 @@ public ResponseEntity<ReviewReplyResponseDto> 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<ReviewReplyResponseDto> updateReply(
@PathVariable UUID reviewId,
Expand All @@ -118,6 +185,15 @@ public ResponseEntity<ReviewReplyResponseDto> 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<Void> deleteReply(
@PathVariable UUID reviewId,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading