Skip to content

#485 [Merge] dev 변경사항을 main에 반영 (v2.0.39) - #486

Merged
si-zero merged 89 commits into
mainfrom
merge/#485
Sep 12, 2026
Merged

#485 [Merge] dev 변경사항을 main에 반영 (v2.0.39)#486
si-zero merged 89 commits into
mainfrom
merge/#485

Conversation

@si-zero

@si-zero si-zero commented Sep 12, 2026

Copy link
Copy Markdown
Member

#️⃣ 연관된 이슈

📝 작업 내용

✨ Feat / 🐛 Fix / 📝 Docs

내용 PR
배틀 시나리오 감정 톤 + 철학자 보이스 매핑 인프라 #456
배틀 대본 붙여넣기 파서 (배틀·시나리오 자동 등록) #468
배틀 대본 파서 대사를 문장 종결부호 단위로 분리 #470
배틀 대본 파서 summary·description·선택지 title 매핑 수정 #472
시나리오 선택지에 선택의 시간 설명 텍스트(title) 추가 #474
철학자 보이스 매핑에 이미지 키(image_key) 추가 #476
철학자 보이스 매핑에 실제 이미지 키 반영 #478
철학자 보이스 imageKey 응답을 호출 가능한 경로로 변환 #480
배틀 옵션 태그 수정 시 유니크 제약 위반(23505) 발생 수정 #482
로컬 draft→S3 promote가 DB 트랜잭션과 분리되어 orphan 파일/유실 위험 수정 #484

📌 공유 사항

  1. 배틀 발행 자동화(v2.0.39) — 기획 구글독스 대본을 어드민에 붙여넣으면 배틀/시나리오 데이터로 자동 파싱·등록되는 기능. 감정 톤(Fish Audio TTS)과 철학자 보이스/이미지 매핑 인프라 포함.
  2. 운영 배포 전 수동 DB 마이그레이션 필요:
    • docs/db/20260910_seed_philosopher_voice.sql
    • docs/db/20260912_update_philosopher_voice_image_key.sql
    • src/main/resources/db/migration/V20260521_01__drop_battle_option_label.sql (배틀 자동화와 무관한 훨씬 오래된 버그지만, 운영 DB에 battle_options.label 레거시 컬럼이 아직 남아있을 가능성이 높아 배포 전 필수 확인 — 실행 안 하면 배틀 옵션 생성/수정 API가 500 남)
  3. 운영 환경변수 확인 필요 (기본값 없어서 없으면 부팅 실패): FISHAUDIO_API_KEY, FISHAUDIO_VOICE_A, FISHAUDIO_VOICE_B, FISHAUDIO_VOICE_USER, FISHAUDIO_VOICE_NARRATOR

✅ 체크리스트

  • Reviewer에 팀원들을 선택했나요?
  • Assignees에 본인을 선택했나요?
  • 컨벤션에 맞는 Type을 선택했나요?
  • Development에 이슈를 연동했나요?
  • Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  • 컨벤션을 지키고 있나요?
  • 로컬에서 실행했을 때 에러가 발생하지 않나요?
  • 팀원들에게 PR 링크 공유를 했나요?

📸 스크린샷

💬 리뷰 요구사항

배포 전 운영 DB 마이그레이션(위 3개 스크립트)과 환경변수 세팅을 먼저 완료해야 정상 동작합니다. 병합만으로 배포가 끝나지 않으니 순서 확인 부탁드립니다.

HYH0804 and others added 30 commits May 27, 2026 21:58
## #️⃣ 연관된 이슈
<!-- Ex) - #이슈번호 -->
<!-- 연관된 이슈 번호를 링크 형태로 작성하세요 -->
- #250 

## 📝 작업 내용
<!-- 이번 PR/이슈에서 실제 수행한 작업 내용을 작성하세요 -->

