본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓7(PageView - auth page)

이번에는 PageView 의 내부에 auth page 를 추가해보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

 

새로운 페이지 이름이 기존 페이지인 auth_screen 과 비슷하여 이름을 수정하였습니다.

 

auth_screen.dart => start_screen.dart

AuthScreen() => StartScreen()

 

 

 

화면 구성은 아래와 같습니다. 자세한 기능은 모델링도 필요하여 차후에 구현하겠습니다.

 

 

 

새로운 패키지 추가 - 입력값을 쉽게 검증하기 위함

 

  flutter_multi_formatter: ^2.5.8

 

 

start_screen.dart - 새로운 페이지 추가

 

class StartScreen extends StatelessWidget {
  StartScreen({Key? key}) : super(key: key);

  final PageController _pageController = PageController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PageView(
        controller: _pageController,
        // 이부분이 활성화 되면 사용자가 화면을 좌/우로 스크롤하지 못하게 설정 가능
        // physics: const NeverScrollableScrollPhysics(),
        children: <Widget>[
          IntroPage(pageController: _pageController),
          const AddressPage(),
          const AuthPage(),
        ],
      ),
    );
  }
}

 

 

auth_page.dart 

 

import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_multi_formatter/flutter_multi_formatter.dart';

import '../../constants/common_size.dart';
import '../../utils/logger.dart';

class AuthPage extends StatefulWidget {
  const AuthPage({Key? key}) : super(key: key);

  @override
  State<AuthPage> createState() => _AuthPageState();
}

const _duration_300 = Duration(microseconds: 300);
const _duration_1000 = Duration(seconds: 1);

class _AuthPageState extends State<AuthPage> {
  final inputBorder = const OutlineInputBorder(
    borderSide: BorderSide(color: Colors.grey),
  );

  // 초기값을 010 으로 시작하게 설정
  final TextEditingController _phoneNumberController =
      TextEditingController(text: "010");

  final TextEditingController _codeController = TextEditingController();

  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  // 인증단계 flag
  VerificationStatus _verificationStatus = VerificationStatus.none;

