Compare commits

..

10 Commits

Author SHA1 Message Date
b871ea03b0 수정 2026-06-29 09:06:04 +09:00
1140553c00 영상 크기 조절 2026-06-26 15:40:04 +09:00
c0fb4d02d5 크기 1차 수정 2026-06-26 15:14:58 +09:00
8ea5c911e7 개발서버 주소 변경 2026-06-25 17:51:36 +09:00
756453d144 readme.md 파일 수정 2026-06-25 11:39:34 +09:00
68db5b7b45 ã…감정표현 2회 재생 후 기본값으로 ë³돌 변경 2026-06-24 17:55:03 +09:00
44100e387b 감정표현, 채팅응답대기 추가 2026-06-24 15:31:06 +09:00
KIMGYEONGRAN
8465be1d06 디자인 2차 초안 2026-06-23 11:14:41 +09:00
989910a3ff 서버 연결 ã…코드 작성ì중 2026-06-19 15:39:15 +09:00
f899d52f66 디자인 1차 초안 완성 2026-06-19 11:45:24 +09:00
42 changed files with 1368 additions and 1879 deletions

View File

@@ -1,16 +1,5 @@
# smartgarden_chat
# 스마트가든 챗봇
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
- 플러터 서버 실행 코드
- flutter run -d web-server --web-hostname 0.0.0.0 --web-port 8080
- 예시) flutter run -d web-server --web-hostname 49.238.167.72 --web-port 8080

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

BIN
assets/images/chat_img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
assets/images/layer_img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

