// ๐Ÿ“… ๋ฐฑํ’ˆํƒ€ ์ถœ์„ ๋‹ฌ๋ ฅ ํ™”๋ฉด (UI ์ „์šฉ) - ๊ฐœ์ธ ์—ฐ์†์ถœ์„(์ŠคํŠธ๋ฆญ) + ๊ด€๋ฆฌ์ž ์ด๋ฒคํŠธ ๋‹ฌ๋ ฅ. // "์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ"๊ณผ ๊ฐ™์€ ํŒจ๋ฐ€๋ฆฌ๋ฃฉ(์ œ๋ชฉ ์•Œ์•ฝ + ํ•„ ํ†ต๊ณ„ + ๋‘ฅ๊ทผ ์นด๋“œ)์„ ๋”ฐ๋ฅธ๋‹ค. // ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/study_calendar_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/study_calendar_controller.dart'; import '../theme/app_palette.dart'; import 'app_notice.dart'; import 'title_pill.dart'; class StudyCalendarScreen extends StatefulWidget { final String studentId; final bool canManageEvents; const StudyCalendarScreen({ super.key, required this.studentId, this.canManageEvents = false, }); @override State createState() => _StudyCalendarScreenState(); } class _StudyCalendarScreenState extends State { late final StudyCalendarController _controller; static const _weekdayLabels = ['์ผ', '์›”', 'ํ™”', '์ˆ˜', '๋ชฉ', '๊ธˆ', 'ํ† ']; @override void initState() { super.initState(); _controller = StudyCalendarController(studentId: widget.studentId); _controller.init(); } @override void dispose() { _controller.dispose(); super.dispose(); } String _dateKey(DateTime d) => '${d.year.toString().padLeft(4, '0')}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; Future _onDayTap(DateTime day) async { final key = _dateKey(day); final event = _controller.events[key]; if (!widget.canManageEvents) { if (event != null) { await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text(event['title'] ?? ''), content: Text( (event['description'] ?? '').toString().isEmpty ? '๋“ฑ๋ก๋œ ์„ค๋ช…์ด ์—†์–ด์š”.' : event['description'], ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('๋‹ซ๊ธฐ'), ), ], ), ); } return; } final titleController = TextEditingController(text: event?['title'] ?? ''); final descController = TextEditingController( text: event?['description'] ?? '', ); await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: AppPalette.paper, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (sheetContext) { return Padding( padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( '$key ์ด๋ฒคํŠธ', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 12), TextField( controller: titleController, maxLength: 30, decoration: InputDecoration( hintText: '์ œ๋ชฉ (์˜ˆ: ๊ธฐํ”„ํ‹ฐ์ฝ˜ ์ด๋ฒคํŠธ)', filled: true, fillColor: AppPalette.mist, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), TextField( controller: descController, maxLength: 100, maxLines: 3, decoration: InputDecoration( hintText: '์„ค๋ช… (์„ ํƒ)', filled: true, fillColor: AppPalette.mist, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), const SizedBox(height: 12), Row( children: [ if (event != null) Expanded( child: OutlinedButton( onPressed: () async { final (success, message) = await _controller .deleteEvent(key); if (sheetContext.mounted) { Navigator.of(sheetContext).pop(); } if (!mounted) return; AppNotice.show(context, message); }, child: const Text( '์‚ญ์ œ', style: TextStyle(color: Colors.red), ), ), ), if (event != null) const SizedBox(width: 8), Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppPalette.ink, foregroundColor: AppPalette.paper, ), onPressed: () async { if (titleController.text.trim().isEmpty) return; final (success, message) = await _controller.saveEvent( date: key, title: titleController.text, description: descController.text, createdBy: widget.studentId, ); if (sheetContext.mounted) { Navigator.of(sheetContext).pop(); } if (!mounted) return; AppNotice.show( context, message, icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded, ); }, child: const Text('์ €์žฅ'), ), ), ], ), ], ), ); }, ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { final month = _controller.visibleMonth; final firstOfMonth = DateTime(month.year, month.month, 1); final daysInMonth = DateTime(month.year, month.month + 1, 0).day; final leadingBlanks = firstOfMonth.weekday % 7; final today = DateTime.now(); return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( backgroundColor: Colors.transparent, foregroundColor: AppPalette.ink, elevation: 0, ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), children: [ const Center(child: TitlePill('์ถœ์„ ๋‹ฌ๋ ฅ')), const SizedBox(height: 16), Row( children: [ Expanded( child: _statPill('์—ฐ์† ์ถœ์„', _controller.currentStreak), ), const SizedBox(width: 8), Expanded( child: _statPill('์ตœ์žฅ ๊ธฐ๋ก', _controller.longestStreak), ), ], ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left_rounded), onPressed: _controller.goToPreviousMonth, ), Text( '${month.year}๋…„ ${month.month}์›”', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), IconButton( icon: const Icon(Icons.chevron_right_rounded), onPressed: _controller.goToNextMonth, ), ], ), const SizedBox(height: 8), Row( children: [ for (final label in _weekdayLabels) Expanded( child: Center( child: Text( label, style: TextStyle( color: Colors.grey[500], fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ), ], ), const SizedBox(height: 6), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 7, mainAxisSpacing: 6, crossAxisSpacing: 6, childAspectRatio: 0.85, ), itemCount: leadingBlanks + daysInMonth, itemBuilder: (context, index) { if (index < leadingBlanks) { return const SizedBox.shrink(); } final day = index - leadingBlanks + 1; final date = DateTime(month.year, month.month, day); final key = _dateKey(date); final studied = _controller.studiedDates.contains(key); final event = _controller.events[key]; final isToday = date.year == today.year && date.month == today.month && date.day == today.day; return GestureDetector( onTap: () => _onDayTap(date), child: Container( decoration: BoxDecoration( color: studied ? AppPalette.ink : AppPalette.paper, borderRadius: BorderRadius.circular(12), border: Border.all( color: isToday ? AppPalette.ink : AppPalette.sage, width: isToday ? 2 : 1, ), ), child: Stack( children: [ Center( child: Text( '$day', style: TextStyle( fontWeight: FontWeight.bold, color: studied ? Colors.white : AppPalette.ink, ), ), ), if (event != null) Positioned( top: 3, right: 3, child: Icon( Icons.card_giftcard_rounded, size: 12, color: studied ? Colors.white : Colors.orange, ), ), ], ), ), ); }, ), const SizedBox(height: 16), Row( children: [ _legendDot(AppPalette.ink, '๊ณต๋ถ€ํ•œ ๋‚ '), const SizedBox(width: 16), _legendIcon( Icons.card_giftcard_rounded, Colors.orange, '์ด๋ฒคํŠธ', ), ], ), ], ), ); }, ); } 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 _legendDot(Color color, String label) { return Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 6), Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ); } Widget _legendIcon(IconData icon, Color color, String label) { return Row( children: [ Icon(icon, size: 14, color: color), const SizedBox(width: 6), Text(label, style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ); } }