Skip to content

#624 [Merge] dev 변경사항을 main에 반영 (v3.0.1) - #625

Merged
si-zero merged 149 commits into
mainfrom
merge/#624
Nov 8, 2026
Merged

si-zero merged 149 commits into
mainfrom
merge/#624

Conversation

@si-zero

@si-zero si-zero commented Nov 8, 2026

Copy link
Copy Markdown
Member

#️⃣ 연관된 이슈

📝 작업 내용

v3.0.0 이후 dev 에 머지된 PR 4개를 main 에 반영합니다.

✨ Feat

내용 PR
어드민 배틀 공개·보관 상태 변경 API (PATCH /api/v1/admin/battles/{battleId}/status) #623
배틀 수준 반영 SQL 에 운영 배틀 이전 제목 17개 추가 #619
기획 답변에 따른 남은 배틀 수준 반영 SQL #621

🐛 Fix

내용 PR
운영 DB 에 남은 이전 펀딩 스키마 정리 SQL #617

📌 공유 사항

  1. SQL 3개(V20261107_02, V20261107_01 갱신, V20261108_01)는 운영 DB 에 이미 반영했습니다. 이번 배포로 추가 실행할 SQL 은 없습니다.
  2. 새 환경변수·DB 스키마 변경 없음.
  3. 어드민 화면은 SWYP-Find/AdminWeb#79 로 이 배포 뒤에 반영합니다.
  4. 병합 방식은 v3.0.0 과 같이 main 을 merge 브랜치에 합치되 내용은 dev 기준입니다 (main 에만 있는 변경 없음).

✅ 체크리스트

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

📸 스크린샷

