32일차 과제 - rlatkddbs99/Flutter GitHub Wiki

과제 1 AuthController에는 User의 정보만을 담고있다. 로그인을 하면 유저를 식별할 수 있는 Token 값도 함께 받아볼 수 있는데, 해당 Token 값을 AuthController 내에 저장할 수 있도록 하고, 코드를 제시

API URL

// http://52.79.115.43:8090/api/collections/users/auth-with-password

API Request

// Method : POST
// data : identity(String), password(String)
// Teddy/sfac12341234

API Response

{
  "token": "JWT_TOKEN",
  "record": {
    "id": "RECORD_ID",
    "collectionId": "_pb_users_auth_",
    "collectionName": "users",
    "created": "2022-01-01 01:00:00Z",
    "updated": "2022-01-01 23:59:59Z",
    "username": "username123",
    "verified": false,
    "emailVisibility": true,
    "email": "[email protected]",
    "name": "test",
    "avatar": "filename.jpg"
  }
}

과제 2 MainController에는 readDocuments라는 멤버 함수(메서드)를 제작하시오. 해당 API의 정보는 다음과 같다 이 때, AuthController를 find하여 Token값이 존재하면 (로그인 되었다면) 실행할 수 있도록 한다.

API URL

// http://52.79.115.43:8090/api/collections/documents/records

API Request (필수)

// Method: GET
// 해당 API는 인증된 사용자만 사용할 수 있기 때문에
// 로그인 시 획득한 Token을 반드시 Request 헤더에 Authorization을 포함시켜야만합니다.

API Response (성공시)

{
  "page": 1,
  "perPage": 30,
  "totalPages": 1,
  "totalItems": 2,
  "items": [
    {
      "id": "RECORD_ID",
      "collectionId": "bjqjkp8usyz0lpb",
      "collectionName": "documents",
      "created": "2022-01-01 01:00:00Z",
      "updated": "2022-01-01 23:59:59Z",
      "title": "test",
      "content": "test",
      "sec_level": "high",
      "attachment": "filename.jpg",
      "attachment_url": "test"
    },
    {
      "id": "RECORD_ID",
      "collectionId": "bjqjkp8usyz0lpb",
      "collectionName": "documents",
      "created": "2022-01-01 01:00:00Z",
      "updated": "2022-01-01 23:59:59Z",
      "title": "test",
      "content": "test",
      "sec_level": "high",
      "attachment": "filename.jpg",
      "attachment_url": "test"
    }
  ]
}

과제 3 위 API 정보를 토대로 응답 데이터형식에 맞게 Document 커스텀 클래스를 제작하고, MainPage의 Home이 다음과 같이 출력되도록 한다. image

아래 FAB를 누르면 readDocuments를 실행하고 결과를 화면에 출력한다. document리스트는 MainController 멤버변수로 저장한다. 다음의 제공되는 코드를 사용할 수 있도록 한다.

// ignore_for_file: public_member_api_docs, sort_constructors_first, non_constant_identifier_names

class Document {
  String title;
  String content;
  String sec_level;
  String? attachment_url;
  Document({
    required this.title,
    required this.content,
    required this.sec_level,
    this.attachment_url,
  });

  factory Document.fromMap(Map<String, dynamic> map) {
    return Document(
      title: map['title'] as String,
      content: map['content'] as String,
      sec_level: map['sec_level'] as String,
      attachment_url:
          map['attachment_url'] != '' ? map['attachment_url'] : null,
    );
  }
}

개발팀 사원리스트(최신) 게시글의 attachment_url에는 해당 이미지의 URL이 담겨있다. ”김스팩의 비고란에 무엇이 써져있는지 비밀”이 뭔지

Android Emulator - flutter_emulator_5554 2023-03-11 21-30-45

main.dart

