Conversation
BattleVoteServiceImpl은 "오늘"을 LocalDate.now(KST)로 판단하는데 테스트는 시스템 기본 시간대의 LocalDate.now()를 썼다. UTC 러너에서는 15:00 UTC(=KST 자정) 이후로 두 값이 하루씩 어긋나 오늘 배틀이 어제 배틀로 취급되면서 크레딧 차감 분기가 반대로 탔다. 같은 브랜치에서 14:42 UTC 실행은 통과하고 15:25 UTC 실행은 실패했다. 테스트도 KST 기준으로 맞춘다.
JwtFilter 가 SecurityConfig 보다 먼저 돌면서 토큰이 없으면 무조건 401 을 던진다. 그런데 isWhitelisted 가 startsWith 매칭이라 "/" 를 넣으면 전체 인증이 무력화된다. 정확히 일치할 때만 통과하는 EXACT_WHITELIST 를 분리해 "/" 와 "/error" 를 넣는다. /error 가 빠져 있던 탓에 존재하지 않는 모든 경로가 404 대신 401 로 나오고 있었다. ad.picke.store 루트가 인증 오류 JSON 을 반환하던 원인이다.
- ad 도메인 신설: 소재(AdCreative), 클릭 로그, 일별 노출 집계
- 앱 API: GET /api/v1/ads (지면별 가중 로테이션), POST /api/v1/ads/impressions
- 클릭 리다이렉트: GET /c/{code} 에서 매체별 추적 파라미터를 병합해 302, 클릭은 비동기 적재
- 관리자 API: 소재 CRUD, 소재별 노출/클릭/CTR, 클릭 내역 목록
- 쿠팡은 subId 를 병합해 지면별 성과를 분리하고,
애드픽은 규격 확인 전까지 pass-through 이며 파라미터명을 설정값으로 둔다
- 소재 등록 시 제휴 링크의 lptag 를 우리 파트너스 아이디와 대조해 남의 링크를 막는다
- enum 컬럼에 CHECK 제약을 선언하고, 제약과 enum 이 어긋나면 테스트가 먼저 깨지게 한다
- 노출 집계와 게재 기간 판단을 KST 로 고정
- ad.picke.store 루트 공개 지면 추가 (매체 심사에서 접속 확인 대상)
- Swagger "3. 광고 API" 그룹으로 분리
ERD 는 docs/erd/ad.puml, 참고 스키마는 docs/db 에 둔다. 운영은 ddl-auto=update 라 테이블은 배포 시 자동 생성되며, SQL 은 수동 관리·사후 검증용이다.
|
CI 관련 참고입니다.
#442를 먼저 머지하면 이 PR의 base가 지금 base를 로컬 검증 결과입니다. 실패 2건은 |
애드픽은 캠페인 리스트 JSON API 를 인증 없이 공개한다.
쿠팡 오픈API 와 달리 승인 절차가 없어 애드픽 소재는 사람이 등록하지 않고 주기 동기화로 채운다.
- 수집한 캠페인을 같은 ad_creatives 에 source=ADPICK_API 로 저장해
로테이션·노출 집계·클릭 추적 경로를 그대로 태운다
- 애드픽 가이드가 최대 1분 1회 이하 호출을 요구하고 실제로 연달아 호출하면 403 을 준다.
요청마다 부르지 않고 스케줄러로만 호출한다
- apRemain 이 0 이면 게재하지 않는다. 잔여가 없으면 클릭해도 전환이 잡히지 않는다
- 피드에서 사라진 캠페인은 지우지 않고 내린다. 쌓인 집계를 계속 읽을 수 있어야 한다
- PAUSED 는 동기화가 되돌리지 않는다. 별도 플래그 없이 어드민의 끄기 스위치로 쓴다
- 동기화 소재는 수정·삭제를 막고 게재 상태만 바꾸게 한다 (PATCH /{id}/status)
- apOS 로 OS 가 갈리므로 target_os 를 두고 GET /api/v1/ads 에 os 파라미터를 받는다
iOS 사용자에게 Android 캠페인을 보여주면 클릭해도 전환이 일어나지 않는다
- affId 가 비어 있으면 동기화를 건너뛴다. 가입 전에도 나머지 기능은 그대로 돈다
There was a problem hiding this comment.
짚고 갈 것
1. AdQueryService.increaseImpression 경쟁 조건 처리 (중요)
@Transactional 안에서 insert 하다 DataIntegrityViolationException 잡고 같은 트랜잭션에서 increment를 다시 부르는데, PostgreSQL은 statement 하나 터지면 그 트랜잭션이 abort 상태로 들어가서 뒤따르는 쿼리가 전부 current transaction is aborted 로 실패함. H2에서만 통과하고 운영에선 복구 increment가 동작 안 함. 게다가 increaseImpression이 예외로 빠져나가면 호출부(recordImpressions)가 UnexpectedRollbackException 맞아서 배치 전체가 날아감.
- insert를
Propagation.REQUIRES_NEW서브트랜잭션으로 빼서 실패를 격리하거나 - PG 네이티브
INSERT ... ON CONFLICT DO UPDATE+ H2용 fallback 분기 - 아니면 재시도 자체를 트랜잭션 밖(서비스 상위)에서
셋 중 하나로 가야 함. 지금 구조는 동시 노출이 실제로 겹치는 순간 깨짐.
2. 브랜치가 dev보다 뒤처져 있음 (중요)
base가 cba91cb인데 그 뒤로 dev에 #444(S3→Railway)가 JwtFilter / SecurityConfig / application.yml 을 건드렸음. rebase 하면 충돌남. 특히:
- dev가 화이트리스트에 추가한
/webjars,/webjars/**가 이 PR엔 없음 → 리베이스하면서 빠뜨리지 말 것 - dev가 정리한
/swagger-ui,/v3/api-docs줄 trailing whitespace가 이 PR에서 다시 살아남
#442 머지 후 base가 dev로 바뀌면 여기서 걸릴 거라 미리 정리해두는 게 나음.
3. POST /api/v1/ads/impressions 무인증
노출 집계가 인증 없이 열려 있어서 아무나 curl 반복으로 노출수 부풀릴 수 있음. 익명 설계라 감수하는 거면 OK인데, CTR이 정산 대조용이라 이 수치가 조작 가능하다는 건 인지하고 가면 좋겠음. 최소한 codes 개수 상한 정도는.
4. AdClickController.resolveClientIp
X-Forwarded-For 첫 값을 그대로 신뢰함 → 클라이언트가 헤더 넣으면 스푸핑됨. ip_hash가 정밀 어뷰징 탐지용은 아니니 지금은 괜찮은데, 나중에 본인클릭 필터링 붙일 거면 프록시 홉 수 기준으로 잘라야 함.
5. 잘못된 landing URL 방어
AffiliateLinks.merge가 build(true)(인코딩 완료 가정)라, 어드민이 인코딩 안 된 URL 넣으면 등록은 되고 클릭 시점에 500 남. validateCoupangOwnership은 COUPANG + 파트너스ID 있을 때만 URL 파싱하니까 애드픽 수동 소재나 단축링크는 검증을 안 탐. 등록 시점에 landingUrl 한 번 파싱해서 형식 검증하는 게 안전함.
6. 애드핏 계획 문서 섞임
docs/superpowers/plans/2026-07-16-adfit-reward.md(1119줄) + specs 2개가 이번 쿠팡/애드픽 PR에 같이 들어와 있음. 이건 리워드 광고 쪽 계획이라 이 PR이랑 무관한데, 의도한 거 아니면 빼는 게 맞음.
리뷰 요청 답변
1. CHECK 제약 + AdCheckConstraintTest 조합 — 합리적임. ddl-auto=update가 제약 갱신 안 하는 것도 SQL 파일 주석에 명시돼 있고, 어긋나면 CI에서 먼저 깨주니까 안전망으로 충분함. 다만 enum 추가할 때 수동 ALTER 잊을 위험은 여전히 사람한테 남으니, 배포 체크리스트에 "ad enum 바뀌면 제약 ALTER" 한 줄 박아두면 좋겠음.
2. 익명 클릭 로그 — 동의함. 공개 URL에 userId 실는 거 열거 위험 맞고, 정산 대조엔 userId 필요 없음. 본인클릭 어뷰징은 나중에 ip_hash + UA 빈도로 사후 필터 가능하니 지금 구조로 놔둬도 됨.
3. creative_id FK 미설정 — 소재 삭제해도 집계 이력 남기려는 의도 이해되고, 우리 다른 집계성 테이블도 논리참조 쓰는 편이라 컨벤션이랑 어긋나지 않음. OK.
- AdImpressionRecorder 를 분리해 집계 행 삽입을 REQUIRES_NEW 로 격리한다. PostgreSQL 은 제약 위반 시 트랜잭션 전체를 중단시켜, 되돌리기가 같은 트랜잭션 안에 있으면 UnexpectedRollbackException 으로 번진다. - 노출 집계 요청에 소재 20개 상한을 둔다. 익명 공개 API 라 배치 크기를 막는다. - 관리자 소재 등록 시 랜딩 URL 형식을 검증한다. 클릭 시점 500 을 앞당겨 잡는다. - 광고 도메인·관리자 테스트 64건 통과.
- enum 을 늘렸을 때 ddl-auto=update 가 CHECK 제약을 갱신하지 않는 문제를 배포 체크리스트로 남긴다. ALTER 는 사람이 직접 돌려야 한다. - 2026-07-16 adfit 리워드 문서는 이번 PR 범위가 아니라 삭제한다. origin/feat/ad-affiliate 에 그대로 남아 있다.
#️⃣ 연관된 이슈
📝 작업 내용
✨ Feat
AdCreative.java,AdClickLog.java,AdImpressionDaily.javaAdController.java,AdQueryService.javaAdClickController.java,AdClickService.java,AffiliateLinks.javaCoupangLinkBuilder.java,AdpickLinkBuilder.javaAdminAdController.java,AdminAdService.javaAdLandingController.java,templates/ad/landing.htmlSwaggerConfig.java🐛 Fix
/error401 해소JwtFilter.java,SecurityConfig.java📌 공유 사항
✅ 체크리스트
📸 스크린샷
@Check제약이 실제 DDL에 나가는지 스키마 생성으로 확인했습니다.💬 리뷰 요구사항