[TIL] 231109 회고

서정한·2023년 11월 8일
0

내일배움캠프 7기

목록 보기
66/66

Intro

  • 저번시간에 구조에대해 보았다면 이번시간에는 실제 코드를 어떤식으로 구현했는지를 적어보려고한다.
  • 적다가 내용이 많아지고 길어질경우 나눠서 포스팅을 하려고한다.

RemoteDataSource

  • MVVM에서는 데이터를 한곳에서 받아오도록 구현해야한다. 오늘 팀원과 MVVM으로 프로젝트를 리펙토링하는것에대해 이야기를 나누는 시간을가졌다. 이 이야기를 하고 팀원을 설득하는과정에서 다시한번 왜 구조를 나눠야하는지에대해 나 스스로에게도 납득할 수 있는 시간이었다.

    팀원분 : "기능이 잘 동작하고있으며, 메서드를 구현할때 3개의 Database를 다 불러와서 써야하는 상황들이 많이 있는데 이걸 굳이 쪼개야하는 이유를 모르겠다"
    나: "님은 님 코드가 머리속에 다 있으셔서 보기에 지금구조로도 충분히 편하실수있으나 우리 코드의 context를 모르는 분이 코드를볼때는 하나의 레퍼지토리에 3개의 Database를 불러오고 그 안에서 3개의 DB에대한 CRUD를 구현해놓은 상황이고 거기에 3개의 데이터베이스를 조합해서 내가 원하는 자료를 소팅하는 메서드들이 여러개가 있는데 이렇게 많은 일을 하는 레퍼지토리는 이해하기 어려운 구조인 것 같습니다. 그래서 관심사별로 class를 나누고 쪼개야 처음 프로젝트를 보시는분이 지금보다 한결 편하게 우리 코드를 이해하실 수 있을것 같습니다."

  • 이제 코드를 살펴보도록 하자.
package kr.sparta.tripmate.data.datasource.remote.community

import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.database.ktx.snapshots
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kr.sparta.tripmate.data.model.community.CommunityModel
import kr.sparta.tripmate.domain.model.search.SearchBlogEntity

/**
 * 작성자: 서정한
 * 내용: Firebase RealtimeDatabase에서 필요한 자료를
 * 요청하고 응답받는 DataSource Class
 * */
class FirebaseBoardRemoteDataSource {
    private val REFERENCE_COMMUNITY_DATA = "CommunityData"
    private fun getReference() = Firebase.database.getReference(REFERENCE_COMMUNITY_DATA)

