using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; public static class PathIndexer { private sealed class NodeMeta { public object Parent; public string Segment; // 예: "production_system", "asrs[0]" } // 모델 클래스를 수정하지 않기 위해 외부 메타 테이블 사용 private static readonly ConditionalWeakTable _meta = new(); /// /// 루트부터 전체 그래프를 순회하며 (Parent, Segment) 메타를 구축합니다. /// 로직 데이터 로딩/디시리얼라이즈 직후 1회 호출을 권장합니다. /// public static void Build(object root) { if (root == null) throw new ArgumentNullException(nameof(root)); var visited = new HashSet(ReferenceEqualityComparer.Instance); Visit(root, parent: null, segment: null, visited); } /// /// 특정 객체 노드의 경로(노드까지)를 반환합니다. 예: "production_system.asrs[0]" /// public static string GetNodePath(object node, string rootPrefix = null) { if (node == null) throw new ArgumentNullException(nameof(node)); var segments = new List(); var cur = node; while (cur != null && _meta.TryGetValue(cur, out var m)) { if (!string.IsNullOrWhiteSpace(m.Segment)) segments.Add(m.Segment); cur = m.Parent; } segments.Reverse(); var path = string.Join(".", segments.Where(s => !string.IsNullOrWhiteSpace(s))); if (!string.IsNullOrWhiteSpace(rootPrefix)) { // 서버가 production_system[0] 같은 추가 프리픽스를 요구하면 여기서 붙이세요. // 예: rootPrefix = "production_system[0]" 또는 "data" 등 path = string.IsNullOrWhiteSpace(path) ? rootPrefix : $"{rootPrefix}.{path}"; } return path; } /// /// (변경된 객체) + (변경된 프로퍼티명)으로 최종 patch path를 만듭니다. /// 예: GetPropertyPath(asrs0, "name") => "production_system.asrs[0].name" /// public static string GetPropertyPath(object node, string propertyName, string rootPrefix = null) { if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException("propertyName is required.", nameof(propertyName)); var basePath = GetNodePath(node, rootPrefix); return string.IsNullOrWhiteSpace(basePath) ? propertyName : $"{basePath}.{propertyName}"; } // ----------------- 내부 구현 ----------------- private static void Visit(object node, object parent, string segment, HashSet visited) { if (node == null) return; var t = node.GetType(); if (IsTerminalType(t)) return; if (!visited.Add(node)) return; AttachMeta(node, parent, segment); // 1) 프로퍼티 순회(기존) var props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0); foreach (var p in props) { object child; try { child = p.GetValue(node); } catch { continue; } VisitChild(node, p.Name, child, visited); } // 2) 필드 순회(추가) var fields = t.GetFields(BindingFlags.Public | BindingFlags.Instance) .Where(f => !f.IsStatic); foreach (var f in fields) { object child; try { child = f.GetValue(node); } catch { continue; } VisitChild(node, f.Name, child, visited); } } private static void VisitChild(object parentNode, string memberName, object child, HashSet visited) { if (child == null) return; var ct = child.GetType(); if (IsTerminalType(ct)) return; // IList면 원소에 memberName[i] 형태로 세그먼트 부여 if (child is IList list && child is not string) { for (int i = 0; i < list.Count; i++) { var elem = list[i]; if (elem == null) continue; var et = elem.GetType(); if (IsTerminalType(et)) continue; Visit(elem, parent: parentNode, segment: $"{memberName}[{i}]", visited); } return; } // 일반 객체 Visit(child, parent: parentNode, segment: memberName, visited); } private static void AttachMeta(object node, object parent, string segment) { // ConditionalWeakTable은 같은 키 add가 안 되므로 교체 _meta.Remove(node); _meta.Add(node, new NodeMeta { Parent = parent, Segment = segment }); } private static bool IsTerminalType(Type t) { if (t.IsPrimitive || t.IsEnum) return true; if (t == typeof(string) || t == typeof(decimal)) return true; if (t == typeof(DateTime) || t == typeof(DateTimeOffset) || t == typeof(Guid) || t == typeof(TimeSpan)) return true; // Nullable 처리 var nt = Nullable.GetUnderlyingType(t); if (nt != null) return IsTerminalType(nt); return false; } private sealed class ReferenceEqualityComparer : IEqualityComparer { public static readonly ReferenceEqualityComparer Instance = new(); public new bool Equals(object x, object y) => ReferenceEquals(x, y); public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); } }