Skip to content

Commit 3e55338

Browse files
authored
Merge pull request #1510 from BCSDLab/feature/1507-add-notification-screen
[Feature] 알림 화면 구현
2 parents 384ce90 + 59a842f commit 3e55338

36 files changed

Lines changed: 971 additions & 24 deletions

File tree

core/navigation/src/main/java/in/koreatech/koin/core/navigation/Navigator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ interface Navigator {
1818
vararg args: Pair<String, Any?> // Extra IDs
1919
): Intent
2020

21+
fun navigateToScheme(
22+
context: Context,
23+
extraUrl: String
24+
): Intent
25+
2126
fun navigateToSignIn(
2227
context: Context,
2328
redirectUrl: String? = null

data/src/main/java/in/koreatech/koin/data/dao/NotificationDao.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import androidx.room.Transaction
88
import `in`.koreatech.koin.data.constant.DBConstant
99
import `in`.koreatech.koin.data.entity.NotificationEntity
1010
import java.time.LocalDateTime
11+
import kotlinx.coroutines.flow.Flow
1112

1213
@Suppress("Detekt.TooManyFunctions")
1314
@Dao
@@ -21,6 +22,9 @@ interface NotificationDao {
2122
@Query("SELECT * FROM ${DBConstant.NOTIFICATION}")
2223
suspend fun getNotifications(): List<NotificationEntity>
2324

25+
@Query("SELECT * FROM ${DBConstant.NOTIFICATION}")
26+
fun getNotificationsFlow(): Flow<List<NotificationEntity>>
27+
2428
@Query("SELECT * FROM ${DBConstant.NOTIFICATION} WHERE originUrl = :url")
2529
suspend fun getNotificationByUrl(url: String): NotificationEntity
2630

data/src/main/java/in/koreatech/koin/data/repository/NotificationRepositoryImpl.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import `in`.koreatech.koin.domain.model.notification.SubscribesType
1616
import `in`.koreatech.koin.domain.repository.NotificationRepository
1717
import javax.inject.Inject
1818
import kotlin.collections.map
19+
import kotlinx.coroutines.flow.Flow
20+
import kotlinx.coroutines.flow.map
1921
import retrofit2.HttpException
2022

2123
@Suppress("Detekt.TooManyFunctions")
@@ -122,6 +124,11 @@ class NotificationRepositoryImpl @Inject constructor(
122124
}
123125
}
124126

127+
override fun getNotificationsFlowFromLocal(): Flow<List<Notification>> {
128+
return notificationLocalDataSource.getNotificationsFlow()
129+
.map { list -> list.map { it.toNotification() } }
130+
}
131+
125132
override suspend fun deleteNotificationFromLocal(id: Int): Result<Unit> {
126133
return suspendRunCatching {
127134
notificationLocalDataSource.deleteNotification(id)

data/src/main/java/in/koreatech/koin/data/source/local/NotificationLocalDataSource.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import `in`.koreatech.koin.data.dao.NotificationDao
44
import `in`.koreatech.koin.data.entity.NotificationEntity
55
import java.time.LocalDateTime
66
import javax.inject.Inject
7+
import kotlinx.coroutines.flow.Flow
78

89
class NotificationLocalDataSource @Inject constructor(
910
private val notificationDao: NotificationDao
@@ -20,6 +21,10 @@ class NotificationLocalDataSource @Inject constructor(
2021
return notificationDao.getNotifications()
2122
}
2223

24+
fun getNotificationsFlow(): Flow<List<NotificationEntity>> {
25+
return notificationDao.getNotificationsFlow()
26+
}
27+
2328
suspend fun updateNotificationReadByUrl(url: String, isRead: Boolean): NotificationEntity {
2429
return notificationDao.updateReadByUrlAndReturn(url, isRead)
2530
}

domain/src/main/java/in/koreatech/koin/domain/repository/NotificationRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import `in`.koreatech.koin.domain.model.notification.Notification
44
import `in`.koreatech.koin.domain.model.notification.NotificationPermissionInfo
55
import `in`.koreatech.koin.domain.model.notification.SubscribesDetailType
66
import `in`.koreatech.koin.domain.model.notification.SubscribesType
7+
import kotlinx.coroutines.flow.Flow
78

89
@Suppress("Detekt.TooManyFunctions")
910
interface NotificationRepository {
@@ -29,6 +30,8 @@ interface NotificationRepository {
2930

3031
suspend fun getNotificationsFromLocal(): Result<List<Notification>>
3132

33+
fun getNotificationsFlowFromLocal(): Flow<List<Notification>>
34+
3235
suspend fun deleteNotificationFromLocal(id: Int): Result<Unit>
3336

3437
suspend fun deleteNotificationsFromLocal(ids: List<Int>): Result<Unit>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package `in`.koreatech.koin.domain.usecase.notification
2+
3+
import `in`.koreatech.koin.domain.repository.NotificationRepository
4+
import javax.inject.Inject
5+
6+
class DeleteNotificationsUseCase @Inject constructor(
7+
private val notificationRepository: NotificationRepository
8+
) {
9+
suspend operator fun invoke(ids: List<Int>): Result<Unit> {
10+
return notificationRepository.deleteNotificationsFromLocal(ids)
11+
}
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package `in`.koreatech.koin.domain.usecase.notification
2+
3+
import `in`.koreatech.koin.domain.model.notification.Notification
4+
import `in`.koreatech.koin.domain.repository.NotificationRepository
5+
import javax.inject.Inject
6+
import kotlinx.coroutines.flow.Flow
7+
8+
class GetNotificationsFlowUseCase @Inject constructor(
9+
private val notificationRepository: NotificationRepository
10+
) {
11+
operator fun invoke(): Flow<List<Notification>> {
12+
return notificationRepository.getNotificationsFlowFromLocal()
13+
}
14+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package `in`.koreatech.koin.domain.usecase.notification
2+
3+
import `in`.koreatech.koin.domain.model.notification.Notification
4+
import `in`.koreatech.koin.domain.repository.NotificationRepository
5+
import javax.inject.Inject
6+
import kotlinx.coroutines.async
7+
import kotlinx.coroutines.awaitAll
8+
import kotlinx.coroutines.coroutineScope
9+
10+
class UpdateNotificationsReadByIdUseCase @Inject constructor(
11+
private val notificationRepository: NotificationRepository
12+
) {
13+
suspend operator fun invoke(
14+
ids: List<Int>,
15+
isRead: Boolean
16+
): Result<List<Notification>> = coroutineScope {
17+
val results = ids.map { id ->
18+
async {
19+
notificationRepository.updateNotificationReadById(
20+
id = id,
21+
isRead = isRead
22+
)
23+
}
24+
}.awaitAll()
25+
26+
Result.success(results.mapNotNull { it.getOrNull() })
27+
}
28+
}

feature/home/src/main/java/in/koreatech/koin/feature/home/HomeScreen.kt

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import androidx.compose.foundation.layout.Box
77
import androidx.compose.foundation.layout.Column
88
import androidx.compose.foundation.layout.Row
99
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.WindowInsets
11+
import androidx.compose.foundation.layout.fillMaxSize
1012
import androidx.compose.foundation.layout.fillMaxWidth
1113
import androidx.compose.foundation.layout.height
1214
import androidx.compose.foundation.layout.padding
1315
import androidx.compose.foundation.layout.size
16+
import androidx.compose.foundation.layout.statusBars
1417
import androidx.compose.foundation.layout.width
1518
import androidx.compose.foundation.rememberScrollState
1619
import androidx.compose.foundation.shape.RoundedCornerShape
1720
import androidx.compose.foundation.verticalScroll
1821
import androidx.compose.material3.Icon
22+
import androidx.compose.material3.Scaffold
1923
import androidx.compose.material3.Text
2024
import androidx.compose.runtime.Composable
2125
import androidx.compose.runtime.getValue
@@ -57,26 +61,38 @@ import org.orbitmvi.orbit.compose.collectAsState
5761

5862
@Composable
5963
fun HomeScreen(
60-
viewModel: HomeViewModel = hiltViewModel()
64+
viewModel: HomeViewModel = hiltViewModel(),
65+
navigateToNotification: () -> Unit = {}
6166
) {
6267
val uiState by viewModel.collectAsState()
6368

64-
HomeScreen(
65-
modifier = Modifier.background(Color(0xFFF8F8FA)),
66-
uiState = uiState
67-
)
69+
Scaffold(
70+
modifier = Modifier.fillMaxSize(),
71+
contentWindowInsets = WindowInsets.statusBars
72+
) { paddingValues ->
73+
HomeScreen(
74+
modifier = Modifier
75+
.fillMaxSize()
76+
.background(Color(0xFFF8F8FA))
77+
.padding(paddingValues),
78+
uiState = uiState,
79+
navigateToNotification = navigateToNotification
80+
)
81+
}
6882
}
6983

7084
@Composable
7185
private fun HomeScreen(
7286
uiState: HomeState,
73-
modifier: Modifier = Modifier
87+
modifier: Modifier = Modifier,
88+
navigateToNotification: () -> Unit = {}
7489
) {
7590
Column(
7691
modifier = modifier.verticalScroll(rememberScrollState())
7792
) {
7893
HeaderSection(
79-
isNewNotificationReceived = uiState.isNewNotificationReceived
94+
isNewNotificationReceived = uiState.isNewNotificationReceived,
95+
onNotificationClick = navigateToNotification
8096
)
8197
InfoSection(
8298
username = uiState.username,
@@ -107,7 +123,8 @@ private fun HeaderSection(
107123
Row(
108124
modifier = modifier
109125
.fillMaxWidth()
110-
.padding(vertical = 8.dp, horizontal = 24.dp)
126+
.padding(vertical = 8.dp, horizontal = 24.dp),
127+
verticalAlignment = Alignment.CenterVertically
111128
) {
112129
Image(
113130
imageVector = ImageVector.vectorResource(R.drawable.ic_bcsd_symbol),

feature/notification/AGENTS.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Feature Notification Module - AGENTS.md
2+
3+
`feature/notification` is a Compose-first Orbit module for Notification list screen.
4+
5+
## Keep In Mind
6+
7+
- Use the existing Compose + Orbit patterns for screens and ViewModels.
8+
- Keep notification-specific flows inside this module.
9+
- Use `rememberNavigator()` and shared navigation helpers for hand-offs back to `koin` Activities.
10+
- Reuse shared onboarding, analytics, and design-system components before adding module-local alternatives.
11+
- Keep Notification-specific UI state and validation in the feature layer; ViewModels still call use cases rather than repositories directly.
12+
13+
## Read First
14+
15+
- Root `AGENTS.md`
16+
- `core/AGENTS.md`
17+
- `core/navigation/AGENTS.md`
18+
- `core/designsystem/AGENTS.md`
19+
- `core/onboarding/AGENTS.md`

0 commit comments

Comments
 (0)