Android/Side Projects

당근마켓 앱 개발하기

화요밍 2022. 2. 22. 16:49
728x90
반응형
Side Proeject - Carrot Market
중고 거래 앱 당근 마켓의 간단한 기능을 구현한 프로젝트

 

 

GitHub - hwayeon351/CarrotMarket

Contribute to hwayeon351/CarrotMarket development by creating an account on GitHub.

github.com


학습 회고

오늘은 마이페이지에서 회원가입, 로그인/로그아웃 하는 기능을 Firebase Authentication과 연동해서 구현하였다.

그리고 홈에서 거래 아이템을 클릭하면 판매자와의 채팅방이 개설되고 메세지를 주고 받을 수 있는 채팅 기능을 구현하였다.

 


오늘 공부한 내용

  • 마이페이지 - Firebase Authentication을 이용한 회원가입/로그인/로그아웃 기능 구현하기

private val auth: FirebaseAuth by lazy {
    Firebase.auth
}
  • 회원 가입

이메일과 비밀번호로 회원가입을 할 수 있도록 Firebase Authentication에 Sign-in method에 이메일/비밀번호를 추가하였다.

EditText에 이메일과 비밀번호를 입력하고 회원가입 버튼을 클릭하면 Firebase Authentication의 Users에 새 사용자를 추가해야 한다.

FirebaseAuth의 createUserWithEmailAndPassword()를 호출해서 매개변수로 사용자가 입력한 EditText의 이메일과 비밀번호를 담아주면 된다.

이때, Firebase Authentication의 Users에 사용자가 성공적으로 추가되었는지의 여부를 받아오기 위해서 addOnCompleteListener() 리스너를 달아서 결과를 받아오는 람다값의 isSuccessful이 true라면 가입 성공 로직을 구현하고, false라면 가입 실패 로직을 구현하였다.

fragmentMypageBinding.signUpButton.setOnClickListener {
    binding?.let { binding ->
        val email = binding.emailEditText.text.toString()
        val password = binding.passwordEditText.text.toString()

        auth.createUserWithEmailAndPassword(email, password)
            .addOnCompleteListener(requireActivity()) { task ->
                if (task.isSuccessful) {
                    Toast.makeText(context, "회원가입에 성공했습니다. 로그인 버튼을 눌러주세요.", Toast.LENGTH_SHORT).show()
                    auth.signOut()
                } else {
                    Toast.makeText(context, "회원가입에 실패했습니다. 이미 가입한 이메일일 수 있습니다.", Toast.LENGTH_SHORT).show()
                }
            }
    }
}

가입에 성공하였을 때, signOut() 함수를 호출하였는데, 이는 createUserWithEmailAndPassword()가 성공적으로 완료되면 자동으로 해당 사용자로 로그인 처리 되기 때문이다.

내가 구현하고자 하는 앱은 회원가입 후 로그인 상태로 자동으로 넘어가는 것이 아니라 사용자가 로그인 버튼을 클릭해야 로그인이 되도록 구현하기 위해서 signOut()을 호출했다.

 

  • 로그인/로그아웃

이메일과 비밀번호를 입력하는 EditText가 둘다 null이 아닌 경우에 로그인/로그아웃 버튼을 활성화하였다.

FirebaseAuth의 getCurrentUser()를 이용해서 현재 앱에 로그인된 사용자가 있는지 없는지의 여부를 알 수 있다.

로그인된 사용자가 없다면 버튼의 텍스트를 로그인으로 바꿔 로그인할 수 있도록 하고, 로그인 된 사용자가 있는 경우에는 버튼의 텍스트를 로그아웃으로 바꿔 로그아웃을 할 수 있도록 구현하였다.

 

로그인을 수행하기 위해서는 FirebaseAuth의 signInWithEmailAndPassword()에 사용자가 입력한 email과 password를 매개변수로 담아 호출한다.

이 역시, 로그인이 성공적으로 수행되었는지 아닌지를 알기 위해 addOnCompleteListener()를 붙여서 람다식을 이용해 성공하였다면 로그인 로직을, 아니라면 로그인 실패를 알리기 위해 Toast 메세지를 띄워주었다.

 

로그아웃을 수행하기 위해서는 FirebaseAuth의 signOut() 함수를 호출하면 된다.