전체 테스트 642개 통과 (#623 기준, 실패 0)

💬 리뷰 요구사항

  1. 배포 후 운영 어드민에서 116 배틀 보관을 확인합니다.

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 <[email protected]>
## #️ 연관된 이슈
  - #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 <[email protected]>
## #️ 연관된 이슈
  - #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 <[email protected]>
## #️⃣연관된 이슈
  - #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: 현준혁 <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
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 <[email protected]>
## #️⃣ 연관된 이슈
<!-- 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 25 commits October 24, 2026 08:27
## #️⃣ 연관된 이슈
- #575

## 📝 작업 내용

### ✨ Feat
| 내용 | 파일 |
|------|------|
| 클래스 상세 초대코드를 운영자뿐 아니라 멤버에게도 노출 (공유하기 버튼용) | `ClassroomConverter.java`,
`ClassDetailResponse.java` |
| 클래스 멤버 표시 이름 일괄 조회 컴포넌트 추가 (ACTIVE 멤버만) |
`ClassDisplayNameResolver.java`, `ClassMemberRepository.java` |
| 클래스 의견·댓글 작성자 이름을 전체 닉네임 대신 클래스 표시 이름으로 노출, `userTag` 는 null |
`PerspectiveService.java`, `PerspectiveCommentService.java` |
| 나간·내보낸 멤버의 의견·댓글을 클래스 목록에서 제외, 해당 의견 상세·댓글 조회·등록은 404 |
`PerspectiveService.java`, `PerspectiveCommentService.java` |
| 클래스 `vote-stats` 를 ACTIVE 멤버의 사후투표만 집계 | `BattleVoteRepository.java`,
`BattleVoteServiceImpl.java` |
| 단위·통합 테스트, API 명세·ERD 노트 갱신 | `ClassBattleApiIntegrationTest.java` 외,
`class-api.md`, `class.puml` |

## 📌 공유 사항
> 1. 응답 구조 변경 없음. 클래스 경로에서만 `user.nickname` 이 클래스 표시 이름, `user.userTag`
가 null 로 바뀝니다. 일반 배틀은 그대로입니다.
> 2. 나간 멤버가 다시 참여하면 기존 의견·댓글·투표가 다시 보입니다(행은 지우지 않음).
> 3. 목록은 페이지 조회 뒤 걸러서, 나간 멤버 글이 섞인 페이지는 `size` 보다 적게 내려갈 수
있습니다(`nextCursor` 는 정상).
> 4. #576, #577 과 같은 파일을 일부 건드리지만 순서와 관계없이 충돌 없이 머지됩니다.

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

## 📸 스크린샷
- 클래스·의견·투표 패키지 테스트 통과 (ClassBattleApiIntegrationTest 10건 포함)

## 💬 리뷰 요구사항
> 1. 의견·댓글 서비스에 클래스 의존(`ClassDisplayNameResolver`)이 들어간 구조가 괜찮은지 의견
부탁드립니다.
> 2. 나간 멤버의 글을 숨기는 정책(삭제가 아닌 숨김)이 기획 의도와 맞는지 확인 부탁드립니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013pr5xNDiEmzxnZ2AbtHxVy

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
## #️⃣ 연관된 이슈
- #576

## 📝 작업 내용

### ✨ Feat
| 내용 | 파일 |
|------|------|
| 생성 요청에 선택 필드 `requestKey` 추가 (1~64자, 영문·숫자·`-`·`_`) |
`ClassCreateRequest.java` |
| 요청 키·본문 해시 저장, UK(owner_id, create_request_key) | `Classroom.java`,
`V20261028_02__classes_create_request_key.sql` |
| 같은 키·같은 내용은 기존 클래스 200, 같은 키·다른 내용은 409 `CLASS_409_REQUEST_KEY`, 키로 만든
클래스가 삭제됐으면 409 `CLASS_409_DELETED` | `ClassroomServiceImpl.java`,
`ClassroomRepository.java`, `ErrorCode.java` |
| 키가 없으면 10초 안에 같은 이름·같은 배틀로 온 요청을 중복으로 보고 직전 클래스 반환 |
`ClassroomServiceImpl.java`, `ClassroomRepository.java` |
| 같은 이용권의 생성 요청을 이용권 행 비관적 락으로 직렬화 (동시 요청 대비) |
`ClassTicketRepository.java`, `ClassroomServiceImpl.java` |
| 단위·통합 테스트(동시 요청 포함), API 명세·ERD 갱신 |
`ClassroomApiIntegrationTest.java`, `ClassroomServiceImplTest.java`,
`class-api.md`, `class.puml` |

## 📌 공유 사항
> 1. **DB 마이그레이션 필요**: `V20261028_02__classes_create_request_key.sql`
(롤백 `V20261028_rollback__classes_create_request_key.sql`). #577 의
`V20261028_01` 과 번호가 겹치지 않습니다.
> 2. 프론트: 생성 화면을 열 때 UUID 하나를 만들어 `requestKey` 로 보내고, 재시도 시 같은 값을 보내
주세요. 안 보내도 기존처럼 동작합니다(10초 판정).
> 3. 이름·운영자 표시 이름의 앞뒤 공백은 같은 내용으로 봅니다.
> 4. #575, #577 과 순서와 관계없이 충돌 없이 머지됩니다.

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

## 📸 스크린샷
- classroom 패키지 테스트 전체 통과 (ClassroomApiIntegrationTest 11건,
ClassroomServiceImplTest 20건 포함)

## 💬 리뷰 요구사항
> 1. 키가 있으면 10초 판정을 건너뛰도록 했습니다(키가 다르면 운영자가 의도한 새 클래스로 봄). 괜찮은지 의견
부탁드립니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013pr5xNDiEmzxnZ2AbtHxVy

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
## #️⃣ 연관된 이슈
- #577

## 📝 작업 내용

### ✨ Feat
| 내용 | 파일 |
|------|------|
| 클래스 중간투표 API `POST /api/v1/classes/{classId}/battle/votes/mid` 추가, 요청
`{checkpointId, optionId}` | `ClassBattleController.java`,
`ClassBattleServiceImpl.java`, `ClassMidVoteRequest.java` |
| 선택 지점(선택지가 있는 시나리오 노드)별 1건 저장, 선택지 문구 스냅샷 저장 | `ClassMidVote.java`,
`ClassMidVoteRepository.java`, `ClassMidVoteService.java`,
`V20261028_01__create_class_mid_votes.sql` |
| 대상: TTS 클래스 + `battles.mid_vote_enabled` + 시나리오에 선택 지점 존재 |
`ClassMidVoteService.java` |
| 고른 경로의 선택 지점에 모두 답해야 사후투표 가능 (`CLASS_409_MID_VOTE_REQUIRED`) |
`ClassMidVoteService.java`, `ClassBattleServiceImpl.java` |
| 첫 기록 시 사전투표 잠금(`TTS_LISTENING`), 사후투표 전까지 변경 가능, votes 행 비관적 락으로 연속 요청
직렬화 | `ClassMidVoteService.java`, `ClassMidVoteRepository.java` |
| 상세 `midVoteEnabled`, 내 투표·내 리포트 `midVotes[]` 노출 |
`ClassMyVoteResponse.java`, `ClassMidVoteInfo.java`,
`ClassMyReportResponse.java` |
| 결과 통계·마감 스냅샷에 지점별 선택 분포(`midVoteCheckpoints`, 활성 멤버만) 반영 |
`ClassResultStats.java`, `ClassResultStatsCalculator.java`,
`ClassResultResponse.java`, `ClassResultServiceImpl.java` |
| AI 개인 피드백·인사이트 입력에 중간 선택 반영, 투표만 한 사람 템플릿에 중간 선택 문구 추가 |
`ClassReportSnapshotService.java`, `ClassReportPrompts.java`,
`ClassReportContentValidator.java`, `MemberFeedbackInput.java` |
| 시나리오 조회 `interactiveOptions[].optionId` 노출 | `OptionResponse.java`,
`ScenarioConverter.java` |
| 에러 코드 `CLASS_409_MID_VOTE`, `CLASS_409_MID_VOTE_LOCKED`,
`CLASS_409_MID_VOTE_REQUIRED`, `CLASS_404_MID_VOTE_OPTION` |
`ErrorCode.java` |
| 단위·통합 테스트, API 명세(class §6·§8, scenario), ERD 갱신 |
`ClassMidVoteServiceTest.java`, `ClassBattleApiIntegrationTest.java`,
`ClassResultApiIntegrationTest.java` 외 |

## 📌 공유 사항
> 1. **DB 마이그레이션 필요**: `V20261028_01__create_class_mid_votes.sql` (롤백
`V20261028_rollback__...`). 운영 반영 전 실행해야 합니다.
> 2. PRD §7 기준으로 checkpoint 별로 기록합니다. checkpointId = 시나리오 노드 ID,
optionId = `interactiveOptions[].optionId`.
> 3. 시나리오 재저장 시 ID가 바뀔 수 있어 FK 없이 선택 문구를 함께 저장합니다. 사라진 선택지도 결과에는 저장된 문구로
남습니다.
> 4. 기존 `votes.mid_vote_option_id` 컬럼은 클래스에서 쓰지 않습니다(일반 배틀 코드는 그대로).
> 5. 이전 마감 스냅샷은 `midVoteEnabled=false`, `midVoteCheckpoints=[]` 로 읽힙니다.

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

## 📸 스크린샷
- classroom·scenario·vote 테스트 227개 중 2개 실패: 기존 ffmpeg 환경 의존
테스트(`ScenarioAudioPipelineServiceTest`, `ScenarioServiceImplTest`), 이번
변경과 무관

## 💬 리뷰 요구사항
> 1. 중간투표가 사후투표의 필수 조건(고른 경로 기준)인 점, 프론트에서 지점마다 호출해야 하는 점 확인 부탁드립니다.
> 2. 중간투표를 보내면 사전투표가 잠기는 규칙이 기획 의도와 맞는지 확인 부탁드립니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013pr5xNDiEmzxnZ2AbtHxVy

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
## #️⃣ 연관된 이슈
- #589

## 📝 작업 내용

### ✨ Feat
| 내용 | 파일 |
|------|------|
| 등록기한 알림 단계 D30(7일 초과~30일 이내)·D7(7일 이내) 정의 |
`RegisterReminderStage.java` |
| 알림 발송 이력 엔티티·리포지토리 (`ticket_register_reminders`) |
`TicketRegisterReminder.java`, `TicketRegisterReminderRepository.java` |
| 대상 조회 JPQL: DELIVERED·UNREGISTERED, 등록기한 구간 안, 해당 단계 SUCCESS 기록 없음 |
`ClassTicketRepository.java` |
| `GET /api/v1/admin/funding/register-reminders?stage=D30\|D7` 대상 목록
(후원자 연락처, 남은 일수 포함) | `AdminFundingController.java`,
`AdminFundingService.java`, `RegisterReminderTargetResponse.java` |
| `POST /api/v1/admin/funding/tickets/{ticketId}/register-reminders` 발송
결과 기록 | `AdminFundingController.java`, `AdminFundingService.java`,
`RegisterReminderRequest.java`, `RegisterReminderResponse.java` |
| 펀딩 현황 요약에 `registerReminderD30`, `registerReminderD7` 추가 |
`FundingSummaryResponse.java` |
| 마이그레이션 (+ 롤백) | `V20261028_03__create_ticket_register_reminders.sql`,
`V20261028_rollback__create_ticket_register_reminders.sql` |
| API 명세 §9.8·§9.9, ERD 갱신 | `class-api.md`, `class.puml` |
| 통합 테스트 (구간 판정, 실패/성공 기록, 409·400·403) |
`AdminFundingApiIntegrationTest.java` |

## 📌 공유 사항
> 1. PRD "등록기한 30일·7일 전 알림은 허용된 연락 수단에서만 발송" 기준입니다. 미등록 이용권은 앱 계정과 연결되지
않아 푸시를 보낼 수 없고, 메일·SMS 발송 인프라도 없습니다. 그래서 코드 전달(9.4)과 같은 방식으로 운영자가 후원자
연락처로 보낸 뒤 결과를 기록합니다.
> 2. 대상은 조회할 때 계산하므로 배치나 스케줄러가 없습니다. SUCCESS로 기록하면 그 단계 대상에서 빠지고,
FAILED는 대상에 남아 재시도할 수 있습니다.
> 3. 운영 DB에는 `V20261028_03` 적용이 필요합니다. 아직 적용되지 않은 `V20261028_01`·`_02`와
함께 적용하면 됩니다.

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

## 📸 스크린샷
- `AdminFundingApiIntegrationTest` 6/6 통과 (KST, UTC 타임존 모두)
- ticket·classroom 테스트 전체 통과

## 💬 리뷰 요구사항
> 1. 자동 발송 대신 "대상 조회 + 운영자 발송 기록"으로 설계했습니다. 추후 메일 발송 인프라가 생기면 기록 API를
그대로 재사용할 수 있는 구조인지 봐주세요.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013pr5xNDiEmzxnZ2AbtHxVy

Co-authored-by: Claude Opus 5.5 <[email protected]>
## #️⃣ 연관된 이슈
- #578 

## 📌 공유 사항
1. 관점·댓글 생성·수정 요청에 200자 제한을 적용하고, 초과 요청은 400으로 처리했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #582 

## 📌 공유 사항
1. 클래스 리포트에서 받은 좋아요 수와 내가 작성한 댓글 목록을 `cursor` 기반으로 조회하도록 추가했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #583

## 📌 공유 사항
1. 홈·배틀 카드에 BEST·최다 토론 라벨, 태그, 소요 시간 등 화면 표시 정보를 추가했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #584

## 📌 공유 사항
1. 회원 탈퇴 시 운영 클래스, 참여 좌석, 작성한 의견·댓글 처리 규칙을 적용했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #585 

## 📌 공유 사항
1. 잘못된 커서 입력은 400, DB 제약 충돌은 409로 응답하도록 정리했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #586 

## 📌 공유 사항
1. 관리자용 주문 검색·요약과 이용권 발급·조회·전달·재발급·환불 기능을 보완했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #587 

## 📌 공유 사항
1. 텀블벅 Start·Run 요금제별 처리와 구매자 엑셀·수동 등록 흐름을 추가했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
## #️⃣ 연관된 이슈
- #588

## 📌 공유 사항
1. Java 파일의 패키지 전체 경로 타입인 com.~ 참조를 import로 정리했습니다.

## 🧪테스트
`./gradlew compileTestJava` - 성공
@si-zero si-zero added the 🔀 Merge (병합) 브랜치를 Merge할 때 사용합니다. label Nov 8, 2026
@si-zero si-zero self-assigned this Nov 8, 2026
@si-zero si-zero linked an issue Nov 8, 2026 that may be closed by this pull request
4 tasks
@si-zero
si-zero merged commit 52753eb into main Nov 8, 2026
1 check passed
@si-zero
si-zero deleted the merge/#624 branch November 8, 2026 03:13
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에 병합 (v3.0.1)

4 participants