CompletionResult 기반 UI 색상 매핑 및 파이프라인 완료 검증 추가

- TwinAgentAutoProcessor에 _completionResults 저장소 추가하여 단계별 완료 결과를 UI에 전달
- EntityPropertyAdapter의 색상 결정을 문자열 매칭에서 CompletionResult 기반으로 변경
  - Complete → 초록, Warning → 주황, Error → 빨강, SubStep → 흰색
- 터미널 로그 및 상태 복원에도 동일한 색상 매핑 적용
- 파이프라인 중단 시 completeTerminal 메시지를 "Process stopped"으로 구분
- MachineInfoItemManager에서 IsFullPipelineCompleted 체크 추가하여 에러 중단 시 UI 바인딩 방지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
wsh
2026-02-10 18:13:42 +09:00
parent 2d03cc7d1b
commit 85f4a959af
8 changed files with 81 additions and 131 deletions

View File

@@ -9,7 +9,8 @@
"Bash(powershell -Command \"\\(Get-Content ''Assets\\\\DownloadAssets\\\\XRLib\\\\Scripts\\\\UVC\\\\Entity\\\\Processors\\\\TwinAgentAutoProcessor.cs'' -Raw\\) -replace ''Completions\\\\[Completions\\\\.Length - 1\\\\]'',''Completions[^1]'' | Set-Content ''Assets\\\\DownloadAssets\\\\XRLib\\\\Scripts\\\\UVC\\\\Entity\\\\Processors\\\\TwinAgentAutoProcessor.cs'' -NoNewline\")",
"Bash(powershell -Command \"\\(Get-Content ''Assets\\\\DownloadAssets\\\\XRLib\\\\Scripts\\\\UVC\\\\Entity\\\\Processors\\\\TwinAgentAutoProcessor.cs'' -Raw\\) -replace ''public struct StepProgressInfo'',''public readonly struct StepProgressInfo'' | Set-Content ''Assets\\\\DownloadAssets\\\\XRLib\\\\Scripts\\\\UVC\\\\Entity\\\\Processors\\\\TwinAgentAutoProcessor.cs'' -NoNewline\")",
"Bash(git add:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nTwinAgentAutoProcessor 가중치 완료조건 및 Entity별 터미널 로그 분리\n\n- CompletionResult enum 추가 \\(Complete/Warning/Error\\)로 완료 상태 분류\n- WeightedCompletion으로 SubStep별 가중치 기반 랜덤 완료 조건 구현\n - Server: OctopusHub 75% / NotFound 25%\\(Error, 중지\\)\n - Protocol: API 75% / MQTT 25%\n - ServerStatusCheck: Done 85% / Busy 10%\\(Warning, 중지\\) / Error 5%\\(중지\\)\n - Speed: 5~120ms 80% / \"300ms~\" 20%\n- IsFullPipelineCompleted 속성으로 전체 파이프라인 완료 여부 판별\n- MessageHistory 버퍼로 Entity별 터미널 로그 이력 관리\n- Entity 전환 시 ClearAllProcessorScopedBindings\\(\\)로 이전 UI 바인딩 해제\n- EntityPropertyAdapter에서 PropertyItem 복원과 터미널 복원 순서 분리\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")"
"Bash(git commit -m \"$\\(cat <<''EOF''\nTwinAgentAutoProcessor 가중치 완료조건 및 Entity별 터미널 로그 분리\n\n- CompletionResult enum 추가 \\(Complete/Warning/Error\\)로 완료 상태 분류\n- WeightedCompletion으로 SubStep별 가중치 기반 랜덤 완료 조건 구현\n - Server: OctopusHub 75% / NotFound 25%\\(Error, 중지\\)\n - Protocol: API 75% / MQTT 25%\n - ServerStatusCheck: Done 85% / Busy 10%\\(Warning, 중지\\) / Error 5%\\(중지\\)\n - Speed: 5~120ms 80% / \"300ms~\" 20%\n- IsFullPipelineCompleted 속성으로 전체 파이프라인 완료 여부 판별\n- MessageHistory 버퍼로 Entity별 터미널 로그 이력 관리\n- Entity 전환 시 ClearAllProcessorScopedBindings\\(\\)로 이전 UI 바인딩 해제\n- EntityPropertyAdapter에서 PropertyItem 복원과 터미널 복원 순서 분리\n\nCo-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(dir:*)"
]
}
}

