seong

회원가입 페이지 본문

Flutter/중계 플랫폼 프로젝트

회원가입 페이지

hyeonseong 2022. 12. 20. 15:47

그려야할 페이지는 아래와 같다.

1. TextField를 사용해서 클라이언트의 텍스트를 입력 받는다.

2. Form을 사용해서 상태 관리

3. 관심사는 Dialog를 사용해서 선택
기본 구조는 Scaffold -> Form -> FormField 이렇게 된다.

그리고 만들때 ScrollerController를 사용해서 TextField를 클릭시 키보드가 올라가게 해준다.

1. JoinPage 큰 구조

- 서버에 요청할 객체는 싱글톤으로 만들어서 계속 값을 입력할때 마다 Set해주었다.

import 'package:finalproject_front/dto/request/auth_req_dto.dart';
import 'package:finalproject_front/pages/auth/components/join_custom_form.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:logger/logger.dart';

class JoinPage extends StatefulWidget {
  JoinPage({required this.role, super.key});

  State<JoinPage> createState() => _JoinPageState();

// ReqDto를 싱글톤으로 사용
  JoinReqDto joinReqDto = JoinReqDto.single();
  String role;
}

class _JoinPageState extends State<JoinPage> {
  late ScrollController scrollController;

  @override
  void initState() {
    super.initState();
    scrollController = new ScrollController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _buildAppBar(context),
      // Form으로 이동
      body: JoinCustomForm(scrollAnimate, role: widget.role, joinReqDto: widget.joinReqDto),
    );
  }

  AppBar _buildAppBar(BuildContext context) {
    return AppBar(
      elevation: 1,
      centerTitle: true,
      title: Text(
        "회원가입",
        style: TextStyle(color: Colors.black),
      ),
      leading: IconButton(
          icon: Icon(
            CupertinoIcons.back,
            color: Colors.black,
          ),
          onPressed: () {
            Navigator.pop(context);
          }),
    );
  }

// 스크롤 애니메이션
  void scrollAnimate() {
    Future.delayed(Duration(milliseconds: 600), () {
      scrollController.animateTo(MediaQuery.of(context).viewInsets.bottom,
          duration: Duration(microseconds: 100), // 0.1초 이후 field가 올라간다.
          curve: Curves.easeIn); //Curves - 올라갈때 애니메이션
    });
  }
}

 

2. Form

- 값을 입력할때 Req객체에 넣는 함수를 만들어서 전달했다. onChanged

import 'package:finalproject_front/controller/user_controller.dart';
import 'package:finalproject_front/dto/request/auth_req_dto.dart';
import 'package:finalproject_front/pages/auth/components/category_select_button.dart';
import 'package:finalproject_front/pages/components/custom_text_field.dart';
import 'package:finalproject_front/size.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';

import '../../../constants.dart';
import '../../components/custom_main_button.dart';

