본문 바로가기

Flutter/12 Clone 당근마켓

[Flutter] Clone - 당근마켓49(Chat - 1)

이번에는 채팅 관련 함수들 및 채팅 화면 추가를 구현해 보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

화면 구현은 아래와 같습니다.

 

 

./src/repo/chat_service.dart - 채팅관련 함수들 추가(채팅룸 생성, 채팅생성, 채팅룸 연결 등등)

 

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';

import '../constants/data_keys.dart';
import '../models/chat_model.dart';
import '../models/chatroom_model.dart';

class ChatService {
// 싱글톤 패턴 적용
  static final ChatService _chatService = ChatService._internal();
  factory ChatService() => _chatService;
  ChatService._internal();

// 채팅룸을 만드는 함수
  Future createNewChatroom(ChatroomModel2 chatroomModel) async {
    var chatroomKey = ChatroomModel2.generateChatRoomKey(
        buyer: chatroomModel.buyerKey, itemKey: chatroomModel.itemKey);

    DocumentReference<Map<String, dynamic>> docRef =
        FirebaseFirestore.instance.collection(COL_CHATROOMS).doc(chatroomKey);

    final DocumentSnapshot documentSnapshot = await docRef.get();

    if (!documentSnapshot.exists) {
      await docRef.set(chatroomModel.toJson());
    }
  }

// 채팅룸 내부에 메시지를 저장하는 함수. 채팅룸 내부에 sub collection 구조로 저장.
  Future createNewChat(String chatroomKey, ChatModel2 chatModel) async {
    // col chatRoom > doc chartoomKey > col chats > doc ...
    // 실제 채팅이 저장되는 위치
    DocumentReference<Map<String, dynamic>> chatDocRef = FirebaseFirestore.instance
        .collection(COL_CHATROOMS)
        .doc(chatroomKey)
        .collection(COL_CHATS)
        .doc();

    DocumentReference<Map<String, dynamic>> chatroomDocRef =
        FirebaseFirestore.instance.collection(COL_CHATROOMS).doc(chatroomKey);

    // await chatDocRef.set(chatModel.toJson());

    // 실제 채팅을 저장하고, 
    // 동시에 채팅룸 정보가 저장된 doc 에 마지막 메시지 정보를 업뎃하여 화면표시가 쉽게 처리.
    await FirebaseFirestore.instance.runTransaction((transaction) async {
      transaction.set(chatDocRef, chatModel.toJson());
      transaction.update(chatroomDocRef, {
        DOC_LASTMSG: chatModel.msg,
        DOC_LASTMSGTIME: chatModel.createdDate,
        DOC_LASTMSGUSERKEY: chatModel.userKey
      });
    });
  }

//todo: get chatroom detail
  // 스트림으로 모든 채팅을 처리하려면 비용적으로나 시간적(데이터 받는)으로 비용이 너무 큼,
  // 1. 채팅룸의 doc 을 스트림으로 연결하고 변경이 생기면 최근 메시지만 받는다,
  // 2. 새로운 메시지가 생기면 새로운 메시지들만 가져온다(중복해서 가져오지 않기)
  // 3. 스크롤링을 해서 마지막 메시지를 표시하면 다시 직전의 메시지 10개를 가져오게 처리할 것,

  // DocumentSnapshot<Map<String, dynamic>> 받는 값을 의미.
  // ChatroomModel2 출력 결과를 의미.
  var snapshotToChatroom =
      StreamTransformer<DocumentSnapshot<Map<String, dynamic>>, ChatroomModel2>.fromHandlers(
          handleData: (snapshot, sink) {
    ChatroomModel2 chatroom = ChatroomModel2.fromJson(snapshot.data()!);
    chatroom.chatroomKey = snapshot.id;
    chatroom.reference = snapshot.reference;

    sink.add(chatroom);
  });

