code:
public void GrantVictoryBonus()
    {
        if (!IsRiskBattle) return;

        // ✅ [AUD-CDX-21802 Fix] GameFlow null 시 GoldManager 직접 폴백 — 골드 유실 방지
        // ✅ [AUD-772-H002] AddGold 반환값 검증 — 실패 시 성공 메시지 차단
        bool goldPaid = false;
        if (GameFlow.Instance != null) goldPaid = GameFlow.Instance.AddGold(BalanceConfig.RISK_PATH_GOLD_BONUS);
        // ✅ [AUD-CDX-33402 Fix] GoldManager null 시 goldPaid=false 유지 — false-positive 보상 성공 방지
        else if (GoldManager.Instance != null) { GoldManager.Instance.AddGoldDirect(BalanceConfig.RISK_PATH_GOLD_BONUS); goldPaid = true; SaveManager.Instance?.SaveGame(forceImmediate: true); }
        else { Debug.LogWarning("[RiskPath] GameFlow/GoldManager 모두 null — 골드 보상 미지급"); }
        // ✅ [AUD-CDX-33401 Fix] CurrentSave null 시 토큰 미지급 명시적 경고 — 성공 메시지 불일치 방지
        if (SaveManager.Instance != null && SaveManager.Instance.CurrentSave != null) SaveManager.Instance.AddTokens(BalanceConfig.RISK_PATH_TOKEN_BONUS);
        else Debug.LogWarning("[RiskPath] SaveManager/CurrentSave null — 토큰 보너스 미지급");

        if (goldPaid)
        {
            MsgManager.Instance?.InputInstantMsg(
                Loc.Fmt("System/MapRiskPathSystem/L85", BalanceConfig.RISK_PATH_GOLD_BONUS, BalanceConfig.RISK_PATH_TOKEN_BONUS),
                InstantMsgType.Good);
            Debug.Log($"[RiskPath] 보상 지급 +{BalanceConfig.RISK_PATH_GOLD_BONUS}G / +{BalanceConfig.RISK_PATH_TOKEN_BONUS}T");
        }
        else
        {
            Debug.LogWarning($"[RiskPath] GrantVictoryBonus — AddGold failed for {BalanceConfig.RISK_PATH_GOLD_BONUS}G");
        }

        DeactivateBattle();
    }


code:
public override void Apply(CharacterBase c, StatRulesSO rules = null)
    {
        if (!Item) return;

        // ✅ null 가드 + AddItem 결과 확인 — 인벤토리 꽉 또는 시스템 없을 때 조용히 실패 방지
        // ✅ [AUD-CDX-22701 Fix] 지급 실패 시 예외 발생 — TryApply false 반환으로 보상 선택 소비 방지
        if (InventorySystem_TRPG.Instance == null)
            throw new System.InvalidOperationException($"[Reward_Item] InventorySystem_TRPG 없음 — 아이템 지급 불가: {Item.ItemName}");
        bool added = InventorySystem_TRPG.Instance.AddItem(Item, Amount);
        if (!added)
            throw new System.InvalidOperationException($"[Reward_Item] 아이템 지급 실패 (인벤토리 꽉 또는 미지원 타입): {Item.ItemName}");

        // ✅ 도감 등록 — Reward 경로는 MapIntegration을 거치지 않으므로 직접 호출
        CollectionManager.Instance?.RegisterItem(Item.ItemId);

        if (AutoApplyEffect)
            ItemEffectExecutor.Apply(Item, c, rules);

        var part = Resources.Load<TextPartSO>("Text/reward_gain_item");
        NarrativeLogger.WriteUnique(part, NarrativeLogger.Vars(
            characterName: c ? c.DisplayName : "Player",
            itemName: Item.ItemName
        ));
    }


code:
public void ClearFormation()
    {
        // ✅ [AUD-230] 동료 메타 진형 보너스를 스탯에서 차감 후 정리 — 누적 방지
        RevertAllCompanionMetaFormationBonuses();
        _rows.Clear();
        _currentParty = null;
        _enemyParty = null;
    }


code:
void ApplySubclassDefaults()
    {
        // 전부 해제
        foreach (var e in _skillEntries) e.Toggle.isOn = false;
        foreach (var e in _passiveEntries) e.Toggle.isOn = false;

        if (_subclass == null && _classSO != null)
        {
            // 서브클래스 없으면 기본 StartSkills 선택
            var defaults = _classSO.StartSkills;
            if (defaults != null)
            {
                foreach (var defSkill in defaults)
                {
                    if (defSkill == null) continue;
                    var match = _skillEntries.FirstOrDefault(e =>
                        e.Skill != null && e.Skill.name == defSkill.name);
                    if (match != null) match.Toggle.isOn = true;
                }
            }
            return;
        }

        // _subclass가 null이면 적용할 기본값 없음 — 예산만 표시
        if (_subclass == null) return;

        // 서브클래스 StartSkillOverrides
        if (_subclass.StartSkillOverrides != null && _subclass.StartSkillOverrides.Count > 0)
        {
            foreach (var defSkill in _subclass.StartSkillOverrides)
            {
                if (defSkill == null) continue;
                var match = _skillEntries.FirstOrDefault(e =>
                    e.Skill != null && e.Skill.name == defSkill.name);
                if (match != null) match.Toggle.isOn = true;
            }
        }
        else if (_classSO != null && _classSO.StartSkills != null)
        {
            // Override 없으면 ClassSO 기본 StartSkills
            foreach (var defSkill in _classSO.StartSkills)
            {
                if (defSkill == null) continue;
                var match = _skillEntries.FirstOrDefault(e =>
                    e.Skill != null && e.Skill.name == defSkill.name);
                if (match != null) match.Toggle.isOn = true;
            }
        }

        // 서브클래스 StartPassiveOverrides
        if (_subclass.StartPassiveOverrides != null)
        {
            foreach (var defPassive in _subclass.StartPassiveOverrides)
            {
                if (defPassive == null) continue;
                var match = _passiveEntries.FirstOrDefault(e =>
                    e.Passive != null && e.Passive.name == defPassive.name);
                if (match != null) match.Toggle.isOn = true;
            }
        }

        UpdateBudgetDisplay();
    }


code:
private static void HideSceneInstancePrefabsInitial()
        {
            HideSceneInstancePrefabs();
            // 매 씬 로드 hook — 새 씬마다 즉시 hide
            if (!_sceneHookAttached)
            {
                // ★ [R80cz-v22u 2026-05-31] 빌드 검증 마커 — 이 줄이 로그에 보이면 v22u 가 컴파일·실행 중. ★
                //   사용자/나 모두 "내 수정이 빌드에 들어갔나"를 1초에 확인 가능 (재컴파일 누락 진단).
                Debug.Log("[BUILD-VER] ★ R80cz-v22u (2026-05-31) 실행 중 — NodeModalLayoutFixer 자식재귀 + 탐험골드 집계 + 현재위치라벨 단일화 ★");
                _sceneHookAttached = true;
                UnityEngine.SceneManagement.SceneManager.sceneLoaded += (scene, mode) =>
                {
                    try { HideSceneInstancePrefabs(); } catch { }
                };
            }
            // 매 frame polling 헬퍼 부착 — Singleton GO (중복 생성 방지)
            EnsureWatcher();
        }


