// ⏱️ 우리 학교 전용 "백품타" 공부 타이머 화면의 기능(서버 통신/상태) 담당 컨트롤러. import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../config.dart'; enum StudyTimerState { idle, running, paused } class StudyTimerController extends ChangeNotifier { final String studentId; final String studentName; StudyTimerController({required this.studentId, required this.studentName}); bool _isLoading = true; bool _isBusy = false; // 시작/일시정지/재개/종료 버튼 눌러서 서버 응답 기다리는 중 StudyTimerState _state = StudyTimerState.idle; int _elapsedSeconds = 0; // 지금 세션에서 "정지 상태"일 때 확정된 누적 초 (일시정지 구간 제외) DateTime? _lastResumedAt; // running일 때만 의미 있음 - 여기서부터 실시간으로 흐른다 int _todaySeconds = 0; int _weekSeconds = 0; int _totalSeconds = 0; Timer? _ticker; bool _disposed = false; bool get isLoading => _isLoading; bool get isBusy => _isBusy; bool get isRunning => _state == StudyTimerState.running; bool get isPaused => _state == StudyTimerState.paused; bool get isIdle => _state == StudyTimerState.idle; /// 지금 세션 하나만의 경과 시간(타이머 화면 큰 숫자용). 일시정지 중엔 멈춰 있다. int get liveElapsedSeconds { if (_state != StudyTimerState.running || _lastResumedAt == null) { return _elapsedSeconds; } final diff = DateTime.now().difference(_lastResumedAt!).inSeconds; return _elapsedSeconds + (diff > 0 ? diff : 0); } /// 지금 진행 중인 세션까지 포함한 "오늘" 누적 초. running일 때만 실시간으로 늘어난다. int get todaySecondsLive => _todaySeconds + _liveDelta; int get weekSecondsLive => _weekSeconds + _liveDelta; int get totalSecondsLive => _totalSeconds + _liveDelta; int get _liveDelta => _state == StudyTimerState.idle ? 0 : liveElapsedSeconds; void _safeNotify() { if (!_disposed) notifyListeners(); } Future init() async { await _fetchStatus(); } @override void dispose() { _disposed = true; _ticker?.cancel(); super.dispose(); } Future _fetchStatus() async { try { final response = await http.get( Uri.parse('$baseUrl/api/study/status?studentId=$studentId'), ); if (response.statusCode == 200) { final data = jsonDecode(utf8.decode(response.bodyBytes)); _state = _parseState(data['state']); _elapsedSeconds = (data['elapsedSeconds'] as num?)?.toInt() ?? 0; _lastResumedAt = data['lastResumedAt'] != null ? DateTime.tryParse(data['lastResumedAt']) : null; _todaySeconds = (data['todaySeconds'] as num?)?.toInt() ?? 0; _weekSeconds = (data['weekSeconds'] as num?)?.toInt() ?? 0; _totalSeconds = (data['totalSeconds'] as num?)?.toInt() ?? 0; _restartTickerIfNeeded(); } } catch (_) { // 조용히 무시 - 마지막으로 알던 상태를 유지한다. } finally { _isLoading = false; _safeNotify(); } } StudyTimerState _parseState(dynamic raw) { switch (raw) { case 'running': return StudyTimerState.running; case 'paused': return StudyTimerState.paused; default: return StudyTimerState.idle; } } void _restartTickerIfNeeded() { _ticker?.cancel(); if (_state == StudyTimerState.running) { _ticker = Timer.periodic( const Duration(seconds: 1), (_) => _safeNotify(), ); } } Future<(bool success, String message)> start() async { return _action( '/api/study/start', body: {"studentId": studentId, "studentName": studentName}, onSuccess: (_) async { await _fetchStatus(); return '공부를 시작했습니다.'; }, ); } Future<(bool success, String message)> pause() async { return _action( '/api/study/pause', onSuccess: (_) async { await _fetchStatus(); return '일시정지했습니다.'; }, ); } Future<(bool success, String message)> resume() async { return _action( '/api/study/resume', onSuccess: (_) async { await _fetchStatus(); return '다시 시작했습니다.'; }, ); } Future<(bool success, String message)> stop() async { return _action( '/api/study/stop', onSuccess: (result) async { await _fetchStatus(); // 오늘/이번주/전체 합계를 서버 값으로 다시 맞춘다. final minutes = ((result['durationSeconds'] as num?)?.toInt() ?? 0) ~/ 60; return '이번 공부 시간: $minutes분 기록 완료!'; }, ); } Future<(bool success, String message)> _action( String path, { Map body = const {}, required Future Function(Map result) onSuccess, }) async { _isBusy = true; _safeNotify(); try { final response = await http.post( Uri.parse('$baseUrl$path'), headers: {"Content-Type": "application/json"}, body: jsonEncode({"studentId": studentId, ...body}), ); final result = jsonDecode(utf8.decode(response.bodyBytes)); if (response.statusCode == 200 && result['status'] == 'success') { final message = await onSuccess(result); return (true, message); } return (false, '${result['message'] ?? '처리 실패'}'); } catch (e) { return (false, '네트워크 에러: $e'); } finally { _isBusy = false; _safeNotify(); } } }