서버 연결 ã 코드 작성ì중
This commit is contained in:
890
lib/main.dart
890
lib/main.dart
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'screens/chat_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const SmartGardenApp());
|
runApp(const SmartGardenApp());
|
||||||
@@ -12,890 +12,12 @@ class SmartGardenApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Smart Garden',
|
title: 'Smart Garden',
|
||||||
theme: ThemeData(primarySwatch: Colors.green, useMaterial3: true),
|
theme: ThemeData(
|
||||||
home: const SmartGardenScreen(),
|
primarySwatch: Colors.green,
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
home: const ChatScreen(),
|
||||||
debugShowCheckedModeBanner: false,
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.15.6"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +253,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ environment:
|
|||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
# versions available, run `flutter pub outdated`.
|
# versions available, run `flutter pub outdated`.
|
||||||
dependencies:
|
dependencies:
|
||||||
|
http: ^1.1.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user