From 96ccacf14a51b22d7d77d2c1a358829aeb2cc8b4 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 12:59:53 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor=20:=20"CustomTagCard=EB=A5=BC=20?= =?UTF-8?q?label/onTap=20=EA=B8=B0=EB=B0=98=20API=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 tag/color 파라미터를 label로 통일하고, 내부 선택 상태를 컴포넌트가 직접 관리하도록 정리하여 채팅/커뮤니티/검색 화면에서 동일한 디자인의 태그 카드를 일관되게 재사용할 수 있도록 했다. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- .../components/custom_tag_card.dart | 72 ++++++++++--------- .../chat/screens/chat_main_screen.dart | 2 +- .../chat/widgets/chat_review_dialog.dart | 3 +- .../widgets/community_filter_tag_list.dart | 2 +- .../search/widgets/search_popup.dart | 3 +- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/core/design_system/components/custom_tag_card.dart b/lib/core/design_system/components/custom_tag_card.dart index fd8c0417..6aaf3ca7 100644 --- a/lib/core/design_system/components/custom_tag_card.dart +++ b/lib/core/design_system/components/custom_tag_card.dart @@ -3,48 +3,50 @@ 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 CustomTagCard extends StatelessWidget { - final String tag; - final Color? color; - final void Function(bool isSelect)? onTap; - final ValueNotifier isSelect; +typedef SelectTag = void Function(bool isSelect); - CustomTagCard({ +class CustomTagCard extends StatefulWidget { + final String label; + final SelectTag? onTap; + + const CustomTagCard({ super.key, - required this.tag, - this.color, + required this.label, this.onTap, - bool isSelect = false, - }) : isSelect = ValueNotifier(isSelect); + }); + + @override + State createState() => _CustomTagCardState(); +} + +class _CustomTagCardState extends State { + bool isFocus = false; @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 = isFocus ? AppColors.white : AppColors.gray90; + final backgroundColor = isFocus ? AppColors.primary : AppColors.gray20; + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + isFocus = !isFocus; + widget.onTap?.call(isFocus); + setState(() {}); }, + child: Container( + padding: AppPadding.chip, + decoration: BoxDecoration( + borderRadius: AppRadius.baseRadius, + color: backgroundColor, + ), + child: Text( + widget.label, + style: AppTextStyles.textMedium( + textColor: foregroundColor, + ), + ), + ), ); } } diff --git a/lib/presentation/chat/screens/chat_main_screen.dart b/lib/presentation/chat/screens/chat_main_screen.dart index 58039a42..7f4953a9 100644 --- a/lib/presentation/chat/screens/chat_main_screen.dart +++ b/lib/presentation/chat/screens/chat_main_screen.dart @@ -41,7 +41,7 @@ class TagList extends GetView { padding: AppPadding.screenHorizontal, scrollDirection: Axis.horizontal, itemBuilder: (context, index) => CustomTagCard( - tag: tags[index], + label: tags[index], onTap: (isSelect) => controller.selectTag(tags[index], isSelect), ), itemCount: tags.length, diff --git a/lib/presentation/chat/widgets/chat_review_dialog.dart b/lib/presentation/chat/widgets/chat_review_dialog.dart index e1fbdaee..28e02abc 100644 --- a/lib/presentation/chat/widgets/chat_review_dialog.dart +++ b/lib/presentation/chat/widgets/chat_review_dialog.dart @@ -69,8 +69,7 @@ class ChatReviewDialog extends GetView { controller.baseCategories[index], isSelect, ), - tag: controller.baseCategories[index], - color: AppColors.gray20, + label: controller.baseCategories[index], ), ), ), diff --git a/lib/presentation/community/widgets/community_filter_tag_list.dart b/lib/presentation/community/widgets/community_filter_tag_list.dart index 21c9a4b0..f1750a01 100644 --- a/lib/presentation/community/widgets/community_filter_tag_list.dart +++ b/lib/presentation/community/widgets/community_filter_tag_list.dart @@ -24,7 +24,7 @@ class CommunityFilterTagList extends GetView { tags[index].$2, isSelect, ), - tag: tags[index].$2, + label: tags[index].$2, ); }, ), 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(), From 50d858e3138362f1325b7388706c6d896b037b48 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:01:26 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feature=20:=20"data/domain=20-=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EA=B8=B0=EB=8A=A5=20=ED=95=B5=EC=8B=AC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReportType/ReportReason enum, ReportRequestModel, ReportRemoteDatasource, ReportRepository/ReportRepositoryImpl, ReportUseCase 추가. ApiConstants에 report 엔드포인트 추가. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- lib/core/constants/report_type.dart | 26 +++++++++++++ .../report/report_remote_datasource.dart | 37 +++++++++++++++++++ .../report/request/report_request_model.dart | 19 ++++++++++ lib/data/network/constants/api_constants.dart | 2 + .../report/report_repository_impl.dart | 24 ++++++++++++ .../report/report_repository.dart | 9 +++++ .../usecases/report/report_use_case.dart | 16 ++++++++ 7 files changed, 133 insertions(+) create mode 100644 lib/core/constants/report_type.dart create mode 100644 lib/data/datasource/report/report_remote_datasource.dart create mode 100644 lib/data/models/report/request/report_request_model.dart create mode 100644 lib/data/repositories/report/report_repository_impl.dart create mode 100644 lib/domain/repositories/report/report_repository.dart create mode 100644 lib/domain/usecases/report/report_use_case.dart 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/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..cff57534 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 : 임시 로그인 데이터 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/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/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); + } +} From 918b82a4e90d7e26e198b7265473c1bcd2775900 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:01:44 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix=20:=20"ApiConstants.serverLog=20statu?= =?UTF-8?q?sCode=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stausCode 오타를 statusCode로 수정하고 호출부(PostRemoteDatasource)도 함께 정정. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- lib/data/datasource/post/post_remote_datasource.dart | 2 +- lib/data/network/constants/api_constants.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/network/constants/api_constants.dart b/lib/data/network/constants/api_constants.dart index cff57534..e8785ab7 100644 --- a/lib/data/network/constants/api_constants.dart +++ b/lib/data/network/constants/api_constants.dart @@ -33,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"]); From bb37ec21b74c176a4a700c0841cce65aa4e0c0be Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:02:10 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feature=20:=20"presentation=20-=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20CustomReportDialog=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=EA=B1=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostReportDialog/UserReportPopup을 ReportType 기반 CustomReportDialog로 통합하고 ReportBinding을 전역 등록하여 게시물/유저/채팅방 신고에서 공용으로 사용하도록 적용. ChatRoomController에 report() 추가 및 채팅방 메뉴 신고 기능 연동. ChatRoomController 메서드명 정리 (sendChat→sendMessage, backChatRoom→back, quitChat→quit). 중복 다이얼로그(PostReportDialog, UserReportPopup, ReportReasonChip) 제거. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- .../components/custom_report_dialog.dart} | 88 ++++++++------ .../router/bindings/navigation_binding.dart | 4 +- .../router/bindings/post_view_binding.dart | 2 - lib/core/router/bindings/report_binding.dart | 22 ++++ .../controllers/chat_room_controller.dart | 22 +++- .../chat/screens/chat_room_screen.dart | 33 +++--- .../controllers/post_view_controller.dart | 13 ++- .../post/screens/post_detail_screen.dart | 3 +- .../profile/screens/other_profile_screen.dart | 8 +- .../profile/widget/report_reason_chip.dart | 37 ------ .../profile/widget/user_report_popup.dart | 110 ------------------ 11 files changed, 131 insertions(+), 211 deletions(-) rename lib/{presentation/post/widgets/post_report_dialog.dart => core/design_system/components/custom_report_dialog.dart} (53%) create mode 100644 lib/core/router/bindings/report_binding.dart delete mode 100644 lib/presentation/profile/widget/report_reason_chip.dart delete mode 100644 lib/presentation/profile/widget/user_report_popup.dart 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..cb172be5 100644 --- a/lib/presentation/post/widgets/post_report_dialog.dart +++ b/lib/core/design_system/components/custom_report_dialog.dart @@ -1,56 +1,69 @@ - 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); - } - }); - } + _selectedReasons.isNotEmpty && controller.text.trim().isNotEmpty; Future _submitReport() async { if (!_isValid) return; + final description = [ + '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 +81,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 +89,19 @@ 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, + onTap: (isSelect) { + setState(() { + if (isSelect) { + _selectedReasons.add(r); + } else { + _selectedReasons.remove(r); + } + }); + }, ); }, ).toList(), 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..f9555c96 --- /dev/null +++ b/lib/core/router/bindings/report_binding.dart @@ -0,0 +1,22 @@ +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/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/presentation/chat/controllers/chat_room_controller.dart b/lib/presentation/chat/controllers/chat_room_controller.dart index 2fac61da..66ded162 100644 --- a/lib/presentation/chat/controllers/chat_room_controller.dart +++ b/lib/presentation/chat/controllers/chat_room_controller.dart @@ -4,7 +4,10 @@ import 'dart:convert'; import 'dart:developer'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.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 +405,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 +434,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { } } - Future _initLoadChatRoomMessages() async { + Future _initLoadChatRoomMessages() async { final res = await loadChatRoomMessageUseCase.call( chatRoomPublicId: chatRoomId, cursor: 0, @@ -487,7 +493,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 +523,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { textController.clear(); } - Future backChatRoom() async { + Future back() async { // WebSocket 읽음 이벤트 전송 sendReadEvent(); // HTTP 읽음 처리 유지 @@ -528,7 +534,7 @@ class ChatRoomController extends GetxController with WidgetsBindingObserver { ); } - Future quitChat() async { + Future quit() async { Get.dialog( CustomAlertDialog( title: '커피챗 종료', @@ -543,4 +549,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_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/post/controllers/post_view_controller.dart b/lib/presentation/post/controllers/post_view_controller.dart index 56f74267..994af275 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; @@ -375,9 +376,13 @@ 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(), + ), ); } 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/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(), - ), - ], - ), - ), - ), - ); - } -} From 712015308db0783236ac7fdc1e8510494932857b Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:02:33 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor=20:=20"ChatInputField=EC=97=90?= =?UTF-8?q?=EC=84=9C=20sendChat=E2=86=92sendMessage=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EB=B6=80=20=EC=A0=95=EB=A6=AC"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatRoomController의 메서드명 변경(sendChat→sendMessage)에 맞춰 호출부를 정리. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- lib/presentation/chat/widgets/chat_input_field.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From adb5dc122bd4889842b8b56ceb2fb359fe657d93 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:55:49 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor=20:=20"CustomTagCard=EB=A5=BC=20?= =?UTF-8?q?Stateless(isSelected)=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini 코드 리뷰 반영하여 CustomTagCard의 내부 상태(isFocus)를 제거하고 isSelected/onTap 기반 controlled 컴포넌트로 전환. 각 사용처는 부모 컨트롤러의 RxSet 상태(selectTagList, reviewTags, _selectedReasons)를 단일 진실 소스로 사용하도록 정리. 아울러 ListView.separated의 itemBuilder는 lazy하게 호출되어 Obx가 의존성을 추적하지 못하는 문제가 있어, Obx를 개별 아이템 단위로 감싸도록 수정 (Community 화면에서 발생한 GetX "improper use of Obx" 런타임 오류 수정). Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- .../components/custom_report_dialog.dart | 15 +++++++---- .../components/custom_tag_card.dart | 23 +++++----------- .../chat/screens/chat_main_screen.dart | 9 ++++--- .../chat/widgets/chat_review_dialog.dart | 27 +++++++++++-------- .../widgets/community_filter_tag_list.dart | 13 +++++---- 5 files changed, 47 insertions(+), 40 deletions(-) diff --git a/lib/core/design_system/components/custom_report_dialog.dart b/lib/core/design_system/components/custom_report_dialog.dart index cb172be5..925415a4 100644 --- a/lib/core/design_system/components/custom_report_dialog.dart +++ b/lib/core/design_system/components/custom_report_dialog.dart @@ -37,15 +37,19 @@ class _CustomReportDialogState extends State { }; bool get _isValid => - _selectedReasons.isNotEmpty && controller.text.trim().isNotEmpty; + _selectedReasons.isNotEmpty || controller.text.trim().isNotEmpty; Future _submitReport() async { if (!_isValid) return; - final description = [ - 'tags', - ..._selectedReasons.map((e) => e.label), - if (controller.text.trim().isNotEmpty) ...['detail', controller.text.trim()], + if (_selectedReasons.isNotEmpty) ...[ + 'tags', + ..._selectedReasons.map((e) => e.label), + ], + if (controller.text.trim().isNotEmpty) ...[ + 'detail', + controller.text.trim(), + ], ].join('\n'); await Get.find()( @@ -93,6 +97,7 @@ class _CustomReportDialogState extends State { (r) { return CustomTagCard( label: r.label, + isSelected: _selectedReasons.contains(r), onTap: (isSelect) { setState(() { if (isSelect) { diff --git a/lib/core/design_system/components/custom_tag_card.dart b/lib/core/design_system/components/custom_tag_card.dart index 6aaf3ca7..272885cb 100644 --- a/lib/core/design_system/components/custom_tag_card.dart +++ b/lib/core/design_system/components/custom_tag_card.dart @@ -5,35 +5,26 @@ import 'package:ondo/core/design_system/app_text_styles.dart'; typedef SelectTag = void Function(bool isSelect); -class CustomTagCard extends StatefulWidget { +class CustomTagCard extends StatelessWidget { final String label; + final bool isSelected; final SelectTag? onTap; const CustomTagCard({ super.key, required this.label, + this.isSelected = false, this.onTap, }); - @override - State createState() => _CustomTagCardState(); -} - -class _CustomTagCardState extends State { - bool isFocus = false; - @override Widget build(BuildContext context) { - final foregroundColor = isFocus ? AppColors.white : AppColors.gray90; - final backgroundColor = isFocus ? AppColors.primary : AppColors.gray20; + final foregroundColor = isSelected ? AppColors.white : AppColors.gray90; + final backgroundColor = isSelected ? AppColors.primary : AppColors.gray20; return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () { - isFocus = !isFocus; - widget.onTap?.call(isFocus); - setState(() {}); - }, + onTap: () => onTap?.call(!isSelected), child: Container( padding: AppPadding.chip, decoration: BoxDecoration( @@ -41,7 +32,7 @@ class _CustomTagCardState extends State { color: backgroundColor, ), child: Text( - widget.label, + label, style: AppTextStyles.textMedium( textColor: foregroundColor, ), diff --git a/lib/presentation/chat/screens/chat_main_screen.dart b/lib/presentation/chat/screens/chat_main_screen.dart index 7f4953a9..a0a257a6 100644 --- a/lib/presentation/chat/screens/chat_main_screen.dart +++ b/lib/presentation/chat/screens/chat_main_screen.dart @@ -40,9 +40,12 @@ class TagList extends GetView { child: ListView.separated( padding: AppPadding.screenHorizontal, scrollDirection: Axis.horizontal, - itemBuilder: (context, index) => CustomTagCard( - label: 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/widgets/chat_review_dialog.dart b/lib/presentation/chat/widgets/chat_review_dialog.dart index 28e02abc..adc59d58 100644 --- a/lib/presentation/chat/widgets/chat_review_dialog.dart +++ b/lib/presentation/chat/widgets/chat_review_dialog.dart @@ -58,18 +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], ), - label: controller.baseCategories[index], ), ), ), diff --git a/lib/presentation/community/widgets/community_filter_tag_list.dart b/lib/presentation/community/widgets/community_filter_tag_list.dart index f1750a01..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, ), - label: tags[index].$2, ); }, ), From 2b3cc3cf9ae929939947825287535c3777d70564 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:56:02 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix=20:=20"ChatRoomController=EC=9D=98=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20import=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit material.dart가 이미 re-export하는 cupertino.dart, widgets.dart import를 제거하여 flutter analyze의 unnecessary_import 경고를 해소. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- lib/presentation/chat/controllers/chat_room_controller.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/presentation/chat/controllers/chat_room_controller.dart b/lib/presentation/chat/controllers/chat_room_controller.dart index 66ded162..4e45427c 100644 --- a/lib/presentation/chat/controllers/chat_room_controller.dart +++ b/lib/presentation/chat/controllers/chat_room_controller.dart @@ -3,9 +3,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.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'; From 7f70bbbead9b393a9f06997143d185054311652d Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:56:17 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix=20:=20"chat=5Fmessage=5Fview=5Fmodel?= =?UTF-8?q?=5Ftest=EC=9D=98=20createdAt=20=EA=B8=B0=EB=8C=80=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20UTC=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppDateUtils.tryParseUtc는 timezone 정보가 없는 서버 타임스탬프를 UTC로 간주하여 .toLocal()로 변환하는데, 기존 테스트는 변환 없이 로컬 시간으로 해석한 값을 기대해 KST 환경에서 실패함. 구현체와 동일하게 UTC로 파싱 후 .toLocal()한 값을 기대값으로 수정. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- .../chat/view_models/chat_message_view_model_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); }); From 448a6fe2436850b19e981c22c184e79c06a8b3d6 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 13:57:11 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor=20:=20"ReportBinding=EC=9D=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EB=8C=80=EC=8B=A0=20ReportReposi?= =?UTF-8?q?tory=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A1=9C?= =?UTF-8?q?=20DI"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get.lazyPut/Get.find 시 ReportRepositoryImpl 구현체 타입 대신 ReportRepository 인터페이스 타입을 사용하도록 수정하여 의존성 역전 원칙을 준수. Resolve: #252 Co-Authored-By: Claude Sonnet 4.6 --- lib/core/router/bindings/report_binding.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/router/bindings/report_binding.dart b/lib/core/router/bindings/report_binding.dart index f9555c96..487fee3b 100644 --- a/lib/core/router/bindings/report_binding.dart +++ b/lib/core/router/bindings/report_binding.dart @@ -2,6 +2,7 @@ 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 { @@ -11,12 +12,12 @@ class ReportBinding extends Bindings { () => ReportRemoteDatasource(Get.find()), ); - Get.lazyPut( + Get.lazyPut( () => ReportRepositoryImpl(Get.find()), ); Get.lazyPut( - () => ReportUseCase(Get.find()), + () => ReportUseCase(Get.find()), ); } } From cabdfeb23071bdca9a4c4bdc8c51523da2b1c4f0 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 15:25:31 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor=20:=20"CommentRemoteDataSource.c?= =?UTF-8?q?reateComment=EC=9D=B4=20=EC=84=B1=EA=B3=B5=20=EC=97=AC=EB=B6=80?= =?UTF-8?q?(bool)=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 댓글 생성 API 응답에 생성된 댓글 데이터가 포함되지 않아, CommentModel 대신 success 여부만 반환하도록 정리 Co-Authored-By: Claude Sonnet 4.6 --- .../datasource/comment/comment_remote_datasource.dart | 9 +++++++-- .../repositories/comment/comment_repository_impl.dart | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) 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/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, ); From 6b90b0b9e781eb2e8dc5c532a7fe7c7f55579af9 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 15:25:37 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor=20:=20"CommentRepository/CreateC?= =?UTF-8?q?ommentUseCase=EC=9D=98=20createComment=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=98=95=EC=9D=84=20bool=EB=A1=9C=20=EB=B3=80=EA=B2=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit data 레이어 변경에 맞춰 인터페이스 및 유스케이스 시그니처를 success 여부(bool) 기준으로 정리 Co-Authored-By: Claude Sonnet 4.6 --- lib/domain/repositories/comment/comment_repository.dart | 2 +- lib/domain/usecases/comment/create_comment_usecase.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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 +} From f62422b4c3c929747f19a460ad062f2625d0aa82 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 15:25:54 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feature=20:=20"=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=A1=B1=ED=94=84=EB=A0=88=EC=8A=A4=20=EC=8B=9C=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=ED=95=98=EA=B8=B0=20=EB=A9=94=EB=89=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostCommentCard에서 댓글을 길게 누르면 신고 메뉴가 뜨고, 선택 시 CustomReportDialog로 댓글 신고 가능 PostViewController에 reportComment 추가 댓글 삭제 시 fetchComments() 대신 로컬 목록에서 바로 제거하도록 변경 Co-Authored-By: Claude Sonnet 4.6 --- .../controllers/post_view_controller.dart | 12 ++- .../post/widgets/post_comment_card.dart | 79 +++++++++++++------ 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/lib/presentation/post/controllers/post_view_controller.dart b/lib/presentation/post/controllers/post_view_controller.dart index 994af275..954dccfc 100644 --- a/lib/presentation/post/controllers/post_view_controller.dart +++ b/lib/presentation/post/controllers/post_view_controller.dart @@ -364,7 +364,7 @@ class PostViewController extends GetxController { await _deleteCommentUseCase(comment.id); - await fetchComments(); + comments.removeWhere((c) => c.id == comment.id); commentCount.value = comments.length; @@ -386,6 +386,16 @@ class PostViewController extends GetxController { ); } + void reportComment(BuildContext context, CommentEntity comment) { + showDialog( + context: context, + builder: (context) => CustomReportDialog( + type: ReportType.comment, + targetId: comment.id.toString(), + ), + ); + } + void editPost() { Get.delete(force: true); Get.lazyPut( 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 +} From 0a671745cf71014a3b3d021dbc6dfb2616ad89f4 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 15:26:00 +0900 Subject: [PATCH 13/14] =?UTF-8?q?style=20:=20"CustomPopupMenuButton=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- lib/core/design_system/components/custom_popup_menu_button.dart | 1 + 1 file changed, 1 insertion(+) 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, From 014fd60548f6eee4bbab46d4981964c656591e12 Mon Sep 17 00:00:00 2001 From: eum108 Date: Mon, 15 Jun 2026 15:26:07 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feature=20:=20"ChatMainScreen=EC=9D=98=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EB=AA=A9=EB=A1=9D=EC=97=90=20=EC=A2=8C?= =?UTF-8?q?=EC=9A=B0=20=ED=8C=A8=EB=94=A9=20=EC=A0=81=EC=9A=A9"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatList를 화면 가로 패딩으로 감싸 다른 화면과 여백 통일 Co-Authored-By: Claude Sonnet 4.6 --- lib/presentation/chat/screens/chat_main_screen.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/presentation/chat/screens/chat_main_screen.dart b/lib/presentation/chat/screens/chat_main_screen.dart index a0a257a6..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, ], ),