Add Tornado scene adapter boundary
This commit is contained in:
105
TornadoAce_CJOnStyle/Services/MockTornadoSceneAdapter.cs
Normal file
105
TornadoAce_CJOnStyle/Services/MockTornadoSceneAdapter.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using TornadoAce_CJOnStyle.Domain;
|
||||
|
||||
namespace TornadoAce_CJOnStyle.Services
|
||||
{
|
||||
public sealed class MockTornadoSceneAdapter : ITornadoSceneAdapter
|
||||
{
|
||||
private readonly AutomationWorkspace workspace;
|
||||
|
||||
public MockTornadoSceneAdapter(AutomationWorkspace workspace)
|
||||
{
|
||||
this.workspace = workspace;
|
||||
}
|
||||
|
||||
public Task<SceneDataSnapshot> CreateOrUpdateSceneAsync(
|
||||
CueSheetRecord cueSheet,
|
||||
SceneTemplateDefinition template,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var visibleObjects = BuildVisibleTextObjects(cueSheet, template);
|
||||
return Task.FromResult(BuildSnapshot(cueSheet, template, visibleObjects));
|
||||
}
|
||||
|
||||
public Task<SceneDataSnapshot> ExtractSceneDataAsync(string scenePath, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var cueSheet = FindCueSheet(scenePath) ?? workspace.CueSheets.First();
|
||||
var visibleObjects = BuildVisibleTextObjects(cueSheet, workspace.ActiveTemplate);
|
||||
|
||||
if (cueSheet.CueSheetId.EndsWith("-002", StringComparison.Ordinal))
|
||||
{
|
||||
visibleObjects["TXT_PRICE"] = "79,900원";
|
||||
}
|
||||
|
||||
if (cueSheet.CueSheetId.EndsWith("-003", StringComparison.Ordinal))
|
||||
{
|
||||
visibleObjects["TXT_PROMOTION"] = "앱 주문 추가 적립 ※";
|
||||
}
|
||||
|
||||
return Task.FromResult(BuildSnapshot(cueSheet, workspace.ActiveTemplate, visibleObjects));
|
||||
}
|
||||
|
||||
public IReadOnlyList<TornadoAdapterCheck> GetAdapterChecks()
|
||||
{
|
||||
return
|
||||
[
|
||||
new TornadoAdapterCheck("SDK-01", "Tornado2 Scene 생성", "Mock", "CreateOrUpdateSceneAsync"),
|
||||
new TornadoAdapterCheck("SDK-02", "Scene Data API 추출", "Mock", "ExtractSceneDataAsync"),
|
||||
new TornadoAdapterCheck("SDK-03", "Tornado3 전환 경계", "준비", "ITornadoSceneAdapter 유지")
|
||||
];
|
||||
}
|
||||
|
||||
private CueSheetRecord? FindCueSheet(string scenePath)
|
||||
{
|
||||
return workspace.CueSheets.FirstOrDefault(cueSheet => scenePath.Contains(cueSheet.BroadcastTime.ToString("HHmm"), StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static SceneDataSnapshot BuildSnapshot(
|
||||
CueSheetRecord cueSheet,
|
||||
SceneTemplateDefinition template,
|
||||
IReadOnlyDictionary<string, string> visibleObjects)
|
||||
{
|
||||
var sceneName = $"{template.TemplateId}_{cueSheet.BroadcastTime:HHmm}.tscn";
|
||||
return new SceneDataSnapshot(sceneName, template.TemplateId, visibleObjects, [BuildImagePath(cueSheet)]);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVisibleTextObjects(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||
{
|
||||
return template.Mappings.ToDictionary(
|
||||
mapping => mapping.SceneObjectName,
|
||||
mapping => ReadCueSheetValue(cueSheet, mapping.CueSheetField),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ReadCueSheetValue(CueSheetRecord cueSheet, string fieldName)
|
||||
{
|
||||
return fieldName switch
|
||||
{
|
||||
"programTitle" => cueSheet.ProgramTitle,
|
||||
"productName" => cueSheet.ProductName,
|
||||
"priceText" => cueSheet.PriceText,
|
||||
"compositionText" => cueSheet.CompositionText,
|
||||
"promotionText" => cueSheet.PromotionText,
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildImagePath(CueSheetRecord cueSheet)
|
||||
{
|
||||
var directoryName = new string(cueSheet.ProductName
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '_')
|
||||
.ToArray()).Trim('_');
|
||||
|
||||
return $@"D:\CJOnStyle\Products\{directoryName}\main.png";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record TornadoAdapterCheck(
|
||||
string CheckId,
|
||||
string Name,
|
||||
string Status,
|
||||
string Evidence);
|
||||
}
|
||||
@@ -6,10 +6,12 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
public sealed class PocAutomationEngine
|
||||
{
|
||||
private readonly AutomationWorkspace workspace;
|
||||
private readonly ITornadoSceneAdapter tornadoSceneAdapter;
|
||||
|
||||
public PocAutomationEngine(AutomationWorkspace workspace)
|
||||
public PocAutomationEngine(AutomationWorkspace workspace, ITornadoSceneAdapter? tornadoSceneAdapter = null)
|
||||
{
|
||||
this.workspace = workspace;
|
||||
this.tornadoSceneAdapter = tornadoSceneAdapter ?? new MockTornadoSceneAdapter(workspace);
|
||||
}
|
||||
|
||||
public CueSheetSelection SelectCueSheet(int index)
|
||||
@@ -25,7 +27,20 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
|
||||
public SceneDataSnapshot GenerateScene(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||
{
|
||||
return BuildSnapshot(cueSheet, template, BuildVisibleTextObjects(cueSheet, template));
|
||||
return tornadoSceneAdapter
|
||||
.CreateOrUpdateSceneAsync(cueSheet, template, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
public IReadOnlyList<TornadoAdapterCheck> GetTornadoAdapterChecks()
|
||||
{
|
||||
return tornadoSceneAdapter is MockTornadoSceneAdapter mockAdapter
|
||||
? mockAdapter.GetAdapterChecks()
|
||||
:
|
||||
[
|
||||
new TornadoAdapterCheck("SDK-01", "Tornado Scene Adapter", "준비", tornadoSceneAdapter.GetType().Name)
|
||||
];
|
||||
}
|
||||
|
||||
public IReadOnlyList<MappingValidationResult> ValidateMappingProfile(SceneTemplateDefinition template)
|
||||
@@ -83,7 +98,16 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
public PocInspectionSummary InspectWorkspace(SceneTemplateDefinition template)
|
||||
{
|
||||
var results = workspace.CueSheets
|
||||
.Select(cueSheet => Inspect(cueSheet, GenerateExtractedScene(cueSheet, template), template))
|
||||
.Select(cueSheet =>
|
||||
{
|
||||
var generatedScene = GenerateScene(cueSheet, template);
|
||||
var extractedScene = tornadoSceneAdapter
|
||||
.ExtractSceneDataAsync(generatedScene.SceneName, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
return Inspect(cueSheet, extractedScene, template);
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var reviewCount = results.Count(result => result.Severity is "High" or "Medium");
|
||||
@@ -171,32 +195,6 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
return new InspectionResult(cueSheet.CueSheetId, cueSheet.ProgramTitle, snapshot.SceneName, matchRate, severity, decision, fieldResults);
|
||||
}
|
||||
|
||||
private SceneDataSnapshot GenerateExtractedScene(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||
{
|
||||
var visibleObjects = BuildVisibleTextObjects(cueSheet, template);
|
||||
|
||||
if (cueSheet.CueSheetId.EndsWith("-002", StringComparison.Ordinal))
|
||||
{
|
||||
visibleObjects["TXT_PRICE"] = "79,900원";
|
||||
}
|
||||
|
||||
if (cueSheet.CueSheetId.EndsWith("-003", StringComparison.Ordinal))
|
||||
{
|
||||
visibleObjects["TXT_PROMOTION"] = "앱 주문 추가 적립 ※";
|
||||
}
|
||||
|
||||
return BuildSnapshot(cueSheet, template, visibleObjects);
|
||||
}
|
||||
|
||||
private static SceneDataSnapshot BuildSnapshot(
|
||||
CueSheetRecord cueSheet,
|
||||
SceneTemplateDefinition template,
|
||||
IReadOnlyDictionary<string, string> visibleObjects)
|
||||
{
|
||||
var sceneName = $"{template.TemplateId}_{cueSheet.BroadcastTime:HHmm}.tscn";
|
||||
return new SceneDataSnapshot(sceneName, template.TemplateId, visibleObjects, [BuildImagePath(cueSheet)]);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> BuildVisibleTextObjects(CueSheetRecord cueSheet, SceneTemplateDefinition template)
|
||||
{
|
||||
return template.Mappings.ToDictionary(
|
||||
@@ -218,15 +216,6 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildImagePath(CueSheetRecord cueSheet)
|
||||
{
|
||||
var directoryName = new string(cueSheet.ProductName
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '_')
|
||||
.ToArray()).Trim('_');
|
||||
|
||||
return $@"D:\CJOnStyle\Products\{directoryName}\main.png";
|
||||
}
|
||||
|
||||
private static MappingValidationResult BuildValidationResult(
|
||||
string checkId,
|
||||
string title,
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
LoadFieldMappings();
|
||||
LoadMappingValidation();
|
||||
LoadEndpoints();
|
||||
LoadTornadoAdapterChecks();
|
||||
LoadRequirementCoverage();
|
||||
LoadScheduleMatches();
|
||||
LoadInspectionQueue(initialInspectionSummary.Results);
|
||||
@@ -132,6 +133,8 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
|
||||
public ObservableCollection<IntegrationEndpointViewModel> Endpoints { get; } = [];
|
||||
|
||||
public ObservableCollection<TornadoAdapterCheckViewModel> TornadoAdapterChecks { get; } = [];
|
||||
|
||||
public ObservableCollection<PocRequirementViewModel> RequirementCoverage { get; } = [];
|
||||
|
||||
public ObservableCollection<ScheduleMatchViewModel> ScheduleMatches { get; } = [];
|
||||
@@ -233,15 +236,15 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
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", "Mock", "SDK 어댑터 자리", Symbol.Repair, Brush(0xFF37D67A)));
|
||||
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 어댑터 준비", "대기", 25, Brush(0xFF37D67A)));
|
||||
PipelineSteps.Add(new PipelineStepViewModel("04", "Scene Data API 추출", "Text/Image visible object snapshot", "초안", 30, Brush(0xFFFFB547)));
|
||||
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)));
|
||||
}
|
||||
|
||||
@@ -272,16 +275,31 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
{
|
||||
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", "설계", 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", "Text/Image 추출 샘플", 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)));
|
||||
}
|
||||
|
||||
@@ -67,6 +67,13 @@ namespace TornadoAce_CJOnStyle.ViewModels
|
||||
public string OwnerAndStatus => $"{Owner} / {Status}";
|
||||
}
|
||||
|
||||
public sealed record TornadoAdapterCheckViewModel(
|
||||
string CheckId,
|
||||
string Name,
|
||||
string Status,
|
||||
string Evidence,
|
||||
SolidColorBrush StatusBrush);
|
||||
|
||||
public sealed record PocRequirementViewModel(
|
||||
string RequirementId,
|
||||
string Title,
|
||||
|
||||
@@ -436,6 +436,54 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{StaticResource PanelStrokeBrush}" />
|
||||
|
||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="Tornado Adapter" />
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.TornadoAdapterChecks, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TornadoAdapterCheckViewModel">
|
||||
<Grid
|
||||
Margin="0,0,0,10"
|
||||
ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="58" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
FontFamily="Cascadia Mono"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource TextMutedBrush}"
|
||||
Text="{x:Bind CheckId}" />
|
||||
<StackPanel Grid.Column="1" Spacing="2">
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Text="{x:Bind Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind Evidence}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Padding="8,3"
|
||||
Background="{x:Bind StatusBrush}"
|
||||
CornerRadius="8">
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="12"
|
||||
Foreground="#071013"
|
||||
Text="{x:Bind Status}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{StaticResource PanelStrokeBrush}" />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
- `Domain`: 큐시트, Scene 템플릿, Scene 데이터 스냅샷, 검수 결과 모델.
|
||||
- `Data`: 큐시트, ERP 편성, Scene 템플릿, Scene 추출값을 담은 JSON 기반 PoC 샘플.
|
||||
- `Services`: 실제 API/Tornado SDK 연결을 받을 인터페이스, JSON 로더, 목 워크스페이스.
|
||||
- `Services`: 실제 API/Tornado SDK 연결을 받을 인터페이스, JSON 로더, 목 워크스페이스, Mock Tornado adapter.
|
||||
- `ViewModels`: 운영 콘솔에 표시할 상태 카드, 파이프라인, 매핑표, 검수 큐.
|
||||
- `Views`: WinUI 3 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
| RFP-01 | 신규 큐시트 플랫폼 API 연동 | JSON Mock 데이터 수신 흐름 구현 | `Data/PocWorkspace.json`, `AutomationWorkspace.CueSheets`, `RefreshCueSheetCommand` |
|
||||
| RFP-02 | ERP 편성데이터 연동 | JSON Mock 편성 데이터 매칭 구현 | `Data/PocWorkspace.json`, `ErpScheduleRecord`, `ScheduleMatches` |
|
||||
| RFP-03 | CG 포맷 제작 자동화, Data Field Mapping | 큐시트 필드와 Scene 객체 매핑 구현 | `SceneTemplateDefinition`, `PocAutomationEngine.GenerateScene` |
|
||||
| RFP-04 | Tornado Scene API | Scene visible Text/Image 스냅샷 샘플 구현 | `SceneDataSnapshot.ToPreviewJson` |
|
||||
| RFP-04 | Tornado Scene API | Mock Tornado adapter 기반 Scene 생성/추출 구현 | `ITornadoSceneAdapter`, `MockTornadoSceneAdapter`, `SceneDataSnapshot.ToPreviewJson` |
|
||||
| RFP-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | JSON 매핑 프로파일과 검증 UI 구현 | `FieldMappings`, `MappingValidations`, `ValidateMappingProfile` |
|
||||
| RFP-06 | 자동 검수 1단계 Text Diffing | 필드별 기대값/실제값/일치율 비교 구현 | `PocAutomationEngine.InspectWorkspace`, `InspectionFieldResult` |
|
||||
| RFP-07 | 자동 검수 2단계 LLM 중요도 피드백 | LLM 요청 후보 payload와 제작자 상세 UI 구현 | `LlmReviewRequest`, `BuildLlmReviewRequests`, `LlmReviewRequests` |
|
||||
@@ -34,6 +34,6 @@
|
||||
|
||||
- 실제 큐시트/ERP API 응답 DTO와 현재 JSON seed 스키마를 대조한다.
|
||||
- 관리자용 매핑 프로파일 편집/저장 권한 정책을 정한다.
|
||||
- Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다.
|
||||
- 실제 Tornado2 SDK 구현체와 Tornado3 SDK 구현체의 메서드 차이를 `ITornadoSceneAdapter`에 맞춰 정리한다.
|
||||
- Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다.
|
||||
- LLM 응답 스키마와 감사 로그 보관 정책을 실제 사내 정책에 맞춘다.
|
||||
|
||||
Reference in New Issue
Block a user