diff --git a/core/designsystem/src/main/java/com/dpm/designsystem/SpotSnackBar.kt b/core/designsystem/src/main/java/com/dpm/designsystem/SpotSnackBar.kt index 1f857428..84a5feda 100644 --- a/core/designsystem/src/main/java/com/dpm/designsystem/SpotSnackBar.kt +++ b/core/designsystem/src/main/java/com/dpm/designsystem/SpotSnackBar.kt @@ -2,20 +2,26 @@ package com.dpm.designsystem import android.graphics.Paint import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import androidx.core.content.ContextCompat +import com.depromeet.designsystem.R import com.depromeet.designsystem.databinding.SpotSnackbarBinding +import com.dpm.designsystem.extension.dpToPx import com.google.android.material.snackbar.Snackbar + class SpotSnackBar( view: View, + private val snackBarBackground: Int, private val message: String, + private val marginBottom: Int, private val endMessage: String, private val onClick: () -> Unit ) { companion object { - fun make(view: View, message: String = "", endMessage: String = "", onClick: () -> Unit) = - SpotSnackBar(view, message, endMessage, onClick) + fun make(view: View, background: Int = R.drawable.rect_transfer_black_03_fill_60, marginBottom: Int = 0, message: String = "", endMessage: String = "", onClick: () -> Unit) = + SpotSnackBar(view = view, snackBarBackground = background, marginBottom = marginBottom, message= message, endMessage = endMessage, onClick = onClick) } private val context = view.context @@ -32,14 +38,24 @@ class SpotSnackBar( private fun initView() { with(snackbarLayout) { removeAllViews() - setPadding(0, 0, 0, 0) + setPadding(0, 0, 0, marginBottom.dpToPx(context)) setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent)) addView(snackbarBinding.root, 0) + + setOnTouchListener { v, event -> + if (event.action == MotionEvent.ACTION_UP) { + v.performClick() + true + } else { + false + } + } } with(snackbarBinding) { tvDescription.text = message tvTrigger.paintFlags = Paint.UNDERLINE_TEXT_FLAG tvTrigger.text = endMessage + clContainer.setBackgroundResource(snackBarBackground) } } diff --git a/core/designsystem/src/main/res/drawable/rect_body_subtitle_fill_60.xml b/core/designsystem/src/main/res/drawable/rect_body_subtitle_fill_60.xml new file mode 100644 index 00000000..155fb747 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/rect_body_subtitle_fill_60.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/core/designsystem/src/main/res/layout/spot_snackbar.xml b/core/designsystem/src/main/res/layout/spot_snackbar.xml index 4781c3b9..a0011f5b 100644 --- a/core/designsystem/src/main/res/layout/spot_snackbar.xml +++ b/core/designsystem/src/main/res/layout/spot_snackbar.xml @@ -4,34 +4,44 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/rect_transfer_black_03_fill_60"> + android:background="@android:color/transparent"> - + tools:background="@drawable/rect_transfer_black_03_fill_60"> - + + + + \ No newline at end of file diff --git a/data/src/main/java/com/dpm/data/datasource/ViewfinderDataSource.kt b/data/src/main/java/com/dpm/data/datasource/ViewfinderDataSource.kt index 1d804959..8b3c5039 100644 --- a/data/src/main/java/com/dpm/data/datasource/ViewfinderDataSource.kt +++ b/data/src/main/java/com/dpm/data/datasource/ViewfinderDataSource.kt @@ -15,4 +15,6 @@ interface ViewfinderDataSource { queryParam: RequestBlockReviewQueryDto ): ResponseBlockReviewDto suspend fun getBlockRow(stadiumId: Int, blockCode: String): ResponseBlockRowDto + suspend fun updateScrap(reviewId: Int) + suspend fun updateLike(reviewId: Int) } \ No newline at end of file diff --git a/data/src/main/java/com/dpm/data/datasource/remote/ViewfinderDataSourceImpl.kt b/data/src/main/java/com/dpm/data/datasource/remote/ViewfinderDataSourceImpl.kt index d4d35dc4..dbf295e4 100644 --- a/data/src/main/java/com/dpm/data/datasource/remote/ViewfinderDataSourceImpl.kt +++ b/data/src/main/java/com/dpm/data/datasource/remote/ViewfinderDataSourceImpl.kt @@ -41,4 +41,12 @@ class ViewfinderDataSourceImpl @Inject constructor( override suspend fun getBlockRow(stadiumId: Int, blockCode: String): ResponseBlockRowDto { return viewfinderService.getBlockRow(stadiumId, blockCode) } + + override suspend fun updateScrap(reviewId: Int) { + viewfinderService.postScrap(reviewId) + } + + override suspend fun updateLike(reviewId: Int) { + viewfinderService.postLike(reviewId) + } } \ No newline at end of file diff --git a/data/src/main/java/com/dpm/data/model/response/viewfinder/ResponseBlockReviewDto.kt b/data/src/main/java/com/dpm/data/model/response/viewfinder/ResponseBlockReviewDto.kt index 537e237f..42993643 100644 --- a/data/src/main/java/com/dpm/data/model/response/viewfinder/ResponseBlockReviewDto.kt +++ b/data/src/main/java/com/dpm/data/model/response/viewfinder/ResponseBlockReviewDto.kt @@ -13,7 +13,7 @@ data class ResponseBlockReviewDto( @SerialName("reviews") val reviews: List, @SerialName("topReviewImages") - val topReviewImages: List, + val topReviewImages: List, @SerialName("totalElements") val totalElements: Long, @SerialName("nextCursor") @@ -56,7 +56,7 @@ data class ResponseBlockReviewDto( @SerialName("block") val block: ResponseReviewBlockDto, @SerialName("row") - val row: ResponseReviewRowDto, + val row: ResponseReviewRowDto?, @SerialName("seat") val seat: ResponseReviewSeatDto?, @SerialName("dateTime") @@ -67,8 +67,13 @@ data class ResponseBlockReviewDto( val images: List, @SerialName("keywords") val keywords: List, - - ) { + @SerialName("likesCount") + val likesCount: Long, + @SerialName("scrapsCount") + val scrapsCount: Long, + @SerialName("reviewType") + val reviewType: String?, + ) { @Serializable data class ResponseReviewImageDto( @SerialName("id") @@ -126,9 +131,9 @@ data class ResponseBlockReviewDto( @Serializable data class ResponseReviewRowDto( @SerialName("id") - val id: Int, + val id: Int?, @SerialName("number") - val number: Int, + val number: Int?, ) @Serializable @@ -140,20 +145,6 @@ data class ResponseBlockReviewDto( ) } - @Serializable - data class ResponseTopReviewImagesDto( - @SerialName("url") - val url: String, - @SerialName("reviewId") - val reviewId: Int, - @SerialName("blockCode") - val blockCode: String, - @SerialName("rowNumber") - val rowNumber: Int, - @SerialName("seatNumber") - val seatNumber: Int?, - ) - @Serializable data class ResponseReviewFilterDto( @SerialName("rowNumber") @@ -171,7 +162,7 @@ fun ResponseBlockReviewDto.toBlockReviewResponse() = ResponseBlockReview( location = location?.toLocationResponse() ?: ResponseBlockReview.ResponseLocation(), keywords = keywords.map { it.toKeywordResponse() }, reviews = reviews.map { it.toReviewResponse() }, - topReviewImages = topReviewImages.map { it.toTopReviewImagesResponse() }, + topReviewImages = topReviewImages.map { it.toReviewResponse() }, totalElements = totalElements, nextCursor = nextCursor ?: "", hasNext = hasNext, @@ -185,15 +176,6 @@ fun ResponseBlockReviewDto.ResponseKeywordDto.toKeywordResponse() = isPositive = isPositive ) -fun ResponseBlockReviewDto.ResponseTopReviewImagesDto.toTopReviewImagesResponse() = - ResponseBlockReview.ResponseTopReviewImages( - url = url, - reviewId = reviewId, - blockCode = blockCode, - rowNumber = rowNumber, - seatNumber = seatNumber ?: 0 - ) - fun ResponseBlockReviewDto.ResponseReviewFilterDto.toReviewFilterResponse() = ResponseBlockReview.ResponseReviewFilter( rowNumber = rowNumber ?: 0, @@ -209,12 +191,18 @@ fun ResponseBlockReviewDto.ResponseReviewDto.toReviewResponse() = stadium = stadium.toReviewStadiumResponse(), section = section.toReviewSectionResponse(), block = block.toReviewBlockResponse(), - row = row.toReviewRowResponse(), - seat = seat?.toReviewSeatResponse() ?: ResponseBlockReview.ResponseReview.ResponseReviewSeat(), + row = row?.toReviewRowResponse() ?: ResponseBlockReview.ResponseReview.ResponseReviewRow(), + seat = seat?.toReviewSeatResponse() + ?: ResponseBlockReview.ResponseReview.ResponseReviewSeat(), dateTime = dateTime, content = content ?: "", images = images.map { it.toReviewImageResponse() }, - keywords = keywords.map { it.toReviewKeywordResponse() } + keywords = keywords.map { it.toReviewKeywordResponse() }, + isLike = false, + isScrap = false, + likesCount = likesCount, + scrapsCount = scrapsCount, + reviewType = reviewType ?: "" ) fun ResponseBlockReviewDto.ResponseLocationDto.toLocationResponse() = @@ -263,8 +251,8 @@ fun ResponseBlockReviewDto.ResponseReviewDto.ResponseReviewBlockDto.toReviewBloc fun ResponseBlockReviewDto.ResponseReviewDto.ResponseReviewRowDto.toReviewRowResponse() = ResponseBlockReview.ResponseReview.ResponseReviewRow( - id = id, - number = number + id = id ?: 0, + number = number ?: 0 ) fun ResponseBlockReviewDto.ResponseReviewDto.ResponseReviewSeatDto.toReviewSeatResponse() = diff --git a/data/src/main/java/com/dpm/data/preference/SharedPreference.kt b/data/src/main/java/com/dpm/data/preference/SharedPreference.kt index b9567759..61c37b9d 100644 --- a/data/src/main/java/com/dpm/data/preference/SharedPreference.kt +++ b/data/src/main/java/com/dpm/data/preference/SharedPreference.kt @@ -47,7 +47,7 @@ class DefaultSharedPreference @Inject constructor( } override var teamName: String - get() = preferences.getString("teamName","").orEmpty() + get() = preferences.getString("teamName", "").orEmpty() set(value) { preferences.edit(commit = true) { putString("teamName", value) @@ -64,12 +64,12 @@ class DefaultSharedPreference @Inject constructor( get() = preferences.getString("levelTitle", "").orEmpty() set(value) { preferences.edit(commit = true) { - putString("levelTitle",value) + putString("levelTitle", value) } } override var profileImage: String - get() = preferences.getString("profileImage","").orEmpty() + get() = preferences.getString("profileImage", "").orEmpty() set(value) { preferences.edit(commit = true) { putString("profileImage", value) @@ -83,4 +83,20 @@ class DefaultSharedPreference @Inject constructor( putBoolean("isFirstTime", value) } } + + override var isFirstShare: Boolean + get() = preferences.getBoolean("isFirstShare", true) + set(value) { + preferences.edit(commit = true) { + putBoolean("isFirstShare", value) + } + } + + override var isFirstLike: Boolean + get() = preferences.getBoolean("isFirstLike", true) + set(value) { + preferences.edit(commit = true) { + putBoolean("isFirstLike", value) + } + } } diff --git a/data/src/main/java/com/dpm/data/remote/ViewfinderService.kt b/data/src/main/java/com/dpm/data/remote/ViewfinderService.kt index 1e0107e0..f28b2d38 100644 --- a/data/src/main/java/com/dpm/data/remote/ViewfinderService.kt +++ b/data/src/main/java/com/dpm/data/remote/ViewfinderService.kt @@ -5,6 +5,7 @@ import com.dpm.data.model.response.viewfinder.ResponseBlockRowDto import com.dpm.data.model.response.viewfinder.ResponseStadiumDto import com.dpm.data.model.response.viewfinder.ResponseStadiumsDto import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -35,4 +36,15 @@ interface ViewfinderService { @Path("stadiumId") stadiumId: Int, @Path("blockCode") blockCode: String ): ResponseBlockRowDto + + @POST("/api/v1/reviews/{reviewId}/scrap") + suspend fun postScrap( + @Path("reviewId") reviewId: Int + ) + + + @POST("/api/v1/reviews/{reviewId}/like") + suspend fun postLike( + @Path("reviewId") reviewId: Int + ) } \ No newline at end of file diff --git a/data/src/main/java/com/dpm/data/repository/ViewfinderRepositoryImpl.kt b/data/src/main/java/com/dpm/data/repository/ViewfinderRepositoryImpl.kt index 16e0edb0..3475ea2f 100644 --- a/data/src/main/java/com/dpm/data/repository/ViewfinderRepositoryImpl.kt +++ b/data/src/main/java/com/dpm/data/repository/ViewfinderRepositoryImpl.kt @@ -48,4 +48,16 @@ class ViewfinderRepositoryImpl @Inject constructor( viewfinderDataSource.getBlockRow(stadiumId, blockCode).toBlockRowResponse() } } + + override suspend fun updateScrap(reviewId: Int): Result { + return runCatching { + viewfinderDataSource.updateScrap(reviewId) + } + } + + override suspend fun updateLike(reviewId: Int): Result { + return runCatching { + viewfinderDataSource.updateLike(reviewId) + } + } } \ No newline at end of file diff --git a/domain/src/main/java/com/dpm/domain/entity/response/viewfinder/ResponseBlockReview.kt b/domain/src/main/java/com/dpm/domain/entity/response/viewfinder/ResponseBlockReview.kt index 40504f0c..c5e4e5e5 100644 --- a/domain/src/main/java/com/dpm/domain/entity/response/viewfinder/ResponseBlockReview.kt +++ b/domain/src/main/java/com/dpm/domain/entity/response/viewfinder/ResponseBlockReview.kt @@ -31,7 +31,7 @@ data class ResponseBlockReview( val location: ResponseLocation = ResponseLocation(), val keywords: List = emptyList(), val reviews: List = emptyList(), - val topReviewImages: List = emptyList(), + val topReviewImages: List = emptyList(), val totalElements: Long = 0, val nextCursor: String = "", val hasNext: Boolean = false, @@ -61,8 +61,12 @@ data class ResponseBlockReview( val content: String = "", val images: List = emptyList(), val keywords: List = emptyList(), - - ) { + val isLike: Boolean, + val isScrap: Boolean, + val likesCount: Long, + val scrapsCount: Long, + val reviewType: String, + ) { data class ResponseReviewImage( val id: Int, val url: String = "", @@ -97,7 +101,7 @@ data class ResponseBlockReview( ) data class ResponseReviewRow( - val id: Int, + val id: Int = 0, val number: Int = 0, ) @@ -116,14 +120,6 @@ data class ResponseBlockReview( } } - data class ResponseTopReviewImages( - val url: String = "", - val reviewId: Int, - val blockCode: String = "", - val rowNumber: Int = 0, - val seatNumber: Int = 0, - ) - data class ResponseReviewFilter( val rowNumber: Int = 0, val seatNumber: Int = 0, diff --git a/domain/src/main/java/com/dpm/domain/preference/NetworkPreference.kt b/domain/src/main/java/com/dpm/domain/preference/NetworkPreference.kt index 20234379..dbedf655 100644 --- a/domain/src/main/java/com/dpm/domain/preference/NetworkPreference.kt +++ b/domain/src/main/java/com/dpm/domain/preference/NetworkPreference.kt @@ -10,5 +10,7 @@ interface SharedPreference { var nickname: String var profileImage : String var isFirstTime: Boolean + var isFirstShare: Boolean + var isFirstLike: Boolean fun clear() } diff --git a/domain/src/main/java/com/dpm/domain/repository/ViewfinderRepository.kt b/domain/src/main/java/com/dpm/domain/repository/ViewfinderRepository.kt index 589d2003..e8fff763 100644 --- a/domain/src/main/java/com/dpm/domain/repository/ViewfinderRepository.kt +++ b/domain/src/main/java/com/dpm/domain/repository/ViewfinderRepository.kt @@ -11,4 +11,6 @@ interface ViewfinderRepository { suspend fun getStadium(id : Int) : Result suspend fun getBlockReviews(stadiumId:Int, blockCode: String, query: RequestBlockReviewQuery): Result suspend fun getBlockRow(stadiumId: Int, blockCode: String): Result + suspend fun updateScrap(reviewId: Int): Result + suspend fun updateLike(reviewId: Int): Result } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/util/KakaoUtils.kt b/presentation/src/main/java/com/dpm/presentation/util/KakaoUtils.kt index d76090d4..d31873de 100644 --- a/presentation/src/main/java/com/dpm/presentation/util/KakaoUtils.kt +++ b/presentation/src/main/java/com/dpm/presentation/util/KakaoUtils.kt @@ -32,6 +32,28 @@ val mockDefaultFeed = FeedTemplate( ) ) +fun seatFeed( + title: String, + description: String, + imageUrl: String, + queryParams: Map +) = FeedTemplate( + content = Content( + title = title, + description = description, + imageUrl = imageUrl, + link = Link() + ), + buttons = listOf( + Button( + title = "SPOT! 앱에서 열기", + link = Link( + androidExecutionParams = queryParams + ) + ) + ) +) + class KakaoUtils() { fun share( context: Context, diff --git a/presentation/src/main/java/com/dpm/presentation/util/StadiumUxWritingUtils.kt b/presentation/src/main/java/com/dpm/presentation/util/StadiumUxWritingUtils.kt index 4b89d50e..15a796be 100644 --- a/presentation/src/main/java/com/dpm/presentation/util/StadiumUxWritingUtils.kt +++ b/presentation/src/main/java/com/dpm/presentation/util/StadiumUxWritingUtils.kt @@ -3,6 +3,7 @@ package com.dpm.presentation.util import com.dpm.domain.entity.response.viewfinder.BASE import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview import com.dpm.domain.entity.response.viewfinder.base +import com.dpm.presentation.viewfinder.uistate.StadiumDetailUiState /** * @UX_Writing : 야구장 구장 화면에서 블록 클릭 후, 블록 별 리뷰 화면의 [n루]•[구역]•[블록] @@ -96,16 +97,23 @@ fun ResponseBlockReview.ResponseReview.toBlockContent(): String { return numberString } -fun ResponseBlockReview.ResponseTopReviewImages.toBlockContent(): String { - var numberString = "" - if (blockCode.isNotEmpty()) { - numberString += if (blockCode in listOf("exciting1", "exciting3", "premium")) { - "" - } else { - "${blockCode}블록 " - } + +/** + * @UX_Writing : 카카오 공유하기 제목 -> 서울 잠실 야구장 1루 네이비석 101블록 3열 12번 좌석시야 + * @author : 조관희 + */ +fun StadiumDetailUiState.ReviewsData.kakaoShareSeatFeedTitle( + index: Int +): String { + val base = when (stadiumContent.stadiumName.base(stadiumContent.blockCode)) { + BASE.Base1 -> "1루" + BASE.Base3 -> "3루" + else -> "" } - if (rowNumber > 0) numberString += "${rowNumber}열 " - if (seatNumber > 0) numberString += "${seatNumber}번" - return numberString + val section = base + stadiumContent.sectionName + val block = if (stadiumContent.blockCode in listOf("exciting1", "exciting3", "premium")) "" else "${stadiumContent.blockCode}블록" + val column = if (reviews[index].row.number == 0) "" else "${reviews[index].row.number}열" + val seatNumber = if (reviews[index].seat.seatNumber == 0) "" else "${reviews[index].seat.seatNumber}번" + + return "${stadiumContent.stadiumName} $section $block $column $seatNumber 좌석시야" } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailActivity.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailActivity.kt index f04f1b04..8a8c3a59 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailActivity.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailActivity.kt @@ -12,7 +12,10 @@ import com.dpm.core.base.BaseActivity import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview import com.depromeet.presentation.R import com.depromeet.presentation.databinding.ActivityStadiumDetailBinding +import com.dpm.domain.preference.SharedPreference import com.dpm.presentation.home.HomeActivity +import com.dpm.presentation.util.KakaoUtils +import com.dpm.presentation.util.seatFeed import com.dpm.presentation.util.toEmptyBlock import com.dpm.presentation.viewfinder.compose.StadiumDetailScreen import com.dpm.presentation.viewfinder.dialog.ReportDialog @@ -20,6 +23,7 @@ import com.dpm.presentation.viewfinder.dialog.StadiumFilterMonthsDialog import com.dpm.presentation.viewfinder.dialog.StadiumSelectSeatDialog import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class StadiumDetailActivity : BaseActivity({ @@ -29,10 +33,15 @@ class StadiumDetailActivity : BaseActivity({ const val REVIEW_ID = "review_id" const val REVIEW_INDEX = "review_index" const val REVIEW_TITLE_WITH_STADIUM = "review_title_with_stadium" + const val REVIEW_TYPE = "review_type" + const val STADIUM_HEADER = "stadium_header" const val STADIUM_REVIEW_CONTENT = "stadium_review_content" } + @Inject + lateinit var sharedPreference: SharedPreference + private val viewModel: StadiumDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -56,9 +65,10 @@ class StadiumDetailActivity : BaseActivity({ MaterialTheme { StadiumDetailScreen( emptyBlockName = toEmptyBlock(viewModel.stadiumId, viewModel.blockCode), + isFirstShare = sharedPreference.isFirstShare, viewModel = viewModel, - onClickReviewPicture = { reviewContent, index, title -> - startToStadiumDetailPictureFragment(reviewContent, index, title) + onClickReviewPicture = { id, index, title -> + startToStadiumDetailPictureFragment(id, index, title, DetailReviewEntryPoint.MAIN_REVIEW) }, onClickSelectSeat = { StadiumSelectSeatDialog.apply { @@ -86,6 +96,12 @@ class StadiumDetailActivity : BaseActivity({ }, onRefresh = { viewModel.getBlockReviews() + }, + onClickTopImage = { id, index, title -> + startToStadiumDetailPictureFragment(id, index, title, DetailReviewEntryPoint.TOP_REVIEW) + }, + onClickShare = { + sharedPreference.isFirstShare = false } ) } @@ -121,15 +137,17 @@ class StadiumDetailActivity : BaseActivity({ } private fun startToStadiumDetailPictureFragment( - reviewContent: ResponseBlockReview.ResponseReview, + id: Long, index: Int, - title: String + title: String, + type: DetailReviewEntryPoint ) { val fragment = StadiumDetailPictureFragment.newInstance().apply { arguments = bundleOf( - REVIEW_ID to reviewContent.id, + REVIEW_ID to id, REVIEW_INDEX to index, - REVIEW_TITLE_WITH_STADIUM to title + REVIEW_TITLE_WITH_STADIUM to title, + REVIEW_TYPE to type.name, ) } diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailPictureFragment.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailPictureFragment.kt index 957d0025..9cf51f90 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailPictureFragment.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/StadiumDetailPictureFragment.kt @@ -13,14 +13,21 @@ import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.commit -import com.dpm.core.base.BindingFragment import com.depromeet.presentation.R import com.depromeet.presentation.databinding.FragmentStadiumDetailPictureBinding +import com.dpm.core.base.BindingFragment +import com.dpm.designsystem.SpotSnackBar +import com.dpm.domain.preference.SharedPreference import com.dpm.presentation.util.Utils import com.dpm.presentation.viewfinder.compose.detailpicture.StadiumDetailPictureScreen import com.dpm.presentation.viewfinder.dialog.ReportDialog import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +enum class DetailReviewEntryPoint { + TOP_REVIEW, MAIN_REVIEW +} @AndroidEntryPoint class StadiumDetailPictureFragment : BindingFragment( @@ -38,10 +45,14 @@ class StadiumDetailPictureFragment : BindingFragment + initSnackBar() + getReviewExtra { reviewId, reviewIndex, title, type -> binding.spotAppbar.setText(title) binding.cvReviewContent.setContent { MaterialTheme { StadiumDetailPictureScreen( reviewId = reviewId, reviewIndex = reviewIndex, + type = type, + isFirstLike = sharedPreference.isFirstLike, stadiumDetailViewModel = stadiumDetailViewModel, + onClickLike = { + sharedPreference.isFirstLike = false + }, + onClickScrap = { id -> + if (stadiumDetailViewModel.checkScrap(id)) { + snackBar.show() + } + }, + onClickShare = { + sharedPreference.isFirstShare = false + } ) } } @@ -66,6 +91,9 @@ class StadiumDetailPictureFragment : BindingFragment Unit) { + private fun initSnackBar() { + snackBar = SpotSnackBar.make( + view = binding.root.rootView, + background = com.depromeet.designsystem.R.drawable.rect_body_subtitle_fill_60, + message = getString(R.string.viewfinder_snackbar_scrap), + endMessage = getString(R.string.viewfinder_underscore_snackbar_scrap), + onClick = { + // TODO : 스크랩 화면으로 이동 + }) + } + + private fun getReviewExtra(callback: (id: Long, index: Int, title: String, type: String) -> Unit) { val reviewId = arguments?.getLong(StadiumDetailActivity.REVIEW_ID) ?: return val reviewIndex = arguments?.getInt(StadiumDetailActivity.REVIEW_INDEX) ?: return val title = arguments?.getString(StadiumDetailActivity.REVIEW_TITLE_WITH_STADIUM) ?: return - callback(reviewId, reviewIndex, title) + val type = arguments?.getString(StadiumDetailActivity.REVIEW_TYPE) ?: return + callback(reviewId, reviewIndex, title, type) } private fun removeFragment() { @@ -137,6 +177,7 @@ class StadiumDetailPictureFragment : BindingFragment Unit ) { Row( modifier = modifier @@ -34,7 +37,7 @@ fun LikeButton( ) .border( width = 1.dp, - color = SpotTheme.colors.strokeTertiary, + color = if (isLike) SpotTheme.colors.actionEnabled else SpotTheme.colors.strokeTertiary, shape = RoundedCornerShape(72.dp) ) .padding( @@ -42,13 +45,15 @@ fun LikeButton( vertical = 8.dp ) .noRippleClickable { - + onClick() }, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Icon( - painter = painterResource(id = R.drawable.ic_like_inactive), + painter = painterResource( + id = if (isLike) R.drawable.ic_like_active else R.drawable.ic_like_inactive + ), contentDescription = null, modifier = Modifier.size(24.dp), tint = Color.Unspecified @@ -61,9 +66,9 @@ fun LikeButton( ) Spacer(modifier = Modifier.width(4.dp)) Text( - text = 12.toString(), + text = likeCount.toString(), style = SpotTheme.typography.label09, - color = SpotTheme.colors.foregroundDisabled + color = if (isLike) SpotTheme.colors.actionEnabled else SpotTheme.colors.strokeTertiary ) } } @@ -71,5 +76,9 @@ fun LikeButton( @Preview @Composable private fun LikeButtonPreview() { - LikeButton() + LikeButton( + isLike = true, + likeCount = 1, + onClick = {} + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/ReviewContentBottom.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/ReviewContentBottom.kt index 32df4b19..a8803a39 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/ReviewContentBottom.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/ReviewContentBottom.kt @@ -8,6 +8,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -18,28 +22,38 @@ import com.depromeet.designsystem.R @Composable fun ReviewContentBottom( - modifier: Modifier = Modifier + isLike: Boolean, + isScrap: Boolean, + likeCount: Long, + modifier: Modifier = Modifier, + onClickLike: () -> Unit, + onClickScrap: () -> Unit, + onClickShare: () -> Unit ) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - LikeButton() + LikeButton( + isLike = isLike, + likeCount = likeCount, + onClick = onClickLike + ) Row { IconButton( - onClick = { } + onClick = onClickScrap ) { Icon( painter = painterResource( - id = R.drawable.ic_scrap_inactive_button + id = if (isScrap) R.drawable.ic_scrap_active_button else R.drawable.ic_scrap_inactive_button ), contentDescription = null, tint = Color.Unspecified ) } Spacer(modifier = Modifier.width(6.dp)) IconButton( - onClick = { } + onClick = onClickShare ) { Icon( painter = painterResource( @@ -55,5 +69,51 @@ fun ReviewContentBottom( @Preview @Composable private fun ReviewContentBottomPreview() { - ReviewContentBottom() + ReviewContentBottom( + isLike = false, + isScrap = false, + likeCount = 0, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) +} + +@Preview +@Composable +private fun ReviewContentBottomLikePreview() { + ReviewContentBottom( + isLike = true, + isScrap = false, + likeCount = 0, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) +} + +@Preview +@Composable +private fun ReviewContentBottomScrapPreview() { + ReviewContentBottom( + isLike = false, + isScrap = true, + likeCount = 0, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) +} + +@Preview +@Composable +private fun ReviewContentBottomLikeAndScrapPreview() { + ReviewContentBottom( + isLike = true, + isScrap = true, + likeCount = 0, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumDetailScreen.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumDetailScreen.kt index ad8f3af5..0c8bbad3 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumDetailScreen.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumDetailScreen.kt @@ -1,6 +1,7 @@ package com.dpm.presentation.viewfinder.compose import android.content.Context +import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -19,37 +20,49 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.dpm.domain.entity.response.viewfinder.BASE import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview +import com.dpm.domain.entity.response.viewfinder.base import com.dpm.presentation.mapper.toKeyword +import com.dpm.presentation.util.KakaoUtils +import com.dpm.presentation.util.kakaoShareSeatFeedTitle +import com.dpm.presentation.util.seatFeed import com.dpm.presentation.util.stadiumBlock import com.dpm.presentation.util.toTitle import com.dpm.presentation.viewfinder.StadiumDetailActivity import com.dpm.presentation.viewfinder.uistate.StadiumDetailUiState import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel +import timber.log.Timber @OptIn(ExperimentalFoundationApi::class) @Composable fun StadiumDetailScreen( emptyBlockName: String, + isFirstShare: Boolean, context: Context = LocalContext.current, modifier: Modifier = Modifier, viewModel: StadiumDetailViewModel = viewModel(), - onClickReviewPicture: (reviewContent: ResponseBlockReview.ResponseReview, index: Int, title: String) -> Unit, + onClickReviewPicture: (id: Long, index: Int, title: String) -> Unit, + onClickTopImage: (id: Long, index: Int, title: String) -> Unit, onClickSelectSeat: () -> Unit, onClickFilterMonthly: () -> Unit, onClickReport: () -> Unit, onClickGoBack: () -> Unit, + onClickShare:() -> Unit = {}, onRefresh: () -> Unit ) { var isMore by remember { mutableStateOf(false) } + var isFirstShare by remember { mutableStateOf(isFirstShare) } val verticalScrollState = rememberLazyListState() val scrollState by viewModel.scrollState.collectAsStateWithLifecycle() val reviewFilter by viewModel.reviewFilter.collectAsStateWithLifecycle() val detailUiState by viewModel.detailUiState.collectAsStateWithLifecycle() val currentIndex by viewModel.currentIndex.collectAsStateWithLifecycle() + LaunchedEffect(key1 = scrollState) { verticalScrollState.scrollToItem(0) viewModel.updateScrollState(false) @@ -108,7 +121,10 @@ fun StadiumDetailScreen( keywords = uiState.keywords.map { it.toKeyword() }, onChangeIsMore = { isMore = it }, onClickSelectSeat = onClickSelectSeat, - onCancelSeat = viewModel::clearSeat + onCancelSeat = viewModel::clearSeat, + onClickTopImage = { id, index -> + onClickTopImage(id, index, uiState.stadiumContent.toTitle()) + } ) Spacer(modifier = Modifier.height(30.dp)) } @@ -138,15 +154,42 @@ fun StadiumDetailScreen( ) { index -> StadiumReviewContent( context = context, + isFirstShare = isFirstShare, + firstReview = uiState.reviews[index] == uiState.reviews.firstOrNull(), reviewContent = uiState.reviews[index], onClick = { reviewContent, index -> onClickReviewPicture( - reviewContent, + reviewContent.id, index, uiState.stadiumContent.toTitle() ) }, - onClickReport = onClickReport + onClickReport = onClickReport, + onClickLike = viewModel::updateLike, + onClickScrap = viewModel::updateScrap, + onClickShare = { + onClickShare() + isFirstShare = false + KakaoUtils().share( + context, + seatFeed( + title = uiState.kakaoShareSeatFeedTitle(index), + description = "출처 : ${uiState.reviews[index].member.nickname}", + imageUrl = uiState.reviews[index].images.firstOrNull()?.url + ?: "", + queryParams = mapOf( + "stadiumId" to viewModel.stadiumId.toString(), + "blockCode" to viewModel.blockCode + ) + ), + onSuccess = { sharingIntent -> + context.startActivity(sharingIntent) + }, + onFailure = { + Timber.e("error : ${it.message}") + } + ) + } ) Spacer(modifier = Modifier.height(40.dp)) } @@ -162,13 +205,15 @@ fun StadiumDetailScreen( private fun StadiumDetailScreenPreview() { Box(modifier = Modifier.background(Color.White)) { StadiumDetailScreen( + isFirstShare = true, emptyBlockName = "207", onClickReviewPicture = { _, _, _ -> }, onClickSelectSeat = {}, onClickFilterMonthly = {}, onClickReport = {}, onClickGoBack = {}, - onRefresh = {} + onRefresh = {}, + onClickTopImage = { _, _, _ -> }, ) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumHeaderContent.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumHeaderContent.kt index fdc7aec0..02d8d144 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumHeaderContent.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumHeaderContent.kt @@ -30,10 +30,11 @@ fun StadiumHeaderContent( stadiumTitle: String, keywords: List, reviewFilter: RequestBlockReviewQuery, - topReviewImages: List, + topReviewImages: List, modifier: Modifier = Modifier, onChangeIsMore: (Boolean) -> Unit, onClickSelectSeat: () -> Unit, + onClickTopImage:(id: Long, index: Int) -> Unit, onCancelSeat: () -> Unit ) { Column( @@ -43,7 +44,8 @@ fun StadiumHeaderContent( StadiumPictureViewPager( context = context, topReviewImages = topReviewImages, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + onClick = onClickTopImage ) Spacer(modifier = Modifier.height(20.dp)) StadiumAreaText( @@ -84,24 +86,74 @@ fun StadiumHeaderContent( @Preview(showBackground = true) @Composable private fun StadiumHeaderContentPreview() { + val review = ResponseBlockReview.ResponseReview( + id = 1, + dateTime = "2023-03-01T19:00:00", + content = "asdfsdfsafsfda", + images = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ), + member = ResponseBlockReview.ResponseReview.ResponseReviewMember( + "https://picsum.photos/200/300", + nickname = "엘지의 왕자", + level = 0 + ), + stadium = ResponseBlockReview.ResponseReview.ResponseReviewStadium( + id = 1, + name = "서울 잠실 야구장" + ), + section = ResponseBlockReview.ResponseReview.ResponseReviewSection( + id = 1, + name = "오렌지석", + alias = "응원석" + ), + block = ResponseBlockReview.ResponseReview.ResponseReviewBlock( + id = 1, + code = "207" + ), + row = ResponseBlockReview.ResponseReview.ResponseReviewRow( + id = 1, + number = 1 + ), + seat = ResponseBlockReview.ResponseReview.ResponseReviewSeat( + id = 1, + seatNumber = 12 + ), + keywords = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ) + ), + isLike = false, + isScrap = false, + likesCount = 1, + scrapsCount = 0, + reviewType = "" + ) StadiumHeaderContent( context = LocalContext.current, isMore = false, topReviewImages = listOf( - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - ), - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - ), + review, review ), reviewFilter = RequestBlockReviewQuery( rowNumber = null, @@ -121,6 +173,7 @@ private fun StadiumHeaderContentPreview() { ), onChangeIsMore = {}, onClickSelectSeat = {}, + onClickTopImage = {_,_ ->}, onCancelSeat = {} ) } @@ -128,25 +181,75 @@ private fun StadiumHeaderContentPreview() { @Preview(showBackground = true) @Composable private fun StadiumHeaderContentIsMorePreview() { + val review = ResponseBlockReview.ResponseReview( + id = 1, + dateTime = "2023-03-01T19:00:00", + content = "asdfsdfsafsfda", + images = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ), + member = ResponseBlockReview.ResponseReview.ResponseReviewMember( + "https://picsum.photos/200/300", + nickname = "엘지의 왕자", + level = 0 + ), + stadium = ResponseBlockReview.ResponseReview.ResponseReviewStadium( + id = 1, + name = "서울 잠실 야구장" + ), + section = ResponseBlockReview.ResponseReview.ResponseReviewSection( + id = 1, + name = "오렌지석", + alias = "응원석" + ), + block = ResponseBlockReview.ResponseReview.ResponseReviewBlock( + id = 1, + code = "207" + ), + row = ResponseBlockReview.ResponseReview.ResponseReviewRow( + id = 1, + number = 1 + ), + seat = ResponseBlockReview.ResponseReview.ResponseReviewSeat( + id = 1, + seatNumber = 12 + ), + keywords = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ) + ), + isLike = false, + isScrap = false, + likesCount = 1, + scrapsCount = 0, + reviewType = "" + ) StadiumHeaderContent( context = LocalContext.current, isMore = true, topReviewImages = listOf( - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - - ), - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - ), + review, + review, ), reviewFilter = RequestBlockReviewQuery( rowNumber = 1, @@ -167,6 +270,7 @@ private fun StadiumHeaderContentIsMorePreview() { ), onChangeIsMore = {}, onClickSelectSeat = {}, + onClickTopImage = {_,_ ->}, onCancelSeat = {} ) } diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumPictureViewPager.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumPictureViewPager.kt index 0198eb79..c655c07e 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumPictureViewPager.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumPictureViewPager.kt @@ -29,14 +29,16 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.dpm.designsystem.compose.ui.SpotTheme import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview +import com.dpm.presentation.extension.noRippleClickable import com.dpm.presentation.util.toBlockContent @OptIn(ExperimentalFoundationApi::class) @Composable fun StadiumPictureViewPager( context: Context, - topReviewImages: List, - modifier: Modifier = Modifier + topReviewImages: List, + modifier: Modifier = Modifier, + onClick: (id: Long, index: Int) -> Unit ) { val pagerState = rememberPagerState(pageCount = { topReviewImages.size }) @@ -51,7 +53,7 @@ fun StadiumPictureViewPager( ) { page -> AsyncImage( model = ImageRequest.Builder(context) - .data(topReviewImages.getOrNull(page)?.url) + .data(topReviewImages.getOrNull(page)?.images?.getOrNull(0)?.url) .crossfade(true) .build(), contentDescription = null, @@ -66,7 +68,10 @@ fun StadiumPictureViewPager( ), modifier = Modifier .fillMaxWidth() - .clip(RectangleShape), + .clip(RectangleShape) + .noRippleClickable { + onClick(topReviewImages[page].id, page) + }, ) } @@ -121,24 +126,75 @@ fun StadiumPictureViewPager( @Preview @Composable private fun StadiumPictureViewPagerPreview() { + val review = ResponseBlockReview.ResponseReview( + id = 1, + dateTime = "2023-03-01T19:00:00", + content = "asdfsdfsafsfda", + images = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ResponseBlockReview.ResponseReview.ResponseReviewImage( + id = 1, + url = "https://picsum.photos/200/300" + ), + ), + member = ResponseBlockReview.ResponseReview.ResponseReviewMember( + "https://picsum.photos/200/300", + nickname = "엘지의 왕자", + level = 0 + ), + stadium = ResponseBlockReview.ResponseReview.ResponseReviewStadium( + id = 1, + name = "서울 잠실 야구장" + ), + section = ResponseBlockReview.ResponseReview.ResponseReviewSection( + id = 1, + name = "오렌지석", + alias = "응원석" + ), + block = ResponseBlockReview.ResponseReview.ResponseReviewBlock( + id = 1, + code = "207" + ), + row = ResponseBlockReview.ResponseReview.ResponseReviewRow( + id = 1, + number = 1 + ), + seat = ResponseBlockReview.ResponseReview.ResponseReviewSeat( + id = 1, + seatNumber = 12 + ), + keywords = listOf( + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ), + ResponseBlockReview.ResponseReview.ResponseReviewKeyword( + id = 1, + content = "", + isPositive = false + ) + ), + isLike = true, + isScrap = true, + likesCount = 1, + scrapsCount = 0, + reviewType = "" + ) StadiumPictureViewPager( context = LocalContext.current, topReviewImages = listOf( - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - ), - ResponseBlockReview.ResponseTopReviewImages( - url = "", - reviewId = 1, - blockCode = "207", - rowNumber = 1, - seatNumber = 12 - ), + review, review ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + onClick = {_,_ ->} ) } diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumReviewContent.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumReviewContent.kt index 45c6a706..0edacde0 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumReviewContent.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/StadiumReviewContent.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest +import com.depromeet.presentation.R import com.dpm.designsystem.compose.ui.SpotTheme import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview -import com.depromeet.presentation.R import com.dpm.presentation.extension.noRippleClickable import com.dpm.presentation.mapper.toKeyword import com.dpm.presentation.util.toBlockContent @@ -53,10 +53,15 @@ private enum class ReviewContentShowMoreState { @Composable fun StadiumReviewContent( context: Context, + isFirstShare: Boolean, + firstReview: Boolean, reviewContent: ResponseBlockReview.ResponseReview, modifier: Modifier = Modifier, onClick: (reviewContent: ResponseBlockReview.ResponseReview, index: Int) -> Unit, - onClickReport: () -> Unit + onClickReport: () -> Unit, + onClickLike: (id: Long) -> Unit = {}, + onClickScrap: (id: Long) -> Unit = {}, + onClickShare: () -> Unit ) { val minimumLineLength = 3 var showMoreButtonState by remember { @@ -240,8 +245,28 @@ fun StadiumReviewContent( ) Spacer(modifier = Modifier.height(12.dp)) ReviewContentBottom( - modifier = Modifier.padding(start = 32.dp, end = 16.dp) + isLike = reviewContent.isLike, + isScrap = reviewContent.isScrap, + likeCount = reviewContent.likesCount, + modifier = Modifier.padding(start = 32.dp, end = 16.dp), + onClickLike = { + onClickLike(reviewContent.id) + }, + onClickScrap = { + onClickScrap(reviewContent.id) + }, + onClickShare = onClickShare ) + if (firstReview && isFirstShare) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd + ) { + ShareTooltip(bias = 0.875f, content = "카카오톡으로 같이가는 친구에게 공유하기") + } + } } } @@ -255,6 +280,8 @@ private fun StadiumReviewContentPreview() { ) { StadiumReviewContent( context = LocalContext.current, + isFirstShare = false, + firstReview = false, reviewContent = ResponseBlockReview.ResponseReview( id = 1, dateTime = "2023-03-01T19:00:00", @@ -311,10 +338,16 @@ private fun StadiumReviewContentPreview() { content = "", isPositive = false ) - ) + ), + isLike = false, + isScrap = false, + likesCount = 1, + scrapsCount = 0, + reviewType = "" ), onClick = { _, _ -> }, - onClickReport = {} + onClickReport = {}, + onClickShare = {} ) } diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailCotentLayer.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailCotentLayer.kt index 15c891ea..83004774 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailCotentLayer.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailCotentLayer.kt @@ -262,7 +262,12 @@ private fun DetailContentLayerPreview() { ResponseBlockReview.ResponseReview.ResponseReviewKeyword( id = 2, content = "싫어요", isPositive = false ) - ) + ), + isLike = false, + isScrap = false, + likesCount = 1, + scrapsCount = 1, + reviewType = "" ) DetailContentLayer( context = LocalContext.current, diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailReviewInteractionItems.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailReviewInteractionItems.kt index b2a4ff8a..9c65cd8c 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailReviewInteractionItems.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailReviewInteractionItems.kt @@ -2,6 +2,8 @@ package com.dpm.presentation.viewfinder.compose.detailpicture import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -17,11 +19,15 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.depromeet.designsystem.R import com.dpm.designsystem.compose.ui.SpotTheme -import com.dpm.presentation.extension.noRippleClickable @Composable fun DetailReviewInteractionItems( - modifier: Modifier = Modifier + isLike: Boolean, + likeCount: Long, + modifier: Modifier = Modifier, + onClickLike: () -> Unit, + onClickScrap: () -> Unit, + onClickShare: () -> Unit ) { Column( modifier = modifier @@ -35,39 +41,61 @@ fun DetailReviewInteractionItems( ), horizontalAlignment = Alignment.CenterHorizontally ) { - IconButton(onClick = { /*TODO*/ }) { + IconButton(onClick = onClickLike) { Icon( - painter = painterResource(id = R.drawable.ic_like_inactive), + painter = painterResource(id = if (isLike) R.drawable.ic_like_active else R.drawable.ic_like_inactive), contentDescription = null, tint = Color.Unspecified, - modifier = Modifier.size(30.dp) + modifier = Modifier.size(24.dp) ) } Text( - text = 10.toString(), + text = likeCount.toString(), style = SpotTheme.typography.label10, - color = SpotTheme.colors.foregroundWhite + color = if (isLike) SpotTheme.colors.actionEnabled else SpotTheme.colors.foregroundWhite ) - IconButton(onClick = { /*TODO*/ }) { + Spacer(modifier = Modifier.height(5.dp)) + IconButton(onClick = { + onClickScrap() + }) { Icon( painter = painterResource(id = R.drawable.ic_scrap), contentDescription = null, - tint = Color.Unspecified, - modifier = Modifier.size(23.dp) + tint = SpotTheme.colors.foregroundWhite, + modifier = Modifier.size(24.dp) ) } - IconButton(onClick = { /*TODO*/ }) { + Spacer(modifier = Modifier.height(5.dp)) + IconButton(onClick = onClickShare) { Icon( painter = painterResource(id = R.drawable.ic_share), contentDescription = null, - tint = Color.Unspecified, + tint = SpotTheme.colors.foregroundWhite, ) } } } +@Preview +@Composable +private fun DetailReviewInteractionItemsLikePreview() { + DetailReviewInteractionItems( + isLike = true, + likeCount = 1, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) +} + @Preview @Composable private fun DetailReviewInteractionItemsPreview() { - DetailReviewInteractionItems() + DetailReviewInteractionItems( + isLike = false, + likeCount = 1, + onClickLike = {}, + onClickScrap = {}, + onClickShare = {} + ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailViewPagerLayer.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailViewPagerLayer.kt index 79ae4518..330843cf 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailViewPagerLayer.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/DetailViewPagerLayer.kt @@ -19,10 +19,16 @@ import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview @Composable fun DetailViewPagerLayer( context: Context, + isLike: Boolean, + likeCount: Long, + isFirstLike: Boolean, isDimmed: Boolean, verticalPagerState: PagerState, pictures: List, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onClickLike: () -> Unit, + onClickScrap: () -> Unit, + onClickShare: () -> Unit ) { Column( modifier = modifier @@ -32,9 +38,15 @@ fun DetailViewPagerLayer( horizontalAlignment = Alignment.CenterHorizontally ) { StadiumDetailPictureViewPager( + isLike = isLike, context = context, pictures = pictures, + isFirstLike = isFirstLike, + likeCount = likeCount, verticalPagerState = verticalPagerState, + onClickLike = onClickLike, + onClickScrap = onClickScrap, + onClickShare = onClickShare ) } } @@ -63,7 +75,13 @@ private fun DetailViewPagerLayerPreview() { DetailViewPagerLayer( context = LocalContext.current, isDimmed = true, + isLike = true, + isFirstLike = true, + likeCount = 1, pictures = pictures, - verticalPagerState = pagerState + verticalPagerState = pagerState, + onClickLike = { }, + onClickScrap = { }, + onClickShare = { } ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/LikeTooltip.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/LikeTooltip.kt index e260c7dd..53ce9dad 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/LikeTooltip.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/LikeTooltip.kt @@ -64,7 +64,6 @@ fun LikeTooltip( Canvas( modifier = Modifier .height(triangleHeight) - .offset(y = tooltipHeight.dp) ) { val trianglePath = Path().apply { moveTo(tooltipWidth * bias, triangleHeight.toPx()) // 삼각형 꼭짓점 diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureScreen.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureScreen.kt index c90f6a12..5c87b121 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureScreen.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureScreen.kt @@ -1,103 +1,85 @@ package com.dpm.presentation.viewfinder.compose.detailpicture import android.content.Context -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import com.dpm.presentation.viewfinder.uistate.StadiumDetailUiState +import com.dpm.presentation.viewfinder.DetailReviewEntryPoint +import com.dpm.presentation.viewfinder.compose.detailpicture.main.StadiumDetailPictureTopScreen +import com.dpm.presentation.viewfinder.compose.detailpicture.top.StadiumDetailPictureMainScreen import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel -@OptIn(ExperimentalFoundationApi::class) @Composable fun StadiumDetailPictureScreen( - context: Context = LocalContext.current, - stadiumDetailViewModel: StadiumDetailViewModel = viewModel(), reviewId: Long, reviewIndex: Int, - modifier: Modifier = Modifier + type: String, + isFirstLike: Boolean, + context: Context = LocalContext.current, + stadiumDetailViewModel: StadiumDetailViewModel = viewModel(), + onClickLike:() -> Unit = {}, + onClickScrap: (id: Long) -> Unit = {}, + onClickShare: () -> Unit = {} ) { val reviews = stadiumDetailViewModel.detailUiState.collectAsStateWithLifecycle() val bottomPadding by stadiumDetailViewModel.bottomPadding.collectAsStateWithLifecycle() - reviews.value.let { uiState -> - when (uiState) { - is StadiumDetailUiState.ReviewsData -> { - val visited = remember { - mutableStateListOf( - *List(uiState.reviews.size) { false }.toTypedArray() - ) - } - - if (uiState.reviews.size - visited.size > 0) { - visited.addAll(List(uiState.reviews.size - visited.size) { false }) - } - - val initPage by remember { - mutableStateOf(uiState.reviews.indexOfFirst { it.id == reviewId }) - } - - val pagerState = rememberPagerState( - pageCount = { uiState.reviews.size }, - initialPage = initPage - ) - - var pageIndex by remember { - mutableStateOf(0) - } - - LaunchedEffect(key1 = pagerState) { - snapshotFlow { pagerState.currentPage }.collect { - pageIndex = it - stadiumDetailViewModel.updateCurrentIndex(it) - if (visited[it]) return@collect - - if (it == initPage) { - visited[it] = true - } - - if (!visited[it]) { - visited[it] = true - } - } - } - - StadiumDetailReviewViewPager( - context = context, - reviews = uiState.reviews, - visited = visited, - position = reviewIndex, - pageState = uiState.hasNext, - pagerState = pagerState, - pageIndex = pageIndex, - bottomPadding = bottomPadding, - modifier = modifier, - onLoadPaging = stadiumDetailViewModel::getBlockReviews, - ) - } + when (type) { + DetailReviewEntryPoint.TOP_REVIEW.name -> { + StadiumDetailPictureTopScreen( + context = context, + uiState = reviews.value, + reviewId = reviewId, + reviewIndex = reviewIndex, + bottomPadding = bottomPadding, + isFirstLike = isFirstLike, + stadiumDetailViewModel = stadiumDetailViewModel, + onClickLike = onClickLike, + onClickScrap = onClickScrap, + onClickShare = onClickShare + ) + } - else -> Unit + DetailReviewEntryPoint.MAIN_REVIEW.name -> { + StadiumDetailPictureMainScreen( + context = context, + uiState = reviews.value, + reviewId = reviewId, + reviewIndex = reviewIndex, + bottomPadding = bottomPadding, + isFirstLike = isFirstLike, + stadiumDetailViewModel = stadiumDetailViewModel, + onClickLike = onClickLike, + onClickScrap = onClickScrap, + onClickShare = onClickShare + ) } } } @Preview @Composable -private fun StadiumDetailPictureScreenPreview() { +private fun StadiumDetailPictureScreenMainPreview() { + StadiumDetailPictureScreen( + isFirstLike = true, + context = LocalContext.current, + reviewId = 1, + reviewIndex = 0, + type = DetailReviewEntryPoint.MAIN_REVIEW.name + ) +} + +@Preview +@Composable +private fun StadiumDetailPictureScreenTopPreview() { StadiumDetailPictureScreen( + isFirstLike = true, context = LocalContext.current, reviewId = 1, - reviewIndex = 0 + reviewIndex = 0, + type = DetailReviewEntryPoint.TOP_REVIEW.name ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureViewPager.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureViewPager.kt index 156ee923..2fd6218a 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureViewPager.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailPictureViewPager.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight @@ -39,68 +40,105 @@ import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview @Composable fun StadiumDetailPictureViewPager( context: Context, + isLike: Boolean, + isFirstLike: Boolean, + likeCount: Long, verticalPagerState: PagerState, pictures: List, modifier: Modifier = Modifier, + onClickLike: () -> Unit, + onClickScrap: () -> Unit, + onClickShare: () -> Unit ) { - Column( + Box( modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally + contentAlignment = Alignment.Center ) { - HorizontalPager( - modifier = Modifier, - state = verticalPagerState, - ) { page -> - AsyncImage( - model = ImageRequest.Builder(context) - .data(pictures.getOrNull(page)?.url) - .crossfade(true) - .build(), - contentScale = ContentScale.FillWidth, - contentDescription = null, - placeholder = ColorPainter(Color.LightGray), - modifier = Modifier - .fillMaxWidth() - .heightIn(max = ((context.resources.displayMetrics.heightPixels / context.resources.displayMetrics.density) * 0.58).dp) - .clip(RectangleShape), - - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - Modifier - .wrapContentHeight() - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally ) { - repeat(verticalPagerState.pageCount) { iteration -> - if (verticalPagerState.currentPage == iteration) { - Box( - modifier = Modifier - .padding(end = 4.dp) - .clip(RoundedCornerShape(8.dp)) - .background( - color = SpotTheme.colors.actionEnabled - ) - .size( - height = 6.dp, - width = 15.dp - ) - ) - } else { - Box( - modifier = Modifier - .padding(end = 4.dp) - .clip(CircleShape) - .background( - color = SpotTheme.colors.backgroundPrimary - ) - .size(6.dp) + HorizontalPager( + modifier = Modifier, + state = verticalPagerState, + ) { page -> + AsyncImage( + model = ImageRequest.Builder(context) + .data(pictures.getOrNull(page)?.url) + .crossfade(true) + .build(), + contentScale = ContentScale.FillWidth, + contentDescription = null, + placeholder = ColorPainter(Color.LightGray), + modifier = Modifier + .fillMaxWidth() + .heightIn(max = ((context.resources.displayMetrics.heightPixels / context.resources.displayMetrics.density) * 0.58).dp) + .clip(RectangleShape), + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + Modifier + .wrapContentHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + repeat(verticalPagerState.pageCount) { iteration -> + if (verticalPagerState.currentPage == iteration) { + Box( + modifier = Modifier + .padding(end = 4.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + color = SpotTheme.colors.actionEnabled + ) + .size( + height = 6.dp, + width = 15.dp + ) + ) + } else { + Box( + modifier = Modifier + .padding(end = 4.dp) + .clip(CircleShape) + .background( + color = SpotTheme.colors.backgroundPrimary + ) + .size(6.dp) + ) + } } } } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 12.dp), + contentAlignment = Alignment.CenterEnd + ) { + DetailReviewInteractionItems( + isLike = isLike, + likeCount = likeCount, + onClickLike = onClickLike, + onClickScrap = onClickScrap, + onClickShare = onClickShare + ) + if (isFirstLike) { + LikeTooltip( + modifier = Modifier + .align(Alignment.TopEnd) + .offset(y = (-32).dp), + bias = 0.8f, + content = "유용했다면, 도움돼요를 눌러주세요!", + ) + } + } } + + } @OptIn(ExperimentalFoundationApi::class) @@ -112,6 +150,9 @@ private fun StadiumDetailPictureViewPagerPreview() { } StadiumDetailPictureViewPager( context = LocalContext.current, + isLike = true, + likeCount = 1, + isFirstLike = true, verticalPagerState = pagerState, pictures = listOf( ResponseBlockReview.ResponseReview.ResponseReviewImage( @@ -124,5 +165,8 @@ private fun StadiumDetailPictureViewPagerPreview() { id = 1, url = "" ) ), + onClickLike = { }, + onClickScrap = { }, + onClickShare = { } ) } \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailReviewViewPager.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailReviewViewPager.kt index c7d07af0..5b85794a 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailReviewViewPager.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/StadiumDetailReviewViewPager.kt @@ -1,7 +1,6 @@ package com.dpm.presentation.viewfinder.compose.detailpicture import android.content.Context -import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -26,16 +25,20 @@ import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview fun StadiumDetailReviewViewPager( context: Context, position: Int, - pageState: Boolean, + hasNext: Boolean = false, + isFirstLike: Boolean, pagerState: PagerState, pageIndex: Int, bottomPadding: Float, reviews: List, - visited: List, + visited: List = emptyList(), modifier: Modifier = Modifier, - onLoadPaging: () -> Unit, + onLoadPaging: () -> Unit = {}, + onClickLike: (id: Long) -> Unit = {}, + onClickScrap: (id: Long) -> Unit = {}, + onClickShare: (imagePosition: Int) -> Unit = {} ) { - if (pageIndex == reviews.size - 1 && pageState) { + if (pageIndex == reviews.size - 1 && hasNext) { onLoadPaging() } @@ -63,8 +66,10 @@ fun StadiumDetailReviewViewPager( ) LaunchedEffect(key1 = Unit) { - if (!visited[page]) { - verticalPagerState.scrollToPage(0) + if (visited.isNotEmpty()) { + if (!visited[page]) { + verticalPagerState.scrollToPage(0) + } } } @@ -80,8 +85,20 @@ fun StadiumDetailReviewViewPager( DetailViewPagerLayer( context = context, isDimmed = isDimmed, + isLike = reviews[page].isLike, + isFirstLike = isFirstLike, + likeCount = reviews[page].likesCount, pictures = reviews[page].images, verticalPagerState = verticalPagerState, + onClickLike = { + onClickLike(reviews[page].id) + }, + onClickScrap = { + onClickScrap(reviews[page].id) + }, + onClickShare = { + onClickShare(verticalPagerState.currentPage) + } ) DetailContentLayer( context = context, @@ -151,7 +168,12 @@ private fun StadiumDetailReviewViewPagerPreview() { ResponseBlockReview.ResponseReview.ResponseReviewKeyword( id = 2, content = "싫어요", isPositive = false ) - ) + ), + isLike = false, + isScrap = false, + likesCount = 0, + scrapsCount = 0, + reviewType = "" ) ) val pagerState = rememberPagerState( @@ -160,9 +182,10 @@ private fun StadiumDetailReviewViewPagerPreview() { StadiumDetailReviewViewPager( context = LocalContext.current, reviews = reviews, + isFirstLike = true, visited = emptyList(), position = 1, - pageState = false, + hasNext = false, pagerState = pagerState, pageIndex = 0, bottomPadding = 0f, @@ -229,7 +252,12 @@ private fun StadiumDetailReviewViewPagerMorePreview() { ResponseBlockReview.ResponseReview.ResponseReviewKeyword( id = 2, content = "싫어요", isPositive = false ) - ) + ), + isLike = false, + isScrap = false, + likesCount = 0, + scrapsCount = 0, + reviewType = "" ) ) val pagerState = rememberPagerState( @@ -240,7 +268,8 @@ private fun StadiumDetailReviewViewPagerMorePreview() { reviews = reviews, visited = emptyList(), position = 1, - pageState = false, + isFirstLike = true, + hasNext = false, pagerState = pagerState, pageIndex = 0, bottomPadding = 0f, diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/main/StadiumDetailPictureTopScreen.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/main/StadiumDetailPictureTopScreen.kt new file mode 100644 index 00000000..1d7baaa0 --- /dev/null +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/main/StadiumDetailPictureTopScreen.kt @@ -0,0 +1,116 @@ +package com.dpm.presentation.viewfinder.compose.detailpicture.main + +import android.content.Context +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.dpm.presentation.util.KakaoUtils +import com.dpm.presentation.util.kakaoShareSeatFeedTitle +import com.dpm.presentation.util.seatFeed +import com.dpm.presentation.viewfinder.DetailReviewEntryPoint +import com.dpm.presentation.viewfinder.compose.detailpicture.StadiumDetailReviewViewPager +import com.dpm.presentation.viewfinder.uistate.StadiumDetailUiState +import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StadiumDetailPictureTopScreen( + context: Context, + reviewId: Long, + reviewIndex: Int, + bottomPadding: Float, + isFirstLike: Boolean, + uiState: StadiumDetailUiState, + modifier: Modifier = Modifier, + stadiumDetailViewModel: StadiumDetailViewModel = viewModel(), + onClickLike: () -> Unit = {}, + onClickScrap: (id: Long) -> Unit = {}, + onClickShare: () -> Unit = {} +) { + when (uiState) { + is StadiumDetailUiState.ReviewsData -> { + val initPage by remember { + mutableStateOf(uiState.topReviewImages.indexOfFirst { it.id == reviewId }) + } + + val pagerState = rememberPagerState( + pageCount = { uiState.topReviewImages.size }, + initialPage = initPage + ) + + var isFirstLikeState by remember { + mutableStateOf(isFirstLike) + } + + var pageIndex by remember { + mutableStateOf(0) + } + + LaunchedEffect(key1 = pagerState) { + snapshotFlow { pagerState.currentPage }.collect { + pageIndex = it + } + } + + StadiumDetailReviewViewPager( + context = context, + reviews = uiState.topReviewImages, + position = reviewIndex, + isFirstLike = isFirstLikeState, + pagerState = pagerState, + pageIndex = pageIndex, + bottomPadding = bottomPadding, + modifier = modifier, + onClickLike = { id -> + onClickLike() + isFirstLikeState = false + stadiumDetailViewModel.updateLike(id) + }, + onClickScrap = onClickScrap, + onClickShare = { imagePosition -> + onClickShare() + KakaoUtils().share( + context, + seatFeed( + title = uiState.kakaoShareSeatFeedTitle(pageIndex), + description = "출처 : ${uiState.reviews[pageIndex].member.nickname}", + imageUrl = uiState.reviews[pageIndex].images[imagePosition].url, + queryParams = mapOf( + "stadiumId" to stadiumDetailViewModel.stadiumId.toString(), + "blockCode" to stadiumDetailViewModel.blockCode + ) + ), + onSuccess = { sharingIntent -> + context.startActivity(sharingIntent) + } + ) + } + ) + } + + else -> Unit + } +} + +@Preview +@Composable +private fun StadiumDetailPictureTopScreenPreview() { + StadiumDetailPictureTopScreen( + context = LocalContext.current, + reviewId = 1, + reviewIndex = 0, + bottomPadding = 0f, + isFirstLike = false, + uiState = StadiumDetailUiState.Empty, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/top/StadiumDetailPictureMainScreen.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/top/StadiumDetailPictureMainScreen.kt new file mode 100644 index 00000000..c6e6c345 --- /dev/null +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/compose/detailpicture/top/StadiumDetailPictureMainScreen.kt @@ -0,0 +1,138 @@ +package com.dpm.presentation.viewfinder.compose.detailpicture.top + +import android.content.Context +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import com.dpm.presentation.util.KakaoUtils +import com.dpm.presentation.util.kakaoShareSeatFeedTitle +import com.dpm.presentation.util.seatFeed +import com.dpm.presentation.viewfinder.compose.detailpicture.StadiumDetailReviewViewPager +import com.dpm.presentation.viewfinder.uistate.StadiumDetailUiState +import com.dpm.presentation.viewfinder.viewmodel.StadiumDetailViewModel + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun StadiumDetailPictureMainScreen( + context: Context, + reviewId: Long, + reviewIndex: Int, + bottomPadding: Float, + isFirstLike: Boolean, + uiState: StadiumDetailUiState, + modifier: Modifier = Modifier, + stadiumDetailViewModel: StadiumDetailViewModel = viewModel(), + onClickLike: () -> Unit = {}, + onClickScrap: (id: Long) -> Unit = {}, + onClickShare: () -> Unit = {} +) { + when(uiState) { + is StadiumDetailUiState.ReviewsData -> { + val visited = remember { + mutableStateListOf( + *List(uiState.reviews.size) { false }.toTypedArray() + ) + } + + if (uiState.reviews.size - visited.size > 0) { + visited.addAll(List(uiState.reviews.size - visited.size) { false }) + } + + val initPage by remember { + mutableStateOf(uiState.reviews.indexOfFirst { it.id == reviewId }) + } + + val pagerState = rememberPagerState( + pageCount = { uiState.reviews.size }, + initialPage = initPage + ) + + var pageIndex by remember { + mutableStateOf(0) + } + + var isFirstLikeState by remember { + mutableStateOf(isFirstLike) + } + + LaunchedEffect(key1 = pagerState) { + snapshotFlow { pagerState.currentPage }.collect { + pageIndex = it + stadiumDetailViewModel.updateCurrentIndex(it) + if (visited[it]) return@collect + + if (it == initPage) { + visited[it] = true + } + + if (!visited[it]) { + visited[it] = true + } + } + } + + StadiumDetailReviewViewPager( + context = context, + reviews = uiState.reviews, + visited = visited, + position = reviewIndex, + isFirstLike = isFirstLikeState, + hasNext = uiState.hasNext, + pagerState = pagerState, + pageIndex = pageIndex, + bottomPadding = bottomPadding, + modifier = modifier, + onLoadPaging = stadiumDetailViewModel::getBlockReviews, + onClickLike = { id -> + onClickLike() + isFirstLikeState = false + stadiumDetailViewModel.updateLike(id) + }, + onClickScrap = onClickScrap, + onClickShare = { imagePosition -> + onClickShare() + KakaoUtils().share( + context, + seatFeed( + title = uiState.kakaoShareSeatFeedTitle(pageIndex), + description = "출처 : ${uiState.reviews[pageIndex].member.nickname}", + imageUrl = uiState.reviews[pageIndex].images[imagePosition].url, + queryParams = mapOf( + "stadiumId" to stadiumDetailViewModel.stadiumId.toString(), + "blockCode" to stadiumDetailViewModel.blockCode + ) + ), + onSuccess = { sharingIntent -> + context.startActivity(sharingIntent) + } + ) + } + ) + } + else -> Unit + } +} + +@Preview +@Composable +private fun StadiumDetailPictureMainScreenPreview() { + StadiumDetailPictureMainScreen( + context = LocalContext.current, + reviewId = 1, + reviewIndex = 0, + bottomPadding = 0f, + isFirstLike = false, + uiState = StadiumDetailUiState.Empty, + ) +} \ No newline at end of file diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/uistate/StadiumDetailUiState.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/uistate/StadiumDetailUiState.kt index 7d5d7edb..21cbffde 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/uistate/StadiumDetailUiState.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/uistate/StadiumDetailUiState.kt @@ -4,7 +4,7 @@ import com.dpm.domain.entity.response.viewfinder.ResponseBlockReview sealed class StadiumDetailUiState { data class ReviewsData( - val topReviewImages: List, + val topReviewImages: List, val stadiumContent: ResponseBlockReview.ResponseLocation, val keywords: List, val total: Long, diff --git a/presentation/src/main/java/com/dpm/presentation/viewfinder/viewmodel/StadiumDetailViewModel.kt b/presentation/src/main/java/com/dpm/presentation/viewfinder/viewmodel/StadiumDetailViewModel.kt index 63610275..965fb723 100644 --- a/presentation/src/main/java/com/dpm/presentation/viewfinder/viewmodel/StadiumDetailViewModel.kt +++ b/presentation/src/main/java/com/dpm/presentation/viewfinder/viewmodel/StadiumDetailViewModel.kt @@ -64,6 +64,66 @@ class StadiumDetailViewModel @Inject constructor( _scrollState.value = state } + fun updateLike(id: Long) { + viewModelScope.launch { + viewfinderRepository.updateLike(id.toInt()) + _detailUiState.value = when (val currentState = _detailUiState.value) { + is StadiumDetailUiState.ReviewsData -> { + val updatedReviews = currentState.reviews.map { review -> + if (review.id == id) { + if (review.isLike) { + review.copy( + isLike = !review.isLike, + likesCount = review.likesCount - 1 + ) + } else { + review.copy( + isLike = !review.isLike, + likesCount = review.likesCount + 1 + ) + } + } else { + review + } + } + currentState.copy(reviews = updatedReviews) + } + + else -> currentState + } + } + } + + fun updateScrap(id: Long) { + viewModelScope.launch { + viewfinderRepository.updateScrap(id.toInt()) + _detailUiState.value = when (val currentState = _detailUiState.value) { + is StadiumDetailUiState.ReviewsData -> { + val updatedReviews = currentState.reviews.map { review -> + if (review.id == id) { + if (review.isScrap) { + review.copy( + isScrap = !review.isScrap, + scrapsCount = review.scrapsCount - 1 + ) + } else { + review.copy( + isScrap = !review.isScrap, + scrapsCount = review.scrapsCount + 1 + ) + } + } else { + review + } + } + currentState.copy(reviews = updatedReviews) + } + + else -> currentState + } + } + } + fun updateMonth(month: Int?) { if (month != _reviewFilter.value.month) { reset = true @@ -80,8 +140,11 @@ class StadiumDetailViewModel @Inject constructor( } fun updateSort(sortBy: String) { - _reviewFilter.value = _reviewFilter.value.copy(sortBy = sortBy) - getBlockReviews(query = _reviewFilter.value) + if (_reviewFilter.value.sortBy != sortBy) { + reset = true + _reviewFilter.value = _reviewFilter.value.copy(sortBy = sortBy, cursor = null) + getBlockReviews(query = _reviewFilter.value) + } } fun updateRequestPathVariable(stadiumId: Int, blockCode: String) { @@ -108,6 +171,7 @@ class StadiumDetailViewModel @Inject constructor( (_detailUiState.value as StadiumDetailUiState.ReviewsData) if (reset) { + reset = false _detailUiState.value = reviewsData.copy( reviews = blockReviews.reviews, hasNext = blockReviews.hasNext, @@ -153,6 +217,15 @@ class StadiumDetailViewModel @Inject constructor( } } + fun checkScrap(id: Long): Boolean { + (_detailUiState.value as StadiumDetailUiState.ReviewsData).reviews.map { review -> + if (review.id == id && review.isScrap) { + return true + } + } + return false + } + fun handleColumNumber( column: Int, number: Int, diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index f27e6516..c55d93e7 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -89,7 +89,9 @@ 꾸준히 다른 구장에 대한 정보가 추가될 예정이에요 화면을 손가락으로 확대하여\n구역을 더 크게 보세요! 현재 잠실야구장만 이용할 수 있어요! + 스크랩이 완료되었어요! ""잠실야구장 보기"" + ""스크랩으로 이동"" 다른 블록에서 새로운 시야를 찾아보세요! 열과 번을 선택해 빠르게 자리를 찾아보세요⚡