BIN
assets/images/send.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'screens/chat_screen.dart';
void main() {
runApp(const SmartGardenApp());
@@ -12,760 +12,12 @@ class SmartGardenApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Smart Garden',
theme: ThemeData(primarySwatch: Colors.green, useMaterial3: true),
home: const SmartGardenScreen(),
theme: ThemeData(
primarySwatch: Colors.green,
useMaterial3: true,
),
home: const ChatScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class ChatMessage {
final String text;
final bool isUser;
final DateTime timestamp;
ChatMessage({
required this.text,
required this.isUser,
required this.timestamp,
});
}
class SmartGardenScreen extends StatefulWidget {
const SmartGardenScreen({Key? key}) : super(key: key);
@override
State<SmartGardenScreen> createState() => _SmartGardenScreenState();
}
class _SmartGardenScreenState extends State<SmartGardenScreen> {
final TextEditingController _textController = TextEditingController();
final List<ChatMessage> _messages = [];
final ScrollController _scrollController = ScrollController();
late VideoPlayerController _videoController;
bool _isVideoInitialized = false;
@override
void initState() {
super.initState();
_initializeVideo();
_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');
});
}
void _addInitialMessage() {
setState(() {
_messages.add(
ChatMessage(
text: '상태 분석이 완료되었습니다.\n무엇을 도와드릴까요?',
isUser: false,
timestamp: DateTime.now(),
),
);
});
}
void _sendMessage(String text) {
if (text.isEmpty) return;
// 사용자 메시지 추가
setState(() {
_messages.add(
ChatMessage(text: text, isUser: true, timestamp: DateTime.now()),
);
});
_textController.clear();
_scrollToBottom();
// 봇 응답 (0.5초 딜레이)
Future.delayed(const Duration(milliseconds: 500), () {
setState(() {
_messages.add(
ChatMessage(
text: '안녕하세요 스마트가든 AI 가이드 푸미입니다',
isUser: false,
timestamp: DateTime.now(),
),
);
});
_scrollToBottom();
});
}
void _scrollToBottom() {
Future.delayed(const Duration(milliseconds: 100), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
void _sendQuickQuestion(String question) {
_textController.text = question;
_sendMessage(question);
}
@override
void dispose() {
_textController.dispose();
_scrollController.dispose();
_videoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background1.png'),
fit: BoxFit.cover,
),
),
child: Center(
child: Container(
margin: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
),
child: Row(
children: [
// 좌측 캐릭터 영역 (60%)
Expanded(
flex: 6,
child: Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
),
child: Column(
children: [
// 헤더
Padding(
padding: const EdgeInsets.all(30),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Color(0xFFC8E6C9),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'🌱',
style: TextStyle(fontSize: 28),
),
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Smart Garden',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
Text(
'스마트가든 챗봇',
style: TextStyle(
fontSize: 16,
color: Color(0xFF757575),
),
),
],
),
],
),
),
// 캐릭터 영역
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(12),
),
child: _isVideoInitialized
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio:
_videoController.value.aspectRatio,
child: VideoPlayer(_videoController),
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 180,
height: 180,
decoration: BoxDecoration(
color: Color(0xFFF1F8E9),
borderRadius: BorderRadius.circular(
12,
),
border: Border.all(
color: Color(0xFFC8E6C9),
width: 1,
),
),
child: const Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Color(0xFF81C784),
),
SizedBox(height: 20),
Text(
'비디오 로딩 중...',
style: TextStyle(
fontSize: 12,
color: Color(0xFF757575),
),
),
],
),
),
),
],
),
),
),
const SizedBox(height: 40),
// 캐릭터 정보 + 설명 문구
Padding(
padding: const EdgeInsets.only(
left: 30,
bottom: 30,
right: 30,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 푸미 정보
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Color(0xFFC8E6C9),
// ← 연한 초록 배경 추가
borderRadius: BorderRadius.circular(24),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Image.asset(
'assets/images/profile.png',
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'푸미',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
),
Text(
'스마트가든 AI 가이드',
style: TextStyle(
fontSize: 14,
color: Color(0xFF757575),
),
),
],
),
],
),
const SizedBox(height: 16),
// 설명 문구 (반응형)
Container(
width: double.infinity, // ← 추가: 양쪽 꽉차게
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(0xFFF1F8E9),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFFC8E6C9),
width: 1,
),
),
child: const Text(
'안녕하세요!\n스마트가든의 식물들이 건강하게 자랄 수 있도록 도와드릴게요.\n궁금한 점을 언제든 물어보세요!',
style: TextStyle(
fontSize: 13,
color: Color(0xFF558B2F),
height: 1.6,
),
),
),
],
),
),
],
),
),
),
// 우측 채팅 영역 (40%)
Expanded(
flex: 4,
child: Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Color(0xFFE0E0E0), width: 1),
),
child: Column(
children: [
// 헤더 (왼쪽정렬)
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
border: Border(
bottom: BorderSide(
color: Color(0xFFE0E0E0),
width: 1,
),
),
),
child: const Text(
'푸미와 대화하기',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
),
textAlign: TextAlign.left, // ← 왼쪽정렬
),
),
// 채팅 메시지 영역
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFFE0E0E0),
width: 0.5,
),
),
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _ChatBubble(message: message);
},
),
),
),
// 자주하는 질문 버튼들 (채팅 아래, 입력창 위)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
// 온도 정보
Expanded(
child: Container(
decoration: BoxDecoration(
color: Color(0xFFBBDEFB),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFF1976D2).withOpacity(0.2),
width: 0.5,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
_sendQuickQuestion('현재 온도는?'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'🌡️',
style: TextStyle(fontSize: 20),
),
const SizedBox(width: 8),
Text(
'온도 정보',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF1976D2),
),
),
],
),
),
),
),
),
),
const SizedBox(width: 8),
// 습도 정보
Expanded(
child: Container(
decoration: BoxDecoration(
color: Color(0xFFC8E6C9),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFF388E3C).withOpacity(0.2),
width: 0.5,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
_sendQuickQuestion('현재 습도는?'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'💧',
style: TextStyle(fontSize: 20),
),
const SizedBox(width: 8),
Text(
'습도 정보',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF388E3C),
),
),
],
),
),
),
),
),
),
const SizedBox(width: 8),
// 물 주기
Expanded(
child: Container(
decoration: BoxDecoration(
color: Color(0xFFFFE0B2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFFFFA000).withOpacity(0.2),
width: 0.5,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _sendQuickQuestion('물을 주세요'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Text(
'💦',
style: TextStyle(fontSize: 20),
),
const SizedBox(width: 8),
Text(
'물 주기',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFFFFA000),
),
),
],
),
),
),
),
),
),
],
),
),
// 입력 필드 + 전송 버튼 (같은 줄)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Row(
children: [
// 입력 필드
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(0xFFE0E0E0),
width: 0.5,
),
),
child: TextField(
controller: _textController,
onSubmitted: (value) {
_sendMessage(
_textController.text,
); // 엔터로 전송
},
decoration: InputDecoration(
hintText: '메시지를 입력하세요...',
hintStyle: TextStyle(
fontSize: 20,
color: Color(0xFF9E9E9E),
),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
style: const TextStyle(fontSize: 20),
maxLines: 1,
),
),
),
const SizedBox(width: 12),
// 전송 버튼
SizedBox(
height: 48,
width: 60,
child: ElevatedButton(
onPressed: () {
_sendMessage(_textController.text);
},
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF81C784),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.zero,
),
child: const Text(
'전송',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
],
),
),
],
),
),
),
],
),
),
),
),
);
}
}
class _ChatBubble extends StatelessWidget {
final ChatMessage message;
const _ChatBubble({required this.message});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: message.isUser
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
if (!message.isUser) ...[
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Color(0xFFC8E6C9), // ← 연한 초록 배경
borderRadius: BorderRadius.circular(20), // ← 원형 (8 → 20)
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20), // ← 원형 (8 → 20)
child: Image.asset(
'assets/images/profile.png',
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 10),
],
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: message.isUser
? Color(0xFFE8F5E9) // ← 연한 녹색
: Colors.white, // ← 하얀색
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1), // ← 연한 그림자
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: SelectableText(
message.text,
style: TextStyle(
fontSize: 20,
color: message.isUser
? Color(0xFF000000) // ← 진한 초록 텍스트
: Color(0xFF424242),
height: 1.4,
),
),
),
),
if (message.isUser) const SizedBox(width: 8),
],
),
);
}
}
class _QuickButton extends StatelessWidget {
final String emoji;
final String title;
final String subtitle;
final Color bgColor;
final Color textColor;
final VoidCallback onPressed;
const _QuickButton({
required this.emoji,
required this.title,
required this.subtitle,
required this.bgColor,
required this.textColor,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(8),
child: Container(
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: textColor.withOpacity(0.2), width: 0.5),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Column(
children: [
Text(emoji, style: const TextStyle(fontSize: 18)),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: textColor,
),
textAlign: TextAlign.center,
),
if (subtitle.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
subtitle,
style: TextStyle(
fontSize: 10,
color: textColor.withOpacity(0.8),
),
textAlign: TextAlign.center,
),
],
],
),
),
),
);
}
}

