// ๐Ÿ“ ์„ ์ƒ๋‹˜ ํ˜ธ์ถœ ํ™”๋ฉด(ํ•™์ƒ์šฉ) (UI ์ „์šฉ). ์„ ์ƒ๋‹˜์„ ๊ณจ๋ผ ๋ฐฉ๋ฌธ ๋ชฉ์ ๊ณผ ํ•จ๊ป˜ ํ˜ธ์ถœํ•œ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/teacher_call_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../data/teacher_call_schedule.dart'; import '../function/teacher_call_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class TeacherCallScreen extends StatefulWidget { final String studentId; final String studentName; const TeacherCallScreen({ super.key, required this.studentId, required this.studentName, }); @override State createState() => _TeacherCallScreenState(); } class _TeacherCallScreenState extends State { late final TeacherCallController _controller; String? _selectedTeacherId; String? _selectedTeacherName; String _selectedPurpose = kCallPurposes.first; final _customPurposeController = TextEditingController(); bool _useCustomPurpose = false; @override void initState() { super.initState(); _controller = TeacherCallController( studentId: widget.studentId, studentName: widget.studentName, ); _controller.init(); } @override void dispose() { _controller.dispose(); _customPurposeController.dispose(); super.dispose(); } Future _call() async { if (_selectedTeacherId == null) { AppNotice.show(context, '์„ ์ƒ๋‹˜์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.'); return; } final purpose = _useCustomPurpose ? _customPurposeController.text.trim() : _selectedPurpose; if (purpose.isEmpty) { AppNotice.show(context, '๋ฐฉ๋ฌธ ๋ชฉ์ ์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.'); return; } final (success, message) = await _controller.callTeacher( teacherId: _selectedTeacherId!, purpose: purpose, ); if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); } @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, centerTitle: true, title: const TitlePill('์„ ์ƒ๋‹˜ ํ˜ธ์ถœ'), ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.refresh, child: ListView( padding: const EdgeInsets.all(16), children: [ if (_controller.ranking.isNotEmpty) _buildRanking(), const SizedBox(height: 16), const Text( '๋ฐฉ๋ฌธ ๋ชฉ์ ', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), _buildPurposeChips(), if (_useCustomPurpose) ...[ const SizedBox(height: 8), TextField( controller: _customPurposeController, maxLength: 40, decoration: InputDecoration( hintText: '์šฉ๋ฌด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”', filled: true, fillColor: AppPalette.paper, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: AppPalette.sage), ), ), ), ], const SizedBox(height: 16), const Text( '์„ ์ƒ๋‹˜ ์„ ํƒ', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), _buildTeacherGrid(), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: _selectedTeacherId == null ? Colors.grey : AppPalette.ink, foregroundColor: AppPalette.paper, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), ), onPressed: _controller.isCalling ? null : _call, child: _controller.isCalling ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( _selectedTeacherName == null ? '์„ ์ƒ๋‹˜์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”' : '$_selectedTeacherName ์„ ์ƒ๋‹˜ ํ˜ธ์ถœํ•˜๊ธฐ', style: const TextStyle( fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 24), const Text( '๋‚ด ํ˜ธ์ถœ ๊ธฐ๋ก', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), _buildMyCalls(), ], ), ), ); }, ); } Widget _buildRanking() { final medals = ['๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰']; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '์˜ค๋Š˜ ์ธ๊ธฐ ์„ ์ƒ๋‹˜', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for (var i = 0; i < _controller.ranking.length; i++) Column( children: [ Text(medals[i], style: const TextStyle(fontSize: 22)), Text( _controller.ranking[i]['teacherName'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold), ), Text( '${_controller.ranking[i]['count']}ํšŒ', style: const TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ], ), ], ), ); } Widget _buildPurposeChips() { return Wrap( spacing: 8, runSpacing: 8, children: [ for (final p in kCallPurposes) ChoiceChip( label: Text(p), selected: !_useCustomPurpose && _selectedPurpose == p, onSelected: (_) => setState(() { _useCustomPurpose = false; _selectedPurpose = p; }), selectedColor: AppPalette.ink, labelStyle: TextStyle( color: !_useCustomPurpose && _selectedPurpose == p ? Colors.white : AppPalette.ink, ), ), ChoiceChip( label: const Text('์ง์ ‘ ์ž…๋ ฅ'), selected: _useCustomPurpose, onSelected: (_) => setState(() => _useCustomPurpose = true), selectedColor: AppPalette.ink, labelStyle: TextStyle( color: _useCustomPurpose ? Colors.white : AppPalette.ink, ), ), ], ); } Widget _buildTeacherGrid() { return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: _controller.teachers.length, itemBuilder: (context, index) { final t = _controller.teachers[index]; final name = t['name'] ?? ''; final location = t['location'] ?? '๊ต๋ฌด์‹ค'; final isSelected = _selectedTeacherId == t['teacherId']; final inClass = _controller.isInClassNow(name); return GestureDetector( onTap: () => setState(() { _selectedTeacherId = t['teacherId']; _selectedTeacherName = name; }), child: Container( decoration: BoxDecoration( color: isSelected ? const Color(0xFFEFF6FF) : AppPalette.paper, borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? AppPalette.ink : AppPalette.sage, width: isSelected ? 2 : 1, ), ), child: Stack( children: [ Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( name, style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( '${kLocationIcons[location] ?? '๐Ÿ“'} $location', style: const TextStyle( fontSize: 11, color: Colors.grey, ), ), ], ), ), if (inClass) Positioned( top: 4, right: 4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 1, ), decoration: BoxDecoration( color: Colors.orange[100], borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.orange), ), child: const Text( '์ˆ˜์—…์ค‘', style: TextStyle(fontSize: 9, color: Colors.orange), ), ), ), ], ), ), ); }, ); } Widget _buildMyCalls() { if (_controller.myCalls.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Text('์•„์ง ํ˜ธ์ถœ ๊ธฐ๋ก์ด ์—†์–ด์š”.', style: TextStyle(color: Colors.grey)), ); } return Column( children: [ for (final c in _controller.myCalls) Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Expanded( child: Text('${c['teacherName']} ์„ ์ƒ๋‹˜ ยท ${c['purpose']}'), ), Text( '${c['createdAt'] ?? ''}'.split(' ').last, style: const TextStyle(color: Colors.grey, fontSize: 12), ), ], ), ), ], ); } }