This commit is contained in:
2025-11-24 17:53:00 +09:00
parent e992a5ca5e
commit dde81cab65
34 changed files with 3718 additions and 469 deletions
@@ -0,0 +1,70 @@
import 'dart:async';
import '../network/network_manager.dart';
import '../model/play_packet.dart';
class ChatMessage {
final String senderName;
final String text;
final bool isMe;
final DateTime timestamp;
ChatMessage(this.senderName, this.text, this.isMe) : timestamp = DateTime.now();
}
class GlobalChatManager {
static final GlobalChatManager _instance = GlobalChatManager._internal();
factory GlobalChatManager() => _instance;
GlobalChatManager._internal();
// UI가 구독할 스트림
final _messageController = StreamController<List<ChatMessage>>.broadcast();
Stream<List<ChatMessage>> get messageStream => _messageController.stream;
final List<ChatMessage> _messages = [];
/// [NetworkManager]로부터 패킷을 전달받음
void onPacketReceived(PlayPacket packet) {
if (packet.type != PacketType.chat) return;
final data = packet.payload as Map<String, dynamic>;
final senderName = data['senderName'];
final text = data['text'];
final isMe = packet.senderId == NetworkManager().me.id;
final chatMsg = ChatMessage(senderName, text, isMe);
_messages.add(chatMsg);
// UI 갱신
_messageController.add(List.from(_messages));
}
/// 메시지 전송
void sendMessage(String text) {
if (text.trim().isEmpty) return;
final myInfo = NetworkManager().me;
// 1. 내 화면에 즉시 추가 (나한테는 네트워크로 안 돌아오므로)
final myMsg = ChatMessage(myInfo.nickname, text, true);
_messages.add(myMsg);
_messageController.add(List.from(_messages));
// 2. 네트워크 전송 (PlayPacket 포장)
final packet = PlayPacket(
type: PacketType.chat,
senderId: myInfo.id,
payload: {
'senderName': myInfo.nickname,
'text': text,
},
timestamp: DateTime.now().millisecondsSinceEpoch,
);
NetworkManager().sendPacket(packet);
}
void clearMessages() {
_messages.clear();
_messageController.add([]);
}
}
@@ -0,0 +1,149 @@
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:drift/drift.dart' as drift;
import 'package:path_provider/path_provider.dart';
import 'package:uuid/uuid.dart';
import '../database/ephemeral_database.dart';
import '../network/network_manager.dart';
import '../model/play_packet.dart';
class MediaManager {
static final MediaManager _instance = MediaManager._internal();
factory MediaManager() => _instance;
MediaManager._internal();
EphemeralDatabase? _db;
String? _currentRoomId;
// UI에서 갤러리 변경을 감지하기 위한 스트림 (DB 변경 시 자동 발동)
Stream<List<MediaItem>> get galleryStream {
if (_db == null) return const Stream.empty();
return _db!.select(_db!.mediaItems).watch(); // watch()는 데이터 변경 시 자동 업데이트됨
}
// ---------------------------------------------------------------------------
// [1] 초기화 및 정리 (Lifecycle)
// ---------------------------------------------------------------------------
/// 방 생성/입장 시 호출 (DB 생성)
Future<void> initialize(String roomId) async {
// 기존 DB가 열려있다면 정리
await cleanup();
_currentRoomId = roomId;
_db = await EphemeralDatabase.create(roomId);
print("[MediaManager] DB Initialized for Room: $roomId");
}
/// 방 나갈 때 호출 (데이터 파괴)
Future<void> cleanup() async {
if (_db != null) {
await _db!.close();
_db = null;
}
// DB 파일 삭제 (흔적 지우기)
if (_currentRoomId != null) {
try {
final dbFolder = await getApplicationDocumentsDirectory();
// EphemeralDatabase.create에서 만든 파일명과 동일해야 함
final file = File('${dbFolder.path}/room_$_currentRoomId.sqlite');
if (await file.exists()) {
await file.delete();
print("[MediaManager] DB File Deleted 🔥");
}
} catch (e) {
print("[MediaManager] Cleanup Error: $e");
}
_currentRoomId = null;
}
}
// ---------------------------------------------------------------------------
// [2] 미디어 전송 (Send)
// ---------------------------------------------------------------------------
Future<void> sendMedia({
required String filePath,
required String type, // 'IMAGE', 'AUDIO'
}) async {
if (_db == null) return;
final myInfo = NetworkManager().me;
final mediaId = const Uuid().v4();
// A. 내 로컬 DB에 먼저 저장 (내가 보낸 것도 보여야 하니까)
await _db!.insertMedia(MediaItemsCompanion(
id: drift.Value(mediaId),
senderId: drift.Value(myInfo.id),
senderName: drift.Value(myInfo.nickname),
type: drift.Value(type),
filePath: drift.Value(filePath),
createdAt: drift.Value(DateTime.now()),
));
// B. 파일 읽기 및 인코딩 (MVP: Base64)
// 주의: 대용량 동영상은 이 방식으로 보내면 앱 멈춤. (추후 Chunk 방식 개선 필요)
final file = File(filePath);
final fileBytes = await file.readAsBytes();
final base64Data = base64Encode(fileBytes);
final fileName = filePath.split('/').last;
// C. 패킷 전송
final packet = PlayPacket(
type: PacketType.media,
senderId: myInfo.id,
payload: {
'id': mediaId,
'senderName': myInfo.nickname,
'type': type,
'data': base64Data,
'fileName': fileName,
},
timestamp: DateTime.now().millisecondsSinceEpoch,
);
NetworkManager().sendPacket(packet);
print("[MediaManager] Sent media: $fileName");
}
// ---------------------------------------------------------------------------
// [3] 미디어 수신 (Receive)
// ---------------------------------------------------------------------------
Future<void> onMediaReceived(PlayPacket packet) async {
if (_db == null) return;
try {
final data = packet.payload as Map<String, dynamic>;
final String base64Data = data['data'];
final String fileName = data['fileName'];
// A. 임시 폴더에 파일 저장
final tempDir = await getTemporaryDirectory();
// 파일명 충돌 방지를 위해 UUID나 Timestamp 붙여도 됨
final savePath = '${tempDir.path}/${const Uuid().v4()}_$fileName';
final bytes = base64Decode(base64Data);
await File(savePath).writeAsBytes(bytes);
// B. DB에 메타데이터 저장 -> watch() 중인 UI가 자동 업데이트됨
await _db!.insertMedia(MediaItemsCompanion(
id: drift.Value(data['id']),
senderId: drift.Value(packet.senderId),
senderName: drift.Value(data['senderName']),
type: drift.Value(data['type']),
filePath: drift.Value(savePath),
createdAt: drift.Value(DateTime.fromMillisecondsSinceEpoch(packet.timestamp)),
));
print("[MediaManager] File Saved: $savePath");
} catch (e) {
print("[MediaManager] Receive Error: $e");
}
}
}