View File

@@ -0,0 +1,33 @@
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로 변환
factory ChatMessage.fromJson(Map<String, dynamic> json) {
return ChatMessage(
text: json['text'] as String,
isUser: json['isUser'] as bool,
timestamp: DateTime.parse(json['timestamp'] as String),
emotionCategory: json['emotionCategory'] as int?,
);
}
// ChatMessage를 JSON으로 변환
Map<String, dynamic> toJson() {
return {
'text': text,
'isUser': isUser,
'timestamp': timestamp.toIso8601String(),
'emotionCategory': emotionCategory,
};
}
}

1122
lib/screens/chat_screen.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,167 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/chat_message.dart';
class ChatService {
// 서버 URL
static const String serverUrl = 'http://49.238.167.122:8000/chat/';
// 타임아웃 설정 (초)
static const int timeoutSeconds = 30;
/// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드
/// 반환값: (메시지, emotion_category) 튜플
static Future<(String, int)> sendMessage(String userMessage) async {
try {
// 요청 생성
final requestBody = {
'device_id': 1,
'locale': 'ko-KR',
'message': userMessage,
'session_id': 'session-001',
'user_id': 'user-123',
};
print('서버로 요청 전송: $requestBody');
// POST 요청 전송
final response = await http.post(
Uri.parse(serverUrl),
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
},
body: jsonEncode(requestBody),
).timeout(
const Duration(seconds: timeoutSeconds),
onTimeout: () => throw TimeoutException('서버 응답 시간 초과'),
);
print('서버 응답 상태코드: ${response.statusCode}');
print('서버 응답 본문: ${response.body}');
// 상태 코드 확인
if (response.statusCode == 200) {
// 성공
final jsonResponse = jsonDecode(response.body);
// 응답에서 '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');
print('감정 카테고리: $emotionCategory');
return (responseMessage, emotionCategory);
} else {
print('응답이 비어있습니다');
return ('응답을 받지 못했습니다', 1);
}
} else {
// 실패
print('서버 오류: ${response.statusCode}');
return ('서버 오류 (${response.statusCode})', 1);
}
} on TimeoutException catch (e) {
print('타임아웃: $e');
return ('서버 응답이 없습니다. 시간 초과', 1);
} on FormatException catch (e) {
print('JSON 파싱 오류: $e');
return ('JSON 형식 오류', 1);
} catch (e) {
print('오류 발생: $e');
return ('오류가 발생했습니다: $e', 1);
}
}
/// 서버 연결 테스트
static Future<bool> testConnection() async {
try {
print('서버 연결 테스트 중...');
final response = await http.get(
Uri.parse(serverUrl),
).timeout(
const Duration(seconds: timeoutSeconds),
);
if (response.statusCode == 200 || response.statusCode == 405) {
print('서버 연결 성공');
return true;
} else {
print('서버 연결 실패: ${response.statusCode}');
return false;
}
} catch (e) {
print('서버 연결 오류: $e');
return false;
}
}
/// 서버 상태 상세 테스트
static Future<void> 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');
}
}
/// 타임아웃 예외
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
@override
String toString() => message;
}

