Files
XRLib/Assets/Scripts/Factory/Playback/UI/UIPlaybackListItem.cs
2025-12-12 19:28:20 +09:00

427 lines
16 KiB
C#

using Best.HTTP;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UVC.Extension;
using UVC.Pool;
using UVC.UI.Modal;
using UVC.Util;
namespace UVC.Factory.Playback.UI
{
/// <summary>
/// 재생 목록의 각 아이템(1시간 단위 파일)을 나타내는 UI 컴포넌트입니다.
/// 다운로드, 삭제, 선택 등 다양한 상태를 관리합니다.
///
/// <para>샘플 사용법:</para>
/// <code>
/// // 1. 풀에서 아이템 생성 및 초기화
/// var itemData = new UIPlaybackListItemData {
/// date = "2024-07-29",
/// time = "13",
/// zipFileName = "2024-07-29_13.zip",
/// sqlFileName = "2024-07-29_13.sql"
/// };
/// var item = UIPlaybackListItem.CreateFromPool(parentTransform);
/// item.Init(itemData);
///
/// // 2. 선택 이벤트 등록
/// item.OnSelect = (data, selected) => {
/// Debug.Log($"{data.time}시 아이템이 {(selected ? "선택됨" : "선택 해제됨")}");
/// };
///
/// // 3. 상태 변경 이벤트 등록
/// item.OnChangeStatus = (data, status) => {
/// Debug.Log($"{data.time}시 아이템 상태: {status}");
/// };
///
/// // 4. 다운로드 버튼 클릭(직접 호출 예시)
/// // item.SendMessage("onClickDownload");
/// </code>
/// </summary>
public enum UIPlaybackListItemStatus
{
/// <summary>기본 상태(다운로드 전)</summary>
Default,
/// <summary>다운로드 중</summary>
Downloading,
/// <summary>다운로드 완료</summary>
Downloaded
}
/// <summary>
/// 재생 목록 아이템의 데이터 구조체입니다.
/// </summary>
public class UIPlaybackListItemData
{
public string date = ""; // 날짜 (예: "2024-07-29")
public string time = ""; // 시간 (예: "13")
public string zipFileName = ""; // ZIP 파일명
public string sqlFileName = ""; // SQL 파일명
public UIPlaybackListItemStatus status = UIPlaybackListItemStatus.Default;
public override string ToString()
{
return $"date:{date}, time:{time}, zipFileName:{zipFileName}, sqlFileName:{sqlFileName}, status:{status}";
}
}
/// <summary>
/// 재생 목록의 각 아이템을 관리하는 UI 컴포넌트입니다.
/// - 풀링, 상태 관리, 다운로드/삭제/중지 버튼, 선택 이벤트 등을 제공합니다.
/// </summary>
public class UIPlaybackListItem : UnityEngine.MonoBehaviour, IPointerClickHandler
{
// 오브젝트 풀: 아이템을 효율적으로 재사용하기 위한 풀입니다.
protected static ItemPool<UIPlaybackListItem> pool;
/// <summary>
/// 프리팹 경로(Resources 폴더 기준)
/// </summary>
public static readonly string PrefabPath = "Factory/Prefabs/Playback/UIPlaybackListItem";
/// <summary>
/// 풀에서 아이템을 생성(또는 재사용)합니다.
/// </summary>
/// <param name="parent">부모 Transform</param>
/// <returns>UIPlaybackListItem 인스턴스</returns>
/// <example>
/// var item = UIPlaybackListItem.CreateFromPool(parentTransform);
/// </example>
public static UIPlaybackListItem CreateFromPool(Transform parent)
{
if (pool == null)
{
GameObject prefab = Resources.Load<GameObject>(PrefabPath);
pool = new ItemPool<UIPlaybackListItem>(prefab, parent, null);
}
return pool.GetItem(true, parent);
}
/// <summary>
/// 현재 활성화된(화면에 보이는) 아이템 목록
/// </summary>
public static List<UIPlaybackListItem> ActiveItems
{
get
{
if (pool != null) return (List<UIPlaybackListItem>)pool.ActiveItems;
return new List<UIPlaybackListItem>();
}
}
/// <summary>
/// 현재 다운로드 중인 아이템 목록
/// </summary>
public static List<UIPlaybackListItem> DownloadingItems
{
get
{
return ActiveItems.Where(x => x.Status == UIPlaybackListItemStatus.Downloading).ToList();
}
}
/// <summary>
/// 모든 아이템을 풀에 반환(비활성화)합니다.
/// </summary>
public static void ReleaseAll()
{
if (pool != null)
{
foreach (var item in pool.ActiveItems) item.Release();
pool.ReturnAll();
}
}
/// <summary>
/// 모든 아이템을 풀에 반환하고, 재활용 목록도 비웁니다.
/// </summary>
public static void ClearAll()
{
if (pool != null)
{
foreach (var item in pool.ActiveItems) item.Release();
pool.ReturnAll();
pool.ClearRecycledItems();
}
}
// --- UI 요소들 (Unity 에디터에서 할당) ---
[SerializeField]
private Image loadingImage;
[SerializeField]
private Image selectedImage;
[SerializeField]
private Button downloadButton;
[SerializeField]
private Button stopButton;
[SerializeField]
private Button deleteButton;
[SerializeField]
private TextMeshProUGUI downloadText;
[SerializeField]
private TextMeshProUGUI text;
/// <summary>
/// 아이템이 선택될 때 호출되는 콜백 (data, 선택여부)
/// </summary>
public Action<UIPlaybackListItemData, bool> OnSelect { get; set; }
/// <summary>
/// 아이템 상태가 변경될 때 호출되는 콜백 (data, 변경된 상태)
/// </summary>
public Action<UIPlaybackListItemData, UIPlaybackListItemStatus> OnChangeStatus { get; set; }
private UIPlaybackListItemData data;
/// <summary>
/// 이 아이템의 데이터
/// </summary>
public UIPlaybackListItemData Data { get => data; }
/// <summary>
/// 아이템의 현재 상태 (Default/Downloading/Downloaded)
/// </summary>
public UIPlaybackListItemStatus Status
{
get => data.status;
set
{
if (data.status != value)
{
data.status = value;
ChangeStatus();
if (OnChangeStatus != null) OnChangeStatus.Invoke(Data, value);
}
}
}
private bool selected = false;
/// <summary>
/// 아이템이 선택되었는지 여부
/// </summary>
public bool Selected
{
get => selected;
set
{
if (selected != value)
{
selected = value;
selectedImage.gameObject.SetActive(selected);
}
}
}
// 다운로드 요청 객체 (다운로드 중일 때만 사용)
private HTTPRequest? downloadRequest;
/// <summary>
/// 아이템을 초기화합니다. (데이터 바인딩 및 버튼 이벤트 등록)
/// </summary>
/// <param name="data">초기화할 데이터</param>
/// <example>
/// var item = UIPlaybackListItem.CreateFromPool(parent);
/// item.Init(data);
/// </example>
public void Init(UIPlaybackListItemData data)
{
transform.localScale = Vector3.one;
selected = false;
selectedImage.gameObject.SetActive(selected);
this.data = data;
text.text = $"{data.time} ~ {int.Parse(data.time) + 1} Hour";
downloadButton.onClick.AddListener(onClickDownload);
stopButton.onClick.AddListener(onClickStop);
deleteButton.onClick.AddListener(onClickDelete);
ChangeStatus();
}
/// <summary>
/// 다운로드 버튼 클릭 시 호출됩니다.
/// - 용량 체크, 동시 다운로드 제한, 다운로드 시작 등 처리
/// </summary>
private void onClickDownload()
{
if (Status == UIPlaybackListItemStatus.Downloading) return;
long freeSize = DriveUtil.GetDriveGigaBytes();
Debug.Log($"여유 공간 freeSize:{freeSize} {10 * (DownloadingItems.Count + 1)} {freeSize < 10 * (DownloadingItems.Count + 1)}");
if (freeSize < 10 * (DownloadingItems.Count + 1))
{
Toast.Show("Playback 하나당 10기가바이트의 용량이 필요합니다. 용량을 확보하고 다운로드 하세요.\nEach Playback requires a capacity of 10 gigabytes. Secure the capacity and download it.");
return;
}
if (DownloadingItems.Count >= 2)
{
Toast.Show("한번에 총 2개 아이템만 다운로드가 가능합니다.\nOnly 2 items can be downloaded at a time.");
//await UIAlertManager.Instance.ShowAlert("Downloading", "한번에 총 2개 아이템만 다운로드가 가능합니다.\nOnly 3 items can be downloaded at a time.");
return;
}
Status = UIPlaybackListItemStatus.Downloading;
// 실제 다운로드 요청 (진행률/완료/에러 콜백 처리)
downloadRequest = PlaybackService.Instance.ReadyData(data.date, data.time, data.zipFileName,
(long read, long total, float percent) =>
{
Debug.Log($"ReadyData {percent}");
downloadText.text = $"{read.ToSizeSuffix()}/{total.ToSizeSuffix()} ({(int)(percent * 100)}%)";
loadingImage.fillAmount = percent;
},
(string errorMessage) =>
{
Debug.Log($"Complete Data errorMessage:{errorMessage}");
if (downloadRequest != null) downloadRequest.Clear();
downloadRequest = null;
if (errorMessage != null)
{
if (errorMessage.ToLower().Contains("abort")) errorMessage = "다운로드를 중지 했습니다.\nThe download has been stopped.";
Toast.Show(errorMessage);
//await UIAlertManager.Instance.ShowAlert("Error", errorMessage);
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
else
{
Status = UIPlaybackListItemStatus.Downloaded;
}
});
}
/// <summary>
/// 다운로드/삭제 시 파일을 실제로 삭제합니다.
/// </summary>
private void deleteFile()
{
try
{
string playbackPath = PlaybackService.PlaybackFolderPath;
string tempPath = Path.Combine(playbackPath, "temp");
string datePath = Path.Combine(playbackPath, data.date);
var fileNameArr = data.zipFileName.Split('.');
DateTime utcDateTime = DateTimeUtil.Parse(fileNameArr[0], "yyyy-MM-dd_H");
string utcSqlFileName = utcDateTime.ToString("yyyy-MM-dd_H") + "." + fileNameArr[1];
string utcZipFileName = utcSqlFileName + "." + fileNameArr[2];
string utcZipFilePath = Path.Combine(datePath, utcZipFileName);
string utcSqlFilePath = Path.Combine(datePath, utcSqlFileName);
System.GC.Collect();
System.GC.WaitForPendingFinalizers();
if (File.Exists(utcZipFilePath)) File.Delete(utcZipFilePath);
if (File.Exists(utcSqlFilePath)) File.Delete(utcSqlFilePath);
// if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true);
}
catch (Exception ex)
{
Debug.LogException(ex);
}
}
/// <summary>
/// 다운로드 중지 버튼 클릭 시 호출됩니다.
/// </summary>
private void onClickStop()
{
if (downloadRequest != null && Status == UIPlaybackListItemStatus.Downloading)
{
downloadRequest.Abort();
downloadRequest.Clear();
downloadRequest = null;
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
}
/// <summary>
/// 삭제 버튼 클릭 시 호출됩니다.
/// </summary>
private void onClickDelete()
{
deleteFile();
Status = UIPlaybackListItemStatus.Default;
}
/// <summary>
/// 아이템을 클릭하면 선택/해제 상태가 토글됩니다.
/// </summary>
/// <param name="eventData">이벤트 데이터</param>
public void OnPointerClick(PointerEventData eventData)
{
Selected = !selected;
if (OnSelect != null) OnSelect.Invoke(data, Selected);
}
/// <summary>
/// 풀에 반환될 때 호출 (이벤트 해제 등)
/// </summary>
public void Release()
{
OnDestroy();
}
/// <summary>
/// 오브젝트가 파괴될 때 호출 (이벤트 해제)
/// </summary>
private void OnDestroy()
{
downloadButton.onClick.RemoveListener(onClickDownload);
stopButton.onClick.RemoveListener(onClickStop);
deleteButton.onClick.RemoveListener(onClickDelete);
OnSelect = null;
OnChangeStatus = null;
transform.SetParent(null);
}
/// <summary>
/// 상태에 따라 UI를 갱신합니다.
/// </summary>
private void ChangeStatus()
{
downloadText.text = "";
if (data.status == UIPlaybackListItemStatus.Default)
{
loadingImage.gameObject.SetActive(false);
downloadButton.gameObject.SetActive(true);
stopButton.gameObject.SetActive(false);
deleteButton.gameObject.SetActive(false);
downloadText.gameObject.SetActive(false);
}
else if (data.status == UIPlaybackListItemStatus.Downloading)
{
loadingImage.gameObject.SetActive(true);
loadingImage.SetAlpha(0.2f);
loadingImage.fillAmount = 0;
downloadButton.gameObject.SetActive(false);
stopButton.gameObject.SetActive(true);
deleteButton.gameObject.SetActive(false);
downloadText.gameObject.SetActive(true);
}
else if (data.status == UIPlaybackListItemStatus.Downloaded)
{
loadingImage.gameObject.SetActive(true);
loadingImage.SetAlpha(1f);
loadingImage.fillAmount = 1;
downloadButton.gameObject.SetActive(false);
stopButton.gameObject.SetActive(false);
deleteButton.gameObject.SetActive(true);
downloadText.gameObject.SetActive(false);
}
}
}
}