Skip to content
이 내용이 도움이 되었나요?

사전 다운로드 가이드

앱인토스 Unity 게임에서 사전 다운로드 기능을 구현하여 게임 플레이 중 끊김 없는 경험을 제공하는 방법을 다뤄요.


1. 사전 다운로드 시스템 개요

사전 다운로드 전략

📥 사전 다운로드 아키텍처
├── 필수 콘텐츠 (게임 시작 전 다운로드)
│   ├── 핵심 게임 로직
│   ├── 기본 UI 리소스
│   ├── 첫 레벨 에셋
│   └── 필수 오디오
├── 우선순위 콘텐츠 (백그라운드 다운로드)
│   ├── 다음 레벨 에셋
│   ├── 캐릭터 스킨
│   ├── 추가 음악
│   └── 이펙트 에셋
├── 선택적 콘텐츠 (필요시 다운로드)
│   ├── 고해상도 텍스처
│   ├── 보너스 콘텐츠
│   ├── 계절 이벤트 에셋
│   └── 언어팩
└── 예측적 콘텐츠 (사용자 패턴 기반)
    ├── 자주 플레이하는 레벨
    ├── 선호 캐릭터 관련
    ├── 개인화 콘텐츠
    └── 추천 에셋

사전 다운로드 매니저

c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public class AppsInTossPreloadManager : MonoBehaviour
{
    public static AppsInTossPreloadManager Instance { get; private set; }

    [System.Serializable]
    public class PreloadItem
    {
        [Header("기본 정보")]
        public string itemId;
        public string itemName;
        public string description;
        public float sizeMB;
        public string version = "1.0.0";

        [Header("다운로드 설정")]
        public PreloadPriority priority;
        public PreloadTrigger downloadTrigger;
        public string[] dependencies;
        public string remoteUrl;
        public string localPath;

        [Header("조건 설정")]
        public bool requiresWiFi = false;
        public bool downloadOnlyWhenIdle = false;
        public int minimumBatteryLevel = 20;
        public long maxStorageUsageMB = 500;

        [Header("게임 로직")]
        public string[] requiredForScenes;
        public string[] requiredForFeatures;
        public bool isUserGenerated = false;
        public System.DateTime expiryDate;
    }

    public enum PreloadPriority
    {
        Critical = 0,    // 게임 진행에 필수
        High = 1,        // 사용자 경험 향상에 중요
        Medium = 2,      // 부가 기능
        Low = 3,         // 선택적 콘텐츠
        OnDemand = 4     // 요청시에만 다운로드
    }

    public enum PreloadTrigger
    {
        Immediate,       // 즉시 다운로드
        OnGameStart,     // 게임 시작시
        OnSceneLoad,     // 특정 씬 로드시
        OnFeatureAccess, // 기능 접근시
        OnUserRequest,   // 사용자 요청시
        OnWiFiAvailable, // WiFi 연결시
        OnIdle,          // 유휴 시간에
        OnBatteryOK      // 배터리 충분시
    }

    [Header("사전 다운로드 설정")]
    public PreloadItem[] preloadItems;

    [Header("네트워크 설정")]
    public int maxConcurrentDownloads = 2;
    public float downloadTimeoutSeconds = 30f;
    public bool enableBackgroundDownload = true;
    public bool enablePredictiveDownload = true;

    [Header("저장소 관리")]
    public long maxTotalCacheSizeMB = 1000;
    public bool enableAutomaticCleanup = true;
    public int keepRecentDays = 30;

    // 내부 상태
    private Dictionary<string, PreloadItem> itemMap = new Dictionary<string, PreloadItem>();
    private HashSet<string> downloadedItems = new HashSet<string>();
    private HashSet<string> downloadingItems = new HashSet<string>();
    private Queue<PreloadItem> downloadQueue = new Queue<PreloadItem>();
    private Dictionary<string, float> downloadProgress = new Dictionary<string, float>();
    private int activeDownloads = 0;
    private bool isInitialized = false;

    // 사용자 패턴 분석
    private Dictionary<string, int> usageFrequency = new Dictionary<string, int>();
    private Dictionary<string, System.DateTime> lastUsedTime = new Dictionary<string, System.DateTime>();

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
            InitializePreloadSystem();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    void InitializePreloadSystem()
    {
        // 아이템 맵 생성
        foreach (var item in preloadItems)
        {
            itemMap[item.itemId] = item;
        }

        // 로컬 캐시 상태 확인
        LoadCacheStatus();

        // 사용 패턴 데이터 로드
        LoadUsagePatterns();

        // 시스템 상태 모니터링 시작
        StartCoroutine(MonitorSystemConditions());

        // 초기 다운로드 큐 설정
        SetupInitialDownloadQueue();

        isInitialized = true;
        Debug.Log("AppsInToss 사전 다운로드 시스템 초기화 완료");
    }

    void LoadCacheStatus()
    {
        string cacheStatusPath = Path.Combine(Application.persistentDataPath, "preload_cache_status.json");

        if (File.Exists(cacheStatusPath))
        {
            try
            {
                string json = File.ReadAllText(cacheStatusPath);
                var cacheStatus = JsonUtility.FromJson<CacheStatus>(json);

                downloadedItems = new HashSet<string>(cacheStatus.downloadedItems);

                Debug.Log($"캐시 상태 로드 완료: {downloadedItems.Count}개 아이템");
            }
            catch (System.Exception e)
            {
                Debug.LogError($"캐시 상태 로드 실패: {e.Message}");
            }
        }
    }

    void SaveCacheStatus()
    {
        string cacheStatusPath = Path.Combine(Application.persistentDataPath, "preload_cache_status.json");

        var cacheStatus = new CacheStatus
        {
            downloadedItems = downloadedItems.ToArray(),
            lastUpdateTime = System.DateTime.UtcNow.ToString("o")
        };

        try
        {
            string json = JsonUtility.ToJson(cacheStatus, true);
            File.WriteAllText(cacheStatusPath, json);
        }
        catch (System.Exception e)
        {
            Debug.LogError($"캐시 상태 저장 실패: {e.Message}");
        }
    }

    void LoadUsagePatterns()
    {
        string patternsPath = Path.Combine(Application.persistentDataPath, "usage_patterns.json");

        if (File.Exists(patternsPath))
        {
            try
            {
                string json = File.ReadAllText(patternsPath);
                var patterns = JsonUtility.FromJson<UsagePatterns>(json);

                foreach (var item in patterns.items)
                {
                    usageFrequency[item.itemId] = item.frequency;
                    if (System.DateTime.TryParse(item.lastUsed, out System.DateTime lastUsed))
                    {
                        lastUsedTime[item.itemId] = lastUsed;
                    }
                }

                Debug.Log($"사용 패턴 로드 완료: {usageFrequency.Count}개 항목");
            }
            catch (System.Exception e)
            {
                Debug.LogError($"사용 패턴 로드 실패: {e.Message}");
            }
        }
    }

    void SaveUsagePatterns()
    {
        string patternsPath = Path.Combine(Application.persistentDataPath, "usage_patterns.json");

        var patterns = new UsagePatterns();
        patterns.items = new List<UsagePatternItem>();

        foreach (var kvp in usageFrequency)
        {
            var item = new UsagePatternItem
            {
                itemId = kvp.Key,
                frequency = kvp.Value,
                lastUsed = lastUsedTime.ContainsKey(kvp.Key) ?
                          lastUsedTime[kvp.Key].ToString("o") :
                          System.DateTime.UtcNow.ToString("o")
            };
            patterns.items.Add(item);
        }

        try
        {
            string json = JsonUtility.ToJson(patterns, true);
            File.WriteAllText(patternsPath, json);
        }
        catch (System.Exception e)
        {
            Debug.LogError($"사용 패턴 저장 실패: {e.Message}");
        }
    }

    IEnumerator MonitorSystemConditions()
    {
        while (true)
        {
            // 네트워크 상태 확인
            bool isWiFiAvailable = Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork;

            // 배터리 상태 확인 (모바일 플랫폼에서만)
            float batteryLevel = SystemInfo.batteryLevel * 100f;

            // 저장 공간 확인
            long availableStorage = GetAvailableStorageSpace();

            // 시스템 조건에 따른 다운로드 제어
            UpdateDownloadBehavior(isWiFiAvailable, batteryLevel, availableStorage);

            // 다운로드 큐 처리
            ProcessDownloadQueue();

            yield return new WaitForSeconds(5f);
        }
    }

    void UpdateDownloadBehavior(bool isWiFiAvailable, float batteryLevel, long availableStorage)
    {
        // WiFi 전용 아이템들 처리
        if (isWiFiAvailable)
        {
            var wifiOnlyItems = preloadItems.Where(item =>
                item.requiresWiFi &&
                !downloadedItems.Contains(item.itemId) &&
                !downloadingItems.Contains(item.itemId)
            ).ToArray();

            foreach (var item in wifiOnlyItems)
            {
                if (ShouldDownloadItem(item, batteryLevel, availableStorage))
                {
                    QueueItemForDownload(item);
                }
            }
        }

        // 배터리 조건 확인
        if (batteryLevel < 20f)
        {
            // 배터리가 부족하면 중요하지 않은 다운로드 일시 정지
            PauseNonCriticalDownloads();
        }

        // 저장 공간 부족시 정리
        if (availableStorage < 100 * 1024 * 1024) // 100MB 미만
        {
            StartCoroutine(CleanupOldCache());
        }
    }

    bool ShouldDownloadItem(PreloadItem item, float batteryLevel, long availableStorage)
    {
        // 배터리 레벨 체크
        if (batteryLevel < item.minimumBatteryLevel)
        {
            return false;
        }

        // 저장 공간 체크
        if (availableStorage < item.sizeMB * 1024 * 1024)
        {
            return false;
        }

        // 트리거 조건 체크
        if (item.downloadTrigger == PreloadTrigger.OnWiFiAvailable &&
            Application.internetReachability != NetworkReachability.ReachableViaLocalAreaNetwork)
        {
            return false;
        }

        // 유휴 시간 체크
        if (item.downloadOnlyWhenIdle && !IsPlayerIdle())
        {
            return false;
        }

        return true;
    }

    void SetupInitialDownloadQueue()
    {
        // 우선순위별로 정렬
        var sortedItems = preloadItems
            .Where(item => item.downloadTrigger == PreloadTrigger.Immediate ||
                          item.downloadTrigger == PreloadTrigger.OnGameStart)
            .OrderBy(item => item.priority)
            .ThenByDescending(item => GetItemPredictionScore(item));

        foreach (var item in sortedItems)
        {
            if (!downloadedItems.Contains(item.itemId))
            {
                QueueItemForDownload(item);
            }
        }

        Debug.Log($"초기 다운로드 큐 설정 완료: {downloadQueue.Count}개 아이템");
    }

    float GetItemPredictionScore(PreloadItem item)
    {
        float score = 0f;

        // 사용 빈도 점수
        if (usageFrequency.ContainsKey(item.itemId))
        {
            score += usageFrequency[item.itemId] * 10f;
        }

        // 최근 사용 점수
        if (lastUsedTime.ContainsKey(item.itemId))
        {
            var daysSinceLastUse = (System.DateTime.UtcNow - lastUsedTime[item.itemId]).TotalDays;
            score += Mathf.Max(0, 30f - (float)daysSinceLastUse);
        }

        // 크기 점수 (작을수록 높은 점수)
        score += Mathf.Max(0, 50f - item.sizeMB);

        return score;
    }

    void ProcessDownloadQueue()
    {
        while (downloadQueue.Count > 0 && activeDownloads < maxConcurrentDownloads)
        {
            var nextItem = downloadQueue.Dequeue();

            if (!downloadedItems.Contains(nextItem.itemId) &&
                !downloadingItems.Contains(nextItem.itemId))
            {
                StartCoroutine(DownloadItem(nextItem));
            }
        }
    }

    IEnumerator DownloadItem(PreloadItem item)
    {
        downloadingItems.Add(item.itemId);
        activeDownloads++;
        downloadProgress[item.itemId] = 0f;

        Debug.Log($"다운로드 시작: {item.itemName} ({item.sizeMB:F1}MB)");
        float startTime = Time.realtimeSinceStartup;

        // 의존성 체크 및 다운로드
        yield return StartCoroutine(EnsureDependencies(item));

        // 실제 파일 다운로드
        string downloadUrl = item.remoteUrl;
        string localPath = GetLocalPath(item);

        using (var www = new WWW(downloadUrl))
        {
            float timeoutTime = Time.realtimeSinceStartup + downloadTimeoutSeconds;

            while (!www.isDone && Time.realtimeSinceStartup < timeoutTime)
            {
                downloadProgress[item.itemId] = www.progress;

                // 진행률 이벤트 발송
                SendDownloadProgressEvent(item.itemId, www.progress);

                yield return null;
            }

            if (www.isDone && string.IsNullOrEmpty(www.error))
            {
                // 파일 저장
                Directory.CreateDirectory(Path.GetDirectoryName(localPath));
                File.WriteAllBytes(localPath, www.bytes);

                // 성공 처리
                OnDownloadSuccess(item, Time.realtimeSinceStartup - startTime);
            }
            else
            {
                // 실패 처리
                OnDownloadFailed(item, www.error);
            }
        }

        downloadingItems.Remove(item.itemId);
        activeDownloads--;
        downloadProgress.Remove(item.itemId);
    }

    IEnumerator EnsureDependencies(PreloadItem item)
    {
        foreach (var dependencyId in item.dependencies)
        {
            if (!downloadedItems.Contains(dependencyId))
            {
                if (itemMap.ContainsKey(dependencyId))
                {
                    var dependency = itemMap[dependencyId];
                    yield return StartCoroutine(DownloadItem(dependency));
                }
            }
        }
    }

    void OnDownloadSuccess(PreloadItem item, float downloadTime)
    {
        downloadedItems.Add(item.itemId);
        SaveCacheStatus();

        Debug.Log($"다운로드 완료: {item.itemName} ({downloadTime:F2}초)");


    }

    void OnDownloadFailed(PreloadItem item, string error)
    {
        Debug.LogError($"다운로드 실패: {item.itemName} - {error}");

        // 재시도 로직 (낮은 우선순위 아이템만)
        if (item.priority >= PreloadPriority.Medium)
        {
            StartCoroutine(RetryDownloadAfterDelay(item, 10f));
        }
    }

    IEnumerator RetryDownloadAfterDelay(PreloadItem item, float delay)
    {
        yield return new WaitForSeconds(delay);

        if (!downloadedItems.Contains(item.itemId))
        {
            QueueItemForDownload(item);
            Debug.Log($"다운로드 재시도 예약: {item.itemName}");
        }
    }

    void SendDownloadProgressEvent(string itemId, float progress)
    {
    }


    string GetLocalPath(PreloadItem item)
    {
        if (!string.IsNullOrEmpty(item.localPath))
        {
            return Path.Combine(Application.persistentDataPath, item.localPath);
        }

        return Path.Combine(Application.persistentDataPath, "PreloadCache", item.itemId);
    }

    long GetAvailableStorageSpace()
    {
        // 플랫폼별 저장 공간 확인 로직
        // 여기서는 간단한 추정값 반환
        return 1024 * 1024 * 1024; // 1GB
    }

    bool IsPlayerIdle()
    {
        // 플레이어 유휴 상태 확인 로직
        return Time.realtimeSinceStartup - Time.time > 30f;
    }

    void PauseNonCriticalDownloads()
    {
        // 현재 진행중인 중요하지 않은 다운로드들을 일시 정지
        Debug.Log("배터리 부족으로 인한 비중요 다운로드 일시 정지");
    }

    IEnumerator CleanupOldCache()
    {
        Debug.Log("저장 공간 부족으로 인한 캐시 정리 시작");

        var cacheDir = Path.Combine(Application.persistentDataPath, "PreloadCache");
        if (Directory.Exists(cacheDir))
        {
            var files = Directory.GetFiles(cacheDir, "*", SearchOption.AllDirectories);
            var fileInfos = files.Select(f => new FileInfo(f)).ToArray();

            // 오래된 파일부터 정렬
            System.Array.Sort(fileInfos, (a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime));

            long freedSpace = 0;
            long targetFreeSpace = 200 * 1024 * 1024; // 200MB

            foreach (var fileInfo in fileInfos)
            {
                if (freedSpace >= targetFreeSpace) break;

                try
                {
                    freedSpace += fileInfo.Length;
                    File.Delete(fileInfo.FullName);

                    // 해당 아이템을 다운로드 목록에서 제거
                    string itemId = Path.GetFileNameWithoutExtension(fileInfo.Name);
                    downloadedItems.Remove(itemId);

                    Debug.Log($"캐시 파일 삭제: {fileInfo.Name}");
                }
                catch (System.Exception e)
                {
                    Debug.LogError($"캐시 파일 삭제 실패: {e.Message}");
                }

                yield return null;
            }

            SaveCacheStatus();
            Debug.Log($"캐시 정리 완료: {freedSpace / (1024 * 1024)}MB 확보");
        }
    }

    // 공개 API
    public void QueueItemForDownload(PreloadItem item)
    {
        if (!downloadedItems.Contains(item.itemId) &&
            !downloadingItems.Contains(item.itemId))
        {
            downloadQueue.Enqueue(item);
            Debug.Log($"다운로드 큐에 추가: {item.itemName}");
        }
    }

    public void QueueItemForDownload(string itemId)
    {
        if (itemMap.ContainsKey(itemId))
        {
            QueueItemForDownload(itemMap[itemId]);
        }
    }

    public bool IsItemDownloaded(string itemId)
    {
        return downloadedItems.Contains(itemId);
    }

    public float GetDownloadProgress(string itemId)
    {
        return downloadProgress.ContainsKey(itemId) ? downloadProgress[itemId] : 0f;
    }

    public bool IsItemDownloading(string itemId)
    {
        return downloadingItems.Contains(itemId);
    }

    public void RecordItemUsage(string itemId)
    {
        if (usageFrequency.ContainsKey(itemId))
        {
            usageFrequency[itemId]++;
        }
        else
        {
            usageFrequency[itemId] = 1;
        }

        lastUsedTime[itemId] = System.DateTime.UtcNow;

        // 주기적으로 패턴 저장
        if (usageFrequency[itemId] % 10 == 0)
        {
            SaveUsagePatterns();
        }
    }

    public long GetTotalCacheSize()
    {
        var cacheDir = Path.Combine(Application.persistentDataPath, "PreloadCache");
        if (!Directory.Exists(cacheDir))
        {
            return 0;
        }

        long totalSize = 0;
        var files = Directory.GetFiles(cacheDir, "*", SearchOption.AllDirectories);

        foreach (var file in files)
        {
            totalSize += new FileInfo(file).Length;
        }

        return totalSize;
    }

    public PreloadItem[] GetQueuedItems()
    {
        return downloadQueue.ToArray();
    }

    public string[] GetDownloadedItemIds()
    {
        return downloadedItems.ToArray();
    }

    void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            // 앱 일시정지시 패턴 데이터 저장
            SaveUsagePatterns();
        }
    }

    void OnDestroy()
    {
        SaveCacheStatus();
        SaveUsagePatterns();
    }
}

