Skip to content
Merged
2 changes: 2 additions & 0 deletions lib/models/apis/objects/chat_room_detail_dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ChatRoomDetailDto {
final int? unreadCount;
final ChatRoomType? chatRoomType;
final String? targetItemImageUrl;
final String? myItemImageUrl;
final bool? blocked;

ChatRoomDetailDto({
Expand All @@ -26,6 +27,7 @@ class ChatRoomDetailDto {
this.unreadCount,
this.chatRoomType,
this.targetItemImageUrl,
this.myItemImageUrl,
this.blocked,
});

Expand Down
2 changes: 2 additions & 0 deletions lib/models/apis/objects/chat_room_detail_dto.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/screens/chat_tab_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ class _ChatTabScreenState extends State<ChatTabScreen> 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;
Expand Down
87 changes: 62 additions & 25 deletions lib/widgets/chat_room_list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -79,7 +116,6 @@ class ChatRoomListItem extends StatelessWidget {

SizedBox(width: 8.h),

// 둘째 줄: 장소 • 시간
// 장소
Text(
location,
Expand All @@ -91,7 +127,7 @@ class ChatRoomListItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
SizedBox(width: 2.w),
// 중간점
// 구분점
Container(
width: 2.w,
height: 2.h,
Expand All @@ -114,7 +150,7 @@ class ChatRoomListItem extends StatelessWidget {

SizedBox(height: 7.h),

// 셋째 줄: 메시지 미리보기
// 둘째 줄: 메시지 미리보기 + 내 물품 사진 + 읽지 않은 메시지 뱃지
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expand All @@ -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),
Expand All @@ -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,
Expand All @@ -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),
Expand Down
76 changes: 60 additions & 16 deletions lib/widgets/chat_trade_info_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(),
),
),
Expand Down
2 changes: 1 addition & 1 deletion lib/widgets/trade_request_target_preview.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading