Add Tornado scene adapter boundary

This commit is contained in:
2026-06-09 14:39:43 +09:00
parent c4891823ca
commit 11559c8ba8
7 changed files with 213 additions and 46 deletions

View 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);
}

View File

@@ -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,

View File

@@ -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)));
}

View File

@@ -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,

View File

@@ -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}" />

View File

@@ -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 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지.

View File

@@ -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 응답 스키마와 감사 로그 보관 정책을 실제 사내 정책에 맞춘다.