diff --git a/lib/ui/friends_screen.dart b/lib/ui/friends_screen.dart index 2560e5d..00603cb 100644 --- a/lib/ui/friends_screen.dart +++ b/lib/ui/friends_screen.dart @@ -163,123 +163,133 @@ class _FriendsScreenState extends State { ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) - : RefreshIndicator( - onRefresh: _controller.refresh, - child: ListView( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), - children: [ - const Center(child: TitlePill('친구')), - const SizedBox(height: 20), - if (_controller.requests.isNotEmpty) ...[ - _sectionHeader( - '받은 친구 신청', - count: _controller.requests.length, - ), - const SizedBox(height: 10), - for (final r in _controller.requests) - Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + : Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: RefreshIndicator( + onRefresh: _controller.refresh, + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), + children: [ + const Center(child: TitlePill('친구')), + const SizedBox(height: 20), + if (_controller.requests.isNotEmpty) ...[ + _sectionHeader( + '받은 친구 신청', + count: _controller.requests.length, ), - decoration: BoxDecoration( - color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppPalette.sage), - ), - child: Row( - children: [ - Expanded( - child: Text( - '${r['name']} (${r['studentId']})', - style: const TextStyle( - fontWeight: FontWeight.bold, + const SizedBox(height: 10), + for (final r in _controller.requests) + Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), + ), + child: Row( + children: [ + Expanded( + child: Text( + '${r['name']} (${r['studentId']})', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), ), - ), - ), - TextButton( - onPressed: () => - _respond(r['requestId'] as int, false), - child: const Text( - '거절', - style: TextStyle(color: Colors.grey), - ), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppPalette.ink, - foregroundColor: AppPalette.paper, - ), - onPressed: () => - _respond(r['requestId'] as int, true), - child: const Text('수락'), - ), - ], - ), - ), - const SizedBox(height: 20), - ], - _sectionHeader('내 친구', count: _controller.friends.length), - const SizedBox(height: 10), - if (_controller.friends.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Text( - '아직 친구가 없어요. 학번으로 친구를 추가해보세요.', - style: TextStyle(color: Colors.grey), - ), - ) - else - for (final f in _controller.friends) - Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppPalette.sage), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: AppPalette.ink.withValues( - alpha: 0.06, + TextButton( + onPressed: () => _respond( + r['requestId'] as int, + false, + ), + child: const Text( + '거절', + style: TextStyle(color: Colors.grey), + ), ), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.person_rounded, - size: 16, - color: AppPalette.ink, - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - '${f['name']} (${f['studentId']})', - style: const TextStyle( - fontWeight: FontWeight.bold, + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppPalette.ink, + foregroundColor: AppPalette.paper, + ), + onPressed: () => + _respond(r['requestId'] as int, true), + child: const Text('수락'), ), - ), + ], ), - IconButton( - icon: const Icon( - Icons.person_remove_outlined, - color: Colors.grey, - ), - onPressed: () => - _remove(f['studentId'] as String), - ), - ], - ), + ), + const SizedBox(height: 20), + ], + _sectionHeader( + '내 친구', + count: _controller.friends.length, ), - ], + const SizedBox(height: 10), + if (_controller.friends.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + '아직 친구가 없어요. 학번으로 친구를 추가해보세요.', + style: TextStyle(color: Colors.grey), + ), + ) + else + for (final f in _controller.friends) + Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: AppPalette.ink.withValues( + alpha: 0.06, + ), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.person_rounded, + size: 16, + color: AppPalette.ink, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + '${f['name']} (${f['studentId']})', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon( + Icons.person_remove_outlined, + color: Colors.grey, + ), + onPressed: () => + _remove(f['studentId'] as String), + ), + ], + ), + ), + ], + ), + ), ), ), ); diff --git a/lib/ui/group_list_screen.dart b/lib/ui/group_list_screen.dart index ad06f90..ec21b8e 100644 --- a/lib/ui/group_list_screen.dart +++ b/lib/ui/group_list_screen.dart @@ -188,257 +188,273 @@ class _GroupListScreenState extends State { ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) - : RefreshIndicator( - onRefresh: _controller.refresh, - child: ListView( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), - children: [ - const Center(child: TitlePill('그룹 스터디')), - const SizedBox(height: 20), - if (_controller.invites.isNotEmpty) ...[ - _sectionHeader( - '받은 그룹 초대', - count: _controller.invites.length, - ), - const SizedBox(height: 10), - for (final inv in _controller.invites) - Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + : Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: RefreshIndicator( + onRefresh: _controller.refresh, + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), + children: [ + const Center(child: TitlePill('그룹 스터디')), + const SizedBox(height: 20), + if (_controller.invites.isNotEmpty) ...[ + _sectionHeader( + '받은 그룹 초대', + count: _controller.invites.length, ), - decoration: BoxDecoration( - color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppPalette.sage), - ), - child: Row( - children: [ - Expanded( - child: Text( - '${inv['groupName']}', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), + const SizedBox(height: 10), + for (final inv in _controller.invites) + Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, ), - TextButton( - onPressed: () => _respondInvite( - inv['inviteId'] as int, - false, - ), - child: const Text( - '거절', - style: TextStyle(color: Colors.grey), - ), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppPalette.ink, - foregroundColor: AppPalette.paper, - ), - onPressed: () => _respondInvite( - inv['inviteId'] as int, - true, - ), - child: const Text('참여'), - ), - ], - ), - ), - const SizedBox(height: 20), - ], - _sectionHeader('내 그룹', count: _controller.groups.length), - const SizedBox(height: 10), - if (_controller.groups.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: Text( - '아직 그룹이 없어요. 새 그룹을 만들거나 초대를 기다려보세요.', - style: TextStyle(color: Colors.grey), - ), - ) - else - for (final g in _controller.groups) - GestureDetector( - onTap: () => _openGroup(g), - child: Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - decoration: BoxDecoration( - color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppPalette.sage), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.03), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppPalette.ink.withValues( - alpha: 0.06, - ), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.groups_rounded, - color: AppPalette.ink, - size: 18, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Flexible( - child: Text( - '${g['name']}', - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), - ), - ), - if (g['isPublic'] == true) ...[ - const SizedBox(width: 6), - const Icon( - Icons.public_rounded, - size: 14, - color: Colors.grey, - ), - ], - if ((g['pendingRequestCount'] ?? - 0) > - 0) ...[ - const SizedBox(width: 6), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: 6, - vertical: 1, - ), - decoration: BoxDecoration( - color: Colors.red[50], - borderRadius: - BorderRadius.circular(20), - border: Border.all( - color: Colors.red, - ), - ), - child: Text( - '대기 ${g['pendingRequestCount']}', - style: const TextStyle( - fontSize: 10, - color: Colors.red, - ), - ), - ), - ], - ], - ), - Text( - '멤버 ${g['memberCount']}명' - '${g['isOwner'] == true ? ' · 그룹장' : ''}', - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - ), - ), - ], - ), - ), - const Icon( - Icons.chevron_right_rounded, - color: Colors.grey, - ), - ], - ), - ), - ), - if (_controller.publicGroups.isNotEmpty) ...[ - const SizedBox(height: 20), - _sectionHeader( - '공개 그룹', - count: _controller.publicGroups.length, - ), - const SizedBox(height: 10), - for (final g in _controller.publicGroups) - Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: AppPalette.paper, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppPalette.sage), - ), - child: Row( - children: [ - const Icon( - Icons.public_rounded, - color: AppPalette.ink, - size: 18, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - '${g['name']}', + child: Row( + children: [ + Expanded( + child: Text( + '${inv['groupName']}', style: const TextStyle( fontWeight: FontWeight.bold, ), ), - Text( - '${g['ownerName']} · 멤버 ${g['memberCount']}명', - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], + ), + TextButton( + onPressed: () => _respondInvite( + inv['inviteId'] as int, + false, + ), + child: const Text( + '거절', + style: TextStyle(color: Colors.grey), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppPalette.ink, + foregroundColor: AppPalette.paper, + ), + onPressed: () => _respondInvite( + inv['inviteId'] as int, + true, + ), + child: const Text('참여'), + ), + ], + ), + ), + const SizedBox(height: 20), + ], + _sectionHeader( + '내 그룹', + count: _controller.groups.length, + ), + const SizedBox(height: 10), + if (_controller.groups.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + '아직 그룹이 없어요. 새 그룹을 만들거나 초대를 기다려보세요.', + style: TextStyle(color: Colors.grey), + ), + ) + else + for (final g in _controller.groups) + GestureDetector( + onTap: () => _openGroup(g), + child: Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues( + alpha: 0.03, ), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppPalette.ink.withValues( + alpha: 0.06, + ), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.groups_rounded, + color: AppPalette.ink, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + '${g['name']}', + overflow: + TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: + FontWeight.bold, + fontSize: 15, + ), + ), + ), + if (g['isPublic'] == true) ...[ + const SizedBox(width: 6), + const Icon( + Icons.public_rounded, + size: 14, + color: Colors.grey, + ), + ], + if ((g['pendingRequestCount'] ?? + 0) > + 0) ...[ + const SizedBox(width: 6), + Container( + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 1, + ), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: + BorderRadius.circular( + 20, + ), + border: Border.all( + color: Colors.red, + ), + ), + child: Text( + '대기 ${g['pendingRequestCount']}', + style: const TextStyle( + fontSize: 10, + color: Colors.red, + ), + ), + ), + ], + ], + ), + Text( + '멤버 ${g['memberCount']}명' + '${g['isOwner'] == true ? ' · 그룹장' : ''}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right_rounded, + color: Colors.grey, ), ], ), ), - if (g['requested'] == true) - Text( - '신청됨', - style: TextStyle(color: Colors.grey[500]), - ) - else - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppPalette.ink, - foregroundColor: AppPalette.paper, - ), - onPressed: () => - _requestJoin(g['groupId'] as int), - child: const Text('참여 신청'), - ), - ], + ), + if (_controller.publicGroups.isNotEmpty) ...[ + const SizedBox(height: 20), + _sectionHeader( + '공개 그룹', + count: _controller.publicGroups.length, ), - ), - ], - ], + const SizedBox(height: 10), + for (final g in _controller.publicGroups) + Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: AppPalette.paper, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppPalette.sage), + ), + child: Row( + children: [ + const Icon( + Icons.public_rounded, + color: AppPalette.ink, + size: 18, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '${g['name']}', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Text( + '${g['ownerName']} · 멤버 ${g['memberCount']}명', + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + ), + ], + ), + ), + if (g['requested'] == true) + Text( + '신청됨', + style: TextStyle( + color: Colors.grey[500], + ), + ) + else + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppPalette.ink, + foregroundColor: AppPalette.paper, + ), + onPressed: () => + _requestJoin(g['groupId'] as int), + child: const Text('참여 신청'), + ), + ], + ), + ), + ], + ], + ), + ), ), ), ); diff --git a/lib/ui/study_ranking_screen.dart b/lib/ui/study_ranking_screen.dart index 3b81fad..7e4262e 100644 --- a/lib/ui/study_ranking_screen.dart +++ b/lib/ui/study_ranking_screen.dart @@ -76,115 +76,128 @@ class _StudyRankingScreenState extends State { : '전체 공부 랭킹', ), ), - 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), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: 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: 12, + vertical: 8, ), - 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, - ), - ), + 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, ), - 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, + 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, + ), + ), + ], ), - Text( - _formatHM((r['totalSeconds'] as num).toInt()), - style: TextStyle( - fontWeight: FontWeight.bold, - color: isMe ? Colors.white : AppPalette.ink, - ), - ), - ], - ), - ); - }, - ), + ); + }, + ), + ), + ], ), - ], + ), ), ); },