리팩토링 중

This commit is contained in:
logonkhi
2025-07-24 18:28:09 +09:00
parent 4d29143d47
commit a0c90b1e82
41 changed files with 618 additions and 301 deletions

249
.editorconfig Normal file
View File

@@ -0,0 +1,249 @@
# 상위 디렉터리에서 .editorconfig 설정을 상속하려면 아래 행을 제거하세요.
root = true
# C# 파일
[*.cs]
#### 코어 EditorConfig 옵션 ####
# 들여쓰기 및 간격
indent_size = 4
indent_style = space
tab_width = 4
# 새 줄 기본 설정
end_of_line = crlf
insert_final_newline = false
charset = utf-8
trim_trailing_whitespace = true
#### .NET 코드 작업 ####
# 멤버 입력
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# 기호 검색
dotnet_search_reference_assemblies = true
#### .NET 코딩 규칙 ####
# Using 구성
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. 및 Me. 기본 설정
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# 언어 키워드 및 BCL 형식 기본 설정
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# 괄호 기본 설정
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# 한정자 기본 설정
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# 식 수준 기본 설정
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# 필드 기본 설정
dotnet_style_readonly_field = true
# 매개 변수 기본 설정
dotnet_code_quality_unused_parameters = all
# 비표시 오류(Suppression) 기본 설정
dotnet_remove_unnecessary_suppression_exclusions = none
# 새 줄 기본 설정
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# 코딩 규칙 ####
# var 기본 설정
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# 식 본문 멤버
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# 패턴 일치 기본 설정
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null 검사 기본 설정
csharp_style_conditional_delegate_call = true
# 한정자 기본 설정
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# 코드 블록 기본 설정
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_top_level_statements = true
# 식 수준 기본 설정
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_implicitly_typed_lambda_expression = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_unbound_generic_type_in_nameof = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' 지시문 기본 설정
csharp_using_directive_placement = outside_namespace
# 새 줄 기본 설정
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# 서식 설정 규칙 ####
# 새 줄 기본 설정
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# 들여쓰기 기본 설정
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# 공간 기본 설정
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# 기본 설정 래핑
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### 명명 스타일 ####
# 명명 규칙
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# 기호 사양
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# 명명 스타일
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@@ -62,6 +62,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 836d6527858b7494297e122eb69601fd, type: 3}
m_Name:
m_EditorClassIdentifier:
inputField: {fileID: 4095973766915737330}
searchButton: {fileID: 3154601933036176681}
--- !u!114 &1278875195337434962
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -69,20 +71,20 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1928778550412568362}
m_Enabled: 0
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 0
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 694507160, guid: fe40b90c67f2f75419562a6fe1003d5b, type: 3}
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -390,7 +390,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -198,7 +198,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -340,7 +340,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -521,7 +521,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -198,7 +198,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -275,7 +275,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -394,7 +394,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -10,9 +10,8 @@ GameObject:
m_Component:
- component: {fileID: 3316965954832882549}
- component: {fileID: 3078805581424071851}
- component: {fileID: 3314964221659757925}
- component: {fileID: 4097232251975178814}
- component: {fileID: 5836275117983516284}
- component: {fileID: 4097232251975178814}
m_Layer: 5
m_Name: UILoading
m_TagString: Untagged
@@ -29,7 +28,7 @@ RectTransform:
m_GameObject: {fileID: 3247177050376678973}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 88171281312113102}
@@ -53,26 +52,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0575433bbc705184a91373cc1596e713, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!222 &3314964221659757925
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3247177050376678973}
m_CullTransparentMesh: 0
--- !u!225 &4097232251975178814
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3247177050376678973}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
canvasGroup: {fileID: 4097232251975178814}
loadinImage: {fileID: 5537735754607583444}
--- !u!223 &5836275117983516284
Canvas:
m_ObjectHideFlags: 0
@@ -94,8 +75,20 @@ Canvas:
m_AdditionalShaderChannelsFlag: 0
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_SortingOrder: 100
m_TargetDisplay: 0
--- !u!225 &4097232251975178814
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3247177050376678973}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!1 &3720191927695001841
GameObject:
m_ObjectHideFlags: 0
@@ -131,7 +124,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 128, y: 128}
m_SizeDelta: {x: 32, y: 32}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7648764534251572644
CanvasRenderer:

View File

@@ -926,7 +926,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -926,7 +926,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -138,7 +138,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -173,7 +173,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -281,7 +281,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -145,7 +145,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -172,7 +172,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &117691393038772277
RectTransform:
m_ObjectHideFlags: 0

View File

