Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
149 commits
Select commit Hold shift + click to select a range
a3c9a37
#250 [Fix] 투표 비율 계산 기준을 preVoteOption에서 postVoteOption으로 수정 (#252)
HYH0804 May 27, 2026
a38c141
#254 [Refactor] 관점 등록/수정 시 GPT 검수 코드 제거 (#256)
HYH0804 Jun 3, 2026
cee2c5d
#255 [Refactor] VoteStatsResponse OptionStat에 title 필드 추가 (#257)
HYH0804 Jun 3, 2026
53533b8
#258 [Feat] 출석체크 API 추가
si-zero Jun 3, 2026
77db7a1
#260 [Feat] 스웨거 어드민/유저 분리
si-zero Jun 4, 2026
a9d4293
[Feat] 알림기능 (#263)
HYH0804 Jun 11, 2026
b5a681b
#262 [Fix] APNs 자격증명 S3 다운로드 시 임시파일 충돌 오류 수정
HYH0804 Jun 11, 2026
d281fa3
#264 [Fix] AdMob SSV 포인트 미적립 버그 수정
si-zero Jun 11, 2026
968d6a3
#262 merge (#266)
HYH0804 Jun 14, 2026
d041631
[Chore] adMob 크롤링 파일 설정: adMob 크롤링 파일 설정 #269
si-zero Jun 16, 2026
ec25882
#270 [Feat] 설정 파일 추가
si-zero Jun 16, 2026
4c45999
[Chore] JWT필터 설정 #271
si-zero Jun 16, 2026
e0c9c1c
#275 [Fix] AdMob SSV 콜백 파라미터 optional 처리 및 서명 검증 활성화
si-zero Jun 18, 2026
ab2cb78
#277 [Fix] AdMob SSV 콜백 URL 검증 요청 처리 실패
si-zero Jun 18, 2026
04d947c
#279 [Fix] AdMob SSV 엔드포인트 검증 실패 시 항상 200 반환
si-zero Jun 18, 2026
d9ea3b5
#283 [Fix] /me/content-activities voteSide null 반환 버그
si-zero Jun 19, 2026
ec27b01
#285 [Feat] content-activities 응답에 option_title 필드 추가
si-zero Jun 20, 2026
6663e7a
#287 [Fix] AdMob SSV 서명 검증 오류로 인한 포인트 미지급 버그
si-zero Jun 24, 2026
39aca84
#289 [Fix] 오늘의 배틀 개수 변경
si-zero Jul 5, 2026
11e2da1
#293 [Fix] 사전투표 철학자 아이콘 미노출
si-zero Jul 5, 2026
aa449f0
#297 [Fix] 댓글 작성 시각이 9시간 전으로 표시되는 버그
si-zero Jul 6, 2026
3259a1a
#301 [Fix] robots.txt 파일 부재로 AdMob app-ads.txt 크롤링 차단
si-zero Jul 6, 2026
e91819b
#305 [Fix] S3 오디오 조각 재다운로드 시 FileAlreadyExistsException으로 TTS 파이프라인 실패
si-zero Jul 6, 2026
d64d5ca
#309 [Fix] JwtFilter 자체 화이트리스트에 /robots.txt 누락으로 401 발생
si-zero Jul 6, 2026
ff16d85
#316 [Fix] 포인트 정책표 대비 코드 불일치 4건 수정
si-zero Jul 7, 2026
f50c749
Merge branch 'main' into dev
si-zero Jul 7, 2026
d9e28a1
#321 [Feat] 알림 미읽음 여부 확인 API 추가
si-zero Jul 7, 2026
4279341
#313 [Hotfix] Railway 배포 환경 FFmpeg 자동 설치 적용 (#320)
jucheonsu Jul 7, 2026
f77048c
#327 [Fix] 알림 전체 읽음 처리 후 모두 읽음 신호가 전달되지 않던 문제 수정
si-zero Jul 13, 2026
99886f9
#331 [Docs] 출석체크 API 명세서를 실제 코드에 맞춰 갱신
si-zero Jul 13, 2026
087b8c0
#333 [Fix] app-ads.txt가 text/html로 응답되어 AdMob 크롤러가 무시하는 문제 수정
si-zero Jul 13, 2026
d33ff1f
#337 [Feat] 인앱 알림 테스트용 API 추가
si-zero Jul 14, 2026
17f35a4
#341 [Fix] 미문서화/중복 포인트 지급 로직 정리
si-zero Jul 15, 2026
acbe078
관리자 페이지를 새 admin-web(Next.js) 앱으로 리다이렉트 (#346)
Jul 15, 2026
de75bdf
#349 [Feat] admin.web.url env 주입 + 기본값 picke.up.railway.app (#350)
Jul 15, 2026
9493351
#352 [Fix] JwtFilter 화이트리스트 /api/v1/auth 전체 매칭으로 logout 인증 우회, 500 에러 발생
si-zero Jul 16, 2026
00bfcb3
#356 [Feat] 이용약관/개인정보처리방침 페이지 백엔드 서빙
si-zero Jul 20, 2026
99c936d
#361 [Feat] 관리자용 테스트 계정 로컬 로그인 기능 추가
si-zero Jul 24, 2026
f0a837d
#360 [Refactor] 1인 1관점 제한을 해제하고 1인 다수 관점 등록 허용
si-zero Jul 24, 2026
9fb601f
#366 [Feat] 내 콘텐츠 활동 내역(댓글 탭)에 내가 작성한 관점 포함
si-zero Jul 24, 2026
6a1abde
#370 [Fix] 사전투표만 한 기록도 내 배틀 기록에 노출되는 버그
si-zero Jul 24, 2026
80f5b9c
[Feat] 관점 200자 제한 해제 (#375)
si-zero Aug 2, 2026
f9bc278
#378 [Feat] 관리자 예약 푸시알림(제목/소제목/발송시간) 설정 및 자동발송
si-zero Aug 8, 2026
99f4765
#382 [Fix] 내 콘텐츠 활동 댓글 정보 매핑 오류 수정 (#383)
jucheonsu Aug 9, 2026
79cb355
#387 [Refactor] 철학자 태그 제한 및 가치관 육각형 계산 보정 (#388)
jucheonsu Aug 13, 2026
bdbe3b3
#391 [Remove] 미사용 파일 정리 (비활성 CI 워크플로우 + 레거시 admin 프론트)
si-zero Aug 13, 2026
a82c575
#392 [CI/CD] Java CI 파이프라인 브랜치명 오류 및 지속적 빌드 실패 수정
si-zero Aug 13, 2026
70ccc90
#395 [Feat] 관리자용 푸시 알림 발송 테스트 API 추가
si-zero Aug 16, 2026
2e72bf7
#396 [Fix] Android FCM 푸시에 title/body가 실리지 않아 알림이 안 뜨던 문제 수정
si-zero Aug 16, 2026
1fc8488
#401 [Fix] FCM 만료된 디바이스 토큰이 정리되지 않아 반복 발송 실패 로그가 쌓이는 문제 수정
si-zero Aug 17, 2026
a4d3b31
#405 [Feat] 어드민 예약 알림 On/Off 전환 API 추가 및 관리자 알림 API 문서화
si-zero Aug 26, 2026
0b9a374
#407 [Feat] 어드민 유저 검색 API 추가
si-zero Aug 26, 2026
869de77
#409 [Feat] 어드민 공지사항 수정/삭제 API 추가
si-zero Aug 26, 2026
087fea5
#411 [Feat] 어드민 공지 발송 결과 조회 API 추가
si-zero Aug 26, 2026
4bdbdf3
#413 [Feat] 어드민 공지 발송 대상자 수 미리보기 API 추가
si-zero Aug 26, 2026
76e8097
#414 [Feat] 어드민 공지 작성 옵션(카테고리) 조회 API 추가
si-zero Aug 26, 2026
26116e8
#415 [Feat] 어드민 예약 알림 테스트 발송 API 추가
si-zero Aug 26, 2026
4234c29
#423 [Docs] 누락된 마이그레이션 SQL 파일 보완 (#409, #411)
si-zero Aug 30, 2026
7aaa1f4
#421 [Feat] 유저 일별 활동 트래킹(DAU/MAU 기반) 인프라 구축
si-zero Aug 30, 2026
d3b9343
#425 [Feat] 어드민 대시보드 오늘 요약 API 추가
si-zero Sep 1, 2026
2129d35
#427 [Feat] 어드민 대시보드 DAU/MAU 추이 API 추가
si-zero Sep 1, 2026
d8fe672
#429 [Feat] 어드민 대시보드 신규 가입자 추이 API 추가
si-zero Sep 1, 2026
6eea0d4
#431 [Feat] 어드민 대시보드 배틀 참여율 API 추가
si-zero Sep 1, 2026
1e134bc
#433 [Feat] 어드민 대시보드 출석 체크율 API 추가
si-zero Sep 1, 2026
cba91cb
#436 [Feat] 어드민 대시보드 크레딧 지급 현황 API 추가
si-zero Sep 2, 2026
23496d1
#444 [Refactor] AWS S3를 Railway Bucket으로 마이그레이션 (개발 서버)
si-zero Sep 7, 2026
7b0b68f
🐛 Fix: BattleVoteServiceImplTest 시간대 불일치로 15시(UTC) 이후 CI 실패 (#442)
Roy-wonji Sep 7, 2026
c99065e
✨ Feat: ad.picke.store 제휴 광고(쿠팡 파트너스·애드픽) 연동 (#443)
Roy-wonji Sep 8, 2026
d22f21a
#441 [Fix] 쿠팡 파트너스 ID 환경변수 누락 시 앱 부팅 실패 해소 (#448)
Roy-wonji Sep 8, 2026
9745f2e
#441 [Fix] 광고 스웨거 기본 서버를 문서를 연 주소로 바꿈 (#449)
Roy-wonji Sep 8, 2026
e5a0422
#441 [Fix] 필수 쿼리 파라미터 누락 시 500 대신 400 응답 (#450)
Roy-wonji Sep 8, 2026
c3bd52b
#441 [Feat] 광고 스웨거 탭에 운영 광고 도메인 추가 (#451)
Roy-wonji Sep 8, 2026
476b0dd
#441 [Feat] 애드픽 쇼핑·핫딜 상품을 광고 재고로 편입 (#454)
Roy-wonji Sep 8, 2026
495f772
#441 [Feat] 애드픽 쇼핑 상품에 더 높은 로테이션 가중치 부여 (#457)
Roy-wonji Sep 9, 2026
093278c
AdFit 수익과 비용을 기록해 관리자 ROI를 확인하도록 지원
Roy-wonji Sep 10, 2026
73a4214
test: 사용자 광고 API 조회 계약 검증 (#460)
Roy-wonji Sep 10, 2026
27e7b79
fix: 사용자 광고에서 설치·가입 캠페인 제외 (#463)
Roy-wonji Sep 10, 2026
a612828
feat: AdFit 일별 수익 자동 조회 연결 (#465)
Roy-wonji Sep 10, 2026
6891d33
#455 [Feat] 배틀 시나리오 감정 톤 + 철학자 보이스 매핑 인프라
si-zero Sep 10, 2026
291918c
#467 [Feat] 배틀 대본 붙여넣기 파서 (배틀·시나리오 자동 등록)
si-zero Sep 11, 2026
3ed96b8
#469 [Feat] 배틀 대본 파서 대사를 문장 종결부호 단위로 분리
si-zero Sep 11, 2026
8efa2ad
#471 [Feat] 배틀 대본 파서 summary·description·선택지 title 매핑 수정
si-zero Sep 11, 2026
073dfd5
#473 [Feat] 시나리오 선택지에 선택의 시간 설명 텍스트(title) 추가
si-zero Sep 11, 2026
58ce7ad
#475 [Feat] 철학자 보이스 매핑에 이미지 키(image_key) 추가
si-zero Sep 12, 2026
759f746
#477 [Docs] 철학자 보이스 매핑에 실제 이미지 키 반영
si-zero Sep 12, 2026
7a41a14
#479 [Fix] 철학자 보이스 imageKey 응답을 호출 가능한 경로로 변환
si-zero Sep 12, 2026
4cf5227
#481 [Fix] 배틀 옵션 태그 수정 시 유니크 제약 위반(23505) 발생
si-zero Sep 12, 2026
a7b0c3b
#483 [Fix] 로컬 draft→S3 promote가 DB 트랜잭션과 분리되어 orphan 파일/유실 위험
si-zero Sep 12, 2026
e3e5bd8
feat: 애드픽 쇼핑 재고 확대 + AdFit 세션 갱신 + Mixpanel·Sentry 지표 (#487)
Roy-wonji Sep 14, 2026
d6566c3
개발 서버도 운영과 같은 관리자 지표 계약을 제공하게 한다
Roy-wonji Sep 14, 2026
9ef0aa0
feat: Sentry 전체 관측 데이터와 가입 추이 개발 반영 (#492)
Roy-wonji Sep 14, 2026
df9acca
[Fix] Sentry trace metric 시계열 집계 오류 수정 (#494)
Roy-wonji Sep 14, 2026
88661ac
feat: Sentry 회원가입 지표를 일별 시계열로 제공 (#496)
Roy-wonji Sep 14, 2026
1e01dde
feat: Sentry 사용자 이벤트 전체 집계 (#498)
Roy-wonji Sep 14, 2026
8935a92
fix: Sentry 이벤트 전체 조회 복구 (#501)
Roy-wonji Sep 14, 2026
724c75e
fix: Sentry 회원가입 사용자 결측값 유지 (#503)
Roy-wonji Sep 14, 2026
e1fa37d
fix: Sentry 이벤트 배치 누락 제거 (#505)
Roy-wonji Sep 14, 2026
9d43c11
feat: Mixpanel 이벤트 퍼널 이탈율 조회 (#507)
Roy-wonji Sep 28, 2026
9a298dc
#509 [Fix] 시나리오 병합 오디오 재생성 시 URL 갱신
si-zero Oct 1, 2026
b9ca424
#513 [Fix] 어드민 시나리오 조회 응답에 노드 연결 정보 추가
si-zero Oct 1, 2026
ce2f45e
#517 [Feat] 배틀 썸네일 AI 후보 생성 API
si-zero Oct 4, 2026
4bb85ab
#519 [Fix] 썸네일 후보를 제목 기반 장면으로 생성하고 안전 필터 거절 구분
si-zero Oct 4, 2026
9d1bb90
#521 [Feat] 썸네일 프롬프트 구조화 및 레퍼런스 이미지 확대
si-zero Oct 14, 2026
a2ba1ca
#524 [Feat] 썸네일 디자인 시스템 팔레트 적용 및 레퍼런스 2장으로 축소
si-zero Oct 19, 2026
1faefbc
#523 [Docs] 클래스 기능 ERD 정의 (#527)
HYH0804 Oct 19, 2026
ec60a31
#528 [Feat] 클래스 엔티티 및 스키마 SQL 정의 (#529)
HYH0804 Oct 19, 2026
4b9f0ce
Fix: 이용권·클래스 PRD 정합성 보완 (본인 재등록, 코드 미리보기, 환불 이용권 마감) (#536)
HYH0804 Oct 19, 2026
27eb7fb
#537 [Feat] 클래스 초대코드 참여 및 멤버 관리 API (#538)
HYH0804 Oct 19, 2026
dabc6e1
#539 [Feat] 클래스 내 배틀 진행 class_id 스코핑 및 일반 배틀 격리 (#540)
HYH0804 Oct 19, 2026
36551e4
#541 [Feat] 클래스 결과·AI 리포트 (개인 피드백·클래스 인사이트) (#542)
HYH0804 Oct 19, 2026
c2ec790
#543 [Feat] 어드민 펀딩 명단 가져오기·이용권 코드 발급·재발급·환불 (#544)
HYH0804 Oct 19, 2026
b7e3037
#545 [Feat] 클래스 커스텀 배틀 AI 주제 제안 및 수정 검증 API
si-zero Oct 21, 2026
03933c0
#547 [Feat] 검증된 주제로 커스텀 배틀 클래스 생성
si-zero Oct 21, 2026
fe3f717
#548 [Feat] 커스텀 배틀 AI 철학자 3라운드 토론
si-zero Oct 21, 2026
e0aee2f
#551 [Feat] 클래스 Picke 배틀 추천 API 및 어드민 배틀 클래스 설정
si-zero Oct 21, 2026
bee8cc6
#553 [Feat] 클래스 참여 코드 확인 API 및 삭제된 클래스 구분
si-zero Oct 21, 2026
da247e3
#555 [Docs] 클래스 API 명세서
si-zero Oct 21, 2026
6c2a2b0
#557 [Fix] 클래스 의견·댓글 좋아요 API 클래스 권한 검사
si-zero Oct 21, 2026
50ec87b
#559 [Fix] 클래스 의견·댓글 신고 API 클래스 권한 검사
si-zero Oct 21, 2026
e821c20
#563 [Feat] AI 철학자 토론 반박 입력 차단 (위협·개인정보)
si-zero Oct 22, 2026
c44e672
#565 [Feat] 이용 종료 후 30일 지난 클래스 원문·AI 대화·리포트 파기
si-zero Oct 22, 2026
1683a90
#567 [Feat] AI 철학자 토론에 대상 수준별 반론 난이도 반영
si-zero Oct 22, 2026
b22072b
#569 [Feat] 클래스 기획 확인 반영 (AI 주장·반박 길이·TTS 청취 필수·필수 의견 삭제 차단)
si-zero Oct 23, 2026
c53e30f
#571 [Feat] 어드민 배틀 목록 수준 노출 및 기존 배틀 수준 초급 일괄 설정
si-zero Oct 23, 2026
179f964
#573 [Fix] 클래스 배틀 테스트 날짜를 KST 기준으로 생성
si-zero Oct 23, 2026
1f63e5c
#575 [Feat] 클래스 멤버 초대코드 노출 및 의견·댓글 작성자 클래스 표시 이름 노출 (#579)
HYH0804 Oct 28, 2026
eea7198
#576 [Feat] 클래스 생성 중복 요청 방지 (#580)
HYH0804 Oct 28, 2026
f2134e1
#577 [Feat] 중간투표 있는 기존 콘텐츠의 클래스 중간투표 수집 및 리포트 반영 (#581)
HYH0804 Oct 28, 2026
1dc6db2
#589 [Feat] 미등록 이용권 등록기한 임박 알림 대상 조회·발송 기록 (#590)
HYH0804 Oct 28, 2026
96e3f4a
#561 [Feat] 텀블벅 후원자 엑셀 연동 및 클래스 이용권 운영 기능 확장 (#562)
jucheonsu Oct 28, 2026
8fa5930
#578 [Feat] 관점·댓글 200자 초과 제한 (#591)
jucheonsu Oct 28, 2026
8dcfcdb
#582 [Feat] 클래스 리포트 내 활동 조회 API 추가 (#592)
jucheonsu Oct 28, 2026
d82439e
#583 [Feat] 홈·배틀 카드 응답 필드 보완 (#593)
jucheonsu Oct 28, 2026
965b3e8
#584 [Feat] 회원 탈퇴 시 클래스·좌석·콘텐츠 처리 (#594)
jucheonsu Oct 28, 2026
57142f3
#585 [Feat] 잘못된 커서·DB 제약 오류를 400/409로 정리 (#595)
jucheonsu Oct 28, 2026
1e2d311
#586 [Feat] 관리자 펀딩 주문·이용권 대시보드 API 확장 (#596)
jucheonsu Oct 28, 2026
4612d98
#587 [Feat] 텀블벅 Start·Run 등록 및 전달 흐름 분리
jucheonsu Oct 28, 2026
517791d
#588 [Refactor] com 패키지 타입 참조를 import로 이동 (#598)
jucheonsu Oct 28, 2026
9770918
#603 [Fix] JwtFilter 계정 상태 검사에서 정상 계정이 401로 막히는 문제 수정
si-zero Nov 2, 2026
2414863
#606 [Fix] 회원 탈퇴·펀딩 변경 이후 깨진 기존 테스트 수정
si-zero Nov 2, 2026
0eb3bf4
#602 [Feat] AI 철학자 반박 프롬프트 고도화 (철학자 프로필·수준별 기준·말풍선)
si-zero Nov 7, 2026
e7f5677
#608 [Feat] AI 주제 제안 실패 사유 응답 추가
si-zero Nov 7, 2026
bb309fb
#610 [Feat] 기획 확정 배틀별 수준 일괄 반영 SQL 추가
si-zero Nov 7, 2026
8145e85
#612 [Fix] 펀딩 다중 수량 발급 이후 깨진 펀딩 테스트와 주문 목록 500 수정
si-zero Nov 7, 2026
7e72bfc
#616 [Fix] 운영 DB에 남은 이전 펀딩 스키마 정리 SQL 추가
si-zero Nov 7, 2026
11ce3d5
#618 [Feat] 배틀 수준 반영 SQL에 운영 배틀 이전 제목 추가
si-zero Nov 7, 2026
8a483fc
#620 [Feat] 기획 답변에 따른 남은 배틀 수준 반영 SQL 추가
si-zero Nov 8, 2026
3278313
#622 [Feat] 어드민 배틀 공개·보관 상태 변경 API
si-zero Nov 8, 2026
534f261
Merge remote-tracking branch 'origin/main' into merge/#624
si-zero Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/api-specs/battle-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@
### 2.6 배틀 삭제
- `DELETE /api/v1/admin/battles/{battleId}`

### 2.7 배틀 공개·보관 상태 변경
- `PATCH /api/v1/admin/battles/{battleId}/status`
- 요청 본문: `{ "status": "ARCHIVED" }` (`PUBLISHED` 공개 / `ARCHIVED` 보관)
- 공개 ↔ 보관 전환만 한다. 보관된 배틀은 사용자 화면(목록·상세·추천)에 나오지 않고, 다시 공개하면 그대로 보인다.
- 같은 상태로 보내면 변경 없이 통과한다.
- 임시저장(`PENDING`) 배틀은 배틀 수정(만들기)으로 발행한다. 임시 이미지 이동이 함께 일어나야 하기 때문이다.
- 응답: `AdminBattleDetailResponse`

| 상황 | 응답 |
|---|---|
| 그 밖의 전환 (임시저장 → 공개, 공개 → 임시저장, 반려 등) | 409 `BATTLE_409_STATUS` |
| 삭제된 배틀 | 404 `BATTLE_404` |

---

## 3. 상태/정책 메모
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swyp.picke.domain.admin.controller;

import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleClassSettingsRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleStatusRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleThumbnailCandidateRequest;
import com.swyp.picke.domain.admin.dto.battle.response.AdminBattleThumbnailCandidatesResponse;
Expand Down Expand Up @@ -89,6 +90,17 @@ public ApiResponse<AdminBattleDetailResponse> updateClassSettings(
return ApiResponse.onSuccess(adminBattleService.updateClassSettings(battleId, request));
}

@Operation(summary = "배틀 공개·보관 상태 변경",
description = "공개(PUBLISHED) ↔ 보관(ARCHIVED) 전환만 합니다. 보관된 배틀은 사용자 화면에 나오지 않습니다. "
+ "임시저장 배틀은 배틀 수정(만들기)으로 발행하며, 그 밖의 전환은 409 BATTLE_409_STATUS, 삭제된 배틀은 404 BATTLE_404.")
@PatchMapping("/{battleId}/status")
public ApiResponse<AdminBattleDetailResponse> updateStatus(
@PathVariable Long battleId,
@RequestBody @Valid AdminBattleStatusRequest request
) {
return ApiResponse.onSuccess(adminBattleService.updateStatus(battleId, request));
}

@Operation(summary = "배틀 삭제")
@DeleteMapping("/{battleId}")
public ApiResponse<AdminBattleDeleteResponse> deleteBattle(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.swyp.picke.domain.admin.dto.battle.request;

import com.swyp.picke.domain.battle.enums.BattleStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;

/** 공개(PUBLISHED) ↔ 보관(ARCHIVED) 전환만 받는다. 임시저장 배틀의 첫 발행은 배틀 수정(만들기)으로 한다 (#622) */
@Schema(description = "배틀 공개·보관 상태 변경 요청")
public record AdminBattleStatusRequest(

@Schema(description = "바꿀 상태. PUBLISHED(공개) 또는 ARCHIVED(보관)", example = "ARCHIVED")
@NotNull
BattleStatus status
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swyp.picke.domain.admin.service;

import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleClassSettingsRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleStatusRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleUpdateRequest;
import com.swyp.picke.domain.admin.dto.battle.response.AdminBattleDeleteResponse;
Expand Down Expand Up @@ -38,6 +39,10 @@ public AdminBattleDetailResponse updateClassSettings(Long battleId, AdminBattleC
return battleService.updateClassSettings(battleId, request);
}

public AdminBattleDetailResponse updateStatus(Long battleId, AdminBattleStatusRequest request) {
return battleService.updateStatus(battleId, request);
}

public AdminBattleDeleteResponse deleteBattle(Long battleId) {
return battleService.deleteBattle(battleId);
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/swyp/picke/domain/battle/entity/Battle.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,9 @@ public void publish() {
this.status = BattleStatus.PUBLISHED;
}

/** 어드민 공개·보관 전환 (#622). 삭제된 배틀과 그 밖의 전환은 서비스에서 막는다 */
public void changePublicationStatus(BattleStatus status) {
this.status = status;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.swyp.picke.domain.battle.service;

import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleClassSettingsRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleStatusRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleUpdateRequest;
import com.swyp.picke.domain.admin.dto.battle.response.AdminBattleDeleteResponse;
Expand Down Expand Up @@ -62,6 +63,8 @@ public interface BattleService {

AdminBattleDetailResponse updateClassSettings(Long battleId, AdminBattleClassSettingsRequest request);

AdminBattleDetailResponse updateStatus(Long battleId, AdminBattleStatusRequest request);

AdminBattleDeleteResponse deleteBattle(Long battleId);

void seedBattle(Long battleId, int botCount);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.swyp.picke.domain.battle.converter.BattleConverter;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleClassSettingsRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleStatusRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleOptionRequest;
import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleUpdateRequest;
Expand Down Expand Up @@ -520,6 +521,26 @@ public AdminBattleDetailResponse updateBattle(Long battleId, AdminBattleUpdateRe
return battleConverter.toAdminDetailResponse(battle, getTagsByBattle(battle), updatedOptions, optionTagsMap);
}

/** 공개 ↔ 보관만 허용한다. 보관된 배틀은 PUBLISHED 조건으로 걸러져 사용자 화면에 나오지 않는다 (#622) */
@Override
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public AdminBattleDetailResponse updateStatus(Long battleId, AdminBattleStatusRequest request) {
Battle battle = findById(battleId);
if (battle.getDeletedAt() != null) {
throw new CustomException(ErrorCode.BATTLE_NOT_FOUND);
}
BattleStatus current = battle.getStatus();
BattleStatus target = request.status();
boolean allowed = (current == BattleStatus.PUBLISHED && target == BattleStatus.ARCHIVED)
|| (current == BattleStatus.ARCHIVED && target == BattleStatus.PUBLISHED);
if (current != target && !allowed) {
throw new CustomException(ErrorCode.BATTLE_STATUS_CHANGE_INVALID);
}
battle.changePublicationStatus(target);
return getAdminBattleDetail(battleId);
}

@Override
@Transactional
@PreAuthorize("hasRole('ADMIN')")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public enum ErrorCode {
BATTLE_ALREADY_PUBLISHED(HttpStatus.CONFLICT, "BATTLE_409_PUB", "이미 발행된 배틀입니다."),
BATTLE_OPTION_NOT_FOUND(HttpStatus.NOT_FOUND, "BATTLE_OPT_404", "존재하지 않는 선택지입니다."),
BATTLE_INVALID_OPTION_COUNT(HttpStatus.BAD_REQUEST, "BATTLE_400_OPT", "배틀 타입에 맞지 않는 선택지 개수입니다."),
BATTLE_STATUS_CHANGE_INVALID(HttpStatus.CONFLICT, "BATTLE_409_STATUS", "공개된 배틀은 보관만, 보관된 배틀은 공개만 할 수 있습니다. 임시저장 배틀은 만들기로 발행해 주세요."),
BATTLE_CANNOT_REVERT_TO_PENDING(HttpStatus.BAD_REQUEST, "BATTLE_400_REVERT", "발행된 시나리오는 다시 대기(PENDING) 상태로 되돌릴 수 없습니다."),
BATTLE_NOT_QUIZ(HttpStatus.BAD_REQUEST, "BATTLE_400_QUIZ", "해당 배틀은 퀴즈 타입이 아닙니다."),
BATTLE_NOT_POLL(HttpStatus.BAD_REQUEST, "BATTLE_400_POLL", "해당 배틀은 투표 타입이 아닙니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
-- #610 기획 확정 배틀별 수준 반영. #571 에서 임시로 초급으로 채운 기존 배틀의 수준을 확정값으로 바꾼다.
-- 기획 목록 115개와, 기획과 제목이 다르게 등록된 배틀의 이전 제목 9개로 DB에 있는 배틀만 찾는다. 목록에 없거나 DB에 없는 배틀은 그대로 둔다.
-- 기획 목록 115개와, 기획과 제목이 다르게 등록된 배틀의 이전 제목 26개로 DB에 있는 배틀만 찾는다. 목록에 없거나 DB에 없는 배틀은 그대로 둔다.
-- 제목은 공백·문장부호를 빼고 비교한다. 사용자가 만든 배틀(USER)과 삭제된 배틀은 제외한다.
-- 카테고리는 같은 제목이 사회 초급·역사 중급에 있는 "살기 위해 반역의 왕위, 받아들일까?"에만 비교한다. 한 배틀에 기획 행이 둘 이상 맞으면 바꾸지 않는다.
-- 멱등하게 작성되어 재실행해도 안전하다. 한 트랜잭션으로 실행한다.
Expand Down Expand Up @@ -134,7 +134,24 @@ INSERT INTO battle_difficulty_plan (plan_no, title, category, difficulty) VALUES
('문학 001 (이전 제목)', '역사 왜곡인가, 창작의 자유인가?', NULL, 'INTERMEDIATE'),
('예술 001 (이전 제목)', 'AI는 예술을 창작할 수 있는가?', NULL, 'INTERMEDIATE'),
('예술 003 (이전 제목)', '클래식은 고상하고 대중음악은 가벼운가?', NULL, 'BEGINNER'),
('바이럴 004 (이전 제목)', '부모가 아이 이름에 종교적 의미를 담는 것은', NULL, 'INTERMEDIATE');
('바이럴 004 (이전 제목)', '부모가 아이 이름에 종교적 의미를 담는 것은', NULL, 'INTERMEDIATE'),
('역사 002 (이전 제목)', '오펜하이머, 영웅인가 악마인가?', NULL, 'ADVANCED'),
('문학 001 (이전 제목)', '역사 왜곡, 창작의 자유일까?', NULL, 'INTERMEDIATE'),
('문학 006 (이전 제목)', '로미오와 줄리엣, 숭고한 로맨스인가?', NULL, 'INTERMEDIATE'),
('사회 014 (이전 제목)', '외모도 실력일까?', NULL, 'BEGINNER'),
('철학 023 (이전 제목)', '죽음을 앞둔 사람에게 진실을 말해야 할까?', NULL, 'ADVANCED'),
('역사 005 (이전 제목)', '이방원, 폭군인가 혁명가인가?', NULL, 'INTERMEDIATE'),
('철학 026 (이전 제목)', '일은 좋아하는 걸 택할까, 잘하는 걸 택할까?', NULL, 'BEGINNER'),
('철학 025 (이전 제목)', '애인이 내 친구의 깻잎 떼줘도 괜찮을까?', NULL, 'BEGINNER'),
('철학 013 (이전 제목)', '사라질 위기의 언어는 의무 교육해야 할까?', NULL, 'INTERMEDIATE'),
('사회 008 (이전 제목)', '노키즈존은 영업상의 자유일까?', NULL, 'BEGINNER'),
('바이럴 007 (이전 제목)', '탕수육, 부먹인가 찍먹인가?', NULL, 'BEGINNER'),
('바이럴 014 (이전 제목)', '축의금, 친한 만큼 낼까 형편만큼 낼까?', NULL, 'INTERMEDIATE'),
('바이럴 019 (이전 제목)', '직장 동료 차 뒷좌석에 타는 건 비매너일까?', NULL, 'INTERMEDIATE'),
('문학 005 (이전 제목)', '5분 요약으로 영화를 봤다고 해도 될까?', NULL, 'BEGINNER'),
('바이럴 030 (이전 제목)', '불매운동은 소비자의 권리인가?', NULL, 'INTERMEDIATE'),
('바이럴 026 (이전 제목)', '진격의 거인, 에렌은 영웅인가 학살자인가?', NULL, 'ADVANCED'),
('바이럴 024 (이전 제목)', '부모의 극단적 개입, 사랑인가 착취인가?', NULL, 'INTERMEDIATE');

-- 제목은 공백·문장부호를 빼고 비교한다
CREATE TEMP TABLE battle_difficulty_match AS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- #616 운영 DB 에 남아 있는 이전 펀딩 스키마 정리.
-- 운영에는 v3.0.0 이전 펀딩 기능이 만든 funding_orders 가 있어 campaign_id 가 BIGINT(funding_campaigns FK)이고,
-- 지금 엔티티가 쓰지 않는 NOT NULL 컬럼(capacity, buyer_name, reward_line_id, reward_type, additional_support_amount)이 남아 있다.
-- 그대로 두면 주문 저장이 NOT NULL 위반으로 실패하고, ddl-auto 의 campaign_id VARCHAR(50) 변경도 FK 때문에 막힌다.
-- 지금 엔티티 기준(dev 와 같은 구조)으로 맞춘다. 멱등하게 작성되어 재실행해도 안전하다. 한 트랜잭션으로 실행한다.
-- 행이 있으면 아무것도 하지 않고 멈춘다 (이전 스키마로 쌓인 데이터를 지우지 않기 위해).

BEGIN;

DO $$
BEGIN
IF EXISTS (SELECT 1 FROM funding_orders) THEN
RAISE EXCEPTION 'funding_orders 에 행이 있어 정리를 멈춥니다. 데이터를 먼저 확인하세요.';
END IF;
END $$;

ALTER TABLE funding_orders DROP CONSTRAINT IF EXISTS fk_funding_order_campaign;
ALTER TABLE funding_orders DROP CONSTRAINT IF EXISTS uk_funding_order_external_line;
ALTER TABLE funding_orders DROP CONSTRAINT IF EXISTS uk_funding_order_supporter_line;
ALTER TABLE funding_orders DROP CONSTRAINT IF EXISTS funding_orders_capacity_check;
DROP INDEX IF EXISTS idx_funding_order_buyer_name;
DROP INDEX IF EXISTS idx_funding_order_contact_hash;

ALTER TABLE funding_orders ALTER COLUMN campaign_id TYPE VARCHAR(50) USING campaign_id::text;

ALTER TABLE funding_orders
DROP COLUMN IF EXISTS external_order_id,
DROP COLUMN IF EXISTS external_supporter_number,
DROP COLUMN IF EXISTS reward_line_id,
DROP COLUMN IF EXISTS reward_type,
DROP COLUMN IF EXISTS capacity,
DROP COLUMN IF EXISTS buyer_name,
DROP COLUMN IF EXISTS buyer_contact_encrypted,
DROP COLUMN IF EXISTS buyer_contact_search_hash,
DROP COLUMN IF EXISTS source_version,
DROP COLUMN IF EXISTS external_gift_text,
DROP COLUMN IF EXISTS reward_amount,
DROP COLUMN IF EXISTS additional_support_amount,
DROP COLUMN IF EXISTS total_support_amount,
DROP COLUMN IF EXISTS notification_requested,
DROP COLUMN IF EXISTS tumblbug_notification_status,
DROP COLUMN IF EXISTS tumblbug_payment_status,
DROP COLUMN IF EXISTS tumblbug_gift_delivery_status,
DROP COLUMN IF EXISTS source_snapshot;

COMMIT;
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,24 @@ INSERT INTO battle_difficulty_plan (plan_no, title, category, difficulty) VALUES
('문학 001 (이전 제목)', '역사 왜곡인가, 창작의 자유인가?', NULL, 'INTERMEDIATE'),
('예술 001 (이전 제목)', 'AI는 예술을 창작할 수 있는가?', NULL, 'INTERMEDIATE'),
('예술 003 (이전 제목)', '클래식은 고상하고 대중음악은 가벼운가?', NULL, 'BEGINNER'),
('바이럴 004 (이전 제목)', '부모가 아이 이름에 종교적 의미를 담는 것은', NULL, 'INTERMEDIATE');
('바이럴 004 (이전 제목)', '부모가 아이 이름에 종교적 의미를 담는 것은', NULL, 'INTERMEDIATE'),
('역사 002 (이전 제목)', '오펜하이머, 영웅인가 악마인가?', NULL, 'ADVANCED'),
('문학 001 (이전 제목)', '역사 왜곡, 창작의 자유일까?', NULL, 'INTERMEDIATE'),
('문학 006 (이전 제목)', '로미오와 줄리엣, 숭고한 로맨스인가?', NULL, 'INTERMEDIATE'),
('사회 014 (이전 제목)', '외모도 실력일까?', NULL, 'BEGINNER'),
('철학 023 (이전 제목)', '죽음을 앞둔 사람에게 진실을 말해야 할까?', NULL, 'ADVANCED'),
('역사 005 (이전 제목)', '이방원, 폭군인가 혁명가인가?', NULL, 'INTERMEDIATE'),
('철학 026 (이전 제목)', '일은 좋아하는 걸 택할까, 잘하는 걸 택할까?', NULL, 'BEGINNER'),
('철학 025 (이전 제목)', '애인이 내 친구의 깻잎 떼줘도 괜찮을까?', NULL, 'BEGINNER'),
('철학 013 (이전 제목)', '사라질 위기의 언어는 의무 교육해야 할까?', NULL, 'INTERMEDIATE'),
('사회 008 (이전 제목)', '노키즈존은 영업상의 자유일까?', NULL, 'BEGINNER'),
('바이럴 007 (이전 제목)', '탕수육, 부먹인가 찍먹인가?', NULL, 'BEGINNER'),
('바이럴 014 (이전 제목)', '축의금, 친한 만큼 낼까 형편만큼 낼까?', NULL, 'INTERMEDIATE'),
('바이럴 019 (이전 제목)', '직장 동료 차 뒷좌석에 타는 건 비매너일까?', NULL, 'INTERMEDIATE'),
('문학 005 (이전 제목)', '5분 요약으로 영화를 봤다고 해도 될까?', NULL, 'BEGINNER'),
('바이럴 030 (이전 제목)', '불매운동은 소비자의 권리인가?', NULL, 'INTERMEDIATE'),
('바이럴 026 (이전 제목)', '진격의 거인, 에렌은 영웅인가 학살자인가?', NULL, 'ADVANCED'),
('바이럴 024 (이전 제목)', '부모의 극단적 개입, 사랑인가 착취인가?', NULL, 'INTERMEDIATE');

-- 제목은 공백·문장부호를 빼고 비교한다
CREATE TEMP TABLE battle_difficulty_match AS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- #620 기획 문의로 남겨 둔 배틀의 기획 답변 반영 (#610, #618 이후).
-- 제목은 V20261107_01 과 같이 공백·문장부호를 빼고 비교한다. 사용자가 만든 배틀(USER)과 삭제된 배틀은 제외한다.
-- "행복은 순간의 감정일까, 남겨진 기억일까?" 는 주제가 모호해 내리기로 해 어드민에서 처리한다 (이 SQL 대상 아님).
-- 멱등하게 작성되어 재실행해도 안전하다. 한 트랜잭션으로 실행한다.

BEGIN;

-- 잘못한 사람, 엄벌할까 사랑으로 감쌀까? → 초급
UPDATE battles
SET difficulty = 'BEGINNER'
WHERE regexp_replace(lower(title), '[[:space:][:punct:]‘’“”·…「」『』]', '', 'g') = '잘못한사람엄벌할까사랑으로감쌀까'
AND deleted_at IS NULL AND creator_type <> 'USER'
AND difficulty IS DISTINCT FROM 'BEGINNER';

-- 살기 위해 반역의 왕위, 받아들일까? → 기존 콘텐츠 파일과 같이 역사, 중급
-- 같은 제목이 기획 표에 사회 초급·역사 중급으로 있어 카테고리가 비어 있는 배틀만 역사로 채운다
UPDATE battles
SET category = COALESCE(category, 'HISTORY'),
difficulty = 'INTERMEDIATE'
WHERE regexp_replace(lower(title), '[[:space:][:punct:]‘’“”·…「」『』]', '', 'g') = '살기위해반역의왕위받아들일까'
AND deleted_at IS NULL AND creator_type <> 'USER'
AND (category IS NULL OR category = 'HISTORY')
AND (category IS NULL OR difficulty IS DISTINCT FROM 'INTERMEDIATE');

COMMIT;
Loading
Loading