// 데이터 클래스들
[System.Serializable]
public class CacheStatus
{
    public string[] downloadedItems;
    public string lastUpdateTime;
}

[System.Serializable]
public class UsagePatterns
{
    public List<UsagePatternItem> items;
}

[System.Serializable]
public class UsagePatternItem
{
    public string itemId;
    public int frequency;
    public string lastUsed;
}

2. 사용자 인터페이스

다운로드 관리 UI

c#
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using System.Linq;

public class PreloadManagerUI : MonoBehaviour
{
    [Header("UI 컴포넌트")]
    public GameObject downloadItemPrefab;
    public Transform downloadItemContainer;
    public Button pauseAllButton;
    public Button resumeAllButton;
    public Button clearCacheButton;
    public TextMeshProUGUI totalSizeText;
    public TextMeshProUGUI availableSpaceText;
    public Slider totalProgressSlider;

    [Header("설정 UI")]
    public Toggle wifiOnlyToggle;
    public Toggle backgroundDownloadToggle;
    public Slider maxCacheSizeSlider;
    public TextMeshProUGUI maxCacheSizeLabel;

    private Dictionary<string, GameObject> downloadItemUIs = new Dictionary<string, GameObject>();
    private bool isPaused = false;

    void Start()
    {
        SetupUI();
        RegisterEventListeners();
        UpdateUI();
    }

