700 lines
24 KiB
C#
700 lines
24 KiB
C#
|
|
#nullable enable
|
||
|
|
using UnityEngine;
|
||
|
|
using UnityEngine.EventSystems;
|
||
|
|
using UnityEngine.UI;
|
||
|
|
|
||
|
|
namespace UVC.UI.List.Tree
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// TreeListItem의 드래그 & 드롭 입력을 처리하는 컴포넌트입니다.
|
||
|
|
///
|
||
|
|
/// 역할:
|
||
|
|
/// 1. 마우스 입력 감지 (클릭, 드래그)
|
||
|
|
/// 2. 드래그 시각 피드백 (알파값 변경, 오프셋 이동)
|
||
|
|
/// 3. 드롭 대상 판단 (마우스 위치 기반)
|
||
|
|
/// 4. 드래그 매니저에 이벤트 전달
|
||
|
|
///
|
||
|
|
/// 구조:
|
||
|
|
/// - PointerDown: 마우스 클릭 감지 → 드래그 시작 준비
|
||
|
|
/// - Drag: 마우스 이동 중 → 드래그 진행, 시각 피드백
|
||
|
|
/// - PointerUp: 마우스 해제 → 드롭 처리
|
||
|
|
/// </summary>
|
||
|
|
public class TreeListItemDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||
|
|
{
|
||
|
|
/// <summary>
|
||
|
|
/// 부모 TreeListItem입니다.
|
||
|
|
/// </summary>
|
||
|
|
private TreeListItem? treeListItem;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 부모 TreeList입니다.
|
||
|
|
/// </summary>
|
||
|
|
private TreeList? treeList;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 & 드롭 매니저입니다.
|
||
|
|
/// </summary>
|
||
|
|
private TreeListDragDropManager? dragDropManager;
|
||
|
|
|
||
|
|
private RectTransform? rectTransform;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 중 시각 피드백을 위한 CanvasGroup입니다.
|
||
|
|
/// </summary>
|
||
|
|
private CanvasGroup? canvasGroup;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 시작 시 원본 알파값입니다.
|
||
|
|
/// </summary>
|
||
|
|
private float originalAlpha = 1f;
|
||
|
|
|
||
|
|
private Vector2 dragOffset = Vector2.zero;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 중 적용할 알파값입니다.
|
||
|
|
/// </summary>
|
||
|
|
[SerializeField]
|
||
|
|
private float dragAlpha = 0.5f;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 활성화 여부입니다.
|
||
|
|
/// </summary>
|
||
|
|
[SerializeField]
|
||
|
|
private bool enableDragDrop = true;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치 표시 막대 프리팹입니다.
|
||
|
|
/// </summary>
|
||
|
|
private Image? dropIndicator;
|
||
|
|
private RectTransform? dropIndicatorRect;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 표시기의 부모 (Content 또는 EntryRoot)
|
||
|
|
/// </summary>
|
||
|
|
private RectTransform? dropIndicatorParent;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 시작 시 아이템의 원본 부모입니다.
|
||
|
|
/// 드래그 후 원래 위치로 복구할 때 사용합니다.
|
||
|
|
/// </summary>
|
||
|
|
private Transform? originalParent;
|
||
|
|
private int originalSiblingIndex;
|
||
|
|
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
rectTransform = GetComponent<RectTransform>();
|
||
|
|
// CanvasGroup 가져오기 (없으면 생성)
|
||
|
|
canvasGroup = GetComponent<CanvasGroup>();
|
||
|
|
if (canvasGroup == null)
|
||
|
|
{
|
||
|
|
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||
|
|
}
|
||
|
|
|
||
|
|
originalAlpha = canvasGroup.alpha;
|
||
|
|
}
|
||
|
|
|
||
|
|
public void SetDragDropManager(TreeListItem item, TreeList list, TreeListDragDropManager manager)
|
||
|
|
{
|
||
|
|
treeListItem = item;
|
||
|
|
treeList = list;
|
||
|
|
dragDropManager = manager;
|
||
|
|
Debug.Log($"[TreeListItemDragHandler] 드래그 핸들러 설정: {item.Data?.Name ?? "Unknown"}");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler)
|
||
|
|
/// OnPointerDown 이후 마우스가 약간 움직이면 자동으로 호출됩니다.
|
||
|
|
/// </summary>
|
||
|
|
public void OnBeginDrag(PointerEventData eventData)
|
||
|
|
{
|
||
|
|
Debug.Log($"[OnPointerDown]");
|
||
|
|
if (!enableDragDrop || treeListItem?.Data == null || dragDropManager == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 마우스 버튼이 왼쪽이 아니면 무시
|
||
|
|
if (eventData.button != PointerEventData.InputButton.Left)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드래그 시작 준비: 마우스 위치와 아이템 위치의 오프셋 계산
|
||
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
|
|
rectTransform,
|
||
|
|
eventData.position,
|
||
|
|
null,
|
||
|
|
out var localPoint);
|
||
|
|
|
||
|
|
dragOffset = localPoint;
|
||
|
|
|
||
|
|
Debug.Log($"[OnPointerDown] {treeListItem.Data.Name}에 포인터 다운, offset: {dragOffset}");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 중에 마우스가 이동할 때 호출됩니다. (IDragHandler)
|
||
|
|
/// </summary>
|
||
|
|
public void OnDrag(PointerEventData eventData)
|
||
|
|
{
|
||
|
|
if (!enableDragDrop || dragDropManager == null || treeListItem?.Data == null || treeList == null || rectTransform == null)
|
||
|
|
{
|
||
|
|
Debug.LogWarning("[OnDrag] 필수 컴포넌트 누락 또는 비활성화됨");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (eventData.button != PointerEventData.InputButton.Left)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 중");
|
||
|
|
|
||
|
|
// 드래그 시작 처리 (첫 드래그 프레임)
|
||
|
|
if (!dragDropManager.IsDragging)
|
||
|
|
{
|
||
|
|
dragDropManager.StartDrag(treeListItem.Data);
|
||
|
|
|
||
|
|
// 원본 부모와 위치 저장
|
||
|
|
originalParent = rectTransform.parent;
|
||
|
|
originalSiblingIndex = rectTransform.GetSiblingIndex();
|
||
|
|
|
||
|
|
// 드래그 중 시각 피드백
|
||
|
|
if (canvasGroup != null)
|
||
|
|
{
|
||
|
|
canvasGroup.alpha = dragAlpha;
|
||
|
|
canvasGroup.blocksRaycasts = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 위치 표시 막대 생성
|
||
|
|
CreateDropIndicator();
|
||
|
|
|
||
|
|
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 시작");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 아이템이 마우스를 따라다니도록 위치 업데이트
|
||
|
|
UpdateDragPosition(eventData);
|
||
|
|
|
||
|
|
// 마우스 위의 드롭 대상 찾기
|
||
|
|
var targetItem = GetItemAtMousePosition(eventData.position);
|
||
|
|
dragDropManager.OnDragOver(targetItem?.Data);
|
||
|
|
|
||
|
|
// 드롭 위치 표시 업데이트
|
||
|
|
UpdateDropIndicator(targetItem);
|
||
|
|
|
||
|
|
if (targetItem != null)
|
||
|
|
{
|
||
|
|
Debug.Log($"[OnDrag] 드래그 중: {targetItem.Data?.Name ?? "Unknown"} 위에 있음");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그가 종료될 때 호출됩니다. (IEndDragHandler)
|
||
|
|
/// 마우스 버튼을 놓으면 자동으로 호출됩니다.
|
||
|
|
/// </summary>
|
||
|
|
public void OnEndDrag(PointerEventData eventData)
|
||
|
|
{
|
||
|
|
Debug.Log($"[OnPointerUp]");
|
||
|
|
if (!enableDragDrop || dragDropManager == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
Debug.Log($"[OnPointerUp] 드래그 완료");
|
||
|
|
|
||
|
|
// 원본 알파값으로 복구
|
||
|
|
if (canvasGroup != null)
|
||
|
|
{
|
||
|
|
canvasGroup.alpha = originalAlpha;
|
||
|
|
canvasGroup.blocksRaycasts = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 원본 부모로 복구 (드래그 중 이동했던 위치 복구)
|
||
|
|
if (originalParent != null)
|
||
|
|
{
|
||
|
|
rectTransform?.SetParent(originalParent);
|
||
|
|
if (rectTransform != null)
|
||
|
|
{
|
||
|
|
rectTransform.SetSiblingIndex(originalSiblingIndex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 위치 표시 막대 숨김
|
||
|
|
HideDropIndicator();
|
||
|
|
|
||
|
|
// 드래그가 시작되지 않았으면 무시
|
||
|
|
if (!dragDropManager.IsDragging)
|
||
|
|
{
|
||
|
|
dragDropManager.EndDrag();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 마우스 위치의 대상 아이템 찾기
|
||
|
|
var targetItem = GetItemAtMousePosition(eventData.position);
|
||
|
|
|
||
|
|
|
||
|
|
// 드롭 시도
|
||
|
|
if (treeListItem?.Data != null)
|
||
|
|
{
|
||
|
|
var result = dragDropManager.TryDrop(targetItem?.Data);
|
||
|
|
Debug.Log($"[OnPointerUp] 드롭 결과: {(result ? "성공" : "실패")}");
|
||
|
|
|
||
|
|
if (result)
|
||
|
|
{
|
||
|
|
// 드롭 성공 → 데이터 동기화
|
||
|
|
HandleDropSuccess(treeListItem.Data, targetItem);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
dragDropManager.EndDrag();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드래그 중 아이템이 마우스를 따라다니도록 위치를 업데이트합니다.
|
||
|
|
/// Y축만 이동 (X축은 고정)
|
||
|
|
/// </summary>
|
||
|
|
private void UpdateDragPosition(PointerEventData eventData)
|
||
|
|
{
|
||
|
|
if (rectTransform == null || treeList == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var canvasRect = treeList.GetComponent<RectTransform>();
|
||
|
|
if (canvasRect == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 스크린 좌표를 캔버스 로컬 좌표로 변환
|
||
|
|
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
|
|
canvasRect,
|
||
|
|
eventData.position,
|
||
|
|
null,
|
||
|
|
out var canvasLocalPoint))
|
||
|
|
{
|
||
|
|
// ✅ Y축만 업데이트 (X는 고정)
|
||
|
|
// 드래그 오프셋을 고려한 Y 위치만 계산
|
||
|
|
var currentPosition = rectTransform.anchoredPosition;
|
||
|
|
rectTransform.anchoredPosition = new Vector2(
|
||
|
|
currentPosition.x, // ✅ X는 그대로 유지
|
||
|
|
canvasLocalPoint.y - dragOffset.y // ✅ Y만 마우스를 따라 이동
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치 표시 막대를 생성합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void CreateDropIndicator()
|
||
|
|
{
|
||
|
|
if (dropIndicator != null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ Root(root)를 부모로 설정
|
||
|
|
// 계층 구조: TreeList > ScrollView > Viewport > Content > Root > TreeListItem
|
||
|
|
dropIndicatorParent = treeList!.Root.parent as RectTransform;// rectTransform?.parent as RectTransform;
|
||
|
|
|
||
|
|
if (dropIndicatorParent == null)
|
||
|
|
{
|
||
|
|
Debug.LogError("[CreateDropIndicator] EntryRoot/Content를 찾을 수 없습니다");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 새로운 GameObject 생성
|
||
|
|
var indicatorGo = new GameObject("DropIndicator");
|
||
|
|
indicatorGo.transform.SetParent(dropIndicatorParent, false);
|
||
|
|
|
||
|
|
// Image 컴포넌트 추가
|
||
|
|
dropIndicator = indicatorGo.AddComponent<Image>();
|
||
|
|
dropIndicator.color = new Color(0.2f, 0.8f, 1f, 0.5f); // 반투명 파란색
|
||
|
|
|
||
|
|
// RectTransform 설정
|
||
|
|
dropIndicatorRect = indicatorGo.GetComponent<RectTransform>();
|
||
|
|
if (dropIndicatorRect != null)
|
||
|
|
{
|
||
|
|
dropIndicatorRect.anchorMin = new Vector2(0, 0.5f);
|
||
|
|
dropIndicatorRect.anchorMax = new Vector2(1, 0.5f);
|
||
|
|
dropIndicatorRect.pivot = new Vector2(0, 0.5f);
|
||
|
|
dropIndicatorRect.sizeDelta = new Vector2(0, 3f); // 높이 3
|
||
|
|
}
|
||
|
|
|
||
|
|
dropIndicator.raycastTarget = false;
|
||
|
|
indicatorGo.SetActive(false);
|
||
|
|
|
||
|
|
Debug.Log("[CreateDropIndicator] 드롭 위치 표시 막대 생성됨");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치 표시 막대를 업데이트합니다.
|
||
|
|
/// VerticalLayoutGroup 환경에서도 정확하게 위치를 계산합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void UpdateDropIndicator(TreeListItem? targetItem)
|
||
|
|
{
|
||
|
|
if (dropIndicator == null || dropIndicatorRect == null || dropIndicatorParent == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (targetItem == null)
|
||
|
|
{
|
||
|
|
dropIndicator.gameObject.SetActive(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 위치 판단
|
||
|
|
var targetRect = targetItem.GetComponent<RectTransform>();
|
||
|
|
if (targetRect == null)
|
||
|
|
{
|
||
|
|
dropIndicator.gameObject.SetActive(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var dropPosition = GetDropPosition(targetRect);
|
||
|
|
|
||
|
|
dropIndicator.gameObject.SetActive(true);
|
||
|
|
|
||
|
|
// ✅ 핵심: 월드 좌표로 변환해서 부모 기준 로컬 좌표 계산
|
||
|
|
Vector3[] targetCorners = new Vector3[4];
|
||
|
|
targetRect.GetWorldCorners(targetCorners);
|
||
|
|
|
||
|
|
// 아이템의 월드 Y 좌표
|
||
|
|
float targetWorldY = targetCorners[0].y;
|
||
|
|
|
||
|
|
// 부모(EntryRoot)의 월드 좌표
|
||
|
|
Vector3[] parentCorners = new Vector3[4];
|
||
|
|
dropIndicatorParent.GetWorldCorners(parentCorners);
|
||
|
|
float parentWorldY = parentCorners[0].y;
|
||
|
|
float parentHeight = parentCorners[1].y - parentCorners[0].y;
|
||
|
|
|
||
|
|
// 월드 Y를 부모 기준 로컬 Y로 변환
|
||
|
|
float relativeY = targetWorldY - parentWorldY;
|
||
|
|
|
||
|
|
// 부모의 pivot을 고려한 로컬 Y 계산
|
||
|
|
float pivotAdjustedY = relativeY - (parentHeight * dropIndicatorParent.pivot.y);
|
||
|
|
|
||
|
|
float indicatorY = 0;
|
||
|
|
|
||
|
|
switch (dropPosition)
|
||
|
|
{
|
||
|
|
case DropPosition.Above:
|
||
|
|
// 대상 아이템 위 (아이템 높이의 절반)
|
||
|
|
indicatorY = pivotAdjustedY + (targetRect.rect.height / 2);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DropPosition.Below:
|
||
|
|
// 대상 아이템 아래
|
||
|
|
indicatorY = pivotAdjustedY - (targetRect.rect.height / 2);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DropPosition.InsideAsChild:
|
||
|
|
// 대상 아이템 중앙
|
||
|
|
indicatorY = pivotAdjustedY;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ✅ DropIndicator 위치 설정
|
||
|
|
dropIndicatorRect.anchoredPosition = new Vector2(0, indicatorY);
|
||
|
|
|
||
|
|
Debug.Log($"[UpdateDropIndicator] 위치: {dropPosition}, targetY: {targetWorldY}, parentY: {parentWorldY}, indicatorY: {indicatorY}");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치 표시 막대를 숨깁니다.
|
||
|
|
/// </summary>
|
||
|
|
private void HideDropIndicator()
|
||
|
|
{
|
||
|
|
if (dropIndicator != null)
|
||
|
|
{
|
||
|
|
dropIndicator.gameObject.SetActive(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 주어진 스크린 좌표에 있는 TreeListItem을 찾습니다.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="screenPosition">스크린 좌표</param>
|
||
|
|
/// <returns>찾은 TreeListItem (없으면 null)</returns>
|
||
|
|
private TreeListItem? GetItemAtMousePosition(Vector2 screenPosition)
|
||
|
|
{
|
||
|
|
if (treeList == null)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Raycast로 UI 요소 찾기
|
||
|
|
var results = new System.Collections.Generic.List<RaycastResult>();
|
||
|
|
var eventData = new PointerEventData(EventSystem.current)
|
||
|
|
{
|
||
|
|
position = screenPosition
|
||
|
|
};
|
||
|
|
|
||
|
|
EventSystem.current.RaycastAll(eventData, results);
|
||
|
|
|
||
|
|
Debug.Log($"[GetItemAtMousePosition] Raycast 결과: {results.Count}개");
|
||
|
|
|
||
|
|
foreach (var result in results)
|
||
|
|
{
|
||
|
|
var item = result.gameObject.GetComponentInParent<TreeListItem>();
|
||
|
|
if (item != null && item != treeListItem)
|
||
|
|
{
|
||
|
|
Debug.Log($"[GetItemAtMousePosition] 찾은 아이템: {item.Data?.Name ?? "Unknown"}");
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 성공 후 데이터를 동기화합니다.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="draggedData">드래그된 아이템</param>
|
||
|
|
/// <param name="targetItem">드롭 대상 UI 아이템</param>
|
||
|
|
private void HandleDropSuccess(TreeListItemData? draggedData, TreeListItem? targetItem)
|
||
|
|
{
|
||
|
|
if (draggedData == null || treeList == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 대상이 없으면 (빈 공간에 드롭) 루트로 이동
|
||
|
|
if (targetItem == null)
|
||
|
|
{
|
||
|
|
Debug.Log($"[HandleDropSuccess] 루트로 이동");
|
||
|
|
MoveToRoot(draggedData);
|
||
|
|
treeList.UpdateFlattenedItemList();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var targetData = targetItem.Data;
|
||
|
|
if (targetData == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 드롭 위치 판단: 대상의 위/아래 또는 자식으로
|
||
|
|
var dropPosition = GetDropPosition(targetItem.GetComponent<RectTransform>());
|
||
|
|
|
||
|
|
Debug.Log($"[HandleDropSuccess] 드롭 위치: {dropPosition}");
|
||
|
|
|
||
|
|
switch (dropPosition)
|
||
|
|
{
|
||
|
|
case DropPosition.InsideAsChild:
|
||
|
|
// 대상의 자식으로 이동
|
||
|
|
MoveAsChild(draggedData, targetData);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DropPosition.Above:
|
||
|
|
// 대상 위에 위치 (같은 부모 내에서)
|
||
|
|
MoveBefore(draggedData, targetData);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case DropPosition.Below:
|
||
|
|
// 대상 아래에 위치 (같은 부모 내에서)
|
||
|
|
MoveAfter(draggedData, targetData);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
treeList.UpdateFlattenedItemList();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치를 판단합니다.
|
||
|
|
/// </summary>
|
||
|
|
/// <param name="targetRect">대상 아이템의 RectTransform</param>
|
||
|
|
/// <returns>드롭 위치</returns>
|
||
|
|
private DropPosition GetDropPosition(RectTransform targetRect)
|
||
|
|
{
|
||
|
|
// 목표: 마우스 Y 위치를 기반으로 위/아래/안쪽 판단
|
||
|
|
// 상위 1/3: 위
|
||
|
|
// 중간 1/3: 자식
|
||
|
|
// 하위 1/3: 아래
|
||
|
|
|
||
|
|
var height = targetRect.rect.height;
|
||
|
|
var thresholdUpper = height * 0.33f;
|
||
|
|
var thresholdLower = height * 0.67f;
|
||
|
|
|
||
|
|
// 월드 좌표에서 로컬 좌표로 변환
|
||
|
|
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||
|
|
targetRect,
|
||
|
|
Input.mousePosition,
|
||
|
|
null,
|
||
|
|
out var localMousePos);
|
||
|
|
|
||
|
|
// 로컬 Y 좌표로 판단 (RectTransform의 피벗이 중앙이면 -height/2 ~ height/2)
|
||
|
|
float relativeY = localMousePos.y;
|
||
|
|
|
||
|
|
if (relativeY > thresholdUpper)
|
||
|
|
{
|
||
|
|
return DropPosition.Above;
|
||
|
|
}
|
||
|
|
else if (relativeY < -thresholdLower)
|
||
|
|
{
|
||
|
|
return DropPosition.Below;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
return DropPosition.InsideAsChild;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템을 루트 레벨로 이동합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void MoveToRoot(TreeListItemData draggedData)
|
||
|
|
{
|
||
|
|
// 기존 부모에서 제거
|
||
|
|
RemoveFromParent(draggedData);
|
||
|
|
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트로 이동");
|
||
|
|
// 루트 레벨에 추가 (트리 구조 데이터에만 적용)
|
||
|
|
// UI 업데이트는 이벤트 구독자가 처리
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템을 다른 아이템의 자식으로 이동합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData)
|
||
|
|
{
|
||
|
|
RemoveFromParent(draggedData);
|
||
|
|
targetData.AddChild(draggedData);
|
||
|
|
Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템을 다른 아이템 앞으로 이동합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void MoveBefore(TreeListItemData draggedData, TreeListItemData targetData)
|
||
|
|
{
|
||
|
|
var parentData = FindParentOfItem(draggedData);
|
||
|
|
RemoveFromParent(draggedData);
|
||
|
|
|
||
|
|
if (parentData != null)
|
||
|
|
{
|
||
|
|
var targetIndex = parentData.Children.IndexOf(targetData);
|
||
|
|
if (targetIndex >= 0)
|
||
|
|
{
|
||
|
|
parentData.Children.Insert(targetIndex, draggedData);
|
||
|
|
parentData.NotifyDataChanged();
|
||
|
|
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 이동");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템을 다른 아이템 뒤로 이동합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void MoveAfter(TreeListItemData draggedData, TreeListItemData targetData)
|
||
|
|
{
|
||
|
|
var parentData = FindParentOfItem(draggedData);
|
||
|
|
RemoveFromParent(draggedData);
|
||
|
|
|
||
|
|
if (parentData != null)
|
||
|
|
{
|
||
|
|
var targetIndex = parentData.Children.IndexOf(targetData);
|
||
|
|
if (targetIndex >= 0)
|
||
|
|
{
|
||
|
|
parentData.Children.Insert(targetIndex + 1, draggedData);
|
||
|
|
parentData.NotifyDataChanged();
|
||
|
|
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 이동");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 아이템을 현재 부모에서 제거합니다.
|
||
|
|
/// </summary>
|
||
|
|
private void RemoveFromParent(TreeListItemData item)
|
||
|
|
{
|
||
|
|
var parent = FindParentOfItem(item);
|
||
|
|
if (parent != null)
|
||
|
|
{
|
||
|
|
parent.RemoveChild(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 주어진 아이템의 부모를 찾습니다.
|
||
|
|
/// </summary>
|
||
|
|
private TreeListItemData? FindParentOfItem(TreeListItemData item)
|
||
|
|
{
|
||
|
|
if (treeList == null)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 루트 아이템들 확인
|
||
|
|
foreach (Transform child in treeList.GetComponent<RectTransform>())
|
||
|
|
{
|
||
|
|
var childItem = child.GetComponent<TreeListItem>();
|
||
|
|
if (childItem?.Data == item)
|
||
|
|
{
|
||
|
|
return null; // 루트 레벨
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전체 트리 순회해서 부모 찾기
|
||
|
|
return FindParentRecursive(item, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
private TreeListItemData? FindParentRecursive(TreeListItemData target, TreeListItemData? currentParent)
|
||
|
|
{
|
||
|
|
if (currentParent != null)
|
||
|
|
{
|
||
|
|
foreach (var child in currentParent.Children)
|
||
|
|
{
|
||
|
|
if (child == target)
|
||
|
|
{
|
||
|
|
return currentParent;
|
||
|
|
}
|
||
|
|
|
||
|
|
var found = FindParentRecursive(target, child);
|
||
|
|
if (found != null)
|
||
|
|
{
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// 루트 레벨의 모든 아이템 확인
|
||
|
|
if (treeList != null)
|
||
|
|
{
|
||
|
|
var root = treeList.GetComponent<RectTransform>();
|
||
|
|
foreach (Transform child in root)
|
||
|
|
{
|
||
|
|
var childItem = child.GetComponent<TreeListItem>();
|
||
|
|
if (childItem?.Data != null)
|
||
|
|
{
|
||
|
|
var found = FindParentRecursive(target, childItem.Data);
|
||
|
|
if (found != null)
|
||
|
|
{
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 드롭 위치를 나타내는 열거형입니다.
|
||
|
|
/// </summary>
|
||
|
|
private enum DropPosition
|
||
|
|
{
|
||
|
|
/// <summary>위 (형제 아이템으로 앞쪽)</summary>
|
||
|
|
Above,
|
||
|
|
|
||
|
|
/// <summary>안쪽 (자식으로)</summary>
|
||
|
|
InsideAsChild,
|
||
|
|
|
||
|
|
/// <summary>아래 (형제 아이템으로 뒤쪽)</summary>
|
||
|
|
Below
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|