Skip to content

Commit

Permalink
kuring-187 쿠링봇 repository 작성 (#287)
Browse files Browse the repository at this point in the history
* module: AI 데이터 명세를 정의하는 :data:ai 모듈 추가

* feature: 쿠링봇 메시지 domain 객체 추가

* feature: 쿠링봇 로직에 필요한 유틸 코드 추가

* feature: 쿠링봇 메시지 domain - entity 변환 코드 추가

* feature: 쿠링봇 메시지 repository 인터페이스 작성

* refactor: KuringBotClient에 특정 프로토콜을 명시하지 않음

* feature: 쿠링봇 메시지 repository 구현체 작성

* test: 쿠링봇 메시지 repository 구현체 테스트 코드 작성

* refactor: 쿠링봇 메시지 repository의 hilt 모듈 작성

* test: 테스트 코드의 람다식 이름 변경

Co-authored-by: HyunWoo Lee (Nunu Lee) <[email protected]>

---------

Co-authored-by: HyunWoo Lee (Nunu Lee) <[email protected]>
  • Loading branch information
mwy3055 and l2hyunwoo authored Aug 5, 2024
1 parent 1280141 commit 65ffa6a
Show file tree
Hide file tree
Showing 16 changed files with 256 additions and 9 deletions.
17 changes: 17 additions & 0 deletions core/util/src/main/java/com/ku_stacks/ku_ring/util/DateTimeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ku_stacks.ku_ring.util

import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.YearMonth
import java.time.ZoneOffset

val LocalDateTime.yearMonth: YearMonth
get() = YearMonth.of(this.year, this.monthValue)

fun Long.toLocalDateTime(): LocalDateTime =
LocalDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneOffset.systemDefault())

fun LocalDateTime.toEpochSecond() = atZone(ZoneOffset.systemDefault()).toEpochSecond()

fun LocalDate.toEpochSecond() = atStartOfDay(ZoneOffset.systemDefault()).toEpochSecond()
1 change: 1 addition & 0 deletions data/ai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
27 changes: 27 additions & 0 deletions data/ai/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import com.ku_stacks.ku_ring.buildlogic.dsl.setNameSpace

plugins {
kuring("feature")
kuringPrimitive("room")
kuringPrimitive("ktor")
kuringPrimitive("test")
}

