Files
XRLib/Assets/Scripts/UVC/UI/List/Draggable/DraggableList.cs
2025-08-01 13:30:58 +09:00

414 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 드래그 가능한 리스트의 전체 기능을 관리하는 메인 컴포넌트입니다.
/// 다른 모든 컴포넌트들을 조합하여 완전한 드래그 앤 드롭 리스트를 만듭니다.
///
/// 설정 방법:
/// 1. ScrollView GameObject에 이 컴포넌트를 추가합니다.
/// - ScrollRectHandler 추가
/// 2. Inspector에서 Item Prefab과 Content를 연결합니다.
/// 3. Item Prefab에는 다음 컴포넌트들이 필요합니다:
/// - ListItemController
/// - ListItemView
/// - DragBehavior
/// 4.ScrollView > Viewport > Content
/// - Vertical Layout Group 추가(Control Child Size: width, Child Force Expand: width)
/// - Content Size Fitter 추가(Vertical Fit: Preferred Size)
/// - ListReorderHandler 추가
///
/// 사용 예시:
/// public class MyListData : IListItemData
/// {
/// public string Id { get; set; }
/// public string DisplayName { get; set; }
/// }
///
/// void Start()
/// {
/// var list = GetComponent<DraggableList>();
///
/// list.OnOrderChanged += (data, oldIndex, newIndex) => Debug.Log($"아이템 순서 변경: {data.DisplayName} ({oldIndex} -> {newIndex})");
/// list.OnItemClicked += (data) => Debug.Log($"아이템 클릭: {data.DisplayName}");
/// list.OnChangedItemData += (data) => Debug.Log($"아이템 데이터 변경: {data.DisplayName}");
///
/// var dataList = new List<MyListData>
/// {
/// new MyListData { Id = "1", DisplayName = "첫 번째 아이템" },
/// new MyListData { Id = "2", DisplayName = "두 번째 아이템" }
/// };
///
/// list.SetData(dataList);
/// }
/// </summary>
public class DraggableList : MonoBehaviour
{
[Header("필수 설정")]
[SerializeField]
[Tooltip("리스트 아이템으로 사용할 프리팹. DragBehavior, ListItemController, ItemDataBinder가 있어야 합니다.")]
private GameObject? itemPrefab;
[SerializeField]
[Tooltip("아이템들이 추가될 부모 Transform (보통 ScrollView > Viewport > Content)")]
private Transform? content;
//[Header("옵션")]
//[SerializeField]
//[Tooltip("순서 변경을 자동으로 저장할지 여부")]
//private bool autoSave = false;
// 컴포넌트 참조들
private ListReorderHandler? reorderHandler;
private ScrollRectHandler? scrollHandler;
// 현재 데이터 리스트
private List<ListItemData> dataList = new List<ListItemData>();
public IReadOnlyList<ListItemData> DataList => dataList;
// 아이템 GameObject와 데이터의 매핑
private Dictionary<GameObject, ListItemData> itemDataMap = new Dictionary<GameObject, ListItemData>();
/// <summary>
/// 아이템 순서가 변경될 때 발생하는 이벤트, oldIndex, newIndex
/// </summary>
public event Action<ListItemData, int, int>? OnOrderChanged;
/// <summary>
/// 아이템이 클릭될 때 발생하는 이벤트
/// </summary>
public event Action<ListItemData>? OnItemClicked;
/// <summary>
/// 아이템 데이터가 변경 됐을 때 발생하는 이벤트
/// </summary>
public event Action<ListItemData>? OnChangedItemData;
/// <summary>
/// 컴포넌트 초기화
/// </summary>
void Awake()
{
ValidateSetup();
SetupComponents();
}
/// <summary>
/// 필수 설정이 올바른지 검증합니다
/// </summary>
private void ValidateSetup()
{
if (itemPrefab == null)
{
Debug.LogError($"[DraggableList] Item Prefab이 설정되지 않았습니다! - {gameObject.name}");
}
if (content == null)
{
Debug.LogError($"[DraggableList] Content Transform이 설정되지 않았습니다! - {gameObject.name}");
}
// 프리팹에 필수 컴포넌트가 있는지 확인
if (itemPrefab != null)
{
if (itemPrefab.GetComponent<DragBehavior>() == null)
if (itemPrefab.GetComponentInChildren<DragBehavior>() == null)
Debug.LogError($"[DraggableList] Item Prefab에 DragBehavior가 없습니다!");
if (itemPrefab.GetComponent<ListItemController>() == null)
Debug.LogError($"[DraggableList] Item Prefab에 ListItemController가 없습니다!");
if (itemPrefab.GetComponent<ListItemView>() == null)
Debug.LogError($"[DraggableList] Item Prefab에 ItemDataBinder가 없습니다!");
}
}
/// <summary>
/// 필요한 컴포넌트들을 설정합니다
/// </summary>
private void SetupComponents()
{
if (content == null) return;
// ListReorderHandler 추가 또는 가져오기
reorderHandler = content.GetComponent<ListReorderHandler>();
if (reorderHandler == null)
{
reorderHandler = content.gameObject.AddComponent<ListReorderHandler>();
//Debug.Log("[DraggableList] ListReorderHandler를 자동으로 추가했습니다.");
}
// ScrollRectHandler 추가 또는 가져오기
var scrollRect = GetComponentInChildren<ScrollRect>();
if (scrollRect != null)
{
scrollHandler = scrollRect.GetComponent<ScrollRectHandler>();
if (scrollHandler == null)
{
scrollHandler = scrollRect.gameObject.AddComponent<ScrollRectHandler>();
//Debug.Log("[DraggableList] ScrollRectHandler를 자동으로 추가했습니다.");
}
}
// 이벤트 연결
if (reorderHandler != null)
{
reorderHandler.OnOrderChanged += HandleOrderChanged;
}
}
/// <summary>
/// 리스트에 표시할 데이터를 설정합니다
/// </summary>
/// <typeparam name="T">IListItemData를 구현한 데이터 타입</typeparam>
/// <param name="data">표시할 데이터 리스트</param>
public void SetData<T>(List<T> data) where T : ListItemData
{
// 기존 데이터와 UI 정리
ClearList();
// 새 데이터 추가
dataList.Clear();
foreach (var item in data)
{
dataList.Add(item);
}
// UI 생성
RefreshUI();
Debug.Log($"[DraggableList] 데이터 설정 완료 - {data.Count}개 아이템");
}
/// <summary>
/// 현재 데이터를 기반으로 UI를 다시 생성합니다
/// </summary>
private void RefreshUI()
{
if (content == null || itemPrefab == null) return;
// 기존 UI 제거
foreach (Transform child in content)
{
Destroy(child.gameObject);
}
itemDataMap.Clear();
// 새 UI 생성
foreach (var data in dataList)
{
CreateItemUI(data);
}
}
/// <summary>
/// 단일 아이템의 UI를 생성합니다
/// </summary>
private void CreateItemUI(ListItemData data)
{
if (itemPrefab == null || content == null) return;
// 프리팹으로부터 새 아이템 생성
GameObject item = Instantiate(itemPrefab, content);
item.name = $"Item_{data.Id ?? "Unknown"}";
// 데이터 아이템 View 컴포넌트 가져오기
var binder = item.GetComponent<ListItemView>();
binder?.BindData(data);
if(binder != null) binder.OnChangeData += (data) => OnChangedItemData?.Invoke(data);
// 클릭 이벤트 설정
var button = item.GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(() => HandleItemClick(data));
}
// 매핑 저장
itemDataMap[item] = data;
}
/// <summary>
/// 아이템 순서 변경을 처리합니다
/// </summary>
private void HandleOrderChanged(int oldIndex, int newIndex)
{
if (oldIndex == newIndex) return;
// 데이터 리스트의 순서도 변경
if (oldIndex >= 0 && oldIndex < dataList.Count &&
newIndex >= 0 && newIndex < dataList.Count)
{
var item = dataList[oldIndex];
dataList.RemoveAt(oldIndex);
dataList.Insert(newIndex, item);
Debug.Log($"[DraggableList] 순서 변경: {oldIndex} -> {newIndex}");
// 이벤트 발생
OnOrderChanged?.Invoke((item as ListItemData)!, oldIndex, newIndex);
// 자동 저장
//if (autoSave)
//{
// SaveOrder();
//}
}
}
/// <summary>
/// 아이템 클릭을 처리합니다
/// </summary>
private void HandleItemClick(object data)
{
OnItemClicked?.Invoke((data as ListItemData)!);
//Debug.Log($"[DraggableList] 아이템 클릭: {(data as ListItemData)?.DisplayName}");
}
/// <summary>
/// 현재 순서를 저장합니다
/// </summary>
private void SaveOrder()
{
// 간단한 예시: ID 순서를 PlayerPrefs에 저장
var ids = new List<string>();
foreach (var item in dataList)
{
if (item is ListItemData listItem)
{
ids.Add(listItem.Id);
}
}
string orderString = string.Join(",", ids);
PlayerPrefs.SetString("ListOrder", orderString);
PlayerPrefs.Save();
Debug.Log($"[DraggableList] 순서 저장됨: {orderString}");
}
/// <summary>
/// 저장된 순서를 불러옵니다
/// </summary>
public void LoadOrder()
{
string orderString = PlayerPrefs.GetString("ListOrder", "");
if (string.IsNullOrEmpty(orderString)) return;
// TODO: 저장된 순서에 따라 dataList를 재정렬하는 로직 구현
Debug.Log($"[DraggableList] 순서 불러옴: {orderString}");
}
/// <summary>
/// 리스트를 완전히 비웁니다
/// </summary>
public void ClearList()
{
if (content != null)
{
foreach (Transform child in content)
{
Destroy(child.gameObject);
}
}
dataList.Clear();
itemDataMap.Clear();
Debug.Log("[DraggableList] 리스트 초기화됨");
}
/// <summary>
/// 특정 아이템을 추가합니다
/// </summary>
public void AddItem<T>(T item) where T : ListItemData
{
dataList.Add(item);
CreateItemUI(item);
//if (autoSave)
//{
// SaveOrder();
//}
}
/// <summary>
/// 특정 ID의 아이템을 제거합니다
/// </summary>
public void RemoveItem(string id)
{
// 데이터에서 찾기
ListItemData? toRemove = null;
foreach (var item in dataList)
{
if (item is ListItemData listItem && listItem.Id == id)
{
toRemove = item;
break;
}
}
if (toRemove != null)
{
dataList.Remove(toRemove);
// UI에서 제거
GameObject? uiToRemove = null;
foreach (var kvp in itemDataMap)
{
if (kvp.Value == toRemove)
{
uiToRemove = kvp.Key;
break;
}
}
if (uiToRemove != null)
{
itemDataMap.Remove(uiToRemove);
Destroy(uiToRemove);
}
//if (autoSave)
//{
// SaveOrder();
//}
}
}
/// <summary>
/// 현재 데이터 리스트를 가져옵니다
/// </summary>
public List<T> GetData<T>() where T : class
{
var result = new List<T>();
foreach (var item in dataList)
{
if (item is T typedItem)
{
result.Add(typedItem);
}
}
return result;
}
/// <summary>
/// 컴포넌트가 파괴될 때 이벤트 구독을 해제합니다
/// </summary>
void OnDestroy()
{
if (reorderHandler != null)
{
reorderHandler.OnOrderChanged -= HandleOrderChanged;
}
}
}
}