Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import com.afternote.afternote_fe.screen.receiver.model.MindRecordSummary
import com.afternote.afternote_fe.screen.receiver.model.ReceiverDownloadState
import com.afternote.afternote_fe.screen.receiver.model.ReceiverHomeUiState
import com.afternote.afternote_fe.screen.receiver.model.SenderMessage
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem
import com.afternote.feature.afternote.domain.model.receiver.AfterNotesListResult
import com.afternote.feature.afternote.domain.repository.receiver.ReceiverRepository
import com.afternote.feature.afternote.presentation.shared.util.getAfternoteDisplayRes
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -52,54 +53,56 @@ class ReceiverHomeViewModel
private fun loadHome() {
_uiState.value = ReceiverHomeUiState.Loading
viewModelScope.launch {
val afternotes = async { receiverRepository.getReceivedAfterNotes() }
val mindRecords = async { receiverRepository.loadMindRecordsCount() }
val timeLetters = async { receiverRepository.loadTimeLettersCount() }
val message = async { receiverRepository.loadSenderMessage() }
val afternotesRes = afternotes.await()
val mindRecordsRes = mindRecords.await()
val timeLettersRes = timeLetters.await()
val messageRes = message.await()
coroutineScope {
val afternotes = async { receiverRepository.getReceivedAfterNotes() }
val mindRecords = async { receiverRepository.loadMindRecordsCount() }
val timeLetters = async { receiverRepository.loadTimeLettersCount() }
val message = async { receiverRepository.loadSenderMessage() }
val afternotesRes = afternotes.await()
val mindRecordsRes = mindRecords.await()
val timeLettersRes = timeLetters.await()
val messageRes = message.await()

// 모든 호출이 실패한 경우만 Error. 일부 실패는 fallback 으로 진행.
if (afternotesRes.isFailure &&
mindRecordsRes.isFailure &&
timeLettersRes.isFailure &&
messageRes.isFailure
) {
_uiState.value =
ReceiverHomeUiState.Error(
afternotesRes.exceptionOrNull() ?: RuntimeException("All home requests failed"),
)
return@coroutineScope
}

val afternotesResult =
afternotesRes.getOrNull() ?: AfterNotesListResult(items = emptyList(), totalCount = 0)
val mindRecordsCount = mindRecordsRes.getOrNull()?.totalCount ?: 0
val timeLettersCount = timeLettersRes.getOrNull()?.totalCount ?: 0
val senderMessageInfo = messageRes.getOrNull()
// senderName / senderMessage 둘 다 blank 가드 — sender 가 이름·메시지 미입력 케이스 대응.
// ".orEmpty()" 만으로는 공백(" ") 통과해 "故 님이 남기신 기록" / "님의 한 마디" UI 깨짐.
val senderName = senderMessageInfo?.senderName?.takeIf { it.isNotBlank() }.orEmpty()
val senderMessageBody = senderMessageInfo?.message?.takeIf { it.isNotBlank() }

// 모든 호출이 실패한 경우만 Error. 일부 실패는 fallback 으로 진행.
if (afternotesRes.isFailure &&
mindRecordsRes.isFailure &&
timeLettersRes.isFailure &&
messageRes.isFailure
) {
_uiState.value =
ReceiverHomeUiState.Error(
afternotesRes.exceptionOrNull() ?: RuntimeException("All home requests failed"),
ReceiverHomeUiState.Success(
senderName = senderName,
// TODO: 백엔드 응답에 date 필드 추가되면 채울 것. 현재 receiver-auth/message 응답은 senderName/message 만.
senderMessage = senderMessageBody?.let { SenderMessage(date = "", body = it) },
mindRecord =
MindRecordSummary(
totalCount = mindRecordsCount,
dailyQuestionCount = 0,
diaryCount = 0,
deepThoughtCount = 0,
),
timeLetterTotalCount = timeLettersCount,
afternoteTotalCount = afternotesResult.totalCount,
afternoteIcons = afternotesResult.items.toAfternoteIcons(),
)
return@launch
}

val afternotesResult =
afternotesRes.getOrNull() ?: AfterNotesListResult(items = emptyList(), totalCount = 0)
val mindRecordsCount = mindRecordsRes.getOrNull()?.totalCount ?: 0
val timeLettersCount = timeLettersRes.getOrNull()?.totalCount ?: 0
val senderMessageInfo = messageRes.getOrNull()
// senderName / senderMessage 둘 다 blank 가드 — sender 가 이름·메시지 미입력 케이스 대응.
// ".orEmpty()" 만으로는 공백(" ") 통과해 "故 님이 남기신 기록" / "님의 한 마디" UI 깨짐.
val senderName = senderMessageInfo?.senderName?.takeIf { it.isNotBlank() }.orEmpty()
val senderMessageBody = senderMessageInfo?.message?.takeIf { it.isNotBlank() }

_uiState.value =
ReceiverHomeUiState.Success(
senderName = senderName,
// TODO: 백엔드 응답에 date 필드 추가되면 채울 것. 현재 receiver-auth/message 응답은 senderName/message 만.
senderMessage = senderMessageBody?.let { SenderMessage(date = "", body = it) },
mindRecord =
MindRecordSummary(
totalCount = mindRecordsCount,
dailyQuestionCount = 0,
diaryCount = 0,
deepThoughtCount = 0,
),
timeLetterTotalCount = timeLettersCount,
afternoteTotalCount = afternotesResult.totalCount,
afternoteIcons = afternotesResult.items.toAfternoteIcons(),
)
}
}

Expand Down Expand Up @@ -134,7 +137,7 @@ class ReceiverHomeViewModel

private const val MAX_AFTERNOTE_ICONS = 4

private fun List<AfterNoteListItemDto>.toAfternoteIcons(): List<AfternoteSourceIcon> =
private fun List<AfterNoteListItem>.toAfternoteIcons(): List<AfternoteSourceIcon> =
asSequence()
.mapNotNull { it.sourceType?.takeIf(String::isNotBlank) }
.distinct()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package com.afternote.feature.afternote.data.mapper

import com.afternote.feature.afternote.data.dto.ReceivedAfternoteResponse
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem

/**
* 서버 카테고리 enum(SOCIAL/GALLERY/PLAYLIST)을 프레젠테이션이 그대로 typeKey로 쓸 수 있는
* 카테고리 키(SOCIAL_NETWORK/GALLERY_AND_FILES/MEMORIAL)로 정규화한다.
*/
fun ReceivedAfternoteResponse.toDomain(): AfterNoteListItemDto =
AfterNoteListItemDto(
fun ReceivedAfternoteResponse.toDomain(): AfterNoteListItem =
AfterNoteListItem(
id = id,
title = title,
sourceType = category?.let { serverCategoryToTypeKey(it) },
lastUpdatedAt = createdAt?.let { formatDateFromServer(it) },
)

fun List<ReceivedAfternoteResponse>.toReceiverDomainList(): List<AfterNoteListItemDto> = map { it.toDomain() }
fun List<ReceivedAfternoteResponse>.toReceiverDomainList(): List<AfterNoteListItem> = map { it.toDomain() }

private fun serverCategoryToTypeKey(serverCategory: String): String =
when (serverCategory.uppercase()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.paging.PagingState
import com.afternote.core.network.model.requireData
import com.afternote.feature.afternote.data.mapper.toReceiverDomainList
import com.afternote.feature.afternote.data.service.ReceiverAfternoteApiService
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem
import kotlinx.coroutines.CancellationException

/**
Expand All @@ -17,10 +17,10 @@ import kotlinx.coroutines.CancellationException
*/
internal class ReceiverAfternotePagingSource(
private val api: ReceiverAfternoteApiService,
) : PagingSource<Int, AfterNoteListItemDto>() {
override fun getRefreshKey(state: PagingState<Int, AfterNoteListItemDto>): Int? = null
) : PagingSource<Int, AfterNoteListItem>() {
override fun getRefreshKey(state: PagingState<Int, AfterNoteListItem>): Int? = null

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AfterNoteListItemDto> =
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AfterNoteListItem> =
try {
val response = api.getReceiverAfternotes().requireData()
LoadResult.Page(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.afternote.feature.afternote.data.local.ReceiverAuthCodeDataSource
import com.afternote.feature.afternote.data.mapper.response.toDomain
import com.afternote.feature.afternote.data.paging.ReceiverAfternotePagingSource
import com.afternote.feature.afternote.data.service.ReceiverAfternoteApiService
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem
import com.afternote.feature.afternote.domain.model.receiver.AfterNotesListResult
import com.afternote.feature.afternote.domain.model.receiver.LoadCountResult
import com.afternote.feature.afternote.domain.model.receiver.ReceivedAfternoteDetail
Expand Down Expand Up @@ -47,7 +47,7 @@ class ReceiverRepositoryImpl
authCodeDataSource.clearCode()
}

override fun getPagedReceivedAfternotes(): Flow<PagingData<AfterNoteListItemDto>> =
override fun getPagedReceivedAfternotes(): Flow<PagingData<AfterNoteListItem>> =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
pagingSourceFactory = { ReceiverAfternotePagingSource(api) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.junit.Test

/**
* [ReceivedAfternoteResponse.toDomain] / [toReceiverDomainList] 회귀 가드.
* 수신자 목록 DTO→[com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto] 매핑.
* 수신자 목록 DTO→[com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem] 매핑.
* 서버 카테고리(SOCIAL/GALLERY/PLAYLIST/MUSIC)를 presentation typeKey로 정규화하는 규칙과
* null 가드(category·createdAt)를 검증한다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package com.afternote.feature.afternote.domain.model.receiver
import com.afternote.feature.afternote.domain.AfternoteServiceType

data class AfterNotesListResult(
val items: List<AfterNoteListItemDto>,
val items: List<AfterNoteListItem>,
val totalCount: Int,
)

data class AfterNoteListItemDto(
data class AfterNoteListItem(
val id: Long,
val title: String?,
val sourceType: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.afternote.feature.afternote.domain.repository.receiver

import androidx.paging.PagingData
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem
import com.afternote.feature.afternote.domain.model.receiver.AfterNotesListResult
import com.afternote.feature.afternote.domain.model.receiver.LoadCountResult
import com.afternote.feature.afternote.domain.model.receiver.ReceivedAfternoteDetail
Expand Down Expand Up @@ -32,7 +32,7 @@ interface ReceiverRepository {
* 수신 애프터노트 스트림. 서버는 페이지네이션 미지원이므로 단일 페이지로 받지만,
* Paging 3 API(LoadState/refresh/cachedIn) 통일과 추후 페이지네이션 도입을 위해 PagingData로 노출한다.
*/
fun getPagedReceivedAfternotes(): Flow<PagingData<AfterNoteListItemDto>>
fun getPagedReceivedAfternotes(): Flow<PagingData<AfterNoteListItem>>

suspend fun getReceivedAfterNotes(): Result<AfterNotesListResult>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ class AfternoteHostViewModel
val isPasskeyRegistered: StateFlow<Boolean?> =
userProfileRepository
.isPasskeyRegisteredFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)

private val _playlistSongs = MutableStateFlow<List<Song>>(emptyList())
val playlistSongs: StateFlow<List<Song>> = _playlistSongs.asStateFlow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import androidx.paging.cachedIn
import androidx.paging.filter
import androidx.paging.map
import com.afternote.feature.afternote.domain.AfternoteServiceType
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItemDto
import com.afternote.feature.afternote.domain.model.receiver.AfterNoteListItem
import com.afternote.feature.afternote.domain.repository.receiver.ReceiverRepository
import com.afternote.feature.afternote.presentation.shared.AfternoteCategory
import com.afternote.feature.afternote.presentation.shared.body.infinite.content.list.item.ListItemUiModel
Expand Down Expand Up @@ -58,7 +58,7 @@ class ReceiverAfternoteHomeViewModel
}
}

private fun AfterNoteListItemDto.toUiModel(): ListItemUiModel {
private fun AfterNoteListItem.toUiModel(): ListItemUiModel {
val typeKey = sourceType.orEmpty()
val displayRes = getAfternoteDisplayRes(typeKey)
val serviceName = getServiceNameForTypeKey(typeKey)
Expand Down
Loading