    void SetupUI()
    {
        // 버튼 리스너 등록
        pauseAllButton.onClick.AddListener(PauseAllDownloads);
        resumeAllButton.onClick.AddListener(ResumeAllDownloads);
        clearCacheButton.onClick.AddListener(ClearCache);

        // 토글 리스너 등록
        wifiOnlyToggle.onValueChanged.AddListener(OnWiFiOnlyChanged);
        backgroundDownloadToggle.onValueChanged.AddListener(OnBackgroundDownloadChanged);

        // 슬라이더 리스너 등록
        maxCacheSizeSlider.onValueChanged.AddListener(OnMaxCacheSizeChanged);

        // 초기 설정 로드
        LoadUserPreferences();
    }

    void RegisterEventListeners()
    {
        AppsInToss.OnEvent += HandlePreloadEvent;
    }

    void HandlePreloadEvent(string eventName, Dictionary<string, object> data)
    {
        switch (eventName)
        {
            case "preload_item_downloaded":
                string itemId = data["item_id"] as string;
                UpdateDownloadItemUI(itemId, 1f, "완료");
                break;

            case "preload_progress":
                itemId = data["item_id"] as string;
                float progress = (float)data["progress"];
                UpdateDownloadItemUI(itemId, progress, $"{progress * 100f:F0}%");
                break;
        }

        UpdateUI();
    }

