Skip to content

Commit c9a8cb6

Browse files
authored
Merge pull request #65 from 9oormthon-univ/feat/qrcode
Feat: 유저별 qr발급
2 parents b2958cc + ab09f31 commit c9a8cb6

18 files changed

+275
-126
lines changed

src/main/java/com/trashheroesbe/feature/coupon/api/CouponPartnerController.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44

55
import com.trashheroesbe.feature.coupon.application.CouponService;
66
import com.trashheroesbe.feature.coupon.dto.request.CouponCreateRequest;
7+
import com.trashheroesbe.feature.coupon.dto.request.CouponUpdateRequest;
78
import com.trashheroesbe.feature.coupon.dto.response.CouponCreateResponse;
8-
import com.trashheroesbe.feature.coupon.dto.response.CouponQrResponse;
9+
import com.trashheroesbe.feature.coupon.dto.response.PartnerCouponResponse;
910
import com.trashheroesbe.global.response.ApiResponse;
1011
import com.trashheroesbe.global.auth.security.CustomerDetails;
1112
import jakarta.validation.Valid;
1213
import lombok.RequiredArgsConstructor;
13-
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.DeleteMapping;
15+
import org.springframework.web.bind.annotation.PathVariable;
1416
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.PatchMapping;
18+
import org.springframework.web.bind.annotation.GetMapping;
1519
import org.springframework.web.bind.annotation.RequestBody;
1620
import org.springframework.web.bind.annotation.RequestMapping;
17-
import org.springframework.web.bind.annotation.RequestParam;
1821
import org.springframework.web.bind.annotation.RestController;
1922
import org.springframework.security.core.annotation.AuthenticationPrincipal;
23+
import java.util.List;
2024

