diff --git a/lib/core/constants/report_type.dart b/lib/core/constants/report_type.dart new file mode 100644 index 00000000..5199181d --- /dev/null +++ b/lib/core/constants/report_type.dart @@ -0,0 +1,26 @@ +enum ReportType { + post('POST'), + comment('COMMENT'), + chatRoom('CHAT_ROOM'), + user('USER') + ; + + final String value; + + const ReportType(this.value); +} + +enum ReportReason { + swear("욕설이 많아요"), + disrespect("상대를 존중하지 않아요"), + offensive("불쾌감을 느끼는 발언을 해요"), + aggressive("공격적인 언어를 사용해요"), + discrimination("신분, 장애, 인종에 대해 차별 발언을 일삼아요"), + badName("부적절한 이름을 사용해요"), + etc("기타") + ; + + final String label; + + const ReportReason(this.label); +} diff --git a/lib/core/design_system/components/custom_popup_menu_button.dart b/lib/core/design_system/components/custom_popup_menu_button.dart index bd319a87..00a96d7f 100644 --- a/lib/core/design_system/components/custom_popup_menu_button.dart +++ b/lib/core/design_system/components/custom_popup_menu_button.dart @@ -15,6 +15,7 @@ class CustomPopupMenuButton extends StatelessWidget { return PopupMenuButton( offset: const Offset(-6, 13), color: AppColors.white, + borderRadius: AppRadius.baseRadius, constraints: BoxConstraints(minWidth: 0), itemBuilder: itemBuilder, diff --git a/lib/presentation/post/widgets/post_report_dialog.dart b/lib/core/design_system/components/custom_report_dialog.dart similarity index 53% rename from lib/presentation/post/widgets/post_report_dialog.dart rename to lib/core/design_system/components/custom_report_dialog.dart index 404f8187..925415a4 100644 --- a/lib/presentation/post/widgets/post_report_dialog.dart +++ b/lib/core/design_system/components/custom_report_dialog.dart @@ -1,56 +1,73 @@ - import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:ondo/core/constants/report_type.dart'; import 'package:ondo/core/design_system/app_colors.dart'; import 'package:ondo/core/design_system/app_layout.dart'; import 'package:ondo/core/design_system/app_text_styles.dart'; import 'package:ondo/core/design_system/component_variants.dart'; import 'package:ondo/core/design_system/components/custom_button.dart'; +import 'package:ondo/core/design_system/components/custom_tag_card.dart'; import 'package:ondo/core/design_system/components/custom_textfield.dart'; -import 'package:ondo/presentation/profile/widget/report_reason_chip.dart'; +import 'package:ondo/domain/usecases/report/report_use_case.dart'; -class PostReportDialog extends StatefulWidget { - const PostReportDialog({ +class CustomReportDialog extends StatefulWidget { + const CustomReportDialog({ super.key, + required this.type, + required this.targetId, }); + final ReportType type; + final String targetId; + @override - State createState() => _PostReportDialogState(); + State createState() => _CustomReportDialogState(); } -class _PostReportDialogState extends State { +class _CustomReportDialogState extends State { final TextEditingController controller = TextEditingController(); - final List _reasons = [ - "욕설이 많아요", - "상대를 존중하지 않아요", - "불쾌감을 느끼는 발언을 해요", - "공격적인 언어를 사용해요", - "신분, 장애, 인종에 대해 차별 발언을 일삼아요", - "부적절한 이름을 사용해요", - "기타", - ]; + final Set _selectedReasons = {}; - final Set _selectedReasons = {}; + String get _title => switch (widget.type) { + ReportType.post => '게시물 신고', + ReportType.comment => '댓글 신고', + ReportType.chatRoom => '채팅방 신고', + ReportType.user => '유저 신고', + }; bool get _isValid => _selectedReasons.isNotEmpty || controller.text.trim().isNotEmpty; - void _toggleReason(String reason) { - setState(() { - if (_selectedReasons.contains(reason)) { - _selectedReasons.remove(reason); - } else { - _selectedReasons.add(reason); - } - }); - } - Future _submitReport() async { if (!_isValid) return; + final description = [ + if (_selectedReasons.isNotEmpty) ...[ + 'tags', + ..._selectedReasons.map((e) => e.label), + ], + if (controller.text.trim().isNotEmpty) ...[ + 'detail', + controller.text.trim(), + ], + ].join('\n'); + await Get.find()( + targetType: widget.type, + targetId: widget.targetId, + description: description, + ); + + if (!mounted) return; Navigator.pop(context); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Dialog( @@ -68,7 +85,7 @@ class _PostReportDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Center(child: Text("게시물 신고", style: AppTextStyles.titleBold20())), + Center(child: Text(_title, style: AppTextStyles.titleBold20())), AppGap.v16, SizedBox( width: MediaQuery.of(context).size.width, @@ -76,12 +93,20 @@ class _PostReportDialogState extends State { crossAxisAlignment: WrapCrossAlignment.start, spacing: AppSpacing.s12, runSpacing: AppSpacing.s12, - children: _reasons.map( - (e) { - return ReportReasonChip( - label: e, - isSelected: _selectedReasons.contains(e), - onTap: () => _toggleReason(e), + children: ReportReason.values.map( + (r) { + return CustomTagCard( + label: r.label, + isSelected: _selectedReasons.contains(r), + onTap: (isSelect) { + setState(() { + if (isSelect) { + _selectedReasons.add(r); + } else { + _selectedReasons.remove(r); + } + }); + }, ); }, ).toList(), diff --git a/lib/core/design_system/components/custom_tag_card.dart b/lib/core/design_system/components/custom_tag_card.dart index fd8c0417..272885cb 100644 --- a/lib/core/design_system/components/custom_tag_card.dart +++ b/lib/core/design_system/components/custom_tag_card.dart @@ -3,48 +3,41 @@ import 'package:ondo/core/design_system/app_colors.dart'; import 'package:ondo/core/design_system/app_layout.dart'; import 'package:ondo/core/design_system/app_text_styles.dart'; +typedef SelectTag = void Function(bool isSelect); + class CustomTagCard extends StatelessWidget { - final String tag; - final Color? color; - final void Function(bool isSelect)? onTap; - final ValueNotifier isSelect; + final String label; + final bool isSelected; + final SelectTag? onTap; - CustomTagCard({ + const CustomTagCard({ super.key, - required this.tag, - this.color, + required this.label, + this.isSelected = false, this.onTap, - bool isSelect = false, - }) : isSelect = ValueNotifier(isSelect); + }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: isSelect, - builder: (context, value, child) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - isSelect.value = !isSelect.value; - onTap?.call(isSelect.value); - }, - child: Container( - padding: AppPadding.chip, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: isSelect.value - ? AppColors.primary - : color ?? AppColors.white, - ), - child: Text( - tag, - style: AppTextStyles.textMedium( - textColor: isSelect.value ? AppColors.white : AppColors.gray90, - ), - ), + final foregroundColor = isSelected ? AppColors.white : AppColors.gray90; + final backgroundColor = isSelected ? AppColors.primary : AppColors.gray20; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => onTap?.call(!isSelected), + child: Container( + padding: AppPadding.chip, + decoration: BoxDecoration( + borderRadius: AppRadius.baseRadius, + color: backgroundColor, + ), + child: Text( + label, + style: AppTextStyles.textMedium( + textColor: foregroundColor, ), - ); - }, + ), + ), ); } } diff --git a/lib/core/router/bindings/navigation_binding.dart b/lib/core/router/bindings/navigation_binding.dart index e4d3eddc..bfa7f6c6 100644 --- a/lib/core/router/bindings/navigation_binding.dart +++ b/lib/core/router/bindings/navigation_binding.dart @@ -7,6 +7,7 @@ import 'package:ondo/core/router/bindings/home_binding.dart'; import 'package:ondo/core/router/bindings/notification_binding.dart'; import 'package:ondo/core/router/bindings/post_binding.dart'; import 'package:ondo/core/router/bindings/profile_binding.dart'; +import 'package:ondo/core/router/bindings/report_binding.dart'; import 'package:ondo/core/router/bindings/user_binding.dart'; import 'package:ondo/data/datasource/auth/auth_local_datasource_impl.dart'; import 'package:ondo/data/datasource/auth/auth_remote_datasource.dart'; @@ -38,8 +39,9 @@ class NavigationBinding extends Bindings { /// 전 화면 공통 controller 등록 Get.lazyPut(() => NavigationController()); - NotificationBinding().dependencies(); + NotificationBinding().dependencies(); + ReportBinding().dependencies(); UserBinding().dependencies(); PostBinding().dependencies(); diff --git a/lib/core/router/bindings/post_view_binding.dart b/lib/core/router/bindings/post_view_binding.dart index f9cfe262..ce74e1a6 100644 --- a/lib/core/router/bindings/post_view_binding.dart +++ b/lib/core/router/bindings/post_view_binding.dart @@ -30,7 +30,6 @@ class PostViewBinding extends Bindings { @override void dependencies() { - // Post UseCases Get.lazyPut( () => GetPostDetailUseCase( @@ -56,7 +55,6 @@ class PostViewBinding extends Bindings { ), ); - // Comment DataSource Get.lazyPut( () => CommentRemoteDataSourceImpl( diff --git a/lib/core/router/bindings/report_binding.dart b/lib/core/router/bindings/report_binding.dart new file mode 100644 index 00000000..487fee3b --- /dev/null +++ b/lib/core/router/bindings/report_binding.dart @@ -0,0 +1,23 @@ +import 'package:get/get.dart'; +import 'package:ondo/data/datasource/report/report_remote_datasource.dart'; +import 'package:ondo/data/network/clients/auth_client.dart'; +import 'package:ondo/data/repositories/report/report_repository_impl.dart'; +import 'package:ondo/domain/repositories/report/report_repository.dart'; +import 'package:ondo/domain/usecases/report/report_use_case.dart'; + +class ReportBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => ReportRemoteDatasource(Get.find()), + ); + + Get.lazyPut( + () => ReportRepositoryImpl(Get.find()), + ); + + Get.lazyPut( + () => ReportUseCase(Get.find()), + ); + } +} diff --git a/lib/data/datasource/comment/comment_remote_datasource.dart b/lib/data/datasource/comment/comment_remote_datasource.dart index 5e44eaa9..1dbe6ad4 100644 --- a/lib/data/datasource/comment/comment_remote_datasource.dart +++ b/lib/data/datasource/comment/comment_remote_datasource.dart @@ -5,7 +5,10 @@ import 'package:ondo/data/models/comment/response/comment_model.dart'; abstract class CommentRemoteDataSource { Future> getComments(int postId); - Future createComment({required int postId, required String content}); + Future createComment({ + required int postId, + required String content, + }); Future deleteComment(int commentId); } @@ -57,7 +60,7 @@ class CommentRemoteDataSourceImpl implements CommentRemoteDataSource { } @override - Future createComment({ + Future createComment({ required int postId, required String content, }) async { @@ -81,6 +84,8 @@ class CommentRemoteDataSourceImpl implements CommentRemoteDataSource { if (body['success'] != true) { throw Exception(body['message']); } + + return true; } catch (e) { log.errorLog(e); rethrow; diff --git a/lib/data/datasource/post/post_remote_datasource.dart b/lib/data/datasource/post/post_remote_datasource.dart index 01ae7faf..aabd7361 100644 --- a/lib/data/datasource/post/post_remote_datasource.dart +++ b/lib/data/datasource/post/post_remote_datasource.dart @@ -17,7 +17,7 @@ class PostRemoteDatasource { final res = await _client.get(Uri.parse("${ApiConstants.post}/popular")); final body = jsonDecode(res.body); - log.serverLog(body, stausCode: res.statusCode); + log.serverLog(body, statusCode: res.statusCode); if (res.statusCode == 200 && body["success"] == true) { return body["data"]; diff --git a/lib/data/datasource/report/report_remote_datasource.dart b/lib/data/datasource/report/report_remote_datasource.dart new file mode 100644 index 00000000..cc898fdd --- /dev/null +++ b/lib/data/datasource/report/report_remote_datasource.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:ondo/data/models/report/request/report_request_model.dart'; +import 'package:ondo/data/network/constants/api_constants.dart'; + +class ReportRemoteDatasource { + final BaseClient _client; + + ReportRemoteDatasource(this._client); + + Future report(ReportRequestModel model) async { + final log = ApiConstants(logName: '신고 접수 서버'); + + try { + final res = await _client.post( + Uri.parse(ApiConstants.report), + headers: ApiConstants.baseHeader, + body: jsonEncode(model.toJson()), + ); + + final body = jsonDecode(res.body); + + log.successLog(body['success']); + log.messageLog(body['message']); + + if (res.statusCode == 200 && body['success'] == true) { + return true; + } + log.statusLog(res.statusCode); + } catch (e) { + log.errorLog(e); + } + + return false; + } +} diff --git a/lib/data/models/report/request/report_request_model.dart b/lib/data/models/report/request/report_request_model.dart new file mode 100644 index 00000000..5c4d86a0 --- /dev/null +++ b/lib/data/models/report/request/report_request_model.dart @@ -0,0 +1,19 @@ +import 'package:ondo/core/constants/report_type.dart'; + +class ReportRequestModel { + final ReportType targetType; + final String targetId; + final String description; + + ReportRequestModel({ + required this.targetType, + required this.targetId, + required this.description, + }); + + Map toJson() => { + 'targetType': targetType.value, + 'targetId': targetId, + 'description': description, + }; +} diff --git a/lib/data/network/constants/api_constants.dart b/lib/data/network/constants/api_constants.dart index f10570b0..e8785ab7 100644 --- a/lib/data/network/constants/api_constants.dart +++ b/lib/data/network/constants/api_constants.dart @@ -24,6 +24,8 @@ class ApiConstants { static String get comment => "${Env.apiBaseUrl}/comments"; + static String get report => "${Env.apiBaseUrl}/reports"; + static const baseHeader = {"Content-Type": "application/json"}; //TODO : 임시 로그인 데이터 @@ -31,9 +33,9 @@ class ApiConstants { static String get loginPassword => Env.loginPassword; - void serverLog(Map body, {int? stausCode}) { - if (stausCode != null) { - statusLog(stausCode); + void serverLog(Map body, {int? statusCode}) { + if (statusCode != null) { + statusLog(statusCode); } successLog(body["success"]); messageLog(body["message"]); diff --git a/lib/data/repositories/comment/comment_repository_impl.dart b/lib/data/repositories/comment/comment_repository_impl.dart index dff38f95..341725cc 100644 --- a/lib/data/repositories/comment/comment_repository_impl.dart +++ b/lib/data/repositories/comment/comment_repository_impl.dart @@ -19,12 +19,12 @@ class CommentRepositoryImpl implements CommentRepository { } @override - Future createComment({ + Future createComment({ required int postId, required String content, }) async { try { - await remoteDataSource.createComment( + return await remoteDataSource.createComment( postId: postId, content: content, ); diff --git a/lib/data/repositories/report/report_repository_impl.dart b/lib/data/repositories/report/report_repository_impl.dart new file mode 100644 index 00000000..d3a3f961 --- /dev/null +++ b/lib/data/repositories/report/report_repository_impl.dart @@ -0,0 +1,24 @@ +import 'package:ondo/core/constants/report_type.dart'; +import 'package:ondo/data/datasource/report/report_remote_datasource.dart'; +import 'package:ondo/data/models/report/request/report_request_model.dart'; +import 'package:ondo/domain/repositories/report/report_repository.dart'; + +class ReportRepositoryImpl extends ReportRepository { + final ReportRemoteDatasource _remoteDatasource; + + ReportRepositoryImpl(this._remoteDatasource); + + @override + Future report( + ReportType targetType, + String targetId, + String description, + ) async { + final model = ReportRequestModel( + targetType: targetType, + targetId: targetId, + description: description, + ); + return await _remoteDatasource.report(model); + } +} diff --git a/lib/domain/repositories/comment/comment_repository.dart b/lib/domain/repositories/comment/comment_repository.dart index 8254239a..fbcbb29c 100644 --- a/lib/domain/repositories/comment/comment_repository.dart +++ b/lib/domain/repositories/comment/comment_repository.dart @@ -3,7 +3,7 @@ import 'package:ondo/data/models/comment/response/comment_model.dart'; abstract class CommentRepository { Future> getComments(int postId); - Future createComment({ + Future createComment({ required int postId, required String content, }); diff --git a/lib/domain/repositories/report/report_repository.dart b/lib/domain/repositories/report/report_repository.dart new file mode 100644 index 00000000..6f205f4c --- /dev/null +++ b/lib/domain/repositories/report/report_repository.dart @@ -0,0 +1,9 @@ +import 'package:ondo/core/constants/report_type.dart'; + +abstract class ReportRepository { + Future report( + ReportType targetType, + String targetId, + String description, + ); +} diff --git a/lib/domain/usecases/comment/create_comment_usecase.dart b/lib/domain/usecases/comment/create_comment_usecase.dart index 8c0a8d48..04b8b9c4 100644 --- a/lib/domain/usecases/comment/create_comment_usecase.dart +++ b/lib/domain/usecases/comment/create_comment_usecase.dart @@ -5,12 +5,12 @@ class CreateCommentUseCase { CreateCommentUseCase(this.repository); - Future call({ + Future call({ required int postId, required String content, }) async { try { - await repository.createComment( + return await repository.createComment( postId: postId, content: content, ); @@ -18,4 +18,4 @@ class CreateCommentUseCase { throw Exception('댓글 작성 실패: $e'); } } -} \ No newline at end of file +} diff --git a/lib/domain/usecases/report/report_use_case.dart b/lib/domain/usecases/report/report_use_case.dart new file mode 100644 index 00000000..d3ec54bb --- /dev/null +++ b/lib/domain/usecases/report/report_use_case.dart @@ -0,0 +1,16 @@ +import 'package:ondo/core/constants/report_type.dart'; +import 'package:ondo/domain/repositories/report/report_repository.dart'; + +class ReportUseCase { + final ReportRepository _repository; + + ReportUseCase(this._repository); + + Future call({ + required ReportType targetType, + required String targetId, + required String description, + }) async { + return await _repository.report(targetType, targetId, description); + } +} diff --git a/lib/presentation/chat/controllers/chat_room_controller.dart b/lib/presentation/chat/controllers/chat_room_controller.dart index 2fac61da..4e45427c 100644 --- a/lib/presentation/chat/controllers/chat_room_controller.dart +++ b/lib/presentation/chat/controllers/chat_room_controller.dart @@ -3,8 +3,9 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:ondo/core/constants/report_type.dart'; +import 'package:ondo/core/design_system/components/custom_report_dialog.dart'; import 'package:ondo/data/datasource/media/media_remote_datasource.dart'; import 'package:get/get.dart'; import 'package:ondo/core/design_system/components/custom_alert_dialog.dart'; @@ -402,7 +403,10 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { } } else { // 상대방 메시지 → 리스트 앞에 삽입 - final viewModel = ChatMessageViewModel.fromJson(json, myPublicId: _myPublicId); + final viewModel = ChatMessageViewModel.fromJson( + json, + myPublicId: _myPublicId, + ); viewChatList.insert(0, viewModel); log('[ChatRoom] 메시지 수신: $content'); @@ -428,7 +432,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { } } - Future _initLoadChatRoomMessages() async { + Future _initLoadChatRoomMessages() async { final res = await loadChatRoomMessageUseCase.call( chatRoomPublicId: chatRoomId, cursor: 0, @@ -487,7 +491,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { /// - 내가 보낸 메시지 즉시 UI 반영 (isMe: true, messageId: null) /// - 서버 echo 수신 시 _onMessageReceived에서 messageId가 업데이트된다 /// - IMAGE 타입은 추후 구현 예정 - void sendChat(String content) { + void sendMessage(String content) { final trimmed = content.trim(); if (trimmed.isEmpty) return; @@ -517,7 +521,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { textController.clear(); } - Future backChatRoom() async { + Future back() async { // WebSocket 읽음 이벤트 전송 sendReadEvent(); // HTTP 읽음 처리 유지 @@ -528,7 +532,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { ); } - Future quitChat() async { + Future quit() async { Get.dialog( CustomAlertDialog( title: '커피챗 종료', @@ -543,4 +547,10 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { ), ); } + + void report(BuildContext context) => showDialog( + context: context, + builder: (context) => + CustomReportDialog(type: ReportType.chatRoom, targetId: chatRoomId), + ); } diff --git a/lib/presentation/chat/screens/chat_main_screen.dart b/lib/presentation/chat/screens/chat_main_screen.dart index 58039a42..6d2ca261 100644 --- a/lib/presentation/chat/screens/chat_main_screen.dart +++ b/lib/presentation/chat/screens/chat_main_screen.dart @@ -20,7 +20,12 @@ class ChatMainScreen extends GetView { AppGap.v16, TagList(), AppGap.v16, - Expanded(child: ChatList()), + Expanded( + child: Padding( + padding: AppPadding.screenHorizontal, + child: ChatList(), + ), + ), AppGap.v16, ], ), @@ -40,9 +45,12 @@ class TagList extends GetView { child: ListView.separated( padding: AppPadding.screenHorizontal, scrollDirection: Axis.horizontal, - itemBuilder: (context, index) => CustomTagCard( - tag: tags[index], - onTap: (isSelect) => controller.selectTag(tags[index], isSelect), + itemBuilder: (context, index) => Obx( + () => CustomTagCard( + label: tags[index], + isSelected: controller.selectTagList.contains(tags[index]), + onTap: (isSelect) => controller.selectTag(tags[index], isSelect), + ), ), itemCount: tags.length, separatorBuilder: (context, index) => AppGap.h16, diff --git a/lib/presentation/chat/screens/chat_room_screen.dart b/lib/presentation/chat/screens/chat_room_screen.dart index 2d312cde..da03f3d3 100644 --- a/lib/presentation/chat/screens/chat_room_screen.dart +++ b/lib/presentation/chat/screens/chat_room_screen.dart @@ -57,17 +57,22 @@ class ChatRoomScreen extends GetView { ), ); - Widget _chatList(int lastReadId, String opponentProfileUrl) => ListView.builder( - controller: controller.scrollController, - reverse: true, - itemBuilder: (context, index) { - final chat = controller.viewChatList[index]; - return _chatBubble(chat, lastReadId, opponentProfileUrl); - }, - itemCount: controller.viewChatList.length, - ); + Widget _chatList(int lastReadId, String opponentProfileUrl) => + ListView.builder( + controller: controller.scrollController, + reverse: true, + itemBuilder: (context, index) { + final chat = controller.viewChatList[index]; + return _chatBubble(chat, lastReadId, opponentProfileUrl); + }, + itemCount: controller.viewChatList.length, + ); - Widget _chatBubble(ChatMessageViewModel chat, int lastReadId, String opponentProfileUrl) { + Widget _chatBubble( + ChatMessageViewModel chat, + int lastReadId, + String opponentProfileUrl, + ) { if (chat.isMe) { final isRead = chat.messageId != null && chat.messageId! <= lastReadId; return Row( @@ -116,7 +121,7 @@ class ChatRoomScreen extends GetView { () => CustomBackButton( moreOptions: true, useUserProfile: true, - backAction: controller.backChatRoom, + backAction: controller.back, subtitle: controller.isOpponentViewing.value ? '채팅 중' : controller.isOpponentOnline.value @@ -136,14 +141,12 @@ class ChatRoomScreen extends GetView { "커피챗 종료하기", ///종료 알림창 표시 - controller.quitChat, + controller.quit, ), _customPopupMenu( "신고하기", //신고 기능 - () { - //TODO : 신고 기능 - }, + () => controller.report(context), ), ], ), diff --git a/lib/presentation/chat/widgets/chat_input_field.dart b/lib/presentation/chat/widgets/chat_input_field.dart index a39e08c6..21daaac4 100644 --- a/lib/presentation/chat/widgets/chat_input_field.dart +++ b/lib/presentation/chat/widgets/chat_input_field.dart @@ -34,7 +34,7 @@ class ChatInputField extends GetView { hintText: "메세지 내용을 입력해 주세요", maxLines: 2, onChanged: controller.onTypingChanged, - onSubmitted: controller.sendChat, + onSubmitted: controller.sendMessage, ), ), AppGap.h16, @@ -45,7 +45,7 @@ class ChatInputField extends GetView { } Widget _sendButton() => GestureDetector( - onTap: () => controller.sendChat(controller.textController.text), + onTap: () => controller.sendMessage(controller.textController.text), child: SvgPicture.asset( AppIcon.send.path, width: AppSpacing.s32, diff --git a/lib/presentation/chat/widgets/chat_review_dialog.dart b/lib/presentation/chat/widgets/chat_review_dialog.dart index e1fbdaee..adc59d58 100644 --- a/lib/presentation/chat/widgets/chat_review_dialog.dart +++ b/lib/presentation/chat/widgets/chat_review_dialog.dart @@ -58,19 +58,23 @@ class ChatReviewDialog extends GetView { Widget _reviewCategoryList() => SizedBox( width: double.maxFinite, - child: Wrap( - direction: Axis.horizontal, - spacing: AppSpacing.s12, - runSpacing: AppSpacing.s12, - children: List.generate( - controller.baseCategories.length, - (index) => CustomTagCard( - onTap: (isSelect) => controller.selectReviewTag( - controller.baseCategories[index], - isSelect, + child: Obx( + () => Wrap( + direction: Axis.horizontal, + spacing: AppSpacing.s12, + runSpacing: AppSpacing.s12, + children: List.generate( + controller.baseCategories.length, + (index) => CustomTagCard( + onTap: (isSelect) => controller.selectReviewTag( + controller.baseCategories[index], + isSelect, + ), + isSelected: controller.reviewTags.contains( + controller.baseCategories[index], + ), + label: controller.baseCategories[index], ), - tag: controller.baseCategories[index], - color: AppColors.gray20, ), ), ), diff --git a/lib/presentation/community/widgets/community_filter_tag_list.dart b/lib/presentation/community/widgets/community_filter_tag_list.dart index 21c9a4b0..ec21b56f 100644 --- a/lib/presentation/community/widgets/community_filter_tag_list.dart +++ b/lib/presentation/community/widgets/community_filter_tag_list.dart @@ -19,12 +19,15 @@ class CommunityFilterTagList extends GetView { itemCount: tags.length, separatorBuilder: (context, index) => AppGap.h16, itemBuilder: (context, index) { - return CustomTagCard( - onTap: (isSelect) => controller.filterPostTag( - tags[index].$2, - isSelect, + return Obx( + () => CustomTagCard( + onTap: (isSelect) => controller.filterPostTag( + tags[index].$2, + isSelect, + ), + isSelected: controller.selectTagList.contains(tags[index].$2), + label: tags[index].$2, ), - tag: tags[index].$2, ); }, ), diff --git a/lib/presentation/post/controllers/post_view_controller.dart b/lib/presentation/post/controllers/post_view_controller.dart index 56f74267..954dccfc 100644 --- a/lib/presentation/post/controllers/post_view_controller.dart +++ b/lib/presentation/post/controllers/post_view_controller.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:ondo/core/constants/report_type.dart'; import 'package:ondo/core/design_system/components/custom_alert_dialog.dart'; +import 'package:ondo/core/design_system/components/custom_report_dialog.dart'; import 'package:ondo/domain/entities/comment/comment_entity.dart'; import 'package:ondo/domain/entities/post/post_detail_entity.dart'; import 'package:ondo/domain/entities/post/post_entity.dart'; @@ -21,7 +23,6 @@ import '../../../domain/usecases/post/unlike_post_usecase.dart'; import '../../../domain/usecases/post/update_post_usecase.dart'; import '../../community/controllers/community_post_create_screen_controller.dart'; import '../../community/screens/community_post_create_screen.dart'; -import '../widgets/post_report_dialog.dart'; class PostViewController extends GetxController { final int postId; @@ -363,7 +364,7 @@ class PostViewController extends GetxController { await _deleteCommentUseCase(comment.id); - await fetchComments(); + comments.removeWhere((c) => c.id == comment.id); commentCount.value = comments.length; @@ -375,9 +376,23 @@ class PostViewController extends GetxController { } } - void reportPost() { - Get.dialog( - PostReportDialog(), + void reportPost(BuildContext context) { + showDialog( + context: context, + builder: (context) => CustomReportDialog( + type: ReportType.post, + targetId: postId.toString(), + ), + ); + } + + void reportComment(BuildContext context, CommentEntity comment) { + showDialog( + context: context, + builder: (context) => CustomReportDialog( + type: ReportType.comment, + targetId: comment.id.toString(), + ), ); } diff --git a/lib/presentation/post/screens/post_detail_screen.dart b/lib/presentation/post/screens/post_detail_screen.dart index 7f68f6e3..138d022a 100644 --- a/lib/presentation/post/screens/post_detail_screen.dart +++ b/lib/presentation/post/screens/post_detail_screen.dart @@ -42,12 +42,13 @@ class _PostDetailScreenState extends State { } List _top() => [ + //TODO : controller에서 publicId를 받아오면, 분기 처리 CustomBackButton( moreOptions: true, itemBuilder: (context) => [ _topPopupItem("게시물 수정하기", controller.editPost), _topPopupItem("게시물 삭제하기", controller.deletePost), - _topPopupItem("게시물 신고하기", controller.reportPost), + _topPopupItem("게시물 신고하기", () => controller.reportPost(context)), ], ), PostTitle(), diff --git a/lib/presentation/post/widgets/post_comment_card.dart b/lib/presentation/post/widgets/post_comment_card.dart index 7b55e78d..42f03f99 100644 --- a/lib/presentation/post/widgets/post_comment_card.dart +++ b/lib/presentation/post/widgets/post_comment_card.dart @@ -17,31 +17,64 @@ class PostCommentCard extends GetView { @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const CustomProfileCircle( - radius: AppSpacing.s24, - ), - AppGap.h12, - Expanded( - child: _body(), - ), - AppGap.h6, - if(comment.isMy) - GestureDetector( - onTap: () { - controller.deleteComment(comment); - }, - child: Text( - '삭제', - style: AppTextStyles.caption( - textColor: AppColors.red, - ), + return GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPressStart: (details) => + _showReportMenu(context, details.globalPosition), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CustomProfileCircle( + radius: AppSpacing.s24, + ), + AppGap.h12, + Expanded( + child: _body(), ), + AppGap.h6, + if (comment.isMy) + GestureDetector( + onTap: () { + controller.deleteComment(comment); + }, + child: Text( + '삭제', + style: AppTextStyles.caption( + textColor: AppColors.red, + ), + ), + ), + ], + ), + ); + } + + void _showReportMenu(BuildContext context, Offset position) { + if (comment.isMy) return; + + showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx, + position.dy, + ), + color: AppColors.white, + shape: const RoundedRectangleBorder(borderRadius: AppRadius.baseRadius), + constraints: const BoxConstraints(minWidth: 0), + items: [ + PopupMenuItem( + value: 'report', + padding: AppPadding.popupManuButton, + child: const Center(child: Text('신고하기')), ), ], - ); + ).then((value) { + if (value == 'report' && context.mounted) { + controller.reportComment(context, comment); + } + }); } Widget _body() { @@ -67,4 +100,4 @@ class PostCommentCard extends GetView { ], ); } -} \ No newline at end of file +} diff --git a/lib/presentation/profile/screens/other_profile_screen.dart b/lib/presentation/profile/screens/other_profile_screen.dart index a5dc6653..cf97e5ec 100644 --- a/lib/presentation/profile/screens/other_profile_screen.dart +++ b/lib/presentation/profile/screens/other_profile_screen.dart @@ -2,12 +2,14 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:ondo/core/constants/report_type.dart'; import 'package:ondo/core/design_system/app_colors.dart'; import 'package:ondo/core/design_system/app_layout.dart'; import 'package:ondo/core/design_system/app_text_styles.dart'; import 'package:ondo/core/design_system/component_variants.dart'; import 'package:ondo/core/design_system/components/custom_back_button.dart'; import 'package:ondo/core/design_system/components/custom_button.dart'; +import 'package:ondo/core/design_system/components/custom_report_dialog.dart'; import 'package:ondo/core/ui/base/base_scaffold.dart'; import 'package:ondo/domain/entities/post/post_entity.dart'; import 'package:ondo/presentation/post/widgets/post_item.dart'; @@ -20,7 +22,6 @@ import 'package:ondo/presentation/profile/widget/user_introduction_text.dart'; import 'package:ondo/presentation/profile/widget/user_name_and_major.dart'; import 'package:ondo/presentation/profile/widget/user_profile_image.dart'; import 'package:ondo/data/models/user/response/user_profile_response_model.dart'; -import 'package:ondo/presentation/profile/widget/user_report_popup.dart'; // TODO: 게시글 API 연동 후 교체 final List> mockPostData = [ @@ -95,7 +96,10 @@ class OtherProfileScreen extends GetView { PopupMenuItem( onTap: () => showDialog( context: context, - builder: (context) => const UserReportPopup(), + builder: (context) => CustomReportDialog( + type: ReportType.user, + targetId: publicId, + ), ), padding: AppPadding.settingSession, child: Text( diff --git a/lib/presentation/profile/widget/report_reason_chip.dart b/lib/presentation/profile/widget/report_reason_chip.dart deleted file mode 100644 index 50bfd27d..00000000 --- a/lib/presentation/profile/widget/report_reason_chip.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ondo/core/design_system/app_colors.dart'; -import 'package:ondo/core/design_system/app_layout.dart'; -import 'package:ondo/core/design_system/app_text_styles.dart'; - -class ReportReasonChip extends StatelessWidget { - const ReportReasonChip({ - super.key, - required this.label, - required this.isSelected, - required this.onTap, - }); - - final String label; - final bool isSelected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: AppPadding.chip, - decoration: BoxDecoration( - color: isSelected ? AppColors.primary : AppColors.gray20, - borderRadius: AppRadius.baseRadius, - ), - child: Text( - label, - style: AppTextStyles.textMedium( - textColor: isSelected ? AppColors.white : AppColors.black, - ), - ), - ), - ); - } -} diff --git a/lib/presentation/profile/widget/user_report_popup.dart b/lib/presentation/profile/widget/user_report_popup.dart deleted file mode 100644 index a380f0ef..00000000 --- a/lib/presentation/profile/widget/user_report_popup.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:ondo/core/design_system/app_colors.dart'; -import 'package:ondo/core/design_system/app_layout.dart'; -import 'package:ondo/core/design_system/app_text_styles.dart'; -import 'package:ondo/core/design_system/component_variants.dart'; -import 'package:ondo/core/design_system/components/custom_button.dart'; -import 'package:ondo/core/design_system/components/custom_textfield.dart'; -import 'package:ondo/presentation/profile/widget/report_reason_chip.dart'; - -class UserReportPopup extends StatefulWidget { - const UserReportPopup({ - super.key, - }); - - @override - State createState() => _UserReportPopupState(); -} - -class _UserReportPopupState extends State { - final TextEditingController controller = TextEditingController(); - - final List _reasons = [ - "욕설이 많아요", - "상대를 존중하지 않아요", - "불쾌감을 느끼는 발언을 해요", - "공격적인 언어를 사용해요", - "신분, 장애, 인종에 대해 차별 발언을 일삼아요", - "부적절한 이름을 사용해요", - "기타", - ]; - - final Set _selectedReasons = {}; - - bool get _isValid => - _selectedReasons.isNotEmpty || controller.text.trim().isNotEmpty; - - void _toggleReason(String reason) { - setState(() { - if (_selectedReasons.contains(reason)) { - _selectedReasons.remove(reason); - } else { - _selectedReasons.add(reason); - } - }); - } - - Future _submitReport() async { - if (!_isValid) return; - - Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { - return Dialog( - alignment: Alignment.center, - backgroundColor: AppColors.white, - insetPadding: AppPadding.screenHorizontal, - child: Container( - padding: AppPadding.actionPopup, - decoration: BoxDecoration( - borderRadius: AppRadius.popupRadius, - color: AppColors.white, - ), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center(child: Text("유저 신고", style: AppTextStyles.titleBold20())), - AppGap.v16, - SizedBox( - width: double.maxFinite, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.start, - spacing: AppSpacing.s12, - runSpacing: AppSpacing.s12, - children: _reasons.map( - (e) { - return ReportReasonChip( - label: e, - isSelected: _selectedReasons.contains(e), - onTap: () => _toggleReason(e), - ); - }, - ).toList(), - ), - ), - AppGap.v16, - CustomTextField( - controller: controller, - hintText: "상세 내용을 입력해주세요", - onChanged: (_) => setState(() {}), - minLines: 4, - maxLines: 4, - ), - AppGap.v16, - CustomButton( - text: "신고", - variant: ButtonVariant.primary, - enabled: _isValid, - onPressed: () => _submitReport(), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/presentation/search/widgets/search_popup.dart b/lib/presentation/search/widgets/search_popup.dart index 01ef182b..fd5cd12d 100644 --- a/lib/presentation/search/widgets/search_popup.dart +++ b/lib/presentation/search/widgets/search_popup.dart @@ -57,8 +57,7 @@ class _Tags extends GetView { .map( (tag) => CustomTagCard( onTap: (isSelect) => controller.selectSearchTag(tag), - tag: tag, - color: AppColors.gray20, + label: tag, ), ) .toList(), diff --git a/test/presentation/chat/view_models/chat_message_view_model_test.dart b/test/presentation/chat/view_models/chat_message_view_model_test.dart index d8409c0f..1bc73f77 100644 --- a/test/presentation/chat/view_models/chat_message_view_model_test.dart +++ b/test/presentation/chat/view_models/chat_message_view_model_test.dart @@ -18,7 +18,7 @@ void main() { expect(vm.messageId, 1); expect(vm.messageType, 'TEXT'); expect(vm.content, '안녕하세요'); - expect(vm.createdAt, DateTime.parse('2026-03-16T12:00:00')); + expect(vm.createdAt, DateTime.parse('2026-03-16T12:00:00Z').toLocal()); expect(vm.isMe, false); expect(vm.profileImageKey, isNull); });