diff --git a/TornadoAce_CJOnStyle/Services/MockTornadoSceneAdapter.cs b/TornadoAce_CJOnStyle/Services/MockTornadoSceneAdapter.cs new file mode 100644 index 0000000..76cd577 --- /dev/null +++ b/TornadoAce_CJOnStyle/Services/MockTornadoSceneAdapter.cs @@ -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 CreateOrUpdateSceneAsync( + CueSheetRecord cueSheet, + SceneTemplateDefinition template, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var visibleObjects = BuildVisibleTextObjects(cueSheet, template); + return Task.FromResult(BuildSnapshot(cueSheet, template, visibleObjects)); + } + + public Task 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 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 visibleObjects) + { + var sceneName = $"{template.TemplateId}_{cueSheet.BroadcastTime:HHmm}.tscn"; + return new SceneDataSnapshot(sceneName, template.TemplateId, visibleObjects, [BuildImagePath(cueSheet)]); + } + + private static Dictionary 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); +} diff --git a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs index 3eb61b0..194b026 100644 --- a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs +++ b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs @@ -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 GetTornadoAdapterChecks() + { + return tornadoSceneAdapter is MockTornadoSceneAdapter mockAdapter + ? mockAdapter.GetAdapterChecks() + : + [ + new TornadoAdapterCheck("SDK-01", "Tornado Scene Adapter", "준비", tornadoSceneAdapter.GetType().Name) + ]; } public IReadOnlyList 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 visibleObjects) - { - var sceneName = $"{template.TemplateId}_{cueSheet.BroadcastTime:HHmm}.tscn"; - return new SceneDataSnapshot(sceneName, template.TemplateId, visibleObjects, [BuildImagePath(cueSheet)]); - } - private static Dictionary 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, diff --git a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs index 8cae46a..71ce3e5 100644 --- a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs +++ b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs @@ -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 Endpoints { get; } = []; + public ObservableCollection TornadoAdapterChecks { get; } = []; + public ObservableCollection RequirementCoverage { get; } = []; public ObservableCollection 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))); } diff --git a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs index f1c15a3..2a952fc 100644 --- a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs +++ b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs @@ -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, diff --git a/TornadoAce_CJOnStyle/Views/MainPage.xaml b/TornadoAce_CJOnStyle/Views/MainPage.xaml index b2c950a..7cf2166 100644 --- a/TornadoAce_CJOnStyle/Views/MainPage.xaml +++ b/TornadoAce_CJOnStyle/Views/MainPage.xaml @@ -436,6 +436,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/initial-architecture.md b/docs/initial-architecture.md index d792912..fbbf38b 100644 --- a/docs/initial-architecture.md +++ b/docs/initial-architecture.md @@ -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 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지. diff --git a/docs/poc-requirements.md b/docs/poc-requirements.md index 89ee18c..06ab3a0 100644 --- a/docs/poc-requirements.md +++ b/docs/poc-requirements.md @@ -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 응답 스키마와 감사 로그 보관 정책을 실제 사내 정책에 맞춘다.