Skip to content
Merged
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
@@ -0,0 +1,76 @@
package com.eatssu.android.data.local

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject

private val Context.favoritePartnershipDataStore by preferencesDataStore(
name = "favorite_partnership",
)

class FavoritePartnershipDataStore @Inject constructor(
@ApplicationContext private val context: Context,
) {
private companion object {
val FAVORITE_ORDER = stringPreferencesKey("favorite_order")
}

val favoriteOrder: Flow<List<Int>> = context.favoritePartnershipDataStore.data
.map { preferences ->
preferences[FAVORITE_ORDER].decodeFavoriteOrder()
}

suspend fun markLiked(partnershipId: Int) {
updateOrder { current -> listOf(partnershipId) + current.filterNot { it == partnershipId } }
}

suspend fun markUnliked(partnershipId: Int) {
updateOrder { current -> current.filterNot { it == partnershipId } }
}

suspend fun reconcile(serverIds: List<Int>): List<Int> {
var reconciled = emptyList<Int>()
context.favoritePartnershipDataStore.edit { preferences ->
reconciled = reconcileFavoriteOrder(
current = preferences[FAVORITE_ORDER].decodeFavoriteOrder(),
serverIds = serverIds,
)
preferences[FAVORITE_ORDER] = Json.encodeToString(reconciled)
}
return reconciled
}

suspend fun clear() {
context.favoritePartnershipDataStore.edit { it.clear() }
}

private suspend fun updateOrder(transform: (List<Int>) -> List<Int>) {
context.favoritePartnershipDataStore.edit { preferences ->
val current = preferences[FAVORITE_ORDER].decodeFavoriteOrder()
preferences[FAVORITE_ORDER] = Json.encodeToString(transform(current))
}
}
}

internal fun reconcileFavoriteOrder(
current: List<Int>,
serverIds: List<Int>,
): List<Int> {
val serverIdSet = serverIds.toSet()
return buildList {
// 저장된 정렬 정보에 중복 ID가 있어도 화면 목록에는 한 번만 노출한다.
addAll(current.filter { it in serverIdSet }.distinct())
addAll(serverIds.filterNot { it in this })
}
}

private fun String?.decodeFavoriteOrder(): List<Int> =
this?.let { value -> runCatching { Json.decodeFromString<List<Int>>(value) }.getOrNull() }
.orEmpty()
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.eatssu.android.data.remote.repository

