diff --git a/TornadoAce_CJOnStyle/Data/PocWorkspace.json b/TornadoAce_CJOnStyle/Data/PocWorkspace.json index 2c249e8..ac08bb7 100644 --- a/TornadoAce_CJOnStyle/Data/PocWorkspace.json +++ b/TornadoAce_CJOnStyle/Data/PocWorkspace.json @@ -28,6 +28,35 @@ "promotionText": "앱 주문 추가 적립" } ], + "erpSchedules": [ + { + "programCode": "PGM-CHOI-0900", + "programTitle": "최화정쇼", + "broadcastTime": "2026-08-17T09:00:00", + "durationMinutes": 60, + "channelName": "CJ OnStyle Live", + "merchandiserName": "MD 이수진", + "productCategory": "패션 이너웨어" + }, + { + "programCode": "PGM-DONG-1100", + "programTitle": "동가게", + "broadcastTime": "2026-08-17T11:00:00", + "durationMinutes": 50, + "channelName": "CJ OnStyle Live", + "merchandiserName": "MD 박정우", + "productCategory": "신선식품" + }, + { + "programCode": "PGM-STYLE-1400", + "programTitle": "스타일 라이브", + "broadcastTime": "2026-08-17T14:00:00", + "durationMinutes": 70, + "channelName": "CJ OnStyle Live", + "merchandiserName": "MD 김하린", + "productCategory": "패션 의류" + } + ], "activeTemplate": { "templateId": "CJ_PRICE_LOWER_01", "templateName": "가격형 하단 자막 / Tornado Scene", diff --git a/TornadoAce_CJOnStyle/Domain/ScheduleModels.cs b/TornadoAce_CJOnStyle/Domain/ScheduleModels.cs new file mode 100644 index 0000000..ce17c69 --- /dev/null +++ b/TornadoAce_CJOnStyle/Domain/ScheduleModels.cs @@ -0,0 +1,11 @@ +namespace TornadoAce_CJOnStyle.Domain +{ + public sealed record ErpScheduleRecord( + string ProgramCode, + string ProgramTitle, + DateTime BroadcastTime, + int DurationMinutes, + string ChannelName, + string MerchandiserName, + string ProductCategory); +} diff --git a/TornadoAce_CJOnStyle/Services/AutomationWorkspace.cs b/TornadoAce_CJOnStyle/Services/AutomationWorkspace.cs index 07b0a1b..b7bb885 100644 --- a/TornadoAce_CJOnStyle/Services/AutomationWorkspace.cs +++ b/TornadoAce_CJOnStyle/Services/AutomationWorkspace.cs @@ -7,11 +7,13 @@ namespace TornadoAce_CJOnStyle.Services { private AutomationWorkspace( IReadOnlyList cueSheets, + IReadOnlyList erpSchedules, SceneTemplateDefinition activeTemplate, SceneDataSnapshot activeScene, IReadOnlyList inspectionResults) { CueSheets = cueSheets; + ErpSchedules = erpSchedules; ActiveTemplate = activeTemplate; ActiveScene = activeScene; InspectionResults = inspectionResults; @@ -19,6 +21,8 @@ namespace TornadoAce_CJOnStyle.Services public IReadOnlyList CueSheets { get; } + public IReadOnlyList ErpSchedules { get; } + public SceneTemplateDefinition ActiveTemplate { get; } public SceneDataSnapshot ActiveScene { get; } @@ -52,12 +56,16 @@ namespace TornadoAce_CJOnStyle.Services var template = seed.ActiveTemplate.ToDomain(); var scene = seed.ActiveScene.ToDomain(); + var schedules = seed.ErpSchedules? + .Select(schedule => schedule.ToDomain()) + .ToArray() ?? []; var inspections = seed.InspectionResults .Select(inspection => inspection.ToDomain()) .ToArray(); return new AutomationWorkspace( cueSheets.Select(cueSheet => cueSheet.ToDomain()).ToArray(), + schedules, template, scene, inspections); @@ -72,6 +80,13 @@ namespace TornadoAce_CJOnStyle.Services new CueSheetRecord("CJ-20260817-003", new DateTime(2026, 8, 17, 14, 0, 0), "스타일 라이브", "썸머 린넨 셋업", "59,900원", "자켓 + 팬츠", "앱 주문 추가 적립") }; + var schedules = new[] + { + new ErpScheduleRecord("PGM-CHOI-0900", "최화정쇼", new DateTime(2026, 8, 17, 9, 0, 0), 60, "CJ OnStyle Live", "MD 이수진", "패션 이너웨어"), + new ErpScheduleRecord("PGM-DONG-1100", "동가게", new DateTime(2026, 8, 17, 11, 0, 0), 50, "CJ OnStyle Live", "MD 박정우", "신선식품"), + new ErpScheduleRecord("PGM-STYLE-1400", "스타일 라이브", new DateTime(2026, 8, 17, 14, 0, 0), 70, "CJ OnStyle Live", "MD 김하린", "패션 의류") + }; + var mappings = new[] { new FieldMappingDefinition("programTitle", "TXT_PROGRAM", "Text", "trim"), @@ -103,11 +118,12 @@ namespace TornadoAce_CJOnStyle.Services new InspectionResult("CJ-20260817-003", "스타일 라이브", "CJ_PRICE_LOWER_01_1400.tscn", 98.1, "Medium", "특수문자 예외 후보") }; - return new AutomationWorkspace(cueSheets, template, scene, inspections); + return new AutomationWorkspace(cueSheets, schedules, template, scene, inspections); } private sealed record WorkspaceSeed( List CueSheets, + List? ErpSchedules, SceneTemplateSeed ActiveTemplate, SceneDataSeed ActiveScene, List InspectionResults); @@ -127,6 +143,28 @@ namespace TornadoAce_CJOnStyle.Services } } + private sealed record ErpScheduleSeed( + string ProgramCode, + string ProgramTitle, + DateTime BroadcastTime, + int DurationMinutes, + string ChannelName, + string MerchandiserName, + string ProductCategory) + { + public ErpScheduleRecord ToDomain() + { + return new ErpScheduleRecord( + ProgramCode, + ProgramTitle, + BroadcastTime, + DurationMinutes, + ChannelName, + MerchandiserName, + ProductCategory); + } + } + private sealed record SceneTemplateSeed( string TemplateId, string TemplateName, diff --git a/TornadoAce_CJOnStyle/Services/IntegrationInterfaces.cs b/TornadoAce_CJOnStyle/Services/IntegrationInterfaces.cs index bad40d4..6619667 100644 --- a/TornadoAce_CJOnStyle/Services/IntegrationInterfaces.cs +++ b/TornadoAce_CJOnStyle/Services/IntegrationInterfaces.cs @@ -7,6 +7,11 @@ namespace TornadoAce_CJOnStyle.Services Task> GetCueSheetsAsync(DateOnly broadcastDate, CancellationToken cancellationToken); } + public interface IErpScheduleClient + { + Task> GetSchedulesAsync(DateOnly broadcastDate, CancellationToken cancellationToken); + } + public interface ITornadoSceneAdapter { Task CreateOrUpdateSceneAsync(CueSheetRecord cueSheet, SceneTemplateDefinition template, CancellationToken cancellationToken); diff --git a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs index 2452774..66b20eb 100644 --- a/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs +++ b/TornadoAce_CJOnStyle/Services/PocAutomationEngine.cs @@ -28,6 +28,15 @@ namespace TornadoAce_CJOnStyle.Services return BuildSnapshot(cueSheet, template, BuildVisibleTextObjects(cueSheet, template)); } + public ErpScheduleRecord? FindSchedule(CueSheetRecord cueSheet) + { + return workspace.ErpSchedules + .OrderBy(schedule => Math.Abs((schedule.BroadcastTime - cueSheet.BroadcastTime).TotalMinutes)) + .FirstOrDefault(schedule => + string.Equals(schedule.ProgramTitle, cueSheet.ProgramTitle, StringComparison.OrdinalIgnoreCase) + && Math.Abs((schedule.BroadcastTime - cueSheet.BroadcastTime).TotalMinutes) <= 15); + } + public PocInspectionSummary InspectWorkspace(SceneTemplateDefinition template) { var results = workspace.CueSheets diff --git a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs index 4538961..ff2d6a2 100644 --- a/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs +++ b/TornadoAce_CJOnStyle/ViewModels/MainViewModel.cs @@ -15,6 +15,7 @@ namespace TornadoAce_CJOnStyle.ViewModels private readonly PocAutomationEngine automationEngine; private int selectedCueSheetIndex; private CueSheetRecord activeCueSheet = null!; + private ErpScheduleRecord? activeSchedule; private SceneDataSnapshot activeScene = null!; public MainViewModel() @@ -25,6 +26,7 @@ namespace TornadoAce_CJOnStyle.ViewModels 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"; @@ -44,6 +46,7 @@ namespace TornadoAce_CJOnStyle.ViewModels LoadFieldMappings(); LoadEndpoints(); LoadRequirementCoverage(); + LoadScheduleMatches(); LoadInspectionQueue(initialInspectionSummary.Results); LoadInspectionDetails(initialInspectionSummary.Results); } @@ -127,6 +130,8 @@ namespace TornadoAce_CJOnStyle.ViewModels public ObservableCollection RequirementCoverage { get; } = []; + public ObservableCollection ScheduleMatches { get; } = []; + public ObservableCollection InspectionQueue { get; } = []; public ObservableCollection InspectionDetails { get; } = []; @@ -182,10 +187,11 @@ namespace TornadoAce_CJOnStyle.ViewModels 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} 큐시트를 선택했습니다."; + OperatorMessage = $"{activeCueSheet.ProgramTitle} / {activeCueSheet.ProductName} 큐시트를 선택했습니다. {FormatScheduleMatch(activeSchedule)}"; StatusCards[0].Metric = selection.TotalCount.ToString(); StatusCards[0].Detail = $"선택 {selection.Position}/{selection.TotalCount}"; ApplySceneSnapshot(activeScene); @@ -225,7 +231,7 @@ namespace TornadoAce_CJOnStyle.ViewModels private void LoadPipelineSteps() { - PipelineSteps.Add(new PipelineStepViewModel("01", "큐시트 데이터 수신", "AI ON Studio / 자막 큐시트 API", "설계", 35, Brush(0xFF45D6D1))); + 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))); @@ -243,7 +249,7 @@ namespace TornadoAce_CJOnStyle.ViewModels 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", "대기", Brush(0xFFFFB547))); + 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("LLM 검수", "POST /api/inspection/llm", "Internal LLM", "정책 확인", Brush(0xFFB48CFF))); } @@ -251,13 +257,32 @@ namespace TornadoAce_CJOnStyle.ViewModels private void LoadRequirementCoverage() { RequirementCoverage.Add(new PocRequirementViewModel("RFP-01", "큐시트 API 연동", "PoC", "JSON Mock 수신", Brush(0xFF45D6D1))); - RequirementCoverage.Add(new PocRequirementViewModel("RFP-02", "ERP 편성 연동", "대기", "엔드포인트 자리", Brush(0xFFFFB547))); + 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-05", "관리자 매핑 기능", "초안", "매핑표 UI", Brush(0xFFB48CFF))); 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 results) { InspectionQueue.Clear(); @@ -315,6 +340,13 @@ namespace TornadoAce_CJOnStyle.ViewModels 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 diff --git a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs index e0f5d81..b751b8a 100644 --- a/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs +++ b/TornadoAce_CJOnStyle/ViewModels/ShellModels.cs @@ -67,6 +67,14 @@ namespace TornadoAce_CJOnStyle.ViewModels string Evidence, SolidColorBrush StatusBrush); + public sealed record ScheduleMatchViewModel( + string ProgramTitle, + string BroadcastSlot, + string ProgramCode, + string OwnerAndCategory, + string Status, + SolidColorBrush StatusBrush); + public sealed record InspectionQueueItemViewModel( string ProgramTitle, string SceneName, diff --git a/TornadoAce_CJOnStyle/Views/MainPage.xaml b/TornadoAce_CJOnStyle/Views/MainPage.xaml index 628a7b7..beb57c5 100644 --- a/TornadoAce_CJOnStyle/Views/MainPage.xaml +++ b/TornadoAce_CJOnStyle/Views/MainPage.xaml @@ -385,6 +385,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/initial-architecture.md b/docs/initial-architecture.md index d18b09e..d792912 100644 --- a/docs/initial-architecture.md +++ b/docs/initial-architecture.md @@ -11,7 +11,7 @@ ## 현재 초기 빌드 범위 - `Domain`: 큐시트, Scene 템플릿, Scene 데이터 스냅샷, 검수 결과 모델. -- `Data`: 큐시트, Scene 템플릿, Scene 추출값을 담은 JSON 기반 PoC 샘플. +- `Data`: 큐시트, ERP 편성, Scene 템플릿, Scene 추출값을 담은 JSON 기반 PoC 샘플. - `Services`: 실제 API/Tornado SDK 연결을 받을 인터페이스, JSON 로더, 목 워크스페이스. - `ViewModels`: 운영 콘솔에 표시할 상태 카드, 파이프라인, 매핑표, 검수 큐. - `Views`: WinUI 3 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지. diff --git a/docs/poc-requirements.md b/docs/poc-requirements.md index 1baaef0..ea89223 100644 --- a/docs/poc-requirements.md +++ b/docs/poc-requirements.md @@ -14,7 +14,7 @@ | ID | RFP 요구사항 | 초기 PoC 반영 상태 | 현재 구현 근거 | | --- | --- | --- | --- | | RFP-01 | 신규 큐시트 플랫폼 API 연동 | JSON Mock 데이터 수신 흐름 구현 | `Data/PocWorkspace.json`, `AutomationWorkspace.CueSheets`, `RefreshCueSheetCommand` | -| RFP-02 | ERP 편성데이터 연동 | 연동 엔드포인트 자리 표시 | `IntegrationEndpointViewModel` | +| 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-05 | 큐시트 데이터와 Tornado Scene 연동 관리자 기능 | 매핑표 UI 초안 구현 | `FieldMappings` 화면 | @@ -32,7 +32,7 @@ ## 다음 구현 후보 -- ERP Mock 데이터를 JSON 파일로 분리하여 실제 편성 API 응답 구조와 맞춘다. +- 실제 큐시트/ERP API 응답 DTO와 현재 JSON seed 스키마를 대조한다. - Tornado2/Tornado3 어댑터를 동일 인터페이스로 유지하고, 버전별 구현체를 분리한다. - Scene 내부 비노출 객체를 구분할 메타데이터 규칙을 정의한다. - 1차 검수 실패 사유를 필드 단위로 누적하고 LLM 요청 후보로 전달한다.