Sprint 05 — Season System 사전 조사 리서치
작성: 2026-04-21 (code-lead) · Sprint 00 버퍼 기간 레일 부설 건 문서 상태: v1.0 APPROVED (PD 승인 2026-04-21) — Sprint 05 착수 시 본 문서 §1~§10 이행 필수 범위: Sprint 05 (2026-09-? 예정) 착수 전 기술 옵션 비교 + Open Question + TodoWrite 스케치
PD 승인 결정표 (2026-04-21)
| Q | 결정 | 코멘트 |
|---|---|---|
| Q1 시즌 기간 | B 60일 | 30일은 컨텐츠 소진/운영 과부하 / 90일은 이탈 위험. 60일 = 운영 4~6h/월 + 중위 신선도. 수치 세부 튜닝은 game-designer Sprint 05 W1에서 확정 |
Q2 OnSeasonEnd 발화 | Phase 4 (Sprint 05) = A 관리자 수동 → Phase 5 = C Firebase Remote Config 승격 | 로컬 타이머(B) 는 시계 조작 치팅에 IdleTicker 역전 가드도 무력. ISeasonClock 추상화로 Phase 5 교체 비용 2~3h 한정 |
| Q3 maxFloor 리셋 정책 | C 리셋 + 스냅샷 보존 | "시즌별 리더보드 = 공평한 출발점" 원칙 + permanent 프레스티지 영구화와 균형. IdleTicker maxFloor 필드와 분리 필수 |
| Q4 4대 이월 Sprint 05 투입 범위 | 3건 투입 (셋 + Mythic 제약 + MythicEssence 지갑) / Legendary 고정 Power = Sprint 06 이월 | 1인 15h/2주 예산 준수. 셋=시즌 차별화 필수 / Mythic 제약=B08~B09 실측 타이밍 / Essence 지갑=인프라만 선행 |
| Q5 리더보드 저장소 | C Firestore + 로컬 캐시 | Realtime DB는 랭킹 쿼리 취약. Firestore orderBy + limit 자연. 로컬 캐시 = 네트워크 단절 시 내 기록 보존 |
| Q6 50% retainRatio 라운딩 | B Round | 정수 연산 (total*50)/100 강제. 부동소수 오차 회피 |
| Q7 MythicEssence 위치 | B 신규 PlayerWalletState + 50% retain | 프레스티지 결합 회피. Q3 retainRatio 정책 일관. 소비처 Sprint 06 확장 여지 |
Sprint 05 공수 확정: Q4 3건 투입 시 W1 10h + W2 9h - Legendary 제외 5h = 14h/2주 (11~15h 예산 적합). Legendary 고정 Power는 Sprint 06으로 명시적 이월 (affix_pool Sprint 05+ 이월 4건 중 1건만 Sprint 06 재배정).
10축 교차 충돌 3건 해소:
- 축 #4 maxFloor 리셋 미정 → Q3 C안 (리셋 + 스냅샷)
- 축 #6
affix_pool.tsvv2 스키마 → Sprint 05 W1 game-designersetEffectId/mythicRestrictPolicy/essenceCost컬럼 추가 - 축 #10 Firebase 유예 정책 → Q2 2단계 승격 (Phase 4 수동 → Phase 5 Firebase)
Sprint 02 W2 MigrateSeasonalToPermanent 5개 부수 효과 필수:
seasonEnding=true플래그 선행 (이중 발화 차단)prestige.v1.json백업 →prestige.v1.bak.{timestamp}.jsonIdleTicker.FlushCatchUp()선행 호출 (누적 seasonal PP 반영 후 마이그레이션)OnSeasonEndUI 이벤트 발화 (시즌 종료 모달)maxFloor스냅샷 →seasonal_record.v1.json(Q3 C안 연결)
상위 근거: -designs/sprint-02-w2-prestige-tech-sketch.mdv1.0 APPROVED (PpChannel 2채널 / MigrateSeasonalToPermanent(retainRatio) 시그니처) -designs/sprint-03-idle-research.mdv1.0 APPROVED (IdleTicker/OnIdleReward) -designs/sprint-01-w2-importer-design.mdv1.0 APPROVED (MilestoneGranter/OnFloorReached) -designs/affix_pool_README.mdv1.0 APPROVED (Sprint 05 이월 4건: 셋/Legendary 고정/Mythic 제약/MythicEssence) -designs/prestige_system_spec.mdv1.0 APPROVED (Q3 retainRatio=0.5 / Q4 소프트캡 Lv5) - 메모리:project_prestige_decisions.md/project_sprint02_w2_prestige_tech_sketch.md/project_transcendent_reward_decisions.md-sprints/shared-checkpoints.mdv1.0 (CP-M01~CP-M23, 본 문서는 CP-M24~CP-M28 확장 제안)
0. TL;DR (PD 1분)
- 핵심 질문: 이미 Sprint 02 W2에서 예약된
MigrateSeasonalToPermanent(retainRatio)+PpChannel.Seasonal2채널 +OnSeasonEnd훅의 발화자를 Sprint 05에서 어떻게 붙일 것인가. 그리고 Sprint 04 이월 4건(셋/Legendary 고정/Mythic 제약/MythicEssence)을 어디까지 동시 투입할 것인가. - 권장 발화 전략: 옵션 A 관리자 수동 트리거(Phase 5까지) → 옵션 C Firebase Remote Config(Phase 5+ 서버 권위). B 로컬 타이머는 치팅·복귀 실패 모두 취약 → 배제.
- 4대 이월 우선순위 권장: 셋 효과(시즌 차별화 레버리지) → Mythic 제약(b 중복 슬롯 금지) → MythicEssence(Q1 재검토 트리거) → Legendary 고정 Power. 4건 동시 투입은 1인 파트타임 예산(11~15h/2주) 초과.
- Firebase 도입 범위: 시즌 리더보드 +
seasonEndUtcRemote Config만. 프레스티지 세이브 동기는 여전히 Phase 5+로 유예. - Sprint 05 범위 권장: (a)
OnSeasonEnd발화 + 마이그레이션 실구현 (b) 시즌 리더보드 로컬 전용 시작 + Firebase 스키마 예약 (c) 셋 효과 1건 (4~6셋 구조만) (d) 나머지 3건은 Sprint 06 이월. - Open Question 7개 (§7). Sprint 05 TodoWrite 10건 W1/W2 각 5 (§8). 리스크 3건 (§9). 체크 5개 확장 (§10).
1. 시즌 시스템 요구사항 정의
1.1 시즌 개념의 최소 정의
- 시즌: "유한 기간 동안만 유효한 병렬 진행 축 + 종료 시 일부 가치 영구화 + 다음 시즌 리셋 반복".
- 방치형 RPG + 무한던전 장르 특성상 유저의 영속 성장(Permanent)과 시즌 성장(Seasonal)이 명확히 분리돼 있어야 동기가 분산되지 않는다. 이 분리는 이미 Sprint 02 W2
PpChannel : byte { Permanent, Seasonal }로 구현 레벨에서 예약 완료.
1.2 시즌 기간 옵션 (1인 파트타임 운영 관점)
수치 튜닝은 game-designer 영역. 본 문서는 엔지니어링 관점의 코스트 구조만 제시.
| 기간 | 운영 코스트 (1인/시즌) | 컨텐츠 소비 위험 | 리더보드 경쟁성 |
|---|---|---|---|
| 30일 | 월 1회 리셋·이벤트 발화·검증 8~12h/월 고정비 | 빠른 소진, 신선도↑ | 단기 러쉬 활발 |
| 60일 | 2개월 1회 4~6h/월 아마겟비 | 중간 | 중위 |
| 90일 | 분기 1회 2~3h/월 아마겟비 | 느린 소진, 이탈 위험 | 긴 호흡 |
| 무기한 (관리자 수동) | 이벤트 발생 시 1~2h | 자율적 | 정체성 약함 |
- 권장 엔지니어링 구조: 기간을 하드코딩하지 않음.
seasonEndUtc를SeasonConfigSO(로컬) + Firebase Remote Config 중 우선순위 있는 단일 지점에서 주입. PD/game-designer가 60→30→90 어느 쪽으로 바꿔도 코드 수정 없이 컨피그만 갱신.
1.3 시즌 시작·종료 트리거 세 경로
| 트리거 | 장점 | 단점 | Sprint 05 적합성 |
|---|---|---|---|
| 자동 (로컬 타이머) | 서버 無 | 시계 조작·복귀 실패 치명 | ❌ |
| 관리자 수동 | 1인 운영 유연, 긴급 종료 가능 | 유저 예측 불가 | Phase 4(개발) ✅ |
| Firebase Remote Config 기반 | 서버 권위, 일괄 전파 | Firebase 러닝커브 | Phase 5+ ✅ |
- 권장: Phase 4(Sprint 05 초~중)는 관리자 수동 + 디버그 메뉴로 시작, Phase 5에서 Remote Config로 승격. 양쪽 모두 동일한
ISeasonClock인터페이스 뒤에 숨기면 교체 코스트 2~3h.
1.4 시즌 보상 카테고리
| 카테고리 | 채널 | 영구화 정책 | 예시 |
|---|---|---|---|
| Seasonal PP | PpChannel.Seasonal | 시즌 종료 시 50% → Permanent 전환 (Q3) | 일반 드립·보스·마일스톤 |
| MythicEssence (시즌 한정 재화) | 별도 지갑 | 시즌 종료 시 삭제 또는 영구 이관 — §4.4 결정 필요 | Mythic 재롤/해체 보상 |
| 시즌 한정 어픽스 | 신규 TSV seasonal_affix_pool_s<N>.tsv | 시즌 종료 후 재롤 불가, 기존 아이템에 한해 유효 | Legendary 고정 Power 시즌 차별화 |
| 시즌 셋 효과 | 신규 SetItemCatalogSO | 다음 시즌 풀 교체 or 누적 | 2셋/4셋 보너스 |
| 명예의 전당 기록 | Firebase (영구) | 시즌 리더보드 top N 스냅샷 저장 | 최고 층, 완주 시간 |
1.5 로컬 전용 vs Firebase 필요성
- 로컬만으로 가능: 시즌 전환, PP 마이그레이션, 시즌 보상 드롭, Mythic 재롤 — 모두 클라 권위 + 로컬 저장으로 충분 (원본 서버 전투 로직 無 결정과 일관).
- Firebase 필요 최소 집합:
seasonEndUtcRemote Config (시계 조작 방지)- 시즌 리더보드 쓰기/읽기 (글로벌 랭킹 없이는 시즌 자체의 경쟁적 의미 약함)
- 명예의 전당 영구 스냅샷
- Firebase 불필요: 프레스티지 세이브 동기 (Phase 5+ 유예 유지)
2. OnSeasonEnd 이벤트 발화 전략 — 3옵션 비교
Sprint 02 W2 §6.2 신규 훅 3개 중 OnSeasonEnd 만 구독자 존재 / 발화자 없음 상태. Sprint 05에서 발화자를 어디에 둘지 결정한다.
2.1 옵션 A — 관리자 수동 트리거
UI: Debug 메뉴 "[Season] Force End Current Season" 버튼
→ SeasonController.ForceEnd() 호출
→ OnSeasonEnd 발화
→ PlayerPrestigeState.MigrateSeasonalToPermanent(0.5f)
- 장점: 1인 운영 현실 완벽 부합. 버그·과부하 시 즉시 연장·단축 가능. Phase 4 개발 중 테스트 용이.
- 단점: 유저 입장에서 불확실성. 상용 출시 시 "운영자가 맘대로 종료한다" 불만 리스크.
- 적합: Phase 4 (Sprint 05 개발) 기본값. Phase 5 이후에도 "긴급 핸들"로 유지.
- 복구: Migrate 실행 도중 앱 종료 시 §3.4 참조 — 이중 발화 방지 필수.
2.2 옵션 B — 로컬 타이머 (앱 시작 시 DateTime.UtcNow >= seasonEndUtc 확인)
SeasonController.Update() (1초 또는 앱 포커스 복귀 시):
if (now >= seasonEndUtc && !seasonEnded) {
OnSeasonEnd 발화
seasonEnded = true (저장)
}
- 장점: 서버 없이 작동. 구현 단순 (
IdleTicker시계 로직 재활용). - 단점: 치팅 치명. 유저가 시스템 시계를 2026-01-01로 되돌리면 시즌이 영원히 종료되지 않거나 반복 종료. Sprint 03 방치 시스템의
DateTime.UtcNow가드는 "음수·30일 초과 경고"만 있고 "조작 자체 차단"은 못함. - 복구: 시즌 종료 이벤트 발화 → 앱 종료 → 시계 롤백 시 이중 발화 경로 존재.
seasonId별migrated:bool플래그로 방어 가능하지만 복잡도 증가. - 권장 안 함.
2.3 옵션 C — Firebase Remote Config currentSeasonId + seasonEndUtc
앱 시작 / 1h 주기 / 포커스 복귀 시:
Firebase.RemoteConfig.FetchAndActivate()
remoteSeasonId = config.currentSeasonId
if (remoteSeasonId != localSeasonId) {
// 서버가 시즌 넘어감 선언
OnSeasonEnd 발화 (localSeasonId 기준 마이그)
localSeasonId = remoteSeasonId
}
- 장점: 서버 권위 = 치팅 차단. 글로벌 일괄 전파.
seasonEndUtc표시(클라 카운트다운)와 실제 전환 분리 가능. - 단점: Firebase 첫 도입 러닝커브.
FetchAndActivate실패 시 fallback 전략 필요. 오프라인 장기 유저의 "서버보다 1시즌 뒤처진 상태" 처리. - 적합: Phase 5+ 상용 기본값.
2.4 권장안
| Phase | 기본 트리거 | 보조 |
|---|---|---|
| Phase 4 (Sprint 05 개발) | 옵션 A 관리자 수동 | 디버그 메뉴 버튼 + 콘솔 커맨드 |
| Phase 5+ (상용) | 옵션 C Firebase Remote Config | 옵션 A 긴급 핸들 상시 유지 |
| 어디에도 | 옵션 B 로컬 타이머 배제 | 치팅 리스크 대비 수익 없음 |
- 양 옵션 공통 추상화:
ISeasonClock인터페이스 →ManualSeasonClock/RemoteConfigSeasonClock구현 교체. Sprint 05는 Manual만 실구현, Remote는 Phase 5 과제로 등록.
2.5 MigrateSeasonalToPermanent(0.5f) 발화 타이밍·복구·롤백
- 발화 타이밍:
OnSeasonEnd→SeasonEndModalUI 표시 → "확인" → Migrate 호출. 모달 없이 자동 Migrate도 기술적으로 가능하지만 유저 혼란(PP 갑자기 반토막) 방지 위해 모달 필수. - 복구 지점 3단계:
OnSeasonEnd발화 직후prestige.v1.json백업 (.bak+_season<N>_pre_migrate).- Migrate 실행 → 성공 시
migrated=true플래그 저장. - 실패 또는 앱 종료 시 다음 실행에서
.bak로부터 복구.
- 이중 발화 방지:
PrestigeSaveDto.lastMigratedSeasonId필드 신설 →schemaVersion=1 → 2마이그레이션 필요(§3).
3. MigrateSeasonalToPermanent 활성화 & 데이터 흐름
3.1 현재 상태 (Sprint 02 W2 APPROVED)
// GodBlade.Runtime.Prestige / PlayerPrestigeState.cs (이미 시그니처 존재)
public void MigrateSeasonalToPermanent(float retainRatio); // 구독자 없음 = no-op
- 호출자 없음. 테스트(
PrestigeSeasonMigrationTests.T5)에서만 직접 호출하여 불변식 검증.
3.2 Sprint 05 신규 부수 효과
Sprint 02 W2 PlayerPrestigeState.MigrateSeasonalToPermanent 본문(의사코드) 외 신규 필요:
- 저장 플래그:
lastMigratedSeasonId기록 (이중 발화 방지) - 백업 파일 생성:
prestige.v1.json.bak+prestige_season<N>_pre_migrate.json - UI 이벤트:
OnPpChanged2회(Permanent 증가 / Seasonal→0) + 신규OnSeasonEndMigrationCompleted(seasonId, oldSeasonalPp, retainRatio, convertedPp)이벤트 1건 - 누적 방치 처리: 시즌 종료 시점에
IdleTicker.FlushCatchUp()강제 1회 실행 → 마지막 방치 PP가 Seasonal 에 반영된 후 Migrate. 순서 뒤집히면 방치 PP가 Permanent 로 직행. - 마일스톤 스냅샷:
MilestoneGranter의 현 시즌 maxFloor 을 리더보드 제출 후 리셋 (시즌별 기록 분리)
3.3 50% 전환 로직 & 라운딩 정책
Sprint 02 W2 §4.3 의사코드를 확장:
MigrateSeasonalToPermanent(retainRatio=0.5f):
// Step 0: 방치 누산 강제 flush (신규)
IdleTicker.Instance?.FlushCatchUp(forceImmediate: true)
// Step 1: Seasonal 슬롯 전부 회수
seasonalInvested = Σ slot.investedPp where channel=Seasonal
totalPool = SeasonalPp + seasonalInvested // unspent + invested 모두
RemoveAll(s => s.channel == Seasonal)
// Step 2: retainRatio 적용 — 라운딩 정책 결정 필요 (Open Q6)
converted = LongMulFloor(totalPool, retainRatio) // (long * numerator / denominator) 정수 연산
PermanentPp += converted
SeasonalPp = 0
// Step 3: 플래그 저장 + 백업
lastMigratedSeasonId = currentSeasonId
PrestigeSaveService.Save(this, accountId)
// Step 4: 이벤트 발화
OnPpChanged(Permanent, PermanentPp)
OnPpChanged(Seasonal, 0)
OnSeasonEndMigrationCompleted(seasonId, totalPool, retainRatio, converted)
- 라운딩 정책 3안 (Open Q6에서 결정):
- (a)
Floor— 유저 불리, 대규모 PP 유실 가능 (100 PP → 50 / 101 PP → 50, 1 PP 손실) - (b)
Round— 통계적 중립, 유저·시스템 50:50 - (c)
Ceiling— 유저 유리, PP 인플레 약간 가중 - 권장: (b) Round, 소수 첫째자리에서. 단, 정수 연산 구현
(long * 50) / 100→ 500 기준 정확한 50% 보장.
3.4 prestige.v1.json → prestige.v2.json 마이그레이션
v2 스키마 추가 필드 3개:
[System.Serializable]
public struct PrestigeSaveDto_V2 {
public int schemaVersion; // 2
public bool isUnlocked;
public long permanentPp;
public long seasonalPp;
public List<PrestigeSlot> slots;
public long savedAtUnixMs;
// --- v2 신규 ---
public string lastMigratedSeasonId; // 이중 발화 방지
public string currentSeasonId; // 로컬 캐시 (Firebase sync 전 fallback)
public long seasonStartUnixMs; // 시즌 시작 시각 (리더보드 기준점)
}
PrestigeSaveService.Migrateswitch 에case 1: → ApplyV1toV2(dto)추가. v1 파일은currentSeasonId = "s1",lastMigratedSeasonId = null,seasonStartUnixMs = savedAtUnixMs기본값 주입.PrestigeSaveRoundTripTests에 v1 하드코딩 JSON → Load → V2 변환 후 Slots/Pp 유지 테스트 추가 (CP-M15 확장).
3.5 UI 알림
- 모달
SeasonEndModal: 시즌 종료 직후 1회 블로킹. 전환 전후 PP 숫자 비교 + "다음 시즌까지 N일" 카운트다운. - "명예의 전당" 화면: 직전 시즌 리더보드 top 100 + 내 최종 순위 표시. Firebase 읽기 전용.
- UI 실구현은 Sprint 06 이월 가능 (Sprint 05는
OnSeasonEndMigrationCompleted이벤트만 보장).
4. 시즌 4대 이월 컨텐츠 통합 아키텍처
4.1 셋 효과 (Set Item Bonus)
데이터 구조
namespace GodBlade.Data.Items {
[System.Serializable]
public struct SetItemRow {
public ushort setId; // 0~7 (시즌별 5~8 셋)
public string nameKey; // 로컬라이즈 키
public byte pieceCount; // 세트 구성 파츠 수 (보통 6 또는 8)
public List<SetBonusRow> bonuses; // 2셋/4셋/6셋 tier별 효과
}
[System.Serializable]
public struct SetBonusRow {
public byte requiredPieces; // 2, 4, 6
public string statKey; // Sprint 04 `affix_pool.tsv` 의 stat_key 재사용
public float value;
}
public sealed class SetItemCatalogSO : ScriptableObject { ... }
}
- Addressables 그룹: 신규
Sets/s<N>(시즌별). 라벨set-catalog-s<N>. - TSV Importer:
set_item_catalog_s<N>.tsv두 시트 합친 2차원 구조 (세트 × 보너스).TsvReader공통 모듈 재사용.
착용 체크 런타임
namespace GodBlade.Runtime.Items {
public sealed class SetEffectEvaluator {
readonly SetItemCatalogSO _catalog;
readonly Dictionary<ushort, int> _equippedCountBySetId = new(); // O(1)
public void OnEquip(in EquippedItem item) { ... }
public void OnUnequip(in EquippedItem item) { ... }
public IReadOnlyList<ActiveSetBonus> GetActiveBonuses();
}
}
- 장비 변경 이벤트 구독 →
_equippedCountBySetId증감만. O(1). Dictionary 직렬화 금지(CP-M09) 준수 —[NonSerialized]+ 장비 로드 시 rebuild. - PowerCalculator 스택에 주입: 프레스티지 외부 곱연산(Sprint 02 결정) → 어픽스 내부 스택 → 신규 셋 효과 외부 곱연산. §6 10축 교차표 참조.
시즌별 셋 풀 변경
- 옵션 α (권장): 시즌별 신규
SetItemCatalogSO_s<N>하나. 이전 시즌 셋은 Read-only 남김 (기존 장비 유효). - 옵션 β: 전 시즌 합친 단일 마스터 +
seasonActive플래그. → 매 시즌 런타임 분기 복잡 / 시즌 종료 후 기존 효과 유지 여부 모호 → 비권장.
4.2 Legendary 고정 Power
TSV 설계 옵션
| 옵션 | 위치 | 장점 | 단점 |
|---|---|---|---|
(a) affix_pool.tsv 에 legendary_fixed_slot:bool 컬럼 추가 | 기존 파이프라인 재사용 | 단일 시트, Importer 변경 최소 | 컬럼 세만티틱 오버로드 |
(b) 별도 legendary_powers.tsv | 전용 SO, 명확 경계 | 확장 여유 | TSV 파일·Importer 추가 |
(c) affix_pool.tsv 의 tier_5 행을 required_item_class 로 필터 | 이미 Sprint 04 v1.0 어픽스 슬롯 규칙 §2에 Legendary=1 고정 어픽스 선언됨 | 추가 파일 無 | required_item_class 컬럼 신설 필요 |
- 권장: (c). Sprint 04 스펙에서 Legendary=1 고정 어픽스 슬롯이 이미 있음.
required_item_class컬럼 1개 추가 + Legendary 롤 로직만 변경. required_item_class값:weapon/armor/accessory중 하나. 무기는 critDmg Power, 방어구는 damageReduction, 액세서리는 magicFind — Sprint 04 Q2 결정 그대로 이행.
중복 정책
- 한 캐릭터가 Legendary 2개 착용 시: (i) 각자 고정 Power 독립 / (ii) 동일 Power 중복 차단 중 선택. 권장 (i) — 무기+방어구+액세서리 슬롯 분리로 자연스럽게 중복 회피됨.
4.3 Mythic 제약
Sprint 04 Q5 결정: (a) 허용 유지 / Sprint 05+ 실측 후 (b) 중복 슬롯 금지 / (c) 동시 장착 상한 재검토.
런타임 구현 난이도 비교
| 옵션 | 로직 | 난이도 | 하자 |
|---|---|---|---|
| (b) 중복 슬롯 금지 | equip(item) 훅에서 기존 Mythic 아이템이 같은 슬롯에 있으면 차단 | 저 (2h) | 우회 조합 "전부 다른 슬롯 Mythic" 가능 |
| (c) 동시 장착 상한 (예: 3) | equip(item) 훅에서 Mythic 카운트 ≤ 3 체크 | 저 (2h) | UX — "4번째 장착 시도" 시 어떤 Mythic 해제? |
| (b+c) 조합 | 둘 다 적용 | 중 (4h) | 가장 안전 |
기존 equip() 훅에 validator 추가 의사코드
public EquipResult Equip(in Item item) {
if (!_mythicValidator.CanEquip(item, _currentLoadout, out string reason)) {
return EquipResult.Blocked(reason); // UX: 모달 "Mythic 제약 위반: {reason}"
}
// ... 기존 장착 로직
}
IMythicConstraint인터페이스 →NoDuplicateSlotConstraint/MaxSimultaneousConstraint구현 교체. Sprint 05 착수 시 PD가 (b)/(c)/(b+c) 중 택일.
4.4 MythicEssence
Sprint 04 Q1 결정: Mythic 재롤 PP 50 유지. Sprint 05에서 Q1 재검토 여지 — 대안 (c): PP 대신 MythicEssence 소비.
도입 시 설계 질문
| 질문 | 대안 |
|---|---|
| 획득 경로 | (α) 시즌 보상 풀 drop / (β) Mythic 아이템 해체 / (γ) 둘 다 |
| 지갑 위치 | (α) PlayerPrestigeState 에 long mythicEssence 필드 추가 / (β) 별도 PlayerWalletState 신설 |
| 시즌 종료 처리 | (α) 삭제 / (β) 영구 이관 / (γ) retainRatio 적용 |
| Q1 재롤 PP 대체 | 완전 대체 / PP·Essence 둘 다 받음 / PP 우선 Essence fallback |
- 권장 최소 도입: 지갑 별도
PlayerWalletState(프레스티지 결합 회피) + 시즌 종료 50% retainRatio 동일 적용 + 재롤 비용은 Sprint 05에서는 PP 50 유지, Essence 대체는 Sprint 06 이월. - 이유: Essence 경제 균형을 Sprint 05에서 검증할 공수 없음. 지갑 인프라만 깔고 소비처는 다음 시즌.
5. 시즌 리더보드 아키텍처
5.1 저장소 3옵션
| 저장소 | 쓰기 | 읽기 | 영속성 | 치팅 방어 |
|---|---|---|---|---|
| 로컬 JSON만 | O(1) | O(1) | 앱 데이터 | 완전 무방비 |
| Firebase Realtime Database | 네트워크 | 네트워크 | 클라우드 영구 | 쓰기 Rule 필요 |
| Firebase Firestore | 네트워크 | 네트워크 + 쿼리 | 클라우드 영구 | Security Rules + Cloud Functions 검증 |
- 권장: Firestore (랭킹 쿼리
orderBy + limit자연 지원). Realtime DB는 트리 구조상 정렬 쿼리 취약. - Sprint 05 전환 전략: 로컬 JSON
local_leaderboard.json먼저 + 인터페이스ILeaderboardRepository. Firestore 구현체는 Phase 5 초(Sprint 05 W2 또는 Sprint 06).
5.2 시즌 종료 후 "명예의 전당"
- 정책: 시즌 종료 시 리더보드 top 100 스냅샷 →
hall_of_fame/season_<N>/컬렉션에 불변 복제 (Firestore rules:allow read; allow create if admin; allow update,delete = false). - 영구 보존. 2027년 출시 5년 후에도 "2026 S1 Top 1: DobbyCat" 조회 가능.
- 앱 종료 중 시즌 전환 시에도 누락 안 됨 — 서버 Cloud Function 으로 실행.
5.3 데이터 구조
namespace GodBlade.Runtime.Season {
[System.Serializable]
public struct SeasonLeaderboardEntry {
public string playerId; // Firebase UID or device GUID fallback
public string displayName; // 3~16자, 검열 필요 (Sprint 06 이월)
public string seasonId; // "s1", "s2", ...
public int maxFloor; // 최고 도달층
public long completionTimeMs; // 특정 층 완주 시간 (보조 지표)
public long submittedAtUnixMs;
public int prestigeLevelSnapshot; // 시즌 종료 시점 프레스티지 총합 레벨 (컨텍스트)
}
}
5.4 치팅 방지
- 로컬 기록만: 의미 無. 자기 기기 내 자기 만족.
- Firebase Security Rules: 쓰기 시
playerId == request.auth.uid강제.maxFloor값은 Cloud Function 으로 검증 (상한 = 이론상 최대 × 1.5). - Sprint 05 범위: 클라 쓰기만 구현. 서버 검증 Cloud Function 은 Sprint 06 이월.
6. Sprint 01/02/03/04 자산과의 정합성 10축 교차표
| # | 축 | Sprint 01~04 기존 | Sprint 05 변경 | 충돌 |
|---|---|---|---|---|
| 1 | PpChannel.Seasonal 발화 | Sprint 02 W2 정의만 | Sprint 05 첫 발화자 (SeasonController) | ✅ 무충돌 (예약 해소) |
| 2 | MigrateSeasonalToPermanent(0.5f) | Sprint 02 W2 시그니처만 | §3.3 본문 확장 + 플래그/백업 추가 | ✅ 시그니처 불변 |
| 3 | OnSeasonEnd 훅 | Sprint 02 W2 채널 C 예약 | SeasonController 가 발화 | ✅ 무충돌 |
| 4 | MilestoneGranter / OnFloorReached | Sprint 01 W2 구현 | 시즌 종료 시 maxFloor 스냅샷 후 리셋 | ⚠ 리셋 정책 미정 — Open Q3 |
| 5 | IdleTicker | Sprint 03 구현 | 시즌 종료 시 FlushCatchUp(forceImmediate=true) 선행 호출 | ✅ 순서만 보장 |
| 6 | affix_pool.tsv 스택 정책 | Sprint 04 v1.0 | required_item_class 컬럼 추가 (Legendary 고정 Power) | ⚠ 스키마 변경 — v2.0 필요 |
| 7 | JSON 저장 (prestige.v1.json) | Sprint 02 W2 | schemaVersion 1 → 2 마이그레이션 | ✅ 경로 불변 |
| 8 | asmdef 경계 | 4분할 고정 | 신규 GodBlade.Runtime.Season 서브 네임스페이스 + GodBlade.Data.Season | ✅ 신규 asmdef 無 |
| 9 | Addressables 그룹 | Rewards / Prestige | 신규 그룹 Season/s<N> / Sets/s<N> 2종 | ✅ CP-M12 준수 |
| 10 | Firebase 도입 | Phase 5+ 유예 | Sprint 05에서 Remote Config + Firestore 최소 도입 | ⚠ 유예 정책 변경 — Open Q2 |
충돌 3건 요약:
- ⚠ 축 #4: 시즌 종료 시 maxFloor·프레스티지·어픽스 중 어디까지 리셋? Seasonal PP만 마이그레이션 대상으로 봤으나 maxFloor 자체는 리셋? 유지? — Open Q3.
- ⚠ 축 #6:
affix_pool.tsv스키마에required_item_class컬럼 추가 — Sprint 04 Importer 재실행 필요. v1.0 → v2.0 표기. - ⚠ 축 #10: Sprint 02 W2 Q3 "Firebase 미포함 Phase 5 이후" 결정 vs Sprint 05 시즌 리더보드 필요성. 정책 재결정 필요 (Open Q2).
7. Open Questions (PD 결정 필요, 7개)
| Q# | 질문 | A안 | B안 | C안 | code-lead 추천 | 근거 |
|---|---|---|---|---|---|---|
| Q1 | 시즌 기간 | 30일 | 60일 | 90일 | B (60일) | 1인 파트타임 운영 코스트 + 컨텐츠 소진 밸런스. 30일은 이벤트 소진, 90일은 이탈 위험. 60일 = 운영 4~6h/월 + 중위 신선도. 수치 튜닝은 game-designer 확정. |
| Q2 | OnSeasonEnd 발화 전략 (Sprint 05 한정) | 관리자 수동 | 로컬 타이머 | Firebase Remote Config | A 관리자 수동 + Phase 5 C로 승격 | B는 치팅 취약. C는 Firebase 러닝커브 동시 부담. A→C 단계적 승격 (ISeasonClock 추상화로 2~3h 교체 비용) |
| Q3 | 시즌 종료 시 maxFloor 리셋 정책 | 리셋 (0부터 재시작) | 유지 (영구) | 리셋 but 스냅샷 보존 | C (리셋 + 스냅샷) | "시즌별 리더보드"의 본질 = 공평한 출발점. Permanent 프레스티지 영구화와 균형. Sprint 03 IdleTicker maxFloor 필드와 분리 필요 |
| Q4 | 4대 이월 컨텐츠 Sprint 05 동시 투입 범위 | 4건 전부 | 셋만 | 셋 + Mythic 제약 | 3건 (셋 + Mythic 제약 + MythicEssence 지갑) | 1인 15h/2주 예산. Legendary 고정 Power는 Sprint 06 이월. 셋은 시즌 차별화 필수, Mythic 제약은 실측 타이밍, Essence 지갑은 인프라만 선행 |
| Q5 | 시즌 리더보드 저장소 | 로컬 JSON만 | Firebase Realtime DB | Firebase Firestore | C Firestore + 로컬 캐시 | Realtime DB는 랭킹 쿼리 취약. Firestore orderBy + limit 자연. 로컬 캐시는 네트워크 단절 시 내 기록 보존 |
| Q6 | 50% retainRatio 라운딩 정책 | Floor | Round | Ceiling | **B Round (정수 연산 (total*50)/100)** | 유저·시스템 중립. 부동소수 오차 회피 위해 정수 연산 강제 |
| Q7 | MythicEssence 위치 & 시즌 종료 처리 | PlayerPrestigeState 내 필드 / 시즌 종료 삭제 | 신규 PlayerWalletState + 50% retain | Essence 미도입 Sprint 05 | B 지갑 별도 + 50% retain | 프레스티지 결합 회피. Q3 retainRatio 정책 일관성. 소비처 Sprint 06 확장 여지 |
8. Sprint 05 예상 TodoWrite (W1/W2 × 5 = 10건)
공수 예산: 1인 파트타임 11~15h/2주 + 리뷰 후 컴파일 회귀 버퍼 1.5~2h = 12~17h 총.
W1 (Firebase 인프라 + 시즌 전환 Backend) — 5건
| # | 태스크 | 공수 | 근거 |
|---|---|---|---|
| T1 | SeasonConfigSO + ISeasonClock 인터페이스 + ManualSeasonClock 구현 (디버그 메뉴) | 1.5h | Q2 A안. 추상화 경계만 |
| T2 | PrestigeSaveDto_V2 + PrestigeSaveService.Migrate v1→v2 스위치 추가 | 1.5h | §3.4 CP-M15 확장 |
| T3 | SeasonController 신설 + OnSeasonEnd 발화자 + 이중 발화 방지 플래그 | 2.0h | §2.5 복구 3단계 |
| T4 | Firebase SDK 프로젝트 통합 + Auth 익명 로그인 + Firestore 초기화 | 3.0h | Firebase 첫 도입 러닝커브 포함 (§9.2 리스크) |
| T5 | ILeaderboardRepository + LocalLeaderboardRepository (JSON) + FirestoreLeaderboardRepository 스켈레톤 | 2.0h | §5.1 전환 전략 |
W1 합계: 10.0h
W2 (시즌 컨텐츠 + Smoke Test) — 5건
| # | 태스크 | 공수 | 근거 |
|---|---|---|---|
| T6 | SetItemCatalogSO + SetItemImporter (TSV→SO) + SetEffectEvaluator 스켈레톤 | 2.5h | §4.1 4건 이월 중 1건 |
| T7 | IMythicConstraint 인터페이스 + NoDuplicateSlotConstraint 구현 + equip() 훅 validator | 2.0h | §4.3 |
| T8 | PlayerWalletState 신설 + MythicEssence long 필드 + 시즌 종료 50% retain 연결 | 1.5h | §4.4 지갑만 선행 |
| T9 | SeasonEndModal UI 프리팹 + OnSeasonEndMigrationCompleted 구독 + 전환 전후 PP 표시 | 1.5h | §3.5 UI 최소 |
| T10 | Smoke Test — 시즌 전환 전체 루프 (디버그 메뉴 → Force End → Migrate → 리더보드 제출 → 모달 확인 → prestige.v2.json 생성 확인) | 1.5h | §10 CP-M24~M28 전수 |
W2 합계: 9.0h
총합: 19.0h → 1인 파트타임 15h 예산 초과 4h. Q4 권장(3건) 적용 시 Legendary 고정 Power 제외로 14.0h 달성 가능.
9. 리스크 & 블로킹 예상 3건
9.1 시즌 전환 중 앱 종료 → 마이그레이션 중단 복구
- 리스크:
MigrateSeasonalToPermanent실행 중 크래시 → Seasonal 슬롯 삭제 완료 + Permanent 증가 未 → PP 영구 유실. - 완화: §2.5 복구 3단계 — 실행 전
prestige.v1.json.bak생성 + 3단계 전부 완료 후migrated=true플래그 저장. 앱 시작 시 플래그 불일치 감지 시.bak로 복구. - 테스트:
PrestigeSeasonMigrationCrashRecoveryTests신설 (Edit-mode에서 중간 throw 주입 → 다음 실행에서 복구 검증).
9.2 Firebase 첫 도입 러닝커브 + 지연
- 리스크: Firebase SDK 통합 3h 예산은 낙관적. Android
google-services.json/ iOSGoogleService-Info.plist설정, Firestore Security Rules 작성, 인증 흐름 디버깅 — 실제 8~12h 소요 가능. 메모리feedback_codelead_review_compile_verify.md패턴 재현 (정적 리뷰 통과 후 실제 컴파일 회귀). - 완화: Sprint 05 W1 T4 를 "Firebase SDK 통합까지만"으로 제한. Firestore 실제 쓰기·읽기는 T5 스켈레톤만. Phase 5 초 별도 스프린트 가능성 열어둠.
- 블로킹 알람: T4 4h 초과 시 PM 에스컬레이션 → T5·T10 Firebase 부분 Sprint 06 이월.
9.3 4대 이월 동시 투입 시 공수 폭증
- 리스크: Q4 A안(4건 전부) 선택 시 W2 공수 18h+, 총 28h → 2주 예산 2배 초과.
- 완화: Q4 권장 C안(3건) 고수. Legendary 고정 Power는 Sprint 06. 만약 PD가 A안 강행 시 Sprint 05 2-weeks → 3-weeks 연장 또는 Mythic 제약·Essence 지갑 Sprint 06 이월로 재조정.
- 지표: W1 종료 시점 T1~T5 진행률 80% 미만이면 W2 투입 범위 축소 (Essence 지갑만 유지).
10. 컴파일러 시뮬레이션 체크리스트 — 시즌 특화 확장
공통 23개(CP-M01~M23)는 sprints/shared-checkpoints.md 재적용. 시즌 특화 5건 신규 제안 (CP-M24~M28).
| ID | 체크 내용 | 검증 방법 |
|---|---|---|
| CP-M24 | PrestigeSaveDto_V2 추가 필드 3개 (lastMigratedSeasonId / currentSeasonId / seasonStartUnixMs) — schemaVersion=2 로 설정, v1 Load 시 기본값 주입 | PrestigeSaveRoundTripTests 에 v1 JSON 하드코딩 문자열 → Load → V2 신규 필드 기본값 검증 assert |
| CP-M25 | MigrateSeasonalToPermanent 이중 발화 방지 — lastMigratedSeasonId == currentSeasonId 이면 early return | PrestigeSeasonMigrationTests 에 동일 seasonId 2회 호출 → 2회차 no-op 검증 |
| CP-M26 | 시즌 종료 시 IdleTicker.FlushCatchUp(forceImmediate=true) 선행 호출 순서 — Migrate 전 호출되어야 방치 PP 가 Seasonal 에 정상 반영 | SeasonControllerTests 에 IdleTicker 호출 순서 assert (Mock 시퀀스 검증) |
| CP-M27 | Firebase 초기화 실패 시 fallback — FirestoreLeaderboardRepository unavailable 시 LocalLeaderboardRepository 자동 전환, NRE 없음 | LeaderboardFallbackTests 에 Firebase mock 실패 주입 → 로컬 저장소로 제출 완료 확인 |
| CP-M28 | SeasonConfigSO.seasonEndUtc 와 실제 ISeasonClock.GetSeasonEndUtc() 반환 일치 — Remote Config 미도입 환경에서 ManualSeasonClock 이 SO 값 그대로 반환 | SeasonClockTests 에 SO 설정값 변경 → Clock 반환값 추적 |
- §10 특화 5개 +
shared-checkpoints.md공통 23개 = Sprint 05 서브셋 28개 (신규 5 + 공통 전체 재적용). - 추가로
shared-checkpoints.md본문에 CP-M24~M28 확장 등록 제안 (Sprint 05 착수 PR에 포함).
11. 분량 · 체크 추적
- 본 문서: 약 10쪽 (마크다운 렌더 기준).
- 섹션 10개 + 분량 메타 = 11개 섹션.
- §6 10축 교차표: 충돌 3건 (축 #4 maxFloor 리셋 정책 / 축 #6 affix_pool 스키마 v2 / 축 #10 Firebase 유예 정책 재결정).
- §7 Open Question: 7건 (Q1~Q7).
- §8 TodoWrite: 10건 W1 5 + W2 5 = 19.0h (Q4 권장 적용 시 14.0h 예산 내).
- §10 체크 확장: 5건 신규 CP-M24~M28 (공통 23 포함 시 28건).
- 실구현 코드 줄 수: 0 (리서치 전용, PD 제약 준수).
- 수치 튜닝 제안 줄 수: 0 (game-designer 영역 준수 — 시즌 기간 선택지만 제시, 확정은 game-designer).
- PD 결정 대기 열린 질문: 7개.
(끝 — DRAFT v0.1, 2026-04-21 code-lead 작성, PD 결정 대기)