import 'package:controller/controller/auth_controller.dart';
import 'package:controller/controller/login_controller.dart';
import 'package:controller/controller/main_controller.dart';
import 'package:controller/view/login_page.dart';
import 'package:controller/view/main_page.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      initialBinding: BindingsBuilder(() {
        //어플이 실행되자 마자 전역에 컨트롤러 올리려고
        Get.put(AuthController());
        Get.lazyPut(() => LoginController()); //메모리에 로그인 페이지에서 사용하기 전에 올림(대기상태)
        Get.lazyPut(() => MainController());
      }),
      getPages: [
        //page이동
        GetPage(name: LoginPage.route, page: () => const LoginPage()),
        GetPage(name: MainPage.route, page: () => const MainPage()),
      ],
      home: Scaffold(
        body: Center(
          child: TextButton(
              onPressed: () => Get.toNamed(LoginPage.route),
              child: const Text("hi")), //toNamed로 페이지 이동
        ),
      ),
    );
  }
}

util/api_routes.dart

//api 관리
class ApiRoutes {
  static const String authwithpassword =
      "/api/collections/users/auth-with-password";
}

controller/auth_controller.dart

import 'package:dio/dio.dart';
import 'package:get/get.dart';

import '../model/user.dart';
import '../util/api_routes.dart';
import '../view/login_page.dart';
import '../view/main_page.dart';

class AuthController extends GetxController {
  final Rxn<User> _user = Rxn(); //User를 안가지고 있을 수도 있어서 n붙임, private으로 생성
  Dio dio = Dio();
  String? _token; //token없을 수도 있어서 ?붙여줘야돼
  String? get token => _token;
  User? get user => _user.value; //_user의 정보를 user로 불러올 수 있도록
  login(String id, String pw) async {
    //data전달 받음

    try {
      // dio.options.baseUrl = "http://52.79.115.43:8090";
      var res = await dio.post(
          "http://52.79.115.43:8090${ApiRoutes.authwithpassword}", //api관리에서 가져오기
          data: {
            'identity': id,
            'password': pw,
          });
      if (res.statusCode == 200) {
        print(res.data['record']);
        var user = User.fromMap(res.data["record"]); //record데이터 가져와
        _user(user); //받아온 데이터 _user로 바꾸기, user가 전달, handleAuth가 실행됨
        _token = res.data['token'];
      }
    } on DioError catch (e) {
      print(e.message);
      print(e.requestOptions.path);
    }
  }

  logout() {
    _user.value = null; //기존에 있던 회원 값 있었으면 null로 바꾸기, 다시 ever가 동작하면서
    //user가 있는지 체크 후 페이지 이동
  }

  _handleAuthChanged(User? data) {
    //ever연결
    //user가 바뀌면 바뀐값이 userData에 들어감
    if (data != null) {
      //회원이 있으면 메인페이지로 이동
      //Get.to, 메인페이지 이동
      Get.toNamed(MainPage.route);
      return;
    }
    //회원이 아니면 로그인 페이지로 이동
    Get.toNamed(LoginPage.route);
    return;
  }

  @override
  void onInit() {
    super.onInit();
    ever(_user, _handleAuthChanged); //함수로 연결
  }
}

controller/login_controller.dart

//로그인 페이지에서만 사용
import 'package:controller/controller/auth_controller.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';

class LoginController extends GetxController {
  var idController = TextEditingController(); //id입력 컨트롤러
  var pwController = TextEditingController(); //pw입력 컨트롤러

  login() {
    Get.find<AuthController>().login(idController.text, pwController.text);
    //AuthCont 찾고 login함수 호출 하는데 매개변수로 id의 텍스트랑 pw의 텍스트 가져감
    //그거 사용해서 로그인 실행
  }
}

controller/main_controller.dart

import 'package:controller/controller/auth_controller.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../model/doucument.dart';

class MainController extends GetxController {
  var pagcontroller = PageController();
  //현재 페이지 가지기
  RxInt curPage = 0.obs;
  Dio dio = Dio();
  //리스트형태로 가질거임
  RxList<Document> documents = RxList();

  onPageTapped(int v) {
    //선택한 항목의 페이지로 이동
    pagcontroller.jumpToPage(v);
    curPage(v); //선택한 페이지 정보 수정
  }