  Stream<ChatroomModel2> connectChatroom(String chatroomKey) {
    // transform 통해서 DocumentSnapshot 을 ChatroomModel2 로 변환이 필요함
    return FirebaseFirestore.instance
        .collection(COL_CHATROOMS)
        .doc(chatroomKey)
        .snapshots()
        .transform(snapshotToChatroom);
  }

//todo: get char list
  Future<List<ChatModel2>> getChatList(String chatroomKey) async {
    QuerySnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore.instance
        .collection(COL_CHATROOMS)
        .doc(chatroomKey)
        .collection(COL_CHATS)
        .orderBy(DOC_CREATEDDATE, descending: true)
        .limit(10)
        .get();

    List<ChatModel2> chatList = [];

    for (var docSnapshot in snapshot.docs) {
      ChatModel2 chatModel = ChatModel2.fromJson(docSnapshot.data());
      chatModel.chatKey = docSnapshot.id;
      chatModel.reference = docSnapshot.reference;
      chatList.add(chatModel);
    }
    return chatList;
  }

//todo: latest chats
  Future<List<ChatModel2>> getLatestChats(
      String chatroomKey, DocumentReference currentLatestChatRef) async {
    QuerySnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore.instance
        .collection(COL_CHATROOMS)
        .doc(chatroomKey)
        .collection(COL_CHATS)
        .orderBy(DOC_CREATEDDATE, descending: true)
        // .endAtDocument(await currentLatestChatRef.get())
        .endBeforeDocument(await currentLatestChatRef.get())
        .get();

    List<ChatModel2> chatList = [];

    for (var docSnapshot in snapshot.docs) {
      ChatModel2 chatModel = ChatModel2.fromJson(docSnapshot.data());
      chatModel.chatKey = docSnapshot.id;
      chatModel.reference = docSnapshot.reference;
      chatList.add(chatModel);
    }
    return chatList;
  }

//todo: older chats
  Future<List<ChatModel2>> getOlderChats(
      String chatroomKey, DocumentReference oldestChatRef) async {
    QuerySnapshot<Map<String, dynamic>> snapshot = await FirebaseFirestore.instance
        .collection(COL_CHATROOMS)
        .doc(chatroomKey)
        .collection(COL_CHATS)
        .orderBy(DOC_CREATEDDATE, descending: true)
        .startAfterDocument(await oldestChatRef.get())
        .limit(10)
        .get();

    List<ChatModel2> chatList = [];

    for (var docSnapshot in snapshot.docs) {
      ChatModel2 chatModel = ChatModel2.fromJson(docSnapshot.data());
      chatModel.chatKey = docSnapshot.id;
      chatModel.reference = docSnapshot.reference;
      chatList.add(chatModel);
    }
    return chatList;
  }

}

 

 

 

./src/screens/home/item_detail_page.dart - 채팅룸 준비 및 이동 기능 추가, 그외 기타 버그 수정.

 

더보기
import 'package:apple_market3/src/screens/home/similar_item.dart';
import 'package:apple_market3/src/states/category_controller.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';

import '../../constants/common_size.dart';
import '../../constants/data_keys.dart';
import '../../models/chatroom_model.dart';
import '../../models/item_model.dart';
import '../../models/user_model.dart';
import '../../repo/chat_service.dart';
import '../../repo/item_service.dart';

import '../../states/category_controller.dart';
import '../../states/user_controller.dart';
import '../../utils/logger.dart';
import '../../utils/time_calculation.dart';

class ItemDetailPage extends StatefulWidget {
  const ItemDetailPage({Key? key}) : super(key: key);

  @override
  _ItemDetailPageState createState() => _ItemDetailPageState();
}

class _ItemDetailPageState extends State<ItemDetailPage> {
  final PageController _pageController = PageController();

  // 스크롤이 얼마나 되었는지 알기 위해서 컨트롤러 등록,
  final ScrollController _scrollController = ScrollController();

  // isAppbarCollapsed 이미지가 화면에서 사라졌는지 확인,
  bool isAppbarCollapsed = false;
  Size? _size;

