Files
school-attendance/lib/ui/study_ranking_screen.dart
T
sihooandClaude Sonnet 5 80667d20f1 백품타 랭킹에 전체랭킹/학년랭킹 필터 추가
grade 쿼리파라미터를 생략하면 전체 학년 통합 랭킹을 주도록 백엔드를
바꾸고, 랭킹 화면에 "N학년"/"전체랭킹" 필 필터를 추가해 시간대
필터(오늘/이번주/전체)와 별도로 고를 수 있게 했다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:19:05 +00:00

225 lines
8.0 KiB
Dart

// 🏆 학년별 공부시간 랭킹 화면 (UI 전용).
// 서버 통신/상태는 lib/function/study_ranking_controller.dart가 담당한다.
import 'package:flutter/material.dart';
import '../function/study_ranking_controller.dart';
import '../theme/app_palette.dart';
import 'title_pill.dart';
class StudyRankingScreen extends StatefulWidget {
final int grade;
final String myStudentId;
const StudyRankingScreen({
super.key,
required this.grade,
required this.myStudentId,
});
@override
State<StudyRankingScreen> createState() => _StudyRankingScreenState();
}
class _StudyRankingScreenState extends State<StudyRankingScreen> {
late final StudyRankingController _controller;
@override
void initState() {
super.initState();
_controller = StudyRankingController(grade: widget.grade);
_controller.init();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
String _formatHM(int totalSeconds) {
final h = totalSeconds ~/ 3600;
final m = (totalSeconds % 3600) ~/ 60;
if (h > 0) return '$h시간 $m분';
return '$m분';
}
Color? _rankColor(int rank) {
switch (rank) {
case 1:
return const Color(0xFFC9A227); // 금
case 2:
return const Color(0xFF9AA0A6); // 은
case 3:
return const Color(0xFFB07A4B); // 동
default:
return null;
}
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: _controller,
builder: (context, _) {
final ranking = _controller.ranking;
return Scaffold(
backgroundColor: AppPalette.mist,
appBar: AppBar(
backgroundColor: Colors.transparent,
foregroundColor: AppPalette.ink,
elevation: 0,
centerTitle: true,
title: TitlePill(
_controller.scope == 'grade'
? '${widget.grade}학년 공부 랭킹'
: '전체 공부 랭킹',
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Row(
children: [
_scopeChip('grade', '${widget.grade}학년'),
const SizedBox(width: 8),
_scopeChip('all', '전체랭킹'),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
_periodChip('today', '오늘'),
const SizedBox(width: 8),
_periodChip('week', '이번주'),
const SizedBox(width: 8),
_periodChip('total', '전체'),
],
),
),
Expanded(
child: _controller.isLoading
? const Center(child: CircularProgressIndicator())
: ranking.isEmpty
? const Center(
child: Text(
'아직 기록이 없어요.\n공부 타이머를 눌러 1등에 도전해보세요.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
itemCount: ranking.length,
itemBuilder: (context, index) {
final r = ranking[index];
final bool isMe =
r['studentId'] == widget.myStudentId;
final int rank = (r['rank'] as num).toInt();
final medalColor = _rankColor(rank);
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isMe ? AppPalette.ink : AppPalette.paper,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isMe ? AppPalette.ink : AppPalette.sage,
),
),
child: Row(
children: [
SizedBox(
width: 32,
child: medalColor != null
? Icon(
Icons.emoji_events_rounded,
color: medalColor,
size: 22,
)
: Text(
'$rank',
style: TextStyle(
fontWeight: FontWeight.bold,
color: isMe
? Colors.white
: AppPalette.ink,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
r['name'] ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isMe
? Colors.white
: AppPalette.ink,
),
),
),
Text(
_formatHM((r['totalSeconds'] as num).toInt()),
style: TextStyle(
fontWeight: FontWeight.bold,
color: isMe ? Colors.white : AppPalette.ink,
),
),
],
),
);
},
),
),
],
),
);
},
);
}
Widget _periodChip(String value, String label) {
final bool selected = _controller.period == value;
return Expanded(
child: ChoiceChip(
label: Center(child: Text(label)),
selected: selected,
onSelected: (_) => _controller.setPeriod(value),
selectedColor: AppPalette.ink,
labelStyle: TextStyle(
color: selected ? Colors.white : AppPalette.ink,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _scopeChip(String value, String label) {
final bool selected = _controller.scope == value;
return Expanded(
child: ChoiceChip(
label: Center(child: Text(label, style: const TextStyle(fontSize: 13))),
selected: selected,
onSelected: (_) => _controller.setScope(value),
selectedColor: AppPalette.ink,
backgroundColor: AppPalette.mist,
labelStyle: TextStyle(
color: selected ? Colors.white : Colors.grey[600],
fontWeight: FontWeight.bold,
),
),
);
}
}