1. 스플래시(SplashPage)
스플래시 페이지란 해당 앱의 로딩을 위한 로고 화면 같은 것이다.
보통 제일 처음 들어갈 때 나오는데, 주로 데이터를 처리하는 과정을 기다릴 때 나온다.
이 코드에서는 세션을 확인하여 LoginPage 로 보내거나, MainPage 로 보내는 역할을 한다.
class SplashPage extends ConsumerWidget {
  const SplashPage({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.read(sessionProvider).autoLogin();
    return Scaffold(
      body: Center(
        child: Image.asset(
          'assets/splash.gif',
          width: double.infinity,
          height: double.infinity,
          fit: BoxFit.cover,
        ),
      ),
    );
  } 
}autoLogin 을 살펴보면 이런 클래스가 나온다.
SessionGM  은 이런 데이터를 처음 갖고 있고 그냥 new 되기 때문에 초기 값은 false 가 된다.import 'package:flutter/cupertino.dart';
import 'package:flutter_blog/main.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SessionGM {
  int? id;
  String? username;
  String? accessToken;
  bool? isLogin;
  SessionGM({this.id, this.username, this.accessToken, this.isLogin = false});
  final mContext = navigatorKey.currentContext!;
  Future<void> autoLogin() async {
    Future.delayed(
      Duration(seconds: 3),
      () {
        Navigator.popAndPushNamed(mContext, "/post/list");
      },
    );
  }
}
final sessionProvider = StateProvider<SessionGM>((ref) {
  return SessionGM();
});
그렇다면 이 
SessionGM 을 static 으로 만들어도 될까?
답은 yes 다. Spring서버와 달리 앱은 혼자 사용하기 때문에 static 으로 생성할 수도 있다.mixin class Session {
  int? id;
  String? username;
  String? accessToken;
  bool? isLogin;
  }
class SessionGM extends Session{
  final mContext = navigatorKey.currentContext!;
  Future<void> login() async {}
  Future<void> join() async {}
  Future<void> logout() async {}
  Future<void> autoLogin() async {
    Future.delayed(
      Duration(seconds: 3),
      () {
        Navigator.popAndPushNamed(mContext, "/post/list");
      },
    );
  }
}mixin 클래스를 만들어서도 관리할 수 있다.
2. 통신
토큰이 유효한지 찾는 녀석. 
서비스 명이 아닌 findMatchAccessToken,verifyJwt 같은 정확한 역할을 이름으로 가져야함.
(그러나 autoLogin 으로 진행함)
class UserRepository {
  
  Future<Map<String, dynamic>> autoLogin(String accessToken) async {
    final response = await dio.post(
      "/auto/login",
      options: Options(headers: {"Authorization": "Bearer $accessToken"}),
    );
    Map<String, dynamic> one = response.data;
    return one;
  }
}Option 을 사용하여 헤더를 넣어줄 수 있다.
그리고 response 의 헤더가 200이 아닌 경우 터지게 된다. 따라서 예외를 잡아주어야한다.
try-catch 내부에 넣는 경우위의 코드를 try 내부에 넣게 되어 catche 에 가게 되면 설정한 응답 false 데이터가 오지 않는다.
따라서 dio의 설정을 추가해야한다.  
validateStatus: (status) => true 를 추가하여 응답 코드가 200이 아닐 때에도 에러가 나지 않도록 하는것.final dio = Dio(
  BaseOptions(
    baseUrl: baseUrl, // 내 IP 입력
    contentType: "application/json; charset=utf-8",
    validateStatus: (status) => true, // 200 이 아니어도 예외 발생안하게 설정
  ),
);2-1. 로그인 통신 코드
일반 로그인 요청을 보낸 경우 이러한 데이터가 응답된다.
{
    "success": true,
    "response": {
        "id": 1,
        "username": "ssar",
        "imgUrl": "/images/1.png"
    },
    "status": 200,
    "errorMessage": null
}그러나 토큰도 
Headers 에 담겨 오기 때문에 리턴 값이 2개가 되어야 한다.(json 데이터와, header 의 Authorization 을 받아야함)
기존의 코드는 응답된 json 데이터만 리턴한다. 따라서 바꿔 주어야함.
// 변경 전
Future<Map<String, dynamic>> login(String username, String password) async {
  final response = await dio.post("/login", data: {
    "username": username,
    "password": password,
  });
  Map<String, dynamic> one = response.data;
  return one;
} // 변경 후 (플러터는 리턴값이 2개가 될 수 있다.)
  Future<(Map<String, dynamic>, String)> login(
      String username, String password) async {
    final response = await dio.post("/login", data: {
      "username": username,
      "password": password,
    });
    String accessToken = response.headers["Authorization"]![0];
    Map<String, dynamic> body = response.data;
    return (body, accessToken);
  }Authorization 은 하나만 응답이 가능 함.