View File

@@ -710,8 +710,8 @@ MonoBehaviour:
m_TargetGraphic: {fileID: 8654063950112981296}
m_HandleRect: {fileID: 0}
m_Direction: 0
m_Value: 0
m_Size: 0.99995804
m_Value: 1
m_Size: 0.9997558
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
@@ -1349,8 +1349,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0.0001373291}
m_SizeDelta: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0.000015258789}
m_SizeDelta: {x: 0, y: -0.17746803}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &4790256706883938101
MonoBehaviour:
@@ -1385,7 +1385,7 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6982330689999531319}
m_Enabled: 1
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:

View File

@@ -273,6 +273,9 @@ namespace UVC.Entity.Processors
/// <summary>완료된 단계 상태 (PropertyId -> Value). PropertyItem 복원용.</summary>
private readonly Dictionary<string, string> _stepStates = new();
/// <summary>완료된 단계의 결과 분류 (PropertyId -> CompletionResult). UI 색상 매핑용.</summary>
private readonly Dictionary<string, CompletionResult> _completionResults = new();
/// <summary>전체 메시지 이력 (Entity별 터미널 로그 복원용)</summary>
private readonly List<(string PropertyId, string Value)> _messageHistory = new();
@@ -287,6 +290,10 @@ namespace UVC.Entity.Processors
/// <summary>전체 파이프라인이 중지 없이 완료되었는지 여부 (Speed까지 완료해야 true)</summary>
public bool IsFullPipelineCompleted { get; private set; }
/// <summary>특정 단계의 완료 결과를 반환합니다. 아직 완료되지 않은 단계는 null.</summary>
public CompletionResult? GetCompletionResult(string propertyId)
=> _completionResults.TryGetValue(propertyId, out var result) ? result : null;
/// <summary>SubStep 진행 시 콜백 (진행도 정보 포함)</summary>
public event Action<StepProgressInfo> onSubStepChanged;
@@ -440,6 +447,7 @@ namespace UVC.Entity.Processors
{
_stepStates.Clear();
_messageHistory.Clear();
_completionResults.Clear();
_currentPropertyId = null;
IsFullPipelineCompleted = false;
Debug.Log("[TwinAgentAutoProcessor] 초기화 완료");
@@ -484,6 +492,7 @@ namespace UVC.Entity.Processors
// 완료 상태 선택 (가중치 기반) 및 전송
var completion = def.SelectCompletion();
var completionStatus = completion.Status;
_completionResults[def.PropertyId] = completion.Result;
SendMessage(def.PropertyId, completionStatus);
_messageHistory.Add((def.PropertyId, completionStatus));
_stepStates[def.PropertyId] = completionStatus;

View File

@@ -254,12 +254,7 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st
// TextColor를 SetValue보다 먼저 설정해야 UI의 UpdateValue에서 올바른 색상을 읽음
if (item is LabelProperty label)
{
label.TextColor = value switch
{
"Done" => Color.green,
"Canceled" => new Color(1f, 0.647f, 0f),
_ => Color.white
};
label.TextColor = GetStepColor(value, processor.GetCompletionResult(propertyId));
}
item.SetValue(value);
@@ -329,18 +324,16 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st
if (propId == processor.ProcessorId) return;
string stepName = stepNames.TryGetValue(propId, out var name) ? name : propId;
if (value == "Done")
tv.AddLog($"> [{stepName}] Done", Color.green);
else if (value == "Canceled")
tv.AddLog($"> [{stepName}] Canceled", new Color(1f, 0.647f, 0f));
else
tv.AddLog($"> [{stepName}] {value}");
var color = GetStepColor(value, processor.GetCompletionResult(propId));
tv.AddLog($"> [{stepName}] {value}", color);
};
completeTerminal = () =>
{
tv.AddLog("> Process completed successfully", Color.green);
if (processor.IsFullPipelineCompleted)
tv.AddLog("> Process completed successfully", Color.green);
else
tv.AddLog("> Process stopped", new Color(1f, 0.647f, 0f));
};
cancelTerminal = () =>
@@ -369,18 +362,18 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st
{
if (propId == processor.ProcessorId) continue;
string stepName = stepNames.TryGetValue(propId, out var sn) ? sn : propId;
if (value == "Done")
tv.AddLog($"> [{stepName}] Done", Color.green);
else if (value == "Canceled")
tv.AddLog($"> [{stepName}] Canceled", new Color(1f, 0.647f, 0f));
else
tv.AddLog($"> [{stepName}] {value}");
var color = GetStepColor(value, processor.GetCompletionResult(propId));
tv.AddLog($"> [{stepName}] {value}", color);
}
// 완료/취소 상태에 따라 터미널 종료 메시지 추가
if (processor.IsCompleted)
tv.AddLog("> Process completed successfully", Color.green);
{
if (processor.IsFullPipelineCompleted)
tv.AddLog("> Process completed successfully", Color.green);
else
tv.AddLog("> Process stopped", new Color(1f, 0.647f, 0f));
}
else if (!processor.IsRunning)
tv.AddLog("> Process canceled", new Color(1f, 0.647f, 0f));
}
@@ -502,15 +495,16 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st
{
if (propId == processor.ProcessorId) continue;
string sn = stepNames.TryGetValue(propId, out var n) ? n : propId;
if (value == "Done")
terminalView?.AddLog($"> [{sn}] Done", Color.green);
else if (value == "Canceled")
terminalView?.AddLog($"> [{sn}] Canceled", new Color(1f, 0.647f, 0f));
else
terminalView?.AddLog($"> [{sn}] {value}");
var color = GetStepColor(value, processor.GetCompletionResult(propId));
terminalView?.AddLog($"> [{sn}] {value}", color);
}
if (processor.IsCompleted)
terminalView?.AddLog("> Process completed successfully", Color.green);
{
if (processor.IsFullPipelineCompleted)
terminalView?.AddLog("> Process completed successfully", Color.green);
else
terminalView?.AddLog("> Process stopped", new Color(1f, 0.647f, 0f));
}
else if (!processor.IsRunning)
terminalView?.AddLog("> Process canceled", new Color(1f, 0.647f, 0f));
@@ -555,6 +549,21 @@ private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject st
}
/// <summary>
/// CompletionResult 기반으로 단계 텍스트 색상을 결정합니다.
/// </summary>
private static Color GetStepColor(string value, CompletionResult? result)
{
if (value == "Canceled") return new Color(1f, 0.647f, 0f);
return result switch
{
CompletionResult.Complete => Color.green,
CompletionResult.Warning => new Color(1f, 0.647f, 0f),
CompletionResult.Error => Color.red,
_ => Color.white
};
}
private static void CreateTwinAgentProgressItems(
List<IPropertyItem> properties,
Dictionary<string, IPropertyItem> propertyDict,

View File

@@ -750,69 +750,6 @@ RectTransform:
m_AnchoredPosition: {x: 150, y: -85}
m_SizeDelta: {x: 100, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &2237869627694328014
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6554659810962538785}
- component: {fileID: 5302855139812990020}
m_Layer: 0
m_Name: Name
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6554659810962538785
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2237869627694328014}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5167543184334540414}
m_Father: {fileID: 9208340457178526904}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 54.3, y: -8.6}
m_SizeDelta: {x: 100, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5302855139812990020
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2237869627694328014}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 5
m_Spacing: -10
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 0
m_ChildControlHeight: 0
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &2289400806680334342
GameObject:
m_ObjectHideFlags: 0
@@ -1020,7 +957,7 @@ RectTransform:
- {fileID: 6745655140557672707}
- {fileID: 7725313684788843977}
- {fileID: 3190177910311329453}
- {fileID: 6554659810962538785}
- {fileID: 5167543184334540414}
- {fileID: 8742493579327078235}
m_Father: {fileID: 4042325045007633289}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -2605,7 +2542,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 84.5, y: 0}
m_AnchoredPosition: {x: 84.5, y: -8}
m_SizeDelta: {x: 26, y: 26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &618883874101042488
@@ -3358,7 +3295,7 @@ GameObject:
- component: {fileID: 7560852003513131910}
- component: {fileID: 9051470887640582819}
m_Layer: 0
m_Name: +55%
m_Name: Rate
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -3376,11 +3313,11 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6554659810962538785}
m_Father: {fileID: 9208340457178526904}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 170.93, y: -38.099976}
m_SizeDelta: {x: 85.74, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7560852003513131910
@@ -3411,7 +3348,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: +55%
m_text: +99%
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 155cf7b89710f7e40aa64ac01771e416, type: 2}
m_sharedMaterial: {fileID: 2250789165734441016, guid: 155cf7b89710f7e40aa64ac01771e416, type: 2}