code:
public void OnRewardSelected()
    {
        // ✅ [AUD-Anti] 1회용 완료 콜백 실행 (SkillScholar 등에서 OnNodeCompleted 지연)
        if (OnRewardDoneCallback != null)
        {
            var done = OnRewardDoneCallback;
            OnRewardDoneCallback = null;
            done.Invoke();
        }

        // ✅ [Round 120] 보스 격파 특별 보상 후 처리 — OnPostBossRewardComplete 콜백 우선
        // 보스전 후 특별 보상을 선택했으면 콜백(OnRunComplete 등)을 실행하고 종료
        var tbc = BattleController.Instance;
        if (tbc?.OnPostBossRewardComplete != null)
        {
            var cb = tbc.OnPostBossRewardComplete;
            tbc.OnPostBossRewardComplete = null; // 1회 소비
            // ✅ [2026-06-14 Codex P2] 콜백 소비 직후 pending=false 즉시 커밋 — RewardSelected 스냅샷(콜백 소비 ★전★)에
            //   SnapshotPendingPostBossReward=true 가 박혀, 보상 적용 후 콜백 완료 전 종료/크래시 시 보스보상 재출현(중복 수령) 차단.
            GameFlow.Instance?.RequestMidRunSnapshot("BossRewardConsumed", force: true);
            cb.Invoke();
            return; // 맵 복귀 생략 — 런 완료 화면으로 이동
        }

        // ★ [2026-06-10] 런 종료(RunEnding/GameOver) 중 보상 선택(승리 해금 픽 등) 후 맵 복귀 차단 —
        //   OnPostBossRewardComplete 소비 후 뜨는 종료기 보상에서 ReturnToMap 이 맵+타이머를 되살리던 버그.
        //   종료 흐름은 자체 씬 전환(Scenes.Victory 등)이 처리하므로 여기서는 복귀하지 않는다.
        var endPhase = GameFlow.Instance != null ? GameFlow.Instance.CurrentRunPhase : GameFlow.RunPhase.Lobby;
        if (endPhase == GameFlow.RunPhase.RunEnding || endPhase == GameFlow.RunPhase.GameOver)
        {
            Debug.Log("[RewardUI] 런 종료 중 보상 선택 — 맵 복귀 생략");
            return;
        }

        // ✅ 레벨업 큐 — 대기 중인 레벨업이 있으면 맵 복귀 대신 다음 레벨업 처리
        if (_isLevelUpReward && PlayerLevel.Instance != null)
        {
            PlayerLevel.Instance.OnLevelUpRewardComplete();
            // ProcessNextLevelUp()이 ShowRewards를 다시 호출했으면 여기서 리턴
            // 큐가 비었으면 맵 복귀로 진행
            bool rewardUiReopened = RootPanel != null ? RootPanel.activeSelf : gameObject.activeSelf;
            if (rewardUiReopened) return; // 실제 보상 패널이 다시 열렸으면 리턴
        }

        // ✅ [2026-06-14 Codex P2] 재개된 노드 보상 — 노드는 이미 완료(스냅샷)이므로 ReturnToMap 의 OnEncounterCompleted
        //   재호출(노드 이중완료=NodesCompleted/유대XP/메타챌린지 중복)을 ★건너뛰고★ 맵 복귀만. 일반 선택은 아래 ReturnToMap.
        if (_resumedNodeReward)
        {
            _resumedNodeReward = false;
            // 맵 복귀 직전 force 스냅샷 — "보상 소비됨+맵 복귀" 상태 원자 커밋(재개 후 재크래시 시 pending 재출현 차단).
            //   이 시점 RewardUI 패널은 Close()로 비활성 → IsNodeRewardPending=false 로 SnapshotPendingNodeReward 커밋.
            GameFlow.Instance?.RequestMidRunSnapshot("ResumedNodeReward.Return", force: true);
            if (GameFlow.Instance != null) GameFlow.Instance.ShowMapPanel();
            else { MapManager.Instance?.EndNodeInteractionPause(); MapUI.Instance?.ShowMap(); }
            return;
        }

        // ✅ [AUD-CDX-88001] NodeExecutor null 시 완료 라이프사이클 보장
        if (NodeExecutor.Instance != null)
        {
            NodeExecutor.Instance.ReturnToMap();
            // ReturnToMap이 OnEncounterCompleted + ShowMapPanel/ShowMap까지 처리하므로 이후 호출 없음
        }
        else
        {
            GameFlow.Instance?.OnEncounterCompleted(
                MapIntegration.Instance?.NodesCompleted ?? 0);
            if (GameFlow.Instance != null)
                GameFlow.Instance.ShowMapPanel();
            else
            {
                // ✅ [AUD-CDX-20602 Fix] GameFlow null 폴백에서 pause 해제 보장
                MapManager.Instance?.EndNodeInteractionPause();
                MapUI.Instance?.ShowMap();
            }
        }
    }


code:
void OnStartRun()
    {
        // [AUD-CDX-31034] 재진입 방지 — 빠른 이중 클릭 시 환전/소모품 중복 실행 차단
        if (_isStarting) return;
        _isStarting = true;

        // 환전 처리 — [AUD-CDX-31027] 명시적 확인(Confirm) 시에만 실행
        // ✅ [2026-06-15 코덱스#258 Ex2] 즉시 커밋 제거 — RunStartFlow 에 ★예약★만 하고, 실제 런 시작 확정
        //   (TryStartGameWith[out]RunBible 의 IsRunStartRequestValid 통과) 직전에 커밋. 스테일/취소 시 폐기 → 토큰 유실 방지.
        if (_exchangeConfirmed && tokensToExchange > 0 && ExchangeSystem != null)
        {
            RunStartFlow.Instance?.SetPendingRunStartExchange(tokensToExchange);
        }
        tokensToExchange = 0;
        _exchangeConfirmed = false;

        // UI 닫기
        gameObject.SetActive(false);

        // ✅ [2026-06-13 Codex P2] 소모품 즉시효과 적용은 GameFlow.CommonRunInit(RunInit/성장/수정자 ★이후★)로 이동.
        //   기존엔 여기서 호출돼 StartLevel3 의 레벨업이 직후 RunInit(Level=1 리셋)에 덮였음. (여기 호출 제거)

#if UNITY_EDITOR || DEVELOPMENT_BUILD
        // ✅ 기지 전략 디버그 프리셋 (BaseLobbyUI 미구현 기간 테스트용)
        //    BaseStrategyManager Inspector에서 DebugPresetEnabled = true 설정 시 동작
        BaseStrategyManager.Instance?.ApplyDebugPreset();
#endif

        // ✅ RunStartFlow에 알림
        var runFlow = RunStartFlow.Instance;
        if (runFlow != null)
        {
            runFlow.OnRunStartComplete();
        }
        else
        {
            // Fallback: 직접 게임 시작
            var gameFlow = GameFlow.Instance;
            if (gameFlow != null)
            {
                gameFlow.StartRun();
            }
        }
    }


