merge and property
This commit is contained in:
2025-12-24 17:36:01 +09:00
parent d4764e304f
commit 6b78b68229
109 changed files with 14167 additions and 625 deletions

View File

@@ -30,6 +30,8 @@ namespace SampleProject
[SerializeField]
private TopMenuController sideMenu;
[SerializeField]
private PropertyWindow propertyWindow;
public Action Initialized;
/// <summary>
@@ -182,7 +184,7 @@ namespace SampleProject
topMenu.AddMenuItem(new MenuItemData("Settings", "Settings", new SettingOpenCommand()));
topMenu.AddMenuItem(new MenuItemData("PropertyWindow", "PropertyWindow", new ActionCommand(async () =>
{
PropertyWindow.Instance.Show();
propertyWindow.Show();
})));
topMenu.Initialize();
@@ -238,7 +240,7 @@ namespace SampleProject
private void SetupPropertyWindow()
{
PropertyWindow.Instance.LoadProperties(new List<IPropertyItem>
propertyWindow.LoadProperties(new List<IPropertyItem>
{
new StringProperty("prop1", "String Property", "Initial Value")
{
@@ -338,7 +340,7 @@ namespace SampleProject
}
});
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
propertyWindow.PropertyValueChanged += (sender, e) =>
{
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
};

View File

@@ -20,7 +20,7 @@ public class ComponentBase : MonoBehaviour,IPointerClickHandler
protected ComponentDataBase data;
protected ModelDataBase modelData;
public ComponentType componentType;
public event Action<ComponentType,ComponentDataBase> onComponentClicked;
public Action<ComponentType,ComponentDataBase> onComponentClicked;
public void SetPosition()
{
@@ -61,8 +61,8 @@ public class ComponentBase : MonoBehaviour,IPointerClickHandler
public void OnPointerClick(PointerEventData eventData)
{
onComponentClicked?.Invoke(componentType, data);
Debug.Log(componentType);
GameObject clickedObject = eventData.pointerCurrentRaycast.gameObject;
Debug.Log(clickedObject.name);
getpath();
}
}

View File

@@ -246,6 +246,7 @@ namespace Simulator.Data
{
var data = await HttpRequester.RequestGet<Totaljson>($"{Constants.HTTP_DOMAIN}/simulation/logics/{SimulationConfig.logicId}", null, null, true);
logicDetailData = data.data.data;
PathIndexer.Build(logicDetailData);
onProjectNameRecieved?.Invoke(data.data.name);
SpawnComponents(data.data.data);
if (logicDetailData.templates != null)

View File

@@ -1,6 +1,7 @@
using sc.modeling.splines.runtime;
using Simulator.Data;
using System.Collections.Generic;
using Unity.VisualScripting;
using System;
using UnityEngine;
using UnityEngine.Splines;
using UVC.Data.Core;
@@ -23,6 +24,8 @@ public class ConveyorComponent : ComponentBase
public float borderEpsilon = 1e-4f;
bool[] occupancy;
ConveyorPath conveyorPath;
public event Action<ConveyorPath> onConveyorClicked;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
@@ -71,7 +74,13 @@ public class ConveyorComponent : ComponentBase
MoveOnConveyor();
}
public void SetComponent(ConveyorPath conveyorData)
{
this.conveyorPath = conveyorData;
onConveyorClicked += FindAnyObjectByType<ConveyorProperty>().SetProertyWindow;
var box=gameObject.AddComponent<BoxCollider>();
box.isTrigger = true;
}
void SetConveyorEntity(Entity entity)
{
var cTarget = entity.gameObject.GetComponent<ConveyorTarget>();
@@ -215,4 +224,9 @@ public class ConveyorComponent : ComponentBase
m.Rebuild();
}
}
public override void getpath()
{
onConveyorClicked?.Invoke(conveyorPath);
}
}

View File

@@ -83,21 +83,23 @@ namespace Simulator.Data
{
if (datas == null || datas.Count <= 0) return;
var data = datas[0];
if (rootParent == null) rootParent = transform;
Nodes.Clear();
Paths.Clear();
_stationAdj.Clear(); // ▲ 추가: 초기화
if (data.nodes != null)
foreach (var data in datas)
{
foreach (var n in data.nodes)
Nodes[n.name] = n;
if (data.nodes != null)
{
foreach (var n in data.nodes)
Nodes[n.name] = n;
}
foreach (var path in data.paths)
CreatePathGOWithSpline(path, Nodes[path.from_node], Nodes[path.to_node]);
}
foreach (var path in data.paths)
CreatePathGOWithSpline(path, Nodes[path.from_node], Nodes[path.to_node]);
SpawnJunction();
@@ -172,6 +174,7 @@ namespace Simulator.Data
if (string.Equals(toNode.node_type, "station"))
AddStationAdj(toNode.name, (fromNode.name, toNode.name), capIndex: 1, otherNode: fromNode.name);
go.SetComponent(path);
}
// ▼ 추가: station 인접 정보 수집

View File

@@ -0,0 +1,16 @@
using Simulator.Data;
using System;
using UnityEngine;
public class NodeComponent : ComponentBase
{
NodeComponent conveyorNode;
public event Action<NodeComponent> onConveyorClicked;
public void SetComponent(NodeComponent conveyorNode)
{
this.conveyorNode = conveyorNode;
onComponentClicked += FindAnyObjectByType<SourceProperty>().SetProertyWindow;
FitCollider();
}
}

View File

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

View File

@@ -1,17 +1,22 @@
using Newtonsoft.Json;
using System;
using System.Security;
using UnityEngine;
namespace Simulator.Data
{
[Serializable]
public class SourceDataClass:ComponentDataBase
public class SourceDataClass : ComponentDataBase
{
public string name;
public string label;
public string modelType;
public string prefab;
[JsonConverter(typeof(PolicyConverter))]
public Policy_Base spawn_time_policy;
[JsonConverter(typeof(PolicyConverter))]
public Policy_Base spawn_count_policy;
[JsonConverter(typeof(PolicyConverter))]
public Policy_Base defect_rate_policy;
public int max_spawn_limit;
public string output_store;
public bool is_unlimited;
}
}

View File

@@ -15,6 +15,7 @@ public class ProcessorComponent : ComponentBase
{
this.processorData = processorData;
data = processorData;
onComponentClicked += FindAnyObjectByType<ProcessorProperty>().SetProertyWindow;
FitCollider();
}
@@ -43,4 +44,8 @@ public class ProcessorComponent : ComponentBase
break;
}
}
public override void getpath()
{
onComponentClicked?.Invoke(componentType, processorData);
}
}

View File

@@ -7,5 +7,6 @@ namespace Simulator.Data
{
public string name;
public string label;
public string processor_type;
}
}

View File

