Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 150 additions & 96 deletions lib/auth/presentation/pages/identity/identity_verification_page.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:code_l/auth/presentation/pages/identity/providers.dart';
import 'package:code_l/auth/presentation/pages/identity/widgets/identity_verification_app_bar.dart';
import 'package:code_l/auth/presentation/widgets/auth_confirm_button.dart';
Expand All @@ -18,22 +20,7 @@ class PhoneVerificationPage extends ConsumerWidget {

return Scaffold(
appBar: IdentityVerificationAppBar(),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(20.0),
child: AuthConfirmButton(
enabled: state.codeSent,
onPressed: () {
if (viewModel.formKey.currentState!.validate()) {
viewModel.verifyCode(() {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
});
}
},
),
),
bottomNavigationBar: _buildBottomButton(context, viewModel, state),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
Expand All @@ -45,88 +32,12 @@ class PhoneVerificationPage extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: AppGaps.gap40),
Text("휴대전화 번호\n인증이 필요합니다", style: AppTypography.header1),
Text(
"원활한 서비스 이용을 위해 번호인증을 해주세요",
style: AppTypography.body2.copyWith(
color: AppColors.grey600,
),
),
_buildHeader(),
SizedBox(height: AppGaps.gap40),
SizedBox(height: AppGaps.gap40),
TextFormField(
controller: viewModel.phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
hintText: '휴대전화 번호',
suffixIcon: ElevatedButton(
onPressed: () {
if (viewModel.formKey.currentState!.validate()) {
viewModel.sendSmsCode();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
textStyle: AppTypography.subtitle2.copyWith(
color: AppColors.white,
),
shape: RoundedRectangleBorder(),
padding: const EdgeInsets.symmetric(
horizontal: AppGaps.gap12,
vertical: AppGaps.gap12,
),
),
child: const Text("인증 요청"),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '휴대전화 번호를 입력하세요.';
}
if (!RegExp(
r'^01[0-9]-?\d{3,4}-?\d{4}$',
).hasMatch(value)) {
return '유효한 휴대전화 번호를 입력하세요.';
}
return null;
},
),

_buildPhoneInput(viewModel, state),
const SizedBox(height: 15),

if (state.codeSent) ...[
const SizedBox(height: 20),
// 인증 코드 입력 필드
TextFormField(
controller: viewModel.smsCodeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
hintText: '인증번호를 입력하세요.',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '인증번호 입력';
}
if (!RegExp(r'^\d{6}$').hasMatch(value)) {
return '6자리 인증번호를 입력하세요.';
}
return null;
},
),
],
if (state.codeSent) _buildSmsCodeInput(viewModel, state),
],
),
),
Expand All @@ -135,4 +46,147 @@ class PhoneVerificationPage extends ConsumerWidget {
),
);
}
}

Widget _buildBottomButton(BuildContext context, dynamic viewModel, dynamic state) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: AuthConfirmButton(
enabled: state.codeSent,
onPressed: () {
if (viewModel.formKey.currentState!.validate()) {
viewModel.verifyCode(() {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
});
}
},
),
);
}

Widget _buildHeader() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("휴대전화 번호\n인증이 필요합니다", style: AppTypography.header1),
Text(
"원활한 서비스 이용을 위해 번호인증을 해주세요",
style: AppTypography.body2.copyWith(
color: AppColors.grey600,
),
),
],
);
}

Widget _buildPhoneInput(dynamic viewModel, dynamic state) {
return TextFormField(
controller: viewModel.phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
hintText: '휴대전화 번호',
suffixIcon: _buildSmsRequestButton(viewModel),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '휴대전화 번호를 입력하세요.';
}
if (!RegExp(
r'^01[0-9]-?\d{3,4}-?\d{4}$',
).hasMatch(value)) {
return '유효한 휴대전화 번호를 입력하세요.';
}
return null;
},
);
}

Widget _buildSmsRequestButton(dynamic viewModel) {
return ElevatedButton(
onPressed: () {
viewModel.sendSmsCode();
viewModel.startTimer();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: AppColors.white,
textStyle: AppTypography.subtitle2.copyWith(
color: AppColors.white,
),
shape: RoundedRectangleBorder(),
padding: const EdgeInsets.symmetric(
horizontal: AppGaps.gap12,
vertical: AppGaps.gap12,
),
),
child: const Text("인증 요청"),
);
}

