diff --git a/buildSrc/build/kotlin/buildSrcjar-classes.txt b/buildSrc/build/kotlin/buildSrcjar-classes.txt index c98318a7..d7dd3381 100644 --- a/buildSrc/build/kotlin/buildSrcjar-classes.txt +++ b/buildSrc/build/kotlin/buildSrcjar-classes.txt @@ -1 +1 @@ -/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/AndroidXDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/ClassPathPlugins.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/ComposeDependency.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/Constants.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/DependenciesKt.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/FirebaseDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/KaptDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/KotlinDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/MaterialDesignDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/TestDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/ThirdPartyDependencies.class:/Users/ssongsik/AndroidStudioProjects/SPOT-Android/buildSrc/build/classes/kotlin/main/Versions.class \ No newline at end of file +/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/AndroidXDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/ClassPathPlugins.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/ComposeDependency.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/Constants.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/DependenciesKt.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/FirebaseDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/KaptDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/KotlinDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/MaterialDesignDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/TestDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/ThirdPartyDependencies.class:/Users/minju1459/DepromeetAndroid/buildSrc/build/classes/kotlin/main/Versions.class \ No newline at end of file diff --git a/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin b/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin index 8ded29aa..5da00f18 100644 Binary files a/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin and b/buildSrc/build/kotlin/compileKotlin/cacheable/last-build.bin differ diff --git a/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin b/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin index b4355cc7..da690789 100644 Binary files a/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin and b/buildSrc/build/kotlin/compileKotlin/local-state/build-history.bin differ diff --git a/buildSrc/build/libs/buildSrc.jar b/buildSrc/build/libs/buildSrc.jar index bfe6d490..e6f39100 100644 Binary files a/buildSrc/build/libs/buildSrc.jar and b/buildSrc/build/libs/buildSrc.jar differ diff --git a/data/src/main/java/com/depromeet/data/datasource/SeatReviewDataSource.kt b/data/src/main/java/com/depromeet/data/datasource/SeatReviewDataSource.kt index 3eae47fd..0568321f 100644 --- a/data/src/main/java/com/depromeet/data/datasource/SeatReviewDataSource.kt +++ b/data/src/main/java/com/depromeet/data/datasource/SeatReviewDataSource.kt @@ -1,8 +1,9 @@ package com.depromeet.data.datasource import com.depromeet.data.model.request.RequestSeatReviewDto +import com.depromeet.data.model.response.seatReview.ResponsePreSignedUrlDto import com.depromeet.data.model.response.seatReview.ResponseSeatBlockDto -import com.depromeet.data.model.response.seatReview.ResponseSeatMaxDto +import com.depromeet.data.model.response.seatReview.ResponseSeatRangeDto import com.depromeet.data.model.response.seatReview.ResponseStadiumNameDto import com.depromeet.data.model.response.seatReview.ResponseStadiumSectionDto @@ -18,12 +19,23 @@ interface SeatReviewDataSource { sectionId: Int, ): List - suspend fun getSeatMaxData( + suspend fun getSeatRangeData( stadiumId: Int, sectionId: Int, - ): ResponseSeatMaxDto + ): List + + suspend fun postImagePreSignedData( + fileExtension: String, + ): ResponsePreSignedUrlDto + + suspend fun putReviewImageData( + presignedUrl: String, + image: ByteArray, + ) suspend fun postSeatReviewData( + blockId: Int, + seatNumber: Int, requestSeatReviewDto: RequestSeatReviewDto, ) } diff --git a/data/src/main/java/com/depromeet/data/datasource/remote/SeatReviewDataSourceImpl.kt b/data/src/main/java/com/depromeet/data/datasource/remote/SeatReviewDataSourceImpl.kt index 32053480..0bd83708 100644 --- a/data/src/main/java/com/depromeet/data/datasource/remote/SeatReviewDataSourceImpl.kt +++ b/data/src/main/java/com/depromeet/data/datasource/remote/SeatReviewDataSourceImpl.kt @@ -1,12 +1,16 @@ package com.depromeet.data.datasource.remote import com.depromeet.data.datasource.SeatReviewDataSource +import com.depromeet.data.model.request.RequestPreSignedUrlDto import com.depromeet.data.model.request.RequestSeatReviewDto +import com.depromeet.data.model.response.seatReview.ResponsePreSignedUrlDto import com.depromeet.data.model.response.seatReview.ResponseSeatBlockDto -import com.depromeet.data.model.response.seatReview.ResponseSeatMaxDto +import com.depromeet.data.model.response.seatReview.ResponseSeatRangeDto import com.depromeet.data.model.response.seatReview.ResponseStadiumNameDto import com.depromeet.data.model.response.seatReview.ResponseStadiumSectionDto import com.depromeet.data.remote.SeatReviewService +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import javax.inject.Inject class SeatReviewDataSourceImpl @Inject constructor( @@ -29,14 +33,38 @@ class SeatReviewDataSourceImpl @Inject constructor( return seatReviewService.getSeatBlock(stadiumId, sectionId) } - override suspend fun getSeatMaxData( + override suspend fun getSeatRangeData( stadiumId: Int, sectionId: Int, - ): ResponseSeatMaxDto { - return seatReviewService.getSeatMax(stadiumId, sectionId) + ): List { + return seatReviewService.getSeatRange(stadiumId, sectionId) } - override suspend fun postSeatReviewData(requestSeatReviewDto: RequestSeatReviewDto) { - return seatReviewService.postSeatReview(requestSeatReviewDto) + override suspend fun postImagePreSignedData( + fileExtension: String, + ): ResponsePreSignedUrlDto { + return seatReviewService.postImagePreSignedUrl( + RequestPreSignedUrlDto(fileExtension), + ) + } + + override suspend fun putReviewImageData( + presignedUrl: String, + image: ByteArray, + ) { + val mediaType = "image/*".toMediaTypeOrNull() + return seatReviewService.putProfileImage(presignedUrl, image.toRequestBody(mediaType)) + } + + override suspend fun postSeatReviewData( + blockId: Int, + seatNumber: Int, + requestSeatReviewDto: RequestSeatReviewDto, + ) { + return seatReviewService.postSeatReview( + blockId, + seatNumber, + requestSeatReviewDto, + ) } } diff --git a/data/src/main/java/com/depromeet/data/model/request/RequestPreSignedUrlDto.kt b/data/src/main/java/com/depromeet/data/model/request/RequestPreSignedUrlDto.kt new file mode 100644 index 00000000..3ab5c0e2 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/request/RequestPreSignedUrlDto.kt @@ -0,0 +1,10 @@ +package com.depromeet.data.model.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPreSignedUrlDto( + @SerialName("fileExtension") + val fileExtension: String, +) diff --git a/data/src/main/java/com/depromeet/data/model/request/RequestSeatReviewDto.kt b/data/src/main/java/com/depromeet/data/model/request/RequestSeatReviewDto.kt index d1cbbbc6..b2ddb10c 100644 --- a/data/src/main/java/com/depromeet/data/model/request/RequestSeatReviewDto.kt +++ b/data/src/main/java/com/depromeet/data/model/request/RequestSeatReviewDto.kt @@ -6,33 +6,21 @@ import kotlinx.serialization.Serializable @Serializable data class RequestSeatReviewDto( - @SerialName("stadiumId") - val stadiumId: Int, - @SerialName("blockId") - val blockId: Int, - @SerialName("rowId") - val rowId: Int, - @SerialName("seatNumber") - val seatNumber: Int, @SerialName("images") val images: List, - @SerialName("date") - val date: String, + @SerialName("dateTime") + val dateTime: String, @SerialName("good") val good: List, @SerialName("bad") val bad: List, @SerialName("content") - val content: String, + val content: String?, ) fun SeatReviewModel.toSeatReview() = RequestSeatReviewDto( - stadiumId = stadiumId, - blockId = blockId, - rowId = rowId, - seatNumber = seatNumber, images = images, - date = date, + dateTime = dateTime, good = good, bad = bad, content = content, diff --git a/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponsePreSignedUrlDto.kt b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponsePreSignedUrlDto.kt new file mode 100644 index 00000000..add748d5 --- /dev/null +++ b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponsePreSignedUrlDto.kt @@ -0,0 +1,16 @@ +package com.depromeet.data.model.response.seatReview + +import com.depromeet.domain.entity.response.seatReview.ResponsePresignedUrlModel +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePreSignedUrlDto( + @SerialName("presignedUrl") + val presignedUrl: String, +) { + + fun toResponsePreSignedUrl(): ResponsePresignedUrlModel { + return ResponsePresignedUrlModel(presignedUrl) + } +} diff --git a/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatMaxDto.kt b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatRangeDto.kt similarity index 59% rename from data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatMaxDto.kt rename to data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatRangeDto.kt index 989609f1..07fabd11 100644 --- a/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatMaxDto.kt +++ b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseSeatRangeDto.kt @@ -1,11 +1,11 @@ package com.depromeet.data.model.response.seatReview -import com.depromeet.domain.entity.response.seatReview.SeatMaxModel +import com.depromeet.domain.entity.response.seatReview.SeatRangeModel import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ResponseSeatMaxDto( +data class ResponseSeatRangeDto( @SerialName("id") val id: Int, @SerialName("code") @@ -19,23 +19,20 @@ data class ResponseSeatMaxDto( val id: Int, @SerialName("number") val number: Int, - @SerialName("minSeatNum") - val minSeatNum: Int, - @SerialName("maxSeatNum") - val maxSeatNum: Int, + @SerialName("seatNumList") + val seatNumList: List, ) { - fun toRowInfo(): SeatMaxModel.RowInfo { - return SeatMaxModel.RowInfo( + fun toRowInfo(): SeatRangeModel.RowInfo { + return SeatRangeModel.RowInfo( id = id, number = number, - minSeatNum = minSeatNum, - maxSeatNum = maxSeatNum, + seatNumList = seatNumList, ) } } - fun toSeatMax(): SeatMaxModel { - return SeatMaxModel( + fun toSeatRange(): SeatRangeModel { + return SeatRangeModel( id = id, code = code, rowInfo = rowInfo.map { it.toRowInfo() }, diff --git a/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseStadiumNameDto.kt b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseStadiumNameDto.kt index 34dfeef3..d363e0b1 100644 --- a/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseStadiumNameDto.kt +++ b/data/src/main/java/com/depromeet/data/model/response/seatReview/ResponseStadiumNameDto.kt @@ -10,11 +10,14 @@ data class ResponseStadiumNameDto( val id: Int, @SerialName("name") val name: String, + @SerialName("isActive") + val isActive: Boolean, ) { fun toStadiumName(): StadiumNameModel { return StadiumNameModel( id = id, name = name, + isActive = isActive, ) } } diff --git a/data/src/main/java/com/depromeet/data/remote/SeatReviewService.kt b/data/src/main/java/com/depromeet/data/remote/SeatReviewService.kt index 43280bb0..26a91bb8 100644 --- a/data/src/main/java/com/depromeet/data/remote/SeatReviewService.kt +++ b/data/src/main/java/com/depromeet/data/remote/SeatReviewService.kt @@ -1,14 +1,19 @@ package com.depromeet.data.remote +import com.depromeet.data.model.request.RequestPreSignedUrlDto import com.depromeet.data.model.request.RequestSeatReviewDto +import com.depromeet.data.model.response.seatReview.ResponsePreSignedUrlDto import com.depromeet.data.model.response.seatReview.ResponseSeatBlockDto -import com.depromeet.data.model.response.seatReview.ResponseSeatMaxDto +import com.depromeet.data.model.response.seatReview.ResponseSeatRangeDto import com.depromeet.data.model.response.seatReview.ResponseStadiumNameDto import com.depromeet.data.model.response.seatReview.ResponseStadiumSectionDto +import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.Url interface SeatReviewService { @GET("/api/v1/stadiums/names") @@ -26,13 +31,26 @@ interface SeatReviewService { ): List @GET("/api/v1/stadiums/{stadiumId}/sections/{sectionId}/blocks/rows") - suspend fun getSeatMax( + suspend fun getSeatRange( @Path("stadiumId") stadiumId: Int, @Path("sectionId") sectionId: Int, - ): ResponseSeatMaxDto + ): List - @POST("/api/v1/reviews") + @POST("/api/v1/reviews/images") + suspend fun postImagePreSignedUrl( + @Body body: RequestPreSignedUrlDto, + ): ResponsePreSignedUrlDto + + @PUT + suspend fun putProfileImage( + @Url preSignedUrl: String, + @Body image: RequestBody, + ) + + @POST("/api/v1/blocks/{blockId}/seats/{seatNumber}/reviews") suspend fun postSeatReview( + @Path("blockId") blockId: Int, + @Path("seatNumber") seatNumber: Int, @Body requestPostSignupDto: RequestSeatReviewDto, ) } diff --git a/data/src/main/java/com/depromeet/data/repository/SeatReviewRepositoryImpl.kt b/data/src/main/java/com/depromeet/data/repository/SeatReviewRepositoryImpl.kt index 6b0ab8a0..2de42db2 100644 --- a/data/src/main/java/com/depromeet/data/repository/SeatReviewRepositoryImpl.kt +++ b/data/src/main/java/com/depromeet/data/repository/SeatReviewRepositoryImpl.kt @@ -3,8 +3,9 @@ package com.depromeet.data.repository import com.depromeet.data.datasource.SeatReviewDataSource import com.depromeet.data.model.request.toSeatReview import com.depromeet.domain.entity.request.SeatReviewModel +import com.depromeet.domain.entity.response.seatReview.ResponsePresignedUrlModel import com.depromeet.domain.entity.response.seatReview.SeatBlockModel -import com.depromeet.domain.entity.response.seatReview.SeatMaxModel +import com.depromeet.domain.entity.response.seatReview.SeatRangeModel import com.depromeet.domain.entity.response.seatReview.StadiumNameModel import com.depromeet.domain.entity.response.seatReview.StadiumSectionModel import com.depromeet.domain.repository.SeatReviewRepository @@ -41,23 +42,47 @@ class SeatReviewRepositoryImpl @Inject constructor( } } - override suspend fun getSeatMax( + override suspend fun getSeatRange( stadiumId: Int, sectionId: Int, - ): Result { + ): Result> { return runCatching { - seatReviewDataSource.getSeatMaxData( - stadiumId, - sectionId, - ).toSeatMax() + val response = seatReviewDataSource.getSeatRangeData(stadiumId, sectionId) + response.map { it.toSeatRange() } + } + } + + override suspend fun postReviewImagePresigned( + fileExtension: String, + ): Result { + return runCatching { + seatReviewDataSource.postImagePreSignedData( + fileExtension, + ).toResponsePreSignedUrl() + } + } + + override suspend fun putImagePreSignedUrl( + presignedUrl: String, + image: ByteArray, + ): Result { + return runCatching { + seatReviewDataSource.putReviewImageData( + presignedUrl, + image, + ) } } override suspend fun postSeatReview( + blockId: Int, + seatNumber: Int, seatReviewInfo: SeatReviewModel, ): Result { return runCatching { seatReviewDataSource.postSeatReviewData( + blockId, + seatNumber, seatReviewInfo.toSeatReview(), ) } diff --git a/domain/src/main/java/com/depromeet/domain/entity/request/RequestUploadUrlModel.kt b/domain/src/main/java/com/depromeet/domain/entity/request/RequestUploadUrlModel.kt new file mode 100644 index 00000000..fcf49fcb --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/request/RequestUploadUrlModel.kt @@ -0,0 +1,5 @@ +package com.depromeet.domain.entity.request + +data class RequestUploadUrlModel( + val fileExtension: String, +) diff --git a/domain/src/main/java/com/depromeet/domain/entity/request/SeatReviewModel.kt b/domain/src/main/java/com/depromeet/domain/entity/request/SeatReviewModel.kt index ad50cdba..c4f7d689 100644 --- a/domain/src/main/java/com/depromeet/domain/entity/request/SeatReviewModel.kt +++ b/domain/src/main/java/com/depromeet/domain/entity/request/SeatReviewModel.kt @@ -1,13 +1,9 @@ package com.depromeet.domain.entity.request data class SeatReviewModel( - val stadiumId: Int, - val blockId: Int, - val rowId: Int, - val seatNumber: Int, val images: List, - val date: String, + val dateTime: String, val good: List, val bad: List, - val content: String, + val content: String?, ) diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/ResponsePresignedUrlModel.kt b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/ResponsePresignedUrlModel.kt new file mode 100644 index 00000000..3a09d548 --- /dev/null +++ b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/ResponsePresignedUrlModel.kt @@ -0,0 +1,5 @@ +package com.depromeet.domain.entity.response.seatReview + +data class ResponsePresignedUrlModel( + val presignedUrl: String = "", +) diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatMaxModel.kt b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatRangeModel.kt similarity index 71% rename from domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatMaxModel.kt rename to domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatRangeModel.kt index ebc467e6..92c47270 100644 --- a/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatMaxModel.kt +++ b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/SeatRangeModel.kt @@ -1,6 +1,6 @@ package com.depromeet.domain.entity.response.seatReview -data class SeatMaxModel( +data class SeatRangeModel( val id: Int, val code: String, val rowInfo: List, @@ -8,7 +8,6 @@ data class SeatMaxModel( data class RowInfo( val id: Int, val number: Int, - val minSeatNum: Int, - val maxSeatNum: Int, + val seatNumList: List, ) } diff --git a/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/StadiumNameModel.kt b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/StadiumNameModel.kt index b821a521..c58e3769 100644 --- a/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/StadiumNameModel.kt +++ b/domain/src/main/java/com/depromeet/domain/entity/response/seatReview/StadiumNameModel.kt @@ -3,4 +3,5 @@ package com.depromeet.domain.entity.response.seatReview data class StadiumNameModel( var id: Int, val name: String, + val isActive: Boolean ) diff --git a/domain/src/main/java/com/depromeet/domain/repository/SeatReviewRepository.kt b/domain/src/main/java/com/depromeet/domain/repository/SeatReviewRepository.kt index d60f11e8..489b974f 100644 --- a/domain/src/main/java/com/depromeet/domain/repository/SeatReviewRepository.kt +++ b/domain/src/main/java/com/depromeet/domain/repository/SeatReviewRepository.kt @@ -1,8 +1,9 @@ package com.depromeet.domain.repository import com.depromeet.domain.entity.request.SeatReviewModel +import com.depromeet.domain.entity.response.seatReview.ResponsePresignedUrlModel import com.depromeet.domain.entity.response.seatReview.SeatBlockModel -import com.depromeet.domain.entity.response.seatReview.SeatMaxModel +import com.depromeet.domain.entity.response.seatReview.SeatRangeModel import com.depromeet.domain.entity.response.seatReview.StadiumNameModel import com.depromeet.domain.entity.response.seatReview.StadiumSectionModel @@ -18,12 +19,23 @@ interface SeatReviewRepository { sectionId: Int, ): Result> - suspend fun getSeatMax( + suspend fun getSeatRange( stadiumId: Int, sectionId: Int, - ): Result + ): Result> + + suspend fun postReviewImagePresigned( + fileExtension: String, + ): Result + + suspend fun putImagePreSignedUrl( + presignedUrl: String, + image: ByteArray, + ): Result suspend fun postSeatReview( + seatId: Int, + seatNumber: Int, seatReviewInfo: SeatReviewModel, ): Result } diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewActivity.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewActivity.kt index 770c7251..ff05d965 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewActivity.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewActivity.kt @@ -1,11 +1,13 @@ package com.depromeet.presentation.seatReview +import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE +import android.webkit.MimeTypeMap import android.widget.FrameLayout import android.widget.ImageView import androidx.activity.viewModels @@ -20,13 +22,19 @@ import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ActivityReviewBinding import com.depromeet.presentation.extension.setOnSingleClickListener import com.depromeet.presentation.extension.toast +import com.depromeet.presentation.home.HomeActivity import com.depromeet.presentation.seatReview.dialog.DatePickerDialog import com.depromeet.presentation.seatReview.dialog.ImageUploadDialog import com.depromeet.presentation.seatReview.dialog.ReviewMySeatDialog import com.depromeet.presentation.seatReview.dialog.SelectSeatDialog import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.FileNotFoundException +import java.io.InputStream import java.text.SimpleDateFormat -import java.util.Calendar import java.util.Locale @AndroidEntryPoint @@ -34,53 +42,34 @@ class ReviewActivity : BaseActivity({ ActivityReviewBinding.inflate(it) }) { companion object { - private const val DATE_FORMAT = "yy.MM.dd" + private const val DATE_FORMAT = "yyyy.MM.dd" + private const val ISO_DATE_FORMAT = "yyyy-MM-dd HH:mm" private const val FRAGMENT_RESULT_KEY = "requestKey" private const val SELECTED_IMAGES = "selected_images" private const val MAX_SELECTED_IMAGES = 3 } private val viewModel by viewModels() - private val selectedImage: List by lazy { - listOf( - binding.ivFirstImage, - binding.ivSecondImage, - binding.ivThirdImage, - ) - } - private val selectedImageLayout: List by lazy { - listOf( - binding.layoutFirstImage, - binding.layoutSecondImage, - binding.layoutThirdImage, - ) - } - private val removeButtons: List by lazy { - listOf( - binding.ivRemoveFirstImage, - binding.ivRemoveSecondImage, - binding.ivRemoveThirdImage, - ) - } + private val selectedImage: List by lazy { listOf(binding.ivFirstImage, binding.ivSecondImage, binding.ivThirdImage) } + private val selectedImageLayout: List by lazy { listOf(binding.layoutFirstImage, binding.layoutSecondImage, binding.layoutThirdImage) } + private val removeButtons: List by lazy { listOf(binding.ivRemoveFirstImage, binding.ivRemoveSecondImage, binding.ivRemoveThirdImage) } private var selectedImageUris: MutableList = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.getStadiumName() observeStadiumName() + observeUploadReview() initDatePickerDialog() initUploadDialog() initSeatReviewDialog() setupFragmentResultListener() setupRemoveButtons() - navigateToReviewDoneActivity() + uploadAllReviewDone() + navigateToHomeActivity() } private fun observeReviewViewModel() { - viewModel.selectedDate.asLiveData().observe(this) { date -> - binding.tvDate.text = date - updateNextButtonState() - } viewModel.selectedImages.asLiveData().observe(this) { image -> updateNextButtonState() } @@ -122,6 +111,31 @@ class ReviewActivity : BaseActivity({ } } + private fun initDatePickerDialog() { + binding.layoutDatePicker.setOnSingleClickListener { + DatePickerDialog().show(supportFragmentManager, "DatePickerDialogTag") + } + viewModel.selectedDate.asLiveData().observe(this) { date -> + val originalFormat = SimpleDateFormat(ISO_DATE_FORMAT, Locale.getDefault()) + val targetFormat = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()) + val dateOnly = originalFormat.parse(date)?.let { targetFormat.format(it) } + binding.tvDate.text = dateOnly ?: date.substring(0, 10) + updateNextButtonState() + } + } + + private fun initUploadDialog() { + binding.btnAddImage.setOnClickListener { + ImageUploadDialog().show(supportFragmentManager, "ImageUploadDialog") + } + } + + private fun navigateToHomeActivity() { + binding.btnBack.setOnSingleClickListener { + Intent(this, HomeActivity::class.java).apply { startActivity(this) } + } + } + private fun observeStadiumName() { viewModel.stadiumNameState.asLiveData().observe(this) { state -> when (state) { @@ -130,7 +144,7 @@ class ReviewActivity : BaseActivity({ if (firstStadium != null) { binding.tvStadiumName.text = firstStadium.name viewModel.getStadiumSection(firstStadium.id) - viewModel.setSelectedStadiumId(firstStadium.id) + viewModel.updateSelectedStadiumId(firstStadium.id) } observeReviewViewModel() } @@ -142,25 +156,6 @@ class ReviewActivity : BaseActivity({ } } - private fun initDatePickerDialog() { - val today = Calendar.getInstance() - val dateFormat = SimpleDateFormat(DATE_FORMAT, Locale.getDefault()) - with(binding) { - tvDate.text = dateFormat.format(today.time) - layoutDatePicker.setOnSingleClickListener { - val datePickerDialogFragment = DatePickerDialog() - datePickerDialogFragment.show(supportFragmentManager, datePickerDialogFragment.tag) - } - } - } - - private fun initUploadDialog() { - binding.btnAddImage.setOnClickListener { - val uploadDialogFragment = ImageUploadDialog() - uploadDialogFragment.show(supportFragmentManager, uploadDialogFragment.tag) - } - } - private fun initSeatReviewDialog() { binding.layoutReviewMySeat.setOnSingleClickListener { ReviewMySeatDialog().show(supportFragmentManager, "ReviewMySeatDialog") @@ -206,7 +201,7 @@ class ReviewActivity : BaseActivity({ val block = viewModel.selectedBlock.value val column = viewModel.selectedColumn.value val number = viewModel.selectedNumber.value - if (listOf(seatName, block, column, number).any { it.isNullOrEmpty()}) { + if (listOf(seatName, block, column, number).any { it.isNullOrEmpty() }) { binding.layoutSeatInfo.visibility = INVISIBLE } else { binding.layoutSeatInfo.visibility = VISIBLE @@ -229,7 +224,6 @@ class ReviewActivity : BaseActivity({ } } for (index in selectedImageUris.size until selectedImage.size) { - val image = selectedImage[index] val layout = selectedImageLayout[index] layout.isVisible = false removeButtons[index].isVisible = false @@ -263,9 +257,98 @@ class ReviewActivity : BaseActivity({ } } - private fun navigateToReviewDoneActivity() { + private fun getFileExtension(context: Context, uri: Uri): String { + val mimeType = context.contentResolver.getType(uri) + return mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" + } + + private fun getFileInputStream(context: Context, uri: Uri): InputStream? { + return try { + context.contentResolver.openInputStream(uri) + } catch (e: FileNotFoundException) { + e.printStackTrace() + null + } + } + + private fun readImageData(context: Context, uri: Uri): ByteArray? { + val inputStream = getFileInputStream(context, uri) + return inputStream?.use { it.readBytes() } + } + + private fun observePreSignedUrl(deferred: CompletableDeferred, imageData: ByteArray) { + viewModel.getPreSignedUrl.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + val preSignedUrl = state.data.presignedUrl + if (viewModel.preSignedUrlImages.value.contains(preSignedUrl).not()) { + viewModel.setPreSignedUrlImages(preSignedUrl) + viewModel.uploadImageToPreSignedUrl(preSignedUrl, imageData).invokeOnCompletion { + if (it == null) { + deferred.complete(true) + } else { + deferred.complete(false) + } + } + } else { + deferred.complete(false) + } + } + is UiState.Failure -> { + toast("Presigned URL 요청 실패") + deferred.complete(false) + } + else -> {} + } + } + } + + private fun observeUploadReview() { + viewModel.postReviewState.asLiveData().observe(this) { state -> + when (state) { + is UiState.Success -> { + uploadAllReviewDone() + Intent(this, ReviewDoneActivity::class.java).apply { + startActivity(this) + } + } + is UiState.Failure -> { + toast("리뷰 등록 실패: $state") + } + else -> {} + } + } + } + + fun uploadAllReviewDone() { binding.tvUploadBtn.setOnSingleClickListener { - Intent(this, ReviewDoneActivity::class.java).apply { startActivity(this) } + val uploadResults = mutableListOf>() + val uniqueImageUris = selectedImageUris.distinct() // 중복된 URI를 제거합니다. + + uniqueImageUris.forEach { imageUriString -> + val imageUri = Uri.parse(imageUriString) + val fileExtension = getFileExtension(this, imageUri) + val imageData = readImageData(this, imageUri) + + if (imageData != null) { + val deferred = CompletableDeferred() + uploadResults.add(deferred) + viewModel.requestPreSignedUrl(fileExtension) + observePreSignedUrl(deferred, imageData) + } else { + toast("파일을 읽을 수 없습니다.") + } + } + + CoroutineScope(Dispatchers.Main).launch { + val allUploadsCompleted = uploadResults.all { it.await() } + if (allUploadsCompleted) { + viewModel.postSeatReview() + } else { + toast("이미지 업로드에 실패했습니다.") + } + } } } + } diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewDoneActivity.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewDoneActivity.kt index eb507a16..c09dde9f 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewDoneActivity.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewDoneActivity.kt @@ -21,6 +21,7 @@ class ReviewDoneActivity : BaseActivity({ lifecycleScope.launch { delay(2000L) finish() + // TODO : 기록된 모든 정보 삭제 } } } diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewViewModel.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewViewModel.kt index 12a431f2..bbf7e349 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewViewModel.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/ReviewViewModel.kt @@ -1,20 +1,26 @@ package com.depromeet.presentation.seatReview +import android.net.Uri +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.depromeet.core.state.UiState +import com.depromeet.domain.entity.request.SeatReviewModel +import com.depromeet.domain.entity.response.seatReview.ResponsePresignedUrlModel import com.depromeet.domain.entity.response.seatReview.SeatBlockModel +import com.depromeet.domain.entity.response.seatReview.SeatRangeModel import com.depromeet.domain.entity.response.seatReview.StadiumNameModel import com.depromeet.domain.entity.response.seatReview.StadiumSectionModel import com.depromeet.domain.repository.SeatReviewRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import retrofit2.HttpException import timber.log.Timber -import java.time.LocalDate +import java.time.LocalDateTime import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -23,16 +29,21 @@ class ReviewViewModel @Inject constructor( private val seatReviewRepository: SeatReviewRepository, ) : ViewModel() { - // 날짜 및 이미지 - private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yy.MM.dd") - private val currentDate: String = LocalDate.now().format(dateFormatter) + // 날짜 + private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + private val currentDate: String = LocalDateTime.now().format(dateFormatter) private val _selectedDate = MutableStateFlow(currentDate) val selectedDate: StateFlow = _selectedDate.asStateFlow() + // 이미지 + private val _selectedImages = MutableStateFlow>(emptyList()) val selectedImages: StateFlow> = _selectedImages.asStateFlow() + private val _preSignedUrlImages = MutableStateFlow>(emptyList()) + val preSignedUrlImages: StateFlow> = _preSignedUrlImages.asStateFlow() + // 시야 후기 private val _reviewCount = MutableStateFlow(0) @@ -45,7 +56,6 @@ class ReviewViewModel @Inject constructor( val selectedBadReview: StateFlow> = _selectedBadReview.asStateFlow() private val _detailReviewText = MutableStateFlow("") - val detailReviewText: StateFlow = _detailReviewText.asStateFlow() // 좌석 선택 @@ -69,16 +79,40 @@ class ReviewViewModel @Inject constructor( private val _selectedStadiumId = MutableStateFlow(0) val selectedStadiumId: StateFlow = _selectedStadiumId.asStateFlow() + private val _selectedSectionId = MutableStateFlow(0) + val selectedSectionId: StateFlow = _selectedSectionId.asStateFlow() + + private val _selectedBlockId = MutableStateFlow(0) + val selectedBlockId: StateFlow = _selectedBlockId.asStateFlow() + private val _stadiumSectionState = MutableStateFlow>(UiState.Empty) val stadiumSectionState: StateFlow> = _stadiumSectionState private val _seatBlockState = MutableStateFlow>>(UiState.Empty) val seatBlockState: StateFlow>> = _seatBlockState - fun setSelectedStadiumId(stadiumId: Int) { + private val _seatRangeState = MutableStateFlow>>(UiState.Empty) + val seatRangeState: StateFlow>> = _seatRangeState + + private val _getPreSignedUrl = + MutableStateFlow>(UiState.Loading) + val getPreSignedUrl = _getPreSignedUrl.asStateFlow() + + private val _postReviewState = MutableStateFlow>(UiState.Empty) + val postReviewState: StateFlow> = _postReviewState.asStateFlow() + + fun updateSelectedStadiumId(stadiumId: Int) { _selectedStadiumId.value = stadiumId } + fun updateSelectedSectionId(sectionId: Int) { + _selectedSectionId.value = sectionId + } + + fun updateSelectedBlockId(blockId: Int) { + _selectedBlockId.value = blockId + } + fun updateSelectedDate(date: String) { _selectedDate.value = date } @@ -87,6 +121,12 @@ class ReviewViewModel @Inject constructor( _selectedImages.value = image } + fun setPreSignedUrlImages(image: String) { + val newImage = removeQueryParameters(image) + val currentImages = _preSignedUrlImages.value.toMutableSet() + currentImages.add(newImage) + _preSignedUrlImages.value = currentImages.toList() + } fun setReviewCount(count: Int) { _reviewCount.value = count } @@ -135,6 +175,7 @@ class ReviewViewModel @Inject constructor( Timber.e("GET NAME FAILURE : ${t.message}", t) if (t is HttpException) { Timber.e("HTTP error code: ${t.code()}") + Timber.e("HTTP error response: ${t.response()?.errorBody()?.string()}") _stadiumNameState.value = UiState.Failure(t.code().toString()) } else { Timber.e("General error: ${t.message ?: "Unknown error"}") @@ -143,12 +184,37 @@ class ReviewViewModel @Inject constructor( } } + fun getStadiumSection(stadiumId: Int) { + viewModelScope.launch { + _stadiumSectionState.value = UiState.Loading + seatReviewRepository.getStadiumSection( + stadiumId, + ).onSuccess { section -> + Timber.d("GET SECTION SUCCESS : $section") + if (section == null) { + _stadiumSectionState.value = UiState.Empty + return@launch + } + _stadiumSectionState.value = when { + section.sectionList.isEmpty() -> UiState.Empty + else -> UiState.Success(section) + } + } + .onFailure { t -> + if (t is HttpException) { + Timber.e("GET SECTION FAILURE : $t") + _stadiumSectionState.value = UiState.Failure(t.code().toString()) + } + } + } + } + fun getSeatBlock(stadiumId: Int, sectionId: Int) { viewModelScope.launch { _seatBlockState.value = UiState.Loading seatReviewRepository.getSeatBlock(stadiumId, sectionId) .onSuccess { blocks -> - Timber.e("GET BLOCK FAILURE : $blocks") + Timber.d("GET BLOCK SUCCESS : $blocks") if (blocks.isEmpty()) { _seatBlockState.value = UiState.Empty } else { @@ -164,26 +230,144 @@ class ReviewViewModel @Inject constructor( } } - fun getStadiumSection(stadiumId: Int) { + fun getSeatRange(stadiumId: Int, sectionId: Int) { viewModelScope.launch { - _stadiumSectionState.value = UiState.Loading - seatReviewRepository.getStadiumSection( + _seatRangeState.value = UiState.Loading + seatReviewRepository.getSeatRange( stadiumId, - ).onSuccess { section -> - Timber.d("GET SECTION SUCCESS : $section") - if (section == null) { - _stadiumSectionState.value = UiState.Empty - return@launch + sectionId, + ).onSuccess { range -> + Timber.d("GET RANGE SUCCESS : $range") + if (range.isEmpty()) { + _seatRangeState.value = UiState.Empty + } else { + _seatRangeState.value = UiState.Success(range) } - _stadiumSectionState.value = when { - section.sectionList.isEmpty() -> UiState.Empty - else -> UiState.Success(section) + }.onFailure { t -> + if (t is HttpException) { + Timber.e("GET RANGE FAILURE : $t") + _seatRangeState.value = UiState.Failure(t.code().toString()) + } + } + } + } + + // presigned URL 요청 + fun requestPreSignedUrl(fileExtension: String) { + viewModelScope.launch { + _getPreSignedUrl.value = UiState.Loading + seatReviewRepository.postReviewImagePresigned(fileExtension) + .onSuccess { response -> + Timber.d("REQUEST PRESIGNED URL SUCCESS : $response") + _getPreSignedUrl.value = UiState.Success(response) + } + .onFailure { t -> + Timber.e("REQUEST PRESIGNED URL FAILURE : $t") + val errorMessage = if (t is HttpException) { + val errorBody = t.response()?.errorBody()?.string() + "HTTP Error ${t.code()}: ${errorBody ?: "Unknown error"}" + } else { + t.message ?: "Unknown error" + } + + _getPreSignedUrl.value = UiState.Failure(errorMessage) + } + } + } + + // 이미지 업로드 + fun uploadImageToPreSignedUrl( + presignedUrl: String, + image: ByteArray, + ): CompletableDeferred { + val deferred = CompletableDeferred() + viewModelScope.launch { + val result = seatReviewRepository.putImagePreSignedUrl(presignedUrl, image) + result.onSuccess { + Timber.d("UPLOAD IMAGE SUCCESS") + deferred.complete(true) + }.onFailure { t -> + Timber.e("UPLOAD IMAGE FAILURE : $t") + if (t is HttpException) { + Timber.e("HTTP error code: ${t.code()}") + Timber.e("HTTP error response: ${t.response()?.errorBody()?.string()}") + } else { + Timber.e("General error: ${t.message ?: "Unknown error"}") } + deferred.complete(false) + } + } + return deferred + } + + private fun removeQueryParameters(url: String): String { + val uri = Uri.parse(url) + return uri.buildUpon().clearQuery().build().toString() + } + + fun postSeatReview() { + viewModelScope.launch { + val seatReviewModel = SeatReviewModel( + images = _preSignedUrlImages.value, + dateTime = _selectedDate.value, + good = _selectedGoodReview.value, + bad = _selectedBadReview.value, + content = _detailReviewText.value, + ) + + // 추후 Timber 삭제 예정 + Timber.d("Selected Images: ${_preSignedUrlImages.value}") + Timber.d("Selected Date: ${_selectedDate.value}") + Timber.d("Good Review: ${_selectedGoodReview.value}") + Timber.d("Bad Review: ${_selectedBadReview.value}") + Timber.d("Detail Review Text: ${_detailReviewText.value}") + Timber.d("Selected Stadium ID: ${_selectedStadiumId.value}") + Timber.d("Selected Block ID: ${_selectedBlockId.value}") + Timber.d("Selected seatNumber: ${selectedNumber.value}") + + val selectedNumberValue = selectedNumber.value + if (selectedNumberValue.isNullOrEmpty()) { + Timber.e("Selected Number is null or empty") + _postReviewState.value = UiState.Failure("Selected Number is required") + return@launch } + + val selectedNumberInt = try { + selectedNumberValue.toInt() + } catch (e: NumberFormatException) { + Timber.e("Selected Number is not a valid integer: $selectedNumberValue") + _postReviewState.value = UiState.Failure("Selected Number is not a valid integer") + return@launch + } + + Timber.d("Selected Number: $selectedNumberInt") + + _postReviewState.value = UiState.Loading + seatReviewRepository.postSeatReview( + _selectedBlockId.value, + selectedNumberInt, + seatReviewModel, + ) + .onSuccess { + _postReviewState.value = UiState.Success(Unit) + Timber.d("POST REVIEW SUCCESS") + } .onFailure { t -> + Timber.e("POST REVIEW FAILURE : $t") + if (t is HttpException) { - Timber.e("GET SECTION FAILURE : $t") - _stadiumSectionState.value = UiState.Failure(t.code().toString()) + val errorBody = t.response()?.errorBody()?.string() + Timber.e("Error Body: $errorBody") + + val errorMessage = when { + t.code() == 403 -> "권한이 없습니다. 요청을 확인하고 다시 시도해 주세요." + errorBody.isNullOrEmpty() -> "HTTP ${t.code()} 에러 발생: ${t.message()}" + else -> errorBody + } + + _postReviewState.value = UiState.Failure(errorMessage) + } else { + _postReviewState.value = UiState.Failure(t.message ?: "알 수 없는 오류") } } } diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/adapter/SelectSeatAdapter.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/adapter/SelectSeatAdapter.kt index 988bb840..dfb29492 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/adapter/SelectSeatAdapter.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/adapter/SelectSeatAdapter.kt @@ -12,9 +12,9 @@ import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ItemSelectSeatBinding import com.depromeet.presentation.util.ItemDiffCallback -class SectionListAdapter( +class SelectSeatAdapter( private val onItemClick: (Int, Int) -> Unit -) : ListAdapter(diffUtil) { +) : ListAdapter(diffUtil) { private var selectedPosition = RecyclerView.NO_POSITION diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ImageUploadDialog.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ImageUploadDialog.kt index d3db5e64..d2bc31ac 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ImageUploadDialog.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ImageUploadDialog.kt @@ -12,7 +12,6 @@ import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.view.View -import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -24,6 +23,7 @@ import com.depromeet.presentation.R import com.depromeet.presentation.databinding.FragmentUploadBottomSheetBinding import com.depromeet.presentation.extension.setOnSingleClickListener import com.depromeet.presentation.extension.toUri +import com.depromeet.presentation.extension.toast import com.depromeet.presentation.home.UploadErrorDialog import dagger.hilt.android.AndroidEntryPoint @@ -55,18 +55,16 @@ class ImageUploadDialog : BindingBottomSheetDialog 15) { val fragment = UploadErrorDialog( getString(R.string.upload_error_capacity_description), diff --git a/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ReviewMySeatDialog.kt b/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ReviewMySeatDialog.kt index 8c4a3228..258d041c 100644 --- a/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ReviewMySeatDialog.kt +++ b/presentation/src/main/java/com/depromeet/presentation/seatReview/dialog/ReviewMySeatDialog.kt @@ -58,10 +58,16 @@ class ReviewMySeatDialog : BindingBottomSheetDialog when (state) { is UiState.Success -> { adapter.submitList(state.data.sectionList) - // TODO : SVG IMAGE LOAD binding.ivSeatAgain.load(state.data.seatChart) } - is UiState.Failure -> { toast("이미지 오류") } + is UiState.Failure -> { + toast("오류가 발생했습니다") + } + is UiState.Loading -> {} is UiState.Empty -> { toast("오류가 발생했습니다") } + else -> {} } } } - private fun observeSeatBLock() { + private fun observeSeatBlock() { viewModel.seatBlockState.asLiveData().observe(this) { state -> when (state) { - is UiState.Success -> { observeSuccessSeatBlock(state.data) } - is UiState.Failure -> { toast("오류가 발생했습니다") } + is UiState.Success -> { + observeSuccessSeatBlock(state.data) + } + + is UiState.Failure -> { + toast("오류가 발생했습니다") + } + is UiState.Loading -> {} is UiState.Empty -> {} else -> {} @@ -90,14 +92,167 @@ class SelectSeatDialog : BindingBottomSheetDialog + when (state) { + is UiState.Success -> { + state.data.forEach { range -> + updateIsExistedColumnUI(range.rowInfo) + updateColumnNumberUI(range) + } + } + + is UiState.Failure -> { + toast("오류가 발생했습니다") + } + + is UiState.Loading -> { + } + + is UiState.Empty -> { + } + + else -> {} + } + } + } + + private fun setupEditTextListeners() { + binding.etColumn.addTextChangedListener { text: Editable? -> + val newColumn = text.toString() + viewModel.setSelectedColumn(newColumn) + viewModel.seatRangeState.value?.let { state -> + if (state is UiState.Success) { + state.data.forEach { range -> + updateColumnNumberUI(range) + } + } + } + } + + binding.etNumber.addTextChangedListener { text: Editable? -> + val newNumber = text.toString() + viewModel.setSelectedNumber(newNumber) + viewModel.seatRangeState.value?.let { state -> + if (state is UiState.Success) { + state.data.forEach { range -> + updateColumnNumberUI(range) + } + } + } + } + } + + // TODO : 추후 코드 개선 예정 (if else 이슈 ㅠㅠ) + private fun updateColumnNumberUI(range: SeatRangeModel) { + if (viewModel.selectedColumn.value.isEmpty() && viewModel.selectedNumber.value.isEmpty()) { + binding.etColumn.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + binding.etNumber.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + binding.tvNoneColumnWarning.visibility = GONE + } + if (range.code == viewModel.selectedBlock.value) { + val matchingRowInfo = + range.rowInfo.find { it.number.toString() == viewModel.selectedColumn.value } + if (matchingRowInfo == null && viewModel.selectedColumn.value.isNotEmpty() && viewModel.selectedColumn.value.isNotEmpty()) { + with(binding) { + etColumn.setBackgroundResource(R.drawable.rect_gray50_fill_red1_line_12) + etNumber.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + tvNoneColumnWarning.text = "존재하지 않는 열이에요" + tvNoneColumnWarning.visibility = VISIBLE + binding.tvCompleteBtn.setBackgroundResource(R.drawable.rect_gray200_fill_6) + binding.tvCompleteBtn.setTextColor( + ContextCompat.getColor( + requireContext(), + android.R.color.white, + ), + ) + binding.tvCompleteBtn.isEnabled = false + } + if (viewModel.selectedNumber.value.isNotEmpty()) { + // 열과 번호 모두 오류인 경우 + with(binding) { + etColumn.setBackgroundResource(R.drawable.rect_gray50_fill_red1_line_12) + etNumber.setBackgroundResource(R.drawable.rect_gray50_fill_red1_line_12) + tvNoneColumnWarning.text = "존재하지 않는 열과 번이에요" + binding.tvCompleteBtn.setBackgroundResource(R.drawable.rect_gray200_fill_6) + binding.tvCompleteBtn.setTextColor( + ContextCompat.getColor( + requireContext(), + android.R.color.white, + ), + ) + binding.tvCompleteBtn.isEnabled = false + } + } + } else if (matchingRowInfo != null && viewModel.selectedNumber.value.isNotEmpty() && viewModel.selectedNumber.value.isNotBlank()) { + if (!matchingRowInfo.seatNumList.contains(viewModel.selectedNumber.value.toInt())) { + with(binding) { + etColumn.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + etNumber.setBackgroundResource(R.drawable.rect_gray50_fill_red1_line_12) + tvNoneColumnWarning.text = "존재하지 않는 번이에요" + tvNoneColumnWarning.visibility = VISIBLE + binding.tvCompleteBtn.setBackgroundResource(R.drawable.rect_gray200_fill_6) + binding.tvCompleteBtn.setTextColor( + ContextCompat.getColor( + requireContext(), + android.R.color.white, + ), + ) + binding.tvCompleteBtn.isEnabled = false + } + } else { + with(binding) { + etColumn.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + etNumber.setBackgroundResource(R.drawable.rect_gray50_fill_gray200_line_12) + tvNoneColumnWarning.visibility = GONE + binding.tvCompleteBtn.isEnabled = true + binding.tvCompleteBtn.setBackgroundResource(R.drawable.rect_gray900_fill_6) + binding.tvCompleteBtn.setTextColor( + ContextCompat.getColor( + requireContext(), + android.R.color.white, + ), + ) + } + } + } + } + } + + private fun updateIsExistedColumnUI(rowInfoList: List) { + if (rowInfoList.size == 1) { + with(binding) { + etColumn.visibility = INVISIBLE + tvColumn.visibility = INVISIBLE + etNumber.visibility = INVISIBLE + etNonColumnNumber.visibility = VISIBLE + } + } else { + with(binding) { + etColumn.visibility = VISIBLE + tvColumn.visibility = VISIBLE + etNumber.visibility = VISIBLE + etNonColumnNumber.visibility = INVISIBLE + } + } + } + private fun observeSuccessSeatBlock(blockItems: List) { - val blockCodes = blockItems.map { it.code } - val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, blockCodes) + val blockCodes = mutableListOf().apply { + add("") + addAll(blockItems.map { it.code }) + } + + val blockCodeToIdMap = blockItems.associate { it.code to it.id } + + val adapter = + ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, blockCodes) adapter.setDropDownViewResource(R.layout.custom_spinner_dropdown_item) with(binding.spinnerBlock) { this.adapter = adapter - this.setSelection(Adapter.NO_SELECTION, false) + this.setSelection(0, false) + this.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected( parent: AdapterView<*>?, @@ -105,38 +260,41 @@ class SelectSeatDialog : BindingBottomSheetDialog?) { viewModel.setSelectedBlock("") + viewModel.updateSelectedBlockId(0) } } } } private fun setupSectionRecyclerView() { - adapter = SectionListAdapter { position, sectionId -> + adapter = SelectSeatAdapter { position, sectionId -> val selectedSeatInfo = adapter.currentList[position] adapter.setItemSelected(position) viewModel.setSelectedSeatZone(selectedSeatInfo.name) viewModel.getSeatBlock(viewModel.selectedStadiumId.value, sectionId) + viewModel.updateSelectedSectionId(sectionId) updateNextBtnState() } binding.rvSelectSeatZone.adapter = adapter } - private fun setupEditTextListeners() { - binding.etColumn.addTextChangedListener { text: Editable? -> - viewModel.setSelectedColumn(text.toString()) - } - - binding.etNumber.addTextChangedListener { text: Editable? -> - viewModel.setSelectedNumber(text.toString()) - } - } - private fun setupTransactionSelectSeat() { with(binding) { layoutSeatAgain.setOnSingleClickListener { @@ -168,22 +326,6 @@ class SelectSeatDialog : BindingBottomSheetDialog - + tools:text="2024.06.30" />