  logout() {
    //로그아웃 기능
    Get.find<AuthController>().logout();
  }

  //document 가져오기
  readDocuments() async {
    var token = Get.find<AuthController>().token; //토큰 가져올건데
    //Auth에 있는 login함수중 토큰 가져올거임. 근데 private이니까 get메소드로 바꿔준겨
    documents.clear();
    var res = await dio.get(
      "http://52.79.115.43:8090/api/collections/documents/records",
      options: Options(headers: {
        'authorization': token
      }), //전에 토큰 넘기는 문제에서 볼 수 있듯이 헤더에 토큰정보 넘겨줘야돼
      data: {token: token}, //근데 데이터 가져가서 넘겨줘야 되니까 이렇게 데이터 넘김
    );
    if (res.statusCode == 200) {
      //데이터 가공필요함 items의 내용 필요, documents에 추가 해야 되니까 List<Map<String,dynamic>>으로 data 만들기
      List<Map<String, dynamic>> data =
          List<Map<String, dynamic>>.from(res.data['items']);
      //document에 추가, add를 하면 오류가 나-> 왜? add는 한가지만 넣을 수 있음
      //지금 data에 있는 리스트는 다수의 요소 추가이기 때문에
      documents.addAll(data.map((e) => Document.fromMap(e)).toList().obs);
    }
  }
}

model/document.dart

// ignore_for_file: public_member_api_docs, sort_constructors_first, non_constant_identifier_names

class Document {
  String title;
  String content;
  String sec_level;
  String? attachment_url;
  Document({
    required this.title,
    required this.content,
    required this.sec_level,
    this.attachment_url,
  });

  factory Document.fromMap(Map<String, dynamic> map) {
    return Document(
      title: map['title'] as String,
      content: map['content'] as String,
      sec_level: map['sec_level'] as String,
      attachment_url:
          map['attachment_url'] != '' ? map['attachment_url'] : null,
    );
  }
}

model/user.dart

import 'dart:convert';

// ignore_for_file: public_member_api_docs, sort_constructors_first
//pub.dev 패키지 : freezed, Json-annotation
//generate.constructor 하고 jsonSerial하면 그동안했던거 싹해줌 개쩐다
class User {
  String id;
  String username;
  String email;
  String name;
  User({
    required this.id,
    required this.username,
    required this.email,
    required this.name,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'id': id,
      'username': username,
      'email': email,
      'name': name,
    };
  }

  factory User.fromMap(Map<String, dynamic> map) {
    return User(
      id: map['id'] as String,
      username: map['username'] as String,
      email: map['email'] as String,
      name: map['name'] as String,
    );
  }

  String toJson() => json.encode(toMap());

  factory User.fromJson(String source) =>
      User.fromMap(json.decode(source) as Map<String, dynamic>);
}

view/home.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';

import '../model/doucument.dart';
import '../model/user.dart';

class Home extends StatelessWidget {
  const Home({super.key, required this.user, required this.document});
  final User user;
  final List<Document> document;

  @override
  Widget build(BuildContext context) {
    return Column(
      //scaffold쓰면 오류남, 얘는 화면을 전체부터 그리는 위젯이 아닌듯?
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          "${user.username}님 안녕하세요",
          style: TextStyle(fontSize: 32),
        ),
        ListView.builder(
          shrinkWrap: true,
          itemCount: document.length, //있는 document길이만큼
          itemBuilder: (context, index) {
            var post = document[index]; //document하나씩 출력하려고 해당하는 거 넣음
            return Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                //docu의 제목 내용 url필요
                Text(post.title),
                Text(post.content),
                if (post.attachment_url !=
                    null) //null인지 체크 안하고 바로 하면 오류 발생..주의해야함
                  Image.network(post.attachment_url!), //무조건 있다!
              ],
            );
          },
        )
      ],
    );
  }
}

view/login_page.dart

import 'package:controller/controller/login_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';