Widget _buildSmsCodeInput(dynamic viewModel, dynamic state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: viewModel.smsCodeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: AppColors.grey400,
width: 0.6,
),
),
hintText: '인증번호를 입력하세요.',
),
validator: (value) {
if (value == null || value.isEmpty) {
return '인증번호 입력';
}
if (!RegExp(r'^\d{6}$').hasMatch(value)) {
return '6자리 인증번호를 입력하세요.';
}
if (!state.isSmsValid) {
return '인증번호가 일치하지 않습니다.';
}
return null;
},
),
SizedBox(height: AppGaps.gap4),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
state.codeExpired
? '인증 시간이 만료되었습니다. 다시 시도해주세요.'
: state.elapsedTime,
style: AppTypography.caption3.copyWith(
color: state.codeExpired ? AppColors.error : AppColors.grey900,
),
),
],
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:code_l/auth/presentation/pages/identity/providers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand All @@ -12,6 +14,49 @@ class PhoneVerificationViewModel extends Notifier<PhoneVerificationState> {
final formKey = GlobalKey<FormState>();
final phoneController = TextEditingController();
final smsCodeController = TextEditingController();
Timer? timer;
int remainingSeconds = 120;


String formatTime(int seconds) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 viewModel에서 처리하기보다, 도메인 로직이다보니 usecase를 사용하는 구조로 변경하면 좋을 것 같아요 !

final minutes = (seconds ~/ 60).toString().padLeft(2, '0');
final remainingSeconds = (seconds % 60).toString().padLeft(2, '0');
return '$minutes:$remainingSeconds';
}

// 타이머 시작
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수가 길어지면 주석 남기는것도 좋습니다~~ 👍

void startTimer() {
resetTimer(); // 초기화
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
if (remainingSeconds > 0) {
remainingSeconds--; // 1초 감소
state = state.copyWith(elapsedTime: formatTime(remainingSeconds));
} else {
stopTimer(); // 시간이 끝나면 타이머 중지
state = state.copyWith(codeExpired: true); // 만료 상태로 업데이트
}
});
}

// 타이머 중지
void stopTimer() {
timer?.cancel();
}

// 타이머 초기화
void resetTimer() {
remainingSeconds = 120; // 초기값으로 재설정
timer?.cancel();
state = state.copyWith(
elapsedTime: formatTime(remainingSeconds),
codeExpired: false,
);
}

@override
void dispose() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 어디에 들어가면 좋을까요?~

timer?.cancel();
}

@override
PhoneVerificationState build() {
Expand All @@ -20,6 +65,7 @@ class PhoneVerificationViewModel extends Notifier<PhoneVerificationState> {
}

Future<void> sendSmsCode() async {
state = state.copyWith(codeExpired: false); // 인증 만료 상태 초기화
if (formKey.currentState!.validate()) {
final number = PhoneNumber.fromLocal(phoneController.text);

Expand All @@ -36,6 +82,7 @@ class PhoneVerificationViewModel extends Notifier<PhoneVerificationState> {
verificationId: verificationId,
codeSent: true,
);
startTimer(); // 타이머 시작
},
);

Expand All @@ -53,8 +100,9 @@ class PhoneVerificationViewModel extends Notifier<PhoneVerificationState> {
Fluttertoast.showToast(msg: '인증이 완료되었습니다');
await _verifyPhoneNumberUseCase.saveUser();
onSuccess();
state = state.copyWith(isSmsValid: true);
} catch (e) {
Fluttertoast.showToast(msg: "인증번호가 올바르지 않습니다");
state = state.copyWith(isSmsValid: false);
}
}
}
12 changes: 12 additions & 0 deletions lib/auth/presentation/pages/identity/phone_verification_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@
class PhoneVerificationState {
final bool codeSent;
final String verificationId;
final bool isSmsValid;
final bool codeExpired;
final String elapsedTime;

PhoneVerificationState({
this.codeSent = false,
this.verificationId = '',
this.isSmsValid = true,
this.codeExpired = false,
this.elapsedTime = '02:00',
});

PhoneVerificationState copyWith({
bool? codeSent,
String? verificationId,
bool? isSmsValid,
bool? codeExpired,
String? elapsedTime,
}) {
return PhoneVerificationState(
codeSent: codeSent ?? this.codeSent,
verificationId: verificationId ?? this.verificationId,
isSmsValid: isSmsValid ?? this.isSmsValid,
codeExpired: codeExpired ?? this.codeExpired,
elapsedTime: elapsedTime ?? this.elapsedTime,
);
}
}