Files
TornadoAce_CJOnStyle/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs

427 lines
18 KiB
C#

using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using System.Collections.ObjectModel;
using TornadoAce_CJOnStyle.Domain;
using TornadoAce_CJOnStyle.Services;
using Windows.UI;
namespace TornadoAce_CJOnStyle.ViewModels
{
public partial class MainViewModel : BaseViewModel
{
private readonly AutomationWorkspace workspace;
private readonly PocAutomationEngine automationEngine;
private int selectedCueSheetIndex;
private CueSheetRecord activeCueSheet = null!;
private ErpScheduleRecord? activeSchedule;
private SceneDataSnapshot activeScene = null!;
public MainViewModel()
{
Title = "CG 제작 자동화";
workspace = AutomationWorkspace.CreateDemo();
automationEngine = new PocAutomationEngine(workspace);
var initialSelection = automationEngine.SelectCueSheet(selectedCueSheetIndex);
var initialInspectionSummary = automationEngine.InspectWorkspace(workspace.ActiveTemplate);
activeCueSheet = initialSelection.Record;
activeSchedule = automationEngine.FindSchedule(activeCueSheet);
activeScene = automationEngine.GenerateScene(activeCueSheet, workspace.ActiveTemplate);
BuildPhase = "INITIAL BUILD";
OperatorMessage = "큐시트 플랫폼, Tornado Scene 템플릿, 자동 검수 결과를 한 화면에서 확인하는 운영 콘솔 초안입니다.";
LastCueSheetSyncText = FormatCueSheetSelection(initialSelection);
ActiveTemplateName = workspace.ActiveTemplate.TemplateName;
InspectionSummary = FormatInspectionSummary(initialInspectionSummary);
SceneSnapshotLabel = $"{activeScene.SceneName} / visible text objects";
SceneSnapshotText = activeScene.ToPreviewJson();
LoginOperatorName = "producer";
LaunchMessage = "TornadoAce";
LlmPolicyText = "사내 LLM 우선, 외부 서비스는 정책 승인 후";
LlmPolicyDetail = "1차 Text Diffing 실패 건만 2차 LLM 판정으로 이동하고, 가격·구성·방송시간 변경은 높은 중요도로 표시합니다.";
LoadStatusCards();
LoadPipelineSteps();
LoadFieldMappings();
LoadMappingValidation();
LoadEndpoints();
LoadTornadoAdapterChecks();
LoadRequirementCoverage();
LoadScheduleMatches();
LoadInspectionQueue(initialInspectionSummary.Results);
LoadInspectionDetails(initialInspectionSummary.Results);
LoadLlmReviewRequests(initialInspectionSummary.Results);
}
private string buildPhase = string.Empty;
private string operatorMessage = string.Empty;
private string lastCueSheetSyncText = string.Empty;
private string activeTemplateName = string.Empty;
private string inspectionSummary = string.Empty;
private string sceneSnapshotLabel = string.Empty;
private string sceneSnapshotText = string.Empty;
private string llmPolicyText = string.Empty;
private string llmPolicyDetail = string.Empty;
private string loginOperatorName = string.Empty;
private string launchMessage = string.Empty;
private Visibility loginScreenVisibility = Visibility.Visible;
private Visibility launchScreenVisibility = Visibility.Collapsed;
private Visibility mainShellVisibility = Visibility.Collapsed;
public string BuildPhase
{
get => buildPhase;
set => SetProperty(ref buildPhase, value);
}
public string OperatorMessage
{
get => operatorMessage;
set => SetProperty(ref operatorMessage, value);
}
public string LastCueSheetSyncText
{
get => lastCueSheetSyncText;
set => SetProperty(ref lastCueSheetSyncText, value);
}
public string ActiveTemplateName
{
get => activeTemplateName;
set => SetProperty(ref activeTemplateName, value);
}
public string InspectionSummary
{
get => inspectionSummary;
set => SetProperty(ref inspectionSummary, value);
}
public string SceneSnapshotLabel
{
get => sceneSnapshotLabel;
set => SetProperty(ref sceneSnapshotLabel, value);
}
public string SceneSnapshotText
{
get => sceneSnapshotText;
set => SetProperty(ref sceneSnapshotText, value);
}
public string LlmPolicyText
{
get => llmPolicyText;
set => SetProperty(ref llmPolicyText, value);
}
public string LlmPolicyDetail
{
get => llmPolicyDetail;
set => SetProperty(ref llmPolicyDetail, value);
}
public ObservableCollection<StatusCardViewModel> StatusCards { get; } = [];
public ObservableCollection<PipelineStepViewModel> PipelineSteps { get; } = [];
public ObservableCollection<FieldMappingViewModel> FieldMappings { get; } = [];
public ObservableCollection<MappingValidationViewModel> MappingValidations { get; } = [];
public ObservableCollection<IntegrationEndpointViewModel> Endpoints { get; } = [];
public ObservableCollection<TornadoAdapterCheckViewModel> TornadoAdapterChecks { get; } = [];
public ObservableCollection<PocRequirementViewModel> RequirementCoverage { get; } = [];
public ObservableCollection<ScheduleMatchViewModel> ScheduleMatches { get; } = [];
public ObservableCollection<InspectionQueueItemViewModel> InspectionQueue { get; } = [];
public ObservableCollection<InspectionDetailItemViewModel> InspectionDetails { get; } = [];
public ObservableCollection<LlmReviewRequestViewModel> LlmReviewRequests { get; } = [];
public string LoginOperatorName
{
get => loginOperatorName;
set => SetProperty(ref loginOperatorName, value);
}
public string LaunchMessage
{
get => launchMessage;
set => SetProperty(ref launchMessage, value);
}
public Visibility LoginScreenVisibility
{
get => loginScreenVisibility;
set => SetProperty(ref loginScreenVisibility, value);
}
public Visibility LaunchScreenVisibility
{
get => launchScreenVisibility;
set => SetProperty(ref launchScreenVisibility, value);
}
public Visibility MainShellVisibility
{
get => mainShellVisibility;
set => SetProperty(ref mainShellVisibility, value);
}
public void BeginLogin(string operatorName, string accessCode)
{
LoginOperatorName = string.IsNullOrWhiteSpace(operatorName) ? "producer" : operatorName.Trim();
LaunchMessage = "TornadoAce";
LoginScreenVisibility = Visibility.Collapsed;
LaunchScreenVisibility = Visibility.Visible;
}
public void CompleteLogin()
{
LaunchScreenVisibility = Visibility.Collapsed;
MainShellVisibility = Visibility.Visible;
OperatorMessage = $"{LoginOperatorName} 제작자가 접속했습니다. 큐시트와 Scene 매핑 상태를 확인하세요.";
}
[RelayCommand]
private void RefreshCueSheet()
{
selectedCueSheetIndex++;
var selection = automationEngine.SelectCueSheet(selectedCueSheetIndex);
activeCueSheet = selection.Record;
activeSchedule = automationEngine.FindSchedule(activeCueSheet);
activeScene = automationEngine.GenerateScene(activeCueSheet, workspace.ActiveTemplate);
LastCueSheetSyncText = FormatCueSheetSelection(selection);
OperatorMessage = $"{activeCueSheet.ProgramTitle} / {activeCueSheet.ProductName} 큐시트를 선택했습니다. {FormatScheduleMatch(activeSchedule)}";
StatusCards[0].Metric = selection.TotalCount.ToString();
StatusCards[0].Detail = $"선택 {selection.Position}/{selection.TotalCount}";
ApplySceneSnapshot(activeScene);
}
[RelayCommand]
private void GenerateScene()
{
activeScene = automationEngine.GenerateScene(activeCueSheet, workspace.ActiveTemplate);
OperatorMessage = $"{activeCueSheet.CueSheetId} 데이터를 {activeScene.SceneName} Scene 스냅샷으로 생성했습니다.";
StatusCards[1].Metric = workspace.ActiveTemplate.Mappings.Count.ToString();
StatusCards[1].Detail = "필드 매핑 완료";
ApplySceneSnapshot(activeScene);
}
[RelayCommand]
private void RunInspection()
{
var summary = automationEngine.InspectWorkspace(workspace.ActiveTemplate);
OperatorMessage = "Scene 추출값을 큐시트와 비교했습니다. 가격/구성 불일치는 높은 중요도로 분류합니다.";
InspectionSummary = FormatInspectionSummary(summary);
StatusCards[2].Metric = $"{summary.AverageMatchRate:0.0}%";
StatusCards[2].Detail = $"확인 {summary.ReviewCount}건";
LoadInspectionQueue(summary.Results);
LoadInspectionDetails(summary.Results);
LoadLlmReviewRequests(summary.Results);
}
private void LoadStatusCards()
{
StatusCards.Add(new StatusCardViewModel("큐시트 API", "12", "수신 대기", Symbol.Sync, Brush(0xFF45D6D1)));
StatusCards.Add(new StatusCardViewModel("Scene 매핑", "5", "템플릿 정의", Symbol.Map, Brush(0xFFB48CFF)));
StatusCards.Add(new StatusCardViewModel("자동 검수", "98.7%", "목표 99%+", Symbol.Accept, Brush(0xFFFFB547)));
StatusCards.Add(new StatusCardViewModel("Tornado", "Adapter", "Mock SDK 경계", Symbol.Repair, Brush(0xFF37D67A)));
}
private void LoadPipelineSteps()
{
PipelineSteps.Add(new PipelineStepViewModel("01", "큐시트/ERP 데이터 수신", "AI ON Studio / CJ OnStyle ERP", "PoC", 55, Brush(0xFF45D6D1)));
PipelineSteps.Add(new PipelineStepViewModel("02", "Data Field Mapping", "큐시트 필드 -> Tornado Scene 객체", "초안", 45, Brush(0xFFB48CFF)));
PipelineSteps.Add(new PipelineStepViewModel("03", "Scene 생성/갱신", "Tornado2 우선, Tornado3 adapter ready", "PoC", 55, Brush(0xFF37D67A)));
PipelineSteps.Add(new PipelineStepViewModel("04", "Scene Data API 추출", "Text/Image visible object snapshot", "PoC", 50, Brush(0xFFFFB547)));
PipelineSteps.Add(new PipelineStepViewModel("05", "자동 검수 피드백", "Text Diffing + LLM 중요도 판정", "초안", 40, Brush(0xFFFF5B61)));
}
private void LoadFieldMappings()
{
foreach (var mapping in workspace.ActiveTemplate.Mappings)
{
FieldMappings.Add(new FieldMappingViewModel(mapping.CueSheetField, mapping.SceneObjectName, mapping.Rule, mapping.Attribute));
}
}
private void LoadMappingValidation()
{
MappingValidations.Clear();
foreach (var validation in automationEngine.ValidateMappingProfile(workspace.ActiveTemplate))
{
MappingValidations.Add(new MappingValidationViewModel(
validation.CheckId,
validation.Title,
validation.Status,
validation.Detail,
SeverityBrush(validation.Severity)));
}
}
private void LoadEndpoints()
{
Endpoints.Add(new IntegrationEndpointViewModel("큐시트 플랫폼", "GET /api/cuesheets/{broadcastDate}", "AI ON Studio", "Mock", Brush(0xFF45D6D1)));
Endpoints.Add(new IntegrationEndpointViewModel("ERP 편성", "GET /api/schedules/{programCode}", "CJ OnStyle ERP", "Mock", Brush(0xFF37D67A)));
Endpoints.Add(new IntegrationEndpointViewModel("Tornado Scene API", "POST /api/scenes/extract", "Tornado Adapter", "Mock", Brush(0xFF37D67A)));
Endpoints.Add(new IntegrationEndpointViewModel("LLM 검수", "POST /api/inspection/llm", "Internal LLM", "정책 확인", Brush(0xFFB48CFF)));
}
private void LoadTornadoAdapterChecks()
{
TornadoAdapterChecks.Clear();
foreach (var check in automationEngine.GetTornadoAdapterChecks())
{
TornadoAdapterChecks.Add(new TornadoAdapterCheckViewModel(
check.CheckId,
check.Name,
check.Status,
check.Evidence,
check.Status == "준비" ? Brush(0xFFFFB547) : Brush(0xFF37D67A)));
}
}
private void LoadRequirementCoverage()
{
RequirementCoverage.Add(new PocRequirementViewModel("RFP-01", "큐시트 API 연동", "PoC", "JSON Mock 수신", Brush(0xFF45D6D1)));
RequirementCoverage.Add(new PocRequirementViewModel("RFP-02", "ERP 편성 연동", "PoC", "편성 JSON 매칭", Brush(0xFF37D67A)));
RequirementCoverage.Add(new PocRequirementViewModel("RFP-03", "Data Field Mapping", "PoC", "Scene 스냅샷 생성", Brush(0xFF37D67A)));
RequirementCoverage.Add(new PocRequirementViewModel("RFP-04", "Tornado Scene API", "PoC", "Mock adapter 추출", Brush(0xFF37D67A)));
RequirementCoverage.Add(new PocRequirementViewModel("RFP-05", "관리자 매핑 기능", "PoC", "프로파일 검증", Brush(0xFF37D67A)));
RequirementCoverage.Add(new PocRequirementViewModel("RFP-06", "1·2단계 자동 검수", "PoC", "필드 상세 Diff + LLM 후보", Brush(0xFF45D6D1)));
}
private void LoadScheduleMatches()
{
ScheduleMatches.Clear();
foreach (var cueSheet in workspace.CueSheets)
{
var schedule = automationEngine.FindSchedule(cueSheet);
var isMatched = schedule is not null;
ScheduleMatches.Add(new ScheduleMatchViewModel(
cueSheet.ProgramTitle,
$"{cueSheet.BroadcastTime:HH:mm} / {cueSheet.ProductName}",
schedule?.ProgramCode ?? "-",
schedule is null ? "편성 미확인" : $"{schedule.MerchandiserName} / {schedule.ProductCategory}",
isMatched ? "매칭" : "확인",
isMatched ? Brush(0xFF37D67A) : Brush(0xFFFFB547)));
}
}
private void LoadInspectionQueue(IEnumerable<InspectionResult> results)
{
InspectionQueue.Clear();
foreach (var item in results)
{
InspectionQueue.Add(new InspectionQueueItemViewModel(
item.ProgramTitle,
item.SceneName,
$"{item.MatchRate:0.0}%",
item.Severity,
item.Decision,
SeverityBrush(item.Severity)));
}
}
private void LoadInspectionDetails(IEnumerable<InspectionResult> results)
{
InspectionDetails.Clear();
var detailItems = results
.SelectMany(result => result.FieldResults.Select(field => new { Result = result, Field = field }))
.Where(item => item.Field.MatchRate < 99.5)
.OrderBy(item => item.Field.MatchRate)
.ThenBy(item => item.Result.CueSheetId)
.Take(5);
foreach (var item in detailItems)
{
InspectionDetails.Add(new InspectionDetailItemViewModel(
item.Result.CueSheetId,
item.Field.SceneObjectName,
item.Field.ExpectedText,
item.Field.ActualText,
$"{item.Field.MatchRate:0.0}%",
item.Field.Severity,
item.Field.Decision,
SeverityBrush(item.Field.Severity)));
}
}
private void LoadLlmReviewRequests(IEnumerable<InspectionResult> results)
{
LlmReviewRequests.Clear();
foreach (var request in automationEngine.BuildLlmReviewRequests(results))
{
LlmReviewRequests.Add(new LlmReviewRequestViewModel(
request.RequestId,
request.ProgramTitle,
request.SceneName,
request.RecommendedPriority,
request.AuditSummary,
request.PromptKey,
SeverityBrush(request.RecommendedPriority)));
}
}
private void ApplySceneSnapshot(SceneDataSnapshot scene)
{
SceneSnapshotLabel = $"{scene.SceneName} / {scene.VisibleTextObjects.Count} text objects";
SceneSnapshotText = scene.ToPreviewJson();
}
private static string FormatCueSheetSelection(CueSheetSelection selection)
{
return $"목 데이터 {selection.Position}/{selection.TotalCount} · {selection.Record.BroadcastTime:HH:mm}";
}
private static string FormatInspectionSummary(PocInspectionSummary summary)
{
return $"통과 {summary.PassedCount}건 / 확인 {summary.ReviewCount}건 / 평균 {summary.AverageMatchRate:0.0}%";
}
private static string FormatScheduleMatch(ErpScheduleRecord? schedule)
{
return schedule is null
? "ERP 편성은 확인 대기입니다."
: $"ERP 편성 {schedule.ProgramCode}와 매칭됐습니다.";
}
private static SolidColorBrush SeverityBrush(string severity)
{
return severity switch
{
"High" => Brush(0xFFFF5B61),
"Medium" => Brush(0xFFFFB547),
_ => Brush(0xFF37D67A)
};
}
private static SolidColorBrush Brush(uint argb)
{
return new SolidColorBrush(Color.FromArgb(
(byte)(argb >> 24),
(byte)(argb >> 16),
(byte)(argb >> 8),
(byte)argb));
}
}
}