@@ -13,6 +13,8 @@ public class SinkComponent : ComponentBase
{
this.sourceData = sourceData;
data = sourceData;
onComponentClicked += FindAnyObjectByType<SinkProperty>().SetProertyWindow;
FitCollider();
}
public override void GetModelData(DataObject modelData)
@@ -22,4 +24,8 @@ public class SinkComponent : ComponentBase
var sinkdata = datas["entity_ids"].ConvertTo<List<string>>();
EntityManager.Instance.DestroyEnity(sinkdata);
}
public override void getpath()
{
onComponentClicked?.Invoke(componentType, sourceData);
}
}

View File

@@ -24,7 +24,7 @@ namespace Simulator.Data
{
this.sourceData = sourceData;
data = sourceData;
onComponentClicked += FindAnyObjectByType<SimulatorProperty>().SetProertyWindow;
onComponentClicked += FindAnyObjectByType<SourceProperty>().SetProertyWindow;
FitCollider();
}
@@ -158,9 +158,7 @@ namespace Simulator.Data
public override void getpath()
{
PathIndexer.Build(ComponentsManager.Instance.logicDetailData);
var path = PathIndexer.GetNodePath(sourceData);
Debug.Log(path);
onComponentClicked?.Invoke(componentType, sourceData);
}
}
}

View File

@@ -0,0 +1,134 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using UnityEngine;
using UnityEngine.Rendering.Universal;
[Serializable]
public abstract class Policy_Base
{
public string type;
public abstract SpawnPolicy policy { get; }
}
[Serializable]
public class Policy_Constant : Policy_Base
{
public float value;
public override SpawnPolicy policy => SpawnPolicy.Constant;
}
[Serializable]
public class Policy_Normal : Policy_Base
{
public float mean;
public float stddev;
public override SpawnPolicy policy => SpawnPolicy.Normal;
}
[Serializable]
public class Policy_Uniform : Policy_Base
{
public float min_val;
public float max_val;
public override SpawnPolicy policy => SpawnPolicy.Uniform;
}
[Serializable]
public class Policy_Exponential : Policy_Base
{
public float mean;
public override SpawnPolicy policy => SpawnPolicy.Exponential;
}
[Serializable]
public class Policy_Triangular : Policy_Base
{
public float min_val;
public float mode;
public float max_val;
public override SpawnPolicy policy => SpawnPolicy.Triangular;
}
public sealed class PolicyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(Policy_Base);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jo = JObject.Load(reader);
var typeStr = (jo["type"]?.ToString() ?? "").Trim().ToLowerInvariant();
Policy_Base target = typeStr switch
{
"constant" => new Policy_Constant(),
"normal" => new Policy_Normal(),
"uniform" => new Policy_Uniform(),
"exponential" => new Policy_Exponential(),
"triangular" => new Policy_Triangular(),
_ => new Policy_Constant()
};
// jo 내용을 target에 채워 넣음
serializer.Populate(jo.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// 필요 시 구현 (서버로 다시 보낼 때)
serializer.Serialize(writer, value);
}
}
public static class PolicyFactory
{
public static Policy_Base Create(string typeLower)
{
switch (typeLower)
{
case "Constant": return new Policy_Constant { type = "constant", value = 0f };
case "Normal": return new Policy_Normal { type = "normal", mean = 0f, stddev = 0f };
case "Uniform": return new Policy_Uniform { type = "uniform", min_val = 0f, max_val = 0f };
case "Exponential": return new Policy_Exponential { type = "exponential", mean = 0f };
case "Triangular": return new Policy_Triangular { type = "triangular", min_val = 0f, mode = 0f, max_val = 0f };
default: throw new ArgumentException($"Unknown policy type: {typeLower}");
}
}
public static Policy_Base ChangeType(Policy_Base oldPolicy, string newTypeLower)
{
var next = Create(newTypeLower);
// 의미 있는 값 일부 이관(원하시는 정책 변환 규칙으로 조정)
// “대표값”을 mean 계열로 최대한 유지하는 예시입니다.
float representative = oldPolicy switch
{
Policy_Exponential e => e.mean,
Policy_Normal n => n.mean,
Policy_Constant c => c.value,
Policy_Triangular t => t.mode,
Policy_Uniform u => (u.min_val + u.max_val) * 0.5f,
_ => 0f
};
switch (next)
{
case Policy_Exponential e: e.mean = representative; break;
case Policy_Normal n: n.mean = representative; n.stddev = 1f; break;
case Policy_Constant c: c.value = representative; break;
case Policy_Uniform u:
u.min_val = Mathf.Max(0f, representative - 0.5f);
u.max_val = representative + 0.5f;
break;
case Policy_Triangular t:
t.mode = representative;
t.min_val = Mathf.Max(0f, representative - 1f);
t.max_val = representative + 1f;
break;
}
return next;
}
}

View File

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

View File

@@ -0,0 +1,63 @@
using Simulator.Data;
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
using static Unity.VisualScripting.Member;
public class ConveyorProperty : MonoBehaviour
{
[SerializeField]
private PropertyWindow propertyWindow;
private void Awake()
{
//propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
}
public void SetProertyWindow(ConveyorPath data)
{
Debug.Log(data);
InitConveyorProperty(data);
}
void SaveChange(object source, object value, string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
//UpdateValueStack.Save();
}
public void InitConveyorProperty(ConveyorPath conveyor)
{
List<IPropertyEntry> entries = new List<IPropertyEntry>
{
new StringProperty("name", "이름", conveyor.name)
{
IsReadOnly = true
},
new StringProperty("from_node", "시작 노드", conveyor.from_node)
{
IsReadOnly = true
},
new StringProperty("to_node", "도착 노드", conveyor.to_node)
{
IsReadOnly = true
},
new FloatProperty("speed", "컨베이어 속도", conveyor.speed)
{
IsReadOnly = false
}.Bind(
setter: v => {conveyor.speed = v;SaveChange(conveyor,v,"speed"); }
),
new IntProperty("capacity", "최대 수용 개체 수", conveyor.capacity)
{
IsReadOnly = false
}.Bind(
setter: v => {conveyor.capacity = v;SaveChange(conveyor,v,"capacity"); }
),
};
propertyWindow.LoadMixedProperties(entries);
}
}

View File

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

View File

@@ -0,0 +1,16 @@
using UnityEngine;
public class NodeProperty : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
}

View File

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

View File

