// ๐Ÿ“ ์„ ์ƒ๋‹˜ ํ˜ธ์ถœ ํ™”๋ฉด(ํ•™์ƒ์šฉ) (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, _) { final teachers = _controller.teachers; final inClassCount = teachers .where((t) => _controller.isInClassNow(t['name'] ?? '')) .length; return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.refresh, child: ListView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ const Center(child: TitlePill('์„ ์ƒ๋‹˜ ํ˜ธ์ถœ')), const SizedBox(height: 16), Row( children: [ Expanded(child: _statPill('์ „์ฒด ์„ ์ƒ๋‹˜', teachers.length)), const SizedBox(width: 8), Expanded(child: _statPill('์ง€๊ธˆ ์ˆ˜์—… ์ค‘', inClassCount)), ], ), if (_controller.ranking.isNotEmpty) ...[ const SizedBox(height: 12), _buildRanking(), ], const SizedBox(height: 20), _sectionHeader('๋ฐฉ๋ฌธ ๋ชฉ์ '), const SizedBox(height: 10), _buildPurposeChips(), if (_useCustomPurpose) ...[ const SizedBox(height: 10), TextField( controller: _customPurposeController, maxLength: 40, decoration: InputDecoration( hintText: '์šฉ๋ฌด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”', filled: true, fillColor: AppPalette.paper, border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: AppPalette.sage), ), ), ), ], const SizedBox(height: 20), _sectionHeader('์„ ์ƒ๋‹˜ ๋ชฉ๋ก', count: teachers.length), const SizedBox(height: 10), _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(28), ), ), 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), _sectionHeader( '๋‚ด ํ˜ธ์ถœ ๊ธฐ๋ก', count: _controller.myCalls.length, ), const SizedBox(height: 10), _buildMyCalls(), ], ), ), ); }, ); } /// ๐Ÿชถ "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ"์˜ _sidebarStatPill๊ณผ ๊ฐ™์€ ํ•„ ๋ชจ์–‘ ํ†ต๊ณ„ ํ‘œ์‹œ(์„ ํƒ ๋ถˆ๊ฐ€, ์ˆซ์ž ๊ฐ•์กฐ์šฉ). Widget _statPill(String label, int count) { return Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(28), border: Border.all(color: AppPalette.sage), ), child: Column( children: [ Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), const SizedBox(height: 4), Text( '$count๋ช…', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppPalette.ink, ), ), ], ), ); } /// ๐Ÿท๏ธ "ํ•™๋…„ ๊ทธ๋ฃน" ํ—ค๋”์ฒ˜๋Ÿผ ์ž‘์€ ๊ฐ•์กฐ ๋ฐ” + ๊ตต์€ ์ œ๋ชฉ + (์„ ํƒ) ๊ฐœ์ˆ˜๋ฅผ ๋ถ™์ธ ์„น์…˜ ์ œ๋ชฉ. 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]), ), ], ], ); } static const List _medalColors = [ Color(0xFFC9A227), // ๊ธˆ Color(0xFF9AA0A6), // ์€ Color(0xFFB07A4B), // ๋™ ]; Widget _buildRanking() { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppPalette.sage), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '์˜ค๋Š˜ ์ธ๊ธฐ ์„ ์ƒ๋‹˜', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey[600], ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for (var i = 0; i < _controller.ranking.length; i++) Column( children: [ Icon( Icons.emoji_events_rounded, color: _medalColors[i], size: 22, ), Text( _controller.ranking[i]['teacherName'] ?? '', style: const TextStyle(fontWeight: FontWeight.bold), ), Text( '${_controller.ranking[i]['count']}ํšŒ', style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), ], ), ], ), ); } /// ๐Ÿฌ ํ•„ ๋ชจ์–‘ ์„ ํƒ ๋ฒ„ํŠผ(์Šคํƒฏ ํ•„๊ณผ ๊ฐ™์€ ํ†ค: ์„ ํƒ=ink ๋ฐฐ๊ฒฝ, ๋ฏธ์„ ํƒ=paper+sage ํ…Œ๋‘๋ฆฌ). Widget _pillChoice(String label, bool selected, VoidCallback onTap) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(28), child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), decoration: BoxDecoration( color: selected ? AppPalette.ink : AppPalette.paper, borderRadius: BorderRadius.circular(28), border: Border.all( color: selected ? AppPalette.ink : AppPalette.sage, ), ), child: Text( label, style: TextStyle( fontWeight: FontWeight.bold, color: selected ? Colors.white : AppPalette.ink, ), ), ), ); } Widget _buildPurposeChips() { return Wrap( spacing: 8, runSpacing: 8, children: [ for (final p in kCallPurposes) _pillChoice( p, !_useCustomPurpose && _selectedPurpose == p, () => setState(() { _useCustomPurpose = false; _selectedPurpose = p; }), ), _pillChoice( '์ง์ ‘ ์ž…๋ ฅ', _useCustomPurpose, () => setState(() => _useCustomPurpose = true), ), ], ); } Widget _buildTeacherGrid() { final teachers = _controller.teachers; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 160, mainAxisSpacing: 12, crossAxisSpacing: 12, mainAxisExtent: 110, ), itemCount: teachers.length, itemBuilder: (context, index) { final t = teachers[index]; final name = t['name'] ?? ''; final location = t['location'] ?? '๊ต๋ฌด์‹ค'; final isSelected = _selectedTeacherId == t['teacherId']; final inClass = _controller.isInClassNow(name); final statusColor = inClass ? Colors.orange : AppPalette.ink; return GestureDetector( onTap: () => setState(() { _selectedTeacherId = t['teacherId']; _selectedTeacherName = name; }), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isSelected ? AppPalette.ink : AppPalette.paper, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? AppPalette.ink : AppPalette.sage, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: isSelected ? Colors.white.withValues(alpha: 0.15) : statusColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( inClass ? Icons.school_rounded : Icons.person_rounded, color: isSelected ? Colors.white : statusColor, size: 16, ), ), const Spacer(), if (inClass) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: isSelected ? Colors.white.withValues(alpha: 0.15) : Colors.orange[50], borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? Colors.white54 : Colors.orange, ), ), child: Text( '์ˆ˜์—…์ค‘', style: TextStyle( fontSize: 9, color: isSelected ? Colors.white : Colors.orange, ), ), ), ], ), const Spacer(), Text( name, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, color: isSelected ? Colors.white : AppPalette.ink, ), ), const SizedBox(height: 2), Row( children: [ Icon( kLocationIcons[location] ?? Icons.location_on_rounded, size: 12, color: isSelected ? Colors.white70 : Colors.grey[500], ), const SizedBox(width: 3), Expanded( child: Text( location, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, color: isSelected ? Colors.white70 : Colors.grey[500], ), ), ), ], ), ], ), ), ); }, ); } 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: 16, vertical: 12), decoration: BoxDecoration( color: AppPalette.paper, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppPalette.sage), ), child: Row( children: [ Expanded( child: Text('${c['teacherName']} ์„ ์ƒ๋‹˜ ยท ${c['purpose']}'), ), Text( '${c['createdAt'] ?? ''}'.split(' ').last, style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), ), ], ); } }