427 lines
18 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|