From c038d7c003ca2d1bb6165debd93dcc412d641f96 Mon Sep 17 00:00:00 2001 From: peridot Date: Fri, 28 Mar 2025 17:53:28 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix(coupon)=20rlock=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/eightyage/domain/event/service/EventService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java index f7d38a6..91c9b5f 100644 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ b/src/main/java/com/example/eightyage/domain/event/service/EventService.java @@ -12,7 +12,6 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Service; import java.time.LocalDateTime; From 80f2c340d3fa7724e48ab520c6ee4e25573c0d4f Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:26:06 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor(coupon)=20event=20=E2=86=92=20coup?= =?UTF-8?q?on,=20coupon=20=E2=86=92=20issuedCoupon=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 32 ++++-- .../controller/IssuedCouponController.java | 36 ++++++ .../coupon/couponstate/CouponState.java | 6 + .../domain/coupon/dto/request/.gitkeep | 0 .../dto/request/CouponRequestDto.java} | 4 +- .../dto/response/CouponResponseDto.java | 26 ++--- .../dto/response/IssuedCouponResponseDto.java | 29 +++++ .../domain/coupon/entity/Coupon.java | 70 +++++++----- .../domain/coupon/entity/CouponState.java | 6 - .../domain/coupon/entity/IssuedCoupon.java | 56 +++++++++ .../coupon/repository/CouponRepository.java | 7 -- .../repository/IssuedCouponRepository.java | 14 +++ .../domain/coupon/service/CouponService.java | 107 +++++++++--------- .../coupon/service/IssuedCouponService.java | 102 +++++++++++++++++ .../domain/coupon/status/Status.java | 6 + .../event/controller/EventController.java | 41 ------- .../event/dto/response/EventResponseDto.java | 27 ----- .../eightyage/domain/event/entity/Event.java | 72 ------------ .../domain/event/entity/EventState.java | 6 - .../event/repository/EventRepository.java | 9 -- .../domain/event/service/EventService.java | 97 ---------------- 21 files changed, 376 insertions(+), 377 deletions(-) create mode 100644 src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep rename src/main/java/com/example/eightyage/domain/{event/dto/request/EventRequestDto.java => coupon/dto/request/CouponRequestDto.java} (90%) create mode 100644 src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java create mode 100644 src/main/java/com/example/eightyage/domain/coupon/status/Status.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/controller/EventController.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/entity/Event.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/entity/EventState.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java delete mode 100644 src/main/java/com/example/eightyage/domain/event/service/EventService.java diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index cdc3b69..ea9a8c2 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -1,11 +1,15 @@ package com.example.eightyage.domain.coupon.controller; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.service.CouponService; import com.example.eightyage.global.dto.AuthUser; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -16,21 +20,25 @@ public class CouponController { private final CouponService couponService; - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(couponService.issueCoupon(authUser, eventId)); + @PreAuthorize("hasRole('ADMIN')") + @PostMapping("/v1/events") + public ResponseEntity createCoupon(@Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.saveCoupon(couponRequestDto)); } - @GetMapping("/v1/coupons/my") - public ResponseEntity> getMyCoupons( - @AuthenticationPrincipal AuthUser authUser, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(couponService.getMyCoupons(authUser, page, size)); + @GetMapping("/v1/events") + public ResponseEntity> getCoupons(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(couponService.getCoupons(page, size)); } - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser,@PathVariable Long couponId) { - return ResponseEntity.ok(couponService.getCoupon(authUser, couponId)); + @GetMapping("/v1/events/{eventId}") + public ResponseEntity getCoupon(@PathVariable long eventId) { + return ResponseEntity.ok(couponService.getCoupon(eventId)); + } + + @PreAuthorize("hasRole('ADMIN')") + @PatchMapping("/v1/events/{eventId}") + public ResponseEntity updateCoupon(@PathVariable long eventId, @Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.updateCoupon(eventId, couponRequestDto)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java new file mode 100644 index 0000000..08fa202 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java @@ -0,0 +1,36 @@ +package com.example.eightyage.domain.coupon.controller; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.service.IssuedCouponService; +import com.example.eightyage.global.dto.AuthUser; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class IssuedCouponController { + + private final IssuedCouponService issuedCouponService; + + @PostMapping("/v1/events/{eventId}/coupons") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { + return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, eventId)); + } + + @GetMapping("/v1/coupons/my") + public ResponseEntity> getMyCoupons( + @AuthenticationPrincipal AuthUser authUser, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size) { + return ResponseEntity.ok(issuedCouponService.getMyCoupons(authUser, page, size)); + } + + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { + return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, couponId)); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java new file mode 100644 index 0000000..1106d56 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/couponstate/CouponState.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.coupon.couponstate; + +public enum CouponState { + VALID, + INVALID +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep b/src/main/java/com/example/eightyage/domain/coupon/dto/request/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java similarity index 90% rename from src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java rename to src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java index 381bd19..6615147 100644 --- a/src/main/java/com/example/eightyage/domain/event/dto/request/EventRequestDto.java +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/request/CouponRequestDto.java @@ -1,4 +1,4 @@ -package com.example.eightyage.domain.event.dto.request; +package com.example.eightyage.domain.coupon.dto.request; import com.example.eightyage.global.dto.ValidationMessage; import jakarta.validation.constraints.Min; @@ -13,7 +13,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor -public class EventRequestDto { +public class CouponRequestDto { @NotBlank(message = ValidationMessage.NOT_BLANK_EVENT_NAME) private String name; diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java index d1eca80..3bb6bd8 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/response/CouponResponseDto.java @@ -1,6 +1,6 @@ package com.example.eightyage.domain.coupon.dto.response; -import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.couponstate.CouponState; import lombok.Getter; import java.time.LocalDateTime; @@ -8,22 +8,20 @@ @Getter public class CouponResponseDto { - private final String couponCode; + private final String name; + private final String description; + private final int quantity; + private final LocalDateTime startDate; + private final LocalDateTime endDate; private final CouponState state; - private final String username; - private final String eventname; - private final LocalDateTime startAt; - private final LocalDateTime endAt; - public CouponResponseDto(String couponCode, CouponState state, - String username, String eventname, - LocalDateTime startAt, LocalDateTime endAt) { - this.couponCode = couponCode; + public CouponResponseDto(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate, CouponState state) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; this.state = state; - this.username = username; - this.eventname = eventname; - this.startAt = startAt; - this.endAt = endAt; } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java b/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java new file mode 100644 index 0000000..c97fb99 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/dto/response/IssuedCouponResponseDto.java @@ -0,0 +1,29 @@ +package com.example.eightyage.domain.coupon.dto.response; + +import com.example.eightyage.domain.coupon.status.Status; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class IssuedCouponResponseDto { + + private final String serialCode; + private final Status status; + private final String username; + private final String eventname; + + private final LocalDateTime startAt; + private final LocalDateTime endAt; + + public IssuedCouponResponseDto(String serialCode, Status status, + String username, String eventname, + LocalDateTime startAt, LocalDateTime endAt) { + this.serialCode = serialCode; + this.status = status; + this.username = username; + this.eventname = eventname; + this.startAt = startAt; + this.endAt = endAt; + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java index 76b1b10..8699d08 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/Coupon.java @@ -1,56 +1,66 @@ package com.example.eightyage.domain.coupon.entity; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.entity.TimeStamped; -import com.example.eightyage.global.util.RandomCodeGenerator; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Entity -@Builder @Getter @NoArgsConstructor -@AllArgsConstructor public class Coupon extends TimeStamped { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - - @Column(unique = true) - private String couponCode; - + private String name; + private String description; + private int quantity; + @Column(name="start_at") + private LocalDateTime startDate; + @Column(name = "end_at") + private LocalDateTime endDate; @Enumerated(EnumType.STRING) private CouponState state; - @ManyToOne(fetch = FetchType.LAZY) - private User user; - - @ManyToOne(fetch = FetchType.LAZY) - private Event event; - - public static Coupon create(User user, Event event) { - return Coupon.builder() - .couponCode(RandomCodeGenerator.generateCouponCode(10)) - .state(CouponState.VALID) - .user(user) - .event(event) - .build(); + public Coupon(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate) { + this.name = name; + this.description = description; + this.quantity = quantity; + this.startDate = startDate; + this.endDate = endDate; } public CouponResponseDto toDto() { return new CouponResponseDto( - this.couponCode, - this.state, - this.user.getNickname(), - this.event.getName(), - this.event.getStartDate(), - this.event.getEndDate() + this.getName(), + this.getDescription(), + this.getQuantity(), + this.getStartDate(), + this.getEndDate(), + this.getState() ); } + + public void update(CouponRequestDto couponRequestDto) { + this.name = couponRequestDto.getName(); + this.description = couponRequestDto.getDescription(); + this.quantity = couponRequestDto.getQuantity(); + this.startDate = couponRequestDto.getStartDate(); + this.endDate = couponRequestDto.getEndDate(); + } + + public boolean isValidAt(LocalDateTime time) { + return (startDate.isBefore(time) || startDate.isEqual(time)) && (endDate.isAfter(time) || endDate.isEqual(time)); + } + + public void updateStateAt(LocalDateTime time) { + CouponState newState = isValidAt(time) ? CouponState.VALID : CouponState.INVALID; + this.state = newState; + } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java b/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java deleted file mode 100644 index 221a935..0000000 --- a/src/main/java/com/example/eightyage/domain/coupon/entity/CouponState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.coupon.entity; - -public enum CouponState { - VALID, - INVALID -} diff --git a/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java b/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java new file mode 100644 index 0000000..0926367 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/entity/IssuedCoupon.java @@ -0,0 +1,56 @@ +package com.example.eightyage.domain.coupon.entity; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.status.Status; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.entity.TimeStamped; +import com.example.eightyage.global.util.RandomCodeGenerator; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class IssuedCoupon extends TimeStamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String serialCode; + + @Enumerated(EnumType.STRING) + private Status status; + + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + private Coupon coupon; + + public static IssuedCoupon create(User user, Coupon coupon) { + return IssuedCoupon.builder() + .serialCode(RandomCodeGenerator.generateCouponCode(10)) + .status(Status.VALID) + .user(user) + .coupon(coupon) + .build(); + } + + public IssuedCouponResponseDto toDto() { + return new IssuedCouponResponseDto( + this.serialCode, + this.status, + this.user.getNickname(), + this.coupon.getName(), + this.coupon.getStartDate(), + this.coupon.getEndDate() + ); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java index a6c5dbd..d617d00 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/CouponRepository.java @@ -1,16 +1,9 @@ package com.example.eightyage.domain.coupon.repository; import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface CouponRepository extends JpaRepository { - boolean existsByUserIdAndEventId(Long userId, Long eventId); - Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java new file mode 100644 index 0000000..210b7cd --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -0,0 +1,14 @@ +package com.example.eightyage.domain.coupon.repository; + +import com.example.eightyage.domain.coupon.entity.IssuedCoupon; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IssuedCouponRepository extends JpaRepository { + boolean existsByUserIdAndEventId(Long userId, Long eventId); + Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 7f45cde..9932875 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -1,19 +1,13 @@ package com.example.eightyage.domain.coupon.service; +import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; import com.example.eightyage.domain.coupon.entity.Coupon; -import com.example.eightyage.domain.coupon.entity.CouponState; +import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.CouponRepository; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.service.EventService; -import com.example.eightyage.domain.user.entity.User; -import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.ErrorMessage; -import com.example.eightyage.global.exception.ForbiddenException; -import com.example.eightyage.global.exception.NotFoundException; import lombok.RequiredArgsConstructor; -import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -21,83 +15,88 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; -import java.util.concurrent.TimeUnit; +import java.time.LocalDateTime; @Service @RequiredArgsConstructor public class CouponService { private final CouponRepository couponRepository; - private final EventService eventService; private final StringRedisTemplate stringRedisTemplate; + private final RedissonClient redissonClient; private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; private static final String EVENT_LOCK_PREFIX = "event:lock:"; - private final RedissonClient redissonClient; - public CouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { + Coupon coupon = new Coupon( + couponRequestDto.getName(), + couponRequestDto.getDescription(), + couponRequestDto.getQuantity(), + couponRequestDto.getStartDate(), + couponRequestDto.getEndDate() + ); - RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); - boolean isLocked = false; + checkEventState(coupon); - try { - isLocked = rLock.tryLock(3, 10, TimeUnit.SECONDS); // 3초 안에 락을 획득, 10초 뒤에는 자동 해제 + Coupon savedCoupon = couponRepository.save(coupon); - if (!isLocked) { - throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 - } + stringRedisTemplate.opsForValue().set("event:quantity:" + savedCoupon.getId(), String.valueOf(savedCoupon.getQuantity())); - Event event = eventService.getValidEventOrThrow(eventId); + return savedCoupon.toDto(); + } - if (couponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); - } + public Page getCoupons(int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page events = couponRepository.findAll(pageable); - Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); - if (remain == 0 || remain < 0) { - throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); - } - stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + // 모든 events들 checkState로 state 상태 갱신하기 + events.forEach(this::checkEventState); - // 쿠폰 발급 및 저장 - Coupon coupon = Coupon.create(User.fromAuthUser(authUser),event); - couponRepository.save(coupon); + return events.map(Coupon::toDto); + } - return coupon.toDto(); + public CouponResponseDto getCoupon(long eventId) { + Coupon coupon = findByIdOrElseThrow(eventId); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new BadRequestException(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage()); - } finally { - if (isLocked) { - rLock.unlock(); - } - } + checkEventState(coupon); + + return coupon.toDto(); } - public Page getMyCoupons(AuthUser authUser, int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page coupons = couponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + public CouponResponseDto updateCoupon(long eventId, CouponRequestDto couponRequestDto) { + Coupon coupon = findByIdOrElseThrow(eventId); + + coupon.update(couponRequestDto); + + checkEventState(coupon); - return coupons.map(Coupon::toDto); + return coupon.toDto(); } - public CouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - Coupon coupon = findByIdOrElseThrow(couponId); + private void checkEventState(Coupon coupon) { + CouponState prevState = coupon.getState(); + coupon.updateStateAt(LocalDateTime.now()); - if(!coupon.getUser().equals(User.fromAuthUser(authUser))) { - throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); + if(coupon.getState() != prevState) { + couponRepository.save(coupon); } + } + + public Coupon getValidCouponOrThrow(Long eventId) { + Coupon coupon = findByIdOrElseThrow(eventId); - if(!coupon.getState().equals(CouponState.VALID)) { - throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); + coupon.updateStateAt(LocalDateTime.now()); + + if(coupon.getState() != CouponState.VALID) { + throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); } - return coupon.toDto(); + return coupon; } - public Coupon findByIdOrElseThrow(Long couponId) { - return couponRepository.findById(couponId) - .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + public Coupon findByIdOrElseThrow(Long eventId) { + return couponRepository.findById(eventId) + .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java new file mode 100644 index 0000000..53ef385 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -0,0 +1,102 @@ +package com.example.eightyage.domain.coupon.service; + +import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; +import com.example.eightyage.domain.coupon.entity.IssuedCoupon; +import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; +import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.user.entity.User; +import com.example.eightyage.global.dto.AuthUser; +import com.example.eightyage.global.exception.BadRequestException; +import com.example.eightyage.global.exception.ErrorMessage; +import com.example.eightyage.global.exception.ForbiddenException; +import com.example.eightyage.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class IssuedCouponService { + + private final IssuedCouponRepository issuedCouponRepository; + private final CouponService couponService; + private final StringRedisTemplate stringRedisTemplate; + + private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; + private static final String EVENT_LOCK_PREFIX = "event:lock:"; + private final RedissonClient redissonClient; + + public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + + RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); + boolean isLocked = false; + + try { + isLocked = rLock.tryLock(3, 10, TimeUnit.SECONDS); // 3초 안에 락을 획득, 10초 뒤에는 자동 해제 + + if (!isLocked) { + throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 + } + + Coupon coupon = couponService.getValidCouponOrThrow(eventId); + + if (issuedCouponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); + } + + Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); + if (remain == 0 || remain < 0) { + throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); + } + stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + + // 쿠폰 발급 및 저장 + IssuedCoupon issuedCoupon = IssuedCoupon.create(User.fromAuthUser(authUser), coupon); + issuedCouponRepository.save(issuedCoupon); + + return issuedCoupon.toDto(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BadRequestException(ErrorMessage.INTERNAL_SERVER_ERROR.getMessage()); + } finally { + if (isLocked) { + rLock.unlock(); + } + } + } + + public Page getMyCoupons(AuthUser authUser, int page, int size) { + Pageable pageable = PageRequest.of(page-1, size); + Page coupons = issuedCouponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + + return coupons.map(IssuedCoupon::toDto); + } + + public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long couponId) { + IssuedCoupon issuedCoupon = findByIdOrElseThrow(couponId); + + if(!issuedCoupon.getUser().equals(User.fromAuthUser(authUser))) { + throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); + } + + if(!issuedCoupon.getStatus().equals(CouponState.VALID)) { + throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); + } + + return issuedCoupon.toDto(); + } + + public IssuedCoupon findByIdOrElseThrow(Long couponId) { + return issuedCouponRepository.findById(couponId) + .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); + } +} diff --git a/src/main/java/com/example/eightyage/domain/coupon/status/Status.java b/src/main/java/com/example/eightyage/domain/coupon/status/Status.java new file mode 100644 index 0000000..cfd21f3 --- /dev/null +++ b/src/main/java/com/example/eightyage/domain/coupon/status/Status.java @@ -0,0 +1,6 @@ +package com.example.eightyage.domain.coupon.status; + +public enum Status { + VALID, + INVALID, +} diff --git a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java b/src/main/java/com/example/eightyage/domain/event/controller/EventController.java deleted file mode 100644 index 17c8a81..0000000 --- a/src/main/java/com/example/eightyage/domain/event/controller/EventController.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.eightyage.domain.event.controller; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.service.EventService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class EventController { - - private final EventService eventService; - - @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/v1/events") - public ResponseEntity createEvent(@Valid @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.saveEvent(eventRequestDto)); - } - - @GetMapping("/v1/events") - public ResponseEntity> getEvents(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { - return ResponseEntity.ok(eventService.getEvents(page, size)); - } - - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getEvent(@PathVariable long eventId) { - return ResponseEntity.ok(eventService.getEvent(eventId)); - } - - @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateEvent(@PathVariable long eventId, @Valid @RequestBody EventRequestDto eventRequestDto) { - return ResponseEntity.ok(eventService.updateEvent(eventId, eventRequestDto)); - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java b/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java deleted file mode 100644 index 98edeb2..0000000 --- a/src/main/java/com/example/eightyage/domain/event/dto/response/EventResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.eightyage.domain.event.dto.response; - -import com.example.eightyage.domain.event.entity.EventState; -import lombok.Getter; - -import java.time.LocalDateTime; - -@Getter -public class EventResponseDto { - - private final String name; - private final String description; - private final int quantity; - private final LocalDateTime startDate; - private final LocalDateTime endDate; - private final EventState state; - - - public EventResponseDto(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate, EventState state) { - this.name = name; - this.description = description; - this.quantity = quantity; - this.startDate = startDate; - this.endDate = endDate; - this.state = state; - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/Event.java b/src/main/java/com/example/eightyage/domain/event/entity/Event.java deleted file mode 100644 index 7b2b560..0000000 --- a/src/main/java/com/example/eightyage/domain/event/entity/Event.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.eightyage.domain.event.entity; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.global.entity.TimeStamped; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Entity -@Getter -@NoArgsConstructor -public class Event extends TimeStamped { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - private String description; - private int quantity; - - @Column(name="start_at") - private LocalDateTime startDate; - @Column(name = "end_at") - private LocalDateTime endDate; - - @Enumerated(EnumType.STRING) - private EventState state; - - public Event(String name, String description, int quantity, LocalDateTime startDate, LocalDateTime endDate) { - this.name = name; - this.description = description; - this.quantity = quantity; - this.startDate = startDate; - this.endDate = endDate; - } - - public void setState(EventState state) { - this.state = state; - } - - public EventResponseDto toDto() { - return new EventResponseDto( - this.getName(), - this.getDescription(), - this.getQuantity(), - this.getStartDate(), - this.getEndDate(), - this.getState() - ); - } - - public void update(EventRequestDto eventRequestDto) { - this.name = eventRequestDto.getName(); - this.description = eventRequestDto.getDescription(); - this.quantity = eventRequestDto.getQuantity(); - this.startDate = eventRequestDto.getStartDate(); - this.endDate = eventRequestDto.getEndDate(); - } - - public boolean isValidAt(LocalDateTime time) { - return (startDate.isBefore(time) || startDate.isEqual(time)) && (endDate.isAfter(time) || endDate.isEqual(time)); - } - - public void updateStateAt(LocalDateTime time) { - EventState newState = isValidAt(time) ? EventState.VALID : EventState.INVALID; - this.state = newState; - } -} diff --git a/src/main/java/com/example/eightyage/domain/event/entity/EventState.java b/src/main/java/com/example/eightyage/domain/event/entity/EventState.java deleted file mode 100644 index 75bb82f..0000000 --- a/src/main/java/com/example/eightyage/domain/event/entity/EventState.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.eightyage.domain.event.entity; - -public enum EventState { - VALID, - INVALID -} diff --git a/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java b/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java deleted file mode 100644 index f6ee989..0000000 --- a/src/main/java/com/example/eightyage/domain/event/repository/EventRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.eightyage.domain.event.repository; - -import com.example.eightyage.domain.event.entity.Event; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface EventRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/eightyage/domain/event/service/EventService.java b/src/main/java/com/example/eightyage/domain/event/service/EventService.java deleted file mode 100644 index 91c9b5f..0000000 --- a/src/main/java/com/example/eightyage/domain/event/service/EventService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.example.eightyage.domain.event.service; - -import com.example.eightyage.domain.event.dto.request.EventRequestDto; -import com.example.eightyage.domain.event.dto.response.EventResponseDto; -import com.example.eightyage.domain.event.entity.Event; -import com.example.eightyage.domain.event.entity.EventState; -import com.example.eightyage.domain.event.repository.EventRepository; -import com.example.eightyage.global.exception.BadRequestException; -import com.example.eightyage.global.exception.ErrorMessage; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; - -@Service -@RequiredArgsConstructor -public class EventService { - - private final EventRepository eventRepository; - private final StringRedisTemplate stringRedisTemplate; - - public EventResponseDto saveEvent(EventRequestDto eventRequestDto) { - Event event = new Event( - eventRequestDto.getName(), - eventRequestDto.getDescription(), - eventRequestDto.getQuantity(), - eventRequestDto.getStartDate(), - eventRequestDto.getEndDate() - ); - - checkEventState(event); - - Event savedEvent = eventRepository.save(event); - - stringRedisTemplate.opsForValue().set("event:quantity:" + savedEvent.getId(), String.valueOf(savedEvent.getQuantity())); - - return savedEvent.toDto(); - } - - public Page getEvents(int page, int size) { - Pageable pageable = PageRequest.of(page-1, size); - Page events = eventRepository.findAll(pageable); - - // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); - - return events.map(Event::toDto); - } - - public EventResponseDto getEvent(long eventId) { - Event event = findByIdOrElseThrow(eventId); - - checkEventState(event); - - return event.toDto(); - } - - public EventResponseDto updateEvent(long eventId, EventRequestDto eventRequestDto) { - Event event = findByIdOrElseThrow(eventId); - - event.update(eventRequestDto); - - checkEventState(event); - - return event.toDto(); - } - - private void checkEventState(Event event) { - EventState prevState = event.getState(); - event.updateStateAt(LocalDateTime.now()); - - if(event.getState() != prevState) { - eventRepository.save(event); - } - } - - public Event getValidEventOrThrow(Long eventId) { - Event event = findByIdOrElseThrow(eventId); - - event.updateStateAt(LocalDateTime.now()); - - if(event.getState() != EventState.VALID) { - throw new BadRequestException(ErrorMessage.INVALID_EVENT_PERIOD.getMessage()); - } - - return event; - } - - public Event findByIdOrElseThrow(Long eventId) { - return eventRepository.findById(eventId) - .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); - } -} From d0206a2d0271892a422a3bb0393ce18c8b0a2b8b Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:39:46 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor(issuedCoupon)=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 3 -- .../repository/IssuedCouponRepository.java | 5 +-- .../domain/coupon/service/CouponService.java | 31 ++++++++----------- .../coupon/service/IssuedCouponService.java | 15 ++++----- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index ea9a8c2..f19b6ca 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -2,15 +2,12 @@ import com.example.eightyage.domain.coupon.dto.request.CouponRequestDto; import com.example.eightyage.domain.coupon.dto.response.CouponResponseDto; -import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.service.CouponService; -import com.example.eightyage.global.dto.AuthUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java index 210b7cd..0c84142 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -2,6 +2,7 @@ import com.example.eightyage.domain.coupon.entity.IssuedCoupon; import com.example.eightyage.domain.coupon.couponstate.CouponState; +import com.example.eightyage.domain.coupon.status.Status; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -9,6 +10,6 @@ @Repository public interface IssuedCouponRepository extends JpaRepository { - boolean existsByUserIdAndEventId(Long userId, Long eventId); - Page findAllByUserIdAndState(Long userId, CouponState state, Pageable pageable); + boolean existsByUserIdAndCouponId(Long userId, Long couponId); + Page findAllByUserIdAndStatus(Long userId, Status status, Pageable pageable); } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java index 9932875..b71ddd1 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/CouponService.java @@ -8,7 +8,6 @@ import com.example.eightyage.global.exception.BadRequestException; import com.example.eightyage.global.exception.ErrorMessage; import lombok.RequiredArgsConstructor; -import org.redisson.api.RedissonClient; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -23,10 +22,6 @@ public class CouponService { private final CouponRepository couponRepository; private final StringRedisTemplate stringRedisTemplate; - private final RedissonClient redissonClient; - - private static final String EVENT_QUANTITIY_PREFIX = "event:quantity:"; - private static final String EVENT_LOCK_PREFIX = "event:lock:"; public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { Coupon coupon = new Coupon( @@ -37,7 +32,7 @@ public CouponResponseDto saveCoupon(CouponRequestDto couponRequestDto) { couponRequestDto.getEndDate() ); - checkEventState(coupon); + checkCouponState(coupon); Coupon savedCoupon = couponRepository.save(coupon); @@ -51,30 +46,30 @@ public Page getCoupons(int page, int size) { Page events = couponRepository.findAll(pageable); // 모든 events들 checkState로 state 상태 갱신하기 - events.forEach(this::checkEventState); + events.forEach(this::checkCouponState); return events.map(Coupon::toDto); } - public CouponResponseDto getCoupon(long eventId) { - Coupon coupon = findByIdOrElseThrow(eventId); + public CouponResponseDto getCoupon(long couponId) { + Coupon coupon = findByIdOrElseThrow(couponId); - checkEventState(coupon); + checkCouponState(coupon); return coupon.toDto(); } - public CouponResponseDto updateCoupon(long eventId, CouponRequestDto couponRequestDto) { - Coupon coupon = findByIdOrElseThrow(eventId); + public CouponResponseDto updateCoupon(long couponId, CouponRequestDto couponRequestDto) { + Coupon coupon = findByIdOrElseThrow(couponId); coupon.update(couponRequestDto); - checkEventState(coupon); + checkCouponState(coupon); return coupon.toDto(); } - private void checkEventState(Coupon coupon) { + private void checkCouponState(Coupon coupon) { CouponState prevState = coupon.getState(); coupon.updateStateAt(LocalDateTime.now()); @@ -83,8 +78,8 @@ private void checkEventState(Coupon coupon) { } } - public Coupon getValidCouponOrThrow(Long eventId) { - Coupon coupon = findByIdOrElseThrow(eventId); + public Coupon getValidCouponOrThrow(Long couponId) { + Coupon coupon = findByIdOrElseThrow(couponId); coupon.updateStateAt(LocalDateTime.now()); @@ -95,8 +90,8 @@ public Coupon getValidCouponOrThrow(Long eventId) { return coupon; } - public Coupon findByIdOrElseThrow(Long eventId) { - return couponRepository.findById(eventId) + public Coupon findByIdOrElseThrow(Long couponId) { + return couponRepository.findById(couponId) .orElseThrow(() -> new BadRequestException(ErrorMessage.EVENT_NOT_FOUND.getMessage())); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index 53ef385..ca3c070 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -5,6 +5,7 @@ import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; import com.example.eightyage.domain.coupon.entity.Coupon; +import com.example.eightyage.domain.coupon.status.Status; import com.example.eightyage.domain.user.entity.User; import com.example.eightyage.global.dto.AuthUser; import com.example.eightyage.global.exception.BadRequestException; @@ -48,7 +49,7 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { Coupon coupon = couponService.getValidCouponOrThrow(eventId); - if (issuedCouponRepository.existsByUserIdAndEventId(authUser.getUserId(), eventId)) { + if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), eventId)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } @@ -76,27 +77,27 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { public Page getMyCoupons(AuthUser authUser, int page, int size) { Pageable pageable = PageRequest.of(page-1, size); - Page coupons = issuedCouponRepository.findAllByUserIdAndState(authUser.getUserId(), CouponState.VALID, pageable); + Page coupons = issuedCouponRepository.findAllByUserIdAndStatus(authUser.getUserId(), Status.VALID, pageable); return coupons.map(IssuedCoupon::toDto); } - public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long couponId) { - IssuedCoupon issuedCoupon = findByIdOrElseThrow(couponId); + public IssuedCouponResponseDto getCoupon(AuthUser authUser, Long issuedCouponId) { + IssuedCoupon issuedCoupon = findByIdOrElseThrow(issuedCouponId); if(!issuedCoupon.getUser().equals(User.fromAuthUser(authUser))) { throw new ForbiddenException(ErrorMessage.COUPON_FORBIDDEN.getMessage()); } - if(!issuedCoupon.getStatus().equals(CouponState.VALID)) { + if(issuedCoupon.getStatus().equals(Status.INVALID)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_USED.getMessage()); } return issuedCoupon.toDto(); } - public IssuedCoupon findByIdOrElseThrow(Long couponId) { - return issuedCouponRepository.findById(couponId) + public IssuedCoupon findByIdOrElseThrow(Long issuedCouponId) { + return issuedCouponRepository.findById(issuedCouponId) .orElseThrow(() -> new NotFoundException(ErrorMessage.COUPON_NOT_FOUND.getMessage())); } } From afab07c7742c1bc200c55e01454f40d51febf042 Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:40:53 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor(issuedCoupon)=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/repository/IssuedCouponRepository.java | 1 - .../eightyage/domain/coupon/service/IssuedCouponService.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java index 0c84142..e7b97da 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java +++ b/src/main/java/com/example/eightyage/domain/coupon/repository/IssuedCouponRepository.java @@ -1,7 +1,6 @@ package com.example.eightyage.domain.coupon.repository; import com.example.eightyage.domain.coupon.entity.IssuedCoupon; -import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.status.Status; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index ca3c070..7b6afe0 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -2,7 +2,6 @@ import com.example.eightyage.domain.coupon.dto.response.IssuedCouponResponseDto; import com.example.eightyage.domain.coupon.entity.IssuedCoupon; -import com.example.eightyage.domain.coupon.couponstate.CouponState; import com.example.eightyage.domain.coupon.repository.IssuedCouponRepository; import com.example.eightyage.domain.coupon.entity.Coupon; import com.example.eightyage.domain.coupon.status.Status; From 78bc89e3b127af767a5cb77a0b6783d6a42767b7 Mon Sep 17 00:00:00 2001 From: peridot Date: Sat, 29 Mar 2025 10:54:21 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor(issuedCoupon)=20api=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/controller/CouponController.java | 16 ++++++++-------- .../controller/IssuedCouponController.java | 12 ++++++------ .../coupon/service/IssuedCouponService.java | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java index f19b6ca..5757dab 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/CouponController.java @@ -18,24 +18,24 @@ public class CouponController { private final CouponService couponService; @PreAuthorize("hasRole('ADMIN')") - @PostMapping("/v1/events") + @PostMapping("/v1/coupons") public ResponseEntity createCoupon(@Valid @RequestBody CouponRequestDto couponRequestDto) { return ResponseEntity.ok(couponService.saveCoupon(couponRequestDto)); } - @GetMapping("/v1/events") + @GetMapping("/v1/coupons") public ResponseEntity> getCoupons(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return ResponseEntity.ok(couponService.getCoupons(page, size)); } - @GetMapping("/v1/events/{eventId}") - public ResponseEntity getCoupon(@PathVariable long eventId) { - return ResponseEntity.ok(couponService.getCoupon(eventId)); + @GetMapping("/v1/coupons/{couponId}") + public ResponseEntity getCoupon(@PathVariable long couponId) { + return ResponseEntity.ok(couponService.getCoupon(couponId)); } @PreAuthorize("hasRole('ADMIN')") - @PatchMapping("/v1/events/{eventId}") - public ResponseEntity updateCoupon(@PathVariable long eventId, @Valid @RequestBody CouponRequestDto couponRequestDto) { - return ResponseEntity.ok(couponService.updateCoupon(eventId, couponRequestDto)); + @PatchMapping("/v1/coupons/{couponId}") + public ResponseEntity updateCoupon(@PathVariable long couponId, @Valid @RequestBody CouponRequestDto couponRequestDto) { + return ResponseEntity.ok(couponService.updateCoupon(couponId, couponRequestDto)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java index 08fa202..d37aeea 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java +++ b/src/main/java/com/example/eightyage/domain/coupon/controller/IssuedCouponController.java @@ -16,9 +16,9 @@ public class IssuedCouponController { private final IssuedCouponService issuedCouponService; - @PostMapping("/v1/events/{eventId}/coupons") - public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long eventId) { - return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, eventId)); + @PostMapping("/v1/coupons/{couponId}/issues") + public ResponseEntity issueCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { + return ResponseEntity.ok(issuedCouponService.issueCoupon(authUser, couponId)); } @GetMapping("/v1/coupons/my") @@ -29,8 +29,8 @@ public ResponseEntity> getMyCoupons( return ResponseEntity.ok(issuedCouponService.getMyCoupons(authUser, page, size)); } - @GetMapping("/v1/coupons/{couponId}") - public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long couponId) { - return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, couponId)); + @GetMapping("/v1/coupons/{issuedCouponId}") + public ResponseEntity getCoupon(@AuthenticationPrincipal AuthUser authUser, @PathVariable Long issuedCouponId) { + return ResponseEntity.ok(issuedCouponService.getCoupon(authUser, issuedCouponId)); } } diff --git a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java index 7b6afe0..913d046 100644 --- a/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java +++ b/src/main/java/com/example/eightyage/domain/coupon/service/IssuedCouponService.java @@ -34,9 +34,9 @@ public class IssuedCouponService { private static final String EVENT_LOCK_PREFIX = "event:lock:"; private final RedissonClient redissonClient; - public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { + public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long couponId) { - RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + eventId); + RLock rLock = redissonClient.getLock(EVENT_LOCK_PREFIX + couponId); boolean isLocked = false; try { @@ -46,17 +46,17 @@ public IssuedCouponResponseDto issueCoupon(AuthUser authUser, Long eventId) { throw new BadRequestException(ErrorMessage.CAN_NOT_ACCESS.getMessage()); // 락 획득 실패 } - Coupon coupon = couponService.getValidCouponOrThrow(eventId); + Coupon coupon = couponService.getValidCouponOrThrow(couponId); - if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), eventId)) { + if (issuedCouponRepository.existsByUserIdAndCouponId(authUser.getUserId(), couponId)) { throw new BadRequestException(ErrorMessage.COUPON_ALREADY_ISSUED.getMessage()); } - Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + eventId)); + Long remain = Long.parseLong(stringRedisTemplate.opsForValue().get(EVENT_QUANTITIY_PREFIX + couponId)); if (remain == 0 || remain < 0) { throw new BadRequestException(ErrorMessage.COUPON_OUT_OF_STOCK.getMessage()); } - stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + eventId); + stringRedisTemplate.opsForValue().decrement(EVENT_QUANTITIY_PREFIX + couponId); // 쿠폰 발급 및 저장 IssuedCoupon issuedCoupon = IssuedCoupon.create(User.fromAuthUser(authUser), coupon);