서버 연결 ã 코드 작성ì중
This commit is contained in:
892
lib/main.dart
892
lib/main.dart
@@ -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,890 +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: Stack(
|
||||
children: [
|
||||
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.withOpacity(0.8), // 투명도 넣기
|
||||
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(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.eco,
|
||||
color: Color(0xFF81C784),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
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(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/chat_img.png',
|
||||
),
|
||||
// ← 배경 이미지 추가
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white.withOpacity(0.9),
|
||||
// 채팅 배경 이미지 투명도 조정
|
||||
BlendMode.lighten,
|
||||
),
|
||||
),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
// 채팅창 투명도
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// 자주하는 질문 버튼들을 감싸는 컨테이너
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFC8E6C9).withOpacity(0.3), // 배경
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 온도 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFF1976D2,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('현재 온도는?'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.thermostat,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'온도 정보',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 습도 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFF388E3C,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('현재 습도는?'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.water_drop,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'습도 정보',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 물 주기
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFFFFA000,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('물을 주세요'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.waves,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'물 주기',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 입력 필드 + 전송 버튼 (같은 줄)
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Layer 이미지 (화면 전체, 클릭 불가)
|
||||
IgnorePointer(
|
||||
child: Image.asset(
|
||||
'assets/images/layer_img1.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatBubble extends StatelessWidget {
|
||||
final ChatMessage message;
|
||||
|
||||
const _ChatBubble({required this.message});
|
||||
|
||||
String _formatTime(DateTime dateTime) {
|
||||
final hour = dateTime.hour;
|
||||
final minute = dateTime.minute;
|
||||
final period = hour >= 12 ? '오후' : '오전';
|
||||
final displayHour = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour);
|
||||
|
||||
return '$period ${displayHour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30), // 채팅창 사이 간격 여백 주기
|
||||
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: Row(
|
||||
mainAxisAlignment: message.isUser
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (message.isUser)
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)),
|
||||
),
|
||||
if (message.isUser) const SizedBox(width: 8),
|
||||
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),
|
||||
if (!message.isUser)
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
lib/models/chat_message.dart
Normal file
29
lib/models/chat_message.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
class ChatMessage {
|
||||
final String text;
|
||||
final bool isUser;
|
||||
final DateTime timestamp;
|
||||
|
||||
ChatMessage({
|
||||
required this.text,
|
||||
required this.isUser,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
|
||||
// ChatMessage를 JSON으로 변환
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'text': text,
|
||||
'isUser': isUser,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
831
lib/screens/chat_screen.dart
Normal file
831
lib/screens/chat_screen.dart
Normal file
@@ -0,0 +1,831 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import '../models/chat_message.dart';
|
||||
import '../services/chat_service.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
const ChatScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final List<ChatMessage> _messages = [];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
late VideoPlayerController _videoController;
|
||||
bool _isVideoInitialized = false;
|
||||
bool _isLoading = 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) async {
|
||||
if (text.isEmpty) return;
|
||||
|
||||
// 사용자 메시지 추가
|
||||
setState(() {
|
||||
_messages.add(
|
||||
ChatMessage(text: text, isUser: true, timestamp: DateTime.now()),
|
||||
);
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
_textController.clear();
|
||||
_scrollToBottom();
|
||||
|
||||
// 서버로 메시지 전송
|
||||
try {
|
||||
final response = await ChatService.sendMessage(text);
|
||||
|
||||
setState(() {
|
||||
_messages.add(
|
||||
ChatMessage(
|
||||
text: response,
|
||||
isUser: false,
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_isLoading = false;
|
||||
});
|
||||
_scrollToBottom();
|
||||
} catch (e) {
|
||||
print('메시지 전송 오류: $e');
|
||||
setState(() {
|
||||
_messages.add(
|
||||
ChatMessage(
|
||||
text: '오류가 발생했습니다. 다시 시도해주세요.',
|
||||
isUser: false,
|
||||
timestamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
_isLoading = false;
|
||||
});
|
||||
_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: Stack(
|
||||
children: [
|
||||
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.withOpacity(0.8), // 투명도 넣기
|
||||
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(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.eco,
|
||||
color: Color(0xFF81C784),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
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(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'assets/images/chat_img.png',
|
||||
),
|
||||
// ← 배경 이미지 추가
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white.withOpacity(0.9),
|
||||
// 채팅 배경 이미지 투명도 조정
|
||||
BlendMode.lighten,
|
||||
),
|
||||
),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
// 채팅창 투명도
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
// 자주하는 질문 버튼들을 감싸는 컨테이너
|
||||
Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFC8E6C9).withOpacity(0.3), // 배경
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 온도 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFF1976D2,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('현재 온도는?'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.thermostat,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'온도 정보',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 습도 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFF388E3C,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('현재 습도는?'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.water_drop,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'습도 정보',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 물 주기
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // ← 흰색
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: Color(
|
||||
0xFFFFA000,
|
||||
).withOpacity(0.2),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_sendQuickQuestion('물을 주세요'),
|
||||
borderRadius: BorderRadius.circular(
|
||||
25,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
6,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.waves,
|
||||
color: Color(0xFF4CAF50),
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'물 주기',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 입력 필드 + 전송 버튼 (같은 줄)
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Layer 이미지 (화면 전체)
|
||||
IgnorePointer(
|
||||
child: Image.asset(
|
||||
'assets/images/layer_img1.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatBubble extends StatelessWidget {
|
||||
final ChatMessage message;
|
||||
|
||||
const _ChatBubble({required this.message});
|
||||
|
||||
String _formatTime(DateTime dateTime) {
|
||||
final hour = dateTime.hour;
|
||||
final minute = dateTime.minute;
|
||||
final period = hour >= 12 ? '오후' : '오전';
|
||||
final displayHour = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour);
|
||||
|
||||
return '$period ${displayHour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
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),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.asset(
|
||||
'assets/images/profile.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Flexible(
|
||||
child: Row(
|
||||
mainAxisAlignment: message.isUser
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (message.isUser)
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)),
|
||||
),
|
||||
if (message.isUser) const SizedBox(width: 8),
|
||||
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),
|
||||
if (!message.isUser)
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF9E9E9E)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
117
lib/services/chat_service.dart
Normal file
117
lib/services/chat_service.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
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.71:8000/chat/';
|
||||
|
||||
// 타임아웃 설정 (초)
|
||||
static const int timeoutSeconds = 30;
|
||||
|
||||
/// 사용자 메시지를 서버로 전송하고 응답을 받는 메서드
|
||||
static Future<String> sendMessage(String userMessage) async {
|
||||
try {
|
||||
// 요청 생성
|
||||
final requestBody = {
|
||||
'device_id': 1,
|
||||
'locale': 'ko-KR',
|
||||
'message': userMessage,
|
||||
'session_id': 'session-001',
|
||||
'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)}');
|
||||
|
||||
// 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);
|
||||
|
||||
print('전체 응답: $jsonResponse');
|
||||
|
||||
// 'message' 필드 추출
|
||||
final responseMessage = jsonResponse['message'] as String?;
|
||||
|
||||
if (responseMessage != null && responseMessage.isNotEmpty) {
|
||||
print('응답 메시지: $responseMessage');
|
||||
return responseMessage;
|
||||
} else {
|
||||
print('message 필드가 없습니다');
|
||||
// 혹시 다른 필드에 응답이 있는지 확인
|
||||
print('응답 전체: ${jsonResponse.toString()}');
|
||||
return '응답을 받지 못했습니다';
|
||||
}
|
||||
} else {
|
||||
// 실패
|
||||
print('서버 오류: ${response.statusCode}');
|
||||
return '서버 오류 (${response.statusCode})';
|
||||
}
|
||||
} on TimeoutException catch (e) {
|
||||
print('타임아웃: $e');
|
||||
return '서버 응답이 없습니다. 시간 초과';
|
||||
} on FormatException catch (e) {
|
||||
print('JSON 파싱 오류: $e');
|
||||
return 'JSON 형식 오류';
|
||||
} catch (e) {
|
||||
print('오류 발생: $e');
|
||||
return '오류가 발생했습니다: $e';
|
||||
}
|
||||
}
|
||||
|
||||
/// 서버 연결 테스트
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 타임아웃 예외
|
||||
class TimeoutException implements Exception {
|
||||
final String message;
|
||||
TimeoutException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
24
pubspec.lock
24
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user