diff --git a/src/main/java/com/swyp/picke/domain/ad/service/AdQueryService.java b/src/main/java/com/swyp/picke/domain/ad/service/AdQueryService.java index c01b429..5f04b72 100644 --- a/src/main/java/com/swyp/picke/domain/ad/service/AdQueryService.java +++ b/src/main/java/com/swyp/picke/domain/ad/service/AdQueryService.java @@ -3,6 +3,8 @@ import com.swyp.picke.domain.ad.dto.response.AdResponse; import com.swyp.picke.domain.ad.entity.AdCreative; import com.swyp.picke.domain.ad.enums.AdSlotCode; +import com.swyp.picke.domain.ad.enums.AdNetwork; +import com.swyp.picke.domain.ad.enums.AdSource; import com.swyp.picke.domain.ad.enums.AdStatus; import com.swyp.picke.domain.ad.enums.AdTargetOs; import com.swyp.picke.domain.ad.repository.AdCreativeRepository; @@ -46,6 +48,7 @@ public List findServableAds(AdSlotCode slot, AdTargetOs os, int size List candidates = adCreativeRepository.findAllBySlotAndStatus(slot, AdStatus.ACTIVE).stream() .filter(creative -> creative.isServable(now)) + .filter(this::isShoppingOrManualAd) .filter(creative -> creative.getTargetOs().matches(requested)) .toList(); @@ -64,6 +67,7 @@ public List findLandingAds() { return adCreativeRepository.findAllByStatusOrderByIdDesc(AdStatus.ACTIVE).stream() .filter(creative -> creative.isServable(now)) + .filter(this::isShoppingOrManualAd) .map(creative -> AdResponse.of(creative, buildClickUrl(creative))) .toList(); } @@ -101,6 +105,14 @@ private void increaseImpression(ImpressionTarget target, LocalDate today) { } } + /** 동기화된 애드픽 설치·가입 캠페인은 제외한다. 쇼핑 식별자는 수집 시 생성한 sh + 해시다. */ + private boolean isShoppingOrManualAd(AdCreative creative) { + if (creative.getNetwork() != AdNetwork.ADPICK || creative.getSource() != AdSource.ADPICK_API) { + return true; + } + return creative.getExternalId() != null && creative.getExternalId().matches("sh[0-9a-f]{10}"); + } + private String buildClickUrl(AdCreative creative) { return adBaseUrl + "/c/" + creative.getCode(); } diff --git a/src/test/java/com/swyp/picke/domain/ad/service/AdQueryServiceTest.java b/src/test/java/com/swyp/picke/domain/ad/service/AdQueryServiceTest.java index 765cef9..ace9f9a 100644 --- a/src/test/java/com/swyp/picke/domain/ad/service/AdQueryServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/ad/service/AdQueryServiceTest.java @@ -5,6 +5,7 @@ import com.swyp.picke.domain.ad.enums.AdNetwork; import com.swyp.picke.domain.ad.enums.AdSlotCode; import com.swyp.picke.domain.ad.enums.AdStatus; +import com.swyp.picke.domain.ad.enums.AdSource; import com.swyp.picke.domain.ad.enums.AdTargetOs; import com.swyp.picke.domain.ad.repository.AdCreativeRepository; import java.time.LocalDate; @@ -147,6 +148,44 @@ void findServableAds_filtersByTargetOs() { assertThat(result).extracting(AdResponse::code).containsExactlyInAnyOrder("ios00001", "all00001"); } + private AdCreative syncedAdpick(String code, String externalId) { + AdCreative ad = creative(code, AdStatus.ACTIVE, 100, null, null); + ReflectionTestUtils.setField(ad, "network", AdNetwork.ADPICK); + ReflectionTestUtils.setField(ad, "source", AdSource.ADPICK_API); + ReflectionTestUtils.setField(ad, "externalId", externalId); + return ad; + } + + @Test + void excludesInstallCampaignsEvenWhenShoppingCannotFillRequestedSize() { + var shopping = syncedAdpick("shop0001", "sh012345abcd"); + var install = syncedAdpick("app00001", "ukjd9nru"); + var signup = syncedAdpick("join0001", "hmvdx3t3"); + var coupang = creative("coup0001", AdStatus.ACTIVE, 1, null, null); + when(adCreativeRepository.findAllBySlotAndStatus(AdSlotCode.HOME_FEED, AdStatus.ACTIVE)) + .thenReturn(List.of(shopping, install, signup, coupang)); + + assertThat(adQueryService.findServableAds(AdSlotCode.HOME_FEED, AdTargetOs.ALL, 10)) + .extracting(AdResponse::code).containsExactlyInAnyOrder("shop0001", "coup0001"); + } + + @Test + void doesNotFallbackToInstallAdsWhenThereAreNoShoppingProducts() { + when(adCreativeRepository.findAllBySlotAndStatus(AdSlotCode.HOME_FEED, AdStatus.ACTIVE)) + .thenReturn(List.of(syncedAdpick("app00001", "ukjd9nru"), + syncedAdpick("bad00001", null))); + assertThat(adQueryService.findServableAds(AdSlotCode.HOME_FEED, AdTargetOs.ANDROID, 10)).isEmpty(); + } + + @Test + void landingAlsoExcludesInstallCampaigns() { + when(adCreativeRepository.findAllByStatusOrderByIdDesc(AdStatus.ACTIVE)) + .thenReturn(List.of(syncedAdpick("shop0001", "sh012345abcd"), + syncedAdpick("app00001", "ukjd9nru"))); + assertThat(adQueryService.findLandingAds()).extracting(AdResponse::code) + .containsExactly("shop0001"); + } + @Test @DisplayName("노출 집계는 같은 날 반복 호출하면 기존 행을 누적한다") void recordImpressions_incrementsExistingRow() {