#nullable enable using Cysharp.Threading.Tasks; using Gpm.Ui; using System; using System.Collections.Generic; using System.Linq; using TMPro; using UnityEngine; using UVC.Extention; using UVC.Locale; using UVC.UI.Modal; using UVC.UI.Tab; namespace UVC.UI.List.ComponentList { /// /// 컴포넌트 목록 UI를 관리하는 클래스입니다. /// InfiniteScroll을 사용하여 대량의 데이터를 효율적으로 표시하고, /// 검색 및 필터링 기능을 제공합니다. /// public class ComponentList : MonoBehaviour, ITabContent { [Tooltip("데이터를 표시할 InfiniteScroll 컴포넌트입니다.")] [SerializeField] protected InfiniteScroll? scrollList = null; [Tooltip("검색어 입력을 위한 TMP_InputField 컴포넌트입니다.")] [SerializeField] protected TMP_InputField? inputField = null; // InfiniteScroll에 표시될 원본 데이터 리스트입니다. protected List? data; protected List? filteredData; #region 이벤트 (Events) /// /// 아이템 클릭 이벤트. /// public Action? OnClickItem; /// /// 아이템 마우스 오른쪽 클릭 이벤트. /// public Action? OnRightClickItem; /// /// 카테고리 확장/축소 이벤트. /// public Action? OnCategoryExpand; /// /// 설정 버튼 클릭 이벤트. /// public Action? OnSettingButtonClick; /// /// 보이기 버튼 클릭 이벤트. /// public Action? OnShowButtonClick; /// /// 숨기기 버튼 클릭 이벤트. /// public Action? OnHideButtonClick; /// /// 검색/이동 버튼 클릭 이벤트. /// public Action? OnSearchButtonClick; #endregion /// /// SingletonScene 초기화 과정에서 호출됩니다. /// 필요한 컴포넌트를 찾고, 이벤트 리스너를 등록합니다. /// protected void Start() { // scrollList가 인스펙터에서 할당되지 않았을 경우, 자식에서 찾아봅니다. if (scrollList == null) { scrollList = GetComponentInChildren(); } if (scrollList == null) { Debug.LogError("InfiniteScroll component is not assigned or found in Children."); return; } // inputField가 인스펙터에서 할당되지 않았을 경우, 자식에서 찾아봅니다. if (inputField == null) { inputField = GetComponentInChildren(); } if (inputField != null) { // 사용자가 검색어를 입력하고 Enter 키를 누르거나 입력을 완료했을 때 OnInputFieldChanged 메서드가 호출되도록 이벤트를 등록합니다. //inputField.onEndEdit.AddListener(OnInputFieldChanged); inputField.onSubmit.AddListener(OnInputFieldChanged); } } protected virtual void OnDestroy() { if (inputField != null) { // Init에서 등록한 리스너를 제거합니다. inputField.onSubmit.RemoveListener(OnInputFieldChanged); } // 이벤트 핸들러 정리 OnClickItem = null; OnRightClickItem = null; OnCategoryExpand = null; OnSettingButtonClick = null; OnShowButtonClick = null; OnHideButtonClick = null; OnSearchButtonClick = null; // 데이터 참조 정리 data?.Clear(); data = null; filteredData?.Clear(); filteredData = null; // 컴포넌트 참조 정리 scrollList = null; inputField = null; } #region 내부 이벤트 핸들러 (Internal Event Handlers) internal void HandleClickItem(ComponentListItemData itemData) { OnClickItem?.Invoke(itemData); } internal void HandleRightClickItem(ComponentListItemData itemData) { OnRightClickItem?.Invoke(itemData); } /// /// 카테고리 확장/축소 처리 (ComponentListItem에서 호출) /// internal void HandleCategoryExpand(ComponentListItemData itemData) { ToggleCategory(itemData); OnCategoryExpand?.Invoke(itemData); } /// /// 설정 버튼 클릭 처리 (ComponentListItem에서 호출) /// internal void HandleSettingButtonClick(ComponentListItemData itemData) { OnSettingButtonClick?.Invoke(itemData); } /// /// 보이기 버튼 클릭 처리 (ComponentListItem에서 호출) /// internal void HandleShowButtonClick(ComponentListItemData itemData) { OnShowButtonClick?.Invoke(itemData); } /// /// 숨기기 버튼 클릭 처리 (ComponentListItem에서 호출) /// internal void HandleHideButtonClick(ComponentListItemData itemData) { OnHideButtonClick?.Invoke(itemData); } /// /// 검색/이동 버튼 클릭 처리 (ComponentListItem에서 호출) /// internal void HandleSearchButtonClick(ComponentListItemData itemData) { OnSearchButtonClick?.Invoke(itemData); } #endregion /// /// 카테고리별로 그룹화된 데이터로 스크롤 리스트를 설정합니다. /// /// 카테고리 이름을 키로, 해당 카테고리의 아이템 목록을 값으로 하는 딕셔너리 public virtual void SetupData(SortedDictionary> objectsData) { if (scrollList == null) { Debug.LogError("InfiniteScroll component is not assigned."); return; } if (inputField != null) inputField.text = string.Empty; // 축소된 카테고리 찾기 List notExpendedCategory = new List(); if (scrollList.GetDataCount() > 0) { List scrollDatalist = scrollList.GetDataList(); foreach (var scrollData in scrollDatalist) { if (scrollData is ComponentListItemData itemData && itemData.IsCategory) { if (!itemData.IsExpanded) notExpendedCategory.Add(itemData.CategoryName); } } } scrollList.ClearData(); if (data != null) data?.Clear(); data = new List(); // 표시용 리스트 List listData = new List(); // 각 카테고리에 대해 루프 foreach (var info in objectsData) { bool isExpended = !notExpendedCategory.Contains(info.Key); // 카테고리 아이템 데이터 추가 var categoryData = new ComponentListItemData { IsCategory = true, CategoryName = info.Key, CategoryBadgeCount = info.Value.Count, IsExpanded = isExpended, }; data.Add(categoryData); listData.Add(categoryData); // 해당 카테고리에 속한 모든 일반 항목 데이터 추가 foreach (var item in info.Value) { data.Add(item); // 카테고리가 축소된 상태라면 자식 항목들을 표시 리스트에 추가하지 않음 if (isExpended) listData.Add(item); } } scrollList.InsertData(listData.ToArray()); } /// /// 카테고리의 확장/축소 상태를 토글하고, 그에 따라 자식 아이템들의 표시 여부를 업데이트합니다. /// /// 상태를 변경할 카테고리 데이터 public virtual void ToggleCategory(ComponentListItemData categoryData) { if (scrollList == null || data == null) return; // 1. 현재 스크롤 리스트에서 토글할 카테고리의 인덱스를 찾습니다. List scrollDatalist = scrollList.GetDataList(); int categoryIndex = scrollDatalist.FindIndex(0, (item) => { if (item is ComponentListItemData data) return data.CategoryName == categoryData.CategoryName; return false; }); if (categoryIndex == -1) return; // 카테고리를 찾지 못하면 중단 List list = filteredData != null ? filteredData : data; // 2. 카테고리의 확장 상태를 변경합니다. categoryData.IsExpanded = !categoryData.IsExpanded; // 3. 확장/축소 상태에 따라 자식 아이템을 추가하거나 제거합니다. if (categoryData.IsExpanded) // 카테고리 확장 { // 원본 데이터 리스트에서 이 카테고리에 속한 자식들을 찾습니다. int originalDataIndex = list.IndexOf(categoryData); if (originalDataIndex != -1) { var childrenToAdd = list.Skip(originalDataIndex + 1) .Take(categoryData.CategoryBadgeCount) .ToArray(); // 카테고리 바로 다음 위치에 자식 아이템들을 삽입합니다. if (childrenToAdd.Length > 0) { scrollList.InsertData(childrenToAdd, categoryIndex + 1, true); } } } else // 카테고리 축소 { // 카테고리 바로 다음에 있는 자식 아이템들을 제거합니다. // 한 번에 하나씩 제거하므로, 항상 categoryIndex + 1 위치의 아이템을 제거합니다. for (int i = 0; i < categoryData.CategoryBadgeCount; i++) { // 제거할 아이템이 리스트 범위 내에 있는지 확인 if (categoryIndex + 1 < scrollList.GetDataCount()) { scrollList.RemoveData(categoryIndex + 1, true); } else { break; // 범위를 벗어나면 중단 } } } } /// /// 검색 입력 필드의 내용이 변경되고 제출(Enter)되었을 때 호출됩니다. /// 입력된 텍스트를 기반으로 scrollList의 내용을 필터링합니다. /// /// 사용자가 입력한 검색어 protected virtual void OnInputFieldChanged(string text) { Debug.Log($"Searching1 for: {text}"); if (scrollList == null || inputField == null) return; // 검색어가 비어있으면 필터링을 수행하지 않습니다. if (string.IsNullOrEmpty(text)) return; // 검색어가 너무 짧으면 사용자에게 알림을 표시하고 입력을 초기화합니다. if (text.Length < 3) { inputField.text = string.Empty; // 입력 필드 초기화 inputField.ActivateInputField(); // 입력 필드에 다시 포커스 설정 Toast.Show("검색어는 3글자 이상 입력해주세요.", 2f); return; } Debug.Log($"Searching2 for: {text}"); // 특수 필터 키워드가 있는지 확인합니다. // 예: "@Category 전기" -> '전기' 카테고리 필터 bool hasCategoryFilter = text.StartsWith("@Category "); // bool hasAreaFilter = text.StartsWith("@Area "); // bool hasFloorFilter = text.StartsWith("@Floor "); // 원본 데이터(data)를 기반으로 필터링된 새로운 리스트를 생성합니다. List filteredItems = data.Where(itemData => { if (itemData.IsCategory) return false; // 카테고리 항목은 항상 제외 if (hasCategoryFilter) { string categoryText = text.Substring("@Category ".Length).Trim(); return itemData.CategoryName.Contains(categoryText, StringComparison.OrdinalIgnoreCase); } /* else if (hasAreaFilter) { string areaText = text.Substring("@Area ".Length).Trim(); return itemData.Area.Contains(areaText, StringComparison.OrdinalIgnoreCase); } else if (hasFloorFilter) { string floorText = text.Substring("@Floor ".Length).Trim(); return itemData.Floor.Contains(floorText, StringComparison.OrdinalIgnoreCase); } */ else { return itemData.Name.Contains(text, StringComparison.OrdinalIgnoreCase); } }).ToList(); if (filteredData == null) { filteredData = new List(); } else { filteredData.Clear(); } string resultText = LocalizationManager.Instance.GetString("검색결과"); // 검색 결과 카테고리 아이템을 추가합니다. filteredData.Add(new ComponentListItemData { IsCategory = true, CategoryName = $"{resultText}: {filteredItems.Count}건", CategoryBadgeCount = filteredItems.Count, IsExpanded = true, // 검색 결과는 항상 펼쳐진 상태로 시작 }); // 필터링된 아이템들을 결과 리스트에 추가합니다. filteredData.AddRange(filteredItems); // 스크롤 리스트를 비우고 필터링된 결과로 새로 채웁니다. scrollList.Clear(); scrollList.InsertData(filteredData.ToArray(), true); } public void SetSearchText(string text) { if (inputField != null) { inputField.text = $"@{text} "; inputField.SetCaretToEndAsync().Forget(); } } /// /// 검색창의 'X' 버튼 클릭 시 호출됩니다. /// 입력 필드를 비우고, 적용되었던 모든 필터를 제거합니다. /// public virtual void OnClickClearText() { if (inputField != null && scrollList != null && data != null) { inputField.text = string.Empty; // 입력 필드 초기화 inputField.ActivateInputField(); // 입력 필드에 다시 포커스 설정 if (filteredData != null) { filteredData.Clear(); // 필터링된 데이터 리스트를 비웁니다. filteredData = null; } // scrollList에 설정된 필터를 제거(null)하여 모든 항목이 보이도록 합니다. scrollList.SetFilter(null); scrollList.ClearData(); // 스크롤 리스트의 내용을 비웁니다. scrollList.InsertData(data.ToArray()); } } /// /// 샘플 데이터를 생성합니다. /// /// public static SortedDictionary> GenerateData() { var data = new SortedDictionary>(); for (int i = 1; i <= 5; i++) { string categoryName = $"Category {i}"; var itemList = new List(); for (int j = 1; j <= 20; j++) { var itemData = new ComponentListItemData { Id = $"Item_{i}_{j}", Name = $"Item {i}-{j}", Option = $"Option {j}", IsCategory = false, }; itemList.Add(itemData); } data.Add(categoryName, itemList); } return data; } /// /// 탭 콘텐츠에 데이터를 전달합니다. /// /// 전달할 데이터 객체 public void SetContentData(object? data) { if (data is SortedDictionary> objectsData) { SetupData(objectsData); } else if (data is List list) { if (list.Count == 0) return; var groupedData = new SortedDictionary>(); groupedData[list.First().CategoryName] = list; SetupData(groupedData); } } /// /// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨 /// /// 전달할 데이터 객체 public void UpdateContentData(object? data) { } /// /// 닫힐 때 실행되는 로직을 처리합니다. /// /// 비동기 닫기 작업을 나타내는 입니다. public UniTask OnCloseAsync() { Debug.Log("TabContentTabComponentList: OnClose called"); return UniTask.CompletedTask; } } }