@@ -0,0 +1,101 @@
using Simulator.Data;
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
public class ProcessorProperty : MonoBehaviour
{
[SerializeField]
private PropertyWindow propertyWindow;
private void Awake()
{
propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
}
public void SetProertyWindow(ComponentType type, ComponentDataBase data)
{
Debug.Log(data);
switch (type)
{
case ComponentType.Processor:
InitSourceProperty(data as ProcessorDataClass);
break;
}
}
void SaveChange(object source, object value, string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
}
public void InitSourceProperty(ProcessorDataClass processor)
{
List<IPropertyEntry> entries = new List<IPropertyEntry>
{
new StringProperty("name", "이름", processor.name)
{
IsReadOnly = true
},
new StringProperty("label", "라벨", processor.label)
{
IsReadOnly = false
}.Bind(
setter: v => { processor.label = v; SaveChange(processor, v, "label"); }
),
new Vector2Property("Position", "X,Y 좌표(m)", new Vector2(processor.physical.position.x, processor.physical.position.z))
{
IsReadOnly = false
}.Bind(
setter: v => { processor.physical.position.x = v.x; processor.physical.position.z = v.y; SaveChange(processor, v, "physical.position"); }
),
new ListProperty("processing_type","처리 유형",new List<string>(){ "combine","seperate","transform" },processor.processor_type)
{
IsReadOnly = false
}.Bind(
setter: v => { processor.processor_type=v; SaveChange(processor, v, "processing_type"); }
),
CreateDynamicVisibilityTestGroup()
};
propertyWindow.LoadMixedProperties(entries);
}
private void OnPropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
Debug.Log($"[PropertyChanged] Id:{e.PropertyId}, Type:{e.PropertyType}, Value:{e.NewValue}");
// 동적 그룹 표시/비표시 테스트
if (e.PropertyId == "display_mode_TimePolicy")
{
string selectedMode = e.NewValue.ToString();
// HandleDisplayModeChanged_TimePolicy(selectedMode);
}
if (e.PropertyId == "display_mode_CountPolicy")
{
string selectedMode = e.NewValue.ToString();
//HandleDisplayModeChanged_CountPolicy(selectedMode);
}
if (e.PropertyId == "display_mode_DefectPolicy")
{
string selectedMode = e.NewValue.ToString();
//HandleDisplayModeChanged_DefectPolicy(selectedMode);
}
}
private PropertyGroup CreateDynamicVisibilityTestGroup()
{
var group = new PropertyGroup("dynamic_test", "투입 개체", isExpanded: true);
int count = 0;
foreach(var prefab in PrefabManager.Instance.prefabDict)
{
var property = new BoolProperty($"prefab{count}", prefab.Key, false);
group.AddItem(property);
}
return group;
}
}

View File

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

View File

