diff --git a/lib/function/device_checkout_controller.dart b/lib/function/device_checkout_controller.dart new file mode 100644 index 0000000..19de90b --- /dev/null +++ b/lib/function/device_checkout_controller.dart @@ -0,0 +1,48 @@ +// ๐Ÿ“ฑ ์Šค๋งˆํŠธ๊ธฐ๊ธฐ(ํŒจ๋“œ) ๋ฐ˜์ถœ ์‹ ์ฒญ ํ™”๋ฉด์˜ ๊ธฐ๋Šฅ(์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ) ๋‹ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ. +// NFC๊ฐ€ ์•ˆ ๋˜๋Š” ํŒจ๋“œ์šฉ ๋Œ€์ฒด ์ ˆ์ฐจ โ€” ์‹œ์ž‘~์ข…๋ฃŒ ์‹œ๊ฐ๊ณผ ์‚ฌ์šฉ ๋ชฉ์ ์„ ์ ์–ด ์‹ ์ฒญํ•˜๋ฉด +// ์„ ์ƒ๋‹˜ ์Šน์ธ ํ›„ ์ €์šธ(์•„๋‘์ด๋…ธ)์ด ์‹ค์ œ ํ”ฝ์—…/๋ฐ˜๋‚ฉ ์—ฌ๋ถ€๋ฅผ ๊ฐ์‹œํ•œ๋‹ค (ํ•˜๋“œ์›จ์–ด๋Š” ๋‚˜์ค‘์— ๋ถ™์ž„). +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import '../config.dart'; + +class DeviceCheckoutController extends ChangeNotifier { + bool _isSubmitting = false; + bool get isSubmitting => _isSubmitting; + + /// โž• ๋ฐ˜์ถœ ์š”์ฒญ ์ œ์ถœ. (์„ฑ๊ณต์—ฌ๋ถ€, ๋ฉ”์‹œ์ง€)๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + Future<(bool success, String message)> submitRequest({ + required String studentId, + required String studentName, + required String purpose, + required String startTime, + required String endTime, + }) async { + _isSubmitting = true; + notifyListeners(); + try { + final response = await http.post( + Uri.parse('$baseUrl/api/device-checkout/request'), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "studentId": studentId, + "studentName": studentName, + "purpose": purpose, + "startTime": startTime, + "endTime": endTime, + }), + ); + final result = jsonDecode(utf8.decode(response.bodyBytes)); + if (response.statusCode == 200 && result['status'] == 'success') { + return (true, 'โœ… ${result['message'] ?? '์š”์ฒญ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}'); + } else { + return (false, 'โŒ ${result['message'] ?? '์š”์ฒญ ์‹คํŒจ'}'); + } + } catch (e) { + return (false, '๐Ÿšจ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ: $e'); + } finally { + _isSubmitting = false; + notifyListeners(); + } + } +} diff --git a/lib/function/device_checkout_ledger_controller.dart b/lib/function/device_checkout_ledger_controller.dart new file mode 100644 index 0000000..69e5af9 --- /dev/null +++ b/lib/function/device_checkout_ledger_controller.dart @@ -0,0 +1,85 @@ +// ๐Ÿ“‹ ์„ ์ƒ๋‹˜์šฉ "์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ" ํ™”๋ฉด์˜ ๊ธฐ๋Šฅ(์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ) ๋‹ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ. +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import '../config.dart'; + +class DeviceCheckoutLedgerController extends ChangeNotifier { + List _requests = []; + bool _isLoading = true; + bool _isWorking = false; + Timer? _timer; + bool _disposed = false; + + List get requests => _requests; + bool get isLoading => _isLoading; + bool get isWorking => _isWorking; + + void _safeNotify() { + if (!_disposed) notifyListeners(); + } + + void init() { + fetchAll(); + _timer = Timer.periodic(const Duration(seconds: 5), (_) => fetchAll()); + } + + @override + void dispose() { + _disposed = true; + _timer?.cancel(); + super.dispose(); + } + + Future fetchAll() async { + try { + final response = await http.get( + Uri.parse('$baseUrl/api/device-checkout/list'), + ); + if (response.statusCode == 200) { + final data = jsonDecode(utf8.decode(response.bodyBytes)); + _requests = data['requests'] ?? []; + } + } catch (_) { + // ์ƒˆ๋กœ๊ณ ์นจ ์‹คํŒจ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜๊ณ  ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์•„์˜จ ๋ชฉ๋ก์„ ์œ ์ง€ํ•œ๋‹ค. + } finally { + _isLoading = false; + _safeNotify(); + } + } + + Future<(bool success, String message)> approve(int requestId) async { + return _postAction('/api/device-checkout/approve', requestId); + } + + Future<(bool success, String message)> reject(int requestId) async { + return _postAction('/api/device-checkout/reject', requestId); + } + + Future<(bool success, String message)> _postAction( + String path, + int requestId, + ) async { + _isWorking = true; + _safeNotify(); + try { + final response = await http.post( + Uri.parse('$baseUrl$path'), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({"requestId": requestId}), + ); + final result = jsonDecode(utf8.decode(response.bodyBytes)); + if (response.statusCode == 200 && result['status'] == 'success') { + await fetchAll(); + return (true, 'โœ… ${result['message'] ?? '์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'}'); + } + return (false, 'โŒ ${result['message'] ?? '์ฒ˜๋ฆฌ ์‹คํŒจ'}'); + } catch (e) { + return (false, '๐Ÿšจ ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ: $e'); + } finally { + _isWorking = false; + _safeNotify(); + } + } +} diff --git a/lib/ui/device_checkout_ledger_page.dart b/lib/ui/device_checkout_ledger_page.dart new file mode 100644 index 0000000..b31acef --- /dev/null +++ b/lib/ui/device_checkout_ledger_page.dart @@ -0,0 +1,262 @@ +// ๐Ÿ“‹ ์„ ์ƒ๋‹˜์šฉ "์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ" ํ™”๋ฉด (UI ์ „์šฉ). ์˜ค๋Š˜ ์‹ ์ฒญ๋œ ๋ฐ˜์ถœ ์š”์ฒญ์„ ๋ณด์—ฌ์ฃผ๊ณ  ์Šน์ธ/๊ฑฐ์ ˆํ•œ๋‹ค. +// ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/device_checkout_ledger_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. +import 'package:flutter/material.dart'; +import '../function/device_checkout_ledger_controller.dart'; + +class DeviceCheckoutLedgerPage extends StatefulWidget { + const DeviceCheckoutLedgerPage({super.key}); + + @override + State createState() => + _DeviceCheckoutLedgerPageState(); +} + +class _DeviceCheckoutLedgerPageState extends State { + final DeviceCheckoutLedgerController _controller = + DeviceCheckoutLedgerController(); + + @override + void initState() { + super.initState(); + _controller.init(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _approve(int id) async { + final (_, message) = await _controller.approve(id); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + + Future _reject(int id) async { + final (_, message) = await _controller.reject(id); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + } + + ({Color color, String label}) _statusInfo(String status) { + switch (status) { + case 'PENDING': + return (color: Colors.orange, label: 'โณ ๋Œ€๊ธฐ์ค‘'); + case 'APPROVED': + return (color: Colors.blue, label: 'โœ… ์Šน์ธ๋จ (ํ”ฝ์—… ๋Œ€๊ธฐ)'); + case 'IN_USE': + return (color: Colors.teal, label: '๐Ÿ“ฑ ์‚ฌ์šฉ์ค‘'); + case 'RETURNED': + return (color: Colors.grey, label: 'โ†ฉ๏ธ ๋ฐ˜๋‚ฉ์™„๋ฃŒ'); + case 'REJECTED': + return (color: Colors.red, label: '๐Ÿšซ ๊ฑฐ์ ˆ๋จ'); + default: + return (color: Colors.grey, label: status); + } + } + + String _timeRange(String start, String end) { + // "YYYY-MM-DD HH:MM:SS" -> "HH:MM" + String hm(String full) => full.length >= 16 ? full.substring(11, 16) : full; + return '${hm(start)} ~ ${hm(end)}'; + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _controller, + builder: (context, _) { + final requests = _controller.requests; + final pendingCount = requests + .where((r) => r['status'] == 'PENDING') + .length; + + return Scaffold( + backgroundColor: Colors.grey[100], + appBar: AppBar( + title: const Text( + '๐Ÿ“ฑ ์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.refresh_rounded), + onPressed: _controller.fetchAll, + tooltip: '์ƒˆ๋กœ๊ณ ์นจ', + ), + ], + ), + body: _controller.isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + if (pendingCount > 0) + Container( + width: double.infinity, + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.orange[100], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange[400]!), + ), + child: Text( + 'โณ ์Šน์ธ ๋Œ€๊ธฐ ์ค‘์ธ ์š”์ฒญ์ด $pendingCount๊ฑด ์žˆ์Šต๋‹ˆ๋‹ค.', + style: TextStyle( + color: Colors.orange[900], + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: requests.isEmpty + ? const Center( + child: Text( + '์˜ค๋Š˜ ์‹ ์ฒญ๋œ ๋ฐ˜์ถœ ์š”์ฒญ์ด ์—†์Šต๋‹ˆ๋‹ค.', + style: TextStyle(color: Colors.grey), + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + itemCount: requests.length, + itemBuilder: (context, index) { + final r = requests[index]; + final String status = r['status']; + final info = _statusInfo(status); + final bool isPending = status == 'PENDING'; + + return Card( + elevation: 1, + margin: const EdgeInsets.only(bottom: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: isPending + ? BorderSide(color: Colors.orange[300]!) + : BorderSide.none, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + '${r['studentName']} (${r['studentId']})', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: info.color.withValues( + alpha: 0.12, + ), + borderRadius: + BorderRadius.circular(20), + ), + child: Text( + info.label, + style: TextStyle( + color: info.color, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + 'โฐ ${_timeRange(r['requestedStart'], r['requestedEnd'])}', + style: TextStyle( + color: Colors.grey[700], + fontSize: 13, + ), + ), + const SizedBox(height: 2), + Text( + '๐Ÿ“ ${r['purpose']}', + style: TextStyle( + color: Colors.grey[700], + fontSize: 13, + ), + ), + if (isPending) ...[ + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: + _controller.isWorking + ? null + : () => _reject(r['id']), + style: + OutlinedButton.styleFrom( + foregroundColor: + Colors.red, + side: const BorderSide( + color: Colors.red, + ), + ), + child: const Text('๊ฑฐ์ ˆ'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: + _controller.isWorking + ? null + : () => _approve(r['id']), + style: + ElevatedButton.styleFrom( + backgroundColor: + Colors.teal, + foregroundColor: + Colors.white, + ), + child: const Text('์Šน์ธ'), + ), + ), + ], + ), + ], + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/device_checkout_request_screen.dart b/lib/ui/device_checkout_request_screen.dart new file mode 100644 index 0000000..06c1433 --- /dev/null +++ b/lib/ui/device_checkout_request_screen.dart @@ -0,0 +1,198 @@ +// ๐Ÿ“ฑ ์Šค๋งˆํŠธ๊ธฐ๊ธฐ(ํŒจ๋“œ) ๋ฐ˜์ถœ ์‹ ์ฒญ ํ™”๋ฉด (UI ์ „์šฉ). ์‹œ์ž‘~์ข…๋ฃŒ ์‹œ๊ฐ๊ณผ ์‚ฌ์šฉ ๋ชฉ์ ์„ ์ž…๋ ฅ๋ฐ›๋Š”๋‹ค. +// ์„œ๋ฒ„ ํ†ต์‹ /์ƒํƒœ๋Š” lib/function/device_checkout_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. +import 'package:flutter/material.dart'; +import '../function/device_checkout_controller.dart'; + +class DeviceCheckoutRequestScreen extends StatefulWidget { + final String studentId; + final String studentName; + + const DeviceCheckoutRequestScreen({ + super.key, + required this.studentId, + required this.studentName, + }); + + @override + State createState() => + _DeviceCheckoutRequestScreenState(); +} + +class _DeviceCheckoutRequestScreenState + extends State { + final DeviceCheckoutController _controller = DeviceCheckoutController(); + final TextEditingController _purposeController = TextEditingController(); + TimeOfDay? _startTime; + TimeOfDay? _endTime; + + @override + void dispose() { + _controller.dispose(); + _purposeController.dispose(); + super.dispose(); + } + + String _formatTime(TimeOfDay time) => + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; + + Future _pickStartTime() async { + final picked = await showTimePicker( + context: context, + initialTime: _startTime ?? TimeOfDay.now(), + helpText: '์‚ฌ์šฉ ์‹œ์ž‘ ์‹œ๊ฐ', + ); + if (picked != null) setState(() => _startTime = picked); + } + + Future _pickEndTime() async { + final picked = await showTimePicker( + context: context, + initialTime: _endTime ?? TimeOfDay.now(), + helpText: '์‚ฌ์šฉ ์ข…๋ฃŒ ์‹œ๊ฐ', + ); + if (picked != null) setState(() => _endTime = picked); + } + + Future _submit() async { + final purpose = _purposeController.text.trim(); + if (_startTime == null || _endTime == null || purpose.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('โš ๏ธ ์‹œ์ž‘/์ข…๋ฃŒ ์‹œ๊ฐ๊ณผ ์‚ฌ์šฉ ๋ชฉ์ ์„ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.')), + ); + return; + } + + final (success, message) = await _controller.submitRequest( + studentId: widget.studentId, + studentName: widget.studentName, + purpose: purpose, + startTime: _formatTime(_startTime!), + endTime: _formatTime(_endTime!), + ); + if (!mounted) return; + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); + if (success) Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _controller, + builder: (context, _) { + return Scaffold( + backgroundColor: Colors.grey[100], + appBar: AppBar( + title: const Text( + '๐Ÿ“ฑ ์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ์‹ ์ฒญ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + elevation: 0, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'ํŒจ๋“œ๋Š” NFC ํƒœ๊ทธ๊ฐ€ ์•ˆ ๋˜๋‹ˆ, ์‚ฌ์šฉ ์‹œ๊ฐ„๊ณผ ๋ชฉ์ ์„ ์ ์–ด ์‹ ์ฒญํ•˜๋ฉด\n' + '์„ ์ƒ๋‹˜์ด ํ™•์ธํ•˜๊ณ  ์Šน์ธํ•ด์ค๋‹ˆ๋‹ค. ์Šน์ธ๋˜๋ฉด ๋ฐ˜๋‚ฉ ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ๊บผ๋‚ด ์“ฐ์„ธ์š”.', + style: TextStyle( + color: Colors.black54, + fontSize: 13, + height: 1.4, + ), + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.04), + blurRadius: 16, + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _pickStartTime, + icon: const Icon(Icons.play_arrow_rounded), + label: Text( + _startTime == null + ? '์‹œ์ž‘ ์‹œ๊ฐ' + : _formatTime(_startTime!), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: _pickEndTime, + icon: const Icon(Icons.stop_rounded), + label: Text( + _endTime == null + ? '์ข…๋ฃŒ ์‹œ๊ฐ' + : _formatTime(_endTime!), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + TextField( + controller: _purposeController, + maxLines: 3, + decoration: const InputDecoration( + labelText: '์‚ฌ์šฉ ๋ชฉ์ ', + hintText: '์˜ˆ: ์ˆ˜ํ–‰ํ‰๊ฐ€ ์ž๋ฃŒ์กฐ์‚ฌ', + alignLabelWithHint: true, + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _controller.isSubmitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.teal, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _controller.isSubmitting + ? const CircularProgressIndicator( + color: Colors.white, + ) + : const Text( + '๋ฐ˜์ถœ ์‹ ์ฒญํ•˜๊ธฐ', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/student_dashboard.dart b/lib/ui/student_dashboard.dart index fa0a9e0..b2a44af 100644 --- a/lib/ui/student_dashboard.dart +++ b/lib/ui/student_dashboard.dart @@ -3,6 +3,7 @@ // ๊ณ„์ • ์‚ญ์ œ ์„œ๋ฒ„ ํ†ต์‹ ์€ lib/function/student_dashboard_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/student_dashboard_controller.dart'; +import 'device_checkout_request_screen.dart'; import 'nfc_poccket_checkin_screen.dart'; import 'login_screen.dart'; import 'teacher_attendance_page.dart'; @@ -346,6 +347,24 @@ class _StudentDashboardState extends State { }, ), + // ๐Ÿ“ฑ ํŒจ๋“œ๋Š” NFC๊ฐ€ ์•ˆ ๋˜๋‹ˆ, ์‹œ๊ฐ„+๋ชฉ์  ์ ์–ด์„œ ์‹ ์ฒญ โ†’ ์„ ์ƒ๋‹˜ ์Šน์ธ๋ฐ›๋Š” ๋Œ€์ฒด ์ ˆ์ฐจ + _buildModernCard( + icon: Icons.tablet_mac_rounded, + title: '์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ', + subtitle: 'ํŒจ๋“œ ์‚ฌ์šฉ ์‹ ์ฒญ (์‹œ๊ฐ„/๋ชฉ์ )', + color: Colors.teal, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + DeviceCheckoutRequestScreen( + studentId: widget.studentId, + studentName: widget.studentName, + ), + ), + ), + ), + if (isDeveloper) _buildModernCard( icon: Icons.monitor_heart_rounded, diff --git a/lib/ui/teacher_dashboard.dart b/lib/ui/teacher_dashboard.dart index a1c31c0..5223903 100644 --- a/lib/ui/teacher_dashboard.dart +++ b/lib/ui/teacher_dashboard.dart @@ -1,5 +1,6 @@ // ๐Ÿ‘จโ€๐Ÿซ ๊ต์‚ฌ ๋Œ€์‹œ๋ณด๋“œ. ์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ™•์ธ, ํ•™์ƒ ๊ณ„์ • ๊ด€๋ฆฌ ํ™”๋ฉด์œผ๋กœ ๊ฐ€๋Š” ๋ฉ”๋‰ด๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; +import 'device_checkout_ledger_page.dart'; import 'login_screen.dart'; import 'teacher_attendance_page.dart'; import 'teacher_student_management_page.dart'; @@ -149,31 +150,44 @@ class _TeacherDashboardState extends State { mainAxisSpacing: 16, childAspectRatio: 0.95, children: [ - _buildModernCard( - icon: Icons.assignment_turned_in_rounded, - title: '์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ™•์ธ', - subtitle: 'ํ•™์ƒ ์ œ์ถœ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง', - color: Colors.blue, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const TeacherAttendancePage(), + _buildModernCard( + icon: Icons.assignment_turned_in_rounded, + title: '์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ™•์ธ', + subtitle: 'ํ•™์ƒ ์ œ์ถœ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง', + color: Colors.blue, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TeacherAttendancePage(), + ), + ), ), - ), - ), - _buildModernCard( - icon: Icons.manage_accounts_rounded, - title: 'ํ•™์ƒ ๊ณ„์ • ๊ด€๋ฆฌ', - subtitle: '๊ณ„์ • ์ถ”๊ฐ€ ๋ฐ ๊ฐ•์ œ ๋ฆฌ์…‹', - color: Colors.orange, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const TeacherStudentManagementPage(), + _buildModernCard( + icon: Icons.manage_accounts_rounded, + title: 'ํ•™์ƒ ๊ณ„์ • ๊ด€๋ฆฌ', + subtitle: '๊ณ„์ • ์ถ”๊ฐ€ ๋ฐ ๊ฐ•์ œ ๋ฆฌ์…‹', + color: Colors.orange, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const TeacherStudentManagementPage(), + ), + ), + ), + _buildModernCard( + icon: Icons.tablet_mac_rounded, + title: '์Šค๋งˆํŠธ๊ธฐ๊ธฐ ๋ฐ˜์ถœ ๋Œ€์žฅ', + subtitle: 'ํŒจ๋“œ ๋ฐ˜์ถœ ์‹ ์ฒญ ์Šน์ธ/๊ฑฐ์ ˆ', + color: Colors.teal, + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const DeviceCheckoutLedgerPage(), + ), + ), ), - ), - ), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 5b6c50b..f654dfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.2.0+6 +version: 1.3.0+7 environment: sdk: ^3.12.2