fragmentMypageBinding.signInOutButton.setOnClickListener {
    binding?.let { binding ->
        val email = binding.emailEditText.text.toString()
        val password = binding.passwordEditText.text.toString()

        if (auth.currentUser == null) {

            auth.signInWithEmailAndPassword(email, password)
                .addOnCompleteListener(requireActivity()) { task ->
                    if (task.isSuccessful) {
                        successSignIn()
                    } else {
                        Toast.makeText(context, "로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요.", Toast.LENGTH_SHORT).show()
                    }
                }

        } else {
            auth.signOut()
            binding.emailEditText.text.clear()
            binding.emailEditText.isEnabled = true
            binding.passwordEditText.text.clear()
            binding.passwordEditText.isEnabled = true

            binding.signInOutButton.text = "로그인"
            binding.signInOutButton.isEnabled = false
            binding.signUpButton.isEnabled = false
            Toast.makeText(context, "로그아웃 되었습니다.", Toast.LENGTH_SHORT).show()
        }

    }
}

 

 

  • 홈 - 거래 아이템 클릭해서 판매자와의 채팅방 생성하기

홈 화면에서 사용자가 거래하고자 하는 아이템을 클릭하였을 때 거래자와 채팅을 나눌 수 있도록 기능을 추가하기 위해 ArticleItem에 onItemClicked 리스너를 달아주었다.

class ArticleAdapter(val onItemClicked: (ArticleModel) -> Unit): ListAdapter<ArticleModel, ArticleAdapter.ViewHolder>(diffUtil) {
    inner class ViewHolder(private val binding: ItemArticleBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(articleModel: ArticleModel) {
            val format = SimpleDateFormat("MM월 dd일")
            val date = Date(articleModel.createdAt)
            val priceFormat = DecimalFormat("#,###")

            binding.titleTextView.text = articleModel.title
            binding.dateTextView.text = format.format(date).toString()
            binding.priceTextView.text = "${priceFormat.format(articleModel.price.toInt())}원"

            if (articleModel.imageURL.isNotEmpty()) {
                Glide.with(binding.thumbnailImageView)
                    .load(articleModel.imageURL)
                    .into(binding.thumbnailImageView)
            }

            binding.root.setOnClickListener {
                onItemClicked(articleModel)
            }
        }

    }

 

onItemClicked 리스너는 HomeFragment에서 ArticlerAdapter를 인스턴스화 할 때 구현해주었다.

선택된 아이템의 sellerId와 title을 이용해서 채팅방의 제목과 판매자 Id을 구성하고 현재 로그인된 사용자 아이디를 구매자 Id로 구성하였다.

채팅방 key값은 임시로 현재 시간으로 설정하였다.

따라서 현재 같은 판매자, 구매자, 거래 아이템으로 여러 채팅방을 개설할 수 있지만 추후 외래키로 채팅방 키를 생성해서 동일한 판매자, 구매자, 거래 아이템으로 생성된 채팅방이 있다면 채팅방을 새로 개설하지 않고 해당 채팅방을 띄워주도록 리팩토링 할 예정이다.

 

새롭게 생성된 채팅룸을 판매자와 구매자 데이터베이스에 추가해주었다.

 

만약, 현재 로그인된 사용자가 올린 거래 아이템이라면 채팅방을 개설하지 않고 사용자에게 안내하기 위해 Snackbar를 띄워주었다.

또한, 로그인되어 있지 않다면 로그인 후 사용해달라는 Snackbar를 띄워주었다.

articleAdapter = ArticleAdapter(onItemClicked = { articleModel ->
    if (auth.currentUser != null) {
        if (auth.currentUser?.uid != articleModel.sellerId) {

            val chatRoom = ChatListItem(
                buyerId = auth.currentUser!!.uid,
                sellerId = articleModel.sellerId,
                itemTitle = articleModel.title,
                key = System.currentTimeMillis()
            )

            userDB.child(auth.currentUser!!.uid)
                .child(CHILD_CHAT)
                .push()
                .setValue(chatRoom)

            userDB.child(articleModel.sellerId)
                .child(CHILD_CHAT)
                .push()
                .setValue(chatRoom)

            Snackbar.make(view, "채팅방이 생성되었습니다. 채팅탭에서 확인해주세요.", Snackbar.LENGTH_LONG).show()

        } else {
            Snackbar.make(view, "내가 올린 아이템 입니다.", Snackbar.LENGTH_LONG).show()
        }
    } else {
        Snackbar.make(view, "로그인 후 사용해주세요.", Snackbar.LENGTH_LONG).show()
    }

})

 

 

