Skip to content

Commit

Permalink
Fix/#93 iOS 백그라운드에서 fcm 수신 시 공지 보관함에 기록되지 않는 버그 (#95)
Browse files Browse the repository at this point in the history
* fix: iOS 백그라운드가 아닐 때도 알림을 클릭하면 DB에 저장되도록 수정

* feat: 공지 알림 저장 로직을 로컬에서 서버로 마이그레이션

* feat: 공지보관함 타이틀 하단에 `공지는 최대 한 달간 보관돼요!` 문구 추가

* fix: iOS DaoModule에서 NotificationDao 의존성 제거하지 않았던 문제 해결

* chore: iOS BGTaskSchedulerPermittedIdentifiers 추가

* chore: iOS 버전 정보 추가

* chore: iOS 권한 설명 추가

* refactor: NotificationDto 프로퍼티 notificationContent를 notificationMessage로 변경

* fix: 대학 공지와 학과 공지 중복 처리 버그 수정

* feat: NoticeEntity의 id, url을 기본키(복합키)로 사용하도록 변경

* refactor: 알림 읽기 API를 호출할 때 memberId를 함께 전달하도록 변경

* refactor: 공지 DTO에서 공지 등록 시간을 제외한 Date만 받도록 변경
  • Loading branch information
tmdgh1592 authored Oct 10, 2024
1 parent b402c3c commit 3e506b3
Show file tree
Hide file tree
Showing 20 changed files with 194 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,4 @@ actual val daoModule: Module = module {
single { getWhatcamDatabase(androidContext()) }
single { get<WhatcamDatabase>().noticeDao() }
single { get<WhatcamDatabase>().searchQueryDao() }
single { get<WhatcamDatabase>().notificationDao() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@

<!-- NotificationArchive -->
<string name="notification_archive_title">알림 보관함</string>
<string name="notification_archive_warning_message">공지는 최대 한 달간 보관돼요!</string>

<!-- Chat -->
<string name="chat_title">왓챗</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package core.data.mapper

import core.common.util.parse
import core.data.model.NotificationDto
import core.data.model.NotificationsDto
import core.model.Notice
import core.model.Notification
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
import kotlinx.datetime.format.byUnicodePattern

@OptIn(FormatStringsInDatetimeFormats::class)
private val notificationDateTimeFormatter = LocalDateTime.Format {
byUnicodePattern("yyyyMMdd HHmmss")
}

internal fun NotificationsDto.toNotifications(): PersistentList<Notification> = notifications
.map { it.toNotification() }
.toPersistentList()

private fun NotificationDto.toNotification(): Notification = Notification.NewNotice(
notificationId = notificationId,
content = notificationMessage,
isRead = isRead,
receivedDatetime = sendDateTime.parse(notificationDateTimeFormatter),
notice = Notice(
id = noticeId,
title = noticeTitle,
datetime = noticeDate.paddingTime().parse(notificationDateTimeFormatter),
url = noticeUrl,
),
)

private fun String.paddingTime(): String = "$this 000000"
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package core.data.model

import kotlinx.serialization.Serializable

@Serializable
internal data class NotificationsDto(
val notifications: List<NotificationDto>,
)

@Serializable
internal data class NotificationDto(
val notificationId: Long,
val notificationMessage: String,
val isRead: Boolean,
val sendDateTime: String,
val noticeId: Long,
val noticeTitle: String,
val noticeDate: String,
val noticeUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,57 @@ package core.data.repository
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import core.database.dao.NotificationDao
import core.database.entity.NotificationEntity
import core.database.mapper.toNotification
import core.database.mapper.toNotificationEntity
import core.data.common.safeGet
import core.data.common.safePatch
import core.data.mapper.toNotifications
import core.data.model.NotificationsDto
import core.datastore.key.NotificationKey
import core.domain.repository.NotificationRepository
import core.model.Notification
import core.model.Response
import io.ktor.client.HttpClient
import io.ktor.client.request.parameter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map

class DefaultNotificationRepository(
private val dataStore: DataStore<Preferences>,
private val notificationDao: NotificationDao,
private val httpClient: HttpClient,
) : NotificationRepository {

override fun flowNotifications(): Flow<PersistentList<Notification>> = notificationDao.getAll()
.map { notifications -> notifications.map(NotificationEntity::toNotification) }
.map { notifications -> notifications.toPersistentList() }
override fun flowNotifications(
userId: Long,
): Flow<Response<PersistentList<Notification>>> = flow {
val requestUrl = "/api/v1/notifications"
val response = httpClient.safeGet<NotificationsDto>(
urlString = requestUrl,
block = { parameter("memberId", userId) },
)

override suspend fun addNotification(notification: Notification) {
notificationDao.insert(notification.toNotificationEntity())
emit(response.map { it.toNotifications() })
}

override fun flowHasNewNotification(): Flow<Boolean> = dataStore.data
.map { pref -> pref[NotificationKey.hasNewNotification] ?: false }

override suspend fun updateHasNewNotification(hasNewNotification: Boolean) {
override suspend fun updateHasNewNotification(
hasNewNotification: Boolean,
) {
dataStore.edit { pref ->
pref[NotificationKey.hasNewNotification] = hasNewNotification
}
}

override suspend fun readNotification(notificationId: Long) {
notificationDao.updateIsRead(id = notificationId)
override suspend fun readNotification(
userId: Long,
notificationId: Long,
) {
val requestUrl = "/api/v1/notifications/$notificationId/read"
httpClient.safePatch<Unit>(
urlString = requestUrl,
block = { parameter("memberId", userId) },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import androidx.room.TypeConverters
import core.database.WhatcamDatabase.Companion.DATABASE_VERSION
import core.database.converter.LocalDateTimeConverter
import core.database.dao.NoticeDao
import core.database.dao.NotificationDao
import core.database.dao.SearchQueryDao
import core.database.entity.NoticeEntity
import core.database.entity.NotificationEntity
import core.database.entity.SearchQueryEntity

@Database(
entities = [
NoticeEntity::class,
SearchQueryEntity::class,
NotificationEntity::class,
],
version = DATABASE_VERSION,
)
Expand All @@ -25,15 +22,14 @@ abstract class WhatcamDatabase : RoomDatabase(), DB {

abstract fun noticeDao(): NoticeDao
abstract fun searchQueryDao(): SearchQueryDao
abstract fun notificationDao(): NotificationDao

override fun clearAllTables() {
super.clearAllTables()
}

companion object {
internal const val DATABASE_NAME = "whatcam.db"
internal const val DATABASE_VERSION = 6
internal const val DATABASE_VERSION = 1
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package core.database.entity

import androidx.room.Entity
import androidx.room.PrimaryKey
import core.database.entity.NoticeEntity.Companion.TABLE_NAME
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime

@Entity(tableName = TABLE_NAME)
@Entity(
tableName = TABLE_NAME,
primaryKeys = ["id", "url"],
)
data class NoticeEntity(
@PrimaryKey val id: Long,
val id: Long,
val title: String,
val datetime: LocalDateTime,
val url: String,
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package core.domain.repository

import core.model.Notification
import core.model.Response
import kotlinx.collections.immutable.PersistentList
import kotlinx.coroutines.flow.Flow

interface NotificationRepository {
fun flowNotifications(): Flow<PersistentList<Notification>>

suspend fun addNotification(notification: Notification)
fun flowNotifications(
userId: Long,
): Flow<Response<PersistentList<Notification>>>

fun flowHasNewNotification(): Flow<Boolean>

suspend fun updateHasNewNotification(hasNewNotification: Boolean)

suspend fun readNotification(notificationId: Long)
suspend fun readNotification(
userId: Long,
notificationId: Long,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ data class GetAllNoticesUseCase(
universityNotices is Response.Failure -> universityNotices
departmentNotices is Response.Failure -> departmentNotices
universityNotices is Response.Success && departmentNotices is Response.Success -> {
val combinedNotices =
(universityNotices.body + departmentNotices.body).distinctBy { notice -> notice.id }
val combinedNotices = (universityNotices.body + departmentNotices.body).distinct()
Response.Success(combinedNotices)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ import core.domain.repository.NotificationRepository
import core.domain.repository.TokenRepository
import core.domain.repository.UserRepository
import core.model.Notice
import core.model.Notification
import feature.app.navigation.WhatcamNavigator
import feature.notice.navigation.NoticeDetailDeepLink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

Expand Down Expand Up @@ -49,6 +45,8 @@ object NotifierInitializer : KoinComponent {
val pushTitle = data[KEY_PUSH_TITLE] as String
val pushMessage = data[KEY_PUSH_MESSAGE] as String

scope.launch { notificationRepository.updateHasNewNotification(hasNewNotification = true) }

if (data.isFromBackground()) {
val noticeDetailDeepLink = NoticeDetailDeepLink(notice = data.toNotice())
WhatcamNavigator.handleDeeplink(deepLink = noticeDetailDeepLink)
Expand All @@ -59,9 +57,6 @@ object NotifierInitializer : KoinComponent {
val user = userRepository.flowUser().firstOrNull() ?: return@launch
if (user.isPushNotificationAllowed.not()) return@launch

notificationRepository.updateHasNewNotification(hasNewNotification = true)
notificationRepository.addNotification(data.toNotification())

NotifierManager.getLocalNotifier().notify(
id = pushTitle.hashCode(),
title = pushTitle,
Expand Down Expand Up @@ -99,16 +94,6 @@ object NotifierInitializer : KoinComponent {
)
}

private fun PayloadData.toNotification(): Notification {
return Notification.NewNotice(
notificationId = 0L,
content = this[KEY_PUSH_MESSAGE] as String,
isRead = false,
receivedDatetime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
notice = this.toNotice()
)
}

private fun PayloadData.toNotice(): Notice {
val noticeId = this[KEY_NOTICE_ID] as String
val noticeTitle = this[KEY_NOTICE_TITLE] as String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import core.common.extensions.collectAsStateMultiplatform
import core.common.extensions.collectUiEvent
import core.common.util.logScreenEvent
import core.designsystem.components.LoadingScreen
import core.di.koinViewModel
Expand All @@ -23,6 +24,7 @@ fun NotificationArchiveScreen(
onClickNewNoticeNotification: (Notification.NewNotice) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateMultiplatform()
viewModel.commonUiEvent.collectUiEvent()

logScreenEvent(screenName = "NotificationArchiveScreen")

Expand Down
Loading

0 comments on commit 3e506b3

Please sign in to comment.