그룹 상세: 멤버를 타일 그리드로, 공부 시간순 정렬, 백품타 바로가기 추가

가로로 길게 늘어지던 멤버 목록을 학생 계정관리 화면 같은 타일
그리드로 바꾸고, 오늘 공부 시간이 많은 순으로 정렬(백엔드).
화면 폭도 다른 화면들처럼 최대 560으로 제한. "백품타 바로가기"
버튼으로 그룹 화면에서 바로 공부 타이머로 이동 가능.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 03:58:21 +00:00
co-authored by Claude Sonnet 5
parent 4f64315197
commit f5aa832ad9
3 changed files with 124 additions and 40 deletions
+119 -40
View File
@@ -5,18 +5,24 @@ import '../function/friend_controller.dart';
import '../function/group_detail_controller.dart'; import '../function/group_detail_controller.dart';
import '../theme/app_palette.dart'; import '../theme/app_palette.dart';
import 'app_notice.dart'; import 'app_notice.dart';
import 'launchpad_transition.dart';
import 'study_timer_screen.dart';
import 'title_pill.dart'; import 'title_pill.dart';
class GroupDetailScreen extends StatefulWidget { class GroupDetailScreen extends StatefulWidget {
final int groupId; final int groupId;
final String groupName; final String groupName;
final String studentId; final String studentId;
final String studentName;
final int? grade;
const GroupDetailScreen({ const GroupDetailScreen({
super.key, super.key,
required this.groupId, required this.groupId,
required this.groupName, required this.groupName,
required this.studentId, required this.studentId,
required this.studentName,
this.grade,
}); });
@override @override
@@ -208,6 +214,23 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
return '${m ~/ 60}시간 ${m % 60}분'; return '${m ~/ 60}시간 ${m % 60}분';
} }
void _openTimer() {
pushLaunchpad(
context,
(context) => StudyTimerScreen(
studentId: widget.studentId,
studentName: widget.studentName,
grade: widget.grade,
),
);
}
/// 넓은 화면(웹 데스크톱)에서 내용이 양옆으로 늘어지지 않게 가운데 최대 560 폭으로 맞춘다.
double _sidePad(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return ((width - 560) / 2).clamp(16.0, double.infinity);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder( return ListenableBuilder(
@@ -254,7 +277,12 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
: RefreshIndicator( : RefreshIndicator(
onRefresh: _controller.refresh, onRefresh: _controller.refresh,
child: ListView( child: ListView(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), padding: EdgeInsets.fromLTRB(
_sidePad(context),
0,
_sidePad(context),
90,
),
children: [ children: [
Center(child: TitlePill(widget.groupName)), Center(child: TitlePill(widget.groupName)),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -265,6 +293,23 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
Expanded(child: _statPill('오늘 공부함', attendedCount)), Expanded(child: _statPill('오늘 공부함', attendedCount)),
], ],
), ),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _openTimer,
icon: const Icon(Icons.timer_rounded),
label: const Text('백품타 바로가기'),
style: OutlinedButton.styleFrom(
foregroundColor: AppPalette.ink,
side: const BorderSide(color: AppPalette.sage),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
),
if (_controller.isOwner && if (_controller.isOwner &&
_controller.joinRequests.isNotEmpty) ...[ _controller.joinRequests.isNotEmpty) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -368,51 +413,85 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
for (final m in members) GridView.builder(
Container( shrinkWrap: true,
margin: const EdgeInsets.only(bottom: 8), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric( gridDelegate:
horizontal: 16, const SliverGridDelegateWithMaxCrossAxisExtent(
vertical: 12, maxCrossAxisExtent: 150,
), mainAxisSpacing: 10,
decoration: BoxDecoration( crossAxisSpacing: 10,
color: AppPalette.paper, mainAxisExtent: 96,
borderRadius: BorderRadius.circular(16), ),
border: Border.all(color: AppPalette.sage), itemCount: members.length,
), itemBuilder: (context, index) {
child: Row( final m = members[index];
children: [ final attended = m['attendedToday'] == true;
Icon( return Container(
m['attendedToday'] == true padding: const EdgeInsets.all(10),
? Icons.check_circle_rounded decoration: BoxDecoration(
: Icons.radio_button_unchecked_rounded, color: attended
color: m['attendedToday'] == true ? AppPalette.ink
: AppPalette.paper,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: attended
? AppPalette.ink ? AppPalette.ink
: Colors.grey[350], : AppPalette.sage,
size: 20,
), ),
const SizedBox(width: 10), ),
Expanded( child: Column(
child: Text( crossAxisAlignment: CrossAxisAlignment.start,
'${m['name']}' children: [
'${m['role'] == 'owner' ? ' (그룹장)' : ''}', Icon(
style: const TextStyle( attended
? Icons.check_circle_rounded
: Icons.radio_button_unchecked_rounded,
color: attended
? Colors.white
: Colors.grey[350],
size: 18,
),
const Spacer(),
Text(
'${m['name']}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13,
color: attended
? Colors.white
: AppPalette.ink,
), ),
), ),
), if (m['role'] == 'owner')
Text( Text(
_formatMinutes( '그룹장',
(m['todaySeconds'] as num?)?.toInt() ?? 0, style: TextStyle(
fontSize: 10,
color: attended
? Colors.white70
: Colors.grey[500],
),
),
const SizedBox(height: 2),
Text(
_formatMinutes(
(m['todaySeconds'] as num?)?.toInt() ?? 0,
),
style: TextStyle(
fontSize: 11,
color: attended
? Colors.white70
: Colors.grey[500],
),
), ),
style: TextStyle( ],
color: Colors.grey[500], ),
fontSize: 12, );
), },
), ),
],
),
),
], ],
), ),
), ),
+4
View File
@@ -11,11 +11,13 @@ import 'title_pill.dart';
class GroupListScreen extends StatefulWidget { class GroupListScreen extends StatefulWidget {
final String studentId; final String studentId;
final String studentName; final String studentName;
final int? grade;
const GroupListScreen({ const GroupListScreen({
super.key, super.key,
required this.studentId, required this.studentId,
required this.studentName, required this.studentName,
this.grade,
}); });
@override @override
@@ -127,6 +129,8 @@ class _GroupListScreenState extends State<GroupListScreen> {
groupId: group['groupId'] as int, groupId: group['groupId'] as int,
groupName: group['name'] as String, groupName: group['name'] as String,
studentId: widget.studentId, studentId: widget.studentId,
studentName: widget.studentName,
grade: widget.grade,
), ),
); );
_controller.refresh(); _controller.refresh();
+1
View File
@@ -394,6 +394,7 @@ class _MainDashboardState extends State<MainDashboard> {
(context) => GroupListScreen( (context) => GroupListScreen(
studentId: _displayId, studentId: _displayId,
studentName: _displayName, studentName: _displayName,
grade: widget.grade,
), ),
), ),
), ),