This commit is contained in:
2025-11-26 18:10:10 +09:00
parent 283f08786e
commit bf40c42c2c
43 changed files with 4454 additions and 971 deletions
+69 -69
View File
@@ -7,11 +7,12 @@ import 'package:bonsoir/bonsoir.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:uuid/uuid.dart';
import '../manager/notification_manager.dart';
import '../model/user_info.dart';
import '../model/play_packet.dart';
import '../manager/global_chat_manager.dart';
import '../manager/media_manager.dart';
import '../manager/notification_manager.dart';
enum NetworkRole { none, host, guest }
@@ -26,9 +27,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
// ------------------------------------------------------------------------
// 상수 설정
// ------------------------------------------------------------------------
// [핵심] 패킷 구분자 (End Of Packet) - Base64와 겹치지 않는 고유 문자열
static const String PACKET_DELIMITER = "|||EOP|||";
static const int HEARTBEAT_INTERVAL_SEC = 3;
static const int TIMEOUT_SEC = 10;
static const int RECONNECT_WAIT_SEC = 5;
@@ -47,8 +46,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
final Map<Socket, UserInfo?> _connectedGuests = {};
final List<UserInfo> guestList = [];
// [버퍼] 소켓별로 들어오다 만 데이터를 저장
final Map<Socket, String> _packetBuffers = {};
BonsoirService? _bonsoirService;
@@ -66,21 +63,21 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
DateTime? _lastPongTime;
bool _isReconnecting = false;
// [추가] 현재 선택된 게임 ID 및 설정
String selectedGameId = 'quiz_mix';
Map<String, dynamic> selectedGameConfig = {}; // 난이도 등 저장
// ------------------------------------------------------------------------
// 초기화
// ------------------------------------------------------------------------
void initialize({
required String nickname,
String? profileImage, // [추가] 선택적 파라미터
}) {
void initialize({required String nickname, String? profileImage}) {
final uuid = const Uuid().v4().substring(0, 8);
final randomColor = 0xFF000000 | (nickname.hashCode & 0xFFFFFF);
me = UserInfo(
id: uuid,
nickname: nickname,
colorValue: randomColor,
profileImageBase64: profileImage, // [적용]
profileImageBase64: profileImage
);
_log("초기화 완료: ${me.nickname}");
}
@@ -103,6 +100,14 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
// ------------------------------------------------------------------------
// 레디 시스템
// ------------------------------------------------------------------------
// [수정] 게임 선택 함수 (Config 추가)
void selectGame(String gameId, {Map<String, dynamic>? config}) {
selectedGameId = gameId;
selectedGameConfig = config ?? {};
notifyListeners();
}
void toggleReady() {
me = me.copyWith(isReady: !me.isReady);
notifyListeners();
@@ -128,7 +133,12 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
if (allGuestsReady) {
_log("🚀 전원 준비 완료! 3초 후 게임 시작...");
Future.delayed(const Duration(seconds: 1), () {
final startPayload = {'type': 'GAME_START', 'gameId': 'quiz_ox'};
// [수정] 게임 시작 패킷에 Config 포함
final startPayload = {
'type': 'GAME_START',
'gameId': selectedGameId,
'config': selectedGameConfig
};
sendMessage(startPayload);
_messageController.add(startPayload);
_resetAllReadyState();
@@ -174,7 +184,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
await _bonsoirBroadcast!.start();
await MediaManager().initialize(roomName);
_startHeartbeat();
notifyListeners();
@@ -187,8 +196,12 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
void _handleNewGuest(Socket client) {
_log("🎉 연결됨: ${client.remoteAddress.address}");
_connectedGuests[client] = null;
_packetBuffers[client] = ""; // 버퍼 초기화
_packetBuffers[client] = "";
final myHandshake = {'type': 'HANDSHAKE', 'payload': me.toJson()};
final jsonString = jsonEncode(myHandshake);
client.add(utf8.encode('$jsonString$PACKET_DELIMITER'));
client.listen(
(Uint8List data) => _onDataReceived(client, data),
onError: (e) => _removeGuest(client),
@@ -243,6 +256,32 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
return controller.stream;
}
// [수정] 싱글 모드 시작 시 Config 추가
Future<void> startSoloMode(String gameId, {Map<String, dynamic>? config}) async {
stopNetwork(force: true);
role = NetworkRole.host;
hostIp = "Solo Mode";
hostPort = 0;
selectedGameId = gameId;
selectedGameConfig = config ?? {}; // Config 저장
await MediaManager().initialize("solo_session");
_log("👤 싱글 플레이 모드 시작: $gameId");
notifyListeners();
// 바로 시작 신호
Future.delayed(const Duration(milliseconds: 100), () {
_messageController.add({
'type': 'GAME_START',
'gameId': gameId,
'config': selectedGameConfig
});
});
}
Future<void> joinRoom(String ip, int port) async {
if (role != NetworkRole.guest) stopNetwork(force: true);
role = NetworkRole.guest;
@@ -254,7 +293,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
_clientSocket = await Socket.connect(ip, port, timeout: const Duration(seconds: 5));
_log("✅ 접속 성공!");
_packetBuffers[_clientSocket!] = ""; // 내 버퍼 초기화
_packetBuffers[_clientSocket!] = "";
sendMessage({'type': 'HANDSHAKE', 'payload': me.toJson()});
@@ -279,7 +318,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
}
// ------------------------------------------------------------------------
// 데이터 송수신 (버퍼링 및 구분자 로직 강화)
// 데이터 송수신
// ------------------------------------------------------------------------
void sendPacket(PlayPacket packet) {
sendMessage(packet.toJson());
@@ -289,8 +328,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
if (role == NetworkRole.guest && _clientSocket == null) return;
final jsonString = jsonEncode(messageMap);
// 로그 필터링
if (messageMap['type'] != 'PING' && messageMap['type'] != 'PONG') {
if (messageMap['type'] == 'chat') {
_log("📤 전송: [CHAT]");
@@ -301,7 +338,6 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
}
}
// [핵심] 메시지 뒤에 고유 구분자를 붙여서 전송
final fullMessage = '$jsonString$PACKET_DELIMITER';
final List<int> data = utf8.encode(fullMessage);
@@ -316,30 +352,17 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
void _onDataReceived(Socket socket, Uint8List data) {
try {
// 1. 기존 버퍼 가져오기
String buffer = _packetBuffers[socket] ?? "";
// 2. 새 데이터 추가
buffer += utf8.decode(data, allowMalformed: true);
// 3. 구분자(PACKET_DELIMITER)를 기준으로 메시지 추출
while (buffer.contains(PACKET_DELIMITER)) {
final int delimiterIndex = buffer.indexOf(PACKET_DELIMITER);
// 완성된 메시지 하나 추출
final String msg = buffer.substring(0, delimiterIndex);
// 버퍼에서 추출한 부분과 구분자 제거
buffer = buffer.substring(delimiterIndex + PACKET_DELIMITER.length);
// 메시지 처리
if (msg.trim().isNotEmpty) {
_processMessage(socket, msg);
}
if (msg.trim().isNotEmpty) _processMessage(socket, msg);
}
// 4. 남은 찌꺼기(다음 패킷의 일부)를 다시 버퍼에 저장
_packetBuffers[socket] = buffer;
} catch (e) {
_log("데이터 수신 에러: $e");
}
@@ -361,7 +384,11 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
if (jsonMap['type'] == 'HANDSHAKE') {
final guestInfo = UserInfo.fromJson(jsonMap['payload']);
_connectedGuests[socket] = guestInfo;
if (role == NetworkRole.host) {
_connectedGuests[socket] = guestInfo;
}
guestList.removeWhere((u) => u.id == guestInfo.id);
guestList.add(guestInfo);
notifyListeners();
@@ -386,6 +413,13 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
}
if (jsonMap['type'] == 'GAME_START') {
if (jsonMap['gameId'] != null) {
selectedGameId = jsonMap['gameId'];
}
// [추가] Config 동기화
if (jsonMap['config'] != null) {
selectedGameConfig = jsonMap['config'];
}
_resetAllReadyState();
_messageController.add(jsonMap);
return;
@@ -410,52 +444,23 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
}
}
// ------------------------------------------------------------------------
// 연결 관리
// ------------------------------------------------------------------------
void _handleConnectionLost(dynamic reason) {
if (role != NetworkRole.guest) return;
_log("⚠️ 연결 끊김: $reason");
_clientSocket?.destroy();
_clientSocket = null;
if (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) return;
// 5초 카운트다운 시작
_disconnectWaitTimer = Timer(const Duration(seconds: RECONNECT_WAIT_SEC), () {
_log("💀 복구 실패. 종료.");
// [추가] 재접속 실패 알림 발송
_sendDisconnectionNotification();
stopNetwork(force: true);
});
_attemptReconnection();
}
// [신규] 연결 끊김 알림 메서드
void _sendDisconnectionNotification() {
// 앱이 현재 화면에 떠있지 않을 때만(백그라운드) 알림
final state = WidgetsBinding.instance.lifecycleState;
if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) {
NotificationManager().showNotification(
id: 9999, // 고정 ID (시스템 알림용)
title: "연결 끊김 ⚠️",
body: "방과의 연결이 종료되었습니다. 앱을 실행해 확인해주세요.",
);
}
}
Future<void> _attemptReconnection() async {
if (hostIp == null || hostPort == null) return;
_isReconnecting = true;
while (_disconnectWaitTimer != null && _disconnectWaitTimer!.isActive) {
try {
await joinRoom(hostIp!, hostPort!);
_log("✅ 재접속 성공!");
_isReconnecting = false;
return;
} catch (e) {
@@ -499,10 +504,7 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
void stopNetwork({bool force = false}) {
if (!force && _disconnectWaitTimer != null) return;
_log("🛑 종료");
MediaManager().cleanup();
_heartbeatTimer?.cancel();
_disconnectWaitTimer?.cancel();
_disconnectWaitTimer = null;
@@ -510,12 +512,10 @@ class NetworkManager extends ChangeNotifier with WidgetsBindingObserver {
_bonsoirDiscovery?.stop();
_serverSocket?.close();
_clientSocket?.close();
for (var s in _connectedGuests.keys) s.close();
_connectedGuests.clear();
_packetBuffers.clear(); // 버퍼 정리
_packetBuffers.clear();
guestList.clear();
role = NetworkRole.none;
_serverSocket = null;
_clientSocket = null;