diff --git a/Assets/Scripts/Editor/PickupDemoSetup.cs b/Assets/Scripts/Editor/PickupDemoSetup.cs
new file mode 100644
index 0000000..06979c1
--- /dev/null
+++ b/Assets/Scripts/Editor/PickupDemoSetup.cs
@@ -0,0 +1,68 @@
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using SinoWms.Gameplay;
+using SinoWms.Player;
+
+namespace SinoWms.EditorTools
+{
+ ///
+ /// 집기/놓기 데모 구성: 플레이어에 PickupController 추가 + 입고 박스 스폰.
+ /// 메뉴 SinoWMS/Setup Pickup Demo. Play 후 박스에 다가가 E키로 집고 놓는다.
+ ///
+ public static class PickupDemoSetup
+ {
+ [MenuItem("SinoWMS/Setup Pickup Demo")]
+ public static void Setup()
+ {
+ var player = GameObject.Find("Player");
+ if (player == null)
+ {
+ Debug.LogError("[PickupDemoSetup] Player가 없습니다. 먼저 'Setup Playground Scene'을 실행하세요.");
+ return;
+ }
+ if (player.GetComponent() == null)
+ player.AddComponent();
+
+ var container = GameObject.Find("InboundBoxes") ?? new GameObject("InboundBoxes");
+
+ // 입고 대기 박스 4개 (남쪽 통로 근처에 배치)
+ var colors = new[]
+ {
+ new Color(0.75f, 0.55f, 0.35f),
+ new Color(0.70f, 0.50f, 0.30f),
+ new Color(0.80f, 0.60f, 0.40f),
+ new Color(0.65f, 0.45f, 0.28f),
+ };
+ string[] names = { "박스 A (생수)", "박스 B (우유)", "박스 C (라면)", "박스 D (잡화)" };
+
+ for (int i = 0; i < 4; i++)
+ {
+ string objName = $"Box_{i + 1}";
+ if (container.transform.Find(objName) != null) continue;
+
+ var box = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ box.name = objName;
+ box.transform.SetParent(container.transform, false);
+ box.transform.localPosition = new Vector3(-3f + i * 2f, 0.5f, -20f);
+ box.transform.localScale = Vector3.one;
+ box.GetComponent().sharedMaterial = MakeMaterial(colors[i]);
+
+ var rb = box.AddComponent();
+ rb.mass = 1f;
+
+ box.AddComponent().displayName = names[i];
+ }
+
+ EditorSceneManager.MarkSceneDirty(player.scene);
+ Selection.activeGameObject = player;
+ Debug.Log("[PickupDemoSetup] 완료. Play 후 박스에 다가가 E키로 집고 놓아 보세요.");
+ }
+
+ private static Material MakeMaterial(Color color)
+ {
+ var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
+ return new Material(shader) { color = color };
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/PickupDemoSetup.cs.meta b/Assets/Scripts/Editor/PickupDemoSetup.cs.meta
new file mode 100644
index 0000000..2691389
--- /dev/null
+++ b/Assets/Scripts/Editor/PickupDemoSetup.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 10eba722501bf4dcaa1d33cc90d763b1
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay.meta b/Assets/Scripts/Gameplay.meta
new file mode 100644
index 0000000..0a85f5b
--- /dev/null
+++ b/Assets/Scripts/Gameplay.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3ee476866b1bf47f5a10539f8b8dcef5
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Gameplay/Carryable.cs b/Assets/Scripts/Gameplay/Carryable.cs
new file mode 100644
index 0000000..a1e6d1a
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Carryable.cs
@@ -0,0 +1,15 @@
+using UnityEngine;
+
+namespace SinoWms.Gameplay
+{
+ /// 플레이어가 집어 옮길 수 있는 물건(박스/팔레트) 표시 컴포넌트.
+ [RequireComponent(typeof(Rigidbody))]
+ [RequireComponent(typeof(Collider))]
+ public class Carryable : MonoBehaviour
+ {
+ [Tooltip("힌트/로그에 표시할 이름. 비우면 오브젝트 이름 사용.")]
+ public string displayName;
+
+ public string Label => string.IsNullOrEmpty(displayName) ? name : displayName;
+ }
+}
diff --git a/Assets/Scripts/Gameplay/Carryable.cs.meta b/Assets/Scripts/Gameplay/Carryable.cs.meta
new file mode 100644
index 0000000..9451e4f
--- /dev/null
+++ b/Assets/Scripts/Gameplay/Carryable.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 315a7e23f649b4187a444b01f14a8e9d
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/PickupController.cs b/Assets/Scripts/Gameplay/PickupController.cs
new file mode 100644
index 0000000..9d2eb85
--- /dev/null
+++ b/Assets/Scripts/Gameplay/PickupController.cs
@@ -0,0 +1,99 @@
+using UnityEngine;
+using UnityEngine.InputSystem;
+using SinoWms.Gameplay;
+
+namespace SinoWms.Player
+{
+ ///
+ /// 캐릭터의 집기/놓기 상호작용. 범위 내 가장 가까운 Carryable을 E키로 집어
+ /// 앞쪽 홀드 포인트에 들고 다니고, 다시 E키로 내려놓는다.
+ ///
+ public class PickupController : MonoBehaviour
+ {
+ [SerializeField] private float pickupRange = 2.5f;
+ [SerializeField] private Transform holdPoint;
+
+ private Carryable _carried;
+ private Carryable _target; // 현재 집을 수 있는 후보(힌트 표시용)
+ private GUIStyle _style;
+
+ private void Awake()
+ {
+ if (holdPoint == null)
+ {
+ var hp = new GameObject("HoldPoint").transform;
+ hp.SetParent(transform, false);
+ hp.localPosition = new Vector3(0f, 0.6f, 0.9f);
+ holdPoint = hp;
+ }
+ }
+
+ private void Update()
+ {
+ var kb = Keyboard.current;
+ if (kb == null) return;
+
+ if (_carried == null)
+ _target = FindNearest();
+
+ if (kb.eKey.wasPressedThisFrame)
+ {
+ if (_carried != null) Drop();
+ else if (_target != null) PickUp(_target);
+ }
+ }
+
+ private Carryable FindNearest()
+ {
+ var hits = Physics.OverlapSphere(transform.position, pickupRange);
+ Carryable best = null;
+ float bestDist = float.MaxValue;
+ foreach (var h in hits)
+ {
+ var c = h.GetComponentInParent();
+ if (c == null || c == _carried) continue;
+ float d = Vector3.Distance(transform.position, c.transform.position);
+ if (d < bestDist) { bestDist = d; best = c; }
+ }
+ return best;
+ }
+
+ private void PickUp(Carryable c)
+ {
+ _carried = c;
+ _target = null;
+
+ if (c.TryGetComponent(out var rb)) rb.isKinematic = true;
+ if (c.TryGetComponent(out var col)) col.enabled = false;
+
+ c.transform.SetParent(holdPoint);
+ c.transform.localPosition = Vector3.zero;
+ c.transform.localRotation = Quaternion.identity;
+ }
+
+ private void Drop()
+ {
+ var c = _carried;
+ _carried = null;
+
+ c.transform.SetParent(null);
+ 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;
+ }
+
+ private void OnGUI()
+ {
+ _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 (msg == null) return;
+
+ GUI.Label(new Rect(Screen.width / 2f - 150f, 24f, 300f, 32f), msg, _style);
+ }
+ }
+}
diff --git a/Assets/Scripts/Gameplay/PickupController.cs.meta b/Assets/Scripts/Gameplay/PickupController.cs.meta
new file mode 100644
index 0000000..9d296f3
--- /dev/null
+++ b/Assets/Scripts/Gameplay/PickupController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8525e0c0e962c47b78e49a7e3371fe97
\ No newline at end of file