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
Expand Up @@ -209,8 +209,8 @@ fun MenuDetailScreen(
timeText = review.createdAt,
reviewText = review.comment,
isLiked = review.isLiked,
likeCount = review.likeCount,
keywords = review.keywordReviews.filter { it != "" },
likeCount = review.likeCount ?: 0L,
keywords = review.keywordReviews.filterNotNull().filter { it.isNotBlank() },
imageUris = review.etc.images?.map { it.toUri() } ?: listOf(),
modifier = Modifier.padding(horizontal = 14.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -69,9 +67,9 @@ fun LeaveReviewRoute(
val menu by vm.menu.observeAsState()
val rating by vm.reviewRating
val selectedKeywords by vm.selectedKeywordList.collectAsState()
var comment by remember { mutableStateOf("") }
val commentHint by vm.commentHint.observeAsState()
val imageUriList by vm.imageUriList.observeAsState()
val comment by vm.comment.collectAsState()

val keyboardState by keyboardAsState()

Expand All @@ -98,11 +96,13 @@ fun LeaveReviewRoute(
imageUriList = imageUriList,
onNavigateUp = onNavigateUp,
onSubmitReview = {
scope.launch { vm.leaveReview(context, rating.toDouble(), comment) }
scope.launch { vm.leaveReview(context) }
},
onRatingChange = { rating -> vm.setReviewRating(rating) },
onSelectKeyword = { idx, keyword -> vm.selectKeyword(idx, keyword) },
onCommentChange = { if (it.length <= 150) comment = it },
onCommentChange = { text ->
if (text.length <= 150) vm.setComment(text)
},
onAddImage = onAddImage,
onClickDetails = onClickDetails,
onDeleteImage = { vm.deleteImageUri(it) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ fun MenuReviewScreen(
timeText = review.createdAt,
reviewText = review.comment,
isLiked = review.isLiked,
likeCount = review.likeCount,
keywords = review.keywordReviews.filter { it != "" },
likeCount = review.likeCount ?: 0L,
keywords = review.keywordReviews.filterNotNull().filter { it.isNotBlank() },
imageUris = review.etc.images?.map { it.toUri() } ?: listOf(),
modifier = Modifier.padding(start = 16.dp, end = 12.dp)
)
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/wafflestudio/siksha2/models/Review.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ data class Review(
@Json(name = "user_id") val userId: Long,
@Json(name = "score") val score: Double,
@Json(name = "comment") val comment: String?,
@Json(name = "keyword_reviews") val keywordReviews: List<String>,
@Json(name = "keyword_reviews") val keywordReviews: List<String?>,
@Json(name = "is_liked") val isLiked: Boolean,
@Json(name = "likeCount") val likeCount: Long,
@Json(name = "likeCount") val likeCount: Long?,
@Json(name = "name_kr") val nameKr: String?,
@Json(name = "name_en") val nameEn: String?,
@Json(name = "created_at") val createdAt: String,
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ interface SikshaApi {
@Part images: List<MultipartBody.Part>
): NetworkResult<LeaveReviewResult>

@Multipart
@PATCH("/reviews/{review_id}")
suspend fun patchMenuReview(
@Path("review_id") reviewId: Long,
@Part("menu_id") menuId: Long,
@Part("score") score: Long,
@Part("taste") taste: String,
@Part("price") price: String,
@Part("food_composition") foodComposition: String,
@Part comment: MultipartBody.Part,
@Part images: List<MultipartBody.Part>
): NetworkResult<LeaveReviewResult>

@POST("/reviews/{review_id}/like")
suspend fun reviewLike(@Path("review_id") reviewId: Long): NetworkResult<Review>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LeaveReviewParam(
@Json(name = "menu_id") val menuId: Long,
@Json(name = "score") val score: Double,
@Json(name = "score") val score: Long,
@Json(name = "taste") val taste: String?,
@Json(name = "price") val price: String?,
@Json(name = "food_composition") val foodComposition: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class MenuRepository @Inject constructor(

suspend fun leaveMenuReview(
menuId: Long,
score: Double,
score: Long,
taste: String?,
price: String?,
foodComposition: String?,
Expand Down Expand Up @@ -139,6 +139,19 @@ class MenuRepository @Inject constructor(
return sikshaApi.leaveMenuReviewImages(menuId, score, taste, price, foodComposition, comment, images)
}

suspend fun patchMenuReview(
reviewId: Long,
menuId: Long,
score: Long,
taste: String = "",
price: String = "",
foodComposition: String = "",
comment: MultipartBody.Part,
images: List<MultipartBody.Part>
): NetworkResult<LeaveReviewResult> {
return sikshaApi.patchMenuReview(reviewId, menuId, score, taste, price, foodComposition, comment, images)
}

suspend fun getReviewRecommendationComments(score: Long): NetworkResult<FetchRecommendationReviewCommentsResult> {
return sikshaApi.fetchRecommendationReviewComments(score)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.wafflestudio.siksha2.ui.common

import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.net.HttpURLConnection
import java.net.URL

suspend fun downloadImageToFile(
context: Context,
imageUrl: String
): File? = withContext(Dispatchers.IO) {
return@withContext try {
val url = URL(imageUrl)
val connection = url.openConnection() as HttpURLConnection
connection.connect()

if (connection.responseCode != HttpURLConnection.HTTP_OK) {
return@withContext null
}

val input = connection.inputStream
val tempFile = File.createTempFile("review_img_", ".jpg", context.cacheDir)

tempFile.outputStream().use { output ->
input.copyTo(output)
}

tempFile
} catch (e: Exception) {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.net.Uri
import androidx.compose.runtime.FloatState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
Expand All @@ -18,17 +19,19 @@ import com.wafflestudio.siksha2.network.dto.LeaveReviewResult
import com.wafflestudio.siksha2.network.dto.ReviewRestaurant
import com.wafflestudio.siksha2.network.result.NetworkResult
import com.wafflestudio.siksha2.repositories.MenuRepository
import com.wafflestudio.siksha2.ui.common.downloadImageToFile
import com.wafflestudio.siksha2.utils.ImageUtil
import com.wafflestudio.siksha2.utils.showToast
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
Expand Down Expand Up @@ -113,16 +116,52 @@ class MenuDetailViewModel @Inject constructor(
val reviewRating: FloatState
get() = _reviewRating

private val _deleteResult = MutableLiveData<Boolean>()
val deleteResult: LiveData<Boolean>
get() = _deleteResult
private val _comment = MutableStateFlow("")
val comment: StateFlow<String>
get() = _comment

private val _editingReview = MutableLiveData<Review?>()
val editingReview: LiveData<Review?>
get() = _editingReview
private val _deleteResult = MutableSharedFlow<Boolean>()
val deleteResult = _deleteResult.asSharedFlow()

fun setEditingReview(review: Review?) {
_editingReview.value = review
private val _editingReviewId = MutableStateFlow<Long?>(null)
val editingReviewId: StateFlow<Long?>
get() = _editingReviewId

fun setEditingReview(context: Context, review: Review?) {
viewModelScope.launch {
if (review == null) {
_editingReviewId.value = null
_reviewRating.floatValue = 5f
_selectedKeywordList.value = listOf("", "", "")
_comment.value = ""
_imageUriList.value = emptyList()
return@launch
}

_editingReviewId.value = review.id
_reviewRating.floatValue = review.score.toFloat()
_selectedKeywordList.value = listOf(
review.keywordReviews.getOrNull(0) ?: "",
review.keywordReviews.getOrNull(1) ?: "",
review.keywordReviews.getOrNull(2) ?: ""
)
_comment.value = review.comment.orEmpty()

val imageUris = mutableListOf<Uri>()
review.etc.images
?.take(3) // 최대 3장 제한
?.forEach { imageUrl ->
val file = downloadImageToFile(context, imageUrl)
if (file != null) {
imageUris.add(file.toUri())
}
}
_imageUriList.value = imageUris
}
}

fun setComment(text: String) {
_comment.value = text
}

fun refreshMenu(menuId: Long) {
Expand Down Expand Up @@ -161,15 +200,10 @@ class MenuDetailViewModel @Inject constructor(
}
}

fun getReviews(menuId: Long): Flow<PagingData<Review>> = Pager(
config = MenuReviewPagingSource.Config,
pagingSourceFactory = { menuRepository.getReviewsPagingSource(menuId) }
).flow.cachedIn(viewModelScope)

fun deleteReview(id: Long) {
viewModelScope.launch {
val success = menuRepository.deleteReview(id)
_deleteResult.postValue(success)
_deleteResult.emit(success)
}
}

Expand Down Expand Up @@ -261,42 +295,79 @@ class MenuDetailViewModel @Inject constructor(
return menuUpdateResponse
}

suspend fun leaveReview(context: Context, score: Double, comment: String): NetworkResult<LeaveReviewResult>? {
Timber.d("LeaveReview ${_menu.value?.id}")
suspend fun leaveReview(context: Context): NetworkResult<LeaveReviewResult>? {
val reviewId = _editingReviewId.value
val menuId = _menu.value?.id ?: return null
Timber.d("not null")
val response = if (_imageUriList.value?.isNotEmpty() == true) {
context.showToast("이미지 압축 중입니다.")
_leaveReviewState.value = ReviewState.COMPRESSING
val imageList = _imageUriList.value?.map {
val score = reviewRating.floatValue.toLong()
val taste = selectedKeywordList.value[0]
val price = selectedKeywordList.value[1]
val foodComposition = selectedKeywordList.value[2]
val comment = comment.value

val imageParts = _imageUriList.value
?.takeIf { it.isNotEmpty() }
?.map {
ImageUtil.getCompressedImage(context, it)
}?.map { file ->
val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
MultipartBody.Part.createFormData("images", file.name, requestBody)
}
val commentBody = MultipartBody.Part.createFormData("comment", comment)
imageList?.let {

val commentPart = MultipartBody.Part.createFormData("comment", comment)

val response = if (imageParts != null) {
context.showToast("이미지 압축 중입니다.")
_leaveReviewState.value = ReviewState.COMPRESSING

if (reviewId != null) {
menuRepository.patchMenuReview(
_editingReviewId.value!!,
menuId,
score,
taste,
price,
foodComposition,
commentPart,
imageParts
)
} else {
menuRepository.leaveMenuReviewImage(
menuId,
score.toLong(),
selectedKeywordList.value[0],
selectedKeywordList.value[1],
selectedKeywordList.value[2],
commentBody,
imageList
score,
taste,
price,
foodComposition,
commentPart,
imageParts
)
}
} else {
menuRepository.leaveMenuReview(
menuId,
score,
selectedKeywordList.value[0],
selectedKeywordList.value[1],
selectedKeywordList.value[2],
comment
)
if (reviewId != null) {
menuRepository.patchMenuReview(
_editingReviewId.value!!,
menuId,
score,
taste,
price,
foodComposition,
commentPart,
emptyList()
)
} else {
menuRepository.leaveMenuReview(
menuId,
score,
taste,
price,
foodComposition,
comment
)
}
}

notifySendReviewEnd()
_editingReviewId.value = null

return response
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class MenuMyReviewChildAdapter(
binding.reviewDate.text = item.createdAt.toLocalDateTime().toParsedTimeString()
binding.reviewContent.text = item.comment ?: "내용 없음"

val tags = item.keywordReviews.orEmpty()
val tags = item.keywordReviews

tagViews.forEachIndexed { index, view ->
val text = tags.getOrNull(index)
Expand Down
Loading
Loading