    void UpdateUI()
    {
        if (AppsInTossPreloadManager.Instance == null) return;

        // 전체 캐시 크기 표시
        long totalCacheSize = AppsInTossPreloadManager.Instance.GetTotalCacheSize();
        totalSizeText.text = $"사용 중: {FormatFileSize(totalCacheSize)}";

        // 사용 가능한 공간 표시 (추정값)
        availableSpaceText.text = "사용 가능: ~1.2GB";

        // 전체 진행률 계산
        UpdateTotalProgress();

        // 개별 다운로드 아이템 UI 업데이트
        UpdateDownloadItemsUI();
    }

    void UpdateTotalProgress()
    {
        var queuedItems = AppsInTossPreloadManager.Instance.GetQueuedItems();
        var downloadedItems = AppsInTossPreloadManager.Instance.GetDownloadedItemIds();

        if (queuedItems.Length == 0)
        {
            totalProgressSlider.value = 1f;
            return;
        }

        float totalItems = queuedItems.Length + downloadedItems.Length;
        float completedItems = downloadedItems.Length;

        // 현재 다운로드 중인 아이템들의 부분 진행률 추가
        foreach (var item in queuedItems)
        {
            if (AppsInTossPreloadManager.Instance.IsItemDownloading(item.itemId))
            {
                float itemProgress = AppsInTossPreloadManager.Instance.GetDownloadProgress(item.itemId);
                completedItems += itemProgress;
            }
        }

        totalProgressSlider.value = totalItems > 0 ? completedItems / totalItems : 1f;
    }

