diff --git a/lib/enums/member_report_reason.dart b/lib/enums/member_report_reason.dart new file mode 100644 index 00000000..d87aa1e4 --- /dev/null +++ b/lib/enums/member_report_reason.dart @@ -0,0 +1,25 @@ +/// 회원 신고 사유 enum (프론트 ↔︎ 백엔드 매핑) +/// +/// id : 백엔드 코드 (MemberReportReason.code) +/// label : 클라이언트 표시용 한글 이름 +/// serverName : 백엔드 enum 이름 +/// +/// 백엔드 enum 참고 (가정): +/// BAD_MANNERS(1), FRAUD(2), MISREPRESENTATION(3), NO_SHOW(4), ETC(5) +/// +/// 사용 예시 +/// MemberReportReason.values.forEach((reason) => print(reason.label)); +/// final codes = selectedReasons.map((e) => e.id).toSet(); // API 전송용 +enum MemberReportReason { + badManners(id: 1, label: '비매너/욕설/혐오/성적 발언', serverName: 'BAD_MANNERS'), + fraud(id: 2, label: '사기 의심/거래 금지 물품', serverName: 'FRAUD'), + misrepresentation(id: 3, label: '물건 상태 불일치(허위 매물)', serverName: 'MISREPRESENTATION'), + noShow(id: 4, label: '노쇼(약속 불이행)', serverName: 'NO_SHOW'), + etc(id: 5, label: '기타(직접 입력)', serverName: 'ETC'); + + final int id; + final String label; + final String serverName; + + const MemberReportReason({required this.id, required this.label, required this.serverName}); +} diff --git a/lib/screens/member_report_screen.dart b/lib/screens/member_report_screen.dart new file mode 100644 index 00000000..ee1099f5 --- /dev/null +++ b/lib/screens/member_report_screen.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import 'package:romrom_fe/models/app_colors.dart'; +import 'package:romrom_fe/models/app_theme.dart'; +import 'package:romrom_fe/widgets/common/completion_button.dart'; +import 'package:romrom_fe/enums/member_report_reason.dart'; +import 'package:romrom_fe/services/apis/report_api.dart'; +import 'package:romrom_fe/utils/error_utils.dart'; +import 'package:romrom_fe/widgets/common/common_modal.dart'; +import 'package:romrom_fe/widgets/common_app_bar.dart'; + +/// 회원 신고하기 페이지 +class MemberReportScreen extends StatefulWidget { + final String memberId; // 신고 대상 회원 ID + + const MemberReportScreen({super.key, required this.memberId}); + + @override + State createState() => _MemberReportScreenState(); +} + +class _MemberReportScreenState extends State { + // 선택된 신고 사유 집합 + final Set _selectedReasons = {}; + late final TextEditingController _extraCommentController; + + @override + void initState() { + super.initState(); + _extraCommentController = TextEditingController(); + _extraCommentController.addListener(() { + setState(() {}); // 글자 수 반영용 + }); + } + + @override + void dispose() { + _extraCommentController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.primaryBlack, + appBar: CommonAppBar( + title: '신고하기', + onBackPressed: () { + Navigator.of(context).pop(); + }, + showBottomBorder: true, + ), + body: Stack( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 40.h), + Text('신고 사유', style: CustomTextStyles.h2.copyWith(fontWeight: FontWeight.w600)), + SizedBox(height: 24.h), + // 신고 사유 리스트 + ...MemberReportReason.values.map((reason) => _buildReasonRow(reason)), + if (_selectedReasons.contains(MemberReportReason.etc)) ...[ + Container( + width: 345.w, + height: 140.h, + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: AppColors.secondaryBlack1, + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: AppColors.textColorWhite.withValues(alpha: 0.3), width: 1.5.w), + ), + child: TextField( + controller: _extraCommentController, + maxLines: null, + maxLength: 300, + style: CustomTextStyles.p2, + cursorColor: AppColors.textColorWhite, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + counterText: '', // 기본 counter 숨김 + hintText: '신고 사유를 상세하게 적어주세요', + hintStyle: CustomTextStyles.p2.copyWith(color: AppColors.textColorWhite.withValues(alpha: 0.4)), + ), + ), + ), + SizedBox(height: 8.h), + Align( + alignment: Alignment.centerRight, + child: Text( + '${_extraCommentController.text.length}/300', + style: CustomTextStyles.p3.copyWith( + fontWeight: FontWeight.w600, + color: AppColors.textColorWhite.withValues(alpha: 0.5), + ), + ), + ), + ], + ], + ), + ), + + // 하단 고정 신고하기 버튼 (bottom 기준 97.h) + Positioned( + left: 24.w, + right: 24.w, + bottom: 97.h, + child: CompletionButton( + isEnabled: _selectedReasons.isNotEmpty, + buttonText: '신고 하기', + enabledOnPressed: () async { + try { + final api = ReportApi(); + await api.reportMember( + memberId: widget.memberId, + memberReportReasons: _selectedReasons.map((e) => e.id).toSet(), + extraComment: _extraCommentController.text.trim(), + ); + } catch (e) { + debugPrint('회원 신고 요청 중 오류: $e'); + // 에러 코드 파싱 + final messageForUser = ErrorUtils.getErrorMessage(e); + + await CommonModal.error( + context: context, + message: messageForUser, + onConfirm: () => Navigator.of(context).pop(), + ); + return; + } + + if (!mounted) return; + + Navigator.of(context).pop(true); // 성공 결과 반환 + }, + ), + ), + ], + ), + ); + } + + /// 신고 사유 선택/해제 토글 + void _toggleReason(MemberReportReason reason) { + setState(() { + if (_selectedReasons.contains(reason)) { + _selectedReasons.remove(reason); + } else { + _selectedReasons.add(reason); + } + }); + } + + /// 신고 사유 행 + Widget _buildReasonRow(MemberReportReason reason) { + return Padding( + padding: EdgeInsets.only(bottom: 24.h), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _toggleReason(reason), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 20.w, + height: 20.w, + child: Checkbox( + value: _selectedReasons.contains(reason), + onChanged: (_) => _toggleReason(reason), + activeColor: AppColors.primaryYellow, + checkColor: AppColors.primaryBlack, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.r)), + side: BorderSide(color: AppColors.primaryYellow, width: 1.w), + ), + ), + SizedBox(width: 12.w), + Expanded(child: Text(reason.label, style: CustomTextStyles.h3)), + ], + ), + ), + ); + } +} diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index 4ceb6a63..aa30b8bb 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -5,9 +5,11 @@ import 'package:romrom_fe/icons/app_icons.dart'; import 'package:romrom_fe/models/app_colors.dart'; import 'package:romrom_fe/models/app_theme.dart'; import 'package:romrom_fe/models/user_info.dart'; +import 'package:romrom_fe/screens/member_report_screen.dart'; import 'package:romrom_fe/screens/my_page/my_profile_edit_screen.dart'; import 'package:romrom_fe/services/apis/member_api.dart'; import 'package:romrom_fe/utils/common_utils.dart'; +import 'package:romrom_fe/widgets/common/common_modal.dart'; import 'package:romrom_fe/widgets/common/common_snack_bar.dart'; import 'package:romrom_fe/widgets/common/romrom_context_menu.dart'; import 'package:romrom_fe/widgets/common_app_bar.dart'; @@ -156,9 +158,16 @@ class _ProfileScreenState extends State { } /// 신고하기 처리 - void _handleReport() { - // TODO: 신고 기능 구현 (API 확인 필요) - CommonSnackBar.show(context: context, message: '신고 기능은 준비 중입니다', type: SnackBarType.info); + Future _handleReport() async { + final bool? reported = await context.navigateTo(screen: MemberReportScreen(memberId: widget.memberId)); + + if (reported == true && mounted) { + await CommonModal.success( + context: context, + message: '신고가 접수되었습니다.', + onConfirm: () => Navigator.of(context).pop(), + ); + } } @override diff --git a/lib/services/apis/report_api.dart b/lib/services/apis/report_api.dart index c676e0f3..077d4a13 100644 --- a/lib/services/apis/report_api.dart +++ b/lib/services/apis/report_api.dart @@ -31,4 +31,29 @@ class ReportApi { }, ); } + + /// 회원 신고 API + /// POST /api/report/member/post + Future reportMember({ + required String memberId, + required Set memberReportReasons, + String? extraComment, + }) async { + const String url = '${AppUrls.baseUrl}/api/report/member/post'; + + final Map fields = { + 'memberId': memberId, + 'memberReportReasons': memberReportReasons.join(','), + if (extraComment != null && extraComment.isNotEmpty) 'extraComment': extraComment, + }; + + await ApiClient.sendMultipartRequest( + url: url, + fields: fields, + isAuthRequired: true, + onSuccess: (_) { + debugPrint('회원 신고 성공'); + }, + ); + } }