class JoinCustomForm extends ConsumerWidget {
  JoinCustomForm(this.scrollAnimate, {required this.role, required this.joinReqDto, super.key});
  late JoinReqDto joinReqDto;
  final Function scrollAnimate;
  final role;
  final _formKey = GlobalKey<FormState>(); // 글로벌 key

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final uc = ref.read(userController);
    return Form(
      key: _formKey, // 해당 키로 Form의 상태를 관리 한다.
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: SingleChildScrollView( // 키보드가 올라오면서 OverFlow발생, SingleChild사용
          child: Column(
            children: [
              CustomTextField(
                scrollAnimate,
                fieldTitle: "아이디",
                hint: "아이디를 입력해주세요",
                lines: 1,
                onChanged: (value) {
                  joinReqDto.username = value.trim(); // trim은 공백을 없애줌.
                },
              ),
              SizedBox(height: gap_m),
              CustomTextField(
                scrollAnimate,
                fieldTitle: "비밀번호",
                hint: "비밀번호를 입력해주세요",
                lines: 1,
                onChanged: (value) {
                  joinReqDto.password = value.trim();
                },
              ),
              SizedBox(height: gap_m),
              CustomTextField(
                scrollAnimate,
                fieldTitle: "이메일",
                hint: "이메일를 입력해주세요",
                lines: 1,
                onChanged: (value) {
                  joinReqDto.email = value.trim();
                },
              ),
              SizedBox(height: gap_m),
              CustomTextField(
                scrollAnimate,
                fieldTitle: "휴대폰번호",
                hint: "휴대폰번호를 입력해주세요",
                lines: 1,
                onChanged: (value) {
                  joinReqDto.phoneNum = value.trim();
                },
              ),
              SizedBox(height: gap_m),
              Column(
                children: [
                  Align(
                    alignment: Alignment.centerLeft,
                    child: Text(
                      "관심사 선택",
                      style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                    ),
                  ),
                  SizedBox(height: gap_m),
                  CategorySelectButton(joinReqDto: joinReqDto),
                ],
              ),
              SizedBox(height: gap_xl),
              _buildJoinButton(uc, context, joinReqDto),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildJoinButton(UserController uc, BuildContext context, JoinReqDto joinReqDto) {
    return ElevatedButton(
      onPressed: () {
        joinReqDto.role = role;
        uc.joinUser(
          joinReqDto: joinReqDto,
        );
      },
      style: ElevatedButton.styleFrom(
        primary: gButtonOffColor,
        minimumSize: Size(getScreenWidth(context), 60),
      ),
      child: Align(
        alignment: Alignment.center,
        child: Text(
          "회원가입 완료",
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
    );
  }
}

TextField (컴포넌트)

- TextField는 여러곳에서 사용해서 미리 Conponent에 만들었었다.

- 공용 컴포넌트로 사용하기때문에 필요한 값들은 생성자로 받아서 처리해주었다.

- 여기서 일부 다른 페이지들은 TextField의 크기가 달라야했다. 처음엔 height를 주어서 바꾸려 했지만, maxline속성으로 조절이 가능했다.

import 'package:finalproject_front/constants.dart';
import 'package:finalproject_front/size.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';

class CustomTextField extends StatelessWidget {
  final ValueChanged<String> onChanged;
  final TextEditingController? fieldController;
  final Function scrollAnimate;
  final String fieldTitle;
  final String? subTitle;
  final String hint;
  final int lines;

  const CustomTextField(this.scrollAnimate,
      {this.subTitle, this.fieldController, required this.fieldTitle, required this.hint, required this.lines, required this.onChanged, Key? key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Align(
            alignment: Alignment.centerLeft,
            child: Text.rich(
              TextSpan(
                children: [
                  TextSpan(
                    text: "${fieldTitle}",
                    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                  ),
                  if (subTitle != null)
                    TextSpan(
                      text: "${subTitle}",
                      style: TextStyle(color: gSubTextColor, fontSize: 10, fontWeight: FontWeight.bold),
                    )
                ],
              ),
            ),
          ),
          SizedBox(height: gap_m),
          TextFormField(
            onTap: (() {
              scrollAnimate;
            }),
            onChanged: onChanged,
            controller: fieldController,
            keyboardType: TextInputType.multiline,
            maxLines: lines,
            decoration: InputDecoration(
              hintText: "${hint}",
              hintStyle: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.normal,
                color: gSubTextColor,
              ),
              //3. 기본 textFormfield 디자인 - enabledBorder
              enabledBorder: OutlineInputBorder(
                borderSide: BorderSide(color: gClientColor, width: 3.0),
                borderRadius: BorderRadius.circular(15),
              ),
              //마우스 올리고 난 후 스타일
              focusedBorder: OutlineInputBorder(
                borderSide: BorderSide(color: gClientColor, width: 3.0),
                borderRadius: BorderRadius.circular(15),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

 

관심사 선택 버튼  - 라이브러리 사용

- 중복선택이 가능한 Select버튼을 찾고 있다가 라이브러리에 DialogField로 선택할 수 있는 라이브러리를 찾아서 적용시켰다.

- 보여질 List는 List<Category>타입이다, View에 뿌려줄땐 이 라이브러리가 MultiSelectItem이 필요하다고 해서

List아이템을 다시 MultiSelectItem으로 담아주는 작업이 필요하다 . - 기존의 List -> Map -> MultiSelectItem로 toList 

import 'package:finalproject_front/constants.dart';
import 'package:finalproject_front/dto/request/auth_req_dto.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:multi_select_flutter/multi_select_flutter.dart';

import '../../../domain/category.dart';

class CategorySelectButton extends StatefulWidget {
  JoinReqDto? joinReqDto;
  UserUpdateReqDto? userUpdateReqDto;
  CategorySelectButton({this.joinReqDto, this.userUpdateReqDto, super.key});

  @override
  State<CategorySelectButton> createState() => _CategorySelectButtonState();
}

class _CategorySelectButtonState extends State<CategorySelectButton> {
  // ignore: prefer_final_fields
  static List<Category> _category = [
    // 리스트
    Category(id: 1, name: "뷰티"),
    Category(id: 2, name: "운동"),
    Category(id: 3, name: "댄스"),
    Category(id: 4, name: "뮤직"),
    Category(id: 5, name: "미술"),
    Category(id: 6, name: "문학"),
    Category(id: 7, name: "공예"),
    Category(id: 8, name: "기타"),
  ];

  final _items = _category.map((category) => MultiSelectItem<Category>(category, category.name)).toList(); // 선택 가능한 항목을 보여줌 -> 리스트를 깊은 복사

  List<Category>? _selectCategory = []; // null이 가능하다, 초기값은 null이다.
  List<int>? categoryId = [];
  final _multiSelectKey = GlobalKey<FormFieldState>();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MultiSelectDialogField<Category>(
      //버튼에 사용할 value타입을 Category 오브젝트 타입임을 선언
      items: _items,
      title: Text("관심사"),
      key: _multiSelectKey,
      itemsTextStyle: TextStyle(color: Colors.black),
      isDismissible: true,
      cancelText: Text(
        "취소",
        style: TextStyle(fontSize: 18),
      ),
      confirmText: Text(
        "선택",
        style: TextStyle(fontSize: 18),
      ),
      selectedItemsTextStyle: TextStyle(color: gPrimaryColor),
      decoration: BoxDecoration(
        border: Border.all(
          color: gClientColor,
          width: 3,
        ),
        borderRadius: BorderRadius.circular(10),
      ),
      buttonIcon: Icon(
        CupertinoIcons.plus_circle,
        color: gClientColor,
        size: 35,
      ),
      buttonText: Text(
        "관심사를 선택해 주세요.",
        style: TextStyle(fontSize: 16, color: gSubTextColor),
      ),

      onConfirm: (results) {
        _selectCategory = results;
        categoryId = _selectCategory?.map((e) => e.id).toList();
        widget.joinReqDto?.categoryIds = categoryId;
        widget.userUpdateReqDto?.categoryIds = categoryId;
      },
    );
  }
}

만들면서 막힌곳 

1. 서버에서 카테고리를 int 타입의 id만 달라고했다. 이 부분에서 막막하기만하고 생각이 나지않았다.

해결 : 기존의 list -> map -> int 타입의 List로 변환 

2. 카테고리 중복 선택 버튼에서 버튼 클릭 시 값이 넘어가지 않음.

해결 : 1. Widget분리 , 2. ReqDto를 생성자로 받아서 넘겨주었다.