    void UpdateDownloadItemsUI()
    {
        var queuedItems = AppsInTossPreloadManager.Instance.GetQueuedItems();
        var downloadedItems = AppsInTossPreloadManager.Instance.GetDownloadedItemIds();

        // 큐에 있는 아이템들 UI 생성/업데이트
        foreach (var item in queuedItems)
        {
            if (!downloadItemUIs.ContainsKey(item.itemId))
            {
                CreateDownloadItemUI(item);
            }

            bool isDownloading = AppsInTossPreloadManager.Instance.IsItemDownloading(item.itemId);
            float progress = isDownloading ?
                AppsInTossPreloadManager.Instance.GetDownloadProgress(item.itemId) : 0f;

            string status = isDownloading ? "다운로드 중" : "대기 중";
            UpdateDownloadItemUI(item.itemId, progress, status);
        }

        // 완료된 아이템들 처리
        foreach (var itemId in downloadedItems)
        {
            if (downloadItemUIs.ContainsKey(itemId))
            {
                UpdateDownloadItemUI(itemId, 1f, "완료");
            }
        }
    }

    void CreateDownloadItemUI(AppsInTossPreloadManager.PreloadItem item)
    {
        var itemUI = Instantiate(downloadItemPrefab, downloadItemContainer);
        downloadItemUIs[item.itemId] = itemUI;

        // UI 컴포넌트 설정
        var nameText = itemUI.transform.Find("NameText").GetComponent<TextMeshProUGUI>();
        var sizeText = itemUI.transform.Find("SizeText").GetComponent<TextMeshProUGUI>();
        var progressSlider = itemUI.transform.Find("ProgressSlider").GetComponent<Slider>();
        var statusText = itemUI.transform.Find("StatusText").GetComponent<TextMeshProUGUI>();
        var cancelButton = itemUI.transform.Find("CancelButton").GetComponent<Button>();

        nameText.text = item.itemName;
        sizeText.text = FormatFileSize((long)(item.sizeMB * 1024 * 1024));
        progressSlider.value = 0f;
        statusText.text = "대기 중";

        // 취소 버튼 이벤트
        cancelButton.onClick.AddListener(() => CancelDownload(item.itemId));

        // 우선순위에 따른 색상 설정
        var priorityIcon = itemUI.transform.Find("PriorityIcon").GetComponent<Image>();
        priorityIcon.color = GetPriorityColor(item.priority);
    }