  num? _statusBarHeight;
  late String newItemKey;

  final Widget _textGap = const SizedBox(height: padding_16);

  Widget _divider(double _height) {
    return Divider(
      height: _height, //padding_16 * 2 + 1,
      thickness: 2,
      color: Colors.grey[300]!,
      // indent: padding_08,
      // endIndent: padding_08,
    );
  }

  @override
  void initState() {
    newItemKey = Get.arguments['itemKey'];
    logger.d('$_size!.width, $kToolbarHeight, $_statusBarHeight, ${isAppbarCollapsed.toString()}');

    // 스크롤이 발생할때 마다 addListener 가 실행됨,
    _scrollController.addListener(() {
      if (_size == null && _statusBarHeight == null) return;

      if (isAppbarCollapsed) {
        // 여기는 이미지가 앱바 아래로 보여지기 시작하는 시점,
        // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
        if (_scrollController.offset < _size!.width - kToolbarHeight - _statusBarHeight!) {
          isAppbarCollapsed = false;
          setState(() {});
        }
      } else {
        // 여기는 이미지가 앱바에 위로 올라가서 안보이기 시작하는 시점,
        // 앱바 사이즈(kToolbarHeight), 상태바 사이즈(_statusBarHeight)
        if (_scrollController.offset > _size!.width - kToolbarHeight - _statusBarHeight!) {
          isAppbarCollapsed = true;
          setState(() {});
        }
      }
    });
    super.initState();
  }