@@ -281,7 +281,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -162,7 +162,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -145,7 +145,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -173,7 +173,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -145,7 +145,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -281,7 +281,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -162,7 +162,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -145,7 +145,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -66,7 +66,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -27720893, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Sprite: {fileID: 21300000, guid: e5829cbc100001646956a9c3ed4e33c5, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
@@ -345,7 +345,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 887145076, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Sprite: {fileID: 21300000, guid: 4cf3568ca3f55f64cb11447d139d7a3d, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1

View File

@@ -39,6 +39,8 @@ namespace SampleProject
{
if (AppConfig.LoadConfig())
{
Application.targetFrameRate = AppConfig.Config.TargetFrameRate;
//기본 언어 설정
bool success = LocalizationManager.Instance.LoadDefaultLocalizationData(AppConfig.Config.Language);
Debug.Log($"LocalizationManager: LoadDefaultLocalizationData success: {success}");
@@ -56,7 +58,7 @@ namespace SampleProject
private void SetNetworkConfig()
{
URLList.Add("baseinfo", "http://localhost:8888/baseinfo/00:00");
URLList.Add("baseinfo", "http://localhost:8888/baseinfo");
var agvDataMask = new DataMask();
agvDataMask.ObjectName = "AGV"; // AGV 객체의 이름을 설정합니다.

View File

@@ -43,6 +43,12 @@ namespace SampleProject.Config
[JsonProperty("language")]
public string Language { get; set; }
/// <summary>
/// 목표 프레임 레이트 설정입니다.
/// </summary>
[JsonProperty("targetFrameRate")]
public int TargetFrameRate { get; set; }
/// <summary>
/// 애플리케이션의 창 관련 설정입니다.
/// </summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UVC.Core;
@@ -6,7 +6,9 @@ using UVC.Data;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Factory.Alarm;
using UVC.Factory.Component;
using UVC.Factory.Playback;
using UVC.UI.Loading;
using UVC.UI.Tooltip;
namespace SampleProject
@@ -33,13 +35,19 @@ namespace SampleProject
/// <remarks>이 메서드는 AGV 관리자 생성과 관련된 필요한 초기화 또는 설정 작업을 수행하기 위한 것입니다.
/// 내부적으로 호출되며 외부 코드에서 직접 사용하도록 의도된 것이 아닙니다.
///</remarks>
internal void OnAGVManagerCreated()
internal void OnAGVCreated()
{
AlarmManager.Instance.Run();
}
private async void OnAppInitialized()
{
// AGVManager 생성 시 이벤트 처리
AGVManager.Instance.OnAGVCreated += OnAGVCreated;
PlaybackService.Instance.OnStopPlayback += OnStopPlayback;
await requestDataAsync();
if (Initialized != null)
@@ -47,21 +55,23 @@ namespace SampleProject
Initialized.Invoke();
}
PlaybackService.Instance.OnStopPlayback += OnStopPlayback;
//MqttReceiver 시작
DataRepository.Instance.MqttReceiver.Start();
}
private async Task requestDataAsync()
{
var httpFetcher = DataRepository.Instance.HttpFetcher;
var splitRequest = new HttpRequestConfig(URLList.Get("baseinfo"))
.setSplitResponseByKey(true) // 응답을 키별로 분할
.AddSplitConfig("AGV", DataMapperValidator.Get("AGV")) // "AGV" 키에 대한 매퍼, Validator 설정
.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM")); // "ALARM" 키에 대한 매퍼, Validator 설정
httpFetcher.Add("baseInfo", splitRequest);
await httpFetcher.Excute("baseInfo");
//MqttReceiver 시작
DataRepository.Instance.MqttReceiver.Start();
UILoading.Show();
//Debug.Log("Requesting BaseInfo data...");
//var httpFetcher = DataRepository.Instance.HttpFetcher;
//var splitRequest = new HttpRequestConfig(URLList.Get("baseinfo"))
// .setSplitResponseByKey(true) // 응답을 키별로 분할
// .AddSplitConfig("AGV", DataMapperValidator.Get("AGV")) // "AGV" 키에 대한 매퍼, Validator 설정
// .AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM")); // "ALARM" 키에 대한 매퍼, Validator 설정
//httpFetcher.Add("baseInfo", splitRequest);
//await httpFetcher.Excute("baseInfo");
//Debug.Log("BaseInfo data request completed.");
UILoading.Hide();
}
private async void OnStopPlayback()

View File

@@ -1,8 +1,11 @@
using Cysharp.Threading.Tasks;
#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Data.Mqtt;
@@ -28,7 +31,7 @@ namespace UVC.Data
/// <summary>
/// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다.
/// </summary>
protected DataRepository()
protected DataRepository()
{
// Best MQTT 초기화 작업을 Main 스레드에서 호출 해야 한다.
Best.HTTP.Shared.HTTPManager.Setup();
@@ -56,7 +59,7 @@ namespace UVC.Data
private Dictionary<string, Action<IDataObject>> dataUpdateHandlers = new Dictionary<string, Action<IDataObject>>();
private HttpDataFetcher httpFetcher = new HttpDataFetcher();
public HttpDataFetcher HttpFetcher => httpFetcher;
public HttpDataFetcher HttpFetcher => httpFetcher;
private MqttDataReceiver mqttReceiver = new MqttDataReceiver();
public MqttDataReceiver MqttReceiver => mqttReceiver;
@@ -81,10 +84,9 @@ namespace UVC.Data
{
if (!dataObjects.ContainsKey(key))
{
var newData = dataObject.Clone(fromPool: false);
dataObjects.Add(key, newData);
dataObject.MarkAllAsUpdated();
var newData = dataObject.Clone(fromPool: false);
dataObjects.Add(key, newData);
NotifyDataUpdate(key, newData);
return dataObject;
}
@@ -101,7 +103,8 @@ namespace UVC.Data
{
newDataObject = dataObject;
}
NotifyDataUpdate(key, obj);
bool shouldInvoke = !updatedDataOnly || newDataObject.UpdatedCount > 0;
if(shouldInvoke) NotifyDataUpdate(key, newDataObject);
return newDataObject;
}
}
@@ -130,7 +133,7 @@ namespace UVC.Data
/// </summary>
/// <param name="key">검색할 데이터 객체의 키</param>
/// <returns>키가 존재하면 해당 데이터 객체, 존재하지 않으면 null</returns>
public IDataObject GetData(string key)
public IDataObject? GetData(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
@@ -233,6 +236,7 @@ namespace UVC.Data
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
if (dataObject == null)
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
//Debug.Log($"NotifyDataUpdate: {key}, {dataObject.GetType().Name}");
lock (syncLock)
{
if (dataUpdateHandlers.ContainsKey(key))

View File

@@ -183,24 +183,24 @@ namespace UVC.Data.Http
{
if (!infoList.ContainsKey(key))
{
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
Debug.LogError($"No HTTP request found with key '{key}'.");
return;
}
Debug.Log($"Executing HTTP request for key: {key}");
HttpRequestConfig info = infoList[key];
// 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지
await StopRepeat(key);
// 스레드풀에서 요청 처리 실행
await UniTask.SwitchToThreadPool();
try
{
if (!info.Repeat)
{
// 단일 실행 로직 호출
await ExecuteSingle(key, info);
await UniTask.SwitchToMainThread();
await UniTask.RunOnThreadPool(() => ExecuteSingle(key, info));
Debug.Log($"HTTP request '{key}' executed successfully.");
}
else
{
@@ -211,8 +211,6 @@ namespace UVC.Data.Http
}
catch (Exception ex)
{
// 예외가 발생한 경우에도 메인 스레드로 복귀
await UniTask.SwitchToMainThread();
throw; // 예외 재발생
}
}
@@ -234,7 +232,6 @@ namespace UVC.Data.Http
/// <exception cref="Exception">HTTP 요청 중 다른 예외가 발생한 경우</exception>
private async UniTask ExecuteSingle(string key, HttpRequestConfig info, CancellationToken cancellationToken = default)
{
int retryCount = 0;
Exception lastException = null;
@@ -264,11 +261,10 @@ namespace UVC.Data.Http
{
result = await HttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
}
Debug.Log($"HTTP request '{key}' completed");
cancellationToken.ThrowIfCancellationRequested();
await HttpDataProcessor.ProcessResponseAsync(key, info, result, cancellationToken);
HttpDataProcessor.ProcessResponse(key, info, result, cancellationToken);
Debug.Log($"HTTP request '{key}' processed successfully");
return;
}
catch (OperationCanceledException)
@@ -349,7 +345,7 @@ namespace UVC.Data.Http
try
{
// 단일 실행 로직 호출
await ExecuteSingle(key, info, cts.Token);
await UniTask.RunOnThreadPool(() => ExecuteSingle(key, info, cts.Token));
// 지정된 횟수만큼 반복한 경우 중지
if (info.RepeatCount > 0)

View File

@@ -1,12 +1,14 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using UnityEngine;
using UVC.Data.Core;
using UVC.Log;
@@ -39,7 +41,7 @@ namespace UVC.Data.Http
/// 5. 설정에 따라 성공(`SuccessHandler`) 또는 실패(`FailHandler`) 핸들러를 메인 스레드에서 호출하여 UI 업데이트 등을 안전하게 처리할 수 있도록 합니다.
/// 6. 모든 과정에서 발생하는 예외(JSON 파싱, 작업 취소 등)를 처리하고 로그를 남깁니다.
/// </remarks>
public static async UniTask ProcessResponseAsync(string key, HttpRequestConfig info, string responseContent, CancellationToken cancellationToken)
public static void ProcessResponse(string key, HttpRequestConfig info, string responseContent, CancellationToken cancellationToken)
{
// 매핑된 최종 데이터 객체를 담을 변수입니다. try-finally 블록에서 자원 해제를 위해 외부에 선언합니다.
IDataObject? mappedObject = null;
@@ -54,12 +56,10 @@ namespace UVC.Data.Http
// 핸들러는 UI와 상호작용할 수 있으므로 메인 스레드에서 호출합니다.
if (info.FailHandler != null)
{
await UniTask.SwitchToMainThread();
info.FailHandler.Invoke("Response content is null or empty.");
UniTask.Post(() => info.FailHandler.Invoke("Response content is null or empty."));
}
return;
}
// ResponseMask를 사용해 응답이 성공적인지 확인하고, 실제 데이터 부분을 추출합니다.
// 예를 들어, 응답이 {"status":"OK", "data":{...}} 형태일 때, "status"가 "OK"인지 확인하고 "data" 부분만 가져옵니다.
HttpResponseResult responseResult = info.ResponseMask.Apply(responseContent);
@@ -68,34 +68,30 @@ namespace UVC.Data.Http
if (info.FailHandler != null)
{
string errorMessage = responseResult.Message!;
await UniTask.SwitchToMainThread();
info.FailHandler.Invoke(errorMessage);
UniTask.Post(() => info.FailHandler.Invoke(errorMessage));
}
return;
}
// 마스크를 통해 추출된 실제 데이터
responseContent = responseResult.Data!.Trim();
// 응답을 최상위 키를 기준으로 분할 처리해야 하는 경우
if (info.SplitResponseByKey && responseContent.StartsWith("{"))
{
await ProcessSplitResponse(info, responseContent, cancellationToken);
ProcessSplitResponse(info, responseContent, cancellationToken);
return; // 분할 처리가 완료되면 이 메서드의 나머지 로직은 실행하지 않습니다.
}
// 응답이 JSON 객체 형태인 경우 (e.g., {"id": 1, "name": "item"})
else if (responseContent.StartsWith("{"))
{
mappedObject = await ProcessObjectResponse(info, responseContent, cancellationToken);
mappedObject = ProcessObjectResponse(info, responseContent, cancellationToken);
if (mappedObject == null) return;
}
// 응답이 JSON 배열 형태인 경우 (e.g., [{"id": 1}, {"id": 2}])
else if (responseContent.StartsWith("["))
{
mappedObject = await ProcessArrayResponse(info, responseContent, cancellationToken);
mappedObject = ProcessArrayResponse(info, responseContent, cancellationToken);
if (mappedObject == null) return;
}
// 핸들러 호출 전 작업 취소 요청이 있었는지 다시 확인합니다.
cancellationToken.ThrowIfCancellationRequested();
@@ -108,7 +104,6 @@ namespace UVC.Data.Http
// 만약 반환된 객체가 원본과 같다면, 핸들러에 전달하기 위해 복제본을 만듭니다.
if (repoObject == mappedObject) repoObject = mappedObject.Clone(fromPool: false);
}
// 'UpdatedDataOnly' 옵션이 켜져 있고, 실제로 데이터가 업데이트되었을 때만 핸들러를 호출합니다.
if (info.UpdatedDataOnly)
{
@@ -117,21 +112,18 @@ namespace UVC.Data.Http
if (info.SuccessHandler != null)
{
var handlerData = repoObject;
await UniTask.SwitchToMainThread();
info.SuccessHandler.Invoke(handlerData);
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
}
return;
}
}
// 최종적으로 처리된 데이터가 있는 경우 성공 핸들러를 호출합니다.
if (repoObject != null)
{
if (info.SuccessHandler != null)
{
var handlerData = repoObject;
await UniTask.SwitchToMainThread();
info.SuccessHandler.Invoke(handlerData);
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
}
}
// 처리된 데이터가 없는 경우 실패 핸들러를 호출합니다.
@@ -139,8 +131,7 @@ namespace UVC.Data.Http
{
if (info.FailHandler != null)
{
await UniTask.SwitchToMainThread();
info.FailHandler.Invoke("Data is Null");
UniTask.Post(() => info.FailHandler.Invoke("Data is Null"));
}
}
}
@@ -150,8 +141,7 @@ namespace UVC.Data.Http
ULog.Error($"JSON parsing error for {key}: {ex.Message}\nResponse: {responseContent}", ex);
if (info.FailHandler != null)
{
await UniTask.SwitchToMainThread();
info.FailHandler.Invoke($"JSON parsing error: {ex.Message}");
UniTask.Post(() => info.FailHandler.Invoke($"JSON parsing error: {ex.Message}"));
}
}
catch (OperationCanceledException)
@@ -165,8 +155,7 @@ namespace UVC.Data.Http
ULog.Error($"Error processing response for {key}: {ex.Message}", ex);
if (info.FailHandler != null)
{
await UniTask.SwitchToMainThread();
info.FailHandler.Invoke($"Error processing response: {ex.Message}");
UniTask.Post(() => info.FailHandler.Invoke($"Error processing response: {ex.Message}"));
}
}
finally
@@ -188,7 +177,7 @@ namespace UVC.Data.Http
/// "AGV"와 "ALARM"을 각각 별개의 데이터로 간주하고 처리합니다.
/// 각 키에 대해 별도의 DataMapper나 Validator를 `HttpRequestConfig`에 설정할 수 있습니다.
/// </remarks>
public static async UniTask ProcessSplitResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken = null)
public static void ProcessSplitResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken = null)
{
JObject responseObject = JObject.Parse(jsonResponse);
// JSON 객체의 모든 프로퍼티(키-값 쌍)를 순회합니다.
@@ -197,6 +186,7 @@ namespace UVC.Data.Http
cancellationToken?.ThrowIfCancellationRequested();
string subKey = property.Name; // "AGV", "ALARM" 등
JToken subToken = property.Value; // `[...]` 또는 `{...}`
IDataObject? subMappedObject = null;
@@ -208,19 +198,24 @@ namespace UVC.Data.Http
var subKeyDataMapper = splitConfig?.DataMapper;
var subKeyValidator = splitConfig?.Validator;
//매퍼가 null인 경우, 해당 키는 처리하지 않습니다.
if (subKeyDataMapper == null) continue;
try
{
// 하위 데이터가 배열 또는 객체 형태인지에 따라 적절한 처리 메서드를 호출합니다.
if (subToken is JArray)
if (subToken is JArray && subToken.Count() > 0)
{
// 분할 처리 중 개별 항목의 실패가 전체 실패로 이어지지 않도록 `invokeFailHandler`를 false로 설정합니다.
subMappedObject = await ProcessArrayResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
subMappedObject = ProcessArrayResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
}
else if (subToken is JObject)
else if (subToken is JObject && subToken.Count() > 0)
{
subMappedObject = await ProcessObjectResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
subMappedObject = ProcessObjectResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false);
}
//Debug.Log($"Processing split response for key: {subKey}, {subToken.Count()}. {subToken.GetType().Name} subMappedObject == null:{subMappedObject == null}");
// 매핑된 결과가 없으면 (예: 유효성 검사 실패) 다음 키로 넘어갑니다.
if (subMappedObject == null)
{
@@ -236,10 +231,10 @@ namespace UVC.Data.Http
if (shouldInvokeHandler && info.SuccessHandler != null)
{
var handlerData = repoObject.Clone(fromPool: false);
await UniTask.SwitchToMainThread();
info.SuccessHandler.Invoke(handlerData);
//await UniTask.SwitchToMainThread();
UniTask.Post(() => info.SuccessHandler.Invoke(handlerData));
// 다음 키 처리를 위해 다시 스레드 풀로 전환합니다.
await UniTask.SwitchToThreadPool();
//await UniTask.SwitchToThreadPool();
}
}
finally
@@ -250,10 +245,10 @@ namespace UVC.Data.Http
}
}
private static async UniTask<IDataObject?> ProcessObjectResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
private static IDataObject? ProcessObjectResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
{
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
return await ProcessObjectResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
return ProcessObjectResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
}
/// <summary>
@@ -270,7 +265,7 @@ namespace UVC.Data.Http
/// 통과하면 `DataMapper`를 사용해 JSON을 `IDataObject`로 변환합니다.
/// 대용량 JSON의 경우 메모리 효율을 위해 스트림 기반 파싱을 지원합니다.
/// </remarks>
public static async UniTask<IDataObject?> ProcessObjectResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
public static IDataObject? ProcessObjectResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
{
// 유효성 검사기가 설정된 경우
if (validator != null)
@@ -293,8 +288,7 @@ namespace UVC.Data.Http
{
if (failHandler != null)
{
await UniTask.SwitchToMainThread();
failHandler.Invoke("Data is not Valid");
UniTask.Post(() => failHandler.Invoke("Data is not Valid"));
}
return null;
}
@@ -324,10 +318,10 @@ namespace UVC.Data.Http
}
}
private static async UniTask<IDataObject?> ProcessArrayResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
private static IDataObject? ProcessArrayResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true)
{
Action<string>? failHandler = invokeFailHandler ? info.FailHandler : null;
return await ProcessArrayResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
return ProcessArrayResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler);
}
/// <summary>
@@ -343,7 +337,7 @@ namespace UVC.Data.Http
/// `DataValidator`를 통해 배열의 각 항목을 필터링하여 유효한 데이터만 포함하는 새 배열을 만들 수 있습니다.
/// 그 후 `DataMapper`를 통해 배열 전체를 `DataArray` 객체로 변환합니다.
/// </remarks>
public static async UniTask<IDataObject?> ProcessArrayResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
public static IDataObject? ProcessArrayResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action<string>? failHandler)
{
JArray? sourceArray;
@@ -366,8 +360,7 @@ namespace UVC.Data.Http
{
if (failHandler != null)
{
await UniTask.SwitchToMainThread();
failHandler.Invoke("Data is not Valid or empty after validation");
UniTask.Post(() => failHandler.Invoke("Data is not Valid or empty after validation"));
}
return null;
}

View File

@@ -1,11 +1,13 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using Newtonsoft.Json.Linq;
using SampleProject.Config;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UVC.Data.Core;
using UVC.Data.Http;
using UVC.Log;
@@ -91,7 +93,7 @@ namespace UVC.Data.Mqtt
/// <summary>
/// 토픽별 파이프라인 정보를 저장하는 딕셔너리
/// </summary>
private Dictionary<string, MqttSubscriptionConfig> infoList;
private ConcurrentDictionary<string, MqttSubscriptionConfig> configList;
private MqttWorker mqttWorker;
@@ -103,7 +105,7 @@ namespace UVC.Data.Mqtt
public MqttDataReceiver()
{
mqttWorker = new MqttWorker();
infoList = new Dictionary<string, MqttSubscriptionConfig>();
configList = new ConcurrentDictionary<string, MqttSubscriptionConfig>();
}
/// <summary>
@@ -141,14 +143,7 @@ namespace UVC.Data.Mqtt
/// </remarks>
public void Add(MqttSubscriptionConfig info)
{
if (!infoList.ContainsKey(info.Topic))
{
infoList.Add(info.Topic, info);
}
else
{
infoList[info.Topic] = info;
}
configList[info.Topic] = info;
}
/// <summary>
@@ -157,10 +152,7 @@ namespace UVC.Data.Mqtt
/// <param name="topic">제거할 토픽 이름</param>
public void Remove(string topic)
{
if (infoList.ContainsKey(topic))
{
infoList.Remove(topic);
}
configList.TryRemove(topic, out _);
}
/// <summary>
@@ -181,7 +173,7 @@ namespace UVC.Data.Mqtt
{
// Mockup 모드인 경우 MockMQTTService를 사용하여 테스트 환경을 설정합니다.
mockupMQTT = new MockMQTTService();
foreach (var topic in infoList.Keys)
foreach (var topic in configList.Keys)
{
mockupMQTT.AddTopicHandler(topic, OnTopicMessage);
}
@@ -231,38 +223,39 @@ namespace UVC.Data.Mqtt
/// 등록된 데이터 매퍼를 통해 메시지를 변환한 후, 해당 토픽에 등록된 핸들러에게 전달합니다.
/// 'UpdatedDataOnly' 설정에 따라 데이터가 변경된 경우에만 핸들러를 호출할 수도 있습니다.
/// </remarks>
private async void OnTopicMessageLogic(string topic, string message)
private void OnTopicMessageLogic(string topic, string message)
{
//Debug.Log($"OnTopicMessageLogic topic: {topic}, configList.ContainsKey(topic): {configList.ContainsKey(topic)}");
// 토픽이 infoList와 readyHandlerList에 존재하고, 준비 상태가 true인 경우에만 처리합니다.
if (infoList.ContainsKey(topic))
if (configList.TryGetValue(topic, out var config))
{
MqttSubscriptionConfig info = infoList[topic];
IDataObject? mappedObject = null;
message = message.Trim();
if (!string.IsNullOrEmpty(message))
{
try
{
if (message.StartsWith("{"))
{
mappedObject = await HttpDataProcessor.ProcessObjectResponse(message, null, info.DataMapper, info.Validator, null);
mappedObject = HttpDataProcessor.ProcessObjectResponse(message, null, config.DataMapper, config.Validator, null);
}
else if (message.StartsWith("["))
{
mappedObject = await HttpDataProcessor.ProcessArrayResponse(message, null, info.DataMapper, info.Validator, null);
mappedObject = HttpDataProcessor.ProcessArrayResponse(message, null, config.DataMapper, config.Validator, null);
}
//Debug.Log($"OnTopicMessageLogic topic: {topic}, mappedObject == null: {mappedObject == null}, config.DataMapper:{config.DataMapper == null}, config.Validator:{config.Validator == null}");
if (mappedObject == null) return;
// DataRepository는 내부적으로 데이터를 복사/업데이트하므로, mappedObject는 여기서 임시 객체가 됩니다.
var repoObject = DataRepository.Instance.AddOrUpdateData(topic, mappedObject, info.UpdatedDataOnly);
var repoObject = DataRepository.Instance.AddOrUpdateData(topic, mappedObject, config.UpdatedDataOnly);
if (repoObject == mappedObject) repoObject = mappedObject.Clone(fromPool: false);
// 핸들러 호출이 필요한지 확인
bool shouldInvoke = !info.UpdatedDataOnly || (repoObject != null && repoObject.UpdatedCount > 0);
if (shouldInvoke)
bool shouldInvoke = !config.UpdatedDataOnly || (repoObject != null && repoObject.UpdatedCount > 0);
if (shouldInvoke && config.Handler != null)
{
var handlerData = repoObject;
// 핸들러를 메인 스레드에서 안전하게 호출
UniTask.Post(() => info.Handler?.Invoke(handlerData));
UniTask.Post(() => config.Handler?.Invoke(handlerData));
}
}
catch (Exception ex)
@@ -292,7 +285,7 @@ namespace UVC.Data.Mqtt
if (!UseMockup)
{
if (!mqttWorker.IsRunning) return;
foreach (var topic in infoList.Keys)
foreach (var topic in configList.Keys)
{
mqttWorker.RemoveListener(topic, OnTopicPacketMessage);
}
@@ -314,7 +307,7 @@ namespace UVC.Data.Mqtt
{
if (!UseMockup) mqttWorker.Dispose();
else mockupMQTT?.Disconnect();
infoList.Clear();
configList.Clear();
}
}

View File

@@ -1,4 +1,4 @@
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks;
using NUnit.Framework;
using System;
using System.Collections.Concurrent;

View File

@@ -1,29 +1,29 @@
using Cysharp.Threading.Tasks;
using Cysharp.Threading.Tasks;
namespace UVC.Event
{
/// <summary>
/// 비동기 이벤트 처리를 위한 제네릭 델리게이트
/// UniTask를 반환하는 이벤트 핸들러를 정의하여 비동기 작업을 이벤트 기반으로 처리할 수 있게
/// 비동기 이벤트 처리를 위한 델리게이트입니다.
/// UniTask를 반환하는 이벤트 핸들러를 정의하여, 비동기 작업을 이벤트 기반으로 처리할 수 있게 합니다.
/// </summary>
/// <typeparam name="TEventArgs">이벤트 데이터를 포함하는 제네릭 타입 매개변수</typeparam>
/// <param name="sender">이벤트를 발생시킨 객체</param>
/// <param name="e">이벤트와 관련된 데이터</param>
/// <returns>비동기 작업을 나타내는 UniTask</returns>
/// <typeparam name="TEventArgs">이벤트 데이터를 는 제네릭 타입 매개변수</typeparam>
/// <param name="sender">이벤트를 발생시킨 객체</param>
/// <param name="e">이벤트에 포함된 데이터</param>
/// <returns>비동기 작업을 나타내는 UniTask</returns>
/// <example>
/// <code>
/// // 이벤트 선언 예시
/// // 이벤트 정의 예시
/// public class NetworkManager
/// {
/// // UniTaskEventHandler 타입의 이벤트 선언
/// // UniTaskEventHandler 타입의 이벤트 선언
/// public event UniTaskEventHandler<DataReceivedEventArgs> OnDataReceived;
///
/// // 이벤트 발생 메
/// // 이벤트 발생 메
/// public async UniTask RaiseDataReceivedEvent(DataReceivedEventArgs args)
/// {
/// if (OnDataReceived != null)
/// {
/// // 모든 등록된 핸들러를 순차적으로 실행
/// // 모든 등록된 핸들러를 비동기적으로 실행
/// foreach (var handler in OnDataReceived.GetInvocationList())
/// {
/// await ((UniTaskEventHandler<DataReceivedEventArgs>)handler).Invoke(this, args);
@@ -32,7 +32,7 @@ namespace UVC.Event
/// }
/// }
///
/// // 이벤트 구독 예시
/// // 이벤트 구독 예시
/// public class DataProcessor
/// {
/// public void Initialize(NetworkManager networkManager)
@@ -42,17 +42,17 @@ namespace UVC.Event
///
/// private async UniTask HandleDataReceived(object sender, DataReceivedEventArgs e)
/// {
/// // 비동기 이벤트 처리 로직
/// // 비동기 이벤트 처리 로직
/// await ProcessDataAsync(e.Data);
/// }
///
/// private async UniTask ProcessDataAsync(byte[] data)
/// private async UniTask ProcessDataAsync(byte[] buffers)
/// {
/// await UniTask.Delay(100); // 예시 비동기 작업
/// // 데이터 처리 로직
/// await UniTask.Delay(100); // 예시 비동기 작업
/// // 데이터 처리 로직
/// }
/// }
/// </code>
/// </example>
public delegate UniTask UniTaskEventHandler<TEventArgs>(object sender, TEventArgs e);
}
}

View File

@@ -4,28 +4,28 @@ using UnityEngine;
namespace UVC.Extension
{
/// <summary>
/// RectTransform에 대한 유용한 확장 메서드를 제공하는 클래스입니다.
/// RectTransform에 대한 확장 메서드를 제공하는 클래스입니다.
/// </summary>
public static class RectTransformEx
{
/// <summary>
/// RectTransform의 마진을 설정합니다. 부모 요소의 전체 영역을 기준으로 상하좌우 여백을 지정합니다.
/// 앵커는 자동으로 모서리에 설정됩니다(anchorMin=[0,0], anchorMax=[1,1]).
/// RectTransform의 여백을 설정합니다. 부모를 기준으로 모든 방향의 여백을 설정하여 크기를 조절합니다.
/// 앵커는 부모의 전체를 채우도록 설정됩니다(anchorMin=[0,0], anchorMax=[1,1]).
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="left">왼쪽 여백</param>
/// <param name="right">오른쪽 여백</param>
/// <param name="top">위쪽 여백</param>
/// <param name="bottom">아래쪽 여백</param>
/// <param name="trans">대상 RectTransform</param>
/// <param name="left">왼쪽 여백</param>
/// <param name="right">오른쪽 여백</param>
/// <param name="top">위쪽 여백</param>
/// <param name="bottom">아래쪽 여백</param>
/// <example>
/// <code>
/// // 게임 오브젝트의 RectTransform 여백 적용
/// // 패널 RectTransform 여백 설정
/// RectTransform panelRect = panel.GetComponent<RectTransform>();
/// panelRect.SetRectMargin(10f, 10f, 10f, 10f); // 사방 10픽셀 여백 설정
/// panelRect.SetRectMargin(10f, 10f, 10f, 10f); // 모든 방향에 10 여백 설정
///
/// // UI 요소를 부모 컨테이너에 맞추되 여백 주기
/// // 자식 UI 요소의 여백 다르게 설정
/// RectTransform childRect = childObject.GetComponent<RectTransform>();
/// childRect.SetRectMargin(5f, 5f, 20f, 5f); // 상단에 더 여백 설정
/// childRect.SetRectMargin(5f, 5f, 20f, 5f); // 위쪽에 더 많은 여백 설정
/// </code>
/// </example>
public static void SetRectMargin(this RectTransform trans, float left, float right, float top, float bottom)
@@ -37,14 +37,14 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 너비를 설정합니다.
/// RectTransform의 너비를 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="width">설정할 너비</param>
/// <param name="trans">대상 RectTransform</param>
/// <param name="width">설정할 너비</param>
/// <example>
/// <code>
/// // 버튼의 너비를 200으로 설정
/// RectTransform buttonRect = button.GetComponent<RectTransform>();
/// // 버튼의 너비를 200으로 설정
/// RectTransform buttonRect = searchButton.GetComponent<RectTransform>();
/// buttonRect.SetWidth(200f);
/// </code>
/// </example>
@@ -54,13 +54,13 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 높이를 설정합니다.
/// RectTransform의 높이를 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="height">설정할 높이</param>
/// <param name="trans">대상 RectTransform</param>
/// <param name="height">설정할 높이</param>
/// <example>
/// <code>
/// // 패널의 높이를 150으로 설정
/// // 패널의 높이를 150으로 설정
/// RectTransform panelRect = panel.GetComponent<RectTransform>();
/// panelRect.SetHeight(150f);
/// </code>
@@ -71,13 +71,13 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 크기를 설정합니다.
/// RectTransform의 크기를 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="size">설정할 크기 (너비, 높이)</param>
/// <param name="trans">대상 RectTransform</param>
/// <param name="size">설정할 크기 (너비, 높이)</param>
/// <example>
/// <code>
/// // 이미지의 크기를 100x100으로 설정
/// // 이미지의 크기를 100x100으로 설정
/// RectTransform imageRect = image.GetComponent<RectTransform>();
/// imageRect.SetSize(new Vector2(100f, 100f));
/// </code>
@@ -89,15 +89,15 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 앵커를 중앙에 위치시키고 피벗 중앙으로 설정합니다.
/// RectTransform의 앵커 피벗 중앙으로 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="trans">대상 RectTransform</param>
/// <example>
/// <code>
/// // 버튼을 화면 중앙에 위치시키기
/// RectTransform buttonRect = button.GetComponent<RectTransform>();
/// // 버튼의 기준점을 중앙으로 설정
/// RectTransform buttonRect = searchButton.GetComponent<RectTransform>();
/// buttonRect.SetAnchorsToCenter();
/// buttonRect.anchoredPosition = Vector2.zero; // 중앙 위치에 배치
/// buttonRect.anchoredPosition = Vector2.zero; // 중앙에 배치
/// </code>
/// </example>
public static void SetAnchorsToCenter(this RectTransform trans)
@@ -108,15 +108,15 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 앵커와 피벗을 왼쪽 상단으로 설정합니다.
/// RectTransform의 앵커와 피벗을 왼쪽 상단으로 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="trans">대상 RectTransform</param>
/// <example>
/// <code>
/// // UI 요소를 왼쪽 상단에 위치시키기
/// // UI 요소의 기준점을 왼쪽 상단으로 설정
/// RectTransform elementRect = element.GetComponent<RectTransform>();
/// elementRect.SetAnchorsToTopLeft();
/// elementRect.anchoredPosition = new Vector2(10f, -10f); // 약간의 여백 추가
/// elementRect.anchoredPosition = new Vector2(10f, -10f); // 왼쪽 상단에서 오프셋 설정
/// </code>
/// </example>
public static void SetAnchorsToTopLeft(this RectTransform trans)
@@ -127,12 +127,12 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform을 부모 영역에 꽉 차게 설정합니다(마진 없음).
/// RectTransform을 부모의 크기에 맞게 늘립니다 (모든 여백 0).
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="trans">대상 RectTransform</param>
/// <example>
/// <code>
/// // 배경 이미지를 패널 전체에 채우기
/// // 배경 이미지를 부모 패널에 꽉 채우기
/// RectTransform backgroundRect = backgroundImage.GetComponent<RectTransform>();
/// backgroundRect.StretchToParentEdges();
/// </code>
@@ -143,13 +143,13 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 위치를 부모 RectTransform 내의 상대 위치(0~1)로 설정합니다.
/// RectTransform의 위치를 부모 RectTransform 내의 정규화된 좌표(0~1)로 설정합니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <param name="normalizedPosition">정규화된 위치 (0,0=왼쪽 하단, 1,1=오른쪽 상단)</param>
/// <param name="trans">대상 RectTransform</param>
/// <param name="normalizedPosition">정규화된 위치 (0,0=왼쪽 하단, 1,1=오른쪽 상단)</param>
/// <example>
/// <code>
/// // 요소를 부모의 오른쪽 상단에 배치
/// // UI 요소를 부모의 오른쪽 상단 근처에 배치
/// RectTransform elementRect = element.GetComponent<RectTransform>();
/// elementRect.SetNormalizedPosition(new Vector2(0.95f, 0.95f));
/// </code>
@@ -170,13 +170,13 @@ namespace UVC.Extension
}
/// <summary>
/// RectTransform의 사각형 정보를 월드 좌표로 반환합니다.
/// RectTransform의 월드 좌표 기준 사각 영역을 가져옵니다.
/// </summary>
/// <param name="trans">대상 RectTransform</param>
/// <returns>월드 좌표계의 사각형 경계</returns>
/// <param name="trans">대상 RectTransform</param>
/// <returns>월드 좌표계에서의 Rect</returns>
/// <example>
/// <code>
/// // UI 요소가 특정 월드 좌표를 포함하는지 확인
/// // 마우스가 UI 요소 위에 있는지 확인
/// RectTransform elementRect = element.GetComponent<RectTransform>();
/// Rect worldRect = elementRect.GetWorldRect();
/// Vector3 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);

View File

@@ -1,10 +1,11 @@
#nullable enable
#nullable enable
using SampleProject;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.LightTransport;
using UVC.Core;
using UVC.Data;
using UVC.Data.Core;
@@ -34,7 +35,7 @@ namespace UVC.Factory.Alarm
// 알람 데이터에 포함된 설비 ID를 이용해 실제 3D 객체를 찾기 위해 사용됩니다.
private FactoryObjectManager? dataManager;
private bool testMode = true; // 테스트 모드 여부를 나타내는 플래그입니다.
private bool testMode = false; // 테스트 모드 여부를 나타내는 플래그입니다.
// 테스트용으로 사용할 AGV 이름 리스트입니다.
private List<string> agvNames = new List<string>();
@@ -45,6 +46,10 @@ namespace UVC.Factory.Alarm
// 테스트용으로, 새로 발생하는 알람에 AGV ID를 순차적으로 할당하기 위한 인덱스입니다.
private int agvIdx = 50;
private List<IDataObject> buffers = new List<IDataObject>();
private bool runned = false;
/// <summary>
/// AlarmManager의 초기화 메서드입니다.
/// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다.
@@ -54,6 +59,9 @@ namespace UVC.Factory.Alarm
// SceneMain의 초기화가 완료되었을 때 OnSceneInitialized 메서드를 호출하도록 이벤트에 등록합니다.
// 이를 통해 필요한 다른 매니저들이 준비된 후에 로직을 실행할 수 있습니다.
SceneMain.Instance.Initialized += OnSceneInitialized;
//playback에서도 데이터를 업데이트 하기에 DataRepository에 핸들러를 추가합니다.
DataRepository.Instance.AddDataUpdateHandler("ALARM", OnUpdateData);
}
/// <summary>
@@ -81,7 +89,8 @@ namespace UVC.Factory.Alarm
/// 이 메서드는 외부에서 호출되어야 알람 데이터 수신이 시작됩니다.
/// </summary>
public void Run()
{
{
Debug.Log($"AlarmManager Run. buffers.Count:{buffers.Count}");
// MQTT 파이프라인 정보(MqttSubscriptionConfig) 생성:
// - "ALARM" 토픽을 구독합니다.
// - 위에서 정의한 dataMask를 사용해 수신된 JSON 데이터를 DataObject로 변환합니다.
@@ -96,10 +105,30 @@ namespace UVC.Factory.Alarm
// 4. 생성한 파이프라인을 전역 MQTT 파이프라인에 추가하여 데이터 수신을 시작합니다.
DataRepository.Instance.MqttReceiver.Add(pipelineInfo);
//playback에서도 데이터를 업데이트 하기에 DataRepository에 핸들러를 추가합니다.
DataRepository.Instance.AddDataUpdateHandler("ALARM", OnUpdateData);
runned = true;
// 초기 데이터가 있다면 즉시 업데이트를 수행합니다.
if (buffers.Count == 0) {
var buffer = DataRepository.Instance.GetData("ALARM");
Debug.Log($"AlarmManager Run. buffer == null:{buffer == null}");
if (buffer != null)
{
OnUpdateData(buffer);
}
}
else
{
// 초기 데이터가 있다면, OnUpdateData를 호출하여 기존 데이터를 처리합니다.
foreach (var buffer in buffers)
{
OnUpdateData(buffer);
}
buffers.Clear(); // 초기 데이터 처리가 끝나면 버퍼를 비웁니다.
}
}
/// <summary>
/// MQTT 파이프라인으로부터 데이터가 업데이트될 때마다 호출되는 메인 핸들러입니다.
/// 수신된 알람 데이터 배열을 분석하여 추가, 수정, 제거된 알람을 각각 처리합니다.
@@ -113,12 +142,21 @@ namespace UVC.Factory.Alarm
DataArray? arr = data as DataArray;
if (arr == null || arr.Count == 0) return;
//Debug.Log($"AlarmManager OnUpdateData: count:{arr.Count}, Added={arr.AddedItems.Count}, Removed={arr.RemovedItems.Count}, Modified={arr.ModifiedList.Count}");
//3d 객체가 생성 된 후 호출 되도록 하기 위해
// Run으로 호출 된 후 호출 되도록
if (!runned)
{
buffers.Add(data);
return;
}
// 데이터 배열에서 추가, 제거, 수정된 항목 리스트를 가져옵니다.
var AddedItems = arr.AddedItems;
var RemovedItems = new List<DataObject>(arr.RemovedItems);
var ModifiedList = arr.ModifiedList;
//Debug.Log($"AlarmManager OnUpdateData: Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
// 'CLEAR_TIME'이 설정된 항목은 '해제된 알람'으로 간주하고, 제거 리스트에 추가합니다.
// AddedItems나 ModifiedList에 포함되어 있더라도 CLEAR_TIME이 있으면 즉시 해제 처리하기 위함입니다.

View File

@@ -7,16 +7,15 @@ using UnityEngine.UI;
using UVC.Factory.Component;
using UVC.Locale;
using UVC.UI.Modal;
using UVC.Util;
namespace UVC.Factory.Buttons
{
public class UISearchInput : MonoBehaviour
{
[SerializeField]
private TMP_InputField inputField;
private Button button;
private GameObject textArea;
[SerializeField]
private Button searchButton;
private bool initialized = false;
@@ -27,31 +26,23 @@ namespace UVC.Factory.Buttons
SceneMain.Instance.Initialized += OnSceneInitialized;
}
private void OnSceneInitialized()
private void OnSceneInitialized()
{
inputField = GetComponent<TMP_InputField>();
button = GetComponentInChildren<Button>();
button.onClick.AddListener(() =>
searchButton.onClick.AddListener(() =>
{
isClicked = !isClicked;
if (isClicked)
{
//button.image.color = ColorUtil.FromHex("#00B0B0");
//textArea.gameObject.SetActive(true);
inputField.Select();
}
else
{
Location(inputField.text);
//button.image.color = Color.white;
//textArea.gameObject.SetActive(false);
inputField.OnDeselect(null);
}
});
textArea = transform.Find("Text Area").gameObject;
inputField.onEndEdit.AddListener(OnFindLocation);
//textArea.gameObject.SetActive(false);
Init();
}
@@ -64,8 +55,7 @@ namespace UVC.Factory.Buttons
{
Location(locationID);
isClicked = false;
button.image.color = Color.white;
//textArea.gameObject.SetActive(false);
searchButton.image.color = Color.white;
inputField.OnDeselect(null);
}
}
@@ -92,7 +82,6 @@ namespace UVC.Factory.Buttons
inputField.placeholder.GetComponent<TextMeshProUGUI>().text = LocalizationManager.Instance.GetString("search_location");
initialized = true;
}
//textArea.gameObject.SetActive(false);
isClicked = false;
}
@@ -106,10 +95,10 @@ namespace UVC.Factory.Buttons
inputField = null;
}
if (button != null)
if (searchButton != null)
{
button.onClick.RemoveAllListeners();
button = null;
searchButton.onClick.RemoveAllListeners();
searchButton = null;
}
}
@@ -120,10 +109,9 @@ namespace UVC.Factory.Buttons
{
if (EventSystem.current.currentSelectedGameObject != null)
return;
//textArea.gameObject.SetActive(false);
inputField.OnDeselect(null);
inputField.text = "";
button.image.color = Color.white;
searchButton.image.color = Color.white;
isClicked = false;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data.Core;
@@ -35,11 +35,17 @@ namespace UVC.Factory.Component
[Tooltip("이 거리(미터)를 초과하면 보간 없이 즉시 위치를 변경합니다.")]
public float teleportDistanceThreshold = 5.0f; // 5미터 이상 차이나면 순간이동
private Renderer renderer;
/// <summary>
/// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다.
/// </summary>
private void Start()
{
renderer = modelObject.GetComponent<Renderer>();
// 시작 시에는 현재 위치를 목표 위치로 설정하여 의도치 않은 움직임을 방지합니다.
targetPosition = transform.position;
targetRotation = transform.rotation;
@@ -81,15 +87,15 @@ namespace UVC.Factory.Component
}
else // 이미 데이터가 있는 경우 (업데이트)
{
if (data.Id == "HFF09CNA8061") Debug.Log($"AGV: {newData}");
// 새 데이터를 기반으로 목표 위치와 회전을 갱신합니다.
UpdatePositionAndRotation(newData);
// 기존 데이터(data)에 새로운 데이터(newData)의 내용을 덮어씁니다.
foreach (var keyValue in newData)
{
if (data.ContainsKey(keyValue.Key))
{
data[keyValue.Key] = keyValue.Value;
}
{
data[keyValue.Key] = keyValue.Value;
}
}
}
@@ -111,11 +117,14 @@ namespace UVC.Factory.Component
}
else // 이후 업데이트의 경우
{
bool isTeleport = false;
bool changed = false;
bool isTeleport = false;
float? newX = newData.GetFloat("X");
float? newY = newData.GetFloat("Y");
if (data.Id == "HFF09CNA8061") Debug.Log($"AGV newX.HasValue:{newX.HasValue}, newY.HasValue:{newY.HasValue}");
if (newX.HasValue || newY.HasValue)
{
float x = data.GetFloat("X").Value;
@@ -124,12 +133,13 @@ namespace UVC.Factory.Component
Vector3 newTargetPosition = transform.position;
if (newX.HasValue && x != newX) newTargetPosition.x = newX.Value * scaleFactor;
if (newY.HasValue && y != newY) newTargetPosition.z = newY.Value * scaleFactor;
if (newTargetPosition != transform.position)
// 현재 위치와 새로운 목표 위치 사이의 거리를 계산합니다.
float distanceToTarget = Vector3.Distance(transform.position, newTargetPosition);
if (data.Id == "HFF09CNA8061") Debug.Log($"AGV distanceToTarget:{distanceToTarget}, x:{newTargetPosition.x}_{transform.position.x}, z:{newTargetPosition.z}_{transform.position.z}, y:{newTargetPosition.y}_{transform.position.y}");
if (distanceToTarget > 0)
{
// 현재 위치와 새로운 목표 위치 사이의 거리를 계산합니다.
float distanceToTarget = Vector3.Distance(transform.position, newTargetPosition);
//Debug.Log($"AGV {data.GetString("VHL_NAME")} moving to new position: {newTargetPosition} (Distance: {distanceToTarget})");
// 거리가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 위치을 설정합니다.
if (distanceToTarget > teleportDistanceThreshold)
@@ -141,6 +151,7 @@ namespace UVC.Factory.Component
// 새로운 목표 지점을 설정합니다.
// (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.)
this.targetPosition = newTargetPosition;
changed = true;
}
}
@@ -157,12 +168,28 @@ namespace UVC.Factory.Component
// 새로운 목표 지점을 설정합니다.
// (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.)
this.targetRotation = newTargetRotation;
changed = true;
}
}
if (data.Id == "HFF09CNA8061") Debug.Log($"AGV changed: {changed}");
if (changed)
{
ChangeColor(Color.red);
}
else
{
//ChangeColor(Color.white);
}
}
}
private void ChangeColor(Color color)
{
renderer.material.color = color;
}
/// <summary>
/// Unity에 의해 매 프레임마다 호출되는 메서드입니다.
/// AGV의 현재 위치/회전을 목표 위치/회전으로 부드럽게 이동시키는 시각적 처리를 담당합니다.
@@ -173,7 +200,7 @@ namespace UVC.Factory.Component
if (transform.position != targetPosition)
{
// 목표 지점과의 거리가 매우 가까우면 (0.01미터 미만) 그냥 목표 위치로 설정하여 미세한 떨림을 방지합니다.
if (Vector3.Distance(transform.position, targetPosition) < 0.1f)
if (Vector3.Distance(transform.position, targetPosition) < 0.01f)
{
// 현재 위치와 목표 위치 사이의 거리가 임계값을 초과하면 순간이동합니다.
transform.position = targetPosition;
@@ -190,7 +217,7 @@ namespace UVC.Factory.Component
if (transform.rotation != targetRotation)
{
// 목표 회전과의 각도 차이가 매우 작으면 (0.1도 미만) 그냥 목표 회전으로 설정합니다.
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.1f)
if (Quaternion.Angle(transform.rotation, targetRotation) < 0.01f)
{
transform.rotation = targetRotation;
}

View File

@@ -1,8 +1,9 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using SampleProject;
using System;
using System.Linq;
using Unity.Burst.Intrinsics;
using UnityEngine;
using UVC.Core;
using UVC.Data;
@@ -94,6 +95,9 @@ namespace UVC.Factory.Component
}
}
public Action? OnAGVCreated;
private bool created = false;
/// <summary>
@@ -102,28 +106,32 @@ namespace UVC.Factory.Component
/// </summary>
protected override void Init()
{
SceneMain.Instance.Initialized += OnSceneInitializedAsync;
InitializePoolAsync().ContinueWith(() =>
{
//playback에서도 데이터를 업데이트 하기에 DataRepository에 핸들러를 추가합니다.
DataRepository.Instance.AddDataUpdateHandler("AGV", OnUpdateData);
var pipelineInfo = new MqttSubscriptionConfig("AGV");
var dataMapperValidator = DataMapperValidator.Get("AGV");
if (dataMapperValidator?.DataMapper != null) pipelineInfo.setDataMapper(dataMapperValidator.DataMapper);
if (dataMapperValidator?.Validator != null) pipelineInfo.setValidator(dataMapperValidator.Validator);
// 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다.
DataRepository.Instance.MqttReceiver.Add(pipelineInfo);
SceneMain.Instance.Initialized += OnSceneInitialized;
});
}
/// <summary>
/// 씬이 완전히 초기화된 후 호출됩니다.
/// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다.
/// </summary>
private async void OnSceneInitializedAsync()
private void OnSceneInitialized()
{
await InitializePoolAsync();
var pipelineInfo = new MqttSubscriptionConfig("AGV");
var dataMapperValidator = DataMapperValidator.Get("AGV");
if (dataMapperValidator?.DataMapper != null) pipelineInfo.setDataMapper(dataMapperValidator.DataMapper);
if (dataMapperValidator?.Validator != null) pipelineInfo.setValidator(dataMapperValidator.Validator);
// 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다.
DataRepository.Instance.MqttReceiver.Add(pipelineInfo);
//playback에서도 데이터를 업데이트 하기에 DataRepository에 핸들러를 추가합니다.
DataRepository.Instance.AddDataUpdateHandler("AGV", OnUpdateData);
}
/// <summary>
@@ -152,7 +160,7 @@ namespace UVC.Factory.Component
{
if (data == null || agvPool == null) return;
DataArray? arr = data as DataArray;
DataArray ? arr = data as DataArray;
if (arr == null || arr.Count == 0) return;
// 데이터 배열에서 추가, 제거, 수정된 항목 리스트를 가져옵니다.
@@ -160,7 +168,7 @@ namespace UVC.Factory.Component
var RemovedItems = arr.RemovedItems;
var ModifiedList = arr.ModifiedList;
//Debug.Log($"AGVManager received data: Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
Debug.Log($"AGVManager received data: count:{arr.Count}, Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}");
// 새로 추가된 AGV 처리
foreach (var item in AddedItems.ToList())
@@ -172,7 +180,7 @@ namespace UVC.Factory.Component
continue;
}
agv.Info = new FactoryObjectInfo(
item.GetString("NODE_ID"),
item.GetString("VHL_NAME"),
item.GetString("VHL_NAME"),
"",
"",
@@ -207,7 +215,8 @@ namespace UVC.Factory.Component
{
created = true;
// 씬이 처음 초기화될 때 AGVManager가 생성되었음을 알립니다.
SceneMain.Instance.OnAGVManagerCreated();
Debug.Log("AGVManager created and initialized.");
OnAGVCreated?.Invoke();
}
}
@@ -218,9 +227,10 @@ namespace UVC.Factory.Component
protected override void OnDestroy()
{
base.OnDestroy();
SceneMain.Instance.Initialized -= OnSceneInitializedAsync;
SceneMain.Instance.Initialized -= OnSceneInitialized;
DataRepository.Instance.RemoveDataUpdateHandler("AGV", OnUpdateData);
agvPool?.ClearRecycledItems();
OnAGVCreated = null; // 이벤트 핸들러 초기화
}
}
}

