diff --git a/core/data/src/main/java/com/youthtalk/di/ApiModule.kt b/core/data/src/main/java/com/youthtalk/di/ApiModule.kt index d97ea2e7..ad013b21 100644 --- a/core/data/src/main/java/com/youthtalk/di/ApiModule.kt +++ b/core/data/src/main/java/com/youthtalk/di/ApiModule.kt @@ -8,12 +8,10 @@ import com.youthtalk.data.LoginService import com.youthtalk.data.PolicyService import com.youthtalk.data.ReportService import com.youthtalk.data.UserService -import com.youthtalk.sse.SseClient import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.util.concurrent.TimeUnit import javax.inject.Qualifier import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -41,10 +39,6 @@ object ApiModule { @Retention(AnnotationRetention.RUNTIME) annotation class Main - @Qualifier - @Retention(AnnotationRetention.RUNTIME) - annotation class Sse - @Provides @Singleton fun provideJson(): Json = Json { @@ -52,46 +46,12 @@ object ApiModule { coerceInputValues = true } - @Provides - @Singleton - @Sse - fun provideOkHttpClient(dataStoreDataSource: DataStoreDataSource): OkHttpClient { - val interceptor = - Interceptor { chain -> - val request = chain.request().newBuilder() - .addHeader("Content-Type", "application/json") - .build() - - val response = chain.proceed(request) - val token = response.headers["Authorization"] - val refreshToken = response.headers["Authorization-refresh"] - if (token != null && refreshToken != null) { - CoroutineScope(Dispatchers.IO).launch { - dataStoreDataSource.saveAccessToken(token) - dataStoreDataSource.saveRefreshToken(refreshToken) - } - } - response - } - - return OkHttpClient.Builder() - .addInterceptor(interceptor) - .readTimeout(0, TimeUnit.MILLISECONDS) - .build() - } - @Provides @Singleton fun provideConverterFactory(json: Json): Factory { return json.asConverterFactory("application/json".toMediaType()) } - @Provides - @Singleton - fun provideSseClient(@Sse okHttpClient: OkHttpClient): SseClient { - return SseClient(okHttpClient) - } - @Singleton @Provides @Login diff --git a/core/data/src/main/java/com/youthtalk/di/DataModule.kt b/core/data/src/main/java/com/youthtalk/di/DataModule.kt index 6f60013a..62c2e269 100644 --- a/core/data/src/main/java/com/youthtalk/di/DataModule.kt +++ b/core/data/src/main/java/com/youthtalk/di/DataModule.kt @@ -8,7 +8,6 @@ import com.core.dataapi.repository.PolicyRepository import com.core.dataapi.repository.ReportRepository import com.core.dataapi.repository.SearchRepository import com.core.dataapi.repository.SpecPolicyRepository -import com.core.dataapi.repository.SseRepository import com.core.dataapi.repository.UserRepository import com.core.datastore.datasource.DataSource import com.core.datastore.datasource.DataStoreDataSource @@ -20,7 +19,6 @@ import com.youthtalk.repository.PolicyRepositoryImpl import com.youthtalk.repository.ReportRepositoryImpl import com.youthtalk.repository.SearchRepositoryImpl import com.youthtalk.repository.SpecPolicyRepositoryImpl -import com.youthtalk.repository.SseRepositoryImpl import com.youthtalk.repository.UserRepositoryImpl import dagger.Binds import dagger.Module @@ -60,7 +58,4 @@ abstract class DataModule { @Binds abstract fun bindsReportRepository(repository: ReportRepositoryImpl): ReportRepository - - @Binds - abstract fun bindsSseRepository(repository: SseRepositoryImpl): SseRepository } diff --git a/core/data/src/main/java/com/youthtalk/repository/HomeRepositoryImpl.kt b/core/data/src/main/java/com/youthtalk/repository/HomeRepositoryImpl.kt index 4ac3c3e8..d46f986c 100644 --- a/core/data/src/main/java/com/youthtalk/repository/HomeRepositoryImpl.kt +++ b/core/data/src/main/java/com/youthtalk/repository/HomeRepositoryImpl.kt @@ -7,42 +7,17 @@ import com.youthtalk.mapper.toDomain import com.youthtalk.model.home.HomeData import com.youthtalk.model.home.NewPolicies import com.youthtalk.model.typeenum.SortType -import com.youthtalk.utils.ErrorUtils.throwableError +import com.youthtalk.utils.ErrorUtils.createResult import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import timber.log.Timber class HomeRepositoryImpl @Inject constructor( private val policyService: PolicyService ) : HomeRepository { - override fun getHome(): Flow = flow { - Timber.e("HomeRepositoryImpl getHome start") - runCatching { policyService.getHome() } - .onSuccess { homeData -> - Timber.e("HomeRepositoryImpl getHome Success $homeData") - homeData.data?.let { data -> - emit(data.toDomain()) - } ?: throw NoDataException() - } - .onFailure { error -> - Timber.e("HomeRepositoryImpl getHome error : $error") - throwableError(error) - } + override suspend fun getHome(): Result = createResult { + policyService.getHome().data?.toDomain() ?: throw NoDataException() } - override fun getNewPolicies(sortType: SortType): Flow = flow { - Timber.e("HomeRepositoryImpl getHome start") - runCatching { policyService.getNewPolicies(sortType) } - .onSuccess { homeData -> - Timber.e("HomeRepositoryImpl getNewPolicies Success $homeData") - homeData.data?.let { data -> - emit(data.toDomain()) - } ?: throw NoDataException() - } - .onFailure { error -> - Timber.e("HomeRepositoryImpl getNewPolicies error : $error") - throwableError(error) - } + override suspend fun getNewPolicies(sortType: SortType): Result = createResult { + policyService.getNewPolicies(sortType).data?.toDomain() ?: throw NoDataException() } } diff --git a/core/data/src/main/java/com/youthtalk/repository/SseRepositoryImpl.kt b/core/data/src/main/java/com/youthtalk/repository/SseRepositoryImpl.kt deleted file mode 100644 index 2df4e4e7..00000000 --- a/core/data/src/main/java/com/youthtalk/repository/SseRepositoryImpl.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.youthtalk.repository - -import android.content.Context -import com.core.dataapi.intent.PendingIntentProvider -import com.core.dataapi.repository.SseRepository -import com.core.datastore.datasource.DataStoreDataSource -import com.youthtalk.model.notification.SseNotification -import com.youthtalk.sse.NotificationSender -import com.youthtalk.sse.SseClient -import javax.inject.Inject -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.SerializationException -import kotlinx.serialization.json.Json -import timber.log.Timber - -class SseRepositoryImpl @Inject constructor( - private val context: Context, - private val dataSource: DataStoreDataSource, - private val sseClient: SseClient, - private val mainPendingIntentProvider: PendingIntentProvider -) : SseRepository { - override fun startSseService() { - val token: String = runBlocking { - dataSource.getAccessToken().first() - } ?: return - - Timber.e("startSseService token $token") - sseClient.start("Bearer $token", "http://13.209.36.122/api/v1/notifications/subscribe") { message -> - Timber.e("SseService sseClient message $message") - try { - val notification = Json.decodeFromString(message) - val data = mutableMapOf() - notification.postId?.let { - data.put("postId", it) - } - notification.policyId?.let { - data.put("policyId", it) - } - val intent = mainPendingIntentProvider.createMainActivityIntent(data) - NotificationSender.notify(context, notification, intent) - } catch (e: SerializationException) { - Timber.e("SseService SerializationException $e") - } catch (e: IllegalArgumentException) { - Timber.e("SseService IllegalArgumentException $e") - } - } - } - - override fun stopSseService() { - sseClient.stop() - } -} diff --git a/core/data/src/main/java/com/youthtalk/sse/NotificationSender.kt b/core/data/src/main/java/com/youthtalk/sse/NotificationSender.kt deleted file mode 100644 index 587e1973..00000000 --- a/core/data/src/main/java/com/youthtalk/sse/NotificationSender.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.youthtalk.sse - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import androidx.core.app.NotificationCompat -import com.youthtalk.model.notification.SseNotification - -object NotificationSender { - private const val CHANNEL_ID = "youth_talk_sse_message" - - fun notify(context: Context, data: SseNotification, pendingIntent: PendingIntent) { - val manager = context.getSystemService(NotificationManager::class.java) - manager.createNotificationChannel( - NotificationChannel(CHANNEL_ID, "SSE 알림", NotificationManager.IMPORTANCE_DEFAULT) - ) - - val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle(data.title) - .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentIntent(pendingIntent) - .setStyle( - NotificationCompat.BigTextStyle() - .bigText(data.message) - ) - .setAutoCancel(true) - .build() - - manager.notify(System.currentTimeMillis().toInt(), notification) - } -} diff --git a/core/data/src/main/java/com/youthtalk/sse/SseClient.kt b/core/data/src/main/java/com/youthtalk/sse/SseClient.kt deleted file mode 100644 index d437c37d..00000000 --- a/core/data/src/main/java/com/youthtalk/sse/SseClient.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.youthtalk.sse - -import javax.inject.Inject -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.sse.EventSource -import okhttp3.sse.EventSourceListener -import okhttp3.sse.EventSources -import timber.log.Timber - -class SseClient @Inject constructor( - private val okHttpClient: OkHttpClient -) { - private var es: EventSource? = null - - fun start(token: String, url: String, onMessage: (String) -> Unit) { - runBlocking { - es?.cancel() - es = null - delay(500L) - } - - Timber.e("SseClient start") - val request = Request.Builder().url(url).addHeader("Authorization", token).build() - EventSources.createFactory(okHttpClient) - .newEventSource( - request, - object : EventSourceListener() { - override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) { - Timber.e("알림 시작 메세지 $data") - onMessage(data) - } - - override fun onClosed(eventSource: EventSource) { - Timber.e("newEventSource onClosed") - super.onClosed(eventSource) - } - - override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) { - Timber.e("newEventSource onFailure error $t") - super.onFailure(eventSource, t, response) - } - - override fun onOpen(eventSource: EventSource, response: Response) { - Timber.e("newEventSource onOpen ${response.body}") - super.onOpen(eventSource, response) - } - } - ) - .also { - es = it - } - Timber.e("eventSource start last lane $es") - } - - fun stop() { - Timber.e("SseClient stop eventSource is null? $es") - es?.cancel() - es = null - } -} diff --git a/core/data/src/test/java/com/youthtalk/repository/HomeRepositoryTest.kt b/core/data/src/test/java/com/youthtalk/repository/HomeRepositoryTest.kt new file mode 100644 index 00000000..ae7dd106 --- /dev/null +++ b/core/data/src/test/java/com/youthtalk/repository/HomeRepositoryTest.kt @@ -0,0 +1,64 @@ +package com.youthtalk.repository + +import com.core.dataapi.repository.HomeRepository +import com.youthtalk.data.PolicyService +import com.youthtalk.dto.CommonResponse +import com.youthtalk.dto.home.HomeDataResponse +import com.youthtalk.dto.home.NewPoliciesResponse +import com.youthtalk.mapper.toDomain +import com.youthtalk.model.typeenum.SortType +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(MockitoJUnitRunner::class) +class HomeRepositoryTest { + + private lateinit var sut: HomeRepository + + @Mock + private lateinit var policyService: PolicyService + + @Before + fun setUp() { + sut = HomeRepositoryImpl(policyService) + } + + @Test + fun given_whenGetHomeData_whenReturnsHomeData() { + runTest { + // given + val homeDataResponse = HomeDataResponse(listOf(), listOf(), listOf()) + whenever(policyService.getHome()).thenReturn(CommonResponse(200, "요청에 성공하였습니다", "S01", homeDataResponse)) + + // when + val result = sut.getHome().getOrThrow() + + // then + assertEquals(homeDataResponse.toDomain(), result) + verify(policyService).getHome() + } + } + + @Test + fun givenSortType_whenGetNewPolicies_whenReturnsPolicies() { + runTest { + // given + val newPoliciesResponse = NewPoliciesResponse(listOf(), listOf(), listOf(), listOf(), listOf(), listOf()) + whenever(policyService.getNewPolicies()).thenReturn(CommonResponse(200, "요창에 성공하였습니다.", "S01", newPoliciesResponse)) + + // when + val result = sut.getNewPolicies(SortType.RECENT).getOrThrow() + + // then + assertEquals(newPoliciesResponse.toDomain(), result) + verify(policyService).getNewPolicies() + } + } +} diff --git a/core/dataApi/src/main/java/com/core/dataapi/repository/HomeRepository.kt b/core/dataApi/src/main/java/com/core/dataapi/repository/HomeRepository.kt index 8ed5a54c..3a983507 100644 --- a/core/dataApi/src/main/java/com/core/dataapi/repository/HomeRepository.kt +++ b/core/dataApi/src/main/java/com/core/dataapi/repository/HomeRepository.kt @@ -3,9 +3,8 @@ package com.core.dataapi.repository import com.youthtalk.model.home.HomeData import com.youthtalk.model.home.NewPolicies import com.youthtalk.model.typeenum.SortType -import kotlinx.coroutines.flow.Flow interface HomeRepository { - fun getHome(): Flow - fun getNewPolicies(sortType: SortType): Flow + suspend fun getHome(): Result + suspend fun getNewPolicies(sortType: SortType): Result } diff --git a/core/dataApi/src/main/java/com/core/dataapi/repository/SseRepository.kt b/core/dataApi/src/main/java/com/core/dataapi/repository/SseRepository.kt deleted file mode 100644 index 566cdda4..00000000 --- a/core/dataApi/src/main/java/com/core/dataapi/repository/SseRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.core.dataapi.repository - -interface SseRepository { - fun startSseService() - fun stopSseService() -} diff --git a/core/domain/src/main/java/com/core/domain/usercase/home/GetHomeDataUseCase.kt b/core/domain/src/main/java/com/core/domain/usercase/home/GetHomeDataUseCase.kt index 76ba1f41..1c2c3647 100644 --- a/core/domain/src/main/java/com/core/domain/usercase/home/GetHomeDataUseCase.kt +++ b/core/domain/src/main/java/com/core/domain/usercase/home/GetHomeDataUseCase.kt @@ -6,5 +6,5 @@ import javax.inject.Inject class GetHomeDataUseCase @Inject constructor( private val homeRepository: HomeRepository ) { - operator fun invoke() = homeRepository.getHome() + suspend operator fun invoke() = homeRepository.getHome() } diff --git a/core/domain/src/main/java/com/core/domain/usercase/home/GetNewPolicesUseCase.kt b/core/domain/src/main/java/com/core/domain/usercase/home/GetNewPolicesUseCase.kt index 552c5485..138a174d 100644 --- a/core/domain/src/main/java/com/core/domain/usercase/home/GetNewPolicesUseCase.kt +++ b/core/domain/src/main/java/com/core/domain/usercase/home/GetNewPolicesUseCase.kt @@ -7,5 +7,5 @@ import javax.inject.Inject class GetNewPolicesUseCase @Inject constructor( private val homeRepository: HomeRepository ) { - operator fun invoke(sortType: SortType = SortType.RECENT) = homeRepository.getNewPolicies(sortType) + suspend operator fun invoke(sortType: SortType = SortType.RECENT) = homeRepository.getNewPolicies(sortType) } diff --git a/core/domain/src/main/java/com/core/domain/usercase/sse/SseServiceUseCase.kt b/core/domain/src/main/java/com/core/domain/usercase/sse/SseServiceUseCase.kt deleted file mode 100644 index 495253c9..00000000 --- a/core/domain/src/main/java/com/core/domain/usercase/sse/SseServiceUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.core.domain.usercase.sse - -import com.core.dataapi.repository.SseRepository -import javax.inject.Inject - -class SseServiceUseCase @Inject constructor( - private val sseRepository: SseRepository -) { - operator fun invoke() = sseRepository.startSseService() -} diff --git a/core/domain/src/main/java/com/core/domain/usercase/sse/SseStopServiceUseCase.kt b/core/domain/src/main/java/com/core/domain/usercase/sse/SseStopServiceUseCase.kt deleted file mode 100644 index 9414fa91..00000000 --- a/core/domain/src/main/java/com/core/domain/usercase/sse/SseStopServiceUseCase.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.core.domain.usercase.sse - -import com.core.dataapi.repository.SseRepository -import javax.inject.Inject - -class SseStopServiceUseCase @Inject constructor( - private val sseRepository: SseRepository -) { - operator fun invoke() = sseRepository.stopSseService() -} diff --git a/feature/home/src/main/java/com/core/home/viewmodel/HomeViewModel.kt b/feature/home/src/main/java/com/core/home/viewmodel/HomeViewModel.kt index 77e7161d..79e9d77e 100644 --- a/feature/home/src/main/java/com/core/home/viewmodel/HomeViewModel.kt +++ b/feature/home/src/main/java/com/core/home/viewmodel/HomeViewModel.kt @@ -17,7 +17,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import timber.log.Timber @@ -179,26 +178,23 @@ class HomeViewModel @Inject constructor( private fun getHomeData(isLoading: Boolean = true) { viewModelScope.launch { - combine( - getUserUseCase(), - getHomeDataUseCase(), - getNewPolicesUseCase() - ) { user, homeData, newPolices -> - HomeUiState( - isLoading = false, - user = user, - homeData = homeData, - newPolicies = newPolices - ) - } + val homeData = getHomeDataUseCase() + val newPolicies = getNewPolicesUseCase() + + getUserUseCase() .onStart { setState { copy(isLoading = isLoading) } - } - .catch { - Timber.e("HomeViewModel getHomeData error $it") - } - .collectLatest { uiState -> - setState { uiState } + }.catch { + Timber.e("error : $it") + }.collectLatest { user -> + setState { + HomeUiState( + isLoading = false, + user = user, + homeData = homeData.getOrThrow(), + newPolicies = newPolicies.getOrThrow() + ) + } } } } diff --git a/feature/home/src/main/java/com/core/home/viewmodel/NewPolicyViewModel.kt b/feature/home/src/main/java/com/core/home/viewmodel/NewPolicyViewModel.kt index 81df4e28..e025014a 100644 --- a/feature/home/src/main/java/com/core/home/viewmodel/NewPolicyViewModel.kt +++ b/feature/home/src/main/java/com/core/home/viewmodel/NewPolicyViewModel.kt @@ -13,7 +13,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import timber.log.Timber @@ -45,15 +44,12 @@ class NewPolicyViewModel @Inject constructor( private fun getPolices(sortType: SortType) { viewModelScope.launch { + setState { copy(isLoading = true, sortType = sortType) } getNewPolicesUseCase(sortType) - .onStart { - setState { copy(isLoading = true, sortType = sortType) } - } - .catch { - Timber.e("NewPolicyViewModel getPolices error $it") - } - .collectLatest { newPolies -> + .onSuccess { newPolies -> setState { copy(isLoading = false, newPolicies = newPolies, sortType = sortType) } + }.onFailure { + Timber.e("error : $it") } } } diff --git a/feature/main/src/main/java/com/youthtalk/MainActivity.kt b/feature/main/src/main/java/com/youthtalk/MainActivity.kt index d2db37c3..55a3b221 100644 --- a/feature/main/src/main/java/com/youthtalk/MainActivity.kt +++ b/feature/main/src/main/java/com/youthtalk/MainActivity.kt @@ -1,13 +1,9 @@ package com.youthtalk -import android.Manifest -import android.content.pm.PackageManager -import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -42,12 +38,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.core.community.navigation.navigateCommunityDetail -import com.core.domain.usercase.sse.SseServiceUseCase -import com.core.domain.usercase.sse.SseStopServiceUseCase import com.core.navigation.navigator.LoginNavigator import com.feature.policydetail.navigation.navigatePolicyDetail import com.youthtalk.component.dialog.ModalDialog @@ -67,24 +60,8 @@ class MainActivity : ComponentActivity() { @Inject lateinit var loginNavigator: LoginNavigator - @Inject - lateinit var sseServiceUseCase: SseServiceUseCase - - @Inject - lateinit var sseStopServiceUseCase: SseStopServiceUseCase - private val viewModel: MainViewModel by viewModels() - private val launcher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - sseServiceUseCase() - } else { - viewModel.setNotificationDialog() - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.e("MainActivity onCreate") @@ -171,20 +148,6 @@ class MainActivity : ComponentActivity() { } } - override fun onResume() { - Timber.e("activity onResume") - super.onResume() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - sseServiceUseCase() - } else { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this@MainActivity, Manifest.permission.POST_NOTIFICATIONS)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - launcher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - } - } - } - @Composable fun AnimatedCustomSnackbarHost(hostState: SnackbarHostState) { var visible by remember { mutableStateOf(false) } diff --git a/feature/main/src/main/java/com/youthtalk/MainViewModel.kt b/feature/main/src/main/java/com/youthtalk/MainViewModel.kt index c7703dcc..d549ce5e 100644 --- a/feature/main/src/main/java/com/youthtalk/MainViewModel.kt +++ b/feature/main/src/main/java/com/youthtalk/MainViewModel.kt @@ -14,8 +14,4 @@ sealed interface MainUiEffect { class MainViewModel @Inject constructor() : ViewModel() { private val _effect = MutableSharedFlow() val effect = _effect.asSharedFlow() - - fun setNotificationDialog() { - _effect.tryEmit(MainUiEffect.Notification) - } }