Add ERP schedule matching PoC
This commit is contained in:
@@ -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",
|
||||
|
||||
11
TornadoAce_CJOnStyle/Domain/ScheduleModels.cs
Normal file
11
TornadoAce_CJOnStyle/Domain/ScheduleModels.cs
Normal file
@@ -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);
|
||||
}
|
||||
@@ -7,11 +7,13 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
{
|
||||
private AutomationWorkspace(
|
||||
IReadOnlyList<CueSheetRecord> cueSheets,
|
||||
IReadOnlyList<ErpScheduleRecord> erpSchedules,
|
||||
SceneTemplateDefinition activeTemplate,
|
||||
SceneDataSnapshot activeScene,
|
||||
IReadOnlyList<InspectionResult> inspectionResults)
|
||||
{
|
||||
CueSheets = cueSheets;
|
||||
ErpSchedules = erpSchedules;
|
||||
ActiveTemplate = activeTemplate;
|
||||
ActiveScene = activeScene;
|
||||
InspectionResults = inspectionResults;
|
||||
@@ -19,6 +21,8 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
|
||||
public IReadOnlyList<CueSheetRecord> CueSheets { get; }
|
||||
|
||||
public IReadOnlyList<ErpScheduleRecord> 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<CueSheetSeed> CueSheets,
|
||||
List<ErpScheduleSeed>? ErpSchedules,
|
||||
SceneTemplateSeed ActiveTemplate,
|
||||
SceneDataSeed ActiveScene,
|
||||
List<InspectionSeed> 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,
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace TornadoAce_CJOnStyle.Services
|
||||
Task<IReadOnlyList<CueSheetRecord>> GetCueSheetsAsync(DateOnly broadcastDate, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IErpScheduleClient
|
||||
{
|
||||
Task<IReadOnlyList<ErpScheduleRecord>> GetSchedulesAsync(DateOnly broadcastDate, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ITornadoSceneAdapter
|
||||
{
|
||||
Task<SceneDataSnapshot> CreateOrUpdateSceneAsync(CueSheetRecord cueSheet, SceneTemplateDefinition template, CancellationToken cancellationToken);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PocRequirementViewModel> RequirementCoverage { get; } = [];
|
||||
|
||||
public ObservableCollection<ScheduleMatchViewModel> ScheduleMatches { get; } = [];
|
||||
|
||||
public ObservableCollection<InspectionQueueItemViewModel> InspectionQueue { get; } = [];
|
||||
|
||||
public ObservableCollection<InspectionDetailItemViewModel> 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<InspectionResult> 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -385,6 +385,57 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border
|
||||
Height="1"
|
||||
Background="{StaticResource PanelStrokeBrush}" />
|
||||
|
||||
<TextBlock Style="{StaticResource SectionTitleTextStyle}" Text="ERP 편성 매칭" />
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.ScheduleMatches, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ScheduleMatchViewModel">
|
||||
<Grid
|
||||
Margin="0,0,0,10"
|
||||
ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
Foreground="{StaticResource TextPrimaryBrush}"
|
||||
Text="{x:Bind ProgramTitle}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource MonoTextStyle}"
|
||||
Text="{x:Bind ProgramCode}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind BroadcastSlot}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock
|
||||
Style="{StaticResource LabelTextStyle}"
|
||||
Text="{x:Bind OwnerAndCategory}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Padding="8,3"
|
||||
VerticalAlignment="Top"
|
||||
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}" />
|
||||
|
||||
@@ -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 기반 첫 화면. 예제 프로젝트와 같은 데스크톱 앱 방향으로 유지.
|
||||
|
||||
@@ -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 요청 후보로 전달한다.
|
||||
|
||||
Reference in New Issue
Block a user