2024-12-14 16:44:36 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2024-12-16 15:55:48 +00:00
|
|
|
import 'package:http/http.dart' as http; // http 패키지 임포트
|
|
|
|
import 'dart:convert'; // JSON 변환을 위해 임포트
|
2024-12-14 16:44:36 +00:00
|
|
|
import 'login_page.dart'; // 로그인 페이지 임포트 추가
|
2024-12-16 15:55:48 +00:00
|
|
|
import 'dart:convert' show utf8; // UTF-8 디코딩을 위해 임포트
|
|
|
|
import 'package:crypto/crypto.dart'; // crypto 패키지 임포트
|
2024-12-14 16:44:36 +00:00
|
|
|
|
|
|
|
class SignUpPage extends StatefulWidget {
|
|
|
|
const SignUpPage({Key? key}) : super(key: key);
|
|
|
|
|
|
|
|
@override
|
|
|
|
_SignUpPageState createState() => _SignUpPageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SignUpPageState extends State<SignUpPage> {
|
2024-12-16 15:55:48 +00:00
|
|
|
// 상태 변수
|
2024-12-14 16:44:36 +00:00
|
|
|
bool _isAgreed = false; // 개인정보 수집 동의 체크박스 상태
|
2024-12-16 15:55:48 +00:00
|
|
|
String _username = '', _password = '', _confirmPassword = '', _nickname = '', _email = '';
|
|
|
|
String _department = '', _introduceMyself = ''; // 소속 및 자기소개
|
|
|
|
String? _usernameError, _passwordError, _confirmPasswordError, _nicknameError, _emailError;
|
2024-12-14 16:44:36 +00:00
|
|
|
|
2024-12-16 15:55:48 +00:00
|
|
|
// 유효성 검사
|
|
|
|
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);
|
2024-12-14 16:44:36 +00:00
|
|
|
|
2024-12-16 15:55:48 +00:00
|
|
|
// 입력값 검증
|
|
|
|
void _validateInput(String label) {
|
2024-12-14 16:44:36 +00:00
|
|
|
setState(() {
|
2024-12-16 15:55:48 +00:00
|
|
|
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);
|
2024-12-14 16:44:36 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-12-16 15:55:48 +00:00
|
|
|
// 회원가입 요청
|
|
|
|
Future<void> _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),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-12-14 16:44:36 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
backgroundColor: Colors.white,
|
|
|
|
appBar: AppBar(
|
|
|
|
title: const Text('ALL SCORE', style: TextStyle(color: Colors.white)),
|
|
|
|
backgroundColor: Colors.black,
|
|
|
|
leading: IconButton(
|
|
|
|
icon: const Icon(Icons.chevron_left, color: Colors.white),
|
2024-12-16 15:55:48 +00:00
|
|
|
onPressed: () => Navigator.pop(context),
|
2024-12-14 16:44:36 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
body: Padding(
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
2024-12-16 15:55:48 +00:00
|
|
|
child: SingleChildScrollView(
|
2024-12-14 16:44:36 +00:00
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
children: [
|
2024-12-16 15:55:48 +00:00
|
|
|
const Text('회원가입', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black)),
|
2024-12-14 16:44:36 +00:00
|
|
|
const SizedBox(height: 32),
|
2024-12-16 15:55:48 +00:00
|
|
|
_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),
|
2024-12-14 16:44:36 +00:00
|
|
|
const SizedBox(height: 16),
|
2024-12-16 15:55:48 +00:00
|
|
|
const Text('개인정보 수집 및 이용 동의서', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
|
2024-12-14 16:44:36 +00:00
|
|
|
const SizedBox(height: 8),
|
|
|
|
Container(
|
2024-12-16 15:55:48 +00:00
|
|
|
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),
|
2024-12-14 16:44:36 +00:00
|
|
|
child: SingleChildScrollView(
|
|
|
|
child: const Padding(
|
2024-12-16 15:55:48 +00:00
|
|
|
padding: EdgeInsets.all(15.0),
|
2024-12-14 16:44:36 +00:00
|
|
|
child: Text(
|
|
|
|
'올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, '
|
|
|
|
'「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. '
|
|
|
|
'회사는 개인정보 수집 및 이용에 관한 사항을 아래와 같이 안내드리오니, '
|
|
|
|
'내용을 충분히 숙지하신 후 동의하여 주시기 바랍니다.\n\n'
|
|
|
|
'1. 수집하는 개인정보 항목\n'
|
|
|
|
'필수 항목: 아이디(ID), 비밀번호(PW), 닉네임(실명 아님), 이메일 주소\n'
|
|
|
|
'선택 항목: 소속, 자기소개\n\n'
|
|
|
|
'2. 개인정보의 수집 및 이용 목적\n'
|
|
|
|
'회원 관리\n'
|
|
|
|
'회원 식별 및 인증\n'
|
|
|
|
'부정 이용 방지 및 비인가 사용 방지\n'
|
|
|
|
'서비스 이용에 따른 문의 사항 처리\n'
|
|
|
|
'서비스 제공\n'
|
2024-12-16 15:55:48 +00:00
|
|
|
'게임 생성 및 참여 등 기본 서비스 제공\n'
|
2024-12-14 16:44:36 +00:00
|
|
|
'통계 및 순위 제공 등 부가 서비스 제공\n'
|
|
|
|
'고객 지원 및 공지사항 전달\n'
|
|
|
|
'서비스 관련 중요한 공지사항 전달\n'
|
|
|
|
'이용자 문의 및 불만 처리\n\n'
|
|
|
|
'3. 개인정보의 보유 및 이용 기간\n'
|
|
|
|
'회원 탈퇴 시: 수집된 모든 개인정보는 회원 탈퇴 즉시 파기합니다.\n'
|
|
|
|
'관련 법령에 따른 보관: 전자상거래 등에서의 소비자 보호에 관한 법률 등 관계 법령의 규정에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 보관합니다.\n'
|
|
|
|
'계약 또는 청약 철회 등에 관한 기록: 5년 보관\n'
|
|
|
|
'대금 결제 및 재화 등의 공급에 관한 기록: 5년 보관\n'
|
|
|
|
'소비자의 불만 또는 분쟁 처리에 관한 기록: 3년 보관\n\n'
|
|
|
|
'4. 개인정보의 파기 절차 및 방법\n'
|
|
|
|
'파기 절차\n'
|
|
|
|
'회원 탈퇴 요청 또는 개인정보 수집 및 이용 목적이 달성된 후 지체 없이 해당 정보를 파기합니다.\n'
|
|
|
|
'파기 방법\n'
|
|
|
|
'전자적 파일 형태: 복구 및 재생이 불가능한 방법으로 영구 삭제\n'
|
|
|
|
'종이 문서 형태: 분쇄하거나 소각\n\n'
|
|
|
|
'5. 이용자의 권리 및 행사 방법\n'
|
|
|
|
'이용자는 언제든지 자신의 개인정보에 대해 열람, 수정, 삭제, 처리 정지를 요구할 수 있습니다.\n'
|
|
|
|
'회원 탈퇴를 원하시는 경우, 서비스 내의 "회원 탈퇴" 기능을 이용하시거나 고객센터를 통해 요청하실 수 있습니다.\n\n'
|
|
|
|
'6. 동의를 거부할 권리 및 거부 시 불이익\n'
|
|
|
|
'이용자는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다.\n'
|
|
|
|
'그러나 필수 항목에 대한 동의를 거부하실 경우 서비스 이용이 제한될 수 있습니다.\n\n'
|
|
|
|
'7. 개인정보 보호책임자\n'
|
|
|
|
'이름: 여정훈\n'
|
|
|
|
'연락처: eld_yeojh@naver.com\n\n'
|
|
|
|
'8. 개인정보의 안전성 확보 조치\n'
|
|
|
|
'회사는 개인정보의 안전한 처리를 위하여 기술적, 관리적 보호조치를 시행하고 있습니다.\n'
|
|
|
|
'개인정보의 암호화\n'
|
|
|
|
'해킹 등에 대비한 대책\n'
|
|
|
|
'접근 통제 장치의 설치 및 운영',
|
|
|
|
textAlign: TextAlign.left,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 동의 체크박스
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Checkbox(
|
|
|
|
value: _isAgreed,
|
|
|
|
onChanged: (value) {
|
|
|
|
setState(() {
|
|
|
|
_isAgreed = value ?? false; // 체크박스 상태 업데이트
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
const Text('개인정보 수집 및 이용에 동의합니다.'),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
ElevatedButton(
|
2024-12-16 15:55:48 +00:00
|
|
|
onPressed: _signUp, // 회원가입 로직 추가
|
2024-12-14 16:44:36 +00:00
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
backgroundColor: Colors.black,
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
),
|
|
|
|
child: const Text('ALL SCORE 회원가입'),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|