From 503851145dc12477ee450816badfe0068f641169 Mon Sep 17 00:00:00 2001 From: kimjihyun Date: Wed, 11 Feb 2026 15:14:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B0=99=EC=9D=B4=EB=B0=A9=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=EC=9E=90=20=EA=B4=80=EB=A6=AC=20(#424)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wedu/management_checklist_screen.dart | 498 ++++++++++++++++++ lib/screens/wedu/wedu_detail_screen.dart | 34 ++ 2 files changed, 532 insertions(+) create mode 100644 lib/screens/wedu/management_checklist_screen.dart diff --git a/lib/screens/wedu/management_checklist_screen.dart b/lib/screens/wedu/management_checklist_screen.dart new file mode 100644 index 0000000..3afd393 --- /dev/null +++ b/lib/screens/wedu/management_checklist_screen.dart @@ -0,0 +1,498 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:http/http.dart' as http; +import 'package:peeroreum_client/api/PeeroreumApi.dart'; +import 'package:peeroreum_client/designs/PeeroreumColor.dart'; +import 'package:peeroreum_client/designs/PeeroreumTypo.dart'; +import 'package:get/get.dart'; + +class ManagementCheckList extends StatefulWidget { + ManagementCheckList(this.memberList, this.title, this.id); + List memberList; + String title; + int id; + + @override + State createState() => + _ManagementCheckListState(memberList, title, id); +} + +class _ManagementCheckListState extends State { + _ManagementCheckListState(this.memberList, this.title, this.id); + List memberList; + String title; + int id; + + List selectedUserList = []; + late List isCheckedList = + List.generate(memberList.length, (index) => false); + late List isActiveList = + List.generate(memberList.length, (index) => true); + + var myNickname; + bool isSelectAll = false; + + @override + void initState() { + super.initState(); + selectedUserList = []; + fetchStatus(); + } + + bool controlAllSelect() { + bool hasUncheckedActiveItem = false; + for (int i = 0; i < isCheckedList.length; i++) { + if (isActiveList[i] == true && isCheckedList[i] == false) { + hasUncheckedActiveItem = true; + break; + } + } + if (hasUncheckedActiveItem) { + return false; + } else { + return true; + } + } + + fetchStatus() async { + myNickname = await FlutterSecureStorage().read(key: "nickname"); + + setState(() { + for (int i = 0; i < memberList.length; i++) { + if (memberList[i]['nickname'] == myNickname) { + isActiveList[i] = false; + } else { + isActiveList[i] = true; + } + } + }); + } + + void kickMember(List nicknameList) async { + String? token = await FlutterSecureStorage().read(key: "accessToken"); + + var result = await http.put( + Uri.parse('${API.hostConnect}/wedu/$id/dismiss'), // TODO: 실제 API로 변경 + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' + }, + body: jsonEncode({"nickname": nicknameList}), + ); + + if (result.statusCode == 200) { + print("참여 종료 성공"); + Fluttertoast.showToast(msg: "선택한 참여자의 참여를 종료했습니다."); + setState(() { + for (var nickname in nicknameList) { + memberList.removeWhere((member) => member['nickname'] == nickname); + } + isCheckedList = List.generate(memberList.length, (index) => false); + isSelectAll = false; + selectedUserList.clear(); + }); + } else { + print("참여 종료 실패 ${result.statusCode} : ${result.body}"); + Fluttertoast.showToast(msg: "오류가 발생했습니다. 다시 시도해 주세요."); + } + } + + showKickDialog() { + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + insetPadding: EdgeInsets.symmetric(horizontal: 20), + contentPadding: EdgeInsets.all(20), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + backgroundColor: PeeroreumColor.white, + surfaceTintColor: Colors.transparent, + content: SizedBox( + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "참여 종료", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Pretendard', + fontSize: 20, + fontWeight: FontWeight.w600, + color: PeeroreumColor.black, + ), + ), + SizedBox(height: 8), + Text( + "이 사용자의 같이방 참여를 종료할까요?", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Pretendard', + fontSize: 14, + fontWeight: FontWeight.w400, + color: PeeroreumColor.gray[600], + ), + ), + SizedBox( + height: 4, + ), + Text( + "종료 후에도 다시 초대할 수 있어요.", + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'Pretendard', + fontSize: 14, + fontWeight: FontWeight.w400, + color: PeeroreumColor.gray[600], + ), + ), + SizedBox( + height: 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButton( + onPressed: () { + Get.back(); + }, + style: TextButton.styleFrom( + backgroundColor: PeeroreumColor.gray[300], + padding: EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + '취소', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w600, + fontSize: 16, + color: PeeroreumColor.gray[600]), + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: TextButton( + onPressed: () async { + print("강퇴할 유저 목록: $selectedUserList"); + Fluttertoast.showToast(msg: '선택한 참여자의 참여를 종료했습니다.'); + Get.back(); + kickMember(selectedUserList); + }, + style: TextButton.styleFrom( + backgroundColor: PeeroreumColor.error, + padding: EdgeInsets.symmetric( + vertical: 12, horizontal: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + '확인', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w600, + fontSize: 16, + color: PeeroreumColor.white), + ), + ), + ), + ], + ) + ], + ), + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: PeeroreumColor.white, + appBar: AppBar( + elevation: 0, + backgroundColor: PeeroreumColor.white, + leading: IconButton( + icon: SvgPicture.asset( + 'assets/icons/x.svg', + color: PeeroreumColor.gray[800], + width: 18, + ), + onPressed: () { + Get.back(); + }, + ), + title: Text( + '참여자 관리', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w500, + fontSize: 20, + color: PeeroreumColor.black), + ), + centerTitle: true, + actions: [ + IconButton( + onPressed: () {}, + icon: SvgPicture.asset( + 'assets/icons/icon_dots_mono.svg', + color: PeeroreumColor.gray[800], + width: 24, + )) + ], + ), + body: FutureBuilder( + future: fetchStatus(), + builder: (context, snapshot) { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + '전체', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w500, + fontSize: 14, + color: PeeroreumColor.gray[500]), + ), + SizedBox( + width: 4, + ), + Text( + '${memberList.length}', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w500, + fontSize: 14, + color: PeeroreumColor.gray[500]), + ), + SizedBox( + width: 2, + ), + Text( + '명', + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w500, + fontSize: 14, + color: PeeroreumColor.gray[500]), + ), + ], + ), + InkWell( + splashColor: Colors.transparent, + highlightColor: PeeroreumColor.gray[100], + onTap: () { + if (isSelectAll == false) { + for (int i = 0; i < isCheckedList.length; i++) { + if (isActiveList[i] == true) { + if (isCheckedList[i] == false) { + setState(() { + isCheckedList[i] = true; + }); + } + } + } + setState(() { + isSelectAll = true; + }); + } else { + isCheckedList = List.generate( + memberList.length, (index) => false); + setState(() { + isSelectAll = false; + }); + } + }, + child: Row( + children: [ + SvgPicture.asset( + 'assets/icons/check.svg', + color: PeeroreumColor.gray[500], + ), + SizedBox( + width: 4, + ), + T5_14px( + text: "전체선택", + color: PeeroreumColor.gray[500], + ), + ], + ), + ) + ], + ), + ), + Divider( + color: PeeroreumColor.gray[100], + thickness: 1, + height: 8, + ), + userListView() + ], + ); + }), + bottomNavigationBar: Container( + padding: EdgeInsets.fromLTRB(20, 8, 20, 28), + child: SizedBox( + height: 48, + child: TextButton( + onPressed: () { + selectedUserList.clear(); + for (int i = 0; i < isCheckedList.length; i++) { + if (isActiveList[i] == true && isCheckedList[i] == true) { + selectedUserList.add(memberList[i]['nickname']); + } + } + + if (selectedUserList.isNotEmpty) { + showKickDialog(); + } else { + Fluttertoast.showToast(msg: '선택된 참여자가 없습니다.'); + } + }, + child: Text( + '참여 종료', + style: TextStyle( + fontFamily: 'Pretendard', + fontSize: 16, + fontWeight: FontWeight.w600, + color: PeeroreumColor.white, + ), + ), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + PeeroreumColor.primaryPuple[400]), + padding: MaterialStateProperty.all( + EdgeInsets.symmetric(vertical: 12)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ))), + ), + ), + )); + } + + userListView() { + return Flexible( + child: ListView.separated( + scrollDirection: Axis.vertical, + itemBuilder: (BuildContext context, int index) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 2, + color: PeeroreumColor + .gradeColor[memberList[index]['grade']]!), + ), + child: Container( + height: 44, + width: 44, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1, + color: PeeroreumColor.white, + ), + image: memberList[index]["profileImage"] != null + ? DecorationImage( + image: NetworkImage( + memberList[index]["profileImage"]), + fit: BoxFit.cover) + : DecorationImage( + image: AssetImage( + 'assets/images/user.jpg')), + ), + ), + ), + SizedBox( + width: 8, + ), + Text( + (memberList[index]['nickname'] == myNickname) + ? '$myNickname (나)' + : memberList[index]['nickname'], + style: TextStyle( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w500, + fontSize: 14, + color: PeeroreumColor.gray[800]), + ) + ], + ), + ), + InkWell( + onTap: () { + if (isActiveList[index]) { + setState(() { + isCheckedList[index] = !isCheckedList[index]; + }); + } + setState(() { + isSelectAll = controlAllSelect(); + }); + }, + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + border: Border.all( + color: (isCheckedList[index] + ? PeeroreumColor.primaryPuple[400]! + : PeeroreumColor.gray[200]!), + width: 1, + ), + borderRadius: BorderRadius.circular(4.0), + color: isActiveList[index] + ? (isCheckedList[index] + ? PeeroreumColor.primaryPuple[400]! + : PeeroreumColor.white) + : PeeroreumColor.gray[300]!, + ), + child: SvgPicture.asset( + 'assets/icons/check.svg', + color: PeeroreumColor.white, + ), + ), + ), + ], + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => Divider( + color: PeeroreumColor.gray[100], + thickness: 1, + height: 8, + ), + itemCount: memberList.length)); + } +} diff --git a/lib/screens/wedu/wedu_detail_screen.dart b/lib/screens/wedu/wedu_detail_screen.dart index 899c3dc..bef7708 100644 --- a/lib/screens/wedu/wedu_detail_screen.dart +++ b/lib/screens/wedu/wedu_detail_screen.dart @@ -15,6 +15,7 @@ import 'package:peeroreum_client/screens/detail_image.dart'; import 'package:peeroreum_client/screens/report.dart'; import 'package:peeroreum_client/screens/wedu/compliment_checklist_screen.dart'; import 'package:peeroreum_client/screens/wedu/encouragement_checklist_screen.dart'; +import 'package:peeroreum_client/screens/wedu/management_checklist_screen.dart'; import 'package:peeroreum_client/screens/wedu/wedu_detail_calendar.dart'; import 'package:peeroreum_client/screens/wedu/wedu_modify_screen.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; @@ -1301,6 +1302,9 @@ class _DetailWeduState extends State { minimumSize: Size.fromHeight(40), alignment: Alignment.centerLeft, padding: EdgeInsets.all(0), + ).copyWith( + overlayColor: + MaterialStateProperty.all(PeeroreumColor.gray[100]), ), child: Text( '같이방 수정', @@ -1312,6 +1316,33 @@ class _DetailWeduState extends State { ), ), ), + TextButton( + onPressed: () { + List allMembers = [ + ...successList, + ...notSuccessList + ]; + Get.to(() => + ManagementCheckList(allMembers, weduTitle, id)); + }, + style: TextButton.styleFrom( + minimumSize: Size.fromHeight(40), + alignment: Alignment.centerLeft, + padding: EdgeInsets.all(0), + ).copyWith( + overlayColor: + MaterialStateProperty.all(PeeroreumColor.gray[100]), + ), + child: Text( + '참여자 관리', + style: TextStyle( + fontFamily: 'Pretendard', + fontSize: 18, + fontWeight: FontWeight.w400, + color: PeeroreumColor.black, + ), + ), + ), TextButton( onPressed: () async { await confirmWeduDeleteMessage(); @@ -1320,6 +1351,9 @@ class _DetailWeduState extends State { minimumSize: Size.fromHeight(40), alignment: Alignment.centerLeft, padding: EdgeInsets.all(0), + ).copyWith( + overlayColor: + MaterialStateProperty.all(PeeroreumColor.gray[100]), ), child: Text( '같이방 삭제',