code:
private string ApplyLuckyBlessing()
        {
            // 3가지 중 랜덤 선택
            int choice = Random.Range(0, 3);

            string result = Loc.Get("Map/Fortune/BlessingDescend") + "\n\n"; // 별들의 축복이 내려옵니다...
            result += "━━━━━━━━━━━━━━━━━━━━━\n\n";

            switch (choice)
            {
                case 0:
                    // 즉시 골드 획득
                    int goldBonus = Random.Range(80, 151);
                    // ✅ [AUD-CDX-21602 Fix] GameFlow null 폴백 추가 — 보상 소실 방지
                    // ✅ [AUD-CDX-255001 Fix] AddGold 반환값 검증 — 지급 실패 시 성공 메시지 억제
                    bool goldBonusOk;
                    if (GameFlow.Instance != null)
                    {
                        goldBonusOk = GameFlow.Instance.AddGold(goldBonus);
                        if (!goldBonusOk) Debug.LogWarning($"[Fortune] 골드 지급 실패 — AddGold({goldBonus}) false");
                    }
                    else { GoldManager.Instance?.AddGoldDirect(goldBonus); SaveManager.Instance?.SaveGame(forceImmediate: true); goldBonusOk = GoldManager.Instance != null; } // ✅ [AUD-CDX-302001] fallback 즉시 커밋
                    if (goldBonusOk)
                    {
                        result += Loc.Fmt("Map/Fortune/BlessingGold", goldBonus) + "\n\n"; // ✨ 황금빛 빛이 쏟아집니다! 💰 골드 +{0}
                        result += Loc.Get("Map/Fortune/BlessingGoldWish"); // 행운이 함께 하길...
                    }
                    else
                        result += Loc.Get("Map/Altar/PrayNoEffect"); // 🙏 평온한 기도. 마음이 평온해집니다. 특별한 효과는 없습니다.
                    break;

                case 1:
                    // ✅ 다음 전투 보상 2배 — BlessingManager 연동 시도
                    var rewardBlessing = Resources.Load<BlessingSO>("Blessings/Bless_EventReward2x");
                    if (rewardBlessing != null && BlessingManager.Instance != null)
                    {
                        BlessingManager.Instance.Grant(rewardBlessing);
                        result += Loc.Get("Map/Fortune/BlessingReward2x") + "\n\n"; // 🎁 행운의 부적을 받았습니다! 다음 전투 보상이 2배가 됩니다!
                    }
                    else
                    {
                        // Fallback: BlessingSO 에셋 없으면 즉시 골드 보너스
                        int bonusGold = Random.Range(80, 161);
                        // ✅ [AUD-CDX-21602 Fix] GameFlow null 폴백 추가 — 보상 소실 방지
                        // ✅ [AUD-CDX-255001 Fix] AddGold 반환값 검증 — 지급 실패 시 성공 메시지 억제
                        bool bonusGoldOk;
                        if (GameFlow.Instance != null)
                        {
                            bonusGoldOk = GameFlow.Instance.AddGold(bonusGold);
                            if (!bonusGoldOk) Debug.LogWarning($"[Fortune] 폴백 골드 지급 실패 — AddGold({bonusGold}) false");
                        }
                        else { GoldManager.Instance?.AddGoldDirect(bonusGold); SaveManager.Instance?.SaveGame(forceImmediate: true); bonusGoldOk = GoldManager.Instance != null; } // ✅ [AUD-CDX-302001] fallback 즉시 커밋
                        result += bonusGoldOk
                            ? Loc.Fmt("Map/Fortune/BlessingBonusGold", bonusGold) + "\n\n" // 🎁 행운의 기운이 넘칩니다! 💰 골드 +{0}
                            : Loc.Get("Map/Altar/PrayNoEffect") + "\n\n"; // 🙏 평온한 기도. 마음이 평온해집니다. 특별한 효과는 없습니다.
                    }
                    result += Loc.Get("Map/Fortune/UseWisely"); // 현명하게 사용하세요...
                    break;

                case 2:
                    // ✅ 다음 상점 할인 30% — BlessingManager 연동 시도
                    var shopBlessing = Resources.Load<BlessingSO>("Blessings/Bless_ShopDiscount30");
                    if (shopBlessing != null && BlessingManager.Instance != null)
                    {
                        BlessingManager.Instance.Grant(shopBlessing);
                        result += Loc.Get("Map/Fortune/BlessingShopDiscount") + "\n\n"; // 💸 상인의 호의를 얻었습니다! 다음 상점에서 30% 할인!
                    }
                    else
                    {
                        // Fallback: BlessingSO 에셋 없으면 즉시 골드 보너스
                        int discountGold = Random.Range(60, 121);
                        // ✅ [AUD-CDX-21602 Fix] GameFlow null 폴백 추가 — 보상 소실 방지
                        // ✅ [AUD-CDX-255001 Fix] AddGold 반환값 검증 — 지급 실패 시 성공 메시지 억제
                        bool discountGoldOk;
                        if (GameFlow.Instance != null)
                        {
                            discountGoldOk = GameFlow.Instance.AddGold(discountGold);
                            if (!discountGoldOk) Debug.LogWarning($"[Fortune] 할인 골드 지급 실패 — AddGold({discountGold}) false");
                        }
                        else { GoldManager.Instance?.AddGoldDirect(discountGold); SaveManager.Instance?.SaveGame(forceImmediate: true); discountGoldOk = GoldManager.Instance != null; } // ✅ [AUD-CDX-302001] fallback 즉시 커밋
                        result += discountGoldOk
                            ? Loc.Fmt("Map/Fortune/BlessingDiscountGold", discountGold) + "\n\n" // 💸 상인의 축복이 내립니다! 💰 골드 +{0}
                            : Loc.Get("Map/Altar/PrayNoEffect") + "\n\n"; // 🙏 평온한 기도. 마음이 평온해집니다. 특별한 효과는 없습니다.
                    }
                    result += Loc.Get("Map/Fortune/GoodDeal"); // 좋은 거래가 되길...
                    break;
            }

            result += "\n━━━━━━━━━━━━━━━━━━━━━";

            return result;
        }


code:
private static bool UpgradeSkill(CharacterBase c)
    {
        var skillbook = c.GetComponent<PlayerSkillbook>();
        if (skillbook == null) skillbook = PlayerSkillbook.Instance;

        if (skillbook != null && skillbook.Skills != null && skillbook.Skills.Count > 0)
        {
            // ✅ [Fix] 실제 스킬 강화 구현 — 랜덤 스킬에 DamageMultiplier +15% 적용
            var randomSkill = skillbook.Skills[Random.Range(0, skillbook.Skills.Count)];
            // [Fix] DamageMultiplier 퍼센트 단위: 15 = +15% (내부에서 /100)
            // ✅ [AUD-CDX-257001 Fix] EnhanceSkill 반환값 검증 — 강화 상한 시 false → 아이템 소모 방지
            bool enhanced = skillbook.EnhanceSkill(randomSkill, SkillEnhancementType.DamageMultiplier, 15f);
            if (!enhanced)
            {
                ShowFeedback($"'{randomSkill.name}' already at enhance cap", Color.yellow);
                Debug.LogWarning($"[ItemEffect] UpgradeSkill → {randomSkill.name} 강화 상한 도달 — 아이템 소모 취소");
                return false;
            }

            ShowFeedback($"'{randomSkill.name}' Enhanced! +15%", Color.cyan);
            Debug.Log($"[ItemEffect] UpgradeSkill → {randomSkill.name} DamageMultiplier +15%");
            return true;
        }

        ShowFeedback("No skills to upgrade", Color.gray);
        return false;
    }


