// ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ ๋ฐฑํ’ˆํƒ€ ์นœ๊ตฌ ํ™”๋ฉด (UI ์ „์šฉ). "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ"๊ณผ ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ๋ฃฉ์„ ๋”ฐ๋ฅธ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/friend_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/friend_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class FriendsScreen extends StatefulWidget { final String studentId; final String studentName; const FriendsScreen({ super.key, required this.studentId, required this.studentName, }); @override State createState() => _FriendsScreenState(); } class _FriendsScreenState extends State { late final FriendController _controller; @override void initState() { super.initState(); _controller = FriendController( studentId: widget.studentId, studentName: widget.studentName, ); _controller.init(); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _showAddFriendDialog() async { final idController = TextEditingController(); final result = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('์นœ๊ตฌ ์ถ”๊ฐ€'), content: TextField( controller: idController, keyboardType: TextInputType.number, decoration: const InputDecoration( labelText: '์นœ๊ตฌ ํ•™๋ฒˆ', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('์ทจ์†Œ', style: TextStyle(color: Colors.grey)), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, ), onPressed: () => Navigator.pop(dialogContext, true), child: const Text('์‹ ์ฒญํ•˜๊ธฐ'), ), ], ), ); if (result != true || idController.text.trim().isEmpty) return; final (success, message) = await _controller.sendRequest( idController.text.trim(), ); if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); } Future _respond(int requestId, bool accept) async { final (_, message) = await _controller.respondRequest(requestId, accept); if (!mounted) return; AppNotice.show(context, message); } Future _remove(String friendId) async { final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('์นœ๊ตฌ ์‚ญ์ œ'), content: const Text('์ด ์นœ๊ตฌ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('์ทจ์†Œ', style: TextStyle(color: Colors.grey)), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), child: const Text('์‚ญ์ œ', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed != true) return; final (_, message) = await _controller.removeFriend(friendId); if (!mounted) return; AppNotice.show(context, message); } Widget _sectionHeader(String title, {int? count}) { return Row( children: [ Container( width: 4, height: 16, decoration: BoxDecoration( color: AppPalette.ink, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppPalette.ink, ), ), if (count != null) ...[ const SizedBox(width: 6), Text( '$count๋ช…', style: TextStyle(fontSize: 13, color: Colors.grey[500]), ), ], ], ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, ), floatingActionButton: FloatingActionButton.extended( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, onPressed: _showAddFriendDialog, icon: const Icon(Icons.person_add_rounded), label: const Text('์นœ๊ตฌ ์ถ”๊ฐ€'), ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.refresh, child: ListView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), children: [ const Center(child: TitlePill('์นœ๊ตฌ')), const SizedBox(height: 20), if (_controller.requests.isNotEmpty) ...[ _sectionHeader( '๋ฐ›์€ ์นœ๊ตฌ ์‹ ์ฒญ', count: _controller.requests.length, ), const SizedBox(height: 10), for (final r in _controller.requests) Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Row( children: [ Expanded( child: Text( '${r['name']} (${r['studentId']})', style: const TextStyle( fontWeight: FontWeight.bold, ), ), ), TextButton( onPressed: () => _respond(r['requestId'] as int, false), child: const Text( '๊ฑฐ์ ˆ', style: TextStyle(color: Colors.grey), ), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, ), onPressed: () => _respond(r['requestId'] as int, true), child: const Text('์ˆ˜๋ฝ'), ), ], ), ), const SizedBox(height: 20), ], _sectionHeader('๋‚ด ์นœ๊ตฌ', count: _controller.friends.length), const SizedBox(height: 10), if (_controller.friends.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Text( '์•„์ง ์นœ๊ตฌ๊ฐ€ ์—†์–ด์š”. ํ•™๋ฒˆ์œผ๋กœ ์นœ๊ตฌ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ณด์„ธ์š”.', style: TextStyle(color: Colors.grey), ), ) else for (final f in _controller.friends) Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppPalette.ink.withValues( alpha: 0.06, ), shape: BoxShape.circle, ), child: const Icon( Icons.person_rounded, size: 16, color: AppPalette.ink, ), ), const SizedBox(width: 10), Expanded( child: Text( '${f['name']} (${f['studentId']})', style: const TextStyle( fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon( Icons.person_remove_outlined, color: Colors.grey, ), onPressed: () => _remove(f['studentId'] as String), ), ], ), ), ], ), ), ); }, ); } }