View File

@@ -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<ChatbotDetailPage> createState() => _ChatbotDetailPageState();
}
class _ChatbotDetailPageState extends State<ChatbotDetailPage> {
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)),
],
),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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<List<double>> 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<List<double>>(
stream: stream,
builder: (context, snapshot) {
final data = snapshot.data ?? const <double>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(data),
const SizedBox(height: 20),
Expanded(child: _buildChart(data)),
],
);
},
),
);
}
// 🏷️ 헤더: 제목 + 현재값 + 아이콘 (센서 카드와 동일한 톤)
Widget _buildHeader(List<double> 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<double> data) {
if (data.isEmpty) {
return const Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFB4E49C),
),
),
);
}
final spots = <FlSpot>[
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),
],
),
),
),
],
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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),
],
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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<DashboardGrid> createState() => _DashboardGridState();
}
class _DashboardGridState extends State<DashboardGrid> {
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)),
],
);
}
}

View File

@@ -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<Widget> 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),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -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),
),
);
}
}

View File

@@ -112,6 +112,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
@@ -237,6 +253,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.6"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:

View File

@@ -28,6 +28,8 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
http: ^1.1.0
flutter:
sdk: flutter
@@ -60,27 +62,20 @@ 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/videos/shy.mp4
- assets/images/profile.png
- assets/images/background.png
- assets/images/background1.png
- assets/images/background2.png
- assets/images/background3.png
- assets/images/layer_img.png
- assets/images/chat_img.png
- assets/images/send.png
# To add assets to your application, add an assets section, like this:
# assets:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -27,7 +27,7 @@
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="icon" type="image/png" href="icons/favicon.png"/>
<title>smartgarden_chat</title>
<link rel="manifest" href="manifest.json">