    void UpdateDownloadItemUI(string itemId, float progress, string status)
    {
        if (!downloadItemUIs.ContainsKey(itemId)) return;

        var itemUI = downloadItemUIs[itemId];
        var progressSlider = itemUI.transform.Find("ProgressSlider").GetComponent<Slider>();
        var statusText = itemUI.transform.Find("StatusText").GetComponent<TextMeshProUGUI>();

        progressSlider.value = progress;
        statusText.text = status;

        // 완료된 아이템은 일정 시간 후 제거
        if (progress >= 1f)
        {
            StartCoroutine(RemoveCompletedItemUI(itemId, 3f));
        }
    }

    System.Collections.IEnumerator RemoveCompletedItemUI(string itemId, float delay)
    {
        yield return new WaitForSeconds(delay);

        if (downloadItemUIs.ContainsKey(itemId))
        {
            Destroy(downloadItemUIs[itemId]);
            downloadItemUIs.Remove(itemId);
        }
    }

    Color GetPriorityColor(AppsInTossPreloadManager.PreloadPriority priority)
    {
        switch (priority)
        {
            case AppsInTossPreloadManager.PreloadPriority.Critical:
                return Color.red;
            case AppsInTossPreloadManager.PreloadPriority.High:
                return Color.yellow;
            case AppsInTossPreloadManager.PreloadPriority.Medium:
                return Color.green;
            case AppsInTossPreloadManager.PreloadPriority.Low:
                return Color.gray;
            default:
                return Color.white;
        }
    }