  @override
  Widget build(BuildContext context) {
    debugPrint("AuthPage >> build");

    return LayoutBuilder(
      builder: (context, constraints) {
        Size size = MediaQuery.of(context).size;

        // 승인증에는 모든 클릭은 무시하게 처리
        return IgnorePointer(
          // 검증 단계에서는 화면의 모든 터치를 무시한다
          ignoring: _verificationStatus == VerificationStatus.verifying,
          child: Form(
            key: _formKey,
            child: Scaffold(
              appBar: AppBar(
                title: Text(
                  '전화번호 로그인',
                  style: Theme.of(context).appBarTheme.titleTextStyle,
                ),
              ),
              body: Padding(
                padding: const EdgeInsets.all(padding_16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    Row(
                      crossAxisAlignment: CrossAxisAlignment.center,
                      children: <Widget>[
                        ExtendedImage.asset(
                          'assets/imgs/padlock.png',
                          width: size.width * 0.15,
                          height: size.width * 0.15,
                        ),
                        const SizedBox(
                          width: padding_08,
                        ),
                        const Text(
                            '사과마켓은 휴대폰 번호로 가입해요 \n번호는 안전하게 보관되며 \n어디에도 공개되지 않아요.'),
                      ],
                    ),
                    const SizedBox(
                      height: padding_16,
                    ),
                    TextFormField(
                      controller: _phoneNumberController,
                      // 숫자자판이 나오게 설정
                      keyboardType: TextInputType.phone,
                      // 입력된 값의 형식을 지정, 지정된 형식 이상으로 입력되지 않음
                      inputFormatters: [MaskedInputFormatter('000 0000 0000')],
                      decoration: InputDecoration(
                        hintText: '전화번호 입력',
                        hintStyle:
                            TextStyle(color: Theme.of(context).hintColor),
                        focusedBorder: inputBorder,
                        border: inputBorder,
                      ),
                      validator: (phoneNumber) {
                        // 전화번호 형식 검증
                        if (phoneNumber != null && phoneNumber.length == 13) {
                          return null;
                        } else {
                          return '전화번호 입력 오류입니다';
                        }
                      },
                    ),
                    const SizedBox(
                      height: padding_16,
                    ),
                    TextButton(
                        onPressed: () async {
                          debugPrint('_verificationStatus: $_verificationStatus');
                          FocusScope.of(context).unfocus();
                          if (_verificationStatus ==
                              VerificationStatus.codeSending) {
                            return;
                          }
                          if (_formKey.currentState != null) {
                            bool passed = _formKey.currentState!.validate();
                            if (passed) {
                              var phoneNum = _phoneNumberController.text;
                              phoneNum = phoneNum.replaceAll(' ', '');
                              phoneNum = phoneNum.replaceFirst('0', '');
                              phoneNum = '+82$phoneNum';

                              setState(() {
                                // 인증단계 코드 전송중~~
                                _verificationStatus =
                                    VerificationStatus.codeSending;
                              });

                              // 임시 시간 딜레이 코드
                              await Future.delayed(const Duration(seconds: 3));
                              setState(() {
                                // 인증단계 코드 전송완료~~
                                _verificationStatus =
                                    VerificationStatus.codeSent;
                              });
                            } else {
                              setState(() {
                                _verificationStatus = VerificationStatus.none;
                              });
                            }
                          }
                          debugPrint('_verificationStatus: $_verificationStatus');

                        },
                        // 검증상태에 따라서 버튼의 문자열을 변경하여 동작중임을 표시함
                        child: _verificationStatus ==
                                VerificationStatus.codeSending
                            ? const SizedBox(
                                height: 26,
                                width: 26,
                                child: CircularProgressIndicator(
                                  color: Colors.white,
                                ),
                              )
                            : const Text('인증문자 발송')),
                    const SizedBox(height: padding_16 * 2),
                    AnimatedOpacity(
                      // 투명도를 설정하는 위젯
                      duration: _duration_300,
                      opacity: (_verificationStatus == VerificationStatus.none)
                          ? 0.0
                          : 1.0,
                      child: AnimatedContainer(
                        // StatelessWidget 은 적용할수 없음, StatefulWidget 만 적용가능
                        duration: _duration_1000,
                        // 에니메이션이 조금 부자연스러워서 추가함
                        curve: Curves.easeInOut,
                        // 인증단계에 따라서 화면에 표시 여부를 정할수 있음
                        height: getVerificationHeight(_verificationStatus),
                        child: TextFormField(
                          controller: _codeController,
                          keyboardType: TextInputType.phone,
                          // 입력된 값의 형식을 지정, 지정된 형식 이상으로 입력되지 않음
                          inputFormatters: [MaskedInputFormatter('000000')],
                          decoration: InputDecoration(
                            hintText: '인증문자 입력',
                            hintStyle:
                                TextStyle(color: Theme.of(context).hintColor),
                            focusedBorder: inputBorder,
                            border: inputBorder,
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: padding_16),
                    AnimatedContainer(
                      duration: _duration_1000,
                      // 인증단계에 따라서 화면에 표시 여부를 정할수 있음 
                      height: getVerificationBtnHeight(_verificationStatus),
                      child: TextButton(
                          onPressed: () {
                            debugPrint('_verificationStatus(onPressed): $_verificationStatus');
                            FocusScope.of(context).unfocus();
                            // 인증 진행중
                            attemptVarify(context);
                            debugPrint('_verificationStatus(onPressed): $_verificationStatus');
                          },
                          // 검증상태에 따라서 버튼의 문자열을 변경하여 동작중임을 표시함
                          child: _verificationStatus ==
                                  VerificationStatus.verifying
                              ? const SizedBox(
                                  height: 26,
                                  width: 26,
                                  child: CircularProgressIndicator(
                                      color: Colors.white),
                                )
                              : const Text('인증')),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );
  }

  double? getVerificationHeight(VerificationStatus status) {
    switch (status) {
      case VerificationStatus.none:
        return 0.0;
      case VerificationStatus.codeSending:
      case VerificationStatus.codeSent:
      case VerificationStatus.verifying:
      case VerificationStatus.verificationDone:
        return 60.0;
    }
  }

  double? getVerificationBtnHeight(VerificationStatus status) {
    switch (status) {
      case VerificationStatus.none:
        return 0.0;
      case VerificationStatus.codeSending:
      case VerificationStatus.codeSent:
      case VerificationStatus.verifying:
      case VerificationStatus.verificationDone:
        return 48.0;
    }
  }

  void attemptVarify(BuildContext context) async {
    setState(() {
      // 인증 진행중
      _verificationStatus = VerificationStatus.verifying;
    });
    debugPrint('_verificationStatus(attemptVarify): $_verificationStatus');

    // 강제 딜레이 추가
    await Future.delayed(const Duration(seconds: 3));

    setState(() {
      // 인증 완료
      _verificationStatus = VerificationStatus.verificationDone;
    });
    debugPrint('_verificationStatus(attemptVarify): $_verificationStatus');

    // context.read<UserProvider>().setUserAuth(true);
  }

}

enum VerificationStatus {
  none,
  codeSending,
  codeSent,
  verifying,
  verificationDone
}