Merge remote-tracking branch 'origin/master'
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
BIN
assets/images/metaq_v.png
Normal file
BIN
assets/images/metaq_v.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
BIN
assets/images/white_1.png
Normal file
BIN
assets/images/white_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/images/white_2.png
Normal file
BIN
assets/images/white_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
@@ -30,7 +30,7 @@ class _HomeScreenContentState extends State<HomeScreenContent> {
|
|||||||
final LockerApi _api = LockerApi();
|
final LockerApi _api = LockerApi();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
Future<void> _runLockerAction(String name, Future<bool> Function() action) async {
|
Future<void> _runLockerAction(String name, Future<bool> Function() action) async {
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
@@ -38,11 +38,10 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
// 실제 명령 전송
|
// 실제 명령 전송
|
||||||
final success = await action();
|
final success = await action();
|
||||||
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
|
||||||
|
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
// 25.12.03 지은 추가 끝
|
// 25.12.03 지은 추가 끝
|
||||||
|
|
||||||
void test() async {
|
void test() async {
|
||||||
@@ -108,7 +107,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
Widget _buildOverviewSection() {
|
Widget _buildOverviewSection() {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(top: 5),
|
margin: const EdgeInsets.only(top: 5),
|
||||||
child: Card(
|
child: DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -170,7 +169,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
_selectedImageIndex == 0
|
_selectedImageIndex == 0
|
||||||
? 'assets/images/storage.png'
|
? 'assets/images/storage.png'
|
||||||
: 'assets/images/top.png',
|
: 'assets/images/top.png',
|
||||||
width: 100,
|
width: 90,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -261,7 +260,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBatteryStatusCard() {
|
Widget _buildBatteryStatusCard() {
|
||||||
return Card(
|
return DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -342,7 +341,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildControlCard() {
|
Widget _buildControlCard() {
|
||||||
return Card(
|
return DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -482,7 +481,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEnvironmentSensorsCard() {
|
Widget _buildEnvironmentSensorsCard() {
|
||||||
return Card(
|
return DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -552,7 +551,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
|
|
||||||
Widget _buildMyLocationCard() {
|
Widget _buildMyLocationCard() {
|
||||||
const LatLng exampleLocation = LatLng(37.5665, 126.9780);
|
const LatLng exampleLocation = LatLng(37.5665, 126.9780);
|
||||||
return Card(
|
return DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
@@ -620,7 +619,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActivityCard() {
|
Widget _buildActivityCard() {
|
||||||
return Card(
|
return DashboardCard(
|
||||||
shadow: _cleanShadow,
|
shadow: _cleanShadow,
|
||||||
cardColor: _cardBackgroundColor,
|
cardColor: _cardBackgroundColor,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -647,9 +646,9 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_activityText('10:45 AM - 헬멧 잠금 해제'),
|
_activityText('10:45 AM - 안전모 잠금 해제'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_activityText('11:00 AM - 헬멧 착용 해제'),
|
_activityText('11:00 AM - 안전모 착용 해제'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -721,7 +720,7 @@ class _LineChartPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Card extends StatelessWidget {
|
class DashboardCard extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
final Clip clipBehavior;
|
final Clip clipBehavior;
|
||||||
@@ -729,7 +728,7 @@ class Card extends StatelessWidget {
|
|||||||
final Color? cardColor;
|
final Color? cardColor;
|
||||||
final BoxShadow? shadow;
|
final BoxShadow? shadow;
|
||||||
|
|
||||||
const Card({
|
const DashboardCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.padding,
|
this.padding,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
|
import 'register_screen.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@@ -15,7 +16,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
final FocusNode _idFocusNode = FocusNode();
|
final FocusNode _idFocusNode = FocusNode();
|
||||||
final FocusNode _pwFocusNode = FocusNode();
|
final FocusNode _pwFocusNode = FocusNode();
|
||||||
|
|
||||||
final Color mainBlueColor = const Color(0xFF007AFF);
|
final Color mainBlueColor = const Color(0xFF0033CC);
|
||||||
|
|
||||||
bool _isIdFocused = false;
|
bool _isIdFocused = false;
|
||||||
bool _isPwFocused = false;
|
bool _isPwFocused = false;
|
||||||
@@ -45,26 +46,21 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.polymer, size: 80, color: mainBlueColor),
|
Image.asset(
|
||||||
const SizedBox(height: 20),
|
'assets/images/metaq_v.png',
|
||||||
Text(
|
width: 86,
|
||||||
'METAQLAB',
|
height: 86,
|
||||||
textAlign: TextAlign.center,
|
fit: BoxFit.contain,
|
||||||
style: TextStyle(
|
|
||||||
color: mainBlueColor,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 1.2,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 70),
|
||||||
const SizedBox(height: 60),
|
|
||||||
|
|
||||||
_buildCustomTextField(
|
_buildCustomTextField(
|
||||||
label: '아이디',
|
label: '아이디',
|
||||||
controller: _idController,
|
controller: _idController,
|
||||||
@@ -72,7 +68,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
isFocused: _isIdFocused,
|
isFocused: _isIdFocused,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
_buildCustomTextField(
|
_buildCustomTextField(
|
||||||
label: '비밀번호',
|
label: '비밀번호',
|
||||||
controller: _pwController,
|
controller: _pwController,
|
||||||
@@ -80,22 +75,19 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
isFocused: _isPwFocused,
|
isFocused: _isPwFocused,
|
||||||
isObscure: true,
|
isObscure: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
String inputId = _idController.text;
|
String inputId = _idController.text;
|
||||||
String inputPw = _pwController.text;
|
String inputPw = _pwController.text;
|
||||||
|
|
||||||
if (inputId == 'user' && inputPw == '1234') {
|
if (inputId == 'user' && inputPw == '1234') {
|
||||||
print('로그인 성공!');
|
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const HomeScreen()),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const HomeScreen()),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print('로그인 실패');
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('아이디 또는 비밀번호가 틀렸습니다.'),
|
content: Text('아이디 또는 비밀번호가 틀렸습니다.'),
|
||||||
@@ -116,22 +108,42 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'로그인',
|
'로그인',
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style:
|
||||||
|
TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
ElevatedButton(
|
||||||
TextButton(
|
onPressed: () {
|
||||||
onPressed: () {},
|
Navigator.push(
|
||||||
child: Text(
|
context,
|
||||||
'비밀번호 찾기 / 회원가입',
|
MaterialPageRoute(
|
||||||
style: TextStyle(color: mainBlueColor, fontWeight: FontWeight.w500),
|
builder: (context) => const RegisterScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: mainBlueColor,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: BorderSide(color: mainBlueColor, width: 1.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'회원가입',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +156,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
}) {
|
}) {
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
@@ -153,23 +165,16 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: TextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
color: isFocused ? mainBlueColor : Colors.grey.shade600,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
obscureText: isObscure,
|
obscureText: isObscure,
|
||||||
decoration: const InputDecoration(
|
decoration : InputDecoration(
|
||||||
|
hintText: label,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -182,8 +187,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
),
|
),
|
||||||
cursorColor: mainBlueColor,
|
cursorColor: mainBlueColor,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ class SmartHelmetApp extends StatelessWidget {
|
|||||||
bodyMedium: TextStyle(color: _subTextColor, fontWeight: FontWeight.w400),
|
bodyMedium: TextStyle(color: _subTextColor, fontWeight: FontWeight.w400),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
home: const HomeScreen(),
|
home: const LoginScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
520
lib/register_screen.dart
Normal file
520
lib/register_screen.dart
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
const String _serviceTermContent =
|
||||||
|
'제1조 (목적)\n'
|
||||||
|
'본 약관은 (주)메타큐랩(이하 "회사")이 제공하는 공유 전동 킥보드용 스마트 안전모 보관함 및 관련 애플리케이션 서비스(이하 "서비스")의 이용과 관련하여 회사와 회원의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
|
||||||
|
'제2조 (용어의 정의)\n'
|
||||||
|
'1. "서비스"란 회사가 제공하는 스마트 안전모 보관함 앱을 통해 안전모를 대여, 반납, 관리하는 모든 제반 서비스를 의미합니다.\n'
|
||||||
|
'2. "보관함"이란 안전모의 보관, 살균, 건조, 잠금 기능을 수행하는 물리적 하드웨어 장치를 말합니다.\n'
|
||||||
|
'3. "회원"이란 앱을 설치하고 본 약관에 동의하여 회사와 이용계약을 체결한 자를 말합니다.\n'
|
||||||
|
'4. "대여"란 앱을 통해 보관함의 잠금을 해제하고 안전모를 수령하는 행위를 말합니다.\n'
|
||||||
|
'5. "반납"이란 사용 후 안전모를 보관함에 넣고 문을 닫아 센서가 안전모를 인식하고 잠금이 완료된 상태를 말합니다.\n\n'
|
||||||
|
'제3조 (이용계약의 체결)\n'
|
||||||
|
'1. 이용계약은 회원이 되고자 하는 자(이하 "가입신청자")가 약관의 내용에 대하여 동의를 한 다음 회원가입 신청을 하고 회사가 이러한 신청을 승낙함으로써 체결됩니다.\n'
|
||||||
|
'2. 가입신청자는 앱 내에서 아이디(이메일), 비밀번호 등 필수 정보를 입력해야 하며, 회사는 필요시 본인 인증을 요구할 수 있습니다.\n\n'
|
||||||
|
'제4조 (서비스의 제공 및 기능)\n'
|
||||||
|
'회사는 회원에게 다음과 같은 서비스를 제공합니다.\n'
|
||||||
|
'1. 안전모 보관함 위치 찾기 (지도 및 리스트 제공)\n'
|
||||||
|
'2. 보관함 잠금장치 원격 제어 (대여/반납)\n'
|
||||||
|
'3. 안전모 살균 및 건조 상태 모니터링\n'
|
||||||
|
'4. 이용 내역 조회 및 알림 서비스\n\n'
|
||||||
|
'제5조 (대여 및 반납)\n'
|
||||||
|
'1. 회원은 앱을 통해 이용 가능한 보관함을 확인하고 잠금을 해제하여 안전모를 대여할 수 있습니다.\n'
|
||||||
|
'2. 회원은 이용 종료 후 안전모를 지정된 보관함에 넣고 문을 닫아야 합니다.\n'
|
||||||
|
'3. (반납의 완료) 반납 처리는 보관함 내부 센서가 안전모를 정상적으로 인식하고 문이 잠길 때 완료됩니다. 안전모가 감지되지 않거나 도어가 닫히지 않을 경우 반납으로 처리되지 않으며, 이에 따른 불이익은 회원이 부담합니다.\n'
|
||||||
|
'4. 센서 오류 등으로 반납 처리가 되지 않을 경우, 회원은 앱 내 "비상 신고 버튼" 또는 사진 업로드 기능을 통해 반납 사실을 증명해야 합니다.\n\n'
|
||||||
|
'제6조 (이용 요금 및 결제)\n'
|
||||||
|
'1. 서비스 이용 요금은 회사의 정책에 따르며 앱 내에 공지합니다.\n'
|
||||||
|
'2. 회원이 안전모를 분실하거나 파손한 경우, 회사는 별도의 실비 변상을 청구할 수 있습니다.\n\n'
|
||||||
|
'제7조 (살균 및 건조)\n'
|
||||||
|
'보관함은 반납 완료 후 자동으로 UV 살균 및 건조 기능을 수행합니다. 단, 회원은 착용 전 안전모의 상태를 육안으로 확인해야 하며, 오염 등이 심한 경우 이용을 중단하고 신고해야 합니다.\n\n'
|
||||||
|
'제8조 (회원의 의무)\n'
|
||||||
|
'1. 회원은 도로교통법 등 관련 법령에 따라 전동 킥보드 탑승 시 반드시 안전모를 착용해야 합니다.\n'
|
||||||
|
'2. 회원은 대여한 안전모를 제3자에게 양도하거나 대여해서는 안 됩니다.\n'
|
||||||
|
'3. 회원은 보관함 및 안전모를 파손하거나 기능을 임의로 조작해서는 안 됩니다.\n\n'
|
||||||
|
'제9조 (위치기반서비스의 내용)\n'
|
||||||
|
'회사는 회원의 위치 정보를 이용하여 주변 보관함 검색 기능을 제공하며, 회원의 위치 정보는 서비스 제공 목적 외에는 사용되지 않습니다.\n\n'
|
||||||
|
'제10조 (책임 제한)\n'
|
||||||
|
'1. 회사는 천재지변 또는 이에 준하는 불가항력으로 인하여 서비스를 제공할 수 없는 경우에는 서비스 제공에 관한 책임이 면제됩니다.\n'
|
||||||
|
'2. 회사는 회원의 귀책사유로 인한 서비스 이용의 장애에 대하여는 책임을 지지 않습니다.\n'
|
||||||
|
'3. 회원은 안전모 미착용 또는 올바르지 않은 착용으로 인해 발생한 사고에 대해 전적으로 책임을 지며, 회사는 이에 대해 책임을 지지 않습니다.\n\n'
|
||||||
|
'제11조 (서비스 알림)\n'
|
||||||
|
'회사는 회원의 안전한 이용을 위해 대여/반납 현황, 미반납 알림, 이상 감지 알림 등을 PUSH 알림으로 전송할 수 있습니다.\n\n'
|
||||||
|
'제12조 (준거법 및 재판관할)\n'
|
||||||
|
'본 약관에 명시되지 않은 사항은 대한민국의 관계 법령에 따르며, 서비스 이용으로 발생한 분쟁에 대해 소송이 제기되는 경우 회사의 본점 소재지를 관할하는 법원을 전속 관할법원으로 합니다.\n\n'
|
||||||
|
'부 칙\n'
|
||||||
|
'본 약관은 2025년 11월 28일부터 시행합니다.\n';
|
||||||
|
|
||||||
|
const String _privacyTermContent =
|
||||||
|
'(주)메타큐랩은 서비스 제공을 위하여 아래와 같이 귀하의 개인정보를 수집·이용합니다. 내용을 자세히 읽으신 후 동의해 주시기 바랍니다.\n\n'
|
||||||
|
'1. 수집 및 이용 목적\n'
|
||||||
|
'- 회원 가입 및 관리: 본인 확인, 개인 식별, 불량 회원의 부정이용 방지, 가입 의사 확인.\n'
|
||||||
|
'- 서비스 제공: 스마트 안전모 보관함 대여/반납 처리, 기기 제어, 이용 내역 관리, 살균/건조 상태 정보 제공.\n'
|
||||||
|
'- 고객 지원: 공지사항 전달, 서비스 장애 및 오류 신고 접수, 민원 처리.\n\n'
|
||||||
|
'2. 수집하는 개인정보 항목\n'
|
||||||
|
'- (필수) 아이디(이메일), 비밀번호, 이름, 휴대전화번호.\n'
|
||||||
|
'- (자동 수집) 기기 식별 정보(Device ID), PUSH 토큰, 서비스 이용 기록, 접속 로그.\n\n'
|
||||||
|
'3. 보유 및 이용 기간\n'
|
||||||
|
'회원 탈퇴 시까지 (단, 관계 법령에 따라 일정 기간 보존이 필요한 경우 해당 기간까지 보관)\n\n'
|
||||||
|
'4. 동의 거부 권리 및 불이익\n'
|
||||||
|
'귀하는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다. 단, 필수 항목에 대한 동의를 거부할 경우 회원 가입 및 서비스 이용이 제한됩니다.\n';
|
||||||
|
|
||||||
|
const String _locationTermContent = """
|
||||||
|
1. 위치정보의 수집 및 이용 목적
|
||||||
|
회사는 이용자의 현재 위치를 확인하여 긴급 구조 요청, 주행 경로 기록, 주변 시설 검색 등 위치 기반 서비스를 제공하기 위해 위치정보를 수집 및 이용합니다.
|
||||||
|
|
||||||
|
2. 위치정보의 보유 및 이용 기간
|
||||||
|
회사는 위치정보의 수집 및 이용 목적이 달성된 후에는 해당 정보를 지체 없이 파기합니다. 단, 관련 법령의 규정에 의하여 보존할 필요가 있는 경우 법령에서 정한 기간 동안 보관합니다.
|
||||||
|
""";
|
||||||
|
|
||||||
|
const String _marketingTermContent =
|
||||||
|
'(주)메타큐랩이 제공하는 이벤트 및 혜택 등 마케팅 정보를 수신하는 것에 동의합니다.\n\n'
|
||||||
|
'1. 수집 및 이용 목적\n'
|
||||||
|
'- 신규 서비스(기능) 개발 및 맞춤 서비스 제공.\n'
|
||||||
|
'- 이벤트, 프로모션, 혜택 등 광고성 정보 제공.\n'
|
||||||
|
'- 서비스 이용 통계 및 설문조사.\n\n'
|
||||||
|
'2. 수집하는 개인정보 항목\n'
|
||||||
|
'- 이름, 휴대전화번호, 이메일, PUSH 토큰.\n\n'
|
||||||
|
'3. 보유 및 이용 기간\n'
|
||||||
|
'- 회원 탈퇴 또는 동의 철회 시까지\n\n'
|
||||||
|
'4. 전송 방법\n'
|
||||||
|
'- 앱 푸시(App Push) 알림, SMS(문자메시지), 이메일 등.\n\n'
|
||||||
|
'5. 동의 거부 권리\n'
|
||||||
|
'귀하는 마케팅 정보 수신에 대한 동의를 거부할 수 있습니다. 동의를 거부하더라도 회원 가입 및 기본 서비스 이용에는 제한이 없습니다. 다만, 이벤트 및 혜택 정보 제공이 제한될 수 있습니다.\n';
|
||||||
|
|
||||||
|
class RegisterScreen extends StatefulWidget {
|
||||||
|
const RegisterScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegisterScreenState extends State<RegisterScreen> {
|
||||||
|
final TextEditingController _idController = TextEditingController();
|
||||||
|
final TextEditingController _pwController = TextEditingController();
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
final TextEditingController _nicknameController = TextEditingController();
|
||||||
|
final TextEditingController _phoneController = TextEditingController();
|
||||||
|
final TextEditingController _emailController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isServiceAgreed = false;
|
||||||
|
bool _isPrivacyAgreed = false;
|
||||||
|
bool _isLocationAgreed = false;
|
||||||
|
bool _isMarketingAgreed = false;
|
||||||
|
|
||||||
|
final Color mainBlueColor = const Color(0xFF0033CC);
|
||||||
|
final Color backgroundColor = const Color(0xFFF5F6F8);
|
||||||
|
final Color iconColor = const Color(0xFF8899A6);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_idController.dispose();
|
||||||
|
_pwController.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_nicknameController.dispose();
|
||||||
|
_phoneController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryRegister() {
|
||||||
|
if (_idController.text.isEmpty) return _showError('아이디를 입력해주세요.');
|
||||||
|
if (_pwController.text.isEmpty) return _showError('비밀번호를 입력해주세요.');
|
||||||
|
if (_emailController.text.isEmpty) return _showError('이메일을 입력해주세요.');
|
||||||
|
if (_nameController.text.isEmpty) return _showError('이름을 입력해주세요.');
|
||||||
|
if (_phoneController.text.isEmpty) return _showError('전화번호를 입력해주세요.');
|
||||||
|
if (_phoneController.text.length < 12) return _showError('올바른 전화번호를 입력해주세요.');
|
||||||
|
if (_nicknameController.text.isEmpty) return _showError('닉네임을 입력해주세요.');
|
||||||
|
|
||||||
|
if (!_isServiceAgreed || !_isPrivacyAgreed || !_isLocationAgreed) {
|
||||||
|
return _showError('(필수) 약관에 모두 동의해주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text('회원가입이 완료되었습니다! 환영합니다.'),
|
||||||
|
backgroundColor: mainBlueColor,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
|
if (mounted) Navigator.pop(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(String message) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTermDetails(String title, String content) {
|
||||||
|
String fullContent = content;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 10, top: 15, bottom: 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.grey),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1, thickness: 1),
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Text(
|
||||||
|
fullContent,
|
||||||
|
style: const TextStyle(fontSize: 14, height: 1.6, color: Colors.black54),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: mainBlueColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'확인',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'회원가입',
|
||||||
|
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 30.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.account_circle,
|
||||||
|
hint: '아이디',
|
||||||
|
controller: _idController,
|
||||||
|
),
|
||||||
|
_buildDivider(),
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.lock_outline,
|
||||||
|
hint: '비밀번호',
|
||||||
|
controller: _pwController,
|
||||||
|
isObscure: true,
|
||||||
|
),
|
||||||
|
_buildDivider(),
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.email_outlined,
|
||||||
|
hint: '이메일',
|
||||||
|
controller: _emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.account_circle,
|
||||||
|
hint: '이름',
|
||||||
|
controller: _nameController,
|
||||||
|
),
|
||||||
|
_buildDivider(),
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.phone_iphone_outlined,
|
||||||
|
hint: '전화번호',
|
||||||
|
controller: _phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
isPhone: true,
|
||||||
|
),
|
||||||
|
_buildDivider(),
|
||||||
|
_buildInputRow(
|
||||||
|
icon: Icons.person_outline,
|
||||||
|
hint: '닉네임',
|
||||||
|
controller: _nicknameController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'약관 동의',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildAgreementRow(
|
||||||
|
title: '(필수) 서비스 이용약관 동의',
|
||||||
|
value: _isServiceAgreed,
|
||||||
|
onChanged: (v) => setState(() => _isServiceAgreed = v!),
|
||||||
|
details: _serviceTermContent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildAgreementRow(
|
||||||
|
title: '(필수) 개인정보 수집 및 이용 동의',
|
||||||
|
value: _isPrivacyAgreed,
|
||||||
|
onChanged: (v) => setState(() => _isPrivacyAgreed = v!),
|
||||||
|
details: _privacyTermContent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildAgreementRow(
|
||||||
|
title: '(필수) 위치기반 서비스 이용약관 동의',
|
||||||
|
value: _isLocationAgreed,
|
||||||
|
onChanged: (v) => setState(() => _isLocationAgreed = v!),
|
||||||
|
details: _locationTermContent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildAgreementRow(
|
||||||
|
title: '(선택) 이벤트 및 마케팅 수신 동의',
|
||||||
|
value: _isMarketingAgreed,
|
||||||
|
onChanged: (v) => setState(() => _isMarketingAgreed = v!),
|
||||||
|
details: _marketingTermContent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _tryRegister,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: mainBlueColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
minimumSize: const Size(double.infinity, 56),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'가입하기',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDivider() {
|
||||||
|
return Divider(height: 1, thickness: 1, color: Colors.grey.shade100, indent: 50, endIndent: 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInputRow({
|
||||||
|
required IconData icon,
|
||||||
|
required String hint,
|
||||||
|
required TextEditingController controller,
|
||||||
|
bool isObscure = false,
|
||||||
|
TextInputType keyboardType = TextInputType.text,
|
||||||
|
bool isPhone = false,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: iconColor, size: 24),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: controller,
|
||||||
|
obscureText: isObscure,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
inputFormatters: isPhone
|
||||||
|
? [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
_PhoneNumberFormatter(),
|
||||||
|
LengthLimitingTextInputFormatter(13),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 15),
|
||||||
|
border: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||||
|
cursorColor: mainBlueColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAgreementRow({
|
||||||
|
required String title,
|
||||||
|
required bool value,
|
||||||
|
required ValueChanged<bool?> onChanged,
|
||||||
|
required String details,
|
||||||
|
}) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: Checkbox(
|
||||||
|
value: value,
|
||||||
|
activeColor: mainBlueColor,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||||
|
side: BorderSide(color: Colors.grey.shade300, width: 1.5),
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onChanged(!value),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _showTermDetails(title, details),
|
||||||
|
icon: const Icon(Icons.arrow_forward_ios_rounded, color: Colors.grey, size: 14),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PhoneNumberFormatter extends TextInputFormatter {
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
|
var text = newValue.text;
|
||||||
|
if (newValue.selection.baseOffset == 0) return newValue;
|
||||||
|
var buffer = StringBuffer();
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
buffer.write(text[i]);
|
||||||
|
var nonZeroIndex = i + 1;
|
||||||
|
if (nonZeroIndex <= 3) {
|
||||||
|
if (nonZeroIndex % 3 == 0 && nonZeroIndex != text.length) buffer.write('-');
|
||||||
|
} else {
|
||||||
|
if (nonZeroIndex % 7 == 0 && nonZeroIndex != text.length && nonZeroIndex > 4) buffer.write('-');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var string = buffer.toString();
|
||||||
|
return newValue.copyWith(text: string, selection: TextSelection.collapsed(offset: string.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -368,7 +368,7 @@ class RentReturnScreen extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
children: [
|
||||||
_buildLogItem("08:58:33", "문 닫힘", Icons.door_front_door, _mainBlueColor),
|
_buildLogItem("08:58:33", "문 닫힘", Icons.door_front_door, _mainBlueColor),
|
||||||
_buildLogItem("08:58:30", "헬멧 반납 확인", Icons.check_circle_outline, _mainBlueColor),
|
_buildLogItem("08:58:30", "안전모 반납 확인", Icons.check_circle_outline, _mainBlueColor),
|
||||||
_buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open, _mainTextColor),
|
_buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open, _mainTextColor),
|
||||||
_buildLogItem("08:30:00", "UV 살균 완료", Icons.cleaning_services, _mainBlueColor),
|
_buildLogItem("08:30:00", "UV 살균 완료", Icons.cleaning_services, _mainBlueColor),
|
||||||
_buildLogItem("08:00:00", "시스템 가동 중", Icons.power_settings_new, _subTextColor),
|
_buildLogItem("08:00:00", "시스템 가동 중", Icons.power_settings_new, _subTextColor),
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
final List<String> _imageList = [
|
final List<String> _imageList = [
|
||||||
'assets/images/top.png',
|
'assets/images/top.png',
|
||||||
'assets/images/info1.png',
|
'assets/images/white_1.png',
|
||||||
'assets/images/info2.png',
|
'assets/images/white_2.png',
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -112,7 +112,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
children: [
|
children: [
|
||||||
_buildStatusCard(context),
|
_buildStatusCard(context),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildProcessSectionTitle('헬멧 대여'),
|
_buildProcessSectionTitle('안전모 대여'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -126,7 +126,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStepRow(2, '문 상태 확인', Icons.sensor_door_outlined),
|
_buildStepRow(2, '문 상태 확인', Icons.sensor_door_outlined),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStepRow(3, '헬멧 꺼내기', Icons.outbox),
|
_buildStepRow(3, '안전모 꺼내기', Icons.outbox),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStepRow(4, '주행 시작', Icons.sentiment_satisfied_alt,
|
_buildStepRow(4, '주행 시작', Icons.sentiment_satisfied_alt,
|
||||||
showDivider: false),
|
showDivider: false),
|
||||||
@@ -134,7 +134,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildProcessSectionTitle('헬멧 반납'),
|
_buildProcessSectionTitle('안전모 반납'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -145,7 +145,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
children: [
|
children: [
|
||||||
_buildStepRow(1, '잠금해제 & 문열림', Icons.lock_open),
|
_buildStepRow(1, '잠금해제 & 문열림', Icons.lock_open),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStepRow(2, '헬멧 넣기', Icons.move_to_inbox),
|
_buildStepRow(2, '안전모 넣기', Icons.move_to_inbox),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStepRow(3, '센서 스캔 & 반납 완료', Icons.sync),
|
_buildStepRow(3, '센서 스캔 & 반납 완료', Icons.sync),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
@@ -477,7 +477,7 @@ Future<void> _runLockerAction(String name, Future<bool> Function() action) async
|
|||||||
children: [
|
children: [
|
||||||
_buildLogItem("08:58:33", "문 열림", Icons.door_front_door,
|
_buildLogItem("08:58:33", "문 열림", Icons.door_front_door,
|
||||||
_mainBlueColor),
|
_mainBlueColor),
|
||||||
_buildLogItem("08:58:30", "헬멧 반납 확인(센서 A)",
|
_buildLogItem("08:58:30", "안전모 반납 확인(센서 A)",
|
||||||
Icons.check_circle_outline, _mainBlueColor),
|
Icons.check_circle_outline, _mainBlueColor),
|
||||||
_buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open,
|
_buildLogItem("08:55:12", "사용자 문 잠금 해제", Icons.lock_open,
|
||||||
_mainTextColor),
|
_mainTextColor),
|
||||||
|
|||||||
@@ -15,6 +15,172 @@ const BoxShadow _cleanShadow = BoxShadow(
|
|||||||
spreadRadius: 0,
|
spreadRadius: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const String _serviceTermContent =
|
||||||
|
'제1조 (목적)\n'
|
||||||
|
'본 약관은 (주)메타큐랩(이하 "회사")이 제공하는 공유 전동 킥보드용 스마트 안전모 보관함 및 관련 애플리케이션 서비스(이하 "서비스")의 이용과 관련하여 회사와 회원의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
|
||||||
|
'제2조 (용어의 정의)\n'
|
||||||
|
'1. "서비스"란 회사가 제공하는 스마트 안전모 보관함 앱을 통해 안전모를 대여, 반납, 관리하는 모든 제반 서비스를 의미합니다.\n'
|
||||||
|
'2. "보관함"이란 안전모의 보관, 살균, 건조, 잠금 기능을 수행하는 물리적 하드웨어 장치를 말합니다.\n'
|
||||||
|
'3. "회원"이란 앱을 설치하고 본 약관에 동의하여 회사와 이용계약을 체결한 자를 말합니다.\n'
|
||||||
|
'4. "대여"란 앱을 통해 보관함의 잠금을 해제하고 안전모를 수령하는 행위를 말합니다.\n'
|
||||||
|
'5. "반납"이란 사용 후 안전모를 보관함에 넣고 문을 닫아 센서가 안전모를 인식하고 잠금이 완료된 상태를 말합니다.\n\n'
|
||||||
|
'제3조 (이용계약의 체결)\n'
|
||||||
|
'1. 이용계약은 회원이 되고자 하는 자(이하 "가입신청자")가 약관의 내용에 대하여 동의를 한 다음 회원가입 신청을 하고 회사가 이러한 신청을 승낙함으로써 체결됩니다.\n'
|
||||||
|
'2. 가입신청자는 앱 내에서 아이디(이메일), 비밀번호 등 필수 정보를 입력해야 하며, 회사는 필요시 본인 인증을 요구할 수 있습니다.\n\n'
|
||||||
|
'제4조 (서비스의 제공 및 기능)\n'
|
||||||
|
'회사는 회원에게 다음과 같은 서비스를 제공합니다.\n'
|
||||||
|
'1. 안전모 보관함 위치 찾기 (지도 및 리스트 제공)\n'
|
||||||
|
'2. 보관함 잠금장치 원격 제어 (대여/반납)\n'
|
||||||
|
'3. 안전모 살균 및 건조 상태 모니터링\n'
|
||||||
|
'4. 이용 내역 조회 및 알림 서비스\n\n'
|
||||||
|
'제5조 (대여 및 반납)\n'
|
||||||
|
'1. 회원은 앱을 통해 이용 가능한 보관함을 확인하고 잠금을 해제하여 안전모를 대여할 수 있습니다.\n'
|
||||||
|
'2. 회원은 이용 종료 후 안전모를 지정된 보관함에 넣고 문을 닫아야 합니다.\n'
|
||||||
|
'3. (반납의 완료) 반납 처리는 보관함 내부 센서가 안전모를 정상적으로 인식하고 문이 잠길 때 완료됩니다. 안전모가 감지되지 않거나 도어가 닫히지 않을 경우 반납으로 처리되지 않으며, 이에 따른 불이익은 회원이 부담합니다.\n'
|
||||||
|
'4. 센서 오류 등으로 반납 처리가 되지 않을 경우, 회원은 앱 내 "비상 신고 버튼" 또는 사진 업로드 기능을 통해 반납 사실을 증명해야 합니다.\n\n'
|
||||||
|
'제6조 (이용 요금 및 결제)\n'
|
||||||
|
'1. 서비스 이용 요금은 회사의 정책에 따르며 앱 내에 공지합니다.\n'
|
||||||
|
'2. 회원이 안전모를 분실하거나 파손한 경우, 회사는 별도의 실비 변상을 청구할 수 있습니다.\n\n'
|
||||||
|
'제7조 (살균 및 건조)\n'
|
||||||
|
'보관함은 반납 완료 후 자동으로 UV 살균 및 건조 기능을 수행합니다. 단, 회원은 착용 전 안전모의 상태를 육안으로 확인해야 하며, 오염 등이 심한 경우 이용을 중단하고 신고해야 합니다.\n\n'
|
||||||
|
'제8조 (회원의 의무)\n'
|
||||||
|
'1. 회원은 도로교통법 등 관련 법령에 따라 전동 킥보드 탑승 시 반드시 안전모를 착용해야 합니다.\n'
|
||||||
|
'2. 회원은 대여한 안전모를 제3자에게 양도하거나 대여해서는 안 됩니다.\n'
|
||||||
|
'3. 회원은 보관함 및 안전모를 파손하거나 기능을 임의로 조작해서는 안 됩니다.\n\n'
|
||||||
|
'제9조 (위치기반서비스의 내용)\n'
|
||||||
|
'회사는 회원의 위치 정보를 이용하여 주변 보관함 검색 기능을 제공하며, 회원의 위치 정보는 서비스 제공 목적 외에는 사용되지 않습니다.\n\n'
|
||||||
|
'제10조 (책임 제한)\n'
|
||||||
|
'1. 회사는 천재지변 또는 이에 준하는 불가항력으로 인하여 서비스를 제공할 수 없는 경우에는 서비스 제공에 관한 책임이 면제됩니다.\n'
|
||||||
|
'2. 회사는 회원의 귀책사유로 인한 서비스 이용의 장애에 대하여는 책임을 지지 않습니다.\n'
|
||||||
|
'3. 회원은 안전모 미착용 또는 올바르지 않은 착용으로 인해 발생한 사고에 대해 전적으로 책임을 지며, 회사는 이에 대해 책임을 지지 않습니다.\n\n'
|
||||||
|
'제11조 (서비스 알림)\n'
|
||||||
|
'회사는 회원의 안전한 이용을 위해 대여/반납 현황, 미반납 알림, 이상 감지 알림 등을 PUSH 알림으로 전송할 수 있습니다.\n\n'
|
||||||
|
'제12조 (준거법 및 재판관할)\n'
|
||||||
|
'본 약관에 명시되지 않은 사항은 대한민국의 관계 법령에 따르며, 서비스 이용으로 발생한 분쟁에 대해 소송이 제기되는 경우 회사의 본점 소재지를 관할하는 법원을 전속 관할법원으로 합니다.\n\n'
|
||||||
|
'부 칙\n'
|
||||||
|
'본 약관은 2025년 11월 28일부터 시행합니다.\n';
|
||||||
|
|
||||||
|
const String _privacyTermContent =
|
||||||
|
'(주)메타큐랩(이하 "회사")은 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 개인정보보호법 등 관련 법령을 준수하며, 이용자의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.\n\n'
|
||||||
|
'1. 개인정보의 수집 및 이용 목적\n'
|
||||||
|
'회사는 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며, 이용 목적이 변경되는 경우에는 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.\n'
|
||||||
|
'가. 회원 가입 및 관리\n'
|
||||||
|
'- 회원제 서비스 제공에 따른 본인 식별·인증\n'
|
||||||
|
'- 회원 자격 유지·관리, 서비스 부정이용 방지\n'
|
||||||
|
'- 만 14세 미만 아동의 개인정보 처리 시 법정대리인의 동의 여부 확인\n'
|
||||||
|
'- 각종 고지·통지, 고충 처리\n'
|
||||||
|
'나. 재화 또는 서비스 제공\n'
|
||||||
|
'- 안전모 보관함 위치 찾기: 지도 기반의 보관함 위치 정보 제공\n'
|
||||||
|
'- 보관함 대여 및 반납: 잠금장치 제어, 대여/반납 이력 관리\n'
|
||||||
|
'- 안전 관리: 안전모 착용 여부 확인, 살균/건조 상태 안내\n'
|
||||||
|
'- 장애 대응: 보관함 고장 신고 시 위치 및 현장 사진 접수\n'
|
||||||
|
'다. 신규 서비스 개발 및 마케팅·광고에의 활용\n'
|
||||||
|
'- 신규 서비스 개발 및 맞춤 서비스 제공\n'
|
||||||
|
'- 이벤트 및 광고성 정보 제공 및 참여기회 제공 (마케팅 동의 시)\n'
|
||||||
|
'- 서비스 유효성 확인, 접속 빈도 파악 또는 회원의 서비스 이용에 대한 통계\n\n'
|
||||||
|
'2. 수집하는 개인정보의 항목 및 수집 방법\n'
|
||||||
|
'회사는 서비스 제공을 위해 아래와 같은 개인정보를 수집하고 있습니다.\n'
|
||||||
|
'가. 필수적 수집 항목\n'
|
||||||
|
'- 로그인 정보: 아이디(이메일), 비밀번호, 이름, 휴대전화번호\n'
|
||||||
|
'- 기기 정보: 기기 식별 고유번호(Device ID), PUSH 토큰(알림 수신용)\n'
|
||||||
|
'나. 서비스 이용 과정에서 생성/수집되는 항목\n'
|
||||||
|
'- 서비스 이용 기록(대여/반납 일시, 이용 보관함 정보), 접속 로그, 쿠키, 접속 IP 정보, 불량 이용 기록\n'
|
||||||
|
'- 위치 정보: 이용자의 현재 위치 (주변 보관함 검색 목적, 서버에 저장되지 않음)\n'
|
||||||
|
'다. 앱 접근 권한을 통한 수집 항목\n'
|
||||||
|
'- 카메라: 반납 인증 사진 촬영, 고장/오류 신고 시 사진 첨부\n'
|
||||||
|
'- 블루투스: 보관함 잠금장치 무선 연결 및 제어\n'
|
||||||
|
'- 위치: 지도상 현재 위치 표시 및 주변 기기 검색\n\n'
|
||||||
|
'3. 개인정보의 처리 및 보유 기간\n'
|
||||||
|
'회사는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.\n'
|
||||||
|
'- 회원 가입 및 관리 정보: 회원 탈퇴 시까지 (단, 관계 법령 위반에 따른 수사·조사 등이 진행 중인 경우에는 해당 수사·조사 종료 시까지)\n'
|
||||||
|
'- 재화 또는 서비스 제공: 재화·서비스 공급완료 및 요금결제·정산 완료 시까지\n'
|
||||||
|
'- 관련 법령에 의한 정보 보유:\n'
|
||||||
|
' 1) 계약 또는 청약철회 등에 관한 기록: 5년 (전자상거래 등에서의 소비자보호에 관한 법률)\n'
|
||||||
|
' 2) 대금결제 및 재화 등의 공급에 관한 기록: 5년\n'
|
||||||
|
' 3) 소비자의 불만 또는 분쟁처리에 관한 기록: 3년\n'
|
||||||
|
' 4) 웹사이트 방문 기록: 3개월 (통신비밀보호법)\n\n'
|
||||||
|
'4. 개인정보의 파기 절차 및 방법\n'
|
||||||
|
'회사는 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체 없이 해당 개인정보를 파기합니다.\n'
|
||||||
|
'- 파기절차: 파기 사유가 발생한 개인정보를 선정하고, 회사의 개인정보 보호책임자의 승인을 받아 파기합니다.\n'
|
||||||
|
'- 파기방법: 전자적 파일 형태로 기록·저장된 개인정보는 기록을 재생할 수 없도록 파기하며, 종이 문서에 기록·저장된 개인정보는 분쇄기로 분쇄하거나 소각하여 파기합니다.\n\n'
|
||||||
|
'5. 이용자와 법정대리인의 권리·의무 및 행사방법\n'
|
||||||
|
'- 이용자는 회사에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다. (앱 내 "내 정보 수정" 또는 "회원 탈퇴" 메뉴 이용)\n'
|
||||||
|
'- 권리 행사는 회사에 대해 서면, 전화, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 회사는 이에 대해 지체 없이 조치하겠습니다.\n\n'
|
||||||
|
'6. 개인정보의 안전성 확보조치\n'
|
||||||
|
'회사는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.\n'
|
||||||
|
'- 관리적 조치: 내부관리계획 수립·시행, 정기적 직원 교육\n'
|
||||||
|
'- 기술적 조치: 개인정보처리시스템 등의 접근권한 관리, 비밀번호 등 중요 정보의 암호화 저장, 보안프로그램 설치 및 주기적 점검\n'
|
||||||
|
'- 물리적 조치: 전산실, 자료보관실 등의 접근통제\n\n'
|
||||||
|
'7. 개인정보 보호책임자\n'
|
||||||
|
'회사는 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.\n'
|
||||||
|
'- 성명: 임지은\n'
|
||||||
|
'- 직책: 대표이사\n'
|
||||||
|
'- 연락처: 070-4272-9322, ljieun9005@gmail.com\n\n'
|
||||||
|
'8. 개인정보 처리방침의 변경 및 고지 의무\n'
|
||||||
|
'회사는 개인정보 처리방침의 법령, 정책 또는 보안기술의 변경에 따라 내용의 추가, 삭제 및 수정이 있을 시에는 개정 최소 7일 전부터 애플리케이션 내 "공지사항", 팝업, 또는 앱 PUSH 알림 등을 통하여 변경 사유 및 내용을 공지하도록 하겠습니다.\n'
|
||||||
|
'다만, 수집하는 개인정보의 항목, 이용목적의 변경 등과 같이 이용자의 권리에 중대한 변경이 발생할 때에는 최소 30일 전에 미리 공지하며, 필요 시 이용자의 동의를 다시 받을 수 있습니다.\n\n'
|
||||||
|
'부 칙\n'
|
||||||
|
'개인정보 처리방침은 2025년 11월 28일부터 시행합니다.\n';
|
||||||
|
|
||||||
|
const String _locationTermContent =
|
||||||
|
'[위치기반서비스 이용약관]\n\n'
|
||||||
|
'제 1 조 (목적)\n'
|
||||||
|
'본 약관은 (주)메타큐랩(이하 "회사")이 제공하는 스마트 안전모 보관함 애플리케이션 서비스(이하 "서비스")와 관련하여 회사와 개인위치정보주체(이하 "회원") 간의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n'
|
||||||
|
'제 2 조 (약관의 효력 및 변경)\n'
|
||||||
|
'1. 본 약관은 서비스를 신청한 고객 또는 개인위치정보주체가 본 약관에 동의하고 회사가 소정의 절차에 따라 서비스의 이용자로 등록함으로써 효력이 발생합니다.\n'
|
||||||
|
'2. 회사는 법률이나 위치기반서비스의 변경사항을 반영하기 위해 본 약관을 수정할 수 있으며, 약관을 개정할 경우에는 적용일자 7일 전부터 웹사이트 또는 애플리케이션 공지사항을 통해 공지합니다. 다만, 회원에게 불리한 내용으로 변경할 경우에는 최소 30일 전에 공지합니다.\n\n'
|
||||||
|
'제 3 조 (서비스의 내용)\n'
|
||||||
|
'회사는 위치정보사업자로부터 위치정보를 전달받아 아래와 같은 위치기반서비스를 제공합니다.\n'
|
||||||
|
'- 내 주변 보관함 찾기: 회원의 현재 위치를 기반으로 주변에 있는 스마트 안전모 보관함의 위치를 지도에 표시하고, 거리순으로 정렬하여 제공합니다.\n'
|
||||||
|
'- 이용 내역 및 경로 관리: 안전모 대여 시점과 반납 시점의 위치 정보를 기록하여 이용 내역을 관리하고, 비정상적인 반납(지정 구역 외 반납 등)을 확인합니다.\n'
|
||||||
|
'- 긴급 구조 및 사고/장애 신고: 보관함 이용 중 사고 또는 장애 발생 시, 신고 접수 단계에서 회원의 현재 위치를 확인하여 신속한 유지보수 및 대응을 지원합니다.\n\n'
|
||||||
|
'제 4 조 (서비스 이용요금)\n'
|
||||||
|
'회사가 제공하는 위치기반서비스는 무료입니다. 단, 무선 서비스 이용 시 발생하는 데이터 통신료는 별도이며, 이는 회원이 가입한 이동통신사의 정책에 따릅니다.\n\n'
|
||||||
|
'제 5 조 (개인위치정보주체의 권리)\n'
|
||||||
|
'1. 회원은 언제든지 개인위치정보의 수집, 이용 또는 제공에 대한 동의의 전부 또는 일부를 철회할 수 있습니다. 이 경우 회사는 수집한 개인위치정보 및 위치정보 이용·제공사실 확인자료를 파기합니다.\n'
|
||||||
|
'2. 회원은 언제든지 개인위치정보의 수집, 이용 또는 제공의 일시적인 중지를 요구할 수 있으며, 회사는 이를 거절할 수 없습니다.\n'
|
||||||
|
'3. 회원은 회사에 대하여 다음 각 호의 자료에 대한 열람 또는 고지를 요구할 수 있고, 당해 자료에 오류가 있는 경우에는 그 정정을 요구할 수 있습니다.\n'
|
||||||
|
'- 본인에 대한 위치정보 수집, 이용, 제공사실 확인자료\n'
|
||||||
|
'- 본인의 개인위치정보가 위치정보의 보호 및 이용 등에 관한 법률 또는 다른 법률 규정에 의하여 제3자에게 제공된 이유 및 내용\n\n'
|
||||||
|
'제 6 조 (위치정보 이용·제공사실 확인자료의 보유근거 및 보유기간)\n'
|
||||||
|
'회사는 위치정보의 보호 및 이용 등에 관한 법률 제16조 제2항에 근거하여 회원의 위치정보 수집, 이용, 제공사실 확인자료를 위치정보시스템에 자동으로 기록하며, 해당 자료는 6개월 이상 보관합니다.\n\n'
|
||||||
|
'제 7 조 (서비스의 변경 및 중지)\n'
|
||||||
|
'1. 회사는 위치정보사업자의 정책 변경 또는 기술적 사정으로 인하여 서비스의 전부 또는 일부를 제한, 변경하거나 중지할 수 있습니다.\n'
|
||||||
|
'2. 위와 같은 경우 회사는 그 사유를 사전에 공지하거나 통지합니다.\n\n'
|
||||||
|
'제 8 조 (개인위치정보의 제3자 제공)\n'
|
||||||
|
'회사는 회원의 동의 없이 개인위치정보를 제3자에게 제공하지 않으며, 제3자에게 제공하는 경우에는 제공받는 자, 제공일시 및 제공목적을 즉시 회원에게 통보합니다.\n'
|
||||||
|
'단, 법령의 규정에 의거하거나, 수사 목적으로 법령에 정해진 절차와 방법에 따라 수사기관의 요구가 있는 경우는 예외로 합니다.\n\n'
|
||||||
|
'제 9 조 (손해배상)\n'
|
||||||
|
'회사가 위치정보의 보호 및 이용 등에 관한 법률 제15조 내지 제26조의 규정을 위반하여 회원에게 손해가 발생한 경우, 회원이 고의 또는 과실 없음을 입증하지 아니하면 회원은 그 손해에 대해 배상을 청구할 수 있습니다.\n\n'
|
||||||
|
'제 10 조 (분쟁의 조정)\n'
|
||||||
|
'1. 회사는 위치정보와 관련된 분쟁에 대해 당사자 간 협의가 이루어지지 아니하거나 협의를 할 수 없는 경우에는 방송통신위원회에 재정을 신청할 수 있습니다.\n'
|
||||||
|
'2. 회사 또는 고객은 위치정보와 관련된 분쟁에 대해 당사자 간 협의가 이루어지지 아니하거나 협의를 할 수 없는 경우에는 개인정보분쟁조정위원회에 조정을 신청할 수 있습니다.\n\n'
|
||||||
|
'제 11 조 (사업자 정보 및 위치정보 관리책임자)\n'
|
||||||
|
'1. 회사의 상호 및 주소 등은 다음과 같습니다.\n'
|
||||||
|
'- 상호: 주식회사 메타큐랩\n'
|
||||||
|
'- 주소: 전라남도 나주시 빛가람로 760, 103호\n'
|
||||||
|
'- 대표전화: 070-4272-9322\n'
|
||||||
|
'2. 회사는 개인위치정보를 적절히 관리·보호하고 개인위치정보주체의 불만을 원활히 처리할 수 있도록 실질적인 책임을 질 수 있는 지위에 있는 자를 위치정보 관리책임자로 지정해 운영합니다.\n'
|
||||||
|
'- 위치정보 관리책임자: 임지은\n'
|
||||||
|
'- 연락처: ljieun9005@gmail.com\n\n'
|
||||||
|
'부칙 제1조 (시행일)\n'
|
||||||
|
'본 약관은 2025년 11월 28일부터 시행합니다.\n';
|
||||||
|
|
||||||
|
const String _marketingTermContent =
|
||||||
|
'(주)메타큐랩이 제공하는 이벤트 및 혜택 등 마케팅 정보를 수신하는 것에 동의합니다.\n\n'
|
||||||
|
'1. 수집 및 이용 목적\n'
|
||||||
|
'- 신규 서비스(기능) 개발 및 맞춤 서비스 제공.\n'
|
||||||
|
'- 이벤트, 프로모션, 혜택 등 광고성 정보 제공.\n'
|
||||||
|
'- 서비스 이용 통계 및 설문조사.\n\n'
|
||||||
|
'2. 수집하는 개인정보 항목\n'
|
||||||
|
'- 이름, 휴대전화번호, 이메일, PUSH 토큰.\n\n'
|
||||||
|
'3. 보유 및 이용 기간\n'
|
||||||
|
'- 회원 탈퇴 또는 동의 철회 시까지\n\n'
|
||||||
|
'4. 전송 방법\n'
|
||||||
|
'- 앱 푸시(App Push) 알림, SMS(문자메시지), 이메일 등.\n\n'
|
||||||
|
'5. 동의 거부 권리\n'
|
||||||
|
'귀하는 마케팅 정보 수신에 대한 동의를 거부할 수 있습니다. 동의를 거부하더라도 회원 가입 및 기본 서비스 이용에는 제한이 없습니다. 다만, 이벤트 및 혜택 정보 제공이 제한될 수 있습니다.\n';
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
@@ -27,6 +193,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _isRentalAlertEnabled = true;
|
bool _isRentalAlertEnabled = true;
|
||||||
bool _isStorageStatusAlert = true;
|
bool _isStorageStatusAlert = true;
|
||||||
bool _isEnvSensorAlert = true;
|
bool _isEnvSensorAlert = true;
|
||||||
|
bool _isMarketingAlertEnabled = true;
|
||||||
|
|
||||||
bool _isBiometricEnabled = false;
|
bool _isBiometricEnabled = false;
|
||||||
bool _isAutoLogoutEnabled = true;
|
bool _isAutoLogoutEnabled = true;
|
||||||
@@ -35,6 +202,120 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
final String _currentAppVersion = "v1.0.2";
|
final String _currentAppVersion = "v1.0.2";
|
||||||
|
|
||||||
|
void _showDeleteAccountDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20.0),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: _warningColor,
|
||||||
|
size: 64.0,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'회원 탈퇴',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _mainTextColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'탈퇴 시 모든 이용 기록이 영구 삭제되며,\n 식제된 데이터는 복구할 수 없습니다.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14.0,
|
||||||
|
color: _subTextColor,
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
side: BorderSide(color: Colors.grey.shade300),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'유지하기',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("탈퇴 요청이 처리되었습니다.")),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: _warningColor,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'탈퇴하기',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showCommonModal(BuildContext context, String title, Widget content, {String rightButtonLabel = '닫기'}) {
|
void _showCommonModal(BuildContext context, String title, Widget content, {String rightButtonLabel = '닫기'}) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -217,17 +498,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showTermsSheet(BuildContext context) {
|
void _showTermContentSheet(BuildContext context, String title, String content) {
|
||||||
_showCommonModal(
|
_showCommonModal(
|
||||||
context,
|
context,
|
||||||
'이용 약관',
|
title,
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
child: Text(
|
child: Text(
|
||||||
'제 1 조 (목적)\n이 약관은 스마트 헬멧 서비스(이하 "서비스")의 이용 조건 및 절차, 이용자와 회사의 권리, 의무, 책임 사항을 규정함을 목적으로 합니다.\n\n제 2 조 (용어의 정의)\n1. "이용자"란 앱에 접속하여 본 약관에 따라 서비스를 이용하는 회원을 말합니다.\n2. "헬멧"이란 회사가 대여하는 스마트 IoT 안전모를 말합니다.\n\n(이하 생략... 더미 데이터입니다.)\n\n제 3 조 (약관의 효력)\n본 약관은 서비스를 신청한 때부터 효력이 발생합니다.',
|
content,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
style: TextStyle(color: _subTextColor, height: 1.6),
|
style: TextStyle(color: _subTextColor, height: 1.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +554,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildInfoLink('전화 상담 연결', Icons.phone, value: '1588-0000'),
|
_buildInfoLink('전화 상담 연결', Icons.phone, value: '070-4272-9322'),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildInfoLink('1:1 채팅 상담', Icons.chat_bubble_outline),
|
_buildInfoLink('1:1 채팅 상담', Icons.chat_bubble_outline),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_buildInfoLink('이메일 문의', Icons.email_outlined, value: 'help@smarthelmet.com'),
|
_buildInfoLink('이메일 문의', Icons.email_outlined, value: 'ljieun9005@gmail.com'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -286,7 +571,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
ListView(
|
ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
_buildFAQItem('Q. 헬멧 대여는 어떻게 하나요?', 'A. 메인 화면의 지도에서 가까운 보관함을 찾은 후, QR코드를 스캔하여 대여할 수 있습니다.'),
|
_buildFAQItem('Q. 안전모 대여는 어떻게 하나요?', 'A. 메인 화면의 지도에서 가까운 보관함을 찾은 후, QR코드를 스캔하여 대여할 수 있습니다.'),
|
||||||
_buildFAQItem('Q. 반납이 안 될 때는 어떻게 하나요?', 'A. 보관함의 통신 상태를 확인해 주세요. 지속적으로 실패할 경우 고객센터로 연락 바랍니다.'),
|
_buildFAQItem('Q. 반납이 안 될 때는 어떻게 하나요?', 'A. 보관함의 통신 상태를 확인해 주세요. 지속적으로 실패할 경우 고객센터로 연락 바랍니다.'),
|
||||||
_buildFAQItem('Q. 결제 수단 변경은 어디서 하나요?', 'A. [마이페이지] > [결제 관리] 메뉴에서 카드 정보를 변경하실 수 있습니다.'),
|
_buildFAQItem('Q. 결제 수단 변경은 어디서 하나요?', 'A. [마이페이지] > [결제 관리] 메뉴에서 카드 정보를 변경하실 수 있습니다.'),
|
||||||
],
|
],
|
||||||
@@ -324,7 +609,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
const Text('알림 권한이 꺼져 있나요?', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
const Text('알림 권한이 꺼져 있나요?', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text('중요한 헬멧 안전 경고 및 반납 알림을 받으려면 기기 설정에서 알림을 허용해야 합니다.', style: TextStyle(color: _subTextColor, height: 1.5)),
|
Text('중요한 안전모 안전 경고 및 반납 알림을 받으려면 기기 설정에서 알림을 허용해야 합니다.', style: TextStyle(color: _subTextColor, height: 1.5)),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
_buildInfoLink('기기 설정으로 이동', Icons.settings, onTap: () => Navigator.pop(context)),
|
_buildInfoLink('기기 설정으로 이동', Icons.settings, onTap: () => Navigator.pop(context)),
|
||||||
],
|
],
|
||||||
@@ -356,7 +641,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'헬멧 대여 및 반납 시 QR코드를 스캔하기 위해\n카메라 접근 권한이 반드시 필요합니다.\n권한을 거부하면 서비스를 이용할 수 없습니다.',
|
'안전모 대여 및 반납 시 QR코드를 스캔하기 위해\n카메라 접근 권한이 반드시 필요합니다.\n권한을 거부하면 서비스를 이용할 수 없습니다.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: _subTextColor, height: 1.5),
|
style: TextStyle(color: _subTextColor, height: 1.5),
|
||||||
),
|
),
|
||||||
@@ -609,6 +894,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
(val) => setState(() => _isEnvSensorAlert = val),
|
(val) => setState(() => _isEnvSensorAlert = val),
|
||||||
showDivider: true,
|
showDivider: true,
|
||||||
),
|
),
|
||||||
|
_buildToggleItem(
|
||||||
|
'이벤트 및 마케팅 알림',
|
||||||
|
'혜택 및 소식 받기',
|
||||||
|
_isMarketingAlertEnabled,
|
||||||
|
(val) => setState(() => _isMarketingAlertEnabled = val),
|
||||||
|
showDivider: true,
|
||||||
|
onTapLabel: () => _showTermContentSheet(context, '마케팅 수신 동의', _marketingTermContent),
|
||||||
|
),
|
||||||
_buildDivider(16),
|
_buildDivider(16),
|
||||||
_buildInfoLink('알림 방식', Icons.notifications_active_outlined, value: '배너 + 진동', onTap: () => _showNotificationStyleSheet(context)),
|
_buildInfoLink('알림 방식', Icons.notifications_active_outlined, value: '배너 + 진동', onTap: () => _showNotificationStyleSheet(context)),
|
||||||
],
|
],
|
||||||
@@ -655,7 +948,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildInfoLink('버전 정보', null, value: _currentAppVersion, showArrow: false),
|
_buildInfoLink('버전 정보', null, value: _currentAppVersion, showArrow: false),
|
||||||
_buildDivider(16),
|
_buildDivider(16),
|
||||||
_buildInfoLink('이용 약관 및 개인정보 처리방침', Icons.article_outlined, onTap: () => _showTermsSheet(context)),
|
_buildInfoLink('서비스 이용약관', Icons.description_outlined, onTap: () => _showTermContentSheet(context, '서비스 이용약관', _serviceTermContent)),
|
||||||
|
_buildDivider(16),
|
||||||
|
_buildInfoLink('개인정보 처리방침', Icons.privacy_tip_outlined, onTap: () => _showTermContentSheet(context, '개인정보 처리방침', _privacyTermContent)),
|
||||||
|
_buildDivider(16),
|
||||||
|
_buildInfoLink('위치기반 서비스 이용약관', Icons.location_on_outlined, onTap: () => _showTermContentSheet(context, '위치기반 서비스 이용약관', _locationTermContent)),
|
||||||
_buildDivider(16),
|
_buildDivider(16),
|
||||||
_buildInfoLink('오픈소스 라이선스', Icons.code, onTap: () => _showLicenseSheet(context)),
|
_buildInfoLink('오픈소스 라이선스', Icons.code, onTap: () => _showLicenseSheet(context)),
|
||||||
_buildDivider(16),
|
_buildDivider(16),
|
||||||
@@ -671,13 +968,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () {},
|
onPressed: () {
|
||||||
|
_showDeleteAccountDialog(context);
|
||||||
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'회원 탈퇴 신청',
|
'회원 탈퇴 신청',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _warningColor,
|
color: Colors.black,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -722,7 +1021,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
|
|
||||||
Widget _buildToggleItem(
|
Widget _buildToggleItem(
|
||||||
String title, String subtitle, bool value, ValueChanged<bool> onChanged,
|
String title, String subtitle, bool value, ValueChanged<bool> onChanged,
|
||||||
{required bool showDivider}) {
|
{required bool showDivider, VoidCallback? onTapLabel}) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -731,15 +1030,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTapLabel,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
Text(title, style: TextStyle(color: _mainTextColor, fontSize: 14, fontWeight: FontWeight.w500)),
|
||||||
|
if (onTapLabel != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
|
child: Icon(Icons.info_outline, size: 14, color: _subTextColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(subtitle, style: TextStyle(color: _subTextColor, fontSize: 11)),
|
Text(subtitle, style: TextStyle(color: _subTextColor, fontSize: 11)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Transform.scale(
|
Transform.scale(
|
||||||
scale: 0.8,
|
scale: 0.8,
|
||||||
child: Switch(
|
child: Switch(
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class CustomHeader extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'스마트 헬멧 보관함',
|
'스마트 안전모 보관함',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
Reference in New Issue
Block a user