code:
private void ShowResumeRunPopup(int slotIndex, string playerName)
    {
        _pendingConfirmAction = () =>
        {
            // [이어하기] → 런 즉시 복귀 (스냅샷 1회 사용 후 GameFlow에서 자동 무효화)
            DLog.D($"[TitleScreen] 중단된 런 복귀: {playerName}");
            StartCoroutine(TransitionToResumeRun());
        };
        _pendingCancelAction = () =>
        {
            // [로비로] → 스냅샷 폐기 + 로비
            DLog.D($"[TitleScreen] 중단된 런 폐기 → 로비");
            var sm = SaveManager.Instance;
            if (sm?.CurrentSave?.MidRunSnapshot != null)
            {
                sm.CurrentSave.MidRunSnapshot.HasSnapshot = false;
                // ✅ [2026-06-15 E1] 런 폐기 시 활성 소모품 재고 차감 — 'use→save/quit→타이틀에서 폐기'로 소모품 무한 환불 차단.
                //   씬 ConsumableSystem 은 타이틀에서 null/empty 라 스냅샷의 ActiveConsumableTypes 기준으로 직접 차감(승리/패배 ConsumeActiveConsumables 와 동일 1개씩).
                var _snapCons = sm.CurrentSave.MidRunSnapshot.ActiveConsumableTypes;
                if (_snapCons != null)
                {
                    foreach (int _raw in _snapCons)
                    {
                        if (!System.Enum.IsDefined(typeof(ConsumableSystem.ConsumableType), _raw)) continue;
                        string _ckey = ConsumableSystem.KeyForType((ConsumableSystem.ConsumableType)_raw);
                        if (string.IsNullOrEmpty(_ckey)) continue;
                        int _cur = sm.CurrentSave.GetConsumableCount(_ckey);
                        if (_cur > 0) sm.CurrentSave.SetConsumableCount(_ckey, _cur - 1);
                    }
                }
                sm.CurrentSave.MidRunSnapshot.MapSeed = 0;
                // ✅ [AUD-CDX-131002 Fix] RunPrep 저장 필드도 함께 정리
                //   스냅샷 폐기 시 RunPrep 잔여 구매 상태가 다음 런에 잔류하는 것 방지
                sm.CurrentSave.RunPrepPurchasedCategoryIds?.Clear();
                sm.CurrentSave.RunPrepActiveBuffIds?.Clear();
                sm.CurrentSave.RunPrepShopGuaranteed = false;
                sm.CurrentSave.RunPrepGuaranteedCompanionId = null;
                sm.CurrentSave.RunPrepAnomalyDisableCount = 0;
                // ✅ [2026-06-15 코덱스#259] 즉시 저장 강제 — 런 포기는 파괴적 변경(스냅샷 폐기+소모품 차감+RunPrep 정리).
                //   기본 SaveGame()은 디바운스라 플러시 前 크래시 시 폐기 미반영 → 포기한 런 재출현/소모품 무한회수. (런종료 폐기와 동일 관례.)
                sm.SaveGame(forceImmediate: true);
            }
            StartCoroutine(TransitionToLobby(resetSave: false));
        };

        ShowConfirmDialog(Loc.Fmt("UI/Title/MidRunDetected", playerName)
            ?? $"{playerName}의 중단된 모험이 있습니다.\n\n이어서 플레이하시겠습니까?\n\n(이어하기 시 다음에 다시 같은 시점으로 돌아갈 수 없습니다)");

        // 버튼 텍스트 변경
        if (_confirmYesButton != null)
        {
            var txt = _confirmYesButton.GetComponentInChildren<TextMeshProUGUI>();
            if (txt != null) txt.text = Loc.Get("UI/Title/ResumeRun", "이어하기");
        }
        if (_confirmNoButton != null)
        {
            var txt = _confirmNoButton.GetComponentInChildren<TextMeshProUGUI>();
            if (txt != null) txt.text = Loc.Get("UI/Title/GoToLobby", "로비로");
        }
    }


code:
private static bool RerollSkill(CharacterBase c)
    {
        var skillbook = c.GetComponent<PlayerSkillbook>();
        if (skillbook == null) skillbook = PlayerSkillbook.Instance;

        if (skillbook != null && skillbook.Skills != null && skillbook.Skills.Count > 0)
        {
            // ✅ [Fix] 실제 리롤 구현 — 대체 스킬 먼저 탐색 후 제거 진행 (파괴적 손실 방지)
            var removeTarget = skillbook.Skills[Random.Range(0, skillbook.Skills.Count)];
            string removedName = removeTarget.name;

            // ✅ [AUD-022-L003] 정적 캐시 사용 — Resources.LoadAll 반복 호출 방지
            if (_cachedAllSkills == null || _cachedAllSkills.Length == 0)
                _cachedAllSkills = Resources.LoadAll<SkillDefinitionSO>("Skills");
            var allSkills = _cachedAllSkills;

            // ✅ [AUD-CDX-259001 Fix] 진행도 우회 방지 — 플레이어 레벨 이하 스킬만 후보 허용
            int playerLevel = PlayerLevel.Instance != null ? PlayerLevel.Instance.Level : 1;
            var candidates = new System.Collections.Generic.List<SkillDefinitionSO>();
            foreach (var s in allSkills)
            {
                if (!skillbook.Skills.Contains(s) && s != removeTarget && s.UnlockLevel <= playerLevel)
                    candidates.Add(s);
            }

            // ✅ [AUD-CDX-259002 Fix] 대체 후보 없으면 스킬 제거하지 않고 false 반환 — 아이템+스킬 이중 손실 방지
            if (candidates.Count == 0)
            {
                ShowFeedback("No replacement available (level or cap)", Color.yellow);
                Debug.LogWarning($"[ItemEffect] RerollSkill → no valid candidates for '{removedName}' — item not consumed");
                return false;
            }

            // 대체 스킬 확정 후 기존 스킬 제거
            var newSkill = candidates[Random.Range(0, candidates.Count)];

            // 스킬 제거
            skillbook.Skills.Remove(removeTarget);

            // 로드아웃에서도 제거
            if (skillbook.ActiveLoadout != null)
            {
                for (int i = 0; i < skillbook.ActiveLoadout.Count; i++)
                {
                    if (skillbook.ActiveLoadout[i] == removeTarget)
                        skillbook.ActiveLoadout[i] = null;
                }
            }

            // KnownSkills에서도 제거
            if (skillbook.KnownSkills != null)
                skillbook.KnownSkills.Remove(removeTarget);

            skillbook.Skills.Add(newSkill);
            if (skillbook.KnownSkills != null)
                skillbook.KnownSkills.Add(newSkill);

            // 빈 로드아웃 슬롯에 자동 배치
            if (skillbook.ActiveLoadout != null)
            {
                for (int i = 0; i < skillbook.ActiveLoadout.Count; i++)
                {
                    if (skillbook.ActiveLoadout[i] == null)
                    {
                        skillbook.ActiveLoadout[i] = newSkill;
                        break;
                    }
                }
            }

            ShowFeedback($"'{removedName}' → '{newSkill.name}'", Color.cyan);
            Debug.Log($"[ItemEffect] RerollSkill → removed '{removedName}', added '{newSkill.name}'");
            return true;
        }

        ShowFeedback("No skills to reroll", Color.gray);
        return false;
    }


