37 KiB
37 KiB
UTKPropertyWindow 작업지시서
1. 개요
1.1 목적
기존 IMGUI 기반 PropertyWindow를 UIToolkit으로 전환하여 UTKPropertyWindow를 구현합니다.
1.2 기존 PropertyWindow 구조 분석
PropertyWindow (Controller/MonoBehaviour)
├── PropertyView (View/MonoBehaviour)
│ ├── PropertyGroupView (그룹 UI)
│ └── PropertyUI들 (각 타입별 UI)
├── PropertyItem (Model/Data)
│ └── 16가지 PropertyType 지원
├── PropertyGroup (그룹 관리)
└── IPropertyUI (UI 인터페이스)
지원 PropertyType (16가지)
| Type | 설명 | 기존 UI 컴포넌트 |
|---|---|---|
| String | 문자열 | TMP_InputField |
| Int | 정수 (슬라이더 옵션) | TMP_InputField / Slider |
| Float | 실수 (슬라이더 옵션) | TMP_InputField / Slider |
| Bool | 불리언 | Toggle |
| Vector2 | 2D 벡터 | 2x InputField |
| Vector3 | 3D 벡터 | 3x InputField |
| Color | 색상 | ColorPicker |
| Date | 날짜 | DatePicker |
| DateTime | 날짜+시간 | DateTimePicker |
| Enum | 열거형 | Dropdown |
| DropdownList | 문자열 목록 | Dropdown |
| RadioGroup | 라디오 그룹 | RadioButton Group |
| IntRange | 정수 범위 | 2x InputField |
| FloatRange | 실수 범위 | 2x InputField |
| DateRange | 날짜 범위 | 2x DatePicker |
| DateTimeRange | 날짜시간 범위 | 2x DateTimePicker |
| ColorState | 상태+색상 | Label + ColorPicker |
2. 선행 의존 컴포넌트 (Prerequisites)
UTKPropertyWindow를 완전히 구현하려면 다음 컴포넌트들을 먼저 UIToolkit으로 전환해야 합니다.
2.1 의존 컴포넌트 목록
| 컴포넌트 | 기존 위치 | 용도 | 우선순위 |
|---|---|---|---|
| UTKTooltip | UVC/UI/Tooltip/ |
마우스 오버 시 툴팁 표시 | 🔴 높음 |
| UTKColorPicker | UVC/UI/Modal/ColorPicker/ |
Color, ColorState 속성 편집 | 🔴 높음 |
| UTKDatePicker | UVC/UI/Modal/DatePicker/ |
Date, DateTime, Range 속성 편집 | 🔴 높음 |
| UTKModalBlocker | Prefabs/UI/Modal/ModalBlocker |
모달 배경 차단 | 🟡 중간 |
2.2 UTKTooltip 설계
기존 구조 분석 (TooltipManager)
// 싱글톤 패턴
public class TooltipManager {
public void Initialize(Transform? defaultParent, string? tooltipPrefabPath);
public void HandlePointerEnter(string tooltipKeyOrText, Vector3 mousePosition);
public void HandlePointerExit();
public void HideTooltip();
public void Dispose();
}
UIToolkit 설계
/// <summary>
/// UIToolkit 기반 툴팁 매니저
/// VisualElement에 마우스 오버 시 툴팁 표시
/// </summary>
public class UTKTooltipManager : IDisposable
{
private static UTKTooltipManager? _instance;
public static UTKTooltipManager Instance => _instance ??= new UTKTooltipManager();
private VisualElement? _tooltipElement;
private Label? _tooltipLabel;
private bool _isVisible;
// 초기화 - VisualElement 트리의 루트에 툴팁 요소 추가
public void Initialize(VisualElement root);
// 표시/숨김
public void Show(string text, Vector2 position);
public void Hide();
// VisualElement에 툴팁 연결 (확장 메서드로도 제공)
public void AttachTooltip(VisualElement element, string tooltip);
public void DetachTooltip(VisualElement element);
public void Dispose();
}
// 확장 메서드
public static class UTKTooltipExtensions
{
public static void SetTooltip(this VisualElement element, string tooltip);
public static void ClearTooltip(this VisualElement element);
}
디렉토리 구조
Assets/Scripts/UVC/UIToolkit/Common/
├── UTKTooltipManager.cs
└── UTKTooltipExtensions.cs
Assets/Resources/UIToolkit/Common/
├── UTKTooltip.uxml
└── UTKTooltip.uss
구현 포인트
- 마우스 PointerEnterEvent/PointerLeaveEvent 활용
- 지연 표시 (500ms 후 표시)
- 화면 경계 체크로 위치 자동 조정
- LocalizationManager 연동 (다국어 지원)
2.3 UTKColorPicker 설계
기존 구조 분석 (ColorPicker)
public class ColorPicker : MonoBehaviour {
// 정적 팩토리 메서드
public static async UniTask<bool> Create(
Color original,
string message,
ColorEvent onColorChanged,
ColorEvent onColorSelected,
Action onCloseAction = null,
bool useAlpha = false
);
public static void Cancel();
public static void Done();
// UI 컴포넌트
public Slider mainComponent; // Hue 슬라이더
public Slider rComponent; // R 슬라이더
public Slider gComponent; // G 슬라이더
public Slider bComponent; // B 슬라이더
public Slider aComponent; // Alpha 슬라이더
public TMP_InputField hexaComponent; // Hex 입력
public RawImage colorComponent; // 미리보기
// HSV 색공간 지원
private sealed class HSV { ... }
}
UIToolkit 설계
/// <summary>
/// UIToolkit 기반 컬러 피커 모달
/// HSV 색공간 + RGB 슬라이더 + Hex 입력 지원
/// </summary>
[UxmlElement]
public partial class UTKColorPicker : VisualElement, IDisposable
{
// 이벤트
public event Action<Color>? OnColorChanged;
public event Action<Color>? OnColorSelected;
public event Action? OnClosed;
// 정적 팩토리 메서드
public static UTKColorPicker Show(
VisualElement parent,
Color initialColor,
string title = "Color Picker",
bool useAlpha = false
);
// 공개 메서드
public void SetColor(Color color);
public Color GetColor();
public void Close();
public void Cancel();
public void Confirm();
public void Dispose();
}
디렉토리 구조
Assets/Scripts/UVC/UIToolkit/Modal/
├── UTKColorPicker.cs
├── UTKColorPickerHSV.cs # HSV 변환 유틸리티
└── UTKModalBlocker.cs # 모달 배경
Assets/Resources/UIToolkit/Modal/
├── UTKColorPicker.uxml
├── UTKColorPicker.uss
├── UTKModalBlocker.uxml
└── UTKModalBlocker.uss
UI 레이아웃
┌─────────────────────────────────────────┐
│ [Title] [X] │
├─────────────────────────────────────────┤
│ ┌───────────────────┐ ┌──────────────┐ │
│ │ │ │ Hue Slider │ │
│ │ SV Color Box │ │ ████████████ │ │
│ │ (Saturation/ │ └──────────────┘ │
│ │ Value) │ │
│ │ │ ┌──────────────┐ │
│ └───────────────────┘ │ Preview │ │
│ └──────────────┘ │
├─────────────────────────────────────────┤
│ R: [████████████] [255] │
│ G: [████████████] [128] │
│ B: [████████████] [64] │
│ A: [████████████] [255] (optional) │
├─────────────────────────────────────────┤
│ Hex: [#FF8040FF] │
├─────────────────────────────────────────┤
│ [Cancel] [OK] │
└─────────────────────────────────────────┘
구현 포인트
- HSV 색공간 박스: VisualElement + PointerEvent로 구현
- Hue 슬라이더: Slider 컨트롤 + 그라데이션 배경
- RGB 슬라이더: Slider + IntegerField 연동
- Hex 입력: TextField + 유효성 검사
- 실시간 미리보기
- 디바운싱으로 과도한 이벤트 방지 (100ms)
2.4 UTKDatePicker 설계
기존 구조 분석 (DatePicker)
public class DatePicker : MonoBehaviour {
public static async UniTask Show(DateTime initialDate, Action<DateTime> onDateSelected);
public static void Hide();
// 캘린더 UI
public TextMeshProUGUI _yearNumText;
public TextMeshProUGUI _monthNumText;
private List<GameObject> _dateItems;
// 네비게이션
public void YearPrev();
public void YearNext();
public void MonthPrev();
public void MonthNext();
// 날짜 선택
public void OnDateItemClick(string day);
}
UIToolkit 설계
/// <summary>
/// UIToolkit 기반 날짜/시간 피커 모달
/// 캘린더 그리드 + 시간 선택 지원
/// </summary>
[UxmlElement]
public partial class UTKDatePicker : VisualElement, IDisposable
{
// 모드
public enum PickerMode { DateOnly, DateAndTime }
// 이벤트
public event Action<DateTime>? OnDateSelected;
public event Action? OnClosed;
// 정적 팩토리 메서드
public static UTKDatePicker Show(
VisualElement parent,
DateTime initialDate,
PickerMode mode = PickerMode.DateOnly,
string title = "Select Date"
);
// 공개 메서드
public void SetDate(DateTime date);
public DateTime GetDate();
public void Close();
// 네비게이션
public void PreviousMonth();
public void NextMonth();
public void PreviousYear();
public void NextYear();
public void Dispose();
}
디렉토리 구조
Assets/Scripts/UVC/UIToolkit/Modal/
├── UTKDatePicker.cs
└── UTKTimePicker.cs # 시간 선택 (옵션)
Assets/Resources/UIToolkit/Modal/
├── UTKDatePicker.uxml
├── UTKDatePicker.uss
├── UTKTimePicker.uxml # DateAndTime 모드용
└── UTKTimePicker.uss
UI 레이아웃
┌─────────────────────────────────────────┐
│ [Title] [X] │
├─────────────────────────────────────────┤
│ [<] [<<] 2026년 01월 [>>] [>] │
├─────────────────────────────────────────┤
│ 일 월 화 수 목 금 토 │
├─────────────────────────────────────────┤
│ 1 2 3 4 5 6 │
│ 7 8 9 10 11 12 13 │
│ 14 15 16 17 18 19 20 │
│ 21 22 23 24 25 [26] 27 │ ← 선택된 날짜
│ 28 29 30 31 │
├─────────────────────────────────────────┤
│ (DateAndTime 모드일 때) │
│ 시간: [14] : [30] [AM/PM] │
├─────────────────────────────────────────┤
│ [Cancel] [OK] │
└─────────────────────────────────────────┘
구현 포인트
- 6x7 그리드 레이아웃 (42개 날짜 셀)
- 요일별 색상 구분 (토:파랑, 일:빨강)
- 오늘 날짜 하이라이트
- 선택 날짜 강조
- 월 경계 비활성 날짜 처리
- 시간 선택: IntegerField (시/분) 또는 DropdownField
2.5 UTKModalBlocker 설계
/// <summary>
/// 모달 뒤의 배경을 차단하고 클릭 시 닫힘 처리
/// </summary>
[UxmlElement]
public partial class UTKModalBlocker : VisualElement, IDisposable
{
public event Action? OnBlockerClicked;
// 팩토리
public static UTKModalBlocker Show(
VisualElement root,
float opacity = 0.7f,
bool closeOnClick = false
);
public void Hide();
public void Dispose();
}
2.6 선행 작업 구현 단계
| Phase | 컴포넌트 | 예상 작업량 | 우선순위 |
|---|---|---|---|
| Phase 0-1 | UTKTooltip | 0.5일 | 🔴 |
| Phase 0-2 | UTKModalBlocker | 0.5일 | 🔴 |
| Phase 0-3 | UTKColorPicker | 1.5일 | 🔴 |
| Phase 0-4 | UTKDatePicker | 1.5일 | 🔴 |
총 예상 작업량: 4일
⚠️ 중요: Phase 1~9의 UTKPropertyWindow 구현을 시작하기 전에 Phase 0의 선행 컴포넌트 구현이 완료되어야 합니다.
3. 기존 UIToolkit 패턴 분석 (UVC.UIToolkit)
3.1 공통 아키텍처 패턴
// 1. UxmlElement 속성으로 UXML에서 사용 가능
[UxmlElement]
public partial class UTKXxx : VisualElement, IDisposable
// 2. Resources에서 UXML/USS 로드
private const string UXML_PATH = "UIToolkit/xxx/UTKXxx";
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
visualTree.CloneTree(this);
// 3. 이벤트 기반 통신 (Action 델리게이트)
public event Action<ItemData>? OnItemClick;
public event Action<ItemData, bool>? OnItemVisibilityChanged;
// 4. IDisposable 패턴으로 리소스 정리
public void Dispose() { ... }
3.2 데이터 바인딩 패턴
// UTKAccordionList 스타일: TreeView 기반 가상화
_treeView.makeItem = MakeTreeItem;
_treeView.bindItem = BindTreeItem;
_treeView.unbindItem = UnbindTreeItem;
// UTKComponentList 스타일: ListView 기반
_listView.makeItem = () => new UTKComponentListItem();
_listView.bindItem = (element, index) => { ... };
3.3 성능 최적화 기법
- 가상화 (Virtualization): TreeView/ListView의 가상화 활용
- 비동기 이미지 로딩: UniTask + CancellationToken
- 스프라이트 캐싱: Dictionary 기반 캐시
- 디바운싱: 빠른 연속 입력 처리
- 이벤트 구독 관리: Dispose 시 명시적 해제
4. UTKPropertyWindow 설계
4.1 디렉토리 구조
Assets/Scripts/UVC/UIToolkit/Property/
├── Core/
│ ├── IUTKPropertyEntry.cs # 엔트리 인터페이스
│ ├── IUTKPropertyGroup.cs # 그룹 인터페이스
│ ├── IUTKPropertyItem.cs # 아이템 인터페이스
│ ├── UTKPropertyGroup.cs # 그룹 구현
│ └── UTKPropertyType.cs # 타입 열거형
├── Data/
│ ├── UTKPropertyItemData.cs # 통합 데이터 클래스
│ └── UTKPropertyGroupData.cs # 그룹 데이터
├── Items/
│ ├── Base/
│ │ └── UTKPropertyItemBase.cs # 아이템 기본 클래스
│ ├── UTKStringPropertyItem.cs
│ ├── UTKIntPropertyItem.cs
│ ├── UTKFloatPropertyItem.cs
│ ├── UTKBoolPropertyItem.cs
│ ├── UTKVector2PropertyItem.cs
│ ├── UTKVector3PropertyItem.cs
│ ├── UTKColorPropertyItem.cs
│ ├── UTKDatePropertyItem.cs
│ ├── UTKDateTimePropertyItem.cs
│ ├── UTKEnumPropertyItem.cs
│ ├── UTKDropdownPropertyItem.cs
│ ├── UTKRadioPropertyItem.cs
│ ├── UTKIntRangePropertyItem.cs
│ ├── UTKFloatRangePropertyItem.cs
│ ├── UTKDateRangePropertyItem.cs
│ ├── UTKDateTimeRangePropertyItem.cs
│ └── UTKColorStatePropertyItem.cs
├── UTKPropertyList.cs # 핵심 리스트 컴포넌트
└── UTKPropertyWindow.cs # 윈도우 래퍼
Assets/Resources/UIToolkit/Property/
├── UTKPropertyList.uxml
├── UTKPropertyList.uss
├── UTKPropertyWindow.uxml
├── UTKPropertyWindow.uss
├── Items/
│ ├── UTKPropertyGroup.uxml
│ ├── UTKPropertyGroup.uss
│ ├── UTKStringPropertyItem.uxml
│ ├── UTKIntPropertyItem.uxml
│ ├── UTKFloatPropertyItem.uxml
│ ├── UTKBoolPropertyItem.uxml
│ ├── UTKVector2PropertyItem.uxml
│ ├── UTKVector3PropertyItem.uxml
│ ├── UTKColorPropertyItem.uxml
│ ├── UTKDatePropertyItem.uxml
│ ├── UTKDateTimePropertyItem.uxml
│ ├── UTKEnumPropertyItem.uxml
│ ├── UTKDropdownPropertyItem.uxml
│ ├── UTKRadioPropertyItem.uxml
│ ├── UTKRangePropertyItem.uxml # Int/Float/Date/DateTime 공용
│ └── UTKColorStatePropertyItem.uxml
└── Common/
└── UTKPropertyCommon.uss # 공통 스타일
Assets/Sample/UIToolkit/
└── UTKPropertyWindowSample.cs # 샘플 코드
4.2 클래스 다이어그램
┌─────────────────────┐
│ UTKPropertyWindow │
│ (VisualElement) │
│ - Window 래퍼 │
└──────────┬──────────┘
│ contains
┌──────────▼──────────┐
│ UTKPropertyList │
│ (VisualElement) │
│ - 핵심 리스트 │
└──────────┬──────────┘
│ uses
┌──────────────────┼──────────────────┐
│ │ │
┌─────────▼─────────┐ ┌─────▼──────┐ ┌─────────▼─────────┐
│ UTKPropertyGroup │ │ TreeView │ │ UTKPropertyItem │
│ (VisualElement) │ │ (가상화) │ │ (VisualElement) │
│ - 그룹 헤더 │ └────────────┘ │ - 개별 속성 UI │
└───────────────────┘ └─────────┬─────────┘
│ extends
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌──────────▼──────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ UTKStringProperty │ │ UTKBoolProperty │ │ UTKColorProperty │
│ UTKIntProperty │ │ UTKEnumProperty │ │ UTKVectorProperty │
│ UTKFloatProperty │ │ UTKDropdownProp │ │ UTKDateProperty │
└─────────────────────┘ └───────────────────┘ └───────────────────┘
5. 핵심 인터페이스 설계
5.1 IUTKPropertyItem
public interface IUTKPropertyItem : IUTKPropertyEntry
{
string Id { get; }
string Name { get; }
string? Description { get; set; }
string? Tooltip { get; set; }
bool IsReadOnly { get; set; }
bool IsVisible { get; set; }
UTKPropertyType PropertyType { get; }
string? GroupId { get; set; }
object GetValue();
void SetValue(object value);
// UIToolkit 전용
VisualElement CreateUI();
void BindUI(VisualElement element);
void UnbindUI(VisualElement element);
}
5.2 IUTKPropertyGroup
public interface IUTKPropertyGroup : IUTKPropertyEntry
{
string GroupId { get; }
string GroupName { get; set; }
bool IsExpanded { get; set; }
bool IsVisible { get; set; }
IReadOnlyList<IUTKPropertyItem> Items { get; }
void AddItem(IUTKPropertyItem item);
void RemoveItem(string itemId);
void Clear();
event Action<IUTKPropertyItem>? ItemAdded;
event Action<IUTKPropertyItem>? ItemRemoved;
}
5.3 UTKPropertyList 주요 API
[UxmlElement]
public partial class UTKPropertyList : VisualElement, IDisposable
{
// 이벤트
public event Action<UTKPropertyValueChangedEventArgs>? OnPropertyValueChanged;
public event Action<IUTKPropertyGroup, bool>? OnGroupExpandedChanged;
public event Action<IUTKPropertyItem>? OnPropertyClicked;
// 데이터 로드 메서드
public void LoadProperties(List<IUTKPropertyItem> items);
public void LoadGroupedProperties(List<IUTKPropertyGroup> groups);
public void LoadMixedProperties(List<IUTKPropertyEntry> entries);
// 그룹 관리
public void AddGroup(IUTKPropertyGroup group);
public void RemoveGroup(string groupId);
public IUTKPropertyGroup? GetGroup(string groupId);
public void SetGroupExpanded(string groupId, bool expanded);
public void ToggleGroupExpanded(string groupId);
// 속성 관리
public void AddProperty(IUTKPropertyItem item);
public void AddPropertyToGroup(string groupId, IUTKPropertyItem item);
public void RemoveProperty(string itemId);
public IUTKPropertyItem? GetProperty(string itemId);
// 값 관리
public void UpdatePropertyValue(string propertyId, object newValue);
public void SetPropertyValue(string propertyId, object value);
// 가시성/읽기전용
public void SetPropertyVisibility(string propertyId, bool visible);
public void SetGroupVisibility(string groupId, bool visible);
public void SetPropertyReadOnly(string propertyId, bool isReadOnly);
public void SetGroupReadOnly(string groupId, bool isReadOnly);
// 유틸리티
public void Clear();
public void Refresh();
public void Show();
public void Hide();
}
6. 구현 세부사항
6.1 가상화 전략 (ListView vs TreeView)
권장: TreeView 사용
이유:
- 그룹의 펼침/접힘이 트리 구조와 자연스럽게 매핑
- UTKAccordionList에서 검증된 패턴
- 아이템 수가 많아질 때 성능 이점
private TreeView _treeView;
private void InitializeTreeView()
{
_treeView = this.Q<TreeView>("property-tree-view");
_treeView.makeItem = MakePropertyItem;
_treeView.bindItem = BindPropertyItem;
_treeView.unbindItem = UnbindPropertyItem;
_treeView.itemExpandedChanged += OnItemExpandedChanged;
_treeView.selectionType = SelectionType.None; // 선택 비활성화
}
6.2 아이템 타입별 UI 생성
private VisualElement MakePropertyItem()
{
var container = new VisualElement();
container.AddToClassList("utk-property-item-container");
return container;
}
private void BindPropertyItem(VisualElement element, int index)
{
var itemData = _treeView.GetItemDataForIndex<IUTKPropertyEntry>(index);
if (itemData == null) return;
element.Clear();
switch (itemData)
{
case IUTKPropertyGroup group:
BindGroupItem(element, group);
break;
case IUTKPropertyItem item:
BindPropertyItemByType(element, item);
break;
}
}
private void BindPropertyItemByType(VisualElement container, IUTKPropertyItem item)
{
VisualElement itemUI = item.PropertyType switch
{
UTKPropertyType.String => CreateStringPropertyUI(item),
UTKPropertyType.Int => CreateIntPropertyUI(item),
UTKPropertyType.Float => CreateFloatPropertyUI(item),
UTKPropertyType.Bool => CreateBoolPropertyUI(item),
UTKPropertyType.Vector2 => CreateVector2PropertyUI(item),
UTKPropertyType.Vector3 => CreateVector3PropertyUI(item),
UTKPropertyType.Color => CreateColorPropertyUI(item),
// ... 나머지 타입들
_ => CreateFallbackPropertyUI(item)
};
container.Add(itemUI);
item.BindUI(itemUI);
}
6.3 값 변경 처리 (디바운싱 적용)
public class UTKStringPropertyItem : UTKPropertyItemBase<string>
{
private TextField? _textField;
private CancellationTokenSource? _debounceCts;
private const int DEBOUNCE_MS = 300;
public override void BindUI(VisualElement element)
{
_textField = element.Q<TextField>("value-field");
if (_textField != null)
{
_textField.value = Value;
_textField.SetEnabled(!IsReadOnly);
_textField.RegisterValueChangedCallback(OnTextChanged);
}
}
private void OnTextChanged(ChangeEvent<string> evt)
{
_debounceCts?.Cancel();
_debounceCts = new CancellationTokenSource();
DebounceValueChange(evt.newValue, _debounceCts.Token).Forget();
}
private async UniTaskVoid DebounceValueChange(string newValue, CancellationToken ct)
{
try
{
await UniTask.Delay(DEBOUNCE_MS, cancellationToken: ct);
var oldValue = Value;
Value = newValue;
NotifyValueChanged(oldValue, newValue);
}
catch (OperationCanceledException) { }
}
public override void UnbindUI(VisualElement element)
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = null;
if (_textField != null)
{
_textField.UnregisterValueChangedCallback(OnTextChanged);
_textField = null;
}
}
}
6.4 그룹 펼침/접힘 애니메이션
// USS
.utk-property-group-content {
overflow: hidden;
transition: height 0.2s ease-out;
}
.utk-property-group-content.collapsed {
height: 0;
}
// C#
private void OnGroupHeaderClicked(IUTKPropertyGroup group)
{
group.IsExpanded = !group.IsExpanded;
var contentElement = _groupElements[group.GroupId].Q("group-content");
if (group.IsExpanded)
{
contentElement.RemoveFromClassList("collapsed");
_treeView.ExpandItem(group.TreeViewId);
}
else
{
contentElement.AddToClassList("collapsed");
_treeView.CollapseItem(group.TreeViewId);
}
OnGroupExpandedChanged?.Invoke(group, group.IsExpanded);
}
6.5 메모리 관리
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 1. 모든 이벤트 구독 해제
if (_treeView != null)
{
_treeView.makeItem = null;
_treeView.bindItem = null;
_treeView.unbindItem = null;
_treeView.itemExpandedChanged -= OnItemExpandedChanged;
}
// 2. 캔슬레이션 토큰 정리
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = null;
// 3. 데이터 정리
foreach (var entry in _entries)
{
if (entry is IDisposable disposable)
disposable.Dispose();
}
_entries.Clear();
_groupIndex.Clear();
_itemIndex.Clear();
// 4. 이벤트 핸들러 정리
OnPropertyValueChanged = null;
OnGroupExpandedChanged = null;
OnPropertyClicked = null;
// 5. UI 참조 정리
_treeView = null;
_searchField = null;
}
7. 구현 단계별 작업
Phase 0: 선행 의존 컴포넌트 (0단계) ⚠️ 필수
| 순서 | 작업 | 상세 |
|---|---|---|
| 0-1 | UTKTooltip | VisualElement 기반 툴팁 시스템 |
| 0-2 | UTKModalBlocker | 모달 배경 차단 컴포넌트 |
| 0-3 | UTKColorPicker | HSV + RGB 컬러 피커 모달 |
| 0-4 | UTKDatePicker | 캘린더 기반 날짜/시간 피커 모달 |
⚠️ Phase 0이 완료되어야 Phase 1~9 진행 가능
Phase 1: 기반 구조 (1단계)
| 순서 | 작업 | 상세 |
|---|---|---|
| 1-1 | 디렉토리 생성 | Scripts/UVC/UIToolkit/Property/ 구조 생성 |
| 1-2 | Core 인터페이스 | IUTKPropertyEntry, IUTKPropertyItem, IUTKPropertyGroup |
| 1-3 | 타입 열거형 | UTKPropertyType (기존 PropertyType과 동일) |
| 1-4 | 기본 데이터 클래스 | UTKPropertyItemBase 추상 클래스 |
| 1-5 | 이벤트 클래스 | UTKPropertyValueChangedEventArgs |
Phase 2: 기본 속성 타입 (2단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 2-1 | UTKStringPropertyItem | UTKStringPropertyItem.uxml/uss |
| 2-2 | UTKIntPropertyItem | UTKIntPropertyItem.uxml/uss |
| 2-3 | UTKFloatPropertyItem | UTKFloatPropertyItem.uxml/uss |
| 2-4 | UTKBoolPropertyItem | UTKBoolPropertyItem.uxml/uss |
Phase 3: 복합 속성 타입 (3단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 3-1 | UTKVector2PropertyItem | UTKVector2PropertyItem.uxml/uss |
| 3-2 | UTKVector3PropertyItem | UTKVector3PropertyItem.uxml/uss |
| 3-3 | UTKColorPropertyItem | UTKColorPropertyItem.uxml/uss |
Phase 4: 날짜/시간 속성 타입 (4단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 4-1 | UTKDatePropertyItem | UTKDatePropertyItem.uxml/uss |
| 4-2 | UTKDateTimePropertyItem | UTKDateTimePropertyItem.uxml/uss |
| 4-3 | UTKDateRangePropertyItem | 공용 UXML 사용 |
| 4-4 | UTKDateTimeRangePropertyItem | 공용 UXML 사용 |
Phase 5: 선택 속성 타입 (5단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 5-1 | UTKEnumPropertyItem | UTKEnumPropertyItem.uxml/uss |
| 5-2 | UTKDropdownPropertyItem | UTKDropdownPropertyItem.uxml/uss |
| 5-3 | UTKRadioPropertyItem | UTKRadioPropertyItem.uxml/uss |
Phase 6: 범위/기타 속성 타입 (6단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 6-1 | UTKIntRangePropertyItem | UTKRangePropertyItem.uxml (공용) |
| 6-2 | UTKFloatRangePropertyItem | 공용 UXML 사용 |
| 6-3 | UTKColorStatePropertyItem | UTKColorStatePropertyItem.uxml/uss |
Phase 7: 그룹 및 리스트 (7단계)
| 순서 | 작업 | UXML/USS |
|---|---|---|
| 7-1 | UTKPropertyGroup | UTKPropertyGroup.uxml/uss |
| 7-2 | UTKPropertyList | UTKPropertyList.uxml/uss |
| 7-3 | TreeView 바인딩 | makeItem/bindItem/unbindItem |
| 7-4 | 그룹 펼침/접힘 | itemExpandedChanged 처리 |
Phase 8: 윈도우 및 최적화 (8단계)
| 순서 | 작업 | 상세 |
|---|---|---|
| 8-1 | UTKPropertyWindow | 헤더, 닫기 버튼 포함 래퍼 |
| 8-2 | 검색 기능 | 속성명/값 필터링 (선택적) |
| 8-3 | 성능 최적화 | 디바운싱, 캐싱 검증 |
| 8-4 | 메모리 누수 테스트 | Dispose 패턴 검증 |
Phase 9: 테스트 및 샘플 (9단계)
| 순서 | 작업 | 상세 |
|---|---|---|
| 9-1 | UTKPropertyWindowSample | 기존 샘플과 동일한 데이터 |
| 9-2 | 성능 비교 | IMGUI vs UIToolkit |
| 9-3 | 문서화 | XML 주석 완성 |
8. 관심사 분리 원칙
8.1 데이터/로직 분리
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ - UTKPropertyItemData (순수 데이터) │
│ - UTKPropertyGroupData (순수 데이터) │
│ - 비즈니스 로직 없음, 직렬화 가능 │
└─────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Logic Layer │
│ - UTKPropertyList (Controller) │
│ - 데이터 관리, 이벤트 발생 │
│ - UI 생성 위임 │
└─────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ View Layer │
│ - UTKPropertyItemBase (추상 UI) │
│ - 각 타입별 PropertyItem (구체적 UI) │
│ - UXML/USS (선언적 스타일) │
└─────────────────────────────────────────────────────────┘
8.2 단일 책임 원칙 적용
| 클래스 | 책임 |
|---|---|
| UTKPropertyItemData | 데이터 저장 및 직렬화 |
| UTKPropertyList | 데이터 관리 및 TreeView 조율 |
| UTKPropertyItemBase | UI 생성/바인딩 공통 로직 |
| UTKXxxPropertyItem | 특정 타입 UI 렌더링 |
| UTKPropertyGroup | 그룹 헤더 렌더링 |
| UTKPropertyWindow | 윈도우 프레임 제공 |
9. 성능 고려사항
9.1 가상화 필수
- TreeView의 가상화로 수백 개 속성도 부드럽게 처리
- makeItem/bindItem/unbindItem 패턴 준수
- 불필요한 요소 생성 방지
9.2 이벤트 디바운싱
// 텍스트 입력 시 300ms 디바운스
// 슬라이더 드래그 시 50ms 디바운스
// 색상 피커 시 100ms 디바운스
9.3 UXML 템플릿 캐싱
private static readonly Dictionary<UTKPropertyType, VisualTreeAsset> _templateCache = new();
private VisualTreeAsset GetTemplate(UTKPropertyType type)
{
if (!_templateCache.TryGetValue(type, out var template))
{
template = Resources.Load<VisualTreeAsset>(GetTemplatePath(type));
_templateCache[type] = template;
}
return template;
}
9.4 USS 통합
- 공통 스타일은 UTKPropertyCommon.uss에 집중
- 타입별 스타일은 개별 USS에 분리
- CSS 변수 활용으로 테마 지원
10. 메모리 관리 체크리스트
- 모든 이벤트 구독은 Dispose에서 해제
- CancellationTokenSource는 Cancel() 후 Dispose()
- Dictionary/List는 Clear() 호출
- VisualElement 참조는 null 할당
- 순환 참조 방지 (WeakReference 검토)
- Unity 리소스(Sprite 등) 명시적 언로드
11. 테스트 시나리오
11.1 기능 테스트
| 시나리오 | 검증 항목 |
|---|---|
| 속성 로드 | 16가지 타입 모두 정상 렌더링 |
| 값 변경 | 이벤트 발생 및 데이터 동기화 |
| 그룹 토글 | 펼침/접힘 상태 유지 |
| 읽기전용 | 입력 비활성화 및 시각적 표시 |
| 가시성 | 개별/그룹 표시/숨김 |
| Undo/Redo | SetPropertyValue로 값 복원 |
11.2 성능 테스트
| 시나리오 | 목표 |
|---|---|
| 100개 속성 로드 | < 100ms |
| 500개 속성 스크롤 | 60 FPS 유지 |
| 빠른 값 변경 | 메모리 누수 없음 |
| 반복 Dispose | GC 부담 최소화 |
12. 마이그레이션 가이드
12.1 기존 코드 호환성
// 기존 (IMGUI)
propertyWindow.LoadMixedProperties(entries);
propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
// 신규 (UIToolkit) - 동일한 API 유지
utkPropertyWindow.LoadMixedProperties(entries);
utkPropertyWindow.OnPropertyValueChanged += OnPropertyValueChanged;
12.2 데이터 클래스 재사용
기존 PropertyItem, PropertyGroup 클래스는 그대로 사용 가능하도록 어댑터 제공:
// 기존 데이터를 UTK 형식으로 변환
var utkEntries = LegacyPropertyConverter.Convert(existingEntries);
utkPropertyList.LoadMixedProperties(utkEntries);
13. 참고 자료
13.1 기존 코드 위치
| 파일 | 경로 |
|---|---|
| PropertyWindow | Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs |
| PropertyView | Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyView.cs |
| PropertyItem | Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyItem.cs |
| PropertyGroup | Assets/Scripts/UVC/UI/Window/PropertyWindow/Core/PropertyGroup.cs |
| PropertyUIBase | Assets/Scripts/UVC/UI/Window/PropertyWindow/View/PropertyUIBase.cs |
| 샘플 | Assets/Sample/PropertyWindowSample.cs |
13.2 UIToolkit 참조 코드
| 파일 | 용도 |
|---|---|
| UTKAccordionList | TreeView 가상화, 드래그, 검색 패턴 |
| UTKComponentList | ListView 바인딩, 이벤트 패턴 |
| UTKComponentListWindow | Window 래퍼 패턴 |
13.3 의존 컴포넌트 기존 코드
| 파일 | 경로 |
|---|---|
| ColorPicker | Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs |
| DatePicker | Assets/Scripts/UVC/UI/Modal/DatePicker/DatePicker.cs |
| TooltipManager | Assets/Scripts/UVC/UI/Tooltip/TooltipManager.cs |
| TooltipHandler | Assets/Scripts/UVC/UI/Tooltip/TooltipHandler.cs |
14. 승인 및 리뷰
- 설계 리뷰 완료
- 기능 명세 확정
- 성능 요구사항 합의
- 마이그레이션 계획 승인
작성일: 2026-01-06 작성자: Claude Code 버전: 1.0