diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt index 7c42d631..4851aa09 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt @@ -3,6 +3,7 @@ package com.goliath.emojihub.data_sources import com.goliath.emojihub.EmojiHubApplication import com.goliath.emojihub.data_sources.api.EmojiApi import com.goliath.emojihub.data_sources.api.PostApi +import com.goliath.emojihub.data_sources.api.ReactionApi import com.goliath.emojihub.data_sources.api.UserApi import dagger.Module import dagger.Provides @@ -59,6 +60,11 @@ object NetworkModule { fun providesPostRestApi(retrofit: Retrofit): PostApi = retrofit.create(PostApi::class.java) + @Provides + @Singleton + fun providesReactionRestApi(retrofit: Retrofit): ReactionApi = + retrofit.create(ReactionApi::class.java) + // empty responses should be handled `success` private val nullOnEmptyConverterFactory = object : Converter.Factory() { fun converterFactory() = this diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt new file mode 100644 index 00000000..e06158f1 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt @@ -0,0 +1,30 @@ +package com.goliath.emojihub.data_sources.api + +import retrofit2.Response +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface ReactionApi { + @POST("reaction") + suspend fun uploadReaction( + @Query("postId") postId: String, + @Query("emojiId") emojiId: String + ): Response + + @GET("reactions") + suspend fun fetchReactionList( + ): Response + + @GET("reaction") + suspend fun getReactionWithId( + @Path("id") id: String + ): Response + + @DELETE("reaction") + suspend fun deleteReaction( + @Query("reactionId") reactionId: String + ): Response +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt b/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt index f72648d3..62fc803a 100644 --- a/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt +++ b/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt @@ -2,12 +2,29 @@ package com.goliath.emojihub.models import com.google.gson.annotations.SerializedName +class Reaction( + dto: ReactionDto +) { + val id: String = dto.id + val createdAt: String = dto.createdAt + val createdBy: String = dto.createdBy + val emojiId: String = dto.emojiId + val postId: String = dto.postId +} data class ReactionMetaDataDto( @SerializedName("emoji_unicode_list") val unicodeList: List ) data class ReactionDto( - @SerializedName("emoji_list") - val emojiList: List + val id: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("created_by") val createdBy: String, + @SerializedName("emoji_id") val emojiId: String, + @SerializedName("post_id") val postId: String +) + +data class UploadReactionDto( + @SerializedName("postId") val postId: String, + @SerializedName("emojiId") val emojiId: String ) \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt index d6a431e0..8aea92bc 100644 --- a/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt @@ -6,6 +6,8 @@ import com.goliath.emojihub.repositories.remote.EmojiRepository import com.goliath.emojihub.repositories.remote.EmojiRepositoryImpl import com.goliath.emojihub.repositories.remote.PostRepository import com.goliath.emojihub.repositories.remote.PostRepositoryImpl +import com.goliath.emojihub.repositories.remote.ReactionRepository +import com.goliath.emojihub.repositories.remote.ReactionRepositoryImpl import com.goliath.emojihub.repositories.remote.UserRepository import com.goliath.emojihub.repositories.remote.UserRepositoryImpl import dagger.Binds @@ -27,4 +29,7 @@ abstract class RepositoryModule { @Binds abstract fun bindsX3dRepository(impl: X3dRepositoryImpl): X3dRepository + + @Binds + abstract fun bindsReactionRepository(impl: ReactionRepositoryImpl): ReactionRepository } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt new file mode 100644 index 00000000..9bf0395c --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt @@ -0,0 +1,37 @@ +package com.goliath.emojihub.repositories.remote + +import androidx.paging.PagingData +import com.goliath.emojihub.data_sources.api.ReactionApi +import com.goliath.emojihub.models.ReactionDto +import kotlinx.coroutines.flow.Flow +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +interface ReactionRepository { + suspend fun fetchReactionList(): Flow> + suspend fun uploadReaction(postId: String, emojiId: String): Response + suspend fun getReactionWithId(id: String) + suspend fun deleteReaction(reactionId: String) +} + +@Singleton +class ReactionRepositoryImpl @Inject constructor( + private val reactionApi: ReactionApi +): ReactionRepository { + override suspend fun fetchReactionList(): Flow> { + TODO() + } + + override suspend fun uploadReaction(postId: String, emojiId: String): Response { + return reactionApi.uploadReaction(postId, emojiId) + } + + override suspend fun getReactionWithId(id: String) { + TODO() + } + + override suspend fun deleteReaction(reactionId: String) { + reactionApi.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt new file mode 100644 index 00000000..95f4c511 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt @@ -0,0 +1,60 @@ +package com.goliath.emojihub.usecases + +import androidx.paging.PagingData +import androidx.paging.map +import com.goliath.emojihub.data_sources.ApiErrorController +import com.goliath.emojihub.models.Reaction +import com.goliath.emojihub.repositories.remote.ReactionRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +interface ReactionUseCase { + + val reactionList: StateFlow> + suspend fun fetchReactionList(): Flow> + suspend fun updateReactionList(data: PagingData) + suspend fun uploadReaction(postId: String, emojiId: String): Boolean + suspend fun getReactionWithId(id: String) + suspend fun deleteReaction(reactionId: String) +} + +@Singleton +class ReactionUseCaseImpl @Inject constructor( + private val repository: ReactionRepository, + private val errorController: ApiErrorController +): ReactionUseCase { + + private val _reactionList = MutableStateFlow>(PagingData.empty()) + override val reactionList: StateFlow> + get() = _reactionList + + override suspend fun updateReactionList(data: PagingData) { + _reactionList.emit(data) + } + + override suspend fun fetchReactionList(): Flow> { + return repository.fetchReactionList().map { it.map { dto -> Reaction(dto) } } + } + + override suspend fun uploadReaction(postId: String, emojiId: String): Boolean { + val response = repository.uploadReaction(postId, emojiId) + return if (response.isSuccessful) { + true + } else { + errorController.setErrorState(response.code()) + false + } + } + + override suspend fun getReactionWithId(id: String) { + repository.getReactionWithId(id) + } + + override suspend fun deleteReaction(reactionId: String) { + repository.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt index d09a6075..5be128cc 100644 --- a/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt @@ -16,4 +16,7 @@ abstract class UseCaseModule { @Binds abstract fun bindsPostUseCase(impl: PostUseCaseImpl): PostUseCase + + @Binds + abstract fun bindsReactionUseCase(impl: ReactionUseCaseImpl): ReactionUseCase } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt index 06945776..45a9e88d 100644 --- a/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt @@ -1,5 +1,8 @@ package com.goliath.emojihub.viewmodels +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.cachedIn @@ -15,6 +18,7 @@ class PostViewModel @Inject constructor( val postList = postUseCase.postList val myPostList = postUseCase.myPostList + var currentPostId by mutableStateOf("") suspend fun fetchPostList() { viewModelScope.launch { diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt new file mode 100644 index 00000000..967aac73 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt @@ -0,0 +1,39 @@ +package com.goliath.emojihub.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.goliath.emojihub.usecases.ReactionUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ReactionViewModel @Inject constructor( + private val reactionUseCase: ReactionUseCase +): ViewModel() { + + val reactionList = reactionUseCase.reactionList + + suspend fun fetchReactionList() { + viewModelScope.launch { + reactionUseCase.fetchReactionList() + .cachedIn(viewModelScope) + .collect { + reactionUseCase.updateReactionList(it) + } + } + } + + suspend fun uploadReaction(postId: String, emojiId: String): Boolean { + return reactionUseCase.uploadReaction(postId, emojiId) + } + + suspend fun getReactionWithId(id: String) { + reactionUseCase.getReactionWithId(id) + } + + suspend fun deleteReaction(reactionId: String) { + reactionUseCase.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt index ffc63c81..b4945961 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt @@ -1,5 +1,6 @@ package com.goliath.emojihub.views.components +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -34,7 +35,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.goliath.emojihub.LocalBottomSheetController import com.goliath.emojihub.LocalNavController -import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.ui.theme.Color.EmojiHubDividerColor import com.goliath.emojihub.extensions.toEmoji import com.goliath.emojihub.models.Emoji @@ -42,6 +42,8 @@ import com.goliath.emojihub.ui.theme.Color.LightGray import kotlinx.coroutines.launch import com.goliath.emojihub.ui.theme.Color.White import com.goliath.emojihub.viewmodels.EmojiViewModel +import com.goliath.emojihub.viewmodels.PostViewModel +import com.goliath.emojihub.viewmodels.ReactionViewModel enum class BottomSheetContent { VIEW_REACTION, ADD_REACTION, EMPTY @@ -58,6 +60,10 @@ fun CustomBottomSheet ( val coroutineScope = rememberCoroutineScope() val viewModel = hiltViewModel() + val reactionViewModel = hiltViewModel() + val postViewModel = hiltViewModel() + val navController = LocalNavController.current + var selectedEmojiClass by remember { mutableStateOf("전체") } val emojisByClass = emojiList.groupBy { it.unicode } val emojiClassFilters = listOf("전체") + emojisByClass.keys.toList() @@ -186,7 +192,15 @@ fun CustomBottomSheet ( items(myCreatedEmojiList.itemCount) { index -> myCreatedEmojiList[index]?.let { EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { - //TODO: add reaction to post + coroutineScope.launch { + val success = reactionViewModel.uploadReaction(postId = postViewModel.currentPostId, emojiId = it.id) + if (success) { + Log.d("addReaction", "postId = ${postViewModel.currentPostId}, emojiId = ${it.id}") + coroutineScope.launch { + bottomSheetState.hide() + } + } + } } } } @@ -194,7 +208,15 @@ fun CustomBottomSheet ( items(mySavedEmojiList.itemCount) { index -> mySavedEmojiList[index]?.let { EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { - //TODO: add emoji reaction to post + coroutineScope.launch { + val success = reactionViewModel.uploadReaction(postId = postViewModel.currentPostId, emojiId = it.id) + if (success) { + Log.d("addReaction", "postId = ${postViewModel.currentPostId}, emojiId = ${it.id}") + coroutineScope.launch { + bottomSheetState.hide() + } + } + } } } } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt index a388bb3c..68df57fa 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt @@ -27,6 +27,7 @@ import com.goliath.emojihub.extensions.reactionsToString import com.goliath.emojihub.models.Post import com.goliath.emojihub.ui.theme.Color.EmojiHubDetailLabel import com.goliath.emojihub.viewmodels.EmojiViewModel +import com.goliath.emojihub.viewmodels.PostViewModel import kotlinx.coroutines.launch @Composable @@ -36,6 +37,7 @@ fun PostCell( val bottomSheetState = LocalBottomSheetController.current val coroutineScope = rememberCoroutineScope() val emojiViewModel = hiltViewModel() + val postViewModel = hiltViewModel() Box( modifier = Modifier @@ -101,6 +103,7 @@ fun PostCell( IconButton(onClick = { coroutineScope.launch { emojiViewModel.bottomSheetContent = BottomSheetContent.ADD_REACTION + postViewModel.currentPostId = post.id bottomSheetState.show() } }) {