code:
public void ApplyPendingBaseDish()
    {
        if (PendingBaseDish == null) return;

        var cs = CookingSystem.Instance;
        if (cs == null)
        {
            // ★ [2026-06-14 Codex P2] CookingSystem 미초기화 시 ★삭제하지 말고 보류★ — 기존엔 ClearPendingBaseDish 로
            //   대기 요리를 적용 못 한 채 영구 삭제(직접 손실)했다. 다음 시작에서 재시도하도록 그대로 둔다.
            Debug.LogWarning("[CookingSynthesizer] CookingSystem not found — PendingBaseDish 적용 보류(다음 시작 재시도, 삭제 안 함)");
            return;
        }

        cs.AddDurationGroupedBuffsAndNotify("SYNTH_BASE", PendingBaseDish.Name, PendingBaseDish.Effects);
        Debug.Log($"[CookingSynthesizer] PendingBaseDish applied: {PendingBaseDish.Name}");

        // 즉시 효과 적용
        ApplyImmediateEffects(PendingBaseDish);

        // ★ [2026-06-14 Codex P2] 적용된 버프를 미드런 스냅샷에 ★커밋한 뒤★ 대기요리 삭제 — 기존엔 삭제(영속 커밋) 직후 force
        //   스냅샷이 없어, 런 시작 직후 종료 시 '대기요리 삭제됨 + 버프 미스냅샷 = 영구 손실'. force 스냅샷이 ExportMidRunBuffs 로
        //   버프를 영속(맵은 GenerateMap 이 CommonRunInit 전이라 이미 존재 → 조기 스냅샷 아님). 그 ★뒤★ 대기요리 소비.
        GameFlow.Instance?.RequestMidRunSnapshot("PendingBaseDishApplied", force: true);

        ClearPendingBaseDish(forceImmediate: true); // ✅ [코덱스#260] 소비 삭제 즉시 커밋 — 다음 신규 런 재적용 차단
    }


code:
private void SetChoice(int index, string text, UnityEngine.Events.UnityAction action)
        {
            Button btn = index == 1 ? _choiceBtn1 : index == 2 ? _choiceBtn2 : _choiceBtn3;
            TextMeshProUGUI txt = index == 1 ? _choiceText1 : index == 2 ? _choiceText2 : _choiceText3;
            if (btn == null) return;
            btn.onClick.RemoveAllListeners();
            if (text != null && action != null)
            {
                btn.gameObject.SetActive(true);
                if (txt != null) txt.text = text;
                btn.onClick.AddListener(action);
            }
            else
            {
                btn.gameObject.SetActive(false);
            }
        }


code:
private void OnUnlockAISkill(SkillDefinitionSO skill, int price)
        {
            int currentLevel = PlayerLevel.Instance?.Level ?? 1;
            if (skill != null && skill.UnlockLevel > currentLevel)
            {
                ShowMessage(Loc.Fmt("Skill/SkillMasterControllerComplete/L226", skill.UnlockLevel)); // 레벨 부족 (필요: Lv.{skill.UnlockLevel})
                return;
            }

            // ✅ [AUD-09004] GoldManager.CanAfford + MapIntegration.SpendGold 불일치 해소
            // MapIntegration.SpendGold 내부에서 GoldManager.CanAfford를 통해 검증 후 GameFlow.AddGold(-price)로 소비
            // 중복 GoldManager.CanAfford 제거 → SpendGold 단일 경로로 통일
            bool success = MapIntegration.Instance?.SpendGold(price) ?? false;
            if (!success)
            {
                ShowMessage(Loc.Fmt("Skill/SkillMasterControllerComplete/L232", price)); // 골드 부족! (필요: {price}G)
                Debug.LogError("[SkillMaster] AI 스킬 골드 차감 실패 (잔액 부족 또는 MapIntegration 없음)");
                return;
            }

            // 플레이어 스킬에 추가
            MapIntegration.Instance?.AddSkill(skill);

            // ✅ [AUD-CDX-261001 Fix] 실지급 확인 — AddSkill no-op 시 골드 환불
            var skillbook261 = PlayerLevel.Instance?.GetComponent<PlayerSkillbook>();
            bool delivered261 = skillbook261?.HasSkill(skill) ?? false;
            if (!delivered261)
            {
                // ✅ [AUD-CDX-274002 Fix] 세금 포함 환불 — 황금 서약 활성 시 실차감(price + tax) 전액 복구
                int _vowTax274ai = (BaseStrategyManager.Instance?.CurrentAbyssVow == "Gold")
                    ? Mathf.Max(0, Mathf.RoundToInt(price * BalanceConfig.GOLD_VOW_TAX_RATE)) : 0;
                int _refund274ai = price + _vowTax274ai;
                // ✅ [AUD-CDX-275001 Fix] GameFlow null 시 GoldManager 직접 환불 — MapIntegration.SpendGold fallback 경로로 차감된 골드 복구 보장
                if (GameFlow.Instance != null) GameFlow.Instance.AddGoldRefund(_refund274ai); // [Codex P2] 환불=통계 미기록
                else
                {
                    GoldManager.Instance?.AddGoldDirect(_refund274ai);
                    // ✅ [AUD-CDX-298002 Fix] AI 스킬 fallback 환불 즉시 커밋 — 환불-저장 원자성 보장
                    SaveManager.Instance?.SaveGame(forceImmediate: true);
                }
                ShowMessage(Loc.Get("Skill/Master/GoldInsufficient")); // 골드가 부족합니다! — 실지급 실패 메시지로 재사용
                Debug.LogWarning($"[SkillMaster] AI 스킬 지급 실패 (AddSkill no-op) — 골드 환불 {_refund274ai}G (세금 {_vowTax274ai}G 포함): {skill.name}");
                return;
            }

            // 카탈로그에서 제거 (중복 구매 방지)
            _aiSkills?.Remove(skill);

            RefreshSkillList();
            UpdateGoldDisplay();
            ShowMessage(Loc.Fmt("Skill/SkillMasterControllerComplete/L251", ResolveSkillDisplayName(skill))); // ✨ [AI 스킬] {skill.NameKey} 획득!
            GameFlow.Instance?.RequestMidRunSnapshot("SkillMaster.OnUnlockAISkill", true);
            Debug.Log($"[SkillMaster] AI 스킬 언락: {skill.NameKey} (-{price}G)");
        }


