diff --git a/src/main/java/com/swyp/picke/domain/reward/controller/AdMobRewardController.java b/src/main/java/com/swyp/picke/domain/reward/controller/AdMobRewardController.java index 88bb50e0..821eeddf 100644 --- a/src/main/java/com/swyp/picke/domain/reward/controller/AdMobRewardController.java +++ b/src/main/java/com/swyp/picke/domain/reward/controller/AdMobRewardController.java @@ -31,10 +31,18 @@ public ApiResponse handleAdMobReward( AdMobRewardRequest request) { log.info("AdMob SSV 콜백 수신: transaction_id={}", request.transaction_id()); - // 서비스에서 "OK" 또는 "Already Processed" 수신 - String status = rewardService.processReward(request); + // AdMob SSV 스펙: 검증 실패 포함 모든 경우에 200 반환 (비정상 응답 시 구글이 재시도) + if (request.transaction_id() == null || request.signature() == null || request.key_id() == null) { + log.info("AdMob URL 검증 요청 수신 - 200 반환"); + return ApiResponse.onSuccess(AdMobRewardResponse.from("OK")); + } - // DTO로 감싸서 반환 (명세서의 data { "reward_status": "..." } 구조 완성) - return ApiResponse.onSuccess(AdMobRewardResponse.from(status)); + try { + String status = rewardService.processReward(request); + return ApiResponse.onSuccess(AdMobRewardResponse.from(status)); + } catch (Exception e) { + log.warn("[AdMob] 처리 실패 - 보상 미지급 후 200 반환: transaction_id={}, reason={}", request.transaction_id(), e.getMessage()); + return ApiResponse.onSuccess(AdMobRewardResponse.from("OK")); + } } } diff --git a/src/main/java/com/swyp/picke/domain/reward/dto/request/AdMobRewardRequest.java b/src/main/java/com/swyp/picke/domain/reward/dto/request/AdMobRewardRequest.java index af4b8716..7de12be0 100644 --- a/src/main/java/com/swyp/picke/domain/reward/dto/request/AdMobRewardRequest.java +++ b/src/main/java/com/swyp/picke/domain/reward/dto/request/AdMobRewardRequest.java @@ -23,14 +23,14 @@ public record AdMobRewardRequest( ) { public AdMobRewardRequest( @RequestParam(value = "ad_network", required = false) String ad_network, - @RequestParam("ad_unit") String ad_unit, + @RequestParam(value = "ad_unit", required = false) String ad_unit, @RequestParam(value = "custom_data", required = false) String custom_data, - @RequestParam("reward_amount") int reward_amount, - @RequestParam("reward_item") String reward_item, - @RequestParam("timestamp") long timestamp, - @RequestParam("transaction_id") String transaction_id, - @RequestParam("signature") String signature, - @RequestParam("key_id") String key_id, + @RequestParam(value = "reward_amount", required = false, defaultValue = "0") int reward_amount, + @RequestParam(value = "reward_item", required = false) String reward_item, + @RequestParam(value = "timestamp", required = false, defaultValue = "0") long timestamp, + @RequestParam(value = "transaction_id", required = false) String transaction_id, + @RequestParam(value = "signature", required = false) String signature, + @RequestParam(value = "key_id", required = false) String key_id, @RequestParam(value = "user_id", required = false) String user_id ) { this.ad_network = ad_network; diff --git a/src/main/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceImpl.java b/src/main/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceImpl.java index b77bb946..bbb0f93d 100644 --- a/src/main/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceImpl.java +++ b/src/main/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceImpl.java @@ -7,9 +7,10 @@ import com.swyp.picke.domain.user.entity.User; import com.swyp.picke.domain.user.enums.CreditType; import com.swyp.picke.domain.user.service.CreditService; -import com.swyp.picke.domain.user.service.UserService; // // 1. UserService 임포트 +import com.swyp.picke.domain.user.service.UserService; import com.swyp.picke.global.common.exception.CustomException; import com.swyp.picke.global.common.exception.ErrorCode; +import com.swyp.picke.global.config.AdMobConfig; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,8 +25,9 @@ public class AdMobRewardServiceImpl implements AdMobRewardService { private final RewardedAdsVerifier rewardedAdsVerifier; private final AdRewardHistoryRepository adRewardHistoryRepository; - private final UserService userService; // // 2. UserRepository 대신 UserService 사용 (태그 조회 로직 집중) + private final UserService userService; private final CreditService creditService; + private final AdMobConfig adMobConfig; @Override @Transactional @@ -33,24 +35,30 @@ public String processReward(AdMobRewardRequest request) { log.info("[AdMob] SSV 처리 시작: transaction_id={}, userTag={}, reward_amount={}, ad_unit={}", request.transaction_id(), request.getUserTag(), request.reward_amount(), request.ad_unit()); - // 1. 서명 검증 (공식 파라미터 기반) - /*if (!verifyAdMobSignature(request)) { + // 1. ad_unit 유효성 검사 + if (!adMobConfig.getAllowedUnitIds().contains(request.ad_unit())) { + log.warn("[AdMob] 허용되지 않은 ad_unit: {}", request.ad_unit()); + return "OK"; + } + + // 2. 서명 검증 (공식 파라미터 기반) + if (!verifyAdMobSignature(request)) { log.warn("[AdMob] 서명 검증 실패: transaction_id={}", request.transaction_id()); - throw new CustomException(ErrorCode.REWARD_INVALID_SIGNATURE); - }*/ + return "OK"; + } - // 2. 중복 처리 방지 + // 3. 중복 처리 방지 if (adRewardHistoryRepository.existsByTransactionId(request.transaction_id())) { log.info("[AdMob] 이미 처리된 요청: transaction_id={}", request.transaction_id()); return "Already Processed"; } - // 3. 유저 확인 (custom_data → user_id 순으로 조회) + // 4. 유저 확인 (custom_data → user_id 순으로 조회) log.info("[AdMob] 유저 조회 시도: userTag={}", request.getUserTag()); User user = userService.findByUserTag(request.getUserTag()); log.info("[AdMob] 유저 확인 완료: userId={}", user.getId()); - // 4. 보상 이력 저장 후 즉시 id 확보 (saveAndFlush) + // 5. 보상 이력 저장 후 즉시 id 확보 (saveAndFlush) AdRewardHistory history = AdRewardHistory.builder() .transactionId(request.transaction_id()) .user(user) @@ -60,7 +68,7 @@ public String processReward(AdMobRewardRequest request) { adRewardHistoryRepository.saveAndFlush(history); log.info("[AdMob] 보상 이력 저장 완료: historyId={}", history.getId()); - // 5. 크레딧 적립 (history.getId()를 referenceId로 사용해 unique 충돌 방지) + // 6. 크레딧 적립 (history.getId()를 referenceId로 사용해 unique 충돌 방지) creditService.addCredit(user.getId(), CreditType.FREE_CHARGE, request.reward_amount(), history.getId()); log.info("[AdMob] 포인트 적립 완료: userId={}, amount={}, historyId={}", user.getId(), request.reward_amount(), history.getId()); diff --git a/src/main/java/com/swyp/picke/global/config/AdMobConfig.java b/src/main/java/com/swyp/picke/global/config/AdMobConfig.java index 59f0d842..cbcb2225 100644 --- a/src/main/java/com/swyp/picke/global/config/AdMobConfig.java +++ b/src/main/java/com/swyp/picke/global/config/AdMobConfig.java @@ -1,12 +1,27 @@ package com.swyp.picke.global.config; import com.google.crypto.tink.apps.rewardedads.RewardedAdsVerifier; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + +@Getter @Configuration public class AdMobConfig { + @Value("${admob.reward.unit-id.ios}") + private String iosRewardUnitId; + + @Value("${admob.reward.unit-id.android}") + private String androidRewardUnitId; + + public List getAllowedUnitIds() { + return List.of(iosRewardUnitId, androidRewardUnitId); + } + @Bean public RewardedAdsVerifier rewardedAdsVerifier() { try { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 447d5f29..e2671442 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -76,7 +76,9 @@ oauth: admob: app-id: ${ADMOB_APP_ID} reward: - unit-id: ${ADMOB_REWARD_UNIT_ID} + unit-id: + ios: ${ADMOB_REWARD_UNIT_ID_IOS} + android: ${ADMOB_REWARD_UNIT_ID_ANDROID} openai: api-key: ${OPENAI_API_KEY} diff --git a/src/test/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceTest.java b/src/test/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceTest.java index b0d7faa3..016a5024 100644 --- a/src/test/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/reward/service/AdMobRewardServiceTest.java @@ -18,6 +18,7 @@ import com.swyp.picke.domain.user.enums.UserStatus; import com.swyp.picke.domain.user.service.CreditService; import com.swyp.picke.domain.user.service.UserService; +import com.swyp.picke.global.config.AdMobConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +27,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; +import java.util.List; + +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.doAnswer; + @ExtendWith(MockitoExtension.class) class AdMobRewardServiceTest { @@ -44,6 +50,9 @@ class AdMobRewardServiceTest { @Mock private CreditService creditService; + @Mock + private AdMobConfig adMobConfig; + @Test @DisplayName("// 1. 정상적인 광고 시청 시 보상 이력이 저장되고 크레딧이 적립된다.") void processReward_Success() throws Exception { @@ -58,6 +67,19 @@ void processReward_Success() throws Exception { .build(); ReflectionTestUtils.setField(mockUser, "id", 1L); + // ad_unit 허용 목록 Mock + given(adMobConfig.getAllowedUnitIds()).willReturn(List.of("ca-app-pub-3940256099942544/5224354917")); + + // 서명 검증 Mock (검증 통과) + willDoNothing().given(rewardedAdsVerifier).verify(org.mockito.ArgumentMatchers.anyString()); + + // saveAndFlush 시 ID 세팅 Mock + doAnswer(invocation -> { + AdRewardHistory history = invocation.getArgument(0); + ReflectionTestUtils.setField(history, "id", 1L); + return history; + }).when(adRewardHistoryRepository).saveAndFlush(any(AdRewardHistory.class)); + // // 1. 중복 체크 Mock given(adRewardHistoryRepository.existsByTransactionId(request.transaction_id())).willReturn(false); @@ -76,7 +98,7 @@ void processReward_Success() throws Exception { // // 4. 호출 검증 verify(creditService, times(1)).addCredit(eq(1L), eq(CreditType.FREE_CHARGE), eq(100), anyLong()); - verify(adRewardHistoryRepository, times(1)).save(any(AdRewardHistory.class)); + verify(adRewardHistoryRepository, times(1)).saveAndFlush(any(AdRewardHistory.class)); verify(userService, times(1)).findByUserTag("pique-1cc4a030"); }