그룹 스터디 공개/비공개 기능 추가
그룹을 공개로 만들면 "공개 그룹" 목록에 뜨고, 학생이 참여 신청을 보내면 그룹장이 허용/거절할 수 있다. 그룹 상세 화면에 그룹장 전용 공개/비공개 전환 버튼과 대기 중인 참여 신청 목록을 추가. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,14 @@ class GroupController extends ChangeNotifier {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
List<dynamic> _groups = [];
|
List<dynamic> _groups = [];
|
||||||
List<dynamic> _invites = [];
|
List<dynamic> _invites = [];
|
||||||
|
List<dynamic> _publicGroups = [];
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
bool get isBusy => _isBusy;
|
bool get isBusy => _isBusy;
|
||||||
List<dynamic> get groups => _groups;
|
List<dynamic> get groups => _groups;
|
||||||
List<dynamic> get invites => _invites;
|
List<dynamic> get invites => _invites;
|
||||||
|
List<dynamic> get publicGroups => _publicGroups;
|
||||||
|
|
||||||
void _safeNotify() {
|
void _safeNotify() {
|
||||||
if (!_disposed) notifyListeners();
|
if (!_disposed) notifyListeners();
|
||||||
@@ -36,7 +38,7 @@ class GroupController extends ChangeNotifier {
|
|||||||
Future<void> refresh() async {
|
Future<void> refresh() async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_safeNotify();
|
_safeNotify();
|
||||||
await Future.wait([_fetchGroups(), _fetchInvites()]);
|
await Future.wait([_fetchGroups(), _fetchInvites(), _fetchPublicGroups()]);
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
_safeNotify();
|
_safeNotify();
|
||||||
}
|
}
|
||||||
@@ -69,7 +71,24 @@ class GroupController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(bool success, String message)> createGroup(String name) async {
|
Future<void> _fetchPublicGroups() async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('$baseUrl/api/groups/public?studentId=$studentId'),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
_publicGroups = data['groups'] ?? [];
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 조용히 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(bool success, String message)> createGroup(
|
||||||
|
String name, {
|
||||||
|
bool isPublic = false,
|
||||||
|
}) async {
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
_safeNotify();
|
_safeNotify();
|
||||||
try {
|
try {
|
||||||
@@ -80,6 +99,7 @@ class GroupController extends ChangeNotifier {
|
|||||||
"ownerId": studentId,
|
"ownerId": studentId,
|
||||||
"ownerName": studentName,
|
"ownerName": studentName,
|
||||||
"name": name.trim(),
|
"name": name.trim(),
|
||||||
|
"isPublic": isPublic,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
@@ -121,4 +141,27 @@ class GroupController extends ChangeNotifier {
|
|||||||
_safeNotify();
|
_safeNotify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<(bool success, String message)> requestJoin(int groupId) async {
|
||||||
|
_isBusy = true;
|
||||||
|
_safeNotify();
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$baseUrl/api/groups/$groupId/join-request'),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({"studentId": studentId, "studentName": studentName}),
|
||||||
|
);
|
||||||
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
if (response.statusCode == 200 && result['status'] == 'success') {
|
||||||
|
await _fetchPublicGroups();
|
||||||
|
return (true, '${result['message'] ?? '참여 신청을 보냈습니다.'}');
|
||||||
|
}
|
||||||
|
return (false, '${result['message'] ?? '참여 신청 실패'}');
|
||||||
|
} catch (e) {
|
||||||
|
return (false, '네트워크 에러: $e');
|
||||||
|
} finally {
|
||||||
|
_isBusy = false;
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,17 @@ class GroupDetailController extends ChangeNotifier {
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
List<dynamic> _members = [];
|
List<dynamic> _members = [];
|
||||||
|
List<dynamic> _joinRequests = [];
|
||||||
|
bool _isPublic = false;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
bool get isLoading => _isLoading;
|
bool get isLoading => _isLoading;
|
||||||
bool get isBusy => _isBusy;
|
bool get isBusy => _isBusy;
|
||||||
List<dynamic> get members => _members;
|
List<dynamic> get members => _members;
|
||||||
|
List<dynamic> get joinRequests => _joinRequests;
|
||||||
|
bool get isPublic => _isPublic;
|
||||||
|
bool get isOwner =>
|
||||||
|
_members.any((m) => m['studentId'] == studentId && m['role'] == 'owner');
|
||||||
|
|
||||||
void _safeNotify() {
|
void _safeNotify() {
|
||||||
if (!_disposed) notifyListeners();
|
if (!_disposed) notifyListeners();
|
||||||
@@ -41,11 +47,77 @@ class GroupDetailController extends ChangeNotifier {
|
|||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
_members = data['members'] ?? [];
|
_members = data['members'] ?? [];
|
||||||
|
_isPublic = data['isPublic'] == true;
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// 조용히 무시
|
// 조용히 무시
|
||||||
|
}
|
||||||
|
if (isOwner) await _fetchJoinRequests();
|
||||||
|
_isLoading = false;
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchJoinRequests() async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse(
|
||||||
|
'$baseUrl/api/groups/$groupId/join-requests?ownerId=$studentId',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
_joinRequests = data['requests'] ?? [];
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 조용히 무시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(bool success, String message)> respondJoinRequest(
|
||||||
|
int requestId,
|
||||||
|
bool accept,
|
||||||
|
) async {
|
||||||
|
_isBusy = true;
|
||||||
|
_safeNotify();
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$baseUrl/api/groups/join-requests/$requestId/respond'),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({"responderId": studentId, "accept": accept}),
|
||||||
|
);
|
||||||
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
if (response.statusCode == 200 && result['status'] == 'success') {
|
||||||
|
await refresh();
|
||||||
|
return (true, '${result['message'] ?? '처리했습니다.'}');
|
||||||
|
}
|
||||||
|
return (false, '${result['message'] ?? '처리 실패'}');
|
||||||
|
} catch (e) {
|
||||||
|
return (false, '네트워크 에러: $e');
|
||||||
} finally {
|
} finally {
|
||||||
_isLoading = false;
|
_isBusy = false;
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(bool success, String message)> setVisibility(bool isPublic) async {
|
||||||
|
_isBusy = true;
|
||||||
|
_safeNotify();
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$baseUrl/api/groups/$groupId/visibility'),
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({"ownerId": studentId, "isPublic": isPublic}),
|
||||||
|
);
|
||||||
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
if (response.statusCode == 200 && result['status'] == 'success') {
|
||||||
|
await refresh();
|
||||||
|
return (true, '${result['message'] ?? '변경했습니다.'}');
|
||||||
|
}
|
||||||
|
return (false, '${result['message'] ?? '변경 실패'}');
|
||||||
|
} catch (e) {
|
||||||
|
return (false, '네트워크 에러: $e');
|
||||||
|
} finally {
|
||||||
|
_isBusy = false;
|
||||||
_safeNotify();
|
_safeNotify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,48 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
|
|||||||
friendController.dispose();
|
friendController.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleVisibility() async {
|
||||||
|
final makePublic = !_controller.isPublic;
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: Text(makePublic ? '공개 그룹으로 전환' : '비공개 그룹으로 전환'),
|
||||||
|
content: Text(
|
||||||
|
makePublic
|
||||||
|
? '이제 누구나 이 그룹을 찾아서 참여 신청을 보낼 수 있어요. 참여는 그룹장이 허용해야 완료됩니다.'
|
||||||
|
: '더 이상 공개 목록에 뜨지 않고, 대기 중인 참여 신청은 모두 거절 처리됩니다.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
|
child: const Text('취소', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppPalette.ink,
|
||||||
|
foregroundColor: AppPalette.paper,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
|
child: const Text('전환'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
final (_, message) = await _controller.setVisibility(makePublic);
|
||||||
|
if (!mounted) return;
|
||||||
|
AppNotice.show(context, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _respondJoinRequest(int requestId, bool accept) async {
|
||||||
|
final (_, message) = await _controller.respondJoinRequest(
|
||||||
|
requestId,
|
||||||
|
accept,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
AppNotice.show(context, message);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _leave() async {
|
Future<void> _leave() async {
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -183,6 +225,16 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
|
|||||||
foregroundColor: AppPalette.ink,
|
foregroundColor: AppPalette.ink,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
actions: [
|
||||||
|
if (_controller.isOwner)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_controller.isPublic
|
||||||
|
? Icons.public_rounded
|
||||||
|
: Icons.lock_rounded,
|
||||||
|
),
|
||||||
|
tooltip: _controller.isPublic ? '공개 그룹' : '비공개 그룹',
|
||||||
|
onPressed: _toggleVisibility,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.exit_to_app_rounded),
|
icon: const Icon(Icons.exit_to_app_rounded),
|
||||||
tooltip: '그룹 나가기',
|
tooltip: '그룹 나가기',
|
||||||
@@ -213,6 +265,86 @@ class _GroupDetailScreenState extends State<GroupDetailScreen> {
|
|||||||
Expanded(child: _statPill('오늘 공부함', attendedCount)),
|
Expanded(child: _statPill('오늘 공부함', attendedCount)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (_controller.isOwner &&
|
||||||
|
_controller.joinRequests.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 4,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppPalette.ink,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'대기 중인 참여 신청',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppPalette.ink,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'${_controller.joinRequests.length}명',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
for (final r in _controller.joinRequests)
|
||||||
|
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: () => _respondJoinRequest(
|
||||||
|
r['requestId'] as int,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'거절',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppPalette.ink,
|
||||||
|
foregroundColor: AppPalette.paper,
|
||||||
|
),
|
||||||
|
onPressed: () => _respondJoinRequest(
|
||||||
|
r['requestId'] as int,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
child: const Text('허용'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
+168
-28
@@ -43,37 +43,58 @@ class _GroupListScreenState extends State<GroupListScreen> {
|
|||||||
|
|
||||||
Future<void> _showCreateGroupDialog() async {
|
Future<void> _showCreateGroupDialog() async {
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
|
bool isPublic = false;
|
||||||
final result = await showDialog<bool>(
|
final result = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => StatefulBuilder(
|
||||||
title: const Text('그룹 만들기'),
|
builder: (dialogContext, setDialogState) => AlertDialog(
|
||||||
content: TextField(
|
title: const Text('그룹 만들기'),
|
||||||
controller: nameController,
|
content: Column(
|
||||||
maxLength: 20,
|
mainAxisSize: MainAxisSize.min,
|
||||||
decoration: const InputDecoration(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
labelText: '그룹 이름',
|
children: [
|
||||||
border: OutlineInputBorder(),
|
TextField(
|
||||||
|
controller: nameController,
|
||||||
|
maxLength: 20,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '그룹 이름',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('공개 그룹으로 만들기'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'누구나 목록에서 보고 참여 신청할 수 있어요\n(그룹장이 허용해야 들어옵니다)',
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: isPublic,
|
||||||
|
activeThumbColor: AppPalette.ink,
|
||||||
|
onChanged: (v) => setDialogState(() => isPublic = v),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
actions: [
|
||||||
actions: [
|
TextButton(
|
||||||
TextButton(
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
onPressed: () => Navigator.pop(dialogContext, false),
|
child: const Text('취소', style: TextStyle(color: Colors.grey)),
|
||||||
child: const Text('취소', style: TextStyle(color: Colors.grey)),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppPalette.ink,
|
|
||||||
foregroundColor: AppPalette.paper,
|
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.pop(dialogContext, true),
|
ElevatedButton(
|
||||||
child: const Text('만들기'),
|
style: ElevatedButton.styleFrom(
|
||||||
),
|
backgroundColor: AppPalette.ink,
|
||||||
],
|
foregroundColor: AppPalette.paper,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
|
child: const Text('만들기'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (result != true || nameController.text.trim().isEmpty) return;
|
if (result != true || nameController.text.trim().isEmpty) return;
|
||||||
final (success, message) = await _controller.createGroup(
|
final (success, message) = await _controller.createGroup(
|
||||||
nameController.text.trim(),
|
nameController.text.trim(),
|
||||||
|
isPublic: isPublic,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
AppNotice.show(
|
AppNotice.show(
|
||||||
@@ -89,6 +110,16 @@ class _GroupListScreenState extends State<GroupListScreen> {
|
|||||||
AppNotice.show(context, message);
|
AppNotice.show(context, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _requestJoin(int groupId) async {
|
||||||
|
final (success, message) = await _controller.requestJoin(groupId);
|
||||||
|
if (!mounted) return;
|
||||||
|
AppNotice.show(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
icon: success ? Icons.check_circle_rounded : Icons.error_outline_rounded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _openGroup(dynamic group) async {
|
Future<void> _openGroup(dynamic group) async {
|
||||||
await pushLaunchpad(
|
await pushLaunchpad(
|
||||||
context,
|
context,
|
||||||
@@ -268,12 +299,54 @@ class _GroupListScreenState extends State<GroupListScreen> {
|
|||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
'${g['name']}',
|
children: [
|
||||||
style: const TextStyle(
|
Flexible(
|
||||||
fontWeight: FontWeight.bold,
|
child: Text(
|
||||||
fontSize: 15,
|
'${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(
|
Text(
|
||||||
'멤버 ${g['memberCount']}명'
|
'멤버 ${g['memberCount']}명'
|
||||||
@@ -294,6 +367,73 @@ class _GroupListScreenState extends State<GroupListScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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('참여 신청'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user