com.kylin.di 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.github/workflows/publish.yml +22 -0
  2. package/CHANGELOG.md +17 -0
  3. package/CLAUDE.md +58 -0
  4. package/LICENSE +21 -0
  5. package/README.md +704 -0
  6. package/Runtime/Attributes/InjectAttribute.cs +11 -0
  7. package/Runtime/Attributes/InjectAttribute.cs.meta +2 -0
  8. package/Runtime/Attributes/ViewModelAttribute.cs +17 -0
  9. package/Runtime/Attributes/ViewModelAttribute.cs.meta +2 -0
  10. package/Runtime/Attributes.meta +8 -0
  11. package/Runtime/Core/DIBehaviour.cs +70 -0
  12. package/Runtime/Core/DIBehaviour.cs.meta +2 -0
  13. package/Runtime/Core/LifetimeScope.cs +264 -0
  14. package/Runtime/Core/LifetimeScope.cs.meta +2 -0
  15. package/Runtime/Core.meta +8 -0
  16. package/Runtime/DI/DependencyBuilder.cs +114 -0
  17. package/Runtime/DI/DependencyBuilder.cs.meta +2 -0
  18. package/Runtime/DI/DependencyInjector.cs +104 -0
  19. package/Runtime/DI/DependencyInjector.cs.meta +2 -0
  20. package/Runtime/DI/InstanceFactory.cs +36 -0
  21. package/Runtime/DI/InstanceFactory.cs.meta +2 -0
  22. package/Runtime/DI/KDI.cs +54 -0
  23. package/Runtime/DI/KDI.cs.meta +2 -0
  24. package/Runtime/DI/Registration.cs +29 -0
  25. package/Runtime/DI/Registration.cs.meta +2 -0
  26. package/Runtime/DI/Scope.cs +183 -0
  27. package/Runtime/DI/Scope.cs.meta +2 -0
  28. package/Runtime/DI/ScopeBuilder.cs +68 -0
  29. package/Runtime/DI/ScopeBuilder.cs.meta +2 -0
  30. package/Runtime/DI/ScopeExtensions.cs +70 -0
  31. package/Runtime/DI/ScopeExtensions.cs.meta +2 -0
  32. package/Runtime/DI.meta +8 -0
  33. package/Runtime/Debug/ClosureAnalyzer.cs +377 -0
  34. package/Runtime/Debug/ClosureAnalyzer.cs.meta +2 -0
  35. package/Runtime/Debug/ClosureProfilerWindow.cs +435 -0
  36. package/Runtime/Debug/ClosureProfilerWindow.cs.meta +2 -0
  37. package/Runtime/Debug/SubscriberInfo.cs +661 -0
  38. package/Runtime/Debug/SubscriberInfo.cs.meta +3 -0
  39. package/Runtime/Debug.meta +3 -0
  40. package/Runtime/Kylin.DI.asmdef +14 -0
  41. package/Runtime/Kylin.DI.asmdef.meta +7 -0
  42. package/Runtime/SubscribableProperty/Reaction.cs +61 -0
  43. package/Runtime/SubscribableProperty/Reaction.cs.meta +2 -0
  44. package/Runtime/SubscribableProperty/SubscribableCollection.cs +325 -0
  45. package/Runtime/SubscribableProperty/SubscribableCollection.cs.meta +3 -0
  46. package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs +24 -0
  47. package/Runtime/SubscribableProperty/SubscribableCollectionExtensions.cs.meta +3 -0
  48. package/Runtime/SubscribableProperty/SubscribableCommand.cs +52 -0
  49. package/Runtime/SubscribableProperty/SubscribableCommand.cs.meta +2 -0
  50. package/Runtime/SubscribableProperty/SubscribableDictionary.cs +350 -0
  51. package/Runtime/SubscribableProperty/SubscribableDictionary.cs.meta +3 -0
  52. package/Runtime/SubscribableProperty/SubscribableProperty.cs +119 -0
  53. package/Runtime/SubscribableProperty/SubscribableProperty.cs.meta +2 -0
  54. package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs +39 -0
  55. package/Runtime/SubscribableProperty/SubscribablePropertyExtensions.cs.meta +3 -0
  56. package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs +86 -0
  57. package/Runtime/SubscribableProperty/SubscribablePropertyLinq.cs.meta +2 -0
  58. package/Runtime/SubscribableProperty.meta +8 -0
  59. package/Runtime/Update/IFixedUpdatable.cs +16 -0
  60. package/Runtime/Update/IFixedUpdatable.cs.meta +2 -0
  61. package/Runtime/Update/ILateUpdatable.cs +16 -0
  62. package/Runtime/Update/ILateUpdatable.cs.meta +2 -0
  63. package/Runtime/Update/IUpdatable.cs +16 -0
  64. package/Runtime/Update/IUpdatable.cs.meta +2 -0
  65. package/Runtime/Update/IUpdatePriority.cs +15 -0
  66. package/Runtime/Update/IUpdatePriority.cs.meta +2 -0
  67. package/Runtime/Update/UpdateLoopManager.cs +240 -0
  68. package/Runtime/Update/UpdateLoopManager.cs.meta +2 -0
  69. package/Runtime/Update.meta +8 -0
  70. package/Runtime.meta +8 -0
  71. package/package.json +19 -0