쿠키는 응답이 ; 로 구분되어 여러개 응답이 가능하다. (List 로 받는다.)그런 이유때문인가 header 의 0번지에 JWT 가 들어있다.
3. 로그인 코드
로그인 코드가 해야하는 역할은 다음과 같다.
1. 통신 2. 성공 (1) SessionGM 값 변경 (2) 휴대폰 하드 저장 (3) dio 에 토큰 세팅 (4) 화면 이동 3. 실패 처리
로그인이 성공했을 경우 토큰을 핸드폰 하드에 저장을해야 한다. 
쉐어드프리페어먼스라는게 있는데 여기에 넣어두면 다른앱들도 쓸 수 있다. 따라서 시큐어 스토리지를 사용하게 되는데 
어플리케이션 Secure Storage를 쉽게 사용할 수 있도록 도와주는 라이브러리를 쓸 수 있다.
이는 해당되는 앱만 사용이 가능하도록 하는것.
dependencies:
  flutter_secure_storage: ^8.0.0 전역변수로 만들어져 있다.
const secureStorage = FlutterSecureStorage();로그인 메서드 실행 코드
Future<void> login(String username, String password) async {
    // 1. 통신 {success:뭐시기, status:뭐시기, errorMassage: 뭐시기, response:오브젝트}
    var (body, accessToken) = await UserRepository().login(username, password);
    // 2. 성공 or 실패 처리
    if (body["success"]) {
      // (1) SessionGM 값 변경
      this.id = body["response"]["id"];
      this.username = body["response"]["username"];
      this.accessToken = accessToken;
      this.isLogin = true; //상태를 바꿨으나 read 만 가능한 provider 라 화면이 다시 그려지진 않음
      // (2) 휴대폰 하드 저장
      await secureStorage.write(key: "accessToken", value: accessToken);
      // (3) dio 에 토큰 세팅
      dio.options.headers["Autorization"] = accessToken;
      // (4) 화면 이동
      Navigator.pushNamed(mContext, "/post/list");
    } else {
      ScaffoldMessenger.of(mContext).showSnackBar(
        SnackBar(content: Text("${body["errorMessage"]}")),
      );
    }
  }로그인 폼
혹시 모를 공백 처리를 위해 trim 추가
 CustomElevatedButton(
            text: "로그인",
            click: () {
              ref
                  .read(sessionProvider)
                  .login(_username.text.trim(), _password.text.trim());
            },
          ),
import 'package:logger/logger.dart';Logger().d("로그인 성공");사용하여 이렇게 로그에 출력할 수 있음. 
로그아웃
시큐어 스토리지에 
accessToken 를 삭제하고, isLogin 을 false 로 바꾼다.  Future<void> logout() async {
    await secureStorage.delete(key: "accessToken");
    this.id = null;
    this.username = null;
    this.accessToken = accessToken;
    this.isLogin = false;
    Navigator.pushNamed(mContext, "/post/list");
  }그리고 로그아웃 버튼에서 해당 메서드를 호출하면 된다. 
 TextButton(
                onPressed: () {  
                  ref.read(sessionProvider).logout();
                },
                child: const Text(
                  "로그아웃",
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.black54,
                  ),
                ),
              ),자동 로그인
1. 시큐어 스토리지에서 accessToken 꺼내기 2. api 호출 3. 세션 값 갱신 4. 정상이면 /post/list 로 이동
  Future<void> autoLogin() async {
    // 1. 시큐어 스토리지에서 accessToken 꺼내기
    String? accessToken = await secureStorage.read(key: "accessToken");
    Logger().d("accessToken? , ${accessToken}");
    
    if (accessToken == null) {
      Navigator.popAndPushNamed(mContext, '/login');
    } else {
      // 2. api 호출
      Map<String, dynamic> body = await UserRepository().autoLogin(accessToken);
      // 3. 세션 값 갱신
      this.id = body["response"]["id"];
      this.username = body["response"]["username"];
      this.accessToken = accessToken;
      this.isLogin = true;
      await secureStorage.write(key: "accessToken", value: accessToken);
     
      dio.options.headers["Autorization"] = accessToken;
      // 4. 화면 이동
      Navigator.pushNamed(mContext, "/post/list");
    }
  }
}3,4 는 기본 로그인과 동일 합니다.
이것을 테스트 하고 싶다면 플러터 에뮬레이터에서 에뮬레이터를 종료 시키지 않고
탭을 끈 뒤 다시 들어가 보면 됩니다. 
Share article