    /**
     * 작성자: 서정한
     * 내용: 게시글 업로드시 사용할 키를 반환함.
     * */
    fun getKey(): String {
        return getReference().push().toString()
    }

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티 게시글 목록 가져오기
     * */
    fun getAllBoards(): Flow<List<CommunityModel?>> {
        val ref = getReference()
        return ref.snapshots.map { snapshot ->
            snapshot.children.mapNotNull {
                it.getValue<CommunityModel>()
            }
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티 게시글 가져오기
     * */
    fun getBoard(key: String): Flow<CommunityModel?> {
        val ref = getReference()
        return ref.snapshots.map { snapshot ->
            snapshot.children.mapNotNull {
                it.getValue<CommunityModel>()
            }.find { it.key == key }
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티에 게시글 추가
     * */
    fun addBoard(item: CommunityModel) {
        val ref = getReference()
        ref.get().addOnSuccessListener { snapshot ->
            val list = snapshot.children.mapNotNull {
                it.getValue<CommunityModel>()
            }.toMutableList()

            // 새로운 글 업로드
            list.add(item)
            ref.setValue(list)

        }
    }

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티 게시글 목록 업데이트
     * */
    fun updateBoard(
        item: CommunityModel
    ) {
        val ref = getReference()
        ref.get().addOnSuccessListener { snapshot ->
            val list = snapshot.children.mapNotNull {
                it.getValue<CommunityModel>()
            }.toMutableList()

            list.forEachIndexed { index, communityModel ->
                if (communityModel.key == item.key) {
                    list[index] = item
                    return@forEachIndexed
                }
            }
            ref.setValue(list)
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티 게시글 삭제
     * */
    fun removeBoard(key: String) {
        val ref = getReference()
        ref.get().addOnSuccessListener { snapshot ->
            val boards = snapshot.children.map {
                it.getValue(CommunityModel::class.java)
            }.toMutableList()

            val removeItem = boards.find { it?.key == key } ?: return@addOnSuccessListener
            boards.remove(removeItem)

            ref.setValue(boards)
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 게시글의 좋아요 클릭시 내 좋아요 클릭목록 업데이트.
     * */
    fun updateBoardLike(uid: String, key: String) {
        val ref = getReference()
        ref.get().addOnSuccessListener { snapshot ->
            // 내가 좋아요누른 게시판Key 가져오기
            val boards = snapshot.children.map {
                it.getValue(CommunityModel::class.java)
            }.toMutableList()

            // key와 일치하는 게시글 불러오기
            val model = boards.find { it?.key == key } ?: return@addOnSuccessListener
            val index = boards.indexOf(model)

            // 게시글의 좋아요 유저에 좋아요 클릭한 user추가
            val currentLikeUsers = model.likeUsers.toMutableList()
            val isLike = currentLikeUsers.find { it == uid }

            if (isLike.isNullOrEmpty()) {
                addBoardLike(boards, index, model, uid)
            } else {
                removeBoardLike(boards, index, model, uid)
            }
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 내가 클릭한 게시글을 내 좋아요 목록에 추가
     * 그리고 좋아요수 +1
     * */
    private fun addBoardLike(
        boards: MutableList<CommunityModel?>,
        index: Int,
        model: CommunityModel,
        uid: String
    ) {
        val ref = getReference()
        // 게시글의 좋아요 유저에 좋아요 클릭한 user추가
        val currentLikeUsers = model.likeUsers.toMutableList()
        currentLikeUsers.add(uid)

        boards[index] = model.copy(
            likes = boards[index]?.likes?.plus(1),
            likeUsers = currentLikeUsers
        )

        ref.setValue(boards)
    }

    /**
     * 작성자: 서정한
     * 내용: 내 좋아요목록에서 해당 게시글 삭제
     * 게시글이 삭제될때만 동작함.
     * 그리고 좋아요 수 -1
     * */
    private fun removeBoardLike(
        boards: MutableList<CommunityModel?>,
        index: Int,
        model: CommunityModel,
        uid: String,
    ) {
        val ref = getReference()
        val currentLikeUsers = model.likeUsers.toMutableList()
        currentLikeUsers.remove(uid)

        boards[index] = model.copy(
            likes = boards[index]?.likes?.minus(1),
            likeUsers = currentLikeUsers
        )

        ref.setValue(boards)
    }

    /**
     * 작성자: 서정한
     * 내용: 내가 스크랩한 게시글을 업데이트합니다.
     * */
    fun updateScrapBoards(uid: String, key: String) {
        val ref = getReference()
        ref.get().addOnSuccessListener { snapshot ->
            // 게시판목록 가져오기
            val boards = snapshot.children.map {
                it.getValue(CommunityModel::class.java)
            }.toMutableList()

            // key와 일치하는 게시글 불러오기
            val model = boards.find { it?.key == key } ?: return@addOnSuccessListener
            val index = boards.indexOf(model)

            // 게시글의 스크랩목록에 내 uid 추가 혹은 제거
            val currentScrapBoards = model.scrapUsers.toMutableList()
            val isScrap = currentScrapBoards.find { it == uid }

            if (isScrap.isNullOrEmpty()) {
                addScrapBoard(boards, index, model, uid)
            } else {
                removeScrapBoard(boards, index, model, uid)
            }
        }
    }

    /**
     * 작성자: 서정한
     * 내용: 내가 클릭한 게시글을 내 게시글 스크랩 목록에 추가
     * */
    private fun addScrapBoard(
        boards: MutableList<CommunityModel?>,
        index: Int,
        model: CommunityModel,
        uid: String
    ) {
        val ref = getReference()
        // 게시글의 스크랩목록에 내 uid 추가 혹은 제거
        val currentScrapBoards = model.scrapUsers.toMutableList()
        currentScrapBoards.add(uid)

        boards[index] = model.copy(
            scrapUsers = currentScrapBoards
        )

        ref.setValue(boards)
    }

    /**
     * 작성자: 서정한
     * 내용: 내 게시글 스크랩 목록에서 해당 게시글 삭제
     * 게시글이 삭제될때만 동작함.
     * */
    private fun removeScrapBoard(
        boards: MutableList<CommunityModel?>,
        index: Int,
        model: CommunityModel,
        uid: String,
    ) {
        val ref = getReference()
        val currentScrapBoards = model.scrapUsers.toMutableList()
        currentScrapBoards.remove(uid)

        boards[index] = model.copy(
            scrapUsers = currentScrapBoards
        )

        ref.setValue(boards)
    }
}
package kr.sparta.tripmate.data.datasource.remote.community

import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.util.Log
import android.widget.ImageView
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.ktx.storage
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject
import java.io.ByteArrayOutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
 * 작성자: 서정한
 * 내용: Firbase Storage에 Image를 저장
 * */
class FirebaseStorageRemoteDataSource {
    private fun getReference(value: String) = Firebase.storage.reference.child(value)

    /**
     * 작성자: 서정한
     * 내용: 커뮤니티 글작성 페이지에서
     * 이미지를 FireStore에 업로드한다.
     * */
    suspend fun uploadImage(
        imgName: String,
        image: Bitmap,
    ): String {
        val imageRef = getReference("$imgName.jpg")
        val baos = ByteArrayOutputStream()
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        val data = baos.toByteArray()
        val uploadTask = imageRef.putBytes(data)

        val result = suspendCoroutine<String> { continuation ->
            uploadTask.addOnFailureListener {
                Log.e("TripMates", "upload Image Error: ${it.toString()}")
            }.addOnSuccessListener {
                imageRef.downloadUrl.addOnSuccessListener { url ->
                    // 발행
                    continuation.resume(url.toString())
                }
            }
        }
        return result
    }
}
package kr.sparta.tripmate.data.datasource.remote.community.scrap

import android.os.Build
import android.text.Html
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.database.ktx.snapshots
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kr.sparta.tripmate.data.model.community.CommunityModel
import kr.sparta.tripmate.data.model.search.SearchBlogModel
import kr.sparta.tripmate.domain.model.search.SearchBlogEntity

/**
 * 작성자: 서정한
 * 내용: 모든 블로그 스크랩데이터 가져오기
 * */
class FirebaseBlogScrapRemoteDataSource {
    private final val REFERENCE_BLOG_SCRAP = "BlogScrap"
    private fun getReference(uid: String) =
        Firebase.database.getReference(REFERENCE_BLOG_SCRAP).child(uid)

    /**
     * 작성자: 서정한
     * 내용:내가 스크랩한 블로그 목록 불러오기
     * */
    fun getAllBlogScrapsFlow(uid: String): Flow<List<SearchBlogModel?>> {
        val ref = getReference(uid)
        return ref.snapshots.map { snapshot ->
            snapshot.children.mapNotNull {
                it.getValue<SearchBlogModel>()
            }
        }
    }

    fun updateBlogScrap(uid: String, model: SearchBlogModel) {
        val ref = getReference(uid)
        ref.get().addOnSuccessListener { snapshot ->
            // 스크랩한 블로그목록 가져오기
            val scrapBlogs = snapshot.children.map {
                it.getValue(SearchBlogModel::class.java)
            }.toMutableList()

            // link가 일치하는 블로그 불러오기
            val blogItem = scrapBlogs.find { it?.link == model.link }

            // 블로그 목록 업데이트
            if (blogItem == null) {
                addBlogScrap(
                    uid = uid,
                    scrapBlogs = scrapBlogs,
                    model = model,
                )
            } else {
                removeBlogScrap(
                    uid = uid,
                    scrapBlogs = scrapBlogs,
                    model = model,
                )
            }
        }

    }

    /**
     * 작성자: 서정한
     * 내용: 해당 key의 블로그 스크랩데이터 가져오기
     * Firebase RDB에 저장한다.
     * */
    private fun addBlogScrap(
        uid: String,
        scrapBlogs: MutableList<SearchBlogModel?>,
        model: SearchBlogModel
    ) {
        /**
         * 작성자: 서정한
         * 내용: 스크랩데이터를 네이버에서 받아올때 html태그가 String에 섞여있음.
         * 검색한 데이터의 String값만 뽑아내기위한 메서드
         * */
        fun stripHtml(html: String): String {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                return Html.fromHtml(html).toString()
            }
            return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY).toString()
        }

        // 제목 html 태그 제거
        val title = model.title?.let { stripHtml(it) }
        // 내용 html 태그 제거
        val description = model.description?.let { stripHtml(it) }

        val ref = getReference(uid)
        scrapBlogs.add(
            model.copy(
                title = title,
                description = description,
            )
        )

        ref.setValue(scrapBlogs)
    }

    /**
     * 작성자: 서정한
     * 내용: 선택한 블로그 스크랩 삭제
     * */
    private fun removeBlogScrap(
        uid: String,
        scrapBlogs: MutableList<SearchBlogModel?>,
        model: SearchBlogModel
    ) {
        val ref = getReference(uid)

        val removeItem = scrapBlogs.find { it?.link == model.link } ?: return
        scrapBlogs.remove(removeItem)

        ref.setValue(scrapBlogs)
    }
}

코드 설명

  • 우선은 같은 Firebase RDB에서 데이터를 가져오지만 하는일들이 많다보니 2개로 나누게되었다. 나눈기준은 게시글, 블로그 스크랩이다. 이렇게 나눈이유는 좋아요나 게시글 스크랩의경우 사용자의 id를 저장하여 해당 게시글에 누가 좋아요를 눌렀는지, 누가 스크랩한 상태인지를 게시글에서 알고있다.그래서 해당 기능은 하나의 RemoteSource에 있어야겠다는 판단이 들었고, 블로그 스크랩의 경우 게시글과다른 모델을 사용하여 데이터List를 저장하고 불러오고 관리하므로 별도의 RemoteSource를 만들게되었다.
  • 만들면서 들었던 고민은 RemoteSource에서 데이터만 받아와서 그 후 Repository에서 기능별로 분류해야하는가?였다. 이번 게시판 리펙토링 전 이전에 한번 리펙토링을 시도했던적이있는데 그때는 remoteSource에서 데이터만 받아오거나 삭제하는 기능만 구현해놓고 Repository에서 구현했었는데 두 버전을 만들면서 아직도 뭐가 더 나은지 이전에는 정리가 잘 되지않았는데 지금와서 생각해보니 Repository에 구현해놓는게 맞다는 생각이들었다.

Repository

profile
잘부탁드립니다!

0개의 댓글