class LoginPage extends GetView<LoginController> {
  //get.find안해도 돼, get.put이 되어있어야함
  const LoginPage({super.key});
  static const String route = "/login"; //getPage 라우팅 위해서
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: controller.idController,
            ),
            TextField(
              controller: controller.pwController,
            ),
            ElevatedButton(
                onPressed: controller.login, //로그인 컨트롤러의 로그인 함수 실행
                
                child: Text("Login")),
          ],
        ),
      ),
    );
  }
}

view/main_page.dart

import 'package:controller/controller/auth_controller.dart';
import 'package:controller/controller/main_controller.dart';
import 'package:controller/view/home.dart';
import 'package:controller/view/my_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';

//컨트롤러연결
class MainPage extends GetView<MainController> {
  const MainPage({super.key});
  static const String route = "/main"; //getPage 라우팅 위해서
  @override
  Widget build(BuildContext context) {
    var user = Get.find<AuthController>().user!; //get 으로 user정보 가져올 수 있도록
    //페이지가 넘어갔으면 무조건 user가 있기 때문에 !붙인거임
    return Scaffold(
        bottomNavigationBar: Obx(
          //실시간으로 바뀌면 바로 반영되도록
          () => BottomNavigationBar(
              //현재 몇번째 페이지인지. RxInt여서 값 가져오려면 .value
              currentIndex: controller.curPage.value,
              onTap: controller.onPageTapped,
              items: [
                BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
                BottomNavigationBarItem(icon: Icon(Icons.person), label: "My")
              ]),
        ),
        //fab생성, onPressed정의 해 놨어 컨트롤러에 그거 연결, 근데 이미 정의한거라 () 쓰지마
        //바로 반응할 수 있도록 Obx써줘
        //그냥 arrow function했을 때 오류. 왜냐? obx를 쓰려면 Rx로 선언되어있어야함
        //플로팅은 컨트롤러에 선언 안되어있음, 그래서 컨트롤러에 선언된 curPage변수를 이용해서
        //조건문 작성 하고 return으로 플로팅 설정하면 된다
        floatingActionButton: Obx(
          () {
            if (controller.curPage.value == 0) {
              return FloatingActionButton(
                onPressed: controller.readDocuments,
                child: const Icon(Icons.refresh),
              );
            }
            return const SizedBox();
          },
        ),
        body: SafeArea(
          child: PageView(
            controller: controller.pagcontroller,
            children: [
              // Column(
              //   crossAxisAlignment: CrossAxisAlignment.start,
              //   children: [
              //     Text(
              //       "${user.username}님 안녕하세요",
              //       style: TextStyle(fontSize: 32),
              //     ),
              //   ],
              // ),
              // Column(
              //   crossAxisAlignment: CrossAxisAlignment.start,
              //   children: [
              //     ListTile(
              //       leading: CircleAvatar(),
              //       title: Text(user.username),
              //       subtitle: Text(user.name),
              //     ),
              //     ListTile(
              //       title: Text("로그아웃"),
              //       leading: Icon(Icons.logout),
              //       onTap: controller
              //           .logout, //기능을 컨트롤러에 만들어놨으니 ()호출 안해도돼. ()쓰면 무제한 실행 주의
              //     ),
              //     Text(
              //       "${user.username}님 안녕하세요",
              //       style: TextStyle(fontSize: 32),
              //     ),
              //   ],
              // )
              //누르면 바로반응 setState필요없음
              Obx(() => Home(
                  user: user,
                  document: controller.documents
                      .toList())), //페이지 이동할 때 user정보랑 document가지고 가기로 home에다 정의
              //리스트 형태니까 toList로 바꿈
              //MyPage로 user정보 가지고 넘어감
              MyPage(
                user: user,
              )
            ],
          ),
        ));
  }
}

view/my_page.dart

import 'package:controller/controller/auth_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';

import '../model/user.dart';

class MyPage extends GetView<AuthController> {
  const MyPage({super.key, required this.user});
  final User user;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(user.username),
        ElevatedButton(onPressed: controller.logout, child: Text("로그아웃"))
      ],
    );
  }
}
⚠️ **GitHub.com Fallback** ⚠️