Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 상품 상세 뷰 구현 #27

Merged
merged 19 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.sopt.kream.data.datasource

import org.sopt.kream.data.model.response.ResponseProductDetailDto
import org.sopt.kream.data.model.response.ResponseSearchProductDto
import org.sopt.kream.util.base.BaseResponse

interface ProductRemoteDataSource {
suspend fun getSearchProduct(findName: String): BaseResponse<ResponseSearchProductDto>

suspend fun getProductDetail(productId: Int): BaseResponse<ResponseProductDetailDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package org.sopt.kream.data.datasourceimpl

import org.sopt.kream.data.ServicePool
import org.sopt.kream.data.datasource.ProductRemoteDataSource
import org.sopt.kream.data.model.response.ResponseProductDetailDto
import org.sopt.kream.data.model.response.ResponseSearchProductDto
import org.sopt.kream.util.base.BaseResponse

class ProductRemoteDataSourceImpl : ProductRemoteDataSource {
private val productService = ServicePool.productService

override suspend fun getSearchProduct(findName: String): BaseResponse<ResponseSearchProductDto> = productService.getSearchProduct(findName = findName)

override suspend fun getProductDetail(productId: Int): BaseResponse<ResponseProductDetailDto> = productService.getProductDetail(productId = productId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.sopt.kream.data.mapper

import org.sopt.kream.data.model.response.ResponseProductDetailDto
import org.sopt.kream.domain.model.ProductDetailModel

fun ResponseProductDetailDto.toProductDetailModel() =
ProductDetailModel(
thumbnailUrl = this.thumbnailUrl,
price = this.price,
engTitle = this.engTitle,
title = this.title,
recentPrice = this.recentPrice,
variablePrice = this.variablePrice,
variablePercent = this.variablePercent,
releasePrice = this.releasePrice,
modelNumber = this.modelNumber,
releaseDate = this.releaseDate,
styleCount = this.styleCount,
styles =
this.styles.map { responseProductDetailStyleDto ->
responseProductDetailStyleDto.toProductDetailStyleModel()
},
isScrap = this.isScrap,
scrapCount = this.scrapCount,
cellPrice = this.cellPrice,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.kream.data.mapper

import org.sopt.kream.data.model.response.ResponseProductDetailDto.ResponseProductDetailStyleDto
import org.sopt.kream.domain.model.ProductDetailStyleModel

fun ResponseProductDetailStyleDto.toProductDetailStyleModel() =
ProductDetailStyleModel(
imageUrl = this.imageUrl,
isVideo = this.isVideo,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.sopt.kream.data.model.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ResponseProductDetailDto(
@SerialName("thumbnailUrl")
val thumbnailUrl: String,
@SerialName("price")
val price: String,
@SerialName("engTitle")
val engTitle: String,
@SerialName("title")
val title: String,
@SerialName("recentPrice")
val recentPrice: String,
@SerialName("variablePrice")
val variablePrice: String,
@SerialName("variablePercent")
val variablePercent: String,
@SerialName("releasePrice")
val releasePrice: String,
@SerialName("modelNumber")
val modelNumber: String,
@SerialName("releaseDate")
val releaseDate: String,
@SerialName("styleCount")
val styleCount: String,
@SerialName("styles")
val styles: List<ResponseProductDetailStyleDto>,
@SerialName("isScrap")
val isScrap: Boolean,
@SerialName("scrapCount")
val scrapCount: String,
@SerialName("cellPrice")
val cellPrice: String,
) {
@Serializable
data class ResponseProductDetailStyleDto(
@SerialName("imageUrl")
val imageUrl: String,
@SerialName("isVideo")
val isVideo: Boolean,
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.sopt.kream.data.repository

import org.sopt.kream.data.datasource.ProductRemoteDataSource
import org.sopt.kream.data.mapper.toProductDetailModel
import org.sopt.kream.data.mapper.toSearchProductModel
import org.sopt.kream.domain.model.ProductDetailModel
import org.sopt.kream.domain.model.SearchProductModel
import org.sopt.kream.domain.repository.ProductRepository

Expand All @@ -12,4 +14,9 @@ class ProductRepositoryImpl(
runCatching {
productRemoteDataSource.getSearchProduct(findName = findName).data.toSearchProductModel()
}

override suspend fun getProductDetail(productId: Int): Result<ProductDetailModel> =
runCatching {
productRemoteDataSource.getProductDetail(productId = productId).data.toProductDetailModel()
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/org/sopt/kream/data/service/ProductService.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.sopt.kream.data.service

import org.sopt.kream.data.model.response.ResponseProductDetailDto
import org.sopt.kream.data.model.response.ResponseReleaseProductDto
import org.sopt.kream.data.model.response.ResponseSearchProductDto
import org.sopt.kream.util.base.BaseResponse
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query

interface ProductService {
Expand All @@ -14,6 +16,12 @@ interface ProductService {
@Query("findName") findName: String,
): BaseResponse<ResponseSearchProductDto>

@GET("product/{productId}")
suspend fun getProductDetail(
@Header("memberId") memberId: Int = MEMBER_ID,
@Path("productId") productId: Int,
): BaseResponse<ResponseProductDetailDto>

@GET("product/release")
suspend fun getReleaseProduct(
@Header("memberId") userid: Int,
Expand All @@ -23,4 +31,8 @@ interface ProductService {
suspend fun deleteScrap(
@Header("memberId") userid: Int,
): BaseResponse<ResponseReleaseProductDto>

companion object {
const val MEMBER_ID = 1
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 여기다가 상수로 선언해주니까 좋네요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

크크 상수화 잘 하면 가독성이 더 높아지는 것 같숨니다

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.sopt.kream.domain.model

data class ProductDetailModel(
val thumbnailUrl: String,
val price: String,
val engTitle: String,
val title: String,
val recentPrice: String,
val variablePrice: String,
val variablePercent: String,
val releasePrice: String,
val modelNumber: String,
val releaseDate: String,
val styleCount: String,
val styles: List<ProductDetailStyleModel>,
val isScrap: Boolean,
val scrapCount: String,
val cellPrice: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.sopt.kream.domain.model

data class ProductDetailStyleModel(
val imageUrl: String,
val isVideo: Boolean,
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.sopt.kream.domain.repository

import org.sopt.kream.domain.model.ProductDetailModel
import org.sopt.kream.domain.model.SearchProductModel

interface ProductRepository {
suspend fun getSearchProduct(findName: String): Result<SearchProductModel>

suspend fun getProductDetail(productId: Int): Result<ProductDetailModel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.sopt.kream.data.datasourceimpl.ProductRemoteDataSourceImpl
import org.sopt.kream.data.repository.DummyRepositoryImpl
import org.sopt.kream.data.repository.ProductRepositoryImpl
import org.sopt.kream.presentation.ui.dummy.DummyViewModel
import org.sopt.kream.presentation.ui.productdetail.ProductDetailViewModel
import org.sopt.kream.presentation.ui.search.SearchViewModel

class ViewModelFactory : ViewModelProvider.Factory {
Expand All @@ -15,6 +16,8 @@ class ViewModelFactory : ViewModelProvider.Factory {
return DummyViewModel(DummyRepositoryImpl(DummyRemoteDataSourceImpl())) as T
} else if (modelClass.isAssignableFrom(SearchViewModel::class.java)) {
return SearchViewModel(ProductRepositoryImpl(ProductRemoteDataSourceImpl())) as T
} else if (modelClass.isAssignableFrom(ProductDetailViewModel::class.java)) {
return ProductDetailViewModel(ProductRepositoryImpl(ProductRemoteDataSourceImpl())) as T
}
throw IllegalArgumentException("Unknown ViewModel Class")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.kream.presentation.ui.model

import org.sopt.kream.presentation.ui.type.ProductDetailInfoType

data class ProductDetailInfo(
val productDetailInfoType: ProductDetailInfoType,
val content: String,
val additionalContent: String? = null,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,120 @@
package org.sopt.kream.presentation.ui.productdetail

import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import coil.load
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.sopt.kream.R
import org.sopt.kream.databinding.FragmentProductDetailBinding
import org.sopt.kream.presentation.common.ViewModelFactory
import org.sopt.kream.presentation.ui.model.ProductDetailInfo
import org.sopt.kream.presentation.ui.search.SearchFragment.Companion.PRODUCT_ID
import org.sopt.kream.presentation.ui.type.ProductDetailButtonType
import org.sopt.kream.presentation.ui.type.ProductDetailInfoType
import org.sopt.kream.util.base.BindingFragment
import org.sopt.kream.util.component.KreamProductDetailStyleImageView
import org.sopt.kream.util.view.UiState

class ProductDetailFragment : BindingFragment<FragmentProductDetailBinding>({ FragmentProductDetailBinding.inflate(it) })
class ProductDetailFragment : BindingFragment<FragmentProductDetailBinding>({ FragmentProductDetailBinding.inflate(it) }) {
private val productDetailViewModel: ProductDetailViewModel by viewModels { ViewModelFactory() }
private lateinit var productDetailInfoAdapter: ProductDetailInfoAdapter

override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)

productDetailViewModel.getProductDetail(getProductId() + 1)
initLayout()
initAdapter()
setIvProductDetailBack()
collectProductDetailState()
}

private fun initLayout() {
with(binding) {
btnProductDetailBottomPurchase.setProductDetailButtonType(ProductDetailButtonType.PURCHASE)
btnProductDetailBottomSale.setProductDetailButtonType(ProductDetailButtonType.SALE)
}
}

private fun initAdapter() {
productDetailInfoAdapter = ProductDetailInfoAdapter()
binding.rvProductDetail.adapter = productDetailInfoAdapter
}

private fun setIvProductDetailBack() {
binding.ivProductDetailBack.setOnClickListener {
findNavController().popBackStack()
}
}

private fun collectProductDetailState() {
productDetailViewModel.productDetailState.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach { productDetailState ->
when (productDetailState) {
is UiState.Success -> {
with(binding) {
ivProductDetailThumbnail.load(productDetailState.data.thumbnailUrl)
tvProductDetailPrice.text = productDetailState.data.price
tvProductDetailEngTitle.text = productDetailState.data.engTitle
tvProductDetailTitle.text = productDetailState.data.title
tvProductDetailStyle.text = getString(R.string.product_style, productDetailState.data.styleCount)
ivProductDetailBottomScrap.setImageResource(if (productDetailState.data.isScrap) R.drawable.ic_saved_1_on_24 else R.drawable.ic_saved_1_off_24)
tvProductDetailBottomScrap.text = productDetailState.data.scrapCount
btnProductDetailBottomPurchase.priceTextView.text = productDetailState.data.price
btnProductDetailBottomSale.priceTextView.text = productDetailState.data.cellPrice

val productDetailStyleList: List<KreamProductDetailStyleImageView> = listOf(ivProductDetailStyleFirst, ivProductDetailStyleSecond, ivProductDetailStyleThird, ivProductDetailStyleFourth, ivProductDetailStyleFifth, ivProductDetailStyleSixth, ivProductDetailStyleSeventh, ivProductDetailStyleEight, ivProductDetailStyleNinth)

productDetailState.data.styles.onEachIndexed { index, productDetailStyleModel ->
productDetailStyleList[index].setImageViewData(productDetailStyleModel = productDetailStyleModel, isLast = index == (productDetailState.data.styles.size - 1))
}

if (productDetailState.data.styles.isEmpty()) {
viewProductDetailDeliveryInfo.background = null
tvProductDetailStyle.visibility = View.GONE
layoutProductDetailStyleUpload.visibility = View.GONE
tvProductDetailStyleMore.visibility = View.GONE
productDetailStyleList.onEach { kreamProductDetailStyleImageView ->
kreamProductDetailStyleImageView.visibility = View.GONE
}
}

productDetailInfoAdapter.submitList(
listOf(
ProductDetailInfo(
productDetailInfoType = ProductDetailInfoType.RECENT_PRICE,
content = productDetailState.data.recentPrice,
additionalContent = productDetailState.data.variablePrice + productDetailState.data.variablePercent,
),
ProductDetailInfo(
productDetailInfoType = ProductDetailInfoType.RELEASE_PRICE,
content = productDetailState.data.releasePrice,
),
ProductDetailInfo(
productDetailInfoType = ProductDetailInfoType.MODEL_NUMBER,
content = productDetailState.data.modelNumber,
),
ProductDetailInfo(
productDetailInfoType = ProductDetailInfoType.RELEASE_DATE,
content = productDetailState.data.releaseDate,
),
),
)
}
}

else -> Unit
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun getProductId(): Int = requireArguments().getInt(PRODUCT_ID)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.sopt.kream.presentation.ui.productdetail

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import org.sopt.kream.databinding.ItemProductDetailInfoBinding
import org.sopt.kream.presentation.ui.model.ProductDetailInfo
import org.sopt.kream.util.view.ItemDiffCallback

class ProductDetailInfoAdapter() : ListAdapter<ProductDetailInfo, ProductDetailInfoViewHolder>(
ItemDiffCallback<ProductDetailInfo>(
onContentsTheSame = { old, new -> old == new },
onItemsTheSame = { old, new -> old.content == new.content },
),
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ProductDetailInfoViewHolder =
ProductDetailInfoViewHolder(
ItemProductDetailInfoBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
),
parent.context,
)

override fun onBindViewHolder(
holder: ProductDetailInfoViewHolder,
position: Int,
) {
holder.onBind(currentList[position], currentList.size - 1 == position)
}
}
Loading
Loading