1. 하단 네비게이션 바 수정

2. 대여소 지도 및 리스트 화면 제작 (RentReturnScreen)
3. 대여/반납 상세 진행 화면 구현 (RentalProcessScreen)
This commit is contained in:
KIMGYEONGRAN
2025-11-25 17:51:50 +09:00
parent 02fd25264d
commit 32daaad975
7 changed files with 712 additions and 28 deletions

View File

@@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class AlertsReportScreen extends StatelessWidget {
const AlertsReportScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'4. 알림/신고 (Alerts/Report) 페이지',
style: TextStyle(color: Colors.white70, fontSize: 20),
),
);
}
}

View File

@@ -14,7 +14,6 @@ class HomeScreenContent extends StatefulWidget {
class _HomeScreenContentState extends State<HomeScreenContent> {
static const double _uniformGap = 16.0;
// 포인트 컬러 (배터리, 상태 표시 등에는 유지)
final Color _pointColor = Colors.redAccent;
final Map<String, bool> _controlToggles = {
@@ -117,8 +116,8 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Image.asset(
'assets/images/helmet.png',
width: 100,
'assets/images/open.png',
width: 120,
),
),
Positioned(
@@ -381,7 +380,6 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
height: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
// ⭐ [수정됨] ON(화이트), OFF(다크그레이) -> 블랙 앤 화이트 테마 적용
color: value ? Colors.white : Colors.grey.shade700,
),
child: Stack(
@@ -397,7 +395,6 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
height: 26,
decoration: BoxDecoration(
shape: BoxShape.circle,
// ⭐ [수정됨] ON(블랙), OFF(화이트) -> 배경과 대비되는 색상
color: value ? const Color(0xFF27292B) : Colors.white,
),
),
@@ -411,7 +408,6 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
// ⭐ [수정됨] ON 텍스트 블랙
color: value
? const Color(0xFF27292B)
: Colors.transparent)))),
@@ -564,6 +560,7 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
TileLayer(
urlTemplate:
'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
userAgentPackageName: 'com.example.app',
subdomains: const ['a', 'b', 'c', 'd'],
retinaMode: true,
),
@@ -636,10 +633,8 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
style: const TextStyle(fontSize: 11, color: Colors.white70));
}
}
// --- _HomeScreenContentState 끝 ---
// 👇 HOME 화면에 사용되는 4가지 커스텀 클래스 정의
class _LineChartPlaceholder extends StatelessWidget {
const _LineChartPlaceholder();

View File

@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import 'package:smarthelmet_app/home_screen_content.dart';
import 'package:smarthelmet_app/control_screen.dart';
import 'package:smarthelmet_app/history_screen.dart';
import 'package:smarthelmet_app/alerts_report_screen.dart';
import 'package:smarthelmet_app/rent_return_screen.dart';
import 'package:smarthelmet_app/settings_screen.dart';
void main() {
@@ -51,7 +51,7 @@ class _HomeScreenState extends State<HomeScreen> {
const HomeScreenContent(),
const HistoryScreen(),
const ControlScreen(),
const AlertsReportScreen(),
const RentReturnScreen(),
const SettingsScreen(),
];
@@ -91,11 +91,11 @@ class _HomeScreenState extends State<HomeScreen> {
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'HOME'),
BottomNavigationBarItem(icon: Icon(Icons.history), label: 'HISTORY'),
BottomNavigationBarItem(icon: Icon(Icons.settings_input_component), label: 'CONTROL'),
BottomNavigationBarItem(icon: Icon(Icons.notifications_active), label: 'ALERTS'),
BottomNavigationBarItem(icon: Icon(Icons.assignment_return_outlined), label: 'RENT/RETURN'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'SETTINGS'),
],
),
),
);
}
}
}

356
lib/rent_return_screen.dart Normal file
View File

