diff --git a/src/main/java/org/socialculture/platform/SocialCultureApplication.java b/src/main/java/org/socialculture/platform/SocialCultureApplication.java index e642940..be64e5f 100644 --- a/src/main/java/org/socialculture/platform/SocialCultureApplication.java +++ b/src/main/java/org/socialculture/platform/SocialCultureApplication.java @@ -2,8 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication public class SocialCultureApplication { diff --git a/src/main/java/org/socialculture/platform/coupon/controller/CouponController.java b/src/main/java/org/socialculture/platform/coupon/controller/CouponController.java new file mode 100644 index 0000000..8859d83 --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/controller/CouponController.java @@ -0,0 +1,33 @@ +package org.socialculture.platform.coupon.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.socialculture.platform.coupon.dto.response.CouponResponseDto; +import org.socialculture.platform.coupon.service.CouponService; +import org.socialculture.platform.global.apiResponse.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 쿠폰 데이터 컨트롤러 + * + * @author ycjung + */ +@RestController +@RequestMapping("/api/v1/coupons") +@RequiredArgsConstructor +@Slf4j +public class CouponController { + private final CouponService couponService; + + @GetMapping + public ResponseEntity>> getAllCouponsByMemberEmail() { + log.info("Received request to get all coupons by member email"); + + return ApiResponse.onSuccess(couponService.getAllCouponsByMemberEmail()); + } +} diff --git a/src/main/java/org/socialculture/platform/coupon/dto/response/CouponResponseDto.java b/src/main/java/org/socialculture/platform/coupon/dto/response/CouponResponseDto.java new file mode 100644 index 0000000..44bea5f --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/dto/response/CouponResponseDto.java @@ -0,0 +1,44 @@ +package org.socialculture.platform.coupon.dto.response; + +import org.socialculture.platform.coupon.entity.CouponEntity; + +import java.time.LocalDateTime; + +/** + * Coupon 에 대한 Response 정보 매핑 + * + * @author ycjung + */ +public record CouponResponseDto( + Long couponId, + String name, + int percent, + boolean isUsed, + LocalDateTime expireTime, + LocalDateTime createdAt +) { + // 정적 팩토리 메서드 of + public static CouponResponseDto of(Long couponId, String name, int percent, boolean isUsed, + LocalDateTime expireTime, LocalDateTime createdAt) { + return new CouponResponseDto( + couponId, + name, + percent, + isUsed, + expireTime, + createdAt + ); + } + + // 엔티티로부터 DTO를 생성하는 메서드 from + public static CouponResponseDto from(CouponEntity couponEntity) { + return new CouponResponseDto( + couponEntity.getCouponId(), + couponEntity.getName(), + couponEntity.getPercent(), + couponEntity.isUsed(), + couponEntity.getExpireTime(), + couponEntity.getCreatedAt() + ); + } +} diff --git a/src/main/java/org/socialculture/platform/coupon/entity/CouponEntity.java b/src/main/java/org/socialculture/platform/coupon/entity/CouponEntity.java new file mode 100644 index 0000000..4855bfb --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/entity/CouponEntity.java @@ -0,0 +1,50 @@ +package org.socialculture.platform.coupon.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.socialculture.platform.global.entity.BaseEntity; +import org.socialculture.platform.member.entity.MemberEntity; + +import java.time.LocalDateTime; + +/** + * 쿠폰 엔티티 + * + * @author ycjung + */ +@Entity +@Getter +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "coupon") +public class CouponEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "coupon_id") + private Long couponId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private MemberEntity member; + + @Column(name = "name", nullable = false, length = 30) + private String name; + + @Column(name = "percent", nullable = false) + private int percent; + + @Column(name = "is_used", nullable = false) + private boolean isUsed; + + @Column(name = "expire_time", nullable = false) + private LocalDateTime expireTime; + + public void setUsed(boolean used) { + this.isUsed = used; + } +} diff --git a/src/main/java/org/socialculture/platform/coupon/repository/CouponRepository.java b/src/main/java/org/socialculture/platform/coupon/repository/CouponRepository.java new file mode 100644 index 0000000..6b1f5c2 --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/repository/CouponRepository.java @@ -0,0 +1,13 @@ +package org.socialculture.platform.coupon.repository; + +import org.socialculture.platform.coupon.entity.CouponEntity; +import org.socialculture.platform.coupon.repository.querydsl.CouponRepositoryCustom; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * 쿠폰 레파지토리 + * + * @author ycjung + */ +public interface CouponRepository extends JpaRepository, CouponRepositoryCustom { +} diff --git a/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustom.java b/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustom.java new file mode 100644 index 0000000..93532a6 --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustom.java @@ -0,0 +1,16 @@ +package org.socialculture.platform.coupon.repository.querydsl; + +import org.socialculture.platform.coupon.dto.response.CouponResponseDto; +import org.socialculture.platform.coupon.entity.CouponEntity; + +import java.util.List; + +/** + * QueryDSL 을 사용하기 위한 repo + * + * @author ycjung + */ +public interface CouponRepositoryCustom { + + List getAllCouponsByMemberEmail(String email); +} diff --git a/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustomImpl.java b/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustomImpl.java new file mode 100644 index 0000000..32137df --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/repository/querydsl/CouponRepositoryCustomImpl.java @@ -0,0 +1,28 @@ +package org.socialculture.platform.coupon.repository.querydsl; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.socialculture.platform.coupon.entity.CouponEntity; +import org.socialculture.platform.coupon.entity.QCouponEntity; + +import java.util.List; + +/** + * QueryDSL 을 사용하기 위한 repo impl + * + * @author ycjung + */ +@RequiredArgsConstructor +public class CouponRepositoryCustomImpl implements CouponRepositoryCustom{ + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List getAllCouponsByMemberEmail(String email) { + QCouponEntity couponEntity = QCouponEntity.couponEntity; + + return jpaQueryFactory.selectFrom(couponEntity) + .where(couponEntity.member.email.eq(email)) + .fetch(); + } +} diff --git a/src/main/java/org/socialculture/platform/coupon/service/CouponService.java b/src/main/java/org/socialculture/platform/coupon/service/CouponService.java new file mode 100644 index 0000000..99ffed9 --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/service/CouponService.java @@ -0,0 +1,15 @@ +package org.socialculture.platform.coupon.service; + +import org.socialculture.platform.coupon.dto.response.CouponResponseDto; + +import java.util.List; + +/** + * 쿠폰 서비스 인터페이스 + * + * @author ycjung + */ +public interface CouponService { + + List getAllCouponsByMemberEmail(); +} diff --git a/src/main/java/org/socialculture/platform/coupon/service/CouponServiceImpl.java b/src/main/java/org/socialculture/platform/coupon/service/CouponServiceImpl.java new file mode 100644 index 0000000..9234c68 --- /dev/null +++ b/src/main/java/org/socialculture/platform/coupon/service/CouponServiceImpl.java @@ -0,0 +1,32 @@ +package org.socialculture.platform.coupon.service; + +import lombok.RequiredArgsConstructor; +import org.socialculture.platform.coupon.dto.response.CouponResponseDto; +import org.socialculture.platform.coupon.repository.CouponRepository; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 쿠폰 서비스 구현체 + * + * @author ycjung + */ +@Service +@RequiredArgsConstructor +public class CouponServiceImpl implements CouponService { + + private final CouponRepository couponRepository; + + private static String MEMBER_EMAIL = "ello@test.com"; // 임시 메일 테스트 -> 토큰 발행되면 수정 + + @Override + public List getAllCouponsByMemberEmail() { + + return couponRepository.getAllCouponsByMemberEmail(MEMBER_EMAIL) + .stream() + .map(CouponResponseDto::from) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/socialculture/platform/global/apiResponse/exception/ErrorStatus.java b/src/main/java/org/socialculture/platform/global/apiResponse/exception/ErrorStatus.java index e03eb58..1b50547 100644 --- a/src/main/java/org/socialculture/platform/global/apiResponse/exception/ErrorStatus.java +++ b/src/main/java/org/socialculture/platform/global/apiResponse/exception/ErrorStatus.java @@ -18,6 +18,11 @@ public enum ErrorStatus implements BaseErrorCode{ _TICKET_INVALID_SORT_OPTION(HttpStatus.BAD_REQUEST, "TICKET400", "잘못된 정렬 옵션입니다. 허용된 값은 'ticketId', 'price', 'expired'입니다."), _TICKET_INVALID_PAGINATION_PARAMETERS(HttpStatus.BAD_REQUEST, "TICKET400", "페이지나 크기 값이 유효하지 않습니다. 0 이상의 값을 입력해 주세요."), + // 쿠폰 + _COUPON_NOT_FOUND(HttpStatus.NOT_FOUND, "TICKET400", "해당 쿠폰을 찾을 수 없습니다."), + _COUPON_ALREADY_USED(HttpStatus.BAD_REQUEST, "COUPON400", "이미 사용된 쿠폰입니다."), + _COUPON_EXPIRED(HttpStatus.BAD_REQUEST, "COUPON400", "만료된 쿠폰입니다."), + // 유저 관련 에러 LOGIN_FAIL(HttpStatus.UNAUTHORIZED, "MEMBER4001", "로그인에 실패했습니다."), diff --git a/src/main/java/org/socialculture/platform/ticket/controller/TicketController.java b/src/main/java/org/socialculture/platform/ticket/controller/TicketController.java index 783d509..c3f8aee 100644 --- a/src/main/java/org/socialculture/platform/ticket/controller/TicketController.java +++ b/src/main/java/org/socialculture/platform/ticket/controller/TicketController.java @@ -1,13 +1,11 @@ package org.socialculture.platform.ticket.controller; -import jakarta.annotation.Nullable; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.socialculture.platform.global.apiResponse.ApiResponse; import org.socialculture.platform.global.apiResponse.exception.ErrorStatus; import org.socialculture.platform.global.apiResponse.exception.GeneralException; +import org.socialculture.platform.ticket.dto.request.TicketRequestDto; import org.socialculture.platform.ticket.dto.response.TicketResponseDto; import org.socialculture.platform.ticket.service.TicketService; import org.springframework.http.ResponseEntity; @@ -30,6 +28,7 @@ public class TicketController { /** * 나의 티켓 전체 조회 - 페이징 처리 + * * @param page * @param size * @param option @@ -54,6 +53,7 @@ public ResponseEntity>> getAllTicketsByMembe /** * 나의 티켓 상세 조회 + * * @param ticketId * @return */ @@ -66,4 +66,11 @@ public ResponseEntity> getTicketById(@PathVariabl } return ApiResponse.onSuccess(ticketService.getTicketByEmailAndTicketId(ticketId)); } + + @PostMapping + public ResponseEntity> buyTicket(@RequestBody TicketRequestDto ticketRequestDto) { + log.info("Buy ticket: {}", ticketRequestDto); + + return ApiResponse.onSuccess(ticketService.registerticket(ticketRequestDto)); + } } diff --git a/src/main/java/org/socialculture/platform/ticket/service/TicketService.java b/src/main/java/org/socialculture/platform/ticket/service/TicketService.java index 2e087b2..373122c 100644 --- a/src/main/java/org/socialculture/platform/ticket/service/TicketService.java +++ b/src/main/java/org/socialculture/platform/ticket/service/TicketService.java @@ -1,12 +1,13 @@ package org.socialculture.platform.ticket.service; +import org.socialculture.platform.ticket.dto.request.TicketRequestDto; import org.socialculture.platform.ticket.dto.response.TicketResponseDto; import java.util.List; /** * 티켓 서비스 인터페이스 - * + * * @author ycjung */ public interface TicketService { @@ -16,7 +17,7 @@ public interface TicketService { // 상세 조회 TicketResponseDto getTicketByEmailAndTicketId(Long ticketId); - // TicketResponse createTicket(Long memberId, TicketRequest ticketRequest); + TicketResponseDto registerticket(TicketRequestDto ticketRequest); // TicketResponse updateTicket(Long id, TicketRequest ticketRequest); diff --git a/src/main/java/org/socialculture/platform/ticket/service/TicketServiceImpl.java b/src/main/java/org/socialculture/platform/ticket/service/TicketServiceImpl.java index 81869bd..1eb3558 100644 --- a/src/main/java/org/socialculture/platform/ticket/service/TicketServiceImpl.java +++ b/src/main/java/org/socialculture/platform/ticket/service/TicketServiceImpl.java @@ -1,8 +1,16 @@ package org.socialculture.platform.ticket.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.socialculture.platform.coupon.entity.CouponEntity; +import org.socialculture.platform.coupon.repository.CouponRepository; import org.socialculture.platform.global.apiResponse.exception.ErrorStatus; import org.socialculture.platform.global.apiResponse.exception.GeneralException; +import org.socialculture.platform.member.entity.MemberEntity; +import org.socialculture.platform.member.repository.MemberRepository; +import org.socialculture.platform.performance.entity.PerformanceEntity; +import org.socialculture.platform.performance.repository.PerformanceRepository; +import org.socialculture.platform.ticket.dto.request.TicketRequestDto; import org.socialculture.platform.ticket.dto.response.TicketResponseDto; import org.socialculture.platform.ticket.entity.TicketEntity; import org.socialculture.platform.ticket.repository.TicketRepository; @@ -11,7 +19,9 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -23,10 +33,29 @@ @RequiredArgsConstructor public class TicketServiceImpl implements TicketService { + private final MemberRepository memberRepository; + private final PerformanceRepository performanceRepository; + private final CouponRepository couponRepository; + private final TicketRepository ticketRepository; private static String MEMBER_EMAIL = "ello@test.com"; // 임시 메일 테스트 -> 토큰 발행되면 수정 + // 내부에서 사용 - 회원 정보 Entity 가져오기(by email) + private MemberEntity findMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new GeneralException(ErrorStatus._UNAUTHORIZED)); + } + + // 내부에서 사용 - 공연 정보 Entity 가져오기(by performanceId) + private PerformanceEntity findPerformanceById(Long performanceId) { + return performanceRepository.findById(performanceId) + .orElseThrow(() -> new GeneralException(ErrorStatus.PERFORMANCE_NOT_FOUND)); + } + + /** + * 티켓 정보 전체 조회(회원 체크, 페이징, Sort-ASC|DESC) + */ @Override public List getAllTicketsByEmailWithPageAndSortOption(int page, int size, String sortOption, boolean isAscending) { // Pageable 객체 생성 (정렬은 sortOption과 isAscending에 기반하여 설정) @@ -39,6 +68,9 @@ public List getAllTicketsByEmailWithPageAndSortOption(int pag .collect(Collectors.toList()); } + /** + * 티켓 상세 조회 + */ @Override public TicketResponseDto getTicketByEmailAndTicketId(Long ticketId) { TicketEntity ticketEntity = ticketRepository.getTicketByEmailAndTicketId(MEMBER_EMAIL, ticketId) @@ -46,4 +78,91 @@ public TicketResponseDto getTicketByEmailAndTicketId(Long ticketId) { return TicketResponseDto.from(ticketEntity); } + + /** + * 티켓 발권 + */ + @Override + @Transactional + public TicketResponseDto registerticket(TicketRequestDto ticketRequest) { + // 회원 검색 + MemberEntity memberEntity = findMemberByEmail(MEMBER_EMAIL); + + // 공연 검색 + PerformanceEntity performanceEntity = findPerformanceById(ticketRequest.performanceId()); + + // 가격 계산 : 가격, 예매인원, 쿠폰 아이디 전달 -> 쿠폰이 있다면 할인율 까지 계산 + int finalPrice = calculateFinalPrice(performanceEntity.getPrice(), ticketRequest.quantity(), ticketRequest.couponId()); + + // 티켓 생성 및 저장 + TicketEntity ticketEntity = createAndSaveTicket(memberEntity, performanceEntity, ticketRequest.quantity(), finalPrice); + + return TicketResponseDto.from(ticketEntity); + } + + private int calculateFinalPrice(int performancePrice, int quantity, Long couponId) { + int discountPercent = 0; + int finalPrice = performancePrice * quantity; + + if (Objects.nonNull(couponId)) { + // 쿠폰 아이디를 통해 쿠폰을 조회하고 내부에서 쿠폰 유효성을 검증한다. + CouponEntity couponEntity = findAndValidateCoupon(couponId); + + // 할인율 적용 + discountPercent = couponEntity.getPercent(); + finalPrice = calculateDiscountedPrice(performancePrice, quantity, discountPercent); + + // 쿠폰 사용 처리 + couponEntity.setUsed(true); // 쿠폰 사용된 상태로 변경 + couponRepository.save(couponEntity); // 쿠폰 상태 수정 반영 + } + + return finalPrice; + } + + public int calculateDiscountedPrice(int performancePrice, int quantity, int discountPercent) { + // 입력 값 유효성 검증 + if (performancePrice < 0 || quantity < 0 || discountPercent < 0) { // 혹시 몰라, 서버 에러 처리 + throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR); + } + + // 총 가격 계산 (공연 가격 * 인원 수) + int totalPrice = performancePrice * quantity; + + // 할인 금액 계산 + int discountAmount = (totalPrice * discountPercent) / 100; + + // 최종 가격 계산 (총 가격 - 할인 금액) + int finalPrice = totalPrice - discountAmount; + + return finalPrice; + } + + private CouponEntity findAndValidateCoupon(Long couponId) { + CouponEntity couponEntity = couponRepository.findById(couponId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COUPON_NOT_FOUND)); + + if (couponEntity.isUsed()) { // 사용 가능한 쿠폰인 지. + throw new GeneralException(ErrorStatus._COUPON_ALREADY_USED); + } + + if (couponEntity.getExpireTime().isBefore(LocalDateTime.now())) { // 쿠폰의 만료시간이 지나지 않았는 지. + throw new GeneralException(ErrorStatus._COUPON_EXPIRED); + } + + return couponEntity; + } + + private TicketEntity createAndSaveTicket(MemberEntity memberEntity, PerformanceEntity performanceEntity, int quantity, int finalPrice) { + TicketEntity ticketEntity = TicketEntity.builder() + .member(memberEntity) + .performance(performanceEntity) + .dateTime(LocalDateTime.now()) + .quantity(quantity) + .price(finalPrice) + .deletedAt(LocalDateTime.now().plusMonths(3)) // 현재 시간으로부터 3개월 후로 설정 + .build(); + + return ticketRepository.save(ticketEntity); + } } diff --git a/src/test/java/org/socialculture/platform/coupon/CouponControllerTest.java b/src/test/java/org/socialculture/platform/coupon/CouponControllerTest.java new file mode 100644 index 0000000..347eec1 --- /dev/null +++ b/src/test/java/org/socialculture/platform/coupon/CouponControllerTest.java @@ -0,0 +1,70 @@ +package org.socialculture.platform.coupon; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.socialculture.platform.coupon.controller.CouponController; +import org.socialculture.platform.coupon.dto.response.CouponResponseDto; +import org.socialculture.platform.coupon.service.CouponService; +import org.socialculture.platform.performance.controller.PerformanceController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(CouponController.class) +@ExtendWith(SpringExtension.class) +public class CouponControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private CouponService couponService; + + @Test + @DisplayName("티켓 전체 조회 테스트") + @WithMockUser + void getAllCoupons() throws Exception { + // given + List couponList = List.of( + CouponResponseDto.of( + 1L, + "Discount10", + 10, + false, + LocalDateTime.of(2024, 12, 31, 23, 59), + LocalDateTime.of(2024, 1, 1, 10, 0) + ), + CouponResponseDto.of( + 2L, + "Discount20", + 20, + true, + LocalDateTime.of(2025, 6, 30, 23, 59), + LocalDateTime.of(2024, 6, 1, 12, 0) + ) + ); + + given(couponService.getAllCouponsByMemberEmail()) + .willReturn(couponList); + + ResultActions result = this.mockMvc.perform( + get("/api/v1/coupons") + .contentType(MediaType.APPLICATION_JSON) + ); + + result.andExpect(status().isOk()); + } +} diff --git a/src/test/java/org/socialculture/platform/ticket/controller/TicketControllerTest.java b/src/test/java/org/socialculture/platform/ticket/controller/TicketControllerTest.java index 8656d0a..e5976c6 100644 --- a/src/test/java/org/socialculture/platform/ticket/controller/TicketControllerTest.java +++ b/src/test/java/org/socialculture/platform/ticket/controller/TicketControllerTest.java @@ -1,8 +1,10 @@ package org.socialculture.platform.ticket.controller; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.socialculture.platform.ticket.dto.request.TicketRequestDto; import org.socialculture.platform.ticket.dto.response.TicketResponseDto; import org.socialculture.platform.ticket.service.TicketService; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +22,8 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -86,4 +90,48 @@ void getTicketDetail() throws Exception { result.andExpect(status().isOk()); } + + @Test + @DisplayName("티켓 컨트롤러 발권 테스트") + @WithMockUser(username = "user@example.com", roles = "LOCAL") + void buyTicket() throws Exception { + // 가짜 티켓 요청 DTO 생성 + TicketRequestDto mockTicketRequest = TicketRequestDto.of( + 1L, + 2, + 1L + ); + // 가짜 티켓 응답 DTO 생성 + TicketResponseDto mockTicketResponse = TicketResponseDto.of( + 1L, + "꿈의 교향곡", + LocalDateTime.now(), + 2, + 90000, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(1) + ); + + given(ticketService.registerticket(mockTicketRequest)) + .willReturn(mockTicketResponse); + + // 실제 요청을 보내는 부분 + ResultActions result = this.mockMvc.perform( + post("/api/v1/tickets") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(mockTicketRequest)) // content 로 JSON 요청 바디 추가 + ); + + result.andExpect(status().isOk()); + } + + // 객체를 JSON 문자열로 변환하는 헬퍼 메서드 + private String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } }