Skip to content

Commit

Permalink
[Feature] #34 알림 화면 구현 (#40)
Browse files Browse the repository at this point in the history
* [BASE] #34 feature:alarm 모듈 생성

* [FEATURE] #34 Alert관련 API, Datasource, Repository, UseCase 구현

* [FEATURE] #34 알림함 화면 구현 및 rootNavHost에 추가

* [FIX] #34 알림함 화면에서 페이지네이션이 정상적으로 수행되지 않던 문제 수정 및 알람 제거시 애니메이션 효과 추가

* [CHORE] #34 알림함 화면 Preview 문제 해결
  • Loading branch information
l5x5l authored Aug 18, 2024
1 parent a65cc3a commit 4fada09
Show file tree
Hide file tree
Showing 39 changed files with 1,140 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ dependencies {
implementation(project(":domain"))
implementation(project(":feature:addlink"))
implementation(project(":feature:addpokit"))
implementation(project(":feature:alarm"))
implementation(project(":feature:login"))
implementation(project(":feature:pokitdetail"))
implementation(project(":feature:search"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ object Setting {
object EditNickname {
val route: String = "editNickname"
}

object Alarm {
val route: String = "alarm"
}
14 changes: 13 additions & 1 deletion app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import com.strayalpaca.addpokit.AddPokitViewModel
import com.strayalpaca.pokitdetail.PokitDetailScreenContainer
import com.strayalpaca.pokitdetail.PokitDetailViewModel
import pokitmons.pokit.LoginViewModel
import pokitmons.pokit.alarm.AlarmScreenContainer
import pokitmons.pokit.alarm.AlarmViewModel
import pokitmons.pokit.home.HomeScreen
import pokitmons.pokit.home.pokit.PokitViewModel
import pokitmons.pokit.login.LoginScreen
import pokitmons.pokit.navigation.PokitDetail.pokitIdArg
import pokitmons.pokit.search.SearchScreenContainer
import pokitmons.pokit.search.SearchViewModel
import pokitmons.pokit.settings.SettingViewModel
Expand Down Expand Up @@ -136,5 +137,16 @@ fun RootNavHost(
onNavigateAddPokit = { navHostController.navigate(AddPokit.route) }
)
}

composable(route = Alarm.route) {
val viewModel: AlarmViewModel = hiltViewModel()
AlarmScreenContainer(
viewModel = viewModel,
onBackPressed = navHostController::popBackStack,
onNavigateToLinkModify = { linkId ->
navHostController.navigate("${AddLink.route}?${AddLink.linkIdArg}=$linkId")
}
)
}
}
}
22 changes: 22 additions & 0 deletions data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pokitmons.pokit.data.api

import pokitmons.pokit.data.model.alert.GetAlertsResponse
import pokitmons.pokit.domain.model.link.LinksSort
import retrofit2.http.GET
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query

interface AlertApi {
@GET("alert")
suspend fun getAlerts(
@Query("page") page: Int,
@Query("size") size: Int,
@Query("sort") sort: List<String> = listOf(LinksSort.RECENT.value),
): GetAlertsResponse

@PUT("alert/{alertId}")
suspend fun deleteAlert(
@Path("alertId") alertId: Int,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pokitmons.pokit.data.datasource.remote.alert

import pokitmons.pokit.data.model.alert.GetAlertsResponse

interface AlertDataSource {
suspend fun getAlerts(page: Int, size: Int): GetAlertsResponse
suspend fun deleteAlert(alertId: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package pokitmons.pokit.data.datasource.remote.alert

import pokitmons.pokit.data.api.AlertApi
import pokitmons.pokit.data.model.alert.GetAlertsResponse
import javax.inject.Inject

class RemoteAlertDataSource @Inject constructor(
private val api: AlertApi,
) : AlertDataSource {
override suspend fun getAlerts(page: Int, size: Int): GetAlertsResponse {
return api.getAlerts(page = page, size = size)
}

override suspend fun deleteAlert(alertId: Int) {
return api.deleteAlert(alertId)
}
}
23 changes: 23 additions & 0 deletions data/src/main/java/pokitmons/pokit/data/di/alert/AlertModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pokitmons.pokit.data.di.alert

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource
import pokitmons.pokit.data.datasource.remote.alert.RemoteAlertDataSource
import pokitmons.pokit.data.repository.alert.AlertRepositoryImpl
import pokitmons.pokit.domain.repository.alert.AlertRepository
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class AlertModule {
@Binds
@Singleton
abstract fun bindAlertRepository(alertRepositoryImpl: AlertRepositoryImpl): AlertRepository

@Binds
@Singleton
abstract fun bindAlertDataSource(alertDataSourceImpl: RemoteAlertDataSource): AlertDataSource
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import pokitmons.pokit.data.api.AlertApi
import pokitmons.pokit.data.api.AuthApi
import pokitmons.pokit.data.api.LinkApi
import pokitmons.pokit.data.api.PokitApi
Expand Down Expand Up @@ -91,4 +92,9 @@ object NetworkModule {
fun provideRemindService(retrofit: Retrofit): RemindApi {
return retrofit.create(RemindApi::class.java)
}

@Provides
fun provideAlertService(retrofit: Retrofit): AlertApi {
return retrofit.create(AlertApi::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pokitmons.pokit.data.mapper.alert

import pokitmons.pokit.data.model.alert.GetAlertsResponse
import pokitmons.pokit.domain.model.alert.Alarm

object AlertMapper {
fun mapperToAlarmList(response: GetAlertsResponse): List<Alarm> {
return response.data.map { data ->
Alarm(
id = data.id,
contentId = data.contentId,
thumbnail = data.thumbNail,
title = data.title,
createdAt = data.createdAt
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pokitmons.pokit.data.model.alert

import kotlinx.serialization.Serializable

@Serializable
data class GetAlertsResponse(
val data: List<Data> = emptyList(),
val page: Int = 0,
val size: Int = 10,
val sort: List<Sort> = emptyList(),
val hasNext: Boolean = true,
) {
@Serializable
data class Data(
val id: Int,
val userId: Int,
val contentId: Int,
val thumbNail: String,
val title: String,
val body: String,
val createdAt: String,
)

@Serializable
data class Sort(
val direction: String,
val nullHandling: String,
val ascending: Boolean,
val property: String,
val ignoreCase: Boolean,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pokitmons.pokit.data.repository.alert

import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource
import pokitmons.pokit.data.mapper.alert.AlertMapper
import pokitmons.pokit.data.model.common.parseErrorResult
import pokitmons.pokit.domain.commom.PokitResult
import pokitmons.pokit.domain.model.alert.Alarm
import pokitmons.pokit.domain.repository.alert.AlertRepository
import javax.inject.Inject

class AlertRepositoryImpl @Inject constructor(
private val dataSource: AlertDataSource,
) : AlertRepository {
override suspend fun getAlerts(page: Int, size: Int): PokitResult<List<Alarm>> {
return runCatching {
val response = dataSource.getAlerts(page = page, size = size)
val mappedResponse = AlertMapper.mapperToAlarmList(response)
PokitResult.Success(result = mappedResponse)
}.getOrElse { throwable ->
parseErrorResult(throwable)
}
}

override suspend fun deleteAlert(alertId: Int): PokitResult<Unit> {
return runCatching {
dataSource.deleteAlert(alertId)
PokitResult.Success(result = Unit)
}.getOrElse { throwable ->
parseErrorResult(throwable)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package pokitmons.pokit.data.datasource

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.coEvery
import io.mockk.mockk
import pokitmons.pokit.data.api.AlertApi
import pokitmons.pokit.data.datasource.remote.alert.RemoteAlertDataSource
import pokitmons.pokit.data.model.alert.GetAlertsResponse

val alertApi: AlertApi = mockk()

class RemoteAlertDataSourceTest : DescribeSpec({
val remoteAlertDataSource = RemoteAlertDataSource(alertApi)
describe("알림 목록 조회시") {
context("알림 목록 조회가 정상적으로 수행되면") {
coEvery { alertApi.getAlerts(page = 0, size = 0) } returns GetAlertsResponse()
it("알림 목록이 반환된다.") {
val response = remoteAlertDataSource.getAlerts(page = 0, size = 0)
response.shouldBeInstanceOf<GetAlertsResponse>()
}
}

context("에러가 발생했다면") {
coEvery { alertApi.getAlerts(page = 0, size = 0) } throws IllegalArgumentException("error")
it("동일한 에러가 발생한다.") {
val exception = shouldThrow<Exception> {
remoteAlertDataSource.getAlerts(page = 0, size = 0)
}
exception.message shouldBe "error"
}
}
}

describe("알림 제거시") {
context("에러가 발생했다면") {
coEvery { alertApi.deleteAlert(alertId = 0) } throws IllegalArgumentException("error")
it("동일한 에러가 발생한다.") {
val exception = shouldThrow<Exception> {
remoteAlertDataSource.deleteAlert(alertId = 0)
}
exception.message shouldBe "error"
}
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package pokitmons.pokit.data.repository

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.coEvery
import io.mockk.mockk
import pokitmons.pokit.data.datasource.remote.alert.AlertDataSource
import pokitmons.pokit.data.model.alert.GetAlertsResponse
import pokitmons.pokit.data.repository.alert.AlertRepositoryImpl
import pokitmons.pokit.domain.commom.PokitResult
import pokitmons.pokit.domain.model.alert.Alarm

val alertDataSource: AlertDataSource = mockk()

class AlertRepositoryImplTest : DescribeSpec({
val alertRepository = AlertRepositoryImpl(alertDataSource)
describe("알림 목록 조회시") {
context("알림 목록 조회가 정상적으로 수행되면") {
coEvery { alertDataSource.getAlerts(page = 0, size = 0) } returns GetAlertsResponse()
it("PokitResult로 래핑된 알림 목록이 반환된다.") {
val response = alertRepository.getAlerts(page = 0, size = 0)
response.shouldBeInstanceOf<PokitResult.Success<List<Alarm>>>()
}
}

context("에러가 발생했다면") {
coEvery { alertDataSource.getAlerts(page = 0, size = 0) } throws IllegalArgumentException()
it("PokitResult로 래핑된 에러 내용이 반환된다.") {
val response = alertRepository.getAlerts(page = 0, size = 0)
response.shouldBeInstanceOf<PokitResult.Error>()
}
}
}

describe("알림 제거시") {
context("제거가 정상적으로 수행되면") {
coEvery { alertDataSource.deleteAlert(alertId = 0) } returns Unit
it("데이터가 없는 PokitResult.Success가 반환된다.") {
val response = alertRepository.deleteAlert(alertId = 0)
response.shouldBeInstanceOf<PokitResult.Success<Unit>>()
}
}

context("에러가 발생했다면") {
coEvery { alertDataSource.deleteAlert(alertId = 0) } throws IllegalArgumentException()
it("PokitResult로 래핑된 에러 내용이 반환된다.") {
val response = alertRepository.deleteAlert(alertId = 0)
response.shouldBeInstanceOf<PokitResult.Error>()
}
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pokitmons.pokit.domain.model.alert

data class Alarm(
val id: Int,
val contentId: Int,
val thumbnail: String? = null,
val title: String,
val createdAt: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package pokitmons.pokit.domain.repository.alert

import pokitmons.pokit.domain.commom.PokitResult
import pokitmons.pokit.domain.model.alert.Alarm

interface AlertRepository {
suspend fun getAlerts(page: Int, size: Int): PokitResult<List<Alarm>>
suspend fun deleteAlert(alertId: Int): PokitResult<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pokitmons.pokit.domain.usecase.alert

import pokitmons.pokit.domain.commom.PokitResult
import pokitmons.pokit.domain.repository.alert.AlertRepository
import javax.inject.Inject

class DeleteAlertUseCase @Inject constructor(
private val repository: AlertRepository,
) {
suspend fun deleteAlert(alertId: Int): PokitResult<Unit> {
return repository.deleteAlert(alertId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package pokitmons.pokit.domain.usecase.alert

import pokitmons.pokit.domain.commom.PokitResult
import pokitmons.pokit.domain.model.alert.Alarm
import pokitmons.pokit.domain.repository.alert.AlertRepository
import javax.inject.Inject

class GetAlertsUseCase @Inject constructor(
private val repository: AlertRepository,
) {
suspend fun getAlerts(page: Int, size: Int): PokitResult<List<Alarm>> {
return repository.getAlerts(page = page, size = size)
}
}
1 change: 1 addition & 0 deletions feature/alarm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
Loading

0 comments on commit 4fada09

Please sign in to comment.