feat: 1단계 — JSON 시나리오 로더 + 스모크 테스트
- ScenarioLoader: JSON 시나리오/창고 레이아웃 → 런타임 ScriptableObject 변환 - warehouse_basic.json: 로케이션 마스터 (해피패스 + 존위반/용량초과 함정) - InboundManager.StartMission(scenario) 오버로드 (로더 결과 주입용) - Editor 스모크 테스트: 로더 + 입고 로직 12항목 검증 (12 PASS) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using SinoWms.Data;
|
||||
|
||||
namespace SinoWms.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON 시나리오/창고 레이아웃을 읽어 런타임 ScriptableObject 인스턴스로 변환한다.
|
||||
/// 교육 콘텐츠를 코드와 분리하기 위한 진입점. UI/매니저는 이 결과를 받아 미션을 시작한다.
|
||||
/// </summary>
|
||||
public static class ScenarioLoader
|
||||
{
|
||||
/// <summary>로더 결과 묶음: 미션 시나리오 + 해당 창고의 로케이션 마스터.</summary>
|
||||
public class LoadedScenario
|
||||
{
|
||||
public InboundScenario scenario;
|
||||
public List<LocationDefinition> locations = new List<LocationDefinition>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resources 폴더에서 시나리오/창고 JSON(TextAsset)을 읽어 로드한다.
|
||||
/// 경로는 확장자 없이 Resources 기준 상대경로 (예: "Scenarios/inbound_basic").
|
||||
/// </summary>
|
||||
public static LoadedScenario LoadFromResources(string scenarioPath, string warehousePath)
|
||||
{
|
||||
var scenarioAsset = Resources.Load<TextAsset>(scenarioPath);
|
||||
if (scenarioAsset == null)
|
||||
throw new InvalidOperationException($"시나리오 JSON을 찾을 수 없습니다: Resources/{scenarioPath}");
|
||||
|
||||
var warehouseAsset = Resources.Load<TextAsset>(warehousePath);
|
||||
if (warehouseAsset == null)
|
||||
throw new InvalidOperationException($"창고 JSON을 찾을 수 없습니다: Resources/{warehousePath}");
|
||||
|
||||
return Load(scenarioAsset.text, warehouseAsset.text);
|
||||
}
|
||||
|
||||
/// <summary>JSON 문자열로부터 직접 로드한다(테스트/외부 소스용).</summary>
|
||||
public static LoadedScenario Load(string scenarioJson, string warehouseJson)
|
||||
{
|
||||
var warehouseDto = JsonUtility.FromJson<WarehouseDto>(warehouseJson);
|
||||
if (warehouseDto?.locations == null)
|
||||
throw new InvalidOperationException("창고 JSON 파싱 실패: locations가 없습니다.");
|
||||
|
||||
var result = new LoadedScenario();
|
||||
var byCode = new Dictionary<string, LocationDefinition>();
|
||||
foreach (var dto in warehouseDto.locations)
|
||||
{
|
||||
var loc = ScriptableObject.CreateInstance<LocationDefinition>();
|
||||
loc.name = dto.locationCode;
|
||||
loc.locationCode = dto.locationCode;
|
||||
loc.zone = ParseZone(dto.zone, dto.locationCode);
|
||||
loc.capacity = dto.capacity;
|
||||
loc.gridPosition = new Vector2Int(dto.gridX, dto.gridY);
|
||||
|
||||
result.locations.Add(loc);
|
||||
byCode[loc.locationCode] = loc;
|
||||
}
|
||||
|
||||
var scenarioDto = JsonUtility.FromJson<ScenarioDto>(scenarioJson);
|
||||
if (scenarioDto?.lines == null)
|
||||
throw new InvalidOperationException("시나리오 JSON 파싱 실패: lines가 없습니다.");
|
||||
|
||||
var scenario = ScriptableObject.CreateInstance<InboundScenario>();
|
||||
scenario.name = scenarioDto.scenarioId;
|
||||
scenario.scenarioId = scenarioDto.scenarioId;
|
||||
scenario.title = scenarioDto.title;
|
||||
scenario.timeLimitSeconds = scenarioDto.timeLimitSeconds;
|
||||
|
||||
foreach (var lineDto in scenarioDto.lines)
|
||||
{
|
||||
var item = ScriptableObject.CreateInstance<ItemDefinition>();
|
||||
item.name = lineDto.sku;
|
||||
item.sku = lineDto.sku;
|
||||
item.displayName = lineDto.displayName;
|
||||
item.barcode = lineDto.barcode;
|
||||
item.requiredZone = ParseZone(lineDto.requiredZone, lineDto.sku);
|
||||
item.unitVolume = lineDto.unitVolume;
|
||||
|
||||
LocationDefinition suggested = null;
|
||||
if (!string.IsNullOrEmpty(lineDto.suggestedLocation))
|
||||
{
|
||||
if (!byCode.TryGetValue(lineDto.suggestedLocation, out suggested))
|
||||
Debug.LogWarning($"[ScenarioLoader] {lineDto.sku}의 정답 로케이션 " +
|
||||
$"'{lineDto.suggestedLocation}'이(가) 창고에 없습니다.");
|
||||
}
|
||||
|
||||
scenario.lines.Add(new InboundLine
|
||||
{
|
||||
item = item,
|
||||
expectedQty = lineDto.expectedQty,
|
||||
actualQty = lineDto.actualQty,
|
||||
suggestedLocation = suggested
|
||||
});
|
||||
}
|
||||
|
||||
result.scenario = scenario;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static StorageZone ParseZone(string raw, string ownerForLog)
|
||||
{
|
||||
if (Enum.TryParse(raw, ignoreCase: true, out StorageZone zone))
|
||||
return zone;
|
||||
|
||||
Debug.LogWarning($"[ScenarioLoader] 알 수 없는 존 '{raw}' ({ownerForLog}) → Ambient로 대체.");
|
||||
return StorageZone.Ambient;
|
||||
}
|
||||
|
||||
// ---- JSON 매핑용 DTO (JsonUtility는 public 필드 + [Serializable] 필요) ----
|
||||
|
||||
[Serializable]
|
||||
private class ScenarioDto
|
||||
{
|
||||
public string scenarioId;
|
||||
public string title;
|
||||
public float timeLimitSeconds;
|
||||
public LineDto[] lines;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class LineDto
|
||||
{
|
||||
public string sku;
|
||||
public string displayName;
|
||||
public string barcode;
|
||||
public string requiredZone;
|
||||
public int unitVolume;
|
||||
public int expectedQty;
|
||||
public int actualQty;
|
||||
public string suggestedLocation;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class WarehouseDto
|
||||
{
|
||||
public string warehouseId;
|
||||
public string title;
|
||||
public LocationDto[] locations;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class LocationDto
|
||||
{
|
||||
public string locationCode;
|
||||
public string zone;
|
||||
public int capacity;
|
||||
public int gridX;
|
||||
public int gridY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe252208b8d334ce181dd57a84601eb1
|
||||
Reference in New Issue
Block a user