    string FormatFileSize(long bytes)
    {
        string[] sizes = { "B", "KB", "MB", "GB" };
        double len = bytes;
        int order = 0;

        while (len >= 1024 && order < sizes.Length - 1)
        {
            order++;
            len = len / 1024;
        }

        return $"{len:0.##} {sizes[order]}";
    }

    void PauseAllDownloads()
    {
        isPaused = true;
        // 다운로드 일시정지 로직 구현
        Debug.Log("모든 다운로드 일시정지");
    }

    void ResumeAllDownloads()
    {
        isPaused = false;
        // 다운로드 재개 로직 구현
        Debug.Log("모든 다운로드 재개");
    }

    void ClearCache()
    {
        // 캐시 정리 확인 다이얼로그 표시
        ShowClearCacheDialog();
    }

    void ShowClearCacheDialog()
    {
        // 간단한 확인 다이얼로그
        bool confirmed = true; // 실제로는 다이얼로그 결과

        if (confirmed)
        {
            // 캐시 정리 실행
            Debug.Log("캐시 정리 시작");
        }
    }

    void CancelDownload(string itemId)
    {
        // 다운로드 취소 로직
        if (downloadItemUIs.ContainsKey(itemId))
        {
            Destroy(downloadItemUIs[itemId]);
            downloadItemUIs.Remove(itemId);
        }

        Debug.Log($"다운로드 취소: {itemId}");
    }