@@ -1,63 +0,0 @@
using Simulator.Data;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
public class SimulatorProperty : MonoBehaviour
{
public Dictionary<ComponentType, List<IPropertyItem>> propertyDict = new Dictionary<ComponentType, List<IPropertyItem>>();
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
public void SetProertyWindow(ComponentType type,ComponentDataBase data)
{
Debug.Log(data);
switch (type)
{
case ComponentType.Source:
InitSourceProperty(data as SourceDataClass);
break;
}
}
public void InitSourceProperty(SourceDataClass source)
{
PropertyWindow.Instance.LoadProperties(
new List<IPropertyItem>()
{
new StringProperty("name", "이름", source.name)
{
IsReadOnly=true
},
new StringProperty("label", "라벨", source.label)
{
IsReadOnly=false
},
new Vector2Property("Position","X,Y 좌표(m)", new Vector2(source.physical.position.x,source.physical.position.z))
{
IsReadOnly=false
},
new ListProperty("prefabType","생성될 개체 유형",new List<string>(PrefabManager.Instance.prefabDict.Keys),source.prefab)
{
IsReadOnly=false
}
}
);
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
{
PathIndexer.Build(ComponentsManager.Instance.logicDetailData);
var path = PathIndexer.GetNodePath(source);
Debug.Log($"{path}.{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
Patch updateData = new Patch();
updateData.value = e.NewValue;
UpdateValueStack.AddPatch($"{path}.{e.PropertyId}", e.NewValue);
UpdateValueStack.Save();
//CursorManager.Instance.SetCursor(CursorType.HandPoint);
};
}
}

View File

@@ -0,0 +1,58 @@
using Simulator.Data;
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
public class SinkProperty : MonoBehaviour
{
[SerializeField]
private PropertyWindow propertyWindow;
private void Awake()
{
//propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
}
public void SetProertyWindow(ComponentType type, ComponentDataBase data)
{
Debug.Log(data);
switch (type)
{
case ComponentType.Sink:
InitSinkProperty(data as SinkDataClass);
break;
}
}
void SaveChange(object source, object value, string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
}
public void InitSinkProperty(SinkDataClass sink)
{
List<IPropertyEntry> entries = new List<IPropertyEntry>
{
new StringProperty("name", "이름", sink.name)
{
IsReadOnly=true
},
new StringProperty("label", "라벨", sink.label)
{
IsReadOnly=false
}.Bind(
setter: v => {sink.label = v;SaveChange(sink,v,"label"); }
),
new Vector2Property("Position","X,Y 좌표(m)", new Vector2(sink.physical.position.x,sink.physical.position.z))
{
IsReadOnly=false
}.Bind(
setter: v => {sink.physical.position.x = v.x;sink.physical.position.z = v.y; SaveChange(sink,v,"physical.position"); }
)
};
propertyWindow.LoadMixedProperties(entries);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0768809a7d49096438588c00517df09b

View File

@@ -0,0 +1,492 @@
using Simulator.Data;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
public enum SpawnPolicy
{
Constant,
Normal,
Uniform,
Exponential,
Triangular
}
public class SourceProperty : MonoBehaviour
{
[SerializeField]
private PropertyWindow propertyWindow;
private void Awake()
{
propertyWindow.PropertyValueChanged += OnPropertyValueChanged;
}
public void SetProertyWindow(ComponentType type, ComponentDataBase data)
{
Debug.Log(data);
switch (type)
{
case ComponentType.Source:
InitSourceProperty(data as SourceDataClass);
break;
}
}
void SaveChange(object source,object value,string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
//UpdateValueStack.Save();
}
public void InitSourceProperty(SourceDataClass source)
{
List<IPropertyEntry> entries = new List<IPropertyEntry>
{
new StringProperty("name", "이름", source.name)
{
IsReadOnly=true
},
new StringProperty("label", "라벨", source.label)
{
IsReadOnly=false
}.Bind(
setter: v => {source.label = v;SaveChange(source,v,"label"); }
),
new Vector2Property("Position","X,Y 좌표(m)", new Vector2(source.physical.position.x,source.physical.position.z))
{
IsReadOnly=false
}.Bind(
setter: v => {source.physical.position.x = v.x;source.physical.position.z = v.y; SaveChange(source,v,"physical.position"); }
),
new ListProperty("prefabType","생성될 개체 유형",new List<string>(PrefabManager.Instance.prefabDict.Keys),source.prefab)
{
IsReadOnly=false
}.Bind(
setter: v => {source.prefab=v; SaveChange(source,v,"prefab"); }
),
new EnumProperty("display_mode_TimePolicy", "생산 간격 정책",source.spawn_time_policy.policy).Bind(
setter: v=>{source.spawn_time_policy=PolicyFactory.Create(v.ToString());SaveChange(source,v.ToString(),"spawn_time_policy.type");}
),
CreateSpawnTimePolicy_Constant(source),
CreateSpawnTimePolicy_Normal(source),
CreateSpawnTimePolicy_Uniform(source),
CreateSpawnTimePolicy_Exponential(source),
CreateSpawnTimePolicy_Triangular(source),
new EnumProperty("display_mode_CountPolicy", "생산 개수 정책",source.spawn_count_policy.policy).Bind(
setter: v=>{source.spawn_count_policy=PolicyFactory.Create(v.ToString());SaveChange(source,v.ToString(),"spawn_count_policy.type");}
),
CreateSpawnCountPolicy_Constant(source),
CreateSpawnCountPolicy_Normal(source),
CreateSpawnCountPolicy_Uniform(source),
CreateSpawnCountPolicy_Exponential(source),
CreateSpawnCountPolicy_Triangular(source),
new BoolProperty("Is_Unlimited","무제한 모드",source.is_unlimited),
new EnumProperty("display_mode_DefectPolicy", "불량률 정책",source.defect_rate_policy.policy).Bind(
setter: v=>{source.defect_rate_policy=PolicyFactory.Create(v.ToString());SaveChange(source,v.ToString(),"defect_rate_policy.type");}
),
CreateSpawnDefectPolicy_Constant(source),
CreateSpawnDefectPolicy_Normal(source),
CreateSpawnDefectPolicy_Uniform(source),
CreateSpawnDefectPolicy_Exponential(source),
CreateSpawnDefectPolicy_Triangular(source),
};
propertyWindow.LoadMixedProperties(entries);
HandleDisplayModeChanged_TimePolicy(source.spawn_time_policy.policy.ToString());
HandleDisplayModeChanged_CountPolicy(source.spawn_count_policy.policy.ToString());
HandleDisplayModeChanged_DefectPolicy(source.defect_rate_policy.policy.ToString());
}
private void OnPropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
Debug.Log($"[PropertyChanged] Id:{e.PropertyId}, Type:{e.PropertyType}, Value:{e.NewValue}");
// 동적 그룹 표시/비표시 테스트
if (e.PropertyId == "display_mode_TimePolicy")
{
string selectedMode = e.NewValue.ToString();
HandleDisplayModeChanged_TimePolicy(selectedMode);
}
if (e.PropertyId == "display_mode_CountPolicy")
{
string selectedMode = e.NewValue.ToString();
HandleDisplayModeChanged_CountPolicy(selectedMode);
}
if (e.PropertyId == "display_mode_DefectPolicy")
{
string selectedMode = e.NewValue.ToString();
HandleDisplayModeChanged_DefectPolicy(selectedMode);
}
}
#region
private PropertyGroup CreateSpawnTimePolicy_Constant(SourceDataClass source)
{
var group = new PropertyGroup("SpawnTimePolicy_Constant", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnTimePolicy_Constant_Value", "상수 값", (source.spawn_time_policy as Policy_Constant) ?.value ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Constant).value = v;SaveChange(source,v,"Spawn_time_Policy.value"); }
),
});
return group;
}
private PropertyGroup CreateSpawnTimePolicy_Normal(SourceDataClass source)
{
var group = new PropertyGroup("SpawnTimePolicy_Normal", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnTimePolicy_Normal_Mean", "정규 분포 표준치", (source.spawn_time_policy as Policy_Normal) ?.mean ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Normal).mean = v;SaveChange(source,v,"Spawn_time_Policy.mean"); }
),
new FloatProperty("SpawnTimePolicy_Normal_Gap", "정규 분포 표준 편차", (source.spawn_time_policy as Policy_Normal) ?.stddev ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Normal).stddev = v;SaveChange(source,v,"Spawn_time_Policy.stddev"); }
),
});
return group;
}
private PropertyGroup CreateSpawnTimePolicy_Uniform(SourceDataClass source)
{
var group = new PropertyGroup("SpawnTimePolicy_Uniform", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnTimePolicy_Uniform_Min", "균등 분포 최소값", (source.spawn_time_policy as Policy_Uniform) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Uniform).min_val = v;SaveChange(source,v,"Spawn_time_Policy.min_val"); }
),
new FloatProperty("SpawnTimePolicy_Uniform_Max", "균등 분포 최대값", (source.spawn_time_policy as Policy_Uniform) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Uniform).max_val = v;SaveChange(source,v,"Spawn_time_Policy.max_val"); }
),
});
return group;
}
private PropertyGroup CreateSpawnTimePolicy_Exponential(SourceDataClass source)
{
var group = new PropertyGroup("SpawnTimePolicy_Exponential", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnTimePolicy_Exponential_Mean", "지수 분포 평균치", (source.spawn_time_policy as Policy_Exponential) ?.mean ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Exponential).mean = v;SaveChange(source,v,"Spawn_time_Policy.mean"); }
),
});
return group;
}
private PropertyGroup CreateSpawnTimePolicy_Triangular(SourceDataClass source)
{
var group = new PropertyGroup("SpawnTimePolicy_Triangular", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnTimePolicy_Triangular_Min", "지수 분포 최소값", (source.spawn_time_policy as Policy_Triangular) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Triangular).min_val = v;SaveChange(source,v,"Spawn_time_Policy.min_val"); }
),
new FloatProperty("SpawnTimePolicy_Triangular_Mean", "지수 분포 최빈값", (source.spawn_time_policy as Policy_Triangular) ?.mode ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Triangular).mode = v;SaveChange(source,v,"Spawn_time_Policy.mode"); }
),
new FloatProperty("SpawnTimePolicy_Triangular_Max", "지수 분포 최대값", (source.spawn_time_policy as Policy_Triangular) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_time_policy as Policy_Triangular).max_val = v;SaveChange(source,v,"Spawn_time_Policy.max_val"); }
),
});
return group;
}
private void HandleDisplayModeChanged_TimePolicy(string mode)
{
// 모든 조건부 그룹 숨김
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Constant", false);
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Normal", false);
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Uniform", false);
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Exponential", false);
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Triangular", false);
// 선택된 모드에 따라 해당 그룹만 표시
switch (mode)
{
case "Constant":
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Constant", true);
break;
case "Normal":
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Normal", true);
break;
case "Uniform":
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Uniform", true);
break;
case "Exponential":
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Exponential", true);
break;
case "Triangular":
propertyWindow.SetGroupVisibility("SpawnTimePolicy_Triangular", true);
break;
}
}
#endregion
#region
private PropertyGroup CreateSpawnCountPolicy_Constant(SourceDataClass source)
{
var group = new PropertyGroup("SpawnCountPolicy_Constant", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnCountPolicy_Constant_Value", "상수 값", (source.spawn_count_policy as Policy_Constant)?.value??0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Constant).value = v;SaveChange(source,v,"spawn_count_policy.value"); }
),
});
return group;
}
private PropertyGroup CreateSpawnCountPolicy_Normal(SourceDataClass source)
{
var group = new PropertyGroup("SpawnCountPolicy_Normal", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnCountPolicy_Normal_Mean", "정규 분포 표준치", (source.spawn_count_policy as Policy_Normal) ?.mean ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Normal).mean = v;SaveChange(source,v,"spawn_count_policy.mean"); }
),
new FloatProperty("SpawnCountPolicy_Normal_Gap", "정규 분포 표준 편차", (source.spawn_count_policy as Policy_Normal) ?.stddev ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Normal).stddev = v;SaveChange(source,v,"spawn_count_policy.stddev"); }
),
});
return group;
}
private PropertyGroup CreateSpawnCountPolicy_Uniform(SourceDataClass source)
{
var group = new PropertyGroup("SpawnCountPolicy_Uniform", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnCountPolicy_Uniform_Min", "균등 분포 최소값", (source.spawn_count_policy as Policy_Uniform) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Uniform).min_val = v;SaveChange(source,v,"spawn_count_policy.min_val"); }
),
new FloatProperty("SpawnCountPolicy_Uniform_Max", "균등 분포 최대값", (source.spawn_count_policy as Policy_Uniform) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Uniform).max_val = v;SaveChange(source,v,"spawn_count_policy.max_val"); }
),
});
return group;
}
private PropertyGroup CreateSpawnCountPolicy_Exponential(SourceDataClass source)
{
var group = new PropertyGroup("SpawnCountPolicy_Exponential", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnCountPolicy_Exponential_Mean", "지수 분포 평균치", (source.spawn_count_policy as Policy_Exponential) ?.mean ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Exponential).mean = v;SaveChange(source,v,"spawn_count_policy.mean"); }
),
});
return group;
}
private PropertyGroup CreateSpawnCountPolicy_Triangular(SourceDataClass source)
{
var group = new PropertyGroup("SpawnCountPolicy_Triangular", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnCountPolicy_Triangular_Min", "지수 분포 최소값", (source.spawn_count_policy as Policy_Triangular) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Triangular).min_val = v;SaveChange(source,v,"spawn_count_policy.min_val"); }
),
new FloatProperty("SpawnCountPolicy_Triangular_Mean", "지수 분포 최빈값", (source.spawn_count_policy as Policy_Triangular) ?.mode ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Triangular).mode = v;SaveChange(source,v,"spawn_count_policy.mode"); }
),
new FloatProperty("SpawnCountPolicy_Triangular_Max", "지수 분포 최대값", (source.spawn_count_policy as Policy_Triangular) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.spawn_count_policy as Policy_Triangular).max_val = v;SaveChange(source,v,"spawn_count_policy.max_val"); }
),
});
return group;
}
private void HandleDisplayModeChanged_CountPolicy(string mode)
{
// 모든 조건부 그룹 숨김
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Constant", false);
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Normal", false);
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Uniform", false);
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Exponential", false);
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Triangular", false);
// 선택된 모드에 따라 해당 그룹만 표시
switch (mode)
{
case "Constant":
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Constant", true);
break;
case "Normal":
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Normal", true);
break;
case "Uniform":
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Uniform", true);
break;
case "Exponential":
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Exponential", true);
break;
case "Triangular":
propertyWindow.SetGroupVisibility("SpawnCountPolicy_Triangular", true);
break;
}
}
#endregion
#region
private PropertyGroup CreateSpawnDefectPolicy_Constant(SourceDataClass source)
{
var group = new PropertyGroup("SpawnDefectPolicy_Constant", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnDefectPolicy_Constant_Value", "상수 값", (source.defect_rate_policy as Policy_Constant)?.value??0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Constant).value = v;SaveChange(source,v,"defect_rate_policy.value"); }
),
});
return group;
}
private PropertyGroup CreateSpawnDefectPolicy_Normal(SourceDataClass source)
{
var group = new PropertyGroup("SpawnDefectPolicy_Normal", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnDefectPolicy_Normal_Mean", "정규 분포 표준치", (source.defect_rate_policy as Policy_Normal)?.mean??0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Normal).mean = v;SaveChange(source,v,"defect_rate_policy.mean"); }
),
new FloatProperty("SpawnDefectPolicy_Normal_Gap", "정규 분포 표준 편차", (source.defect_rate_policy as Policy_Normal)?.stddev??0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Normal).stddev = v;SaveChange(source,v,"defect_rate_policy.stddev"); }
),
});
return group;
}
private PropertyGroup CreateSpawnDefectPolicy_Uniform(SourceDataClass source)
{
var group = new PropertyGroup("SpawnDefectPolicy_Uniform", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnDefectPolicy_Uniform_Min", "균등 분포 최소값", (source.defect_rate_policy as Policy_Uniform) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Uniform).min_val = v;SaveChange(source,v,"defect_rate_policy.min_val"); }
),
new FloatProperty("SpawnDefectPolicy_Uniform_Max", "균등 분포 최대값", (source.defect_rate_policy as Policy_Uniform) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Uniform).max_val = v;SaveChange(source,v,"defect_rate_policy.max_val"); }
),
});
return group;
}
private PropertyGroup CreateSpawnDefectPolicy_Exponential(SourceDataClass source)
{
var group = new PropertyGroup("SpawnDefectPolicy_Exponential", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnDefectPolicy_Exponential_Mean", "지수 분포 평균치", (source.defect_rate_policy as Policy_Exponential) ?.mean ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Exponential).mean = v;SaveChange(source,v,"defect_rate_policy.mean"); }
),
});
return group;
}
private PropertyGroup CreateSpawnDefectPolicy_Triangular(SourceDataClass source)
{
var group = new PropertyGroup("SpawnDefectPolicy_Triangular", "", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("SpawnDefectPolicy_Triangular_Min", "지수 분포 최소값", (source.defect_rate_policy as Policy_Triangular) ?.min_val ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Triangular).min_val = v;SaveChange(source,v,"defect_rate_policy.min_val"); }
),
new FloatProperty("SpawnDefectPolicy_Triangular_Mean", "지수 분포 최빈값", (source.defect_rate_policy as Policy_Triangular) ?.mode ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Triangular).mode = v;SaveChange(source,v,"defect_rate_policy.mode"); }
),
new FloatProperty("SpawnDefectPolicy_Triangular_Max", "지수 분포 최대값", (source.defect_rate_policy as Policy_Triangular) ?.max_val ?? 0f)
{
}.Bind(
setter: v => {(source.defect_rate_policy as Policy_Triangular).max_val = v;SaveChange(source,v,"defect_rate_policy.max_val"); }
),
});
return group;
}
private void HandleDisplayModeChanged_DefectPolicy(string mode)
{
// 모든 조건부 그룹 숨김
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Constant", false);
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Normal", false);
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Uniform", false);
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Exponential", false);
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Triangular", false);
// 선택된 모드에 따라 해당 그룹만 표시
switch (mode)
{
case "Constant":
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Constant", true);
break;
case "Normal":
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Normal", true);
break;
case "Uniform":
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Uniform", true);
break;
case "Exponential":
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Exponential", true);
break;
case "Triangular":
propertyWindow.SetGroupVisibility("SpawnDefectPolicy_Triangular", true);
break;
}
}
#endregion
}

