diff --git a/app/src/main/java/com/wafflestudio/siksha2/models/Review.kt b/app/src/main/java/com/wafflestudio/siksha2/models/Review.kt index 632f7e127..fc91e395c 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/models/Review.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/models/Review.kt @@ -10,7 +10,12 @@ data class Review( @Json(name = "user_id") val userId: Long, @Json(name = "score") val score: Double, @Json(name = "comment") val comment: String?, + @Json(name = "keyword_reviews") val keywordReviews: List?, + @Json(name = "is_liked") val isLiked: Boolean?, + @Json(name = "name_kr") val nameKr: String?, + @Json(name = "name_en") val nameEn: String?, @Json(name = "created_at") val createdAt: String, + @Json(name = "updated_at") val updatedAt: String, @Json(name = "etc") val etc: Etc? ) diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt b/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt index b0d534e1f..d476f1df9 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt @@ -46,6 +46,24 @@ interface SikshaApi { @POST("/reviews") suspend fun leaveMenuReview(@Body req: LeaveReviewParam): NetworkResult + @PATCH("/reviews") + suspend fun updateReviews( + @Query("menu_id") menuId: Long, + @Query("page") page: Long, + @Query("per_page") perPage: Long + ): NetworkResult + + @DELETE("/reviews/{review_id}") + suspend fun deleteReviews( + @Path("review_id") reviewId: Long + ): Response + + @GET("/reviews/me") + suspend fun fetchMyReviews( + @Query("page") page: Long, + @Query("perPage") perPage: Long + ): NetworkResult + @Multipart @POST("/reviews/images") suspend fun leaveMenuReviewImages( diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/dto/FetchMyReviewsResult.kt b/app/src/main/java/com/wafflestudio/siksha2/network/dto/FetchMyReviewsResult.kt new file mode 100644 index 000000000..9b6a61ded --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/dto/FetchMyReviewsResult.kt @@ -0,0 +1,20 @@ +package com.wafflestudio.siksha2.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.wafflestudio.siksha2.models.Review + +@JsonClass(generateAdapter = true) +data class ReviewRestaurant( + @Json(name = "restaurant_id") val restaurantId: Long, + @Json(name = "name_kr") val nameKr: String?, + @Json(name = "name_en") val nameEn: String?, + @Json(name = "reviews") val reviews: List +) + +@JsonClass(generateAdapter = true) +data class FetchMyReviewsResult( + @Json(name = "total_count") val totalCount: Int, + @Json(name = "has_next") val hasNext: Boolean, + @Json(name = "result") val result: List +) diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt index 740a66598..f25d5675a 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt @@ -13,7 +13,9 @@ import com.wafflestudio.siksha2.network.dto.FetchReviewDistributionResult import com.wafflestudio.siksha2.network.dto.FetchReviewsResult import com.wafflestudio.siksha2.network.dto.LeaveReviewParam import com.wafflestudio.siksha2.network.dto.LeaveReviewResult +import com.wafflestudio.siksha2.network.dto.ReviewRestaurant import com.wafflestudio.siksha2.network.result.NetworkResult +import com.wafflestudio.siksha2.ui.menuDetail.MenuMyReviewPagingSource import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewPagingSource import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewWithImagePagingSource import com.wafflestudio.siksha2.utils.toLocalDate @@ -80,6 +82,18 @@ class MenuRepository @Inject constructor( ).flow } + fun getMyPagedReviewsByMenuIdFlow(): Flow> { + return Pager( + config = MenuMyReviewPagingSource.Config, + pagingSourceFactory = { MenuMyReviewPagingSource(sikshaApi) } + ).flow + } + + suspend fun deleteReview(reviewId: Long): Boolean { + val response = sikshaApi.deleteReviews(reviewId) + return response.isSuccessful + } + fun getPagedReviewsOnlyHaveImagesByMenuIdFlow(menuId: Long): Flow> { return Pager( config = MenuReviewWithImagePagingSource.Config, diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingFragment.kt index cc58b3782..0a4e6311a 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingFragment.kt @@ -70,6 +70,12 @@ class SettingFragment : Fragment() { findNavController().navigate(action) } + binding.myReviewRow.setOnClickListener { + val action = + MainFragmentDirections.actionMainFragmentToMyReviewFragment() + findNavController().navigate(action) + } + binding.orderRestaurantRow.setOnClickListener { val action = MainFragmentDirections.actionMainFragmentToReorderRestaurantFragment() diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt index 45625d825..6b167cb32 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt @@ -2,8 +2,10 @@ package com.wafflestudio.siksha2.ui.menuDetail import android.Manifest import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore @@ -30,7 +32,12 @@ import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.utils.hasFinalConsInKr import com.wafflestudio.siksha2.utils.setVisibleOrGone import com.wafflestudio.siksha2.utils.showToast +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.net.HttpURLConnection +import java.net.URL class LeaveReviewFragment : Fragment() { private lateinit var binding: FragmentLeaveReviewBinding @@ -93,6 +100,32 @@ class LeaveReviewFragment : Fragment() { } } + vm.editingReview.observe(viewLifecycleOwner) { review -> + review ?: return@observe + + // 평점 + vm.setReviewRating(review.score.toFloat()) + binding.rateText.text = review.score.toInt().toString() + + // 코멘트 세팅 + binding.commentEdit.setText(review.comment ?: "") + + // Todo : keyword 세팅, patch api 연결 + + // 이미지(url) → Uri 로 변환 + review.etc?.images?.let { urls -> + lifecycleScope.launch { + urls.forEach { url: String -> + val file = downloadImageToFile(requireContext(), url) + file?.let { + val uri = Uri.fromFile(it) + vm.addImageUri(uri, onFailure = {}) + } + } + } + } + } + vm.commentHint.observe(viewLifecycleOwner) { hint -> binding.commentEdit.hint = hint } @@ -193,6 +226,32 @@ class LeaveReviewFragment : Fragment() { } } + suspend fun downloadImageToFile( + context: Context, + imageUrl: String + ): File? = withContext(Dispatchers.IO) { + return@withContext try { + val url = URL(imageUrl) + val connection = url.openConnection() as HttpURLConnection + connection.connect() + + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + return@withContext null + } + + val input = connection.inputStream + val tempFile = File.createTempFile("review_img_", ".jpg", context.cacheDir) + + tempFile.outputStream().use { output -> + input.copyTo(output) + } + + tempFile + } catch (e: Exception) { + null + } + } + private fun requestPermission(onGranted: () -> Unit) { if (Build.VERSION.SDK_INT >= 33) { onGranted() diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt index 2ce98b2a2..dee5ab55b 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt @@ -12,6 +12,7 @@ import androidx.paging.PagingData import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.network.dto.LeaveReviewResult +import com.wafflestudio.siksha2.network.dto.ReviewRestaurant import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.MenuRepository import com.wafflestudio.siksha2.utils.ImageUtil @@ -65,6 +66,18 @@ class MenuDetailViewModel @Inject constructor( val reviewRating: FloatState get() = _reviewRating + private val _deleteResult = MutableLiveData() + val deleteResult: LiveData + get() = _deleteResult + + private val _editingReview = MutableLiveData() + val editingReview: LiveData + get() = _editingReview + + fun setEditingReview(review: Review?) { + _editingReview.value = review + } + fun refreshMenu(menuId: Long) { _networkResultState.value = State.LOADING viewModelScope.launch { @@ -103,10 +116,21 @@ class MenuDetailViewModel @Inject constructor( } } + fun deleteReview(id: Long) { + viewModelScope.launch { + val success = menuRepository.deleteReview(id) + _deleteResult.postValue(success) + } + } + fun getReviews(menuId: Long): Flow> { return menuRepository.getPagedReviewsByMenuIdFlow(menuId) } + fun getMyReviews(): Flow> { + return menuRepository.getMyPagedReviewsByMenuIdFlow() + } + fun getReviewsWithImages(menuId: Long): Flow> { return menuRepository.getPagedReviewsOnlyHaveImagesByMenuIdFlow(menuId) } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewAdapter.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewAdapter.kt new file mode 100644 index 000000000..ad769a9b2 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewAdapter.kt @@ -0,0 +1,76 @@ +package com.wafflestudio.siksha2.ui.menuDetail + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.wafflestudio.siksha2.databinding.ItemMyReviewRestaurantBinding +import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.network.dto.ReviewRestaurant + +class MenuMyReviewAdapter( + private val onMenuClick: (Review) -> Unit, + private val onEditClick: (Review) -> Unit, + private val onDeleteClick: (Review) -> Unit +) : + PagingDataAdapter(DiffCallback) { + + companion object { + private val DiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ReviewRestaurant, newItem: ReviewRestaurant): Boolean { + return oldItem.restaurantId == newItem.restaurantId + } + + override fun areContentsTheSame(oldItem: ReviewRestaurant, newItem: ReviewRestaurant): Boolean { + return oldItem == newItem + } + } + } + + inner class RestaurantViewHolder(private val binding: ItemMyReviewRestaurantBinding) : + RecyclerView.ViewHolder(binding.root) { + + private var isExpanded = false + + fun bind(item: ReviewRestaurant) { + if (bindingAdapterPosition == 0) { + isExpanded = true + binding.topDivider.isVisible = true + } + + binding.restaurantName.text = item.nameKr + binding.arrowIcon.rotation = if (isExpanded) 180f else 0f + + // 리뷰 어댑터 (Nested RecyclerView) + binding.reviewRecycler.layoutManager = LinearLayoutManager(binding.root.context) + + val reviewAdapter = MenuMyReviewChildAdapter(onMenuClick, onEditClick, onDeleteClick) + binding.reviewRecycler.adapter = reviewAdapter + + reviewAdapter.submitList(item.reviews) + + // 펼치기/접기 + binding.root.setOnClickListener { + isExpanded = !isExpanded + binding.arrowIcon.animate().rotation(if (isExpanded) 180f else 0f).start() + binding.reviewRecycler.isVisible = isExpanded + binding.topDivider.isVisible = isExpanded + } + + // 초기에는 접혀있게 + binding.reviewRecycler.isVisible = isExpanded + } + } + + override fun onBindViewHolder(holder: RestaurantViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestaurantViewHolder { + val binding = ItemMyReviewRestaurantBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return RestaurantViewHolder(binding) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewChildAdapter.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewChildAdapter.kt new file mode 100644 index 000000000..d71a17d5a --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewChildAdapter.kt @@ -0,0 +1,174 @@ +package com.wafflestudio.siksha2.ui.menuDetail + +import android.app.AlertDialog +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.databinding.DialogDefaultBinding +import com.wafflestudio.siksha2.databinding.ItemMyReviewBinding +import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.utils.toLocalDateTime +import com.wafflestudio.siksha2.utils.toParsedTimeString + +class MenuMyReviewChildAdapter( + private val onMenuClick: (Review) -> Unit, + private val onEditClick: (Review) -> Unit, + private val onDeleteClick: (Review) -> Unit +) : ListAdapter(DiffCallback) { + + companion object { + private val DiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Review, newItem: Review): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Review, newItem: Review): Boolean { + return oldItem == newItem + } + } + } + + inner class ReviewViewHolder(private val binding: ItemMyReviewBinding) : + RecyclerView.ViewHolder(binding.root) { + + private val imageViews by lazy { + listOf( + binding.reviewImage1, + binding.reviewImage2, + binding.reviewImage3, + binding.reviewImage4, + binding.reviewImage5 + ) + } + + private val tagViews = listOf( + binding.reviewTag1, + binding.reviewTag2, + binding.reviewTag3 + ) + + fun bind(item: Review) { + binding.menuName.text = item.nameKr ?: "" + setRatingStars(binding.starsContainer, item.score.toFloat()) + binding.reviewDate.text = item.createdAt.toLocalDateTime().toParsedTimeString() + binding.reviewContent.text = item.comment ?: "내용 없음" + + val tags = item.keywordReviews.orEmpty() + + tagViews.forEachIndexed { index, view -> + val text = tags.getOrNull(index) + + if (text.isNullOrBlank()) { + view.visibility = View.GONE + } else { + view.text = text + view.visibility = View.VISIBLE + } + } + + // ✅ 이미지 + val urls = item.etc?.images.orEmpty() + imageViews.forEachIndexed { index, imageView -> + if (index < urls.size) { + imageView.visibility = View.VISIBLE + Glide.with(imageView.context) + .load(urls[index]) + .centerCrop() + .into(imageView) + } else { + imageView.visibility = View.GONE + } + } + + binding.reviewHeader.setOnClickListener { + onMenuClick(item) + } + + binding.deleteButton.setOnClickListener { + showDeleteDialog(binding.root.context) { + onDeleteClick(item) + } + } + + binding.editButton.setOnClickListener { + onEditClick(item) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReviewViewHolder { + val binding = ItemMyReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ReviewViewHolder(binding) + } + + override fun onBindViewHolder(holder: ReviewViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private fun setRatingStars(container: LinearLayout, rating: Float, maxStars: Int = 5) { + container.removeAllViews() // 기존 별 제거 + val fullStar = R.drawable.ic_full_star + val halfStar = R.drawable.ic_half_star + val emptyStar = R.drawable.ic_empty_star + + // 별 폭과 높이는 container의 높이 기준으로 맞추기 + val starSize = container.height.takeIf { it > 0 } ?: LinearLayout.LayoutParams.WRAP_CONTENT + + for (i in 1..maxStars) { + val imageView = ImageView(container.context).apply { + val starRes = when { + i <= rating.toInt() -> fullStar // 정수 부분: 꽉 찬 별 + i == rating.toInt() + 1 && rating % 1 >= 0.5 -> halfStar // 소수점 0.5 이상이면 반 별 + else -> emptyStar // 나머지는 빈 별 + } + + setImageResource(starRes) + + layoutParams = LinearLayout.LayoutParams( + 0, + starSize, + 1f + ).apply { + marginEnd = 0 + } + scaleType = ImageView.ScaleType.FIT_CENTER + } + container.addView(imageView) + } + } + + private fun showDeleteDialog(context: android.content.Context, onDelete: () -> Unit) { + val dialogBinding = DialogDefaultBinding.inflate(LayoutInflater.from(context)) + + val dialog = AlertDialog.Builder(context) + .setView(dialogBinding.root) + .create() + + // 타이틀, 내용 + dialogBinding.dialogTitle.text = "평가 삭제" + dialogBinding.dialogContent.text = "평가를 정말 삭제하시겠습니까?" + + // 버튼 이벤트 + dialogBinding.tvPositiveButton.text = "삭제" + dialogBinding.tvNegativeButton.text = "취소" + + dialogBinding.tvPositiveButton.setOnClickListener { + dialog.dismiss() + onDelete() + } + + dialogBinding.tvNegativeButton.setOnClickListener { + dialog.dismiss() + } + + dialog.window?.setBackgroundDrawableResource(android.R.color.transparent) + dialog.show() + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewPagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewPagingSource.kt new file mode 100644 index 000000000..9110f0326 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuMyReviewPagingSource.kt @@ -0,0 +1,37 @@ +package com.wafflestudio.siksha2.ui.menuDetail + +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.dto.ReviewRestaurant +import com.wafflestudio.siksha2.network.result.NetworkResult + +class MenuMyReviewPagingSource( + private val api: SikshaApi +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val key = params.key ?: STARTING_PAGE_INDEX + return when (val response = api.fetchMyReviews(key, params.loadSize.toLong())) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result, + prevKey = if (key == 1L) null else key - 1, + nextKey = if (response.body.result.isEmpty()) null else if (key == STARTING_PAGE_INDEX) key + params.loadSize / PAGE_LOAD_SIZE else key + 1 + ) + } + else -> LoadResult.Error(RuntimeException("")) + } + } + + override fun getRefreshKey(state: PagingState): Long { + return STARTING_PAGE_INDEX + } + + companion object { + const val STARTING_PAGE_INDEX = 1L + private const val PAGE_LOAD_SIZE = 7 + val Config = PagingConfig(pageSize = PAGE_LOAD_SIZE) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/MyReviewFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/MyReviewFragment.kt new file mode 100644 index 000000000..365fe3a6a --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/MyReviewFragment.kt @@ -0,0 +1,96 @@ +package com.wafflestudio.siksha2.ui.menuDetail.review + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.paging.LoadState +import androidx.recyclerview.widget.LinearLayoutManager +import com.wafflestudio.siksha2.databinding.FragmentMyReviewBinding +import com.wafflestudio.siksha2.ui.menuDetail.MenuDetailViewModel +import com.wafflestudio.siksha2.ui.menuDetail.MenuMyReviewAdapter +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId +import kotlin.getValue + +class MyReviewFragment : Fragment() { + private lateinit var binding: FragmentMyReviewBinding + private val vm: MenuDetailViewModel by activityViewModels() + private lateinit var reviewsAdapter: MenuMyReviewAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentMyReviewBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Adapter 초기화 + reviewsAdapter = MenuMyReviewAdapter( + onMenuClick = { review -> + val action = MyReviewFragmentDirections.actionMyReviewFragmentToMenuDetailFragment( + review.menuId, + Instant.parse(review.createdAt).atZone(ZoneId.systemDefault()).toLocalDate() == LocalDate.now() + ) + findNavController().navigate(action) + }, + onEditClick = { review -> + vm.refreshMenu(review.menuId) + vm.setEditingReview(review) + + val action = MyReviewFragmentDirections + .actionMyReviewFragmentToLeaveReviewFragment() + findNavController().navigate(action) + }, + onDeleteClick = { review -> + vm.deleteReview(review.id) + } + ) + + // RecyclerView 연결 + binding.myReviewList.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = reviewsAdapter + } + + viewLifecycleOwner.lifecycleScope.launch { + vm.getMyReviews().collectLatest { pagingData -> + reviewsAdapter.submitData(pagingData) + } + } + + // 로딩 / 에러 상태 UI 처리 (optional) + viewLifecycleOwner.lifecycleScope.launch { + reviewsAdapter.loadStateFlow.collectLatest { loadStates -> + binding.textNoReviews.isVisible = loadStates.refresh is LoadState.Error + } + } + + vm.deleteResult.observe(viewLifecycleOwner) { success -> + if (success) { + Toast.makeText(requireContext(), "삭제 완료", Toast.LENGTH_SHORT).show() + reviewsAdapter.refresh() + } else { + Toast.makeText(requireContext(), "삭제 실패", Toast.LENGTH_SHORT).show() + } + } + + binding.closeButton.setOnClickListener { + findNavController().popBackStack() + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/utils/StringFormatter.kt b/app/src/main/java/com/wafflestudio/siksha2/utils/StringFormatter.kt index 8bc3b3076..0972d458f 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/utils/StringFormatter.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/utils/StringFormatter.kt @@ -62,7 +62,7 @@ fun LocalDateTime.toParsedTimeString(): String { val days = diff.toDays() return when { days > 0 -> { - format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")) } hours > 0 -> { "${hours}시간 전" diff --git a/app/src/main/res/drawable/bg_review_card.xml b/app/src/main/res/drawable/bg_review_card.xml new file mode 100644 index 000000000..8a5495c8f --- /dev/null +++ b/app/src/main/res/drawable/bg_review_card.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_review_header.xml b/app/src/main/res/drawable/bg_review_header.xml new file mode 100644 index 000000000..b125c7c1a --- /dev/null +++ b/app/src/main/res/drawable/bg_review_header.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_review_tag.xml b/app/src/main/res/drawable/bg_review_tag.xml new file mode 100644 index 000000000..8ab21df88 --- /dev/null +++ b/app/src/main/res/drawable/bg_review_tag.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/divider_gray_line.xml b/app/src/main/res/drawable/divider_gray_line.xml new file mode 100644 index 000000000..9cff55c15 --- /dev/null +++ b/app/src/main/res/drawable/divider_gray_line.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/frame_corner_radius_10.xml b/app/src/main/res/drawable/frame_corner_radius_10.xml index 825f0dedc..fdd4499e6 100644 --- a/app/src/main/res/drawable/frame_corner_radius_10.xml +++ b/app/src/main/res/drawable/frame_corner_radius_10.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_empty_star.xml b/app/src/main/res/drawable/ic_empty_star.xml index a9ce3cd9f..ba52892b6 100644 --- a/app/src/main/res/drawable/ic_empty_star.xml +++ b/app/src/main/res/drawable/ic_empty_star.xml @@ -1,9 +1,13 @@ - + android:width="13dp" + android:height="13dp" + android:viewportWidth="13" + android:viewportHeight="13"> + + + + diff --git a/app/src/main/res/drawable/ic_full_star.xml b/app/src/main/res/drawable/ic_full_star.xml index 3ffd1cce4..d070014da 100644 --- a/app/src/main/res/drawable/ic_full_star.xml +++ b/app/src/main/res/drawable/ic_full_star.xml @@ -1,9 +1,13 @@ - + android:width="13dp" + android:height="13dp" + android:viewportWidth="13" + android:viewportHeight="13"> + + + + diff --git a/app/src/main/res/drawable/ic_half_star.xml b/app/src/main/res/drawable/ic_half_star.xml index dc79598eb..ec88be141 100644 --- a/app/src/main/res/drawable/ic_half_star.xml +++ b/app/src/main/res/drawable/ic_half_star.xml @@ -1,9 +1,16 @@ - + android:width="13dp" + android:height="13dp" + android:viewportWidth="13" + android:viewportHeight="13"> + + + + + diff --git a/app/src/main/res/drawable/review_arrow_down.xml b/app/src/main/res/drawable/review_arrow_down.xml new file mode 100644 index 000000000..c1e783bc7 --- /dev/null +++ b/app/src/main/res/drawable/review_arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/review_arrow_right.xml b/app/src/main/res/drawable/review_arrow_right.xml new file mode 100644 index 000000000..7e45a834c --- /dev/null +++ b/app/src/main/res/drawable/review_arrow_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_default.xml b/app/src/main/res/layout/dialog_default.xml index e08676c37..2e7cd64fc 100644 --- a/app/src/main/res/layout/dialog_default.xml +++ b/app/src/main/res/layout/dialog_default.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="40dp" + android:layout_marginHorizontal="30dp" android:background="@drawable/frame_corner_radius_10"> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_setting.xml b/app/src/main/res/layout/fragment_setting.xml index 02b243488..95f4815b8 100644 --- a/app/src/main/res/layout/fragment_setting.xml +++ b/app/src/main/res/layout/fragment_setting.xml @@ -61,18 +61,48 @@ android:layout_marginHorizontal="16dp" android:layout_marginTop="20dp" android:background="@drawable/frame_setting_layout" - android:orientation="horizontal" + android:orientation="vertical" android:layout_gravity="center"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_my_review_restaurant.xml b/app/src/main/res/layout/item_my_review_restaurant.xml new file mode 100644 index 000000000..4edacae1d --- /dev/null +++ b/app/src/main/res/layout/item_my_review_restaurant.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index b39b77564..bad099120 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -63,6 +63,9 @@ app:exitAnim="@anim/wait_anim" app:popEnterAnim="@anim/wait_anim" app:popExitAnim="@anim/slide_right" /> + @@ -241,6 +244,17 @@ android:name="comment_id" app:argType="long" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d7660bcba..2707ca307 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ 사진 리뷰 모아보기 리뷰 리뷰가 없습니다. + 나의 평가 관리 순서를 지정할 식당이 없습니다. 즐겨찾기에 추가된 식당이 없습니다.\n식당 탭에서 별을 눌러 추가해보세요. 네트워크 연결이 불안정합니다.