@@ -0,0 +1,356 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'rental_process_screen.dart';
class StationInfo {
final String name;
final String address;
final String status;
final String distance;
StationInfo({
required this.name,
required this.address,
required this.status,
required this.distance,
});
}
class RentReturnScreen extends StatelessWidget {
const RentReturnScreen({super.key});
@override
Widget build(BuildContext context) {
final LatLng centerLocation = const LatLng(35.1595, 126.8526);
final List<StationInfo> stations = [
StationInfo(
name: 'STATION A - GU.UNIV',
address: '277 Hyodeok-ro',
status: 'ONLINE',
distance: '0.5 km away'),
StationInfo(
name: 'STATION B - CITY HALL',
address: '123 City Hall Ave',
status: 'ONLINE',
distance: '1.2 km away'),
StationInfo(
name: 'STATION C - PARK',
address: '55 Park Lane',
status: 'OFFLINE',
distance: '2.8 km away'),
StationInfo(
name: 'STATION D - TECH HUB',
address: '88 Innovation Blvd',
status: 'ONLINE',
distance: '4.1 km away'),
];
return Scaffold(
backgroundColor: const Color(0xFF1E1E1E),
appBar: AppBar(
title: const Text(
'RENT/ RETURN',
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white),
),
backgroundColor: const Color(0xFF2C2C2E),
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.white),
onPressed: () {},
)
],
),
body: Column(
children: [
Expanded(
flex: 50,
child: FlutterMap(
options: MapOptions(
initialCenter: centerLocation,
initialZoom: 15.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.helmet_app',
),
MarkerLayer(
markers: [
_buildBlackMarker(centerLocation),
_buildBlackMarker(const LatLng(35.162, 126.855)),
_buildBlackMarker(const LatLng(35.157, 126.850)),
_buildBlackMarker(const LatLng(35.160, 126.858)),
],
),
],
),
),
Expanded(
flex: 45,
child: Container(
color: const Color(0xFF1E1E1E),
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'LIST VIEW',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 20),
itemCount: stations.length,
itemBuilder: (context, index) {
return _buildStationItem(context, stations[index]);
},
),
),
],
),
),
),
],
),
);
}
Widget _buildStationItem(BuildContext context, StationInfo station) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF2C2C2E),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[800],
borderRadius: BorderRadius.circular(12),
),
child: Image.asset(
'assets/images/storage.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => const Icon(Icons.inventory_2, color: Colors.white54),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
station.name,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
station.address,
style: const TextStyle(
color: Colors.grey, fontSize: 11, height: 1.2),
maxLines: 2,
),
const SizedBox(height: 2),
Text(
'STATUS: ${station.status}',
style: TextStyle(
color: station.status.startsWith('OFFLINE')
? Colors.redAccent
: Colors.grey,
fontSize: 11,
),
),
Text(
station.distance,
style: const TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RentalProcessScreen(
stationName: station.name,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
minimumSize: const Size(80, 40),
),
child: const Text(
'SELECT',
style: TextStyle(fontWeight: FontWeight.w500, letterSpacing: 0.8, fontSize: 14)
),
),
],
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'LOGS',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
InkWell(
onTap: () => _showLogHistory(context, station.name),
child: Text(
'VIEW MORE >',
style: TextStyle(
color: Colors.grey[400],
fontSize: 10,
),
),
),
],
),
const SizedBox(height: 6),
const Text(
'Available: Door Fully Closed',
style: TextStyle(color: Colors.grey, fontSize: 11),
),
const Text(
'(2025-11-19)/(08:58)',
style: TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
],
),
);
}
Marker _buildBlackMarker(LatLng point) {
return Marker(
point: point,
width: 40,
height: 40,
child: const Icon(
Icons.location_on,
color: Colors.black,
size: 40,
),
);
}
void _showLogHistory(BuildContext context, String stationName) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) {
return Container(
height: MediaQuery.of(context).size.height * 0.6,
decoration: const BoxDecoration(
color: Color(0xFF1E1E1E),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
const SizedBox(height: 12),
Container(
width: 40, height: 4,
decoration: BoxDecoration(color: Colors.grey[600], borderRadius: BorderRadius.circular(2)),
),
Padding(
padding: const EdgeInsets.all(20.0),
child: Text("LOGS: $stationName", style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)),
),
const Divider(color: Colors.white24, height: 1),
Expanded(
child: ListView(
padding: const EdgeInsets.all(20),
children: [
_buildLogItem("08:58:33", "Door Fully Closed", Icons.door_front_door, Colors.green),
_buildLogItem("08:58:30", "Helmet Returned", Icons.check_circle_outline, Colors.blue),
_buildLogItem("08:55:12", "User Unlocked Door", Icons.lock_open, Colors.white),
_buildLogItem("08:30:00", "UV Sanitization Complete", Icons.cleaning_services, Colors.purpleAccent),
_buildLogItem("08:00:00", "System Boot Up", Icons.power_settings_new, Colors.grey),
],
),
),
],
),
);
},
);
}
Widget _buildLogItem(String time, String message, IconData icon, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(time, style: const TextStyle(color: Colors.grey, fontSize: 12)),
const SizedBox(width: 16),
Column(
children: [
Icon(icon, color: color, size: 20),
Container(width: 2, height: 20, color: Colors.white12),
],
),
const SizedBox(width: 16),
Expanded(
child: Text(message, style: const TextStyle(color: Colors.white70, fontSize: 14)),
),
],
),
);
}
}

