Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ public ApiResponse<AdMobRewardResponse> 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"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,33 +25,40 @@ 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
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)
Expand All @@ -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());
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/swyp/picke/global/config/AdMobConfig.java
Original file line number Diff line number Diff line change
@@ -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<String> getAllowedUnitIds() {
return List.of(iosRewardUnitId, androidRewardUnitId);
}

@Bean
public RewardedAdsVerifier rewardedAdsVerifier() {
try {
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -44,6 +50,9 @@ class AdMobRewardServiceTest {
@Mock
private CreditService creditService;

@Mock
private AdMobConfig adMobConfig;

@Test
@DisplayName("// 1. 정상적인 광고 시청 시 보상 이력이 저장되고 크레딧이 적립된다.")
void processReward_Success() throws Exception {
Expand All @@ -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);

Expand All @@ -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");
}

Expand Down
Loading