code:
private List<LegacyWillEntry> ApplyInheritedWills(List<LegacyWillEntry> selected)
        {
            var actuallyGranted = new List<LegacyWillEntry>(); // ✅ [AUD-78101 Fix] 실제 지급 성공 항목만 추적

            var player    = PlayerLevel.Instance?.GetComponent<CharacterBase>();
            var relicSys  = player?.GetComponent<RelicEffectSystem>();
            var skillbook = PlayerSkillbook.Instance;

            foreach (var will in selected)
            {
                bool willGranted = false;
                if (will.Type == LegacyWillEntry.WillType.Relic && relicSys != null)
                {
                    // 유물 이름으로 SO 탐색 (서브폴더 폴백 포함)
                    var relicSO = ResolveRelicBySourceName(will.SourceName);
                    if (relicSO != null)
                    {
                        // ✅ [AUD-CDX-267001 Fix] AddRelic 결과 검증 — 실패 시 성공 메시지 오출력 방지
                        willGranted = relicSys.AddRelic(relicSO);
                        if (willGranted)
                            Debug.Log($"[LegacyWillSystem] 유언 유물 상속: {will.DisplayName}");
                        else
                            Debug.LogWarning($"[LegacyWillSystem] 유언 유물 상속 실패 (AddRelic 거부): {will.DisplayName}");
                    }
                    else
                    {
                        Debug.LogWarning($"[LegacyWillSystem] 유물 SO를 찾을 수 없음: {will.SourceName}");
                    }
                }
                else if (will.Type == LegacyWillEntry.WillType.Skill && skillbook != null)
                {
                    // 스킬 이름으로 SO 탐색 (서브폴더 폴백 포함)
                    var skillSO = ResolveSkillBySourceName(will.SourceName);
                    if (skillSO != null)
                    {
                        // ✅ [AUD-CDX-267001 Fix] 스킬 지급 전 보유 여부 확인 — no-op 시 성공 메시지 오출력 방지
                        bool hadBefore = skillbook.HasSkill(skillSO);
                        skillbook.AddSkill(skillSO);
                        willGranted = !hadBefore && skillbook.HasSkill(skillSO);
                        if (willGranted)
                            Debug.Log($"[LegacyWillSystem] 유언 스킬 상속: {will.DisplayName}");
                        else
                            Debug.LogWarning($"[LegacyWillSystem] 유언 스킬 상속 no-op (이미 보유): {will.DisplayName}");
                    }
                    else
                    {
                        Debug.LogWarning($"[LegacyWillSystem] 스킬 SO를 찾을 수 없음: {will.SourceName}");
                    }
                }

                // 성공 메시지는 실제 지급이 이루어진 경우에만 출력
                if (willGranted)
                {
                    actuallyGranted.Add(will);
                    MsgManager.Instance?.InputInstantMsg(
                        Loc.Fmt("Antigravity/LegacyWillSystem/L286", ResolveMaybeLocalized(will.DisplayName)), // 📜 [유언 상속] '{will.DisplayName}' 전승됨!
                        InstantMsgType.Good);
                }
            }

            // ✅ [AUD-78101 Fix] _inheritedThisRun에 실제 지급 성공 항목만 기록
            _inheritedThisRun = new List<LegacyWillEntry>(actuallyGranted);

            // ✅ [InheritedEffectScale] 상속 유물/스킬 적용 후 전투력 보정 적용
            // InheritedEffectScale = 0.7 → 플레이어 PhyPower/MagPower를 70%로 감쇠
            // MultDamage value = scale - 1f = -0.3 → stat *= (1 + (-0.3)) = stat * 0.7
            // ✅ [AUD-777-H002 Fix] selected.Count → actualGrantedCount: 지급 실패 시 감쇠 적용 방지
            if (actuallyGranted.Count > 0 && player != null && InheritedEffectScale < 1f)
            {
                ApplyInheritedScaleModifier(player);
            }

            return actuallyGranted;
        }


code:
public void OnNodeMoved(NodeType nodeType)
        {
            // 전투 노드가 아닌 경우에만
            if (nodeType == NodeType.Combat || nodeType == NodeType.Elite ||
                nodeType == NodeType.Boss || nodeType == NodeType.MidBoss ||
                nodeType == NodeType.Seal) return;

            foreach (var skill in _activeSkills)
            {
                if (skill.Data == null) continue;
                if (skill.Data.EffectType == FieldSkillEffect.CompanionPartyHeal)
                {
                    // ✅ [Codex #268] 중앙 회복 정책 경유 — HealingService.Heal 이 NoHealing/맹세차단/맹세배율/
                    //    월드지변 배율 + NotifyHealUsed 를 일괄 처리. 기존 ApplyDeltaHP 직접호출은 맹세/지변 우회였음
                    //    (플레이어 필드스킬 SoulAbsorb·보상회복은 이미 HealingService 경유 — 정책 일관화).
                    //    NoHealing 조기 스킵 로그는 정보용 유지(HealingService 도 동일 체크하므로 무해).
                    if (RunModifierSystem.Instance != null && !RunModifierSystem.Instance.CanHeal())
                    {
                        Debug.Log($"[CompanionFieldSkill] {skill.CompanionName}: 야전 수리 차단 (NoHealing 수정자)");
                        continue;
                    }

                    float healRate = skill.EffectValue; // e.g., 0.03 = 3%
                    var companions = CompanionManager.Instance?.GetActiveCompanions();
                    if (companions == null) continue;

                    // 플레이어 + 동료 전체 회복 (HealingService 경유, 실제 회복량 추적)
                    bool anyHealed = false;
                    var player = PlayerLevel.Instance?.GetComponent<CharacterBase>();
                    if (player != null && player.Stat != null && !player.IsDead())
                    {
                        int healAmount = Mathf.Max(1, Mathf.RoundToInt(player.Stat.MaxHP * healRate));
                        if (HealingService.Heal(player, healAmount) > 0) anyHealed = true;
                    }
                    foreach (var comp in companions)
                    {
                        if (comp != null && comp.Stat != null && !comp.IsDead())
                        {
                            int healAmount = Mathf.Max(1, Mathf.RoundToInt(comp.Stat.MaxHP * healRate));
                            if (HealingService.Heal(comp, healAmount) > 0) anyHealed = true;
                        }
                    }
                    // NotifyHealUsed 는 HealingService.Heal 내부가 실제 회복 시 호출 → 수동 호출 제거(이중 알림 방지)
                    Debug.Log($"[CompanionFieldSkill] {skill.CompanionName}: 야전 수리 발동 ({healRate * 100:F0}% HP, healed={anyHealed})");
                }
            }
        }


code:
private void OnClericNodeCompleted(CharacterBase player)
    {
        if (player == null || player.Stat == null) return; // [AUD-709-H002]
        // [Fix] NoHealing 수정자 체크
        if (RunModifierSystem.Instance != null && !RunModifierSystem.Instance.CanHeal()) return;

        float recoverRate = BalanceConfig.INSTINCT_CLERIC_HP_RECOVER * _metaMultiplier;
        int hpRecover = Mathf.Max(1, Mathf.RoundToInt(player.Stat.MaxHP * recoverRate));

        if (player.Stat.HP < player.Stat.MaxHP)
        {
            // ✅ [Codex #269] 중앙 회복 정책 경유 — HealingService.Heal 이 맹세차단/맹세배율/월드지변배율 + NotifyHealUsed 일괄.
            //    기존 ApplyDeltaHP 직접호출은 맹세/지변 우회였음(노드완료 맹세리젠 OathSystem L88 은 이미 HealingService 경유 → 정책 일관화).
            int healed = HealingService.Heal(player, hpRecover);
            if (healed > 0)
                MsgManager.Instance?.InputInstantMsg(
                    Loc.Fmt("Skill/ClassInstinctSystem/L203", healed), // 💡 [신의 은총] HP +{healed} 회복 (실제 회복량)
                    InstantMsgType.Normal);
        }
    }


