414 lines
14 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|