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:
jklee
2026-06-14 19:19:55 +09:00
parent 1051958641
commit de5a40678e
9 changed files with 329 additions and 1 deletions
+132
View File
@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using SinoWms.Core;
using SinoWms.Data;
using SinoWms.Inbound;
namespace SinoWms.EditorTools
{
/// <summary>
/// UI 없이 입고 로직(로더 + 서비스)을 검증하는 스모크 테스트.
/// 메뉴 <b>SinoWMS/Run Inbound Smoke Test</b>로 실행하면 콘솔에 PASS/FAIL 리포트가 찍힌다.
/// 정식 EditMode 테스트(asmdef 분리)로 대체되기 전까지의 임시 검증 수단.
/// </summary>
public static class InboundSmokeTest
{
private static int _pass;
private static int _fail;
[MenuItem("SinoWMS/Run Inbound Smoke Test")]
public static void Run()
{
_pass = 0;
_fail = 0;
Debug.Log("===== 입고 스모크 테스트 시작 =====");
var loaded = ScenarioLoader.LoadFromResources(
"Scenarios/inbound_basic", "Scenarios/warehouse_basic");
TestLoader(loaded);
TestHappyPath(loaded);
TestUnknownBarcode(loaded);
TestPutawayBeforeInspect(loaded);
TestZoneMismatch(loaded);
TestOverCapacity(loaded);
var summary = $"===== 결과: {_pass} PASS / {_fail} FAIL =====";
if (_fail == 0) Debug.Log(summary);
else Debug.LogError(summary);
}
// ---- 개별 테스트 ----
private static void TestLoader(ScenarioLoader.LoadedScenario l)
{
Check("로더: 시나리오 ID", l.scenario.scenarioId == "INB-001");
Check("로더: 라인 3건", l.scenario.lines.Count == 3);
Check("로더: 로케이션 7건", l.locations.Count == 7);
Check("로더: 우유 존=Chilled",
Line(l, "SKU-2002").item.requiredZone == StorageZone.Chilled);
Check("로더: 정답 로케이션 해석(생수→A-01-01)",
Line(l, "SKU-1001").suggestedLocation?.locationCode == "A-01-01");
Check("로더: 수량 차이 함정(우유 예정8/실제6)",
Line(l, "SKU-2002").actualQty == 6 && Line(l, "SKU-2002").expectedQty == 8);
}
private static void TestHappyPath(ScenarioLoader.LoadedScenario l)
{
var progress = NewProgress(l);
var scan = new ScanService(progress);
var putaway = new PutawayService();
var lp = progress.First(p => p.line.item.sku == "SKU-1001");
var r1 = scan.Scan(lp.line.item.barcode, out _);
Check("해피: 정상 스캔", r1 == ScanResult.Ok && lp.scanned);
lp.countedQty = lp.line.actualQty;
lp.inspected = true;
var r2 = putaway.Commit(lp, Loc(l, "A-01-01"));
Check("해피: 정상 적치", r2 == PutawayResult.Ok && lp.putaway);
}
private static void TestUnknownBarcode(ScenarioLoader.LoadedScenario l)
{
var scan = new ScanService(NewProgress(l));
var r = scan.Scan("0000000000000", out var matched);
Check("함정: 미등록 바코드 → UnknownBarcode",
r == ScanResult.UnknownBarcode && matched == null);
}
private static void TestPutawayBeforeInspect(ScenarioLoader.LoadedScenario l)
{
var progress = NewProgress(l);
var putaway = new PutawayService();
var lp = progress.First(p => p.line.item.sku == "SKU-1001");
lp.scanned = true; // 검수는 생략
var r = putaway.Validate(lp, Loc(l, "A-01-01"));
Check("함정: 검수 전 적치 → NotInspected", r == PutawayResult.NotInspected);
}
private static void TestZoneMismatch(ScenarioLoader.LoadedScenario l)
{
var progress = NewProgress(l);
var putaway = new PutawayService();
var lp = progress.First(p => p.line.item.sku == "SKU-1001"); // Ambient 품목
lp.countedQty = lp.line.actualQty;
lp.inspected = true;
var r = putaway.Validate(lp, Loc(l, "C-02-03")); // Chilled 존
Check("함정: 존 위반 → ZoneMismatch", r == PutawayResult.ZoneMismatch);
}
private static void TestOverCapacity(ScenarioLoader.LoadedScenario l)
{
var progress = NewProgress(l);
var putaway = new PutawayService();
var lp = progress.First(p => p.line.item.sku == "SKU-1001"); // 10개 × 부피2 = 20
lp.countedQty = lp.line.actualQty;
lp.inspected = true;
var r = putaway.Validate(lp, Loc(l, "A-99-01")); // 용량 5
Check("함정: 용량 초과 → OverCapacity", r == PutawayResult.OverCapacity);
}
// ---- 헬퍼 ----
private static List<LineProgress> NewProgress(ScenarioLoader.LoadedScenario l)
=> l.scenario.lines.Select(line => new LineProgress(line)).ToList();
private static InboundLine Line(ScenarioLoader.LoadedScenario l, string sku)
=> l.scenario.lines.First(line => line.item.sku == sku);
private static LocationDefinition Loc(ScenarioLoader.LoadedScenario l, string code)
=> l.locations.First(loc => loc.locationCode == code);
private static void Check(string label, bool ok)
{
if (ok) { _pass++; Debug.Log($" ✅ {label}"); }
else { _fail++; Debug.LogError($" ❌ {label}"); }
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 75a0eb77b8a87495cb9a80a685e0e1a8