View File

@@ -27,6 +27,9 @@ namespace Simulator
public event Action<LogicDetailData> onRequestDataReceived;
[SerializeField]
private PropertyWindow propertyWindow;
/// <summary>
/// 초기 화 메서드입니다.
/// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다.
@@ -103,8 +106,8 @@ namespace Simulator
{
new MenuItemData("explorer_window", "탐색창", new ActionCommand(()=> Debug.Log("탐색창"))),
new MenuItemData("property_window", "속성창", new ActionCommand(()=> {
if(PropertyWindow.Instance.IsVisible) PropertyWindow.Instance.Hide();
else PropertyWindow.Instance.Show();
if(propertyWindow.IsVisible) propertyWindow.Hide();
else propertyWindow.Show();
})),
}),
new MenuItemData("help", "도움말", subMenuItems: new List<MenuItemData>
@@ -118,7 +121,7 @@ namespace Simulator
private void SetupPropertyWindow()
{
PropertyWindow.Instance.LoadProperties(new List<IPropertyItem>
propertyWindow.LoadProperties(new List<IPropertyItem>
{
new StringProperty("prop1", "String Property", "Initial Value")
{
@@ -218,7 +221,7 @@ namespace Simulator
}
});
PropertyWindow.Instance.PropertyValueChanged += (sender, e) =>
propertyWindow.PropertyValueChanged += (sender, e) =>
{
Debug.Log($"Property Id:{e.PropertyId}, type:{e.PropertyType}, newValue: {e.NewValue}");
};

View File

@@ -1,8 +1,11 @@
using Newtonsoft.Json;
using NUnit.Framework.Constraints;
using Simulator.Config;
using Simulator.Data;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Network;
[Serializable]
public class Patches
@@ -29,7 +32,7 @@ public class UpdateValueStack : MonoBehaviour
patches.Add(path, value);
}
}
public static void Save()
public static async void Save()
{
Patches change = new Patches();
foreach (var patch in patches)
@@ -41,7 +44,9 @@ public class UpdateValueStack : MonoBehaviour
}
var data=JsonConvert.SerializeObject(change);
Debug.Log(data);
var request = await HttpRequester.Request_<Totaljson>($"{Constants.HTTP_DOMAIN}/simulation/logics/{SimulationConfig.logicId}/data", data, methodString:"patch", null,true);
Debug.Log(request.code);
patches.Clear();
}
}