View File

@@ -0,0 +1,349 @@
import 'package:flutter/material.dart';
class RentalProcessScreen extends StatelessWidget {
final String stationName;
const RentalProcessScreen({
super.key,
required this.stationName,
});
static const Color kBackgroundColor = Color(0xFF1E1E1E);
static const Color kCardColor = Color(0xFF2C2C2E);
static const Color kPrimaryGreen = Color(0xFF4CAF50);
static const Color kPrimaryOrange = Color(0xFFFF3D00);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kBackgroundColor,
appBar: AppBar(
title: Text(
stationName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white),
),
backgroundColor: kCardColor,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.white),
onPressed: () {},
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildStatusCard(context),
const SizedBox(height: 24),
_buildProcessSectionTitle('HELMET RENTAL PROCESS'),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildStepRow(1, 'UNLOCK & OPEN', Icons.lock_open, isCompleted: true),
_buildDivider(),
_buildStepRow(2, 'DOOR STATUS', Icons.sensor_door_outlined),
_buildDivider(),
_buildStepRow(3, 'TAKE HELMET', Icons.outbox),
_buildDivider(),
_buildStepRow(4, 'ENJOY RIDING!', Icons.sentiment_satisfied_alt, showDivider: false),
],
),
),
const SizedBox(height: 24),
_buildProcessSectionTitle('HELMET RETURN PROCESS'),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
_buildStepRow(1, 'UNLOCK & OPEN', Icons.lock_open),
_buildDivider(),
_buildStepRow(2, 'INSERT HELMET', Icons.move_to_inbox),
_buildDivider(),
_buildStepRow(3, 'SENSOR SCANNING\n& RETURN COMPLETE', Icons.sync),
_buildDivider(),
_buildStepRow(4, 'SANITIZING', Icons.shield_outlined, showDivider: false),
],
),
),
const SizedBox(height: 24),
_buildErrorAlertBox(),
const SizedBox(height: 24),
_buildLogsCard(context),
const SizedBox(height: 20),
],
),
),
),
);
}
Widget _buildStatusCard(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
children: const [
Icon(Icons.circle, color: kPrimaryOrange, size: 16),
SizedBox(width: 6),
Text('IN USE', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 150,
height: 150,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: kBackgroundColor,
borderRadius: BorderRadius.circular(12),
),
child: Image.asset(
'assets/images/open.png',
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) => const Icon(Icons.inventory_2, size: 50, color: Colors.white54),
),
),
const SizedBox(width: 20),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('NOW', style: TextStyle(color: Colors.grey, fontSize: 12)),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('문이 열렸습니다! (OPEN 버튼 눌림)'),
duration: Duration(seconds: 1),
backgroundColor: Colors.blue,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('OPEN', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: kBackgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Text(
'SENSOR ERROR:\nLOCK FAIL',
style: TextStyle(color: kPrimaryOrange, fontWeight: FontWeight.bold, height: 1.2),
textAlign: TextAlign.center,
),
),
),
],
),
),
],
),
],
),
);
}
Widget _buildProcessSectionTitle(String title) {
return Text(
title,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
);
}
Widget _buildStepRow(int step, String title, IconData icon, {bool isCompleted = false, bool showDivider = true}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('STEP $step', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
if (isCompleted) ...[
const SizedBox(width: 8),
const Icon(Icons.check_circle, color: kPrimaryGreen, size: 16),
],
],
),
const SizedBox(height: 4),
Text(title, style: const TextStyle(color: Colors.white, fontSize: 18, letterSpacing: 0.6)),
],
),
const Spacer(),
Icon(icon, color: isCompleted ? Colors.white : Colors.grey, size: 48),
],
),
);
}
Widget _buildDivider() {
return const Divider(color: Colors.white, height: 1, thickness: 1, indent: 20, endIndent: 20);
}
Widget _buildErrorAlertBox() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: kCardColor.withOpacity(0.5),
border: Border.all(color: kPrimaryOrange, width: 2),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: const [
Icon(Icons.warning_amber_rounded, color: kPrimaryOrange, size: 40),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('SENSOR ERROR:', style: TextStyle(color: kPrimaryOrange, fontWeight: FontWeight.bold, fontSize: 16)),
Text('LOCK FAILURE', style: TextStyle(color: kPrimaryOrange, fontWeight: FontWeight.bold, fontSize: 16)),
],
),
],
),
);
}
Widget _buildLogsCard(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: kCardColor,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('LOGS', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
InkWell(
onTap: () => _showLogHistory(context),
child: Text('VIEW MORE >', style: TextStyle(color: Colors.grey[400], fontSize: 12)),
),
],
),
const SizedBox(height: 12),
Row(
children: const [
Icon(Icons.check_circle, color: kPrimaryGreen, size: 16),
SizedBox(width: 8),
Text('Available: Door Fully Closed', style: TextStyle(color: Colors.white70)),
],
),
Padding(
padding: const EdgeInsets.only(left: 24.0, top: 4),
child: Text('(2025-11-19)/(08:58)', style: TextStyle(color: Colors.grey[600], fontSize: 12)),
),
],
),
);
}
void _showLogHistory(BuildContext context) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) {
return Container(
height: MediaQuery.of(context).size.height * 0.6,
decoration: const BoxDecoration(
color: Color(0xFF1E1E1E),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
const SizedBox(height: 12),
Container(
width: 40, height: 4,
decoration: BoxDecoration(color: Colors.grey[600], borderRadius: BorderRadius.circular(2)),
),
const Padding(
padding: EdgeInsets.all(20.0),
child: Text("DEVICE LOGS", style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
),
const Divider(color: Colors.white24, height: 1),
Expanded(
child: ListView(
padding: const EdgeInsets.all(20),
children: [
_buildLogItem("08:58:33", "Door Fully Closed", Icons.door_front_door, Colors.green),
_buildLogItem("08:58:30", "Helmet Returned (Sensor A)", Icons.check_circle_outline, Colors.blue),
_buildLogItem("08:55:12", "User Unlocked Door", Icons.lock_open, Colors.white),
_buildLogItem("08:30:00", "UV Sanitization Complete", Icons.cleaning_services, Colors.purpleAccent),
_buildLogItem("08:00:00", "System Boot Up", Icons.power_settings_new, Colors.grey),
],
),
),
],
),
);
},
);
}
Widget _buildLogItem(String time, String message, IconData icon, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(time, style: const TextStyle(color: Colors.grey, fontSize: 12)),
const SizedBox(width: 16),
Column(
children: [
Icon(icon, color: color, size: 20),
Container(width: 2, height: 20, color: Colors.white12),
],
),
const SizedBox(width: 16),
Expanded(
child: Text(message, style: const TextStyle(color: Colors.white70, fontSize: 14)),
),
],
),
);
}
}