Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] #34 알림 화면 구현 #40

Merged
merged 5 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading