using System; using System.Collections.Generic; using System.IO; using UnityEngine; // 로컬에 다운로드된 곡 목록을 추적하고 persistentDataPath에 저장 public class SongLibrary : MonoBehaviour { public static SongLibrary Instance { get; private set; } private const string FileName = "song_library.json"; private static string SavePath => Path.Combine(Application.persistentDataPath, FileName); private LibraryData _data = new LibraryData(); // ── Unity ──────────────────────────────────────────────── private void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); Load(); } // ── Public API ─────────────────────────────────────────── // 다운로드 완료 시 호출 public void MarkDownloaded(string songId, string difficulty) { LibraryEntry entry = GetOrCreate(songId); if (!entry.difficulties.Contains(difficulty)) entry.difficulties.Add(difficulty); entry.lastAccessedAt = DateTime.UtcNow.ToString("o"); Save(); } // 난이도 삭제 시 호출 public void MarkDifficultyRemoved(string songId, string difficulty) { LibraryEntry entry = Find(songId); if (entry == null) return; entry.difficulties.Remove(difficulty); if (entry.difficulties.Count == 0) _data.entries.Remove(entry); Save(); } // 곡 전체 삭제 시 호출 public void MarkSongRemoved(string songId) { _data.entries.RemoveAll(e => e.songId == songId); Save(); } // 플레이 시작 시 호출 → LRU 갱신 public void TouchSong(string songId) { LibraryEntry entry = Find(songId); if (entry == null) return; entry.lastAccessedAt = DateTime.UtcNow.ToString("o"); Save(); } public bool IsSongDownloaded(string songId) => Find(songId) != null; public bool IsDifficultyDownloaded(string songId, string difficulty) => Find(songId)?.difficulties.Contains(difficulty) ?? false; public List GetAll() => _data.entries; // 앱 시작 시 실제 파일과 비교해 유효성 검사 public void ValidateWithFileSystem(DownloadManager dm, List songs) { bool dirty = false; foreach (SongInfo song in songs) { LibraryEntry entry = Find(song.id); if (entry == null) continue; // 오디오 파일이 없으면 항목 전체 제거 if (!dm.IsSongDownloaded(song.id)) { _data.entries.Remove(entry); dirty = true; continue; } // 없어진 난이도 맵 파일 제거 entry.difficulties.RemoveAll(d => !dm.IsDifficultyDownloaded(song, d)); if (entry.difficulties.Count == 0) { _data.entries.Remove(entry); dirty = true; } } if (dirty) Save(); } // ── 내부 구현 ───────────────────────────────────────────── private LibraryEntry Find(string songId) => _data.entries.Find(e => e.songId == songId); private LibraryEntry GetOrCreate(string songId) { LibraryEntry entry = Find(songId); if (entry != null) return entry; entry = new LibraryEntry { songId = songId }; _data.entries.Add(entry); return entry; } private void Load() { if (!File.Exists(SavePath)) return; try { string json = File.ReadAllText(SavePath); _data = JsonUtility.FromJson(json) ?? new LibraryData(); } catch (Exception e) { Debug.LogWarning($"[SongLibrary] 로드 실패, 초기화: {e.Message}"); _data = new LibraryData(); } } private void Save() { File.WriteAllText(SavePath, JsonUtility.ToJson(_data, true)); } } // ── 직렬화 데이터 구조 ───────────────────────────────────── [Serializable] public class LibraryData { public List entries = new List(); } [Serializable] public class LibraryEntry { public string songId; public List difficulties = new List(); public string lastAccessedAt; }