Skip to content

Commit 8af93ed

Browse files
authored
feat: 타 가족 걸음수 랭킹 화면 구현 (Neibce/OnGi#155)
* feat: 타가족 걸음수 랭킹 화면 * feat: 타가족 걸음수 랭킹 화면 연결 * feat: 타가족 걸음수 랭킹 화면 연결 * feat: 타가족 걸음수 랭킹 화면 연결 * feat: 도넛 차트 api 연결
1 parent a8ee5a5 commit 8af93ed

File tree

7 files changed

+682
-137
lines changed

7 files changed

+682
-137
lines changed
32.5 KB
Loading
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
import 'package:flutter/material.dart';
2+
import '../../core/app_colors.dart';
3+
import 'package:flutter_svg/flutter_svg.dart';
4+
import 'package:ongi/widgets/date_carousel.dart';
5+
import 'package:ongi/services/step_rank_service.dart';
6+
import 'package:ongi/utils/prefs_manager.dart';
7+
8+
class CrossFamilyRankingScreen extends StatefulWidget {
9+
const CrossFamilyRankingScreen({super.key});
10+
11+
@override
12+
State<CrossFamilyRankingScreen> createState() =>
13+
_CrossFamilyRankingScreenState();
14+
}
15+
16+
class _CrossFamilyRankingScreenState extends State<CrossFamilyRankingScreen> {
17+
bool _isLoading = false;
18+
String? _errorMessage;
19+
List<FamilyStepRank> _familyRanks = [];
20+
21+
@override
22+
void initState() {
23+
super.initState();
24+
_fetchFamilyRanks();
25+
}
26+
27+
Future<void> _fetchFamilyRanks() async {
28+
setState(() {
29+
_isLoading = true;
30+
_errorMessage = null;
31+
});
32+
33+
try {
34+
final String? accessToken = await PrefsManager.getAccessToken();
35+
if (accessToken == null) {
36+
throw Exception('로그인이 필요합니다.');
37+
}
38+
39+
final List<FamilyStepRank> ranks = await StepRankService.fetchFamilyStepRanks(accessToken);
40+
41+
setState(() {
42+
_familyRanks = ranks;
43+
});
44+
} catch (e) {
45+
setState(() {
46+
_errorMessage = e.toString().contains('Exception:')
47+
? e.toString().replaceFirst('Exception: ', '')
48+
: '가족 랭킹 조회 실패';
49+
});
50+
} finally {
51+
if (mounted) {
52+
setState(() {
53+
_isLoading = false;
54+
});
55+
}
56+
}
57+
}
58+
59+
@override
60+
Widget build(BuildContext context) {
61+
final screenWidth = MediaQuery.of(context).size.width;
62+
final circleSize = screenWidth * 1.56;
63+
64+
return Scaffold(
65+
backgroundColor: AppColors.ongiLigntgrey,
66+
body: SafeArea(
67+
child: Stack(
68+
clipBehavior: Clip.none,
69+
children: [
70+
Align(
71+
alignment: Alignment.topCenter,
72+
child: Transform.translate(
73+
offset: Offset(0, -circleSize * 0.76),
74+
child: OverflowBox(
75+
maxWidth: double.infinity,
76+
maxHeight: double.infinity,
77+
child: Container(
78+
width: circleSize,
79+
height: circleSize,
80+
decoration: BoxDecoration(
81+
shape: BoxShape.circle,
82+
color: AppColors.ongiOrange,
83+
),
84+
child: Center(
85+
child: Padding(
86+
padding: EdgeInsets.only(top: circleSize * 0.86),
87+
child: OverflowBox(
88+
maxHeight: double.infinity,
89+
child: Column(
90+
children: [
91+
const Text(
92+
'다른 가족들은',
93+
style: TextStyle(
94+
fontSize: 25,
95+
color: Colors.white,
96+
fontWeight: FontWeight.w600,
97+
height: 1,
98+
),
99+
),
100+
const Text(
101+
'얼마나 걸었을까요?',
102+
style: TextStyle(
103+
fontSize: 40,
104+
color: Colors.white,
105+
fontWeight: FontWeight.w600,
106+
),
107+
),
108+
Container(
109+
margin: EdgeInsets.symmetric(
110+
horizontal: 0,
111+
vertical: 6,
112+
),
113+
child: Image.asset(
114+
'assets/images/cross_family_ranking_title_logo.png',
115+
width: circleSize * 0.2,
116+
),
117+
),
118+
],
119+
),
120+
),
121+
),
122+
),
123+
),
124+
),
125+
),
126+
),
127+
// 상단 정보 박스
128+
Positioned(
129+
top: circleSize * 0.5,
130+
left: 15,
131+
right: 15,
132+
child: Container(
133+
padding: const EdgeInsets.all(20),
134+
decoration: BoxDecoration(
135+
color: Colors.white,
136+
borderRadius: BorderRadius.circular(20),
137+
),
138+
child: Column(
139+
crossAxisAlignment: CrossAxisAlignment.start,
140+
children: [
141+
Text(
142+
'이번주 우리가족은',
143+
style: const TextStyle(
144+
fontFamily: 'Pretendard',
145+
fontWeight: FontWeight.w600,
146+
fontSize: 20,
147+
height: 1.2,
148+
color: Color(0xFFFD6C01),
149+
),
150+
),
151+
const SizedBox(height: 2),
152+
RichText(
153+
text: TextSpan(
154+
children: [
155+
const TextSpan(
156+
text: '평균 ',
157+
style: TextStyle(
158+
fontFamily: 'Pretendard',
159+
fontWeight: FontWeight.w600,
160+
fontSize: 20,
161+
color: Color(0xFFFD6C01),
162+
),
163+
),
164+
TextSpan(
165+
text: _isLoading
166+
? '0걸음'
167+
: (_familyRanks.isNotEmpty
168+
? (_familyRanks.firstWhere((rank) => rank.isOurFamily,
169+
orElse: () => FamilyStepRank(familyName: '', averageSteps: 0, isOurFamily: false)).averageSteps)
170+
.toString()
171+
.replaceAllMapped(
172+
RegExp(
173+
r'(\d{1,3})(?=(\d{3})+(?!\d))',
174+
),
175+
(m) => '${m[1]},',
176+
) +
177+
'걸음'
178+
: '0걸음'),
179+
style: const TextStyle(
180+
fontFamily: 'Pretendard',
181+
fontWeight: FontWeight.w700,
182+
fontSize: 35,
183+
color: Color(0xFFFD6C01),
184+
),
185+
),
186+
const TextSpan(
187+
text: ' 걸었어요!',
188+
style: TextStyle(
189+
fontFamily: 'Pretendard',
190+
fontWeight: FontWeight.w600,
191+
fontSize: 20,
192+
color: Color(0xFFFD6C01),
193+
),
194+
),
195+
],
196+
),
197+
),
198+
const SizedBox(height: 8),
199+
Align(
200+
alignment: Alignment.bottomRight,
201+
child: Text(
202+
'산정 방식: (1주간 가족 총 걸음 수) ÷ 가족 인원 수',
203+
style: TextStyle(
204+
fontFamily: 'Pretendard',
205+
fontWeight: FontWeight.w400,
206+
fontSize: 10,
207+
color: Colors.grey[600],
208+
),
209+
),
210+
),
211+
],
212+
),
213+
),
214+
),
215+
// 하단 랭킹 박스
216+
Positioned(
217+
top: circleSize * 0.5 + 140,
218+
left: 15,
219+
right: 15,
220+
bottom: 15,
221+
child: Container(
222+
padding: const EdgeInsets.all(20),
223+
decoration: BoxDecoration(
224+
color: Colors.white,
225+
borderRadius: BorderRadius.circular(20),
226+
),
227+
child: SingleChildScrollView(
228+
child: Column(
229+
children: [
230+
if (_errorMessage != null)
231+
Padding(
232+
padding: const EdgeInsets.symmetric(vertical: 8),
233+
child: Text(
234+
_errorMessage!,
235+
style: const TextStyle(
236+
color: Colors.red,
237+
fontSize: 14,
238+
),
239+
),
240+
)
241+
else if (_isLoading && _familyRanks.isEmpty)
242+
const Padding(
243+
padding: EdgeInsets.symmetric(vertical: 8),
244+
child: Center(child: CircularProgressIndicator()),
245+
)
246+
else
247+
Column(
248+
crossAxisAlignment: CrossAxisAlignment.center,
249+
children: [
250+
for (int i = 0; i < _familyRanks.length; i++)
251+
_buildRankingMember(
252+
context: context,
253+
rank: i + 1,
254+
name: _familyRanks[i].familyName,
255+
steps: _familyRanks[i].averageSteps,
256+
isCurrentUser: _familyRanks[i].isOurFamily,
257+
),
258+
if (_familyRanks.isEmpty)
259+
const Padding(
260+
padding: EdgeInsets.symmetric(vertical: 8),
261+
child: Text('가족 랭킹 데이터가 없습니다.'),
262+
),
263+
],
264+
),
265+
],
266+
),
267+
),
268+
),
269+
),
270+
],
271+
),
272+
),
273+
);
274+
}
275+
}
276+
277+
278+
279+
Widget _buildRankingMember({
280+
required BuildContext context,
281+
required int rank,
282+
required String name,
283+
required int steps,
284+
required bool isCurrentUser,
285+
}) {
286+
return Container(
287+
margin: const EdgeInsets.only(bottom: 12),
288+
child: Stack(
289+
clipBehavior: Clip.none,
290+
children: [
291+
// 메인 컨테이너
292+
Transform.translate(
293+
offset: Offset(isCurrentUser ? 20 : 40, 0),
294+
child: Container(
295+
width: MediaQuery.of(context).size.width * 0.7,
296+
decoration: BoxDecoration(
297+
color: isCurrentUser ? AppColors.ongiOrange : Colors.white,
298+
borderRadius: BorderRadius.circular(20),
299+
border: isCurrentUser
300+
? null
301+
: Border.all(color: AppColors.ongiOrange, width: 1.5),
302+
),
303+
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
304+
child: Stack(
305+
children: [
306+
//이름
307+
Positioned(
308+
top: 0,
309+
left: 0,
310+
child: Text(
311+
name,
312+
style: TextStyle(
313+
fontFamily: 'Pretendard',
314+
fontWeight: FontWeight.w600,
315+
fontSize: 16,
316+
color: isCurrentUser
317+
? Colors.white
318+
: AppColors.ongiOrange,
319+
),
320+
),
321+
),
322+
//걸음수
323+
Center(
324+
child: Row(
325+
mainAxisAlignment: MainAxisAlignment.end,
326+
crossAxisAlignment: CrossAxisAlignment.baseline,
327+
textBaseline: TextBaseline.alphabetic,
328+
children: [
329+
Text(
330+
steps.toString().replaceAllMapped(
331+
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
332+
(m) => '${m[1]},',
333+
),
334+
style: TextStyle(
335+
fontFamily: 'Pretendard',
336+
fontWeight: FontWeight.w800,
337+
fontSize: 32,
338+
color: isCurrentUser
339+
? Colors.white
340+
: AppColors.ongiOrange,
341+
),
342+
),
343+
const SizedBox(width: 4),
344+
Text(
345+
'걸음',
346+
style: TextStyle(
347+
fontFamily: 'Pretendard',
348+
fontWeight: FontWeight.w500,
349+
fontSize: 16,
350+
color: isCurrentUser
351+
? Colors.white
352+
: AppColors.ongiOrange,
353+
),
354+
),
355+
],
356+
),
357+
),
358+
],
359+
),
360+
),
361+
),
362+
// 우리 가족 순위
363+
if (isCurrentUser)
364+
Positioned(
365+
left: -25,
366+
top: -25,
367+
child: Container(
368+
child: Center(
369+
child: Text(
370+
'$rank',
371+
style: TextStyle(
372+
fontFamily: 'Pretendard',
373+
fontWeight: FontWeight.w800,
374+
fontSize: 64,
375+
color: AppColors.ongiOrange,
376+
),
377+
),
378+
),
379+
),
380+
),
381+
],
382+
),
383+
);
384+
}

0 commit comments

Comments
 (0)