diff --git a/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt index dd07d70b0..3d1b7a1d5 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt @@ -11,7 +11,10 @@ import com.eatssu.android.data.service.ReviewService import com.eatssu.android.domain.repository.ReviewRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File import javax.inject.Inject class ReviewRepositoryImpl @Inject constructor(private val reviewService: ReviewService) : @@ -55,12 +58,11 @@ class ReviewRepositoryImpl @Inject constructor(private val reviewService: Review flow { emit(reviewService.getMealReviewInfo(mealId)) } - - override suspend fun getImageString( - image: MultipartBody.Part, - ): Flow> = - flow { - emit(reviewService.uploadImage(image)) - } + override suspend fun getImageString(file: File): Flow> = flow { + val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) + val multipart = MultipartBody.Part.createFormData("image", file.name, requestFile) + val response = reviewService.uploadImage(multipart) + emit(response) + } } diff --git a/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt b/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt index 0328fe041..8f7049c46 100644 --- a/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt +++ b/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt @@ -1,30 +1,10 @@ package com.eatssu.android.di.network -import android.content.Context -import android.content.Intent -import android.os.Handler -import android.os.Looper -import android.widget.Toast -import com.eatssu.android.BuildConfig.BASE_URL -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.TokenResponse import com.eatssu.android.domain.usecase.auth.GetAccessTokenUseCase -import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase -import com.eatssu.android.domain.usecase.auth.LogoutUseCase -import com.eatssu.android.domain.usecase.auth.SetAccessTokenUseCase -import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase -import com.eatssu.android.presentation.login.LoginActivity -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.runBlocking import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import timber.log.Timber -import java.lang.reflect.Type import javax.inject.Inject /** @@ -37,8 +17,6 @@ class TokenInterceptor @Inject constructor( companion object { private const val HEADER_AUTHORIZATION = "Authorization" private const val HEADER_CONTENT_TYPE = "Content-Type" - private const val HEADER_ACCEPT = "accept" - } override fun intercept(chain: Interceptor.Chain): Response { @@ -46,13 +24,10 @@ class TokenInterceptor @Inject constructor( val originalRequest = chain.request() val request = originalRequest.newBuilder() - .addHeader(HEADER_ACCEPT, "application/hal+json") .addHeader(HEADER_CONTENT_TYPE, "application/json") .addHeader(HEADER_AUTHORIZATION, "Bearer $accessToken") .build() - Timber.d("AccessToken 헤더 추가됨: $accessToken") - return chain.proceed(request) } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt index 4e66037ba..93289d82c 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt @@ -8,7 +8,7 @@ import com.eatssu.android.data.dto.response.GetMenuReviewInfoResponse import com.eatssu.android.data.dto.response.GetReviewListResponse import com.eatssu.android.data.dto.response.ImageResponse import kotlinx.coroutines.flow.Flow -import okhttp3.MultipartBody +import java.io.File interface ReviewRepository { @@ -42,6 +42,6 @@ interface ReviewRepository { ): Flow> suspend fun getImageString( - image: MultipartBody.Part, + file: File ): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt index 3643b417d..4561a8998 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt @@ -4,14 +4,14 @@ import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.ImageResponse import com.eatssu.android.domain.repository.ReviewRepository import kotlinx.coroutines.flow.Flow -import okhttp3.MultipartBody +import java.io.File import javax.inject.Inject class GetImageUrlUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { suspend operator fun invoke( - image: MultipartBody.Part, + file: File ): Flow> = - reviewRepository.getImageString(image) + reviewRepository.getImageString(file) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewAdapter.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewAdapter.kt index 0f904f401..1810458e8 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewAdapter.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewAdapter.kt @@ -35,17 +35,14 @@ class ReviewAdapter : binding.tvMenuName.text = data.menu binding.rbRate.rating = data.mainGrade.toFloat() - if (!data.imgUrl.isNullOrEmpty()) { + val firstImageUrl = data.imgUrl?.firstOrNull() + + if (!firstImageUrl.isNullOrEmpty()) { Glide.with(itemView) - .load(data.imgUrl[0]) + .load(firstImageUrl) .into(binding.ivReviewPhoto) binding.ivReviewPhoto.visibility = View.VISIBLE binding.cvPhotoReview.visibility = View.VISIBLE - - if (data.imgUrl[0].isEmpty()) { - binding.ivReviewPhoto.visibility = View.GONE - binding.cvPhotoReview.visibility = View.GONE - } } else { binding.ivReviewPhoto.visibility = View.GONE binding.cvPhotoReview.visibility = View.GONE diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ImageViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ImageViewModel.kt deleted file mode 100644 index f5878bc18..000000000 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ImageViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.eatssu.android.presentation.cafeteria.review.write - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.eatssu.android.domain.usecase.review.GetImageUrlUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import timber.log.Timber -import java.io.File -import javax.inject.Inject - -@HiltViewModel -class ImageViewModel -@Inject constructor( - private val getImageUrlUseCase: GetImageUrlUseCase, -) : ViewModel() { - - private val _imageUrl: MutableStateFlow = MutableStateFlow("") - val imageUrl: StateFlow get() = _imageUrl - - private var _imageFile: MutableStateFlow = MutableStateFlow(null) - val imageFile: StateFlow get() = _imageFile - - private val _uiState: MutableStateFlow = MutableStateFlow(ImageState()) - val uiState: StateFlow = _uiState.asStateFlow() - - - fun setImageFile(imageFile: File) { - _imageFile.value = imageFile - } - - fun deleteFile() { - _imageFile.value?.delete() - _imageUrl.value = "" - } - - - fun saveS3() { - if (imageFile.value?.exists() == true) { - val requestFile = imageFile.value?.asRequestBody("image/*".toMediaTypeOrNull()) - val multipart = requestFile?.let { - MultipartBody.Part.createFormData( - "image", - imageFile.value?.name, - it - ) - } - viewModelScope.launch { - if (multipart != null) { - getImageUrlUseCase(multipart).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { it.copy(error = true, toastMessage = "이미지 변환에 실패하였습니다.") } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - result.result?.apply { - _uiState.update { - it.copy( - loading = false, - error = false, - isImageUploadDone = true, - imgUrl = url.toString() - ) - } - } - } - } - } - } - } -} - - -data class ImageState( - var loading: Boolean = true, - var error: Boolean = false, - var toastMessage: String = "", - - var imgUrl: String = "", - var isImageUploadDone: Boolean = false, -) \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteRateActivity.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteRateActivity.kt index ad64bcfac..62d36db61 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteRateActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteRateActivity.kt @@ -1,12 +1,10 @@ package com.eatssu.android.presentation.cafeteria.review.write import android.Manifest -import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle -import android.provider.MediaStore import android.text.Editable import android.text.TextWatcher import android.view.View @@ -15,35 +13,34 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions +import com.eatssu.android.data.dto.request.WriteReviewRequest import com.eatssu.android.databinding.ActivityReviewWriteRateBinding +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.base.BaseActivity import com.eatssu.android.presentation.util.showToast import dagger.hilt.android.AndroidEntryPoint import id.zelory.compressor.Compressor -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber import java.io.File -import java.text.DecimalFormat -import kotlin.math.log10 -import kotlin.math.pow @AndroidEntryPoint class ReviewWriteRateActivity : BaseActivity(ActivityReviewWriteRateBinding::inflate) { private val viewModel: UploadReviewViewModel by viewModels() - private val imageviewModel: ImageViewModel by viewModels() private var itemId: Long = 0 private lateinit var itemName: String private var comment: String? = "" private var imageFile: File? = null - private var compressedImage: File? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -61,10 +58,11 @@ class ReviewWriteRateActivity : // 외부 저장소에 대한 런타임 퍼미션 요청 requestStoragePermission() - // 텍스트 리뷰 입력 관련 설정 setupTextReviewInput() - setOnClickListener() + + observeState() + observeEvents() } fun setOnClickListener() { @@ -77,23 +75,26 @@ class ReviewWriteRateActivity : binding.btnNextReview2.setOnClickListener { if (binding.rbMain.rating.toInt() == 0 || binding.rbAmount.rating.toInt() == 0 || binding.rbTaste.rating.toInt() == 0) { - showToast("별점을 등록해주세요") + showToast("별점을 모두 등록해주세요") } - if ((comment?.trim()?.length ?: 0) < 3) { - showToast("3자 이상 입력해주세요") - } - - //파일 업로드가 끝났거나, 파일을 첨부하지 않거나 if (imageFile?.exists() == true) { - showToast("리뷰 업로드 중") - compressImage() - - Timber.d("s3 시작") + lifecycleScope.launch { + val compressed = compressImage() + if (compressed != null) { + val imageUrl = viewModel.saveS3(compressed) + if (imageUrl != null) { + postPhotoReview(imageUrl) + } else { + showToast("이미지 업로드에 실패했습니다.") + } + } else { + showToast("이미지 압축에 실패했습니다.") + } + } } else { postReview() - } } @@ -101,140 +102,144 @@ class ReviewWriteRateActivity : } - - private fun compressImage() { - imageFile?.let { imageFile -> - lifecycleScope.launch { - // Default compression - compressedImage = Compressor.compress(this@ReviewWriteRateActivity, imageFile) - Timber.d("압축 된 사이즈+" + (compressedImage?.length()?.div(1024)).toString()) - setCompressedImage() + private fun observeState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState.collect { state -> + when (state) { + is UiState.Loading -> showLoading(true) + is UiState.Success -> finish() + else -> { + showLoading(false) + } + } + } } - } ?: showError("Please choose an image!") - } - - private fun showError(errorMessage: String) { - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() + } } - private fun setCompressedImage() { - compressedImage?.let { it -> - imageviewModel.setImageFile(it) - imageviewModel.saveS3() //이미지 url 반환 api 호출 - lifecycleScope.launch { - imageviewModel.uiState.collectLatest { - if (it.isImageUploadDone) { - postReview() + private fun observeEvents() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiEvent.collect { event -> + when (event) { + is UiEvent.ShowToast -> showToast(event.message) } } } - Timber.d("Compressed image save in " + getReadableFileSize(it.length())) } } + private fun postPhotoReview(imageUrl: String) { - private fun getReadableFileSize(size: Long): String { - if (size <= 0) { - return "0" - } - val units = arrayOf("B", "KB", "MB", "GB", "TB") - val digitGroups = (log10(size.toDouble()) / log10(1024.0)).toInt() - return DecimalFormat("#,##0.#").format(size / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] + val photoReview = WriteReviewRequest( + mainRating = binding.rbMain.rating.toInt(), + amountRating = binding.rbAmount.rating.toInt(), + tasteRating = binding.rbTaste.rating.toInt(), + content = comment.toString(), + imageUrl = imageUrl + ) + + viewModel.postReview(itemId, photoReview) + Timber.d("사진있는 리뷰 전송") } + private fun postReview() { + val review = WriteReviewRequest( + mainRating = binding.rbMain.rating.toInt(), + amountRating = binding.rbAmount.rating.toInt(), + tasteRating = binding.rbTaste.rating.toInt(), + content = comment.toString(), + ) - // // 이미지를 결과값으로 받는 변수 - private val imageResult = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == RESULT_OK) { - // 이미지를 받으면 ImageView에 적용한다 - val imageUri = result.data?.data - imageUri?.let { + viewModel.postReview(itemId, review) + Timber.d("사진없는 리뷰 전송") + } + + private suspend fun compressImage(): File? { + return imageFile?.let { originalFile -> + Compressor.compress(this@ReviewWriteRateActivity, originalFile) + } + } - // 서버 업로드를 위해 파일 형태로 변환한다 - imageFile = File(getRealPathFromURI(it)) + // 이미지를 결과값으로 받는 변수 + private val imageResult = registerForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + Timber.d("Selected image URI: $uri") + uri?.let { + try { // 이미지를 불러온다 Glide.with(this) - .load(imageUri) + .load(uri) .fitCenter() .apply(RequestOptions().override(500, 500)) .into(binding.ivImage) - binding.ivImage.visibility = View.VISIBLE binding.btnDelete.visibility = View.VISIBLE - } - } - - } - // 이미지 실제 경로 반환 - private fun getRealPathFromURI(uri: Uri): String { + // 임시 파일 생성 + val inputStream = contentResolver.openInputStream(uri) + val tempFile = File(cacheDir, "temp_image_${System.currentTimeMillis()}.jpg") + inputStream?.use { input -> + tempFile.outputStream().use { output -> + input.copyTo(output) + } + } - val buildName = Build.MANUFACTURER - if (buildName.equals("Xiaomi")) { - return uri.path!! - } - var columnIndex = 0 - val proj = arrayOf(MediaStore.Images.Media.DATA) - val cursor = contentResolver.query(uri, proj, null, null, null) - if (cursor!!.moveToFirst()) { - columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + imageFile = tempFile + Timber.d("Image loaded successfully to: ${tempFile.absolutePath}") + } catch (e: Exception) { + Timber.e(e, "Error processing selected image") + showToast("이미지 처리 중 오류가 발생했습니다.") + } + } ?: run { + Timber.d("No image selected") } - val result = cursor.getString(columnIndex) - cursor.close() - - Timber.d("realPath: $result") - return result } // 갤러리를 부르는 메서드 private fun checkPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val readMediaImagePermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_MEDIA_IMAGES + ) - checkSelfPermission(Manifest.permission.READ_MEDIA_IMAGES) - - val readMediaImagePermission = - ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) - - //권한 확인 if (readMediaImagePermission == PackageManager.PERMISSION_DENIED) { ActivityCompat.requestPermissions( - this, arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - ), PERMISSION_REQUEST_CODE + this, + arrayOf(Manifest.permission.READ_MEDIA_IMAGES), + PERMISSION_REQUEST_CODE ) Timber.e("권한 없음") - } else { openGallery() - } - } else { - checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + val writePermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + val readPermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_EXTERNAL_STORAGE + ) - val writePermission = - ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - val readPermission = - ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - //권한 확인 if (writePermission == PackageManager.PERMISSION_DENIED || readPermission == PackageManager.PERMISSION_DENIED ) { - - // 권한 요청 ActivityCompat.requestPermissions( - this, arrayOf( + this, + arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE - ), PERMISSION_REQUEST_CODE + ), + PERMISSION_REQUEST_CODE ) Timber.e("권한 없음") - } else { openGallery() } @@ -243,24 +248,47 @@ class ReviewWriteRateActivity : private fun openGallery() { - val intent = Intent(Intent.ACTION_PICK) - // intent의 data와 type을 동시에 설정하는 메서드 - intent.setDataAndType( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - "image/*" - ) - - imageResult.launch(intent) + try { + Timber.d("Opening gallery picker") + imageResult.launch("image/*") + } catch (e: Exception) { + Timber.e(e, "Error opening gallery") + showToast("갤러리를 열 수 없습니다.") + } } private fun requestStoragePermission() { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != - PackageManager.PERMISSION_GRANTED - ) { - requestPermissions( - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - PERMISSION_REQUEST_CODE - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_MEDIA_IMAGES + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.READ_MEDIA_IMAGES), + PERMISSION_REQUEST_CODE + ) + } + } else { + if (ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE + ), + PERMISSION_REQUEST_CODE + ) + } } } @@ -283,8 +311,7 @@ class ReviewWriteRateActivity : if (imageFile?.exists() == true) { Toast.makeText(this, "리뷰 작성을 중지합니다.", Toast.LENGTH_SHORT).show() binding.ivImage.setImageDrawable(null) - imageFile!!.delete() //file을 날린다. - compressedImage?.delete() //file을 날린다. + imageFile!!.delete() } } @@ -295,53 +322,40 @@ class ReviewWriteRateActivity : if (imageFile?.exists() == true) { showToast("이미지가 삭제되었습니다.") binding.ivImage.setImageDrawable(null) - imageFile!!.delete() //file을 날린다. - compressedImage?.delete() //file을 날린다. + imageFile!!.delete() binding.ivImage.visibility = View.GONE binding.btnDelete.visibility = View.GONE - imageviewModel.deleteFile() - } else { showToast("이미지를 삭제할 수 없습니다.") } } - private fun postReview() { - //Todo imageurl을 체크해야하는 이유? - viewModel.setReviewData( - itemId, - binding.rbMain.rating.toInt(), - binding.rbAmount.rating.toInt(), - binding.rbTaste.rating.toInt(), - comment.toString(), - imageviewModel.imageUrl.value ?: "" - ) - - viewModel.postReview() - Timber.d("리뷰 전송") - - - lifecycleScope.launch { - viewModel.uiState.collectLatest { - if (it.error) { - showToast(viewModel.uiState.value.toastMessage) - } - if (!it.error && !it.loading && it.isUpload) { - showToast(viewModel.uiState.value.toastMessage) - Timber.d("리뷰 작성 성공") - finish() + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + PERMISSION_REQUEST_CODE -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + openGallery() + } else { + showToast("권한이 거부되어 이미지를 선택할 수 없습니다.") } } } } - companion object { - - const val REVIEW_MIN_LENGTH = 10 + private fun showLoading(isLoading: Boolean) { + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.btnNextReview2.visibility = if (isLoading) View.INVISIBLE else View.VISIBLE + } + companion object { // 갤러리 권한 요청 const val PERMISSION_REQUEST_CODE = 1 } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt index b013b6394..b896f56da 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt @@ -3,87 +3,69 @@ package com.eatssu.android.presentation.cafeteria.review.write import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eatssu.android.data.dto.request.WriteReviewRequest +import com.eatssu.android.domain.usecase.review.GetImageUrlUseCase import com.eatssu.android.domain.usecase.review.WriteReviewUseCase +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber +import java.io.File import javax.inject.Inject @HiltViewModel class UploadReviewViewModel @Inject constructor( private val writeReviewUseCase: WriteReviewUseCase, + private val getImageUrlUseCase: GetImageUrlUseCase, ) : ViewModel() { - private val _uiState: MutableStateFlow = - MutableStateFlow(UploadReviewState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow>(UiState.Init) + val uiState = _uiState.asStateFlow() - private val _reviewData: MutableStateFlow = - MutableStateFlow(WriteReviewRequest()) - val reviewData: StateFlow = _reviewData.asStateFlow() + private val _uiEvent: MutableSharedFlow = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() - private val _menuId: MutableStateFlow = MutableStateFlow(-1) - val menuId: StateFlow = _menuId.asStateFlow() - - fun setReviewData( - menuId: Long, - mainRating: Int, - amountRating: Int, - tasteRating: Int, - comment: String, - imageUrl: String, - ) { - _menuId.value = menuId - _reviewData.value = - WriteReviewRequest(mainRating, amountRating, tasteRating, comment, imageUrl) - } - - - fun postReview() { + fun postReview(menuId: Long, reviewData: WriteReviewRequest) { viewModelScope.launch { - writeReviewUseCase( - menuId.value, reviewData.value - ).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { - it.copy( - loading = false, - error = true, - toastMessage = "리뷰 작성에 실패하였습니다.", - isUpload = false, - ) + writeReviewUseCase(menuId, reviewData) + .onStart { + _uiState.value = UiState.Loading } - Timber.d(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - _uiState.update { - it.copy( - loading = false, - error = false, - toastMessage = "리뷰가 작성되었습니다.", - isUpload = true, - ) + .catch { e -> + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("리뷰 작성에 실패하였습니다.")) + Timber.e(e) + } + .collectLatest { + _uiState.value = UiState.Success() + _uiEvent.emit(UiEvent.ShowToast("리뷰가 작성되었습니다.")) } - } } } + + suspend fun saveS3(file: File): String? { + return getImageUrlUseCase(file) + .onStart { + _uiState.value = UiState.Loading + } + .catch { e -> + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("이미지 업로드에 실패하였습니다.")) + Timber.e(e) + } + .map { it.result?.url } + .firstOrNull() + } } -data class UploadReviewState( - var toastMessage: String = "", - var loading: Boolean = true, - var error: Boolean = false, - var isUpload: Boolean = false, -) \ No newline at end of file +sealed class UploadReviewState diff --git a/app/src/main/res/layout/activity_review_write_rate.xml b/app/src/main/res/layout/activity_review_write_rate.xml index b17040949..fa18b58ff 100644 --- a/app/src/main/res/layout/activity_review_write_rate.xml +++ b/app/src/main/res/layout/activity_review_write_rate.xml @@ -245,6 +245,18 @@ + +