-
Notifications
You must be signed in to change notification settings - Fork 44
[volume-4] 쿠폰 도메인 추가, 동시성 제어 적용 - 이주옥 #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: juoklee
Are you sure you want to change the base?
Changes from all commits
bb47b53
10566b6
53b8c87
4007344
a7588f2
453728b
ee5cab8
54676ad
f8cf21f
8f3b78a
7bcf457
131421b
55d3677
5cb3f15
b392d59
b967fa5
5d0ffaf
203adbf
6f3b81c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package com.loopers.application.coupon; | ||
|
|
||
| import com.loopers.application.PagedInfo; | ||
| import com.loopers.domain.PageResult; | ||
| import com.loopers.domain.coupon.Coupon; | ||
| import com.loopers.domain.coupon.CouponService; | ||
| import com.loopers.domain.coupon.CouponType; | ||
| import com.loopers.domain.coupon.MemberCoupon; | ||
| import com.loopers.domain.member.Member; | ||
| import com.loopers.domain.member.MemberService; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class CouponFacade { | ||
|
|
||
| private final MemberService memberService; | ||
| private final CouponService couponService; | ||
|
|
||
| // ── Admin ── | ||
|
|
||
| public CouponInfo createCoupon(String name, String type, Long value, Long minOrderAmount, | ||
| LocalDateTime expiredAt, Integer validDays, Integer totalQuantity) { | ||
| CouponType couponType; | ||
| try { | ||
| couponType = CouponType.valueOf(type); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 쿠폰 타입입니다: " + type); | ||
| } | ||
| Coupon coupon = couponService.createCoupon(name, couponType, value, minOrderAmount, expiredAt, validDays, totalQuantity); | ||
| return CouponInfo.of(coupon); | ||
| } | ||
|
|
||
| public CouponInfo getCoupon(Long couponId) { | ||
| Coupon coupon = couponService.getCoupon(couponId); | ||
| return CouponInfo.of(coupon); | ||
| } | ||
|
|
||
| public PagedInfo<CouponInfo> getCoupons(int page, int size) { | ||
| PageResult<Coupon> result = couponService.getCoupons(page, size); | ||
| List<CouponInfo> infos = result.content().stream().map(CouponInfo::of).toList(); | ||
| return new PagedInfo<>(infos, result.totalElements(), result.totalPages(), result.page(), result.size()); | ||
| } | ||
|
|
||
| public void updateCoupon(Long couponId, String name, Long value, Long minOrderAmount, LocalDateTime expiredAt, Integer validDays) { | ||
| couponService.updateCoupon(couponId, name, value, minOrderAmount, expiredAt, validDays); | ||
| } | ||
|
|
||
| public void deleteCoupon(Long couponId) { | ||
| couponService.deleteCoupon(couponId); | ||
| } | ||
|
|
||
| public PagedInfo<MemberCouponInfo> getIssuedCoupons(Long couponId, int page, int size) { | ||
| Coupon coupon = couponService.getCoupon(couponId); | ||
| CouponInfo couponInfo = CouponInfo.of(coupon); | ||
| PageResult<MemberCoupon> result = couponService.getIssuedCoupons(couponId, page, size); | ||
| List<MemberCouponInfo> infos = result.content().stream() | ||
| .map(mc -> MemberCouponInfo.of(mc, couponInfo)) | ||
| .toList(); | ||
| return new PagedInfo<>(infos, result.totalElements(), result.totalPages(), result.page(), result.size()); | ||
| } | ||
|
|
||
| // ── 대고객 ── | ||
|
|
||
| public MemberCouponInfo issueCoupon(String loginId, Long couponId) { | ||
| Long memberId = getMemberId(loginId); | ||
| MemberCoupon memberCoupon = couponService.issueCoupon(couponId, memberId); | ||
| Coupon coupon = couponService.getCoupon(couponId); | ||
| return MemberCouponInfo.of(memberCoupon, CouponInfo.of(coupon)); | ||
| } | ||
|
|
||
| public PagedInfo<MemberCouponInfo> getMyCoupons(String loginId, int page, int size) { | ||
| Long memberId = getMemberId(loginId); | ||
| PageResult<MemberCoupon> result = couponService.getMemberCoupons(memberId, page, size); | ||
|
|
||
| List<Long> couponIds = result.content().stream().map(MemberCoupon::getCouponId).distinct().toList(); | ||
| Map<Long, Coupon> couponMap = couponService.getCoupons(couponIds).stream() | ||
| .collect(Collectors.toMap(Coupon::getId, Function.identity())); | ||
|
|
||
| List<MemberCouponInfo> infos = result.content().stream() | ||
| .map(mc -> MemberCouponInfo.of(mc, CouponInfo.of(couponMap.get(mc.getCouponId())))) | ||
| .toList(); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return new PagedInfo<>(infos, result.totalElements(), result.totalPages(), result.page(), result.size()); | ||
| } | ||
|
|
||
| private Long getMemberId(String loginId) { | ||
| Member member = memberService.getMemberByLoginId(loginId); | ||
| return member.getId(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.loopers.application.coupon; | ||
|
|
||
| import com.loopers.domain.coupon.Coupon; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record CouponInfo( | ||
| Long id, | ||
| String name, | ||
| String type, | ||
| Long value, | ||
| Long minOrderAmount, | ||
| LocalDateTime expiredAt, | ||
| Integer validDays, | ||
| Integer totalQuantity, | ||
| int issuedQuantity | ||
| ) { | ||
| public static CouponInfo of(Coupon coupon) { | ||
| return new CouponInfo( | ||
| coupon.getId(), | ||
| coupon.getName(), | ||
| coupon.getType().name(), | ||
| coupon.getValue(), | ||
| coupon.getMinOrderAmount(), | ||
| coupon.getExpiredAt(), | ||
| coupon.getValidDays(), | ||
| coupon.getTotalQuantity(), | ||
| coupon.getIssuedQuantity() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.loopers.application.coupon; | ||
|
|
||
| import com.loopers.domain.coupon.MemberCoupon; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record MemberCouponInfo( | ||
| Long id, | ||
| Long memberId, | ||
| Long couponId, | ||
| String status, | ||
| LocalDateTime usedAt, | ||
| LocalDateTime expiredAt, | ||
| CouponInfo coupon | ||
| ) { | ||
| public static MemberCouponInfo of(MemberCoupon memberCoupon, CouponInfo couponInfo) { | ||
| return new MemberCouponInfo( | ||
| memberCoupon.getId(), | ||
| memberCoupon.getMemberId(), | ||
| memberCoupon.getCouponId(), | ||
| memberCoupon.getStatus().name(), | ||
| memberCoupon.getUsedAt(), | ||
| memberCoupon.getExpiredAt(), | ||
| couponInfo | ||
| ); | ||
| } | ||
|
Comment on lines
+16
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "MemberCouponInfo.java" -o -name "*.java" -path "*/coupon/*" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1811 🏁 Script executed: rg -n -B3 "MemberCouponInfo\.of\(" --type=java -A3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 3012 🏁 Script executed: # Check the actual MemberCouponInfo.java file
cat -n apps/commerce-api/src/main/java/com/loopers/application/coupon/MemberCouponInfo.java | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 985 🏁 Script executed: cat -n apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponFacade.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4676 🏁 Script executed: # Check CouponInfo.of() for null handling
cat -n apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponInfo.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1050 🏁 Script executed: # Check MemberCoupon and Coupon domain classes
cat -n apps/commerce-api/src/main/java/com/loopers/domain/coupon/MemberCoupon.java | head -60Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2358 🏁 Script executed: cat -n apps/commerce-api/src/main/java/com/loopers/domain/coupon/Coupon.java | head -60Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2240 🏁 Script executed: cat -n apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java | head -100Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4822 🏁 Script executed: # Check CouponReader implementation
cat -n apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponReader.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 439 🏁 Script executed: # Check if there's error handling in CouponService
rg -n "getCoupon|issueCoupon" apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java -A 5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1607 🏁 Script executed: # Check for potential null handling in CouponInfo.of and related code
rg -n "couponMap\.get|orElseThrow|Optional" apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponFacade.java -B 2 -A 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 398 🏁 Script executed: # Verify the getMyCoupons flow - check if all coupons in couponMap are guaranteed
cat -n apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponFacade.java | sed -n '73,86p'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1005 🏁 Script executed: # Check what happens when coupon is null
rg -n "getType|getId" apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponInfo.java -B 1 -A 1Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 256 MemberCouponInfo.of() 및 CouponInfo.of()에서 null 입력에 대한 방어가 없다. 실제 null 위험은 다음을 고려해야 한다:
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,11 +36,14 @@ public LikeToggleInfo toggleProductLike(String loginId, Long productId) { | |
|
|
||
| boolean liked = likeService.toggleLike(memberId, LikeTargetType.PRODUCT, productId); | ||
|
|
||
| int likeCount = liked | ||
| ? productService.increaseLikeCount(productId) | ||
| : productService.decreaseLikeCount(productId); | ||
|
|
||
| return new LikeToggleInfo(liked, likeCount); | ||
| if (liked) { | ||
| productService.increaseLikeCount(productId); | ||
| } else { | ||
| productService.decreaseLikeCount(productId); | ||
| } | ||
|
|
||
| Product product = productService.getProduct(productId); | ||
| return new LikeToggleInfo(liked, product.getLikeCount()); | ||
| } | ||
|
|
||
| @Transactional | ||
|
|
@@ -50,11 +53,14 @@ public LikeToggleInfo toggleBrandLike(String loginId, Long brandId) { | |
|
|
||
| boolean liked = likeService.toggleLike(memberId, LikeTargetType.BRAND, brandId); | ||
|
|
||
| int likeCount = liked | ||
| ? brandService.increaseLikeCount(brandId) | ||
| : brandService.decreaseLikeCount(brandId); | ||
| if (liked) { | ||
| brandService.increaseLikeCount(brandId); | ||
| } else { | ||
| brandService.decreaseLikeCount(brandId); | ||
| } | ||
|
|
||
| return new LikeToggleInfo(liked, likeCount); | ||
| Brand brand = brandService.getBrand(brandId); | ||
| return new LikeToggleInfo(liked, brand.getLikeCount()); | ||
|
Comment on lines
+56
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brand 토글에도 동일한 stale read 문제 존재
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |||||||||||||||||||||||||||||||
| import com.loopers.domain.member.Gender; | ||||||||||||||||||||||||||||||||
| import com.loopers.domain.member.Member; | ||||||||||||||||||||||||||||||||
| import com.loopers.domain.member.MemberService; | ||||||||||||||||||||||||||||||||
| import com.loopers.support.error.CoreException; | ||||||||||||||||||||||||||||||||
| import com.loopers.support.error.ErrorType; | ||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -22,7 +24,12 @@ public boolean authenticate(String loginId, String rawPassword) { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public MemberInfo register(String loginId, String rawPassword, String name, | ||||||||||||||||||||||||||||||||
| LocalDate birthDate, String gender, String email, String phone) { | ||||||||||||||||||||||||||||||||
| Gender genderEnum = Gender.valueOf(gender); | ||||||||||||||||||||||||||||||||
| Gender genderEnum; | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| genderEnum = Gender.valueOf(gender); | ||||||||||||||||||||||||||||||||
| } catch (IllegalArgumentException e) { | ||||||||||||||||||||||||||||||||
| throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 성별입니다: " + gender); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛡️ null 체크 추가 제안 Gender genderEnum;
+if (gender == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "성별은 필수 입력값입니다.");
+}
try {
genderEnum = Gender.valueOf(gender);
} catch (IllegalArgumentException e) {
throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 성별입니다: " + gender);
}운영 환경에서 null이 입력되는 경우, 로그에 NPE 스택트레이스가 남고 클라이언트는 명확하지 않은 500 에러를 받게 된다. null 케이스에 대한 E2E 테스트도 추가를 권장한다. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| Member member = memberService.register(loginId, rawPassword, name, birthDate, genderEnum, email, phone); | ||||||||||||||||||||||||||||||||
| return MemberInfo.from(member); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.