Skip to content

Commit 21f158e

Browse files
committed
merge
2 parents 204b08d + eb31995 commit 21f158e

39 files changed

+513
-182
lines changed

.github/workflows/gradle.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Java CI with Gradle
2+
3+
on:
4+
push:
5+
branches: [ "develop" ]
6+
pull_request:
7+
branches: [ "develop" ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Set up JDK
17+
uses: actions/setup-java@v4
18+
with:
19+
distribution: 'temurin'
20+
java-version: '21'
21+
22+
- name: Grant execute permission for gradlew
23+
run: chmod +x ./gradlew
24+
25+
- name: Build without running tests
26+
run: ./gradlew build -x test
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.threestar.trainus.domain.coupon.admin.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestBody;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto;
11+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto;
12+
import com.threestar.trainus.domain.coupon.admin.service.AdminCouponService;
13+
import com.threestar.trainus.global.annotation.LoginUser;
14+
import com.threestar.trainus.global.unit.BaseResponse;
15+
16+
import io.swagger.v3.oas.annotations.Operation;
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import jakarta.validation.Valid;
19+
import lombok.RequiredArgsConstructor;
20+
21+
@Tag(name = "관리자 쿠폰 API", description = "관리자 쿠폰 생성,수정 및 삭제 관련 API")
22+
@RestController
23+
@RequestMapping("/api/v1/coupons")
24+
@RequiredArgsConstructor
25+
public class AdminCouponController {
26+
27+
private final AdminCouponService adminCouponService;
28+
29+
@PostMapping
30+
@Operation(summary = "쿠폰 생성", description = "관리자가 새로운 쿠폰을 생성")
31+
public ResponseEntity<BaseResponse<CouponCreateResponseDto>> createCoupon(
32+
@Valid @RequestBody CouponCreateRequestDto request,
33+
@LoginUser Long loginUserId
34+
) {
35+
CouponCreateResponseDto response = adminCouponService.createCoupon(request, loginUserId);
36+
return BaseResponse.ok("쿠폰 생성이 완료되었습니다.", response, HttpStatus.OK);
37+
}
38+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.threestar.trainus.domain.coupon.admin.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
import com.threestar.trainus.domain.coupon.user.entity.CouponCategory;
6+
import com.threestar.trainus.domain.coupon.user.entity.CouponStatus;
7+
8+
import jakarta.validation.constraints.Min;
9+
import jakarta.validation.constraints.NotBlank;
10+
import jakarta.validation.constraints.NotNull;
11+
import jakarta.validation.constraints.Size;
12+
13+
public record CouponCreateRequestDto(
14+
15+
@NotBlank(message = "쿠폰명은 필수입니다")
16+
@Size(max = 45, message = "쿠폰명은 45자 이하여야 합니다")
17+
String couponName,
18+
19+
LocalDateTime expirationDate,
20+
21+
@NotBlank(message = "할인가격은 필수입니다")
22+
String discountPrice,
23+
24+
@NotNull(message = "최소 주문 금액은 필수입니다")
25+
@Min(value = 0, message = "최소 주문 금액은 0원 이상이어야 합니다")
26+
Integer minOrderPrice,
27+
28+
@NotNull(message = "쿠폰 상태는 필수입니다")
29+
CouponStatus status,
30+
31+
@Min(value = 1, message = "수량은 1개 이상이어야 합니다")
32+
Integer quantity,
33+
34+
@NotNull(message = "쿠폰 카테고리는 필수입니다")
35+
CouponCategory category,
36+
37+
@NotNull(message = "오픈 시간은 필수입니다")
38+
LocalDateTime couponOpenAt,
39+
40+
@NotNull(message = "마감 시간은 필수입니다")
41+
LocalDateTime couponDeadlineAt
42+
) {
43+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.threestar.trainus.domain.coupon.admin.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
public record CouponCreateResponseDto(
6+
Long couponId,
7+
String couponName,
8+
String status,
9+
LocalDateTime createdAt
10+
) {
11+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.threestar.trainus.domain.coupon.admin.mapper;
2+
3+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto;
4+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto;
5+
import com.threestar.trainus.domain.coupon.user.entity.Coupon;
6+
7+
public class AdminCouponMapper {
8+
9+
private AdminCouponMapper() {
10+
}
11+
12+
public static Coupon toEntity(CouponCreateRequestDto request) {
13+
return Coupon.builder()
14+
.name(request.couponName())
15+
.expirationDate(request.expirationDate())
16+
.discountPrice(request.discountPrice())
17+
.minOrderPrice(request.minOrderPrice())
18+
.status(request.status())
19+
.quantity(request.quantity())
20+
.category(request.category())
21+
.openAt(request.couponOpenAt())
22+
.closeAt(request.couponDeadlineAt())
23+
.build();
24+
}
25+
26+
public static CouponCreateResponseDto toCreateResponseDto(Coupon coupon) {
27+
return new CouponCreateResponseDto(
28+
coupon.getId(),
29+
coupon.getName(),
30+
coupon.getStatus().name(),
31+
coupon.getCreatedAt()
32+
);
33+
}
34+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.threestar.trainus.domain.coupon.admin.service;
2+
3+
import java.time.LocalDateTime;
4+
5+
import org.springframework.stereotype.Service;
6+
import org.springframework.transaction.annotation.Transactional;
7+
8+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateRequestDto;
9+
import com.threestar.trainus.domain.coupon.admin.dto.CouponCreateResponseDto;
10+
import com.threestar.trainus.domain.coupon.admin.mapper.AdminCouponMapper;
11+
import com.threestar.trainus.domain.coupon.user.entity.Coupon;
12+
import com.threestar.trainus.domain.coupon.user.entity.CouponCategory;
13+
import com.threestar.trainus.domain.coupon.user.repository.CouponRepository;
14+
import com.threestar.trainus.domain.user.service.UserService;
15+
import com.threestar.trainus.global.exception.domain.ErrorCode;
16+
import com.threestar.trainus.global.exception.handler.BusinessException;
17+
18+
import lombok.RequiredArgsConstructor;
19+
20+
@Service
21+
@RequiredArgsConstructor
22+
public class AdminCouponService {
23+
24+
private final CouponRepository couponRepository;
25+
private final UserService userService;
26+
27+
@Transactional
28+
public CouponCreateResponseDto createCoupon(CouponCreateRequestDto request, Long userId) {
29+
// 관리자 권한 검증
30+
userService.validateAdminRole(userId);
31+
32+
// 쿠폰 생성 검증
33+
validateCouponRequest(request);
34+
35+
Coupon coupon = AdminCouponMapper.toEntity(request);
36+
Coupon savedCoupon = couponRepository.save(coupon);
37+
38+
// 응답 DTO 반환
39+
return AdminCouponMapper.toCreateResponseDto(savedCoupon);
40+
}
41+
42+
private void validateCouponRequest(CouponCreateRequestDto request) {
43+
// 할인가격이 뭔지 검증(퍼센트인지 금액인지)
44+
validateDiscountPrice(request.discountPrice());
45+
46+
// 오픈 시간이 마감 시간보다 늦으면 안됨
47+
if (request.couponOpenAt().isAfter(request.couponDeadlineAt())) {
48+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
49+
}
50+
51+
// 오픈 시간이 현재 시간보다 과거면 안됨 -> 즉시 오픈은 허용함!
52+
if (request.couponOpenAt().isBefore(LocalDateTime.now().minusMinutes(1))) {
53+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
54+
}
55+
56+
// 선착순 쿠폰 검증
57+
if (request.category() == CouponCategory.OPEN_RUN) {
58+
// 선착순 쿠폰은 수량이 필수
59+
if (request.quantity() == null || request.quantity() <= 0) {
60+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
61+
}
62+
}
63+
64+
// 일반 쿠폰의 경우 수량은 null도 허용
65+
if (request.category() == CouponCategory.NORMAL) {
66+
// 수량이 설정된 경우에만 검증
67+
if (request.quantity() != null && request.quantity() <= 0) {
68+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
69+
}
70+
}
71+
72+
}
73+
74+
//할인 형식을 검증
75+
private void validateDiscountPrice(String discountPrice) {
76+
if (discountPrice == null || discountPrice.trim().isEmpty()) {
77+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
78+
}
79+
80+
String cutPrice = discountPrice.trim();
81+
82+
if (cutPrice.endsWith("%")) {
83+
// 퍼센트 할인 검증
84+
String percentStr = cutPrice.substring(0, cutPrice.length() - 1);
85+
try {
86+
int percent = Integer.parseInt(percentStr);
87+
if (percent <= 0 || percent > 100) {
88+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
89+
}
90+
} catch (NumberFormatException e) {
91+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
92+
}
93+
} else {
94+
// 금액 할인 검증
95+
try {
96+
int amount = Integer.parseInt(cutPrice);
97+
if (amount <= 0) {
98+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
99+
}
100+
} catch (NumberFormatException e) {
101+
throw new BusinessException(ErrorCode.INVALID_REQUEST_DATA);
102+
}
103+
}
104+
}
105+
}

src/main/java/com/threestar/trainus/domain/coupon/entity/CouponCategory.java

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/main/java/com/threestar/trainus/domain/coupon/entity/CouponStatus.java

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/main/java/com/threestar/trainus/domain/coupon/controller/CouponController.java renamed to src/main/java/com/threestar/trainus/domain/coupon/user/controller/CouponController.java

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.threestar.trainus.domain.coupon.controller;
1+
package com.threestar.trainus.domain.coupon.user.controller;
22

33
import org.springframework.http.HttpStatus;
44
import org.springframework.http.ResponseEntity;
@@ -9,18 +9,19 @@
99
import org.springframework.web.bind.annotation.RequestParam;
1010
import org.springframework.web.bind.annotation.RestController;
1111

12-
import com.threestar.trainus.domain.coupon.dto.CouponPageResponseDto;
13-
import com.threestar.trainus.domain.coupon.dto.CreateUserCouponResponseDto;
14-
import com.threestar.trainus.domain.coupon.dto.UserCouponPageResponseDto;
15-
import com.threestar.trainus.domain.coupon.entity.CouponStatus;
16-
import com.threestar.trainus.domain.coupon.service.CouponService;
17-
import com.threestar.trainus.global.exception.domain.ErrorCode;
18-
import com.threestar.trainus.global.exception.handler.BusinessException;
12+
import com.threestar.trainus.domain.coupon.user.dto.CouponPageResponseDto;
13+
import com.threestar.trainus.domain.coupon.user.dto.CreateUserCouponResponseDto;
14+
import com.threestar.trainus.domain.coupon.user.dto.UserCouponPageResponseDto;
15+
import com.threestar.trainus.domain.coupon.user.entity.CouponStatus;
16+
import com.threestar.trainus.domain.coupon.user.service.CouponService;
17+
import com.threestar.trainus.global.annotation.LoginUser;
1918
import com.threestar.trainus.global.unit.BaseResponse;
2019

21-
import jakarta.servlet.http.HttpSession;
20+
import io.swagger.v3.oas.annotations.Operation;
21+
import io.swagger.v3.oas.annotations.tags.Tag;
2222
import lombok.RequiredArgsConstructor;
2323

24+
@Tag(name = "쿠폰 API", description = "쿠폰 발급/조회 관련 API")
2425
@RequiredArgsConstructor
2526
@RestController
2627
@RequestMapping("api/v1/coupons")
@@ -29,37 +30,30 @@ public class CouponController {
2930
private final CouponService couponService;
3031

3132
@PostMapping("/{couponId}")
32-
public ResponseEntity<BaseResponse<CreateUserCouponResponseDto>> createUserCoupon(@PathVariable Long couponId,
33-
HttpSession session
33+
@Operation(summary = "쿠폰 발급 API", description = "couponId에 맞는 쿠폰을 유저에게 발급하는 API입니다.")
34+
public ResponseEntity<BaseResponse<CreateUserCouponResponseDto>> createUserCoupon(
35+
@PathVariable Long couponId,
36+
@LoginUser Long userId
3437
) {
35-
Long userId = (Long)session.getAttribute("LOGIN_USER");
36-
if (userId == null) {
37-
throw new BusinessException(ErrorCode.AUTHENTICATION_REQUIRED);
38-
}
3938
CreateUserCouponResponseDto dto = couponService.createUserCoupon(userId, couponId);
4039

4140
return BaseResponse.ok("쿠폰 발급 완료", dto, HttpStatus.CREATED);
4241
}
4342

4443
@GetMapping("/my-coupons")
44+
@Operation(summary = "내 쿠폰 목록 조회 API", description = "내가 보유한 쿠폰 목록을 조회하는 API입니다.")
4545
public ResponseEntity<BaseResponse<UserCouponPageResponseDto>> getUserCoupons(
46-
@RequestParam(required = false) CouponStatus status, HttpSession session
46+
@RequestParam(required = false) CouponStatus status,
47+
@LoginUser Long userId
4748
) {
48-
Long userId = (Long)session.getAttribute("LOGIN_USER");
49-
if (userId == null) {
50-
throw new BusinessException(ErrorCode.AUTHENTICATION_REQUIRED);
51-
}
5249
UserCouponPageResponseDto dto = couponService.getUserCoupons(userId, status);
5350

5451
return BaseResponse.ok("사용자 보유 쿠폰 조회 성공", dto, HttpStatus.OK);
5552
}
5653

5754
@GetMapping
58-
public ResponseEntity<BaseResponse<CouponPageResponseDto>> getCoupons(HttpSession session) {
59-
Long userId = (Long)session.getAttribute("LOGIN_USER");
60-
if (userId == null) {
61-
throw new BusinessException(ErrorCode.AUTHENTICATION_REQUIRED);
62-
}
55+
@Operation(summary = "발급 가능 쿠폰 목록 조회 API", description = "발급 가능한 쿠폰 목록을 조회하는 API입니다.")
56+
public ResponseEntity<BaseResponse<CouponPageResponseDto>> getCoupons(@LoginUser Long userId) {
6357
CouponPageResponseDto dto = couponService.getCoupons(userId);
6458

6559
return BaseResponse.ok("발급가능한 쿠폰 조회 성공", dto, HttpStatus.OK);

src/main/java/com/threestar/trainus/domain/coupon/dto/CouponPageResponseDto.java renamed to src/main/java/com/threestar/trainus/domain/coupon/user/dto/CouponPageResponseDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.threestar.trainus.domain.coupon.dto;
1+
package com.threestar.trainus.domain.coupon.user.dto;
22

33
import java.util.List;
44

0 commit comments

Comments
 (0)