import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'dart:ui' as ui; import 'package:smarthelmet_app/widgets/custom_header.dart'; //25.12.03 지은 추가 import 'package:smarthelmet_app/services/locker_api.dart'; class HomeScreenContent extends StatefulWidget { const HomeScreenContent({super.key}); @override State createState() => _HomeScreenContentState(); } class _HomeScreenContentState extends State { static const double _uniformGap = 16.0; final Color _mainBlueColor = const Color(0xFF002FA7); final Color _mainTextColor = Colors.black; final Color _subTextColor = const Color(0xFF6A717B); final Color _pageBackgroundColor = const Color(0xFFF5F7F9); final Color _cardBackgroundColor = Colors.white; final Color _accentContainerColor = const Color(0xFFF0F2F5); final Color _toggleOffTrackColor = const Color(0xFFE0E0E0); final Color _toggleOffKnobBorderColor = const Color(0xFFE0E0E0); // 25.12.03 지은 추가 시작 final LockerApi _api = LockerApi(); bool _isLoading = false; Future _runLockerAction(String name, Future Function() action) async { if (_isLoading) return; setState(() => _isLoading = true); // 실제 명령 전송 final success = await action(); if (!mounted) return; setState(() => _isLoading = false); } // 25.12.03 지은 추가 끝 void test() async { final data = await _api.getTimeseries(target: "*", limit: 200); if (data != null) { print("받은 데이터 길이: ${data['samples'].length}"); print(data['samples'][0]); // 첫 번째 데이터 출력 } } int _selectedImageIndex = 0; static const BoxShadow _cleanShadow = BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.07), blurRadius: 8, offset: Offset(0, 4), spreadRadius: 0, ); final Map _controlToggles = { 'UV LED': false, 'CHARGING': true, 'HELMET': true, 'FAN': false, }; @override Widget build(BuildContext context) { test(); return Container( color: _pageBackgroundColor, child: Column( children: [ const CustomHeader(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: _uniformGap), child: Column( children: [ const SizedBox(height: _uniformGap), _buildOverviewSection(), const SizedBox(height: _uniformGap), _buildBatteryStatusCard(), const SizedBox(height: _uniformGap), _buildControlCard(), const SizedBox(height: _uniformGap), _buildEnvironmentSensorsCard(), const SizedBox(height: _uniformGap), _buildMyLocationCard(), const SizedBox(height: _uniformGap), _buildActivityCard(), const SizedBox(height: _uniformGap * 2), ], ), ), ), ], ), ); } Widget _buildOverviewSection() { return Container( margin: const EdgeInsets.only(top: 5), child: DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Column( children: [ _buildOverviewHeader(), Padding( padding: const EdgeInsets.all(12.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: _buildImageCard()), const SizedBox(width: 12), Expanded(child: _buildInfoCard()), ], ), ), ], ), ), ); } Widget _buildOverviewHeader() { return Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 0), child: Row( children: [ Text('장비 상태 / 사용자 정보', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const Spacer(), Icon(Icons.search, color: _subTextColor, size: 20), const SizedBox(width: 8), Icon(Icons.notifications_outlined, color: _subTextColor, size: 20), ], ), ); } Widget _buildImageCard() { return AspectRatio( aspectRatio: 1.0, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _accentContainerColor, borderRadius: BorderRadius.circular(8), ), child: Stack( alignment: Alignment.center, children: [ Container( decoration: BoxDecoration( color: _cardBackgroundColor, borderRadius: BorderRadius.circular(5), ), ), Padding( padding: const EdgeInsets.only(bottom: 20.0), child: Image.asset( _selectedImageIndex == 0 ? 'assets/images/storage.png' : 'assets/images/top.png', width: 90, ), ), Positioned( bottom: 12, child: Row( children: [ _buildSelectableIndicator(0), const SizedBox(width: 8), _buildSelectableIndicator(1), ], ), ) ], ), ), ); } Widget _buildSelectableIndicator(int index) { final bool isSelected = _selectedImageIndex == index; return GestureDetector( onTap: () { setState(() { _selectedImageIndex = index; }); }, child: Container( width: 30, height: 6, decoration: BoxDecoration( color: isSelected ? _mainTextColor : Colors.grey.shade300, borderRadius: BorderRadius.circular(3), ), ), ); } Widget _buildInfoCard() { return SizedBox( height: 164, child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildInfoRow('ID', Icon(Icons.person, color: _mainTextColor, size: 20), 'USER', '001', value1Color: _mainTextColor, value2Color: _mainTextColor), const SizedBox(height: 8), _buildInfoRow('STATUS', null, '잠금 해제', '● 활성', value1Color: _mainTextColor, value2Color: _mainTextColor), ], ), ); } Widget _buildInfoRow(String title, Widget? icon, String value1, String value2, {Color? value1Color, Color? value2Color}) { return Expanded( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _accentContainerColor, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Expanded( flex: 2, child: Text(title, textAlign: TextAlign.center, style: TextStyle(color: _subTextColor, fontSize: 11, height: 1.4)), ), VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( flex: 3, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (icon != null) ...[icon, const SizedBox(height: 4)], Text(value1, style: TextStyle(color: value1Color ?? _mainTextColor, fontWeight: FontWeight.bold, fontSize: 12)), const SizedBox(height: 2), Text(value2, style: TextStyle(color: value2Color ?? _mainTextColor, fontWeight: FontWeight.w500, fontSize: 12)), ], ), ), ], ), ), ); } Widget _buildBatteryStatusCard() { return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('배터리 상태 (%)', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const SizedBox(height: 20), Row( children: [ SizedBox( width: 70, height: 70, child: Stack( alignment: Alignment.center, children: [ SizedBox.expand( child: CustomPaint( painter: _BatteryArcPainter( backgroundColor: _accentContainerColor, color: _mainBlueColor, percentage: 1.0, ), ), ), Text('86', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: _mainTextColor)), ], ), ), const SizedBox(width: 20), Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('현재', style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.bold)), Text('사용 중', style: TextStyle(color: _mainBlueColor, fontSize: 14, fontWeight: FontWeight.bold)), ], ), Divider(color: _subTextColor.withOpacity(0.5), height: 20, thickness: 1), Row( children: [ Expanded( flex: 4, child: Text('태양광 패널', style: TextStyle(color: _mainTextColor, fontSize: 14)), ), Expanded( flex: 5, child: Row( children: [ Expanded(child: Text('전압: 00', style: TextStyle(color: _subTextColor, fontSize: 14))), SizedBox( height: 20, child: VerticalDivider(color: _subTextColor.withOpacity(0.5), thickness: 1), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text('전류: 00', style: TextStyle(color: _subTextColor, fontSize: 14)), ), ), ], ), ), ], ), ], ), ), ], ), ], ), ), ); } Widget _buildControlCard() { return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('보관함 원격 제어', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const SizedBox(height: 16), SizedBox( height: 70, child: Row( children: [ Expanded( child: _buildStyledToggleSwitch( 'UV LED', _controlToggles['UV LED']!, // 25.12.03 지은 수정 시작 (val) { _runLockerAction("UV 제어", () async { bool success = await _api.setUV(val); if (success) { setState(() => _controlToggles['UV LED'] = val); } return success; } ); }, ), ), // 25.12.03 지은 수정 끝 VerticalDivider(color: _mainBlueColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( 'CHARGING', _controlToggles['CHARGING']!, (val) => setState(() => _controlToggles['CHARGING'] = val))), VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( 'HELMET', _controlToggles['HELMET']!, (val) => setState(() => _controlToggles['HELMET'] = val))), VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch('FAN', _controlToggles['FAN']!, // 25.12.03 지은 수정 시작 (val) { print("👉 [디버깅] fan 눌림! 값: $val"); _runLockerAction("FAN 제어", () async { print("👉 [디버깅] API 요청 시작..."); bool success = await _api.setFan(val); if (success) { setState(() => _controlToggles['FAN'] = val); } return success; }); }, // 25.12.03 지은 수정 끝 ), ), ], ), ) ], ), ), ); } Widget _buildStyledToggleSwitch(String title, bool value, ValueChanged onChanged) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(title, style: TextStyle(color: _mainTextColor, fontSize: 13, fontWeight: FontWeight.bold)), const SizedBox(height: 12), GestureDetector( onTap: () => onChanged(!value), child: AnimatedContainer( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, width: 60, height: 30, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: value ? _mainBlueColor : _toggleOffTrackColor, ), child: Stack( children: [ AnimatedAlign( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, alignment: value ? Alignment.centerRight : Alignment.centerLeft, child: Padding( padding: const EdgeInsets.all(2.0), child: Container( width: 26, height: 26, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all( color: value ? _mainBlueColor : _toggleOffKnobBorderColor, width: 1.5), ), ), ), ), Row( children: [ Expanded( child: Center( child: Text('ON', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: value ? Colors.white : Colors.transparent)))), Expanded( child: Center( child: Text('OFF', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: value ? Colors.transparent : _mainTextColor)))), ], ) ], ), ), ), ], ); } Widget _buildEnvironmentSensorsCard() { return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('환경 모니터링', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const Spacer(), InkWell( onTap: () {}, child: Row( children: [ Text('더보기', style: TextStyle(color: _mainTextColor, fontSize: 9)), const SizedBox(width: 4), Icon(Icons.arrow_forward_ios, size: 10, color: _subTextColor), ], ), ), ], ), const SizedBox(height: 20), Row( children: [ Expanded( flex: 1, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSensorInfoRow(Icons.water_drop_outlined, '습도: 60%', '습도'), const SizedBox(height: 24), _buildSensorInfoRow(Icons.thermostat, '온도: 24.5℃', '온도'), ], ), ), const SizedBox(width: 16), Expanded( flex: 1, child: SizedBox( height: 60, child: _LineChartPlaceholder(lineColor: _mainTextColor, subLabelColor: _subTextColor))), ], ), ], ), ), ); } Widget _buildSensorInfoRow(IconData icon, String text, String type) { return Row(children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: _accentContainerColor, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: _mainTextColor, size: 24), ), const SizedBox(width: 12), Text(text, style: TextStyle(fontSize: 14, color: _mainTextColor, fontWeight: FontWeight.w600)) ]); } Widget _buildMyLocationCard() { const LatLng exampleLocation = LatLng(37.5665, 126.9780); return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, clipBehavior: Clip.antiAlias, child: SizedBox( height: 200.0, child: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('현위치', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const SizedBox(height: 4), Text('주소: 남구 효덕로 277', style: TextStyle(fontSize: 11, color: _mainTextColor)), ], ), InkWell( onTap: () {}, child: Row( children: [ Text('더보기', style: TextStyle(color: _mainTextColor, fontSize: 9)), const SizedBox(width: 4), Icon(Icons.arrow_forward_ios, size: 10, color: _subTextColor), ], ), ), ], ), ), Expanded( child: FlutterMap( options: const MapOptions( initialCenter: exampleLocation, initialZoom: 15.0, interactionOptions: InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', userAgentPackageName: 'com.example.app', subdomains: const ['a', 'b', 'c', 'd'], retinaMode: true, ), MarkerLayer( markers: [ Marker( point: exampleLocation, width: 80, height: 80, child: Icon(Icons.location_pin, size: 40, color: _mainTextColor), ), ], ), ], ), ), ], ), ), ); } Widget _buildActivityCard() { return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( padding: const EdgeInsets.all(_uniformGap), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('최근 활동', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: _mainTextColor)), const SizedBox(height: 12), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _activityText('08:15 AM - 배터리 충전 완료'), const SizedBox(height: 8), _activityText('9:30 AM - UV LED 활성화 됨'), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _activityText('10:45 AM - 안전모 잠금 해제'), const SizedBox(height: 8), _activityText('11:00 AM - 안전모 착용 해제'), ], ), ), ], ), ], ), ), ); } Widget _activityText(String text) { return Text(text, style: TextStyle(fontSize: 11, color: _mainTextColor)); } } class _LineChartPlaceholder extends StatelessWidget { final Color lineColor; final Color subLabelColor; const _LineChartPlaceholder({ super.key, required this.lineColor, required this.subLabelColor }); @override Widget build(BuildContext context) { return Column(children: [ Expanded( child: CustomPaint(painter: _LineChartPainter(color: lineColor), size: Size.infinite)), const SizedBox(height: 4), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('24 시간 전', style: TextStyle(fontSize: 8, color: subLabelColor)), Text('12시간 전', style: TextStyle(fontSize: 8, color: subLabelColor)), Text('현재', style: TextStyle(fontSize: 8, color: subLabelColor)) ]) ]); } } class _LineChartPainter extends CustomPainter { final Color color; _LineChartPainter({required this.color}); @override void paint(ui.Canvas canvas, ui.Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final path = ui.Path(); path.moveTo(0, size.height * 0.6); path.cubicTo(size.width * 0.1, size.height * 0.8, size.width * 0.2, size.height * 0.4, size.width * 0.3, size.height * 0.6); path.cubicTo(size.width * 0.4, size.height * 0.8, size.width * 0.45, size.height * 0.2, size.width * 0.6, size.height * 0.5); path.cubicTo(size.width * 0.75, size.height * 0.8, size.width * 0.8, size.height * 0.3, size.width, size.height * 0.2); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } } class DashboardCard extends StatelessWidget { final Widget child; final EdgeInsetsGeometry? padding; final Clip clipBehavior; final Color? borderColor; final Color? cardColor; final BoxShadow? shadow; const DashboardCard({ super.key, required this.child, this.padding, this.clipBehavior = Clip.none, this.borderColor, this.cardColor, this.shadow, }); @override Widget build(BuildContext context) { return Container( clipBehavior: clipBehavior, decoration: BoxDecoration( color: cardColor ?? Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: shadow != null ? [shadow!] : null, ), child: child, ); } } class _BatteryArcPainter extends CustomPainter { final Color backgroundColor; final Color color; final double percentage; _BatteryArcPainter({ required this.backgroundColor, required this.color, required this.percentage, }); @override void paint(Canvas canvas, Size size) { final Paint backgroundPaint = Paint() ..color = backgroundColor ..strokeWidth = 8 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final Paint foregroundPaint = Paint() ..color = color ..strokeWidth = 8 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; final Rect rect = Rect.fromLTWH(0, 0, size.width, size.height); const double startAngle = -2.35; const double sweepAngle = 4.7; canvas.drawArc(rect, startAngle, sweepAngle, false, backgroundPaint); final double progressAngle = sweepAngle * percentage; canvas.drawArc(rect, startAngle, progressAngle, false, foregroundPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } }