feat: API 인증 시스템 구현 및 로그인 화면 연동

- AuthService, AuthRemoteDataSource 구현
- JWT 토큰 관리 (SecureStorage 사용)
- 로그인 화면 API 연동 및 에러 처리
- freezed 패키지로 Auth 관련 DTO 모델 생성
- 의존성 주입 설정 업데이트
This commit is contained in:
JiWoong Sul
2025-07-24 15:14:53 +09:00
parent 2b31d3af5f
commit c573096d84
26 changed files with 2063 additions and 59 deletions

View File

@@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
import 'package:superport/core/errors/failures.dart';
import 'package:superport/data/models/auth/login_request.dart';
import 'package:superport/di/injection_container.dart';
import 'package:superport/services/auth_service.dart';
/// 로그인 화면의 상태 및 비즈니스 로직을 담당하는 ChangeNotifier 기반 컨트롤러
class LoginController extends ChangeNotifier {
final AuthService _authService = inject<AuthService>();
/// 아이디 입력 컨트롤러
final TextEditingController idController = TextEditingController();
@@ -16,6 +21,14 @@ class LoginController extends ChangeNotifier {
/// 아이디 저장 여부
bool saveId = false;
/// 로딩 상태
bool _isLoading = false;
bool get isLoading => _isLoading;
/// 에러 메시지
String? _errorMessage;
String? get errorMessage => _errorMessage;
/// 아이디 저장 체크박스 상태 변경
void setSaveId(bool value) {
@@ -23,11 +36,68 @@ class LoginController extends ChangeNotifier {
notifyListeners();
}
/// 로그인 처리 (샘플)
bool login() {
// 실제 인증 로직은 구현하지 않음
// 항상 true 반환 (샘플)
return true;
/// 로그인 처리
Future<bool> login() async {
// 입력값 검증
if (idController.text.trim().isEmpty) {
_errorMessage = '이메일을 입력해주세요.';
notifyListeners();
return false;
}
if (pwController.text.isEmpty) {
_errorMessage = '비밀번호를 입력해주세요.';
notifyListeners();
return false;
}
// 이메일 형식 검증
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(idController.text.trim())) {
_errorMessage = '올바른 이메일 형식이 아닙니다.';
notifyListeners();
return false;
}
// 로딩 시작
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
// 로그인 요청
final request = LoginRequest(
email: idController.text.trim(),
password: pwController.text,
);
final result = await _authService.login(request);
return result.fold(
(failure) {
_errorMessage = failure.message;
_isLoading = false;
notifyListeners();
return false;
},
(loginResponse) {
_isLoading = false;
notifyListeners();
return true;
},
);
} catch (e) {
_errorMessage = '로그인 중 오류가 발생했습니다.';
_isLoading = false;
notifyListeners();
return false;
}
}
/// 에러 메시지 초기화
void clearError() {
_errorMessage = null;
notifyListeners();
}
@override

View File

@@ -27,10 +27,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
late AnimationController _slideController;
late Animation<Offset> _slideAnimation;
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _rememberMe = false;
bool _isLoading = false;
@override
void initState() {
@@ -66,39 +63,23 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
void dispose() {
_fadeController.dispose();
_slideController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (_usernameController.text.isEmpty || _passwordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('사용자명과 비밀번호를 입력해주세요.'),
backgroundColor: ShadcnTheme.destructive,
),
);
return;
final success = await widget.controller.login();
if (success) {
widget.onLoginSuccess();
}
setState(() {
_isLoading = true;
});
// 실제 로그인 로직 (임시로 2초 대기)
await Future.delayed(const Duration(seconds: 2));
setState(() {
_isLoading = false;
});
widget.onLoginSuccess();
}
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider.value(
value: widget.controller,
child: Consumer<LoginController>(
builder: (context, controller, _) {
return Scaffold(
backgroundColor: ShadcnTheme.background,
body: SafeArea(
child: Center(
@@ -128,6 +109,8 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
),
),
),
);
},
),
);
}
@@ -190,6 +173,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
}
Widget _buildLoginCard() {
final controller = context.watch<LoginController>();
return ShadcnCard(
padding: const EdgeInsets.all(ShadcnTheme.spacing8),
child: Column(
@@ -210,7 +194,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
ShadcnInput(
label: '사용자명',
placeholder: '사용자명을 입력하세요',
controller: _usernameController,
controller: controller.idController,
prefixIcon: const Icon(Icons.person_outline),
keyboardType: TextInputType.text,
),
@@ -220,7 +204,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
ShadcnInput(
label: '비밀번호',
placeholder: '비밀번호를 입력하세요',
controller: _passwordController,
controller: controller.pwController,
prefixIcon: const Icon(Icons.lock_outline),
obscureText: true,
keyboardType: TextInputType.visiblePassword,
@@ -231,11 +215,9 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
Row(
children: [
Checkbox(
value: _rememberMe,
value: controller.saveId,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
controller.setSaveId(value ?? false);
},
activeColor: ShadcnTheme.primary,
),
@@ -244,6 +226,38 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
],
),
const SizedBox(height: ShadcnTheme.spacing8),
// 에러 메시지 표시
if (controller.errorMessage != null)
Container(
padding: const EdgeInsets.all(ShadcnTheme.spacing3),
margin: const EdgeInsets.only(bottom: ShadcnTheme.spacing4),
decoration: BoxDecoration(
color: ShadcnTheme.destructive.withOpacity(0.1),
borderRadius: BorderRadius.circular(ShadcnTheme.borderRadius),
border: Border.all(
color: ShadcnTheme.destructive.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 20,
color: ShadcnTheme.destructive,
),
const SizedBox(width: ShadcnTheme.spacing2),
Expanded(
child: Text(
controller.errorMessage!,
style: ShadcnTheme.bodyMedium.copyWith(
color: ShadcnTheme.destructive,
),
),
),
],
),
),
// 로그인 버튼
ShadcnButton(
@@ -253,7 +267,7 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
textColor: Colors.white,
size: ShadcnButtonSize.large,
fullWidth: true,
loading: _isLoading,
loading: controller.isLoading,
),
const SizedBox(height: ShadcnTheme.spacing4),
@@ -261,8 +275,8 @@ class _LoginViewRedesignState extends State<LoginViewRedesign>
ShadcnButton(
text: '테스트 로그인',
onPressed: () {
_usernameController.text = 'admin';
_passwordController.text = 'password';
controller.idController.text = 'admin@example.com';
controller.pwController.text = 'admin123';
_handleLogin();
},
variant: ShadcnButtonVariant.secondary,