Skip to content

Commit 27e7b79

Browse files
authored
fix: 사용자 광고에서 설치·가입 캠페인 제외 (#463)
사용자 광고 API와 공개 광고 랜딩에서 애드픽 동기화 설치·가입형 캠페인을 제외합니다. 쇼핑·핫딜 상품과 쿠팡 및 수동 등록 광고는 기존 조건으로 제공하고, 쇼핑 재고가 부족해도 설치형 캠페인으로 채우지 않습니다. 기존 소재와 성과 기록은 보존합니다. 검증: 광고 조회·동기화 테스트 통과. 설치/가입형 제외, 재고 부족 시 빈 목록, 공개 랜딩 제외 회귀 테스트를 추가했습니다.
1 parent 73a4214 commit 27e7b79

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

‎src/main/java/com/swyp/picke/domain/ad/service/AdQueryService.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.swyp.picke.domain.ad.dto.response.AdResponse;
44
import com.swyp.picke.domain.ad.entity.AdCreative;
55
import com.swyp.picke.domain.ad.enums.AdSlotCode;
6+
import com.swyp.picke.domain.ad.enums.AdNetwork;
7+
import com.swyp.picke.domain.ad.enums.AdSource;
68
import com.swyp.picke.domain.ad.enums.AdStatus;
79
import com.swyp.picke.domain.ad.enums.AdTargetOs;
810
import com.swyp.picke.domain.ad.repository.AdCreativeRepository;
@@ -46,6 +48,7 @@ public List<AdResponse> findServableAds(AdSlotCode slot, AdTargetOs os, int size
4648

4749
List<AdCreative> candidates = adCreativeRepository.findAllBySlotAndStatus(slot, AdStatus.ACTIVE).stream()
4850
.filter(creative -> creative.isServable(now))
51+
.filter(this::isShoppingOrManualAd)
4952
.filter(creative -> creative.getTargetOs().matches(requested))
5053
.toList();
5154

@@ -64,6 +67,7 @@ public List<AdResponse> findLandingAds() {
6467

6568
return adCreativeRepository.findAllByStatusOrderByIdDesc(AdStatus.ACTIVE).stream()
6669
.filter(creative -> creative.isServable(now))
70+
.filter(this::isShoppingOrManualAd)
6771
.map(creative -> AdResponse.of(creative, buildClickUrl(creative)))
6872
.toList();
6973
}
@@ -101,6 +105,14 @@ private void increaseImpression(ImpressionTarget target, LocalDate today) {
101105
}
102106
}
103107

108+
/** 동기화된 애드픽 설치·가입 캠페인은 제외한다. 쇼핑 식별자는 수집 시 생성한 sh + 해시다. */
109+
private boolean isShoppingOrManualAd(AdCreative creative) {
110+
if (creative.getNetwork() != AdNetwork.ADPICK || creative.getSource() != AdSource.ADPICK_API) {
111+
return true;
112+
}
113+
return creative.getExternalId() != null && creative.getExternalId().matches("sh[0-9a-f]{10}");
114+
}
115+
104116
private String buildClickUrl(AdCreative creative) {
105117
return adBaseUrl + "/c/" + creative.getCode();
106118
}

‎src/test/java/com/swyp/picke/domain/ad/service/AdQueryServiceTest.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.swyp.picke.domain.ad.enums.AdNetwork;
66
import com.swyp.picke.domain.ad.enums.AdSlotCode;
77
import com.swyp.picke.domain.ad.enums.AdStatus;
8+
import com.swyp.picke.domain.ad.enums.AdSource;
89
import com.swyp.picke.domain.ad.enums.AdTargetOs;
910
import com.swyp.picke.domain.ad.repository.AdCreativeRepository;
1011
import java.time.LocalDate;
@@ -147,6 +148,44 @@ void findServableAds_filtersByTargetOs() {
147148
assertThat(result).extracting(AdResponse::code).containsExactlyInAnyOrder("ios00001", "all00001");
148149
}
149150

151+
private AdCreative syncedAdpick(String code, String externalId) {
152+
AdCreative ad = creative(code, AdStatus.ACTIVE, 100, null, null);
153+
ReflectionTestUtils.setField(ad, "network", AdNetwork.ADPICK);
154+
ReflectionTestUtils.setField(ad, "source", AdSource.ADPICK_API);
155+
ReflectionTestUtils.setField(ad, "externalId", externalId);
156+
return ad;
157+
}
158+
159+
@Test
160+
void excludesInstallCampaignsEvenWhenShoppingCannotFillRequestedSize() {
161+
var shopping = syncedAdpick("shop0001", "sh012345abcd");
162+
var install = syncedAdpick("app00001", "ukjd9nru");
163+
var signup = syncedAdpick("join0001", "hmvdx3t3");
164+
var coupang = creative("coup0001", AdStatus.ACTIVE, 1, null, null);
165+
when(adCreativeRepository.findAllBySlotAndStatus(AdSlotCode.HOME_FEED, AdStatus.ACTIVE))
166+
.thenReturn(List.of(shopping, install, signup, coupang));
167+
168+
assertThat(adQueryService.findServableAds(AdSlotCode.HOME_FEED, AdTargetOs.ALL, 10))
169+
.extracting(AdResponse::code).containsExactlyInAnyOrder("shop0001", "coup0001");
170+
}
171+
172+
@Test
173+
void doesNotFallbackToInstallAdsWhenThereAreNoShoppingProducts() {
174+
when(adCreativeRepository.findAllBySlotAndStatus(AdSlotCode.HOME_FEED, AdStatus.ACTIVE))
175+
.thenReturn(List.of(syncedAdpick("app00001", "ukjd9nru"),
176+
syncedAdpick("bad00001", null)));
177+
assertThat(adQueryService.findServableAds(AdSlotCode.HOME_FEED, AdTargetOs.ANDROID, 10)).isEmpty();
178+
}
179+
180+
@Test
181+
void landingAlsoExcludesInstallCampaigns() {
182+
when(adCreativeRepository.findAllByStatusOrderByIdDesc(AdStatus.ACTIVE))
183+
.thenReturn(List.of(syncedAdpick("shop0001", "sh012345abcd"),
184+
syncedAdpick("app00001", "ukjd9nru")));
185+
assertThat(adQueryService.findLandingAds()).extracting(AdResponse::code)
186+
.containsExactly("shop0001");
187+
}
188+
150189
@Test
151190
@DisplayName("노출 집계는 같은 날 반복 호출하면 기존 행을 누적한다")
152191
void recordImpressions_incrementsExistingRow() {

0 commit comments

Comments
 (0)