#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 { /// /// 드래그 가능한 ScrollRect 목록을 관리하는 메인 컨트롤러 /// Model-View 패턴을 적용하여 데이터와 UI를 분리 /// /// /// 사용 예제: /// 1. 이벤트 구독 /// 2. DraggableItemData 설정 /// /// 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)); /// } /// /// /// /// /// 아이템 순서 변경 이벤트 처리 /// /// /// /// 이벤트 발생자 /// /// 이벤트 인자 /// private void OnItemReordered(object? sender, DraggableItemReorderEventArgs e) /// { /// Debug.Log($"아이템 순서 변경됨: ID={e.ItemId}, {e.OldIndex} -> {e.NewIndex}"); /// /// // 여기에 순서 변경에 대한 비즈니스 로직 구현 /// // 예: 서버에 변경사항 전송, 설정 저장 등 /// } /// /// /// /// /// 아이템 선택 이벤트 처리 /// /// /// /// 이벤트 발생자 /// /// 선택된 아이템 /// private void OnItemSelected(object? sender, DraggableListItem item) /// { /// if (item?.Data != null) /// { /// Debug.Log($"아이템 선택됨: {item.Data.Id}"); /// /// // 선택된 아이템에 대한 처리 /// // 예: 상세 정보 표시, 편집 모드 진입 등 /// } /// } /// /// /// /// /// 컴포넌트 정리 /// /// /// private void OnDestroy() /// { /// if (draggableList != null) /// { /// draggableList.OnItemReordered -= OnItemReordered; /// draggableList.OnItemSelected -= OnItemSelected; /// } /// } /// } /// /// 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? OnItemReordered; public event EventHandler? OnItemSelected; // 데이터 및 UI 관리 private List itemDataList = new List(); private List itemUIList = new List(); 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; /// /// 현재 아이템 데이터 목록 (읽기 전용) /// public IReadOnlyList ItemDataList => itemDataList.AsReadOnly(); /// /// 컴포넌트 초기화 /// private void Awake() { InitializeComponents(); LoadItemPrefab(); } /// /// UI 카메라 참조 설정 /// private void Start() { // UI 카메라 찾기 (Canvas의 카메라 또는 메인 카메라) Canvas? canvas = GetComponentInParent(); uiCamera = canvas?.worldCamera ?? Camera.main; // 드롭 라인 생성 (Start에서 호출하여 모든 컴포넌트가 초기화된 후 실행) CreateDropLine(); } /// /// 필수 컴포넌트들 초기화 및 검증 /// private void InitializeComponents() { // ScrollRect 자동 할당 if (scrollRect == null) scrollRect = GetComponent(); // Content 부모 자동 할당 if (contentParent == null && scrollRect?.content != null) contentParent = scrollRect.content; // LayoutGroup 자동 할당 if (layoutGroup == null && contentParent != null) layoutGroup = contentParent.GetComponent(); // 필수 컴포넌트 검증 if (scrollRect == null) Debug.LogError($"[{nameof(DraggableScrollList)}] ScrollRect 컴포넌트를 찾을 수 없습니다!"); if (contentParent == null) Debug.LogError($"[{nameof(DraggableScrollList)}] Content 부모 Transform을 찾을 수 없습니다!"); } /// /// 드롭 라인을 동적으로 생성 /// 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(); // RectTransform 참조 가져오기 dropLineRectTransform = dropLineObject.GetComponent(); // 드롭 라인 설정 적용 ConfigureDropLine(); // 초기에는 비활성화 if (dropLineObject != null) dropLineObject.SetActive(false); Debug.Log($"[{nameof(DraggableScrollList)}] 드롭 라인이 동적으로 생성되었습니다."); } catch (Exception ex) { Debug.LogError($"[{nameof(DraggableScrollList)}] 드롭 라인 생성 중 오류 발생: {ex.Message}"); } } /// /// 드롭 라인의 시각적 속성 설정 /// 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(); } /// /// 드롭 라인의 RectTransform 설정 /// 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; } /// /// 기본 드롭 라인 스프라이트 생성 (Sprite가 없을 경우) /// /// 생성된 기본 스프라이트 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; } /// /// 아이템 프리팹 로드 /// private void LoadItemPrefab() { try { itemPrefab = Resources.Load(itemPrefabPath); if (itemPrefab == null) { Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹을 로드할 수 없습니다: {itemPrefabPath}"); } } catch (Exception ex) { Debug.LogError($"[{nameof(DraggableScrollList)}] 프리팹 로드 중 오류 발생: {ex.Message}"); } } /// /// 목록에 새 아이템 추가 /// /// 추가할 아이템 데이터 public void AddItem(DraggableItemData data) { if (data == null) { Debug.LogWarning($"[{nameof(DraggableScrollList)}] null 데이터는 추가할 수 없습니다."); return; } // 데이터 추가 data.SortOrder = itemDataList.Count; itemDataList.Add(data); // UI 생성 CreateItemUI(data); } /// /// 여러 아이템을 한번에 추가 (성능 최적화) /// /// 추가할 아이템 데이터 목록 public void AddItems(IEnumerable 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); } } } /// /// 특정 아이템 제거 /// /// 제거할 아이템의 ID /// 제거 성공 여부 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; } /// /// 모든 아이템 제거 /// 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; } /// /// 아이템 UI 생성 /// /// 아이템 데이터 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(); 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}"); } } /// /// 아이템 이벤트 구독 /// /// 구독할 아이템 private void SubscribeToItemEvents(DraggableListItem item) { if (item == null) return; item.OnBeginDragEvent += OnItemBeginDrag; item.OnDragEvent += OnItemDrag; item.OnEndDragEvent += OnItemEndDrag; } /// /// 아이템 이벤트 구독 해제 /// /// 구독 해제할 아이템 private void UnsubscribeFromItemEvents(DraggableListItem item) { if (item == null) return; item.OnBeginDragEvent -= OnItemBeginDrag; item.OnDragEvent -= OnItemDrag; item.OnEndDragEvent -= OnItemEndDrag; } /// /// 아이템 드래그 시작 이벤트 처리 /// /// 드래그 시작된 아이템 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})"); } /// /// 아이템 드래그 중 이벤트 처리 /// /// 드래그 중인 아이템 /// item의 anchoredPosition private void OnItemDrag(DraggableListItem item, Vector2 itemAnchoredPosition) { if (item != currentDraggingItem || uiCamera == null) return; // 자동 스크롤 처리 if (enableAutoScroll) HandleAutoScroll(itemAnchoredPosition); // 드롭 위치 계산 및 시각적 피드백 UpdateDropIndicator(itemAnchoredPosition); } /// /// 아이템 드래그 종료 이벤트 처리 /// /// 드래그 종료된 아이템 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); } } /// /// 아이템 순서 변경 /// /// 원래 위치 /// 새로운 위치 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}"); } } /// /// 모든 아이템의 정렬 순서 업데이트 /// private void UpdateSortOrders() { for (int i = 0; i < itemDataList.Count; i++) { itemDataList[i].SortOrder = i; } } /// /// 자동 스크롤 처리 /// /// item의 anchoredPosition 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); //} } /// /// 드롭 위치 시각적 표시 업데이트 /// /// 드래그 되고 있는 item의 anchoredPosition 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); } /// /// 로컬 좌표를 기준으로 드롭 인덱스 계산 /// /// Content 내의 로컬 좌표 /// 드롭될 인덱스 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; } /// /// 기존 드롭 위치 계산 (fallback용) /// /// 드롭될 인덱스 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); } /// /// 지정된 인덱스 위치에 드롭 라인 표시 /// /// 드롭 라인을 표시할 인덱스 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}"); } /// /// 드롭 라인 표시 /// private void ShowDropLine() { if (dropLineObject == null) return; if (!isDropLineVisible) { dropLineObject.SetActive(true); isDropLineVisible = true; } } /// /// 드롭 라인 숨기기 /// private void HideDropLine() { if (dropLineObject == null) return; if (isDropLineVisible) { dropLineObject.SetActive(false); isDropLineVisible = false; } } /// /// 드롭 라인 색상 설정 /// /// 설정할 색상 public void SetDropLineColor(Color color) { dropLineColor = color; if (dropLineImage != null) { dropLineImage.color = color; } } /// /// 드롭 라인 높이 설정 /// /// 설정할 높이 public void SetDropLineHeight(float height) { dropLineHeight = height; if (dropLineRectTransform != null) { dropLineRectTransform.sizeDelta = new Vector2(dropLineRectTransform.sizeDelta.x, height); } } /// /// 드롭 라인 스프라이트 변경 /// /// 새로운 스프라이트 (null일 경우 기본 스프라이트 사용) 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; } } } /// /// 드롭 라인 Material 설정 /// /// 적용할 Material public void SetDropLineMaterial(Material? material) { dropLineMaterial = material; if (dropLineImage != null) { dropLineImage.material = material; } } /// /// 드롭 라인 재생성 (설정 변경 후 적용 시 사용) /// public void RecreateDropLine() { // 기존 드롭 라인 제거 if (dropLineObject != null) { DestroyImmediate(dropLineObject); dropLineObject = null; dropLineImage = null; dropLineRectTransform = null; isDropLineVisible = false; } // 새로운 드롭 라인 생성 CreateDropLine(); } /// /// 특정 위치로 스크롤 /// /// 스크롤할 아이템 ID 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); } } /// /// 현재 목록 상태를 JSON으로 직렬화 /// /// 직렬화된 JSON 문자열 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; } } /// /// 컴포넌트 정리 /// private void OnDestroy() { // 동적으로 생성한 텍스처 정리 (메모리 누수 방지) if (dropLineImage?.sprite?.texture != null && dropLineImage.sprite.name == "DefaultDropLineSprite") { DestroyImmediate(dropLineImage.sprite.texture); } } } }