View File

@@ -14,8 +14,8 @@ namespace Simulator
{
onParameterRecived += ComponentsManager.Instance.testRequest;
#if UNITY_EDITOR
SimulationConfig.projectId = 278;
SimulationConfig.logicId = 292;
SimulationConfig.projectId = 292;
SimulationConfig.logicId = 306;
onParameterRecived?.Invoke();
#else
Application.ExternalCall("loadingComplete");

View File

@@ -205,6 +205,11 @@ namespace UVC.Network
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
}
public static async UniTask<T> RequestPut<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
{
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "put", header, useAuth);
}
/// <summary>
/// HTTP 요청을 처리하는 내부 메소드
/// </summary>
@@ -223,15 +228,15 @@ namespace UVC.Network
/// <param name="header">추가할 헤더 정보</param>
/// <param name="useAuth">인증 토큰 사용 여부</param>
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
private static async UniTask<T> Request_<T>(string url, string body = null, string methodString = "post", Dictionary<string, string> header = null, bool useAuth = false)
public static async UniTask<T> Request_<T>(string url, string body = null, string methodString = "post", Dictionary<string, string> header = null, bool useAuth = false)
{
HTTPMethods method = StringToMethod(methodString);
Debug.Log(method);
if (!url.Contains("http")) url = $"{Domain}{url}";
var request = SelectHTTPRequest(method, url);
request.DownloadSettings = new Best.HTTP.Request.Settings.DownloadSettings() { ContentStreamMaxBuffered = 1024 * 1024 * 200 };
request.MethodType = method;
request.SetHeader("Content-Type", "application/json; charset=utf-8");
@@ -643,13 +648,16 @@ namespace UVC.Network
switch (methods)
{
case HTTPMethods.Get:
if (onRequest != null)
return HTTPRequest.CreateGet(url, onRequest);
else
return HTTPRequest.CreateGet(url);
case HTTPMethods.Post:
return HTTPRequest.CreatePost(url);
case HTTPMethods.Put:
return HTTPRequest.CreatePut(url);
case HTTPMethods.Patch:
return HTTPRequest.CreatePatch(url);
}
return null;
@@ -667,6 +675,8 @@ namespace UVC.Network
{
"get" => HTTPMethods.Get,
"post" => HTTPMethods.Post,
"put" => HTTPMethods.Put,
"patch"=>HTTPMethods.Patch,
_ => throw new ArgumentException($"Unsupported HTTP method: {method}"),
};
}

View File

@@ -0,0 +1,17 @@
using Simulator;
using Simulator.Config;
using System;
using UnityEngine;
using UVC.UI.Commands;
public class SimulatorSaveCommand : ICommand
{
public SimulatorSaveCommand(object? parameter = null)
{
}
public async void Execute(object? parameter = null)
{
UpdateValueStack.Save();
}
}

View File

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

View File

