From 3dda41d37150fd57215c7cfe5189920b598c43a7 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Tue, 26 May 2026 13:04:08 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix:=20=EC=A7=80=EA=B8=88=20=EB=9C=A8?= =?UTF-8?q?=EB=8A=94=20=EA=B8=80=20=EB=82=B4=EC=9A=A9=20null=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 작품 연결이 되어있지 않는 글을 뜨지 않게 하기 위해 null 허용으로 전환 --- app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt | 2 +- .../websoso/data/remote/response/PopularFeedsResponseDto.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt index 509272e3f..549ffe774 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt @@ -111,7 +111,7 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity = popularFeeds = popularFeeds.map { feed -> PopularFeedsEntity.PopularFeedEntity( feedId = feed.feedId, - feesContent = feed.feedContent, + feesContent = feed.feedContent.orEmpty(), likeCount = feed.likeCount, commentCount = feed.commentCount, isSpoiler = feed.isSpoiler, diff --git a/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt index 3a7bdb72e..3e91bf8cf 100644 --- a/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt +++ b/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt @@ -13,7 +13,7 @@ data class PopularFeedsResponseDto( @SerialName("feedId") val feedId: Long, @SerialName("feedContent") - val feedContent: String, + val feedContent: String? = null, @SerialName("likeCount") val likeCount: Int, @SerialName("commentCount") From 91c91fa320c31a2346649e921267c0e4bb80b52a Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Tue, 26 May 2026 13:12:39 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=A7=80=EA=B8=88=20=EB=9C=A8?= =?UTF-8?q?=EB=8A=94=20=EA=B8=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=99=88=20=ED=99=94=EB=A9=B4=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/into/websoso/data/mapper/FeedMapper.kt | 4 ++++ .../into/websoso/data/model/PopularFeedsEntity.kt | 4 ++++ .../data/remote/response/PopularFeedsResponseDto.kt | 12 ++++++++++++ app/src/main/res/layout/fragment_home.xml | 2 +- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt index 549ffe774..022e9843a 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt @@ -115,6 +115,10 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity = likeCount = feed.likeCount, commentCount = feed.commentCount, isSpoiler = feed.isSpoiler, + isPublic = feed.isPublic ?: true, + novelTitle = feed.novelTitle ?: feed.title.orEmpty(), + novelImage = feed.novelImage ?: feed.novelThumbnailImage.orEmpty(), + novelGenreImage = feed.novelGenreImage.orEmpty(), ) }, ) diff --git a/app/src/main/java/com/into/websoso/data/model/PopularFeedsEntity.kt b/app/src/main/java/com/into/websoso/data/model/PopularFeedsEntity.kt index 7399e789f..e6be43c24 100644 --- a/app/src/main/java/com/into/websoso/data/model/PopularFeedsEntity.kt +++ b/app/src/main/java/com/into/websoso/data/model/PopularFeedsEntity.kt @@ -9,5 +9,9 @@ data class PopularFeedsEntity( val likeCount: Int, val commentCount: Int, val isSpoiler: Boolean, + val isPublic: Boolean, + val novelTitle: String, + val novelImage: String, + val novelGenreImage: String, ) } diff --git a/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt b/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt index 3e91bf8cf..021300230 100644 --- a/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt +++ b/app/src/main/java/com/into/websoso/data/remote/response/PopularFeedsResponseDto.kt @@ -20,5 +20,17 @@ data class PopularFeedsResponseDto( val commentCount: Int, @SerialName("isSpoiler") val isSpoiler: Boolean, + @SerialName("isPublic") + val isPublic: Boolean? = null, + @SerialName("title") + val title: String? = null, + @SerialName("novelTitle") + val novelTitle: String? = null, + @SerialName("novelImage") + val novelImage: String? = null, + @SerialName("novelThumbnailImage") + val novelThumbnailImage: String? = null, + @SerialName("novelGenreImage") + val novelGenreImage: String? = null, ) } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 8ffff268d..1fc981807 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -191,7 +191,7 @@ Date: Tue, 26 May 2026 13:33:56 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=ED=94=BC=EB=93=9C=20UI=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지금 뜨는 글 피드 슬롯 구성을 3개에서 2개로 변경 - 피드 상세 정보(제목, 썸네일, 장르) 표시를 위한 UI 레이아웃 대폭 수정 - `HomeViewModel`에서 피드 상세 정보를 함께 조회하도록 데이터 가공 로직 추가 - `PopularFeedsViewHolder`에서 썸네일(Coil 사용) 및 제목 생략 처리 등 바인딩 로직 고도화 - `FeedRepository`에 단일 피드 조회(`fetchFeed`) 메서드 추가 - `PopularFeedsAdapter`의 리스트 비교(`DiffUtil`) 로직 수정 --- .../websoso/data/repository/FeedRepository.kt | 3 + .../websoso/ui/main/home/HomeViewModel.kt | 38 +++++- .../main/home/adpater/PopularFeedsAdapter.kt | 2 +- .../home/adpater/PopularFeedsViewHolder.kt | 71 ++++++++-- app/src/main/res/layout/item_popular_feed.xml | 33 +---- .../res/layout/item_popular_feed_slot.xml | 123 +++++++++--------- 6 files changed, 167 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt index fefadad96..b9a31bb2c 100644 --- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt @@ -1,6 +1,7 @@ package com.into.websoso.data.repository import com.into.websoso.data.mapper.toData +import com.into.websoso.data.model.FeedDetailEntity import com.into.websoso.data.model.FeedEntity import com.into.websoso.data.model.FeedsEntity import com.into.websoso.data.model.PopularFeedsEntity @@ -35,6 +36,8 @@ class FeedRepository suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData() + suspend fun fetchFeed(feedId: Long): FeedDetailEntity = feedApi.getFeed(feedId).toData() + suspend fun saveRemovedFeed(feedId: Long) { runCatching { feedApi.deleteFeed(feedId) diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index c17f6ffe0..f8e195a09 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -18,6 +18,7 @@ import com.into.websoso.ui.main.home.model.HomeUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -96,7 +97,7 @@ class HomeViewModel loading = false, error = false, popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.popularFeeds.chunked(3), + popularFeeds = popularFeeds.toHomePopularFeeds(), recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, ) }.onFailure { @@ -142,7 +143,7 @@ class HomeViewModel _uiState.value = uiState.value?.copy( loading = false, popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.popularFeeds.chunked(3), + popularFeeds = popularFeeds.toHomePopularFeeds(), ) }.onFailure { _uiState.value = uiState.value?.copy( @@ -159,7 +160,7 @@ class HomeViewModel feedRepository.fetchPopularFeeds() }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( - popularFeeds = popularFeeds.popularFeeds.chunked(3), + popularFeeds = popularFeeds.toHomePopularFeeds(), ) }.onFailure { _uiState.value = uiState.value?.copy(error = true) @@ -251,4 +252,35 @@ class HomeViewModel } } } + + private suspend fun PopularFeedsEntity.toHomePopularFeeds(): List> = + coroutineScope { + popularFeeds + .map { feed -> + async { + runCatching { + val feedDetail = feedRepository.fetchFeed(feed.feedId) + val novel = feedDetail.novel ?: return@runCatching null + + feed.copy( + feesContent = feedDetail.content, + isSpoiler = feedDetail.isSpoiler, + isPublic = feedDetail.isPublic, + novelTitle = novel.title, + novelImage = feed.novelImage.ifBlank { novel.thumbnail }, + ) + }.getOrNull() + } + }.awaitAll() + .filterNotNull() + .filter { feed -> + feed.isPublic && + feed.novelTitle.isNotBlank() && + feed.novelImage.isNotBlank() + }.chunked(POPULAR_FEED_PAGE_SIZE) + } + + companion object { + private const val POPULAR_FEED_PAGE_SIZE = 2 + } } diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsAdapter.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsAdapter.kt index 73800c8f2..1ccad23d1 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsAdapter.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsAdapter.kt @@ -32,7 +32,7 @@ class PopularFeedsAdapter( override fun areItemsTheSame( oldItem: List, newItem: List, - ): Boolean = oldItem.firstOrNull()?.feedId == newItem.firstOrNull()?.feedId + ): Boolean = oldItem.map { it.feedId } == newItem.map { it.feedId } @SuppressLint("DiffUtilEquals") override fun areContentsTheSame( diff --git a/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsViewHolder.kt b/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsViewHolder.kt index d4cd81b3d..31575539a 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsViewHolder.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/adpater/PopularFeedsViewHolder.kt @@ -1,9 +1,17 @@ package com.into.websoso.ui.main.home.adpater +import android.util.Patterns import android.view.View import androidx.recyclerview.widget.RecyclerView +import coil.decode.SvgDecoder +import coil.load +import coil.transform.RoundedCornersTransformation +import com.into.websoso.core.common.util.getS3ImageUrl +import com.into.websoso.core.common.util.toFloatPxFromDp +import com.into.websoso.core.resource.R.drawable.img_loading_thumbnail import com.into.websoso.data.model.PopularFeedsEntity.PopularFeedEntity import com.into.websoso.databinding.ItemPopularFeedBinding +import com.into.websoso.databinding.ItemPopularFeedSlotBinding class PopularFeedsViewHolder( private val binding: ItemPopularFeedBinding, @@ -13,24 +21,63 @@ class PopularFeedsViewHolder( listOf( binding.itemPopularFeesSlot1, binding.itemPopularFeesSlot2, - binding.itemPopularFeesSlot3, ) } fun bind(feedItems: List) { + binding.viewPopularFeedDivider1.visibility = + if (feedItems.size > 1) View.VISIBLE else View.GONE + slots.forEachIndexed { index, slotBinding -> - slotBinding.apply { - feedItems.getOrNull(index)?.let { feed -> - tvPopularFeedContent.text = feed.feesContent - tvPopularFeedLikeCount.text = feed.likeCount.toString() - tvPopularFeedCommentCount.text = feed.commentCount.toString() - tvPopularFeedContent.visibility = - if (feed.isSpoiler) View.INVISIBLE else View.VISIBLE - tvPopularFeedContentSpoiler.visibility = - if (feed.isSpoiler) View.VISIBLE else View.GONE - root.setOnClickListener { onFeedClick(feed.feedId) } - } + val feed = feedItems.getOrNull(index) + if (feed == null) { + slotBinding.root.visibility = View.INVISIBLE + slotBinding.root.setOnClickListener(null) + return@forEachIndexed + } + + slotBinding.root.visibility = View.VISIBLE + slotBinding.bind(feed) + } + } + + private fun ItemPopularFeedSlotBinding.bind(feed: PopularFeedEntity) { + tvPopularFeedTitle.text = feed.novelTitle.ellipsizeByLength() + tvPopularFeedContent.text = if (feed.isSpoiler) "" else feed.feesContent + tvPopularFeedContent.visibility = if (feed.isSpoiler) View.GONE else View.VISIBLE + tvPopularFeedContentSpoiler.visibility = if (feed.isSpoiler) View.VISIBLE else View.GONE + ivPopularFeedThumbnail.load(feed.novelImage.toImageUrl()) { + crossfade(true) + transformations(RoundedCornersTransformation(8f.toFloatPxFromDp())) + error(img_loading_thumbnail) + } + val isGenreVisible = feed.novelGenreImage.isNotBlank() + ivPopularFeedGenreFrame.visibility = if (isGenreVisible) View.VISIBLE else View.GONE + ivPopularFeedGenre.visibility = if (isGenreVisible) View.VISIBLE else View.GONE + if (isGenreVisible) { + ivPopularFeedGenre.load(feed.novelGenreImage.toImageUrl()) { + crossfade(true) + decoderFactory(SvgDecoder.Factory()) } } + root.setOnClickListener { onFeedClick(feed.feedId) } + } + + private fun String.toImageUrl(): String = + when { + isBlank() -> "" + Patterns.WEB_URL.matcher(this).matches() -> this + else -> itemView.getS3ImageUrl(this) + } + + private fun String.ellipsizeByLength(): String = + if (length > MAX_TITLE_LENGTH) { + take(MAX_TITLE_LENGTH) + "..." + } else { + this + } + + companion object { + private const val MAX_TITLE_LENGTH = 16 } } diff --git a/app/src/main/res/layout/item_popular_feed.xml b/app/src/main/res/layout/item_popular_feed.xml index 9d5d58f52..c2501cd86 100644 --- a/app/src/main/res/layout/item_popular_feed.xml +++ b/app/src/main/res/layout/item_popular_feed.xml @@ -10,47 +10,28 @@ android:id="@+id/item_popular_fees_slot1" layout="@layout/item_popular_feed_slot" android:layout_width="0dp" - android:layout_height="wrap_content" - app:layout_constraintBottom_toTopOf="@id/view_popular_feed_divider_1" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" /> - - - - + app:layout_constraintTop_toBottomOf="@id/item_popular_fees_slot1" /> + app:layout_constraintTop_toBottomOf="@id/item_popular_fees_slot1" /> diff --git a/app/src/main/res/layout/item_popular_feed_slot.xml b/app/src/main/res/layout/item_popular_feed_slot.xml index 4ab801b4b..8edfdd036 100644 --- a/app/src/main/res/layout/item_popular_feed_slot.xml +++ b/app/src/main/res/layout/item_popular_feed_slot.xml @@ -9,85 +9,86 @@ + android:layout_height="match_parent" + android:paddingHorizontal="20dp" + android:paddingVertical="16dp"> + app:layout_constraintHorizontal_bias="0" + tools:text="피폐 역하렘 남주들의 막내 처제가..." /> + + + app:layout_constraintEnd_toEndOf="@id/tv_popular_feed_title" + app:layout_constraintStart_toStartOf="@id/tv_popular_feed_title" + app:layout_constraintTop_toBottomOf="@id/tv_popular_feed_title" + tools:visibility="visible" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/tv_popular_feed_title" + app:layout_constraintTop_toTopOf="parent"> - + - + - + + From a43ef50eefbde027867f936b9c024591b0c7fc9a Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Wed, 27 May 2026 11:25:21 +0900 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=20=ED=94=BC=EB=93=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20Repository=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `HomeViewModel`에 있던 인기 피드 가공 로직(`toHomePopularFeeds`)을 `FeedRepository`의 `fetchHomePopularFeeds` 메서드로 이관 - `FeedRepository`에서 인기 피드 목록 조회 시 각 피드의 상세 정보를 병렬로 호출하여 데이터(내용, 스포일러 여부, 작품 정보 등)를 보충하도록 수정 - 공개 상태이며 작품 제목과 이미지가 유효한 피드만 필터링하고 페이지 사이즈(2개)에 맞춰 청킹(Chunk) 처리하는 로직 추가 - `HomeViewModel`의 데이터 로딩 로직을 `runCatching`과 `async/await`을 활용해 예외 처리 및 가독성 개선 - 공통 에러 핸들링을 위한 `handleFailureState` 메서드 추출 및 적용 --- .../websoso/data/repository/FeedRepository.kt | 36 ++++- .../websoso/ui/main/home/HomeViewModel.kt | 146 +++++++----------- 2 files changed, 94 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt index b9a31bb2c..6b292a368 100644 --- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt @@ -1,11 +1,13 @@ package com.into.websoso.data.repository import com.into.websoso.data.mapper.toData -import com.into.websoso.data.model.FeedDetailEntity import com.into.websoso.data.model.FeedEntity import com.into.websoso.data.model.FeedsEntity import com.into.websoso.data.model.PopularFeedsEntity import com.into.websoso.data.remote.api.FeedApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import javax.inject.Inject import javax.inject.Singleton @@ -36,7 +38,33 @@ class FeedRepository suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData() - suspend fun fetchFeed(feedId: Long): FeedDetailEntity = feedApi.getFeed(feedId).toData() + suspend fun fetchHomePopularFeeds(): List> = + coroutineScope { + fetchPopularFeeds() + .popularFeeds + .map { feed -> + async { + runCatching { + val feedDetail = feedApi.getFeed(feed.feedId).toData() + val novel = feedDetail.novel ?: return@runCatching null + + feed.copy( + feesContent = feedDetail.content, + isSpoiler = feedDetail.isSpoiler, + isPublic = feedDetail.isPublic, + novelTitle = novel.title, + novelImage = feed.novelImage.ifBlank { novel.thumbnail }, + ) + }.getOrNull() + } + }.awaitAll() + .filterNotNull() + .filter { feed -> + feed.isPublic && + feed.novelTitle.isNotBlank() && + feed.novelImage.isNotBlank() + }.chunked(HOME_POPULAR_FEED_PAGE_SIZE) + } suspend fun saveRemovedFeed(feedId: Long) { runCatching { @@ -63,4 +91,8 @@ class FeedRepository false -> feedApi.postLikes(selectedFeedId) } } + + companion object { + private const val HOME_POPULAR_FEED_PAGE_SIZE = 2 + } } diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index f8e195a09..59d82cf42 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.into.websoso.data.model.PopularFeedsEntity import com.into.websoso.data.model.PopularNovelsEntity import com.into.websoso.data.model.RecommendedNovelsByUserTasteEntity import com.into.websoso.data.model.TermsAgreementEntity @@ -70,42 +69,38 @@ class HomeViewModel } } - private fun fetchUserHomeData() { - viewModelScope.launch { - runCatching { - val results = listOf( - async { runCatching { novelRepository.fetchPopularNovels() } }, - async { runCatching { feedRepository.fetchPopularFeeds() } }, - async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } }, - ).awaitAll() - - val failures = results.filter { it.isFailure } - if (failures.isNotEmpty()) { - throw failures.first().exceptionOrNull() - ?: IllegalStateException("Unknown error") - } + private suspend fun fetchUserHomeData() { + coroutineScope { + val popularNovelsDeferred = + async { runCatching { novelRepository.fetchPopularNovels() } } + val popularFeedsDeferred = + async { runCatching { feedRepository.fetchHomePopularFeeds() } } + val recommendedNovelsDeferred = + async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } } - val popularNovels = results[0].getOrNull() as? PopularNovelsEntity - ?: PopularNovelsEntity(emptyList()) - val popularFeeds = results[1].getOrNull() as? PopularFeedsEntity - ?: PopularFeedsEntity(emptyList()) - val recommendedNovels = - results[2].getOrNull() as? RecommendedNovelsByUserTasteEntity - ?: RecommendedNovelsByUserTasteEntity(emptyList()) + val popularNovelsResult = popularNovelsDeferred.await() + val popularFeedsResult = popularFeedsDeferred.await() + val recommendedNovelsResult = recommendedNovelsDeferred.await() - _uiState.value = uiState.value?.copy( - loading = false, - error = false, - popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.toHomePopularFeeds(), - recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - loading = false, - error = true, - ) + val failure = popularNovelsResult.exceptionOrNull() + ?: popularFeedsResult.exceptionOrNull() + ?: recommendedNovelsResult.exceptionOrNull() + if (failure != null) { + handleFailureState() + return@coroutineScope } + + val popularNovels = popularNovelsResult.getOrThrow() + val popularFeeds = popularFeedsResult.getOrThrow() + val recommendedNovels = recommendedNovelsResult.getOrThrow() + + _uiState.value = uiState.value?.copy( + loading = false, + error = false, + popularNovels = popularNovels.popularNovels, + popularFeeds = popularFeeds, + recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, + ) } } @@ -130,37 +125,47 @@ class HomeViewModel } private suspend fun fetchGuestData() { - viewModelScope.launch { - runCatching { - listOf( - async { novelRepository.fetchPopularNovels() }, - async { feedRepository.fetchPopularFeeds() }, - ).awaitAll() - }.onSuccess { responses -> - val popularNovels = responses[0] as PopularNovelsEntity - val popularFeeds = responses[1] as PopularFeedsEntity + coroutineScope { + val popularNovelsDeferred = + async { runCatching { novelRepository.fetchPopularNovels() } } + val popularFeedsDeferred = + async { runCatching { feedRepository.fetchHomePopularFeeds() } } - _uiState.value = uiState.value?.copy( - loading = false, - popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds.toHomePopularFeeds(), - ) - }.onFailure { - _uiState.value = uiState.value?.copy( - loading = false, - error = true, - ) + val popularNovelsResult = popularNovelsDeferred.await() + val popularFeedsResult = popularFeedsDeferred.await() + + val failure = popularNovelsResult.exceptionOrNull() + ?: popularFeedsResult.exceptionOrNull() + if (failure != null) { + handleFailureState() + return@coroutineScope } + + val popularNovels = popularNovelsResult.getOrThrow() + val popularFeeds = popularFeedsResult.getOrThrow() + + _uiState.value = uiState.value?.copy( + loading = false, + popularNovels = popularNovels.popularNovels, + popularFeeds = popularFeeds, + ) } } + private fun handleFailureState() { + _uiState.value = uiState.value?.copy( + loading = false, + error = true, + ) + } + fun updateFeed() { viewModelScope.launch { runCatching { - feedRepository.fetchPopularFeeds() + feedRepository.fetchHomePopularFeeds() }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( - popularFeeds = popularFeeds.toHomePopularFeeds(), + popularFeeds = popularFeeds, ) }.onFailure { _uiState.value = uiState.value?.copy(error = true) @@ -252,35 +257,4 @@ class HomeViewModel } } } - - private suspend fun PopularFeedsEntity.toHomePopularFeeds(): List> = - coroutineScope { - popularFeeds - .map { feed -> - async { - runCatching { - val feedDetail = feedRepository.fetchFeed(feed.feedId) - val novel = feedDetail.novel ?: return@runCatching null - - feed.copy( - feesContent = feedDetail.content, - isSpoiler = feedDetail.isSpoiler, - isPublic = feedDetail.isPublic, - novelTitle = novel.title, - novelImage = feed.novelImage.ifBlank { novel.thumbnail }, - ) - }.getOrNull() - } - }.awaitAll() - .filterNotNull() - .filter { feed -> - feed.isPublic && - feed.novelTitle.isNotBlank() && - feed.novelImage.isNotBlank() - }.chunked(POPULAR_FEED_PAGE_SIZE) - } - - companion object { - private const val POPULAR_FEED_PAGE_SIZE = 2 - } } From 07ba1a23666b538617ed4815dc5abd32ec3bff97 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 15:37:48 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=AA=85=EC=B9=AD=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `FeedRepository`의 `fetchHomePopularFeeds` 메서드명을 `fetchPopularFeedsWithDetails`로 변경 - `HomeViewModel`에서 변경된 메서드명을 참조하도록 수정 및 반영 --- .../java/com/into/websoso/data/repository/FeedRepository.kt | 2 +- .../java/com/into/websoso/ui/main/home/HomeViewModel.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt index 6b292a368..08118350a 100644 --- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt @@ -38,7 +38,7 @@ class FeedRepository suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData() - suspend fun fetchHomePopularFeeds(): List> = + suspend fun fetchPopularFeedsWithDetails(): List> = coroutineScope { fetchPopularFeeds() .popularFeeds diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index 59d82cf42..4d7e2c63b 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -74,7 +74,7 @@ class HomeViewModel val popularNovelsDeferred = async { runCatching { novelRepository.fetchPopularNovels() } } val popularFeedsDeferred = - async { runCatching { feedRepository.fetchHomePopularFeeds() } } + async { runCatching { feedRepository.fetchPopularFeedsWithDetails() } } val recommendedNovelsDeferred = async { runCatching { novelRepository.fetchRecommendedNovelsByUserTaste() } } @@ -129,7 +129,7 @@ class HomeViewModel val popularNovelsDeferred = async { runCatching { novelRepository.fetchPopularNovels() } } val popularFeedsDeferred = - async { runCatching { feedRepository.fetchHomePopularFeeds() } } + async { runCatching { feedRepository.fetchPopularFeedsWithDetails() } } val popularNovelsResult = popularNovelsDeferred.await() val popularFeedsResult = popularFeedsDeferred.await() @@ -162,7 +162,7 @@ class HomeViewModel fun updateFeed() { viewModelScope.launch { runCatching { - feedRepository.fetchHomePopularFeeds() + feedRepository.fetchPopularFeedsWithDetails() }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( popularFeeds = popularFeeds, From d3f492d3758c17d05a4bf38885790e3054c8c44c Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 15:43:39 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=95=20=EB=A1=9C=EC=A7=81=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `FeedRepository`에서 수행하던 인기 피드 리스트 chunk(페이징) 처리를 `HomeViewModel`로 이동 - `FeedRepository.fetchPopularFeedsWithDetails`의 반환 타입을 `List>`에서 `List<...>`로 변경 - `HomeViewModel`에 `HOME_POPULAR_FEED_PAGE_SIZE` 상수 추가 및 UI 상태 업데이트 시 데이터 가공 로직 반영 --- .../com/into/websoso/data/repository/FeedRepository.kt | 8 ++------ .../com/into/websoso/ui/main/home/HomeViewModel.kt | 10 +++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt index 08118350a..a871cf196 100644 --- a/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt +++ b/app/src/main/java/com/into/websoso/data/repository/FeedRepository.kt @@ -38,7 +38,7 @@ class FeedRepository suspend fun fetchPopularFeeds(): PopularFeedsEntity = feedApi.getPopularFeeds().toData() - suspend fun fetchPopularFeedsWithDetails(): List> = + suspend fun fetchPopularFeedsWithDetails(): List = coroutineScope { fetchPopularFeeds() .popularFeeds @@ -63,7 +63,7 @@ class FeedRepository feed.isPublic && feed.novelTitle.isNotBlank() && feed.novelImage.isNotBlank() - }.chunked(HOME_POPULAR_FEED_PAGE_SIZE) + } } suspend fun saveRemovedFeed(feedId: Long) { @@ -91,8 +91,4 @@ class FeedRepository false -> feedApi.postLikes(selectedFeedId) } } - - companion object { - private const val HOME_POPULAR_FEED_PAGE_SIZE = 2 - } } diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index 4d7e2c63b..d1717dd14 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -98,7 +98,7 @@ class HomeViewModel loading = false, error = false, popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds, + popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE), recommendedNovelsByUserTaste = recommendedNovels.tasteNovels, ) } @@ -147,7 +147,7 @@ class HomeViewModel _uiState.value = uiState.value?.copy( loading = false, popularNovels = popularNovels.popularNovels, - popularFeeds = popularFeeds, + popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE), ) } } @@ -165,7 +165,7 @@ class HomeViewModel feedRepository.fetchPopularFeedsWithDetails() }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( - popularFeeds = popularFeeds, + popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE), ) }.onFailure { _uiState.value = uiState.value?.copy(error = true) @@ -257,4 +257,8 @@ class HomeViewModel } } } + + companion object { + private const val HOME_POPULAR_FEED_PAGE_SIZE = 2 + } } From 1bae043bf17660ba27dd0c01920ef4bf31fce7e2 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 15:47:09 +0900 Subject: [PATCH 07/10] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=20=EC=97=AC=EB=B6=80=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `FeedMapper`에서 `isPublic` 필드의 기본값을 `true`에서 `false`로 변경 --- app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt index 022e9843a..dd8060626 100644 --- a/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt +++ b/app/src/main/java/com/into/websoso/data/mapper/FeedMapper.kt @@ -115,7 +115,7 @@ fun PopularFeedsResponseDto.toData(): PopularFeedsEntity = likeCount = feed.likeCount, commentCount = feed.commentCount, isSpoiler = feed.isSpoiler, - isPublic = feed.isPublic ?: true, + isPublic = feed.isPublic ?: false, novelTitle = feed.novelTitle ?: feed.title.orEmpty(), novelImage = feed.novelImage ?: feed.novelThumbnailImage.orEmpty(), novelGenreImage = feed.novelGenreImage.orEmpty(), From 930eef46aebe36544c9bd1eabdbc2f7f81bbd6d0 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 15:56:51 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=EC=9D=B8=EA=B8=B0=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=A0=9C=EC=95=BD=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EB=86=92=EC=9D=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인기 피드 슬롯의 높이를 고정값(122dp)에서 `0dp`로 변경하여 영역을 가변적으로 차지하도록 수정 - 구분선(divider)을 중앙에 배치하고, 이를 기준으로 슬롯 1과 2가 상하로 균등하게 배치되도록 제약 조건 조정 --- app/src/main/res/layout/item_popular_feed.xml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/item_popular_feed.xml b/app/src/main/res/layout/item_popular_feed.xml index c2501cd86..6f6b22f2c 100644 --- a/app/src/main/res/layout/item_popular_feed.xml +++ b/app/src/main/res/layout/item_popular_feed.xml @@ -10,28 +10,29 @@ android:id="@+id/item_popular_fees_slot1" layout="@layout/item_popular_feed_slot" android:layout_width="0dp" - android:layout_height="122dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/view_popular_feed_divider_1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/view_popular_feed_divider_1" /> From fcb029c7dbb417cea45f9ab83bcce5790d5e18df Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 16:03:32 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=93=9C=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5=20=EC=8B=9C=20=EC=97=90=EB=9F=AC=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `HomeViewModel`에서 인기 작품 및 인기 피드 데이터 로드 성공 시 `error` 상태를 `false`로 변경하도록 수정 --- .../main/java/com/into/websoso/ui/main/home/HomeViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt index d1717dd14..619050877 100644 --- a/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/into/websoso/ui/main/home/HomeViewModel.kt @@ -146,6 +146,7 @@ class HomeViewModel _uiState.value = uiState.value?.copy( loading = false, + error = false, popularNovels = popularNovels.popularNovels, popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE), ) @@ -165,6 +166,7 @@ class HomeViewModel feedRepository.fetchPopularFeedsWithDetails() }.onSuccess { popularFeeds -> _uiState.value = uiState.value?.copy( + error = false, popularFeeds = popularFeeds.chunked(HOME_POPULAR_FEED_PAGE_SIZE), ) }.onFailure { From 0bcf7982be06da7685054092918f0f42b0a81eca Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 28 May 2026 17:24:50 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=82=B4=20=EC=84=9C=EC=9E=AC=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=84=B9=EC=85=98=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 내 서재 통계 배경색 변경 및 틴트 적용 (`primary_20_F5F7FF`) - 통계 수치 텍스트 색상을 검정색에서 보라색(`primary_100_6A5DFD`)으로 변경 - 통계 항목 간의 구분선(`View`) 제거 - 기타 레이아웃 속성 정렬 및 정리 --- app/src/main/res/layout/fragment_my_page.xml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/layout/fragment_my_page.xml b/app/src/main/res/layout/fragment_my_page.xml index 1de2e3357..5eabdef49 100644 --- a/app/src/main/res/layout/fragment_my_page.xml +++ b/app/src/main/res/layout/fragment_my_page.xml @@ -140,6 +140,7 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" android:background="@drawable/bg_my_library_gray50_radius_14dp" + android:backgroundTint="@color/primary_20_F5F7FF" android:gravity="center" android:orientation="horizontal" android:paddingVertical="16dp" @@ -160,7 +161,7 @@ android:layout_weight="1" android:text='@{String.valueOf(myLibraryViewModel.uiState.novelStats.interestNovelCount)}' android:textAppearance="@style/title2" - android:textColor="@color/black" + android:textColor="@color/primary_100_6A5DFD" tools:text="12" /> - -