    void OnWiFiOnlyChanged(bool value)
    {
        // WiFi 전용 설정 변경
        SaveUserPreference("wifi_only", value);
    }

    void OnBackgroundDownloadChanged(bool value)
    {
        // 백그라운드 다운로드 설정 변경
        SaveUserPreference("background_download", value);
    }

    void OnMaxCacheSizeChanged(float value)
    {
        // 최대 캐시 크기 설정 변경
        long maxSizeMB = (long)(value * 1000);
        maxCacheSizeLabel.text = $"최대 캐시 크기: {maxSizeMB}MB";
        SaveUserPreference("max_cache_size_mb", maxSizeMB);
    }

    void LoadUserPreferences()
    {
        wifiOnlyToggle.isOn = GetUserPreference("wifi_only", false);
        backgroundDownloadToggle.isOn = GetUserPreference("background_download", true);

        long maxCacheSizeMB = GetUserPreference("max_cache_size_mb", 500L);
        maxCacheSizeSlider.value = maxCacheSizeMB / 1000f;
        maxCacheSizeLabel.text = $"최대 캐시 크기: {maxCacheSizeMB}MB";
    }

    void SaveUserPreference(string key, object value)
    {
        // 사용자 설정 저장
        PlayerPrefs.SetString($"preload_{key}", value.ToString());
        PlayerPrefs.Save();
    }

    T GetUserPreference<T>(string key, T defaultValue)
    {
        // 사용자 설정 로드
        string prefKey = $"preload_{key}";
        if (PlayerPrefs.HasKey(prefKey))
        {
            string value = PlayerPrefs.GetString(prefKey);
            try
            {
                return (T)System.Convert.ChangeType(value, typeof(T));
            }
            catch
            {
                return defaultValue;
            }
        }
        return defaultValue;
    }

    void Update()
    {
        // UI 주기적 업데이트
        if (Time.frameCount % 60 == 0) // 1초마다
        {
            UpdateUI();
        }
    }

    void OnDestroy()
    {
        AppsInToss.OnEvent -= HandlePreloadEvent;
    }
}

사전 다운로드를 통해 게임 플레이 중 끊김을 최소화하되, 사용자의 데이터 요금과 배터리, 저장 공간을 고려한 스마트한 다운로드 전략을 구현하세요.