diff --git a/lib/models/apis/objects/chat_room_detail_dto.dart b/lib/models/apis/objects/chat_room_detail_dto.dart index faf15af9..f4815c09 100644 --- a/lib/models/apis/objects/chat_room_detail_dto.dart +++ b/lib/models/apis/objects/chat_room_detail_dto.dart @@ -15,6 +15,7 @@ class ChatRoomDetailDto { final int? unreadCount; final ChatRoomType? chatRoomType; final String? targetItemImageUrl; + final String? myItemImageUrl; final bool? blocked; ChatRoomDetailDto({ @@ -26,6 +27,7 @@ class ChatRoomDetailDto { this.unreadCount, this.chatRoomType, this.targetItemImageUrl, + this.myItemImageUrl, this.blocked, }); diff --git a/lib/models/apis/objects/chat_room_detail_dto.g.dart b/lib/models/apis/objects/chat_room_detail_dto.g.dart index 34f972a0..78974d29 100644 --- a/lib/models/apis/objects/chat_room_detail_dto.g.dart +++ b/lib/models/apis/objects/chat_room_detail_dto.g.dart @@ -15,6 +15,7 @@ ChatRoomDetailDto _$ChatRoomDetailDtoFromJson(Map json) => Chat unreadCount: (json['unreadCount'] as num?)?.toInt(), chatRoomType: $enumDecodeNullable(_$ChatRoomTypeEnumMap, json['chatRoomType']), targetItemImageUrl: json['targetItemImageUrl'] as String?, + myItemImageUrl: json['myItemImageUrl'] as String?, blocked: json['blocked'] as bool?, ); @@ -27,6 +28,7 @@ Map _$ChatRoomDetailDtoToJson(ChatRoomDetailDto instance) => with TickerProviderStateM messagePreview: chatRoomDetail.lastMessageContent ?? '', unreadCount: chatRoomDetail.unreadCount ?? 0, targetItemImageUrl: chatRoomDetail.targetItemImageUrl, + myItemImageUrl: chatRoomDetail.myItemImageUrl ?? '', isNew: chatRoomDetail.unreadCount != null && chatRoomDetail.unreadCount! > 0, onProfileTap: () { final targetMember = chatRoomDetail.targetMember; diff --git a/lib/widgets/chat_room_list_item.dart b/lib/widgets/chat_room_list_item.dart index 4da51b7e..02874c0c 100644 --- a/lib/widgets/chat_room_list_item.dart +++ b/lib/widgets/chat_room_list_item.dart @@ -16,6 +16,7 @@ class ChatRoomListItem extends StatelessWidget { final String timeAgo; final String messagePreview; final String? targetItemImageUrl; + final String? myItemImageUrl; final int unreadCount; final bool isNew; final VoidCallback onTap; @@ -31,6 +32,7 @@ class ChatRoomListItem extends StatelessWidget { required this.timeAgo, required this.messagePreview, this.targetItemImageUrl, + this.myItemImageUrl, this.unreadCount = 0, this.isNew = false, required this.onTap, @@ -39,36 +41,71 @@ class ChatRoomListItem extends StatelessWidget { @override Widget build(BuildContext context) { + final double itemImageSize = 48.w; + final double profileAvatarSize = 22.w; + final bool isDeletedAccount = accountStatus == AccountStatus.deleteAccount.serverName; + return GestureDetector( onTap: onTap, child: Container( color: AppColors.transparent, - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h), + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h), child: Row( children: [ - // 1. 프로필 이미지 (40×40, 원형) - GestureDetector( - onTap: onProfileTap, - child: UserProfileCircularAvatar( - avatarSize: Size(40.w, 40.h), - profileUrl: profileImageUrl, - hasBorder: true, - isDeleteAccount: accountStatus == AccountStatus.deleteAccount.serverName, - ), + // 왼쪽: 물품 이미지 + 우하단 프로필 아바타 오버레이 + Stack( + clipBehavior: Clip.none, + children: [ + SizedBox( + width: itemImageSize, + height: itemImageSize, + child: ClipRRect( + borderRadius: BorderRadius.circular(4.r), + child: targetItemImageUrl != null + ? CachedImage( + imageUrl: targetItemImageUrl!, + fit: BoxFit.cover, + errorWidget: Container(color: AppColors.imagePlaceholderBackground), + ) + : Container(color: AppColors.imagePlaceholderBackground), + ), + ), + + // 프로필 아바타 (22×22, 원형, 우하단 오버레이) + Positioned( + right: -6.w, + bottom: -6.h, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: AppColors.primaryBlack, width: 2.w), + ), + child: GestureDetector( + onTap: onProfileTap, + child: UserProfileCircularAvatar( + avatarSize: Size(profileAvatarSize, profileAvatarSize), + profileUrl: profileImageUrl, + hasBorder: false, + isDeleteAccount: isDeletedAccount, + ), + ), + ), + ), + ], ), - SizedBox(width: 16.w), + SizedBox(width: 10.w), - // 2. 중앙 정보 영역 + // 중앙 정보 영역 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 첫 줄: 닉네임 • 장소 • 시간 Row( children: [ - // 첫 줄: 닉네임 Text( - accountStatus == AccountStatus.deleteAccount.serverName ? '(탈퇴)$nickname' : nickname, + isDeletedAccount ? '(탈퇴)$nickname' : nickname, style: CustomTextStyles.p1.copyWith( color: AppColors.textColorWhite, fontWeight: FontWeight.w500, @@ -79,7 +116,6 @@ class ChatRoomListItem extends StatelessWidget { SizedBox(width: 8.h), - // 둘째 줄: 장소 • 시간 // 장소 Text( location, @@ -91,7 +127,7 @@ class ChatRoomListItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), SizedBox(width: 2.w), - // 중간점 + // 구분점 Container( width: 2.w, height: 2.h, @@ -114,7 +150,7 @@ class ChatRoomListItem extends StatelessWidget { SizedBox(height: 7.h), - // 셋째 줄: 메시지 미리보기 + // 둘째 줄: 메시지 미리보기 + 내 물품 사진 + 읽지 않은 메시지 뱃지 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -126,7 +162,8 @@ class ChatRoomListItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - // 3. 읽지 않은 메시지 뱃지 + + // 읽지 않은 메시지 뱃지 (99 초과 시 '99+' 표시) if (unreadCount > 0) ...[ Padding( padding: EdgeInsets.only(right: 10.0.w, left: 8.w), @@ -136,7 +173,6 @@ class ChatRoomListItem extends StatelessWidget { decoration: const BoxDecoration(shape: BoxShape.circle, color: AppColors.chatUnreadBadge), alignment: Alignment.center, child: Text( - // 99 초과 시 '99+' 표시 unreadCount > 99 ? '99+' : '$unreadCount', style: CustomTextStyles.p3.copyWith( color: AppColors.textColorWhite, @@ -151,16 +187,17 @@ class ChatRoomListItem extends StatelessWidget { ], ), ), + + // 오른쪽: 물품 이미지 SizedBox( - width: 40.w, - height: 40.w, + width: itemImageSize, + height: itemImageSize, child: ClipRRect( - borderRadius: BorderRadius.circular(8.r), - child: targetItemImageUrl != null + borderRadius: BorderRadius.circular(4.r), + child: myItemImageUrl != null ? CachedImage( - imageUrl: targetItemImageUrl!, + imageUrl: myItemImageUrl!, fit: BoxFit.cover, - errorWidget: Container(color: AppColors.imagePlaceholderBackground), ) : Container(color: AppColors.imagePlaceholderBackground), diff --git a/lib/widgets/chat_trade_info_card.dart b/lib/widgets/chat_trade_info_card.dart index 39b3e3cb..ea218fee 100644 --- a/lib/widgets/chat_trade_info_card.dart +++ b/lib/widgets/chat_trade_info_card.dart @@ -6,9 +6,12 @@ import 'package:romrom_fe/models/apis/objects/item.dart'; import 'package:romrom_fe/models/app_colors.dart'; import 'package:romrom_fe/models/app_theme.dart'; import 'package:romrom_fe/screens/item_detail_description_screen.dart'; +import 'package:romrom_fe/screens/profile/member_profile_screen.dart'; import 'package:romrom_fe/utils/common_utils.dart'; import 'package:romrom_fe/widgets/common/cached_image.dart'; +import 'package:romrom_fe/widgets/common/common_modal.dart'; import 'package:romrom_fe/widgets/common/request_management_trade_option_tag.dart'; +import 'package:romrom_fe/widgets/user_profile_circular_avatar.dart'; /// 채팅방 상단 거래 아이템 정보 카드 class ChatTradeInfoCard extends StatelessWidget { @@ -22,6 +25,15 @@ class ChatTradeInfoCard extends StatelessWidget { final isMyTakeItem = chatRoom.tradeRequestHistory?.takeItem.member?.memberId == myMemberId; final targetItem = isMyTakeItem ? chatRoom.tradeRequestHistory?.giveItem : chatRoom.tradeRequestHistory?.takeItem; final myItem = isMyTakeItem ? chatRoom.tradeRequestHistory?.takeItem : chatRoom.tradeRequestHistory?.giveItem; + final opponentId = isMyTakeItem + ? chatRoom.tradeRequestHistory?.giveItem.member?.memberId + : chatRoom.tradeRequestHistory?.takeItem.member?.memberId; + final isDeletedAccount = isMyTakeItem + ? chatRoom.tradeRequestHistory?.giveItem.member?.accountStatus == 'DELETE_ACCOUNT' + : chatRoom.tradeRequestHistory?.takeItem.member?.accountStatus == 'DELETE_ACCOUNT'; + final profileImageUrl = isMyTakeItem + ? chatRoom.tradeRequestHistory?.giveItem.member?.profileUrl + : chatRoom.tradeRequestHistory?.takeItem.member?.profileUrl; final tradeOptions = chatRoom.tradeRequestHistory?.itemTradeOptions ?? []; return Container( @@ -33,21 +45,53 @@ class ChatTradeInfoCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: () => _navigateToItem( - context, - itemId: targetItem?.itemId, - isMyItem: false, - heroTag: 'first_item_${targetItem?.itemId}', - imageUrl: targetItem?.primaryImageUrl, - ), - child: CachedImage( - imageUrl: targetItem?.primaryImageUrl ?? '', - width: 48.w, - height: 48.w, - borderRadius: BorderRadius.circular(8.r), - errorWidget: const SizedBox.shrink(), - ), + Stack( + clipBehavior: Clip.none, + children: [ + GestureDetector( + onTap: () => _navigateToItem( + context, + itemId: targetItem?.itemId, + isMyItem: false, + heroTag: 'first_item_${targetItem?.itemId}', + imageUrl: targetItem?.primaryImageUrl, + ), + child: CachedImage( + imageUrl: targetItem?.primaryImageUrl ?? '', + width: 48.w, + height: 48.w, + borderRadius: BorderRadius.circular(4.r), + errorWidget: const SizedBox.shrink(), + ), + ), + + // 프로필 아바타 (22×22, 원형, 우하단 오버레이) + Positioned( + right: -6.w, + bottom: -6.h, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: AppColors.primaryBlack, width: 2.w), + ), + child: GestureDetector( + onTap: opponentId != null + ? () => context.navigateTo(screen: MemberProfileScreen(memberId: opponentId)) + : () => CommonModal.error( + context: context, + message: '존재하지 않는 회원입니다.', + onConfirm: () => Navigator.of(context).pop(), + ), + child: UserProfileCircularAvatar( + avatarSize: Size(22.w, 22.w), + profileUrl: profileImageUrl, + hasBorder: false, + isDeleteAccount: isDeletedAccount, + ), + ), + ), + ), + ], ), SizedBox(width: 16.w), Expanded( @@ -99,7 +143,7 @@ class ChatTradeInfoCard extends StatelessWidget { imageUrl: myItem?.itemImages?.firstOrNull?.imageUrl ?? '', width: 48.w, height: 48.w, - borderRadius: BorderRadius.circular(8.r), + borderRadius: BorderRadius.circular(4.r), errorWidget: const SizedBox.shrink(), ), ), diff --git a/lib/widgets/trade_request_target_preview.dart b/lib/widgets/trade_request_target_preview.dart index 1fa95000..10ec543c 100644 --- a/lib/widgets/trade_request_target_preview.dart +++ b/lib/widgets/trade_request_target_preview.dart @@ -32,7 +32,7 @@ class TradeRequestTargetPreview extends StatelessWidget { children: [ // 물품 이미지 썸네일 ClipRRect( - borderRadius: BorderRadius.circular(10.r), + borderRadius: BorderRadius.circular(4.r), child: SizedBox(width: 48.w, height: 48.w, child: _buildImage()), ), SizedBox(width: 16.w),