diff --git a/lib/ui/teacher_call_screen.dart b/lib/ui/teacher_call_screen.dart index 429db5c..aee8ecd 100644 --- a/lib/ui/teacher_call_screen.dart +++ b/lib/ui/teacher_call_screen.dart @@ -1,4 +1,5 @@ -// ๐Ÿ“ ์„ ์ƒ๋‹˜ ํ˜ธ์ถœ ํ™”๋ฉด(ํ•™์ƒ์šฉ) (UI ์ „์šฉ). ์„ ์ƒ๋‹˜์„ ๊ณจ๋ผ ๋ฐฉ๋ฌธ ๋ชฉ์ ๊ณผ ํ•จ๊ป˜ ํ˜ธ์ถœํ•œ๋‹ค. +// ๐Ÿ“ ์„ ์ƒ๋‹˜ ํ˜ธ์ถœ ํ™”๋ฉด(ํ•™์ƒ์šฉ) (UI ์ „์šฉ). "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ" ํ™”๋ฉด๊ณผ ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ๋ฃฉ +// (์ œ๋ชฉ ์•Œ์•ฝ + ํ•„ ๋ชจ์–‘ ํ†ต๊ณ„/ํ•„ํ„ฐ + ๊ทธ๋ฆผ์ž ์žˆ๋Š” ๋‘ฅ๊ทผ ํƒ€์ผ)์„ ๋”ฐ๋ฅธ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/teacher_call_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../data/teacher_call_schedule.dart'; @@ -75,32 +76,44 @@ class _TeacherCallScreenState extends State { 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, - centerTitle: true, - title: const TitlePill('์„ ์ƒ๋‹˜ ํ˜ธ์ถœ'), ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.refresh, child: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ - if (_controller.ranking.isNotEmpty) _buildRanking(), + const Center(child: TitlePill('์„ ์ƒ๋‹˜ ํ˜ธ์ถœ')), const SizedBox(height: 16), - const Text( - '๋ฐฉ๋ฌธ ๋ชฉ์ ', - style: TextStyle(fontWeight: FontWeight.bold), + Row( + children: [ + Expanded(child: _statPill('์ „์ฒด ์„ ์ƒ๋‹˜', teachers.length)), + const SizedBox(width: 8), + Expanded(child: _statPill('์ง€๊ธˆ ์ˆ˜์—… ์ค‘', inClassCount)), + ], ), - const SizedBox(height: 8), + if (_controller.ranking.isNotEmpty) ...[ + const SizedBox(height: 12), + _buildRanking(), + ], + const SizedBox(height: 20), + _sectionHeader('๋ฐฉ๋ฌธ ๋ชฉ์ '), + const SizedBox(height: 10), _buildPurposeChips(), if (_useCustomPurpose) ...[ - const SizedBox(height: 8), + const SizedBox(height: 10), TextField( controller: _customPurposeController, maxLength: 40, @@ -109,18 +122,15 @@ class _TeacherCallScreenState extends State { filled: true, fillColor: AppPalette.paper, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), borderSide: BorderSide(color: AppPalette.sage), ), ), ), ], - const SizedBox(height: 16), - const Text( - '์„ ์ƒ๋‹˜ ์„ ํƒ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), + const SizedBox(height: 20), + _sectionHeader('์„ ์ƒ๋‹˜ ๋ชฉ๋ก', count: teachers.length), + const SizedBox(height: 10), _buildTeacherGrid(), const SizedBox(height: 20), SizedBox( @@ -133,7 +143,7 @@ class _TeacherCallScreenState extends State { foregroundColor: AppPalette.paper, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(28), ), ), onPressed: _controller.isCalling ? null : _call, @@ -157,11 +167,11 @@ class _TeacherCallScreenState extends State { ), ), const SizedBox(height: 24), - const Text( + _sectionHeader( '๋‚ด ํ˜ธ์ถœ ๊ธฐ๋ก', - style: TextStyle(fontWeight: FontWeight.bold), + count: _controller.myCalls.length, ), - const SizedBox(height: 8), + const SizedBox(height: 10), _buildMyCalls(), ], ), @@ -171,21 +181,89 @@ class _TeacherCallScreenState extends State { ); } + /// ๐Ÿชถ "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ"์˜ _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]), + ), + ], + ], + ); + } + Widget _buildRanking() { final medals = ['๐Ÿฅ‡', '๐Ÿฅˆ', '๐Ÿฅ‰']; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), + 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: [ - const Text( + Text( '์˜ค๋Š˜ ์ธ๊ธฐ ์„ ์ƒ๋‹˜', - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), ), const SizedBox(height: 8), Row( @@ -201,7 +279,7 @@ class _TeacherCallScreenState extends State { ), Text( '${_controller.ranking[i]['count']}ํšŒ', - style: const TextStyle(color: Colors.grey, fontSize: 12), + style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), @@ -212,111 +290,161 @@ class _TeacherCallScreenState extends State { ); } + /// ๐Ÿฌ ํ•„ ๋ชจ์–‘ ์„ ํƒ ๋ฒ„ํŠผ(์Šคํƒฏ ํ•„๊ณผ ๊ฐ™์€ ํ†ค: ์„ ํƒ=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) - ChoiceChip( - label: Text(p), - selected: !_useCustomPurpose && _selectedPurpose == p, - onSelected: (_) => setState(() { + _pillChoice( + p, + !_useCustomPurpose && _selectedPurpose == p, + () => 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, ), + _pillChoice( + '์ง์ ‘ ์ž…๋ ฅ', + _useCustomPurpose, + () => setState(() => _useCustomPurpose = true), ), ], ); } Widget _buildTeacherGrid() { + final teachers = _controller.teachers; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1, - crossAxisSpacing: 10, - mainAxisSpacing: 10, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 160, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + mainAxisExtent: 110, ), - itemCount: _controller.teachers.length, + itemCount: teachers.length, itemBuilder: (context, index) { - final t = _controller.teachers[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 ? const Color(0xFFEFF6FF) : AppPalette.paper, - borderRadius: BorderRadius.circular(12), + color: isSelected ? AppPalette.ink : AppPalette.paper, + borderRadius: BorderRadius.circular(20), 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, - ), - ), - ], - ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 12, + offset: const Offset(0, 4), ), - if (inClass) - Positioned( - top: 4, - right: 4, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 1, - ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.orange[100], - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.orange), + color: isSelected + ? Colors.white.withValues(alpha: 0.15) + : statusColor.withValues(alpha: 0.1), + shape: BoxShape.circle, ), - child: const Text( - '์ˆ˜์—…์ค‘', - style: TextStyle(fontSize: 9, color: Colors.orange), + 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), + Text( + '${kLocationIcons[location] ?? '๐Ÿ“'} $location', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + color: isSelected ? Colors.white70 : Colors.grey[500], + ), + ), ], ), ), @@ -337,10 +465,11 @@ class _TeacherCallScreenState extends State { for (final c in _controller.myCalls) Container( margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppPalette.paper, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), ), child: Row( children: [ @@ -349,7 +478,7 @@ class _TeacherCallScreenState extends State { ), Text( '${c['createdAt'] ?? ''}'.split(' ').last, - style: const TextStyle(color: Colors.grey, fontSize: 12), + style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ], ), diff --git a/lib/ui/teacher_location_screen.dart b/lib/ui/teacher_location_screen.dart index 62489a3..eec89a9 100644 --- a/lib/ui/teacher_location_screen.dart +++ b/lib/ui/teacher_location_screen.dart @@ -1,4 +1,5 @@ -// ๐Ÿ“ ์„ ์ƒ๋‹˜ ์œ„์น˜ ๋“ฑ๋ก ํ™”๋ฉด(๊ต์‚ฌ์šฉ) (UI ์ „์šฉ). ๋ณธ์ธ ์œ„์น˜๋ฅผ ๋ฐ”๊พธ๊ณ , ๋ฐ›์€ ํ˜ธ์ถœ์„ ํ™•์ธํ•œ๋‹ค. +// ๐Ÿ“ ์„ ์ƒ๋‹˜ ์œ„์น˜ ๋“ฑ๋ก ํ™”๋ฉด(๊ต์‚ฌ์šฉ) (UI ์ „์šฉ). "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ" ํ™”๋ฉด๊ณผ ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ๋ฃฉ +// (์ œ๋ชฉ ์•Œ์•ฝ + ํ•„ ๋ชจ์–‘ ํ†ต๊ณ„ + ๊ทธ๋ฆผ์ž ์žˆ๋Š” ๋‘ฅ๊ทผ ํƒ€์ผ)์„ ๋”ฐ๋ฅธ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/teacher_location_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../data/teacher_call_schedule.dart'; @@ -42,6 +43,37 @@ class _TeacherLocationScreenState extends State { ); } + 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( @@ -53,54 +85,56 @@ class _TeacherLocationScreenState extends State { backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, - centerTitle: true, - title: const TitlePill('๋‚ด ์œ„์น˜ ์•Œ๋ฆฌ๊ธฐ'), ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : RefreshIndicator( onRefresh: _controller.fetchReceivedCalls, child: ListView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ + const Center(child: TitlePill('๋‚ด ์œ„์น˜ ์•Œ๋ฆฌ๊ธฐ')), + const SizedBox(height: 16), Container( - padding: const EdgeInsets.all(14), + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 18), decoration: BoxDecoration( - color: const Color(0xFFEFF6FF), - borderRadius: BorderRadius.circular(14), + color: AppPalette.ink, + borderRadius: BorderRadius.circular(24), ), - child: Row( + child: Column( children: [ const Text( 'ํ˜„์žฌ ์œ„์น˜', - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), ), - const Spacer(), + const SizedBox(height: 6), Text( '${kLocationIcons[_controller.currentLocation] ?? '๐Ÿ“'} ${_controller.currentLocation}', style: const TextStyle( fontWeight: FontWeight.bold, - color: AppPalette.ink, + fontSize: 20, + color: Colors.white, ), ), ], ), ), const SizedBox(height: 20), - const Text( - '์œ„์น˜ ์„ ํƒ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), + _sectionHeader('์œ„์น˜ ์„ ํƒ'), + const SizedBox(height: 10), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 1.3, - crossAxisSpacing: 10, - mainAxisSpacing: 10, + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 160, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + mainAxisExtent: 90, ), itemCount: kLocationOptions.length, itemBuilder: (context, index) { @@ -115,22 +149,40 @@ class _TeacherLocationScreenState extends State { color: isSelected ? AppPalette.ink : AppPalette.paper, - borderRadius: BorderRadius.circular(12), + 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: [ - Text( - kLocationIcons[loc] ?? '๐Ÿ“', - style: const TextStyle(fontSize: 20), + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withValues( + alpha: 0.15, + ) + : AppPalette.linen, + shape: BoxShape.circle, + ), + child: Text( + kLocationIcons[loc] ?? '๐Ÿ“', + style: const TextStyle(fontSize: 16), + ), ), - const SizedBox(height: 4), + const SizedBox(height: 6), Text( loc, style: TextStyle( @@ -149,11 +201,11 @@ class _TeacherLocationScreenState extends State { }, ), const SizedBox(height: 24), - const Text( + _sectionHeader( '๋‚˜๋ฅผ ์ฐพ์€ ํ•™์ƒ ๊ธฐ๋ก', - style: TextStyle(fontWeight: FontWeight.bold), + count: _controller.receivedCalls.length, ), - const SizedBox(height: 8), + const SizedBox(height: 10), _buildReceivedCalls(), ], ), @@ -175,19 +227,33 @@ class _TeacherLocationScreenState extends State { for (final c in _controller.receivedCalls) Container( margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppPalette.paper, - borderRadius: BorderRadius.circular(12), + 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: const TextStyle(color: Colors.grey, fontSize: 11), + style: TextStyle(color: Colors.grey[500], fontSize: 11), ), ], ),