diff --git a/Assets/Scripts/GlobalTimer.cs b/Assets/Scripts/GlobalTimer.cs
new file mode 100644
index 0000000..f575666
--- /dev/null
+++ b/Assets/Scripts/GlobalTimer.cs
@@ -0,0 +1,269 @@
+using System;
+using Unity.Netcode;
+using UnityEngine;
+
+namespace Northbound
+{
+ ///
+ /// 전역 타이머 - 주기적으로 반복되며 모든 클라이언트에 동기화
+ ///
+ public class GlobalTimer : NetworkBehaviour
+ {
+ public static GlobalTimer Instance { get; private set; }
+
+ [Header("Timer Settings")]
+ public float cycleLength = 60f; // 한 주기 길이 (초)
+ public bool autoStart = true;
+ public bool pauseOnZero = false; // 0에 도달하면 일시정지
+
+ [Header("Debug")]
+ public bool showDebugLogs = false;
+
+ // 현재 타이머 값 (초)
+ private NetworkVariable _currentTime = new NetworkVariable(
+ 0f,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // 타이머 실행 중 여부
+ private NetworkVariable _isRunning = new NetworkVariable(
+ false,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // 현재 사이클 번호
+ private NetworkVariable _cycleCount = new NetworkVariable(
+ 0,
+ NetworkVariableReadPermission.Everyone,
+ NetworkVariableWritePermission.Server
+ );
+
+ // 이벤트
+ public event Action OnTimerTick; // 매 프레임 업데이트
+ public event Action OnCycleComplete; // 사이클 완료
+ public event Action OnCycleStart; // 새 사이클 시작
+ public event Action OnHalfwayPoint; // 사이클 중간 지점
+
+ private bool _hasReachedHalfway;
+
+ private void Awake()
+ {
+ if (Instance != null && Instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+ Instance = this;
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ base.OnNetworkSpawn();
+
+ if (IsServer && autoStart)
+ {
+ StartTimer();
+ }
+
+ // 클라이언트도 이벤트 수신을 위해 변경 감지
+ _currentTime.OnValueChanged += OnCurrentTimeChanged;
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ _currentTime.OnValueChanged -= OnCurrentTimeChanged;
+ }
+
+ private void Update()
+ {
+ if (!IsServer || !_isRunning.Value)
+ return;
+
+ _currentTime.Value -= Time.deltaTime;
+
+ // 중간 지점 체크
+ if (!_hasReachedHalfway && _currentTime.Value <= cycleLength / 2f)
+ {
+ _hasReachedHalfway = true;
+ OnHalfwayPoint?.Invoke(cycleLength / 2f);
+ NotifyHalfwayClientRpc();
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 사이클 중간 지점 도달");
+ }
+
+ // 사이클 완료
+ if (_currentTime.Value <= 0f)
+ {
+ CompleteCycle();
+ }
+
+ OnTimerTick?.Invoke(_currentTime.Value);
+ }
+
+ private void CompleteCycle()
+ {
+ OnCycleComplete?.Invoke();
+ NotifyCycleCompleteClientRpc();
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 사이클 {_cycleCount.Value} 완료");
+
+ if (pauseOnZero)
+ {
+ _isRunning.Value = false;
+ _currentTime.Value = 0f;
+ }
+ else
+ {
+ // 다음 사이클 시작
+ _currentTime.Value = cycleLength;
+ _cycleCount.Value++;
+ _hasReachedHalfway = false;
+
+ OnCycleStart?.Invoke(_cycleCount.Value);
+ NotifyCycleStartClientRpc(_cycleCount.Value);
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 사이클 {_cycleCount.Value} 시작");
+ }
+ }
+
+ private void OnCurrentTimeChanged(float previousValue, float newValue)
+ {
+ // 클라이언트에서도 Tick 이벤트 발생
+ if (!IsServer)
+ {
+ OnTimerTick?.Invoke(newValue);
+ }
+ }
+
+ #region Public API
+
+ ///
+ /// 타이머 시작 (서버만)
+ ///
+ public void StartTimer()
+ {
+ if (!IsServer) return;
+
+ _currentTime.Value = cycleLength;
+ _isRunning.Value = true;
+ _cycleCount.Value = 1;
+ _hasReachedHalfway = false;
+
+ OnCycleStart?.Invoke(_cycleCount.Value);
+ NotifyCycleStartClientRpc(_cycleCount.Value);
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 타이머 시작: {cycleLength}초");
+ }
+
+ ///
+ /// 타이머 일시정지 (서버만)
+ ///
+ public void PauseTimer()
+ {
+ if (!IsServer) return;
+
+ _isRunning.Value = false;
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 타이머 일시정지");
+ }
+
+ ///
+ /// 타이머 재개 (서버만)
+ ///
+ public void ResumeTimer()
+ {
+ if (!IsServer) return;
+
+ _isRunning.Value = true;
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 타이머 재개");
+ }
+
+ ///
+ /// 타이머 리셋 (서버만)
+ ///
+ public void ResetTimer()
+ {
+ if (!IsServer) return;
+
+ _currentTime.Value = cycleLength;
+ _cycleCount.Value = 0;
+ _isRunning.Value = false;
+ _hasReachedHalfway = false;
+
+ if (showDebugLogs)
+ Debug.Log($"[GlobalTimer] 타이머 리셋");
+ }
+
+ ///
+ /// 현재 남은 시간 (초)
+ ///
+ public float GetCurrentTime() => _currentTime.Value;
+
+ ///
+ /// 현재 남은 시간 (분:초 형식)
+ ///
+ public string GetFormattedTime()
+ {
+ int minutes = Mathf.FloorToInt(_currentTime.Value / 60f);
+ int seconds = Mathf.FloorToInt(_currentTime.Value % 60f);
+ return $"{minutes:00}:{seconds:00}";
+ }
+
+ ///
+ /// 진행률 (0.0 ~ 1.0)
+ ///
+ public float GetProgress() => 1f - (_currentTime.Value / cycleLength);
+
+ ///
+ /// 현재 사이클 번호
+ ///
+ public int GetCycleCount() => _cycleCount.Value;
+
+ ///
+ /// 타이머 실행 중 여부
+ ///
+ public bool IsRunning() => _isRunning.Value;
+
+ #endregion
+
+ #region Client RPCs
+
+ [ClientRpc]
+ private void NotifyCycleCompleteClientRpc()
+ {
+ if (!IsServer)
+ {
+ OnCycleComplete?.Invoke();
+ }
+ }
+
+ [ClientRpc]
+ private void NotifyCycleStartClientRpc(int cycleNumber)
+ {
+ if (!IsServer)
+ {
+ OnCycleStart?.Invoke(cycleNumber);
+ }
+ }
+
+ [ClientRpc]
+ private void NotifyHalfwayClientRpc()
+ {
+ if (!IsServer)
+ {
+ OnHalfwayPoint?.Invoke(cycleLength / 2f);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/GlobalTimer.cs.meta b/Assets/Scripts/GlobalTimer.cs.meta
new file mode 100644
index 0000000..b1ee10d
--- /dev/null
+++ b/Assets/Scripts/GlobalTimer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b643f7c446ade3f448e57e7501ac5a67
\ No newline at end of file