친구/그룹 스터디/백품타 랭킹 화면 폭 제한 (양옆으로 길게 늘어지던 것 수정)

백품타 랭킹, 그룹 스터디 목록은 최대 560px로(기존 대비 절반 느낌),
친구 목록은 최대 400px로(더 좁게, 1/3 느낌) 가운데 정렬되게 제한.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 06:10:30 +00:00
co-authored by Claude Sonnet 5
parent ae3a3f18ea
commit 153c341efc
3 changed files with 490 additions and 451 deletions
+121 -111
View File
@@ -163,123 +163,133 @@ class _FriendsScreenState extends State<FriendsScreen> {
), ),
body: _controller.isLoading body: _controller.isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: RefreshIndicator( : Center(
onRefresh: _controller.refresh, child: ConstrainedBox(
child: ListView( constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), child: RefreshIndicator(
children: [ onRefresh: _controller.refresh,
const Center(child: TitlePill('친구')), child: ListView(
const SizedBox(height: 20), padding: const EdgeInsets.fromLTRB(16, 0, 16, 90),
if (_controller.requests.isNotEmpty) ...[ children: [
_sectionHeader( const Center(child: TitlePill('친구')),
'받은 친구 신청', const SizedBox(height: 20),
count: _controller.requests.length, if (_controller.requests.isNotEmpty) ...[
), _sectionHeader(
const SizedBox(height: 10), '받은 친구 신청',
for (final r in _controller.requests) count: _controller.requests.length,
Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
), ),
decoration: BoxDecoration( const SizedBox(height: 10),
color: AppPalette.paper, for (final r in _controller.requests)
borderRadius: BorderRadius.circular(16), Container(
border: Border.all(color: AppPalette.sage), margin: const EdgeInsets.only(bottom: 8),
), padding: const EdgeInsets.symmetric(
child: Row( horizontal: 16,
children: [ vertical: 12,
Expanded( ),
child: Text( decoration: BoxDecoration(
'${r['name']} (${r['studentId']})', color: AppPalette.paper,
style: const TextStyle( borderRadius: BorderRadius.circular(16),
fontWeight: FontWeight.bold, 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(
TextButton( r['requestId'] as int,
onPressed: () => false,
_respond(r['requestId'] as int, false), ),
child: const Text( child: const Text(
'거절', '거절',
style: TextStyle(color: Colors.grey), 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,
), ),
shape: BoxShape.circle, ElevatedButton(
), style: ElevatedButton.styleFrom(
child: const Icon( backgroundColor: AppPalette.ink,
Icons.person_rounded, foregroundColor: AppPalette.paper,
size: 16, ),
color: AppPalette.ink, onPressed: () =>
), _respond(r['requestId'] as int, true),
), child: const Text('수락'),
const SizedBox(width: 10),
Expanded(
child: Text(
'${f['name']} (${f['studentId']})',
style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
), ],
), ),
IconButton( ),
icon: const Icon( const SizedBox(height: 20),
Icons.person_remove_outlined, ],
color: Colors.grey, _sectionHeader(
), '내 친구',
onPressed: () => count: _controller.friends.length,
_remove(f['studentId'] as String),
),
],
),
), ),
], 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),
),
],
),
),
],
),
),
), ),
), ),
); );
+254 -238
View File
@@ -188,257 +188,273 @@ class _GroupListScreenState extends State<GroupListScreen> {
), ),
body: _controller.isLoading body: _controller.isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: RefreshIndicator( : Center(
onRefresh: _controller.refresh, child: ConstrainedBox(
child: ListView( constraints: const BoxConstraints(maxWidth: 560),
padding: const EdgeInsets.fromLTRB(16, 0, 16, 90), child: RefreshIndicator(
children: [ onRefresh: _controller.refresh,
const Center(child: TitlePill('그룹 스터디')), child: ListView(
const SizedBox(height: 20), padding: const EdgeInsets.fromLTRB(16, 0, 16, 90),
if (_controller.invites.isNotEmpty) ...[ children: [
_sectionHeader( const Center(child: TitlePill('그룹 스터디')),
'받은 그룹 초대', const SizedBox(height: 20),
count: _controller.invites.length, if (_controller.invites.isNotEmpty) ...[
), _sectionHeader(
const SizedBox(height: 10), '받은 그룹 초대',
for (final inv in _controller.invites) count: _controller.invites.length,
Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
), ),
decoration: BoxDecoration( const SizedBox(height: 10),
color: AppPalette.paper, for (final inv in _controller.invites)
borderRadius: BorderRadius.circular(16), Container(
border: Border.all(color: AppPalette.sage), margin: const EdgeInsets.only(bottom: 8),
), padding: const EdgeInsets.symmetric(
child: Row( horizontal: 16,
children: [ vertical: 12,
Expanded(
child: Text(
'${inv['groupName']}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
), ),
TextButton( decoration: BoxDecoration(
onPressed: () => _respondInvite( color: AppPalette.paper,
inv['inviteId'] as int, borderRadius: BorderRadius.circular(16),
false, border: Border.all(color: AppPalette.sage),
),
child: const Text(
'거절',
style: TextStyle(color: Colors.grey),
),
), ),
ElevatedButton( child: Row(
style: ElevatedButton.styleFrom( children: [
backgroundColor: AppPalette.ink, Expanded(
foregroundColor: AppPalette.paper, child: Text(
), '${inv['groupName']}',
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']}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text( ),
'${g['ownerName']} · 멤버 ${g['memberCount']}명', TextButton(
style: TextStyle( onPressed: () => _respondInvite(
fontSize: 12, inv['inviteId'] as int,
color: Colors.grey[500], 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( if (_controller.publicGroups.isNotEmpty) ...[
'신청됨', const SizedBox(height: 20),
style: TextStyle(color: Colors.grey[500]), _sectionHeader(
) '공개 그룹',
else count: _controller.publicGroups.length,
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppPalette.ink,
foregroundColor: AppPalette.paper,
),
onPressed: () =>
_requestJoin(g['groupId'] as int),
child: const Text('참여 신청'),
),
],
), ),
), 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('참여 신청'),
),
],
),
),
],
],
),
),
), ),
), ),
); );
+115 -102
View File
@@ -76,115 +76,128 @@ class _StudyRankingScreenState extends State<StudyRankingScreen> {
: '전체 공부 랭킹', : '전체 공부 랭킹',
), ),
), ),
body: Column( body: Center(
children: [ child: ConstrainedBox(
Padding( constraints: const BoxConstraints(maxWidth: 560),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Column(
child: Row( children: [
children: [ Padding(
_scopeChip('grade', '${widget.grade}학년'), padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
const SizedBox(width: 8), child: Row(
_scopeChip('all', '전체랭킹'), children: [
], _scopeChip('grade', '${widget.grade}학년'),
), const SizedBox(width: 8),
), _scopeChip('all', '전체랭킹'),
Padding( ],
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), ),
child: Row( ),
children: [ Padding(
_periodChip('today', '오늘'), padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
const SizedBox(width: 8), child: Row(
_periodChip('week', '이번주'), children: [
const SizedBox(width: 8), _periodChip('today', '오늘'),
_periodChip('total', '전체'), const SizedBox(width: 8),
], _periodChip('week', '이번주'),
), const SizedBox(width: 8),
), _periodChip('total', '전체'),
Expanded( ],
child: _controller.isLoading ),
? const Center(child: CircularProgressIndicator()) ),
: ranking.isEmpty Expanded(
? const Center( child: _controller.isLoading
child: Text( ? const Center(child: CircularProgressIndicator())
'아직 기록이 없어요.\n공부 타이머를 눌러 1등에 도전해보세요.', : ranking.isEmpty
textAlign: TextAlign.center, ? const Center(
style: TextStyle(color: Colors.grey), child: Text(
), '아직 기록이 없어요.\n공부 타이머를 눌러 1등에 도전해보세요.',
) textAlign: TextAlign.center,
: ListView.builder( style: TextStyle(color: Colors.grey),
padding: const EdgeInsets.symmetric( ),
horizontal: 16, )
vertical: 8, : ListView.builder(
),
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( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 12, vertical: 8,
), ),
decoration: BoxDecoration( itemCount: ranking.length,
color: isMe ? AppPalette.ink : AppPalette.paper, itemBuilder: (context, index) {
borderRadius: BorderRadius.circular(14), final r = ranking[index];
border: Border.all( final bool isMe =
color: isMe ? AppPalette.ink : AppPalette.sage, r['studentId'] == widget.myStudentId;
), final int rank = (r['rank'] as num).toInt();
), final medalColor = _rankColor(rank);
child: Row(
children: [ return Container(
SizedBox( margin: const EdgeInsets.only(bottom: 8),
width: 32, padding: const EdgeInsets.symmetric(
child: medalColor != null horizontal: 16,
? Icon( vertical: 12,
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), decoration: BoxDecoration(
Expanded( color: isMe
child: Text( ? AppPalette.ink
r['name'] ?? '', : AppPalette.paper,
maxLines: 1, borderRadius: BorderRadius.circular(14),
overflow: TextOverflow.ellipsis, border: Border.all(
style: TextStyle( color: isMe
fontWeight: FontWeight.bold, ? AppPalette.ink
color: isMe : AppPalette.sage,
? Colors.white ),
: AppPalette.ink, ),
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, ],
),
),
],
),
);
},
),
), ),
], ),
), ),
); );
}, },