View File

@@ -630,6 +630,14 @@ PrefabInstance:
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1205322734457541541, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0.00012207031
objectReference: {fileID: 0}
- target: {fileID: 1554032197748324192, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3}
propertyPath: m_Enabled
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1602519165350808158, guid: 4b98d7ee8b805ff42be384e91f3bf8a4, type: 3}
propertyPath: m_Name
value: PropertyWindow
@@ -4752,9 +4760,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 55e9e2efe7596a746b0c8f0332bab2f9, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::OCTOPUS_TWIN.CameraRoute
gizmoResolution: 20
gizmoColor: {r: 0, g: 1, b: 1, a: 1}
movePoints: []
--- !u!4 &1716751011
Transform:
m_ObjectHideFlags: 0

View File

@@ -219,27 +219,28 @@ namespace ChunilENG.Management
{
if (stageObject.TryGetProcessor<TwinAgentAutoProcessor>(out var processor))
{
if (processor.IsCompleted)
if (processor.IsCompleted && processor.IsFullPipelineCompleted)
{
RegistStageObject(stageObject);
return;
}
processor.onComplete += () => OnProcessorCompleted(stageObject);
processor.onComplete += () => OnProcessorCompleted(stageObject, processor);
return;
}
// 프로세서가 아직 없으면 등록 시점에 구독
stageObject.OnProcessorRegistered += registeredProcessor =>
{
if (registeredProcessor is TwinAgentAutoProcessor)
if (registeredProcessor is TwinAgentAutoProcessor autoProcessor)
{
registeredProcessor.onComplete += () => OnProcessorCompleted(stageObject);
registeredProcessor.onComplete += () => OnProcessorCompleted(stageObject, autoProcessor);
}
};
}
private void OnProcessorCompleted(StageObjectManager.StageObject stageObject)
private void OnProcessorCompleted(StageObjectManager.StageObject stageObject, TwinAgentAutoProcessor processor)
{
if (!processor.IsFullPipelineCompleted) return;
if (itemToStageObjects.ContainsValue(stageObject)) return;
RegistStageObject(stageObject);
}

