// ๐Ÿ“ ์„ ์ƒ๋‹˜ ์œ„์น˜ ๋“ฑ๋ก ํ™”๋ฉด(๊ต์‚ฌ์šฉ) (UI ์ „์šฉ). "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ" ํ™”๋ฉด๊ณผ ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ๋ฃฉ // (์ œ๋ชฉ ์•Œ์•ฝ + ํ•„ ๋ชจ์–‘ ํ†ต๊ณ„ + ๊ทธ๋ฆผ์ž ์žˆ๋Š” ๋‘ฅ๊ทผ ํƒ€์ผ)์„ ๋”ฐ๋ฅธ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/teacher_location_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../data/teacher_call_schedule.dart'; import '../function/teacher_location_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class TeacherLocationScreen extends StatefulWidget { final String teacherId; const TeacherLocationScreen({super.key, required this.teacherId}); @override State createState() => _TeacherLocationScreenState(); } class _TeacherLocationScreenState extends State { late final TeacherLocationController _controller; @override void initState() { super.initState(); _controller = TeacherLocationController(teacherId: widget.teacherId); _controller.init(); } @override void dispose() { _controller.dispose(); super.dispose(); } Future _selectLocation(String location) async { final (success, message) = await _controller.updateLocation(location); if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); } 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]), ), ], ], ); } /// ๋„“์€ ํ™”๋ฉด(์›น ๋ฐ์Šคํฌํ†ฑ)์—์„œ ๋‚ด์šฉ์ด ์–‘์˜†์œผ๋กœ ๋Š˜์–ด์ง€์ง€ ์•Š๊ฒŒ ๊ฐ€์šด๋ฐ ์ตœ๋Œ€ 560 ํญ์œผ๋กœ ๋งž์ถ˜๋‹ค. double _sidePad(BuildContext context) { final width = MediaQuery.of(context).size.width; return ((width - 560) / 2).clamp(16.0, double.infinity); } @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, ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.fetchReceivedCalls, child: ListView( padding: EdgeInsets.fromLTRB( _sidePad(context), 0, _sidePad(context), 16, ), children: [ const Center(child: TitlePill('๋‚ด ์œ„์น˜ ์•Œ๋ฆฌ๊ธฐ')), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 18), decoration: BoxDecoration( color: AppPalette.ink, borderRadius: BorderRadius.circular(24), ), child: Column( children: [ const Text( 'ํ˜„์žฌ ์œ„์น˜', style: TextStyle( color: Colors.white70, fontSize: 12, ), ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( kLocationIcons[_controller.currentLocation] ?? Icons.location_on_rounded, color: Colors.white, size: 20, ), const SizedBox(width: 8), Text( _controller.currentLocation, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20, color: Colors.white, ), ), ], ), ], ), ), const SizedBox(height: 20), _sectionHeader('์œ„์น˜ ์„ ํƒ'), const SizedBox(height: 10), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 160, mainAxisSpacing: 12, crossAxisSpacing: 12, mainAxisExtent: 90, ), itemCount: kLocationOptions.length, itemBuilder: (context, index) { final loc = kLocationOptions[index]; final isSelected = _controller.currentLocation == loc; return GestureDetector( onTap: _controller.isSaving ? null : () => _selectLocation(loc), child: Container( 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: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: isSelected ? Colors.white.withValues( alpha: 0.15, ) : AppPalette.linen, shape: BoxShape.circle, ), child: Icon( kLocationIcons[loc] ?? Icons.location_on_rounded, size: 16, color: isSelected ? Colors.white : AppPalette.ink, ), ), const SizedBox(height: 6), Text( loc, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isSelected ? Colors.white : AppPalette.ink, ), ), ], ), ), ), ); }, ), const SizedBox(height: 24), _sectionHeader( '๋‚˜๋ฅผ ์ฐพ์€ ํ•™์ƒ ๊ธฐ๋ก', count: _controller.receivedCalls.length, ), const SizedBox(height: 10), _buildReceivedCalls(), ], ), ), ); }, ); } Widget _buildReceivedCalls() { if (_controller.receivedCalls.isEmpty) { return const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Text('์•„์ง ํ˜ธ์ถœ ๊ธฐ๋ก์ด ์—†์–ด์š”.', style: TextStyle(color: Colors.grey)), ); } return Column( children: [ for (final c in _controller.receivedCalls) 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.notifications_active_rounded, size: 16, color: AppPalette.ink, ), ), const SizedBox(width: 10), Expanded( child: Text('${c['studentName']} ํ•™์ƒ ยท ${c['purpose']}'), ), Text( '${c['createdAt'] ?? ''}', style: TextStyle(color: Colors.grey[500], fontSize: 11), ), ], ), ), ], ); } }