← 자료실/designsLOWAPPROVED

Sprint 02 W2 — Prestige Entity / Runtime 기술 스케치

designs/sprint-02-w2-prestige-tech-sketch.md

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 전제: 슬롯 수 × 상태 성격

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 가 실시간 산출 (카탈로그 공식 변경 시 재컴파일 대응)
  }
}

1.4 Sprint 01 W2 DQ1(SO 옵션 A)과의 관계


2. 항목 카탈로그 (PrestigeItemCatalogSO)

2.1 위치·asmdef

2.2 TSV 입력 (game-designer Sprint 02 W1 산출물 가정)

  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   ...
  ...

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);
  }
}

2.4 Importer (PrestigeItemImporter)

2.5 Sprint 02 W1 TSV 공통화 연계

  1. TsvReader.cs 공통화 (Sprint 00 Stage Importer 재연결)
  2. PrestigeItemImporter.cs 신규 (공통 TsvReader 사용)

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 시스템 확정 후 재조정)
    }
  }
}

4. PP 슬롯 2채널 구조 (Q3 핵심)

4.1 설계 목표

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)

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     ← 매 저장 전 이전 파일 백업 (마이그레이션 실패 대비)

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 저장 타이밍


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는 핸들러만 존재)
OnSeasonEndSprint 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+)
  }
}

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 구독자만 등록.

7. 테스트 전략

7.1 asmdef 배치 (Sprint 01 W2 DQ4 재활용)

  Assets/_Project/Tests/Editor/
    PrestigeCalculatorTests.cs
    PrestigeStateInvariantTests.cs
    PrestigeSaveRoundTripTests.cs
    PrestigeSeasonMigrationTests.cs
    PrestigeUnlockGateTests.cs

7.2 핵심 테스트 목록 (7개)

#테스트검증
T1Unlock_BeforeFloor100_Throws100층 미도달 상태 Spend 호출 → NOT_UNLOCKED
T2Unlock_AtFloor100_SetsIsUnlockedTrueAward(100) 경로 → IsUnlocked == true
T3CalculateLevel_SoftCapBoundaryinvestedPp=4,5,6 시 레벨 산출 (선형→log 전환 경계)
T4Spend_IdempotentRefund30항목 중복 투입 후 RefundAll 시 PP 풀 복원
T5MigrateSeasonal_50PercentRetainedSeasonal 100 PP + 투자 50 → Migrate → Permanent +75 / Seasonal 0
T6SaveLoad_RoundTrip_PreservesAllSlotsSave → Load 후 PermanentPp / Slots 완전 일치
T7Invariant_SumOfInvestedPp_LessOrEqualPool무작위 1000 시뮬 후 I2·I3 불변식 유지

7.3 테스트 프레임워크 통합 상태

7.4 Play-mode 테스트


8. 컴파일러 시뮬레이션 체크리스트 (§9 재적용 + 프레스티지 특화)

8.1 Sprint 01 W2 11개 체크포인트 재적용 요약

CPSprint 01 W2 원문프레스티지 재적용
CP9-1asmdef references 배열 완결성GodBlade.Tests.Editor.asmdef 에 GodBlade.Runtime 추가 필수
CP9-2Editor asmdef의 Unity.Addressables.EditorPrestigeItemImporter 가 Addressables 그룹 생성 → 동일 참조 필요
CP9-3InternalsVisibleTo 중복 금지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-6TSV CRLF/LF TrimSprint 01 W2 TsvReader 공통화 재사용 → 자동 커버
CP9-7BOM 트림 중복공통 TsvReader 한 곳만 — 프레스티지 Importer는 호출만
CP9-8Dictionary 직렬화 금지PrestigeItemCatalogSO._byItemId 는 [NonSerialized] + OnEnable 빌드
CP9-9Enum underlying typePpChannel : byte, PrestigeTab : byte — DOTS 친화. 단 itemId : ushort 로 16bit 여유 확보
CP9-10Editor에서 System.Random 금지Importer는 결정론. RNG는 PrestigePpAccumulator 런타임에서만
CP9-11Addressables 그룹 중복settings.FindGroup("Prestige") 선확인 후 없을 때만 생성

8.2 프레스티지 특화 체크포인트 (5개, 신규)

  1. [CP-P1] 새 enum asmdef 경계 using 누락
  1. [CP-P2] PrestigeItemCatalogSO 기본값 초기화 누락
     [SerializeField] List<PrestigeItemRow> rows = new();  // C# 9 target-typed new
  1. [CP-P3] 저장 스키마 변경 시 구 JSON 호환성
  1. [CP-P4] long 오버플로
  1. [CP-P5] Sprint 00 회귀 2건 재발 경고 (반드시 유지)

8.3 PR 머지 전 필수 절차

  1. Unity 6 에디터를 연 상태에서 파일 저장.
  2. Console 새 에러 0 확인 (Warning은 허용, Error는 금지).
  3. Edit-mode 테스트 7개(§7.2) 전부 Green.
  4. Addressables Build > Clean Build → Build Player Content 성공.
  5. 위 4개 중 하나라도 실패 시 PR 재작업 (Sprint 00 정적 리뷰 통과 후 회귀 2건 재발 방지).

9. 리스크 · 미해결

9.1 Sprint 03 DOTS 전환 시 런타임 레이어 재작성 비용

9.2 Sprint 05 Seasonal 도입 시 이미 Permanent에 쌓인 PP 재분배 정책

9.3 30항목 × 무한 레벨 UI 표시

9.4 accountId 싱글 고정 → Firebase UID 전환 시점


10. PD 결정 필요 — 열린 질문

Q1. 데이터 모델 옵션 A/B/C

Q2. 로컬 저장 방식

Q3. Firebase 클라우드 동기 Sprint 02 포함 여부

Q4. Sprint 05 Seasonal → Permanent retainRatio 값 동결 시점


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 enumGodBlade.Data본 문서 변경 없음✅
테스트 asmdefGodBlade.Tests.Editor 신설재활용 + GodBlade.Runtime 참조 추가✅
§9 체크리스트11개11개 재적용 + 5개 특화 추가 = 16개✅ (확장)
DOTS 전환 전략스키마 호환만, 도입 보류동일 원칙✅

결론: Sprint 01 W2 v1.0 APPROVED 설계와 구조적 충돌 0건. 확장·재활용만 존재.


12. 분량 · 체크 추적


(끝 — DRAFT v0.1, PD 결정 대기)