diff --git a/flutter_01.png b/flutter_01.png new file mode 100644 index 0000000..b1008f1 Binary files /dev/null and b/flutter_01.png differ diff --git a/lib/id_finding_page.dart b/lib/id_finding_page.dart index 47e984e..72d6eda 100644 --- a/lib/id_finding_page.dart +++ b/lib/id_finding_page.dart @@ -1,11 +1,190 @@ import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:convert' show utf8; import 'login_page.dart'; import 'pw_finding_page.dart'; import 'signup_page.dart'; -class IdFindingPage extends StatelessWidget { +class IdFindingPage extends StatefulWidget { const IdFindingPage({Key? key}) : super(key: key); + @override + _IdFindingPageState createState() => _IdFindingPageState(); +} + +class _IdFindingPageState extends State { + final TextEditingController nicknameController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + String nicknameErrorMessage = ''; + String emailErrorMessage = ''; + String foundIdMessage = ''; + String authId = ''; + + Future _findId(String nickname, String email) async { + // 로딩 인디케이터 표시 + showDialog( + context: context, + barrierDismissible: false, // 바깥 클릭으로 닫지 않도록 설정 + builder: (BuildContext context) { + return const Center(child: CircularProgressIndicator()); + }, + ); + + try { + final response = await http.post( + Uri.parse('https://eldsoft.com:8097/user/find/id'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'nickname': nickname, + 'user_email': email, + }), + ).timeout(const Duration(seconds: 10)); // 10초 타임아웃 설정 + + String responseBody = utf8.decode(response.bodyBytes); + + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + + if (response.statusCode == 200) { + final Map jsonResponse = jsonDecode(responseBody); + print('ID 찾기 성공: $jsonResponse'); + + // 초기화 + setState(() { + nicknameErrorMessage = ''; + emailErrorMessage = ''; + foundIdMessage = ''; // ID 메시지 초기화 + }); + + if (jsonResponse['response_info']['msg_title'] == '닉네임 확인') { + setState(() { + nicknameErrorMessage = '닉네임을 다시 확인해주세요'; // 닉네임 오류 메시지 설정 + }); + } else if (jsonResponse['response_info']['msg_title'] == '이메일 확인') { + setState(() { + emailErrorMessage = '이메일을 다시 확인해주세요'; // 이메일 오류 메시지 설정 + }); + } else if (jsonResponse['result'] == 'OK') { + // ID 찾기 성공 시 처리 + setState(() { + foundIdMessage = '당신의 ID는 ${jsonResponse['data']['user_id']} 입니다'; // ID 메시지 설정 + authId = jsonResponse['data']['auth']; // auth_id 값 저장 + }); + } else { + _showErrorDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY'); + } + } else { + // 요청이 실패했을 때 모달 창 띄우기 + _showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } catch (e) { + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + _showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } + + Future _findAllId() async { + // ID 전체 찾기 요청 처리 + print('ID 전체 찾기 요청 $authId'); // 요청 시 출력 + + // 로딩 인디케이터 표시 + showDialog( + context: context, + barrierDismissible: false, // 바깥 클릭으로 닫지 않도록 설정 + builder: (BuildContext context) { + return const Center(child: CircularProgressIndicator()); + }, + ); + + try { + final response = await http.post( + Uri.parse('https://eldsoft.com:8097/user/find/id/full'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'auth': authId, // authId 값 포함 + }), + ).timeout(const Duration(seconds: 10)); // 10초 타임아웃 설정 + + String responseBody = utf8.decode(response.bodyBytes); // UTF-8 디코딩 + + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + + if (response.statusCode == 200) { + final Map jsonResponse = jsonDecode(responseBody); + print('ID 전체 찾기 성공: $jsonResponse'); + + if (jsonResponse['result'] == 'OK') { + // 성공 시 모달 창 띄우기 + _showSuccessDialog('이메일로 전체 ID를 발송했습니다.'); + } else { + // 실패 시 모달 창 띄우기 + _showErrorDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY'); + } + } else { + // 요청이 실패했을 때 모달 창 띄우기 + _showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } catch (e) { + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + _showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } + + void _showErrorDialog(String title, String content, String action) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, // 모달 배경색을 흰색으로 설정 + title: Text(title, style: const TextStyle(color: Colors.black)), + content: Text(content, style: const TextStyle(color: Colors.black)), + actions: [ + Center( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, // 텍스트 색상 + ), + child: const Text('확인'), + onPressed: () { + Navigator.of(context).pop(); // 모달 닫기 + if (action == 'LOGIN') { + Navigator.of(context).pop(); // 로그인 페이지로 이동 + } + }, + ), + ), + ], + ); + }, + ); + } + + void _showSuccessDialog(String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('성공'), + content: Text(message), + actions: [ + TextButton( + child: const Text('확인'), + onPressed: () { + Navigator.of(context).pop(); // 모달 닫기 + Navigator.of(context).pop(); // 로그인 페이지로 이동 + }, + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -16,76 +195,116 @@ class IdFindingPage extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'ID 찾기', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(height: 32), - TextField( - decoration: InputDecoration( - labelText: '닉네임', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (foundIdMessage.isEmpty) ...[ + const Text( + 'ID 찾기', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), ), - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '이메일', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), + const SizedBox(height: 32), + TextField( + controller: nicknameController, + decoration: InputDecoration( + labelText: '닉네임', + labelStyle: const TextStyle(color: Colors.black), + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.black, width: 2.0), + ), + ), ), + if (nicknameErrorMessage.isNotEmpty) // 닉네임 오류 메시지 표시 + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + nicknameErrorMessage, + style: const TextStyle(color: Colors.red), + ), + ), + const SizedBox(height: 16), + TextField( + controller: emailController, + decoration: InputDecoration( + labelText: '이메일', + labelStyle: const TextStyle(color: Colors.black), + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.black, width: 2.0), + ), + ), + ), + if (emailErrorMessage.isNotEmpty) // 이메일 오류 메시지 표시 + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + emailErrorMessage, + style: const TextStyle(color: Colors.red), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + _findId(nicknameController.text, emailController.text); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + child: const Text('ID 찾기'), + ), + ] else ...[ + // ID 찾기 성공 시 메시지 표시 + Text( + foundIdMessage, + style: const TextStyle(fontSize: 20, color: Colors.black), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + _findAllId(); // ID 전체 찾기 버튼 클릭 시 처리 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + child: const Text('ID 전체 찾기'), + ), + ], + const SizedBox(height: 16), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('로그인', style: TextStyle(color: Colors.black)), ), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - // ID 찾기 로직 추가 - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - foregroundColor: Colors.white, + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const PwFindingPage()), + ); + }, + child: const Text('PW 찾기', style: TextStyle(color: Colors.black)), ), - child: const Text('ID 찾기'), - ), - const SizedBox(height: 16), - TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const PwFindingPage()), - ); - }, - child: const Text('PW 찾기', style: TextStyle(color: Colors.black)), - ), - TextButton( - onPressed: () { - Navigator.pop(context); // 로그인 페이지로 이동 - }, - child: const Text('로그인', style: TextStyle(color: Colors.black)), - ), - TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SignUpPage()), - ); - }, - child: const Text('회원가입', style: TextStyle(color: Colors.black)), - ), - ], + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SignUpPage()), + ); + }, + child: const Text('회원가입', style: TextStyle(color: Colors.black)), + ), + ], + ), ), ), ); diff --git a/lib/login_page.dart b/lib/login_page.dart index 5177935..199e43f 100644 --- a/lib/login_page.dart +++ b/lib/login_page.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:convert' show utf8; +import 'package:crypto/crypto.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'id_finding_page.dart'; import 'pw_finding_page.dart'; import 'signup_page.dart'; @@ -11,7 +16,80 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { - bool _isChecked = false; + final TextEditingController idController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + bool autoLogin = false; + String loginErrorMessage = ''; + + Future _login() async { + String id = idController.text; + String password = passwordController.text; + String autoLoginStatus = autoLogin ? 'Y' : 'N'; + + var bytes = utf8.encode(password); + var digest = sha256.convert(bytes); + + try { + final response = await http.post( + Uri.parse('https://eldsoft.com:8097/user/login'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'user_id': id, + 'user_pw': digest.toString(), + }), + ).timeout(const Duration(seconds: 10)); + + String responseBody = utf8.decode(response.bodyBytes); + + if (response.statusCode == 200) { + final Map jsonResponse = jsonDecode(responseBody); + + if (jsonResponse['result'] == 'OK') { + print('로그인 성공'); + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_token', jsonResponse['auth']['token']); + await prefs.setBool('auto_login', autoLogin); + } else if (jsonResponse['response_info']['msg_title'] == '로그인 실패') { + setState(() { + loginErrorMessage = '회원정보를 다시 확인해주세요.'; + }); + } + } else { + _showDialog('오류', '로그인에 실패했습니다. 관리자에게 문의해주세요.'); + } + } catch (e) { + _showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.'); + } + } + + void _showDialog(String title, String content) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, + title: Text(title, style: const TextStyle(color: Colors.black)), + content: Text(content, style: const TextStyle(color: Colors.black)), + actions: [ + Center( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + ), + child: const Text('확인'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + }, + ); + } @override Widget build(BuildContext context) { @@ -36,6 +114,7 @@ class _LoginPageState extends State { ), const SizedBox(height: 32), TextField( + controller: idController, decoration: InputDecoration( labelText: 'ID', labelStyle: const TextStyle(color: Colors.black), @@ -47,6 +126,8 @@ class _LoginPageState extends State { ), const SizedBox(height: 16), TextField( + controller: passwordController, + obscureText: true, decoration: InputDecoration( labelText: 'PW', labelStyle: const TextStyle(color: Colors.black), @@ -55,15 +136,23 @@ class _LoginPageState extends State { borderSide: const BorderSide(color: Colors.black, width: 2.0), ), ), - obscureText: true, ), + if (loginErrorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + loginErrorMessage, + style: const TextStyle(color: Colors.red), + ), + ), Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ Checkbox( - value: _isChecked, - onChanged: (value) { + value: autoLogin, + onChanged: (bool? value) { setState(() { - _isChecked = value ?? false; + autoLogin = value ?? false; }); }, ), @@ -72,9 +161,7 @@ class _LoginPageState extends State { ), const SizedBox(height: 16), ElevatedButton( - onPressed: () { - // 로그인 로직 추가 - }, + onPressed: _login, style: ElevatedButton.styleFrom( backgroundColor: Colors.black, foregroundColor: Colors.white, diff --git a/lib/pw_finding_page.dart b/lib/pw_finding_page.dart index 0a84aa3..1a0fa96 100644 --- a/lib/pw_finding_page.dart +++ b/lib/pw_finding_page.dart @@ -1,11 +1,118 @@ import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'dart:convert' show utf8; import 'login_page.dart'; // 로그인 페이지 임포트 추가 import 'signup_page.dart'; // 회원가입 페이지 임포트 추가 import 'id_finding_page.dart'; // ID 찾기 페이지 임포트 추가 -class PwFindingPage extends StatelessWidget { +class PwFindingPage extends StatefulWidget { const PwFindingPage({Key? key}) : super(key: key); + @override + _PwFindingPageState createState() => _PwFindingPageState(); +} + +class _PwFindingPageState extends State { + final TextEditingController idController = TextEditingController(); // ID 입력 컨트롤러 + final TextEditingController emailController = TextEditingController(); // 이메일 입력 컨트롤러 + String emailErrorMessage = ''; // 이메일 오류 메시지 + String idErrorMessage = ''; // ID 오류 메시지 + + Future _findPassword(String id, String email) async { + // PW 찾기 요청 처리 + print('PW 찾기 요청: ID: $id, 이메일: $email'); // 요청 시 출력 + + // 로딩 인디케이터 표시 + showDialog( + context: context, + barrierDismissible: false, // 바깥 클릭으로 닫지 않도록 설정 + builder: (BuildContext context) { + return const Center(child: CircularProgressIndicator()); + }, + ); + + try { + final response = await http.post( + Uri.parse('https://eldsoft.com:8097/user/find/password'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'user_id': id, + 'user_email': email, + }), + ).timeout(const Duration(seconds: 10)); // 10초 타임아웃 설정 + + String responseBody = utf8.decode(response.bodyBytes); // UTF-8 디코딩 + + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + + if (response.statusCode == 200) { + final Map jsonResponse = jsonDecode(responseBody); + print('PW 찾기 성공: $jsonResponse'); + + // 추기화 + setState(() { + emailErrorMessage = ''; + idErrorMessage = ''; + }); + + if (jsonResponse['response_info']['msg_title'] == '아이디 확인') { + setState(() { + idErrorMessage = '아이디를 다시 확인해주세요'; // ID 오류 메시지 설정 + }); + } else if (jsonResponse['response_info']['msg_title'] == '이메일 확인') { + setState(() { + emailErrorMessage = '이메일을 다시 확인해주세요'; // 이메일 오류 메시지 설정 + }); + } else if (jsonResponse['result'] == 'OK') { + // 성공 시 모달 창 띄우기 + _showDialog('비밀번호 찾기 안내', '임시 비밀번호가 입력하신 이메일로 발송되었습니다.', 'LOGIN'); + } else { + // 실패 시 모달 창 띄우기 + _showDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY'); + } + } else { + // 요청이 실패했을 때 모달 창 띄우기 + _showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } catch (e) { + Navigator.of(context).pop(); // 로딩 인디케이터 닫기 + _showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY'); + } + } + + void _showDialog(String title, String content, String action) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, // 모달 배경색을 흰색으로 설정 + title: Text(title, style: const TextStyle(color: Colors.black)), + content: Text(content, style: const TextStyle(color: Colors.black)), + actions: [ + Center( + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, // 텍스트 색상 + ), + child: const Text('확인'), + onPressed: () { + Navigator.of(context).pop(); // 모달 닫기 + if (action == 'LOGIN') { + Navigator.of(context).pop(); // 로그인 페이지로 이동 + } + }, + ), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -29,6 +136,7 @@ class PwFindingPage extends StatelessWidget { ), const SizedBox(height: 32), TextField( + controller: idController, // ID 입력 필드 decoration: InputDecoration( labelText: 'ID', labelStyle: const TextStyle(color: Colors.black), @@ -38,8 +146,17 @@ class PwFindingPage extends StatelessWidget { ), ), ), + if (idErrorMessage.isNotEmpty) // ID 오류 메시지 표시 + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + idErrorMessage, + style: const TextStyle(color: Colors.red), + ), + ), const SizedBox(height: 16), TextField( + controller: emailController, // 이메일 입력 필드 decoration: InputDecoration( labelText: '이메일', labelStyle: const TextStyle(color: Colors.black), @@ -49,10 +166,18 @@ class PwFindingPage extends StatelessWidget { ), ), ), + if (emailErrorMessage.isNotEmpty) // 이메일 오류 메시지 표시 + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + emailErrorMessage, + style: const TextStyle(color: Colors.red), + ), + ), const SizedBox(height: 16), ElevatedButton( onPressed: () { - // PW 찾기 로직 추가 + _findPassword(idController.text, emailController.text); // PW 찾기 요청 }, style: ElevatedButton.styleFrom( backgroundColor: Colors.black, diff --git a/lib/signup_page.dart b/lib/signup_page.dart index 50d4d39..3b360b3 100644 --- a/lib/signup_page.dart +++ b/lib/signup_page.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; // http 패키지 임포트 +import 'dart:convert'; // JSON 변환을 위해 임포트 import 'login_page.dart'; // 로그인 페이지 임포트 추가 +import 'dart:convert' show utf8; // UTF-8 디코딩을 위해 임포트 +import 'package:crypto/crypto.dart'; // crypto 패키지 임포트 class SignUpPage extends StatefulWidget { const SignUpPage({Key? key}) : super(key: key); @@ -9,27 +13,170 @@ class SignUpPage extends StatefulWidget { } class _SignUpPageState extends State { + // 상태 변수 bool _isAgreed = false; // 개인정보 수집 동의 체크박스 상태 - String _username = ''; // 아이디 입력값 - String? _usernameError; // 아이디 오류 메시지 + String _username = '', _password = '', _confirmPassword = '', _nickname = '', _email = ''; + String _department = '', _introduceMyself = ''; // 소속 및 자기소개 + String? _usernameError, _passwordError, _confirmPasswordError, _nicknameError, _emailError; - // 아이디 패턴 검증 - bool _isUsernameValid(String username) { - final RegExp regex = RegExp(r'^(?![0-9])[A-Za-z0-9]{6,20}$'); - return regex.hasMatch(username); - } + // 유효성 검사 + bool _isUsernameValid(String username) => RegExp(r'^(?![0-9])[A-Za-z0-9]{6,20}$').hasMatch(username); + bool _isPasswordValidPattern(String password) => RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$').hasMatch(password); + bool _isEmailValid(String email) => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + bool _isNicknameValid(String nickname) => RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname); - void _validateUsername(String value) { + // 입력값 검증 + void _validateInput(String label) { setState(() { - _username = value; - if (_isUsernameValid(value)) { - _usernameError = null; // 유효한 경우 오류 메시지 제거 - } else { - _usernameError = '* 아이디는 6~20자 영문 대소문자와 숫자 조합이어야 하며, 숫자로 시작할 수 없습니다.'; + if (label == '아이디') { + _usernameError = _isUsernameValid(_username) ? null : '아이디는 6~20자 영문 대소문자와 숫자 조합이어야 하며, 숫자로 시작할 수 없습니다.'; + } else if (label == '비밀번호') { + _passwordError = _isPasswordValidPattern(_password) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함된 조합이어야 합니다.'; + } else if (label == '비밀번호 확인') { + _confirmPasswordError = _password == _confirmPassword ? null : '비밀번호가 일치하지 않습니다.'; + } else if (label == '닉네임') { + _nicknameError = _isNicknameValid(_nickname) ? null : '* 닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.'; + } else if (label == '이메일') { + _emailError = _isEmailValid(_email) ? null : (_email.isNotEmpty ? '올바른 이메일 형식을 입력해주세요.' : null); } }); } + // 회원가입 요청 + Future _signUp() async { + final url = 'https://eldsoft.com:8097/user/signup'; + + // 체크박스 상태에 따라 mandatory_terms_yn 값 설정 + final mandatoryTermsYn = _isAgreed ? 'Y' : 'N'; + + // 비밀번호를 SHA-256으로 해싱 + final hashedPassword = _hashPassword(_password); + + final body = { + "user_id": _username, + "user_pw": hashedPassword, // 해싱된 비밀번호 사용 + "nickname": _nickname, + "user_email": _email, + "department": _department, + "introduce_myself": _introduceMyself, + "mandatory_terms_yn": mandatoryTermsYn // 체크박스 상태에 따라 값 설정 + }; + + try { + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: json.encode(body), + ); + + // 응답 body를 UTF-8로 디코딩하여 변수에 저장 + final resBody = json.decode(utf8.decode(response.bodyBytes)); + + if (response.statusCode == 200) { + // result가 OK이어야만 성공여부 판단 가능 + print(resBody); + if (resBody['result'] == 'OK') { + if (resBody['response_info']['msg_type'] == 'OK') { + _showDialog('회원가입 성공', '회원가입이 완료되었습니다.'); + } else { + _showDialog('회원가입 실패', '${resBody['response_info']['msg_content']}'); + } + } else { + _showDialog('회원가입 실패', '${resBody['response_info']['msg_content']}'); + } + } else { + final errorData = json.decode(response.body); + _showDialog('회원가입 실패', errorData['message'] ?? '회원가입 실패'); + } + } catch (error) { + _showDialog('네트워크 오류', '네트워크 오류: $error'); + } + } + + // 모밀번호 해싱 함수 + String _hashPassword(String password) { + final bytes = utf8.encode(password); // 비밀번호를 바이트로 변환 + final digest = sha256.convert(bytes); // SHA-256 해싱 + return digest.toString(); // 해싱된 비밀번호를 문자열로 반환 + } + + // 모달 창 표시 + void _showDialog(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, // 배경색 + title: Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, // 제목 색상 + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, // 내용 크기에 맞게 조정 + children: [ + Text( + message, + style: const TextStyle( + fontSize: 16, + color: Colors.black, // 내용 색상 + ), + ), + const SizedBox(height: 20), // 여백 추가 + TextButton( + onPressed: () { + Navigator.of(context).pop(); // 모달 닫기 + if (title == '회원가입 성공') { + Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginPage())); // 로그인 페이지로 이동 + } + // '회원가입 성공'이 아닐 경우 아무 동작도 하지 않음 + }, + style: TextButton.styleFrom( + foregroundColor: Colors.white, // 버튼 텍스트 색상 + backgroundColor: Colors.black, // 버튼 배경색 + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), // 버튼 패딩 + ), + child: const Text('확인'), + ), + ], + ), + ); + }, + ); + } + + // 입력 필드 위젯 + Widget _buildTextField(String label, Function(String) onChanged, {bool obscureText = false, String? errorText}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + onChanged: (value) { + onChanged(value); + _validateInput(label); // label에 따라 유효성 검사 수행 + }, + obscureText: obscureText, + decoration: InputDecoration( + labelText: label, + labelStyle: const TextStyle(color: Colors.black), + border: OutlineInputBorder(), + focusedBorder: OutlineInputBorder(borderSide: const BorderSide(color: Colors.black, width: 2.0)), + errorStyle: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + if (errorText != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(errorText, style: const TextStyle(color: Colors.red, fontSize: 12)), + ), + const SizedBox(height: 16), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -39,136 +186,36 @@ class _SignUpPageState extends State { backgroundColor: Colors.black, leading: IconButton( icon: const Icon(Icons.chevron_left, color: Colors.white), - onPressed: () { - Navigator.pop(context); // 로그인 페이지로 이동 - }, + onPressed: () => Navigator.pop(context), ), ), body: Padding( padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( // 스크롤 가능하게 설정 + child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - const Text( - '회원가입', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), + const Text('회원가입', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black)), const SizedBox(height: 32), - TextField( - onChanged: _validateUsername, // 아이디 입력 시 검증 - decoration: InputDecoration( - labelText: '아이디', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - ), - if (_usernameError != null) // 오류 메시지 표시 - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _usernameError!, - style: const TextStyle(color: Colors.red, fontSize: 12), // 폰트 크기 줄임 - ), - ), + _buildTextField('아이디', (value) => _username = value, errorText: _usernameError), + _buildTextField('비밀번호', (value) => _password = value, obscureText: true, errorText: _passwordError), + _buildTextField('비밀번호 확인', (value) => _confirmPassword = value, obscureText: true, errorText: _confirmPasswordError), + _buildTextField('닉네임', (value) => _nickname = value, errorText: _nicknameError), + _buildTextField('이메일', (value) => _email = value, errorText: _emailError), + _buildTextField('소속(선택사항)', (value) => _department = value), + _buildTextField('자기소개(선택사항)', (value) => _introduceMyself = value), const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '비밀번호', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - obscureText: true, - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '비밀번호 확인', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - obscureText: true, - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '닉네임', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '이메일', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '소속', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - ), - const SizedBox(height: 16), - TextField( - decoration: InputDecoration( - labelText: '자기소개', - labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), - ), - ), - maxLines: 3, // 여러 줄 입력 가능 - ), - const SizedBox(height: 16), - const Text( - '개인정보 수집 및 이용 동의서', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), + const Text('개인정보 수집 및 이용 동의서', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)), const SizedBox(height: 8), Container( - height: 200, // 스크롤 가능한 영역의 높이 설정 - decoration: BoxDecoration( - border: Border.all(color: Colors.black.withOpacity(0.5)), // 부드러운 테두리 - borderRadius: BorderRadius.circular(8), // 모서리 둥글게 - ), - child: Scrollbar( // 스크롤바 추가 - thickness: 5, // 스크롤바 두께 - radius: Radius.circular(5), // 스크롤바 둥글게 + height: 200, + decoration: BoxDecoration(border: Border.all(color: Colors.black.withOpacity(0.5)), borderRadius: BorderRadius.circular(8)), + child: Scrollbar( + thickness: 5, + radius: const Radius.circular(5), child: SingleChildScrollView( child: const Padding( - padding: EdgeInsets.all(15.0), // 여백 추가 + padding: EdgeInsets.all(15.0), child: Text( '올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, ' '「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. ' @@ -183,7 +230,7 @@ class _SignUpPageState extends State { '부정 이용 방지 및 비인가 사용 방지\n' '서비스 이용에 따른 문의 사항 처리\n' '서비스 제공\n' - '게임 방 생성 및 참여 등 기본 서비스 제공\n' + '게임 생성 및 참여 등 기본 서비스 제공\n' '통계 및 순위 제공 등 부가 서비스 제공\n' '고객 지원 및 공지사항 전달\n' '서비스 관련 중요한 공지사항 전달\n' @@ -237,10 +284,7 @@ class _SignUpPageState extends State { ), const SizedBox(height: 16), ElevatedButton( - onPressed: () { - // 회원가입 로직 추가 - Navigator.pop(context); // 로그인 페이지로 이동 - }, + onPressed: _signUp, // 회원가입 로직 추가 style: ElevatedButton.styleFrom( backgroundColor: Colors.black, foregroundColor: Colors.white, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 538c560..5bd61fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + crypto: + dependency: "direct dev" + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -57,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -75,6 +99,27 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: "direct dev" + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + url: "https://pub.dev" + source: hosted + version: "4.1.1" leak_tracker: dependency: transitive description: @@ -139,6 +184,102 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + shared_preferences: + dependency: "direct dev" + description: + name: shared_preferences + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -192,6 +333,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: @@ -208,6 +357,22 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index a60365f..73f06c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + http: ^1.2.2 + crypto: ^3.0.1 + shared_preferences: ^2.0.6 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is