사용자 정보 모달, 드래그 리스트 개발

This commit is contained in:
logonkhi
2025-07-31 18:31:51 +09:00
parent f7befb048c
commit 23da311db0
64 changed files with 5990 additions and 4576 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 521099f40c2c22441b4506477f820698
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,162 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 드래그 동작만을 처리하는 컴포넌트입니다.
/// UI 요소를 드래그할 수 있게 만들어주는 기본 기능을 제공합니다.
///
/// 사용 예시:
/// 1. 드래그하고 싶은 UI GameObject에 이 컴포넌트를 추가합니다.
/// 2. 드래그 이벤트를 받고 싶다면 아래와 같이 구독합니다:
///
/// void Start()
/// {
/// var dragBehavior = GetComponent<DragBehavior>();
/// dragBehavior.OnDragStarted += (eventData) => Debug.Log("드래그 시작!");
/// dragBehavior.OnDragging += (eventData) => Debug.Log("드래그 중...");
/// dragBehavior.OnDragEnded += (eventData) => Debug.Log("드래그 끝!");
/// }
/// </summary>
public class DragBehavior : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Tooltip("드래그 대상 RectTransform. 이 RectTransform이 드래그 중에 따라다닙니다.")]
[SerializeField]
private RectTransform dragTarget;
[SerializeField, Tooltip("수직 드래그 사용 여부")]
private bool verticalDrag = true;
[SerializeField, Tooltip("수평 드래그 사용 여부")]
private bool horizontalDrag = false;
/// <summary>
/// 드래그가 시작될 때 발생하는 이벤트
/// </summary>
public Action<PointerEventData>? OnDragStarted;
/// <summary>
/// 드래그 중일 때 계속 발생하는 이벤트
/// </summary>
public Action<PointerEventData>? OnDragging;
/// <summary>
/// 드래그가 끝날 때 발생하는 이벤트
/// </summary>
public Action<PointerEventData>? OnDragEnded;
// 드래그 시작 전의 원래 위치를 저장
private Vector2 originalPosition;
// 드래그 시작 전의 원래 부모 Transform을 저장
private Transform? originalParent;
// 드래그 시작 전의 원래 순서(인덱스)를 저장
private int originalIndex;
/// <summary>
/// 원래 순서를 가져옵니다
/// </summary>
public int OriginalIndex => originalIndex;
/// <summary>
/// 원래 부모 Transform을 가져옵니다
/// </summary>
public Transform? OriginalParent => originalParent;
void Awake()
{
// dragTarget이 설정되지 않은 경우 현재 GameObject의 RectTransform을 사용
if (dragTarget == null)
{
dragTarget = GetComponent<RectTransform>();
}
if (dragTarget == null)
{
Debug.LogError($"[DragBehavior] 드래그 대상 RectTransform이 설정되지 않았습니다! - {gameObject.name}");
}
}
/// <summary>
/// Unity가 드래그 시작을 감지했을 때 자동으로 호출됩니다
/// </summary>
public void OnBeginDrag(PointerEventData eventData)
{
// 현재 상태를 저장합니다
originalPosition = dragTarget.position;
originalParent = dragTarget.parent;
originalIndex = dragTarget.GetSiblingIndex();
// 구독자들에게 드래그 시작을 알립니다
OnDragStarted?.Invoke(eventData);
// 디버그 로그 (개발 중 확인용)
Debug.Log($"[DragBehavior] 드래그 시작 - 아이템: {dragTarget.name}, 원래 인덱스: {originalIndex}");
}
/// <summary>
/// Unity가 드래그 중임을 감지했을 때 매 프레임 호출됩니다
/// </summary>
public void OnDrag(PointerEventData eventData)
{
// 오브젝트를 마우스 위치로 이동시킵니다
if (verticalDrag)
dragTarget.anchoredPosition += new Vector2(0, eventData.delta.y);
else if (horizontalDrag)
dragTarget.anchoredPosition += new Vector2(eventData.delta.x, 0);
else
dragTarget.anchoredPosition += eventData.delta;
// 구독자들에게 드래그 중임을 알립니다
OnDragging?.Invoke(eventData);
}
/// <summary>
/// Unity가 드래그 종료를 감지했을 때 자동으로 호출됩니다
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
// 구독자들에게 드래그 종료를 알립니다
OnDragEnded?.Invoke(eventData);
Debug.Log($"[DragBehavior] 드래그 종료 - 아이템: {dragTarget.name}");
}
/// <summary>
/// 드래그 종료 후 새로운 위치 정보를 업데이트합니다
/// </summary>
public void UpdatePositionInfo()
{
originalParent = dragTarget.parent;
originalIndex = dragTarget.GetSiblingIndex();
originalPosition = dragTarget.position;
Debug.Log($"[DragBehavior] 위치 정보 업데이트 - 새 인덱스: {originalIndex}");
}
/// <summary>
/// 오브젝트를 원래 위치로 되돌립니다
///
/// 사용 예시:
/// if (드래그가_유효하지_않음)
/// {
/// dragBehavior.ResetPosition();
/// }
/// </summary>
public void ResetPosition()
{
if (originalParent != null)
{
dragTarget.SetParent(originalParent);
dragTarget.SetSiblingIndex(originalIndex);
dragTarget.position = originalPosition;
Debug.Log($"[DragBehavior] 위치 리셋 - 아이템: {dragTarget.name}");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 261f68435056bc44fbadb799304989f6

View File

@@ -0,0 +1,413 @@
#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);
// 이벤트 발생
OnOrderChanged?.Invoke((item as ListItemData)!, oldIndex, newIndex);
// 자동 저장
//if (autoSave)
//{
// SaveOrder();
//}
Debug.Log($"[DraggableList] 순서 변경: {oldIndex} -> {newIndex}");
}
}
/// <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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b8727d0aa73f3da458d5425f3097273f

