Sprint 02 W2 — Prestige Entity / Runtime 기술 스케치
작성: 2026-04-21 (code-lead) · 근거: designs/prestige_system_spec.md v1.0 APPROVED · designs/sprint-01-w2-importer-design.md v1.0 APPROVED 문서 상태: APPROVED v1.0 (PD 승인 2026-04-21) 범위: Sprint 02 W2 기술 스케치 전용 — 실구현 코드 작성 금지, C# 시그니처·의사코드·구조도만 수치 경계: 본 문서는 저장소·실행 레이어 설계만. 구체 수식·수치는 prestige_system_spec.md 인용 외 신규 결정 없음 (game-designer 영역). --- ## PD 승인 결정 (2026-04-21) | # | 질문 | 확정 | 비고 | |---|------|------|------| | Q1 | 데이터 모델 | 옵션 B — PrestigeSlot blittable struct + List<PrestigeSlot> | read-write hot path 대응. Sprint 01 W2 DQ1(SO 옵션 A)와 다른 결정 명시 — read-only 카탈로그 vs read-write 상태의 성격 차. Sprint 03 DOTS 전환 시 IComponentData 1:1 이식 | | Q2 | 로컬 저장 | JSON + schemaVersion 필드 | 경로: Application.persistentDataPath/account/<accountId>/prestige.v1.json | | Q3 | Firebase 동기 S02 포함 | 미포함 (Phase 5 이후) | 로드맵 일관성 유지 | | Q4 | retainRatio 저장 위치 | 파라미터 유지 (MigrateSeasonalToPermanent(float retainRatio)) | Sprint 05 시즌 도입 시 재논의 가능 | 부수 확정: - PlayerPrestigeState + PrestigeCalculator 분리 유지 (수식 변경 시그니처 불변) - PrestigeItemCatalogSO 는 Sprint 01 W2 DQ1 옵션 A 패턴 재활용, TsvReader 공통화의 첫 소비자 - §8 컴파일러 시뮬레이션 체크리스트 16개 (CP9-1~11 + CP-P1~P5) 필수 - Sprint 01 W2 설계 정합성 검증 통과 (§11 10축 교차표, 충돌 0건) --- ## 0. 요약 (PD 1분) - 데이터 모델: 옵션 B (PrestigeSlot struct + 60-slot List) 권장 — Sprint 01 W2 DQ1의 "SO 옵션 A"와 성격이 다름(read-only 카탈로그 vs 고빈도 변경 상태). 혼동 금지. - 카탈로그는 별개: PrestigeItemCatalogSO 30행 = 정적 정의(이름·탭·소프트캡·공식 선택자). TSV Importer로 변환. - PP 채널 2개(Permanent / Seasonal) 사전 분리. Sprint 05 시즌 도입 시 추가 코드 0. - 저장: 로컬 JSON 우선, 경로 Application.persistentDataPath/account/prestige.v1.json. Firebase는 Phase 5+. - 이벤트 훅: Sprint 01 W2 기존 3 + 신규 3 = 6개. - §8 컴파일러 시뮬레이션 체크리스트 11 + 5 = 16개 (Sprint 01 W2 11개 재적용 + 프레스티지 특화 5개). - Sprint 01 W2 설계 정합성: asmdef·네이밍·InternalsVisibleTo 패턴 1:1 일치. 충돌 0.
1. 데이터 모델 선택
1.1 전제: 슬롯 수 × 상태 성격
- 슬롯 수: 30항목 × 2채널 = 유저 1인당 60 슬롯.
- 성격: 투자·회수·리스펙 빈도 높음. Read-mostly가 아니라 read-write hot path.
- Sprint 01 W2
DropRateTableSO / MilestoneRewardCatalogSO 는 read-only 카탈로그 → SO 적합. 프레스티지 상태는 유저 세이브 → SO 적합도 낮음.
1.2 3옵션 비교
| 옵션 | 구조 | 장점 | 단점 | Sprint 02 적합성 |
|---|
| A. 단일 SO 60필드 | PlayerPrestigeStateSO 에 permanent_00 ~ permanent_29, seasonal_00 ~ seasonal_29 필드 60개 | Sprint 01 W2 SO 패턴 1:1 | 필드 60개 직렬화 노이즈 / 항목 추가 시 재컴파일 / 리플렉션 없이 인덱서 불가 | ❌ |
| B. Struct + List (권장) | PrestigeSlot struct 60개를 List<PrestigeSlot> 에 평탄 저장 + (channel, itemId) 인덱서 | blittable struct → DOTS 전환 친화 / JSON 직렬화 자연 / 항목 확장 무공사 | SO 아님 → ScriptableObject가 아닌 POCO. 별도 저장 파이프 필요 | ✅ 권장 |
C. ECS IComponentData + DynamicBuffer<PrestigePointSlot> | DOTS 네이티브 | 수십만 유저 시뮬 시 캐시 친화 | 싱글 플레이어(유저 1명) / Sprint 02 범위 과도 / Conversion Workflow 도입 비용 | ❌ Sprint 03+ 측정 후 재검토 |
1.3 권장 = 옵션 B
// GodBlade.Runtime — Assets/_Project/Scripts/Runtime/Prestige/PrestigeSlot.cs
namespace GodBlade.Runtime.Prestige {
public enum PpChannel : byte {
Permanent = 0,
Seasonal = 1,
}
[System.Serializable]
public struct PrestigeSlot : System.IEquatable<PrestigeSlot> {
public ushort itemId; // PrestigeItemCatalogSO 의 row index (0~29)
public PpChannel channel; // 1 byte
public int investedPp; // 누적 투자 PP (회수 시 감소)
// Level·Bonus 는 저장하지 않음 — PrestigeCalculator 가 실시간 산출 (카탈로그 공식 변경 시 재컴파일 대응)
}
}
PrestigeSlot 은 blittable (byte + ushort + int) → Sprint 03 DOTS 전환 시 IComponentData 로 1:1 이식 가능.- 저장 엔티티는 아래(§3)의
PlayerPrestigeState 한 클래스가 소유.
1.4 Sprint 01 W2 DQ1(SO 옵션 A)과의 관계
- 다른 축: Sprint 01 W2 DQ1의 "SO 옵션 A"는 카탈로그(정적 정의) 저장 방식 결정. 본 문서의 "옵션 A/B/C"는 유저 런타임 상태 저장 방식 결정.
- 프레스티지도 카탈로그(§2)는 Sprint 01 W2 DQ1과 동일하게 SO 옵션 A 유지 → 일관성 확보.
2. 항목 카탈로그 (PrestigeItemCatalogSO)
2.1 위치·asmdef
- 파일:
Assets/_Project/Scripts/Data/PrestigeItemCatalogSO.cs - 어셈블리:
GodBlade.Data (Sprint 01 W2 DropRateTableSO 와 동일 asmdef) - Addressables 주소:
Prestige/ItemCatalog (신규 그룹 Prestige, 라벨 prestige-catalog)
2.2 TSV 입력 (game-designer Sprint 02 W1 산출물 가정)
- 경로:
designs/prestige_items.tsv (game-designer 소관 Sprint 02 W1) - 30행 × 스키마:
item_id tab name stat_key soft_cap curve_id pre_softcap_per_pp post_softcap_coeff notes
0 Offense 공격력% AtkPct 5 linear_then_log 0.5 2.5 "공격력 +N% 가산"
1 Offense 치명타율 CritRate 5 linear_then_log 0.3 1.5 ...
...
curve_id 는 공식 선택자 (열거형 키). 실제 공식은 PrestigeCalculator 내부 switch. 수식 튜닝 시 TSV만 갱신 + Importer 재실행으로 반영.
2.3 SO 필드 시그니처
// GodBlade.Data — Assets/_Project/Scripts/Data/PrestigeItemCatalogSO.cs
namespace GodBlade.Data.Prestige {
public enum PrestigeTab : byte { Offense = 0, Defense = 1, Utility = 2 }
public enum PrestigeCurveId : byte { LinearThenLog = 0, /* Sprint 03+ 확장 여지 */ }
[System.Serializable]
public struct PrestigeItemRow {
public ushort itemId; // 0~29
public PrestigeTab tab;
public string name; // UI 표시용 (Sprint 04 로컬라이즈 키로 대체 예정)
public string statKey; // "AtkPct", "CritRate" 등 — Sprint 03 Stat 시스템 연동 키
public byte softCap; // Q4 결정 = 5 (고정이지만 튜닝 여지)
public PrestigeCurveId curveId; // 공식 선택자
public float preSoftCapPerPp; // curve_id=LinearThenLog 시 의미: Lv1~soft 구간 PP당 %
public float postSoftCapCoeff; // 소프트캡 이후 log 계수
}
[UnityEngine.CreateAssetMenu(
fileName = "PrestigeItemCatalog",
menuName = "GodBlade/Data/Prestige Item Catalog"
)]
public sealed class PrestigeItemCatalogSO : UnityEngine.ScriptableObject {
[UnityEngine.SerializeField] List<PrestigeItemRow> rows; // 30행
[System.NonSerialized] Dictionary<ushort, int> _byItemId; // OnEnable lazy build (Sprint 01 W2 §4.2 패턴)
public int Count => rows?.Count ?? 0;
public PrestigeItemRow Get(ushort itemId) { /* lookup */ }
public IReadOnlyList<PrestigeItemRow> GetByTab(PrestigeTab tab);
}
}
Dictionary 직렬화 금지(§8 CP9-8) → List 저장 + OnEnable lazy build 일관.[assembly: InternalsVisibleTo("GodBlade.Editor")] 는 GodBlade.Data 어셈블리 내 한 파일에만 선언(Sprint 01 W2 CP9-3 준수).
2.4 Importer (PrestigeItemImporter)
- 위치:
Assets/Editor/Importers/PrestigeItemImporter.cs (GodBlade.Editor asmdef) - 패턴: Sprint 01 W2
DropRateTableImporter 와 1:1 동일. TsvReader 공통 모듈의 첫 소비자 (Sprint 01 W2 DQ3 이월분 활성). - 메뉴:
GodBlade/Data/Import Prestige Item TSVGodBlade/Data/Import ALL (Stage → Drop → Milestone → Affix → Prestige) (기존 ALL 메뉴 확장)- 검증: TSV 30행 강제 (29 또는 31 시 Importer 중단 + DisplayDialog).
softCap 범위 [1, 20] 이외 시 ArgumentException.
2.5 Sprint 02 W1 TSV 공통화 연계
- Sprint 01 W2 DQ3 이월 과제 =
TsvReader 공통화. 본 Importer는 그 공통화의 첫 소비자로 설계. - Sprint 02 W1 PR 순서:
TsvReader.cs 공통화 (Sprint 00 Stage Importer 재연결)PrestigeItemImporter.cs 신규 (공통 TsvReader 사용)
- 의존 보장: 1 머지 후 2 시작. PM 에스컬레이션 대상.
3. PP 적립·소비 로직 계층
3.1 클래스 배치
GodBlade.Runtime / Assets/_Project/Scripts/Runtime/Prestige/
PlayerPrestigeState.cs — 유저 런타임 상태 (POCO, SerializableClass)
PrestigeCalculator.cs — 정적 계산기 (순수 함수)
PrestigeSlot.cs — §1.3 struct
PrestigeSaveSchema.cs — §5 직렬화 DTO
PrestigeBootstrap.cs — 로드·저장 오케스트레이터 (MonoBehaviour 또는 static Service)
3.2 PlayerPrestigeState 시그니처
namespace GodBlade.Runtime.Prestige {
public sealed class PlayerPrestigeState {
// 계정 단위 (Q2). 현 Sprint 02는 싱글 계정 전제.
public bool IsUnlocked { get; private set; }
public long PermanentPp { get; private set; } // §3.3 (A)(B)(C)(D)(E) 합산
public long SeasonalPp { get; private set; } // Sprint 05 활성화 전엔 0 고정
public IReadOnlyList<PrestigeSlot> Slots => _slots; // 최대 60
readonly List<PrestigeSlot> _slots = new(); // channel × itemId 평탄 저장
// —— 생명주기 ——
public void Unlock(); // 100층 마일스톤 훅. 이미 Unlock이면 noop.
// —— PP 적립 ——
public void AddPermanentPp(long amount); // amount < 0 → ArgumentException
public void AddSeasonalPp(long amount); // Sprint 05 전에도 호출 가능하지만 결과는 Pp 누적만.
// —— PP 소비·회수 ——
public SpendResult Spend(ushort itemId, PpChannel channel, int amount);
public void RefundAll(PpChannel channel); // 리스펙. 채널 전체 투자분 반환.
// —— 시즌 마이그레이션 (Q3) ——
public void MigrateSeasonalToPermanent(float retainRatio); // Sprint 05 OnSeasonEnd 에서 호출
// retainRatio=0.5f 가 스펙 §6.2 결정
// —— 이벤트 (UI 구독) ——
public event System.Action<PpChannel, long> OnPpChanged;
public event System.Action<ushort, PpChannel, int> OnSlotChanged;
public event System.Action OnUnlocked;
}
public readonly struct SpendResult {
public readonly bool Success;
public readonly string FailReason; // "INSUFFICIENT_PP" / "NOT_UNLOCKED" / "INVALID_ITEM" / "OVER_SOFTCAP_FORBIDDEN"(없음, 소프트캡은 디미니싱이지 금지 아님)
public readonly int NewInvestedPp;
}
}
3.3 PrestigeCalculator 시그니처
namespace GodBlade.Runtime.Prestige {
public static class PrestigeCalculator {
// 투자 PP → 항목 레벨 (소프트캡 전 선형, 이후 log — 스펙 §5.1 공식 인용)
public static int CalculateLevel(int investedPp, byte softCap);
// 항목 ID + 레벨 + 카탈로그 row → 실효 보너스 %
public static float CalculateBonus(in PrestigeItemRow row, int level);
// 여러 슬롯 합산 — UI 요약 표시용
public static BuildSummary SummarizeChannel(
IReadOnlyList<PrestigeSlot> slots,
PpChannel channel,
PrestigeItemCatalogSO catalog);
public readonly struct BuildSummary {
public readonly float TotalAtkPct;
public readonly float TotalCritPct;
public readonly float TotalDropPct;
// ... 스탯 집계 필드 (Sprint 03 Stat 시스템 확정 후 재조정)
}
}
}
- 시그니처 불변 원칙: 공식 계수가 바뀌어도(game-designer Sprint 02 W1 튜닝 예정)
CalculateLevel / CalculateBonus 의 입출력 타입은 고정. 내부 switch 분기만 교체. BuildSummary 필드는 Sprint 03 Stat 시스템 확정 후 1차 동결. 현 W2는 Sprint 02 W1 TSV가 정한 statKey 집합만 반영.
4. PP 슬롯 2채널 구조 (Q3 핵심)
4.1 설계 목표
- Sprint 02~04 기간:
Seasonal 채널은 UI에서 잠금 표시(탭 노출 자체 금지 또는 회색 처리). AddSeasonalPp 는 호출 가능하지만 트리거 경로 없음 → 실질적으로 0 유지. - Sprint 05 시점:
SeasonSystem 신설 → OnSeasonEnd 이벤트가 MigrateSeasonalToPermanent(0.5f) 호출. 본 W2에서는 메서드 시그니처만 존재, 호출자 없음.
4.2 채널 경계 불변식
불변식 I1: IsUnlocked == false 이면 Slots.Count == 0
불변식 I2: Σ(slot.investedPp where channel=Permanent) ≤ PermanentPp
불변식 I3: Σ(slot.investedPp where channel=Seasonal) ≤ SeasonalPp
불변식 I4: Sprint 02~04 기간 SeasonalPp == 0 이고 Seasonal 채널 슬롯 없음
불변식 I5: MigrateSeasonalToPermanent 후 SeasonalPp == 0, Seasonal 슬롯 모두 Refund → Permanent Pp 풀로 편입 (비율 적용)
4.3 마이그레이션 알고리즘 (의사코드)
MigrateSeasonalToPermanent(retainRatio=0.5):
// Step 1: Seasonal 슬롯 전부 회수 (invested pp 모음)
seasonalInvested = Σ slot.investedPp where channel=Seasonal
totalPool = SeasonalPp + seasonalInvested // 미투자 + 투자 모두
RemoveAll(s => s.channel == Seasonal)
// Step 2: retainRatio 적용하여 Permanent Pp 풀에 적립
converted = floor(totalPool * retainRatio)
PermanentPp += converted
SeasonalPp = 0
// Step 3: 이벤트 발화 (UI 갱신)
OnPpChanged(Permanent, PermanentPp)
OnPpChanged(Seasonal, 0)
retainRatio 인자화 → Sprint 05 시점에 PD가 비율 조정 가능 (기본 0.5 = 스펙 Q3).- 부동소수 누적 오차 방지:
long float 대신 long numerator / denominator 정수 연산 고려(Sprint 05 재검토).
5. 저장·직렬화
5.1 저장 방식 비교
| 방식 | 장점 | 단점 | Sprint 02 적합 |
|---|
JSON (Unity JsonUtility) | 사람이 읽기 쉬움 / 디버그 / 스키마 버전 필드 자연 | Dictionary 미지원(이미 List 구조) / long 직렬화 OK | ✅ |
Binary (BinaryFormatter) | 크기 작음 | .NET 공식 권장 중단 / 디버그 어려움 | ❌ |
| PlayerPrefs | 편의성 | 60 슬롯·long 값에 부적합 / 문자열 크기 제한 | ❌ |
5.2 권장 = JSON + schemaVersion 필드
namespace GodBlade.Runtime.Prestige {
[System.Serializable]
public struct PrestigeSaveDto {
public int schemaVersion; // 현재 1. 구조 변경 시 증가.
public bool isUnlocked;
public long permanentPp;
public long seasonalPp;
public List<PrestigeSlot> slots;
public long savedAtUnixMs; // 역산·디버그용
}
public static class PrestigeSaveService {
public const int CURRENT_SCHEMA = 1;
public static void Save(PlayerPrestigeState state, string accountId);
public static PlayerPrestigeState Load(string accountId); // 파일 없으면 새 상태 반환
public static PlayerPrestigeState Migrate(PrestigeSaveDto dto);
// schemaVersion < CURRENT_SCHEMA → 순차 마이그레이션 훅
// schemaVersion > CURRENT_SCHEMA → 버전 불일치 경고 + 백업 + 신규 초기화
}
}
5.3 저장 경로
Application.persistentDataPath/
account/
<accountId>/
prestige.v1.json ← 현 Sprint 02
prestige.v1.json.bak ← 매 저장 전 이전 파일 백업 (마이그레이션 실패 대비)
accountId 는 현재 싱글 계정 전제 = "local" 고정. Phase 5 Firebase 도입 시 Firebase UID 치환.- 계정 영구 (Q2): 캐릭터 교체·삭제해도
account/<accountId>/prestige.v1.json 이 남음 → 진전 보존. - 멀티 캐릭터 도입 시점에도 경로는
account/ 하위지 character/ 하위 아님.
5.4 마이그레이션 훅 (schemaVersion 1 → 2 시 대비)
public static PlayerPrestigeState Migrate(PrestigeSaveDto dto) {
switch (dto.schemaVersion) {
case 1: return LoadV1(dto);
// case 2: return LoadV2(dto); ← Sprint 05 시즌 스키마 변경 시 추가
default:
Debug.LogWarning($"[PrestigeSave] Unknown schemaVersion {dto.schemaVersion}, starting fresh.");
BackupCurruptFile(dto);
return new PlayerPrestigeState();
}
}
5.5 저장 타이밍
- 즉시 저장:
Spend / RefundAll / Unlock / MigrateSeasonalToPermanent 호출 직후. - 배치 저장:
AddPermanentPp / AddSeasonalPp 는 1초 디바운스 (방치 드립 초당 다회 호출 대비). - 게임 종료 훅(
OnApplicationQuit / OnApplicationPause(true)) 에서 강제 flush.
6. 이벤트 훅 연결
6.1 Sprint 01 W2 기존 훅 (3개)
| 훅 | Sprint 01 W2 상태 | 본 Sprint 02 W2 연결 |
|---|
MilestoneGranter.Award(100) | 구현 완료 · GrantResult 반환만 | 신규: PlayerPrestigeState.Unlock() 호출 |
MilestoneGranter.Award(200) | §10.4 "TODO 주석만, 미발화" | 활성화: 이벤트 발화 + AddPermanentPp(bonus) |
OnFloorReached(floor) | 이벤트 채널 B (§7.4) | PrestigePpAccumulator.OnStageCompleted 구독 추가 |
6.2 신규 훅 (3개)
| 훅 | 발화 시점 | 핸들러 |
|---|
OnBossKilled(floor, bossCode) | 10층 단위 보스 사망 | PrestigePpAccumulator.GrantBossBonus(floor) |
OnIdleReward(elapsedSec, maxFloor) | 앱 재진입 시 1회 | PrestigePpAccumulator.GrantIdleDrip(elapsed, maxFloor) (Sprint 03 IdleTicker 가 실제 호출, W2는 핸들러만 존재) |
OnSeasonEnd | Sprint 05 시즌 시스템 | PlayerPrestigeState.MigrateSeasonalToPermanent(0.5f) (W2는 구독자만 등록, 발화자 없음) |
합계 = 기존 3 + 신규 3 = 6개 훅.
6.3 PrestigePpAccumulator 스케치
namespace GodBlade.Runtime.Prestige {
public sealed class PrestigePpAccumulator {
readonly PlayerPrestigeState _state;
readonly MilestoneRewardCatalogSO _milestone; // Sprint 01 W2 자산 재활용
public PrestigePpAccumulator(PlayerPrestigeState state, MilestoneRewardCatalogSO milestone);
// 이벤트 핸들러 — Sprint 01 W2 이벤트 채널 구독
public void OnStageCompleted(int floor); // §3.2 (A) 층 첫 클리어
public void GrantBossBonus(int floor); // §3.2 (B) 보스 반복
public void GrantIdleDrip(long elapsedSec, int maxFloor); // §3.2 (C) 방치 드립 (Sprint 03 틱커 발화)
public void GrantMilestone(int floor); // §3.2 (D) 마일스톤 1회성
public void GrantDailyQuest(); // §3.2 (E) 일일/광고 (Sprint 03+)
}
}
- 계산 공식은
prestige_system_spec.md §3.2 인용. 계수 하드코딩 대신 PrestigeItemCatalogSO 의 형제 SO(PrestigeGainRatesSO) 에 TSV 주입 — Sprint 03+ 확장 여지 남김 (W2 스코프 아님). - W2 범위:
GrantIdleDrip 은 핸들러 시그니처만. 발화자(IdleTicker)는 Sprint 03 방치 시스템에서 구현.
6.4 이벤트 채널 분리 (Sprint 01 W2 §7.4 연장)
이벤트 채널 A: OnMonsterKilled(floor, mobKind, monsterCode)
→ LootRoller (Sprint 02)
→ PrestigePpAccumulator.GrantBossBonus (mobKind=Boss 시만) ← 신규 구독
이벤트 채널 B: OnFloorReached(floor)
→ MilestoneGranter.Award (Sprint 01 W2 기존)
→ PrestigePpAccumulator.OnStageCompleted ← 신규 구독
→ (floor==100 && !IsUnlocked) → PlayerPrestigeState.Unlock() ← 신규 경로
→ (floor==200) → PrestigePpAccumulator.GrantMilestone(200) ← 신규 활성화
이벤트 채널 C (신규): OnSeasonEnd
→ PlayerPrestigeState.MigrateSeasonalToPermanent(0.5f)
→ 발화자는 Sprint 05+ SeasonSystem. W2 구독자만 등록.
- 기존 채널 A·B 확장만으로 구조 안정. 채널 C는 Sprint 05까지 정의만 존재.
7. 테스트 전략
7.1 asmdef 배치 (Sprint 01 W2 DQ4 재활용)
- 신설 asmdef: 없음 — Sprint 01 W2
GodBlade.Tests.Editor 재활용. - 파일 추가:
Assets/_Project/Tests/Editor/
PrestigeCalculatorTests.cs
PrestigeStateInvariantTests.cs
PrestigeSaveRoundTripTests.cs
PrestigeSeasonMigrationTests.cs
PrestigeUnlockGateTests.cs
GodBlade.Tests.Editor.asmdef 의 references 에 GodBlade.Runtime 추가 (Sprint 01 W2에선 GodBlade.Data / GodBlade.Editor 만 포함돼 있을 가능성).
7.2 핵심 테스트 목록 (7개)
| # | 테스트 | 검증 |
|---|
| T1 | Unlock_BeforeFloor100_Throws | 100층 미도달 상태 Spend 호출 → NOT_UNLOCKED |
| T2 | Unlock_AtFloor100_SetsIsUnlockedTrue | Award(100) 경로 → IsUnlocked == true |
| T3 | CalculateLevel_SoftCapBoundary | investedPp=4,5,6 시 레벨 산출 (선형→log 전환 경계) |
| T4 | Spend_IdempotentRefund | 30항목 중복 투입 후 RefundAll 시 PP 풀 복원 |
| T5 | MigrateSeasonal_50PercentRetained | Seasonal 100 PP + 투자 50 → Migrate → Permanent +75 / Seasonal 0 |
| T6 | SaveLoad_RoundTrip_PreservesAllSlots | Save → Load 후 PermanentPp / Slots 완전 일치 |
| T7 | Invariant_SumOfInvestedPp_LessOrEqualPool | 무작위 1000 시뮬 후 I2·I3 불변식 유지 |
7.3 테스트 프레임워크 통합 상태
- Sprint 01 W2에서
com.unity.test-framework: 1.4.5 확정 · GodBlade.Tests.Editor.asmdef 신설 확정. - 본 W2에서는 참조만 추가 → Unity 첫 Edit-mode 실행으로 검증.
7.4 Play-mode 테스트
- 본 W2 범위 제외. Addressables Play-mode 로드 테스트는 Sprint 03+ 스모크 스윗에 통합.
8. 컴파일러 시뮬레이션 체크리스트 (§9 재적용 + 프레스티지 특화)
8.1 Sprint 01 W2 11개 체크포인트 재적용 요약
| CP | Sprint 01 W2 원문 | 프레스티지 재적용 |
|---|
| CP9-1 | asmdef references 배열 완결성 | GodBlade.Tests.Editor.asmdef 에 GodBlade.Runtime 추가 필수 |
| CP9-2 | Editor asmdef의 Unity.Addressables.Editor | PrestigeItemImporter 가 Addressables 그룹 생성 → 동일 참조 필요 |
| CP9-3 | InternalsVisibleTo 중복 금지 | GodBlade.Data 어셈블리 기존 한 파일 고수. 신규 PrestigeItemCatalogSO.cs 에 중복 선언 금지 |
| CP9-4 | #if UNITY_EDITOR 가드 | PrestigeSaveService 에 에디터 디버그 Inspector 훅 넣을 시 주의 |
| CP9-5 | 신규 네임스페이스 충돌 | GodBlade.Runtime.Prestige / GodBlade.Data.Prestige 서브 네임스페이스 — rootNamespace 수정 금지. NOTE: Editor asmdef는 파일명=GodBlade.Editor / rootNamespace=GodBlade.EditorTools (Sprint 00 컨벤션). PrestigeItemImporter 는 namespace GodBlade.EditorTools 사용. (scaffold sync audit 2026-04-21 D1 반영) |
| CP9-6 | TSV CRLF/LF Trim | Sprint 01 W2 TsvReader 공통화 재사용 → 자동 커버 |
| CP9-7 | BOM 트림 중복 | 공통 TsvReader 한 곳만 — 프레스티지 Importer는 호출만 |
| CP9-8 | Dictionary 직렬화 금지 | PrestigeItemCatalogSO._byItemId 는 [NonSerialized] + OnEnable 빌드 |
| CP9-9 | Enum underlying type | PpChannel : byte, PrestigeTab : byte — DOTS 친화. 단 itemId : ushort 로 16bit 여유 확보 |
| CP9-10 | Editor에서 System.Random 금지 | Importer는 결정론. RNG는 PrestigePpAccumulator 런타임에서만 |
| CP9-11 | Addressables 그룹 중복 | settings.FindGroup("Prestige") 선확인 후 없을 때만 생성 |
8.2 프레스티지 특화 체크포인트 (5개, 신규)
- [CP-P1] 새 enum asmdef 경계 using 누락
PpChannel 은 GodBlade.Runtime.Prestige 네임스페이스. GodBlade.Data 의 PrestigeItemRow 는 PpChannel 을 참조하지 않음(채널 분리는 런타임 상태 소관). 혼동 시 using GodBlade.Runtime.Prestige; 삽입 유혹 발생 → 금지(역방향 참조로 asmdef 사이클).- 대응:
PpChannel 은 GodBlade.Runtime.Prestige 에만. Data 어셈블리에서 참조 금지.
- [CP-P2]
PrestigeItemCatalogSO 기본값 초기화 누락
rows = null 상태로 OnEnable 진입 시 NRE. 생성자 시점 빈 리스트 초기화 필수:
[SerializeField] List<PrestigeItemRow> rows = new(); // C# 9 target-typed new
- 빈 SO 생성(메뉴
Create > GodBlade/Data/Prestige Item Catalog) 직후 Inspector 열었을 때 NRE 방지.
- [CP-P3] 저장 스키마 변경 시 구 JSON 호환성
- Sprint 05 시즌 도입 시
PrestigeSaveDto 에 seasonalRetainRatio 등 신규 필드 추가 가능. - Unity
JsonUtility 는 누락 필드에 기본값 주입하지만, 신구 필드 혼재 테스트 없이 릴리스 시 Editor 실행 중 유저 데이터 유실 위험. - 대응:
PrestigeSaveRoundTripTests.cs 에 v1 JSON 파일 하드코딩 문자열 → Load → 현 상태 재현 테스트 고정. 스키마 변경 PR 마다 이 테스트 갱신.
- [CP-P4]
long 오버플로
PermanentPp 가 long 이지만 방치 드립은 초당 호출 가능. Sprint 03 틱커 도입 후 long.MaxValue 근처까지 쌓을 시나리오는 없지만 AddPermanentPp(long.MaxValue) 같은 치트 입력 방어 필요.- 대응:
AddPermanentPp 에 checked 블록 + amount < 0 가드 + 누적값 상한 1e15 경고 로그.
- [CP-P5] Sprint 00 회귀 2건 재발 경고 (반드시 유지)
- CS0246 (
Unity.ResourceManager 누락): PrestigeBootstrap 이 Addressables로 PrestigeItemCatalogSO 로드 → GodBlade.Runtime.asmdef 의 Unity.ResourceManager 참조 확인 (Sprint 01 W2 §10.3 기 확인, 재확인 필수). - CS1061 (
InternalsVisibleTo 오타): GodBlade.Data 내 기존 한 파일의 [assembly: InternalsVisibleTo("GodBlade.Editor")] 가 이미 존재 → Importer에서 내부 세터 접근 시 자동 통과. 신규 SO 파일에 중복 선언 금지(CP9-3 재강조).
8.3 PR 머지 전 필수 절차
- Unity 6 에디터를 연 상태에서 파일 저장.
- Console 새 에러 0 확인 (Warning은 허용, Error는 금지).
- Edit-mode 테스트 7개(§7.2) 전부 Green.
- Addressables
Build > Clean Build → Build Player Content 성공. - 위 4개 중 하나라도 실패 시 PR 재작업 (Sprint 00 정적 리뷰 통과 후 회귀 2건 재발 방지).
9. 리스크 · 미해결
9.1 Sprint 03 DOTS 전환 시 런타임 레이어 재작성 비용
PrestigeSlot 을 blittable struct 로 설계(§1.3) → IComponentData 이식 1:1.PlayerPrestigeState 클래스는 재작성 필요 (MonoBehaviour / POCO → IComponentData + System 분리). 추정 8~16h.- 이벤트 기반 훅(§6)은 ECS World Event 로 리라우팅 필요 → Sprint 03 범위.
- 완화: Sprint 03 DOTS 전환 결정 전 프로파일러 측정 데이터 확보. 유저 1명 / 60슬롯 / 초당 1틱 시뮬은 MonoBehaviour 로 충분.
9.2 Sprint 05 Seasonal 도입 시 이미 Permanent에 쌓인 PP 재분배 정책
- 현재 정책: Permanent는 불가역. Seasonal은 시즌 종료 시 50% Permanent 전환.
- 리스크: Sprint 02~04 기간 유저가 Permanent에 700+ PP 쌓음 → Sprint 05 첫 시즌 참여 동기 약화 우려.
- 완화 옵션(Sprint 05 결정 유예):
- (a) 시즌 종료 시 Seasonal → Permanent 100% 전환 (스펙 Q3 변경 필요)
- (b) 시즌 한정 보너스 컨텐츠로 Seasonal 참여 유인
- 현 W2 설계는 양쪽 모두 수용 가능 (파라미터
retainRatio 만 조정).
9.3 30항목 × 무한 레벨 UI 표시
- 본 범위 밖 (UX 설계 영역 = game-designer).
- W2 는
PlayerPrestigeState.OnSlotChanged 이벤트만 제공 → UI 레이어는 Sprint 02 W2 후반부 또는 Sprint 03 에 UIToolkit/UGUI 로 소비. - 리스트 스크롤 vs 트리 vs 탭 — PD/game-designer 결정 대기.
9.4 accountId 싱글 고정 → Firebase UID 전환 시점
- 현
"local" 고정. Phase 5 Firebase 도입 시 PrestigeSaveService.Load(firebaseUid) 호출자만 변경. - 저장 파일 병합 정책: 기기 1
"local" + Firebase 최초 로그인 시 → 어느 쪽 우선? Phase 5 결정 유예.
10. PD 결정 필요 — 열린 질문
Q1. 데이터 모델 옵션 A/B/C
- 옵션 A(단일 SO 60필드) / B(
PrestigeSlot struct + List) / C(DOTS ECS) - 권장: B. read-write hot path 에 SO 부적합, DOTS는 Sprint 03+ 측정 후.
Q2. 로컬 저장 방식
- JSON(권장) / Binary / PlayerPrefs
- 권장: JSON + schemaVersion. 디버그·마이그레이션 용이.
Q3. Firebase 클라우드 동기 Sprint 02 포함 여부
- 미포함(권장, Phase 5) / Sprint 02 포함
- 권장: 미포함. 로컬 저장만으로 Sprint 02~04 커버 가능. Phase 5에
PrestigeSaveService.Load(firebaseUid) 스위치 추가.
Q4. Sprint 05 Seasonal → Permanent retainRatio 값 동결 시점
- 현재: Sprint 02 코드에 기본값
0.5f 하드코딩 - Sprint 05 시점에 재논의 가능하도록 파라미터로 유지 (권장) / Sprint 02 단계부터 SO로 분리
- 권장: 파라미터. 현 범위에선 하드코딩 충분. Sprint 05 시점 SO 주입 전환 과제 별도 등록.
11. Sprint 01 W2 정합성 검증
| 축 | Sprint 01 W2 기준 | 본 Sprint 02 W2 | 일치? |
|---|
| asmdef 구조 | GodBlade.Data / GodBlade.Runtime / GodBlade.Editor / GodBlade.Tests.Editor | 동일 4개 재활용 | ✅ |
| 카탈로그 저장 | SO 옵션 A (개별 SO) | SO 옵션 A (PrestigeItemCatalogSO 단일) | ✅ |
TsvReader 공통화 | Sprint 02 이월 (DQ3) | 본 W2가 첫 소비자 | ✅ (PR 순서 보장 필요) |
InternalsVisibleTo 위치 | Data 어셈블리 한 파일 | 신규 SO는 중복 선언 금지 | ✅ |
| Addressables 그룹 | Rewards | 신규 Prestige 그룹 | ✅ (별도 그룹, 충돌 없음) |
| 이벤트 채널 | A(몬스터) · B(층) | A·B 확장 + C(시즌) 신설 | ✅ |
Grade / MobKind enum | GodBlade.Data | 본 문서 변경 없음 | ✅ |
| 테스트 asmdef | GodBlade.Tests.Editor 신설 | 재활용 + GodBlade.Runtime 참조 추가 | ✅ |
| §9 체크리스트 | 11개 | 11개 재적용 + 5개 특화 추가 = 16개 | ✅ (확장) |
| DOTS 전환 전략 | 스키마 호환만, 도입 보류 | 동일 원칙 | ✅ |
결론: Sprint 01 W2 v1.0 APPROVED 설계와 구조적 충돌 0건. 확장·재활용만 존재.
12. 분량 · 체크 추적
- 본 문서: 약 7쪽 (마크다운 렌더 기준).
- 섹션 10개 + 정합성 검증 + 분량 메타 = 12개 섹션.
- §8 체크리스트: 16개 (Sprint 01 W2 11 재적용 + 프레스티지 특화 5).
- §6 이벤트 훅: 6개 (기존 3 + 신규 3).
- 실구현 코드 줄 수: 0 (설계 시그니처·의사코드만, PD 제약 준수).
- PD 결정 대기 열린 질문: 4개.
(끝 — DRAFT v0.1, PD 결정 대기)