diff --git a/src/main/java/com/swyp/picke/domain/ad/entity/AdCreative.java b/src/main/java/com/swyp/picke/domain/ad/entity/AdCreative.java index 8803f7e..2eb1a5d 100644 --- a/src/main/java/com/swyp/picke/domain/ad/entity/AdCreative.java +++ b/src/main/java/com/swyp/picke/domain/ad/entity/AdCreative.java @@ -133,7 +133,8 @@ public void update(AdNetwork network, AdSlotCode slot, String title, String subt * 어드민이 꺼둔 소재는 다시 켜지 않는다. 별도 플래그 없이 PAUSED 를 끄기 스위치로 쓴다. */ public void syncFromAdpick(String title, String subtitle, String imageUrl, String ctaText, - String landingUrl, AdTargetOs targetOs, AdSlotCode slot, boolean servable) { + String landingUrl, AdTargetOs targetOs, AdSlotCode slot, int weight, + boolean servable) { this.title = title; this.subtitle = subtitle; this.imageUrl = imageUrl; @@ -141,6 +142,7 @@ public void syncFromAdpick(String title, String subtitle, String imageUrl, Strin this.landingUrl = landingUrl; this.targetOs = targetOs; this.slot = slot; + this.weight = weight; if (this.status != AdStatus.PAUSED) { this.status = servable ? AdStatus.ACTIVE : AdStatus.DRAFT; } diff --git a/src/main/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncService.java b/src/main/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncService.java index 888d6b7..93b0f90 100644 --- a/src/main/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncService.java +++ b/src/main/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncService.java @@ -52,6 +52,16 @@ public class AdpickCampaignSyncService { @Value("${picke.ad.adpick.cta-text:설치하고 받기}") private String ctaText; + /** + * 로테이션 가중치. 쇼핑 상품을 앱 설치형보다 자주 띄우려고 나눠 둔다. + * 앱 캠페인은 7건뿐이라 같은 가중치면 지면마다 같은 소재가 반복해서 뽑힌다. + */ + @Value("${picke.ad.adpick.weight:1}") + private int campaignWeight; + + @Value("${picke.ad.adpick.shopping-weight:3}") + private int shoppingWeight; + /** * 쇼핑·핫딜 상품을 흩어 놓을 지면. * 앱 캠페인은 7건뿐이라 한 지면밖에 못 채운다. 상품 60건을 나눠 담아 나머지 지면을 채운다. @@ -127,6 +137,7 @@ private void upsertShopping(AdCreative found, String externalId, AdpickShoppingR product.buyUrl(), AdTargetOs.ALL, target, + shoppingWeight, true); return; } @@ -141,7 +152,7 @@ private void upsertShopping(AdCreative found, String externalId, AdpickShoppingR .ctaText(shoppingCtaText) .landingUrl(product.buyUrl()) .status(AdStatus.ACTIVE) - .weight(1) + .weight(shoppingWeight) .source(AdSource.ADPICK_API) .externalId(externalId) .targetOs(AdTargetOs.ALL) @@ -185,6 +196,7 @@ private void upsert(AdCreative found, AdpickCampaignResponse campaign) { campaign.trackingLink(), AdTargetOs.fromAdpick(campaign.os()), slot, + campaignWeight, campaign.hasRemaining()); return; } @@ -199,7 +211,7 @@ private void upsert(AdCreative found, AdpickCampaignResponse campaign) { .ctaText(ctaText) .landingUrl(campaign.trackingLink()) .status(campaign.hasRemaining() ? AdStatus.ACTIVE : AdStatus.DRAFT) - .weight(1) + .weight(campaignWeight) .source(AdSource.ADPICK_API) .externalId(campaign.offerId()) .targetOs(AdTargetOs.fromAdpick(campaign.os())) diff --git a/src/test/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncServiceTest.java b/src/test/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncServiceTest.java index 4415622..c61e20f 100644 --- a/src/test/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncServiceTest.java +++ b/src/test/java/com/swyp/picke/domain/ad/service/AdpickCampaignSyncServiceTest.java @@ -26,6 +26,7 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -56,6 +57,8 @@ void setUp() { ReflectionTestUtils.setField(syncService, "ctaText", CTA); ReflectionTestUtils.setField(syncService, "shoppingSlots", List.of(AdSlotCode.HOME_FEED)); ReflectionTestUtils.setField(syncService, "shoppingCtaText", "구매하러 가기"); + ReflectionTestUtils.setField(syncService, "campaignWeight", 1); + ReflectionTestUtils.setField(syncService, "shoppingWeight", 3); when(adpickCampaignClient.isConfigured()).thenReturn(true); when(adCreativeCodeGenerator.generate()).thenReturn("syn23456"); when(adCreativeRepository.findAllBySource(AdSource.ADPICK_API)).thenReturn(List.of()); @@ -236,4 +239,25 @@ void sync_skipsShoppingWhenNotConfigured() { verify(adpickShoppingClient, never()).fetchProducts(); } + + @Test + @DisplayName("쇼핑 상품은 앱 캠페인보다 높은 가중치로 담긴다. 앱 캠페인 7건이 반복 노출되지 않게 한다") + void sync_weightsShoppingHigher() { + when(adpickCampaignClient.isConfigured()).thenReturn(true); + when(adpickCampaignClient.fetchCampaigns()).thenReturn(List.of(campaign("aaaa1", "Both", 5))); + when(adpickShoppingClient.isConfigured()).thenReturn(true); + when(adpickShoppingClient.fetchProducts()).thenReturn(List.of( + product("상품", "https://adpick.co.kr/apis/goshopping.php?affid=ab2c41&offer=1&url=w"))); + + syncService.sync(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AdCreative.class); + verify(adCreativeRepository, times(2)).save(captor.capture()); + assertThat(captor.getAllValues()).extracting(AdCreative::getExternalId, AdCreative::getWeight) + .containsExactlyInAnyOrder(tuple("aaaa1", 1), + tuple(captor.getAllValues().stream() + .map(AdCreative::getExternalId) + .filter(id -> id.startsWith("sh")) + .findFirst().orElseThrow(), 3)); + } }