1076 lines
38 KiB
C#
1076 lines
38 KiB
C#
|
|
#nullable enable
|
||
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Linq;
|
||
|
|
using Unity.VisualScripting;
|
||
|
|
using UnityEditorInternal.VersionControl;
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.EventSystems;
|
||
|
|
using UnityEngine.UI;
|
||
|
|
|
||
|
|
namespace UVC.UI.List
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 가능한 ScrollRect 목록을 관리하는 메인 컨트롤러
|
||
|
|
/// Model-View 패턴을 적용하여 데이터와 UI를 분리
|
||
|
|
/// </summary>
|
||
|
|
/// <example>
|
||
|
|
/// <b>사용 예제:</b>
|
||
|
|
/// 1. 이벤트 구독
|
||
|
|
/// 2. DraggableItemData 설정
|
||
|
|
/// <code>
|
||
|
|
/// public class DraggableScrollListSetup : MonoBehaviour
|
||
|
|
/// {
|
||
|
|
/// [SerializeField]
|
||
|
|
/// private DraggableScrollList? draggableList;
|
||
|
|
///
|
||
|
|
/// protected virtual void Awake()
|
||
|
|
/// {
|
||
|
|
/// if (draggableList == null)
|
||
|
|
/// {
|
||
|
|
/// Debug.LogError("draggableList 참조가 설정되지 않았습니다.");
|
||
|
|
/// return;
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// // 이벤트 구독
|
||
|
|
/// draggableList.OnItemReordered += OnItemReordered;
|
||
|
|
/// draggableList.OnItemSelected += OnItemSelected;
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// void Start()
|
||
|
|
/// {
|
||
|
|
/// // 1. DraggableItemData 설정
|
||
|
|
/// draggableList?.AddItem(new DraggableItemData("AGV", 0));
|
||
|
|
/// draggableList?.AddItem(new DraggableItemData("ALARM", 1));
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// /// <summary>
|
||
|
|
/// /// 아이템 순서 변경 이벤트 처리
|
||
|
|
/// /// </summary>
|
||
|
|
/// /// <param name="sender">이벤트 발생자</param>
|
||
|
|
/// /// <param name="e">이벤트 인자</param>
|
||
|
|
/// private void OnItemReordered(object? sender, DraggableItemReorderEventArgs e)
|
||
|
|
/// {
|
||
|
|
/// Debug.Log($"아이템 순서 변경됨: ID={e.ItemId}, {e.OldIndex} -> {e.NewIndex}");
|
||
|
|
///
|
||
|
|
/// // 여기에 순서 변경에 대한 비즈니스 로직 구현
|
||
|
|
/// // 예: 서버에 변경사항 전송, 설정 저장 등
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// /// <summary>
|
||
|
|
/// /// 아이템 선택 이벤트 처리
|
||
|
|
/// /// </summary>
|
||
|
|
/// /// <param name="sender">이벤트 발생자</param>
|
||
|
|
/// /// <param name="item">선택된 아이템</param>
|
||
|
|
/// private void OnItemSelected(object? sender, DraggableListItem item)
|
||
|
|
/// {
|
||
|
|
/// if (item?.Data != null)
|
||
|
|
/// {
|
||
|
|
/// Debug.Log($"아이템 선택됨: {item.Data.Id}");
|
||
|
|
///
|
||
|
|
/// // 선택된 아이템에 대한 처리
|
||
|
|
/// // 예: 상세 정보 표시, 편집 모드 진입 등
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
///
|
||
|
|
/// /// <summary>
|
||
|
|
/// /// 컴포넌트 정리
|
||
|
|
/// /// </summary>
|
||
|
|
/// private void OnDestroy()
|
||
|
|
/// {
|
||
|
|
/// if (draggableList != null)
|
||
|
|
/// {
|
||
|
|
/// draggableList.OnItemReordered -= OnItemReordered;
|
||
|
|
/// draggableList.OnItemSelected -= OnItemSelected;
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// }
|
||
|
|
/// </code>
|
||
|
|
/// </example>
|
||
|
|
public class DraggableScrollList : MonoBehaviour
|
||
|
|
{
|
||
|
|
[Header("UI 참조")]
|
||
|
|
[SerializeField] private ScrollRect? scrollRect;
|
||
|
|
[SerializeField] private RectTransform? contentParent;
|
||
|
|
[SerializeField] private VerticalLayoutGroup? layoutGroup;
|
||
|
|
|
||
|
|
[Header("드롭 인디케이터")]
|
||
|
|
[SerializeField] private Sprite? dropLineSprite;
|
||
|
|
[SerializeField] private Color dropLineColor = Color.cyan;
|
||
|
|
[SerializeField] private float dropLineHeight = 3f;
|
||
|
|
[SerializeField] private float dropLineMargin = 10f;
|
||
|
|
[SerializeField] private Material? dropLineMaterial; // 선택적: 특별한 Material 사용 시
|
||
|
|
|
||
|
|
[Header("프리팹 설정")]
|
||
|
|
[SerializeField] private string itemPrefabPath = "Prefabs/UI/List/DraggableListItem";
|
||
|
|
|
||
|
|
[Header("드래그 설정")]
|
||
|
|
[SerializeField] private float dropZoneThreshold = 50f;
|
||
|
|
[SerializeField] private float scrollSensitivity = 100f;
|
||
|
|
[SerializeField] private bool enableAutoScroll = true;
|
||
|
|
|
||
|
|
// 이벤트
|
||
|
|
public event EventHandler<DraggableItemReorderEventArgs>? OnItemReordered;
|
||
|
|
public event EventHandler<DraggableListItem>? OnItemSelected;
|
||
|
|
|
||
|
|
// 데이터 및 UI 관리
|
||
|
|
private List<DraggableItemData> itemDataList = new List<DraggableItemData>();
|
||
|
|
private List<DraggableListItem> itemUIList = new List<DraggableListItem>();
|
||
|
|
private GameObject? itemPrefab;
|
||
|
|
|
||
|
|
// 드래그 상태 관리
|
||
|
|
private DraggableListItem? currentDraggingItem;
|
||
|
|
private int dragStartIndex = -1;
|
||
|
|
private int currentDropIndex = -1;
|
||
|
|
private Camera? uiCamera;
|
||
|
|
|
||
|
|
// 드롭 라인 관리 (동적 생성)
|
||
|
|
private GameObject? dropLineObject;
|
||
|
|
private Image? dropLineImage;
|
||
|
|
private RectTransform? dropLineRectTransform;
|
||
|
|
private bool isDropLineVisible = false;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 현재 아이템 데이터 목록 (읽기 전용)
|
||
|
|
/// </summary>
|
||
|
|
public IReadOnlyList<DraggableItemData> ItemDataList => itemDataList.AsReadOnly();
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 컴포넌트 초기화
|
||
|
|
/// </summary>
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
InitializeComponents();
|
||
|
|
LoadItemPrefab();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// UI 카메라 참조 설정
|
||
|
|
/// </summary>
|
||
|
|
private void Start()
|
||
|
|
{
|
||
|
|
// UI 카메라 찾기 (Canvas의 카메라 또는 메인 카메라)
|
||
|
|
Canvas? canvas = GetComponentInParent<Canvas>();
|
||
|
|
uiCamera = canvas?.worldCamera ?? Camera.main;
|
||
|
|
|
||
|
|
// 드롭 라인 생성 (Start에서 호출하여 모든 컴포넌트가 초기화된 후 실행)
|
||
|
|
CreateDropLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 필수 컴포넌트들 초기화 및 검증
|
||
|
|
/// </summary>
|
||
|
|
private void InitializeComponents()
|
||
|
|
{
|
||
|
|
// ScrollRect 자동 할당
|
||
|
|
if (scrollRect == null)
|
||
|
|
scrollRect = GetComponent<ScrollRect>();
|
||
|
|
|
||
|
|
// Content 부모 자동 할당
|
||
|
|
if (contentParent == null && scrollRect?.content != null)
|
||
|
|
contentParent = scrollRect.content;
|
||
|
|
|
||
|
|
// LayoutGroup 자동 할당
|
||
|
|
if (layoutGroup == null && contentParent != null)
|
||
|
|
layoutGroup = contentParent.GetComponent<VerticalLayoutGroup>();
|
||
|
|
|
||
|
|
// 필수 컴포넌트 검증
|
||
|
|
if (scrollRect == null)
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] ScrollRect 컴포넌트를 찾을 수 없습니다!");
|
||
|
|
|
||
|
|
if (contentParent == null)
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] Content 부모 Transform을 찾을 수 없습니다!");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인을 동적으로 생성
|
||
|
|
/// </summary>
|
||
|
|
private void CreateDropLine()
|
||
|
|
{
|
||
|
|
if (contentParent == null)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] Content 부모가 없어 드롭 라인을 생성할 수 없습니다!");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 드롭 라인 GameObject 생성
|
||
|
|
dropLineObject = new GameObject("DropLineIndicator");
|
||
|
|
|
||
|
|
// Content의 자식으로 설정
|
||
|
|
dropLineObject.transform.SetParent(contentParent, false);
|
||
|
|
|
||
|
|
// Image 컴포넌트 추가
|
||
|
|
dropLineImage = dropLineObject.AddComponent<Image>();
|
||
|
|
|
||
|
|
// RectTransform 참조 가져오기
|
||
|
|
dropLineRectTransform = dropLineObject.GetComponent<RectTransform>();
|
||
|
|
|
||
|
|
// 드롭 라인 설정 적용
|
||
|
|
ConfigureDropLine();
|
||
|
|
|
||
|
|
// 초기에는 비활성화
|
||
|
|
if (dropLineObject != null) dropLineObject.SetActive(false);
|
||
|
|
|
||
|
|
Debug.Log($"[{nameof(DraggableScrollList)}] 드롭 라인이 동적으로 생성되었습니다.");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 드롭 라인 생성 중 오류 발생: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인의 시각적 속성 설정
|
||
|
|
/// </summary>
|
||
|
|
private void ConfigureDropLine()
|
||
|
|
{
|
||
|
|
if (dropLineImage == null || dropLineRectTransform == null) return;
|
||
|
|
|
||
|
|
// Sprite 설정
|
||
|
|
if (dropLineSprite != null)
|
||
|
|
{
|
||
|
|
dropLineImage.sprite = dropLineSprite;
|
||
|
|
dropLineImage.type = Image.Type.Sliced; // 스프라이트 늘어남 방지
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 기본 스프라이트 생성 (단색 사각형)
|
||
|
|
dropLineImage.sprite = CreateDefaultDropLineSprite();
|
||
|
|
dropLineImage.type = Image.Type.Simple;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 색상 설정
|
||
|
|
dropLineImage.color = dropLineColor;
|
||
|
|
|
||
|
|
// Material 설정 (선택적)
|
||
|
|
if (dropLineMaterial != null)
|
||
|
|
{
|
||
|
|
dropLineImage.material = dropLineMaterial;
|
||
|
|
}
|
||
|
|
|
||
|
|
// RectTransform 설정
|
||
|
|
SetupDropLineRectTransform();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인의 RectTransform 설정
|
||
|
|
/// </summary>
|
||
|
|
private void SetupDropLineRectTransform()
|
||
|
|
{
|
||
|
|
if (dropLineRectTransform == null) return;
|
||
|
|
|
||
|
|
// 앵커와 피벗 설정 (가로 전체를 차지하도록)
|
||
|
|
dropLineRectTransform.anchorMin = new Vector2(0f, 0.5f);
|
||
|
|
dropLineRectTransform.anchorMax = new Vector2(1f, 0.5f);
|
||
|
|
dropLineRectTransform.pivot = new Vector2(0.5f, 0.5f);
|
||
|
|
|
||
|
|
// 크기 설정 (가로는 부모에 맞춤, 세로는 설정값 사용)
|
||
|
|
dropLineRectTransform.sizeDelta = new Vector2(0f, dropLineHeight);
|
||
|
|
|
||
|
|
// 위치 초기화
|
||
|
|
dropLineRectTransform.anchoredPosition = Vector2.zero;
|
||
|
|
|
||
|
|
// 레이캐스트 차단 방지 (드래그 중 방해하지 않도록)
|
||
|
|
dropLineImage!.raycastTarget = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 기본 드롭 라인 스프라이트 생성 (Sprite가 없을 경우)
|
||
|
|
/// </summary>
|
||
|
|
/// <returns>생성된 기본 스프라이트</returns>
|
||
|
|
private Sprite CreateDefaultDropLineSprite()
|
||
|
|
{
|
||
|
|
// 1x1 픽셀 텍스처 생성
|
||
|
|
Texture2D texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
|
||
|
|
texture.SetPixel(0, 0, Color.white);
|
||
|
|
texture.Apply();
|
||
|
|
|
||
|
|
// 스프라이트 생성
|
||
|
|
Sprite defaultSprite = Sprite.Create(
|
||
|
|
texture,
|
||
|
|
new Rect(0, 0, 1, 1),
|
||
|
|
new Vector2(0.5f, 0.5f),
|
||
|
|
100f // pixelsPerUnit
|
||
|
|
);
|
||
|
|
|
||
|
|
defaultSprite.name = "DefaultDropLineSprite";
|
||
|
|
|
||
|
|
Debug.Log($"[{nameof(DraggableScrollList)}] 기본 드롭 라인 스프라이트가 생성되었습니다.");
|
||
|
|
|
||
|
|
return defaultSprite;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 프리팹 로드
|
||
|
|
/// </summary>
|
||
|
|
private void LoadItemPrefab()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
itemPrefab = Resources.Load<GameObject>(itemPrefabPath);
|
||
|
|
if (itemPrefab == null)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹을 로드할 수 없습니다: {itemPrefabPath}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹 로드 중 오류 발생: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 목록에 새 아이템 추가
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="data">추가할 아이템 데이터</param>
|
||
|
|
public void AddItem(DraggableItemData data)
|
||
|
|
{
|
||
|
|
if (data == null)
|
||
|
|
{
|
||
|
|
Debug.LogWarning($"[{nameof(DraggableScrollList)}] null 데이터는 추가할 수 없습니다.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 추가
|
||
|
|
data.SortOrder = itemDataList.Count;
|
||
|
|
itemDataList.Add(data);
|
||
|
|
|
||
|
|
// UI 생성
|
||
|
|
CreateItemUI(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 여러 아이템을 한번에 추가 (성능 최적화)
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="dataList">추가할 아이템 데이터 목록</param>
|
||
|
|
public void AddItems(IEnumerable<DraggableItemData> dataList)
|
||
|
|
{
|
||
|
|
if (dataList == null) return;
|
||
|
|
|
||
|
|
var dataArray = dataList.ToArray();
|
||
|
|
if (dataArray.Length == 0) return;
|
||
|
|
|
||
|
|
// 레이아웃 업데이트 일시 중지 (성능 최적화)
|
||
|
|
if (layoutGroup != null)
|
||
|
|
layoutGroup.enabled = false;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
foreach (var data in dataArray)
|
||
|
|
{
|
||
|
|
if (data != null)
|
||
|
|
{
|
||
|
|
data.SortOrder = itemDataList.Count;
|
||
|
|
itemDataList.Add(data);
|
||
|
|
CreateItemUI(data);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
// 레이아웃 업데이트 재개
|
||
|
|
if (layoutGroup != null)
|
||
|
|
{
|
||
|
|
layoutGroup.enabled = true;
|
||
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(contentParent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 특정 아이템 제거
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="itemId">제거할 아이템의 ID</param>
|
||
|
|
/// <returns>제거 성공 여부</returns>
|
||
|
|
public bool RemoveItem(string itemId)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(itemId)) return false;
|
||
|
|
|
||
|
|
// 데이터에서 제거
|
||
|
|
var dataIndex = itemDataList.FindIndex(d => d.Id == itemId);
|
||
|
|
if (dataIndex == -1) return false;
|
||
|
|
|
||
|
|
itemDataList.RemoveAt(dataIndex);
|
||
|
|
|
||
|
|
// UI에서 제거
|
||
|
|
var uiIndex = itemUIList.FindIndex(ui => ui.Data?.Id == itemId);
|
||
|
|
if (uiIndex >= 0)
|
||
|
|
{
|
||
|
|
var itemUI = itemUIList[uiIndex];
|
||
|
|
itemUIList.RemoveAt(uiIndex);
|
||
|
|
|
||
|
|
// 이벤트 해제
|
||
|
|
UnsubscribeFromItemEvents(itemUI);
|
||
|
|
|
||
|
|
// GameObject 파괴
|
||
|
|
if (itemUI != null)
|
||
|
|
Destroy(itemUI.gameObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 순서 재정렬
|
||
|
|
UpdateSortOrders();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 모든 아이템 제거
|
||
|
|
/// </summary>
|
||
|
|
public void ClearItems()
|
||
|
|
{
|
||
|
|
// UI 제거
|
||
|
|
foreach (var itemUI in itemUIList)
|
||
|
|
{
|
||
|
|
if (itemUI != null)
|
||
|
|
{
|
||
|
|
UnsubscribeFromItemEvents(itemUI);
|
||
|
|
Destroy(itemUI.gameObject);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 목록 초기화
|
||
|
|
itemDataList.Clear();
|
||
|
|
itemUIList.Clear();
|
||
|
|
currentDraggingItem = null;
|
||
|
|
dragStartIndex = -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 UI 생성
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="data">아이템 데이터</param>
|
||
|
|
private void CreateItemUI(DraggableItemData data)
|
||
|
|
{
|
||
|
|
Debug.Log($"[{nameof(DraggableScrollList)}]1 itemPrefab == null:{itemPrefab == null}, contentParent == null:{contentParent == null}");
|
||
|
|
if (itemPrefab == null || contentParent == null) return;
|
||
|
|
Debug.Log($"[{nameof(DraggableScrollList)}]2 아이템 UI 생성: {data.Id}");
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 프리팹 인스턴스 생성
|
||
|
|
GameObject itemObject = Instantiate(itemPrefab, contentParent);
|
||
|
|
|
||
|
|
// DraggableListItem 컴포넌트 가져오기
|
||
|
|
DraggableListItem? itemUI = itemObject.GetComponentInChildren<DraggableListItem>();
|
||
|
|
if (itemUI == null)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹에 DraggableListItem 컴포넌트가 없습니다!");
|
||
|
|
Destroy(itemObject);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 데이터 설정
|
||
|
|
itemUI.SetData(data);
|
||
|
|
|
||
|
|
// 이벤트 연결
|
||
|
|
SubscribeToItemEvents(itemUI);
|
||
|
|
|
||
|
|
// UI 목록에 추가
|
||
|
|
itemUIList.Add(itemUI);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 아이템 UI 생성 중 오류: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 이벤트 구독
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="item">구독할 아이템</param>
|
||
|
|
private void SubscribeToItemEvents(DraggableListItem item)
|
||
|
|
{
|
||
|
|
if (item == null) return;
|
||
|
|
|
||
|
|
item.OnBeginDragEvent += OnItemBeginDrag;
|
||
|
|
item.OnDragEvent += OnItemDrag;
|
||
|
|
item.OnEndDragEvent += OnItemEndDrag;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 이벤트 구독 해제
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="item">구독 해제할 아이템</param>
|
||
|
|
private void UnsubscribeFromItemEvents(DraggableListItem item)
|
||
|
|
{
|
||
|
|
if (item == null) return;
|
||
|
|
|
||
|
|
item.OnBeginDragEvent -= OnItemBeginDrag;
|
||
|
|
item.OnDragEvent -= OnItemDrag;
|
||
|
|
item.OnEndDragEvent -= OnItemEndDrag;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 드래그 시작 이벤트 처리
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="item">드래그 시작된 아이템</param>
|
||
|
|
private void OnItemBeginDrag(DraggableListItem item)
|
||
|
|
{
|
||
|
|
if (item?.Data == null) return;
|
||
|
|
|
||
|
|
currentDraggingItem = item;
|
||
|
|
dragStartIndex = itemUIList.IndexOf(item);
|
||
|
|
|
||
|
|
// ScrollRect 드래그 비활성화 (아이템 드래그와 충돌 방지)
|
||
|
|
if (scrollRect != null)
|
||
|
|
scrollRect.enabled = false;
|
||
|
|
|
||
|
|
if (layoutGroup != null) layoutGroup.enabled = false;
|
||
|
|
|
||
|
|
// 드래그 중인 아이템을 최상위로 이동 (다른 아이템 위에 표시)
|
||
|
|
item.transform.SetAsLastSibling();
|
||
|
|
|
||
|
|
// 드롭 라인을 최상위로 이동 (드래그 아이템 다음)
|
||
|
|
if (dropLineObject != null)
|
||
|
|
dropLineObject.transform.SetAsLastSibling();
|
||
|
|
|
||
|
|
Debug.Log($"드래그 시작: {item.Data.Id} (인덱스: {dragStartIndex})");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 드래그 중 이벤트 처리
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="item">드래그 중인 아이템</param>
|
||
|
|
/// <param name="itemAnchoredPosition">item의 anchoredPosition</param>
|
||
|
|
private void OnItemDrag(DraggableListItem item, Vector2 itemAnchoredPosition)
|
||
|
|
{
|
||
|
|
if (item != currentDraggingItem || uiCamera == null) return;
|
||
|
|
|
||
|
|
// 자동 스크롤 처리
|
||
|
|
if (enableAutoScroll)
|
||
|
|
HandleAutoScroll(itemAnchoredPosition);
|
||
|
|
|
||
|
|
// 드롭 위치 계산 및 시각적 피드백
|
||
|
|
UpdateDropIndicator(itemAnchoredPosition);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 드래그 종료 이벤트 처리
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="item">드래그 종료된 아이템</param>
|
||
|
|
private void OnItemEndDrag(DraggableListItem item)
|
||
|
|
{
|
||
|
|
if (item != currentDraggingItem) return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// 드롭 위치 계산
|
||
|
|
int dropIndex = CalculateDropIndex();
|
||
|
|
|
||
|
|
// 순서 변경 처리
|
||
|
|
if (dropIndex != dragStartIndex && dropIndex >= 0)
|
||
|
|
{
|
||
|
|
ReorderItem(dragStartIndex, dropIndex);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 순서 변경이 없으면 원래 위치로 복원
|
||
|
|
item.ResetToOriginalPosition();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
// 드롭 라인 숨기기
|
||
|
|
HideDropLine();
|
||
|
|
|
||
|
|
// 상태 초기화
|
||
|
|
currentDraggingItem = null;
|
||
|
|
dragStartIndex = -1;
|
||
|
|
|
||
|
|
// ScrollRect 다시 활성화
|
||
|
|
if (scrollRect != null)
|
||
|
|
scrollRect.enabled = true;
|
||
|
|
|
||
|
|
if (layoutGroup != null) layoutGroup.enabled = true;
|
||
|
|
|
||
|
|
// 레이아웃 강제 업데이트
|
||
|
|
if (layoutGroup != null)
|
||
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(contentParent);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템 순서 변경
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="fromIndex">원래 위치</param>
|
||
|
|
/// <param name="toIndex">새로운 위치</param>
|
||
|
|
private void ReorderItem(int fromIndex, int toIndex)
|
||
|
|
{
|
||
|
|
if (fromIndex == toIndex || fromIndex < 0 || toIndex < 0 ||
|
||
|
|
fromIndex >= itemUIList.Count || toIndex >= itemUIList.Count)
|
||
|
|
return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
// UI 목록에서 순서 변경
|
||
|
|
var itemUI = itemUIList[fromIndex];
|
||
|
|
itemUIList.RemoveAt(fromIndex);
|
||
|
|
itemUIList.Insert(toIndex, itemUI);
|
||
|
|
|
||
|
|
// 데이터 목록에서 순서 변경
|
||
|
|
var itemData = itemDataList[fromIndex];
|
||
|
|
itemDataList.RemoveAt(fromIndex);
|
||
|
|
itemDataList.Insert(toIndex, itemData);
|
||
|
|
|
||
|
|
// Transform 계층 구조에서 순서 변경
|
||
|
|
itemUI.transform.SetSiblingIndex(toIndex);
|
||
|
|
|
||
|
|
// 정렬 순서 업데이트
|
||
|
|
UpdateSortOrders();
|
||
|
|
|
||
|
|
// 이벤트 발생
|
||
|
|
var eventArgs = new DraggableItemReorderEventArgs(itemData.Id, fromIndex, toIndex);
|
||
|
|
OnItemReordered?.Invoke(this, eventArgs);
|
||
|
|
|
||
|
|
Debug.Log($"아이템 순서 변경: {itemData.Id} ({fromIndex} -> {toIndex})");
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] 순서 변경 중 오류: {ex.Message}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 모든 아이템의 정렬 순서 업데이트
|
||
|
|
/// </summary>
|
||
|
|
private void UpdateSortOrders()
|
||
|
|
{
|
||
|
|
for (int i = 0; i < itemDataList.Count; i++)
|
||
|
|
{
|
||
|
|
itemDataList[i].SortOrder = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 자동 스크롤 처리
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="itemAnchoredPosition">item의 anchoredPosition</param>
|
||
|
|
private void HandleAutoScroll(Vector2 itemAnchoredPosition)
|
||
|
|
{
|
||
|
|
if (scrollRect?.viewport == null) return;
|
||
|
|
|
||
|
|
// 뷰포트 영역을 스크린 좌표로 변환
|
||
|
|
Vector3[] viewportCorners = new Vector3[4];
|
||
|
|
scrollRect.viewport.GetWorldCorners(viewportCorners);
|
||
|
|
|
||
|
|
// contentParent의 월드 좌표를 로컬로 변환
|
||
|
|
Vector2 viewportTopLeft = contentParent.InverseTransformPoint(viewportCorners[1]);
|
||
|
|
Vector2 viewportBottomLeft = contentParent.InverseTransformPoint(viewportCorners[0]);
|
||
|
|
|
||
|
|
float topY = viewportTopLeft.y;
|
||
|
|
float bottomY = viewportBottomLeft.y;
|
||
|
|
float scrollZone = 100f; // 스크롤 감지 영역 높이
|
||
|
|
|
||
|
|
// 상단 스크롤
|
||
|
|
if (itemAnchoredPosition.y > topY - scrollZone && itemAnchoredPosition.y < topY)
|
||
|
|
{
|
||
|
|
float scrollSpeed = (itemAnchoredPosition.y - (topY - scrollZone)) / scrollZone * scrollSensitivity;
|
||
|
|
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||
|
|
scrollRect.verticalNormalizedPosition + scrollSpeed * Time.deltaTime);
|
||
|
|
}
|
||
|
|
// 하단 스크롤
|
||
|
|
else if (itemAnchoredPosition.y < bottomY + scrollZone && itemAnchoredPosition.y > bottomY)
|
||
|
|
{
|
||
|
|
float scrollSpeed = ((bottomY + scrollZone) - itemAnchoredPosition.y) / scrollZone * scrollSensitivity;
|
||
|
|
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||
|
|
scrollRect.verticalNormalizedPosition - scrollSpeed * Time.deltaTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
//float topY = viewportCorners[1].y;
|
||
|
|
//float bottomY = viewportCorners[0].y;
|
||
|
|
//float scrollZone = 100f; // 스크롤 감지 영역 높이
|
||
|
|
|
||
|
|
//// 상단 스크롤
|
||
|
|
//if (screenPosition.y > topY - scrollZone && screenPosition.y < topY)
|
||
|
|
//{
|
||
|
|
// float scrollSpeed = (screenPosition.y - (topY - scrollZone)) / scrollZone * scrollSensitivity;
|
||
|
|
// scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||
|
|
// scrollRect.verticalNormalizedPosition + scrollSpeed * Time.deltaTime);
|
||
|
|
//}
|
||
|
|
//// 하단 스크롤
|
||
|
|
//else if (screenPosition.y < bottomY + scrollZone && screenPosition.y > bottomY)
|
||
|
|
//{
|
||
|
|
// float scrollSpeed = ((bottomY + scrollZone) - screenPosition.y) / scrollZone * scrollSensitivity;
|
||
|
|
// scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||
|
|
// scrollRect.verticalNormalizedPosition - scrollSpeed * Time.deltaTime);
|
||
|
|
//}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치 시각적 표시 업데이트
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="itemAnchoredPosition">드래그 되고 있는 item의 anchoredPosition</param>
|
||
|
|
protected virtual void UpdateDropIndicator(Vector2 itemAnchoredPosition)
|
||
|
|
{
|
||
|
|
if (currentDraggingItem?.RectTransform == null ||
|
||
|
|
contentParent == null ||
|
||
|
|
dropLineRectTransform == null ||
|
||
|
|
uiCamera == null)
|
||
|
|
{
|
||
|
|
HideDropLine();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
// 스크린 좌표를 로컬 좌표로 변환
|
||
|
|
//Vector2 localPoint;
|
||
|
|
//if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
|
|
// contentParent,
|
||
|
|
// eventData.position,
|
||
|
|
// uiCamera,
|
||
|
|
// out localPoint))
|
||
|
|
//{
|
||
|
|
// HideDropLine();
|
||
|
|
// return;
|
||
|
|
//}
|
||
|
|
|
||
|
|
// 드롭 인덱스 계산
|
||
|
|
int newDropIndex = CalculateDropIndexFromPosition(itemAnchoredPosition);
|
||
|
|
Debug.Log($"드롭 인덱스 계산: {newDropIndex}, {itemAnchoredPosition})");
|
||
|
|
if (newDropIndex != currentDropIndex)
|
||
|
|
{
|
||
|
|
currentDropIndex = newDropIndex;
|
||
|
|
}
|
||
|
|
ShowDropLineAtIndex(newDropIndex);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 로컬 좌표를 기준으로 드롭 인덱스 계산
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="localPoint">Content 내의 로컬 좌표</param>
|
||
|
|
/// <returns>드롭될 인덱스</returns>
|
||
|
|
private int CalculateDropIndexFromPosition(Vector2 localPoint)
|
||
|
|
{
|
||
|
|
if (itemUIList.Count == 0) return 0;
|
||
|
|
|
||
|
|
// 드래그 중인 아이템을 제외한 아이템들과 비교
|
||
|
|
float targetY = localPoint.y;
|
||
|
|
for (int i = 0; i < itemUIList.Count; i++)
|
||
|
|
{
|
||
|
|
var itemUI = itemUIList[i];
|
||
|
|
if (itemUI == currentDraggingItem || itemUI?.RectTransform == null)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
//float itemY = itemUI.RectTransform.anchoredPosition.y;
|
||
|
|
//float itemHeight = itemUI.RectTransform.rect.height;
|
||
|
|
// 아이템의 월드 좌표를 contentParent의 로컬 좌표로 변환
|
||
|
|
Vector3 itemWorldPos = itemUI.RectTransform.position;
|
||
|
|
Vector2 itemLocalPos = contentParent.InverseTransformPoint(itemWorldPos);
|
||
|
|
|
||
|
|
float itemY = itemLocalPos.y;
|
||
|
|
float itemHeight = itemUI.RectTransform.rect.height;
|
||
|
|
Debug.Log($"targetY:{localPoint}, 아이템 {i} 위치 Y 좌표: {itemY}, 높이: {itemHeight}, {itemY - itemHeight / 2}");
|
||
|
|
// 아이템의 중앙을 기준으로 위/아래 판단
|
||
|
|
if (targetY > itemY - itemHeight / 2)
|
||
|
|
{
|
||
|
|
return i; // 현재 아이템 위에 드롭
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 모든 아이템보다 아래에 있으면 마지막 위치
|
||
|
|
return itemUIList.Count;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 기존 드롭 위치 계산 (fallback용)
|
||
|
|
/// </summary>
|
||
|
|
/// <returns>드롭될 인덱스</returns>
|
||
|
|
private int CalculateDropIndex()
|
||
|
|
{
|
||
|
|
if (currentDraggingItem?.RectTransform == null || uiCamera == null)
|
||
|
|
return -1;
|
||
|
|
|
||
|
|
Vector2 localPoint;
|
||
|
|
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
|
|
contentParent,
|
||
|
|
Input.mousePosition,
|
||
|
|
uiCamera,
|
||
|
|
out localPoint))
|
||
|
|
{
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return CalculateDropIndexFromPosition(localPoint);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 지정된 인덱스 위치에 드롭 라인 표시
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="index">드롭 라인을 표시할 인덱스</param>
|
||
|
|
private void ShowDropLineAtIndex(int index)
|
||
|
|
{
|
||
|
|
if (dropLineRectTransform == null || contentParent == null) return;
|
||
|
|
|
||
|
|
Vector2 linePosition;
|
||
|
|
|
||
|
|
if (index >= itemUIList.Count)
|
||
|
|
{
|
||
|
|
// 마지막 위치에 드롭하는 경우
|
||
|
|
if (itemUIList.Count > 0)
|
||
|
|
{
|
||
|
|
var lastItem = itemUIList[itemUIList.Count - 1];
|
||
|
|
if (lastItem?.RectTransform != null)
|
||
|
|
{
|
||
|
|
//드래그 하는 아이템이 동일한 경우
|
||
|
|
if (dragStartIndex == itemUIList.Count - 1 && itemUIList.Count > 1)
|
||
|
|
{
|
||
|
|
lastItem = itemUIList[itemUIList.Count - 2];
|
||
|
|
}
|
||
|
|
float lastItemY = lastItem.RectTransform.anchoredPosition.y;
|
||
|
|
float lastItemHeight = lastItem.RectTransform.rect.height;
|
||
|
|
linePosition = new Vector2(0, lastItemY - lastItemHeight);// - dropLineMargin);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
linePosition = Vector2.zero;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
linePosition = Vector2.zero;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (index <= 0)
|
||
|
|
{
|
||
|
|
// 첫 번째 위치에 드롭하는 경우
|
||
|
|
if (itemUIList.Count > 0)
|
||
|
|
{
|
||
|
|
var firstItem = itemUIList[0];
|
||
|
|
if (firstItem?.RectTransform != null)
|
||
|
|
{
|
||
|
|
//드래그 하는 아이템이 동일한 경우
|
||
|
|
if (dragStartIndex == 0 && itemUIList.Count > 1)
|
||
|
|
{
|
||
|
|
firstItem = itemUIList[1];
|
||
|
|
}
|
||
|
|
float firstItemY = firstItem.RectTransform.anchoredPosition.y;
|
||
|
|
linePosition = new Vector2(0, firstItemY);// + dropLineMargin);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
linePosition = Vector2.zero;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
linePosition = Vector2.zero;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 중간 위치에 드롭하는 경우
|
||
|
|
var upperItem = itemUIList[index - 1];
|
||
|
|
var lowerItem = itemUIList[index];
|
||
|
|
|
||
|
|
if (upperItem?.RectTransform != null && lowerItem?.RectTransform != null)
|
||
|
|
{
|
||
|
|
|
||
|
|
float upperItemY = upperItem.RectTransform.anchoredPosition.y;
|
||
|
|
float lowerItemY = lowerItem.RectTransform.anchoredPosition.y;
|
||
|
|
float upperItemHeight = upperItem.RectTransform.rect.height;
|
||
|
|
|
||
|
|
//드래그 하는 아이템이 동일한 경우
|
||
|
|
if (dragStartIndex == index - 1)
|
||
|
|
{
|
||
|
|
upperItemY = lowerItemY - upperItemHeight - layoutGroup.spacing;
|
||
|
|
}else if (dragStartIndex == index)
|
||
|
|
{
|
||
|
|
lowerItemY = upperItemY - upperItemHeight - layoutGroup.spacing;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 두 아이템 사이의 중간 지점에 라인 표시
|
||
|
|
float middleY = (upperItemY - upperItemHeight + lowerItemY) / 2f;
|
||
|
|
linePosition = new Vector2(0, middleY);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
linePosition = Vector2.zero;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 라인 위치 설정
|
||
|
|
dropLineRectTransform.anchoredPosition = linePosition;
|
||
|
|
|
||
|
|
// 드롭 라인 표시
|
||
|
|
ShowDropLine();
|
||
|
|
|
||
|
|
Debug.Log($"드롭 라인 표시: 인덱스 {index}, 위치 {linePosition}");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 표시
|
||
|
|
/// </summary>
|
||
|
|
private void ShowDropLine()
|
||
|
|
{
|
||
|
|
if (dropLineObject == null) return;
|
||
|
|
|
||
|
|
if (!isDropLineVisible)
|
||
|
|
{
|
||
|
|
dropLineObject.SetActive(true);
|
||
|
|
isDropLineVisible = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 숨기기
|
||
|
|
/// </summary>
|
||
|
|
private void HideDropLine()
|
||
|
|
{
|
||
|
|
if (dropLineObject == null) return;
|
||
|
|
|
||
|
|
if (isDropLineVisible)
|
||
|
|
{
|
||
|
|
dropLineObject.SetActive(false);
|
||
|
|
isDropLineVisible = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 색상 설정
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="color">설정할 색상</param>
|
||
|
|
public void SetDropLineColor(Color color)
|
||
|
|
{
|
||
|
|
dropLineColor = color;
|
||
|
|
if (dropLineImage != null)
|
||
|
|
{
|
||
|
|
dropLineImage.color = color;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 높이 설정
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="height">설정할 높이</param>
|
||
|
|
public void SetDropLineHeight(float height)
|
||
|
|
{
|
||
|
|
dropLineHeight = height;
|
||
|
|
if (dropLineRectTransform != null)
|
||
|
|
{
|
||
|
|
dropLineRectTransform.sizeDelta = new Vector2(dropLineRectTransform.sizeDelta.x, height);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 스프라이트 변경
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="sprite">새로운 스프라이트 (null일 경우 기본 스프라이트 사용)</param>
|
||
|
|
public void SetDropLineSprite(Sprite? sprite)
|
||
|
|
{
|
||
|
|
dropLineSprite = sprite;
|
||
|
|
if (dropLineImage != null)
|
||
|
|
{
|
||
|
|
if (sprite != null)
|
||
|
|
{
|
||
|
|
dropLineImage.sprite = sprite;
|
||
|
|
dropLineImage.type = Image.Type.Sliced;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
dropLineImage.sprite = CreateDefaultDropLineSprite();
|
||
|
|
dropLineImage.type = Image.Type.Simple;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 Material 설정
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="material">적용할 Material</param>
|
||
|
|
public void SetDropLineMaterial(Material? material)
|
||
|
|
{
|
||
|
|
dropLineMaterial = material;
|
||
|
|
if (dropLineImage != null)
|
||
|
|
{
|
||
|
|
dropLineImage.material = material;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 라인 재생성 (설정 변경 후 적용 시 사용)
|
||
|
|
/// </summary>
|
||
|
|
public void RecreateDropLine()
|
||
|
|
{
|
||
|
|
// 기존 드롭 라인 제거
|
||
|
|
if (dropLineObject != null)
|
||
|
|
{
|
||
|
|
DestroyImmediate(dropLineObject);
|
||
|
|
dropLineObject = null;
|
||
|
|
dropLineImage = null;
|
||
|
|
dropLineRectTransform = null;
|
||
|
|
isDropLineVisible = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 새로운 드롭 라인 생성
|
||
|
|
CreateDropLine();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 특정 위치로 스크롤
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="itemId">스크롤할 아이템 ID</param>
|
||
|
|
public void ScrollToItem(string itemId)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(itemId) || scrollRect == null) return;
|
||
|
|
|
||
|
|
var itemUI = itemUIList.FirstOrDefault(ui => ui.Data?.Id == itemId);
|
||
|
|
if (itemUI?.RectTransform == null) return;
|
||
|
|
|
||
|
|
// 아이템 위치 계산하여 스크롤
|
||
|
|
Canvas.ForceUpdateCanvases();
|
||
|
|
|
||
|
|
var contentRect = scrollRect.content;
|
||
|
|
var itemRect = itemUI.RectTransform;
|
||
|
|
|
||
|
|
if (contentRect != null && itemRect != null)
|
||
|
|
{
|
||
|
|
var itemPosition = (Vector2)scrollRect.transform.InverseTransformPoint(itemRect.position);
|
||
|
|
var contentPosition = (Vector2)scrollRect.transform.InverseTransformPoint(contentRect.position);
|
||
|
|
|
||
|
|
var newY = itemPosition.y - contentPosition.y;
|
||
|
|
contentRect.anchoredPosition = new Vector2(contentRect.anchoredPosition.x, -newY);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 현재 목록 상태를 JSON으로 직렬화
|
||
|
|
/// </summary>
|
||
|
|
/// <returns>직렬화된 JSON 문자열</returns>
|
||
|
|
public virtual string SerializeToJson()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var serializableData = itemDataList.Select(data => new
|
||
|
|
{
|
||
|
|
id = data.Id,
|
||
|
|
sortOrder = data.SortOrder
|
||
|
|
// 주의: Sprite는 직렬화되지 않음 (필요시 별도 처리)
|
||
|
|
}).ToArray();
|
||
|
|
|
||
|
|
return JsonUtility.ToJson(new { items = serializableData }, true);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Debug.LogError($"[{nameof(DraggableScrollList)}] JSON 직렬화 오류: {ex.Message}");
|
||
|
|
return string.Empty;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 컴포넌트 정리
|
||
|
|
/// </summary>
|
||
|
|
private void OnDestroy()
|
||
|
|
{
|
||
|
|
// 동적으로 생성한 텍스처 정리 (메모리 누수 방지)
|
||
|
|
if (dropLineImage?.sprite?.texture != null &&
|
||
|
|
dropLineImage.sprite.name == "DefaultDropLineSprite")
|
||
|
|
{
|
||
|
|
DestroyImmediate(dropLineImage.sprite.texture);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|