  • 채팅 - 채팅방 목록 불러오기

채팅방 목록
Users DB 구조

로그인한 사용자가 참여한 채팅방 목록을 최초 한 번 불러오기 위해서 사용자 아이디 > Chats에 addListenerForSingleValueEvent()를 달았다. 채팅방 리스트가 추가될 때마다 아이템을 불러오기 위해 ValueEventListener를 사용하여 각각의 아이템을 getValue()로 받아왔다. data class로 정의한 ChatListItem의 새로운 인스턴스로 받아오기 위해서 getValue의 매개변수에 ChatListItem::class.java를 담아주었다.

채팅방 리스트를 관리하는 ChatListAdapter에 chatRoomList를 담아 submitList()를 호출해주고 DiffUtil이 잘 작동되지 않을 것을 대미하여 notifyDataSetChanged()도 호출해주었다.

val chatDB = Firebase.database.reference.child(DB_USER).child(auth.currentUser!!.uid).child(
    CHILD_CHAT)
chatDB.addListenerForSingleValueEvent(object: ValueEventListener{
    override fun onDataChange(snapshot: DataSnapshot) {
        snapshot.children.forEach {
            val model = it.getValue(ChatListItem::class.java)
            model ?: return

            chatRoomList.add(model)
        }

        chatListAdapter.submitList(chatRoomList)
        chatListAdapter.notifyDataSetChanged()

    }

    override fun onCancelled(error: DatabaseError) {
    }

})
data class ChatListItem(
    val buyerId: String,
    val sellerId: String,
    val key: Long,
    val itemTitle: String
) {
    constructor(): this("", "", 0, "")
}

 

 

  • 채팅방 - 실시간 채팅 구현하기

채팅방 목록에서 특정 채팅방을 선택해서 실시간 채팅을 구현하였다.

채팅방

  • 대화 불러오기

채팅방 목록을 보여주는 CHatListFragment에서 특정 채팅방을 클릭하였을 때 채팅방 키를 인텐트에 담아 ChatRoomActivity를 실행하였다.

chatListAdapter = ChatListAdapter(onItemClicked = { chatRoom ->
    context?.let {
        val intent = Intent(it, ChatRoomActivity::class.java)
        intent.putExtra("chatKey", chatRoom.key)
        startActivity(intent)
    }
})

 

그리고 ChatRoomActivity에서 채팅방 키를 받아와서 데이터베이스에 필드를 만들어주었다.

val chatKey = intent.getLongExtra("chatKey", -1)
chatDB = Firebase.database.reference.child(DB_CHATS).child("$chatKey")

 

그리고 실시간으로 전송된 대화를 받아오기 위해 chatDB에 addChildEventListener를 붙여주었다.

chatDB!!.addChildEventListener(object: ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
        val chatItem = snapshot.getValue(ChatItem::class.java)
        chatItem ?: return
        Log.d("ChatRoomActivity", "${chatItem.message}, ${chatItem.senderId}" )
        chatList.add(chatItem)
        adapter.submitList(chatList)
        adapter.notifyDataSetChanged()
    }

    override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}

    override fun onChildRemoved(snapshot: DataSnapshot) {}

    override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}

    override fun onCancelled(error: DatabaseError) {}

})

새로운 메세지가 생성되면 ChatItem으로 받아와 ChatList에 추가해주고 이를 어뎁터에 담아서 submitList()를 호출하고 notifyDataSetChanged()를 호출해서 실시간 채팅을 구현하였다.

 

  • 메세지 전송하기

전송 버튼을 클릭해 새로운 메세지를 전송할 때에는 EditText에 담긴 텍스트와 현재 앱 사용자 아이디를 담은 ChatItem을 chatDB에 Push() 해주었다.

findViewById<Button>(R.id.sendButton).setOnClickListener {
    val chatItem = ChatItem(
        senderId = auth.currentUser!!.uid,
        message = findViewById<EditText>(R.id.messageEditText).text.toString()
    )

    chatDB!!.push().setValue(chatItem)

}

 

 

 

 

728x90
반응형

'Android > Side Projects' 카테고리의 다른 글

Airbnb 앱 개발하기  (0) 2022.02.24
Airbnb 앱 개발하기  (0) 2022.02.23
당근마켓 앱 개발하기  (0) 2022.02.21
당근마켓 앱 개발하기  (0) 2022.02.20
당근마켓 앱 개발하기  (1) 2022.02.19