@@ -0,0 +1,661 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Linq;
4
+ using System.Reflection;
5
+ using System.Diagnostics;
6
+ using UnityEngine;
7
+ using Debug = UnityEngine.Debug;
8
+ #if UNITY_EDITOR
9
+ using UnityEditor;
10
+ #endif
11
+
12
+ namespace Kylin.SubscribableProperty
13
+ {
14
+ // 디버그 정보를 담는 클래스
15
+ public class SPSubscriberInfo
16
+ {
17
+ public WeakReference SubscriberRef { get; }
18
+ public string SubscriberTypeName { get; }
19
+ public string SubscriberInstanceName { get; }
20
+ public string MethodName { get; }
21
+ public DateTime SubscribeTime { get; }
22
+ public string SubscribeCallStack { get; }
23
+
24
+ public SPSubscriberInfo(object subscriber, string methodName)
25
+ {
26
+ SubscriberRef = new WeakReference(subscriber);
27
+ SubscriberTypeName = subscriber?.GetType().Name ?? "Unknown";
28
+ SubscriberInstanceName = GetInstanceName(subscriber);
29
+ MethodName = methodName;
30
+ SubscribeTime = DateTime.Now;
31
+
32
+ #if UNITY_EDITOR
33
+ // 호출 스택 저장 (성능을 위해 간단히)
34
+ var stackTrace = new StackTrace(3, true);
35
+ var frames = stackTrace.GetFrames()?.Take(5).ToArray();
36
+ if (frames != null && frames.Length > 0)
37
+ {
38
+ SubscribeCallStack = string.Join("\n", frames.Select(f =>
39
+ $"{f.GetMethod()?.DeclaringType?.Name}.{f.GetMethod()?.Name} (Line: {f.GetFileLineNumber()})"));
40
+ }
41
+ #endif
42
+ }
43
+
44
+ private string GetInstanceName(object subscriber)
45
+ {
46
+ if (subscriber == null) return "null";
47
+
48
+ // Unity 컴포넌트인 경우
49
+ if (subscriber is MonoBehaviour mono && mono != null)
50
+ return $"{mono.name} ({mono.GetType().Name})";
51
+
52
+ // 일반 객체인 경우 이름 속성 찾기
53
+ try
54
+ {
55
+ var nameProperty = subscriber.GetType().GetProperty("Name") ??
56
+ subscriber.GetType().GetProperty("name");
57
+ if (nameProperty != null)
58
+ {
59
+ var name = nameProperty.GetValue(subscriber)?.ToString();
60
+ if (!string.IsNullOrEmpty(name))
61
+ return $"{name} ({subscriber.GetType().Name})";
62
+ }
63
+ }
64
+ catch { /* ignore */ }
65
+
66
+ return $"{subscriber.GetType().Name}#{subscriber.GetHashCode()}";
67
+ }
68
+
69
+ public bool IsAlive => SubscriberRef.IsAlive;
70
+ public object Subscriber => SubscriberRef.Target;
71
+ }
72
+
73
+ // 개별 프로퍼티의 디버그 정보
74
+ public class PropertyDebugInfo
75
+ {
76
+ public WeakReference PropertyRef { get; }
77
+ public string PropertyName { get; }
78
+ public string OwnerTypeName { get; }
79
+ public string OwnerInstanceName { get; }
80
+ public Type PropertyType { get; }
81
+ public DateTime CreatedTime { get; }
82
+
83
+ private readonly List<SPSubscriberInfo> _subscribers = new List<SPSubscriberInfo>();
84
+ private readonly object _lock = new object();
85
+
86
+ public PropertyDebugInfo(object property, string propertyName, object owner)
87
+ {
88
+ PropertyRef = new WeakReference(property);
89
+ PropertyType = property.GetType();
90
+ PropertyName = propertyName;
91
+ CreatedTime = DateTime.Now;
92
+
93
+ if (owner != null)
94
+ {
95
+ OwnerTypeName = owner.GetType().Name;
96
+ OwnerInstanceName = GetOwnerInstanceName(owner);
97
+ }
98
+ else
99
+ {
100
+ OwnerTypeName = "Unknown";
101
+ OwnerInstanceName = "Unknown";
102
+ }
103
+ }
104
+
105
+ private string GetOwnerInstanceName(object owner)
106
+ {
107
+ if (owner == null) return "null";
108
+
109
+ if (owner is MonoBehaviour mono && mono != null)
110
+ return $"{mono.name} ({mono.GetType().Name})";
111
+
112
+ try
113
+ {
114
+ var nameProperty = owner.GetType().GetProperty("Name") ??
115
+ owner.GetType().GetProperty("name");
116
+ if (nameProperty != null)
117
+ {
118
+ var name = nameProperty.GetValue(owner)?.ToString();
119
+ if (!string.IsNullOrEmpty(name))
120
+ return $"{name} ({owner.GetType().Name})";
121
+ }
122
+ }
123
+ catch { /* ignore */ }
124
+
125
+ return $"{owner.GetType().Name}#{owner.GetHashCode()}";
126
+ }
127
+
128
+ public void AddSubscriber(object subscriber, string methodName)
129
+ {
130
+ lock (_lock)
131
+ {
132
+ _subscribers.Add(new SPSubscriberInfo(subscriber, methodName));
133
+ }
134
+ }
135
+
136
+ public void RemoveSubscriber(object subscriber)
137
+ {
138
+ lock (_lock)
139
+ {
140
+ _subscribers.RemoveAll(s => ReferenceEquals(s.Subscriber, subscriber));
141
+ }
142
+ }
143
+
144
+ public IEnumerable<SPSubscriberInfo> GetAliveSubscribers()
145
+ {
146
+ lock (_lock)
147
+ {
148
+ // 죽은 구독자들 제거
149
+ _subscribers.RemoveAll(s => !s.IsAlive);
150
+ return _subscribers.ToList();
151
+ }
152
+ }
153
+
154
+ public int SubscriberCount => GetAliveSubscribers().Count();
155
+ public bool IsAlive => PropertyRef.IsAlive;
156
+ public object Property => PropertyRef.Target;
157
+ }
158
+
159
+ // 디버그 정보 관리 클래스
160
+ public static class SubscribablePropertyDebugger
161
+ {
162
+ private static readonly Dictionary<object, PropertyDebugInfo> _debugInfos
163
+ = new Dictionary<object, PropertyDebugInfo>();
164
+ private static readonly object _lock = new object();
165
+
166
+ public static bool IsTestModeEnabled =>
167
+ #if UNITY_EDITOR
168
+ SubscribablePropertyDebugWindow.IsWindowOpen &&
169
+ SubscribablePropertyDebugWindow.IsTestModeOn;
170
+ #else
171
+ false;
172
+ #endif
173
+
174
+ public static void RegisterProperty(object property, string propertyName, object owner)
175
+ {
176
+ if (!IsTestModeEnabled) return;
177
+
178
+ lock (_lock)
179
+ {
180
+ if (!_debugInfos.ContainsKey(property))
181
+ {
182
+ _debugInfos[property] = new PropertyDebugInfo(property, propertyName, owner);
183
+ }
184
+ }
185
+ }
186
+
187
+ public static void UnregisterProperty(object property)
188
+ {
189
+ lock (_lock)
190
+ {
191
+ _debugInfos.Remove(property);
192
+ }
193
+ }
194
+
195
+ public static void AddSubscriber(object property, object subscriber, string methodName)
196
+ {
197
+ if (!IsTestModeEnabled) return;
198
+
199
+ lock (_lock)
200
+ {
201
+ if (_debugInfos.TryGetValue(property, out var info))
202
+ {
203
+ info.AddSubscriber(subscriber, methodName);
204
+ }
205
+ }
206
+ }
207
+
208
+ public static void RemoveSubscriber(object property, object subscriber)
209
+ {
210
+ lock (_lock)
211
+ {
212
+ if (_debugInfos.TryGetValue(property, out var info))
213
+ {
214
+ info.RemoveSubscriber(subscriber);
215
+ }
216
+ }
217
+ }
218
+
219
+ public static IEnumerable<PropertyDebugInfo> GetAllPropertyInfos()
220
+ {
221
+ lock (_lock)
222
+ {
223
+ // 죽은 프로퍼티들 정리
224
+ var deadProperties = _debugInfos.Keys.Where(p => p == null || !_debugInfos[p].IsAlive).ToList();
225
+ foreach (var dead in deadProperties)
226
+ {
227
+ _debugInfos.Remove(dead);
228
+ }
229
+
230
+ return _debugInfos.Values.ToList();
231
+ }
232
+ }
233
+
234
+ public static void Clear()
235
+ {
236
+ lock (_lock)
237
+ {
238
+ _debugInfos.Clear();
239
+ }
240
+ }
241
+
242
+ // Owner 찾기 헬퍼 메서드
243
+ public static object FindOwner(object property)
244
+ {
245
+ if (property == null) return null;
246
+
247
+ try
248
+ {
249
+ // 스택 트레이스를 통해 호출자 찾기
250
+ var stackTrace = new StackTrace();
251
+ var frames = stackTrace.GetFrames();
252
+
253
+ if (frames != null)
254
+ {
255
+ foreach (var frame in frames.Skip(1).Take(10)) // 첫 번째 프레임은 현재 메서드
256
+ {
257
+ var method = frame.GetMethod();
258
+ if (method?.DeclaringType != null &&
259
+ !method.DeclaringType.IsSubclassOf(typeof(SubscribableProperty<>)) &&
260
+ method.DeclaringType != typeof(SubscribablePropertyDebugger))
261
+ {
262
+ // 생성자나 필드 초기화인 경우, 해당 타입의 인스턴스를 찾아야 함
263
+ if (method.IsConstructor || method.Name.Contains("ctor"))
264
+ {
265
+ return method.DeclaringType; // 타입 정보만 반환
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ catch { /* ignore */ }
272
+
273
+ return null;
274
+ }
275
+ }
276
+ }
277
+
278
+ // SubscribableProperty에 디버그 기능 추가 (Partial)
279
+ namespace Kylin.SubscribableProperty
280
+ {
281
+ public partial class SubscribableProperty<T>
282
+ {
283
+ private PropertyDebugInfo _debugInfo;
284
+ private bool _debugInitialized = false;
285
+
286
+ partial void OnPropertyCreated()
287
+ {
288
+ InitializeDebugInfo();
289
+ }
290
+
291
+ partial void OnSubscribe(Action<T> onNext)
292
+ {
293
+ #if UNITY_EDITOR
294
+ if (SubscribablePropertyDebugger.IsTestModeEnabled)
295
+ {
296
+ EnsureDebugInfo();
297
+ SubscribablePropertyDebugger.AddSubscriber(this, onNext.Target, onNext.Method.Name);
298
+
299
+ // 클로저 캡처 감지
300
+ if (ClosureProfiler.IsEnabled && onNext.Target != null)
301
+ {
302
+ var targetType = onNext.Target.GetType();
303
+ if (targetType.Name.Contains("DisplayClass") || targetType.Name.Contains("<>c"))
304
+ {
305
+ var location = $"{PropertyName ?? "Unknown"} in {OwnerTypeName ?? "Unknown"}";
306
+ ClosureProfiler.RecordCapture(onNext.Target, location);
307
+ }
308
+ }
309
+ }
310
+ #endif
311
+ }
312
+
313
+ partial void OnUnsubscribe(Action<T> onNext)
314
+ {
315
+ #if UNITY_EDITOR
316
+ if (SubscribablePropertyDebugger.IsTestModeEnabled)
317
+ {
318
+ SubscribablePropertyDebugger.RemoveSubscriber(this, onNext.Target);
319
+
320
+ // 클로저 구독 해제 기록
321
+ if (ClosureProfiler.IsEnabled && onNext.Target != null)
322
+ {
323
+ ClosureProfiler.RecordUnsubscribe(onNext.Target);
324
+ }
325
+ }
326
+ #endif
327
+ }
328
+
329
+ private string PropertyName => _debugInfo?.PropertyName;
330
+ private string OwnerTypeName => _debugInfo?.OwnerTypeName;
331
+
332
+ private void InitializeDebugInfo()
333
+ {
334
+ if (!SubscribablePropertyDebugger.IsTestModeEnabled || _debugInitialized) return;
335
+
336
+ _debugInitialized = true;
337
+
338
+ // Owner와 프로퍼티 이름 찾기
339
+ var (owner, propertyName) = FindOwnerAndPropertyName();
340
+
341
+ SubscribablePropertyDebugger.RegisterProperty(this, propertyName, owner);
342
+ }
343
+
344
+ private void EnsureDebugInfo()
345
+ {
346
+ if (!_debugInitialized)
347
+ {
348
+ InitializeDebugInfo();
349
+ }
350
+ }
351
+
352
+ private (object owner, string propertyName) FindOwnerAndPropertyName()
353
+ {
354
+ try
355
+ {
356
+ var stackTrace = new StackTrace();
357
+ var frames = stackTrace.GetFrames();
358
+
359
+ if (frames != null)
360
+ {
361
+ foreach (var frame in frames.Skip(1).Take(15))
362
+ {
363
+ var method = frame.GetMethod();
364
+ if (method?.DeclaringType != null &&
365
+ method.DeclaringType != typeof(SubscribableProperty<T>) &&
366
+ method.DeclaringType != typeof(SubscribablePropertyDebugger) &&
367
+ !method.DeclaringType.FullName.Contains("SubscribableProperty"))
368
+ {
369
+ var declaringType = method.DeclaringType;
370
+
371
+ // 생성자인 경우 - 필드에서 생성된 것으로 추정
372
+ if (method.IsConstructor || method.Name.Contains("ctor"))
373
+ {
374
+ var propertyName = FindPropertyNameInType(declaringType);
375
+ return (declaringType, propertyName);
376
+ }
377
+
378
+ // 일반 메서드인 경우 - 인스턴스를 찾을 수 없으므로 타입만
379
+ if (!method.IsStatic)
380
+ {
381
+ var propertyName = FindPropertyNameInType(declaringType);
382
+ return (declaringType, propertyName);
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ catch { /* ignore */ }
389
+
390
+ return (null, $"Unknown_{GetHashCode()}");
391
+ }
392
+
393
+ private string FindPropertyNameInType(Type type)
394
+ {
395
+ try
396
+ {
397
+ // 타입의 SubscribableProperty 필드들 찾기
398
+ var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
399
+ var spFields = fields.Where(f => f.FieldType.IsGenericType &&
400
+ f.FieldType.GetGenericTypeDefinition() == typeof(SubscribableProperty<>))
401
+ .ToList();
402
+
403
+ if (spFields.Count == 1)
404
+ {
405
+ return CleanFieldName(spFields[0].Name);
406
+ }
407
+ else if (spFields.Count > 1)
408
+ {
409
+ // 여러 개인 경우, 타입을 기준으로 추정
410
+ var matchingField = spFields.FirstOrDefault(f => f.FieldType == typeof(SubscribableProperty<T>));
411
+ if (matchingField != null)
412
+ {
413
+ return CleanFieldName(matchingField.Name);
414
+ }
415
+ return $"Property_{typeof(T).Name}_{GetHashCode()}";
416
+ }
417
+ }
418
+ catch { /* ignore */ }
419
+
420
+ return $"Property_{typeof(T).Name}";
421
+ }
422
+
423
+ private string CleanFieldName(string fieldName)
424
+ {
425
+ // _progressSP -> Progress, m_statusProperty -> Status 등
426
+ return fieldName.TrimStart('_', 'm')
427
+ .Replace("SP", "")
428
+ .Replace("Property", "")
429
+ .Replace("Field", "");
430
+ }
431
+ }
432
+ }
433
+
434
+ #if UNITY_EDITOR
435
+ namespace Kylin.SubscribableProperty
436
+ {
437
+ // 디버그 윈도우
438
+ public class SubscribablePropertyDebugWindow : EditorWindow
439
+ {
440
+ private static SubscribablePropertyDebugWindow _instance;
441
+ private static bool _isTestModeOn = false;
442
+
443
+ private Vector2 _scrollPosition;
444
+ private readonly Dictionary<PropertyDebugInfo, bool> _foldoutStates = new Dictionary<PropertyDebugInfo, bool>();
445
+ private string _searchFilter = "";
446
+ private bool _showOnlyWithSubscribers = true;
447
+ private bool _autoRefresh = true;
448
+ private double _lastRefreshTime;
449
+
450
+ public static bool IsWindowOpen => _instance != null;
451
+ public static bool IsTestModeOn => _isTestModeOn;
452
+
453
+ [MenuItem("Tools/Subscribable Property Debugger")]
454
+ public static void ShowWindow()
455
+ {
456
+ _instance = GetWindow<SubscribablePropertyDebugWindow>("구독 프로퍼티 디버거");
457
+ _instance.Show();
458
+ }
459
+
460
+ private void OnEnable()
461
+ {
462
+ _instance = this;
463
+ EditorApplication.update += OnEditorUpdate;
464
+ }
465
+
466
+ private void OnDisable()
467
+ {
468
+ _instance = null;
469
+ EditorApplication.update -= OnEditorUpdate;
470
+ }
471
+
472
+ private void OnEditorUpdate()
473
+ {
474
+ if (_autoRefresh && EditorApplication.timeSinceStartup - _lastRefreshTime > 1.0)
475
+ {
476
+ _lastRefreshTime = EditorApplication.timeSinceStartup;
477
+ Repaint();
478
+ }
479
+ }
480
+
481
+ private void OnGUI()
482
+ {
483
+ DrawHeader();
484
+ DrawFilters();
485
+ DrawPropertyList();
486
+ }
487
+
488
+ private void DrawHeader()
489
+ {
490
+ EditorGUILayout.BeginVertical("Box");
491
+
492
+ EditorGUILayout.LabelField("구독 프로퍼티 디버그 도구", EditorStyles.boldLabel);
493
+
494
+ EditorGUI.BeginDisabledGroup(Application.isPlaying);
495
+ bool newTestMode = EditorGUILayout.Toggle("테스트 모드 활성화", _isTestModeOn);
496
+ if (newTestMode != _isTestModeOn)
497
+ {
498
+ _isTestModeOn = newTestMode;
499
+ if (!_isTestModeOn)
500
+ {
501
+ SubscribablePropertyDebugger.Clear();
502
+ }
503
+ }
504
+ EditorGUI.EndDisabledGroup();
505
+
506
+ _autoRefresh = EditorGUILayout.Toggle("자동 새로고침", _autoRefresh);
507
+
508
+ if (Application.isPlaying && _isTestModeOn)
509
+ {
510
+ EditorGUILayout.HelpBox("테스트 모드가 활성화되었습니다. 구독 활동을 모니터링 중입니다.", MessageType.Info);
511
+ }
512
+ else if (Application.isPlaying)
513
+ {
514
+ EditorGUILayout.HelpBox("플레이 모드 중에는 테스트 모드가 비활성화됩니다. 플레이 모드 진입 전에 활성화하세요.", MessageType.Warning);
515
+ }
516
+
517
+ EditorGUILayout.EndVertical();
518
+ }
519
+
520
+ private void DrawFilters()
521
+ {
522
+ EditorGUILayout.BeginVertical("Box");
523
+ EditorGUILayout.LabelField("필터", EditorStyles.boldLabel);
524
+
525
+ _searchFilter = EditorGUILayout.TextField("검색", _searchFilter);
526
+ _showOnlyWithSubscribers = EditorGUILayout.Toggle("구독자가 있는 항목만 표시", _showOnlyWithSubscribers);
527
+
528
+ EditorGUILayout.BeginHorizontal();
529
+ if (GUILayout.Button("새로고침"))
530
+ {
531
+ Repaint();
532
+ }
533
+ if (GUILayout.Button("모두 지우기"))
534
+ {
535
+ SubscribablePropertyDebugger.Clear();
536
+ _foldoutStates.Clear();
537
+ }
538
+ if (GUILayout.Button("강제 GC"))
539
+ {
540
+ System.GC.Collect();
541
+ System.GC.WaitForPendingFinalizers();
542
+ System.GC.Collect();
543
+ }
544
+ EditorGUILayout.EndHorizontal();
545
+
546
+ EditorGUILayout.EndVertical();
547
+ }
548
+
549
+ private void DrawPropertyList()
550
+ {
551
+ if (!_isTestModeOn)
552
+ {
553
+ EditorGUILayout.HelpBox("모니터링을 시작하려면 테스트 모드를 활성화하세요.", MessageType.Info);
554
+ return;
555
+ }
556
+
557
+ var properties = SubscribablePropertyDebugger.GetAllPropertyInfos()
558
+ .Where(p => p.IsAlive)
559
+ .Where(p => !_showOnlyWithSubscribers || p.SubscriberCount > 0)
560
+ .Where(p => string.IsNullOrEmpty(_searchFilter) ||
561
+ p.PropertyName.ToLower().Contains(_searchFilter.ToLower()) ||
562
+ p.OwnerTypeName.ToLower().Contains(_searchFilter.ToLower()))
563
+ .OrderByDescending(p => p.SubscriberCount)
564
+ .ThenBy(p => p.OwnerTypeName)
565
+ .ThenBy(p => p.PropertyName)
566
+ .ToList();
567
+
568
+ var totalSubscribers = properties.Sum(p => p.SubscriberCount);
569
+ EditorGUILayout.LabelField($"프로퍼티: {properties.Count} | 전체 구독자: {totalSubscribers}", EditorStyles.boldLabel);
570
+
571
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
572
+
573
+ foreach (var propertyInfo in properties)
574
+ {
575
+ DrawPropertyInfo(propertyInfo);
576
+ }
577
+
578
+ EditorGUILayout.EndScrollView();
579
+ }
580
+
581
+ private void DrawPropertyInfo(PropertyDebugInfo propertyInfo)
582
+ {
583
+ if (!_foldoutStates.ContainsKey(propertyInfo))
584
+ _foldoutStates[propertyInfo] = false;
585
+
586
+ EditorGUILayout.BeginVertical("Box");
587
+
588
+ // 헤더
589
+ EditorGUILayout.BeginHorizontal();
590
+
591
+ var subscriberCount = propertyInfo.SubscriberCount;
592
+ var headerText = $"[{subscriberCount}] {propertyInfo.OwnerTypeName}.{propertyInfo.PropertyName}";
593
+
594
+ // 구독자 수에 따른 색상
595
+ var prevColor = GUI.color;
596
+ if (subscriberCount > 5)
597
+ GUI.color = Color.red;
598
+ else if (subscriberCount > 2)
599
+ GUI.color = Color.yellow;
600
+ else if (subscriberCount > 0)
601
+ GUI.color = Color.green;
602
+
603
+ _foldoutStates[propertyInfo] = EditorGUILayout.Foldout(_foldoutStates[propertyInfo], headerText, true);
604
+
605
+ GUI.color = prevColor;
606
+
607
+ EditorGUILayout.LabelField($"({propertyInfo.PropertyType.Name})", EditorStyles.miniLabel, GUILayout.Width(100));
608
+
609
+ EditorGUILayout.EndHorizontal();
610
+
611
+ // 상세 정보
612
+ if (_foldoutStates[propertyInfo])
613
+ {
614
+ EditorGUI.indentLevel++;
615
+
616
+ EditorGUILayout.LabelField("소유자:", propertyInfo.OwnerInstanceName);
617
+ EditorGUILayout.LabelField("생성 시간:", propertyInfo.CreatedTime.ToString("HH:mm:ss"));
618
+
619
+ if (subscriberCount > 0)
620
+ {
621
+ EditorGUILayout.LabelField("구독자:", EditorStyles.boldLabel);
622
+ EditorGUI.indentLevel++;
623
+
624
+ foreach (var subscriber in propertyInfo.GetAliveSubscribers())
625
+ {
626
+ DrawSubscriberInfo(subscriber);
627
+ }
628
+
629
+ EditorGUI.indentLevel--;
630
+ }
631
+ else
632
+ {
633
+ EditorGUILayout.LabelField("활성 구독자 없음", EditorStyles.miniLabel);
634
+ }
635
+
636
+ EditorGUI.indentLevel--;
637
+ }
638
+
639
+ EditorGUILayout.EndVertical();
640
+ }
641
+
642
+ private void DrawSubscriberInfo(SPSubscriberInfo subscriberInfo)
643
+ {
644
+ EditorGUILayout.BeginVertical("Box");
645
+
646
+ EditorGUILayout.LabelField($"• {subscriberInfo.SubscriberInstanceName}.{subscriberInfo.MethodName}", EditorStyles.label);
647
+ EditorGUILayout.LabelField($" 구독 시간: {subscriberInfo.SubscribeTime:HH:mm:ss}", EditorStyles.miniLabel);
648
+
649
+ if (!string.IsNullOrEmpty(subscriberInfo.SubscribeCallStack))
650
+ {
651
+ if (GUILayout.Button("호출 스택 표시", EditorStyles.miniButton))
652
+ {
653
+ Debug.Log($"{subscriberInfo.SubscriberInstanceName}의 구독 호출 스택:\n{subscriberInfo.SubscribeCallStack}");
654
+ }
655
+ }
656
+
657
+ EditorGUILayout.EndVertical();
658
+ }
659
+ }
660
+ }
661
+ #endif
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: da142d145fb745e2a53dfb8a05e66001
3
+ timeCreated: 1755694315
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: c21f968617634dba9fba9d232bd87e43
3
+ timeCreated: 1755694310
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "Kylin.DI",
3
+ "rootNamespace": "Kylin",
4
+ "references": [],
5
+ "includePlatforms": [],
6
+ "excludePlatforms": [],
7
+ "allowUnsafeCode": false,
8
+ "overrideReferences": false,
9
+ "precompiledReferences": [],
10
+ "autoReferenced": true,
11
+ "defineConstraints": [],
12
+ "versionDefines": [],
13
+ "noEngineReferences": false
14
+ }
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: 809554a2ae1714148a5a7c00020adf75
3
+ AssemblyDefinitionImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant: