feat: 입고 PDA UI (UI Toolkit) — 보류 작업물 정리
캐릭터-first 전환 전에 만든 PDA/창고 UI 작업물을 히스토리에 보존. - InboundPda.uxml/.uss/.tss + InboundPanelSettings - InboundUIController: 단계별 렌더링 + 입력 위임 - InboundSceneSetup: UIDocument/PanelSettings 자동 와이어링 씬에는 비활성 상태로 존재. 추후 입고 로직 재연결 시 재사용 가능. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using SinoWms.Inbound;
|
||||
using SinoWms.UI;
|
||||
|
||||
namespace SinoWms.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 입고 UI 씬을 한 번에 구성한다(런타임 UI Toolkit 와이어링 자동화).
|
||||
/// 메뉴 <b>SinoWMS/Setup Inbound Scene</b> 실행 후 Play 하면 PDA 미션이 뜬다.
|
||||
/// </summary>
|
||||
public static class InboundSceneSetup
|
||||
{
|
||||
private const string UiDir = "Assets/UI";
|
||||
private const string ThemePath = UiDir + "/InboundRuntimeTheme.tss";
|
||||
private const string UxmlPath = UiDir + "/InboundPda.uxml";
|
||||
private const string UssPath = UiDir + "/InboundPda.uss";
|
||||
private const string PanelSettingsPath = UiDir + "/InboundPanelSettings.asset";
|
||||
|
||||
[MenuItem("SinoWMS/Setup Inbound Scene")]
|
||||
public static void Setup()
|
||||
{
|
||||
var panelSettings = EnsurePanelSettings();
|
||||
if (panelSettings == null) return;
|
||||
|
||||
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
||||
if (uxml == null)
|
||||
{
|
||||
Debug.LogError($"[InboundSceneSetup] UXML을 찾을 수 없습니다: {UxmlPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
var go = GameObject.Find("InboundUI") ?? new GameObject("InboundUI");
|
||||
|
||||
var manager = go.GetComponent<InboundManager>() ?? go.AddComponent<InboundManager>();
|
||||
|
||||
var doc = go.GetComponent<UIDocument>() ?? go.AddComponent<UIDocument>();
|
||||
doc.panelSettings = panelSettings;
|
||||
doc.visualTreeAsset = uxml;
|
||||
|
||||
// 컨트롤러는 UIDocument 이후에 추가(RequireComponent 충족) + manager 연결
|
||||
var controller = go.GetComponent<InboundUIController>() ?? go.AddComponent<InboundUIController>();
|
||||
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||
if (styleSheet == null)
|
||||
Debug.LogWarning($"[InboundSceneSetup] USS를 찾을 수 없습니다: {UssPath}");
|
||||
var so = new SerializedObject(controller);
|
||||
so.FindProperty("manager").objectReferenceValue = manager;
|
||||
so.FindProperty("styleSheet").objectReferenceValue = styleSheet;
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
Selection.activeGameObject = go;
|
||||
EditorSceneManager.MarkSceneDirty(go.scene);
|
||||
Debug.Log("[InboundSceneSetup] 완료. 'InboundUI' 오브젝트 생성 → Play 하면 미션이 시작됩니다.");
|
||||
}
|
||||
|
||||
private static PanelSettings EnsurePanelSettings()
|
||||
{
|
||||
var existing = AssetDatabase.LoadAssetAtPath<PanelSettings>(PanelSettingsPath);
|
||||
if (existing != null) return existing;
|
||||
|
||||
var theme = AssetDatabase.LoadAssetAtPath<ThemeStyleSheet>(ThemePath);
|
||||
if (theme == null)
|
||||
{
|
||||
Debug.LogError($"[InboundSceneSetup] 런타임 테마를 찾을 수 없습니다: {ThemePath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var ps = ScriptableObject.CreateInstance<PanelSettings>();
|
||||
ps.themeStyleSheet = theme;
|
||||
AssetDatabase.CreateAsset(ps, PanelSettingsPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[InboundSceneSetup] PanelSettings 생성: {PanelSettingsPath}");
|
||||
return ps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a169582dd1dc4035b39ac7c0635fadb
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09512757295aa4e849a36067192a5348
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,419 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using SinoWms.Core;
|
||||
using SinoWms.Data;
|
||||
using SinoWms.Inbound;
|
||||
|
||||
namespace SinoWms.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 입고 미션 PDA + 탑다운 창고 UI (UI Toolkit).
|
||||
/// InboundManager의 이벤트를 구독해 단계별 화면을 다시 그리고, 입력을 매니저로 위임한다.
|
||||
/// mockup/inbound_pda.html 의 화면 흐름을 그대로 옮긴 구현.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(UIDocument))]
|
||||
public class InboundUIController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InboundManager manager;
|
||||
[SerializeField] private StyleSheet styleSheet; // UXML <Style> 대신 코드로 확실히 적용
|
||||
[SerializeField] private string scenarioResource = "Scenarios/inbound_basic";
|
||||
[SerializeField] private string warehouseResource = "Scenarios/warehouse_basic";
|
||||
|
||||
private ScenarioLoader.LoadedScenario _loaded;
|
||||
private LineProgress _currentLine; // 스캔으로 매칭된, 처리 중인 라인
|
||||
private int _finalScore = -1;
|
||||
|
||||
private VisualElement _grid;
|
||||
private VisualElement _stepbar;
|
||||
private VisualElement _content;
|
||||
private VisualElement _feedback;
|
||||
private Label _scenarioSub;
|
||||
|
||||
private string _fbMsg;
|
||||
private bool _fbOk;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
var root = GetComponent<UIDocument>().rootVisualElement;
|
||||
if (styleSheet != null && !root.styleSheets.Contains(styleSheet))
|
||||
root.styleSheets.Add(styleSheet);
|
||||
|
||||
_grid = root.Q("grid");
|
||||
_stepbar = root.Q("stepbar");
|
||||
_content = root.Q("content");
|
||||
_feedback = root.Q("feedback");
|
||||
_scenarioSub = root.Q<Label>("scenario-sub");
|
||||
|
||||
if (manager == null) manager = GetComponent<InboundManager>();
|
||||
if (manager == null)
|
||||
{
|
||||
Debug.LogError("[InboundUIController] InboundManager를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_loaded = ScenarioLoader.LoadFromResources(scenarioResource, warehouseResource);
|
||||
if (_scenarioSub != null)
|
||||
_scenarioSub.text = $"{_loaded.scenario.title} ({_loaded.scenario.scenarioId})";
|
||||
|
||||
manager.StepChanged += OnStep;
|
||||
manager.Feedback += OnFeedback;
|
||||
manager.Completed += OnCompleted;
|
||||
|
||||
manager.StartMission(_loaded.scenario);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (manager == null) return;
|
||||
manager.StepChanged -= OnStep;
|
||||
manager.Feedback -= OnFeedback;
|
||||
manager.Completed -= OnCompleted;
|
||||
}
|
||||
|
||||
// ---- 매니저 이벤트 ----
|
||||
|
||||
private void OnStep(InboundStep step)
|
||||
{
|
||||
// 완료 화면은 점수를 받은 OnCompleted에서 그린다(StepChanged가 먼저 와서 점수가 비어있음).
|
||||
if (step == InboundStep.Completed) return;
|
||||
Render(step);
|
||||
}
|
||||
|
||||
private void OnFeedback(string msg)
|
||||
{
|
||||
_fbMsg = msg;
|
||||
_fbOk = !string.IsNullOrEmpty(msg) && !msg.Contains("⚠");
|
||||
RenderFeedback();
|
||||
}
|
||||
|
||||
private void OnCompleted(int score)
|
||||
{
|
||||
_finalScore = score;
|
||||
Render(InboundStep.Completed);
|
||||
}
|
||||
|
||||
// ---- 렌더링 ----
|
||||
|
||||
private void Render(InboundStep step)
|
||||
{
|
||||
RenderStepBar(step);
|
||||
RenderGrid(step);
|
||||
|
||||
_content.Clear();
|
||||
switch (step)
|
||||
{
|
||||
case InboundStep.ReviewAsn: RenderReview(); break;
|
||||
case InboundStep.Scan: RenderScan(); break;
|
||||
case InboundStep.Inspect: RenderInspect(); break;
|
||||
case InboundStep.SelectLocation: RenderSelectLocation(); break;
|
||||
case InboundStep.Completed: RenderResult(); break;
|
||||
}
|
||||
RenderFeedback();
|
||||
}
|
||||
|
||||
private void RenderStepBar(InboundStep step)
|
||||
{
|
||||
_stepbar.Clear();
|
||||
int current = (int)step;
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
var seg = new VisualElement();
|
||||
seg.AddToClassList("step-seg");
|
||||
if (i <= current) seg.AddToClassList("step-seg--on");
|
||||
_stepbar.Add(seg);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderGrid(InboundStep step)
|
||||
{
|
||||
_grid.Clear();
|
||||
bool selectable = step == InboundStep.SelectLocation && _currentLine != null;
|
||||
|
||||
// gridY 행 → gridX 열 순으로 배치
|
||||
var rows = new SortedDictionary<int, List<LocationDefinition>>();
|
||||
foreach (var loc in _loaded.locations)
|
||||
{
|
||||
if (!rows.TryGetValue(loc.gridPosition.y, out var list))
|
||||
rows[loc.gridPosition.y] = list = new List<LocationDefinition>();
|
||||
list.Add(loc);
|
||||
}
|
||||
|
||||
foreach (var kv in rows)
|
||||
{
|
||||
kv.Value.Sort((a, b) => a.gridPosition.x.CompareTo(b.gridPosition.x));
|
||||
var rowEl = new VisualElement();
|
||||
rowEl.AddToClassList("grid-row");
|
||||
foreach (var loc in kv.Value)
|
||||
rowEl.Add(BuildLocationCell(loc, selectable));
|
||||
_grid.Add(rowEl);
|
||||
}
|
||||
}
|
||||
|
||||
private VisualElement BuildLocationCell(LocationDefinition loc, bool selectable)
|
||||
{
|
||||
int used = UsedVolume(loc);
|
||||
bool filled = used > 0;
|
||||
|
||||
var cell = new VisualElement();
|
||||
cell.AddToClassList("loc");
|
||||
cell.AddToClassList(ZoneClass(loc.zone));
|
||||
if (filled) cell.AddToClassList("loc--filled");
|
||||
|
||||
var code = new Label(loc.locationCode);
|
||||
code.AddToClassList("loc-code");
|
||||
var cap = new Label($"{used}/{loc.capacity}");
|
||||
cap.AddToClassList("loc-cap");
|
||||
cell.Add(code);
|
||||
cell.Add(cap);
|
||||
|
||||
if (selectable && !filled)
|
||||
{
|
||||
cell.AddToClassList("loc--selectable");
|
||||
cell.RegisterCallback<ClickEvent>(_ => manager.HandlePutaway(_currentLine, loc));
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
private void RenderReview()
|
||||
{
|
||||
_content.Add(StepTitle("STEP 1 / 입고예정"));
|
||||
_content.Add(StepHead($"오늘 입고예정 {_loaded.scenario.lines.Count}건"));
|
||||
foreach (var line in _loaded.scenario.lines)
|
||||
{
|
||||
var card = LineCard(line.item, $"예정 {line.expectedQty} · {line.item.barcode}");
|
||||
_content.Add(card);
|
||||
}
|
||||
_content.Add(PrimaryButton("확인 완료 → 스캔 시작", () => manager.ConfirmAsnReviewed()));
|
||||
}
|
||||
|
||||
private void RenderScan()
|
||||
{
|
||||
var lp = NextUnscanned();
|
||||
if (lp == null) return;
|
||||
var line = lp.line;
|
||||
|
||||
_content.Add(StepTitle($"STEP 2 / 스캔 ({ProcessedCount() + 1}/{_loaded.scenario.lines.Count})"));
|
||||
_content.Add(StepHead("바코드를 스캔하세요"));
|
||||
_content.Add(LineCard(line.item, $"정답 바코드: {line.item.barcode}"));
|
||||
|
||||
var input = new TextField { value = line.item.barcode };
|
||||
_content.Add(input);
|
||||
_content.Add(PrimaryButton("📷 스캔", () =>
|
||||
{
|
||||
var matched = manager.HandleScan(input.value);
|
||||
if (matched != null) _currentLine = matched;
|
||||
}));
|
||||
_content.Add(GhostButton("잘못된 바코드 스캔 (오류 체험)",
|
||||
() => manager.HandleScan("0000000000000")));
|
||||
}
|
||||
|
||||
private void RenderInspect()
|
||||
{
|
||||
if (_currentLine == null) return;
|
||||
var line = _currentLine.line;
|
||||
|
||||
_content.Add(StepTitle("STEP 3 / 검수"));
|
||||
_content.Add(StepHead("실제 수량을 세어 입력"));
|
||||
|
||||
var card = new VisualElement();
|
||||
card.AddToClassList("line-card");
|
||||
var name = new Label(line.item.displayName);
|
||||
name.AddToClassList("name");
|
||||
card.Add(name);
|
||||
card.Add(Meta($"예정 수량: {line.expectedQty}"));
|
||||
card.Add(Meta($"실제 도착: {line.actualQty}" +
|
||||
(line.expectedQty != line.actualQty ? " ⚠ 예정과 다름" : "")));
|
||||
_content.Add(card);
|
||||
|
||||
var qty = new IntegerField { value = line.expectedQty };
|
||||
_content.Add(qty);
|
||||
_content.Add(PrimaryButton("검수 확정", () => manager.HandleInspect(_currentLine, qty.value)));
|
||||
}
|
||||
|
||||
private void RenderSelectLocation()
|
||||
{
|
||||
if (_currentLine == null) return;
|
||||
var line = _currentLine.line;
|
||||
int need = _currentLine.countedQty * line.item.unitVolume;
|
||||
|
||||
_content.Add(StepTitle("STEP 4 / 로케이션 선택"));
|
||||
_content.Add(StepHead("적치할 위치를 클릭 →"));
|
||||
|
||||
var card = LineCard(line.item, $"필요 용량: {need} · 요구 존: {ZoneLabel(line.item.requiredZone)}");
|
||||
var hint = Meta($"왼쪽 격자에서 {ZoneLabel(line.item.requiredZone)} 칸을 선택하세요");
|
||||
hint.style.color = new StyleColor(new Color32(43, 111, 255, 255));
|
||||
card.Add(hint);
|
||||
_content.Add(card);
|
||||
}
|
||||
|
||||
private void RenderResult()
|
||||
{
|
||||
_content.Add(StepTitle("완료"));
|
||||
_content.Add(StepHead("교육 결과"));
|
||||
|
||||
var score = new Label($"{_finalScore}점");
|
||||
score.AddToClassList("result-score");
|
||||
_content.Add(score);
|
||||
|
||||
var errors = manager.GetErrors();
|
||||
var summary = new VisualElement();
|
||||
summary.AddToClassList("line-card");
|
||||
summary.Add(Meta($"처리 {_loaded.scenario.lines.Count}건 · 오류 {errors.Count}건"));
|
||||
_content.Add(summary);
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
var errBox = new VisualElement();
|
||||
errBox.AddToClassList("line-card");
|
||||
foreach (var e in errors)
|
||||
{
|
||||
var item = new Label($"⚠ {e.message}");
|
||||
item.AddToClassList("err-item");
|
||||
errBox.Add(item);
|
||||
}
|
||||
_content.Add(errBox);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ok = new Label("완벽합니다! 오류 없음 👏");
|
||||
ok.AddToClassList("feedback");
|
||||
ok.AddToClassList("fb-ok");
|
||||
_content.Add(ok);
|
||||
}
|
||||
|
||||
_content.Add(PrimaryButton("다시 하기", Restart));
|
||||
}
|
||||
|
||||
private void RenderFeedback()
|
||||
{
|
||||
if (_feedback == null) return;
|
||||
_feedback.Clear();
|
||||
if (string.IsNullOrEmpty(_fbMsg)) return;
|
||||
|
||||
var fb = new Label(_fbMsg);
|
||||
fb.AddToClassList("feedback");
|
||||
fb.AddToClassList(_fbOk ? "fb-ok" : "fb-warn");
|
||||
_feedback.Add(fb);
|
||||
}
|
||||
|
||||
// ---- 헬퍼 ----
|
||||
|
||||
private void Restart()
|
||||
{
|
||||
_currentLine = null;
|
||||
_finalScore = -1;
|
||||
_fbMsg = null;
|
||||
manager.StartMission(_loaded.scenario);
|
||||
}
|
||||
|
||||
private LineProgress NextUnscanned()
|
||||
{
|
||||
foreach (var lp in manager.Progress)
|
||||
if (!lp.scanned) return lp;
|
||||
return null;
|
||||
}
|
||||
|
||||
private int ProcessedCount()
|
||||
{
|
||||
int n = 0;
|
||||
foreach (var lp in manager.Progress)
|
||||
if (lp.putaway) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
private int UsedVolume(LocationDefinition loc)
|
||||
{
|
||||
int used = 0;
|
||||
foreach (var lp in manager.Progress)
|
||||
if (lp.placedLocation == loc)
|
||||
used += lp.countedQty * lp.line.item.unitVolume;
|
||||
return used;
|
||||
}
|
||||
|
||||
private static Label StepTitle(string t)
|
||||
{
|
||||
var l = new Label(t);
|
||||
l.AddToClassList("step-title");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static Label StepHead(string t)
|
||||
{
|
||||
var l = new Label(t);
|
||||
l.AddToClassList("step-head");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static Label Meta(string t)
|
||||
{
|
||||
var l = new Label(t);
|
||||
l.AddToClassList("meta");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static VisualElement LineCard(ItemDefinition item, string metaText)
|
||||
{
|
||||
var card = new VisualElement();
|
||||
card.AddToClassList("line-card");
|
||||
|
||||
var nameRow = new VisualElement();
|
||||
nameRow.AddToClassList("row");
|
||||
var name = new Label(item.displayName);
|
||||
name.AddToClassList("name");
|
||||
nameRow.Add(name);
|
||||
nameRow.Add(ZoneBadge(item.requiredZone));
|
||||
card.Add(nameRow);
|
||||
|
||||
card.Add(Meta(metaText));
|
||||
return card;
|
||||
}
|
||||
|
||||
private static Label ZoneBadge(StorageZone zone)
|
||||
{
|
||||
var b = new Label(ZoneLabel(zone));
|
||||
b.AddToClassList("badge");
|
||||
switch (zone)
|
||||
{
|
||||
case StorageZone.Chilled: b.AddToClassList("b-chilled"); break;
|
||||
case StorageZone.Frozen: b.AddToClassList("b-frozen"); break;
|
||||
default: b.AddToClassList("b-ambient"); break;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private static Button PrimaryButton(string text, System.Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = text };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Button GhostButton(string text, System.Action onClick)
|
||||
{
|
||||
var btn = new Button(onClick) { text = text };
|
||||
btn.AddToClassList("ghost");
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static string ZoneClass(StorageZone zone)
|
||||
{
|
||||
switch (zone)
|
||||
{
|
||||
case StorageZone.Chilled: return "chilled";
|
||||
case StorageZone.Frozen: return "frozen";
|
||||
default: return "ambient";
|
||||
}
|
||||
}
|
||||
|
||||
private static string ZoneLabel(StorageZone zone)
|
||||
{
|
||||
switch (zone)
|
||||
{
|
||||
case StorageZone.Chilled: return "냉장";
|
||||
case StorageZone.Frozen: return "냉동";
|
||||
case StorageZone.Hazardous: return "위험물";
|
||||
default: return "상온";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcb5c4bbb0d9448658ddc5b9fbe59d64
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88d854ac9c88045d5b950fb2704463e8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_Name: InboundPanelSettings
|
||||
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.PanelSettings
|
||||
themeUss: {fileID: -4733365628477956816, guid: 874e1b7e4085a4bb1b379902fbefadc2, type: 3}
|
||||
m_DisableNoThemeWarning: 0
|
||||
m_TargetTexture: {fileID: 0}
|
||||
m_RenderMode: 0
|
||||
m_ColliderUpdateMode: 0
|
||||
m_ColliderIsTrigger: 1
|
||||
m_ScaleMode: 1
|
||||
m_ReferenceSpritePixelsPerUnit: 100
|
||||
m_PixelsPerUnit: 100
|
||||
m_Scale: 1
|
||||
m_ReferenceDpi: 96
|
||||
m_FallbackDpi: 96
|
||||
m_ReferenceResolution: {x: 1200, y: 800}
|
||||
m_ScreenMatchMode: 0
|
||||
m_Match: 0
|
||||
m_SortingOrder: 0
|
||||
m_TargetDisplay: 0
|
||||
m_BindingLogLevel: 0
|
||||
m_ClearDepthStencil: 1
|
||||
m_ClearColor: 0
|
||||
m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0}
|
||||
m_VertexBudget: 0
|
||||
m_TextureSlotCount: 8
|
||||
m_DynamicAtlasSettings:
|
||||
m_MinAtlasSize: 64
|
||||
m_MaxAtlasSize: 4096
|
||||
m_MaxSubTextureSize: 64
|
||||
m_ActiveFilters: 31
|
||||
m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_DefaultShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_RuntimeGaussianBlurShader: {fileID: 20300, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_RuntimeColorEffectShader: {fileID: 20301, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_SDFShader: {fileID: 19011, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_BitmapShader: {fileID: 9001, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_SpriteShader: {fileID: 19012, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_ICUDataAsset: {fileID: 0}
|
||||
forceGammaRendering: 0
|
||||
textSettings: {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e19a4aaa28b74028a362e66ff3771e5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,245 @@
|
||||
/* SINO WMS 입고 교육 — PDA/창고 UI 스타일 (mockup/inbound_pda.html 이식) */
|
||||
|
||||
.root {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 24px;
|
||||
background-color: rgb(31, 36, 48);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 18px;
|
||||
color: rgb(230, 233, 239);
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
color: rgb(139, 147, 167);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ---- 탑다운 창고 ---- */
|
||||
.warehouse {
|
||||
background-color: rgb(39, 45, 58);
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
margin-right: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.loc {
|
||||
width: 64px;
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
border-width: 2px;
|
||||
border-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.loc-code {
|
||||
font-size: 11px;
|
||||
color: rgb(230, 233, 239);
|
||||
}
|
||||
|
||||
.loc-cap {
|
||||
font-size: 9px;
|
||||
color: rgb(170, 178, 197);
|
||||
}
|
||||
|
||||
.ambient { background-color: rgb(58, 66, 86); }
|
||||
.chilled { background-color: rgb(30, 74, 95); }
|
||||
.frozen { background-color: rgb(30, 58, 111); }
|
||||
|
||||
.loc--selectable {
|
||||
border-color: rgb(110, 168, 254);
|
||||
}
|
||||
|
||||
.loc--filled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.legend {
|
||||
flex-direction: row;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.legend-item Label {
|
||||
font-size: 11px;
|
||||
color: rgb(139, 147, 167);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dot-ambient { background-color: rgb(58, 66, 86); }
|
||||
.dot-chilled { background-color: rgb(30, 74, 95); }
|
||||
.dot-frozen { background-color: rgb(30, 58, 111); }
|
||||
|
||||
/* ---- PDA ---- */
|
||||
.pda {
|
||||
width: 300px;
|
||||
background-color: rgb(17, 21, 28);
|
||||
border-radius: 28px;
|
||||
padding: 16px;
|
||||
border-width: 8px;
|
||||
border-color: rgb(10, 13, 18);
|
||||
}
|
||||
|
||||
.pda-screen {
|
||||
background-color: rgb(244, 246, 251);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
min-height: 460px;
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
flex-direction: row;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.step-seg {
|
||||
flex-grow: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
background-color: rgb(211, 217, 230);
|
||||
}
|
||||
|
||||
.step-seg--on {
|
||||
background-color: rgb(43, 111, 255);
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 13px;
|
||||
color: rgb(43, 111, 255);
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.step-head {
|
||||
font-size: 17px;
|
||||
color: rgb(31, 36, 48);
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 12px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.line-card {
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-width: 1px;
|
||||
border-color: rgb(227, 232, 242);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.line-card .name {
|
||||
-unity-font-style: bold;
|
||||
font-size: 14px;
|
||||
color: rgb(31, 36, 48);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.line-card .meta {
|
||||
font-size: 12px;
|
||||
color: rgb(107, 114, 128);
|
||||
margin-top: 2px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 10px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
border-radius: 6px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.b-ambient { background-color: rgb(231, 236, 251); color: rgb(43, 78, 255); }
|
||||
.b-chilled { background-color: rgb(223, 241, 246); color: rgb(12, 116, 145); }
|
||||
.b-frozen { background-color: rgb(223, 235, 255); color: rgb(30, 58, 111); }
|
||||
|
||||
Button {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-width: 0;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(43, 111, 255);
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 14px;
|
||||
-unity-font-style: bold;
|
||||
margin-top: 6px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
Button.ghost {
|
||||
background-color: rgb(238, 241, 248);
|
||||
color: rgb(43, 111, 255);
|
||||
}
|
||||
|
||||
TextField, IntegerField {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
font-size: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
margin-top: 10px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.fb-ok { background-color: rgb(227, 247, 234); color: rgb(26, 127, 67); }
|
||||
.fb-warn { background-color: rgb(253, 236, 236); color: rgb(192, 57, 43); }
|
||||
|
||||
.result-score {
|
||||
font-size: 42px;
|
||||
-unity-font-style: bold;
|
||||
-unity-text-align: middle-center;
|
||||
color: rgb(43, 111, 255);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.err-item {
|
||||
font-size: 12px;
|
||||
color: rgb(192, 57, 43);
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 075aa5cc7aa18459b912d40ea7c9410b
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
unsupportedSelectorAction: 0
|
||||
@@ -0,0 +1,40 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements">
|
||||
<Style src="InboundPda.uss" />
|
||||
<ui:VisualElement class="root">
|
||||
|
||||
<!-- 탑다운 창고 -->
|
||||
<ui:VisualElement class="warehouse">
|
||||
<ui:Label text="창고 (탑다운 뷰)" class="h1" />
|
||||
<ui:Label text="로케이션을 클릭해 적치합니다" class="sub" />
|
||||
<ui:VisualElement name="grid" class="grid" />
|
||||
<ui:VisualElement class="legend">
|
||||
<ui:VisualElement class="legend-item">
|
||||
<ui:VisualElement class="dot dot-ambient" />
|
||||
<ui:Label text="상온" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="legend-item">
|
||||
<ui:VisualElement class="dot dot-chilled" />
|
||||
<ui:Label text="냉장" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="legend-item">
|
||||
<ui:VisualElement class="dot dot-frozen" />
|
||||
<ui:Label text="냉동" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- PDA -->
|
||||
<ui:VisualElement class="pda-wrap">
|
||||
<ui:Label text="PDA — 입고 교육" class="h1" />
|
||||
<ui:Label name="scenario-sub" text="" class="sub" />
|
||||
<ui:VisualElement class="pda">
|
||||
<ui:VisualElement class="pda-screen">
|
||||
<ui:VisualElement name="stepbar" class="step-bar" />
|
||||
<ui:VisualElement name="content" />
|
||||
<ui:VisualElement name="feedback" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd2a30127b39b47f9847c26859259fa6
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1 @@
|
||||
@import url("unity-theme://default");
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 874e1b7e4085a4bb1b379902fbefadc2
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0}
|
||||
disableValidation: 0
|
||||
unsupportedSelectorAction: 0
|
||||
+5
-4
@@ -10,12 +10,13 @@
|
||||
- `ScenarioLoader` — JSON 시나리오/창고 레이아웃을 런타임 ScriptableObject로 변환
|
||||
- 창고 레이아웃 데이터 `Assets/Resources/Scenarios/warehouse_basic.json` (로케이션 마스터)
|
||||
- `InboundManager.StartMission(InboundScenario)` 오버로드 (로더 결과 주입용)
|
||||
- 입고 로직 스모크 테스트 (`SinoWMS/Run Inbound Smoke Test` 메뉴 — 해피패스 + 함정 5종 검증)
|
||||
- 입고 로직 스모크 테스트 (`SinoWMS/Run Inbound Smoke Test` 메뉴 — 해피패스 + 함정 4종 검증)
|
||||
- PDA + 탑다운 창고 UI (UI Toolkit): `InboundPda.uxml` / `InboundPda.uss` / `InboundUIController`
|
||||
- 씬 자동 구성 메뉴 `SinoWMS/Setup Inbound Scene` (UIDocument/PanelSettings 와이어링)
|
||||
|
||||
### 예정
|
||||
- 탑다운 카메라 + 창고 그리드 씬
|
||||
- PDA UI 프리팹 (UI Toolkit 또는 uGUI)
|
||||
- 채점 결과 화면
|
||||
- 로케이션 슬롯 프리팹 + 적치 애니메이션
|
||||
- 채점 결과 화면 폴리시
|
||||
- 2번째 모듈(피킹)
|
||||
|
||||
## [0.1.0] - 2026-06-14
|
||||
|
||||
Reference in New Issue
Block a user