android {
setNameSpace("ai")

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

dependencies {
implementation(projects.core.util)
implementation(projects.data.domain)
implementation(projects.data.remote)
implementation(projects.data.local)

testImplementation(libs.kotlinx.coroutines.test)
}
Empty file added data/ai/consumer-rules.pro
Empty file.
4 changes: 4 additions & 0 deletions data/ai/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ku_stacks.ku_ring.ai.di

import com.ku_stacks.ku_ring.ai.repository.KuringBotRepository
import com.ku_stacks.ku_ring.ai.repository.KuringBotRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindKuringBotMessageRepository(repositoryImpl: KuringBotRepositoryImpl): KuringBotRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ku_stacks.ku_ring.ai.mapper

import com.ku_stacks.ku_ring.domain.KuringBotMessage
import com.ku_stacks.ku_ring.local.entity.KuringBotMessageEntity
import com.ku_stacks.ku_ring.util.toEpochSecond

internal fun KuringBotMessage.toEntity() = KuringBotMessageEntity(
id = id,
message = message,
postedEpochSeconds = postedDate.toEpochSecond(),
isQuery = isQuery,
)

internal fun List<KuringBotMessage>.toEntity() = map { it.toEntity() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.ku_stacks.ku_ring.ai.mapper

import com.ku_stacks.ku_ring.domain.KuringBotMessage
import com.ku_stacks.ku_ring.local.entity.KuringBotMessageEntity
import com.ku_stacks.ku_ring.util.toLocalDateTime

internal fun KuringBotMessageEntity.toDomain() = KuringBotMessage(
id = id,
message = message,
postedDate = postedEpochSeconds.toLocalDateTime(),
isQuery = isQuery,
)

internal fun List<KuringBotMessageEntity>.toDomain() = map { it.toDomain() }
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.ku_stacks.ku_ring.ai.repository

import com.ku_stacks.ku_ring.domain.KuringBotMessage
import java.time.LocalDate

interface KuringBotRepository {
/**
* 쿠링봇 메시지 세션을 열고, 서버로부터 데이터가 주어졌을 때 작업을 실행한다.
*
* @param query 질문 내용
* @param token 사용자 FCM 토큰
* @param onReceived 데이터를 수신했을 때 실행할 작업
*/
suspend fun openKuringBotSession(
query: String,
token: String,
onReceived: (String) -> Unit,
)

/**
* 로컬 DB에 저장된 쿠링봇 메시지를 모두 반환한다.
*
* @return 로컬에 저장된 모든 쿠링봇 메시지
*/
suspend fun getAllMessages(): List<KuringBotMessage>

/**
* 로컬 DB에 쿠링봇 메시지 여러 개를 저장한다.
* 이미 저장되어 있는 메시지는 중복으로 저장되지 않는다.
*
* @param messages 로컬에 저장할 메시지
*/
suspend fun insertMessages(messages: List<KuringBotMessage>)

/**
* 로컬 DB에 쿠링봇 메시지를 저장한다.
* 이미 저장되어 있는 메시지라면, 아무 일도 일어나지 않는다.
*
* @param message 로컬에 저장할 메시지
*/
suspend fun insertMessage(message: KuringBotMessage)

/**
* 지정된 기간 동안에 전송한 질문의 수를 반환한다.
*
* @param from 기간의 시작
* @param to 기간의 끝
* @return [from]부터 [to]까지 전송한 질문의 수 (inclusive)
*/
suspend fun getQueryCount(from: LocalDate, to: LocalDate): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.ku_stacks.ku_ring.ai.repository

import com.ku_stacks.ku_ring.ai.mapper.toDomain
import com.ku_stacks.ku_ring.ai.mapper.toEntity
import com.ku_stacks.ku_ring.domain.KuringBotMessage
import com.ku_stacks.ku_ring.local.room.KuringBotMessageDao
import com.ku_stacks.ku_ring.remote.kuringbot.KuringBotClient
import com.ku_stacks.ku_ring.util.toEpochSecond
import java.time.LocalDate
import javax.inject.Inject

class KuringBotRepositoryImpl @Inject constructor(
private val kuringBotClient: KuringBotClient,
private val kuringBotMessageDao: KuringBotMessageDao,
) : KuringBotRepository {

override suspend fun openKuringBotSession(
query: String,
token: String,
onReceived: (String) -> Unit,
) {
try {
kuringBotClient.openKuringBotConnection(query, token, onReceived)
} catch (e: Exception) {
onReceived(UNKNOWN_ERROR)
}
}

override suspend fun getAllMessages(): List<KuringBotMessage> {
return kuringBotMessageDao.getAllMessages().toDomain()
}

override suspend fun insertMessages(messages: List<KuringBotMessage>) {
kuringBotMessageDao.insertMessages(messages.toEntity())
}

override suspend fun insertMessage(message: KuringBotMessage) {
kuringBotMessageDao.insertMessage(message.toEntity())
}

override suspend fun getQueryCount(from: LocalDate, to: LocalDate): Int {
return kuringBotMessageDao.getQueryCount(from.toEpochSecond(), to.toEpochSecond())
}

companion object {
internal const val UNKNOWN_ERROR = "예상치 못한 오류가 발생했어요. 다음에 다시 시도해 주세요."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.ku_stacks.ku_ring.ai

import com.ku_stacks.ku_ring.ai.repository.KuringBotRepository
import com.ku_stacks.ku_ring.ai.repository.KuringBotRepositoryImpl
import com.ku_stacks.ku_ring.local.room.KuringBotMessageDao
import com.ku_stacks.ku_ring.remote.kuringbot.KuringBotClient
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.exceptions.base.MockitoException

class KuringBotRepositoryImplTest {
private val kuringBotClient = Mockito.mock(KuringBotClient::class.java)
private val dao = Mockito.mock(KuringBotMessageDao::class.java)
private lateinit var repository: KuringBotRepository

private val query = "교내,외 장학금 및 학자금 대출 관련 전화번호들을 안내를 해줘"

@Before
fun setup() {
repository = KuringBotRepositoryImpl(kuringBotClient, dao)
}

@Test
fun `test when exception is thrown`() = runTest {
val tokens = mutableListOf<String>()
val onReceived: (String) -> Unit = { data ->
tokens.add(data)
}

// given
val token = System.currentTimeMillis().toString()
Mockito.`when`(kuringBotClient.openKuringBotConnection(query, token, onReceived))
.thenThrow(MockitoException("test exception"))

// when
repository.openKuringBotSession(query, token, onReceived)

// then
assert(tokens.size == 1)
assert(tokens.contains(KuringBotRepositoryImpl.UNKNOWN_ERROR))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ku_stacks.ku_ring.domain

import java.time.LocalDateTime

data class KuringBotMessage(
val id: Int,
val message: String,
val postedDate: LocalDateTime,
val isQuery: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ package com.ku_stacks.ku_ring.remote.kuringbot

interface KuringBotClient {
/**
* 쿠링봇 SSE 세션을 열고, 데이터를 문자열 형식으로 받아 처리한다.
* SSE 데이터를 받았을 때, [onReceived]에 접두어 `data:`를 떼지 않고 그대로 제공한다.
* 쿠링봇 연결을 만들고, 응답을 받아 문자열 형식으로 처리한다.
*
* @param query 질문 내용
* @param token 사용자 FCM 토큰
* @param onReceived 데이터를 수신했을 때 실행할 작업.
*/
suspend fun openKuringBotSSESession(
suspend fun openKuringBotConnection(
query: String,
token: String,
onReceived: (String) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class KuringBotSSEClient @Inject constructor(
private val client: HttpClient,
) : KuringBotClient {

override suspend fun openKuringBotSSESession(
override suspend fun openKuringBotConnection(
query: String,
token: String,
onReceived: (String) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class KuringBotSSEClientTest {

// when
val tokens = mutableListOf<String>()
kuringBotClient.openKuringBotSSESession(query, testToken) {
kuringBotClient.openKuringBotConnection(query, testToken) {
tokens.add(it)
}

Expand All @@ -43,7 +43,7 @@ class KuringBotSSEClientTest {

// when
val tokens = mutableListOf<String>()
kuringBotClient.openKuringBotSSESession(wrongQuery, testToken) {
kuringBotClient.openKuringBotConnection(wrongQuery, testToken) {
tokens.add(it)
}

Expand All @@ -57,11 +57,11 @@ class KuringBotSSEClientTest {
val testToken = System.currentTimeMillis().toString()

// when
kuringBotClient.openKuringBotSSESession(query, testToken) {}
kuringBotClient.openKuringBotSSESession(query, testToken) {}
kuringBotClient.openKuringBotConnection(query, testToken) {}
kuringBotClient.openKuringBotConnection(query, testToken) {}

val tokens = mutableListOf<String>()
kuringBotClient.openKuringBotSSESession(query, testToken) {
kuringBotClient.openKuringBotConnection(query, testToken) {
tokens.add(it)
}

Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ include(
":data:notice",
":data:notice:test",
":core:preferences",
":data:ai",
":data:push",
":data:push:test",
":data:user",
Expand Down

0 comments on commit 65ffa6a

Please sign in to comment.