#nullable enable using Gpm.Ui; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UVC.Util; namespace UVC.UI.List { public class PrefabGridItem : InfiniteScrollItem, IPointerEnterHandler, IPointerExitHandler, IBeginDragHandler, IDragHandler, IEndDragHandler { [SerializeField] private Image image; [SerializeField] private TextMeshProUGUI text; // Drag state private Canvas _canvas = default!; private Image? _dragGhost; private PrefabGridItemData? _boundData; private PrefabGrid? _prefabGrid; private void Awake() { // cache canvas for drag visuals if (_canvas == null) { _canvas = GetComponentInParent(); if (_canvas == null) { // 최상위 Canvas가 없다면 씬에서 하나를 찾아 사용 _canvas = FindFirstObjectByType(); } } // PrefabGrid 참조 캐싱 if (_prefabGrid == null) { _prefabGrid = GetComponentInParent(); } } /// /// InfiniteScroll에 의해 데이터가 이 아이템에 할당될 때 호출되는 핵심 메서드입니다. /// 스크롤 시 아이템이 재활용될 때마다 새로운 데이터로 이 메서드가 호출되어 UI를 갱신합니다. /// /// 이 아이템에 표시할 데이터입니다. `InfiniteScrollData`를 상속받은 `ComponentListItemData` 객체입니다. /// /// /// // 1. 표시할 데이터를 생성합니다. /// var categoryData = new ComponentListItemData /// { /// isCategory = true, /// categoryName = "전기 설비", /// categoryBadgeCount = 10 /// }; /// /// var generalData = new ComponentListItemData /// { /// isCategory = false, /// Name = "분전반", /// Option = "옵션 A", /// factoryObjectInfo = new FactoryObjectInfo { Id = "some-unique-Id", Name = "분전반" } /// }; /// /// // 2. 생성한 데이터를 InfiniteScroll에 추가합니다. /// // infiniteScroll은 InfiniteScroll 컴포넌트의 인스턴스입니다. /// infiniteScroll.InsertData(categoryData); /// infiniteScroll.InsertData(generalData); /// /// // 위 코드가 실행되면, InfiniteScroll은 각 데이터에 맞는 ComponentListItem을 생성(또는 재활용)하고, /// // 이 UpdateData 메서드를 호출하여 UI를 데이터에 맞게 설정합니다. /// /// public override async void UpdateData(InfiniteScrollData scrollData) { // 1. 부모 클래스의 UpdateData를 호출하여 기본 초기화 작업을 수행합니다. // 이 과정에서 this.scrollData에 매개변수로 받은 scrollData가 할당됩니다. base.UpdateData(scrollData); PrefabGridItemData data = (PrefabGridItemData)scrollData; text.text = data.ItemName; image.sprite = await ResourceManager.LoadSpriteAsync(data.ImagePrefabPath); _boundData = data; } public void OnClick() { if (_boundData == null || _prefabGrid == null) return; _prefabGrid.HandleItemClick(_boundData); } public void OnPointerEnter(PointerEventData eventData) { CursorManager.Instance.SetCursor(CursorType.HandPoint); } public void OnPointerExit(PointerEventData eventData) { CursorManager.Instance.SetCursor(CursorType.Default); } /// /// 드래그 시작 시 호출됩니다. 드래그 고스트 이미지를 생성하고 초기 위치를 설정합니다. /// /// 포인터 이벤트 데이터. public void OnBeginDrag(PointerEventData eventData) { if (image == null || image.sprite == null || _canvas == null) return; // PrefabGrid의 이벤트를 통해 드래그 시작 처리 if (_boundData != null && _prefabGrid != null) { _prefabGrid.HandleItemBeginDrag(_boundData, eventData.position); } // DragImageFollowCursor가 true일 때만 고스트 이미지 생성 if (_prefabGrid != null && _prefabGrid.DragImageFollowCursor) { // 고스트 생성 (이미지 스프라이트 복사) var go = new GameObject("DragGhost", typeof(RectTransform), typeof(CanvasGroup), typeof(Image)); go.transform.SetParent(_canvas.transform, false); go.transform.SetAsLastSibling(); var ghostImg = go.GetComponent(); ghostImg.sprite = image.sprite; ghostImg.preserveAspect = true; ghostImg.raycastTarget = false; // 드래그 중에는 이벤트 차단하지 않도록 ghostImg.color = new Color(1f, 1f, 1f, 0.6f); // 크기/위치 초기화 var srcRT = image.rectTransform; var dstRT = (RectTransform)go.transform; dstRT.sizeDelta = srcRT.rect.size; SetGhostPosition(eventData, dstRT); _dragGhost = ghostImg; } } /// /// 드래그 중에 호출됩니다. 고스트 이미지를 현재 포인터 위치로 이동합니다. /// /// 포인터 이벤트 데이터. public void OnDrag(PointerEventData eventData) { // PrefabGrid의 이벤트를 통해 드래그 중 처리 if (_boundData != null && _prefabGrid != null) { _prefabGrid.HandleItemDrag(_boundData, eventData.position); } if (_dragGhost == null || _canvas == null) return; SetGhostPosition(eventData, (RectTransform)_dragGhost.transform); } /// /// 드래그 종료 시 호출됩니다. 고스트를 제거하고 드랍 이벤트를 발생시킵니다. /// /// 포인터 이벤트 데이터. public void OnEndDrag(PointerEventData eventData) { // 고스트 제거 if (_dragGhost != null) { Destroy(_dragGhost.gameObject); _dragGhost = null; } // PrefabGrid의 이벤트를 통해 드롭 처리 if (_boundData != null && _prefabGrid != null) { _prefabGrid.HandleItemEndDrag(_boundData, eventData.position); _prefabGrid.HandleItemDrop(_boundData); } } /// /// 고스트 RectTransform을 현재 포인터 위치로 배치합니다. /// Canvas의 렌더 모드에 따라 화면/월드 좌표를 변환합니다. /// /// 포인터 이벤트 데이터. /// 이동시킬 고스트 RectTransform. private void SetGhostPosition(PointerEventData eventData, RectTransform ghostRt) { if (_canvas == null) return; if (_canvas.renderMode == RenderMode.ScreenSpaceOverlay) { ghostRt.position = eventData.position; } else { var cam = _canvas.worldCamera != null ? _canvas.worldCamera : eventData.pressEventCamera; if (RectTransformUtility.ScreenPointToWorldPointInRectangle(_canvas.transform as RectTransform, eventData.position, cam, out var worldPos)) { ghostRt.position = worldPos; } else { ghostRt.position = eventData.position; // fallback } } } /// /// 종료 시 등록된 콜백과 드래그 고스트를 정리합니다. /// private void OnDestroy() { // 드래그 고스트 정리 if (_dragGhost != null) { Destroy(_dragGhost.gameObject); _dragGhost = null; } // 참조 정리 _boundData = null; _prefabGrid = null; } } }