diff --git a/assets/videos/angry.mp4 b/assets/videos/angry.mp4 index 3633293..0d5962c 100644 Binary files a/assets/videos/angry.mp4 and b/assets/videos/angry.mp4 differ diff --git a/assets/videos/angry_img.mp4 b/assets/videos/angry_img.mp4 deleted file mode 100644 index 0d5962c..0000000 Binary files a/assets/videos/angry_img.mp4 and /dev/null differ diff --git a/assets/videos/basic.mp4 b/assets/videos/basic.mp4 deleted file mode 100644 index 6b1c610..0000000 Binary files a/assets/videos/basic.mp4 and /dev/null differ diff --git a/assets/videos/cold.mp4 b/assets/videos/cold.mp4 index 3b2e159..21a72ba 100644 Binary files a/assets/videos/cold.mp4 and b/assets/videos/cold.mp4 differ diff --git a/assets/videos/cold_img.mp4 b/assets/videos/cold_img.mp4 deleted file mode 100644 index 21a72ba..0000000 Binary files a/assets/videos/cold_img.mp4 and /dev/null differ diff --git a/assets/videos/basic_img.mp4 b/assets/videos/default.mp4 similarity index 100% rename from assets/videos/basic_img.mp4 rename to assets/videos/default.mp4 diff --git a/assets/videos/joy.mp4 b/assets/videos/joy.mp4 index 365c754..76198aa 100644 Binary files a/assets/videos/joy.mp4 and b/assets/videos/joy.mp4 differ diff --git a/assets/videos/joy_img.mp4 b/assets/videos/joy_img.mp4 deleted file mode 100644 index 76198aa..0000000 Binary files a/assets/videos/joy_img.mp4 and /dev/null differ diff --git a/assets/videos/know.mp4 b/assets/videos/know.mp4 deleted file mode 100644 index bd80abe..0000000 Binary files a/assets/videos/know.mp4 and /dev/null differ diff --git a/assets/videos/know_img.mp4 b/assets/videos/realization.mp4 similarity index 100% rename from assets/videos/know_img.mp4 rename to assets/videos/realization.mp4 diff --git a/assets/videos/thinking.mp4 b/assets/videos/thinking.mp4 index 20a5c69..ddbde17 100644 Binary files a/assets/videos/thinking.mp4 and b/assets/videos/thinking.mp4 differ diff --git a/assets/videos/thinking_img.mp4 b/assets/videos/thinking_img.mp4 deleted file mode 100644 index 47d0ab6..0000000 Binary files a/assets/videos/thinking_img.mp4 and /dev/null differ diff --git a/assets/videos/thirst.mp4 b/assets/videos/thirst.mp4 index 82e6667..796c6e0 100644 Binary files a/assets/videos/thirst.mp4 and b/assets/videos/thirst.mp4 differ diff --git a/assets/videos/thirst_img.mp4 b/assets/videos/thirst_img.mp4 deleted file mode 100644 index 796c6e0..0000000 Binary files a/assets/videos/thirst_img.mp4 and /dev/null differ diff --git a/lib/models/chat_message.dart b/lib/models/chat_message.dart index c9981e9..f6ac719 100644 --- a/lib/models/chat_message.dart +++ b/lib/models/chat_message.dart @@ -2,11 +2,13 @@ class ChatMessage { final String text; final bool isUser; final DateTime timestamp; + final int? emotionCategory; // ← 새로 추가 (봇 메시지의 감정) ChatMessage({ required this.text, required this.isUser, required this.timestamp, + this.emotionCategory, }); // JSON에서 ChatMessage로 변환 @@ -15,6 +17,7 @@ class ChatMessage { text: json['text'] as String, isUser: json['isUser'] as bool, timestamp: DateTime.parse(json['timestamp'] as String), + emotionCategory: json['emotionCategory'] as int?, ); } @@ -24,6 +27,7 @@ class ChatMessage { 'text': text, 'isUser': isUser, 'timestamp': timestamp.toIso8601String(), + 'emotionCategory': emotionCategory, }; } } \ No newline at end of file diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 0eba754..3ec0646 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../models/chat_message.dart'; import '../services/chat_service.dart'; +import 'dart:math'; //26.06.24 추가 + class ChatScreen extends StatefulWidget { const ChatScreen({Key? key}) : super(key: key); @@ -18,31 +20,73 @@ class _ChatScreenState extends State { late VideoPlayerController _videoController; bool _isVideoInitialized = false; bool _isLoading = false; + int _currentEmotion = 1; // ← 추가 (현재 감정, 기본값: 1 = DEFAULT) @override void initState() { super.initState(); - _initializeVideo(); + // 더미 컨트롤러로 먼저 초기화 + _videoController = VideoPlayerController.asset('assets/videos/default.mp4'); + // 실제 감정에 따른 비디오 초기화 + _initializeVideo(_currentEmotion); _addInitialMessage(); - } - void _initializeVideo() { - _videoController = - VideoPlayerController.asset('assets/videos/basic_img.mp4') - ..initialize() - .then((_) { - _videoController.setLooping(true); - _videoController.setVolume(0.0); - _videoController.play(); - setState(() { - _isVideoInitialized = true; - }); - }) - .catchError((error) { - print('비디오 로드 오류: $error'); + //26.06.24 추가 시작 + String _getVideoFileName(int emotionCategory) { + switch(emotionCategory) { + case 1: + return 'default.mp4'; // DEFAULT + case 2: + return 'joy.mp4'; // JOY + case 3: + return 'thinking.mp4'; // THINKING + case 4: + return 'realization.mp4'; // REALIZATION + case 5: + return 'angry.mp4'; // ANGER + case 6: + return 'thirst.mp4'; // THIRST + case 7: + return 'cold.mp4'; // COLD + default: + return 'default.mp4'; // 기본값 + } + } + + void _initializeVideo(int emotionCategory) { + final videoFileName = _getVideoFileName(emotionCategory); + + print('비디오 변경: emotion=$emotionCategory → $videoFileName'); + + // 새 비디오 컨트롤러 생성 + final newController = VideoPlayerController.asset('assets/videos/$videoFileName'); + + // 새 비디오를 먼저 로드 + newController.initialize().then((_) { + // 로드 완료 후에 기존 컨트롤러 교체 + try { + if (_videoController.hasListeners) { + _videoController.dispose(); + } + } catch (e) { + print('기존 컨트롤러 dispose 스킵: $e'); + } + + // 새 컨트롤러로 교체 + _videoController = newController; + _videoController.setLooping(true); + _videoController.setVolume(0.0); + _videoController.play(); + + setState(() { + _isVideoInitialized = true; }); + }).catchError((error) { + print('비디오 로드 오류: $error'); + }); } + //26.06.24 추가 끝 void _addInitialMessage() { setState(() { @@ -51,35 +95,68 @@ class _ChatScreenState extends State { text: '상태 분석이 완료되었습니다.\n무엇을 도와드릴까요?', isUser: false, timestamp: DateTime.now(), + emotionCategory: _currentEmotion, // 감정 추가 ), ); }); } + //26.06.24 추가 시작 void _sendMessage(String text) async { if (text.isEmpty) return; // 사용자 메시지 추가 setState(() { _messages.add( - ChatMessage(text: text, isUser: true, timestamp: DateTime.now()), + ChatMessage( + text: text, + isUser: true, + timestamp: DateTime.now(), + emotionCategory: null, // 사용자 메시지는 감정값 없음 + ), ); - _isLoading = true; }); _textController.clear(); _scrollToBottom(); + // 로딩 메시지 추가 (... 표시) + setState(() { + _messages.add( + ChatMessage( + text: '로딩 중...', + isUser: false, + timestamp: DateTime.now(), + emotionCategory: _currentEmotion, // 현재 감정값 유지 + ), + ); + _isLoading = true; + }); + + _scrollToBottom(); + // 서버로 메시지 전송 try { - final response = await ChatService.sendMessage(text); + final (responseMessage, emotionCategory) = await ChatService.sendMessage(text); + + print('받은 감정값: $emotionCategory (기존: $_currentEmotion)'); setState(() { + // 마지막 로딩 메시지를 실제 응답으로 교체 + _messages.removeLast(); // 로딩 메시지 제거 + // 감정값이 변경되었으면 비디오 변경 + if (emotionCategory != _currentEmotion) { + print('감정 변경! $emotionCategory로 비디오 교체'); + _currentEmotion = emotionCategory; + // 백그라운드에서 비디오 로드 (화면 깜빡임 없음) + _initializeVideo(_currentEmotion); + } _messages.add( ChatMessage( - text: response, + text: responseMessage, isUser: false, timestamp: DateTime.now(), + emotionCategory: emotionCategory, // ← 추가 ), ); _isLoading = false; @@ -88,11 +165,13 @@ class _ChatScreenState extends State { } catch (e) { print('메시지 전송 오류: $e'); setState(() { + _messages.removeLast(); // ← 로딩 메시지 제거 _messages.add( ChatMessage( text: '오류가 발생했습니다. 다시 시도해주세요.', isUser: false, timestamp: DateTime.now(), + emotionCategory: _currentEmotion, // ← 추가 ), ); _isLoading = false; @@ -100,6 +179,7 @@ class _ChatScreenState extends State { _scrollToBottom(); } } + //26.06.24 추가 끝 void _scrollToBottom() { Future.delayed(const Duration(milliseconds: 100), () { @@ -453,10 +533,18 @@ class _ChatScreenState extends State { controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: _messages.length, + //26.06.24 추가 시작 itemBuilder: (context, index) { final message = _messages[index]; - return _ChatBubble(message: message); + final isLoadingMessage = _isLoading && + index == _messages.length - 1 && + !message.isUser; + return _ChatBubble( + message: message, + isLoading: isLoadingMessage, + ); }, + //26.06.24 추가 끝 ), ), ), @@ -748,8 +836,12 @@ class _ChatScreenState extends State { class _ChatBubble extends StatelessWidget { final ChatMessage message; + final bool isLoading; //26.06.24 추가 - const _ChatBubble({required this.message}); + const _ChatBubble({ + required this.message, + this.isLoading = false //26.06.24 추가 + }); String _formatTime(DateTime dateTime) { final hour = dateTime.hour; @@ -819,16 +911,20 @@ class _ChatBubble extends StatelessWidget { ), ], ), - child: SelectableText( + // 26.06.24 추가 시작 + child: isLoading + ? _LoadingAnimation() // 로딩 중 + : SelectableText( message.text, style: TextStyle( fontSize: 20, color: message.isUser - ? Colors.black - : Colors.black, + ? Color(0xFF000000) + : Color(0xFF424242), height: 1.4, ), ), + // 26.06.24 추가 끝 ), ), if (!message.isUser) const SizedBox(width: 8), @@ -844,4 +940,128 @@ class _ChatBubble extends StatelessWidget { ), ); } -} \ No newline at end of file +} + +//26.06.24 추가 시작 +class _LoadingAnimation extends StatefulWidget { + const _LoadingAnimation(); + + @override + State<_LoadingAnimation> createState() => _LoadingAnimationState(); +} + +class _LoadingAnimationState extends State<_LoadingAnimation> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 900), //움직이는 속도 + vsync: this, + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + double _getOffset(double progress, int index) { + final delay = index * 0.15; + final adjustedProgress = (progress + delay) % 1.0; + return sin(adjustedProgress * pi * 2) * 2; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + final progress = _controller.value; + + return SizedBox( + height: 20, // 일반 텍스트 높이와 동일 + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _AnimatedDot( + progress: progress, + index: 0, + getOffset: _getOffset, + ), + const SizedBox(width: 3), + _AnimatedDot( + progress: progress, + index: 1, + getOffset: _getOffset, + ), + const SizedBox(width: 3), + _AnimatedDot( + progress: progress, + index: 2, + getOffset: _getOffset, + ), + ], + ), + ); + }, + ); + } +} + +// 개별 원형 점 위젯 +class _AnimatedDot extends StatelessWidget { + final double progress; + final int index; + final double Function(double, int) getOffset; + + const _AnimatedDot({ + required this.progress, + required this.index, + required this.getOffset, + }); + + @override + Widget build(BuildContext context) { + final offset = getOffset(progress, index); + + return Transform.translate( + offset: Offset(0, offset), + child: SizedBox( //원형 사이즈 + width: 5, + height: 5, + child: CustomPaint( + painter: _DotPainter(), + ), + ), + ); + } +} + +// CustomPaint로 원 그리기 +class _DotPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xFF424242) + ..style = PaintingStyle.fill; + + // 원의 중심과 반지름 + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2; + + canvas.drawCircle(center, radius, paint); + } + + @override + bool shouldRepaint(_DotPainter oldDelegate) => false; +} +//26.06.24 추가 끝 + +//26.06.24 추가 시작 +//26.06.24 추가 끝 \ No newline at end of file diff --git a/lib/services/chat_service.dart b/lib/services/chat_service.dart index fe9e5a7..f392e61 100644 --- a/lib/services/chat_service.dart +++ b/lib/services/chat_service.dart @@ -10,7 +10,8 @@ class ChatService { static const int timeoutSeconds = 30; /// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드 - static Future sendMessage(String userMessage) async { + /// 반환값: (메시지, emotion_category) 튜플 + static Future<(String, int)> sendMessage(String userMessage) async { try { // 요청 생성 final requestBody = { @@ -21,15 +22,7 @@ class ChatService { 'user_id': 'user-123', }; - // JSON 형식 확인 - final jsonBody = jsonEncode(requestBody); - print('서버로 요청 전송 (Map): $requestBody'); - print('서버로 요청 전송 (JSON): $jsonBody'); - - final uri = Uri.parse(serverUrl); - print('요청 URI: $uri'); - print('요청 헤더: {Content-Type: application/json, accept: application/json}'); - print('요청 바디: ${jsonEncode(requestBody)}'); + print('서버로 요청 전송: $requestBody'); // POST 요청 전송 final response = await http.post( @@ -49,36 +42,37 @@ class ChatService { // 상태 코드 확인 if (response.statusCode == 200) { + // 성공 final jsonResponse = jsonDecode(response.body); - print('전체 응답: $jsonResponse'); - - // 'message' 필드 추출 + // 응답에서 'message' 필드 추출 final responseMessage = jsonResponse['message'] as String?; + // 응답에서 'emotion_category' 필드 추출 (기본값: 1) + final emotionCategory = jsonResponse['emotion_category'] as int? ?? 1; + if (responseMessage != null && responseMessage.isNotEmpty) { print('응답 메시지: $responseMessage'); - return responseMessage; + print('감정 카테고리: $emotionCategory'); + return (responseMessage, emotionCategory); } else { - print('message 필드가 없습니다'); - // 혹시 다른 필드에 응답이 있는지 확인 - print('응답 전체: ${jsonResponse.toString()}'); - return '응답을 받지 못했습니다'; + print('응답이 비어있습니다'); + return ('응답을 받지 못했습니다', 1); } } else { // 실패 print('서버 오류: ${response.statusCode}'); - return '서버 오류 (${response.statusCode})'; + return ('서버 오류 (${response.statusCode})', 1); } } on TimeoutException catch (e) { print('타임아웃: $e'); - return '서버 응답이 없습니다. 시간 초과'; + return ('서버 응답이 없습니다. 시간 초과', 1); } on FormatException catch (e) { print('JSON 파싱 오류: $e'); - return 'JSON 형식 오류'; + return ('JSON 형식 오류', 1); } catch (e) { print('오류 발생: $e'); - return '오류가 발생했습니다: $e'; + return ('오류가 발생했습니다: $e', 1); } } @@ -105,6 +99,62 @@ class ChatService { return false; } } + + /// 서버 상태 상세 테스트 + static Future testServerDetailed() async { + print('\n=== 서버 상세 테스트 시작 ===\n'); + + try { + print('서버 주소: $serverUrl'); + print('연결 시도 중...\n'); + + // 1. 연결 테스트 (GET) + print('⃣ GET 요청 테스트...'); + try { + final getResponse = await http.get( + Uri.parse(serverUrl), + ).timeout(const Duration(seconds: 5)); + + print('GET 상태코드: ${getResponse.statusCode}'); + print('GET 응답: ${getResponse.body}\n'); + } catch (e) { + print('GET 오류: $e\n'); + } + + // 2. POST 테스트 (실제 요청) + print('POST 요청 테스트...'); + final testBody = { + 'device_id': 1, + 'locale': 'ko-KR', + 'message': '테스트', + 'session_id': 'session-001', + 'user_id': 'user-123', + }; + + print('전송할 데이터: $testBody\n'); + + try { + final postResponse = await http.post( + Uri.parse(serverUrl), + headers: { + 'Content-Type': 'application/json', + 'accept': 'application/json', + }, + body: jsonEncode(testBody), + ).timeout(const Duration(seconds: 5)); + + print('POST 상태코드: ${postResponse.statusCode}'); + print('POST 응답: ${postResponse.body}\n'); + } catch (e) { + print('POST 오류: $e\n'); + } + + } catch (e) { + print('테스트 오류: $e\n'); + } + + print('=== 테스트 완료 ===\n'); + } } /// 타임아웃 예외 diff --git a/lib/widgets/chatbot_detail_page.dart b/lib/widgets/chatbot_detail_page.dart deleted file mode 100644 index 91657b5..0000000 --- a/lib/widgets/chatbot_detail_page.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; // 비디오 패키지 임포트 - -class ChatbotDetailPage extends StatefulWidget { - final VoidCallback onBack; - - const ChatbotDetailPage({super.key, required this.onBack}); - - @override - State createState() => _ChatbotDetailPageState(); -} - -class _ChatbotDetailPageState extends State { - late VideoPlayerController _knowVideoController; - bool _isVideoInitialized = false; - bool _hasVideoError = false; - - @override - void initState() { - super.initState(); - _initializeKnowVideo(); - } - - void _initializeKnowVideo() { - const String videoAssetPath = 'assets/assets/videos/know.mp4'; - _knowVideoController = VideoPlayerController.asset(videoAssetPath); - - _knowVideoController.initialize().then((_) { - if (!mounted) return; - setState(() { - _isVideoInitialized = true; - }); - _knowVideoController.setLooping(true); - _knowVideoController.setVolume(0.0); - _knowVideoController.play(); - }).catchError((error) { - debugPrint("1차 지식 영상 로딩 실패, 백업 경로 시도 중...: $error"); - - _knowVideoController = VideoPlayerController.asset('assets/videos/know.mp4'); - _knowVideoController.initialize().then((_) { - if (!mounted) return; - setState(() { - _isVideoInitialized = true; - }); - _knowVideoController.setLooping(true); - _knowVideoController.setVolume(0.0); - _knowVideoController.play(); - }).catchError((retryError) { - if (!mounted) return; - setState(() { - _hasVideoError = true; - }); - debugPrint("최종 지식 비디오 초기화 에러: $retryError"); - }); - }); - } - - @override - void dispose() { - _knowVideoController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // 상단 미니 센서 바 - SizedBox( - height: 100, - child: Row( - children: [ - _buildMiniSensor('온도', '24.5°C', Colors.orangeAccent), - const SizedBox(width: 10), - _buildMiniSensor('습도', '65%', Colors.blueAccent), - const SizedBox(width: 10), - _buildMiniSensor('CO2', '420 ppm', Colors.greenAccent), - const SizedBox(width: 10), - _buildMiniSensor('TVOC', '0.15 mg/m³', Colors.tealAccent), - const SizedBox(width: 10), - _buildMiniSensor('미세먼지', '15', Colors.white70), - ], - ), - ), - const SizedBox(height: 20), - - // 메인 콘텐츠 영역 - Expanded( - child: Row( - children: [ - // 🤖 [좌측] 푸미 캐릭터 전용 패널 (✨ flex: 60 으로 수정) - Expanded( - flex: 60, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFF1A1D23), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withOpacity(0.05)), - ), - child: Stack( - children: [ - // 🔙 "목록으로 돌아가기" 버튼 - Positioned( - top: 20, - left: 20, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: widget.onBack, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFB4E49C).withOpacity(0.4)), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.arrow_back_ios_new, color: Color(0xFFB4E49C), size: 14), - SizedBox(width: 8), - Text( - '목록으로 돌아가기', - style: TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - letterSpacing: -0.5, - ), - ), - ], - ), - ), - ), - ), - ), - - // 정중앙 know.mp4 대형 배치 - Center( - child: _hasVideoError - ? Icon(Icons.smart_toy, size: 160, color: const Color(0xFFB4E49C).withOpacity(0.3)) - : _isVideoInitialized - ? SizedBox( - width: 600, // ✨ 메인 화면과 짝을 맞춰 550 스케일로 정돈 극대화! - child: AspectRatio( - aspectRatio: _knowVideoController.value.aspectRatio, - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: VideoPlayer(_knowVideoController), - ), - ), - ) - : const SizedBox( - width: 100, - height: 100, - child: Center(child: CircularProgressIndicator(color: Color(0xFFB4E49C))), - ), - ), - ], - ), - ), - ), - const SizedBox(width: 20), - - // 💬 [우측] 채팅창 (✨ flex: 40 으로 수정) - Expanded( - flex: 40, - child: Container( - decoration: BoxDecoration( - color: const Color(0xFF1A1D23), - borderRadius: BorderRadius.circular(24), - ), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(20), - child: Row( - children: [ - CircleAvatar(backgroundColor: Color(0xFFB4E49C), radius: 4), - SizedBox(width: 10), - Text('푸미 일지', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - ], - ), - ), - Expanded( - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 16), - children: [ - _buildChatBubble("상태 분석이 완료되었습니다. 무엇을 도와드릴까요?", true), - _buildChatBubble("현재 습도가 적당하니?", false), - _buildChatBubble("네, 습도 65%로 최적의 상태입니다.", true), - ], - ), - ), - // 하단 입력창 - Padding( - padding: const EdgeInsets.all(16), - child: TextField( - decoration: InputDecoration( - hintText: 'Ask AI...', - hintStyle: const TextStyle(color: Colors.white24, fontSize: 14), - filled: true, - fillColor: Colors.white.withOpacity(0.05), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), - suffixIcon: const Icon(Icons.send, color: Color(0xFFB4E49C), size: 20), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ], - ); - } - - // 채팅 말풍선 헬퍼 - Widget _buildChatBubble(String msg, bool isBot) { - return Align( - alignment: isBot ? Alignment.centerLeft : Alignment.centerRight, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 5), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: isBot ? Colors.white.withOpacity(0.08) : const Color(0xFFB4E49C).withOpacity(0.2), - borderRadius: BorderRadius.circular(15), - ), - child: Text(msg, style: const TextStyle(color: Colors.white, fontSize: 13)), - ), - ); - } - - // 미니 센서 카드 헬퍼 - Widget _buildMiniSensor(String t, String v, Color c) { - return Expanded( - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: const Color(0xFF1A1D23), borderRadius: BorderRadius.circular(15)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(t, style: const TextStyle(color: Colors.white54, fontSize: 11)), - Text(v, style: TextStyle(color: c, fontSize: 16, fontWeight: FontWeight.bold)), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/co2_data_page.dart b/lib/widgets/co2_data_page.dart deleted file mode 100644 index ab0177b..0000000 --- a/lib/widgets/co2_data_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class Co2DataPage extends StatelessWidget { - const Co2DataPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '🍃 실시간 CO2 농도 변화 추이 분석', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/common_chart_card.dart b/lib/widgets/common_chart_card.dart deleted file mode 100644 index de6a8b4..0000000 --- a/lib/widgets/common_chart_card.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fl_chart/fl_chart.dart'; - -/// 📊 [공통 위젯] 모든 센서(온도/습도/CO2 등)가 공유하는 실시간 차트 카드. -/// -/// DashboardGrid 의 `_buildSensorCard` 스타일(배경 #1F242C, 라운드 16, -/// white10 테두리, 헤더 + 강조 색상)을 그대로 따르며, 하단에 fl_chart -/// 라인 차트를 붙여 [stream] 으로 흘러오는 데이터를 실시간으로 그린다. -/// -/// 사용 예: -/// ```dart -/// SensorChartCard( -/// title: '온도', -/// icon: Icons.thermostat, -/// accentColor: Colors.orangeAccent, -/// unit: '°C', -/// stream: tempService.stream, -/// ) -/// ``` -class SensorChartCard extends StatelessWidget { - const SensorChartCard({ - super.key, - required this.title, - required this.icon, - required this.accentColor, - required this.unit, - required this.stream, - }); - - /// 카드 제목 (예: '온도', '습도', '이산화탄소') - final String title; - - /// 헤더 우측에 표시할 아이콘 - final IconData icon; - - /// 라인/아이콘/현재값에 쓰일 강조 색상 - final Color accentColor; - - /// 값 뒤에 붙는 단위 (예: '°C', '%', 'ppm') - final String unit; - - /// 갱신될 때마다 "전체 측정값 리스트"가 흘러나오는 스트림 - final Stream> stream; - - static const Color _cardColor = Color(0xFF1F242C); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: _cardColor, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white10), - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: StreamBuilder>( - stream: stream, - builder: (context, snapshot) { - final data = snapshot.data ?? const []; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(data), - const SizedBox(height: 20), - Expanded(child: _buildChart(data)), - ], - ); - }, - ), - ); - } - - // 🏷️ 헤더: 제목 + 현재값 + 아이콘 (센서 카드와 동일한 톤) - Widget _buildHeader(List data) { - final hasData = data.isNotEmpty; - final latest = hasData ? data.last : null; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 16, - color: Colors.white70, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - hasData ? latest!.toStringAsFixed(1) : '--', - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - const SizedBox(width: 4), - Text( - unit, - style: const TextStyle(fontSize: 14, color: Colors.white38), - ), - ], - ), - ], - ), - Icon(icon, color: accentColor.withOpacity(0.8), size: 22), - ], - ); - } - - // 📈 라인 차트 (fl_chart) - Widget _buildChart(List data) { - if (data.isEmpty) { - return const Center( - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Color(0xFFB4E49C), - ), - ), - ); - } - - final spots = [ - for (var i = 0; i < data.length; i++) FlSpot(i.toDouble(), data[i]), - ]; - - final minY = data.reduce((a, b) => a < b ? a : b); - final maxY = data.reduce((a, b) => a > b ? a : b); - // 위아래로 약간 여백을 둬서 선이 테두리에 붙지 않게 한다. - final padding = ((maxY - minY).abs() * 0.15).clamp(1.0, double.infinity); - - return LineChart( - LineChartData( - minX: 0, - maxX: (data.length - 1).toDouble(), - minY: minY - padding, - maxY: maxY + padding, - gridData: FlGridData( - show: true, - drawVerticalLine: false, - horizontalInterval: ((maxY + padding) - (minY - padding)) / 4, - getDrawingHorizontalLine: (value) => - const FlLine(color: Colors.white10, strokeWidth: 1), - ), - titlesData: const FlTitlesData(show: false), - borderData: FlBorderData(show: false), - lineTouchData: LineTouchData( - touchTooltipData: LineTouchTooltipData( - getTooltipColor: (_) => _cardColor.withOpacity(0.9), - getTooltipItems: (touchedSpots) => [ - for (final s in touchedSpots) - LineTooltipItem( - '${s.y.toStringAsFixed(1)} $unit', - const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ], - ), - ), - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: true, - color: accentColor, - barWidth: 2.5, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - accentColor.withOpacity(0.25), - accentColor.withOpacity(0.0), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/widgets/device_manage_page.dart b/lib/widgets/device_manage_page.dart deleted file mode 100644 index 5e5a0c0..0000000 --- a/lib/widgets/device_manage_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class DeviceManagePage extends StatelessWidget { - const DeviceManagePage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '⚙️ 모터, 팬, LED 광원 장비 제어 관리 시스템', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/dust_data_page.dart b/lib/widgets/dust_data_page.dart deleted file mode 100644 index 0b634f5..0000000 --- a/lib/widgets/dust_data_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class DustDataPage extends StatelessWidget { - const DustDataPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '😷 실시간 미세먼지(PM2.5 / PM10) 모니터링', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/header.dart b/lib/widgets/header.dart deleted file mode 100644 index 7bea07d..0000000 --- a/lib/widgets/header.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; - -class Header extends StatelessWidget { - // ✨ 메인 화면(main.dart)으로부터 동적 타이틀과 토글 함수를 전달받도록 변수 설정 - final String title; - final bool isSidebarOpen; - final VoidCallback onToggleSidebar; - - const Header({ - super.key, - required this.title, // ✨ 필수 파라미터로 등록 - required this.isSidebarOpen, - required this.onToggleSidebar, - }); - - @override - Widget build(BuildContext context) { - return Container( - height: 70, - padding: const EdgeInsets.symmetric(horizontal: 16), - color: const Color(0xFF13161A), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - // 사이드바 토글 메뉴 버튼 - IconButton( - icon: Icon( - isSidebarOpen ? Icons.menu_open : Icons.menu, - color: const Color(0xFFB4E49C), - ), - onPressed: onToggleSidebar, - ), - const SizedBox(width: 8), - - // ✨ 고정된 텍스트 대신 전달받은 title 변수를 출력합니다! - Text( - title, - style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.white), - ), - const SizedBox(width: 8), - const Icon(Icons.help_outline, size: 16, color: Colors.white38), - ], - ), - const Icon(Icons.notifications_none, color: Colors.white70, size: 24), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/humid_data_page.dart b/lib/widgets/humid_data_page.dart deleted file mode 100644 index ab5d6ca..0000000 --- a/lib/widgets/humid_data_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class HumidDataPage extends StatelessWidget { - const HumidDataPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '💧 실시간 토양 및 대기 습도 데이터 현황', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/main_dashboard.dart b/lib/widgets/main_dashboard.dart deleted file mode 100644 index a881c58..0000000 --- a/lib/widgets/main_dashboard.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; // 비디오 패키지 임포트 - -class DashboardGrid extends StatefulWidget { - final VoidCallback onCharacterTap; - - const DashboardGrid({super.key, required this.onCharacterTap}); - - @override - State createState() => _DashboardGridState(); -} - -class _DashboardGridState extends State { - late VideoPlayerController _videoController; - bool _isVideoInitialized = false; - bool _hasVideoError = false; - - @override - void initState() { - super.initState(); - _initializeWebVideo(); - } - - void _initializeWebVideo() { - const String videoAssetPath = 'assets/assets/videos/basic.mp4'; - _videoController = VideoPlayerController.asset(videoAssetPath); - - _videoController.initialize().then((_) { - if (!mounted) return; - setState(() { - _isVideoInitialized = true; - }); - _videoController.setLooping(true); // 🔄 무한 반복 재생 - _videoController.setVolume(0.0); // 🔇 크롬 자동재생 정책 우회 - _videoController.play(); // ▶️ 자동 재생 시작 - }).catchError((error) { - debugPrint("1차 웹 비디오 로딩 실패, 백업 경로 시도 중...: $error"); - - _videoController = VideoPlayerController.asset('assets/videos/basic.mp4'); - _videoController.initialize().then((_) { - if (!mounted) return; - setState(() { - _isVideoInitialized = true; - }); - _videoController.setLooping(true); - _videoController.setVolume(0.0); - _videoController.play(); - }).catchError((retryError) { - if (!mounted) return; - setState(() { - _hasVideoError = true; - }); - debugPrint("최종 비디오 초기화 에러: $retryError"); - }); - }); - } - - @override - void dispose() { - _videoController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 🤖 1. 좌측 영역: 푸미 캐릭터 비디오 화면 (✨ flex: 60 으로 수정) - Expanded( - flex: 60, - child: GestureDetector( - onTap: widget.onCharacterTap, - child: _buildCharacterCard(), - ), - ), - const SizedBox(width: 20), - - // 📊 2. 우측 영역: 센서 데이터 모음 (✨ flex: 40 으로 수정) - Expanded( - flex: 40, - child: Column( - children: [ - Expanded( - flex: 10, - child: Row( - children: [ - Expanded(child: _buildSensorCard('온도', '24.5°C', Icons.thermostat, Colors.orangeAccent)), - const SizedBox(width: 16), - Expanded(child: _buildSensorCard('습도', '65%', Icons.water_drop, Colors.blueAccent)), - ], - ), - ), - const SizedBox(height: 16), - Expanded( - flex: 10, - child: Row( - children: [ - Expanded(child: _buildSensorCard('이산화탄소', '420 ppm', Icons.cloud, Colors.greenAccent)), - const SizedBox(width: 16), - Expanded(child: _buildSensorCard('TVOC', '0.15 mg/m³', Icons.biotech, Colors.tealAccent)), - ], - ), - ), - const SizedBox(height: 16), - Expanded( - flex: 11, - child: _buildDustCard(), - ), - ], - ), - ), - ], - ); - } - - // 🤖 [좌측] 캐릭터 전용 카드 빌더 - Widget _buildCharacterCard() { - return Container( - decoration: BoxDecoration( - color: const Color(0xFF1F242C), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white10), - ), - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildMiniInfo(Icons.eco_outlined, '학습된 식물: 51종'), - const SizedBox(width: 20), - _buildMiniInfo(Icons.trending_up, '예측 정확도: 98.7%'), - ], - ), - - // 🎬 중앙 로봇 에셋 비디오 플레이어 - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (_hasVideoError) - const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.video_library_outlined, size: 80, color: Colors.white24), - SizedBox(height: 12), - Text('비디오 에셋을 로드할 수 없습니다.', style: TextStyle(color: Colors.white38, fontSize: 13)), - ], - ) - else if (_isVideoInitialized) - SizedBox( - width: 600, - child: AspectRatio( - aspectRatio: _videoController.value.aspectRatio, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: VideoPlayer(_videoController), - ), - ), - ) - else - const SizedBox( - height: 280, - child: Center(child: CircularProgressIndicator(color: Color(0xFFB4E49C))), - ), - const SizedBox(height: 16), - - // 안내 뱃지 문구 - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: const Color(0xFFB4E49C).withOpacity(0.06), - borderRadius: BorderRadius.circular(20), - border: Border.all(color: const Color(0xFFB4E49C).withOpacity(0.2)), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.touch_app_outlined, size: 14, color: Color(0xFFB4E49C)), - SizedBox(width: 6), - Text( - '여기를 클릭하면 AI 채팅창으로 이동합니다.', - style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold), - ), - ], - ), - ), - ], - ), - ), - ), - - const Divider(color: Colors.white10, height: 24), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('푸미', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white)), - const SizedBox(height: 4), - Row( - children: [ - const Text('상태: ', style: TextStyle(color: Colors.white54, fontSize: 13)), - Text('최상 - 모니터링 중', style: TextStyle(color: const Color(0xFFB4E49C), fontSize: 13, fontWeight: FontWeight.bold)), - ], - ), - ], - ), - ], - ), - ], - ), - ); - } - - // 📊 센서 카드 빌더 - Widget _buildSensorCard(String title, String value, IconData icon, Color accentColor) { - return Container( - decoration: BoxDecoration( - color: const Color(0xFF1F242C), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white10), - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: const TextStyle(fontSize: 16, color: Colors.white70, fontWeight: FontWeight.w500)), - Icon(icon, color: accentColor.withOpacity(0.8), size: 22), - ], - ), - Text(value, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)), - Container( - height: 4, - width: double.infinity, - decoration: BoxDecoration(color: Colors.white10, borderRadius: BorderRadius.circular(2)), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: 0.65, - child: Container(decoration: BoxDecoration(color: accentColor, borderRadius: BorderRadius.circular(2))), - ), - ) - ], - ), - ); - } - - // 💨 미세먼지 카드 빌더 - Widget _buildDustCard() { - return Container( - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFF1F242C), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white10), - ), - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('미세먼지', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Row( - children: [ - Icon(Icons.air, color: Colors.white54, size: 36), - SizedBox(width: 16), - Text('15', style: TextStyle(fontSize: 52, fontWeight: FontWeight.bold, color: Colors.white)), - SizedBox(width: 4), - Text('µg/m³', style: TextStyle(fontSize: 14, color: Colors.white38)), - ], - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.03), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white10), - ), - child: const Row( - children: [ - Icon(Icons.check_circle, color: Colors.greenAccent, size: 28), - SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('PM2.5 : 1.50', style: TextStyle(color: Colors.white70, fontSize: 13)), - SizedBox(height: 2), - Text('PM10 : 1.30', style: TextStyle(color: Colors.white70, fontSize: 13)), - ], - ) - ], - ), - ) - ], - ), - const SizedBox(height: 2), - ], - ), - ); - } - - Widget _buildMiniInfo(IconData icon, String text) { - return Row( - children: [ - Icon(icon, size: 14, color: const Color(0xFFB4E49C)), - const SizedBox(width: 6), - Text(text, style: const TextStyle(color: Colors.white54, fontSize: 12)), - ], - ); - } -} \ No newline at end of file diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart deleted file mode 100644 index 0f9b10d..0000000 --- a/lib/widgets/sidebar.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/material.dart'; - -class Sidebar extends StatelessWidget { - final bool isSidebarOpen; - final String selectedPage; - final Function(String) onPageChanged; - - const Sidebar({ - super.key, - required this.isSidebarOpen, - required this.selectedPage, - required this.onPageChanged, - }); - - @override - Widget build(BuildContext context) { - return Container( - width: 260, - color: const Color(0xFF181C22), // 어두운 테마 배경색 유지 - padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 8.0), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - curve: isSidebarOpen ? const Interval(0.6, 1.0, curve: Curves.easeIn) : Curves.easeOut, - opacity: isSidebarOpen ? 1.0 : 0.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 최상단 앱 타이틀 - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'Smart Garden', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Color(0xFFB4E49C), // 페이모바일 포인트 컬러 유지 - ), - ), - ), - const SizedBox(height: 32), - - // 📜 대분류 및 하위 메뉴 영역 - Expanded( - child: ListView( - physics: const BouncingScrollPhysics(), - children: [ - // 1. Dashboard 그룹 - _buildExpansionMenu( - icon: Icons.dashboard_outlined, - title: 'Dashboard', - children: [ - _buildSubMenuItem('스마트가든 챗봇'), - ], - ), - - // 2. 데이터 조회 그룹 - _buildExpansionMenu( - icon: Icons.analytics_outlined, - title: '데이터 조회', - initiallyExpanded: true, // 자주 보는 데이터 조회를 기본으로 열어둡니다. - children: [ - _buildSubMenuItem('온도 데이터'), - _buildSubMenuItem('습도 데이터'), - _buildSubMenuItem('이산화탄소 데이터'), - _buildSubMenuItem('TVOC 데이터'), - _buildSubMenuItem('미세먼지 데이터'), - ], - ), - - // 3. 장비 관리 (하위 메뉴가 없는 단일 대분류 버튼) - _buildSingleMenu( - icon: Icons.widgets_outlined, - title: '장비 관리', - ), - ], - ), - ), - - // 하단 로그아웃 버튼 - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFB4E49C), - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - child: const Text('Log Out', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - ), - ], - ), - ), - ); - } - - // ✨ 그룹형 (대분류 > 소분류) 메뉴를 만드는 빌더 - Widget _buildExpansionMenu({ - required IconData icon, - required String title, - required List children, - bool initiallyExpanded = false, - }) { - return Theme( - // ExpansionTile 고유의 상하단 구분선(Border)을 제거하는 테마 트릭 - data: ThemeData.dark().copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - initiallyExpanded: initiallyExpanded, - leading: Icon(icon, color: Colors.white70, size: 22), - title: Text( - title, - style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold), - ), - trailing: const Icon(Icons.keyboard_arrow_down, color: Colors.white38, size: 18), - childrenPadding: const EdgeInsets.only(left: 16.0), // 하위 메뉴 들여쓰기 구조 설정 - children: children, - ), - ); - } - - // ✨ 하위(소분류) 메뉴 아이템 빌더 - Widget _buildSubMenuItem(String title) { - bool isSelected = (title == selectedPage); - - return Container( - margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 8), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFFB4E49C).withOpacity(0.15) : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: ListTile( - title: Text( - title, - style: TextStyle( - color: isSelected ? const Color(0xFFB4E49C) : Colors.white60, - fontSize: 14, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - dense: true, - visualDensity: const VisualDensity(vertical: -2), // 메뉴 간격을 좀 더 콤팩트하게 조절 - onTap: () => onPageChanged(title), - ), - ); - } - - // ✨ 하위 메뉴가 없는 단독 대분류 메뉴 빌더 (ex: 장비 관리) - Widget _buildSingleMenu({required IconData icon, required String title}) { - bool isSelected = (title == selectedPage); - - return Container( - margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFFB4E49C).withOpacity(0.15) : Colors.transparent, - borderRadius: BorderRadius.circular(8), - ), - child: ListTile( - leading: Icon(icon, color: isSelected ? const Color(0xFFB4E49C) : Colors.white70, size: 22), - title: Text( - title, - style: TextStyle( - color: isSelected ? const Color(0xFFB4E49C) : Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - dense: true, - onTap: () => onPageChanged(title), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/temp_data_page.dart b/lib/widgets/temp_data_page.dart deleted file mode 100644 index 025a592..0000000 --- a/lib/widgets/temp_data_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class TempDataPage extends StatelessWidget { - const TempDataPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '🌡️ 실시간 온도 데이터 수집 분석 현황', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/lib/widgets/tvoc_data_page.dart b/lib/widgets/tvoc_data_page.dart deleted file mode 100644 index 5083e1f..0000000 --- a/lib/widgets/tvoc_data_page.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class TvocDataPage extends StatelessWidget { - const TvocDataPage({super.key}); - - @override - Widget build(BuildContext context) { - return const Center( - child: Text( - '🧪 실시간 총휘발성유기화합물(TVOC) 측정값', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white60), - ), - ); - } -} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d7dddf8..18589ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,21 +62,13 @@ flutter: uses-material-design: true assets: - - assets/videos/angry.mp4 - - assets/videos/basic.mp4 - - assets/videos/cold.mp4 + - assets/videos/default.mp4 - assets/videos/joy.mp4 - - assets/videos/know.mp4 - assets/videos/thinking.mp4 + - assets/videos/realization.mp4 + - assets/videos/angry.mp4 - assets/videos/thirst.mp4 - - - assets/videos/angry_img.mp4 - - assets/videos/basic_img.mp4 - - assets/videos/cold_img.mp4 - - assets/videos/joy_img.mp4 - - assets/videos/know_img.mp4 - - assets/videos/thinking_img.mp4 - - assets/videos/thirst_img.mp4 + - assets/videos/cold.mp4 - assets/images/profile.png - assets/images/background.png