View File

@@ -60,13 +60,16 @@ namespace ChunilENG.UI
transform.TryGetComponentInChildren(nameof(goodqtyrate), out goodqtyrate);
transform.TryGetComponentInChildren(nameof(workdt), out workdt);
transform.TryGetComponentInChildren(nameof(wordno), out wordno);
transform.TryGetComponentInChildren("Rate", out TextMeshProUGUI rate);
var r = UnityEngine.Random.Range(90, 99);
rate.SetText($"+{r}%");
}
public void SetData(CompleteInfo data, string machineName)
{
this.data = data;
eorate.SetText(DecimalPointCalculate(data.eorate).ToString() + "%");
daynight.SetText(SetTextData((data.daynight == "1")? "ÁÖ°£" : "¾ß°£"));
daynight.SetText(SetTextData((data.daynight == "1")? "<EFBFBD>ְ<EFBFBD>" : "<EFBFBD>߰<EFBFBD>"));
workcd.SetText(SetTextData(data.workcd));
goodqtyrate.SetText(DecimalPointCalculate(data.goodqtyrate).ToString() + "%");
porate.SetText(DecimalPointCalculate(data.porate).ToString() + "%");
@@ -81,23 +84,8 @@ namespace ChunilENG.UI
}
private Color SetStatusColor(string value)
{
var color = Color.white;
var color = Color.green;
switch (value)
{
case "-":
color = Color.black;
break;
case "°¡µ¿Áß":
color = Color.green;
break;
case "ºñ°¡µ¿":
color = Color.red;
break;
default:
color = Color.yellow;
break;
}
return color;
}
private bool CheckDataExists(string value)