import 'package:bonsoir/bonsoir.dart'; import 'package:flutter/material.dart'; import 'package:playwith_core/playwith_core.dart'; class LobbyScreen extends StatefulWidget { const LobbyScreen({super.key}); @override State createState() => _LobbyScreenState(); } class _LobbyScreenState extends State { final _net = NetworkManager(); // Singleton 인스턴스 @override Widget build(BuildContext context) { // NetworkManager의 상태(notifyListeners)가 변경될 때마다 화면 다시 그림 return ListenableBuilder( listenable: _net, builder: (context, child) { return Scaffold( appBar: AppBar( title: Text('안녕하세요, ${_net.me.nickname}님'), actions: [ if (_net.role != NetworkRole.none) IconButton( icon: const Icon(Icons.close), onPressed: () => _net.stopNetwork(), // 연결 끊기 ) ], ), body: _buildBody(), ); }, ); } Widget _buildBody() { // 1. 아무 역할도 없을 때 -> 선택 화면 if (_net.role == NetworkRole.none) { return Center( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _BigButton( title: "방 만들기\n(Host)", color: Colors.blue[100]!, onTap: () => _net.startHosting("${_net.me.nickname}의 방"), ), _BigButton( title: "방 찾기\n(Guest)", color: Colors.green[100]!, onTap: () => _showRoomListDialog(), ), ], ), ); } // 2. Host 상태일 때 -> 대기실 화면 if (_net.role == NetworkRole.host) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("👑 방장입니다", style: TextStyle(fontSize: 24)), const SizedBox(height: 20), const CircularProgressIndicator(), const SizedBox(height: 20), const Text("참가자를 기다리는 중..."), // TODO: 여기에 접속한 게스트 목록 표시 예정 ], ), ); } // 3. Guest 상태일 때 -> 대기실 화면 if (_net.role == NetworkRole.guest) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text("✅ 접속 완료!", style: TextStyle(fontSize: 24)), SizedBox(height: 20), Text("방장이 게임을 시작하기를 기다리세요."), ], ), ); } return const SizedBox(); } // [Guest용] 방 목록 팝업 void _showRoomListDialog() { showDialog( context: context, builder: (context) { return AlertDialog( title: const Text("방 찾는 중..."), content: SizedBox( width: double.maxFinite, height: 300, child: StreamBuilder>( stream: _net.discoverRooms(), // Core의 방 찾기 스트림 builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text("발견된 방이 없습니다.\n(같은 와이파이인지 확인하세요)")); } final services = snapshot.data!; return ListView.builder( itemCount: services.length, itemBuilder: (context, index) { final service = services[index]; // 이름 포맷: "방이름#ID" -> "방이름"만 파싱 final displayName = service.name.split('#').first; return ListTile( leading: const Icon(Icons.meeting_room), title: Text(displayName), subtitle: Text(service.host ?? "IP 정보 없음"), onTap: () async { Navigator.pop(context); // 다이얼로그 닫기 // 해당 방으로 접속 시도 (IP는 service.attributes나 resolve 과정 필요) // Bonsoir는 service.host에 호스트네임이 들어오므로 resolve 필요 // MVP 단계에서는 간단히: await _resolveAndJoin(service); }, ); }, ); }, ), ), ); }, ); } // Bonsoir Service Resolve (IP 주소 알아내기) Future _resolveAndJoin(BonsoirService service) async { // 실제로는 service.resolve() 호출 후 IP 획득 과정을 거쳐야 함. // Bonsoir 패키지 특성상 resolve가 비동기로 돔. // MVP 간소화를 위해 service에 host 정보가 있다고 가정하거나 // Broadcast 시점에 attributes에 IP를 넣는 방식을 추천하지만, // 일단 resolve 시도: if (service is BonsoirBroadcast) { // 이미 broadcast 객체라면 바로 정보가 있음 (내가 만든 방) } else { await service.resolve(service.resolveRealService); } // IP가 ipv4 형태인지 확인 필요. 보통 service.ip 나 attributes 사용 // 여기선 port만 확실하므로, 실제 IP 획득은 Bonsoir 예제 참고 필요 // (테스트 환경에서는 보통 service.attributes에 {'ip': '192.168...'} 넣어서 보냄) // *중요*: 실제 구현 시 NetworkManager.startHosting에서 attributes에 IP를 넣어주는게 가장 확실함. // 일단 현재 코드는 로직 흐름만 잡음. // _net.joinRoom('192.168.0.xxx', service.port); } } class _BigButton extends StatelessWidget { final String title; final Color color; final VoidCallback onTap; const _BigButton({required this.title, required this.color, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( width: 150, height: 150, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(20)), child: Center(child: Text(title, textAlign: TextAlign.center, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold))), ), ); } }