diff --git a/Assets/Scripts/Editor/MissionSetup.cs b/Assets/Scripts/Editor/MissionSetup.cs
new file mode 100644
index 0000000..b626b26
--- /dev/null
+++ b/Assets/Scripts/Editor/MissionSetup.cs
@@ -0,0 +1,26 @@
+using UnityEditor;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+using SinoWms.Gameplay;
+
+namespace SinoWms.EditorTools
+{
+ ///
+ /// 미션 판정 매니저를 씬에 추가한다. 메뉴 SinoWMS/Setup Mission.
+ /// Play 후 모든 슬롯을 정답으로 채우면 완료 + 점수 표시.
+ ///
+ public static class MissionSetup
+ {
+ [MenuItem("SinoWMS/Setup Mission")]
+ public static void Setup()
+ {
+ var go = GameObject.Find("Mission") ?? new GameObject("Mission");
+ if (go.GetComponent() == null)
+ go.AddComponent();
+
+ EditorSceneManager.MarkSceneDirty(go.scene);
+ Selection.activeGameObject = go;
+ Debug.Log("[MissionSetup] 완료. Play 후 모든 슬롯을 정답으로 채우면 미션 완료.");
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/MissionSetup.cs.meta b/Assets/Scripts/Editor/MissionSetup.cs.meta
new file mode 100644
index 0000000..bcb5e44
--- /dev/null
+++ b/Assets/Scripts/Editor/MissionSetup.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9a8d41437743f4655996e49253cc72ad
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/MissionManager.cs b/Assets/Scripts/Gameplay/MissionManager.cs
new file mode 100644
index 0000000..bc29012
--- /dev/null
+++ b/Assets/Scripts/Gameplay/MissionManager.cs
@@ -0,0 +1,105 @@
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.InputSystem;
+
+namespace SinoWms.Gameplay
+{
+ ///
+ /// 씬의 모든 적치 슬롯을 감시해 미션 진행/완료를 판정한다.
+ /// 모든 슬롯이 정답으로 채워지면 완료 처리하고 점수(정답·오답·시간)를 표시한다.
+ ///
+ public class MissionManager : MonoBehaviour
+ {
+ [SerializeField] private float targetSeconds = 60f; // 시간 보너스 기준
+ private const int ScorePerSlot = 100;
+ private const int MistakePenalty = 25;
+
+ private PutawaySlot[] _slots;
+ private float _startTime;
+ private int _mistakes;
+ private bool _completed;
+ private int _finalScore;
+ private float _elapsedAtComplete;
+
+ private GUIStyle _hudStyle;
+ private GUIStyle _panelStyle;
+
+ private void Start()
+ {
+ _slots = Object.FindObjectsByType(FindObjectsSortMode.None);
+ _startTime = Time.time;
+ foreach (var s in _slots)
+ s.Changed += OnSlotChanged;
+ }
+
+ private void OnDestroy()
+ {
+ if (_slots == null) return;
+ foreach (var s in _slots)
+ if (s != null) s.Changed -= OnSlotChanged;
+ }
+
+ private void Update()
+ {
+ if (_completed && (Keyboard.current?.rKey.wasPressedThisFrame ?? false))
+ SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
+ }
+
+ private void OnSlotChanged(PutawaySlot slot)
+ {
+ if (_completed) return;
+
+ // 오답 적치는 누적 집계(교육 감점용)
+ if (slot.IsOccupied && !slot.IsCorrect)
+ _mistakes++;
+
+ int correct = 0;
+ foreach (var s in _slots)
+ if (s.IsCorrect) correct++;
+
+ if (correct == _slots.Length)
+ Complete(correct);
+ }
+
+ private void Complete(int correct)
+ {
+ _completed = true;
+ _elapsedAtComplete = Time.time - _startTime;
+
+ int score = correct * ScorePerSlot - _mistakes * MistakePenalty;
+ if (_elapsedAtComplete < targetSeconds)
+ score += Mathf.RoundToInt((1f - _elapsedAtComplete / targetSeconds) * 50f);
+ _finalScore = Mathf.Max(0, score);
+ }
+
+ private int CorrectCount()
+ {
+ int c = 0;
+ foreach (var s in _slots)
+ if (s != null && s.IsCorrect) c++;
+ return c;
+ }
+
+ private void OnGUI()
+ {
+ _hudStyle ??= new GUIStyle(GUI.skin.box) { fontSize = 18, alignment = TextAnchor.MiddleCenter };
+ _panelStyle ??= new GUIStyle(GUI.skin.box) { fontSize = 22, alignment = TextAnchor.MiddleCenter };
+
+ if (_slots == null || _slots.Length == 0) return;
+
+ if (!_completed)
+ {
+ string hud = $"적치 {CorrectCount()}/{_slots.Length} 오답 {_mistakes}";
+ GUI.Label(new Rect(Screen.width / 2f - 130f, 64f, 260f, 32f), hud, _hudStyle);
+ return;
+ }
+
+ float w = 360f, h = 160f;
+ var rect = new Rect(Screen.width / 2f - w / 2f, Screen.height / 2f - h / 2f, w, h);
+ string text =
+ $"미션 완료!\n\n점수 {_finalScore}점\n" +
+ $"소요 {_elapsedAtComplete:0}초 · 오답 {_mistakes}건\n\nR: 다시 하기";
+ GUI.Label(rect, text, _panelStyle);
+ }
+ }
+}
diff --git a/Assets/Scripts/Gameplay/MissionManager.cs.meta b/Assets/Scripts/Gameplay/MissionManager.cs.meta
new file mode 100644
index 0000000..a7d182e
--- /dev/null
+++ b/Assets/Scripts/Gameplay/MissionManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 27f48d56c8f68491ba5b07b4887165cc
\ No newline at end of file
diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs b/Assets/Scripts/Gameplay/PutawaySlot.cs
index da6809c..b526623 100644
--- a/Assets/Scripts/Gameplay/PutawaySlot.cs
+++ b/Assets/Scripts/Gameplay/PutawaySlot.cs
@@ -21,6 +21,9 @@ namespace SinoWms.Gameplay
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 event System.Action Changed;
+
public Carryable Occupant { get; private set; }
public bool IsOccupied => Occupant != null;
public bool IsCorrect =>
@@ -55,6 +58,7 @@ namespace SinoWms.Gameplay
{
if (pad == null) return;
pad.material.color = !IsOccupied ? Empty : (IsCorrect ? Correct : Wrong);
+ Changed?.Invoke(this);
}
}
}