import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:go_router/go_router.dart'; import '../../../core/services/auth_proxy_service.dart'; class SignupScreen extends StatefulWidget { const SignupScreen({super.key}); @override State createState() => _SignupScreenState(); } class _SignupScreenState extends State { final _formKey = GlobalKey(); int _currentStep = 1; // 1: 인증, 2: 정보, 3: 비밀번호 // Controllers final _emailController = TextEditingController(); final _emailCodeController = TextEditingController(); final _phoneController = TextEditingController(); final _phoneCodeController = TextEditingController(); final _nameController = TextEditingController(); final _passwordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); final _deptController = TextEditingController(); // State bool _isEmailVerified = false; bool _isPhoneVerified = false; String _affiliationType = 'GENERAL'; String? _companyCode; bool _termsAccepted = false; bool _privacyAccepted = false; bool _isLoading = false; // Inline Errors String? _emailError; String? _phoneError; String? _passwordError; String? _confirmPasswordError; // Timers Timer? _emailTimer; int _emailSeconds = 0; Timer? _phoneTimer; int _phoneSeconds = 0; @override void dispose() { _emailTimer?.cancel(); _phoneTimer?.cancel(); _emailController.dispose(); _emailCodeController.dispose(); _phoneController.dispose(); _phoneCodeController.dispose(); _nameController.dispose(); _passwordController.dispose(); _confirmPasswordController.dispose(); _deptController.dispose(); super.dispose(); } // --- Logic Methods --- void _startTimer(String type) { if (type == 'email') { _emailSeconds = 300; _emailTimer?.cancel(); _emailTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) return; setState(() { if (_emailSeconds > 0) _emailSeconds--; else timer.cancel(); }); }); } else { _phoneSeconds = 180; _phoneTimer?.cancel(); _phoneTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) return; setState(() { if (_phoneSeconds > 0) _phoneSeconds--; else timer.cancel(); }); }); } } String _formatTime(int seconds) { final m = seconds ~/ 60; final s = seconds % 60; return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; } Future _sendEmailCode() async { final email = _emailController.text.trim(); final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(email)) { setState(() => _emailError = '유효한 이메일 형식이 아닙니다.'); return; } setState(() { _isLoading = true; _emailError = null; }); try { final available = await AuthProxyService.checkEmailAvailability(email); if (!available) { setState(() => _emailError = '이미 가입된 이메일입니다.'); return; } await AuthProxyService.sendSignupCode(email, 'email'); _startTimer('email'); } catch (e) { setState(() => _emailError = '발송 실패: $e'); } finally { setState(() => _isLoading = false); } } Future _verifyEmailCode() async { final code = _emailCodeController.text.trim(); if (code.length != 6) return; try { final success = await AuthProxyService.verifySignupCode(_emailController.text.trim(), 'email', code); if (success) { setState(() { _isEmailVerified = true; _emailTimer?.cancel(); _emailSeconds = 0; _emailError = null; }); } else { setState(() => _emailError = '인증코드가 일치하지 않습니다.'); } } catch (e) { setState(() => _emailError = '인증 실패: $e'); } } Future _sendPhoneCode() async { final phone = _phoneController.text.trim(); if (phone.isEmpty) return; setState(() { _isLoading = true; _phoneError = null; }); try { await AuthProxyService.sendSignupCode(phone, 'phone'); _startTimer('phone'); } catch (e) { setState(() => _phoneError = '발송 실패: $e'); } finally { setState(() => _isLoading = false); } } Future _verifyPhoneCode() async { final code = _phoneCodeController.text.trim(); if (code.length != 6) return; try { final success = await AuthProxyService.verifySignupCode(_phoneController.text.trim(), 'phone', code); if (success) { setState(() { _isPhoneVerified = true; _phoneTimer?.cancel(); _phoneSeconds = 0; _phoneError = null; }); } else { setState(() => _phoneError = '인증코드가 일치하지 않습니다.'); } } catch (e) { setState(() => _phoneError = '인증 실패: $e'); } } Future _handleSignup() async { if (_passwordController.text != _confirmPasswordController.text) { setState(() => _confirmPasswordError = '비밀번호가 일치하지 않습니다.'); return; } if (!_formKey.currentState!.validate()) return; if (!_termsAccepted || !_privacyAccepted) return; setState(() { _isLoading = true; _passwordError = null; }); try { await AuthProxyService.signup( email: _emailController.text.trim(), password: _passwordController.text, name: _nameController.text.trim(), phone: _phoneController.text.trim(), affiliationType: _affiliationType, companyCode: _affiliationType == 'AFFILIATE' ? _companyCode : null, department: _deptController.text.trim().isEmpty ? (_affiliationType == 'GENERAL' ? 'External' : '') : _deptController.text.trim(), termsAccepted: true, ); if (mounted) _showSuccessDialog(); } catch (e) { String eStr = e.toString().toLowerCase(); setState(() { if (eStr.contains('uppercase')) _passwordError = '대문자가 최소 1개 이상 포함되어야 합니다.'; else if (eStr.contains('lowercase')) _passwordError = '소문자가 최소 1개 이상 포함되어야 합니다.'; else if (eStr.contains('digit') || eStr.contains('number')) _passwordError = '숫자가 최소 1개 이상 포함되어야 합니다.'; else if (eStr.contains('symbol') || eStr.contains('special')) _passwordError = '특수문자가 최소 1개 이상 포함되어야 합니다.'; else if (eStr.contains('length') || eStr.contains('12 characters')) _passwordError = '비밀번호는 최소 12자 이상이어야 합니다.'; else _passwordError = '가입 실패: $e'; }); } finally { setState(() => _isLoading = false); } } void _showSuccessDialog() { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('회원가입 완료'), content: const Text('성공적으로 가입되었습니다.'), actions: [TextButton(onPressed: () => context.go('/login'), child: const Text('로그인하기'))], ), ); } // --- UI Components --- Widget _buildStepIndicator() { return Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( children: [ _stepCircle(1, '본인인증'), _stepLine(1), _stepCircle(2, '정보입력'), _stepLine(2), _stepCircle(3, '비밀번호'), ], ), ); } Widget _stepCircle(int step, String label) { bool isDone = _currentStep > step; bool isCurrent = _currentStep == step; return Column( children: [ CircleAvatar( radius: 15, backgroundColor: isDone ? Colors.green : (isCurrent ? Colors.black : Colors.grey[300]), child: isDone ? const Icon(Icons.check, size: 16, color: Colors.white) : Text('$step', style: TextStyle(color: isCurrent ? Colors.white : Colors.black54, fontSize: 12)), ), const SizedBox(height: 4), Text(label, style: TextStyle(fontSize: 10, color: isCurrent ? Colors.black : Colors.grey, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal)), ], ); } Widget _stepLine(int afterStep) { return Expanded( child: Container( margin: const EdgeInsets.only(bottom: 16, left: 4, right: 4), height: 2, color: _currentStep > afterStep ? Colors.green : Colors.grey[300], ), ); } Widget _buildStep1() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('이메일 인증', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), Row( children: [ Expanded( child: TextFormField( controller: _emailController, decoration: InputDecoration(labelText: '이메일 주소', border: const OutlineInputBorder(), errorText: _emailError), readOnly: _isEmailVerified, ), ), const SizedBox(width: 8), SizedBox( height: 55, child: ElevatedButton( onPressed: (_isEmailVerified || _isLoading) ? null : _sendEmailCode, child: Text(_emailSeconds > 0 ? '재발송' : '인증요청'), ), ), ], ), if (_emailSeconds > 0 && !_isEmailVerified) ...[ const SizedBox(height: 8), TextFormField( controller: _emailCodeController, decoration: InputDecoration( labelText: '이메일 인증코드 6자리', suffixText: _formatTime(_emailSeconds), border: const OutlineInputBorder(), ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)], onChanged: (val) { if(val.length == 6) _verifyEmailCode(); }, ), ], if (_isEmailVerified) const Padding( padding: EdgeInsets.only(top: 8), child: Text('✅ 이메일 인증이 완료되었습니다.', style: TextStyle(color: Colors.green, fontSize: 13)), ), const SizedBox(height: 24), Text('휴대폰 인증', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), Row( children: [ Expanded( child: TextFormField( controller: _phoneController, decoration: InputDecoration(labelText: '휴대폰 번호 (-없이)', border: const OutlineInputBorder(), errorText: _phoneError), readOnly: _isPhoneVerified, keyboardType: TextInputType.phone, ), ), const SizedBox(width: 8), SizedBox( height: 55, child: ElevatedButton( onPressed: (_isPhoneVerified || _isLoading) ? null : _sendPhoneCode, child: Text(_phoneSeconds > 0 ? '재발송' : '인증요청'), ), ), ], ), if (_phoneSeconds > 0 && !_isPhoneVerified) ...[ const SizedBox(height: 8), TextFormField( controller: _phoneCodeController, decoration: InputDecoration( labelText: '휴대폰 인증코드 6자리', suffixText: _formatTime(_phoneSeconds), border: const OutlineInputBorder(), ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)], onChanged: (val) { if(val.length == 6) _verifyPhoneCode(); }, ), ], if (_isPhoneVerified) const Padding( padding: EdgeInsets.only(top: 8), child: Text('✅ 휴대폰 인증이 완료되었습니다.', style: TextStyle(color: Colors.green, fontSize: 13)), ), ], ); } Widget _buildStep2() { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('사용자 정보를 입력해주세요', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 16), TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: '이름', border: OutlineInputBorder()), ), const SizedBox(height: 16), DropdownButtonFormField( value: _affiliationType, decoration: const InputDecoration(labelText: '소속 유형', border: OutlineInputBorder()), items: const [ DropdownMenuItem(value: 'GENERAL', child: Text('일반 사용자')), DropdownMenuItem(value: 'AFFILIATE', child: Text('가족사 임직원')), ], onChanged: (val) => setState(() { _affiliationType = val!; }), ), const SizedBox(height: 16), if (_affiliationType == 'AFFILIATE') ...[ DropdownButtonFormField( value: _companyCode, decoration: const InputDecoration(labelText: '가족사 선택', border: OutlineInputBorder()), items: const [ DropdownMenuItem(value: 'HANMAC', child: Text('한맥')), DropdownMenuItem(value: 'SAMAN', child: Text('삼안')), DropdownMenuItem(value: 'PTC', child: Text('PTC')), DropdownMenuItem(value: 'JANGHEON', child: Text('장헌')), DropdownMenuItem(value: 'BARON', child: Text('바론')), DropdownMenuItem(value: 'HALLA', child: Text('한라')), ], onChanged: (val) => setState(() => _companyCode = val), ), const SizedBox(height: 16), ], TextFormField( controller: _deptController, decoration: InputDecoration( labelText: _affiliationType == 'AFFILIATE' ? '부서명' : '소속 정보 (선택)', border: const OutlineInputBorder() ), ), ], ); } Widget _buildStep3() { // 실시간 비밀번호 체크 로직 String p = _passwordController.text; bool hasUpper = p.contains(RegExp(r'[A-Z]')); bool hasLower = p.contains(RegExp(r'[a-z]')); bool hasDigit = p.contains(RegExp(r'[0-9]')); bool hasSpecial = p.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); bool hasLength = p.length >= 12; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('보안 설정', style: GoogleFonts.outfit(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: true, onChanged: (_) => setState(() {}), decoration: InputDecoration( labelText: '비밀번호', border: const OutlineInputBorder(), errorText: _passwordError, ), ), const SizedBox(height: 8), // 실시간 체크 표시 UI Wrap( spacing: 10, children: [ _cryptoCheck('12자 이상', hasLength), _cryptoCheck('대문자', hasUpper), _cryptoCheck('소문자', hasLower), _cryptoCheck('숫자', hasDigit), _cryptoCheck('특수문자', hasSpecial), ], ), const SizedBox(height: 16), TextFormField( controller: _confirmPasswordController, obscureText: true, onChanged: (val) { setState(() { if (val != _passwordController.text) { _confirmPasswordError = '비밀번호가 일치하지 않습니다.'; } else { _confirmPasswordError = null; } }); }, decoration: InputDecoration( labelText: '비밀번호 확인', border: const OutlineInputBorder(), errorText: _confirmPasswordError, ), ), const SizedBox(height: 24), CheckboxListTile( title: const Text('이용약관 및 개인정보 처리방침 동의', style: TextStyle(fontSize: 13)), value: _termsAccepted && _privacyAccepted, onChanged: (val) => setState(() { _termsAccepted = val!; _privacyAccepted = val; }), controlAffinity: ListTileControlAffinity.leading, contentPadding: EdgeInsets.zero, ), ], ); } Widget _cryptoCheck(String label, bool isValid) { return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(isValid ? Icons.check_circle : Icons.circle_outlined, size: 14, color: isValid ? Colors.green : Colors.grey), const SizedBox(width: 4), Text(label, style: TextStyle(fontSize: 11, color: isValid ? Colors.green : Colors.grey)), ], ); } @override Widget build(BuildContext context) { bool canGoNext = false; if (_currentStep == 1 && _isEmailVerified && _isPhoneVerified) canGoNext = true; if (_currentStep == 2 && _nameController.text.isNotEmpty && (_affiliationType == 'GENERAL' || _companyCode != null)) canGoNext = true; return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text('회원가입', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)), elevation: 0, backgroundColor: Colors.white, foregroundColor: Colors.black, ), body: SafeArea( child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: _buildStepIndicator(), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: _currentStep == 1 ? _buildStep1() : (_currentStep == 2 ? _buildStep2() : _buildStep3()), ), ), ), Padding( padding: const EdgeInsets.all(24), child: Row( children: [ if (_currentStep > 1) ...[ Expanded( child: OutlinedButton( onPressed: () => setState(() => _currentStep--), style: OutlinedButton.styleFrom(minimumSize: const Size.fromHeight(55)), child: const Text('이전'), ), ), const SizedBox(width: 12), ], Expanded( child: FilledButton( onPressed: _currentStep < 3 ? (canGoNext ? () => setState(() => _currentStep++) : null) : (_isLoading || !_termsAccepted ? null : _handleSignup), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(55), backgroundColor: Colors.black, ), child: _isLoading ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text(_currentStep < 3 ? '다음 단계' : '가입 완료'), ), ), ], ), ), ], ), ), ); } }