View File

@@ -0,0 +1,210 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 리스트 아이템의 드래그와 비주얼을 관리하는 컴포넌트입니다.
/// DragBehavior와 함께 사용되어 드래그 시 시각적 효과를 처리합니다.
///
/// 필수 설정:
/// 1. 이 컴포넌트가 있는 GameObject에 또는 자식에 DragBehavior를 함께 추가해야 합니다.
/// 2. CanvasGroup이 없으면 자동으로 추가됩니다.
///
/// Inspector 설정:
/// - Drag Alpha: 드래그 중 투명도 (0~1, 기본값 0.6)
///
/// 사용 예시:
/// GameObject listItem = Instantiate(itemPrefab);
/// listItem.AddComponent<DragBehavior>();
/// listItem.AddComponent<ListItemController>();
/// </summary>
public class ListItemController : MonoBehaviour
{
// 컴포넌트 참조들
private CanvasGroup? canvasGroup;
private ListReorderHandler? reorderHandler;
private GameObject? placeholder;
[Header("DragBehavior 설정")]
[SerializeField]
private DragBehavior? dragBehavior;
[Header("비주얼 설정")]
[SerializeField, Range(0f, 1f)]
[Tooltip("드래그 중일 때의 투명도 값입니다. 0은 완전 투명, 1은 완전 불투명")]
private float dragAlpha = 0.6f;
/// <summary>
/// 현재 생성된 플레이스홀더를 가져옵니다
/// </summary>
public GameObject? Placeholder => placeholder;
/// <summary>
/// 원래 순서(인덱스)를 가져옵니다
/// </summary>
public int OriginalIndex => dragBehavior?.OriginalIndex ?? -1;
/// <summary>
/// 컴포넌트 초기화 - Unity가 자동으로 호출
/// </summary>
void Awake()
{
// 필수 컴포넌트들을 가져옵니다
if(dragBehavior == null)
dragBehavior = GetComponentInChildren<DragBehavior>();
// CanvasGroup이 없으면 추가합니다 (알파값 조절을 위해 필요)
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
Debug.Log($"[ListItemController] CanvasGroup을 자동으로 추가했습니다 - {gameObject.name}");
}
// 드래그 이벤트에 핸들러 메서드들을 연결합니다
if (dragBehavior != null)
{
dragBehavior.OnDragStarted += HandleDragStart;
dragBehavior.OnDragging += HandleDragging;
dragBehavior.OnDragEnded += HandleDragEnd;
}
else
{
Debug.LogError($"[ListItemController] DragBehavior를 찾을 수 없습니다! - {gameObject.name}");
}
}
/// <summary>
/// Start는 Awake 이후에 호출되며, 다른 오브젝트의 컴포넌트를 찾을 때 사용
/// </summary>
void Start()
{
// 부모에서 ListReorderHandler를 찾습니다
reorderHandler = GetComponentInParent<ListReorderHandler>();
if (reorderHandler == null)
{
Debug.LogWarning($"[ListItemController] ListReorderHandler를 찾을 수 없습니다. 재정렬 기능이 작동하지 않을 수 있습니다. - {gameObject.name}");
}
}
/// <summary>
/// 드래그가 시작될 때 호출되는 핸들러
/// </summary>
private void HandleDragStart(PointerEventData eventData)
{
Debug.Log($"[ListItemController] 드래그 시작 처리 - {gameObject.name}");
// 1. 플레이스홀더(빈 공간 표시)를 생성합니다
CreatePlaceholder();
// 2. 드래그 중인 아이템을 반투명하게 만듭니다
if (canvasGroup != null)
{
canvasGroup.alpha = dragAlpha;
canvasGroup.blocksRaycasts = false; // 드래그 중에는 클릭을 받지 않습니다
}
// 3. 아이템을 최상위 Canvas로 이동시켜 다른 UI 위에 표시되도록 합니다
Canvas? rootCanvas = GetComponentInParent<Canvas>();
if (rootCanvas != null)
{
transform.SetParent(rootCanvas.transform);
transform.SetAsLastSibling(); // 가장 위에 표시
}
// 4. 리오더 핸들러에 드래그 시작을 알립니다
reorderHandler?.StartReorder(this);
}
/// <summary>
/// 드래그 중일 때 계속 호출되는 핸들러
/// </summary>
private void HandleDragging(PointerEventData eventData)
{
// 리오더 핸들러가 현재 마우스 위치에서 적절한 드롭 위치를 계산하도록 합니다
reorderHandler?.UpdateReorderPosition(this, eventData.position);
}
/// <summary>
/// 드래그가 끝날 때 호출되는 핸들러
/// </summary>
private void HandleDragEnd(PointerEventData eventData)
{
Debug.Log($"[ListItemController] 드래그 종료 처리 - {gameObject.name}");
// 1. 아이템을 원래 투명도로 복원합니다
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.blocksRaycasts = true; // 다시 클릭을 받을 수 있도록
}
// 2. 리오더 핸들러가 최종 위치를 결정하도록 합니다
reorderHandler?.EndReorder(this);
// 3. 플레이스홀더를 제거합니다
if (placeholder != null)
{
Destroy(placeholder);
placeholder = null;
}
// 중요: 드래그 종료 후 새로운 위치 정보를 업데이트
dragBehavior?.UpdatePositionInfo();
}
/// <summary>
/// 플레이스홀더(빈 공간 표시용 오브젝트)를 생성합니다
/// 드래그 중인 아이템이 원래 있던 자리를 표시하는 용도입니다
/// </summary>
private void CreatePlaceholder()
{
// 이미 플레이스홀더가 있다면 생성하지 않습니다
if (placeholder != null) return;
// 새 GameObject를 만들어 플레이스홀더로 사용합니다
placeholder = new GameObject("Placeholder");
// RectTransform 컴포넌트를 추가하고 설정합니다
var rect = placeholder.AddComponent<RectTransform>();
if (dragBehavior?.OriginalParent != null)
{
rect.SetParent(dragBehavior.OriginalParent);
rect.SetSiblingIndex(dragBehavior.OriginalIndex);
// 원본과 같은 크기로 설정합니다
var originalRect = GetComponent<RectTransform>();
if (originalRect != null)
{
rect.sizeDelta = originalRect.sizeDelta;
}
}
// 시각적으로 구분할 수 있도록 반투명한 회색 이미지를 추가합니다
var image = placeholder.AddComponent<Image>();
image.color = new Color(0.5f, 0.5f, 0.5f, 0.3f);
Debug.Log($"[ListItemController] 플레이스홀더 생성됨 - {gameObject.name}");
}
/// <summary>
/// 컴포넌트가 파괴될 때 이벤트 구독을 해제합니다
/// </summary>
void OnDestroy()
{
// 메모리 누수를 방지하기 위해 이벤트 구독을 해제합니다
if (dragBehavior != null)
{
dragBehavior.OnDragStarted -= HandleDragStart;
dragBehavior.OnDragging -= HandleDragging;
dragBehavior.OnDragEnded -= HandleDragEnd;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 98e103c1fce908c4baee1f553081f2f1

View File

@@ -0,0 +1,44 @@
#nullable enable
using UnityEngine;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 리스트 아이템 데이터의 기본 클래스 입니다.
/// 모든 리스트 아이템 데이터는 이 클래스를 상속해야 합니다
///
/// 구현 예시:
/// public class MyItemData : ListItemData
/// {
/// public string Title;
/// public MyItemData(string title, string id, string displayName, Sprite? icon = null) : base(id, displayName, icon)
/// {
/// Title = title;
/// }
/// }
/// </summary>
public class ListItemData
{
/// <summary>
/// 아이템의 고유 식별자
/// </summary>
public string Id;
/// <summary>
/// UI에 표시될 이름
/// </summary>
public string DisplayName;
/// <summary>
/// 아이템의 아이콘 스프라이트
/// </summary>
public Sprite? Icon;
public ListItemData(string id, string displayName, Sprite? icon = null)
{
Id = id;
DisplayName = displayName;
Icon = icon;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 909d02b315354e748b0ed02efa4133c6

View File

@@ -0,0 +1,137 @@
#nullable enable
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using static RTG.GizmoTransform;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// UI 요소와 데이터를 연결하는 컴포넌트입니다.
/// 데이터 변경 시 UI를 자동으로 업데이트합니다.
///
/// 사용 방법:
/// 1. 리스트 아이템 프리팹에 이 컴포넌트를 추가합니다.
/// 2. Inspector에서 Text와 Image 컴포넌트를 연결합니다.
/// 3. BindData 메서드로 데이터를 설정합니다.
///
/// 예시:
/// var binder = listItem.GetComponent<ListItemView>();
/// binder.BindData(new MyItemData { DisplayName = "아이템 1" });
///
/// // 나중에 데이터 가져오기
/// var data = binder.GetData<MyItemData>();
/// </summary>
public class ListItemView : MonoBehaviour
{
[Header("UI 기본 요소 연결")]
[SerializeField]
[Tooltip("아이템의 제목을 표시할 TextMeshProUGUI 컴포넌트")]
protected TextMeshProUGUI? titleText;
[SerializeField]
[Tooltip("아이템의 아이콘을 표시할 Image 컴포넌트 (선택사항)")]
protected Image? iconImage;
// 현재 바인딩된 데이터를 저장
protected ListItemData? boundData;
public Action<ListItemData>? OnChangeData;
/// <summary>
/// 데이터를 UI에 바인딩합니다
/// </summary>
/// <param name="data">바인딩할 데이터 객체</param>
public virtual void BindData(ListItemData data)
{
boundData = data;
UpdateUI();
Debug.Log($"[ListItemView] 데이터 바인딩 완료 - {gameObject.name}");
}
/// <summary>
/// 바인딩된 데이터를 기반으로 UI를 업데이트합니다
/// </summary>
protected virtual void UpdateUI()
{
// IListItemData 인터페이스를 구현한 경우
if (boundData != null)
{
// 제목 텍스트 업데이트
if (titleText != null)
{
titleText.text = boundData.DisplayName;
}
// IHasIcon 인터페이스도 구현한 경우 아이콘 업데이트
if (iconImage != null && boundData.Icon != null)
{
iconImage.sprite = boundData.Icon;
iconImage.gameObject.SetActive(true);
}
else if (iconImage != null)
{
// 아이콘이 없으면 숨깁니다
iconImage.gameObject.SetActive(false);
}
}
}
/// <summary>
/// 현재 바인딩된 데이터를 특정 타입으로 가져옵니다
/// </summary>
/// <typeparam name="T">가져올 데이터의 타입</typeparam>
/// <returns>바인딩된 데이터, 타입이 맞지 않거나 없으면 null</returns>
public T? GetData<T>() where T : ListItemData
{
return boundData as T;
}
/// <summary>
/// 바인딩된 데이터가 있는지 확인합니다
/// </summary>
public bool HasData => boundData != null;
/// <summary>
/// UI를 수동으로 새로고침합니다
/// </summary>
public void RefreshUI()
{
if (boundData != null)
{
UpdateUI();
}
}
/// <summary>
/// 바인딩을 해제하고 UI를 초기화합니다
/// </summary>
public virtual void ClearBinding()
{
boundData = null;
if (titleText != null)
{
titleText.text = "";
}
if (iconImage != null)
{
iconImage.sprite = null;
iconImage.gameObject.SetActive(false);
}
//Debug.Log($"[ListItemView] 바인딩 해제됨 - {gameObject.name}");
}
protected virtual void OnDestroy()
{
// 바인딩 해제 시 추가 작업이 필요하면 여기에 작성
ClearBinding();
OnChangeData = null;
//Debug.Log($"[ListItemView] 컴포넌트 파괴됨 - {gameObject.name}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 056b568ab6ec8dc47b22943eea32fe61

View File

@@ -0,0 +1,181 @@
#nullable enable
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extension;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 리스트 아이템들의 순서 변경을 관리하는 컴포넌트입니다.
/// 드래그 중인 아이템의 위치를 계산하고 순서를 재정렬합니다.
///
/// 사용 방법:
/// 1. ScrollView의 Content GameObject에 이 컴포넌트를 추가합니다.
/// 2. OnOrderChanged 이벤트를 구독하여 순서 변경을 감지합니다.
///
/// 예시:
/// void Start()
/// {
/// var reorderHandler = content.GetComponent<ListReorderHandler>();
/// reorderHandler.OnOrderChanged += (oldIndex, newIndex) =>
/// {
/// Debug.Log($"아이템이 {oldIndex}에서 {newIndex}로 이동했습니다");
/// };
/// }
/// </summary>
public class ListReorderHandler : MonoBehaviour
{
/// <summary>
/// 아이템 순서가 변경될 때 발생하는 이벤트
/// 첫 번째 매개변수: 이전 인덱스, 두 번째 매개변수: 새 인덱스
/// </summary>
public Action<int, int>? OnOrderChanged;
// 현재 드래그 중인 아이템을 추적합니다
private ListItemController? currentDragging;
// 자동 스크롤을 관리하는 핸들러
private ScrollRectHandler? scrollHandler;
/// <summary>
/// 컴포넌트 초기화
/// </summary>
void Awake()
{
// 부모 GameObject에 있는 ScrollRectHandler를 찾습니다
scrollHandler = GetComponentInParent<ScrollRectHandler>();
}
/// <summary>
/// 드래그가 시작될 때 호출됩니다
/// </summary>
/// <param name="item">드래그를 시작한 아이템</param>
public void StartReorder(ListItemController item)
{
currentDragging = item;
// 자동 스크롤을 활성화합니다 (있는 경우)
scrollHandler?.EnableAutoScroll(true);
Debug.Log($"[ListReorderHandler] 재정렬 시작 - {item.gameObject.name}");
}
/// <summary>
/// 드래그 중일 때 호출되어 플레이스홀더의 위치를 업데이트합니다
/// </summary>
/// <param name="item">드래그 중인 아이템</param>
/// <param name="position">현재 마우스/터치 위치 (스크린 좌표)</param>
public void UpdateReorderPosition(ListItemController item, Vector3 position)
{
// 플레이스홀더가 없으면 처리하지 않습니다
if (item.Placeholder == null) return;
// 마우스 위치에서 가장 가까운 인덱스를 계산합니다
int targetIndex = CalculateTargetIndex(position);
Debug.Log($"[ListReorderHandler] 드래그 위치 업데이트 - 타겟 인덱스: {targetIndex}");
if (targetIndex >= 0)
{
// 현재 플레이스홀더의 인덱스와 다를 때만 이동
int currentPlaceholderIndex = item.Placeholder.transform.GetSiblingIndex();
if (targetIndex != currentPlaceholderIndex)
{
item.Placeholder.transform.SetSiblingIndex(targetIndex);
// 레이아웃 즉시 업데이트
LayoutRebuilder.ForceRebuildLayoutImmediate(transform as RectTransform);
}
}
}
/// <summary>
/// 드래그가 끝날 때 호출되어 최종 위치를 확정합니다
/// </summary>
/// <param name="item">드래그가 끝난 아이템</param>
public void EndReorder(ListItemController item)
{
// 자동 스크롤을 비활성화합니다
scrollHandler?.EnableAutoScroll(false);
if (item.Placeholder != null)
{
// 플레이스홀더의 현재 위치가 최종 위치입니다
int newIndex = item.Placeholder.transform.GetSiblingIndex();
// 아이템을 원래 부모로 되돌리고 새 위치에 배치합니다
item.transform.SetParent(transform);
item.transform.SetSiblingIndex(newIndex);
// Layout 재정렬을 위해 강제 업데이트
LayoutRebuilder.ForceRebuildLayoutImmediate(transform as RectTransform);
// 순서가 실제로 변경되었다면 이벤트를 발생시킵니다
if (item.OriginalIndex != newIndex)
{
OnOrderChanged?.Invoke(item.OriginalIndex, newIndex);
Debug.Log($"[ListReorderHandler] 순서 변경됨: {item.OriginalIndex} -> {newIndex}");
}
}
else
{
// 플레이스홀더가 없는 경우 원래 위치로 복귀
item.transform.SetParent(transform);
}
currentDragging = null;
}
/// <summary>
/// 주어진 스크린 좌표에서 가장 가까운 아이템의 인덱스를 계산합니다
/// </summary>
/// <param name="worldPosition">스크린 좌표</param>
/// <returns>가장 가까운 인덱스, 찾지 못하면 -1</returns>
private int CalculateTargetIndex(Vector3 worldPosition)
{
// 스크린 좌표를 로컬 좌표로 변환합니다
RectTransformUtility.ScreenPointToLocalPointInRectangle(
transform as RectTransform,
worldPosition,
null, // 오버레이 캔버스의 경우 카메라가 null
out Vector2 localPos
);
float closestDistance = float.MaxValue;
int closestIndex = -1;
// 모든 아이템과의 거리를 계산하여 가장 가까운 것을 찾습니다
var children = transform.GetChildren<RectTransform>();
for (int i = 0; i < children.Length; i++)
{
// Y 좌표 차이만 계산합니다 (세로 리스트 기준)
// 가로 리스트의 경우 localPos.x - children[i].anchoredPosition.x 를 사용하세요
float distance = Mathf.Abs(localPos.y - (children[i].anchoredPosition.y + children[i].rect.height / 2));
if (distance < closestDistance)
{
closestDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
/// <summary>
/// 특정 인덱스의 아이템을 가져옵니다
/// </summary>
/// <param name="index">가져올 아이템의 인덱스</param>
/// <returns>해당 인덱스의 Transform, 없으면 null</returns>
public Transform? GetItemAtIndex(int index)
{
if (index >= 0 && index < transform.childCount)
{
return transform.GetChild(index);
}
return null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 156b4800a1dadfb4c821381a5d6220d0

View File

@@ -0,0 +1,172 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace UVC.UI.List.Draggable
{
/// <summary>
/// 드래그 중 화면 가장자리에서 자동으로 스크롤하는 기능을 제공합니다.
/// ScrollRect와 함께 사용되어 긴 리스트에서도 드래그 앤 드롭이 가능하게 합니다.
///
/// 필수 설정:
/// 1. ScrollRect 컴포넌트가 있는 GameObject에 추가합니다.
///
/// Inspector 설정:
/// - Scroll Zone Height: 자동 스크롤이 시작되는 영역의 높이 (픽셀)
/// - Scroll Speed: 스크롤 속도
///
/// 사용 예시:
/// scrollHandler.EnableAutoScroll(true); // 자동 스크롤 시작
/// scrollHandler.EnableAutoScroll(false); // 자동 스크롤 중지
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class ScrollRectHandler : MonoBehaviour
{
[Header("자동 스크롤 설정")]
[SerializeField]
[Tooltip("화면 가장자리에서 이 거리(픽셀) 안에 마우스가 있으면 자동 스크롤이 시작됩니다")]
private float scrollZoneHeight = 50f;
[SerializeField]
[Tooltip("자동 스크롤 속도 (픽셀/초)")]
private float scrollSpeed = 600f;
// ScrollRect 컴포넌트 참조
private ScrollRect? scrollRect;
// 자동 스크롤 코루틴 참조
private Coroutine? autoScrollCoroutine;
/// <summary>
/// 컴포넌트 초기화
/// </summary>
void Awake()
{
scrollRect = GetComponent<ScrollRect>();
if (scrollRect == null)
{
Debug.LogError($"[ScrollRectHandler] ScrollRect 컴포넌트를 찾을 수 없습니다! - {gameObject.name}");
}
}
/// <summary>
/// 자동 스크롤을 활성화하거나 비활성화합니다
/// </summary>
/// <param name="enable">true: 활성화, false: 비활성화</param>
public void EnableAutoScroll(bool enable)
{
if (scrollRect == null) return;
if (enable)
{
// 드래그 중에는 ScrollRect의 기본 드래그를 비활성화합니다
scrollRect.enabled = false;
// 자동 스크롤 코루틴을 시작합니다
if (autoScrollCoroutine == null)
{
autoScrollCoroutine = StartCoroutine(AutoScrollRoutine());
Debug.Log("[ScrollRectHandler] 자동 스크롤 시작");
}
}
else
{
// ScrollRect를 다시 활성화합니다
scrollRect.enabled = true;
// 자동 스크롤 코루틴을 중지합니다
if (autoScrollCoroutine != null)
{
StopCoroutine(autoScrollCoroutine);
autoScrollCoroutine = null;
Debug.Log("[ScrollRectHandler] 자동 스크롤 중지");
}
}
}
/// <summary>
/// 자동 스크롤을 처리하는 코루틴
/// 마우스가 화면 가장자리에 있을 때 스크롤을 자동으로 움직입니다
/// </summary>
private IEnumerator AutoScrollRoutine()
{
if (scrollRect == null) yield break;
var scrollTransform = scrollRect.GetComponent<RectTransform>();
if (scrollTransform == null) yield break;
// 스크롤 영역의 모서리 좌표를 저장할 배열
Vector3[] corners = new Vector3[4];
while (true)
{
// ScrollRect의 월드 좌표 모서리를 가져옵니다
// corners[0]: 왼쪽 아래, corners[1]: 왼쪽 위, corners[2]: 오른쪽 위, corners[3]: 오른쪽 아래
scrollTransform.GetWorldCorners(corners);
float mouseY = Input.mousePosition.y;
float topY = corners[1].y; // 위쪽 가장자리 Y 좌표
float bottomY = corners[0].y; // 아래쪽 가장자리 Y 좌표
float scrollDelta = 0f;
// 마우스가 위쪽 가장자리 근처에 있으면 위로 스크롤
if (mouseY > topY - scrollZoneHeight && mouseY <= topY)
{
// 가장자리에 가까울수록 빠르게 스크롤
float normalizedDistance = (topY - mouseY) / scrollZoneHeight;
scrollDelta = scrollSpeed * (1f - normalizedDistance) * Time.deltaTime;
Debug.Log($"[ScrollRectHandler] 위로 스크롤 중... 속도: {scrollDelta}");
}
// 마우스가 아래쪽 가장자리 근처에 있으면 아래로 스크롤
else if (mouseY < bottomY + scrollZoneHeight && mouseY >= bottomY)
{
// 가장자리에 가까울수록 빠르게 스크롤
float normalizedDistance = (mouseY - bottomY) / scrollZoneHeight;
scrollDelta = -scrollSpeed * (1f - normalizedDistance) * Time.deltaTime;
Debug.Log($"[ScrollRectHandler] 아래로 스크롤 중... 속도: {scrollDelta}");
}
// 스크롤 적용
if (Mathf.Abs(scrollDelta) > 0.01f)
{
// 스크롤 델타를 정규화된 값으로 변환 (0~1 범위)
float normalized = scrollDelta / scrollRect.content.rect.height;
// 새로운 스크롤 위치 계산 (0~1 범위로 제한)
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
scrollRect.verticalNormalizedPosition + normalized
);
}
// 다음 프레임까지 대기
yield return null;
}
}
/// <summary>
/// 현재 자동 스크롤이 활성화되어 있는지 확인합니다
/// </summary>
/// <returns>자동 스크롤이 활성화되어 있으면 true</returns>
public bool IsAutoScrolling()
{
return autoScrollCoroutine != null;
}
/// <summary>
/// 컴포넌트가 비활성화될 때 자동 스크롤을 중지합니다
/// </summary>
void OnDisable()
{
EnableAutoScroll(false);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3349b75020336074d9c7d2b5bf6a7796

View File

@@ -1,53 +0,0 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.UI.List
{
/// <summary>
/// 개별 드래그 가능한 아이템의 UI 컴포넌트
/// 드래그 동작과 시각적 피드백을 담당
/// </summary>
public class DraggableItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
// 이벤트
public Action<PointerEventData>? OnBeginDragEvent;
public Action<PointerEventData>? OnDragEvent;
public Action<PointerEventData>? OnEndDragEvent;
/// <summary>
/// 드래그 시작 시 호출
/// </summary>
public void OnBeginDrag(PointerEventData eventData)
{
OnBeginDragEvent?.Invoke(eventData);
}
/// <summary>
/// 드래그 중 호출
/// </summary>
public void OnDrag(PointerEventData eventData)
{
// 이벤트 발생
OnDragEvent?.Invoke(eventData);
}
/// <summary>
/// 드래그 종료 시 호출
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
// 이벤트 발생
OnEndDragEvent?.Invoke(eventData);
}
void OnDestroy()
{
// 이벤트 구독 해제
OnBeginDragEvent = null;
OnDragEvent = null;
OnEndDragEvent = null;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2d93b757e3738184492e84c051530130

View File

@@ -1,42 +0,0 @@
#nullable enable
using System;
using UnityEngine;
namespace UVC.UI.List
{
/// <summary>
/// 드래그 가능한 목록 아이템의 데이터 모델
/// </summary>
[Serializable]
public class DraggableItemData
{
[SerializeField] protected string id;
[SerializeField] protected int sortOrder;
public string Id => id;
public int SortOrder { get => sortOrder; set => sortOrder = value; }
public DraggableItemData(string id, int sortOrder = 0)
{
this.id = id ?? throw new ArgumentNullException(nameof(id));
this.sortOrder = sortOrder;
}
}
/// <summary>
/// 드래그 작업의 결과를 나타내는 이벤트 인자
/// </summary>
public class DraggableItemReorderEventArgs : EventArgs
{
public string ItemId { get; }
public int OldIndex { get; }
public int NewIndex { get; }
public DraggableItemReorderEventArgs(string itemId, int oldIndex, int newIndex)
{
ItemId = itemId;
OldIndex = oldIndex;
NewIndex = newIndex;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2a576b71abcd5ff41a1c3d0adf21a45c

View File

@@ -1,191 +0,0 @@
#nullable enable
using System;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.UI.List
{
/// <summary>
/// 개별 드래그 가능한 아이템의 UI 컴포넌트
/// 드래그 동작과 시각적 피드백을 담당
/// </summary>
public class DraggableListItem : MonoBehaviour
{
[Header("UI 컴포넌트")]
[SerializeField] protected CanvasGroup? canvasGroup;
[SerializeField] protected RectTransform? rectTransform;
[SerializeField] protected DraggableItem? dragAnchor;
[SerializeField] protected TMP_InputField? inputField;
[Header("드래그 설정")]
[SerializeField] protected float dragAlpha = 0.6f;
[SerializeField] protected bool blockRaycastsWhileDragging = false;
// 프로퍼티
public DraggableItemData? Data { get; private set; }
public RectTransform? RectTransform => rectTransform;
public bool IsDragging { get; private set; }
public event Action<DraggableListItem>? OnBeginDragEvent;
public event Action<DraggableListItem, Vector2>? OnDragEvent;
public event Action<DraggableListItem>? OnEndDragEvent;
private Vector2 originalPosition;
private Transform? originalParent;
private int originalSiblingIndex;
/// <summary>
/// 컴포넌트 초기화
/// </summary>
private void Awake()
{
// null 체크 및 자동 할당
if (rectTransform == null)
rectTransform = GetComponent<RectTransform>();
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
// CanvasGroup이 없으면 추가
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
if (dragAnchor == null)
{
Debug.LogError("Drag Anchor is not assigned. Please assign it in the inspector.");
return;
}
dragAnchor.OnBeginDragEvent += OnBeginDrag;
dragAnchor.OnDragEvent += OnDrag;
dragAnchor.OnEndDragEvent += OnEndDrag;
}
/// <summary>
/// 아이템 데이터로 UI 업데이트
/// </summary>
/// <param name="data">표시할 데이터</param>
public void SetData(DraggableItemData? data)
{
if (data == null) return;
Data = data;
UpdateUI();
}
/// <summary>
/// UI 요소들을 데이터에 맞게 업데이트
/// </summary>
protected virtual void UpdateUI()
{
if (Data == null) return;
if(inputField != null)
{
inputField.text = Data.Id;
}
}
/// <summary>
/// 드래그 시작 시 호출
/// </summary>
public void OnBeginDrag(PointerEventData eventData)
{
if (rectTransform == null) return;
IsDragging = true;
// 원래 위치와 부모 저장
originalPosition = rectTransform.anchoredPosition;
originalParent = transform.parent;
originalSiblingIndex = transform.GetSiblingIndex();
// 시각적 피드백 적용
ApplyDragVisuals(true);
// 이벤트 발생
OnBeginDragEvent?.Invoke(this);
}
/// <summary>
/// 드래그 중 호출
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (rectTransform == null) return;
// 마우스 위치로 아이템 이동
rectTransform.anchoredPosition += new Vector2(0, eventData.delta.y);//eventData.delta
// 이벤트 발생
OnDragEvent?.Invoke(this, rectTransform.anchoredPosition);
}
/// <summary>
/// 드래그 종료 시 호출
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
IsDragging = false;
// 시각적 피드백 복원
ApplyDragVisuals(false);
// 이벤트 발생
OnEndDragEvent?.Invoke(this);
}
/// <summary>
/// 드래그 시각적 효과 적용/해제
/// </summary>
/// <param name="isDragging">드래그 중인지 여부</param>
private void ApplyDragVisuals(bool isDragging)
{
if (canvasGroup == null) return;
if (isDragging)
{
canvasGroup.alpha = dragAlpha;
canvasGroup.blocksRaycasts = blockRaycastsWhileDragging;
}
else
{
canvasGroup.alpha = 1f;
canvasGroup.blocksRaycasts = true;
}
}
/// <summary>
/// 원래 위치로 되돌리기 (드래그 취소 시 사용)
/// </summary>
public void ResetToOriginalPosition()
{
if (rectTransform == null || originalParent == null) return;
transform.SetParent(originalParent);
transform.SetSiblingIndex(originalSiblingIndex);
rectTransform.anchoredPosition = originalPosition;
}
public void OnDestroy()
{
// 이벤트 구독 해제
if (dragAnchor != null)
{
dragAnchor.OnBeginDragEvent -= OnBeginDrag;
dragAnchor.OnDragEvent -= OnDrag;
dragAnchor.OnEndDragEvent -= OnEndDrag;
}
OnBeginDragEvent = null;
OnDragEvent = null;
OnEndDragEvent = null;
// 리소스 정리
canvasGroup = null;
rectTransform = null;
dragAnchor = null;
}
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 7cdfe032ad5874e4cbc571f344516b93

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 71e6121c6103b0a4c9aeadc24c891b86

View File

@@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UVC.Data.Core;
using UVC.Factory.Component;
using UVC.Factory.Modal.Config;
using UVC.Factory.Playback;
using UVC.Locale;
using UVC.Log;
@@ -241,16 +242,10 @@ namespace UVC.UI.Menu
ULog.Debug($"사용자가 네트워크 오류 알림을 확인했나요? {result}");
}))
}));
model.MenuItems.Add(new MenuItemData("language", "menu_language", subMenuItems: new List<MenuItemData>
model.MenuItems.Add(new MenuItemData("Settings", "Settings", subMenuItems: new List<MenuItemData>
{
// 각 언어 메뉴 아이템에 ChangeLanguageCommand를 연결하여 언어 변경 기능 수행
new MenuItemData("lang_ko", "menu_lang_korean", new ChangeLanguageCommand("ko"), commandParameter: "ko"),
new MenuItemData("lang_en", "menu_lang_english", new ChangeLanguageCommand("en")),
new MenuItemData("file_save", "menu_file_save", command: new DebugLogCommand("저장 선택됨 (Command 실행)") , subMenuItems: new List<MenuItemData>
{
new MenuItemData("file_save_as", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command 실행)"))
}),
// 필요에 따라 다른 언어들도 추가 가능
new MenuItemData("info", "info", new ConfigDataOrderCommand()),
}));
}

View File

@@ -372,12 +372,12 @@ namespace UVC.UI.Modal
}
else // bool이 아닌 다른 타입의 경우 ModalView.GetResult() 사용
{
object resultFromView = null;
object? resultFromView = null;
// modalViewToClose는 파괴되었을 수도 있으므로, modalInstanceToDestroy에서 다시 가져오거나 null 체크 강화
ModalView viewForGetResult = modalInstanceToDestroy?.GetComponent<ModalView>() ?? modalViewToClose;
if (viewForGetResult != null)
{
resultFromView = viewForGetResult.GetResult();
resultFromView = await viewForGetResult.GetResult();
}
// 📜 이야기: 이제 가져온 결과를 '약속 증서'에 적어야 하는데, 타입이 맞는지 잘 확인해야 해요.

View File

@@ -1,6 +1,5 @@
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UVC.Locale;
using UVC.Log; // LocalizationManager 사용을 위해 추가
namespace UVC.UI.Modal
{
@@ -124,7 +123,7 @@ namespace UVC.UI.Modal
PrefabPath = prefabPath;
// Title, Message 등 다른 텍스트 속성도 필요한 경우 여기서 기본 다국어 키를 사용하여 초기화할 수 있습니다.
// 예: Title = LocalizationManager.Instance.GetString("default_modal_title");
}
}
/// <summary>
/// 🚀 모달 창이 화면에 나타나기 *직전*에 이 레시피가 할 일을 정해요. (비동기 작업 가능)

View File

@@ -71,32 +71,41 @@ namespace UVC.UI.Modal
/// 🏷️ 모달 창의 제목을 보여줄 글상자(TextMeshProUGUI)예요.
/// Unity 에디터의 인스펙터 창에서 실제 UI 요소를 끌어다 연결해줘야 해요.
/// </summary>
[SerializeField]
public TextMeshProUGUI titleText;
/// <summary>
/// 💬 모달 창의 주요 메시지를 보여줄 글상자예요. 이것도 연결해주세요!
/// </summary>
[SerializeField]
public TextMeshProUGUI messageText;
/// <summary>
/// ✅ '확인' 버튼이에요. 연결 필수!
/// </summary>
[SerializeField]
public Button confirmButton;
/// <summary>
/// 확인 버튼 안에 있는 글상자예요. 확인 버튼 글자를 바꿀 때 사용돼요.
/// </summary>
[SerializeField]
public TextMeshProUGUI confirmButtonText;
/// <summary>
/// ❌ '취소' 버튼이에요. 이것도 연결해주세요!
/// </summary>
[SerializeField]
public Button cancelButton;
/// <summary>
/// 취소 버튼 안에 있는 글상자예요. 취소 버튼 글자를 바꿀 때 사용돼요.
/// </summary>
[SerializeField]
public TextMeshProUGUI cancelButtonText;
/// <summary>
/// ✖️ 모달 창을 닫는 (보통 오른쪽 위에 있는 X 모양) 버튼이에요.
/// </summary>
[SerializeField]
public Button closeButton; // 닫기 버튼
protected bool autoPositionButtons = true; // 버튼 위치 자동 조정 여부 (기본값: true)
// 필요에 따라 다른 UI 요소들을 추가할 수 있습니다.
// 예: public Image backgroundImage;
// 예: public InputField inputField;
@@ -149,7 +158,7 @@ namespace UVC.UI.Modal
// 버튼 위치를 예쁘게 조정해요 (예: 버튼이 하나만 있으면 가운데로).
AdjustButtonPositions();
if(autoPositionButtons) AdjustButtonPositions();
await UniTask.CompletedTask; // 비동기 메서드라서 마지막에 이걸 붙여줘요.
}
@@ -253,7 +262,7 @@ namespace UVC.UI.Modal
/// public TMP_InputField inputField;
/// // (OnOpen에서 inputField 초기화 및 이벤트 연결)
///
/// public override object GetResult()
/// public override UniTask<object?> GetResult()
/// {
/// return inputField != null ? inputField.text : string.Empty;
/// }
@@ -261,9 +270,9 @@ namespace UVC.UI.Modal
/// // Modal.Open<string>(...) 이렇게 호출하면, 입력된 문자열을 받을 수 있어요.
/// </code>
/// </example>
public virtual object? GetResult()
public virtual UniTask<object?> GetResult()
{
return null;
return UniTask.FromResult<object?>(null);
}
/// <summary>