라이브러리 정리

This commit is contained in:
logonkhi
2025-12-08 21:06:05 +09:00
parent bfee6d8745
commit cf31cc0159
330 changed files with 68800 additions and 42167 deletions

View File

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

View File

@@ -0,0 +1,15 @@
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// PropertyWindow에 추가할 수 있는 항목의 통합 인터페이스입니다.
/// 그룹(IPropertyGroup) 또는 개별 아이템(IPropertyItem) 모두 이 타입으로 처리됩니다.
/// </summary>
public interface IPropertyEntry
{
/// <summary>
/// 렌더링 순서를 결정하는 값입니다.
/// 낮은 값이 먼저 표시됩니다.
/// </summary>
int Order { get; set; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86e2cde5cc1d5cb4d88c1937d95bae42

View File

@@ -0,0 +1,61 @@
using System.Collections.Generic;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 속성 아이템들을 그룹으로 묶어서 관리하는 인터페이스입니다.
/// Unity Inspector 스타일의 접이식 그룹(Foldout Group)을 지원합니다.
/// </summary>
public interface IPropertyGroup : IPropertyEntry
{
/// <summary>
/// 그룹의 고유 식별자
/// </summary>
string GroupId { get; }
/// <summary>
/// UI에 표시될 그룹 이름
/// </summary>
string GroupName { get; set; }
/// <summary>
/// 그룹의 접힘/펼침 상태
/// true: 펼쳐진 상태, false: 접힌 상태
/// </summary>
bool IsExpanded { get; set; }
/// <summary>
/// 그룹에 포함된 속성 아이템들의 읽기 전용 목록
/// </summary>
IReadOnlyList<IPropertyItem> Items { get; }
/// <summary>
/// 그룹에 속성 아이템을 추가합니다.
/// </summary>
/// <param name="item">추가할 속성 아이템</param>
void AddItem(IPropertyItem item);
/// <summary>
/// 그룹에 여러 속성 아이템을 한번에 추가합니다.
/// </summary>
/// <param name="items">추가할 속성 아이템들</param>
void AddItems(IEnumerable<IPropertyItem> items);
/// <summary>
/// 그룹에서 특정 ID의 속성 아이템을 제거합니다.
/// </summary>
/// <param name="itemId">제거할 아이템의 ID</param>
/// <returns>제거 성공 여부</returns>
bool RemoveItem(string itemId);
/// <summary>
/// 그룹의 모든 속성 아이템을 제거합니다.
/// </summary>
void Clear();
/// <summary>
/// 그룹에 포함된 아이템 수를 반환합니다.
/// </summary>
int Count { get; }
}
}

View File

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

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// IPropertyGroup 인터페이스의 기본 구현 클래스입니다.
/// 속성 아이템들을 그룹으로 묶어서 관리합니다.
/// </summary>
public class PropertyGroup : IPropertyGroup
{
private readonly List<IPropertyItem> _items = new List<IPropertyItem>();
/// <summary>
/// 그룹의 고유 식별자
/// </summary>
public string GroupId { get; }
/// <summary>
/// UI에 표시될 그룹 이름
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// 그룹의 접힘/펼침 상태
/// </summary>
public bool IsExpanded { get; set; } = true;
/// <summary>
/// 렌더링 순서
/// </summary>
public int Order { get; set; }
/// <summary>
/// 그룹에 포함된 속성 아이템들의 읽기 전용 목록
/// </summary>
public IReadOnlyList<IPropertyItem> Items => _items.AsReadOnly();
/// <summary>
/// 그룹에 포함된 아이템 수
/// </summary>
public int Count => _items.Count;
/// <summary>
/// 아이템이 추가되었을 때 발생하는 이벤트
/// </summary>
public event EventHandler<PropertyGroupItemEventArgs> ItemAdded;
/// <summary>
/// 아이템이 제거되었을 때 발생하는 이벤트
/// </summary>
public event EventHandler<PropertyGroupItemEventArgs> ItemRemoved;
/// <summary>
/// 그룹이 비워졌을 때 발생하는 이벤트
/// </summary>
public event EventHandler Cleared;
/// <summary>
/// PropertyGroup을 생성합니다.
/// </summary>
/// <param name="groupId">그룹 고유 ID</param>
/// <param name="groupName">그룹 표시명</param>
/// <param name="order">렌더링 순서 (기본값: 0)</param>
/// <param name="isExpanded">초기 펼침 상태 (기본값: true)</param>
public PropertyGroup(string groupId, string groupName, int order = 0, bool isExpanded = true)
{
GroupId = groupId ?? throw new ArgumentNullException(nameof(groupId));
GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName));
Order = order;
IsExpanded = isExpanded;
}
/// <summary>
/// 그룹에 속성 아이템을 추가합니다.
/// </summary>
public void AddItem(IPropertyItem item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
// 중복 ID 체크
if (_items.Any(i => i.Id == item.Id))
{
UnityEngine.Debug.LogWarning($"[PropertyGroup] 이미 존재하는 아이템 ID입니다: {item.Id}");
return;
}
item.GroupId = GroupId;
_items.Add(item);
ItemAdded?.Invoke(this, new PropertyGroupItemEventArgs(GroupId, item));
}
/// <summary>
/// 그룹에 여러 속성 아이템을 한번에 추가합니다.
/// </summary>
public void AddItems(IEnumerable<IPropertyItem> items)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
foreach (var item in items)
{
AddItem(item);
}
}
/// <summary>
/// 그룹에서 특정 ID의 속성 아이템을 제거합니다.
/// </summary>
public bool RemoveItem(string itemId)
{
var item = _items.FirstOrDefault(i => i.Id == itemId);
if (item != null)
{
item.GroupId = null;
_items.Remove(item);
ItemRemoved?.Invoke(this, new PropertyGroupItemEventArgs(GroupId, item));
return true;
}
return false;
}
/// <summary>
/// 그룹의 모든 속성 아이템을 제거합니다.
/// </summary>
public void Clear()
{
foreach (var item in _items)
{
item.GroupId = null;
}
_items.Clear();
Cleared?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 특정 ID의 아이템을 가져옵니다.
/// </summary>
public IPropertyItem GetItem(string itemId)
{
return _items.FirstOrDefault(i => i.Id == itemId);
}
/// <summary>
/// 특정 ID의 아이템이 존재하는지 확인합니다.
/// </summary>
public bool ContainsItem(string itemId)
{
return _items.Any(i => i.Id == itemId);
}
}
/// <summary>
/// PropertyGroup 아이템 관련 이벤트 인자
/// </summary>
public class PropertyGroupItemEventArgs : EventArgs
{
public string GroupId { get; }
public IPropertyItem Item { get; }
public PropertyGroupItemEventArgs(string groupId, IPropertyItem item)
{
GroupId = groupId;
Item = item;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
using System;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 그룹 추가/제거 이벤트 인자
/// </summary>
public class PropertyGroupEventArgs : EventArgs
{
/// <summary>
/// 이벤트 대상 그룹
/// </summary>
public IPropertyGroup Group { get; }
public PropertyGroupEventArgs(IPropertyGroup group)
{
Group = group;
}
}
/// <summary>
/// 그룹 펼침/접힘 상태 변경 이벤트 인자
/// </summary>
public class PropertyGroupExpandedEventArgs : EventArgs
{
/// <summary>
/// 이벤트 대상 그룹
/// </summary>
public IPropertyGroup Group { get; }
/// <summary>
/// 변경 전 펼침 상태
/// </summary>
public bool WasExpanded { get; }
/// <summary>
/// 변경 후 펼침 상태
/// </summary>
public bool IsExpanded { get; }
public PropertyGroupExpandedEventArgs(IPropertyGroup group, bool wasExpanded, bool isExpanded)
{
Group = group;
WasExpanded = wasExpanded;
IsExpanded = isExpanded;
}
}
/// <summary>
/// 엔트리(그룹 또는 아이템) 추가/제거 이벤트 인자
/// </summary>
public class PropertyEntryEventArgs : EventArgs
{
/// <summary>
/// 이벤트 대상 엔트리
/// </summary>
public IPropertyEntry Entry { get; }
/// <summary>
/// 엔트리가 그룹인지 여부
/// </summary>
public bool IsGroup => Entry is IPropertyGroup;
/// <summary>
/// 엔트리가 아이템인지 여부
/// </summary>
public bool IsItem => Entry is IPropertyItem;
public PropertyEntryEventArgs(IPropertyEntry entry)
{
Entry = entry;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,353 @@
using System.Collections.Generic;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// PropertyUI 오브젝트들을 풀링하여 재사용하는 시스템입니다.
/// 매번 Instantiate/Destroy 대신 풀에서 가져오고 반환하여 성능을 향상시킵니다.
/// </summary>
public class PropertyUIPool : MonoBehaviour
{
[Header("Pool Settings")]
[SerializeField] private int _defaultPoolSize = 5;
[SerializeField] private Transform _poolContainer;
[Header("Prefabs")]
[SerializeField] private GameObject _groupPrefab;
[SerializeField] private GameObject _stringPropertyPrefab;
[SerializeField] private GameObject _numberPropertyPrefab;
[SerializeField] private GameObject _boolPropertyPrefab;
[SerializeField] private GameObject _vector2PropertyPrefab;
[SerializeField] private GameObject _vector3PropertyPrefab;
[SerializeField] private GameObject _colorPropertyPrefab;
[SerializeField] private GameObject _datePropertyPrefab;
[SerializeField] private GameObject _dateTimePropertyPrefab;
[SerializeField] private GameObject _enumPropertyPrefab;
[SerializeField] private GameObject _listPropertyPrefab;
[SerializeField] private GameObject _radioGroupPropertyPrefab;
[SerializeField] private GameObject _numberRangePropertyPrefab;
[SerializeField] private GameObject _dateRangePropertyPrefab;
[SerializeField] private GameObject _dateTimeRangePropertyPrefab;
/// <summary>
/// PropertyType별 UI 오브젝트 풀
/// </summary>
private readonly Dictionary<PropertyType, Queue<GameObject>> _itemPools = new Dictionary<PropertyType, Queue<GameObject>>();
/// <summary>
/// 그룹 UI 오브젝트 풀
/// </summary>
private readonly Queue<PropertyGroupView> _groupPool = new Queue<PropertyGroupView>();
/// <summary>
/// PropertyType별 프리팹 매핑
/// </summary>
private Dictionary<PropertyType, GameObject> _prefabMap;
private bool _isInitialized = false;
private void Awake()
{
Initialize();
}
/// <summary>
/// 풀을 초기화합니다.
/// </summary>
public void Initialize()
{
if (_isInitialized) return;
// 풀 컨테이너 생성 (없으면)
if (_poolContainer == null)
{
var containerObj = new GameObject("PropertyUIPool_Container");
containerObj.transform.SetParent(transform);
containerObj.SetActive(false);
_poolContainer = containerObj.transform;
}
// 프리팹 맵 초기화
InitializePrefabMap();
// 각 타입별 풀 초기화
foreach (PropertyType type in System.Enum.GetValues(typeof(PropertyType)))
{
if (!_itemPools.ContainsKey(type))
{
_itemPools[type] = new Queue<GameObject>();
}
}
_isInitialized = true;
}
/// <summary>
/// PropertyType별 프리팹 매핑을 초기화합니다.
/// </summary>
private void InitializePrefabMap()
{
_prefabMap = new Dictionary<PropertyType, GameObject>
{
{ PropertyType.String, _stringPropertyPrefab },
{ PropertyType.Int, _numberPropertyPrefab },
{ PropertyType.Float, _numberPropertyPrefab },
{ PropertyType.Bool, _boolPropertyPrefab },
{ PropertyType.Vector2, _vector2PropertyPrefab },
{ PropertyType.Vector3, _vector3PropertyPrefab },
{ PropertyType.Color, _colorPropertyPrefab },
{ PropertyType.Date, _datePropertyPrefab },
{ PropertyType.DateTime, _dateTimePropertyPrefab },
{ PropertyType.Enum, _enumPropertyPrefab },
{ PropertyType.DropdownList, _listPropertyPrefab },
{ PropertyType.RadioGroup, _radioGroupPropertyPrefab },
{ PropertyType.IntRange, _numberRangePropertyPrefab },
{ PropertyType.FloatRange, _numberRangePropertyPrefab },
{ PropertyType.DateRange, _dateRangePropertyPrefab },
{ PropertyType.DateTimeRange, _dateTimeRangePropertyPrefab }
};
}
#region Item Pool Methods
/// <summary>
/// 풀에서 PropertyUI 오브젝트를 가져옵니다.
/// 풀이 비어있으면 새로 생성합니다.
/// </summary>
/// <param name="type">속성 타입</param>
/// <returns>UI GameObject 또는 프리팹이 없으면 null</returns>
public GameObject GetItemUI(PropertyType type)
{
if (!_isInitialized) Initialize();
// 풀에서 가져오기 시도
if (_itemPools.TryGetValue(type, out var pool) && pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
// 풀이 비어있으면 새로 생성
return CreateItemUI(type);
}
/// <summary>
/// PropertyUI 오브젝트를 풀에 반환합니다.
/// </summary>
/// <param name="type">속성 타입</param>
/// <param name="obj">반환할 오브젝트</param>
public void ReturnItemUI(PropertyType type, GameObject obj)
{
if (obj == null) return;
// UI 컴포넌트 정리
var propertyUI = obj.GetComponent<IPropertyUI>();
if (propertyUI is IPoolable poolable)
{
poolable.OnReturnToPool();
}
obj.SetActive(false);
obj.transform.SetParent(_poolContainer, false);
if (_itemPools.TryGetValue(type, out var pool))
{
pool.Enqueue(obj);
}
}
/// <summary>
/// 새로운 PropertyUI 오브젝트를 생성합니다.
/// </summary>
private GameObject CreateItemUI(PropertyType type)
{
if (_prefabMap == null || !_prefabMap.TryGetValue(type, out var prefab) || prefab == null)
{
Debug.LogWarning($"[PropertyUIPool] '{type}' 타입에 대한 프리팹이 설정되지 않았습니다.");
return null;
}
var obj = Instantiate(prefab);
return obj;
}
#endregion
#region Group Pool Methods
/// <summary>
/// 풀에서 PropertyGroupView를 가져옵니다.
/// </summary>
/// <returns>PropertyGroupView 또는 프리팹이 없으면 null</returns>
public PropertyGroupView GetGroupUI()
{
if (!_isInitialized) Initialize();
// 풀에서 가져오기 시도
if (_groupPool.Count > 0)
{
var groupView = _groupPool.Dequeue();
groupView.gameObject.SetActive(true);
return groupView;
}
// 풀이 비어있으면 새로 생성
return CreateGroupUI();
}
/// <summary>
/// PropertyGroupView를 풀에 반환합니다.
/// </summary>
/// <param name="groupView">반환할 그룹 뷰</param>
public void ReturnGroupUI(PropertyGroupView groupView)
{
if (groupView == null) return;
groupView.Reset();
groupView.gameObject.SetActive(false);
groupView.transform.SetParent(_poolContainer, false);
_groupPool.Enqueue(groupView);
}
/// <summary>
/// 새로운 PropertyGroupView를 생성합니다.
/// </summary>
private PropertyGroupView CreateGroupUI()
{
if (_groupPrefab == null)
{
Debug.LogWarning("[PropertyUIPool] 그룹 프리팹이 설정되지 않았습니다.");
return null;
}
var obj = Instantiate(_groupPrefab);
return obj.GetComponent<PropertyGroupView>();
}
#endregion
#region Pool Management
/// <summary>
/// 특정 타입의 UI를 미리 생성하여 풀에 추가합니다.
/// </summary>
/// <param name="type">속성 타입</param>
/// <param name="count">미리 생성할 개수</param>
public void Prewarm(PropertyType type, int count)
{
if (!_isInitialized) Initialize();
for (int i = 0; i < count; i++)
{
var obj = CreateItemUI(type);
if (obj != null)
{
ReturnItemUI(type, obj);
}
}
}
/// <summary>
/// 그룹 UI를 미리 생성하여 풀에 추가합니다.
/// </summary>
/// <param name="count">미리 생성할 개수</param>
public void PrewarmGroups(int count)
{
if (!_isInitialized) Initialize();
for (int i = 0; i < count; i++)
{
var groupView = CreateGroupUI();
if (groupView != null)
{
ReturnGroupUI(groupView);
}
}
}
/// <summary>
/// 모든 풀을 비우고 오브젝트를 파괴합니다.
/// </summary>
public void Clear()
{
// 아이템 풀 정리
foreach (var pool in _itemPools.Values)
{
while (pool.Count > 0)
{
var obj = pool.Dequeue();
if (obj != null)
{
Destroy(obj);
}
}
}
_itemPools.Clear();
// 그룹 풀 정리
while (_groupPool.Count > 0)
{
var groupView = _groupPool.Dequeue();
if (groupView != null)
{
Destroy(groupView.gameObject);
}
}
// 풀 컨테이너의 모든 자식 삭제
if (_poolContainer != null)
{
foreach (Transform child in _poolContainer)
{
Destroy(child.gameObject);
}
}
}
/// <summary>
/// 현재 풀 상태를 반환합니다 (디버그용).
/// </summary>
public string GetPoolStatus()
{
var status = new System.Text.StringBuilder();
status.AppendLine("[PropertyUIPool Status]");
status.AppendLine($"Groups in pool: {_groupPool.Count}");
foreach (var kvp in _itemPools)
{
if (kvp.Value.Count > 0)
{
status.AppendLine($"{kvp.Key}: {kvp.Value.Count}");
}
}
return status.ToString();
}
#endregion
private void OnDestroy()
{
Clear();
}
}
/// <summary>
/// 풀링 가능한 UI 컴포넌트가 구현해야 하는 인터페이스입니다.
/// </summary>
public interface IPoolable
{
/// <summary>
/// 풀에서 가져올 때 호출됩니다.
/// </summary>
void OnGetFromPool();
/// <summary>
/// 풀에 반환될 때 호출됩니다.
/// 이벤트 해제 및 상태 초기화를 수행해야 합니다.
/// </summary>
void OnReturnToPool();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a9acf4264efbf44e957f7c7870280dc

View File

@@ -65,8 +65,9 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 모든 속성 항목이 구현해야 하는 기본 인터페이스입니다.
/// IPropertyEntry를 상속하여 그룹과 혼용하여 사용할 수 있습니다.
/// </summary>
public interface IPropertyItem
public interface IPropertyItem : IPropertyEntry
{
/// <summary>
/// 속성의 고유 식별자 (필수)
@@ -108,6 +109,12 @@ namespace UVC.UI.Window.PropertyWindow
/// 속성의 데이터 타입
/// </summary>
PropertyType PropertyType { get; }
/// <summary>
/// 아이템이 속한 그룹의 ID입니다.
/// null이면 그룹에 속하지 않은 독립 아이템입니다.
/// </summary>
string GroupId { get; set; }
}
/// <summary>
@@ -124,6 +131,16 @@ namespace UVC.UI.Window.PropertyWindow
public bool IsReadOnly { get; set; } = false;
public abstract PropertyType PropertyType { get; }
/// <summary>
/// 렌더링 순서를 결정하는 값입니다. (IPropertyEntry 구현)
/// </summary>
public int Order { get; set; } = 0;
/// <summary>
/// 아이템이 속한 그룹의 ID입니다. null이면 그룹에 속하지 않습니다.
/// </summary>
public string GroupId { get; set; }
/// <summary>
/// 실제 데이터가 저장되는 필드
/// </summary>

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow
@@ -5,19 +7,19 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 속성창의 UI를 담당하는 View 클래스입니다.
/// Controller로부터 데이터를 받아와 동적으로 UI 요소들을 생성하고 관리합니다.
/// 이 클래스는 MonoBehaviour를 상속받아 Unity 씬에 배치될 수 있습니다.
/// 그룹과 개별 아이템을 혼용하여 렌더링할 수 있습니다.
/// </summary>
public class PropertyView : MonoBehaviour
{
/// <summary>
/// UI 요소들이 생성될 부모 컨테이너입니다.
/// Unity 에디터에서 Vertical Layout Group 컴포넌트가 추가된 Panel 등을 연결합니다.
/// </summary>
[Tooltip("속성 UI들이 생성될 부모 Transform (예: Vertical Layout Group이 있는 Panel)")]
[SerializeField] private Transform _container;
// 각 속성 타입에 맞는 UI 프리팹들입니다.
// 실제 프로젝트에서는 이 프리팹들을 만들고 여기에 연결해야 합니다.
[Header("Group UI Prefab")]
[SerializeField] private GameObject _groupPrefab;
[Header("Property UI Prefabs")]
[SerializeField] private GameObject _stringPropertyPrefab;
[SerializeField] private GameObject _numberPropertyPrefab;
@@ -39,63 +41,216 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
private PropertyWindow _controller;
/// <summary>
/// 현재 표시 중인 그룹 뷰들의 캐시
/// </summary>
private readonly Dictionary<string, PropertyGroupView> _groupViews = new Dictionary<string, PropertyGroupView>();
/// <summary>
/// 현재 표시 중인 아이템 UI들의 캐시 (아이템 ID -> GameObject)
/// </summary>
private readonly Dictionary<string, GameObject> _itemViews = new Dictionary<string, GameObject>();
/// <summary>
/// Controller를 View에 설정하고 UI를 초기화합니다.
/// </summary>
/// <param name="controller">사용할 PropertyWindow</param>
public void Initialize(PropertyWindow controller)
{
// 기존 컨트롤러 이벤트 해제
if (_controller != null)
{
_controller.PropertyValueChanged -= OnPropertyValueChanged;
_controller.GroupExpandedChanged -= OnGroupExpandedChanged;
}
_controller = controller;
// Controller가 null이 아니면, 이벤트 핸들러를 등록하고 UI를 그립니다.
if (_controller != null)
{
_controller.PropertyValueChanged += OnPropertyValueChanged;
DrawProperties();
_controller.GroupExpandedChanged += OnGroupExpandedChanged;
DrawEntries();
}
}
/// <summary>
/// Controller에 있는 속성 목록을 기반으로 UI를 생성합니다.
/// Controller에 있는 엔트리 목록을 기반으로 UI를 생성합니다.
/// 그룹과 개별 아이템을 혼용하여 렌더링합니다.
/// </summary>
private void DrawProperties()
private void DrawEntries()
{
// UI를 다시 그리기 전에 기존에 생성된 모든 자식 오브젝트를 삭제합니다.
foreach (Transform child in _container)
{
DestroyImmediate(child.gameObject);
}
// 기존 UI 정리
ClearAllViews();
if (_controller == null) return;
// 각 속성 항목에 대해 적절한 UI를 생성합니다.
foreach (var propertyItem in _controller.Properties)
{
// 속성 타입에 맞는 UI 프리팹을 찾습니다.
GameObject prefab = GetPrefabForProperty(propertyItem.PropertyType);
if (prefab != null)
{
// 프리팹을 인스턴스화하여 컨테이너의 자식으로 추가합니다.
GameObject uiInstance = Instantiate(prefab, _container);
// Order 순으로 정렬된 엔트리들을 렌더링
var sortedEntries = _controller.Entries;
// 생성된 UI 인스턴스에서 IPropertyUI 컴포넌트를 찾아 Setup을 호출합니다.
var propertyUI = uiInstance.GetComponent<IPropertyUI>();
if (propertyUI != null)
foreach (var entry in sortedEntries)
{
if (entry is IPropertyGroup group)
{
DrawGroup(group);
}
else if (entry is IPropertyItem item && item.GroupId == null)
{
// 그룹에 속하지 않은 개별 아이템만 직접 렌더링
DrawPropertyItem(item, _container);
}
}
}
/// <summary>
/// 그룹 UI를 생성합니다.
/// </summary>
private void DrawGroup(IPropertyGroup group)
{
if (_groupPrefab == null)
{
Debug.LogWarning("[PropertyView] 그룹 프리팹이 설정되지 않았습니다. 그룹 내 아이템만 표시합니다.");
// 그룹 프리팹이 없으면 아이템들만 직접 렌더링
foreach (var item in group.Items)
{
DrawPropertyItem(item, _container);
}
return;
}
// 그룹 UI 생성
GameObject groupInstance = Instantiate(_groupPrefab, _container);
var groupView = groupInstance.GetComponent<PropertyGroupView>();
if (groupView != null)
{
groupView.Setup(group, _controller);
_groupViews[group.GroupId] = groupView;
// 그룹이 펼쳐진 상태면 자식 아이템들 렌더링
if (group.IsExpanded)
{
foreach (var item in group.Items)
{
propertyUI.Setup(propertyItem, _controller);
DrawPropertyItem(item, groupView.ItemContainer);
}
else
}
}
else
{
Debug.LogError($"[PropertyView] 그룹 프리팹 '{_groupPrefab.name}'에 PropertyGroupView 컴포넌트가 없습니다.");
Destroy(groupInstance);
}
}
/// <summary>
/// 개별 속성 아이템 UI를 생성합니다.
/// </summary>
private void DrawPropertyItem(IPropertyItem item, Transform container)
{
GameObject prefab = GetPrefabForProperty(item.PropertyType);
if (prefab != null)
{
GameObject uiInstance = Instantiate(prefab, container);
var propertyUI = uiInstance.GetComponent<IPropertyUI>();
if (propertyUI != null)
{
propertyUI.Setup(item, _controller);
_itemViews[item.Id] = uiInstance;
}
else
{
Debug.LogError($"[PropertyView] 프리팹 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
Destroy(uiInstance);
}
}
else
{
Debug.LogWarning($"[PropertyView] '{item.PropertyType}' 타입에 대한 UI 프리팹이 지정되지 않았습니다.");
}
}
/// <summary>
/// 모든 View를 정리합니다.
/// </summary>
private void ClearAllViews()
{
// 그룹 뷰 정리
foreach (var groupView in _groupViews.Values)
{
if (groupView != null)
{
groupView.Reset();
Destroy(groupView.gameObject);
}
}
_groupViews.Clear();
// 아이템 뷰 정리
foreach (var itemView in _itemViews.Values)
{
if (itemView != null)
{
Destroy(itemView);
}
}
_itemViews.Clear();
// 컨테이너의 모든 자식 삭제 (혹시 누락된 것이 있을 경우)
foreach (Transform child in _container)
{
Destroy(child.gameObject);
}
}
/// <summary>
/// 그룹 펼침/접힘 상태가 변경되었을 때 호출됩니다.
/// </summary>
private void OnGroupExpandedChanged(object sender, PropertyGroupExpandedEventArgs e)
{
if (_groupViews.TryGetValue(e.Group.GroupId, out var groupView))
{
groupView.UpdateExpandedState();
// 펼쳐진 경우 자식 아이템들 렌더링
if (e.IsExpanded)
{
foreach (var item in e.Group.Items)
{
Debug.LogError($"[PropertyView] 프리팹 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
// 이미 존재하는 아이템은 건너뜀
if (!_itemViews.ContainsKey(item.Id))
{
DrawPropertyItem(item, groupView.ItemContainer);
}
}
}
else
{
Debug.LogWarning($"[PropertyView] '{propertyItem.PropertyType}' 타입에 대한 UI 프리팹이 지정되지 않았습니다.");
// 접힌 경우 자식 아이템들 제거
groupView.ClearItems();
foreach (var item in e.Group.Items)
{
if (_itemViews.TryGetValue(item.Id, out var itemView))
{
Destroy(itemView);
_itemViews.Remove(item.Id);
}
}
}
}
}
/// <summary>
/// [하위 호환성] 기존 방식으로 속성 목록을 그립니다.
/// </summary>
[System.Obsolete("Use DrawEntries() instead. This method is kept for backward compatibility.")]
private void DrawProperties()
{
DrawEntries();
}
/// <summary>
/// 속성 타입에 맞는 UI 프리팹을 반환합니다.
/// 실제 구현에서는 더 많은 case가 필요합니다.
@@ -160,7 +315,12 @@ namespace UVC.UI.Window.PropertyWindow
if (_controller != null)
{
_controller.PropertyValueChanged -= OnPropertyValueChanged;
_controller.GroupExpandedChanged -= OnGroupExpandedChanged;
}
// 캐시 정리
_groupViews.Clear();
_itemViews.Clear();
}
}
}

View File

@@ -13,35 +13,414 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 속성 데이터를 관리하고, 데이터 변경 시 이벤트를 발생시키는 컨트롤러 클래스입니다.
/// Model과 View 사이의 중재자 역할을 합니다.
/// 그룹과 개별 아이템을 혼용하여 사용할 수 있습니다.
/// </summary>
public class PropertyWindow: SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
public class PropertyWindow : SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
{
[SerializeField]
private PropertyView _view;
#region Internal Data Structures
/// <summary>
/// 통합 엔트리 목록 (그룹과 개별 아이템 혼합 저장)
/// </summary>
private readonly List<IPropertyEntry> _entries = new List<IPropertyEntry>();
/// <summary>
/// 빠른 그룹 조회를 위한 인덱스
/// </summary>
private readonly Dictionary<string, IPropertyGroup> _groupIndex = new Dictionary<string, IPropertyGroup>();
/// <summary>
/// 빠른 아이템 조회를 위한 인덱스
/// </summary>
private readonly Dictionary<string, IPropertyItem> _itemIndex = new Dictionary<string, IPropertyItem>();
#endregion
#region Public Properties
/// <summary>
/// 현재 컨트롤러가 관리하는 모든 속성 항목의 목록입니다.
/// 하위 호환성을 위해 유지됩니다. 그룹에 속한 아이템도 포함됩니다.
/// </summary>
public List<IPropertyItem> Properties { get; private set; } = new List<IPropertyItem>();
public List<IPropertyItem> Properties
{
get
{
var allItems = new List<IPropertyItem>();
foreach (var entry in _entries)
{
if (entry is IPropertyItem item && item.GroupId == null)
{
allItems.Add(item);
}
else if (entry is IPropertyGroup group)
{
allItems.AddRange(group.Items);
}
}
return allItems;
}
}
/// <summary>
/// 정렬된 엔트리 목록을 반환합니다.
/// </summary>
public IReadOnlyList<IPropertyEntry> Entries => _entries.OrderBy(e => e.Order).ToList().AsReadOnly();
/// <summary>
/// 모든 그룹의 읽기 전용 목록을 반환합니다.
/// </summary>
public IReadOnlyList<IPropertyGroup> Groups => _groupIndex.Values.ToList().AsReadOnly();
#endregion
#region Events
/// <summary>
/// 속성 값이 변경될 때 발생하는 이벤트입니다.
/// View는 이 이벤트를 구독하여 UI를 업데이트할 수 있습니다.
/// </summary>
public event EventHandler<PropertyValueChangedEventArgs>? PropertyValueChanged;
/// <summary>
/// 새로운 속성 목록을 로드하고 초기화합니다.
/// 그룹이 추가되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<PropertyGroupEventArgs>? GroupAdded;
/// <summary>
/// 그룹이 제거되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<PropertyGroupEventArgs>? GroupRemoved;
/// <summary>
/// 그룹의 펼침/접힘 상태가 변경되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<PropertyGroupExpandedEventArgs>? GroupExpandedChanged;
/// <summary>
/// 엔트리가 추가되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<PropertyEntryEventArgs>? EntryAdded;
/// <summary>
/// 엔트리가 제거되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<PropertyEntryEventArgs>? EntryRemoved;
/// <summary>
/// 모든 엔트리가 제거되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler? EntriesCleared;
#endregion
#region Load Methods ( + + )
/// <summary>
/// [기존 방식] 그룹 없이 속성 목록을 로드합니다.
/// 모든 아이템이 flat하게 표시됩니다.
/// </summary>
/// <param name="items">표시할 속성 항목들의 목록</param>
public void LoadProperties(List<IPropertyItem> items)
{
Properties = items ?? new List<IPropertyItem>();
// 필요하다면 여기서 추가적인 초기화 로직을 수행할 수 있습니다.
if(_view != null) _view.Initialize(this);
Clear();
if (items != null)
{
foreach (var item in items)
{
item.GroupId = null; // 그룹 없음 명시
AddEntryInternal(item);
}
}
Refresh();
}
/// <summary>
/// [그룹 방식] 그룹화된 속성 목록을 로드합니다.
/// </summary>
/// <param name="groups">표시할 속성 그룹들의 목록</param>
public void LoadGroupedProperties(List<IPropertyGroup> groups)
{
Clear();
if (groups != null)
{
foreach (var group in groups)
{
AddEntryInternal(group);
}
}
Refresh();
}
/// <summary>
/// [혼용 방식] 그룹과 개별 아이템을 함께 로드합니다.
/// </summary>
/// <param name="entries">표시할 엔트리들의 목록 (그룹 또는 아이템)</param>
public void LoadMixedProperties(List<IPropertyEntry> entries)
{
Clear();
if (entries != null)
{
foreach (var entry in entries)
{
AddEntryInternal(entry);
}
}
Refresh();
}
#endregion
#region Group Management
/// <summary>
/// 그룹을 추가합니다.
/// </summary>
/// <param name="group">추가할 그룹</param>
public void AddGroup(IPropertyGroup group)
{
if (group == null)
throw new ArgumentNullException(nameof(group));
if (_groupIndex.ContainsKey(group.GroupId))
{
Debug.LogWarning($"[PropertyWindow] 이미 존재하는 그룹 ID입니다: {group.GroupId}");
return;
}
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
Refresh();
}
/// <summary>
/// 그룹을 제거합니다.
/// </summary>
/// <param name="groupId">제거할 그룹의 ID</param>
public void RemoveGroup(string groupId)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
// 그룹 내 모든 아이템의 GroupId 초기화 및 인덱스에서 제거
foreach (var item in group.Items)
{
item.GroupId = null;
_itemIndex.Remove(item.Id);
}
group.Clear();
_groupIndex.Remove(groupId);
_entries.Remove(group);
GroupRemoved?.Invoke(this, new PropertyGroupEventArgs(group));
Refresh();
}
}
/// <summary>
/// 특정 ID의 그룹을 가져옵니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <returns>그룹 또는 null</returns>
public IPropertyGroup? GetGroup(string groupId)
{
_groupIndex.TryGetValue(groupId, out var group);
return group;
}
/// <summary>
/// 그룹의 펼침/접힘 상태를 변경합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="isExpanded">펼침 상태</param>
public void SetGroupExpanded(string groupId, bool isExpanded)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
bool wasExpanded = group.IsExpanded;
if (wasExpanded != isExpanded)
{
group.IsExpanded = isExpanded;
GroupExpandedChanged?.Invoke(this, new PropertyGroupExpandedEventArgs(group, wasExpanded, isExpanded));
Refresh();
}
}
}
/// <summary>
/// 그룹의 펼침/접힘 상태를 토글합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
public void ToggleGroupExpanded(string groupId)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
SetGroupExpanded(groupId, !group.IsExpanded);
}
}
#endregion
#region Property Management
/// <summary>
/// 개별 속성 아이템을 추가합니다 (그룹 없이).
/// </summary>
/// <param name="item">추가할 속성 아이템</param>
public void AddProperty(IPropertyItem item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
item.GroupId = null;
AddEntryInternal(item);
EntryAdded?.Invoke(this, new PropertyEntryEventArgs(item));
Refresh();
}
/// <summary>
/// 특정 그룹에 속성 아이템을 추가합니다.
/// 그룹이 없으면 새로 생성합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="item">추가할 속성 아이템</param>
/// <param name="groupNameIfNew">그룹이 새로 생성될 경우 사용할 이름 (null이면 groupId 사용)</param>
public void AddPropertyToGroup(string groupId, IPropertyItem item, string? groupNameIfNew = null)
{
if (string.IsNullOrEmpty(groupId))
throw new ArgumentNullException(nameof(groupId));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (!_groupIndex.TryGetValue(groupId, out var group))
{
// 그룹이 없으면 새로 생성
group = new PropertyGroup(groupId, groupNameIfNew ?? groupId);
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
}
group.AddItem(item);
_itemIndex[item.Id] = item;
Refresh();
}
/// <summary>
/// 여러 속성 아이템을 한번에 그룹에 추가합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="items">추가할 속성 아이템들</param>
/// <param name="groupNameIfNew">그룹이 새로 생성될 경우 사용할 이름</param>
public void AddPropertiesToGroup(string groupId, IEnumerable<IPropertyItem> items, string? groupNameIfNew = null)
{
if (string.IsNullOrEmpty(groupId))
throw new ArgumentNullException(nameof(groupId));
if (items == null)
throw new ArgumentNullException(nameof(items));
if (!_groupIndex.TryGetValue(groupId, out var group))
{
group = new PropertyGroup(groupId, groupNameIfNew ?? groupId);
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
}
foreach (var item in items)
{
group.AddItem(item);
_itemIndex[item.Id] = item;
}
Refresh();
}
/// <summary>
/// 속성 아이템을 그룹에서 제거하고 독립 아이템으로 변경합니다.
/// </summary>
/// <param name="itemId">아이템 ID</param>
public void UngroupProperty(string itemId)
{
if (_itemIndex.TryGetValue(itemId, out var item) && item.GroupId != null)
{
if (_groupIndex.TryGetValue(item.GroupId, out var group))
{
group.RemoveItem(itemId);
}
item.GroupId = null;
AddEntryInternal(item); // 독립 엔트리로 추가
Refresh();
}
}
/// <summary>
/// 속성 아이템을 제거합니다.
/// </summary>
/// <param name="itemId">제거할 아이템 ID</param>
public void RemoveProperty(string itemId)
{
if (_itemIndex.TryGetValue(itemId, out var item))
{
if (item.GroupId != null && _groupIndex.TryGetValue(item.GroupId, out var group))
{
group.RemoveItem(itemId);
}
else
{
_entries.Remove(item);
}
_itemIndex.Remove(itemId);
EntryRemoved?.Invoke(this, new PropertyEntryEventArgs(item));
Refresh();
}
}
/// <summary>
/// 특정 ID의 속성 아이템을 가져옵니다.
/// </summary>
/// <param name="itemId">아이템 ID</param>
/// <returns>속성 아이템 또는 null</returns>
public IPropertyItem? GetProperty(string itemId)
{
_itemIndex.TryGetValue(itemId, out var item);
return item;
}
#endregion
#region Clear and Refresh
/// <summary>
/// 모든 엔트리를 제거합니다.
/// </summary>
public void Clear()
{
foreach (var group in _groupIndex.Values)
{
group.Clear();
}
_entries.Clear();
_groupIndex.Clear();
_itemIndex.Clear();
EntriesCleared?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// View를 갱신합니다.
/// </summary>
public void Refresh()
{
if (_view != null)
{
_view.Initialize(this);
}
}
#endregion
#region Value Update
/// <summary>
/// 특정 ID를 가진 속성의 값을 업데이트합니다.
/// 이 메서드는 주로 View에서 사용자 입력이 발생했을 때 호출됩니다.
@@ -51,45 +430,71 @@ namespace UVC.UI.Window.PropertyWindow
/// <param name="newValue">새로운 값</param>
public void UpdatePropertyValue(string propertyId, PropertyType propertyType, object newValue)
{
// ID에 해당하는 속성을 찾습니다.
var propertyItem = Properties.FirstOrDefault(p => p.Id == propertyId);
if (propertyItem == null)
if (!_itemIndex.TryGetValue(propertyId, out var propertyItem))
{
// 해당 ID의 속성이 없으면 오류를 기록하고 반환합니다.
UnityEngine.Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
return;
}
// 이전 값을 저장합니다.
object oldValue = propertyItem.GetValue();
object? oldValue = propertyItem.GetValue();
// 값 타입일 때 새 값과 이전 값이 같은지 확인합니다. 참조 타입은 PropertyUI에서 필터링 (불필요한 이벤트 발생 방지)
if (oldValue.GetType().IsValueType && Equals(oldValue, newValue))
// 값 타입일 때 새 값과 이전 값이 같은지 확인합니다.
if (oldValue != null && oldValue.GetType().IsValueType && Equals(oldValue, newValue))
{
return;
}
// 속성 객체의 값을 새로운 값으로 설정합니다.
propertyItem.SetValue(newValue);
// 값이 변경되었음을 알리는 이벤트를 발생시킵니다.
OnPropertyValueChanged(propertyId, propertyType, oldValue, newValue);
OnPropertyValueChanged(propertyId, propertyType, oldValue!, newValue);
}
/// <summary>
/// PropertyValueChanged 이벤트를 안전하게 발생시키는 보호된 가상 메서드입니다.
/// </summary>
/// <param name="propertyId">변경된 속성 ID</param>
/// <param name="oldValue">이전 값</param>
/// <param name="newValue">새로운 값</param>
protected virtual void OnPropertyValueChanged(string propertyId, PropertyType propertyType, object oldValue, object newValue)
{
// 이벤트 핸들러가 등록되어 있는지 확인하고 이벤트를 발생시킵니다.
PropertyValueChanged?.Invoke(this, new PropertyValueChangedEventArgs(propertyId, propertyType, oldValue, newValue));
}
#endregion
#region Internal Helpers
/// <summary>
/// 엔트리를 내부 컬렉션에 추가합니다.
/// </summary>
private void AddEntryInternal(IPropertyEntry entry)
{
if (entry is IPropertyGroup group)
{
if (!_groupIndex.ContainsKey(group.GroupId))
{
_groupIndex[group.GroupId] = group;
_entries.Add(group);
// 그룹 내 아이템들도 인덱스에 추가
foreach (var item in group.Items)
{
_itemIndex[item.Id] = item;
}
}
}
else if (entry is IPropertyItem item)
{
if (!_itemIndex.ContainsKey(item.Id))
{
_itemIndex[item.Id] = item;
if (item.GroupId == null)
{
_entries.Add(item);
}
}
}
}
#endregion
#region Visibility
public bool IsVisible => gameObject.activeSelf;
@@ -101,26 +506,29 @@ namespace UVC.UI.Window.PropertyWindow
public void Hide()
{
gameObject.SetActive(false);
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
FactoryCameraController.Instance.Enable = true;
}
#endregion
#region Pointer Events
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
/// UI와 상호작용하는 동안 3D 뷰의 카메라가 움직이지 않도록 컨트롤러를 비활성화합니다.
/// </summary>
public virtual void OnPointerEnter(PointerEventData eventData)
{
FactoryCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
FactoryCameraController.Instance.Enable = false;
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
/// 카메라 컨트롤을 다시 활성화하여 3D 뷰를 조작할 수 있도록 합니다.
/// </summary>
public virtual void OnPointerExit(PointerEventData eventData)
{
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
FactoryCameraController.Instance.Enable = true;
}
#endregion
}
}

View File

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

View File

@@ -0,0 +1,151 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 속성 그룹의 UI를 담당하는 View 클래스입니다.
/// 접이식 헤더와 자식 아이템들의 컨테이너를 관리합니다.
/// </summary>
public class PropertyGroupView : MonoBehaviour
{
[Header("Header")]
[SerializeField] private Button _headerButton;
[SerializeField] private TextMeshProUGUI _groupNameLabel;
[SerializeField] private Image _expandIcon;
[Header("Icons")]
[SerializeField] private Sprite _expandedIcon;
[SerializeField] private Sprite _collapsedIcon;
[Header("Content")]
[SerializeField] private Transform _itemContainer;
[SerializeField] private GameObject _contentPanel;
private IPropertyGroup _group;
private PropertyWindow _controller;
/// <summary>
/// 자식 PropertyItem UI들이 생성될 컨테이너입니다.
/// </summary>
public Transform ItemContainer => _itemContainer;
/// <summary>
/// 그룹 데이터
/// </summary>
public IPropertyGroup Group => _group;
/// <summary>
/// 그룹 ID
/// </summary>
public string GroupId => _group?.GroupId;
/// <summary>
/// PropertyGroupView를 초기화합니다.
/// </summary>
/// <param name="group">표시할 그룹 데이터</param>
/// <param name="controller">상호작용할 컨트롤러</param>
public void Setup(IPropertyGroup group, PropertyWindow controller)
{
_group = group;
_controller = controller;
// 그룹명 설정
if (_groupNameLabel != null)
{
_groupNameLabel.text = group.GroupName;
}
// 헤더 버튼 이벤트 등록
if (_headerButton != null)
{
_headerButton.onClick.RemoveAllListeners();
_headerButton.onClick.AddListener(OnHeaderClicked);
}
// 펼침/접힘 상태 반영
UpdateExpandedState();
}
/// <summary>
/// 헤더 클릭 시 호출됩니다.
/// </summary>
private void OnHeaderClicked()
{
if (_controller != null && _group != null)
{
_controller.ToggleGroupExpanded(_group.GroupId);
}
}
/// <summary>
/// 펼침/접힘 상태를 UI에 반영합니다.
/// </summary>
public void UpdateExpandedState()
{
if (_group == null) return;
bool isExpanded = _group.IsExpanded;
// 컨텐츠 패널 표시/숨김
if (_contentPanel != null)
{
_contentPanel.SetActive(isExpanded);
}
// 아이콘 변경
if (_expandIcon != null)
{
if (isExpanded && _expandedIcon != null)
{
_expandIcon.sprite = _expandedIcon;
}
else if (!isExpanded && _collapsedIcon != null)
{
_expandIcon.sprite = _collapsedIcon;
}
// 아이콘 회전으로 표현할 경우
_expandIcon.transform.rotation = Quaternion.Euler(0, 0, isExpanded ? 0 : -90);
}
}
/// <summary>
/// 그룹 내 모든 아이템 UI를 제거합니다.
/// </summary>
public void ClearItems()
{
if (_itemContainer == null) return;
foreach (Transform child in _itemContainer)
{
Destroy(child.gameObject);
}
}
/// <summary>
/// 풀에 반환하기 전에 정리합니다.
/// </summary>
public void Reset()
{
_group = null;
_controller = null;
if (_headerButton != null)
{
_headerButton.onClick.RemoveAllListeners();
}
ClearItems();
}
private void OnDestroy()
{
if (_headerButton != null)
{
_headerButton.onClick.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1292fe635565fff43a6bd494f3f597b8

View File

@@ -0,0 +1,231 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 모든 PropertyUI의 공통 베이스 클래스입니다.
/// 공통 기능을 제공하고 IPropertyUI, IPoolable, IDisposable을 구현합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public abstract class PropertyUIBase<T> : MonoBehaviour, IPropertyUI, IPoolable, IDisposable
where T : class, IPropertyItem
{
[Header("Common UI Elements")]
[SerializeField] protected TextMeshProUGUI _nameLabel;
[SerializeField] protected TextMeshProUGUI _descriptionLabel;
/// <summary>
/// 현재 표시 중인 속성 아이템
/// </summary>
protected T _propertyItem;
/// <summary>
/// 상호작용할 컨트롤러
/// </summary>
protected PropertyWindow _controller;
/// <summary>
/// 이미 해제되었는지 여부
/// </summary>
protected bool _isDisposed = false;
#region IPropertyUI Implementation
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화합니다.
/// </summary>
public virtual void Setup(IPropertyItem item, PropertyWindow controller)
{
// 기존 상태 정리
Cleanup();
// 타입 체크
if (!(item is T typedItem))
{
Debug.LogError($"[{GetType().Name}] 잘못된 타입의 PropertyItem이 전달되었습니다. 예상: {typeof(T).Name}, 실제: {item.GetType().Name}");
return;
}
_propertyItem = typedItem;
_controller = controller;
_isDisposed = false;
// 공통 UI 설정
SetupCommonUI();
// 파생 클래스의 추가 설정
SetupValueUI();
// 이벤트 구독
SubscribeEvents();
}
#endregion
#region Common UI Setup
/// <summary>
/// 공통 UI 요소들을 설정합니다.
/// </summary>
protected virtual void SetupCommonUI()
{
if (_propertyItem == null) return;
// 이름 설정
if (_nameLabel != null)
{
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정
var tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
}
// 설명 설정
if (_descriptionLabel != null)
{
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
}
}
/// <summary>
/// 파생 클래스에서 값 관련 UI를 설정합니다.
/// </summary>
protected abstract void SetupValueUI();
#endregion
#region Event Management
/// <summary>
/// 이벤트를 구독합니다. 파생 클래스에서 재정의할 수 있습니다.
/// </summary>
protected virtual void SubscribeEvents()
{
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
}
/// <summary>
/// 이벤트 구독을 해제합니다. 파생 클래스에서 재정의할 수 있습니다.
/// </summary>
protected virtual void UnsubscribeEvents()
{
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
}
#endregion
#region Value Update
/// <summary>
/// 컨트롤러를 통해 값 변경을 통지합니다.
/// </summary>
/// <param name="newValue">새로운 값</param>
protected void NotifyValueChanged(object newValue)
{
if (_controller == null || _propertyItem == null) return;
// 값이 변경되지 않았으면 무시
var oldValue = _propertyItem.GetValue();
if (Equals(oldValue, newValue)) return;
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="interactable">UI 요소</param>
protected void ApplyReadOnlyState(Selectable interactable)
{
if (interactable != null && _propertyItem != null)
{
interactable.interactable = !_propertyItem.IsReadOnly;
}
}
/// <summary>
/// 읽기 전용 상태를 설정합니다 (TMP_InputField용).
/// </summary>
/// <param name="inputField">입력 필드</param>
protected void ApplyReadOnlyState(TMP_InputField inputField)
{
if (inputField != null && _propertyItem != null)
{
inputField.interactable = !_propertyItem.IsReadOnly;
}
}
#endregion
#region Cleanup and Disposal
/// <summary>
/// 상태를 정리합니다.
/// </summary>
protected virtual void Cleanup()
{
UnsubscribeEvents();
_propertyItem = null;
_controller = null;
}
/// <summary>
/// IDisposable 구현
/// </summary>
public void Dispose()
{
if (_isDisposed) return;
Cleanup();
_isDisposed = true;
}
#endregion
#region IPoolable Implementation
/// <summary>
/// 풀에서 가져올 때 호출됩니다.
/// </summary>
public virtual void OnGetFromPool()
{
_isDisposed = false;
}
/// <summary>
/// 풀에 반환될 때 호출됩니다.
/// </summary>
public virtual void OnReturnToPool()
{
Cleanup();
}
#endregion
#region Unity Lifecycle
protected virtual void OnDestroy()
{
Dispose();
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97fb1aaa6e72f9742b99c8d8ddf43ebc

View File

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

View File

@@ -0,0 +1,87 @@
using TMPro;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// StringProperty를 위한 PropertyUIBase 기반 UI 스크립트입니다.
/// 기존 StringPropertyUI의 리팩토링 버전입니다.
/// </summary>
public class StringPropertyUIBase : PropertyUIBase<StringProperty>
{
[Header("String Property UI")]
[SerializeField] private TMP_InputField _valueInput;
#region PropertyUIBase Implementation
/// <summary>
/// 값 관련 UI를 설정합니다.
/// </summary>
protected override void SetupValueUI()
{
if (_valueInput == null || _propertyItem == null) return;
// 값 설정
_valueInput.text = _propertyItem.Value;
// 읽기 전용 상태 설정
ApplyReadOnlyState(_valueInput);
}
/// <summary>
/// 이벤트를 구독합니다.
/// </summary>
protected override void SubscribeEvents()
{
base.SubscribeEvents();
if (_valueInput != null)
{
_valueInput.onEndEdit.AddListener(OnValueSubmitted);
}
}
/// <summary>
/// 이벤트 구독을 해제합니다.
/// </summary>
protected override void UnsubscribeEvents()
{
base.UnsubscribeEvents();
if (_valueInput != null)
{
_valueInput.onEndEdit.RemoveListener(OnValueSubmitted);
}
}
#endregion
#region Event Handlers
/// <summary>
/// 사용자가 InputField 수정 완료 후 Enter를 누르거나 포커스를 잃었을 때 호출됩니다.
/// </summary>
/// <param name="newValue">InputField에 입력된 새로운 문자열</param>
private void OnValueSubmitted(string newValue)
{
NotifyValueChanged(newValue);
}
#endregion
#region IPoolable Implementation
public override void OnReturnToPool()
{
base.OnReturnToPool();
// 입력 필드 초기화
if (_valueInput != null)
{
_valueInput.text = string.Empty;
}
}
#endregion
}
}

View File

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