Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
13f3fc4
feat: 피드 좋아요 상태 캐싱을 위한 데이터 모델 추가
devfeijoa Jun 22, 2026
6df2b10
fix: 피드 상세 좋아요 상태 동기화 및 캐싱 로직 추가
devfeijoa Jun 22, 2026
3198b31
fix: UpdatedFeedRepository 내 좋아요 상태 관리 변수 및 메서드 명칭 수정
devfeijoa Jun 22, 2026
77a2704
refactor: 피드 좋아요 상태 동기화 메서드 명칭 수정
devfeijoa Jun 22, 2026
efbaa6e
feat: 피드 좋아요 보류 상태 저장을 위한 `PendingFeedLikeStore` 인터페이스 추가
devfeijoa Jun 22, 2026
ecd3b39
feat: PendingFeedLikeDataStore를 위한 Dagger Qualifier 추가
devfeijoa Jun 22, 2026
92956a8
feat: PendingFeedLikeDataStore를 위한 Dagger Qualifier 추가
devfeijoa Jun 22, 2026
88fcbd5
feat: 피드 좋아요 보류 상태 저장을 위한 데이터 모델 추가
devfeijoa Jun 22, 2026
d324628
feat: 피드 좋아요 보류 상태 저장을 위한 DataStore 구현
devfeijoa Jun 22, 2026
7f2ef93
feat: 피드 좋아요 보류 상태 저장을 위한 DataStore 구현
devfeijoa Jun 22, 2026
47ffc95
feat: 피드 좋아요 보류 상태 삭제 기능 추가
devfeijoa Jun 22, 2026
65b4e63
feat: 좋아요 상태 변경 시 로컬 저장소 동기화 로직 추가
devfeijoa Jun 22, 2026
e90aceb
fix: 동기화 완료된 피드 좋아요 상태의 DataStore 삭제 로직 추가
devfeijoa Jun 22, 2026
808fd0a
fix: UpdatedFeedRepository 내 펜딩된 좋아요 상태 동기화 로직 추가
devfeijoa Jun 22, 2026
9bd1122
fix: 좋아요 상태 동기화 로직 보완 및 메모리/로컬 저장소 관리 개선
devfeijoa Jun 22, 2026
40a43ea
fix: 피드 좋아요 상태 저장 로직의 순차 처리 보장 및 초기화 방식 수정
devfeijoa Jun 22, 2026
6ae731a
fix: `UpdatedFeedRepository` 내 좋아요 상태 저장 로직의 동시성 제어 방식 수정
devfeijoa Jun 23, 2026
d77e967
refactor: 피드 좋아요 동기화 로직 개선 및 예외 케이스 처리
devfeijoa Jun 24, 2026
6d8fa9d
fix: 피드 좋아요 상태의 원본 값 캐싱 로직 추가
devfeijoa Jun 24, 2026
faaa6e7
refactor: DefaultPendingFeedLikeStore 내 MutableMap 저장 로직 수정
devfeijoa Jun 24, 2026
407e534
docs: PendingFeedLikeStore 내 좋아요 상태 일치 여부에 따른 삭제 메서드 주석 추가
devfeijoa Jun 24, 2026
33441ce
build: data:feed 모듈에 유닛 테스트 실행 환경 추가
devfeijoa Sep 28, 2026
035a452
test: UpdatedFeedRepository 피드 좋아요 동기화 로직 단위 테스트 추가
devfeijoa Sep 28, 2026
55913df
test: 피드 저장소 테스트의 Fake API 구조 고도화 및 에지 케이스 테스트 추가
devfeijoa Sep 28, 2026
e4d60ba
refactor: ktlint formatting
devfeijoa Sep 28, 2026
7fe1221
refactor: 좋아요 테스트 공통 도구 분리
devfeijoa Sep 29, 2026
663208c
test: 좋아요 계정 전환 결함 재현 테스트 추가
devfeijoa Sep 29, 2026
6ca7a4e
test: 피드 좋아요 상태 변경 시 상세 정보 동기화 로직 추가
devfeijoa Oct 5, 2026
bb08596
feat: 피드 좋아요 상태 동기화 및 복구 로직 개선
devfeijoa Oct 5, 2026
6fca46f
feat: 피드 좋아요 동기화 상태 표시 및 재시도 기능 구현
devfeijoa Oct 5, 2026
d322b58
feat: 피드 좋아요 동기화 상태 표시 UI 및 재시도 로직 구현
devfeijoa Oct 5, 2026
174ed8d
refactor: 피드 좋아요 동기화 중 상태 표시 제거 및 노출 로직 단순화
devfeijoa Oct 5, 2026
8fb0988
fix: 저장된 좋아요 복원 실패 시 크래시 대신 저장·전송을 보류하도록 수정
devfeijoa Oct 5, 2026
161b9e2
test: `core:datastore` 모듈 내 JUnit 테스트 의존성 추가
devfeijoa Oct 5, 2026
1fce844
fix: 손상된 좋아요 저장 파일을 초기화하고 초기화 표시를 남기도록 수정
devfeijoa Oct 5, 2026
d1ea2a8
test: DefaultPendingFeedLikeStore 데이터 손상 복구 및 초기화 알림 로직 단위 테스트 추가
devfeijoa Oct 5, 2026
d621daa
fix: 좋아요 복원 시 저장 파일 초기화 여부를 기록하도록 수정
devfeijoa Oct 5, 2026
46924e2
fix: 해석할 수 없는 좋아요 기록을 덮어쓰지 않고 보관하도록 수정
devfeijoa Oct 5, 2026
cab3637
test: DefaultPendingFeedLikeStore의 데이터 손상 및 해석 실패 시나리오 대응 테스트 추가
devfeijoa Oct 5, 2026
0be6311
fix: 복원 실패 중 보류한 좋아요를 복원 성공 후 저장하도록 수정
devfeijoa Oct 5, 2026
3fe6f0e
test: 저장소 복구 시 보류되었던 좋아요 상태의 누적 저장 검증 테스트 추가
devfeijoa Oct 5, 2026
7cb260a
refactor: `UpdatedFeedRepository` 내 좋아요 복구 실패 상태를 화면에서 볼 수 있게 공개 하는 방…
devfeijoa Oct 6, 2026
ebdad7b
test: 좋아요 복구 실패 상태(`isLikeRestoreFailed`)에 대한 검증 로직 추가
devfeijoa Oct 6, 2026
600e595
feat: 좋아요 복원 실패 시 피드 목록과 상세에 재시도 배너 표시
devfeijoa Oct 6, 2026
9e996f6
fix: 미복원 안내 표시를 사용자에게 알린 뒤 지우도록 수정
devfeijoa Oct 6, 2026
a251502
feat: 반영되지 않았을 수 있는 좋아요를 피드 목록 토스트로 안내
devfeijoa Oct 6, 2026
05d9319
fix: 늦게 도착한 조회 응답이 최신 좋아요 상태를 덮지 않도록 수정
devfeijoa Oct 6, 2026
f785c37
fix: 화면에 없는 피드도 늦게 온 조회 응답이 좋아요를 덮지 않도록 수정
devfeijoa Oct 6, 2026
10c064e
fix: 좋아요 재시도 버튼의 글꼴과 색상을 디자인 시스템에 맞게 수정
devfeijoa Oct 9, 2026
2a2c079
Merge branch 'develop' into feat/891
devfeijoa Oct 10, 2026
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
@@ -1,6 +1,5 @@
package com.into.websoso.ui.feedDetail

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
Expand All @@ -10,16 +9,16 @@ import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager.LayoutParams.WRAP_CONTENT
import android.widget.PopupWindow
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.load
import coil.transform.RoundedCornersTransformation
import com.into.websoso.R.id.tv_feed_thumb_up_count
import com.into.websoso.R.layout.activity_feed_detail
import com.into.websoso.core.common.ui.base.BaseActivity
import com.into.websoso.core.common.ui.model.ResultFrom.BlockUser
Expand All @@ -30,12 +29,12 @@ import com.into.websoso.core.common.ui.model.ResultFrom.FeedDetailRefreshed
import com.into.websoso.core.common.ui.model.ResultFrom.NovelDetailBack
import com.into.websoso.core.common.ui.model.ResultFrom.OtherUserProfileBack
import com.into.websoso.core.common.ui.model.ResultFrom.WithdrawUser
import com.into.websoso.core.common.util.SingleEventHandler
import com.into.websoso.core.common.util.getS3ImageUrl
import com.into.websoso.core.common.util.hideKeyboard
import com.into.websoso.core.common.util.showWebsosoSnackBar
import com.into.websoso.core.common.util.toFloatPxFromDp
import com.into.websoso.core.common.util.tracker.Tracker
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.resource.R.drawable.ic_blocked_user_snack_bar
import com.into.websoso.core.resource.R.drawable.ic_novel_detail_check
import com.into.websoso.core.resource.R.string.block_user_success_message
Expand All @@ -44,10 +43,12 @@ import com.into.websoso.core.resource.R.string.feed_popup_menu_content_report_is
import com.into.websoso.core.resource.R.string.feed_removed_feed_snackbar
import com.into.websoso.core.resource.R.string.feed_server_error
import com.into.websoso.core.resource.R.string.other_user_page_withdraw_user
import com.into.websoso.data.feed.repository.model.LikeSyncStatus
import com.into.websoso.databinding.ActivityFeedDetailBinding
import com.into.websoso.databinding.DialogRemovePopupMenuBinding
import com.into.websoso.databinding.DialogReportPopupMenuBinding
import com.into.websoso.databinding.MenuFeedPopupBinding
import com.into.websoso.feature.feed.component.FeedLikeSyncNotice
import com.into.websoso.ui.createFeed.CreateFeedActivity
import com.into.websoso.ui.expandedFeedImage.ExpandedFeedImageActivity
import com.into.websoso.ui.feedDetail.FeedDetailActivity.MenuType.COMMENT
Expand Down Expand Up @@ -88,7 +89,6 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed
onCommentClick(),
)
}
private val singleEventHandler: SingleEventHandler by lazy { SingleEventHandler.from() }
private lateinit var activityResultCallback: ActivityResultLauncher<Intent>
private val popupBinding: MenuFeedPopupBinding by lazy {
MenuFeedPopupBinding.inflate(LayoutInflater.from(this))
Expand All @@ -99,30 +99,12 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed

private fun onFeedContentClick(): FeedDetailClickListener =
object : FeedDetailClickListener {
@SuppressLint("CutPasteId")
override fun onLikeButtonClick(
view: View,
feedId: Long,
) {
val likeCount: Int =
view
.findViewById<TextView>(tv_feed_thumb_up_count)
.text
.toString()
.toInt()
val updatedLikeCount: Int = when (view.isSelected) {
true -> if (likeCount > 0) likeCount - 1 else 0
false -> likeCount + 1
}

view.findViewById<TextView>(tv_feed_thumb_up_count).text =
updatedLikeCount.toString()
view.isSelected = !view.isSelected

singleEventHandler.debounce(timeMillis = 100L, coroutineScope = lifecycleScope) {
tracker.trackEvent("feed_detail_like")
feedDetailViewModel.updateLike()
}
tracker.trackEvent("feed_detail_like")
feedDetailViewModel.updateLike()
}

override fun onNovelInfoClick(novelId: Long) {
Expand Down Expand Up @@ -357,6 +339,21 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed
super.onCreate(savedInstanceState)

setupView()
binding.cvFeedLikeSync.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val states by feedDetailViewModel.likeSyncStates.collectAsStateWithLifecycle()
val isRestoreFailed by feedDetailViewModel.isLikeRestoreFailed.collectAsStateWithLifecycle()
WebsosoTheme {
FeedLikeSyncNotice(
isRestoreFailed = isRestoreFailed,
needsRetry = states[feedId] == LikeSyncStatus.NEEDS_RETRY,
isSyncing = states[feedId] == LikeSyncStatus.SYNCING,
onRetry = feedDetailViewModel::retryLike,
)
}
}
}
setupObserver()
onFeedDetailClick()
refreshView()
Expand All @@ -371,7 +368,7 @@ class FeedDetailActivity : BaseActivity<ActivityFeedDetailBinding>(activity_feed
NovelDetailBack.RESULT_OK,
CreateFeed.RESULT_OK,
OtherUserProfileBack.RESULT_OK,
-> {
-> {
feedDetailViewModel.updateFeedDetail(feedId, CreateFeed)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class UpdatedFeedDetailViewModel
private val _feedDetailUiState: MutableLiveData<FeedDetailUiState> =
MutableLiveData(FeedDetailUiState())
val feedDetailUiState: LiveData<FeedDetailUiState> get() = _feedDetailUiState
val likeSyncStates = feedRepository.likeSyncStates
val isLikeRestoreFailed = feedRepository.isLikeRestoreFailed

fun retryLike() {
if (feedId != -1L) feedRepository.retryPendingLikes(setOf(feedId))
}

fun updateCommentId(newCommentId: Long) {
this.commentId = newCommentId
Expand Down Expand Up @@ -75,19 +81,25 @@ class UpdatedFeedDetailViewModel
feedRepository.sosoAllFeeds,
feedRepository.sosoRecommendedFeeds,
feedRepository.myFeeds,
) { sosoAllFeeds, sosoRecommendedFeeds, myFeeds ->
sosoAllFeeds.find { it.id == targetFeedId }
feedRepository.feedDetailLikeStates,
) { sosoAllFeeds, sosoRecommendedFeeds, myFeeds, detailLikes ->
val feed = sosoAllFeeds.find { it.id == targetFeedId }
?: sosoRecommendedFeeds.find { it.id == targetFeedId }
?: myFeeds.find { it.id == targetFeedId }
feed to detailLikes[targetFeedId]
}.distinctUntilChanged()
.collect { feedEntity ->
if (feedEntity != null) {
val currentUiState = _feedDetailUiState.value ?: FeedDetailUiState()
.collect { (feedEntity, likeState) ->
val currentUiState = _feedDetailUiState.value ?: FeedDetailUiState()
val feed = feedEntity?.toFeedModel() ?: currentUiState.feedDetail.feed
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (feed != null) {
_feedDetailUiState.value = currentUiState.copy(
loading = false,
isRefreshed = true,
feedDetail = currentUiState.feedDetail.copy(
feed = feedEntity.toFeedModel(),
feed = feed.copy(
isLiked = likeState?.isLiked ?: feed.isLiked,
likeCount = likeState?.likeCount ?: feed.likeCount,
),
novel = currentUiState.feedDetail.novel,
),
)
Expand Down Expand Up @@ -145,7 +157,7 @@ class UpdatedFeedDetailViewModel
*/
override fun onCleared() {
super.onCleared()
feedRepository.syncDirtyFeeds()
feedRepository.syncPendingLikes()
}

/**
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/into/websoso/ui/main/feed/FeedFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import androidx.fragment.app.viewModels
import com.into.websoso.R
import com.into.websoso.R.layout.fragment_feed
import com.into.websoso.core.common.ui.base.BaseFragment
import com.into.websoso.core.common.util.collectWithLifecycle
import com.into.websoso.core.common.util.showWebsosoToast
import com.into.websoso.core.common.util.tracker.Tracker
import com.into.websoso.core.designsystem.theme.WebsosoTheme
import com.into.websoso.core.resource.R.drawable.ic_novel_rating_alert
import com.into.websoso.core.resource.R.string.feed_like_unrestored_message
import com.into.websoso.databinding.DialogRemovePopupMenuBinding
import com.into.websoso.databinding.DialogReportPopupMenuBinding
import com.into.websoso.databinding.FragmentFeedBinding
Expand Down Expand Up @@ -189,6 +193,16 @@ class FeedFragment : BaseFragment<FragmentFeedBinding>(fragment_feed) {
super.onViewCreated(view, savedInstanceState)

tracker.trackEvent("feed_all")
collectUnrestoredLikes()
}

private fun collectUnrestoredLikes() {
feedViewModel.hasUnrestoredLikes.collectWithLifecycle(viewLifecycleOwner) { hasUnrestoredLikes ->
if (hasUnrestoredLikes) {
showWebsosoToast(requireContext(), getString(feed_like_unrestored_message), ic_novel_rating_alert)
feedViewModel.acknowledgeUnrestoredLikes()
}
}
}

companion object {
Expand Down
10 changes: 9 additions & 1 deletion app/src/main/res/layout/activity_feed_detail.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@

</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.compose.ui.platform.ComposeView
android:id="@+id/cv_feed_like_sync"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/cl_feed_detail_toolbar" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_feed_detail"
android:layout_width="match_parent"
Expand All @@ -64,7 +72,7 @@
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@+id/cl_feed_detail_input"
app:layout_constraintTop_toBottomOf="@+id/cl_feed_detail_toolbar" />
app:layout_constraintTop_toBottomOf="@id/cv_feed_like_sync" />

<com.into.websoso.core.common.ui.custom.WebsosoLoadingLayout
android:id="@+id/wll_feed"
Expand Down
4 changes: 3 additions & 1 deletion core/datastore/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ android {
dependencies {
implementation(projects.core.common)
implementation(projects.data.account)
implementation(projects.data.feed)
implementation(projects.data.library)
implementation(projects.data.user)

implementation(libs.datastore.preferences)
implementation(libs.serialization.json)

testImplementation(libs.junit)
}
Loading
Loading