@@ -0,0 +1,17 @@
using Simulator.UI.Commands;
using UnityEngine;
using UVC.UI.Commands.Mono;
public class SimulatorSaveCommandMono : MonoBehaviourCommand
{
SimulatorSaveCommand command;
private void Start()
{
command = new SimulatorSaveCommand();
}
public override void Execute()
{
Debug.Log("asdf");
command.Execute();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 592e9e72ea21a984599385e593d4a596

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

@@ -11,5 +11,11 @@ namespace UVC.UI.Window.PropertyWindow
/// <param name="item">표시할 속성 데이터</param>
/// <param name="controller">상호작용할 컨트롤러</param>
void Setup(IPropertyItem item, PropertyWindow controller);
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
void SetReadOnly(bool isReadOnly);
}
}

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,11 +131,23 @@ 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>
protected T _value;
private Action<T>? _setter;
/// <summary>
/// 속성의 현재 값 (제네릭 타입)
/// </summary>
@@ -145,6 +164,12 @@ namespace UVC.UI.Window.PropertyWindow
_value = initialValue;
}
public PropertyItem<T> Bind(Action<T>? setter)
{
_setter = setter; // 읽기전용이면 null 가능
return this;
}
// IPropertyItem 인터페이스 구현
public object GetValue() => _value;
public void SetValue(object value)
@@ -153,6 +178,10 @@ namespace UVC.UI.Window.PropertyWindow
if (value is T typedValue)
{
_value = typedValue;
if (_setter != null)
{
_setter(typedValue);
}
}
else
{

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,63 @@ namespace UVC.UI.Window.PropertyWindow
if (_controller != null)
{
_controller.PropertyValueChanged -= OnPropertyValueChanged;
_controller.GroupExpandedChanged -= OnGroupExpandedChanged;
}
// 캐시 정리
_groupViews.Clear();
_itemViews.Clear();
}
#region Property/Group Visibility
/// <summary>
/// 특정 속성 아이템의 가시성을 설정합니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="visible">가시성 여부</param>
public void SetPropertyVisibility(string propertyId, bool visible)
{
if (_itemViews.TryGetValue(propertyId, out var itemView) && itemView != null)
{
itemView.SetActive(visible);
}
}
/// <summary>
/// 특정 그룹의 가시성을 설정합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="visible">가시성 여부</param>
public void SetGroupVisibility(string groupId, bool visible)
{
if (_groupViews.TryGetValue(groupId, out var groupView) && groupView != null)
{
groupView.gameObject.SetActive(visible);
}
}
#endregion
#region Property/Group ReadOnly
/// <summary>
/// 특정 속성 아이템의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public void SetPropertyReadOnly(string propertyId, bool isReadOnly)
{
if (_itemViews.TryGetValue(propertyId, out var itemView) && itemView != null)
{
var propertyUI = itemView.GetComponent<IPropertyUI>();
if (propertyUI != null)
{
propertyUI.SetReadOnly(isReadOnly);
}
}
}
#endregion
}
}

View File

@@ -4,44 +4,421 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Core;
using UVC.Factory;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 속성 데이터를 관리하고, 데이터 변경 시 이벤트를 발생시키는 컨트롤러 클래스입니다.
/// Model과 View 사이의 중재자 역할을 합니다.
/// 그룹과 개별 아이템을 혼용하여 사용할 수 있습니다.
/// </summary>
public class PropertyWindow: SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
public class PropertyWindow : MonoBehaviour
{
[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;
// 이벤트를 통해 PropertyView.OnGroupExpandedChanged()가 호출되어
// 부분적으로 UI를 업데이트합니다. 전체 Refresh()는 스크롤 위치를 초기화하므로 호출하지 않습니다.
GroupExpandedChanged?.Invoke(this, new PropertyGroupExpandedEventArgs(group, wasExpanded, isExpanded));
}
}
}
/// <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 +428,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 +504,195 @@ namespace UVC.UI.Window.PropertyWindow
public void Hide()
{
gameObject.SetActive(false);
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
}
public void ToggleVisibility()
{
gameObject.SetActive(!gameObject.activeSelf);
}
public void SetVisibility(bool visible)
{
gameObject.SetActive(visible);
}
#endregion
#region Property/Group Visibility
/// <summary>
/// 특정 속성 아이템의 가시성을 설정합니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="visible">가시성 여부</param>
public void SetPropertyVisibility(string propertyId, bool visible)
{
if (_view != null)
{
_view.SetPropertyVisibility(propertyId, visible);
}
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
/// UI와 상호작용하는 동안 3D 뷰의 카메라가 움직이지 않도록 컨트롤러를 비활성화합니다.
/// 여러 속성 아이템의 가시성을 한번에 설정합니다.
/// </summary>
public virtual void OnPointerEnter(PointerEventData eventData)
/// <param name="propertyIds">속성 아이템 ID 목록</param>
/// <param name="visible">가시성 여부</param>
public void SetPropertiesVisibility(IEnumerable<string> propertyIds, bool visible)
{
FactoryCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
if (_view != null && propertyIds != null)
{
foreach (var propertyId in propertyIds)
{
_view.SetPropertyVisibility(propertyId, visible);
}
}
}
/// <summary>
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
/// 카메라 컨트롤을 다시 활성화하여 3D 뷰를 조작할 수 있도록 합니다.
/// 특정 그룹의 가시성을 설정합니다.
/// </summary>
public virtual void OnPointerExit(PointerEventData eventData)
/// <param name="groupId">그룹 ID</param>
/// <param name="visible">가시성 여부</param>
public void SetGroupVisibility(string groupId, bool visible)
{
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
if (_view != null)
{
_view.SetGroupVisibility(groupId, visible);
}
}
/// <summary>
/// 여러 그룹의 가시성을 한번에 설정합니다.
/// </summary>
/// <param name="groupIds">그룹 ID 목록</param>
/// <param name="visible">가시성 여부</param>
public void SetGroupsVisibility(IEnumerable<string> groupIds, bool visible)
{
if (_view != null && groupIds != null)
{
foreach (var groupId in groupIds)
{
_view.SetGroupVisibility(groupId, visible);
}
}
}
#endregion
#region Property/Group ReadOnly (Enable/Disable)
/// <summary>
/// 특정 속성 아이템의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetPropertyReadOnly(string propertyId, bool isReadOnly)
{
if (_itemIndex.TryGetValue(propertyId, out var item))
{
item.IsReadOnly = isReadOnly;
if (_view != null)
{
_view.SetPropertyReadOnly(propertyId, isReadOnly);
}
}
}
/// <summary>
/// 특정 속성 아이템의 활성화 상태를 설정합니다.
/// SetPropertyReadOnly의 반대 동작입니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="enabled">활성화 여부 (true: 활성화, false: 비활성화)</param>
public void SetPropertyEnabled(string propertyId, bool enabled)
{
SetPropertyReadOnly(propertyId, !enabled);
}
/// <summary>
/// 여러 속성 아이템의 읽기 전용 상태를 한번에 설정합니다.
/// </summary>
/// <param name="propertyIds">속성 아이템 ID 목록</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public void SetPropertiesReadOnly(IEnumerable<string> propertyIds, bool isReadOnly)
{
if (propertyIds != null)
{
foreach (var propertyId in propertyIds)
{
SetPropertyReadOnly(propertyId, isReadOnly);
}
}
}
/// <summary>
/// 여러 속성 아이템의 활성화 상태를 한번에 설정합니다.
/// </summary>
/// <param name="propertyIds">속성 아이템 ID 목록</param>
/// <param name="enabled">활성화 여부</param>
public void SetPropertiesEnabled(IEnumerable<string> propertyIds, bool enabled)
{
SetPropertiesReadOnly(propertyIds, !enabled);
}
/// <summary>
/// 특정 그룹 내 모든 속성 아이템의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public void SetGroupReadOnly(string groupId, bool isReadOnly)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
foreach (var item in group.Items)
{
item.IsReadOnly = isReadOnly;
if (_view != null)
{
_view.SetPropertyReadOnly(item.Id, isReadOnly);
}
}
}
}
/// <summary>
/// 특정 그룹 내 모든 속성 아이템의 활성화 상태를 설정합니다.
/// SetGroupReadOnly의 반대 동작입니다.
/// </summary>
/// <param name="groupId">그룹 ID</param>
/// <param name="enabled">활성화 여부</param>
public void SetGroupEnabled(string groupId, bool enabled)
{
SetGroupReadOnly(groupId, !enabled);
}
/// <summary>
/// 여러 그룹 내 모든 속성 아이템의 읽기 전용 상태를 한번에 설정합니다.
/// </summary>
/// <param name="groupIds">그룹 ID 목록</param>
/// <param name="isReadOnly">읽기 전용 여부</param>
public void SetGroupsReadOnly(IEnumerable<string> groupIds, bool isReadOnly)
{
if (groupIds != null)
{
foreach (var groupId in groupIds)
{
SetGroupReadOnly(groupId, isReadOnly);
}
}
}
/// <summary>
/// 여러 그룹 내 모든 속성 아이템의 활성화 상태를 한번에 설정합니다.
/// </summary>
/// <param name="groupIds">그룹 ID 목록</param>
/// <param name="enabled">활성화 여부</param>
public void SetGroupsEnabled(IEnumerable<string> groupIds, bool enabled)
{
SetGroupsReadOnly(groupIds, !enabled);
}
#endregion
}
}