code:
public void AddSkill(SkillDefinitionSO skill)
        {
            if (PlayerSkillbook != null)
            {
                // ✅ [AUD-CDX-269001 Fix] 지급 전 보유 여부 확인 — no-op 시 사이드이펙트 오실행 방지
                bool hadBefore269 = PlayerSkillbook.HasSkill(skill);
                PlayerSkillbook.AddSkill(skill);
                bool effectivelyAdded269 = !hadBefore269 && PlayerSkillbook.HasSkill(skill);

                if (!effectivelyAdded269)
                {
                    Debug.LogWarning($"[MapIntegration] AddSkill no-op (이미 보유): {skill?.name} — 사이드이펙트 생략");
                    return;
                }

                Debug.Log($"[MapIntegration] +Skill: {skill.Dev.NameKo}");

                // 컬렉션 해금 기록 — 즉시 저장 제거 (AUD-145)
                // 조기 SaveGame() 및 RequestMidRunSnapshot(force:true)은 스킬은 저장되지만
                // 노드 방문 상태가 갱신되기 전이라 강제 종료 후 동일 스킬을 반복 획득 가능.
                // 저장은 OnEncounterCompleted → CompleteNode + RequestMidRunSnapshot 경로로만 수행.
                if (SaveManager.Instance?.CurrentSave != null)
                {
                    SaveManager.Instance.CurrentSave.UnlockSkill(skill.name);
                }

                // ✅ Phase 9.3 TASK-4: 스킬 획득 업적 기록
                // ★ [2026-06-15 Codex P2] saveNow:false — 노드 완료 전 저장 금지 가드 준수(카운터 farming 차단)
                AchievementManager.Instance?.RecordSkillCollect(saveNow: false);

                // 획득 팝업
                AcquirePopupUI.Instance?.ShowSkill(skill);
            }
        }


code:
void OnBlessingSelected(BlessingSO blessing)
    {
        if (blessing == null)
        {
            Debug.LogWarning("[Rest] OnBlessingSelected called with null blessing.");
            return;
        }

        if (BlessingPanel != null) BlessingPanel.SetActive(false);

        string rawBK2 = !string.IsNullOrEmpty(blessing.NameKey) ? blessing.NameKey : blessing.name;
        string displayName = Loc.Get(rawBK2);
        if (string.IsNullOrEmpty(displayName)) displayName = rawBK2;

        Debug.Log($"[Rest] Blessing selected: {displayName}");

        // ✅ 실제 축복 적용 — MapIntegration.AddBlessing(id) 사용
        // ✅ [AUD-CDX-271001 Fix] 커밋 성공 후 hasBlessing 설정 — 사전 성공 래치 방지
        string blessingId = !string.IsNullOrEmpty(blessing.BlessingId) ? blessing.BlessingId : blessing.name;
        bool blessingApplied271 = false;
        if (MapIntegration.Instance != null)
        {
            blessingApplied271 = MapIntegration.Instance.TryAddBlessing(blessingId);
            if (!blessingApplied271)
                Debug.LogWarning($"[Rest] Blessing apply failed via MapIntegration: {blessingId}");
        }
        else
        {
            // MapIntegration 없으면 BlessingManager에 직접 Grant
            var mgr = BlessingManager.Instance;
            if (mgr != null)
            {
                mgr.Grant(blessing);
                blessingApplied271 = true;
            }
            else
                Debug.LogWarning("[Rest] Neither MapIntegration nor BlessingManager found — blessing not applied.");
        }

        if (blessingApplied271)
        {
            hasBlessing = true;
            UpdateOptions();

            // Update narration
            if (RestNarratorText != null)
            {
                RestNarratorText.text = Loc.Fmt("UI/Correct_RestSiteUI/L1156", displayName); // {displayName}을(를) 받았다!
            }
        }

        // ✅ [RunPrep] prep_karma_surge — 축복 2개 동시 획득
        if (_prepKarmaSurgeActive)
        {
            _prepKarmaSurgeActive = false; // 1회만 발동
            var allBlessingsForKarma = CollectionManager.Instance != null
                ? CollectionManager.Instance.GetAllBlessingsCached()
                : Resources.LoadAll<BlessingSO>("Blessings");
            if (allBlessingsForKarma != null && allBlessingsForKarma.Length > 0)
            {
                var candidates = new System.Collections.Generic.List<BlessingSO>();
                foreach (var b in allBlessingsForKarma)
                {
                    if (b != null && b != blessing) candidates.Add(b);
                }
                if (candidates.Count > 0)
                {
                    var bonus = candidates[Random.Range(0, candidates.Count)];
                    string bonusId = !string.IsNullOrEmpty(bonus.BlessingId) ? bonus.BlessingId : bonus.name;
                    // ✅ [AUD-CDX-229003 Fix] TryAddBlessing 반환값 검증 — 실패 시 성공 메시지 오출력 방지
                    // ✅ [AUD-CDX-271002 Fix] BlessingManager null 시 blessingAdded 오설정 방지
                    bool blessingAdded;
                    if (MapIntegration.Instance != null)
                        blessingAdded = MapIntegration.Instance.TryAddBlessing(bonusId);
                    else
                    {
                        var bonusMgr = BlessingManager.Instance;
                        if (bonusMgr != null)
                        {
                            bonusMgr.Grant(bonus);
                            blessingAdded = true;
                        }
                        else
                            blessingAdded = false;
                    }

                    if (blessingAdded)
                    {
                        string bonusName = Loc.Get(!string.IsNullOrEmpty(bonus.NameKey) ? bonus.NameKey : bonus.name);
                        if (string.IsNullOrEmpty(bonusName)) bonusName = bonus.name;
                        MsgManager.Instance?.InputInstantMsg(
                            Loc.Fmt("UI/Correct_RestSiteUI/KarmaSurge", bonusName), InstantMsgType.Good);
                        Debug.Log($"[RunPrep] prep_karma_surge 발동: 보너스 축복 {bonusId}");
                    }
                    else
                        Debug.LogWarning($"[RunPrep] prep_karma_surge 보너스 축복 지급 실패: {bonusId}");
                }
            }
        }

        // ✅ [검증 #31] 축복 선택 후 즉시 스냅샷 — 앱 강제종료 시 축복 상태 보존
        GameFlow.Instance?.RequestMidRunSnapshot("RestSiteUI.Blessing", force: true);
        // ✅ [AUD-BUFF-Phase40 Fix] 축복 지급 후 즉시 디스크 커밋 — 강제 종료 리롤 방지
        SaveManager.Instance?.SaveGame(forceImmediate: true);
    }


