diff --git a/assets/images/info1.png b/assets/images/info1.png deleted file mode 100644 index 5063eba..0000000 Binary files a/assets/images/info1.png and /dev/null differ diff --git a/assets/images/info2.png b/assets/images/info2.png deleted file mode 100644 index 10f4435..0000000 Binary files a/assets/images/info2.png and /dev/null differ diff --git a/assets/images/metaq_v.png b/assets/images/metaq_v.png new file mode 100644 index 0000000..2c5ef8b Binary files /dev/null and b/assets/images/metaq_v.png differ diff --git a/assets/images/open.png b/assets/images/open.png deleted file mode 100644 index 71bdad9..0000000 Binary files a/assets/images/open.png and /dev/null differ diff --git a/assets/images/white_1.png b/assets/images/white_1.png new file mode 100644 index 0000000..705dfb4 Binary files /dev/null and b/assets/images/white_1.png differ diff --git a/assets/images/white_2.png b/assets/images/white_2.png new file mode 100644 index 0000000..0e12db0 Binary files /dev/null and b/assets/images/white_2.png differ diff --git a/lib/home_screen_content.dart b/lib/home_screen_content.dart index a89739d..1aa3403 100644 --- a/lib/home_screen_content.dart +++ b/lib/home_screen_content.dart @@ -30,19 +30,18 @@ class _HomeScreenContentState extends State { final LockerApi _api = LockerApi(); bool _isLoading = false; -Future _runLockerAction(String name, Future Function() action) async { + Future _runLockerAction(String name, Future Function() action) async { if (_isLoading) return; setState(() => _isLoading = true); - + // 실제 명령 전송 final success = await action(); - - setState(() => _isLoading = false); if (!mounted) return; - } + setState(() => _isLoading = false); + } // 25.12.03 지은 추가 끝 int _selectedImageIndex = 0; @@ -98,7 +97,7 @@ Future _runLockerAction(String name, Future Function() action) async Widget _buildOverviewSection() { return Container( margin: const EdgeInsets.only(top: 5), - child: Card( + child: DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Column( @@ -160,7 +159,7 @@ Future _runLockerAction(String name, Future Function() action) async _selectedImageIndex == 0 ? 'assets/images/storage.png' : 'assets/images/top.png', - width: 100, + width: 90, ), ), Positioned( @@ -251,7 +250,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildBatteryStatusCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -332,7 +331,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildControlCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -348,24 +347,24 @@ Future _runLockerAction(String name, Future Function() action) async child: Row( children: [ Expanded( - child: _buildStyledToggleSwitch( - 'UV LED', - _controlToggles['UV LED']!, - // 25.12.03 지은 수정 시작 - (val) { - _runLockerAction("UV 제어", () async { - bool success = await _api.setUV(val); + child: _buildStyledToggleSwitch( + 'UV LED', + _controlToggles['UV LED']!, + // 25.12.03 지은 수정 시작 + (val) { + _runLockerAction("UV 제어", () async { + bool success = await _api.setUV(val); - if (success) { - setState(() => _controlToggles['UV LED'] = val); - } - return success; - } - ); - }, - ), - ), - // 25.12.03 지은 수정 끝 + if (success) { + setState(() => _controlToggles['UV LED'] = val); + } + return success; + } + ); + }, + ), + ), + // 25.12.03 지은 수정 끝 VerticalDivider(color: _mainBlueColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( child: _buildStyledToggleSwitch( @@ -380,24 +379,24 @@ Future _runLockerAction(String name, Future Function() action) async (val) => setState(() => _controlToggles['HELMET'] = val))), VerticalDivider(color: _subTextColor.withOpacity(0.5), indent: 10, endIndent: 10), Expanded( - child: _buildStyledToggleSwitch('FAN', - _controlToggles['FAN']!, - // 25.12.03 지은 수정 시작 - (val) { - print("👉 [디버깅] fan 눌림! 값: $val"); - _runLockerAction("FAN 제어", () async { - print("👉 [디버깅] API 요청 시작..."); - bool success = await _api.setFan(val); - - if (success) { - setState(() => _controlToggles['FAN'] = val); - } - return success; - }); - }, - // 25.12.03 지은 수정 끝 - ), - ), + child: _buildStyledToggleSwitch('FAN', + _controlToggles['FAN']!, + // 25.12.03 지은 수정 시작 + (val) { + print("👉 [디버깅] fan 눌림! 값: $val"); + _runLockerAction("FAN 제어", () async { + print("👉 [디버깅] API 요청 시작..."); + bool success = await _api.setFan(val); + + if (success) { + setState(() => _controlToggles['FAN'] = val); + } + return success; + }); + }, + // 25.12.03 지은 수정 끝 + ), + ), ], ), ) @@ -472,7 +471,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildEnvironmentSensorsCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -542,7 +541,7 @@ Future _runLockerAction(String name, Future Function() action) async Widget _buildMyLocationCard() { const LatLng exampleLocation = LatLng(37.5665, 126.9780); - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, clipBehavior: Clip.antiAlias, @@ -610,7 +609,7 @@ Future _runLockerAction(String name, Future Function() action) async } Widget _buildActivityCard() { - return Card( + return DashboardCard( shadow: _cleanShadow, cardColor: _cardBackgroundColor, child: Padding( @@ -711,7 +710,7 @@ class _LineChartPainter extends CustomPainter { } } -class Card extends StatelessWidget { +class DashboardCard extends StatelessWidget { final Widget child; final EdgeInsetsGeometry? padding; final Clip clipBehavior; @@ -719,7 +718,7 @@ class Card extends StatelessWidget { final Color? cardColor; final BoxShadow? shadow; - const Card({ + const DashboardCard({ super.key, required this.child, this.padding, diff --git a/lib/login_screen.dart b/lib/login_screen.dart index bcd04d4..013d2df 100644 --- a/lib/login_screen.dart +++ b/lib/login_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'main.dart'; +import 'register_screen.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -15,7 +16,7 @@ class _LoginScreenState extends State { final FocusNode _idFocusNode = FocusNode(); final FocusNode _pwFocusNode = FocusNode(); - final Color mainBlueColor = const Color(0xFF007AFF); + final Color mainBlueColor = const Color(0xFF0033CC); bool _isIdFocused = false; bool _isPwFocused = false; @@ -45,90 +46,101 @@ class _LoginScreenState extends State { return Scaffold( backgroundColor: Colors.white, body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Icon(Icons.polymer, size: 80, color: mainBlueColor), - const SizedBox(height: 20), - Text( - 'METAQLAB', - textAlign: TextAlign.center, - style: TextStyle( - color: mainBlueColor, - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), - ), - const SizedBox(height: 60), - - _buildCustomTextField( - label: '아이디', - controller: _idController, - focusNode: _idFocusNode, - isFocused: _isIdFocused, - ), - const SizedBox(height: 16), - - _buildCustomTextField( - label: '비밀번호', - controller: _pwController, - focusNode: _pwFocusNode, - isFocused: _isPwFocused, - isObscure: true, - ), - - const SizedBox(height: 40), - - ElevatedButton( - onPressed: () { - String inputId = _idController.text; - String inputPw = _pwController.text; - - if (inputId == 'user' && inputPw == '1234') { - print('로그인 성공!'); - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); - } else { - print('로그인 실패'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('아이디 또는 비밀번호가 틀렸습니다.'), - backgroundColor: Colors.redAccent, - duration: Duration(seconds: 2), - ), - ); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: mainBlueColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + child: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.asset( + 'assets/images/metaq_v.png', + width: 86, + height: 86, + fit: BoxFit.contain, ), - elevation: 0, - ), - child: const Text( - '로그인', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - ), + const SizedBox(height: 70), + _buildCustomTextField( + label: '아이디', + controller: _idController, + focusNode: _idFocusNode, + isFocused: _isIdFocused, + ), + const SizedBox(height: 16), + _buildCustomTextField( + label: '비밀번호', + controller: _pwController, + focusNode: _pwFocusNode, + isFocused: _isPwFocused, + isObscure: true, + ), + const SizedBox(height: 40), + ElevatedButton( + onPressed: () { + String inputId = _idController.text; + String inputPw = _pwController.text; - const SizedBox(height: 20), - TextButton( - onPressed: () {}, - child: Text( - '비밀번호 찾기 / 회원가입', - style: TextStyle(color: mainBlueColor, fontWeight: FontWeight.w500), - ), + if (inputId == 'user' && inputPw == '1234') { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen()), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('아이디 또는 비밀번호가 틀렸습니다.'), + backgroundColor: Colors.redAccent, + duration: Duration(seconds: 2), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: mainBlueColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + ), + child: const Text( + '로그인', + style: + TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 12), + + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + 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 { }) { return AnimatedContainer( duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: BoxDecoration( color: Colors.white, border: Border.all( @@ -153,36 +165,27 @@ class _LoginScreenState extends State { ), borderRadius: BorderRadius.circular(8), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle( - color: isFocused ? mainBlueColor : Colors.grey.shade600, - fontSize: 12, - fontWeight: FontWeight.w600, - ), + child: TextField( + controller: controller, + focusNode: focusNode, + obscureText: isObscure, + decoration : InputDecoration( + hintText: label, + hintStyle: TextStyle( + color: Colors.grey.shade400, + fontSize: 16, ), - const SizedBox(height: 4), - TextField( - controller: controller, - focusNode: focusNode, - obscureText: isObscure, - decoration: const InputDecoration( - isDense: true, - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - ), - style: const TextStyle( - fontSize: 16, - color: Colors.black87, - ), - cursorColor: mainBlueColor, - ), - ], + isDense: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + style: const TextStyle( + fontSize: 16, + color: Colors.black87, + ), + cursorColor: mainBlueColor, ), ); } diff --git a/lib/main.dart b/lib/main.dart index 22b59f3..57894a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -39,7 +39,7 @@ class SmartHelmetApp extends StatelessWidget { bodyMedium: TextStyle(color: _subTextColor, fontWeight: FontWeight.w400), ), ), - home: const HomeScreen(), + home: const LoginScreen(), ); } } diff --git a/lib/register_screen.dart b/lib/register_screen.dart new file mode 100644 index 0000000..b3a7b22 --- /dev/null +++ b/lib/register_screen.dart @@ -0,0 +1,437 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class RegisterScreen extends StatefulWidget { + const RegisterScreen({super.key}); + + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State { + 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 _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) { + 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.length < 50 + ? '$content\n\n' + '제1조 (목적)\n본 약관은 메타큐랩(이하 "회사")이 제공하는 모든 서비스의 이용조건 및 절차, 이용자와 회사의 권리, 의무, 책임사항과 기타 필요한 사항을 규정함을 목적으로 합니다.\n\n' + '제2조 (용어의 정의)\n1. "서비스"라 함은 회원이 이용할 수 있는 관련 제반 서비스를 의미합니다.\n' + '2. "회원"이라 함은 회사의 "서비스"에 접속하여 본 약관에 따라 회사와 이용계약을 체결하고 회사가 제공하는 "서비스"를 이용하는 고객을 말합니다.\n\n' + '제3조 (약관의 게시와 개정)\n1. 회사는 이 약관의 내용을 회원이 쉽게 알 수 있도록 서비스 초기 화면에 게시합니다.\n' + '2. 회사는 "약관의 규제에 관한 법률" 등 관련 법령을 위배하지 않는 범위에서 이 약관을 개정할 수 있습니다.\n\n' + '(이하 생략)' + : 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: '서비스 이용약관 내용입니다...', + ), + const SizedBox(height: 12), + _buildAgreementRow( + title: '(필수) 개인정보 수집 및 이용 동의', + value: _isPrivacyAgreed, + onChanged: (v) => setState(() => _isPrivacyAgreed = v!), + details: '개인정보 처리방침 내용입니다...', + ), + const SizedBox(height: 12), + _buildAgreementRow( + title: '(선택) 이벤트 및 마케팅 수신 동의', + value: _isMarketingAgreed, + onChanged: (v) => setState(() => _isMarketingAgreed = v!), + details: '마케팅 정보 수신 동의 내용입니다...', + ), + ], + ), + ), + + 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 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)); + } +} \ No newline at end of file diff --git a/lib/rental_process_screen.dart b/lib/rental_process_screen.dart index 54fa613..f36717a 100644 --- a/lib/rental_process_screen.dart +++ b/lib/rental_process_screen.dart @@ -53,8 +53,8 @@ Future _runLockerAction(String name, Future Function() action) async Timer? _timer; final List _imageList = [ 'assets/images/top.png', - 'assets/images/info1.png', - 'assets/images/info2.png', + 'assets/images/white_1.png', + 'assets/images/white_2.png', ]; @override diff --git a/lib/settings_screen.dart b/lib/settings_screen.dart index aec5e55..2e86a5b 100644 --- a/lib/settings_screen.dart +++ b/lib/settings_screen.dart @@ -35,6 +35,120 @@ class _SettingsScreenState extends State { 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 = '닫기'}) { showModalBottomSheet( context: context, @@ -671,13 +785,15 @@ class _SettingsScreenState extends State { const SizedBox(height: 40), Center( child: TextButton( - onPressed: () {}, + onPressed: () { + _showDeleteAccountDialog(context); + }, child: Text( '회원 탈퇴 신청', style: TextStyle( - color: _warningColor, - fontWeight: FontWeight.bold, - fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.w500, + fontSize: 12, ), ), ),