diff --git a/Assets/Scripts/Editor/SlotSetup.cs b/Assets/Scripts/Editor/SlotSetup.cs
new file mode 100644
index 0000000..02bd402
--- /dev/null
+++ b/Assets/Scripts/Editor/SlotSetup.cs
@@ -0,0 +1,65 @@
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using SinoWms.Gameplay;
+
+namespace SinoWms.EditorTools
+{
+ ///
+ /// 적치 슬롯을 창고에 배치한다. 각 슬롯은 바닥 패드 + 트리거 콜라이더 + 기대 품목을 가진다.
+ /// 메뉴 SinoWMS/Setup Slots. Play 후 박스를 들고 슬롯에서 E키로 적치(정답=초록/오답=빨강).
+ ///
+ public static class SlotSetup
+ {
+ // 슬롯 위치와 기대 품목 (PickupDemoSetup의 박스 이름과 일치시켜 정답 구성)
+ private static readonly (Vector3 pos, string expected)[] Slots =
+ {
+ (new Vector3(-6f, 0.03f, 8f), "박스 A (생수)"),
+ (new Vector3(-2f, 0.03f, 8f), "박스 B (우유)"),
+ (new Vector3(2f, 0.03f, 8f), "박스 C (라면)"),
+ (new Vector3(6f, 0.03f, 8f), "박스 D (잡화)"),
+ };
+
+ [MenuItem("SinoWMS/Setup Slots")]
+ public static void Setup()
+ {
+ var container = GameObject.Find("PutawaySlots") ?? new GameObject("PutawaySlots");
+
+ for (int i = 0; i < Slots.Length; i++)
+ {
+ string slotName = $"Slot_{i + 1}";
+ if (container.transform.Find(slotName) != null) continue;
+
+ var slotGo = new GameObject(slotName);
+ slotGo.transform.SetParent(container.transform, false);
+ slotGo.transform.localPosition = Slots[i].pos;
+
+ // 바닥 패드 (상태 색 표시)
+ var pad = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ pad.name = "Pad";
+ Object.DestroyImmediate(pad.GetComponent());
+ pad.transform.SetParent(slotGo.transform, false);
+ pad.transform.localScale = new Vector3(1.2f, 0.05f, 1.2f);
+ pad.GetComponent().sharedMaterial =
+ new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"))
+ { color = new Color(1f, 0.82f, 0.3f) };
+
+ // 적치 감지용 트리거
+ var trigger = slotGo.AddComponent();
+ trigger.isTrigger = true;
+ trigger.size = new Vector3(1.4f, 1.5f, 1.4f);
+ trigger.center = new Vector3(0f, 0.75f, 0f);
+
+ var slot = slotGo.AddComponent();
+ var so = new SerializedObject(slot);
+ so.FindProperty("pad").objectReferenceValue = pad.GetComponent();
+ so.FindProperty("expectedLabel").stringValue = Slots[i].expected;
+ so.ApplyModifiedProperties();
+ }
+
+ EditorSceneManager.MarkSceneDirty(container.scene);
+ Selection.activeGameObject = container;
+ Debug.Log("[SlotSetup] 완료. 박스를 들고 슬롯 위에서 E키로 적치하세요(정답=초록/오답=빨강).");
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/SlotSetup.cs.meta b/Assets/Scripts/Editor/SlotSetup.cs.meta
new file mode 100644
index 0000000..aaf7b6c
--- /dev/null
+++ b/Assets/Scripts/Editor/SlotSetup.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bab24c45943e84dc9bf7ace8f7d0d618
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/PickupController.cs b/Assets/Scripts/Gameplay/PickupController.cs
index 9d2eb85..69a60d4 100644
--- a/Assets/Scripts/Gameplay/PickupController.cs
+++ b/Assets/Scripts/Gameplay/PickupController.cs
@@ -15,6 +15,7 @@ namespace SinoWms.Player
private Carryable _carried;
private Carryable _target; // 현재 집을 수 있는 후보(힌트 표시용)
+ private PutawaySlot _targetSlot; // 들고 있을 때 적치 가능한 빈 슬롯
private GUIStyle _style;
private void Awake()
@@ -34,7 +35,14 @@ namespace SinoWms.Player
if (kb == null) return;
if (_carried == null)
+ {
_target = FindNearest();
+ _targetSlot = null;
+ }
+ else
+ {
+ _targetSlot = FindNearestSlot();
+ }
if (kb.eKey.wasPressedThisFrame)
{
@@ -58,8 +66,27 @@ namespace SinoWms.Player
return best;
}
+ private PutawaySlot FindNearestSlot()
+ {
+ var hits = Physics.OverlapSphere(transform.position, pickupRange);
+ PutawaySlot best = null;
+ float bestDist = float.MaxValue;
+ foreach (var h in hits)
+ {
+ var s = h.GetComponentInParent();
+ if (s == null || s.IsOccupied) continue;
+ float d = Vector3.Distance(transform.position, s.transform.position);
+ if (d < bestDist) { bestDist = d; best = s; }
+ }
+ return best;
+ }
+
private void PickUp(Carryable c)
{
+ // 슬롯에 적치돼 있던 박스라면 슬롯을 비운다
+ var fromSlot = c.GetComponentInParent();
+ if (fromSlot != null && fromSlot.Occupant == c) fromSlot.Clear();
+
_carried = c;
_target = null;
@@ -75,11 +102,18 @@ namespace SinoWms.Player
{
var c = _carried;
_carried = null;
-
c.transform.SetParent(null);
+
+ // 근처 빈 슬롯이 있으면 적치(스냅 + 정답 판정)
+ if (_targetSlot != null && _targetSlot.TryPlace(c))
+ {
+ _targetSlot = null;
+ return;
+ }
+
+ // 아니면 그냥 바닥에 내려놓기
c.transform.position = holdPoint.position;
c.transform.rotation = holdPoint.rotation;
-
if (c.TryGetComponent(out var col)) col.enabled = true;
if (c.TryGetComponent(out var rb)) rb.isKinematic = false;
}
@@ -89,8 +123,12 @@ namespace SinoWms.Player
_style ??= new GUIStyle(GUI.skin.box) { fontSize = 18, alignment = TextAnchor.MiddleCenter };
string msg = null;
- if (_carried != null) msg = $"E: 내려놓기 — {_carried.Label}";
- else if (_target != null) msg = $"E: 집기 — {_target.Label}";
+ if (_carried != null)
+ msg = _targetSlot != null
+ ? $"E: 적치 — {_targetSlot.Title}"
+ : $"E: 내려놓기 — {_carried.Label}";
+ else if (_target != null)
+ msg = $"E: 집기 — {_target.Label}";
if (msg == null) return;
GUI.Label(new Rect(Screen.width / 2f - 150f, 24f, 300f, 32f), msg, _style);
diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs b/Assets/Scripts/Gameplay/PutawaySlot.cs
new file mode 100644
index 0000000..da6809c
--- /dev/null
+++ b/Assets/Scripts/Gameplay/PutawaySlot.cs
@@ -0,0 +1,60 @@
+using UnityEngine;
+
+namespace SinoWms.Gameplay
+{
+ ///
+ /// 적치 슬롯. 캐릭터가 들고 온 Carryable을 받아 스냅 배치하고,
+ /// 기대 품목(expectedLabel)과 비교해 정답/오답을 바닥 패드 색으로 표시한다.
+ ///
+ public class PutawaySlot : MonoBehaviour
+ {
+ [Tooltip("이 슬롯에 와야 하는 품목 라벨. 비우면 아무거나 정답 처리.")]
+ public string expectedLabel;
+
+ [Tooltip("바닥 표시용 패드 렌더러(색으로 상태 표시).")]
+ [SerializeField] private Renderer pad;
+
+ [Tooltip("박스가 스냅될 패드 위 높이.")]
+ [SerializeField] private float dropHeight = 0.5f;
+
+ private static readonly Color Empty = new Color(1f, 0.82f, 0.3f);
+ private static readonly Color Correct = new Color(0.3f, 0.8f, 0.4f);
+ private static readonly Color Wrong = new Color(0.85f, 0.3f, 0.3f);
+
+ public Carryable Occupant { get; private set; }
+ public bool IsOccupied => Occupant != null;
+ public bool IsCorrect =>
+ IsOccupied && (string.IsNullOrEmpty(expectedLabel) || Occupant.Label == expectedLabel);
+
+ public string Title => string.IsNullOrEmpty(expectedLabel) ? name : expectedLabel;
+
+ private void Start() => UpdateMarker();
+
+ public bool TryPlace(Carryable c)
+ {
+ if (IsOccupied) return false;
+ Occupant = c;
+
+ c.transform.SetParent(transform);
+ c.transform.localPosition = new Vector3(0f, dropHeight, 0f);
+ c.transform.localRotation = Quaternion.identity;
+ if (c.TryGetComponent(out var col)) col.enabled = true;
+ if (c.TryGetComponent(out var rb)) rb.isKinematic = true;
+
+ UpdateMarker();
+ return true;
+ }
+
+ public void Clear()
+ {
+ Occupant = null;
+ UpdateMarker();
+ }
+
+ private void UpdateMarker()
+ {
+ if (pad == null) return;
+ pad.material.color = !IsOccupied ? Empty : (IsCorrect ? Correct : Wrong);
+ }
+ }
+}
diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs.meta b/Assets/Scripts/Gameplay/PutawaySlot.cs.meta
new file mode 100644
index 0000000..810e91f
--- /dev/null
+++ b/Assets/Scripts/Gameplay/PutawaySlot.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c9d533b08ac4a487182866db79a8e2e3
\ No newline at end of file