Skip to content

[Feat] ad.picke.store 제휴 광고(쿠팡 파트너스·애드픽) 도메인 및 API - #435

Closed
Roy-wonji wants to merge 5 commits into
devfrom
feat/ad-affiliate
Closed

[Feat] ad.picke.store 제휴 광고(쿠팡 파트너스·애드픽) 도메인 및 API#435
Roy-wonji wants to merge 5 commits into
devfrom
feat/ad-affiliate

Conversation

@Roy-wonji

Copy link
Copy Markdown

요약

앱 지면에 제휴 광고 배너를 네이티브로 노출하고, 탭하면 외부 브라우저로 제휴 링크에 다이렉트되는 구조를 붙였습니다. 매체는 쿠팡 파트너스와 애드픽 두 곳입니다.

설계 문서: docs/superpowers/specs/2026-09-01-ad-picke-store-design.md

동작

  • 앱이 지면 코드로 소재를 조회해 네이티브로 렌더 (GET /api/v1/ads?slot=)
  • 실제로 화면에 그려진 시점에 노출 집계 (POST /api/v1/ads/impressions)
  • 탭하면 GET /c/{code} → 매체별 추적 파라미터 병합 후 302, 클릭은 비동기 적재
  • 관리자에서 소재 CRUD와 노출/클릭/CTR, 클릭 내역 조회

주요 결정

지면은 테이블이 아니라 enum입니다. 앱이 그릴 줄 모르는 지면을 어드민에서 만들어봐야 아무 일도 일어나지 않습니다. 지면 추가는 어차피 앱 배포와 묶입니다.

노출은 raw 로그로 쌓지 않습니다. 배너가 스크롤에 걸릴 때마다 행이 생기면 금방 수천만 건이 됩니다. 일별 카운터 upsert로 CTR을 뽑는 데 충분합니다.

클릭 로그는 익명입니다. /c/{code}는 외부 브라우저에서 열려 Authorization 헤더가 없습니다. 공개 URL에 사용자 식별자를 실으면 열거 공격과 프라이버시 문제가 생기고, 지면별 CTR과 정산 대조에는 userId가 필요 없습니다.

쿠팡은 subId를 병합하고 애드픽은 pass-through입니다. 쿠팡은 subId로 지면별 실매출이 파트너스 리포트에서 갈립니다. 애드픽은 서브아이디 파라미터 규격을 아직 확인하지 못해 원본 링크를 그대로 넘깁니다. 지면별 성과 분리만 안 될 뿐 노출·클릭·리다이렉트는 정상 동작하고, 규격이 확인되면 AdpickLinkBuilder에 파라미터명만 채우면 됩니다.

곁들여 고친 것

JwtFilter가 SecurityConfig보다 먼저 돌면서 토큰이 없으면 무조건 401을 던지고 있었습니다. 게다가 isWhitelistedstartsWith 매칭이라 "/"를 넣으면 전체 인증이 무력화됩니다. 정확히 일치할 때만 통과하는 EXACT_WHITELIST를 분리해 //error를 넣었습니다.

/error가 빠져 있던 탓에 존재하지 않는 모든 경로가 404 대신 401로 나오고 있었습니다. 실제로 https://ad.picke.store/가 인증 오류 JSON을 반환하던 원인입니다.

Swagger

광고 API는 3. 광고 API 그룹으로 분리했습니다. 기존 사용자 그룹은 FE_USED_OPERATIONS 화이트리스트로 필터링되므로 거기 넣으면 어차피 보이지 않습니다.

배포 전 확인

  • docs/db/20260901_create_ad_tables.sql 실행 필요
  • 지면(AdSlotCode) 목록은 앱팀 확정 후 채워야 합니다. 현재 HOME_FEED, BATTLE_RESULT_BOTTOM 두 개
  • 애드픽 파트너 가입 및 이용정책상 자체 앱 배너 노출 허용 여부 확인 필요

테스트

광고 도메인 테스트 통과 (링크 병합, 게재 기간 필터, 가중 로테이션, 노출 upsert, 만료 코드 처리).

전체 스위트에서 ScenarioAudioPipelineServiceTestScenarioServiceImplTest 2건이 실패하지만, 이 브랜치 변경을 걷어낸 상태에서도 동일하게 실패하는 기존 이슈입니다.

- ad 도메인 신설: 소재(AdCreative), 클릭 로그, 일별 노출 집계
- 앱 API: GET /api/v1/ads (지면별 가중 로테이션), POST /api/v1/ads/impressions
- 클릭 리다이렉트: GET /c/{code} -> 매체별 추적 파라미터 병합 후 302, 클릭 비동기 적재
- 관리자 API: 소재 CRUD, 소재별 노출/클릭/CTR, 클릭 내역 목록
- 쿠팡은 subId를 병합해 지면별 성과를 분리하고, 애드픽은 규격 확인 전까지 pass-through
- ad.picke.store 루트 공개 지면 추가 (매체 심사에서 접속 확인 대상)
- Swagger "3. 광고 API" 그룹으로 분리
- JwtFilter에 정확 일치 화이트리스트를 분리해 루트·/error가 401로 막히던 문제 해결
- 지면을 iOS 실제 화면 기준 5개로 확장하고 CPI 허용 여부를 지면 속성으로 표기
- 애드픽 서브아이디 파라미터를 picke.ad.adpick.sub-id-param 설정값으로 분리
  규격 확인 시 배포 없이 환경변수만 채우면 지면별 성과 분리가 켜진다
- 소재 등록·수정 시 제휴 링크의 lptag를 coupang.partners.id와 대조
  단축 링크에는 lptag가 없으므로 파라미터가 있을 때만 검사한다
