Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.sparta.tdd.domain.coupon.dto.CouponRequestDto;
import com.sparta.tdd.domain.coupon.dto.CouponResponseDto;
import com.sparta.tdd.domain.coupon.service.CouponService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
Expand All @@ -28,12 +29,22 @@ public class CouponController {

private final CouponService couponService;

@Operation(
summary = "가게 쿠폰 목록 조회",
description = "해당 가게의 모든 쿠폰을 조회합니다."
)
@GetMapping("/list/{storeId}")
public ResponseEntity<List<CouponResponseDto>> getStoreCoupons(@PathVariable UUID storeId) {
return ResponseEntity.status(HttpStatus.OK)
.body(couponService.getStoreCoupons(storeId));
}

@Operation(
summary = "STORE 쿠폰 등록",
description =
"해당 가게에만 적용할 수 있는 STORE 쿠폰을 발급합니다. OWNER, MASTER 권한만 접근 가능하며, 쿠폰 발급 시 최초 쿠폰 SCOPE는 STORE입니다."
+ "TYPE 에서 FIXED(고정된 할인값), PERCENT(할인율)을 선택할 수 있으며 사용가능한 최소 금액과 만료기간을 필수로 입력해야 합니다."
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@PostMapping("/{storeId}")
public ResponseEntity<CouponResponseDto> createStoreCoupon(@PathVariable UUID storeId,
Expand All @@ -43,6 +54,12 @@ public ResponseEntity<CouponResponseDto> createStoreCoupon(@PathVariable UUID st
.body(couponService.createStoreCoupon(storeId, dto, userDetails.getUserId()));
}

@Operation(
summary = "MASTER 쿠폰 등록",
description =
"가게와 무관하게 적용할 수 있는 MASTER 쿠폰을 발급합니다. MASTER 권한만 접근 가능하며, 쿠폰 발급 시 최초 쿠폰 SCOPE는 MASTER입니다."
+ "TYPE 에서 FIXED(고정된 할인값), PERCENT(할인율)을 선택할 수 있으며 사용가능한 최소 금액과 만료기간을 필수로 입력해야 합니다."
)
@PreAuthorize("hasAnyRole('MASTER')")
@PostMapping("/master")
public ResponseEntity<CouponResponseDto> createMasterCoupon(
Expand All @@ -51,6 +68,10 @@ public ResponseEntity<CouponResponseDto> createMasterCoupon(
.body(couponService.createMasterCoupon(dto));
}

@Operation(
summary = "쿠폰 수정",
description = "쿠폰을 수정합니다. 해당 가게 점주(OWNER), MASTER 권한만 가능하며 이미 1회라도 사용자가 발급했다면 수정할 수 없습니다."
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@PatchMapping("/{storeId}/{couponId}")
public ResponseEntity<Void> updateCoupon(@PathVariable UUID storeId,
Expand All @@ -61,6 +82,12 @@ public ResponseEntity<Void> updateCoupon(@PathVariable UUID storeId,
return ResponseEntity.noContent().build();
}

@Operation(
summary = "쿠폰 삭제",
description =
"쿠폰을 삭제합니다(sort delete). 해당 가게 점주(OWNER), MASTER 권한만 가능하며 해당 쿠폰이 삭제되어도(추가 발급 불가)"
+ " 이미 발급된 유저쿠폰은 적용할 수 있습니다."
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@DeleteMapping("/{storeId}/{couponId}")
public ResponseEntity<Void> deleteCoupon(@PathVariable UUID storeId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.sparta.tdd.domain.auth.UserDetailsImpl;
import com.sparta.tdd.domain.coupon.dto.UserCouponResponseDto;
import com.sparta.tdd.domain.coupon.service.UserCouponService;
import io.swagger.v3.oas.annotations.Operation;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,14 +25,22 @@ public class UserCouponController {

private final UserCouponService userCouponService;


@Operation(
summary = "내 쿠폰 목록 조회",
description = "사용자의 쿠폰 목록을 조회합니다. CUSTOMER와 MASTER 권한만 가능하며, ACTIVE 상태 쿠폰부터 최신 발급 순 "
+ "> USED/EXPIRED 상태 쿠폰 최신 발급 순으로 조회됩니다. 만료 후 7일이 지난 쿠폰은 조회되지 않습니다."
)
@GetMapping("/my/list")
public ResponseEntity<List<UserCouponResponseDto>> getMyCoupons(
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseEntity.status(HttpStatus.OK)
.body(userCouponService.getMyCoupons(userDetails.getUserId()));
}

@Operation(
summary = "유저쿠폰 발급",
description = "쿠폰을 발급하여 유저쿠폰을 생성합니다. CUSTOMER와 MASTER 권한만 가능하며, 발급된 유저쿠폰의 최초 CouponStatus는 ACTIVE 입니다."
)
@PostMapping("/{couponId}")
public ResponseEntity<UserCouponResponseDto> createUserCoupon(@PathVariable UUID couponId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,28 @@
import com.sparta.tdd.domain.coupon.enums.Scope;
import com.sparta.tdd.domain.coupon.enums.Type;
import com.sparta.tdd.domain.store.entity.Store;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import lombok.Builder;

@Schema(description = "쿠폰 요청 DTO")
@Builder
public record CouponRequestDto(
@Schema(description = "쿠폰 이름", example = "2000원 할인 쿠폰")
@NotNull @Size(max = 20) String name,
@Schema(description = "할인 종류", example = "FIXED")
@NotNull Type type,
@Schema(description = "사용 범위", example = "STORE")
@NotNull Scope scope,
@Schema(description = "할인값", example = "2000")
@NotNull Integer discountValue,
@Schema(description = "최소 주문 금액", example = "15000")
@NotNull Integer minOrderPrice,
@Schema(description = "발행 개수", example = "100")
int quantity,
@Schema(description = "만료일자", example = "2025-09-29T12:00:00.000000000")
@NotNull LocalDateTime expiredAt
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@
import com.sparta.tdd.domain.coupon.entity.Coupon;
import com.sparta.tdd.domain.coupon.enums.Scope;
import com.sparta.tdd.domain.coupon.enums.Type;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Builder;

@Schema(description = "쿠폰 응답 DTO")
@Builder
public record CouponResponseDto(
@Schema(description = "쿠폰 ID", example = "551e8400-e29b-41d4-a716-446655440001")
UUID couponId,
@Schema(description = "쿠폰 이름", example = "2000원 할인 쿠폰")
String name,
@Schema(description = "할인 종류", example = "FIXED")
Type type,
@Schema(description = "사용 범위", example = "STORE")
Scope scope,
@Schema(description = "할인값", example = "2000")
Integer discountValue,
@Schema(description = "최소 주문 금액", example = "15000")
Integer minOrderPrice,
@Schema(description = "발행 개수", example = "100")
int quantity,
@Schema(description = "발급된 수량", example = "30")
int issuedCount,
@Schema(description = "만료일자", example = "2025-09-29T12:00:00.000000000")
LocalDateTime expiredAt
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@

import com.sparta.tdd.domain.coupon.entity.UserCoupon;
import com.sparta.tdd.domain.coupon.enums.CouponStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.Builder;

@Schema(description = "유저쿠폰 응답 DTO")
@Builder
public record UserCouponResponseDto(
@Schema(description = "유저쿠폰 ID", example = "550e8400-e29b-41d4-a716-446655440000")
UUID userCouponId,
@Schema(description = "유저 ID", example = "1")
Long userId,
@Schema(description = "쿠폰 ID", example = "551e8400-e29b-41d4-a716-446655440001")
UUID couponId,
@Schema(description = "유저 쿠폰 상태", example = "ACTIVE")
CouponStatus couponStatus
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.sparta.tdd.domain.menu.dto.MenuResponseDto;
import com.sparta.tdd.domain.menu.service.MenuService;
import com.sparta.tdd.domain.user.enums.UserAuthority;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
Expand All @@ -29,6 +31,10 @@ public class MenuController {

private final MenuService menuService;

@Operation(
summary = "메뉴 목록 조회",
description = "가게의 메뉴 목록을 조회합니다. OWERN나 MASTER가 아니면 숨겨진 메뉴를 볼 수 없습니다."
)
@GetMapping("/{storeId}/menu")
public ResponseEntity<List<MenuResponseDto>> getMenus(@PathVariable UUID storeId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
Expand All @@ -37,6 +43,10 @@ public ResponseEntity<List<MenuResponseDto>> getMenus(@PathVariable UUID storeId
.body(menuService.getMenus(storeId, authority));
}

@Operation(
summary = "메뉴 상세 조회",
description = "가게의 특정 메뉴를 조회합니다. OWNER나 MASTER가 아니면 숨겨진 메뉴를 볼 수 없습니다. "
)
@GetMapping("/{storeId}/menu/{menuId}")
public ResponseEntity<MenuResponseDto> getMenu(@PathVariable UUID storeId,
@PathVariable UUID menuId,
Expand All @@ -46,16 +56,24 @@ public ResponseEntity<MenuResponseDto> getMenu(@PathVariable UUID storeId,
.body(menuService.getMenu(storeId, menuId, authority));
}

@Operation(
summary = "메뉴 등록",
description = "가게의 메뉴를 등록합니다. OWNER나 MASTER만 가능하며, 해당 가게 점주가 아니라면 예외가 발생합니다. "
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@PostMapping("/{storeId}/menu")
public ResponseEntity<MenuResponseDto> createMenu(@PathVariable UUID storeId,
@RequestBody MenuRequestDto menuRequestDto,
@Valid @RequestBody MenuRequestDto menuRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
Long userId = userDetails.getUserId();
return ResponseEntity.status(HttpStatus.CREATED)
.body(menuService.createMenu(storeId, menuRequestDto, userId));
}

@Operation(
summary = "메뉴 수정",
description = "가게의 메뉴를 수정합니다. OWNER나 MASTER만 가능하며, 해당 가게 점주가 아니라면 예외가 발생합니다. "
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@PatchMapping("/{storeId}/menu/{menuId}")
public ResponseEntity<Void> updateMenu(@PathVariable UUID storeId, @PathVariable UUID menuId,
Expand All @@ -66,6 +84,10 @@ public ResponseEntity<Void> updateMenu(@PathVariable UUID storeId, @PathVariable
return ResponseEntity.noContent().build();
}

@Operation(
summary = "메뉴 상태 변경",
description = "가게의 메뉴 상태를 변경합니다. 메뉴를 숨기거나, 공개할 수 있습니다. OWNER나 MASTER만 가능하며, 해당 가게 점주가 아니라면 예외가 발생합니다. "
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@PatchMapping("/{storeId}/menu/{menuId}/status")
public ResponseEntity<Void> updateMenuStatus(@PathVariable UUID storeId,
Expand All @@ -77,6 +99,10 @@ public ResponseEntity<Void> updateMenuStatus(@PathVariable UUID storeId,
return ResponseEntity.noContent().build();
}

@Operation(
summary = "메뉴 삭제",
description = "가게의 메뉴를 삭제합니다. OWNER나 MASTER만 가능하며, 해당 가게 점주가 아니라면 예외가 발생합니다. 메뉴는 soft delete 됩니다. "
)
@PreAuthorize("hasAnyRole('OWNER', 'MASTER')")
@DeleteMapping("/{storeId}/menu/{menuId}")
public ResponseEntity<Void> deleteMenu(@PathVariable UUID storeId, @PathVariable UUID menuId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import com.sparta.tdd.domain.menu.entity.Menu;
import com.sparta.tdd.domain.store.entity.Store;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;

@Schema(description = "메뉴 등록 및 수정 요청 DTO")
@Builder
public record MenuRequestDto(
String name,
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
@NotNull @Size(max = 20) String name,
String description,
Integer price,
@Schema(description = "가격", example = "10000")
@NotNull Integer price,
String imageUrl
) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.sparta.tdd.domain.menu.dto;

import com.sparta.tdd.domain.menu.entity.Menu;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.UUID;
import lombok.Builder;

@Schema(description = "메뉴 응답 DTO")
@Builder
public record MenuResponseDto(
@Schema(description = "menu 아이디", example = "1")
UUID menuId,
@Schema(description = "메뉴명", example = "햄버거")
String name,
@Schema(description = "메뉴 설명", example = "바삭한 치킨과 신선한 야채가 들어간 버거")
String description,
@Schema(description = "가격", example = "10000")
Integer price,
@Schema(description = "메뉴 이미지 URL", example = "https://example.com/images/chicken-burger.jpg")
String imageUrl,
@Schema(description = "숨김 여부", example = "Boolean.False")
Boolean isHidden
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import com.sparta.tdd.domain.user.entity.User;
import com.sparta.tdd.domain.user.enums.UserAuthority;
import com.sparta.tdd.domain.user.repository.UserRepository;
import com.sparta.tdd.global.exception.BusinessException;
import com.sparta.tdd.global.exception.ErrorCode;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -40,7 +42,7 @@ public List<MenuResponseDto> getMenus(UUID storeId, UserAuthority authority) {
public MenuResponseDto getMenu(UUID storeId, UUID menuId, UserAuthority authority) {
Menu menu = findMenu(storeId, menuId);
if (authority.isCustomerOrManager() && menu.isHidden()) {
throw new IllegalArgumentException("숨겨진 메뉴입니다.");
throw new BusinessException(ErrorCode.MENU_HIDDEN);
}
return MenuResponseDto.from(menu);
}
Expand Down Expand Up @@ -89,22 +91,22 @@ public void deleteMenu(UUID storeId, UUID menuId, Long userId) {

private Menu findMenu(UUID storeId, UUID menuId) {
return menuRepository.findByIdAndStoreIdAndIsDeletedFalse(menuId, storeId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 메뉴입니다."));
.orElseThrow(() -> new BusinessException(ErrorCode.MENU_NOT_FOUND));
}

private Store findStore(UUID storeId) {
return storeRepository.findById(storeId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 가게입니다."));
.orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
}

private User findUser(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다."));
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}

private void validateUserOnMenu(User user, Store store) {
if (!store.isOwner(user)) {
throw new IllegalArgumentException("권한이 없는 사용자입니다.");
throw new BusinessException(ErrorCode.MENU_PERMISSION_DENIED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public enum ErrorCode {
ORDER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 주문입니다."),

// MENU 도메인 관련
MENU_HIDDEN(HttpStatus.FORBIDDEN, "숨겨진 메뉴입니다."),
MENU_PERMISSION_DENIED(HttpStatus.FORBIDDEN, "해당 메뉴에 대한 권한이 없습니다."),

// REVIEW 도메인 관련
REVIEW_NOT_OWNED(HttpStatus.FORBIDDEN, "본인의 리뷰만 수정/삭제할 수 있습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ void ownerDeletesMenu() throws Exception {
mockMvc.perform(
get("/v1/store/{storeId}/menu/{menuId}", storeId, menuId)
.with(csrf()))
.andExpectAll(status().isBadRequest());
.andExpectAll(status().isNotFound());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.sparta.tdd.domain.user.entity.User;
import com.sparta.tdd.domain.user.enums.UserAuthority;
import com.sparta.tdd.domain.user.repository.UserRepository;
import com.sparta.tdd.global.exception.BusinessException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -170,7 +171,7 @@ void getMenusCustomerTest() {
.thenReturn(Optional.of(menu2));

// when & then
assertThrows(IllegalArgumentException.class,
assertThrows(BusinessException.class,
() -> menuService.getMenu(store.getId(), menu2.getId(),
customer.getAuthority()));
verify(menuRepository, times(1)).findByIdAndStoreIdAndIsDeletedFalse(menu2.getId(), store.getId());
Expand Down