  @override
  void dispose() {
    _pageController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  void _goToChatroom(ItemModel2 itemModel, UserModel1 userModel) async {
    String chatroomKey =
        ChatroomModel2.generateChatRoomKey(buyer: userModel.userKey, itemKey: newItemKey);
    logger.d({
      'buyerKey': '[${userModel.userKey}]',
      'sellerKey': '[${itemModel.userKey}]',
      'newItemKey': '[$newItemKey]',
      'itemKey': '[${itemModel.itemKey}]',
    });

    ChatroomModel2 _chatroomModel = ChatroomModel2(
      itemImage: itemModel.imageDownloadUrls[0],
      itemTitle: itemModel.title,
      itemKey: newItemKey,
      itemAddress: itemModel.address,
      itemPrice: itemModel.price,
      sellerKey: itemModel.userKey,
      buyerKey: userModel.userKey,
      sellerImage: 'https://minimaltoolkit.com/images/randomdata/male/101.jpg',
      buyerImage: 'https://minimaltoolkit.com/images/randomdata/female/41.jpg',
      geoFirePoint: itemModel.geoFirePoint,
      chatroomKey: chatroomKey,
      lastMsgTime: DateTime.now().toUtc(),
    );
    await ChatService().createNewChatroom(_chatroomModel);

    // context.beamToNamed('/$LOCATION_ITEM/:$newItemKey/:$chatroomKey');
    // Get.toNamed("/user/1424?name=Flutter&age=22");
    Get.toNamed("/$ROUTE_ITEM_DETAIL/$chatroomKey");
  }

  @override
  Widget build(BuildContext context) {
    logger.d('item detail screen >> build >>> [$newItemKey]');

    return FutureBuilder<ItemModel2>(
      future: ItemService().getItem(newItemKey),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          // FutureBuilder 의 snapshot 에서 게시글 데이터 가져오기
          ItemModel2 itemModel = snapshot.data!;
          // provider 입포트하고 고객 데이터 가져오기
          // UserModel2 userModel = context.read<UserNotifier>().userModel!;
          UserModel1 userModel = UserController.to.userModel.value!;

          return LayoutBuilder(
            builder: (context, constraints) {
              _size = MediaQuery.of(context).size;
              // 상태바 길이 가져오는 공식,
              _statusBarHeight = MediaQuery.of(context).padding.top;
              return Stack(
                // 스택에서는 하단 위젯부터 화면에서는 front 에 위치,
                // fit 은 Stack 에 있는 모든 아이콘들이 화면에 가득차게 하는 옵션,
                fit: StackFit.expand,
                children: [
                  // 메인 정보를 표시하는 영역
                  Scaffold(
                    // 화면 하단을 네비로 표시함
                    bottomNavigationBar: SafeArea(
                      // SafeArea - 전화기의 하단 메뉴바 영역과 앱 영역이 중복되는거 피하기 위해서,
                      top: false,
                      bottom: true, // 아래 영역만 피하기 위해서,
                      child: Container(
                        // Row/VerticalDivider 의 사이즈를 제한하기 위해서 Container&height 처리함,
                        height: 80,
                        decoration: BoxDecoration(
                          border: Border(
                            top: BorderSide(color: Colors.grey[300]!),
                            bottom: BorderSide(color: Colors.grey[300]!),
                          ),
                        ),
                        child: Padding(
                          padding: const EdgeInsets.all(padding_08),
                          child: Row(
                            children: [
                              IconButton(
                                onPressed: () {},
                                icon: const Icon(Icons.favorite_border),
                              ),
                              const VerticalDivider(
                                thickness: 1,
                                width: padding_08 * 2 + 1,
                                indent: padding_08,
                                endIndent: padding_08,
                              ),
                              Column(
                                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                                // crossAxisAlignment: CrossAxisAlignment.auth,
                                children: [
                                  Text(
                                    itemModel.price.toString(),
                                    style: Theme.of(context).textTheme.bodyText1,
                                  ),
                                  Text(
                                    itemModel.negotiable ? '가격제안가능' : '가격제안불가',
                                    style: itemModel.negotiable
                                        ? Theme.of(context)
                                            .textTheme
                                            .bodyText2!
                                            .copyWith(color: Colors.blue)
                                        : Theme.of(context).textTheme.bodyText2,
                                  ),
                                ],
                              ),
                              Expanded(child: Container()),
                              TextButton(
                                onPressed: () {
                                  _goToChatroom(itemModel, userModel);
                                },
                                child: const Text('채팅으로 거래하기'),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                    // 메인정보를 표시, CustomScrollView 는 listView 유사함
                    // listView 대신에 CustomScrollView 사용하는 이유는
                    // slivers 를 이용해서 화면을 구역으로 나눠서 각 구역마다 슬라이스를 구현할 수 있다,
                    body: CustomScrollView(
                      controller: _scrollController,
                      // children 을 대신하는 slivers 있고, slivers 안에는 sliver 형식의 위젯을 넣어줘야 한다
                      slivers: [
                        // 업로드한 사진 정보를 표시하는 영역
                        _imageAppBar(itemModel),
                        // 상세 텍스트 정보를 표시하는 영역
                        SliverPadding(
                          padding: const EdgeInsets.all(padding_16),
                          sliver: SliverList(
                            // SliverToBoxAdapter 로 각각 처리할수도 있고,
                            // SliverList 를 이용하여 리스트 형식으로도 처리 가능,
                            delegate: SliverChildListDelegate([
// ***** 그외 기타 버그 수정 - userModel -> itemModel
                              _userSection(itemModel),
                              _divider(padding_16 * 2 + 1),
                              Text(
                                itemModel.title,
                                style: Theme.of(context).textTheme.headline6,
                              ),
                              _textGap,
                              Row(
                                children: [
                                  Text(
                                    categoriesMapEngToKor[itemModel.category] ?? '선택',
                                    style: Theme.of(context)
                                        .textTheme
                                        .bodyText2!
                                        .copyWith(decoration: TextDecoration.underline),
                                  ),
                                  Text(
                                    ' · ${TimeCalculation.getTimeDiff(itemModel.createdDate)}',
                                    style: Theme.of(context).textTheme.bodyText2,
                                  ),
                                ],
                              ),
                              _textGap,
                              Text(
                                itemModel.detail,
                                style: Theme.of(context).textTheme.bodyText1,
                              ),
                              _textGap,
                              Text(
                                '조회 33',
                                style: Theme.of(context).textTheme.caption,
                              ),
                              _textGap,
                              _divider(2),
                              MaterialButton(
                                padding: EdgeInsets.zero,
                                onPressed: () {
                                  // 이메일로 처리하고 싶으면 flutter_email_sender 참고할 것
                                  // https://pub.dev/packages/flutter_email_sender
                                  logger.d('게시글을 신고합니다');
                                },
                                child: Align(
                                  alignment: Alignment.centerLeft,
                                  child: Text(
                                    '이 게시글 신고하기',
                                    style: Theme.of(context)
                                        .textTheme
                                        .button!
                                        .copyWith(color: Colors.black87),
                                  ),
                                ),
                              ),
                              _divider(2),
                            ]),
                          ),
                        ),
                        SliverToBoxAdapter(
                          child: Padding(
                            padding: const EdgeInsets.symmetric(horizontal: padding_16),
                            // 판매자의 다른 상품 보기
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
                              children: [
                                Text(
                                  '판매자의 다른 상품',
                                  style: Theme.of(context).textTheme.bodyText1,
                                ),
                                SizedBox(
                                  width: _size!.width / 4,
                                  child: MaterialButton(
                                    // padding: EdgeInsets.zero,
                                    onPressed: () {},
                                    child: Align(
                                      alignment: Alignment.centerRight,
                                      child: Text(
                                        '더보기',
                                        style: Theme.of(context)
                                            .textTheme
                                            .button!
                                            .copyWith(color: Colors.grey),
                                      ),
                                    ),
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                        // 일반위젯을 sliver 안에 넣으러면 SliverToBoxAdapter 로 wrapping 해야 함,
                        SliverToBoxAdapter(
                          child: FutureBuilder<List<ItemModel2>>(
                            future: ItemService().getUserItems(
                              userKey: itemModel.userKey,
                              itemKey: itemModel.itemKey,
                            ),
                            builder: (context, snapshot) {
                              if (snapshot.hasData) {
                                return Padding(
                                  padding: const EdgeInsets.all(padding_08),
                                  child: GridView.count(
                                    padding: const EdgeInsets.symmetric(horizontal: padding_08),
                                    //EdgeInsets.zero,
                                    // GridView 자체의 스크롤을 off 시키는 옵션,
                                    physics: const NeverScrollableScrollPhysics(),
                                    // 기본은 false 인데, false 면 전체화면을 차지하려하여 에러발생,
                                    // ture 면, 가져온 정보를 기초로 화면을 차지함,
                                    shrinkWrap: true,
                                    crossAxisCount: 2,
                                    mainAxisSpacing: padding_08,
                                    crossAxisSpacing: padding_16,
                                    childAspectRatio: 6 / 7,
                                    children: List.generate(snapshot.data!.length,
                                        (index) => SimilarItem(snapshot.data![index])),
                                  ),
                                );
                              }
                              return Container();
                            },
                          ),
                        ),
                      ],
                    ),
                  ),
                  // 앱바 영역에 그라데이션 표현 추가
                  Positioned(
                    left: 0,
                    right: 0,
                    top: 0,
                    height: kToolbarHeight + _statusBarHeight!,
                    child: Container(
                      height: kToolbarHeight + _statusBarHeight!,
                      decoration: const BoxDecoration(
                        gradient: LinearGradient(
                          begin: Alignment.topCenter,
                          end: Alignment.bottomCenter,
                          colors: [
                            Colors.black45,
                            Colors.black38,
                            Colors.black26,
                            Colors.black12,
                            Colors.transparent,
                          ],
                        ),
                      ),
                    ),
                  ),
                  // 화면 스크롤업 하면 앱바를 힌색으로 변경.
                  // 이전에 구현한 인디케이터가 appBar 타이틀위치에서 보여주던걸 숨김,
                  Positioned(
                    left: 0,
                    right: 0,
                    top: 0,
                    height: kToolbarHeight + _statusBarHeight!,
                    child: Scaffold(
                      backgroundColor: Colors.transparent,
                      appBar: AppBar(
                        centerTitle: true,
// ***** 그외 기타 버그 수정 - 로그인사용자 정보 표시
                        title: Text(
                          'User ID : [${userModel.phoneNumber}]',
                          style: TextStyle(
                              color: isAppbarCollapsed ? Colors.black87 : Colors.transparent),
                        ),
                        shadowColor: Colors.transparent,
                        backgroundColor: isAppbarCollapsed ? Colors.white : Colors.transparent,
                        foregroundColor: isAppbarCollapsed ? Colors.black87 : Colors.white,
                      ),
                    ),
                  )
                ],
              );
            },
          );
        }
        return Container(
          color: Colors.white,
          child: const Center(child: CircularProgressIndicator()),
        );
      },
    );
  }

  SliverAppBar _imageAppBar(ItemModel2 itemModel) {
    return SliverAppBar(
      // expandedHeight 에서는 세로 길이를 정해줄 수 있음,
      expandedHeight: _size!.width,
      // pinned: true 면 앱바 역역을 남기는 역할, false 면 스크롤시 같이 사라짐,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        centerTitle: true,
        // title: const Text('testing', style: TextStyle(color: Colors.black)),
        // 타이틀 부분에 인디케이터 표시하고 아래에 위치함, 패키지 추가 필요함,
        title: SizedBox(
          child: SmoothPageIndicator(
              controller: _pageController,
              // PageController
              count: itemModel.imageDownloadUrls.length,
              effect: const WormEffect(
                activeDotColor: Colors.white,
                //Theme.of(context).primaryColor,
                dotColor: Colors.black45,
                //Theme.of(context).colorScheme.background,
                radius: 3,
                dotHeight: 6,
                dotWidth: 6,
              ),
              // your preferred effect
              onDotClicked: (index) {}),
        ),

        // background 로 이미지를 넣으면 됨, 이미지 표시
        background: PageView.builder(
          // 좌/우로 스크롤 가능하게 처리,
          controller: _pageController,
          // 옆페이지로 이동시 포커스를 옆페이지로 이동시켜 로딩을 미리하게 설정함,
          allowImplicitScrolling: true,
          itemBuilder: (BuildContext context, int index) {
            return ExtendedImage.network(
              itemModel.imageDownloadUrls[index],
              fit: BoxFit.cover,
              // 캐싱을 했지만 다시 로딩하는 경우가 있어서 이미지 사이즈를 줄여줌,
              scale: 0.1,
            );
          },
          itemCount: itemModel.imageDownloadUrls.length,
        ),
      ),
    );
  }

// ***** 그외 기타 버그 수정 - userModel -> itemModel 로 변경 및 맴버변수 수정
  Widget _userSection(ItemModel2 _itemModel) {
    int phoneCnt = _itemModel.userPhone.length;
    //[서울특별시, 용산구, xxx길, 11, (xxx2가)]
    //[서울특별시, 중구, 태평로x가, xx]
    List _address = _itemModel.address.split(' ');
    String _detail = _address[_address.length - 1];
    String _location = '';

    if (_detail.contains('(') && _detail.contains(')')) {
      _location = _detail.replaceAll('(', '').replaceAll(')', '');
    } else {
      _location = _address[2];
    }

    return Row(
      children: [
        ExtendedImage.network(
          'https://picsum.photos/50',
          fit: BoxFit.cover,
          width: _size!.width / 10,
          height: _size!.width / 10,
          shape: BoxShape.circle,
        ),
        const SizedBox(width: padding_16),
        SizedBox(
          height: _size!.width / 10,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            // crossAxisAlignment: CrossAxisAlignment.auth,
            children: [
              Text(
                _itemModel.userPhone.substring(phoneCnt - 4).toString(),
                style: Theme.of(context).textTheme.bodyText1,
              ),
              Text(
                _location,
                style: Theme.of(context).textTheme.bodyText2,
              ),
            ],
          ),
        ),
        // 매너온도를 오른쪽으로 보내기 위해서 중간에 빈박스를 Expanded 로 처리,
        Expanded(child: Container()),
        Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Row(
              children: [
                SizedBox(
                  // 온도와 LinearProgressIndicator 를 가로 길이를 같게 하기위해서,
                  width: 42,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      const FittedBox(
                        child: Text(
                          '37.3 °C',
                          style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueAccent),
                        ),
                      ),
                      const SizedBox(height: 6),
                      ClipRRect(
                        borderRadius: BorderRadius.circular(1),
                        child: LinearProgressIndicator(
                          color: Colors.blueAccent,
                          value: 0.365,
                          minHeight: 3,
                          backgroundColor: Colors.grey[200],
                        ),
                      )
                    ],
                  ),
                ),
                const SizedBox(width: 6),
                const ImageIcon(
                  ExtendedAssetImageProvider('assets/imgs/happiness.png'),
                  color: Colors.blue,
                ),
              ],
            ),
            const SizedBox(height: padding_08),
            Text(
              '매너온도',
              style: Theme.of(context)
                  .textTheme
                  .bodyText2!
                  .copyWith(decoration: TextDecoration.underline),
            ),
          ],
        ),
        // Text('aaa'),
      ],
    );
  }
}

 

 

중요 추가 부분

 

void _goToChatroom(ItemModel2 itemModel, UserModel1 userModel) async {
  String chatroomKey =
      ChatroomModel2.generateChatRoomKey(buyer: userModel.userKey, itemKey: newItemKey);
  logger.d({
    'buyerKey': '[${userModel.userKey}]',
    'sellerKey': '[${itemModel.userKey}]',
    'newItemKey': '[$newItemKey]',
    'itemKey': '[${itemModel.itemKey}]',
  });

  ChatroomModel2 _chatroomModel = ChatroomModel2(
    itemImage: itemModel.imageDownloadUrls[0],
    itemTitle: itemModel.title,
    itemKey: newItemKey,
    itemAddress: itemModel.address,
    itemPrice: itemModel.price,
    sellerKey: itemModel.userKey,
    buyerKey: userModel.userKey,
    sellerImage: 'https://minimaltoolkit.com/images/randomdata/male/101.jpg',
    buyerImage: 'https://minimaltoolkit.com/images/randomdata/female/41.jpg',
    geoFirePoint: itemModel.geoFirePoint,
    chatroomKey: chatroomKey,
    lastMsgTime: DateTime.now().toUtc(),
  );
  await ChatService().createNewChatroom(_chatroomModel);

  Get.toNamed("/$ROUTE_ITEM_DETAIL/$chatroomKey");
}

 

 

 

./src/screens/chat/chatroom_screen.dart - 채팅룸용 화면추가(임시 더비 화면)

 

 

import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../../utils/logger.dart';

class ChatroomScreen extends StatefulWidget {
  const ChatroomScreen({Key? key}) : super(key: key);

  @override
  State<ChatroomScreen> createState() => _ChatroomScreenState();
}

class _ChatroomScreenState extends State<ChatroomScreen> {
  @override
  Widget build(BuildContext context) {
    logger.d('${Get.parameters['chatroomKey']}');
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.yellowAccent,
        alignment: Alignment.center,
        child: Text('${Get.parameters['chatroomKey']}'),
      ),
    );
  }
}

 

 

 

./src/models/chatroom_model.dart - 생성자 조건 수정

 

ChatroomModel2({
  required this.itemImage,
  required this.itemTitle,
  required this.itemKey,
  required this.itemAddress,
  required this.itemPrice,
  required this.sellerKey,
  required this.buyerKey,
  required this.sellerImage,
  required this.buyerImage,
  required this.geoFirePoint,
  this.lastMsg = '',
  required this.lastMsgTime,
  this.lastMsgUserKey = '',
  required this.chatroomKey,
  this.reference,
});