### ♻️ Refactor
<!-- 기존 코드 리팩토링 내용 작성 -->
| 내용 | 파일 |
|------|------|
| 불필요한 import 제거 | `UserService.java` |
| 투표 비율 계산 기준을 preVoteOption → postVoteOption으로 수정 |
`BattleVoteServiceImpl.java` |
| totalCount도 사후 투표 완료된 건만 카운트하도록 수정 | `BattleVoteServiceImpl.java` |
| countByBattleAndPostVoteOption,
countByBattleAndPostVoteOptionIsNotNull 메서드 추가 |
`BattleVoteServiceImpl.java` |

## 📌 공유 사항
<!-- 팀원에게 공유할 내용이나 참고 사항 작성 -->
<!-- 노션 환경 설정 파일 확인 부탁드립니다! -->
> ▎ 1. 기존 코드에서 투표 비율(%)이 preVoteOption 기준으로 계산되어 사전 투표만 한 유저도 카운트에 포함되고
있었습니다.
  ▎ 2. 사후 투표(postVoteOption)가 완료된 건만 기준으로 비율을 계산하도록 수정했습니다.

## ✅ 체크리스트
<!-- PR 제출 전에 체크해야 할 사항들 -->
- [x] Reviewer에 팀원들을 선택했나요?
- [x] Assignees에 본인을 선택했나요?
- [x] 컨벤션에 맞는 Type을 선택했나요?
- [x] Development에 이슈를 연동했나요?
- [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
- [x] 컨벤션을 지키고 있나요?
- [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
- [x] 팀원들에게 PR 링크 공유를 했나요?

## 📸 스크린샷
<!-- Swagger, Postman, JUnit 테스트 화면 첨부 -->
<!-- 기능 동작 화면이나 테스트 결과 캡처를 첨부하면 좋습니다 -->

## 💬 리뷰 요구사항
<!-- 리뷰어에게 요청하는 구체적인 사항 작성 -->
<!-- 종료 의도 판단을 Java 키워드 → AI 2차 검증 구조로 설계했는데
해당 구조가 유지보수 및 확장 측면에서 적절한지 의견 부탁드립니다. -->
> 1.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## #️ 연관된 이슈
  - #254

  ## 📝 작업 내용

  ### ♻️ Refactor
  | 내용 | 파일 |
  |------|------|
  | 관점 등록/수정 시 GPT 검수 주석 코드 제거 | `PerspectiveService.java` |

  ## 📌 공유 사항
> 1. 기존에 `createPerspective()`, `updatePerspective()`에서 GPT 검수 호출이 주석
처리된 채 남아있던 dead code를 제거했습니다. 동작 방식은 동일하게 등록/수정 즉시 `PUBLISHED` 상태로
저장됩니다.

  ## ✅  체크리스트
  - [x] Reviewer에 팀원들을 선택했나요?
  - [x] Assignees에 본인을 선택했나요?
  - [x] 컨벤션에 맞는 Type을 선택했나요?
  - [x] Development에 이슈를 연동했나요?
  - [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  - [x] 컨벤션을 지키고 있나요?
  - [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
  - [x] 팀원들에게 PR 링크 공유를 했나요?

  ## 📸 스크린샷
  해당 없음 (코드 정리 작업)

  ## 💬 리뷰 요구사항
  > 1. 없습니다.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## #️ 연관된 이슈
  - #255

  ## 📝 작업 내용

  ### ♻️ Refactor
  | 내용 | 파일 |
  |------|------|
| VoteStatsResponse OptionStat에 title 필드 추가 | `VoteStatsResponse.java`,
`BattleVoteServiceImpl.java` |

  ## 📌 공유 사항
> 1. `GET /api/v1/battles/{battleId}/vote-stats` 응답의 `OptionStat`에
`title` 필드가 추가되었습니다. 옵션 이름(ex. "변기는 변기다")을 투표 % 바 및 관점 탭 구성에 활용해 주세요.
> 2. 관점 목록 조회 시 옵션 탭 필터는 `optionId` 파라미터로 전달하면 됩니다. (`GET
/api/v1/battles/{battleId}/perspectives?optionId={optionId}`)

  ## ✅  체크리스트
  - [x] Reviewer에 팀원들을 선택했나요?
  - [x] Assignees에 본인을 선택했나요?
  - [x] 컨벤션에 맞는 Type을 선택했나요?
  - [x] Development에 이슈를 연동했나요?
  - [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  - [x] 컨벤션을 지키고 있나요?
  - [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
  - [x] 팀원들에게 PR 링크 공유를 했나요?

  ## 📸 스크린샷
  해당 없음 (응답 필드 추가)

  ## 💬 리뷰 요구사항
  > 1. 없습니다.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## #️⃣연관된 이슈
  - #262
  
  ## 📝 작업 내용

  ### ✨ Feat                                                           
  | 내용 | 파일 |
  |------|------|
| 디바이스 푸시 토큰 등록/해제 API 추가 (Android: FCM 토큰, iOS: APNs 디바이스 토큰) |
`DeviceController.java`, `DeviceService.java`,
`RegisterDeviceRequest.java`,
`UserDevice.java`, `DevicePlatform.java`, `UserDeviceRepository.java` |
| FCM(Android) 푸시 발송 인프라 구축 (Firebase Admin SDK) |
`FirebaseConfig.java`, `FcmPushService.java`, `build.gradle` |
| iOS 푸시를 FCM 경유 방식에서 다이렉트 APNs 발송으로 전환 (Pushy) | `ApnsConfig.java`,
`ApnsPushService.java`, `FcmPushService.java`,
`NotificationDispatchService.java`,
  `build.gradle` |
| 새 배틀 발행 시 인앱 알림 + 푸시 발송 | `BattleServiceImpl.java`,
`NotificationDispatchService.java` |
| 답글 좋아요 / 새 댓글 발생 시 인앱 알림 + 푸시 발송 | `CommentLikeService.java`,
`PerspectiveCommentService.java`, `NotificationDispatchService.java` |
  | 포인트 적립 시 인앱 알림 발송 | `CreditService.java` |
  | 알림 API 명세 문서 작성 | `docs/api-specs/notification-api.md` |

  ### ♻️ Refactor
  | 내용 | 파일 |
  |------|------|
| 답글 좋아요/새 댓글 알림 문구에 배틀 제목 포함하도록 개선 |
`NotificationDispatchService.java`, `CommentLikeService.java`,
`PerspectiveCommentService.java` |

  ### 🐛 Fix
  | 내용 | 파일 |
  |------|------|
| 컨테이너 배포 환경(Railway)에서 FCM/APNs 자격증명 파일(JSON/.p8)을 읽지 못해 서버 기동 자체가 실패하던
문제 수정 — S3에서 자격증명을 다운로드해 사용하도록 변경 (로컬은 기존
파일 경로 방식 그대로 폴백) | `FirebaseConfig.java`, `ApnsConfig.java`,
`application.yml` |

  ## 📌 공유 사항
> 1. **(중요) Railway dev Variables 추가 완료** — 이 PR이 배포되려면 아래 값이 필요하며, dev
환경엔 이미 세팅해뒀습니다.
> - `FCM_CREDENTIALS_S3_KEY`, `APNS_CREDENTIALS_S3_KEY` (S3
`pique-bucket/credentials/`에 Firebase 서비스 계정 JSON, APNs `.p8` 키 업로드 완료)
> - `APNS_ENABLED=true`, `APNS_KEY_ID`, `APNS_BUNDLE_ID`,
`APNS_PRODUCTION`
  >    - `APNS_TEAM_ID`는 따로 안 넣어도 기존 `APPLE_TEAM_ID`로 폴백됩니다.
  > 2. **운영(main) 배포 시에도 동일하게 Railway 변수 추가 + S3 파일 업로드가 필요**합니다.
> 3. iOS는 더 이상 FCM을 거치지 않고 서버가 APNs로 직접 발송합니다. 디바이스 등록 시 `fcmToken` 필드에
iOS는 APNs 디바이스 토큰(hex 문자열)을 보내야 합니다. 자세한 내용은
  `docs/api-specs/notification-api.md` 참고.
  > 4. 새 의존성 추가: `firebase-admin:9.9.0`, `com.eatthepath:pushy:0.15.4`

  ## ✅ 체크리스트
  - [x] Reviewer에 팀원들을 선택했나요?
  - [x] Assignees에 본인을 선택했나요?
  - [x] 컨벤션에 맞는 Type을 선택했나요?
  - [x] Development에 이슈를 연동했나요?
  - [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  - [x] 컨벤션을 지키고 있나요?
  - [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
  - [x] 팀원들에게 PR 링크 공유를 했나요?

  ## 📸 스크린샷
  <!-- Swagger/Postman으로 디바이스 등록, 알림함 조회, 푸시 수신 테스트 캡처 첨부 -->

  ## 💬 리뷰 요구사항
> 1. APNs 환경(sandbox/production)이 `APNS_PRODUCTION` 전역 설정 1개로 고정되는 구조인데,
개발(TestFlight)/운영(App Store) 빌드를 동시에 지원하려면 별도 설계가 필요해 보입니다 —
  의견 부탁드립니다.
> 2. FCM/APNs 자격증명을 S3에서 받아오는 방식(이번 Fix)이 적절한지, 다른 방식을 선호하시면 의견 부탁드립니다.

---------

Co-authored-by: 현준혁 <guswnsgur2276@metabuild.co.kr>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
createTempFile로 생성된 빈 파일이 이미 존재해 S3 getObject의
toFile 다운로드가 FileAlreadyExistsException으로 실패하던 문제 수정
## #️ 연관된 이슈
  - #262

  ## 📝 작업 내용

  ### ✨  Feat
  | 내용 | 파일 |
  |------|------|
| 새 배틀/답글 좋아요/새 댓글/관리자 공지·이벤트 알림 발송 시 UserSettings의 ON/OFF 설정을 확인해 OFF인
사용자에게는 푸시를 보내지 않도록 분기 (인앱 알림은 항상 생성) |
`NotificationDispatchService.java`,
`UserSettingsRepository.java`, `UserDeviceRepository.java`,
`AdminNotificationService.java` |
| 오늘의 픽으로 자동 배정된 배틀에도 새 배틀 알림(인앱+푸시) 발송 추가 | `BattleServiceImpl.java` |

  ### 🐛 Fix
  | 내용 | 파일 |
  |------|------|
| 관점/댓글 삭제 시 좋아요·신고 잔여 데이터가 남아 FK 제약 조건 위반 오류가 발생하던 문제 수정 |
`PerspectiveService.java`, `PerspectiveCommentService.java`,
`CommentLikeRepository.java`,
`CommentReportRepository.java`, `PerspectiveCommentRepository.java`,
`PerspectiveLikeRepository.java`, `PerspectiveReportRepository.java` |

  ## 📌 공유 사항
  > 1. 알림 ON/OFF 설정 매핑은 다음과 같습니다.
  >    - `newBattleEnabled` → 새 배틀 알림
  >    - `contentLikeEnabled` → 좋아요 알림
  >    - `newCommentEnabled` → 새 댓글 알림
  >    - `marketingEventEnabled` → 이벤트 및 소식 알림 (공지/이벤트)
> - `commentReplyEnabled`, `battleResultEnabled` → 현재 매핑되는 알림 이벤트가 없어 보류
(정책 미정)
> 2. OFF 상태여도 인앱 Notification은 항상 생성되고, 푸시 발송만 차단됩니다. UserSettings가 없는
레거시 유저는 기본값 ON으로 처리됩니다.

  ## ✅  체크리스트
  - [x] Reviewer에 팀원들을 선택했나요?
  - [x] Assignees에 본인을 선택했나요?
  - [x] 컨벤션에 맞는 Type을 선택했나요?
  - [x] Development에 이슈를 연동했나요?
  - [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
  - [x] 컨벤션을 지키고 있나요?
  - [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
  - [x] 팀원들에게 PR 링크 공유를 했나요?

  ## 📸 스크린샷


  ## 💬 리뷰 요구사항
> 1. 관리자 공지/이벤트 알림(POLICY_CHANGE/PROMOTION)을 `marketingEventEnabled` 하나로
묶어서 게이팅했는데, 이 매핑이 적절한지 의견 부탁드립니다.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## #️⃣ 연관된 이슈
<!-- Ex) - #이슈번호 -->
<!-- 연관된 이슈 번호를 링크 형태로 작성하세요 -->
- closed #268  

## 📝 작업 내용
<!-- 이번 PR/이슈에서 실제 수행한 작업 내용을 작성하세요 -->

### 🔨 Chore
<!-- 새로운 기능 구현 내용 작성 -->
| 내용 | 파일 |
|------|------|
| 크롤링 파일 추가 | `app-ads.txt` |
|  |  |

## 📌 공유 사항
<!-- 팀원에게 공유할 내용이나 참고 사항 작성 -->
<!-- 노션 환경 설정 파일 확인 부탁드립니다! -->
> 없음

## ✅ 체크리스트
<!-- PR 제출 전에 체크해야 할 사항들 -->
- [x] Reviewer에 팀원들을 선택했나요?
- [x] Assignees에 본인을 선택했나요?
- [x] 컨벤션에 맞는 Type을 선택했나요?
- [x] Development에 이슈를 연동했나요?
- [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
- [x] 컨벤션을 지키고 있나요?
- [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
- [ ] 팀원들에게 PR 링크 공유를 했나요?

## 📸 스크린샷
<!-- Swagger, Postman, JUnit 테스트 화면 첨부 -->
<!-- 기능 동작 화면이나 테스트 결과 캡처를 첨부하면 좋습니다 -->
> 없음

## 💬 리뷰 요구사항
<!-- 리뷰어에게 요청하는 구체적인 사항 작성 -->
<!-- 종료 의도 판단을 Java 키워드 → AI 2차 검증 구조로 설계했는데
해당 구조가 유지보수 및 확장 측면에서 적절한지 의견 부탁드립니다. -->
> 없음
## #️⃣ 연관된 이슈
<!-- Ex) - #이슈번호 -->
<!-- 연관된 이슈 번호를 링크 형태로 작성하세요 -->
- #268

## 📝 작업 내용
<!-- 이번 PR/이슈에서 실제 수행한 작업 내용을 작성하세요 -->

### ✨ Feat
<!-- 새로운 기능 구현 내용 작성 -->
| 내용 | 파일 |
|------|------|
| 허용 설정 추가 | `SecurityConfig.java` |
|  |  |

## 📌 공유 사항
<!-- 팀원에게 공유할 내용이나 참고 사항 작성 -->
<!-- 노션 환경 설정 파일 확인 부탁드립니다! -->
> 없음

## ✅ 체크리스트
<!-- PR 제출 전에 체크해야 할 사항들 -->
- [x] Reviewer에 팀원들을 선택했나요?
- [x] Assignees에 본인을 선택했나요?
- [x] 컨벤션에 맞는 Type을 선택했나요?
- [x] Development에 이슈를 연동했나요?
- [x] Merge 하려는 브랜치가 올바르게 설정되어 있나요?
- [x] 컨벤션을 지키고 있나요?
- [x] 로컬에서 실행했을 때 에러가 발생하지 않나요?
- [ ] 팀원들에게 PR 링크 공유를 했나요?

## 📸 스크린샷
<!-- Swagger, Postman, JUnit 테스트 화면 첨부 -->
<!-- 기능 동작 화면이나 테스트 결과 캡처를 첨부하면 좋습니다 -->
> 없음

## 💬 리뷰 요구사항
<!-- 리뷰어에게 요청하는 구체적인 사항 작성 -->
<!-- 종료 의도 판단을 Java 키워드 → AI 2차 검증 구조로 설계했는데
해당 구조가 유지보수 및 확장 측면에서 적절한지 의견 부탁드립니다. -->
> 없음
si-zero and others added 24 commits September 2, 2026 15:37
## 문제

dev 스웨거에서 광고 API 를 Try it out 하면 401 이 떨어집니다.

```
https://dev.picke.store/swagger-ui/index.html?urls.primaryName=3.+광고+API
→ Execute
→ 401 AUTH_401 "인증이 필요합니다"
```

요청이 dev 가 아니라 **운영으로 나가고 있었습니다.** Swagger UI 는 서버 목록의 첫 번째를 기본으로 골라 두는데,
공통 목록이 운영을 맨 앞에 두고 있습니다.

```
1. https://picke.store      ← 기본 선택
2. http://localhost:8080
3. https://dev.picke.store
```

운영에는 광고 경로가 아직 배포되지 않아 JwtFilter 화이트리스트에 없고, 그래서 401 입니다.

| 요청 대상 | 결과 |
|---|---|
| `dev.picke.store/api/v1/ads?slot=HOME_FEED` | 200
`{"statusCode":200,"data":[]}` |
| `picke.store/api/v1/ads?slot=HOME_FEED` | 401 `AUTH_401` |

지금은 401 로 막히지만, 광고가 운영에 배포되면 같은 실수가 조용히 통과합니다. 광고 그룹에는 소재 등록·수정·삭제가 함께
묶여 있어 **운영 데이터가 바뀝니다.**

## 왜 프로파일로 가르지 않는가

`prod` 프로파일이면 운영, 아니면 dev 로 나누는 방법을 먼저 검토했습니다. **쓸 수 없습니다.**

dev 서버도 `prod` 프로파일로 돕니다. 부팅 로그에 그대로 남아 있습니다.

```
The following 1 profile is active: "prod"     ← dev.picke.store 컨테이너
```

`application-prod.yml` 에도 호스트를 가르는 설정이 없습니다. 프로파일로 나누면 dev 스웨거가 운영을 가리키는
지금 상태가 그대로 유지됩니다.

## 변경

- 광고 그룹의 서버 목록 맨 앞에 상대 경로(`/`)를 둔다. Swagger UI 가 이를 현재 origin 으로 풀어 주므로,
운영에서 연 문서는 운영으로, dev 에서 연 문서는 dev 로 나간다.
- 절대 주소 세 개는 목록에 남겨 둔다. 다른 환경을 일부러 고르는 것은 그대로 가능하다.
- 다른 그룹(사용자·관리자·전체)의 서버 순서는 건드리지 않는다.
- 서버 정의가 두 곳에서 필요해져 메서드로 뽑았다. 값은 그대로다.
- 광고 그룹의 기본 서버와, 다른 그룹이 종전 순서를 유지하는지 검사하는 테스트를 추가한다.

## 검증

- `./gradlew test --tests "com.swyp.picke.domain.ad.*" --tests
"com.swyp.picke.global.config.*" --rerun-tasks` → BUILD SUCCESSFUL
- `SwaggerAdGroupTest` 4건 통과 (failures 0, errors 0)
- 역검증: 상대 경로를 빼고 운영 우선으로 되돌리면 해당 테스트 FAILED

## 남은 것

이 PR 범위 밖이라 손대지 않았습니다. 별도로 다룰지 판단이 필요합니다.

`slot` 파라미터를 빼면 400 이 아니라 500 이 납니다. 스웨거에서 파라미터를 채우지 않고 Execute 하면 이 응답을
보게 됩니다.

```
GET /api/v1/ads   → 500 COMMON_500 "서버 오류가 발생했습니다"
```

`MissingServletRequestParameterException` 이 전역 핸들러에 잡히지 않는 것으로 보입니다.
## 문제

운영 광고를 스웨거에서 조회할 방법이 없습니다. 광고 그룹의 서버 목록에 `ad.picke.store` 가 아예 없습니다.

세 도메인이 각각 다른 Railway 서비스입니다.

| 도메인 | Railway 서비스 | 광고 기능 |
|---|---|---|
| dev.picke.store | `hqjtvzmx` | 있음 |
| picke.store (운영 API) | `uz2tygnb` | 없음 |
| ad.picke.store (운영 광고) | `u9l7w51v` | 없음 (구 빌드) |

운영에서 광고를 받는 곳은 `ad.picke.store` 인데, 목록에는 `picke.store` 만 있어 고를 수가 없었습니다.

## 변경

- 광고 그룹 서버 목록에 `https://ad.picke.store` 를 추가한다. 문서를 연 주소(`/`) 다음 순서에 둔다.
- 문서를 연 주소를 첫 번째로 두는 동작은 그대로 둔다.
- 다른 그룹의 서버 목록은 건드리지 않는다.

## 검증

- `./gradlew test --tests "com.swyp.picke.domain.ad.*" --tests
"com.swyp.picke.global.*" --rerun-tasks` → BUILD SUCCESSFUL
- `SwaggerAdGroupTest` 5건 통과 (failures 0, errors 0)

## 이 PR 만으로는 운영 광고가 뜨지 않습니다

`ad.picke.store` 서비스가 **광고 코드가 없는 구 빌드**로 돌고 있습니다. 스웨거 그룹이 `0. 모든 API /
1. 사용자 API / 2. 관리자 API` 뿐이고 `3. 광고 API` 가 없습니다. `/` 와 `/c/{code}` 모두
401 이라 클릭 리다이렉트와 랜딩이 동작하지 않습니다.

별도로 필요한 조치입니다.

1. `dev` → `main` 릴리스 (광고 기능이 dev 에만 있음)
2. `u9l7w51v` 서비스를 그 빌드로 재배포
3. 환경변수 주입 (아래)
## 문제

지면을 채울 광고가 부족합니다. 애드픽 앱 캠페인이 이 계정 기준 **7건뿐**입니다.

파라미터를 바꿔 봐도 늘지 않는 것을 실측으로 확인했습니다.

| 호출 | 결과 |
|---|---|
| `os` 생략 | 7건 |
| `os=ios` | 7건 (동일 offer) |
| `os=android` | 7건 (동일 offer) |
| `adtype=CPA` | 1건 |

7건으로는 한 지면밖에 못 채웁니다.

## 변경

애드픽에는 캠페인 리스트와 별도로 **쇼핑·핫딜 API** 가 있습니다. 각각 30건씩 내려줍니다.

- `sdk_shopping.php` — 추천 상품 30건
- `sdk_shopping_hotdeal.php` — 제휴몰 실시간 핫딜 30건

이 둘을 붙여 재고를 **7건 → 67건** 으로 늘립니다.

- 상품의 `buyurl` 은 이미 `affid` 가 박힌 완성된 추적 링크라 그대로 랜딩 URL 로 쓴다.
- 상품을 지면에 흩어 담는다. 지면 목록은 `ADPICK_SHOPPING_SLOTS` 로 조정한다.
- 부가 피드라 조회에 실패해도 앱 캠페인 동기화까지 막지 않는다.

## 두 가지 판단

**JSON 을 text/html 로 내려줍니다.** 컨텐츠 타입만 믿고 디코딩하면 전부 실패해 0건이 됩니다. 문자열로 받아
직접 파싱합니다. 이 때문에 처음 구현이 조용히 0건을 반환했습니다.

**`AdSource` 를 새로 만들지 않았습니다.** 새 값을 넣으면 `ck_ad_creatives_source` CHECK
제약을 배포 때 사람이 직접 ALTER 해야 합니다(`ddl-auto=update` 는 기존 제약을 갱신하지 않음). 상품 편입에
그 운영 절차를 얹을 이유가 없어, 기존 `ADPICK_API` 를 쓰되 식별자에 `sh` 접두사를 둬 앱 캠페인과 섞이지 않게
했습니다.

상품에는 애드픽이 주는 코드가 없어 `buyurl` 해시로 식별자를 만듭니다. 지면도 식별자로 정합니다 — 매 동기화마다 다시
뽑으면 같은 상품이 지면을 옮겨 다녀 노출 집계가 흩어집니다.

## 검증

실제 애드픽 API 로 전체 파이프라인을 돌린 결과입니다.

```
PROOF_TOTAL=67                    (앱 7 + 쇼핑 30 + 핫딜 30)
HOME_FEED            18건
BATTLE_RESULT_BOTTOM 15건
CHAT_ROOM_INLINE     12건
ATTENDANCE_COMPLETE  10건
PROFILE_BOTTOM       12건
```

- `./gradlew test --tests "com.swyp.picke.domain.ad.*" --tests
"com.swyp.picke.global.*" --rerun-tasks` → BUILD SUCCESSFUL
- `AdpickCampaignSyncServiceTest` 10건 통과 (쇼핑 편입·중복 제거·미설정 시 스킵 포함)
- 검증에 쓴 임시 테스트는 외부 API 를 호출하므로 커밋하지 않았습니다.

## 배포

추가 환경변수는 필요 없습니다. `ADPICK_AFF_ID` 하나로 캠페인·쇼핑·핫딜이 모두 동작합니다. 지면 분배를 바꾸려면
`ADPICK_SHOPPING_SLOTS` 를 설정합니다(기본값: 5개 지면 전체).
AdFit 수익과 비용을 기록해 관리자 ROI를 확인하도록 지원

Constraint: AdFit 수익은 콘솔 수동 입력값이며 자동 동기화가 아니다
Tested: AdFit 계산 및 권한과 기존 광고 회귀 테스트 28건
Confidence: high
Scope-risk: narrow
사용자 광고 API가 지면·OS·개수를 서비스에 전달하고 응답을 그대로 반환하는 계약을 검증합니다. 조회 시 노출 집계를 변경하지
않는 것도 확인합니다.

검증: AdControllerTest 및 AdQueryServiceTest 10개 통과.
사용자 광고 API와 공개 광고 랜딩에서 애드픽 동기화 설치·가입형 캠페인을 제외합니다. 쇼핑·핫딜 상품과 쿠팡 및 수동 등록
광고는 기존 조건으로 제공하고, 쇼핑 재고가 부족해도 설치형 캠페인으로 채우지 않습니다. 기존 소재와 성과 기록은 보존합니다.

검증: 광고 조회·동기화 테스트 통과. 설치/가입형 제외, 재고 부족 시 빈 목록, 공개 랜딩 제외 회귀 테스트를 추가했습니다.
관리자에서 날짜를 조회하면 AdFit 콘솔의 계정 종합 일별 수익을 자동 요청합니다. 기존 수동 데이터와 별도 account
필드로 제공하고, 미설정·인증 만료·조회 실패를 구분합니다. 비용을 제공하지 않는 계정 보고서의 ROI는 null입니다.

- 검증: AdFit 테스트 및 Spring 빈 생성 통과. 실제 endpoint의 미인증 HTTP 419 확인.
- 운영 활성화: ADFIT_SESSION_COOKIE 비밀 환경변수 필요. 인증 후 수익 조회는 아직 미검증.
- 콘솔 내부 API라 세션 만료 시 환경변수 갱신이 필요합니다. 비밀 값은 코드나 응답에 노출하지 않습니다.
@si-zero si-zero self-assigned this Sep 12, 2026
@si-zero si-zero added the 🔀 Merge (병합) 브랜치를 Merge할 때 사용합니다. label Sep 12, 2026
@si-zero si-zero linked an issue Sep 12, 2026 that may be closed by this pull request
5 tasks
@si-zero
si-zero merged commit c342799 into main Sep 12, 2026
1 check passed
@si-zero
si-zero deleted the merge/#485 branch September 12, 2026 10:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔀 Merge (병합) 브랜치를 Merge할 때 사용합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔀 Merge: dev를 main에 병합 (v2.0.39)

4 participants