2125
@RestController
2226
@RequiredArgsConstructor
@@ -25,6 +29,14 @@ public class CouponPartnerController implements CouponPartnerControllerApi {
2529

2630
private final CouponService couponService;
2731

32+
@Override
33+
@GetMapping("/coupons")
34+
public ApiResponse<List<PartnerCouponResponse>> getPartnerCoupons(
35+
@AuthenticationPrincipal CustomerDetails customerDetails
36+
) {
37+
return ApiResponse.success(OK, couponService.getPartnerCoupons(customerDetails));
38+
}
39+
2840
@Override
2941
@PostMapping("/coupons")
3042
public ApiResponse<CouponCreateResponse> createCoupon(
@@ -35,11 +47,23 @@ public ApiResponse<CouponCreateResponse> createCoupon(
3547
}
3648

3749
@Override
38-
@GetMapping("/coupons/qr")
39-
public ApiResponse<CouponQrResponse> getCouponByQr(
40-
@RequestParam Long couponId,
41-
@RequestParam String qrToken
50+
@PatchMapping("/coupons/{couponId}")
51+
public ApiResponse<CouponCreateResponse> updateCoupon(
52+
@AuthenticationPrincipal CustomerDetails customerDetails,
53+
@PathVariable Long couponId,
54+
@Valid @RequestBody CouponUpdateRequest request
4255
) {
43-
return ApiResponse.success(OK, couponService.findByQr(couponId, qrToken));
56+
return ApiResponse.success(OK, couponService.updateCoupon(customerDetails, couponId, request));
4457
}
58+
59+
@Override
60+
@DeleteMapping("/coupons/{couponId}")
61+
public ApiResponse<Void> deleteCoupon(
62+
@AuthenticationPrincipal CustomerDetails customerDetails,
63+
@PathVariable Long couponId
64+
) {
65+
couponService.deleteCoupon(customerDetails, couponId);
66+
return ApiResponse.success(OK);
67+
}
68+
4569
}
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
11
package com.trashheroesbe.feature.coupon.api;
22

33
import com.trashheroesbe.feature.coupon.dto.request.CouponCreateRequest;
4+
import com.trashheroesbe.feature.coupon.dto.request.CouponUpdateRequest;
45
import com.trashheroesbe.feature.coupon.dto.response.CouponCreateResponse;
5-
import com.trashheroesbe.feature.coupon.dto.response.CouponQrResponse;
6+
import com.trashheroesbe.feature.coupon.dto.response.PartnerCouponResponse;
67
import com.trashheroesbe.global.response.ApiResponse;
78
import io.swagger.v3.oas.annotations.Operation;
89
import io.swagger.v3.oas.annotations.Parameter;
910
import io.swagger.v3.oas.annotations.tags.Tag;
1011
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1112
import jakarta.validation.Valid;
13+
import org.springframework.web.bind.annotation.PathVariable;
1214
import org.springframework.web.bind.annotation.RequestBody;
13-
import org.springframework.web.bind.annotation.RequestParam;
15+
import java.util.List;
1416
import com.trashheroesbe.global.auth.security.CustomerDetails;
1517

1618
@Tag(name = "Coupon", description = "쿠폰 생성/조회 API")
1719
public interface CouponPartnerControllerApi {
1820

21+
@Operation(summary = "파트너 쿠폰 전체 조회", description = "파트너가 발급한 쿠폰 목록을 조회합니다.")
22+
ApiResponse<List<PartnerCouponResponse>> getPartnerCoupons(
23+
@AuthenticationPrincipal CustomerDetails customerDetails
24+
);
25+
1926
@Operation(summary = "쿠폰 생성", description = "파트너가 쿠폰을 생성합니다.")
2027
ApiResponse<CouponCreateResponse> createCoupon(
2128
@AuthenticationPrincipal CustomerDetails customerDetails,
22-
@Valid @RequestBody CouponCreateRequest request
29+
@Valid @RequestBody CouponCreateRequest request
2330
);
2431

25-
@Operation(summary = "QR로 쿠폰 조회", description = "쿠폰 ID와 QR 토큰으로 쿠폰 정보를 조회합니다.")
26-
ApiResponse<CouponQrResponse> getCouponByQr(
27-
@Parameter(description = "쿠폰 ID", required = true) @RequestParam Long couponId,
28-
@Parameter(description = "QR 토큰", required = true) @RequestParam String qrToken
32+
@Operation(summary = "쿠폰 수정", description = "파트너가 본인 소유의 쿠폰을 수정합니다.")
33+
ApiResponse<CouponCreateResponse> updateCoupon(
34+
@AuthenticationPrincipal CustomerDetails customerDetails,
35+
@Parameter(description = "쿠폰 ID", required = true) @PathVariable Long couponId,
36+
@Valid @RequestBody CouponUpdateRequest request
2937
);
38+
39+
@Operation(summary = "쿠폰 삭제", description = "파트너가 본인 소유의 쿠폰을 삭제합니다.")
40+
ApiResponse<Void> deleteCoupon(
41+
@AuthenticationPrincipal CustomerDetails customerDetails,
42+
@Parameter(description = "쿠폰 ID", required = true) @PathVariable Long couponId
43+
);
44+
3045
}

src/main/java/com/trashheroesbe/feature/coupon/api/UserCouponController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1414
import org.springframework.web.bind.annotation.GetMapping;
1515
import org.springframework.web.bind.annotation.PathVariable;
16+
import org.springframework.web.bind.annotation.RequestParam;
1617
import org.springframework.web.bind.annotation.RequestMapping;
1718
import org.springframework.web.bind.annotation.RestController;
1819

@@ -45,4 +46,14 @@ public ApiResponse<UserCouponResponse> getUserCouponById(
4546
);
4647
return ApiResponse.success(OK, response);
4748
}
49+
50+
@Override
51+
@GetMapping("/qr")
52+
public ApiResponse<UserCouponResponse> getUserCouponByQr(
53+
@RequestParam Long userCouponId,
54+
@RequestParam String qrToken
55+
) {
56+
UserCouponResponse response = userCouponService.getUserCouponByQr(userCouponId, qrToken);
57+
return ApiResponse.success(OK, response);
58+
}
4859
}

src/main/java/com/trashheroesbe/feature/coupon/api/UserCouponControllerApi.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.trashheroesbe.feature.coupon.api;
22

3-
import com.trashheroesbe.feature.coupon.dto.response.CouponStoreListResponse;
43
import com.trashheroesbe.feature.coupon.dto.response.UserCouponListResponse;
54
import com.trashheroesbe.feature.coupon.dto.response.UserCouponResponse;
65
import com.trashheroesbe.global.auth.security.CustomerDetails;
76
import com.trashheroesbe.global.response.ApiResponse;
87
import io.swagger.v3.oas.annotations.Operation;
98
import io.swagger.v3.oas.annotations.tags.Tag;
109
import java.util.List;
10+
import org.springframework.web.bind.annotation.RequestParam;
1111

1212
@Tag(name = "UserCoupon", description = "내가 구매한 쿠폰 관련 API")
1313
public interface UserCouponControllerApi {
@@ -20,4 +20,10 @@ ApiResponse<UserCouponResponse> getUserCouponById(
2020
Long userCouponId,
2121
CustomerDetails customerDetails
2222
);
23+
24+
@Operation(summary = "QR로 유저쿠폰 조회", description = "userCouponId와 qrToken으로 유저쿠폰을 조회합니다.")
25+
ApiResponse<UserCouponResponse> getUserCouponByQr(
26+
@RequestParam Long userCouponId,
27+
@RequestParam String qrToken
28+
);
2329
}

src/main/java/com/trashheroesbe/feature/coupon/application/CouponService.java

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,34 @@
11
package com.trashheroesbe.feature.coupon.application;
22

3-
import static com.trashheroesbe.global.response.type.ErrorCode.ENTITY_NOT_FOUND;
4-
53
import com.trashheroesbe.feature.coupon.domain.entity.Coupon;
64
import com.trashheroesbe.feature.coupon.dto.request.CouponCreateRequest;
5+
import com.trashheroesbe.feature.coupon.dto.request.CouponUpdateRequest;
76
import com.trashheroesbe.feature.coupon.dto.response.CouponCreateResponse;
8-
import com.trashheroesbe.feature.coupon.dto.response.CouponQrResponse;
7+
import com.trashheroesbe.feature.coupon.dto.response.PartnerCouponResponse;
98
import com.trashheroesbe.feature.coupon.infrastructure.CouponRepository;
109
import com.trashheroesbe.feature.partner.domain.entity.Partner;
1110
import com.trashheroesbe.global.exception.BusinessException;
12-
import com.trashheroesbe.global.qrcode.QrCodeGenerator;
13-
import com.trashheroesbe.global.util.CouponQrUtil;
14-
import com.trashheroesbe.infrastructure.port.s3.FileStoragePort;
1511
import com.trashheroesbe.global.response.type.ErrorCode;
1612
import com.trashheroesbe.global.auth.security.CustomerDetails;
13+
import java.util.List;
1714
import lombok.RequiredArgsConstructor;
18-
import org.springframework.beans.factory.annotation.Value;
1915
import org.springframework.stereotype.Service;
2016
import org.springframework.transaction.annotation.Transactional;
21-
import java.util.UUID;
2217

2318
@Service
2419
@Transactional
2520
@RequiredArgsConstructor
2621
public class CouponService {
2722

2823
private final CouponRepository couponRepository;
29-
private final QrCodeGenerator qrCodeGenerator;
30-
private final FileStoragePort fileStoragePort;
3124

32-
@Value("${qr.coupon-url}")
33-
private String couponQrBaseUrl;
25+
@Transactional(readOnly = true)
26+
public List<PartnerCouponResponse> getPartnerCoupons(CustomerDetails customerDetails) {
27+
Partner partner = extractPartner(customerDetails);
28+
return couponRepository.findAllByPartnerIdFetch(partner.getId()).stream()
29+
.map(PartnerCouponResponse::from)
30+
.toList();
31+
}
3432

3533
public CouponCreateResponse createCoupon(
3634
CustomerDetails customerDetails,
@@ -39,25 +37,40 @@ public CouponCreateResponse createCoupon(
3937
Partner partner = extractPartner(customerDetails);
4038
Coupon saved = couponRepository.save(Coupon.create(request, partner));
4139

42-
String qrToken = UUID.randomUUID().toString();
43-
String payload = CouponQrUtil.buildPayload(couponQrBaseUrl, saved.getId(), qrToken);
44-
byte[] qrBytes = qrCodeGenerator.generatePngBytes(payload, 300);
45-
String key = CouponQrUtil.buildKey(saved.getId());
46-
String qrUrl = fileStoragePort.uploadFile(key, "image/png", qrBytes);
47-
saved.attachQr(qrToken, qrUrl);
48-
4940
return CouponCreateResponse.from(saved);
5041
}
5142

52-
@Transactional(readOnly = true)
53-
public CouponQrResponse findByQr(Long couponId, String qrToken) {
54-
if (couponId == null || qrToken == null || qrToken.isBlank()) {
55-
throw new BusinessException(ErrorCode.VALIDATION_FAILED);
43+
@Transactional
44+
public void deleteCoupon(CustomerDetails customerDetails, Long couponId) {
45+
Partner partner = extractPartner(customerDetails);
46+
Coupon coupon = couponRepository.findByIdFetchPartner(couponId)
47+
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
48+
if (!coupon.getPartner().getId().equals(partner.getId())) {
49+
throw new BusinessException(ErrorCode.ACCESS_DENIED_EXCEPTION);
5650
}
57-
Coupon coupon = couponRepository.findByIdAndQrToken(couponId, qrToken)
58-
.orElseThrow(() -> new BusinessException(ENTITY_NOT_FOUND));
51+
couponRepository.delete(coupon);
52+
}
5953

60-
return CouponQrResponse.from(coupon);
54+
@Transactional
55+
public CouponCreateResponse updateCoupon(CustomerDetails customerDetails, Long couponId,
56+
CouponUpdateRequest request) {
57+
Partner partner = extractPartner(customerDetails);
58+
Coupon coupon = couponRepository.findByIdFetchPartner(couponId)
59+
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
60+
if (!coupon.getPartner().getId().equals(partner.getId())) {
61+
throw new BusinessException(ErrorCode.ACCESS_DENIED_EXCEPTION);
62+
}
63+
coupon.applyUpdate(
64+
request.title(),
65+
request.content(),
66+
request.type(),
67+
request.pointCost(),
68+
request.discountType(),
69+
request.discountValue(),
70+
request.totalStock(),
71+
request.isActive()
72+
);
73+
return CouponCreateResponse.from(coupon);
6174
}
6275

6376
private Partner extractPartner(CustomerDetails customerDetails) {

src/main/java/com/trashheroesbe/feature/coupon/application/CouponStoreService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
import com.trashheroesbe.feature.coupon.dto.response.PurchaseUserCouponResponse;
1313
import com.trashheroesbe.feature.coupon.infrastructure.CouponRepository;
1414
import com.trashheroesbe.feature.coupon.infrastructure.UserCouponRepository;
15+
import com.trashheroesbe.global.qrcode.QrCodeGenerator;
16+
import com.trashheroesbe.global.util.UserCouponQrUtil;
1517
import com.trashheroesbe.feature.point.application.PointService;
1618
import com.trashheroesbe.feature.point.domain.type.PointReason;
1719
import com.trashheroesbe.feature.user.domain.entity.User;
1820
import com.trashheroesbe.global.exception.BusinessException;
21+
import com.trashheroesbe.infrastructure.port.s3.FileStoragePort;
1922
import java.util.List;
2023
import java.util.stream.Collectors;
2124
import lombok.RequiredArgsConstructor;
@@ -25,6 +28,8 @@
2528
import org.springframework.retry.annotation.Retryable;
2629
import org.springframework.stereotype.Service;
2730
import org.springframework.transaction.annotation.Transactional;
31+
import org.springframework.beans.factory.annotation.Value;
32+
import java.util.UUID;
2833

2934
@Slf4j
3035
@Service
@@ -36,6 +41,11 @@ public class CouponStoreService {
3641

3742
private final CouponRepository couponRepository;
3843
private final UserCouponRepository userCouponRepository;
44+
private final QrCodeGenerator qrCodeGenerator;
45+
private final FileStoragePort fileStoragePort;
46+
47+
@Value("${qr.user-coupon-url}")
48+
private String userCouponQrBaseUrl;
3949

4050
public List<CouponStoreListResponse> getCouponStoreList() {
4151
List<Coupon> coupons = couponRepository.findAll();
@@ -84,6 +94,13 @@ public PurchaseUserCouponResponse purchaseCoupon(
8494
UserCoupon userCoupon = UserCoupon.create(user, coupon);
8595
UserCoupon savedUserCoupon = userCouponRepository.save(userCoupon);
8696

97+
String qrToken = UUID.randomUUID().toString();
98+
String payload = UserCouponQrUtil.buildPayload(userCouponQrBaseUrl, savedUserCoupon.getId(), qrToken);
99+
byte[] qrBytes = qrCodeGenerator.generatePngBytes(payload, 300);
100+
String key = UserCouponQrUtil.buildKey(savedUserCoupon.getId());
101+
String qrUrl = fileStoragePort.uploadFile(key, "image/png", qrBytes);
102+
savedUserCoupon.attachQr(qrToken, qrUrl);
103+
87104
log.info("쿠폰 구매 완료 - userId: {}, couponId: {}, pointsUsed: {}",
88105
user.getId(), request.couponId(), pointCost);
89106

src/main/java/com/trashheroesbe/feature/coupon/application/UserCouponService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.stream.Collectors;
1414
import lombok.RequiredArgsConstructor;
1515
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
1617

1718
@Service
1819
@RequiredArgsConstructor
@@ -29,7 +30,7 @@ public List<UserCouponListResponse> getUserCouponList(User user) {
2930
}
3031

3132
public UserCouponResponse getUserCouponById(Long userCouponId, User user) {
32-
UserCoupon userCoupon = userCouponRepository.findById(userCouponId)
33+
UserCoupon userCoupon = userCouponRepository.findWithDetailsById(userCouponId)
3334
.orElseThrow(() -> new BusinessException(ENTITY_NOT_FOUND));
3435

3536
if (!userCoupon.getUser().getId().equals(user.getId())) {
@@ -38,4 +39,14 @@ public UserCouponResponse getUserCouponById(Long userCouponId, User user) {
3839

3940
return UserCouponResponse.from(userCoupon);
4041
}
42+
43+
@Transactional(readOnly = true)
44+
public UserCouponResponse getUserCouponByQr(Long userCouponId, String qrToken) {
45+
if (userCouponId == null || qrToken == null || qrToken.isBlank()) {
46+
throw new BusinessException(ErrorCode.VALIDATION_FAILED);
47+
}
48+
UserCoupon userCoupon = userCouponRepository.findByIdAndQrTokenFetchAll(userCouponId, qrToken)
49+
.orElseThrow(() -> new BusinessException(ErrorCode.ENTITY_NOT_FOUND));
50+
return UserCouponResponse.from(userCoupon);
51+
}
4152
}

0 commit comments

Comments
 (0)