View File

@@ -61,7 +61,7 @@ namespace UVC.Factory.Playback
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
await HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}
@@ -87,7 +87,7 @@ namespace UVC.Factory.Playback
httpRequestConfig.AddSplitConfig("ALARM", DataMapperValidator.Get("ALARM"));
foreach (var item in list)
{
await HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
HttpDataProcessor.ProcessSplitResponse(httpRequestConfig, item.data);
}
}
}

View File

@@ -224,6 +224,7 @@ namespace UVC.Network
/// <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)
{
HTTPMethods method = StringToMethod(methodString);
if (!url.Contains("http")) url = $"{Domain}{url}";
@@ -249,11 +250,13 @@ namespace UVC.Network
//Debug.Log($"Request APIToken :{AuthService.Instance.Entiti.accessToken}");
if (body != null)
{
request.UploadSettings.UploadStream =
new MemoryStream(Encoding.UTF8.GetBytes(body));
request.UploadSettings.UploadStream = new MemoryStream(Encoding.UTF8.GetBytes(body));
}
bool isMainThread = PlayerLoopHelper.IsMainThread;
//var response = await request.GetFromJsonResultAsync<T>();
var response = await request.GetAsStringAsync();
if(!isMainThread) await UniTask.SwitchToThreadPool();
log.ResponseData = response;
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
ServerLog.LogHttpResponse(log);
@@ -418,7 +421,9 @@ namespace UVC.Network
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
//var response = await request.GetFromJsonResultAsync<T>();
var now = DateTime.UtcNow;
bool isMainThread = PlayerLoopHelper.IsMainThread;
var response = await request.GetAsStringAsync();
if (!isMainThread) await UniTask.SwitchToThreadPool();
var diff = DateTime.UtcNow - now;
log.ResponseData = response;
log.ResponseDate = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Cysharp.Threading.Tasks;
using NUnit.Framework;
@@ -192,11 +192,11 @@ namespace UVC.Tests.Data
}
/// <summary>
/// HttpDataFetcher의 private infoList 필드 가져오기
/// HttpDataFetcher의 private configList 필드 가져오기
/// </summary>
private Dictionary<string, HttpRequestConfig> GetInfoListField()
{
var fieldInfo = typeof(HttpDataFetcher).GetField("infoList",
var fieldInfo = typeof(HttpDataFetcher).GetField("configList",
BindingFlags.NonPublic | BindingFlags.Instance);
return (Dictionary<string, HttpRequestConfig>)fieldInfo.GetValue(pipeLine);

View File

@@ -31,24 +31,23 @@ namespace UVC.UI.Loading
}
}
[SerializeField]
private CanvasGroup canvasGroup;
private float target = 0;
[SerializeField]
private Image loadinImage;
private float duration = 0.25f;
private float target = 0;
private float alpha = 1;
private bool animatting = false;
private Image loadinImage;
private Transform loadingImageTransform;
private float loadingSpeed = 1.5f;
private float rotationSpeed = 1.0f;
private float rotationSpeed = -1.0f;
private void Awake()
{
canvasGroup = GetComponent<CanvasGroup>();
loadinImage = transform.Find("loadingImage").GetComponent<Image>();
{
loadingImageTransform = loadinImage.transform;
}

View File

@@ -3,6 +3,7 @@ using UnityEngine;
using UnityEngine.UI;
using UVC.Data.Core;
using UVC.Factory.Component;
using UVC.Factory.Playback;
using UVC.Locale;
using UVC.Log;
using UVC.UI.Commands;
@@ -216,7 +217,7 @@ namespace UVC.UI.Menu
MenuItemData.CreateSeparator("file_sep2"), // 또 다른 구분선 추가
new MenuItemData("file_exit", "menu_file_exit", new QuitApplicationCommand()) // 애플리케이션 종료 명령 연결
}));
model.MenuItems.Add(new MenuItemData("Playback", "Playback", new PlaybackCommand()));
// pool 로그
model.MenuItems.Add(new MenuItemData("log", "Log", subMenuItems: new List<MenuItemData>
{

View File

@@ -1,5 +1,6 @@
{
"language": "ko",
"targetFrameRate": 60,
"fullScreenResolution": null,
"window": {
"fullScreenResolution": {