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