From be9028283de3778fc3bdcc4d284914c9dfd64c0f Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:48:50 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=EC=BF=A0=ED=8F=B0=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C(=EA=B4=80=EB=A6=AC=EC=9E=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminCouponController.java | 24 ++++++++++++++- .../coupon/admin/dto/CouponListItemDto.java | 22 ++++++++++++++ .../admin/dto/CouponListResponseDto.java | 9 ++++++ .../admin/mapper/AdminCouponMapper.java | 30 +++++++++++++++++++ .../admin/service/AdminCouponService.java | 23 ++++++++++++++ .../user/repository/CouponRepository.java | 16 ++++++++++ 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java index 208ebf8..448a007 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java @@ -2,25 +2,32 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; import com.threestar.trainus.domain.coupon.admin.service.AdminCouponService; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; import com.threestar.trainus.global.annotation.LoginUser; import com.threestar.trainus.global.unit.BaseResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; @Tag(name = "관리자 쿠폰 API", description = "관리자 쿠폰 생성,수정 및 삭제 관련 API") @RestController -@RequestMapping("/api/v1/coupons") +@RequestMapping("/api/v1/admin/coupons") @RequiredArgsConstructor public class AdminCouponController { @@ -35,4 +42,19 @@ public ResponseEntity> createCoupon( CouponCreateResponseDto response = adminCouponService.createCoupon(request, loginUserId); return BaseResponse.ok("쿠폰 생성이 완료되었습니다.", response, HttpStatus.OK); } + + @GetMapping + @Operation(summary = "쿠폰 목록 조회", description = "관리자가 쿠폰 목록을 조회") + public ResponseEntity> getCoupons( + @RequestParam(defaultValue = "1") @Min(value = 1, message = "페이지는 1 이상이어야 합니다.") + @Max(value = 1000, message = "페이지는 1000 이하여야 합니다.") int page, + @RequestParam(defaultValue = "5") @Min(value = 1, message = "limit는 1 이상이어야 합니다.") + @Max(value = 100, message = "limit는 100 이하여야 합니다.") int limit, + @RequestParam(required = false) CouponStatus status, + @RequestParam(required = false) CouponCategory category, + @LoginUser Long loginUserId + ) { + CouponListResponseDto response = adminCouponService.getCoupons(page, limit, status, category, loginUserId); + return BaseResponse.ok("쿠폰 목록 조회 완료.", response, HttpStatus.OK); + } } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java new file mode 100644 index 0000000..dc14b2a --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java @@ -0,0 +1,22 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.time.LocalDateTime; + +/** + * 개별의 쿠폰 정보 + */ +public record CouponListItemDto( + Long couponId, + String couponName, + LocalDateTime expirationDate, + String discountPrice, + Integer minOrderPrice, + LocalDateTime createdAt, + LocalDateTime updatedAt, + String status, + Integer quantity, + String category, + LocalDateTime couponOpenAt, + LocalDateTime couponDeadlineAt +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java new file mode 100644 index 0000000..6e38f75 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java @@ -0,0 +1,9 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.util.List; + +public record CouponListResponseDto( + Long totalCount, + List couponList +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java index 14f7b44..00ce4f3 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java @@ -1,7 +1,11 @@ package com.threestar.trainus.domain.coupon.admin.mapper; +import org.springframework.data.domain.Page; + import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListItemDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; import com.threestar.trainus.domain.coupon.user.entity.Coupon; public class AdminCouponMapper { @@ -31,4 +35,30 @@ public static CouponCreateResponseDto toCreateResponseDto(Coupon coupon) { coupon.getCreatedAt() ); } + + public static CouponListItemDto toCouponListItemDto(Coupon coupon) { + return new CouponListItemDto( + coupon.getId(), + coupon.getName(), + coupon.getExpirationDate(), + coupon.getDiscountPrice(), + coupon.getMinOrderPrice(), + coupon.getCreatedAt(), + coupon.getUpdatedAt(), + coupon.getStatus().name(), + coupon.getQuantity(), + coupon.getCategory().name(), + coupon.getOpenAt(), + coupon.getCloseAt() + ); + } + + public static CouponListResponseDto toCouponListResponseDto(Page couponPage) { + return new CouponListResponseDto( + couponPage.getTotalElements(), + couponPage.getContent().stream() + .map(AdminCouponMapper::toCouponListItemDto) + .toList() + ); + } } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java index fa8c2a9..e6e5266 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java @@ -2,14 +2,20 @@ import java.time.LocalDateTime; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; import com.threestar.trainus.domain.coupon.admin.mapper.AdminCouponMapper; import com.threestar.trainus.domain.coupon.user.entity.Coupon; import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; import com.threestar.trainus.domain.coupon.user.repository.CouponRepository; import com.threestar.trainus.domain.user.service.UserService; import com.threestar.trainus.global.exception.domain.ErrorCode; @@ -24,6 +30,7 @@ public class AdminCouponService { private final CouponRepository couponRepository; private final UserService userService; + //쿠폰 생성 @Transactional public CouponCreateResponseDto createCoupon(CouponCreateRequestDto request, Long userId) { // 관리자 권한 검증 @@ -39,6 +46,22 @@ public CouponCreateResponseDto createCoupon(CouponCreateRequestDto request, Long return AdminCouponMapper.toCreateResponseDto(savedCoupon); } + //쿠폰 조회 + @Transactional(readOnly = true) + public CouponListResponseDto getCoupons(int page, int limit, CouponStatus status, CouponCategory category, + Long userId) { + // 관리자 권한 검증 + userService.validateAdminRole(userId); + + Pageable pageable = PageRequest.of(page - 1, limit, Sort.by("createdAt").descending()); + + // 조건에 따른 쿠폰 조회 + Page couponPage = couponRepository.findCouponsWithFilters(status, category, pageable); + + //응답 DTO 변환 + return AdminCouponMapper.toCouponListResponseDto(couponPage); + } + private void validateCouponRequest(CouponCreateRequestDto request) { // 할인가격이 뭔지 검증(퍼센트인지 금액인지) validateDiscountPrice(request.discountPrice()); diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java index 0cb8dcb..7f21068 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; @@ -11,6 +13,8 @@ import com.threestar.trainus.domain.coupon.user.dto.CouponResponseDto; import com.threestar.trainus.domain.coupon.user.entity.Coupon; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; import jakarta.persistence.LockModeType; @@ -34,4 +38,16 @@ List findAvailableCouponsWithOwnership(@Param("userId") Long @Query("SELECT c from Coupon c where c.id = :couponId") Optional findByIdWithPessimisticLock(Long couponId); + //관리자 쿠폰 목록 조회 -> 필터링 + @Query(""" + SELECT c FROM Coupon c + WHERE (:status IS NULL OR c.status = :status) + AND (:category IS NULL OR c.category = :category) + ORDER BY c.createdAt DESC + """) + Page findCouponsWithFilters( + @Param("status") CouponStatus status, + @Param("category") CouponCategory category, + Pageable pageable + ); } From 7672daf116250623c127c88cb37ba2ae9e451dcd Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:56:13 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=EA=B4=80=EB=A6=AC=EC=9E=90=EA=B3=84?= =?UTF-8?q?=EC=A0=95=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/MockDataInitializer.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java b/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java index 5a0fef1..2e6095d 100644 --- a/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java +++ b/src/main/java/com/threestar/trainus/global/config/MockDataInitializer.java @@ -107,6 +107,30 @@ private List createInstructors() { instructors.add(savedInstructor); } + // 관리자 계정 추가 + User admin = User.builder() + .email("admin@test.com") + .password(passwordEncoder.encode("admin123")) + .nickname("관리자") + .role(UserRole.ADMIN) //관리자 + .build(); + + User savedAdmin = userRepository.save(admin); + + // 관리자 Profile 생성 + Profile adminProfile = ProfileMapper.toDefaultEntity(savedAdmin); + adminProfile.updateProfile( + "https://example.com/admin.jpg", + "시스템 관리자입니다." + ); + profileRepository.save(adminProfile); + + // 관리자 ProfileMetadata 생성 + ProfileMetadata adminMetadata = ProfileMetadataMapper.toDefaultEntity(savedAdmin); + profileMetadataRepository.save(adminMetadata); + + instructors.add(savedAdmin); + return instructors; } From 8138426b3288c41434d1873c639c824de6ac12af Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:09:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=EA=B2=80=EC=A6=9D=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dto/CouponCreateRequestDto.java | 2 +- .../admin/service/AdminCouponService.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponCreateRequestDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponCreateRequestDto.java index 872b051..b044361 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponCreateRequestDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponCreateRequestDto.java @@ -26,7 +26,7 @@ public record CouponCreateRequestDto( Integer minOrderPrice, @NotNull(message = "쿠폰 상태는 필수입니다") - CouponStatus status, + CouponStatus status, //TODO : 일단은 내가 상태설정하게 두고, 리팩토링때 스케줄러로 처리하도록 변경 @Min(value = 1, message = "수량은 1개 이상이어야 합니다") Integer quantity, diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java index e6e5266..d5fc94b 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java @@ -76,6 +76,9 @@ private void validateCouponRequest(CouponCreateRequestDto request) { throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); } + // 상태와 오픈 시간 논리적으로 맞는지 검증 + validateStatusConsistency(request.status(), request.couponOpenAt()); + // 선착순 쿠폰 검증 if (request.category() == CouponCategory.OPEN_RUN) { // 선착순 쿠폰은 수량이 필수 @@ -125,4 +128,19 @@ private void validateDiscountPrice(String discountPrice) { } } } + + //status랑 오픈시각이랑 검증 메서드 -> open시간이 미래인데 active일경우 + private void validateStatusConsistency(CouponStatus status, LocalDateTime openAt) { + LocalDateTime now = LocalDateTime.now(); + + // ACTIVE 상태인데 오픈 시간이 미래인 경우 + if (status == CouponStatus.ACTIVE && openAt.isAfter(now)) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + + // INACTIVE 상태인데 오픈 시간이 과거인 경우 + if (status == CouponStatus.INACTIVE && openAt.isBefore(now)) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } } From 85c2d1b3fcb53f46841be6f4ff47a0bb7aa8ee14 Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:47:37 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=EC=BF=A0=ED=8F=B0=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminCouponController.java | 27 +++ .../admin/dto/CouponDetailResponseDto.java | 23 +++ .../admin/dto/CouponUpdateRequestDto.java | 32 ++++ .../admin/dto/CouponUpdateResponseDto.java | 17 ++ .../admin/mapper/AdminCouponMapper.java | 32 ++++ .../admin/service/AdminCouponService.java | 167 ++++++++++++++++++ .../domain/coupon/user/entity/Coupon.java | 26 +++ .../user/repository/UserCouponRepository.java | 5 + 8 files changed, 329 insertions(+) create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateRequestDto.java create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java index 448a007..fff698d 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java @@ -3,6 +3,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,7 +13,10 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateRequestDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateResponseDto; import com.threestar.trainus.domain.coupon.admin.service.AdminCouponService; import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; @@ -57,4 +62,26 @@ public ResponseEntity> getCoupons( CouponListResponseDto response = adminCouponService.getCoupons(page, limit, status, category, loginUserId); return BaseResponse.ok("쿠폰 목록 조회 완료.", response, HttpStatus.OK); } + + @GetMapping("/{couponId}") + @Operation(summary = "쿠폰 상세 조회", description = "관리자가 특정 쿠폰의 상세 정보를 조회") + public ResponseEntity> getCouponDetail( + @PathVariable Long couponId, + @LoginUser Long loginUserId + ) { + CouponDetailResponseDto response = adminCouponService.getCouponDetail(couponId, loginUserId); + return BaseResponse.ok("쿠폰 상세 조회 완료", response, HttpStatus.OK); + } + + @PatchMapping("/{couponId}") + @Operation(summary = "쿠폰 수정", description = "관리자가 쿠폰 정보를 수정") + public ResponseEntity> updateCoupon( + @PathVariable Long couponId, + @Valid @RequestBody CouponUpdateRequestDto request, + @LoginUser Long loginUserId + ) { + CouponUpdateResponseDto response = adminCouponService.updateCoupon(couponId, request, loginUserId); + return BaseResponse.ok("쿠폰 수정이 완료되었습니다.", response, HttpStatus.OK); + } + } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java new file mode 100644 index 0000000..b752da4 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java @@ -0,0 +1,23 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.time.LocalDateTime; + +/** + * 쿠폰 상세 조회 응답 DTO + */ +public record CouponDetailResponseDto( + Long id, + String couponName, + LocalDateTime expirationDate, + String discountPrice, + Integer minOrderPrice, + String status, + Integer quantity, + String couponCategory, + LocalDateTime couponOpenAt, + LocalDateTime couponDeadlineAt, + LocalDateTime createdAt, + LocalDateTime updatedAt, + Integer issuedCount +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateRequestDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateRequestDto.java new file mode 100644 index 0000000..98953ea --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateRequestDto.java @@ -0,0 +1,32 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.time.LocalDateTime; + +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; + +/** + * 쿠폰 수정 요청 DTO + * 일반쿠폰(NORMAL): 수량은 프론트에서 비활성화, 백엔드에서 자동으로 null 처리 + * 선착순쿠폰(OPEN_RUN): 수량 필수 입력 + */ +public record CouponUpdateRequestDto( + + @Size(max = 45, message = "쿠폰명은 45자 이하여야 합니다") + String couponName, + + CouponStatus status, + + @Min(value = 1, message = "수량은 1개 이상이어야 합니다") + Integer quantity, + + CouponCategory category, + + LocalDateTime couponOpenAt, + + LocalDateTime couponDeadlineAt +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java new file mode 100644 index 0000000..3a377fa --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java @@ -0,0 +1,17 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.time.LocalDateTime; + +/** + * 쿠폰 수정 응답 DTO + */ +public record CouponUpdateResponseDto( + String couponName, + String status, + Integer quantity, + String category, + LocalDateTime couponOpenAt, + LocalDateTime couponDeadlineAt, + LocalDateTime updatedAt +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java index 00ce4f3..78c145d 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java @@ -4,8 +4,10 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListItemDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateResponseDto; import com.threestar.trainus.domain.coupon.user.entity.Coupon; public class AdminCouponMapper { @@ -61,4 +63,34 @@ public static CouponListResponseDto toCouponListResponseDto(Page couponP .toList() ); } + + public static CouponDetailResponseDto toCouponDetailResponseDto(Coupon coupon, Integer issuedCount) { + return new CouponDetailResponseDto( + coupon.getId(), + coupon.getName(), + coupon.getExpirationDate(), + coupon.getDiscountPrice(), + coupon.getMinOrderPrice(), + coupon.getStatus().name(), + coupon.getQuantity(), + coupon.getCategory().name(), + coupon.getOpenAt(), + coupon.getCloseAt(), + coupon.getCreatedAt(), + coupon.getUpdatedAt(), + issuedCount + ); + } + + public static CouponUpdateResponseDto toCouponUpdateResponseDto(Coupon coupon) { + return new CouponUpdateResponseDto( + coupon.getName(), + coupon.getStatus().name(), + coupon.getQuantity(), + coupon.getCategory().name(), + coupon.getOpenAt(), + coupon.getCloseAt(), + coupon.getUpdatedAt() + ); + } } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java index d5fc94b..3915840 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java @@ -11,12 +11,16 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateRequestDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateResponseDto; import com.threestar.trainus.domain.coupon.admin.mapper.AdminCouponMapper; import com.threestar.trainus.domain.coupon.user.entity.Coupon; import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; import com.threestar.trainus.domain.coupon.user.repository.CouponRepository; +import com.threestar.trainus.domain.coupon.user.repository.UserCouponRepository; import com.threestar.trainus.domain.user.service.UserService; import com.threestar.trainus.global.exception.domain.ErrorCode; import com.threestar.trainus.global.exception.handler.BusinessException; @@ -28,6 +32,7 @@ public class AdminCouponService { private final CouponRepository couponRepository; + private final UserCouponRepository userCouponRepository; private final UserService userService; //쿠폰 생성 @@ -62,6 +67,49 @@ public CouponListResponseDto getCoupons(int page, int limit, CouponStatus status return AdminCouponMapper.toCouponListResponseDto(couponPage); } + //쿠폰 상세 조횧 + @Transactional(readOnly = true) + public CouponDetailResponseDto getCouponDetail(Long couponId, Long userId) { + // 관리자 권한 검증 + userService.validateAdminRole(userId); + + // 쿠폰 조회 + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + + // 발급된 쿠폰 수 조회 + Long issuedCountLong = userCouponRepository.countByCouponId(couponId); + Integer issuedCount = issuedCountLong.intValue(); + + // 응답 DTO 변환 + return AdminCouponMapper.toCouponDetailResponseDto(coupon, issuedCount); + } + + //쿠폰 수정 + @Transactional + public CouponUpdateResponseDto updateCoupon(Long couponId, CouponUpdateRequestDto request, Long userId) { + // 관리자 권한 검증 + userService.validateAdminRole(userId); + + // 쿠폰 조회 + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + + // 발급된 쿠폰 수 조회 + Long issuedCount = userCouponRepository.countByCouponId(couponId); + + // 수정 검증 로직 + validateCouponUpdate(coupon, request, issuedCount); + + // 수정된 쿠폰 업데이트 + updateCouponFields(coupon, request); + + Coupon updatedCoupon = couponRepository.save(coupon); + + // 응답 DTO 변환 + return AdminCouponMapper.toCouponUpdateResponseDto(updatedCoupon); + } + private void validateCouponRequest(CouponCreateRequestDto request) { // 할인가격이 뭔지 검증(퍼센트인지 금액인지) validateDiscountPrice(request.discountPrice()); @@ -143,4 +191,123 @@ private void validateStatusConsistency(CouponStatus status, LocalDateTime openAt throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); } } + + private void validateCouponUpdate(Coupon coupon, CouponUpdateRequestDto request, Long issuedCount) { + LocalDateTime now = LocalDateTime.now(); + + // 선착순 쿠폰인데 수량이 없는 경우 + if (request.category() != null) { + if (request.category() == CouponCategory.OPEN_RUN && request.quantity() == null) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + + // 수량 수정 검증 -> 선착순 쿠폰의 경우 + if (request.quantity() != null) { + // 이미 발급된 수량보다 적게 설정 불가 + if (request.quantity() < issuedCount.intValue()) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + + // 카테고리 변경 검증 -> 이미 발급한 쿠폰이 있다면 변경 못하게 막기 + if (request.category() != null && issuedCount > 0) { + // 이미 발급된 쿠폰이 있으면 카테고리 변경 제한 + if (!coupon.getCategory().equals(request.category())) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + + // 선착순 → 일반 변경은 아무도 발급받지 않은 경우만 가능 + if (request.category() == CouponCategory.NORMAL && coupon.getCategory() == CouponCategory.OPEN_RUN + && issuedCount > 0) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + + // 이미 오픈된 쿠폰의 오픈시각은 수정 불가 + if (request.couponOpenAt() != null) { + if (coupon.getOpenAt().isBefore(now)) { + // 이미 오픈된 쿠폰의 오픈 시각은 수정 불가 + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + + // 새로운 오픈 시각이 마감 시각보다 늦으면 안됨 + LocalDateTime newCloseAt = + request.couponDeadlineAt() != null ? request.couponDeadlineAt() : coupon.getCloseAt(); + if (request.couponOpenAt().isAfter(newCloseAt)) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + + // 이미 마감된 쿠폰의 마감시각은 수정 불가 -> 진행중인경우 연장만 가능 + if (request.couponDeadlineAt() != null) { + if (coupon.getCloseAt().isBefore(now)) { + // 이미 마감된 쿠폰의 마감 시각은 수정 불가 + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + + if (coupon.getOpenAt().isBefore(now) && coupon.getCloseAt().isAfter(now)) { + // 진행 중인 쿠폰은 연장만 가능 -> 단축은 못함 + if (request.couponDeadlineAt().isBefore(coupon.getCloseAt())) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + + // 새로운 마감 시각이 오픈 시각보다 이르면 안됨 + LocalDateTime newOpenAt = request.couponOpenAt() != null ? request.couponOpenAt() : coupon.getOpenAt(); + if (request.couponDeadlineAt().isBefore(newOpenAt)) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + } + } + + private void updateCouponFields(Coupon coupon, CouponUpdateRequestDto request) { + if (request.couponName() != null) { + coupon.updateName(request.couponName()); + } + + if (request.status() != null) { + // INACTIVE → ACTIVE로 변경 시, 오픈시각을 현재 시간으로 업데이트 + if (request.status() == CouponStatus.ACTIVE && coupon.getStatus() == CouponStatus.INACTIVE) { + LocalDateTime now = LocalDateTime.now(); + if (coupon.getOpenAt().isAfter(now)) { + coupon.updateOpenAt(now); + } + } + + coupon.updateStatus(request.status()); + } + + // 카테고리 변경 시 수량도 함께 처리 + if (request.category() != null) { + coupon.updateCategory(request.category()); + + if (request.category() == CouponCategory.NORMAL) { + // 일반 쿠폰으로 변경 시 수량을 null로 설정 + coupon.updateQuantity(null); + } else if (request.category() == CouponCategory.OPEN_RUN) { + // 선착순 쿠폰으로 변경 시 수량 설정 + if (request.quantity() != null) { + coupon.updateQuantity(request.quantity()); + } + } + } + + // 수량만 변경하는 경우 -> 선착순 쿠폰일 경우만 + if (request.quantity() != null && request.category() == null) { + // 현재 선착순 쿠폰인 경우에만 수량 변경 허용 + if (coupon.getCategory() == CouponCategory.OPEN_RUN) { + coupon.updateQuantity(request.quantity()); + } + } + + if (request.couponOpenAt() != null) { + coupon.updateOpenAt(request.couponOpenAt()); + } + + if (request.couponDeadlineAt() != null) { + coupon.updateCloseAt(request.couponDeadlineAt()); + } + } + } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java index e649fdd..bf25390 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java @@ -71,4 +71,30 @@ public void decreaseQuantity() { } this.quantity--; } + + //쿠폰 수정 관련 메서드 추가 + public void updateName(String name) { + this.name = name; + } + + public void updateStatus(CouponStatus status) { + this.status = status; + } + + public void updateQuantity(Integer quantity) { + this.quantity = quantity; + } + + public void updateCategory(CouponCategory category) { + this.category = category; + } + + public void updateOpenAt(LocalDateTime openAt) { + this.openAt = openAt; + } + + public void updateCloseAt(LocalDateTime closeAt) { + this.closeAt = closeAt; + } + } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/UserCouponRepository.java b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/UserCouponRepository.java index bd2c336..9de5a34 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/UserCouponRepository.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/UserCouponRepository.java @@ -19,4 +19,9 @@ public interface UserCouponRepository extends JpaRepository { List findAllByUserIdAndStatusWithCoupon(@Param("userId") Long userId, @Param("status") CouponStatus status); + // 특정 쿠폰의 발급 수 조회 + Long countByCouponId(Long couponId); + + // 특정 사용자가 특정 쿠폰을 발급받은 수 조회 + Long countByUserIdAndCouponId(Long userId, Long couponId); } From f50fd8cf300a3d595bbc350a529709f9099ff677 Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:21:02 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=EC=BF=A0=ED=8F=B0=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminCouponController.java | 12 +++++++ .../admin/dto/CouponDeleteResponseDto.java | 13 ++++++++ .../admin/mapper/AdminCouponMapper.java | 9 +++++ .../admin/service/AdminCouponService.java | 33 +++++++++++++++++++ .../domain/coupon/user/entity/Coupon.java | 16 +++++++++ .../user/repository/CouponRepository.java | 2 ++ 6 files changed, 85 insertions(+) create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java index fff698d..8041c82 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -13,6 +14,7 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDeleteResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateRequestDto; @@ -84,4 +86,14 @@ public ResponseEntity> updateCoupon( return BaseResponse.ok("쿠폰 수정이 완료되었습니다.", response, HttpStatus.OK); } + @DeleteMapping("/{couponId}") + @Operation(summary = "쿠폰 삭제", description = "관리자가 쿠폰을 삭제") + public ResponseEntity> deleteCoupon( + @PathVariable Long couponId, + @LoginUser Long loginUserId + ) { + CouponDeleteResponseDto response = adminCouponService.deleteCoupon(couponId, loginUserId); + return BaseResponse.ok("쿠폰 삭제가 완료되었습니다.", response, HttpStatus.OK); + } + } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java new file mode 100644 index 0000000..be7684b --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java @@ -0,0 +1,13 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.time.LocalDateTime; + +/** + * 쿠폰 삭제 응답 DTO + */ +public record CouponDeleteResponseDto( + Long couponId, + String couponName, + LocalDateTime deletedAt +) { +} \ No newline at end of file diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java index 78c145d..ff180fb 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java @@ -4,6 +4,7 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDeleteResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListItemDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; @@ -93,4 +94,12 @@ public static CouponUpdateResponseDto toCouponUpdateResponseDto(Coupon coupon) { coupon.getUpdatedAt() ); } + + public static CouponDeleteResponseDto toCouponDeleteResponseDto(Coupon coupon) { + return new CouponDeleteResponseDto( + coupon.getId(), + coupon.getName(), + coupon.getDeletedAt() + ); + } } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java index 3915840..edb56cc 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java @@ -11,6 +11,7 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponDeleteResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateRequestDto; @@ -110,6 +111,35 @@ public CouponUpdateResponseDto updateCoupon(Long couponId, CouponUpdateRequestDt return AdminCouponMapper.toCouponUpdateResponseDto(updatedCoupon); } + //쿠폰 삭제 + @Transactional + public CouponDeleteResponseDto deleteCoupon(Long couponId, Long userId) { + // 관리자 권한 검증 + userService.validateAdminRole(userId); + + // 쿠폰 조회 + Coupon coupon = couponRepository.findById(couponId) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + + // 이미 삭제된 쿠폰인지 확인 + if (coupon.isDeleted()) { + throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA); + } + + // todo : 발급된 쿠폰 수 조회 + Long issuedCount = userCouponRepository.countByCouponId(couponId); + + // todo : 삭제 가능 여부 검증 -> 지금은 필요없는데 일단은 만들어둠! + validateCouponDeletion(coupon, issuedCount); + + // 쿠폰 삭제 처리 + coupon.markAsDeleted(); + Coupon deletedCoupon = couponRepository.save(coupon); + + // 응답 DTO 변환 + return AdminCouponMapper.toCouponDeleteResponseDto(deletedCoupon); + } + private void validateCouponRequest(CouponCreateRequestDto request) { // 할인가격이 뭔지 검증(퍼센트인지 금액인지) validateDiscountPrice(request.discountPrice()); @@ -310,4 +340,7 @@ private void updateCouponFields(Coupon coupon, CouponUpdateRequestDto request) { } } + private void validateCouponDeletion(Coupon coupon, Long issuedCount) { + //todo: 일단은 다 삭제가능하게 설정 해놨는데, 나중에 결제 붙으면여기에 여러 검증들을 추가할 예정 + } } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java index bf25390..5de4c6d 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/Coupon.java @@ -65,6 +65,8 @@ public class Coupon extends BaseDateEntity { @Column(nullable = false) private LocalDateTime closeAt; + private LocalDateTime deletedAt; + public void decreaseQuantity() { if (this.quantity <= 0) { throw new BusinessException(ErrorCode.COUPON_BE_EXHAUSTED); @@ -97,4 +99,18 @@ public void updateCloseAt(LocalDateTime closeAt) { this.closeAt = closeAt; } + //삭제관련 메서드 추가 + public void markAsDeleted() { + this.deletedAt = LocalDateTime.now(); + } + + //쿠폰이 삭제된 상태인지 확인 + public boolean isDeleted() { + return this.deletedAt != null; + } + + public LocalDateTime getDeletedAt() { + return this.deletedAt; + } + } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java index 7f21068..cd51f82 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/repository/CouponRepository.java @@ -29,6 +29,7 @@ public interface CouponRepository extends JpaRepository { FROM Coupon c LEFT JOIN UserCoupon uc ON c.id = uc.coupon.id AND uc.user.id = :userId WHERE c.closeAt > :now + AND c.deletedAt IS NULL ORDER BY c.openAt ASC """) List findAvailableCouponsWithOwnership(@Param("userId") Long userId, @@ -43,6 +44,7 @@ List findAvailableCouponsWithOwnership(@Param("userId") Long SELECT c FROM Coupon c WHERE (:status IS NULL OR c.status = :status) AND (:category IS NULL OR c.category = :category) + AND c.deletedAt IS NULL ORDER BY c.createdAt DESC """) Page findCouponsWithFilters( From 75c8d19c6a49c0c11084180446a31d1a6e26bf2b Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:38:24 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminCouponController.java | 12 +++++++++--- .../coupon/admin/dto/CouponListResponseDto.java | 2 +- .../coupon/admin/dto/CouponListWrapperDto.java | 8 ++++++++ .../coupon/admin/mapper/AdminCouponMapper.java | 7 ++++++- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListWrapperDto.java diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java index 8041c82..26928fc 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/controller/AdminCouponController.java @@ -17,13 +17,16 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponDeleteResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListWrapperDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateRequestDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateResponseDto; +import com.threestar.trainus.domain.coupon.admin.mapper.AdminCouponMapper; import com.threestar.trainus.domain.coupon.admin.service.AdminCouponService; import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; import com.threestar.trainus.global.annotation.LoginUser; import com.threestar.trainus.global.unit.BaseResponse; +import com.threestar.trainus.global.unit.PagedResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -52,7 +55,7 @@ public ResponseEntity> createCoupon( @GetMapping @Operation(summary = "쿠폰 목록 조회", description = "관리자가 쿠폰 목록을 조회") - public ResponseEntity> getCoupons( + public ResponseEntity> getCoupons( @RequestParam(defaultValue = "1") @Min(value = 1, message = "페이지는 1 이상이어야 합니다.") @Max(value = 1000, message = "페이지는 1000 이하여야 합니다.") int page, @RequestParam(defaultValue = "5") @Min(value = 1, message = "limit는 1 이상이어야 합니다.") @@ -61,8 +64,11 @@ public ResponseEntity> getCoupons( @RequestParam(required = false) CouponCategory category, @LoginUser Long loginUserId ) { - CouponListResponseDto response = adminCouponService.getCoupons(page, limit, status, category, loginUserId); - return BaseResponse.ok("쿠폰 목록 조회 완료.", response, HttpStatus.OK); + CouponListResponseDto couponsInfo = adminCouponService.getCoupons(page, limit, status, category, loginUserId); + + CouponListWrapperDto coupons = AdminCouponMapper.toCouponListWrapperDto(couponsInfo); + + return PagedResponse.ok("쿠폰 목록 조회 완료.", coupons, couponsInfo.totalCount(), HttpStatus.OK); } @GetMapping("/{couponId}") diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java index 6e38f75..94e496f 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListResponseDto.java @@ -3,7 +3,7 @@ import java.util.List; public record CouponListResponseDto( - Long totalCount, + Integer totalCount, List couponList ) { } diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListWrapperDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListWrapperDto.java new file mode 100644 index 0000000..538d2d7 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListWrapperDto.java @@ -0,0 +1,8 @@ +package com.threestar.trainus.domain.coupon.admin.dto; + +import java.util.List; + +public record CouponListWrapperDto( + List coupons +) { +} diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java index ff180fb..56854f8 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java @@ -8,6 +8,7 @@ import com.threestar.trainus.domain.coupon.admin.dto.CouponDetailResponseDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListItemDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponListResponseDto; +import com.threestar.trainus.domain.coupon.admin.dto.CouponListWrapperDto; import com.threestar.trainus.domain.coupon.admin.dto.CouponUpdateResponseDto; import com.threestar.trainus.domain.coupon.user.entity.Coupon; @@ -58,7 +59,7 @@ public static CouponListItemDto toCouponListItemDto(Coupon coupon) { public static CouponListResponseDto toCouponListResponseDto(Page couponPage) { return new CouponListResponseDto( - couponPage.getTotalElements(), + (int)couponPage.getTotalElements(), couponPage.getContent().stream() .map(AdminCouponMapper::toCouponListItemDto) .toList() @@ -102,4 +103,8 @@ public static CouponDeleteResponseDto toCouponDeleteResponseDto(Coupon coupon) { coupon.getDeletedAt() ); } + + public static CouponListWrapperDto toCouponListWrapperDto(CouponListResponseDto couponsInfo) { + return new CouponListWrapperDto(couponsInfo.couponList()); + } } From 5bf92d8cea6486b28e778112095b6bfabbffd8e3 Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:41:52 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:checkstyle=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/coupon/admin/dto/CouponDeleteResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java index be7684b..f1bd2f6 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDeleteResponseDto.java @@ -10,4 +10,4 @@ public record CouponDeleteResponseDto( String couponName, LocalDateTime deletedAt ) { -} \ No newline at end of file +} From 515f00e96e088baab3813c45422fea9c793574ca Mon Sep 17 00:00:00 2001 From: Jieun Kim <83564946+iamjieunkim@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:30:08 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:enum,=EA=B3=B5=ED=86=B5=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coupon/admin/dto/CouponDetailResponseDto.java | 7 +++++-- .../coupon/admin/dto/CouponListItemDto.java | 7 +++++-- .../coupon/admin/dto/CouponUpdateResponseDto.java | 7 +++++-- .../coupon/admin/mapper/AdminCouponMapper.java | 12 ++++++------ .../coupon/admin/service/AdminCouponService.java | 15 +++++++++------ 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java index b752da4..02cc8ba 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponDetailResponseDto.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; + /** * 쿠폰 상세 조회 응답 DTO */ @@ -11,9 +14,9 @@ public record CouponDetailResponseDto( LocalDateTime expirationDate, String discountPrice, Integer minOrderPrice, - String status, + CouponStatus status, Integer quantity, - String couponCategory, + CouponCategory couponCategory, LocalDateTime couponOpenAt, LocalDateTime couponDeadlineAt, LocalDateTime createdAt, diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java index dc14b2a..99e922a 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponListItemDto.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; + /** * 개별의 쿠폰 정보 */ @@ -13,9 +16,9 @@ public record CouponListItemDto( Integer minOrderPrice, LocalDateTime createdAt, LocalDateTime updatedAt, - String status, + CouponStatus status, Integer quantity, - String category, + CouponCategory category, LocalDateTime couponOpenAt, LocalDateTime couponDeadlineAt ) { diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java index 3a377fa..7cab7e8 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/dto/CouponUpdateResponseDto.java @@ -2,14 +2,17 @@ import java.time.LocalDateTime; +import com.threestar.trainus.domain.coupon.user.entity.CouponCategory; +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; + /** * 쿠폰 수정 응답 DTO */ public record CouponUpdateResponseDto( String couponName, - String status, + CouponStatus status, Integer quantity, - String category, + CouponCategory category, LocalDateTime couponOpenAt, LocalDateTime couponDeadlineAt, LocalDateTime updatedAt diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java index 56854f8..3d77f15 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/mapper/AdminCouponMapper.java @@ -49,9 +49,9 @@ public static CouponListItemDto toCouponListItemDto(Coupon coupon) { coupon.getMinOrderPrice(), coupon.getCreatedAt(), coupon.getUpdatedAt(), - coupon.getStatus().name(), + coupon.getStatus(), coupon.getQuantity(), - coupon.getCategory().name(), + coupon.getCategory(), coupon.getOpenAt(), coupon.getCloseAt() ); @@ -73,9 +73,9 @@ public static CouponDetailResponseDto toCouponDetailResponseDto(Coupon coupon, I coupon.getExpirationDate(), coupon.getDiscountPrice(), coupon.getMinOrderPrice(), - coupon.getStatus().name(), + coupon.getStatus(), coupon.getQuantity(), - coupon.getCategory().name(), + coupon.getCategory(), coupon.getOpenAt(), coupon.getCloseAt(), coupon.getCreatedAt(), @@ -87,9 +87,9 @@ public static CouponDetailResponseDto toCouponDetailResponseDto(Coupon coupon, I public static CouponUpdateResponseDto toCouponUpdateResponseDto(Coupon coupon) { return new CouponUpdateResponseDto( coupon.getName(), - coupon.getStatus().name(), + coupon.getStatus(), coupon.getQuantity(), - coupon.getCategory().name(), + coupon.getCategory(), coupon.getOpenAt(), coupon.getCloseAt(), coupon.getUpdatedAt() diff --git a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java index edb56cc..ed5d8d6 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/admin/service/AdminCouponService.java @@ -75,8 +75,7 @@ public CouponDetailResponseDto getCouponDetail(Long couponId, Long userId) { userService.validateAdminRole(userId); // 쿠폰 조회 - Coupon coupon = couponRepository.findById(couponId) - .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + Coupon coupon = findCouponById(couponId); // 발급된 쿠폰 수 조회 Long issuedCountLong = userCouponRepository.countByCouponId(couponId); @@ -93,8 +92,7 @@ public CouponUpdateResponseDto updateCoupon(Long couponId, CouponUpdateRequestDt userService.validateAdminRole(userId); // 쿠폰 조회 - Coupon coupon = couponRepository.findById(couponId) - .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + Coupon coupon = findCouponById(couponId); // 발급된 쿠폰 수 조회 Long issuedCount = userCouponRepository.countByCouponId(couponId); @@ -118,8 +116,7 @@ public CouponDeleteResponseDto deleteCoupon(Long couponId, Long userId) { userService.validateAdminRole(userId); // 쿠폰 조회 - Coupon coupon = couponRepository.findById(couponId) - .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + Coupon coupon = findCouponById(couponId); // 이미 삭제된 쿠폰인지 확인 if (coupon.isDeleted()) { @@ -343,4 +340,10 @@ private void updateCouponFields(Coupon coupon, CouponUpdateRequestDto request) { private void validateCouponDeletion(Coupon coupon, Long issuedCount) { //todo: 일단은 다 삭제가능하게 설정 해놨는데, 나중에 결제 붙으면여기에 여러 검증들을 추가할 예정 } + + //쿠폰id로 쿠폰을 조회하는 공통 메서드 + public Coupon findCouponById(Long couponId) { + return couponRepository.findById(couponId) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REQUEST_DATA)); + } }