View File

@@ -95,6 +95,22 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_valueToggle != null)
{
_valueToggle.interactable = !isReadOnly;
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// 메모리 누수를 방지하기 위해 등록된 리스너를 제거합니다.

View File

@@ -130,6 +130,20 @@ namespace UVC.UI.Window.PropertyWindow.UI
openningColorPickered = false;
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_colorLabel != null) _colorLabel.interactable = !isReadOnly;
if (_colorPickerButton != null) _colorPickerButton.gameObject.SetActive(!isReadOnly);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -110,6 +110,24 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newDate);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_dateText != null) _dateText.interactable = !isReadOnly;
if (_datePickerButton != null)
{
_datePickerButton.interactable = !isReadOnly;
_datePickerButton.gameObject.SetActive(!isReadOnly);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -147,6 +147,30 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_startDateText != null) _startDateText.interactable = !isReadOnly;
if (_endDateText != null) _endDateText.interactable = !isReadOnly;
if (_startDatePickerButton != null)
{
_startDatePickerButton.interactable = !isReadOnly;
_startDatePickerButton.gameObject.SetActive(!isReadOnly);
}
if (_endDatePickerButton != null)
{
_endDatePickerButton.interactable = !isReadOnly;
_endDatePickerButton.gameObject.SetActive(!isReadOnly);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -172,6 +172,26 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newDateTime);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_dateText != null) _dateText.interactable = !isReadOnly;
if (_dateTimePickerButton != null)
{
_dateTimePickerButton.interactable = !isReadOnly;
_dateTimePickerButton.gameObject.SetActive(!isReadOnly);
}
if (_hourDropDown != null) _hourDropDown.interactable = !isReadOnly;
if (_miniteDropDown != null) _miniteDropDown.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -275,6 +275,34 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_startDateText != null) _startDateText.interactable = !isReadOnly;
if (_endDateText != null) _endDateText.interactable = !isReadOnly;
if (_startDatePickerButton != null)
{
_startDatePickerButton.interactable = !isReadOnly;
_startDatePickerButton.gameObject.SetActive(!isReadOnly);
}
if (_endDatePickerButton != null)
{
_endDatePickerButton.interactable = !isReadOnly;
_endDatePickerButton.gameObject.SetActive(!isReadOnly);
}
if (_startHourDropDown != null) _startHourDropDown.interactable = !isReadOnly;
if (_startMiniteDropDown != null) _startMiniteDropDown.interactable = !isReadOnly;
if (_endHourDropDown != null) _endHourDropDown.interactable = !isReadOnly;
if (_endMiniteDropDown != null) _endMiniteDropDown.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -110,6 +110,19 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_dropdown != null) _dropdown.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -107,6 +107,19 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_dropdown != null) _dropdown.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -195,6 +195,26 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, v);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_valueInput != null)
{
_valueInput.interactable = !isReadOnly;
}
if (_slider != null)
{
_slider.interactable = !isReadOnly;
}
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.

View File

@@ -181,6 +181,20 @@ namespace UVC.UI.Window.PropertyWindow.UI
_minInputField.text = newMin.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_minInputField != null) _minInputField.interactable = !isReadOnly;
if (_maxInputField != null) _maxInputField.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -140,6 +140,25 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, selectedValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
foreach (var toggle in _toggles)
{
if (toggle != null)
{
toggle.interactable = !isReadOnly;
}
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -88,6 +88,22 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_valueInput != null)
{
_valueInput.interactable = !isReadOnly;
}
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.

View File

@@ -110,6 +110,20 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_xInputField != null) _xInputField.interactable = !isReadOnly;
if (_yInputField != null) _yInputField.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -121,6 +121,21 @@ namespace UVC.UI.Window.PropertyWindow.UI
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
if (_xInputField != null) _xInputField.interactable = !isReadOnly;
if (_yInputField != null) _yInputField.interactable = !isReadOnly;
if (_zInputField != null) _zInputField.interactable = !isReadOnly;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

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,255 @@
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();
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// 파생 클래스에서 재정의하여 구체적인 UI 요소에 적용해야 합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
public virtual void SetReadOnly(bool isReadOnly)
{
if (_propertyItem != null)
{
_propertyItem.IsReadOnly = isReadOnly;
}
// 파생 클래스에서 구체적인 UI 요소 비활성화 로직을 구현
ApplyReadOnlyStateToUI(isReadOnly);
}
/// <summary>
/// 파생 클래스에서 구체적인 UI 요소에 읽기 전용 상태를 적용합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부</param>
protected virtual void ApplyReadOnlyStateToUI(bool isReadOnly)
{
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
}
#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