From 7ad26123edf31ffe1595abbaa327d7aa6c37eb53 Mon Sep 17 00:00:00 2001 From: wooh Date: Wed, 29 Jul 2026 18:17:50 +0900 Subject: [PATCH] =?UTF-8?q?[Fix]=20=EC=BF=A0=ED=8F=B0=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=8C=80=EC=8B=9C=20=EC=97=86=EB=8A=94=2012?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 쿠폰 번호 검증 형식을 영문 또는 숫자 12자리로 변경 - 대시 포함 쿠폰 번호 입력 차단 - 테스트 및 환경변수 예시를 연속 문자열 형식으로 수정 - 쿠폰 등록과 중복 사용 방지 테스트 보완 --- .env.example | 2 +- .env.production.example | 2 +- .../dto/request/CouponRedeemRequest.java | 4 +-- .../payment/service/CouponServiceTest.java | 28 +++++++++++++------ src/test/resources/application-test.yaml | 2 +- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 25d09da..fe38945 100644 --- a/.env.example +++ b/.env.example @@ -43,7 +43,7 @@ TOSS_PAY_API_KEY=change-me TOSS_PAY_RETURN_URL=http://localhost:3000/credit/payment-result TOSS_PAY_CANCEL_URL=http://localhost:3000/credit/payment-cancel TOSS_PAY_RESULT_CALLBACK_URL=http://localhost:8080/api/payments/toss/callback -PAYMENT_COUPON_CODE=ABCD-EFGH-IJKL +PAYMENT_COUPON_CODE=ABCDEFGHIJKL JOB_POSTING_CLASSIFICATION_CONFIDENCE_THRESHOLD=0.65 APP_WORKER_EXCHANGE=jobdri.worker.exchange diff --git a/.env.production.example b/.env.production.example index 634f4e4..b250acc 100644 --- a/.env.production.example +++ b/.env.production.example @@ -53,7 +53,7 @@ TOSS_PAY_API_KEY=change-me TOSS_PAY_RETURN_URL=https://jobdri.site/credit/payment-result TOSS_PAY_CANCEL_URL=https://jobdri.site/credit/payment-cancel TOSS_PAY_RESULT_CALLBACK_URL=https://api.jobdri.site/api/payments/toss/callback -PAYMENT_COUPON_CODE=ABCD-EFGH-IJKL +PAYMENT_COUPON_CODE=ABCDEFGHIJKL AWS_REGION=ap-northeast-2 S3_BUCKET=your-s3-bucket diff --git a/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java b/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java index 2213dbe..4f85e89 100644 --- a/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java +++ b/src/main/java/com/jobdri/jobdri_api/domain/payment/dto/request/CouponRedeemRequest.java @@ -6,8 +6,8 @@ public record CouponRedeemRequest( @NotBlank(message = "couponCode는 필수입니다.") @Pattern( - regexp = "^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$", - message = "couponCode는 XXXX-XXXX-XXXX 형식이어야 합니다." + regexp = "^[A-Za-z0-9]{12}$", + message = "couponCode는 영문 또는 숫자 12자리여야 합니다." ) String couponCode ) { diff --git a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java index f07e4ef..d5cb25a 100644 --- a/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/domain/payment/service/CouponServiceTest.java @@ -9,6 +9,7 @@ import com.jobdri.jobdri_api.domain.user.repository.UserRepository; import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; +import jakarta.validation.Validator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -44,19 +45,22 @@ class CouponServiceTest { @Autowired private CouponRedemptionRepository couponRedemptionRepository; + @Autowired + private Validator validator; + @Test @DisplayName("유효한 쿠폰 번호를 등록하면 크레딧 1회가 충전되고 사용 이력이 저장된다") void redeem() { User user = saveUser("coupon-redeem@example.com"); - CouponRedeemResponse response = couponService.redeem(user, new CouponRedeemRequest(" test-coup-2026 ")); + CouponRedeemResponse response = couponService.redeem(user, new CouponRedeemRequest(" testcoup2026 ")); - assertThat(response.couponCode()).isEqualTo("TEST-COUP-2026"); + assertThat(response.couponCode()).isEqualTo("TESTCOUP2026"); assertThat(response.creditAmount()).isEqualTo(1); assertThat(response.creditBalance()).isEqualTo(2); assertThat(response.redeemedAt()).isNotNull(); assertThat(userRepository.findById(user.getId()).orElseThrow().getCredit()).isEqualTo(2); - assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TEST-COUP-2026")).isPresent(); + assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TESTCOUP2026")).isPresent(); assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc( user.getId(), CreditTransactionType.COUPON @@ -68,7 +72,7 @@ void redeem() { void redeemThrowsWhenCouponCodeIsInvalid() { User user = saveUser("coupon-invalid@example.com"); - assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("ABCD-EFGH-IJKL"))) + assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("ABCDEFGHIJKM"))) .isInstanceOf(GeneralException.class) .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_INVALID); @@ -78,9 +82,9 @@ void redeemThrowsWhenCouponCodeIsInvalid() { @DisplayName("같은 사용자가 동일 쿠폰을 다시 등록하면 중복 사용을 막는다") void redeemThrowsWhenCouponAlreadyRedeemed() { User user = saveUser("coupon-duplicate@example.com"); - couponService.redeem(user, new CouponRedeemRequest("TEST-COUP-2026")); + couponService.redeem(user, new CouponRedeemRequest("TESTCOUP2026")); - assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("TEST-COUP-2026"))) + assertThatThrownBy(() -> couponService.redeem(user, new CouponRedeemRequest("TESTCOUP2026"))) .isInstanceOf(GeneralException.class) .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_ALREADY_REDEEMED); @@ -96,7 +100,7 @@ void redeemThrowsWhenCouponAlreadyRedeemed() { @DisplayName("동일 쿠폰 등록 요청이 동시에 들어와도 한 번만 충전한다") void redeemConcurrentlyChargesOnlyOnce() throws Exception { User user = saveUser("coupon-concurrent@example.com"); - CouponRedeemRequest request = new CouponRedeemRequest("TEST-COUP-2026"); + CouponRedeemRequest request = new CouponRedeemRequest("TESTCOUP2026"); List results = runConcurrently(2, () -> { try { @@ -115,13 +119,21 @@ void redeemConcurrentlyChargesOnlyOnce() throws Exception { .extracting("code") .isEqualTo(GeneralErrorCode.COUPON_ALREADY_REDEEMED)); assertThat(userRepository.findById(user.getId()).orElseThrow().getCredit()).isEqualTo(2); - assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TEST-COUP-2026")).isPresent(); + assertThat(couponRedemptionRepository.findByUserIdAndCouponCode(user.getId(), "TESTCOUP2026")).isPresent(); assertThat(creditTransactionRepository.findAllByUserIdAndTypeOrderByCreatedAtDescIdDesc( user.getId(), CreditTransactionType.COUPON )).hasSize(1); } + @Test + @DisplayName("쿠폰 번호는 대시 없는 영문 또는 숫자 12자리 형식만 허용한다") + void validateCouponCodeFormat() { + assertThat(validator.validate(new CouponRedeemRequest("ABCDEFGHIJKL"))).isEmpty(); + assertThat(validator.validate(new CouponRedeemRequest("ABCD-EFGH-IJKL"))).isNotEmpty(); + assertThat(validator.validate(new CouponRedeemRequest("ABCDEFGHIJK"))).isNotEmpty(); + } + private User saveUser(String email) { return userRepository.save(User.signup("테스트 사용자", email, "encoded-password")); } diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 75206e4..ae8516f 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -79,7 +79,7 @@ job-posting: payment: coupon: - code: TEST-COUP-2026 + code: TESTCOUP2026 toss: client-key: test-toss-client-key secret-key: test-toss-secret-key