tgyuuAn / BaekyoungE

자연어 처리 기반 진로 상담 chat bot 어플리케이션
5 stars 0 forks source link

[FEATURE]: 채팅 로그 기능 추가 #41

Closed tgyuuAn closed 1 month ago

tgyuuAn commented 2 months ago

작업 사항

채팅 로그 로컬에 저장!!!!!

image

Todo

기타사항

tgyuuAn commented 1 month ago

Horizontal Divider의 default Modifier가 fillMaxWidth() 인데요?

목표 -

image




문제 1 -

                    itemsIndexed(yearList) { index, year ->
                        Column(
                            modifier = Modifier.clickable { showSpinner = false },
                        ) {
                            Text(
                                text = year,
                                style = BaekyoungTheme.typography.labelRegular.copy(fontSize = 13.sp),
                                color = BaekyoungTheme.colors.gray95,
                                textAlign = TextAlign.Center,
                                modifier = Modifier.padding(horizontal = 20.dp, vertical = 6.dp)
                            )

                            if (index < yearList.size - 1) {
                                HorizontalDivider(color = BaekyoungTheme.colors.grayAC)
                            }
                        }
                    }
                }

image




문제 1의 원인 -

Horizontal Divider의 기본 Default Modifier가 fillMaxWidth() 였음.




문제 2 -

                    itemsIndexed(yearList) { index, year ->
                        Column(
                            modifier = Modifier
                                .width(IntrinsicSize.Min) // <-- 요 녀석 추가해서 Column 내부의 최소 width를 기준으로 그리도록 변경
                                .clickable { showSpinner = false },
                        ) {
                            Text(
                                text = year,
                                style = BaekyoungTheme.typography.labelRegular.copy(fontSize = 13.sp),
                                color = BaekyoungTheme.colors.gray95,
                                textAlign = TextAlign.Center,
                                modifier = Modifier.padding(horizontal = 20.dp, vertical = 6.dp)
                            )

                            if (index < yearList.size - 1) {
                                HorizontalDivider(color = BaekyoungTheme.colors.grayAC)
                            }
                        }
                    }

image

근데 왜 Text에 개행이 될까요?

이건 아직도 잘 모르겠따.




여하튼 최종 -

레퍼런스

                    itemsIndexed(yearList) { index, year ->
                        Column(
                            modifier = Modifier
                                .width(IntrinsicSize.Min)
                                .clickable { showSpinner = false },
                        ) {
                            Text(
                                text = year,
                                style = BaekyoungTheme.typography.labelRegular.copy(fontSize = 13.sp),
                                color = BaekyoungTheme.colors.gray95,
                                textAlign = TextAlign.Center,
                                modifier = Modifier
                                    .padding(horizontal = 20.dp, vertical = 6.dp)
                                    .requiredWidth(IntrinsicSize.Max),    // <-- 이 녀석을 추가해서 가능한 한 많이 차지하도록 변경
                            )

                            if (index < yearList.size - 1) {
                                HorizontalDivider(color = BaekyoungTheme.colors.grayAC)
                            }
                        }
                    }

image

네이쓰...!




근데 바로 유기

image

tgyuuAn commented 1 month ago

채팅방 데이터베이스 구조 설계는 어떻게 하면 좋을까요...?

일단 아래와 같이 사용....! 레퍼런스

image

근데 지금은 이제 1:1 채팅방이라서 회원 정보는 없는.




@Entity(tableName = "chatting_room")
data class ChattingRoomEntity(
    @PrimaryKey
    val id: String,
    @ColumnInfo(name = "last_chatting")
    val lastChatting: String,
    @ColumnInfo(name = "created_at")
    val createdAt: String,
)
@Entity(tableName = "message")
data class MessageEntity(
    @PrimaryKey
    val id: String,
    @ColumnInfo(name = "chatting_room_id")
    val chattingRoomId: String,
    @ColumnInfo(name = "message_from")
    val messageFrom: String, 
    @ColumnInfo(name = "message_to")
    val messageTo: String,
    val content: String,
    @ColumnInfo(name = "created_at")
    val createdAt: String,
)

메세지에 from, to가 있는 이유는 AI 채팅과 유저 채팅의 구분 목적임.

추후에 멘토, 멘티 채팅을 고려하기도 했음.




DAO는 각 Entity별로 구분하는 것이 좋을까요 ...?

지금 위에 Message, ChattingRoomt Entity가 총 2개가 있음.

근데 DAO를 설계할 때 어차피 둘 다 채팅 정보 하나의 목적을 가지고 있는데 ChattingDao 하나로 퉁칠지,

아니면 각 Entity 별로 DAO를 설계할 지 고민이 되었음.




image

근데 생각해보니까 뭐 엄청나게 거대한 규모도 아니고 1:1 채팅인데 그냥 하나의 DAO로 해결하면 좋을 것 같다고 판단.

무엇보다 귀찮으니까

@Dao
interface ChattingDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertMessage(message: MessageEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertChattingRoom(chattingRoom: ChattingRoomEntity)

    @Query("SELECT * FROM message WHERE chatting_room_id MATCH :roomId ORDER BY created_at DESC")
    suspend fun getAllChattingRoomMessages(roomId: String): List<MessageEntity>

    @Query("SELECT * FROM chatting_room ORDER BY created_at DESC")
    suspend fun getAllChattingRoom(): List<ChattingRoomEntity>
}
tgyuuAn commented 1 month ago

Firebase RealtimeDB 실시간 변경 감지하기

Hoooooooooooolyyyyyyyyyyyyyyy 완전 맛있는 기능

image




https://firebase.google.com/docs/firestore/query-data/listen?hl=ko#view_changes_between_snapshots 에서 제공하는 addSnapshotListner를 이용하면 Document 혹은 Collection의 변동 사항을 감지해서 리스너로 쏴준다.

즉, while문 돌려서 DB를 일일이 확인할 필요가 없다는 마알쓰음.

    override suspend fun getAllMessage(roomId: String): Flow<MentoringChatResponse> = callbackFlow {
        val listenerRegistration = firebaseFirestore.collection(CHATTING_ROOM_COLLECTION)
            .document(CHATTING_ROOM_COLLECTION)
            .collection(roomId)
            .addSnapshotListener { value, error ->
                if (error != null){
                    Log.d("test", "listen:error", error)
                    return@addSnapshotListener
                }

                for (dc in value!!.documentChanges){
                    when(dc.type){
                        DocumentChange.Type.ADDED -> trySend(dc.document.toObject<MentoringChatResponse>())
                        DocumentChange.Type.MODIFIED -> Log.d("test", "Modified message: ${dc.document.data}")
                        DocumentChange.Type.REMOVED -> Log.d("test", "Removed message: ${dc.document.data}")
                    }
                }
            }

        awaitClose { listenerRegistration.remove() }
    }

이 때 응답은 callback으로 오기 때문에, callbackFlow를 이용해서 trySend() 살짝 쿵 먹여준다.

(이렇게 하는 이유는 addSnapshotListner 내부가 suspend 블럭이 아닌데, trySend()를 씀으로써 일반 함수 블럭 -> Flow 블럭으로 갖고 오게 해줌 크크)




awaitClose { listenerRegistration.remove() }

해당 코루틴이 cancel되거나 close 되었을 때 변화를 감지하는 listner를 닫는 역할!