- 링크 병합 로직을 AffiliateLinks로 추출해 매체별 빌더가 공유
- 마이그레이션 SQL은 참고용임을 명시 (운영은 ddl-auto=update로 자동 생성)
BattleVoteServiceImpl은 "오늘"을 LocalDate.now(KST)로 판단하는데
테스트는 시스템 기본 시간대의 LocalDate.now()를 썼다.

UTC 러너에서는 15:00 UTC(=KST 자정) 이후로 두 값이 하루씩 어긋나
오늘 배틀이 어제 배틀로 취급되면서 크레딧 차감 분기가 반대로 탔다.
14:42 UTC 실행은 통과하고 15:25 UTC 실행은 실패한 이유다.

테스트도 KST 기준으로 맞춘다. 광고 작업과는 무관한 기존 결함이지만 CI를 막고 있어 함께 고친다.
진입점에서 TimeZone.setDefault(Asia/Seoul)을 하고 있어 런타임 동작은 문제없었지만,
기본 시간대 설정에 기대면 일별 노출 버킷이 엉뚱한 시각에 갈릴 수 있다.
BattleVoteServiceImpl과 같은 방식으로 KST를 명시한다.
@si-zero

si-zero commented Sep 2, 2026

Copy link
Copy Markdown
Member

기능 자체는 잘 보이는데 몇 가지 짚고 갈 게 있어서 정리해봤어요.

1. enum 컬럼에 CHECK 제약조건이 없음
network/slot/status@Enumerated(STRING) + VARCHAR로 매핑은 맞게 했는데, DB 레벨 CHECK 제약이 하나도 없네요. notifications 테이블 때 CHECK 제약 갱신 빼먹어서 발송이 조용히 실패했던 적 있었는데(#378 관련), 똑같은 리스크 안고 가는 거라 이건 확인해보면 좋을 것 같아요.

2. docs/erd/에 파일 없음
ad_creatives, ad_click_logs, ad_impression_daily 3개 테이블 새로 생겼는데 ERD 파일이 없어요. 다른 도메인들은 다 있으니 이것도 만들어주세요.

3. 주석에 <p> 태그
Javadoc 문법 자체는 틀린 게 아닌데, 이 repo 다른 코드들은 이런 식으로 HTML 태그 넣은 멀티 문단 주석 안 쓰거든요. 스타일 통일 좀 부탁드려요.

4. 이슈/PR/브랜치 템플릿 안 지킴
연결된 이슈 없고, 라벨도 없고, PR 제목에 이슈번호 접두사도 없고, 브랜치명도 feat/ad-affiliatefeat/#이슈번호 컨벤션이랑 다르네요. PR 본문도 템플릿 양식(연관이슈/체크리스트) 아니고 자유 서술형이고요. 이슈부터 만들고 다시 정리해주시면 좋겠어요.

5. .DS_Store 7개가 그대로 커밋됨
docs/.DS_Store, src/.DS_Store 등등... .gitignore.DS_Store 자체가 없어서 안 걸러진 것 같아요. .gitignore에 추가하고 이 파일들 빼주세요.

6. 쿠팡 파트너스 ID가 하드코딩된 기본값으로 들어가있음
application.ymlid: ${COUPANG_PARTNERS_ID:AF6830373}로 실제 파트너스 ID가 fallback으로 박혀있는데, 다른 시크릿들(카카오/구글/애플 client-id)은 다 fallback 없이 env var 필수로 되어있어요. 여기만 다르게 갈 이유가 있는지, 아니면 그냥 fallback 빼고 env var 필수로 맞춰주세요.

7. picke.ad.host 설정값이 안 쓰이는 것 같은데, 확인 한 번 부탁드려요
application.yml에 추가는 됐는데 코드 쪽에서 참조하는 곳을 못 찾았어요. 혹시 나중에 쓸 계획이 있으신 거면 그대로 둬도 될 것 같고, 아니면 정리하면 좋을 것 같아요.

8. generateUniqueCode() 쪽 미세한 race condition
existsByCode 체크하고 나중에 save()하는 구조라 동시 요청 시 이론상 유니크 제약 위반 가능성 있어요. 코드 공간이 32^8이라 확률은 거의 없긴 한데, 그냥 참고삼아 남겨요. (심각도 낮음, 블로커는 아님)

9. startsAt/endsAt 역전 검증 없음
종료일을 시작일보다 빠르게 넣어도 에러 안 나고 그냥 영원히 게재 안 되는 소재가 돼요. 크게 문제는 아닌데 어드민이 실수로 넣었을 때 알아채기 어려울 것 같아서, @AssertTrue 같은 걸로 검증 하나 추가하면 좋을 것 같아요.


전체적으로 기능 설계나 로직은 꼼꼼하게 잘 짜신 것 같아요! 고생하셨습니당

@si-zero si-zero left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제로 깃허브에 올라가면 안되는 파일들이 보입니다. 위 리뷰 확인하셔서 수정 부탁드립니다!

@si-zero si-zero added the ✨ Feat (기능 추가) 새로운 기능을 추가할 때 사용합니다. label Sep 2, 2026
@Roy-wonji

Copy link
Copy Markdown
Author

이슈/브랜치/PR 컨벤션에 맞춰 다시 정리했습니다. #442 (fix/#440), #443 (feat/#441) 으로 대체합니다.

@Roy-wonji Roy-wonji closed this Sep 2, 2026
@Roy-wonji
Roy-wonji deleted the feat/ad-affiliate branch September 2, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feat (기능 추가) 새로운 기능을 추가할 때 사용합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants