[github 링크]
github - 7주차 주간평가
로그인페이지에서 다음과 같이 [로그인정보 저장] 기능을 구현한다.
- 체크박스를 누르고 로그인을 진행하면, 앱을 재시작해도 (새로고침하여도) 자동으로 로그인이 진행되도록 한다.
- 이 때, 로그인시 얻은 token값을 저장해야 하며, 아래 API를 사용하여 유저정보를 가져올 수 있다.
- POST http://52.79.115.43:8090/api/collections/users/auth-refresh
- header에 Authorization값으로 “Bearer $token”을 보내면 다음의 결과를 얻을 수 있다.
// 성공시 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": "test@example.com", "name": "test", "avatar": "filename.jpg" } }
class ApiRoutes {
static const String baseUrl = 'http://52.79.115.43:8090/';
static const String authWithPassword = 'api/collections/users/auth-with-password';
static const String signUp = 'api/collections/users/records';
static const String readSecret = 'api/collections/secrets/records?sort=-created';
static const String uploadSecret = 'api/collections/secrets/records';
// 자동 로그인에 필요한 api 경로 추가
static const String autoLogin = 'http://52.79.115.43:8090/api/collections/users/auth-refresh';
}
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/user.dart';
import '../util/app_routes.dart';
import '../util/api_routes.dart';
class AuthController extends GetxController {
final Rxn<User> _user = Rxn();
String? _token;
Dio dio = Dio();
User? get user => _user.value;
login(String id, String pw, bool saveToken) async {
dio.options.baseUrl = ApiRoutes.baseUrl;
try {
var res = await dio.post(
ApiRoutes.authWithPassword,
data: {'identity': id, 'password': pw}
);
if (res.statusCode == 200) {
var user = User.fromMap(res.data['record']);
_user(user);
// login 함수에 토큰 저장 코드 추가
// ~~~ 추가된 부분 ~~~
if (saveToken) {
SharedPreferences prefs = await SharedPreferences.getInstance();
_token = res.data['token'];
prefs.setString('token', _token!);
}
// ~~~ 여기까지 ~~~
}
} on DioError catch(e) {
Get.snackbar('로그인 실패!', '아이디와 비밀번호를 다시 확인해보게...');
print(e.message);
}
}
signUp(String email, String password, String passwordConfirm, String username) async {
dio.options.baseUrl = ApiRoutes.baseUrl;
try {
await dio.post(
ApiRoutes.signUp,
data: {
'email': email,
'password': password,
'passwordConfirm': passwordConfirm,
'username': username,
}
);
Get.snackbar('회원가입 성공~', '환영합니다');
Get.back();
} on DioError catch(e) {
print(e.message);
Get.snackbar('회원가입 실패', '환영할뻔');
}
}
logout() {
_user.value = null;
}
// auth_controller에 autoLogin 함수 추가
// ~~~ 추가된 부분 ~~~
autoLogin() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
if (_token != null) {
try {
var res = await dio.post(
ApiRoutes.autoLogin,
options: Options(headers: {"Authorization": 'Bearer $_token'}),
);
if (res.statusCode == 200) {
var user = User.fromMap(res.data['record']);
_user(user);
return;
}
} on DioError catch(e) {
print('auto login : ${e.message}');
}
}
Get.offNamed(AppRoutes.login);
}
// ~~~ 여기까지 ~~~
_handleAuthChanged(User? userData) {
if (userData != null) {
Get.offNamed(AppRoutes.main);
return;
}
Get.offAllNamed(AppRoutes.login);
return;
}
void onInit() {
super.onInit();
ever(_user, _handleAuthChanged);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'auth_controller.dart';
class LoginController extends GetxController {
var idController = TextEditingController();
var pwController = TextEditingController();
RxBool saveToken = false.obs;
login() async {
Get.find<AuthController>().login(idController.text, pwController.text, saveToken.value);
}
}
...
Row(
children: [
Obx(() => Checkbox(
value: controller.saveToken.value,
onChanged: (value) {
controller.saveToken(value!);
}
)),
Text('로그인 정보 저장'),
],
),
...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controller/auth_controller.dart';
class SplashPage extends StatelessWidget {
const SplashPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
Future.delayed(const Duration(seconds: 3), () {
Get.find<AuthController>().autoLogin();
});
return Scaffold(
body: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/secret_hamburger/Splash.png'),
fit: BoxFit.fill
)
),
)
);
}
}
비밀을 업로드할 때 작성자를 남길 수 있다.
[내이름 공개하기] 체크버튼이 활성화 된 상태라면 현재 사용자의 이름을 함께 남길 수 있도록 하시오.
- 이 때 필요시 Upload API를 확인한다. (33일차 과제의 내용과 같음)
📄 Upload 비밀을 업로드하는 기능
- POST http://52.79.115.43:8090/api/collections/secrets/records
- Request
- secret (String)
- author (String, (optional))
- 해당 author는 User Record ID를 입력할 것
- authorName (String, (optional))
- 해당 authorName은 닉네임을 입력할 것
- Success Response
{ "id": "RECORD_ID", "collectionId": "5647cebjvtwtcu1", "collectionName": "secrets", "created": "2022-01-01 01:00:00Z", "updated": "2022-01-01 23:59:59Z", "secret": "test", "author": "RELATION_RECORD_ID", "authorName":"test" }
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../util/api_routes.dart';
import 'auth_controller.dart';
class UploadController extends GetxController {
TextEditingController secretController = TextEditingController();
Dio dio = Dio();
// ~~~ 추가된 코드 ~~~
RxBool showName = false.obs;
// ~~~ 추가된 코드 ~~~
RxString hamText = '진짜\n나만 알고\n잇을게'.obs;
var user = Get.find<AuthController>().user;
upload() async {
dio.options.baseUrl = ApiRoutes.baseUrl;
if (secretController.text == '') return;
try {
var res = await dio.post(
ApiRoutes.uploadSecret,
data: {
'secret': secretController.text,
'author': user!.id,
// ~~~ 수정된 코드 ~~~
'authorName': showName.value ? user!.username : '',
// ~~~ 수정된 코드 ~~~
}
);
if (res.statusCode == 200) {
Get.snackbar('비밀 등록 성공!', '야호');
hamText('헤헤\n뻥이지롱');
Future.delayed(Duration(seconds: 3), () {
hamText('진짜\n나만 알고\n잇을게');
});
}
} on DioError catch(e) {
Get.snackbar('비밀 등록 실패', 'ㅠㅠ');
print(e.message);
}
}
}
Row(
children: [
Obx(() => Checkbox(
value: controller.showName.value,
onChanged: (value) {
controller.showName(value!);
}
)),
Text('이름 공개하기'),
],
),
Singleton이라는 디자인 패턴에 대해 학습하고, 내용을 정리하며 Dart 코드를 제시하시오.
- 이 때, Singleton의 코드는 검색을 통해 참고할 수 있도록 한다.
- 어떠한 경우에 Singleton 디자인 패턴이 유용하게 쓰일 수 있을지 고민하고 정리하시오.
특징
- 하나의 인스턴스싱글톤 클래스는 오직 하나의 인스턴스만을 생성→ 인스턴스는 클래스 내부에서 관리되며, 외부에서는 직접 생성할 수 없음
- 전역적인 접근싱글톤 인스턴스는 전역적으로 접근 가능함→ 다른 클래스나 모듈에서 인스턴스에 접근할 수 있으며 동시에 같은 인스턴스에 접근할 수 있음
- 지연된 초기화싱글톤 인스턴스는 필요한 시점에서 생성됨→ 처음 사용되기 전까지는 인스턴스가 생성되지 않음→ 자원의 낭비를 방지할 수 있음
class Singleton {
static Singleton _instance;
// private 생성자
Singleton._();
factory Singleton.getInstance() {
if (_instance == null) {
_instance = Singleton._();
}
return _instance;
}
// 싱글톤 인스턴스의 기능과 데이터
}
_instance
가 null인 경우에만 인스턴스를 생성하고, 그렇지 않은 경우에는 이미 생성된 인스턴스를 반환 → 싱글톤 클래스의 인스턴스는 오직 하나만 생성되고 전역적으로 접근할 수 있음기존의 비밀듣는고양이의 앱은 각 컨트롤러에서 네트워크 통신이 필요하면 서로 다른 Dio객체를 각각 생성해야 했었다. Singleton으로 단일 객체를 사용하기 위하여 Dio를 Singleton을 통해 사용할 수 있는 방법에 대한 코드를 OpenAI의 ChatGPT를 활용하여 생성하시오.
- 이 때 클래스의 이름은 CustomDio로 정의한다.
- 얻은 코드를 사용하여 비밀듣는고양이 앱에 사용되는 다음의 Controller에 각각 적용하고
과정을 설명하시오.
- AuthController의 Login, Signup기능
- SecretController의 비밀읽기 기능
- UploadController의 비밀업로드 기능- 이 때 Dio를 사용한 CustomDio를 만들어 Singleton으로 사용하면 어떠한 이점이 있는지 고민하고 정리하시오
챗선생님의 말씀…
아래의 코드는
CustomDio
라는 Singleton 클래스를 생성하여 Dio 객체를 관리하는 예시입니다. 코드는 Flutter에서 사용할 수 있도록 작성되었습니다.import 'package:dio/dio.dart'; class CustomDio { static CustomDio? _instance; late Dio _dio; factory CustomDio() { _instance ??= CustomDio._(); // 인스턴스가 없는 경우에만 생성 return _instance!; } CustomDio._() { _dio = Dio(); // Dio 객체 초기화 // 추가적인 설정을 할 수 있습니다. } Dio get dio => _dio; }
위의 코드를 사용하면
CustomDio
클래스를 Singleton으로 사용할 수 있습니다. 다음은 사용 예시입니다:void main() { CustomDio customDio = CustomDio(); // Singleton 객체 생성 Dio dio1 = customDio.dio; // Singleton 객체에서 Dio 인스턴스 가져오기 dio1.get('https://api.example.com/endpoint1'); Dio dio2 = customDio.dio; // 이미 생성된 Singleton 객체에서 다시 Dio 인스턴스 가져오기 dio2.post('https://api.example.com/endpoint2'); }
위의 코드에서
CustomDio
클래스의dio
getter 메서드를 통해 Singleton 객체 내부의 Dio 인스턴스를 얻을 수 있습니다. Singleton으로 생성된CustomDio
객체를 통해 다양한 컨트롤러에서 동일한 Dio 인스턴스에 접근할 수 있습니다.
import 'package:dio/dio.dart';
import 'api_routes.dart';
class CustomDio {
static CustomDio? _instance;
late Dio _dio;
factory CustomDio() {
_instance ??= CustomDio._(); // 인스턴스가 없는 경우에만 생성
return _instance!;
}
CustomDio._() {
_dio = Dio(); // Dio 객체 초기화
// 추가적인 설정을 할 수 있습니다.
dio.options.baseUrl = ApiRoutes.baseUrl;
}
Dio get dio => _dio!;
}
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../model/user.dart';
import '../util/app_routes.dart';
import '../util/api_routes.dart';
import '../util/custom_dio.dart';
class AuthController extends GetxController {
final Rxn<User> _user = Rxn();
String? _token;
Dio dio = CustomDio().dio;
User? get user => _user.value;
login(String id, String pw, bool saveToken) async {
try {
var res = await dio.post(
ApiRoutes.authWithPassword,
data: {'identity': id, 'password': pw}
);
if (res.statusCode == 200) {
var user = User.fromMap(res.data['record']);
_user(user);
if (saveToken) {
SharedPreferences prefs = await SharedPreferences.getInstance();
_token = res.data['token'];
prefs.setString('token', _token!);
}
}
} on DioError catch(e) {
Get.snackbar('로그인 실패!', '아이디와 비밀번호를 다시 확인해보게...');
print(e.message);
}
}
signUp(String email, String password, String passwordConfirm, String username) async {
try {
await dio.post(
ApiRoutes.signUp,
data: {
'email': email,
'password': password,
'passwordConfirm': passwordConfirm,
'username': username,
}
);
Get.snackbar('회원가입 성공~', '환영합니다');
Get.back();
} on DioError catch(e) {
print(e.message);
Get.snackbar('회원가입 실패', '환영할뻔');
}
}
logout() {
_user.value = null;
}
autoLogin() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
if (_token != null) {
try {
var res = await dio.post(
ApiRoutes.autoLogin,
options: Options(headers: {"Authorization": 'Bearer $_token'}),
);
if (res.statusCode == 200) {
var user = User.fromMap(res.data['record']);
_user(user);
return;
}
} on DioError catch(e) {
print('auto login : ${e.message}');
}
}
Get.offNamed(AppRoutes.login);
}
_handleAuthChanged(User? userData) {
if (userData != null) {
Get.offNamed(AppRoutes.main);
return;
}
Get.offAllNamed(AppRoutes.login);
return;
}
void onInit() {
super.onInit();
ever(_user, _handleAuthChanged);
}
}
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../model/secret.dart';
import '../util/api_routes.dart';
import '../util/custom_dio.dart';
class SecretController extends GetxController {
RxList<Secret> secrets = RxList();
Dio dio = CustomDio().dio;
PageController pageController = PageController();
RxBool isPageChanging = false.obs;
int currentPageIndex = 0;
readSecrets() async {
try {
var res = await dio.get(ApiRoutes.readSecret);
if (res.statusCode == 200) {
List<Map<String, dynamic>> data = List<Map<String, dynamic>>.from(res.data['items']);
secrets(data.map((e) => Secret.fromMap(e)).toList().obs);
}
} on DioError catch(e) {
print(e.message);
}
}
void _handlePageChange() {
if (pageController.page!.toInt() != currentPageIndex) {
isPageChanging.value = true;
} else {
isPageChanging.value = false;
}
}
void onInit() {
super.onInit();
readSecrets();
pageController = PageController(initialPage: currentPageIndex);
pageController.addListener(_handlePageChange);
}
void onClose() {
super.onClose();
pageController.dispose();
}
}
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../util/api_routes.dart';
import '../util/custom_dio.dart';
import 'auth_controller.dart';
class UploadController extends GetxController {
TextEditingController secretController = TextEditingController();
Dio dio = CustomDio().dio;
RxBool showName = false.obs;
RxString hamText = '진짜\n나만 알고\n잇을게'.obs;
var user = Get.find<AuthController>().user;
upload() async {
if (secretController.text == '') return;
try {
var res = await dio.post(
ApiRoutes.uploadSecret,
data: {
'secret': secretController.text,
'author': user!.id,
'authorName': showName.value ? user!.username : '',
}
);
if (res.statusCode == 200) {
Get.snackbar('비밀 등록 성공!', '야호');
hamText('헤헤\n뻥이지롱');
Future.delayed(Duration(seconds: 3), () {
hamText('진짜\n나만 알고\n잇을게');
});
}
} on DioError catch(e) {
Get.snackbar('비밀 등록 실패', 'ㅠㅠ');
print(e.message);
}
}
}
Dio를 사용한 CustomDio를 만들어 Singleton으로 사용하면
- 리소스 공유
모든 컨트롤러 또는 클래스에서 동일한 Dio 인스턴스를 공유할 수 있음
→ 여러 곳에서 동시에 Dio 인스턴스를 생성하거나 관리할 필요가 없으므로 리소스 사용이 줄어든다- 일관성
동일한 Dio 인스턴스를 사용하면 네트워크 요청에 대한 일관성을 유지할 수 있음
→ 인증 토큰이나 인터셉터와 같은 Dio 구성을 단일 인스턴스에 적용하면 앱 전체에서 일관된 설정을 사용할 수 있음- 성능 개선
Dio 인스턴스를 생성하는 데는 약간의 오버헤드가 발생할 수 있음
→ Singleton 패턴을 사용하면 앱 시작 시에 Dio 인스턴스를 미리 생성하고 필요할 때마다 공유하여 이러한 오버헤드를 줄일 수 있음- 테스트 용이성
Dio를 Singleton으로 사용하면 테스트를 더 쉽게 수행할 수 있음
→ 모든 테스트에서 동일한 Dio 인스턴스를 사용하여 일관된 결과를 얻을 수 있음
→ 테스트 중에 Dio 인스턴스를 모의(mock) 또는 대체(substitute)할 수도 있음
조금 늦었지만 ..^^ 7주차도 끝냈습니다~ (8주차 수요일에 작성중)
이론과정이 끝나가고 있네요... 시간이 왜케빨리가지....
이제 드디어 7주차 과제들을 모두 해치워서 파이어베이스로 넘어갈 수 있게 되었습니다 ㅎㅎ
재밋겟다!!!!!!!!!