Skip to content

Commit 10c79db

Browse files
authored
feat: 로그인 화면 구현 및 api 연결 완료 (#17)
* fix: pretendard 파일 수정 * feat: 시작 화면 구현 및 이메일 유효성 검사 구현 완료 * fix: emailScreen 배경 수정 * feat: signup 기능 구현 * fix: familyname_screen.dart 디자인 수정 * feat: 가족 생성 및 기존 가족 참가 api 연결, snackbar 추가, 로딩 인디케이터 수정, 회원가입 이메일 한글 예외 처리 추가 * fix: 필요 없는 코드 수정 * feat: login 화면 구현 및 api 연결 완료
1 parent 7523b50 commit 10c79db

File tree

4 files changed

+285
-6
lines changed

4 files changed

+285
-6
lines changed

frontend/ongi/lib/main.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import 'package:flutter/material.dart';
2-
import 'package:ongi/screens/parent_init_screen.dart';
3-
import 'package:ongi/screens/health_log_screen.dart';
4-
import 'package:ongi/screens/home_screen.dart';
5-
import 'package:ongi/screens/login_screen.dart';
2+
import 'package:ongi/screens/login/login_pw_screen.dart';
63
import 'package:ongi/screens/start_screen.dart';
74
import 'package:ongi/screens/signup/password_screen.dart';
8-
import 'package:ongi/screens/bottom_nav.dart';
95

106
void main() {
117
runApp(const OngiApp());
@@ -27,7 +23,7 @@ class OngiApp extends StatelessWidget {
2723
home: const StartScreen(),
2824

2925
routes: {
30-
// '/login': (context) => const LoginScreen(),
26+
'/login': (context) => const LoginPwScreen(),
3127
'/signup': (context) => const PasswordScreen(),
3228
}
3329
);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:ongi/core/app_colors.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
4+
import 'package:ongi/services/login_service.dart';
5+
import 'package:ongi/screens/login/login_ready_screen.dart';
6+
7+
class LoginPwScreen extends StatefulWidget {
8+
const LoginPwScreen({super.key});
9+
10+
@override
11+
State<LoginPwScreen> createState() => _LoginPwScreenState();
12+
}
13+
14+
class _LoginPwScreenState extends State<LoginPwScreen> {
15+
final TextEditingController _passwordCtrl = TextEditingController();
16+
final LoginService _loginService = LoginService();
17+
bool _isLoading = false;
18+
19+
void _showErrorSnackBar(String message) {
20+
ScaffoldMessenger.of(context).showSnackBar(
21+
SnackBar(
22+
content: Text(
23+
message,
24+
style: const TextStyle(color: AppColors.ongiOrange),
25+
),
26+
backgroundColor: Colors.white,
27+
behavior: SnackBarBehavior.floating,
28+
shape: RoundedRectangleBorder(
29+
borderRadius: BorderRadius.circular(20),
30+
),
31+
duration: const Duration(seconds: 2),
32+
),
33+
);
34+
}
35+
36+
@override
37+
Widget build(BuildContext context) {
38+
return Scaffold(
39+
backgroundColor: Colors.white,
40+
body: SingleChildScrollView(
41+
child: Column(
42+
crossAxisAlignment: CrossAxisAlignment.start,
43+
children: [
44+
Padding(
45+
padding: const EdgeInsets.only(left: 30, right: 30, top: 150),
46+
child: Column(
47+
crossAxisAlignment: CrossAxisAlignment.start,
48+
children: const [
49+
const Text(
50+
'비밀번호를',
51+
style: TextStyle(
52+
fontSize: 60,
53+
fontWeight: FontWeight.w800,
54+
height: 1.2,
55+
color: AppColors.ongiOrange,
56+
),
57+
),
58+
const Text(
59+
'입력해주세요',
60+
style: TextStyle(
61+
fontSize: 60,
62+
fontWeight: FontWeight.w200,
63+
height: 1.2,
64+
color: AppColors.ongiOrange,
65+
),
66+
),
67+
],
68+
),
69+
),
70+
Padding(
71+
padding: const EdgeInsets.only(left: 40, right: 40, top: 40),
72+
child: Text(
73+
'반갑습니다!',
74+
style: TextStyle(
75+
fontSize: 20,
76+
fontWeight: FontWeight.w300,
77+
color: Colors.grey
78+
)
79+
)
80+
),
81+
Padding(
82+
padding: const EdgeInsets.only(left: 40, right: 40, top: 10),
83+
child: TextField(
84+
controller: _passwordCtrl,
85+
keyboardType: TextInputType.visiblePassword,
86+
style: const TextStyle(fontSize: 25, color: AppColors.ongiOrange),
87+
decoration: InputDecoration(
88+
hintText: 'PASSWORD',
89+
hintStyle:
90+
TextStyle(color: Colors.grey),
91+
contentPadding: const EdgeInsets.symmetric(
92+
horizontal: 24, vertical: 13),
93+
filled: true,
94+
fillColor: Colors.transparent,
95+
enabledBorder: OutlineInputBorder(
96+
borderSide:
97+
const BorderSide(color: AppColors.ongiOrange, width: 1),
98+
borderRadius: BorderRadius.circular(20),
99+
),
100+
focusedBorder: OutlineInputBorder(
101+
borderSide:
102+
const BorderSide(color: AppColors.ongiOrange, width: 1),
103+
borderRadius: BorderRadius.circular(20),
104+
),
105+
),
106+
),
107+
),
108+
Padding(
109+
padding: const EdgeInsets.only(left: 40, right: 40, top: 20),
110+
child: SizedBox(
111+
width: double.infinity,
112+
height: 65,
113+
child: ElevatedButton(
114+
style: ElevatedButton.styleFrom(
115+
padding: const EdgeInsets.symmetric(vertical: 10),
116+
backgroundColor: AppColors.ongiOrange,
117+
shape: RoundedRectangleBorder(
118+
borderRadius: BorderRadius.circular(20),
119+
),
120+
),
121+
onPressed: _isLoading
122+
? null
123+
: () async {
124+
final password = _passwordCtrl.text.trim();
125+
if (password.isEmpty) {
126+
_showErrorSnackBar('비밀번호를 입력해주세요.');
127+
return;
128+
}
129+
130+
setState(() => _isLoading = true);
131+
132+
try {
133+
final prefs = await SharedPreferences.getInstance();
134+
final email = prefs.getString('signup_email') ?? '';
135+
136+
final result = await _loginService.login(
137+
email: email,
138+
password: password,
139+
);
140+
141+
if (!mounted) return;
142+
143+
Navigator.pushReplacement(
144+
context,
145+
MaterialPageRoute(
146+
builder: (_) => LoginReadyScreen(
147+
username: result['userInfo']['name'] ?? '사용자',
148+
),
149+
),
150+
);
151+
} catch (e) {
152+
_showErrorSnackBar('로그인에 실패했어요.T_T 비밀번호를 다시 확인해주세요.');
153+
} finally {
154+
setState(() => _isLoading = false);
155+
}
156+
},
157+
child: _isLoading
158+
? const CircularProgressIndicator(color: AppColors.ongiOrange)
159+
: const Text(
160+
'로그인',
161+
style: TextStyle(
162+
fontSize: 33,
163+
fontWeight: FontWeight.w400,
164+
color: Colors.white,
165+
),
166+
),
167+
),
168+
),
169+
),
170+
]
171+
),
172+
),
173+
);
174+
}
175+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import 'dart:async';
2+
import 'package:flutter/material.dart';
3+
import 'package:ongi/core/app_background.dart';
4+
import 'package:ongi/screens/home_screen.dart';
5+
6+
class LoginReadyScreen extends StatefulWidget {
7+
final String username;
8+
const LoginReadyScreen({required this.username, super.key});
9+
10+
@override
11+
State<LoginReadyScreen> createState() => _LoginReadyScreenState();
12+
}
13+
14+
class _LoginReadyScreenState extends State<LoginReadyScreen> {
15+
Timer? _timer;
16+
17+
@override
18+
void initState() {
19+
super.initState();
20+
21+
_timer = Timer(const Duration(seconds: 2), () {
22+
if (!mounted) return;
23+
Navigator.of(context).pushReplacement(
24+
MaterialPageRoute(builder: (_) => const HomeScreen()),
25+
);
26+
});
27+
}
28+
29+
@override
30+
void dispose() {
31+
_timer?.cancel();
32+
super.dispose();
33+
}
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
return Scaffold(
38+
backgroundColor: Colors.transparent,
39+
body: AppBackground(
40+
child: Column(
41+
crossAxisAlignment: CrossAxisAlignment.start,
42+
children: [
43+
Padding(
44+
padding: const EdgeInsets.only(left: 30, right: 30, top: 150),
45+
child: Column(
46+
crossAxisAlignment: CrossAxisAlignment.start,
47+
children: [
48+
const Text(
49+
'반가워요',
50+
style: TextStyle(
51+
fontSize: 60,
52+
fontWeight: FontWeight.w200,
53+
height: 1.2,
54+
color: Colors.white,
55+
),
56+
),
57+
Text(
58+
'${widget.username}님',
59+
style: const TextStyle(
60+
fontSize: 60,
61+
fontWeight: FontWeight.w800,
62+
height: 1.2,
63+
color: Colors.white,
64+
),
65+
),
66+
],
67+
),
68+
),
69+
],
70+
),
71+
),
72+
);
73+
}
74+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'dart:convert';
2+
import 'package:http/http.dart' as http;
3+
4+
import '../utils/token_storage.dart';
5+
6+
class LoginService {
7+
static const String baseUrl = 'https://ongi-1049536928483.asia-northeast1.run.app';
8+
9+
Future<Map<String, dynamic>> login({
10+
required String email,
11+
required String password,
12+
}) async {
13+
final url = Uri.parse('$baseUrl/auth/login');
14+
15+
final response = await http.post(
16+
url,
17+
headers: {
18+
'Content-Type': 'application/json',
19+
},
20+
body: jsonEncode({
21+
'email': email,
22+
'password': password,
23+
}),
24+
);
25+
26+
if (response.statusCode == 200) {
27+
final responseJson = jsonDecode(response.body);
28+
await TokenStorage.saveAccessToken(responseJson["accessToken"]);
29+
return responseJson;
30+
} else {
31+
throw Exception('로그인 실패: ${response.statusCode} ${response.body}');
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)