오늘의 학습 키워드
쿠폰(Coupon) 기능 구현
Event 와 연관관계 매핑
발행 쿠폰(Issued Coupon) 기능 구현
발행 쿠폰 만료에 대해서
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Coupon {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private LocalDateTime expirationDate;
@Column
private int quantity;
public Coupon(CouponRequest request) {
this.expirationDate = request.getExpirationDate();
this.quantity = request.getQuantity();
}
public void decrease() {
this.quantity -= 1;
}
}
@Getter
public class CouponRequest {
@NotNull(message = "쿠폰 만료일을 등록해주세요.")
private LocalDateTime expirationDate;
@NotNull(message = "쿠폰 수량을 등록해주세요.")
private int quantity;
}
@Getter
@AllArgsConstructor
public class CouponResponse {
private Long couponId;
private LocalDateTime expirationDate;
private int quantity;
public static CouponResponse from(Coupon coupon) {
return new CouponResponse(
coupon.getId(),
coupon.getExpirationDate(),
coupon.getQuantity()
);
}
}
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/coupons")
public class CouponController {
private final CouponService couponService;
@PostMapping
@PreAuthorize("hasAnyRole('ROLE_SELLER')")
public ResponseEntity<String> createCoupon(@Valid @RequestBody CouponRequest request) {
Long couponId = couponService.createCoupon(request);
return ResponseEntity.created(URI.create("/api/v1/events/" + couponId)).build();
}
@GetMapping("/{couponId}")
@PreAuthorize("hasAnyRole('ROLE_SELLER')")
public ResponseEntity<CouponResponse> getCoupon(@PathVariable Long couponId) {
return ResponseEntity.ok().body(couponService.getCoupon(couponId));
}
@DeleteMapping("/{couponId}")
@PreAuthorize("hasAnyRole('ROLE_SELLER')")
public ResponseEntity<Void> deleteCoupon(@PathVariable Long couponId) {
couponService.deleteCoupon(couponId);
return ResponseEntity.ok().build();
}
}
@Service
@RequiredArgsConstructor
public class CouponServiceImpl implements CouponService {
private final CouponRepository couponRepository;
@Transactional
public Long createCoupon(CouponRequest request) {
Coupon coupon = new Coupon(request);
Coupon savedCoupon = couponRepository.save(coupon);
return savedCoupon.getId();
}
@Transactional(readOnly = true)
public CouponResponse getCoupon(Long couponId) {
Coupon coupon = findCoupon(couponId);
return CouponResponse.from(coupon);
}
@Transactional
public void deleteCoupon(Long couponId) {
couponRepository.delete(findCoupon(couponId));
}
private Coupon findCoupon(Long couponId) {
return couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(NOT_FOUND));
}
}
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@SQLDelete(sql = "UPDATE event SET deleted_at=CURRENT_TIMESTAMP where id=?")
@SQLRestriction("deleted_at IS NULL")
public class Event extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String content;
@Column
private LocalDate openAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "seller_id")
private User user;
@OneToOne
@JoinColumn(name = "coupon_id")
private Coupon coupon;
public Event(EventRequest request, User user) {
this.title = request.getTitle();
this.content = request.getContent();
this.openAt = request.getOpenAt();
this.user = user;
}
public void update(EventUpdateRequest request) {
this.title = request.getTitle();
this.content = request.getContent();
this.openAt = request.getOpenAt();
}
public void addCoupon(Coupon coupon) {
this.coupon = coupon;
}
}
@Getter
public class EventRequest {
@NotNull(message = "이벤트를 등록하시려면 제목을 입력하세요.")
private String title;
@NotNull(message = "이벤트를 등록하시려면 내용을 입력하세요.")
private String content;
@NotNull(message = "이벤트를 등록하시려면 오픈일자를 입력하세요.")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate openAt;
private List<EventProductRequest> eventProducts;
private Long couponId;
}
@Getter
public class EventUpdateRequest {
@NotNull(message = "이벤트를 등록하시려면 제목을 입력하세요.")
private String title;
@NotNull(message = "이벤트를 등록하시려면 내용을 입력하세요.")
private String content;
@NotNull(message = "이벤트를 등록하시려면 오픈일자를 입력하세요.")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate openAt;
}
@Getter
@AllArgsConstructor
public class EventResponse {
private Long id;
private String title;
private String content;
private LocalDate openAt;
private Long couponId;
private List<Long> eventProducts;
public static EventResponse from(Event event, List<Long> eventProducts) {
return new EventResponse(
event.getId(),
event.getTitle(),
event.getContent(),
event.getOpenAt(),
event.getCoupon().getId(),
eventProducts
);
}
}
@Transactional
public Long createEvent(EventRequest eventRequest, User user) {
Event event = new Event(eventRequest, user);
Event savedEvent = eventRepository.save(event);
eventRequest.getEventProducts().stream()
.map(EventProductRequest::getProductId)
.map(productId -> productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 상품입니다.")))
.map(product -> new EventProduct(savedEvent, product))
.forEach(eventProductRepository::save);
if(eventRequest.getCouponId() != null) {
Coupon coupon = couponRepository.findById(eventRequest.getCouponId())
.orElseThrow(() -> new NotFoundException(NOT_FOUND));
savedEvent.addCoupon(coupon);
}
return savedEvent.getId();
}
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@SQLRestriction("deleted_at IS NULL")
public class IssuedCoupon extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "coupon_id")
private Coupon coupon;
public IssuedCoupon(User user, Coupon coupon) {
this.user = user;
this.coupon = coupon;
}
}
@Transactional
public void issueCoupon(Long eventId, Long couponId, User user, LocalDateTime now) {
Event event = findEvent(eventId);
if(now.isBefore(event.getCreatedAt())) {
throw new InvalidCouponException(INVALID_COUPON);
}
Coupon coupon = findCoupon(couponId);
if(coupon.getQuantity() > 0) {
coupon.decrease();
IssuedCoupon issuedCoupon = new IssuedCoupon(user, coupon);
issuedCouponRepository.save(issuedCoupon);
} else {
throw new InvalidCouponException(INVALID_COUPON);
}
}
@Slf4j(topic = "Scheduler")
@Component
@RequiredArgsConstructor
public class Scheduler {
private final IssuedCouponQuery issuedCouponQuery;
@Scheduled(cron = "0 0 1 * * *") // 매일 새벽 1시
public void updateIssuedCoupon() throws InterruptedException {
log.info("발행 쿠폰 만료 처리 실행");
issuedCouponQuery.deleteExpiredCoupon();
}
}
@Transactional
public void deleteExpiredCoupon() {
QIssuedCoupon qIssuedCoupon = QIssuedCoupon.issuedCoupon;
LocalDateTime now = LocalDateTime.now();
long count = jpaQueryFactory
.update(qIssuedCoupon)
.set(qIssuedCoupon.deletedAt, now)
.where(qIssuedCoupon.coupon.expirationDate.before(now))
.execute();
}