code:
private void ApplyPrayEffect()
        {
            float roll = Random.value;
            float enhanceMul = 1f + FieldSkillExecutor.GetAltarEnhanceRate();

            string result;

            if (roll < 0.3f)  // 30% - 좋은 효과
            {
                int goldBonus = Mathf.RoundToInt(Random.Range(50, 101) * enhanceMul);
                // AltarPray Fix: AddGold → GameFlow.AddGold 경유
                bool goldOk;
                if (GameFlow.Instance != null)
                {
                    // ✅ [AUD-CDX-151002 Fix] AddGold 반환값 검증 — 지급 실패 시 메시지 억제
                    goldOk = GameFlow.Instance.AddGold(goldBonus);
                    if (!goldOk)
                        Debug.LogWarning($"[Altar] 골드 지급 실패 — GameFlow.AddGold({goldBonus}) false 반환");
                }
                else
                {
                    // ✅ [AUD-CDX-21502 Fix] GameFlow null 폴백 — 보상 소실 방지 (통계 미기록 감수)
                    goldOk = GoldManager.Instance != null;
                    GoldManager.Instance?.AddGoldDirect(goldBonus);
                    SaveManager.Instance?.SaveGame(forceImmediate: true); // ✅ [AUD-CDX-306001] 폴백 골드 즉시 커밋
                    Debug.LogWarning($"[Altar] GameFlow 없음 — 골드 직접 지급(통계 미기록): {goldBonus}");
                }

                result = goldOk
                    ? Loc.Fmt("Map/Altar/PrayGold", goldBonus) // ✨ 신의 은총 기도가 받아들여졌습니다! 💰 +{0} 골드 축복이 함께 하길...
                    : Loc.Get("Map/Altar/PrayNoEffect"); // 🙏 평온한 기도. 마음이 평온해집니다. 특별한 효과는 없습니다.
            }
            else if (roll < 0.6f)  // 30% - 평범한 효과
            {
                if (Player != null && Player.Stat != null)
                {
                    // ✅ [Codex #272] HealingService 경유 — NoHealing/맹세차단/맹세배율/지변배율 + NotifyHealUsed 일괄.
                    //    기존 직접 ApplyDeltaHP 는 맹세·지변 우회였음(ApplyHealModifier조차 없어 지변도 미적용).
                    //    raw healAmount 전달 → HealingService 가 배율 적용, 결과 메시지는 실제 회복량 기준.
                    int healAmount = Mathf.RoundToInt(Player.Stat.MaxHP * 0.3f * enhanceMul);
                    int actualHeal = HealingService.Heal(Player, healAmount);
                    result = actualHeal > 0
                        ? Loc.Get("Map/Altar/PrayHeal") // 🙏 평온한 기도 마음이 평온해집니다. ❤ HP 30% 회복 신의 평안이 함께 하길...
                        : Loc.Get("Map/Altar/PrayNoEffect"); // 🙏 평온한 기도. 마음이 평온해집니다. 특별한 효과는 없습니다.
                }
                else
                {
                    result = Loc.Get("Map/Altar/PrayNoEffect"); // 🙏 평온한 기도 마음이 평온해집니다. 특별한 효과는 없습니다.
                }
            }
            else  // 40% - 아무 효과 없음
            {
                result = Loc.Get("Map/Altar/PraySilence"); // ... 침묵 ... 신은 대답하지 않습니다. 아무 일도 일어나지 않았습니다.
            }

            ShowResult(Loc.Get("Map/Altar/PrayResultTitle"), result); // 기도의 결과
        }


code:
private void ApplyStatBonus(StatType statType)
        {
            if (Player == null || Player.Ability == null || Player.Stat == null) return;

            int bonus = GetStatBonus(statType);

            switch (statType)
            {
                case StatType.STR:
                    Player.Ability.STR += bonus;
                    PlayerLevel.Instance?.Rules?.Recalc(Player); // ✅ [Codex #275] 파생 물리공격력 즉시 반영
                    break;

                case StatType.DEX:
                    Player.Ability.DEX += bonus;
                    PlayerLevel.Instance?.Rules?.Recalc(Player); // ✅ [Codex #275] 명중/회피/치명/속도/물공 즉시 반영
                    break;

                case StatType.CON:
                    Player.Ability.CON += bonus;
                    // ✅ Fix: StatRulesSO.Recalc() 경유로 MaxHP를 공식에 맞게 재계산
                    // (Stat.MaxHP 직접 수정 대신 Recalc 사용하여 수식 일관성 유지)
                    {
                        var rules = PlayerLevel.Instance?.Rules;
                        if (rules != null)
                        {
                            float prevMaxHP = Player.Stat.MaxHP;
                            rules.Recalc(Player);
                            // 훈련 보너스: MaxHP 증가분만큼 현재 HP도 회복 (ApplyDeltaHP 경유)
                            float delta = Player.Stat.MaxHP - prevMaxHP;
                            if (delta > 0) Player.ApplyDeltaHP(Mathf.RoundToInt(delta));
                        }
                        else
                        {
                            // Rules 없을 시 BalanceConfig 기준 폴백
                            float hpBonus = bonus * BalanceConfig.HP_PER_CON;
                            Player.Stat.MaxHP += hpBonus;
                            Player.ApplyDeltaHP(Mathf.RoundToInt(hpBonus));
                        }
                    }
                    break;

                case StatType.INT:
                    Player.Ability.INT += bonus;
                    PlayerLevel.Instance?.Rules?.Recalc(Player); // ✅ [Codex #275] 마법공격력/MaxMP/MP재생/쿨감 즉시 반영
                    break;

                case StatType.WIS:
                    Player.Ability.WIS += bonus;
                    // ✅ Fix: Recalc으로 MagDef/RegenHP 재계산 (WIS는 공식상 MaxMP에 직접 연결 없음)
                    // MaxMP 보너스는 UI 표시 일관성을 위해 수동으로 추가 (3f per WIS 유지)
                    {
                        var rules = PlayerLevel.Instance?.Rules;
                        if (rules != null) rules.Recalc(Player);

                        float mpBonus = bonus * 3f;  // WIS→MaxMP 수동 보너스 (UI: "최대 MP 증가")
                        Player.Stat.RewardBonusMaxMP += mpBonus; // Recalc 영속 레이어에 기록
                        Player.Stat.MaxMP += mpBonus; // Recalc 이후이므로 즉시 반영
                        Player.ApplyDeltaMP(mpBonus);
                    }
                    break;

                case StatType.CHA:
                    Player.Ability.CHA += bonus;
                    PlayerLevel.Instance?.Rules?.Recalc(Player); // ✅ [Codex #275] 골드획득/상점할인/이벤트성공 즉시 반영
                    break;
            }

            Debug.Log($"[Training] {statType} increased by {bonus}");
        }


code:
void OnItemSelected(ItemSO item)
    {
        Debug.Log($"[Combat] Item selected: {item.NameKey}");

        // ✅ [Auto_488#F1] BattleController 경유 아이템 사용 — 턴 진행 및 효과 처리 위임
        if (BattleController != null)
        {
            BattleController.UseItemBySelection(item);
            CloseItemPanel();
            return;
        }

        // BattleController 없을 때 직접 처리 (레거시 폴백)
        var player = PlayerLevel.Instance?.GetComponent<CharacterBase>();
        var rules = PlayerLevel.Instance?.Rules;

        if (player != null && rules != null)
        {
            // ✅ [Codex #277] 레거시 폴백도 백엔드 엔트리 사용 API(UseItemEntry)로 통일 — 기존 ApplyItem+RemoveItem(itemId)
            //   조합은 맹세 아이템차단/NoItems 모디파이어/챌린지 사용추적/미드런 스냅샷·즉시저장/엔트리정체성(강화배율 오-차감)을
            //   모두 우회했음. UseItemEntry 가 효과적용+차단체크+선택엔트리 직접차감+스냅샷/저장을 일괄 수행(주 경로와 통일).
            var inv = InventorySystem_TRPG.Instance;
            var uiItemEntry = inv?.Items.FirstOrDefault(e => e.Item?.ItemId == item.ItemId);
            if (uiItemEntry != null)
                inv.UseItemEntry(uiItemEntry);
            else
                Debug.LogWarning($"[CombatUI] 레거시 폴백 — 인벤 엔트리 없음, 사용 취소: {item?.ItemId}");
        }

        CloseItemPanel();
    }