diff --git a/core/designsystem/src/main/java/designsystem/components/bottomsheet/BottomSheetType.kt b/core/designsystem/src/main/java/designsystem/components/bottomsheet/BottomSheetType.kt new file mode 100644 index 00000000..05347b9f --- /dev/null +++ b/core/designsystem/src/main/java/designsystem/components/bottomsheet/BottomSheetType.kt @@ -0,0 +1,6 @@ +package designsystem.components.bottomsheet + +enum class BottomSheetType { + LINK, + CLIP, +} diff --git a/core/designsystem/src/main/java/designsystem/components/bottomsheet/LinkMindBottomSheet.kt b/core/designsystem/src/main/java/designsystem/components/bottomsheet/LinkMindBottomSheet.kt index 002e4a01..b03d99d3 100644 --- a/core/designsystem/src/main/java/designsystem/components/bottomsheet/LinkMindBottomSheet.kt +++ b/core/designsystem/src/main/java/designsystem/components/bottomsheet/LinkMindBottomSheet.kt @@ -30,10 +30,12 @@ class LinkMindBottomSheet(context: Context) { window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) binding.etvBottomSheet.editText.requestFocus() } + } + fun setBottomSheetType(bottomSheetType: BottomSheetType) { binding.etvBottomSheet.apply { throttleAfterTextChanged { - handleTextChange() + handleTextChange(bottomSheetType) } onClickTextClear { @@ -43,6 +45,7 @@ class LinkMindBottomSheet(context: Context) { } } } + fun setBottomSheetHint(@StringRes textId: Int) { binding.etvBottomSheet.editText.setHint(textId) } @@ -62,8 +65,8 @@ class LinkMindBottomSheet(context: Context) { } } - fun handleTextChange() { - val isError = showErrorMsg() + private fun handleTextChange(bottomSheetType: BottomSheetType) { + val isError = showErrorMsg(bottomSheetType) binding.apply { tvBottomSheetErrorText.isVisible = isError if (isError) binding.etvBottomSheet.editText.filters = arrayOf(InputFilter.LengthFilter(16)) @@ -76,7 +79,18 @@ class LinkMindBottomSheet(context: Context) { fun setTitle(@StringRes textId: Int) { binding.tvBottomSheetTitle.setText(textId) } - fun showErrorMsg(): Boolean = binding.etvBottomSheet.editText.text.length > 15 || binding.etvBottomSheet.editText.text.isEmpty() + + fun showErrorMsg(bottomSheetType: BottomSheetType): Boolean { + return when (bottomSheetType) { + BottomSheetType.LINK -> { + binding.etvBottomSheet.editText.text.isEmpty() + } + + BottomSheetType.CLIP -> { + binding.etvBottomSheet.editText.text.length > 15 || binding.etvBottomSheet.editText.text.isEmpty() + } + } + } fun setErroMsg(@StringRes textId: Int) { binding.tvBottomSheetErrorText.setText(textId) diff --git a/feature/timer/src/main/java/org/sopt/timer/model/Clip.kt b/core/model/src/main/java/org/sopt/model/timer/Clip.kt similarity index 91% rename from feature/timer/src/main/java/org/sopt/timer/model/Clip.kt rename to core/model/src/main/java/org/sopt/model/timer/Clip.kt index 8aa98ef0..01475ecb 100644 --- a/feature/timer/src/main/java/org/sopt/timer/model/Clip.kt +++ b/core/model/src/main/java/org/sopt/model/timer/Clip.kt @@ -1,4 +1,4 @@ -package org.sopt.timer.model +package org.sopt.model.timer import org.sopt.model.category.Category diff --git a/data-remote/link/src/main/java/org/sopt/remote/link/api/LinkService.kt b/data-remote/link/src/main/java/org/sopt/remote/link/api/LinkService.kt index d9cc306b..035ec160 100644 --- a/data-remote/link/src/main/java/org/sopt/remote/link/api/LinkService.kt +++ b/data-remote/link/src/main/java/org/sopt/remote/link/api/LinkService.kt @@ -2,9 +2,11 @@ package org.sopt.remote.link.api import org.sopt.network.model.response.base.BaseResponse import org.sopt.remote.link.request.RequestIsReadDto +import org.sopt.remote.link.request.RequestPatchCategoryDto import org.sopt.remote.link.request.RequestPatchTitleDto import org.sopt.remote.link.request.RequestWriteDto import org.sopt.remote.link.response.ResponseIsReadDto +import org.sopt.remote.link.response.ResponsePatchCategoryDto import org.sopt.remote.link.response.ResponsePatchTitleDto import retrofit2.http.Body import retrofit2.http.DELETE @@ -19,6 +21,7 @@ interface LinkService { const val ISREAD = "is-read" const val SAVE = "save" const val TITLE = "title" + const val CATEGORY = "category" } @POST("/$TOAST/$SAVE") @@ -34,4 +37,7 @@ interface LinkService { @PATCH("/$TOAST/$TITLE") suspend fun patchLinkTitle(@Body requestPatchTitleDto: RequestPatchTitleDto): BaseResponse + + @PATCH("/$TOAST/$CATEGORY") + suspend fun patchToastCategory(@Body requestPatchCategoryDto: RequestPatchCategoryDto): BaseResponse } diff --git a/data-remote/link/src/main/java/org/sopt/remote/link/datasource/RemoteLinkDataSourceImpl.kt b/data-remote/link/src/main/java/org/sopt/remote/link/datasource/RemoteLinkDataSourceImpl.kt index ca8404b0..9f7cb9ad 100644 --- a/data-remote/link/src/main/java/org/sopt/remote/link/datasource/RemoteLinkDataSourceImpl.kt +++ b/data-remote/link/src/main/java/org/sopt/remote/link/datasource/RemoteLinkDataSourceImpl.kt @@ -3,6 +3,7 @@ package org.sopt.remote.link.datasource import org.sopt.data.link.datasource.RemoteLinkDataSource import org.sopt.remote.link.api.LinkService import org.sopt.remote.link.request.RequestIsReadDto +import org.sopt.remote.link.request.RequestPatchCategoryDto import org.sopt.remote.link.request.RequestPatchTitleDto import org.sopt.remote.link.request.RequestWriteDto import javax.inject.Inject @@ -37,4 +38,12 @@ class RemoteLinkDataSourceImpl @Inject constructor( title = title, ), ).data!!.updatedTitle + + override suspend fun patchLinkCategory(toastId: Long, categoryId: Long): Long = + linkService.patchToastCategory( + RequestPatchCategoryDto( + toastId = toastId, + categoryId = categoryId, + ), + ).data!!.categoryId } diff --git a/data-remote/link/src/main/java/org/sopt/remote/link/request/RequestPatchCategoryDto.kt b/data-remote/link/src/main/java/org/sopt/remote/link/request/RequestPatchCategoryDto.kt new file mode 100644 index 00000000..f699d3d9 --- /dev/null +++ b/data-remote/link/src/main/java/org/sopt/remote/link/request/RequestPatchCategoryDto.kt @@ -0,0 +1,12 @@ +package org.sopt.remote.link.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RequestPatchCategoryDto( + @SerialName("toastId") + val toastId: Long, + @SerialName("categoryId") + val categoryId: Long, +) diff --git a/data-remote/link/src/main/java/org/sopt/remote/link/response/ResponsePatchCategoryDto.kt b/data-remote/link/src/main/java/org/sopt/remote/link/response/ResponsePatchCategoryDto.kt new file mode 100644 index 00000000..63716e2e --- /dev/null +++ b/data-remote/link/src/main/java/org/sopt/remote/link/response/ResponsePatchCategoryDto.kt @@ -0,0 +1,10 @@ +package org.sopt.remote.link.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponsePatchCategoryDto( + @SerialName("categoryId") + val categoryId: Long, +) diff --git a/data/link/src/main/java/org/sopt/data/link/datasource/RemoteLinkDataSource.kt b/data/link/src/main/java/org/sopt/data/link/datasource/RemoteLinkDataSource.kt index 214d4592..80278a7d 100644 --- a/data/link/src/main/java/org/sopt/data/link/datasource/RemoteLinkDataSource.kt +++ b/data/link/src/main/java/org/sopt/data/link/datasource/RemoteLinkDataSource.kt @@ -4,6 +4,6 @@ interface RemoteLinkDataSource { suspend fun postSaveLink(linkUrl: String, categoryId: Long?): Int suspend fun deleteLink(toastId: Long): Int suspend fun patchReadLink(toastId: Long, isRead: Boolean): Boolean - suspend fun patchLinkTitle(toastId: Long, title: String): String + suspend fun patchLinkCategory(toastId: Long, categoryId: Long): Long } diff --git a/data/link/src/main/java/org/sopt/data/link/repository/LinkRepoImpl.kt b/data/link/src/main/java/org/sopt/data/link/repository/LinkRepoImpl.kt index 7fbc0fcf..ee617a55 100644 --- a/data/link/src/main/java/org/sopt/data/link/repository/LinkRepoImpl.kt +++ b/data/link/src/main/java/org/sopt/data/link/repository/LinkRepoImpl.kt @@ -18,4 +18,7 @@ class LinkRepoImpl @Inject constructor( override suspend fun patchLinkTitle(toastId: Long, title: String): Result = runCatching { remoteCategoryDataSource.patchLinkTitle(toastId, title) } + + override suspend fun patchToastCategory(toastId: Long, categoryId: Long): Result = + runCatching { remoteCategoryDataSource.patchLinkCategory(toastId, categoryId) } } diff --git a/domain/link/src/main/java/org/sopt/domain/link/repository/LinkRepository.kt b/domain/link/src/main/java/org/sopt/domain/link/repository/LinkRepository.kt index 08c3798b..2e4a02f6 100644 --- a/domain/link/src/main/java/org/sopt/domain/link/repository/LinkRepository.kt +++ b/domain/link/src/main/java/org/sopt/domain/link/repository/LinkRepository.kt @@ -4,6 +4,6 @@ interface LinkRepository { suspend fun postSaveLink(linkUrl: String, categoryId: Long?): Result suspend fun deleteLink(toastId: Long): Result suspend fun patchReadLink(toastId: Long, isRead: Boolean): Result - suspend fun patchLinkTitle(toastId: Long, title: String): Result + suspend fun patchToastCategory(toastId: Long, categoryId: Long): Result } diff --git a/domain/link/src/main/java/org/sopt/domain/link/usecase/PatchLinkCategoryUseCase.kt b/domain/link/src/main/java/org/sopt/domain/link/usecase/PatchLinkCategoryUseCase.kt new file mode 100644 index 00000000..c527b587 --- /dev/null +++ b/domain/link/src/main/java/org/sopt/domain/link/usecase/PatchLinkCategoryUseCase.kt @@ -0,0 +1,18 @@ +package org.sopt.domain.link.usecase + +import org.sopt.domain.link.repository.LinkRepository +import javax.inject.Inject + +class PatchLinkCategoryUseCase @Inject constructor( + private val linkRepository: LinkRepository, +) { + suspend operator fun invoke(param: Param): Result = linkRepository.patchToastCategory( + toastId = param.toastId, + categoryId = param.categoryId, + ) + + data class Param( + val toastId: Long, + val categoryId: Long, + ) +} diff --git a/feature/clip/src/main/java/org/sopt/clip/DeleteLinkBottomSheetFragment.kt b/feature/clip/src/main/java/org/sopt/clip/DeleteLinkBottomSheetFragment.kt index 10fa582c..c32f0262 100644 --- a/feature/clip/src/main/java/org/sopt/clip/DeleteLinkBottomSheetFragment.kt +++ b/feature/clip/src/main/java/org/sopt/clip/DeleteLinkBottomSheetFragment.kt @@ -1,48 +1,76 @@ package org.sopt.clip import android.os.Bundle -import android.util.Log import android.view.View +import androidx.core.view.isVisible import org.sopt.clip.databinding.FragmentDeleteLinkBottomSheetBinding import org.sopt.ui.base.BindingBottomSheetDialogFragment import org.sopt.ui.view.onThrottleClick class DeleteLinkBottomSheetFragment() : BindingBottomSheetDialogFragment({ FragmentDeleteLinkBottomSheetBinding.inflate(it) }) { - var id: Int? = null + var clipId: Long? = null + var isFullClipSize: Boolean? = null private var handleDelete: () -> Unit = {} + private var handleChange: () -> Unit = {} private var handleModify: () -> Unit = {} + private var isClipListEmptySnackBar: () -> Unit = {} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + clipId = arguments?.getLong("clipId") + isFullClipSize = arguments?.getBoolean("isFullClipSize") + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (clipId?.toInt() == 0) { + binding.tvDeleteLinkChange.isVisible = false + } + binding.ivDeleteLinkBottomSheetClose.setOnClickListener { dismiss() } binding.tvDeleteLinkDelete.onThrottleClick { - Log.d("test", "test") handleDelete.invoke() dismiss() } + + binding.tvDeleteLinkChange.onThrottleClick { + if (isFullClipSize == true) { + handleChange.invoke() + } else { + isClipListEmptySnackBar.invoke() + } + dismiss() + } + binding.tvDeleteLinkModify.onThrottleClick { handleModify.invoke() dismiss() } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - id = arguments?.getInt("id") - } - companion object { - fun newInstance(id: Int, handleDeleteButton: () -> Unit, handleModifyButton: () -> Unit): DeleteLinkBottomSheetFragment { + fun newInstance( + clipId: Long, + isFullClipSize: Boolean, + isClipListEmpty: () -> Unit, + handleDeleteButton: () -> Unit, + handleChangeButton: () -> Unit, + handleModifyButton: () -> Unit, + ): DeleteLinkBottomSheetFragment { val args = Bundle().apply { - putInt("id", id) + putLong("clipId", clipId) + putBoolean("isFullClipSize", isFullClipSize) } return DeleteLinkBottomSheetFragment().apply { arguments = args handleDelete = handleDeleteButton + handleChange = handleChangeButton handleModify = handleModifyButton + isClipListEmptySnackBar = isClipListEmpty } } } diff --git a/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt b/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt index 7d4f5b47..6ac5bc02 100644 --- a/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt +++ b/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import designsystem.components.bottomsheet.BottomSheetType import designsystem.components.bottomsheet.LinkMindBottomSheet import designsystem.components.toast.linkMindSnackBar import kotlinx.coroutines.flow.launchIn @@ -115,6 +116,7 @@ class ClipFragment : BindingFragment({ FragmentClipBinding. val addClipBottomSheet = LinkMindBottomSheet(requireContext()) addClipBottomSheet.show() addClipBottomSheet.apply { + setBottomSheetType(BottomSheetType.CLIP) setBottomSheetHint(org.sopt.mainfeature.R.string.clip_new_clip_info) setTitle(org.sopt.mainfeature.R.string.clip_add_clip) setErroMsg(org.sopt.mainfeature.R.string.error_clip_length) diff --git a/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeAdapter.kt b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeAdapter.kt new file mode 100644 index 00000000..b33531a2 --- /dev/null +++ b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeAdapter.kt @@ -0,0 +1,62 @@ +package org.sopt.clip.clipchange + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.ListAdapter +import org.sopt.clip.databinding.ItemClipChangeBinding +import org.sopt.model.timer.Clip +import org.sopt.ui.view.ItemDiffCallback + +class ClipChangeAdapter( + private val onClick: (Clip, Int) -> Unit, + private val context: Context, +) : ListAdapter(DiffUtil) { + var selectedPosition = -1 + override fun onBindViewHolder(holder: ClipChangeViewHolder, position: Int) { + holder.onBind(getItem(position), position) { clip, position -> + selectItemByPosition(position, clip) + onClick(clip, position) + } + + if (position == 0) { + val disMissClickColor = ContextCompat.getColor(context, org.sopt.mainfeature.R.color.neutrals400) + holder.binding.ivItemClipChange.setColorFilter(disMissClickColor) + holder.binding.tvItemClipChangeName.setTextColor(disMissClickColor) + holder.binding.tvItemClipChangeCount.setTextColor(disMissClickColor) + holder.binding.root.isEnabled = false + } + } + + private fun selectItemByPosition(position: Int, clip: Clip) { + if (selectedPosition != position) { + if (selectedPosition != -1) { + getItem(selectedPosition).isSelected = false + notifyItemChanged(selectedPosition) + } + clip.isSelected = true + selectedPosition = position + } else { + clip.isSelected = !clip.isSelected + if (!clip.isSelected) { + selectedPosition = -1 + } + } + notifyItemChanged(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ClipChangeViewHolder { + return ClipChangeViewHolder( + ItemClipChangeBinding.inflate(LayoutInflater.from(parent.context), parent, false), + context, + ) + } + + companion object { + private val DiffUtil = ItemDiffCallback( + onItemsTheSame = { old, new -> old.id == new.id }, + onContentsTheSame = { old, new -> old == new }, + ) + } +} diff --git a/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeFragment.kt b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeFragment.kt new file mode 100644 index 00000000..d6b364a9 --- /dev/null +++ b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeFragment.kt @@ -0,0 +1,102 @@ +package org.sopt.clip.clipchange + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import dagger.hilt.android.AndroidEntryPoint +import designsystem.components.button.state.LinkMindButtonState +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.sopt.clip.cliplink.ClipLinkViewModel +import org.sopt.clip.databinding.FragmentClipChangeBinding +import org.sopt.model.timer.Clip +import org.sopt.model.timer.toUiModel +import org.sopt.ui.base.BindingFragment +import org.sopt.ui.fragment.viewLifeCycle +import org.sopt.ui.fragment.viewLifeCycleScope +import org.sopt.ui.view.UiState +import org.sopt.ui.view.onThrottleClick + +@AndroidEntryPoint +class ClipChangeFragment : + BindingFragment({ FragmentClipChangeBinding.inflate(it) }) { + private lateinit var clipAdapter: ClipChangeAdapter + private val viewModel: ClipLinkViewModel by activityViewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val args: ClipChangeFragmentArgs by navArgs() + getCategoryAll() + collectClipState(args) + initCloseButtonClickListener() + } + + private fun getCategoryAll() { + viewModel.getCategoryAll() + } + + private fun collectClipState(args: ClipChangeFragmentArgs) { + viewModel.categoryState.flowWithLifecycle(viewLifeCycle).onEach { state -> + when (state) { + is UiState.Success -> { + initClipSelectAdapter(state.data.toUiModel(), args.toastId, args.clipId) + } + + else -> {} + } + }.launchIn(viewLifeCycleScope) + } + + private fun initClipSelectAdapter(list: List, toastId: Long, currentClipId: Long) { + val clipList = excludeCurrentClipId(list, currentClipId) + clipAdapter = ClipChangeAdapter( + onClick = { clip, index -> + handleClipClick(clip, clipList, index, clip.id, toastId) + }, + context = requireContext(), + ) + clipAdapter.selectedPosition = clipList.indexOfFirst { it.isSelected } + clipAdapter.submitList(clipList) + binding.btnClipChangeSelectNext.state = if (clipAdapter.selectedPosition != -1) LinkMindButtonState.ENABLE else LinkMindButtonState.DISABLE + binding.rvClipChangeSelect.adapter = clipAdapter + binding.rvClipChangeSelect.itemAnimator = null + } + + private fun handleClipClick( + clip: Clip, + list: List, + index: Int, + newClipId: Long, + toastId: Long, + ) { + if (clip.isSelected) { + list.onEach { it.isSelected = false } + list[index].isSelected = true + binding.btnClipChangeSelectNext.state = LinkMindButtonState.ENABLE + } else { + list.onEach { it.isSelected = false } + binding.btnClipChangeSelectNext.state = LinkMindButtonState.DISABLE + } + + initNextButtonClickListener(toastId, newClipId) + } + + private fun initNextButtonClickListener(toastId: Long, newClipId: Long) { + binding.btnClipChangeSelectNext.btnClick { + viewModel.patchLinkCategory(toastId = toastId, categoryId = newClipId) + findNavController().popBackStack() + } + } + + private fun initCloseButtonClickListener() { + binding.ivClipChangeClose.onThrottleClick { + findNavController().popBackStack() + } + } + + private fun excludeCurrentClipId(clipList: List, currentClipId: Long): List = + clipList.filter { it.id != currentClipId } +} diff --git a/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeViewHolder.kt b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeViewHolder.kt new file mode 100644 index 00000000..ce0e7759 --- /dev/null +++ b/feature/clip/src/main/java/org/sopt/clip/clipchange/ClipChangeViewHolder.kt @@ -0,0 +1,48 @@ +package org.sopt.clip.clipchange + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import org.sopt.clip.databinding.ItemClipChangeBinding +import org.sopt.model.timer.Clip +import org.sopt.ui.view.onThrottleClick + +class ClipChangeViewHolder( + val binding: ItemClipChangeBinding, + val context: Context, +) : RecyclerView.ViewHolder(binding.root) { + fun onBind(data: Clip?, pos: Int, onClick: (Clip, Int) -> Unit) { + if (data == null) return + with(binding) { + tvItemClipChangeName.text = data.name + tvItemClipChangeCount.text = "${data.count}개" + setSelectedClipColor(data, pos) + root.onThrottleClick { + onClick(data, bindingAdapterPosition) + bindingAdapter?.notifyItemChanged(pos) + } + } + } + + private fun ItemClipChangeBinding.setSelectedClipColor( + data: Clip, + pos: Int, + ) { + val selectedColor = androidx.core.content.ContextCompat.getColor(context, org.sopt.mainfeature.R.color.primary) + val defaultColor = androidx.core.content.ContextCompat.getColor(context, org.sopt.mainfeature.R.color.neutrals900) + if (data.isSelected) { + ivItemClipChange.setImageResource( + org.sopt.mainfeature.R.drawable.ic_clip_all_24_primary.takeIf { pos == 0 } + ?: org.sopt.mainfeature.R.drawable.ic_clip_24_primary, + ) + tvItemClipChangeCount.setTextColor(selectedColor) + tvItemClipChangeName.setTextColor(selectedColor) + } else { + ivItemClipChange.setImageResource( + org.sopt.mainfeature.R.drawable.ic_clip_all_24.takeIf { pos == 0 } + ?: org.sopt.mainfeature.R.drawable.ic_clip_24, + ) + tvItemClipChangeCount.setTextColor(defaultColor) + tvItemClipChangeName.setTextColor(defaultColor) + } + } +} diff --git a/feature/clip/src/main/java/org/sopt/clip/clipedit/ClipEditFragment.kt b/feature/clip/src/main/java/org/sopt/clip/clipedit/ClipEditFragment.kt index 2acab12e..c56a2e1e 100644 --- a/feature/clip/src/main/java/org/sopt/clip/clipedit/ClipEditFragment.kt +++ b/feature/clip/src/main/java/org/sopt/clip/clipedit/ClipEditFragment.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ItemTouchHelper import dagger.hilt.android.AndroidEntryPoint +import designsystem.components.bottomsheet.BottomSheetType import designsystem.components.bottomsheet.LinkMindBottomSheet import designsystem.components.dialog.LinkMindDialog import designsystem.components.toast.linkMindSnackBar @@ -90,6 +91,7 @@ class ClipEditFragment : BindingFragment({ FragmentClip val editTitleBottomSheet = LinkMindBottomSheet(requireContext()) editTitleBottomSheet.show() editTitleBottomSheet.apply { + setBottomSheetType(BottomSheetType.CLIP) setBottomSheetHint(org.sopt.mainfeature.R.string.home_new_clip_info) setTitle(org.sopt.mainfeature.R.string.edit_clip_edit_title) setBottomSheetText(itemText) diff --git a/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkFragment.kt b/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkFragment.kt index 771c720a..49914d22 100644 --- a/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkFragment.kt +++ b/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkFragment.kt @@ -4,11 +4,12 @@ import android.os.Bundle import android.view.View import android.widget.TextView import androidx.core.view.isVisible -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.flowWithLifecycle import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint +import designsystem.components.bottomsheet.BottomSheetType import designsystem.components.bottomsheet.LinkMindBottomSheet import designsystem.components.toast.linkMindSnackBar import kotlinx.coroutines.flow.launchIn @@ -17,6 +18,7 @@ import org.sopt.clip.DeleteLinkBottomSheetFragment import org.sopt.clip.R import org.sopt.clip.databinding.FragmentClipLinkBinding import org.sopt.common.util.delSpace +import org.sopt.model.category.Category import org.sopt.ui.base.BindingFragment import org.sopt.ui.fragment.colorOf import org.sopt.ui.fragment.viewLifeCycle @@ -29,12 +31,14 @@ import java.nio.charset.StandardCharsets @AndroidEntryPoint class ClipLinkFragment : BindingFragment({ FragmentClipLinkBinding.inflate(it) }) { - private val viewModel: ClipLinkViewModel by viewModels() + private val viewModel: ClipLinkViewModel by activityViewModels() private lateinit var clipLinkAdapter: ClipLinkAdapter - var isDataNull: Boolean = true + private var isDataNull: Boolean = true override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.initState() + viewModel.getCategoryAll() + val args: ClipLinkFragmentArgs by navArgs() val categoryId = args.categoryId val categoryName = args.categoryName @@ -80,12 +84,13 @@ class ClipLinkFragment : BindingFragment({ FragmentClip binding.tvClipLinkRead, ) } - initClipAdapter() + initClipAdapter(args.categoryId) initViewState(isDataNull) updateLinkDelete(categoryId) updateLinkView() updateAllCount() updateLinkTitle(categoryId) + updateLinkTitles() onClickBackButton() } @@ -127,6 +132,7 @@ class ClipLinkFragment : BindingFragment({ FragmentClip } }.launchIn(viewLifeCycleScope) } + private fun updateAllCount() { viewModel.allClipCount.flowWithLifecycle(viewLifeCycle).onEach { state -> when (state) { @@ -141,6 +147,18 @@ class ClipLinkFragment : BindingFragment({ FragmentClip }.launchIn(viewLifeCycleScope) } + private fun updateLinkTitles() { + viewModel.patchLinkCategory.flowWithLifecycle(viewLifeCycle).onEach { state -> + when (state) { + is UiState.Success -> { + requireContext().linkMindSnackBar(binding.vSnack, "클립 이동 완료", true) + } + + else -> {} + } + }.launchIn(viewLifeCycleScope) + } + private fun updateLinkDelete(categoryId: Long) { viewModel.deleteState.flowWithLifecycle(viewLifeCycle).onEach { state -> when (state) { @@ -237,7 +255,7 @@ class ClipLinkFragment : BindingFragment({ FragmentClip } } - private fun initClipAdapter() { + private fun initClipAdapter(clipId: Long) { clipLinkAdapter = ClipLinkAdapter { linkDTO, state -> when (state) { "click" -> { @@ -245,15 +263,23 @@ class ClipLinkFragment : BindingFragment({ FragmentClip } "delete" -> { - DeleteLinkBottomSheetFragment.newInstance( - linkDTO.toastId.toInt(), - handleDeleteButton = { - viewModel.deleteLink(linkDTO.toastId) - }, - handleModifyButton = { - showClipLinkBottomSheet(linkDTO.toastId, linkDTO.toastTitle) - }, - ).show(parentFragmentManager, this.tag) + getAllClip { categoryList -> + val isFullClipSize = categoryList.size > 2 + DeleteLinkBottomSheetFragment.newInstance( + clipId, + isFullClipSize, + { requireContext().linkMindSnackBar(binding.vSnack, "클립 하나임", true) }, + handleDeleteButton = { + viewModel.deleteLink(linkDTO.toastId) + }, + handleChangeButton = { + navigateToDestination("featureClipChange://clipChangeFragment/$clipId/${linkDTO.toastId}") + }, + handleModifyButton = { + showClipLinkBottomSheet(linkDTO.toastId, linkDTO.toastTitle) + }, + ).show(parentFragmentManager, this.tag) + } } } } @@ -264,10 +290,10 @@ class ClipLinkFragment : BindingFragment({ FragmentClip val editTitleBottomSheet = LinkMindBottomSheet(requireContext()) editTitleBottomSheet.show() editTitleBottomSheet.apply { + setBottomSheetType(BottomSheetType.LINK) setBottomSheetHint(itemText) setTitle(org.sopt.mainfeature.R.string.clip_link_bottom_sheet_modify_title) setBottomSheetText(itemText) - setErroMsg(org.sopt.mainfeature.R.string.error_clip_length) bottomSheetConfirmBtnClick { // dto 수정됨 val newTitle = getText() viewModel.patchLinkTitle(itemId, newTitle) @@ -276,6 +302,23 @@ class ClipLinkFragment : BindingFragment({ FragmentClip } } + private fun getAllClip(callback: (List) -> Unit) { + viewModel.categoryState + .flowWithLifecycle(viewLifeCycle) + .onEach { state -> + when (state) { + is UiState.Success -> { + callback(state.data) + } + + else -> { + initViewState(true) + } + } + } + .launchIn(viewLifeCycleScope) + } + private fun naviagateToWebViewFragment(site: String, toastId: Long, isRead: Boolean) { val encodedURL = URLEncoder.encode(site, StandardCharsets.UTF_8.toString()) navigateToDestination("featureSaveLink://webViewFragment/$toastId/$isRead/${true}/$encodedURL") diff --git a/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkViewModel.kt b/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkViewModel.kt index 1f4b01dc..9d90c454 100644 --- a/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkViewModel.kt +++ b/feature/clip/src/main/java/org/sopt/clip/cliplink/ClipLinkViewModel.kt @@ -9,9 +9,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.sopt.clip.SelectedToggle +import org.sopt.domain.category.category.usecase.GetCategoryAllUseCase import org.sopt.domain.category.category.usecase.GetCategoryLinkUseCase import org.sopt.domain.link.usecase.DeleteLinkUseCase +import org.sopt.domain.link.usecase.PatchLinkCategoryUseCase import org.sopt.domain.link.usecase.PatchLinkTitleUseCase +import org.sopt.model.category.Category import org.sopt.model.category.CategoryLink import org.sopt.ui.view.UiState import javax.inject.Inject @@ -21,6 +24,8 @@ class ClipLinkViewModel @Inject constructor( private val getCategoryLink: GetCategoryLinkUseCase, private val deleteLinkUseCase: DeleteLinkUseCase, private val patchLinkTitleUseCase: PatchLinkTitleUseCase, + private val getCategoryAll: GetCategoryAllUseCase, + private val patchLinkCategoryUseCase: PatchLinkCategoryUseCase, ) : ViewModel() { private val _linkState = MutableStateFlow>>(UiState.Empty) val linkState: StateFlow>> = _linkState.asStateFlow() @@ -34,6 +39,12 @@ class ClipLinkViewModel @Inject constructor( private val _patchLinkTitle = MutableStateFlow>(UiState.Empty) val patchLinkTitle: StateFlow> = _patchLinkTitle.asStateFlow() + private val _categoryState = MutableStateFlow>>(UiState.Empty) + val categoryState: StateFlow>> = _categoryState.asStateFlow() + + private val _patchLinkCategory = MutableStateFlow>(UiState.Empty) + val patchLinkCategory: StateFlow> = _patchLinkCategory.asStateFlow() + var toggleSelectedPast: SelectedToggle = SelectedToggle.ALL fun deleteLink(toastId: Long) = viewModelScope.launch { deleteLinkUseCase.invoke(param = DeleteLinkUseCase.Param(toastId = toastId)).onSuccess { @@ -50,6 +61,7 @@ class ClipLinkViewModel @Inject constructor( fun updateDeleteState() = viewModelScope.launch { _deleteState.emit(UiState.Success(false)) } + fun getCategoryLink(filter: String?, categoryId: Long?) = viewModelScope.launch { getCategoryLink(param = GetCategoryLinkUseCase.Param(filter = filter, categoryId = categoryId)).onSuccess { val list: MutableList = it.toastListDto.toMutableList() @@ -68,7 +80,27 @@ class ClipLinkViewModel @Inject constructor( } } + fun getCategoryAll() = viewModelScope.launch { + getCategoryAll.invoke().onSuccess { + val allCategoryList = listOf( + Category(0, "전체 클립", it.toastNumberInEntire), + ) + _categoryState.emit(UiState.Success(allCategoryList + it.categories)) + }.onFailure { + Log.e("실패", it.message.toString()) + } + } + + fun patchLinkCategory(toastId: Long, categoryId: Long) = viewModelScope.launch { + patchLinkCategoryUseCase(param = PatchLinkCategoryUseCase.Param(toastId = toastId, categoryId = categoryId)).onSuccess { + _patchLinkCategory.emit(UiState.Success(it)) + }.onFailure { + _patchLinkCategory.emit(UiState.Failure("fail")) + } + } + fun initState() { _linkState.value = UiState.Empty + _patchLinkCategory.value = UiState.Empty } } diff --git a/feature/clip/src/main/res/layout/fragment_clip_change.xml b/feature/clip/src/main/res/layout/fragment_clip_change.xml new file mode 100644 index 00000000..7ffaf9fa --- /dev/null +++ b/feature/clip/src/main/res/layout/fragment_clip_change.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + diff --git a/feature/clip/src/main/res/layout/fragment_delete_link_bottom_sheet.xml b/feature/clip/src/main/res/layout/fragment_delete_link_bottom_sheet.xml index 8d8760d0..f43f25be 100644 --- a/feature/clip/src/main/res/layout/fragment_delete_link_bottom_sheet.xml +++ b/feature/clip/src/main/res/layout/fragment_delete_link_bottom_sheet.xml @@ -1,75 +1,95 @@ - - - - - - + android:background="@drawable/shape_neutrals_fill_top20_rect"> + android:id="@+id/tv_delete_link_bottom_sheet_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="20dp" + android:layout_marginTop="21dp" + android:text="@string/clip_link_bottom_sheet_title" + android:textAppearance="@style/Typography.suit.bold_18" + android:textColor="@color/neutrals_black" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - + + + + + + + + + + diff --git a/feature/clip/src/main/res/layout/item_clip_change.xml b/feature/clip/src/main/res/layout/item_clip_change.xml new file mode 100644 index 00000000..4cdbdee7 --- /dev/null +++ b/feature/clip/src/main/res/layout/item_clip_change.xml @@ -0,0 +1,42 @@ + + + + + + + diff --git a/feature/clip/src/main/res/navigation/nav_graph_clip.xml b/feature/clip/src/main/res/navigation/nav_graph_clip.xml index f8bf148d..b19b9013 100644 --- a/feature/clip/src/main/res/navigation/nav_graph_clip.xml +++ b/feature/clip/src/main/res/navigation/nav_graph_clip.xml @@ -8,11 +8,10 @@ android:id="@+id/navigation_clip" android:name="org.sopt.clip.clip.ClipFragment" android:label="fragment_clip" - tools:layout="@layout/fragment_clip" > + tools:layout="@layout/fragment_clip"> - + app:destination="@id/navigation_clip_link"> @@ -24,20 +23,22 @@ tools:layout="@layout/fragment_clip_link"> + app:argType="long" /> - + app:argType="string" /> + + + android:label="WebViewFragment"> @@ -50,19 +51,31 @@ - + + tools:layout="@layout/fragment_clip_edit" /> + tools:layout="@layout/fragment_search"> + + + + + diff --git a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt index 83099eb0..2b58bee4 100644 --- a/feature/home/src/main/java/org/sopt/home/HomeFragment.kt +++ b/feature/home/src/main/java/org/sopt/home/HomeFragment.kt @@ -64,6 +64,7 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. is HomeSideEffect.NavigateClipLink -> navigateToDestination( "featureSaveLink://ClipLinkFragment/${viewModel.container.stateFlow.value.categoryId}/${viewModel.container.stateFlow.value.categoryName}", ) + is HomeSideEffect.NavigateSaveLink -> navigateToDestinationWithoutAnim("featureSaveLink://saveLinkFragment?clipboardLink=") is HomeSideEffect.NavigateWebView -> { val encodedURL = URLEncoder.encode(viewModel.container.stateFlow.value.url, StandardCharsets.UTF_8.toString()) @@ -71,6 +72,7 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. "featureSaveLink://webViewFragment/${0}/${false}/${false}/$encodedURL", ) } + is HomeSideEffect.NavigateAllClip -> navigateToDestinationWithoutAnim("featureSaveLink://ClipLinkFragment/0/전체 클립") is HomeSideEffect.ShowPopupInfo -> showPopupInfo(viewModel.container.stateFlow.value.popupList) is HomeSideEffect.ShowUpdateDialog -> showUpdateDialog(viewModel.container.stateFlow.value.marketUpdate) diff --git a/feature/savelink/src/main/java/org/sopt/savelink/ui/savelinksetclip/SaveLinkSetClipFragment.kt b/feature/savelink/src/main/java/org/sopt/savelink/ui/savelinksetclip/SaveLinkSetClipFragment.kt index 867fce6d..3266aad6 100644 --- a/feature/savelink/src/main/java/org/sopt/savelink/ui/savelinksetclip/SaveLinkSetClipFragment.kt +++ b/feature/savelink/src/main/java/org/sopt/savelink/ui/savelinksetclip/SaveLinkSetClipFragment.kt @@ -6,6 +6,7 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import designsystem.components.bottomsheet.BottomSheetType import designsystem.components.bottomsheet.LinkMindBottomSheet import designsystem.components.button.state.LinkMindButtonState import designsystem.components.dialog.LinkMindDialog @@ -137,12 +138,13 @@ class SaveLinkSetClipFragment : BindingFragment( val linkMindBottomSheet = LinkMindBottomSheet(requireContext()) linkMindBottomSheet.show() linkMindBottomSheet.apply { + setBottomSheetType(BottomSheetType.CLIP) setTitle(R.string.clip_add_clip) setErroMsg(R.string.error_clip_length) setBottomSheetHint(R.string.home_new_clip_info) bottomSheetConfirmBtnClick { viewModel.getCategoryDuplicate(it) - if (showErrorMsg()) return@bottomSheetConfirmBtnClick + if (showErrorMsg(BottomSheetType.CLIP)) return@bottomSheetConfirmBtnClick dismiss() } } diff --git a/feature/timer/src/main/java/org/sopt/timer/settimer/SetTimerViewModel.kt b/feature/timer/src/main/java/org/sopt/timer/settimer/SetTimerViewModel.kt index b9a84895..1c664e5c 100644 --- a/feature/timer/src/main/java/org/sopt/timer/settimer/SetTimerViewModel.kt +++ b/feature/timer/src/main/java/org/sopt/timer/settimer/SetTimerViewModel.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.sopt.domain.category.category.usecase.GetCategoryAllUseCase import org.sopt.model.category.Category +import org.sopt.model.timer.Clip import org.sopt.model.timer.Repeat -import org.sopt.timer.model.Clip +import org.sopt.model.timer.toUiModel import org.sopt.timer.model.TimePicker -import org.sopt.timer.model.toUiModel import org.sopt.timer.usecase.FormatRepeatListToIntList import org.sopt.timer.usecase.FormatRepeatListToStringList import org.sopt.timer.usecase.PatchTimerUseCase diff --git a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectAdapter.kt b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectAdapter.kt index 3af88a0c..fa700257 100644 --- a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectAdapter.kt +++ b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectAdapter.kt @@ -4,8 +4,8 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter +import org.sopt.model.timer.Clip import org.sopt.timer.databinding.ItemTimerClipSelectBinding -import org.sopt.timer.model.Clip import org.sopt.ui.view.ItemDiffCallback class ClipSelectAdapter( diff --git a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectViewHolder.kt b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectViewHolder.kt index 8dcfacce..a894ded9 100644 --- a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectViewHolder.kt +++ b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/ClipSelectViewHolder.kt @@ -3,8 +3,8 @@ package org.sopt.timer.settimer.clipselect import android.content.Context import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import org.sopt.model.timer.Clip import org.sopt.timer.databinding.ItemTimerClipSelectBinding -import org.sopt.timer.model.Clip import org.sopt.ui.view.onThrottleClick class ClipSelectViewHolder( diff --git a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/TimerClipSelectFragment.kt b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/TimerClipSelectFragment.kt index a3d6ad0e..e7563ccd 100644 --- a/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/TimerClipSelectFragment.kt +++ b/feature/timer/src/main/java/org/sopt/timer/settimer/clipselect/TimerClipSelectFragment.kt @@ -10,9 +10,9 @@ import designsystem.components.button.state.LinkMindButtonState import designsystem.components.dialog.LinkMindDialog import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.sopt.model.timer.Clip import org.sopt.timer.R import org.sopt.timer.databinding.FragmentTimerClipSelectBinding -import org.sopt.timer.model.Clip import org.sopt.timer.settimer.SetTimerViewModel import org.sopt.ui.base.BindingFragment import org.sopt.ui.fragment.viewLifeCycle @@ -28,8 +28,8 @@ class TimerClipSelectFragment : BindingFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) getCategoryAll() - initCloseButtonClickListener() collectClipState() + initCloseButtonClickListener() } private fun getCategoryAll() {