import com.eatssu.android.data.local.FavoritePartnershipDataStore
import com.eatssu.android.data.model.ApiResult
import com.eatssu.android.data.model.isSuccess
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.model.orNull
Expand All @@ -14,6 +17,7 @@ import javax.inject.Inject
class PartnershipRepositoryImpl @Inject constructor(
private val partnershipService: PartnershipService,
private val userService: UserService,
private val favoritePartnershipDataStore: FavoritePartnershipDataStore,
) : PartnershipRepository {

// 유저의 학과 상관없이 모든 제휴 정보 조회
Expand All @@ -26,8 +30,30 @@ class PartnershipRepositoryImpl @Inject constructor(
override suspend fun getPartnershipById(partnershipId: Int): PartnershipRestaurant? =
partnershipService.getPartnershipById(partnershipId).map { it.toDomain() }.orNull()

// 특정 식당 클릭 시 제휴 정보 조회
override suspend fun likePartnership(
partnershipId: Int,
wasLiked: Boolean?,
): ApiResult<Unit> {
val result = partnershipService.likePartnership(partnershipId)
if (result.isSuccess() && wasLiked != null) {
if (wasLiked) {
favoritePartnershipDataStore.markUnliked(partnershipId)
} else {
favoritePartnershipDataStore.markLiked(partnershipId)
}
}
return result
}


// 유저의 학과에 해당하는 제휴 정보 조회
override suspend fun getUserCollegePartnerships(): List<Partnership> =
userService.getUserDepartmentPartnerships().map { list -> list.map { it.toDomain() } }
.orEmptyList()

// 서버에서 사용자가 찜한 제휴만 조회한다.
override suspend fun getUserFavoritePartnerships(): List<Partnership> =
userService.getUserFavoritePartnerships().map { list -> list.map { it.toDomain() } }
.orEmptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.eatssu.android.data.model.ApiResult
import com.eatssu.android.data.remote.dto.response.PartnershipResponse
import com.eatssu.android.data.remote.dto.response.PartnershipRestaurantResponse
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

interface PartnershipService {

Expand All @@ -13,10 +15,14 @@ interface PartnershipService {

// 개별 제휴 조회
@GET("partnerships/{partnershipId}")
suspend fun getPartnershipById(partnershipId: Int): ApiResult<PartnershipRestaurantResponse>
suspend fun getPartnershipById(
@Path("partnershipId") partnershipId: Int
): ApiResult<PartnershipRestaurantResponse>

// TODO 제휴 찜/등록하기/ 취소하기
@GET("partnerships/{partnershipId}/like")
suspend fun likePartnership(partnershipId: Int): ApiResult<Unit>
@POST("partnerships/{partnershipId}/like")
suspend fun likePartnership(
@Path("partnershipId") partnershipId: Int
): ApiResult<Unit>

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ interface UserService {
@GET("users/department/partnerships") // 유저 학과의 제휴 조회
suspend fun getUserDepartmentPartnerships(): ApiResult<List<PartnershipResponse>>

@GET("users/partnerships") // 유저가 찜한 제휴 조회
suspend fun getUserFavoritePartnerships(): ApiResult<List<PartnershipResponse>>

@GET("users/language") // 언어 설정 조회
suspend fun getUserLanguage(): ApiResult<LanguageResponse>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.eatssu.android.domain.model

import android.os.Parcelable
import com.eatssu.common.enums.StoreType
import kotlinx.parcelize.Parcelize

@Parcelize
data class PartnershipRestaurant(
val id: Int,
val partnershipType: String,
Expand All @@ -18,4 +21,4 @@ data class PartnershipRestaurant(
val likedByUser: Boolean,
val naverMapUrl: String? = null,
val kakaoMapUrl: String? = null,
)
) : Parcelable
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.eatssu.android.domain.repository

import com.eatssu.android.data.model.ApiResult
import com.eatssu.android.domain.model.Partnership
import com.eatssu.android.domain.model.PartnershipRestaurant

interface PartnershipRepository {
suspend fun getAllPartnerships(): List<Partnership>
suspend fun getPartnershipById(partnershipId: Int): PartnershipRestaurant?
suspend fun likePartnership(
partnershipId: Int,
wasLiked: Boolean? = null,
): ApiResult<Unit>
suspend fun getUserCollegePartnerships(): List<Partnership>
}
suspend fun getUserFavoritePartnerships(): List<Partnership>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.eatssu.android.domain.usecase.auth

import com.eatssu.android.data.local.AccountDataStore
import com.eatssu.android.data.local.FavoritePartnershipDataStore
import com.eatssu.android.data.local.SettingDataStore
import com.eatssu.android.data.local.TokenStore
import com.eatssu.common.analytics.AnalyticsTracker
Expand All @@ -10,12 +11,14 @@ class LogoutUseCase @Inject constructor(
private val accountDataStore: AccountDataStore,
private val tokenStore: TokenStore,
private val settingDataStore: SettingDataStore,
private val favoritePartnershipDataStore: FavoritePartnershipDataStore,
private val analyticsTracker: AnalyticsTracker,
) {
suspend operator fun invoke() {
accountDataStore.clear()
tokenStore.clear()
settingDataStore.clear()
favoritePartnershipDataStore.clear()
analyticsTracker.resetIdentity()
}
}
53 changes: 48 additions & 5 deletions app/src/main/java/com/eatssu/android/presentation/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ import android.view.View.GONE
import androidx.activity.viewModels
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.work.WorkManager
import com.eatssu.android.R
import com.eatssu.android.databinding.ActivityMainBinding
import com.eatssu.android.domain.model.PartnershipRestaurant
import com.eatssu.android.presentation.base.BaseActivity
import com.eatssu.android.presentation.event.AnyoneButMeEventPopupController
import com.eatssu.android.presentation.event.AnyoneButMeEventTooltipController
import com.eatssu.android.presentation.favorite.FavoriteDetailFragment
import com.eatssu.android.presentation.login.LoginActivity
import com.eatssu.android.presentation.mypage.MyPageViewModel
import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity
Expand Down Expand Up @@ -57,6 +61,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(

private val mainViewModel: MainViewModel by viewModels()
private val myPageViewModel: MyPageViewModel by viewModels()
private lateinit var navController: NavController

@SuppressLint("SuspiciousIndentation")
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -77,7 +82,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(
private fun setNavigation() {
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
navController = navHostFragment.navController
binding.bottomNaviBar.itemIconTintList = null

binding.bottomNaviBar.setOnSingleItemSelectedListener { item ->
Expand All @@ -92,10 +97,15 @@ class MainActivity : BaseActivity<ActivityMainBinding>(
true
}

R.id.anyone_but_me_menu -> {
mainViewModel.analyticsPlzNotMe()
anyoneButMeEventPopupController.openAnyoneButMePage()
false
// R.id.anyone_but_me_menu -> {
// mainViewModel.analyticsPlzNotMe()
// anyoneButMeEventPopupController.openAnyoneButMePage()
// false
// }

R.id.favorite_menu -> {
navController.navigate(R.id.favoriteFragment)
true
}

R.id.mypage_menu -> {
Expand All @@ -108,6 +118,39 @@ class MainActivity : BaseActivity<ActivityMainBinding>(
}
}
}

navController.addOnDestinationChangedListener { _, destination, _ ->
val menuItemId = when (destination.id) {
R.id.cafeteria_menu -> R.id.cafeteria_menu
R.id.mapFragment, R.id.favoriteDetailFragment -> R.id.map_menu
R.id.favoriteFragment -> R.id.favorite_menu
R.id.myPageFragment -> R.id.mypage_menu
else -> null
}
menuItemId?.let { binding.bottomNaviBar.menu.findItem(it).isChecked = true }
}
}

fun openFavoriteTab() {
if (navController.currentDestination?.id != R.id.favoriteFragment) {
navController.navigate(R.id.favoriteFragment)
}
}

fun openMapTab() {
if (navController.currentDestination?.id != R.id.mapFragment) {
navController.navigate(R.id.mapFragment)
}
}

fun openFavoriteDetail(partnership: PartnershipRestaurant) {
navController.navigate(
R.id.favoriteDetailFragment,
bundleOf(
FavoriteDetailFragment.ARG_PARTNERSHIP_ID to partnership.id,
FavoriteDetailFragment.ARG_PARTNERSHIP to partnership,
),
)
}

private fun bindEventPopup(showOnLaunch: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.eatssu.android.presentation.favorite

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.eatssu.android.analytics.ProvideAnalyticsTracker
import com.eatssu.android.domain.model.PartnershipRestaurant
import com.eatssu.android.presentation.map.MapExternalNavigator
import com.eatssu.common.analytics.AnalyticsTracker
import com.eatssu.design_system.theme.EatssuTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class FavoriteDetailFragment : Fragment() {

@Inject
lateinit var analyticsTracker: AnalyticsTracker

@Inject
lateinit var mapExternalNavigator: MapExternalNavigator

private val viewModel: FavoriteDetailViewModel by viewModels()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val partnership = requireNotNull(
BundleCompat.getParcelable(
requireArguments(),
ARG_PARTNERSHIP,
PartnershipRestaurant::class.java,
),
)

return ComposeView(requireContext()).apply {
setContent {
ProvideAnalyticsTracker(analyticsTracker) {
EatssuTheme {
FavoriteDetailRoute(
partnership = partnership,
viewModel = viewModel,
mapExternalNavigator = mapExternalNavigator,
onBack = { findNavController().popBackStack() },
)
}
}
}
}
}

companion object {
const val ARG_PARTNERSHIP_ID = "partnershipId"
const val ARG_PARTNERSHIP = "partnership"
}
}
Loading
Loading