Skip to content

Commit

Permalink
Merge pull request #94 from team-JMT/feat/register_review
Browse files Browse the repository at this point in the history
�후기 등록 기능 구현
  • Loading branch information
soopeach authored Mar 12, 2024
2 parents c29de4f + 3769f38 commit 9c20c23
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.gdsc.data.datasource

import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import org.gdsc.data.database.RegisteredRestaurant
import org.gdsc.data.database.ReviewPaging
import org.gdsc.data.model.RegisteredRestaurantResponse
Expand Down Expand Up @@ -44,4 +45,6 @@ interface RestaurantDataSource {

suspend fun getRestaurantReviews(restaurantId: Int): ReviewPaging

suspend fun postRestaurantReview(restaurantId: Int, reviewContent: String, reviewImages: List<MultipartBody.Part>): Boolean

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.gdsc.data.database.RegisteredRestaurant
import org.gdsc.data.database.RestaurantByMapPagingSource
Expand Down Expand Up @@ -40,7 +42,7 @@ class RestaurantDataSourceImpl @Inject constructor(
private val db: RestaurantDatabase,
) : RestaurantDataSource {

private val coroutineScope : CoroutineScope = CoroutineScope(Dispatchers.IO)
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override suspend fun getRestaurantLocationInfo(
query: String,
latitude: String,
Expand All @@ -51,7 +53,10 @@ class RestaurantDataSourceImpl @Inject constructor(
return restaurantAPI.getRestaurantLocationInfo(query, latitude, longitude, page).data
}

override suspend fun getRecommendRestaurantInfo(recommendRestaurantId: Int, userLocation: UserLocation): RestaurantInfoResponse {
override suspend fun getRecommendRestaurantInfo(
recommendRestaurantId: Int,
userLocation: UserLocation
): RestaurantInfoResponse {
return restaurantAPI.getRecommendRestaurantInfo(recommendRestaurantId, userLocation).data
}

Expand Down Expand Up @@ -86,7 +91,8 @@ class RestaurantDataSourceImpl @Inject constructor(
mapOf(
"name" to restaurantRegistrationRequest.name.toRequestBody(),
"introduce" to restaurantRegistrationRequest.introduce.toRequestBody(),
"categoryId" to restaurantRegistrationRequest.categoryId.toString().toRequestBody(),
"categoryId" to restaurantRegistrationRequest.categoryId.toString()
.toRequestBody(),
"canDrinkLiquor" to restaurantRegistrationRequest.canDrinkLiquor.toString()
.toRequestBody(),
"goWellWithLiquor" to restaurantRegistrationRequest.goWellWithLiquor.toRequestBody(),
Expand Down Expand Up @@ -117,7 +123,11 @@ class RestaurantDataSourceImpl @Inject constructor(

@OptIn(ExperimentalPagingApi::class)
override suspend fun getRestaurants(
userId: Int, locationData: Location, sortType: SortType, foodCategory: FoodCategory, drinkPossibility: DrinkPossibility
userId: Int,
locationData: Location,
sortType: SortType,
foodCategory: FoodCategory,
drinkPossibility: DrinkPossibility
): Flow<PagingResult<RegisteredRestaurant>> {
val categoryFilter = when (foodCategory) {
FoodCategory.INIT, FoodCategory.ETC -> null
Expand All @@ -135,7 +145,7 @@ class RestaurantDataSourceImpl @Inject constructor(
FoodCategory.INIT, FoodCategory.ETC -> String.Empty
else -> foodCategory.key
},
isCanDrinkLiquor = isCanDrinkLiquor,
isCanDrinkLiquor = isCanDrinkLiquor,
)

val restaurantSearchMapRequest = RestaurantSearchMapRequest(filter, locationData)
Expand All @@ -155,9 +165,23 @@ class RestaurantDataSourceImpl @Inject constructor(
) {
with(db.restaurantDao()) {
when (sortType) {
SortType.DISTANCE -> getRegisteredRestaurantsSortedDistance(userId, categoryFilter, isCanDrinkLiquor)
SortType.RECENCY -> getRegisteredRestaurantsSortedRecent(userId, categoryFilter, isCanDrinkLiquor)
SortType.LIKED -> getRegisteredRestaurants(userId, categoryFilter, isCanDrinkLiquor)
SortType.DISTANCE -> getRegisteredRestaurantsSortedDistance(
userId,
categoryFilter,
isCanDrinkLiquor
)

SortType.RECENCY -> getRegisteredRestaurantsSortedRecent(
userId,
categoryFilter,
isCanDrinkLiquor
)

SortType.LIKED -> getRegisteredRestaurants(
userId,
categoryFilter,
isCanDrinkLiquor
)
}
}

Expand All @@ -173,7 +197,12 @@ class RestaurantDataSourceImpl @Inject constructor(
}

override suspend fun getRestaurantsByMap(
userLocation: Location?, startLocation: Location?, endLocation: Location?, sortType: SortType, foodCategory: FoodCategory?, drinkPossibility: DrinkPossibility?
userLocation: Location?,
startLocation: Location?,
endLocation: Location?,
sortType: SortType,
foodCategory: FoodCategory?,
drinkPossibility: DrinkPossibility?
): Flow<PagingData<RegisteredRestaurantResponse>> {
val restaurantSearchMapRequest = RestaurantSearchMapRequest(
userLocation = userLocation,
Expand All @@ -196,7 +225,8 @@ class RestaurantDataSourceImpl @Inject constructor(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = true
)) {
)
) {
RestaurantByMapPagingSource(
restaurantAPI,
restaurantSearchMapRequest
Expand All @@ -208,4 +238,16 @@ class RestaurantDataSourceImpl @Inject constructor(
return restaurantAPI.getRestaurantReviews(restaurantId).data
}

override suspend fun postRestaurantReview(
restaurantId: Int,
reviewContent: String,
reviewImages: List<MultipartBody.Part>
): Boolean {

return restaurantAPI.postRestaurantReview(
restaurantId,
MultipartBody.Part.createFormData("reviewContent", reviewContent), reviewImages
).code == "RESTAURANT_REVIEW_CREATED"
}

}
8 changes: 8 additions & 0 deletions data/src/main/java/org/gdsc/data/network/RestaurantAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,12 @@ interface RestaurantAPI {
@Path("recommendRestaurantId") recommendRestaurantId: Int,
): Response<ReviewPaging>

@Multipart
@POST("/api/v1/restaurant/{recommendRestaurantId}/review")
suspend fun postRestaurantReview(
@Path("recommendRestaurantId") recommendRestaurantId: Int,
@Part reviewContent: MultipartBody.Part,
@Part reviewImages: List<MultipartBody.Part>,
): Response<String>

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.paging.PagingData
import androidx.paging.map
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import okhttp3.MultipartBody
import org.gdsc.data.datasource.RestaurantDataSource
import org.gdsc.domain.DrinkPossibility
import org.gdsc.domain.FoodCategory
Expand Down Expand Up @@ -125,4 +126,12 @@ class RestaurantRepositoryImpl @Inject constructor(
override suspend fun getRestaurantReviews(restaurantId: Int): List<Review> {
return restaurantDataSource.getRestaurantReviews(restaurantId).reviewList
}

override suspend fun postRestaurantReview(
restaurantId: Int,
reviewContent: String,
reviewImages: List<MultipartBody.Part>
): Boolean {
return restaurantDataSource.postRestaurantReview(restaurantId, reviewContent, reviewImages)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.gdsc.domain.repository

import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import org.gdsc.domain.DrinkPossibility
import org.gdsc.domain.FoodCategory
import org.gdsc.domain.SortType
Expand All @@ -13,7 +14,6 @@ import org.gdsc.domain.model.Review
import org.gdsc.domain.model.UserLocation
import org.gdsc.domain.model.request.ModifyRestaurantInfoRequest
import org.gdsc.domain.model.request.RestaurantRegistrationRequest
import org.gdsc.domain.model.request.RestaurantSearchMapRequest
import org.gdsc.domain.model.response.RestaurantInfoResponse

interface RestaurantRepository {
Expand Down Expand Up @@ -43,4 +43,6 @@ interface RestaurantRepository {

suspend fun getRestaurantReviews(restaurantId: Int): List<Review>

suspend fun postRestaurantReview(restaurantId: Int, reviewContent: String, reviewImages: List<MultipartBody.Part>): Boolean

}
14 changes: 14 additions & 0 deletions domain/src/main/java/org/gdsc/domain/usecase/PostReviewUseCase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.gdsc.domain.usecase

import okhttp3.MultipartBody
import org.gdsc.domain.repository.RestaurantRepository
import javax.inject.Inject

class PostReviewUseCase @Inject constructor(
private val restaurantRepository: RestaurantRepository
) {

suspend operator fun invoke(restaurantId: Int, reviewContent: String, reviewImages: List<MultipartBody.Part>): Boolean {
return restaurantRepository.postRestaurantReview(restaurantId, reviewContent, reviewImages)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.gdsc.presentation.view.mypage.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import org.gdsc.presentation.databinding.ItemPhotoWillBeUploadedBinding

class PhotoWillBeUploadedAdapter(
private val onDeleteButtonClicked: (String) -> Unit
) :
ListAdapter<String, PhotoWillBeUploadedAdapter.PhotoWillBeUploadedViewHolder>(
diffUtil
) {
inner class PhotoWillBeUploadedViewHolder(private val binding: ItemPhotoWillBeUploadedBinding) :
RecyclerView.ViewHolder(binding.root) {

fun bind(url: String) {

Glide.with(binding.root)
.load(url.toUri())
.into(binding.photoWillBeUploaded)

binding.deleteButton.setOnClickListener {
onDeleteButtonClicked(url)
}
}
}

companion object {
val diffUtil = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}

override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): PhotoWillBeUploadedViewHolder {
val inflater = LayoutInflater.from(parent.context)
return PhotoWillBeUploadedViewHolder(
ItemPhotoWillBeUploadedBinding.inflate(
inflater,
parent,
false
)
)
}

override fun onBindViewHolder(holder: PhotoWillBeUploadedViewHolder, position: Int) {
holder.apply {
bind(getItem(position))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@ import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.net.toUri
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import org.gdsc.presentation.R
import org.gdsc.presentation.databinding.FragmentRestaurantDetailBinding
import org.gdsc.presentation.utils.BitmapUtils.getCompressedBitmapFromUri
import org.gdsc.presentation.utils.BitmapUtils.saveBitmapToFile
import org.gdsc.presentation.utils.CalculatorUtils
import org.gdsc.presentation.utils.repeatWhenUiStarted
import org.gdsc.presentation.view.mypage.adapter.PhotoWillBeUploadedAdapter
import org.gdsc.presentation.view.mypage.adapter.RestaurantDetailPagerAdapter
import org.gdsc.presentation.view.mypage.viewmodel.RestaurantDetailViewModel

Expand All @@ -28,6 +39,10 @@ class RestaurantDetailFragment : Fragment() {

private val viewModel: RestaurantDetailViewModel by activityViewModels()

private val adapter = PhotoWillBeUploadedAdapter {
viewModel.deletePhotoForReviewState(it)
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
Expand All @@ -41,6 +56,66 @@ class RestaurantDetailFragment : Fragment() {
setButtons()
setTabLayout()
observeData()

repeatWhenUiStarted {
viewModel.photosForReviewState.collect {
adapter.submitList(it)
}
}

binding.rvImageListWillBeUploaded.adapter = adapter

binding.addImageIcon.setOnClickListener {
val directions =
RestaurantDetailFragmentDirections.actionRestaurantDetailFragmentToMultiImagePickerFragment()

findNavController().navigate(directions)
}

binding.btnRegister.setOnClickListener {

val pictures = mutableListOf<MultipartBody.Part>()

viewModel.photosForReviewState.value.forEachIndexed { index, sUri ->

sUri.toUri()
.getCompressedBitmapFromUri(requireContext())
?.saveBitmapToFile(requireContext(), "$index.jpg")?.let { imageFile ->

val requestFile =
RequestBody.create(
MediaType.parse("image/png"),
imageFile
)

val body =
MultipartBody.Part.createFormData(
"reviewImages",
imageFile.name,
requestFile
)

pictures.add(body)

}

}

viewModel.postReview(
binding.etReview.text.toString(),
pictures
) {
binding.etReview.text.clear()
Toast.makeText(requireContext(), "후기가 등록되었습니다!", Toast.LENGTH_SHORT).show()
}
}

setFragmentResultListener("pickImages") { _, bundle ->
val images = bundle.getStringArray("imagesUri")
viewModel.setPhotosForReviewState(images?.toList() ?: emptyList())

if (images.isNullOrEmpty()) return@setFragmentResultListener
}
}

private fun setButtons() {
Expand Down
Loading

0 comments on commit 9c20c23

Please sign in to comment.