diff --git a/.idea/misc.xml b/.idea/misc.xml index 5acfade6..2d3d48fd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,16 @@ - + + + + \ No newline at end of file diff --git a/app/src/main/java/org/sopt/pingle/domain/model/PingleEntity.kt b/app/src/main/java/org/sopt/pingle/domain/model/PingleEntity.kt new file mode 100644 index 00000000..d32dd6ea --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/domain/model/PingleEntity.kt @@ -0,0 +1,16 @@ +package org.sopt.pingle.domain.model + +data class PingleEntity( + val id: Long, + val category: String, + val name: String, + val ownerName: String, + val location: String, + val date: String, + val startAt: String, + val endAt: String, + val maxParticipants: Int, + val curParticipants: Int, + val isParticipating: Boolean, + val chatLink: String +) diff --git a/app/src/main/java/org/sopt/pingle/domain/model/PlanEntity.kt b/app/src/main/java/org/sopt/pingle/domain/model/PlanEntity.kt new file mode 100644 index 00000000..f0b62c1b --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/domain/model/PlanEntity.kt @@ -0,0 +1,14 @@ +package org.sopt.pingle.domain.model + +data class PlanEntity( + val category: String, + val name: String, + val startAt: String, + val endAt: String, + val x: Double, + val y: Double, + val address: String, + val location: String, + val maxParticipants: Int, + val chatLink: String +) diff --git a/app/src/main/java/org/sopt/pingle/domain/model/PlanLocationEntity.kt b/app/src/main/java/org/sopt/pingle/domain/model/PlanLocationEntity.kt new file mode 100644 index 00000000..0daf3179 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/domain/model/PlanLocationEntity.kt @@ -0,0 +1,11 @@ +package org.sopt.pingle.domain.model + +import androidx.databinding.ObservableBoolean + +data class PlanLocationEntity( + val location: String, + val address: String, + val x: Double, + val y: Double, + var isSelected: ObservableBoolean = ObservableBoolean(false) +) diff --git a/app/src/main/java/org/sopt/pingle/presentation/mapper/CategoryTypeMapper.kt b/app/src/main/java/org/sopt/pingle/presentation/mapper/CategoryTypeMapper.kt new file mode 100644 index 00000000..a7bc4ce8 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/mapper/CategoryTypeMapper.kt @@ -0,0 +1,12 @@ +package org.sopt.pingle.presentation.mapper + +import org.sopt.pingle.presentation.type.CategoryType +import org.sopt.pingle.presentation.ui.main.home.map.MapFragment + +fun CategoryType.toMarkerIcon(isSelected: Boolean) = + when (this) { + CategoryType.PLAY -> if (isSelected) MapFragment.OVERLAY_IMAGE_PLAY_BIG else MapFragment.OVERLAY_IMAGE_PLAY_SMALL + CategoryType.STUDY -> if (isSelected) MapFragment.OVERLAY_IMAGE_STUDY_BIG else MapFragment.OVERLAY_IMAGE_STUDY_SMALL + CategoryType.MULTI -> if (isSelected) MapFragment.OVERLAY_IMAGE_MULTI_BIG else MapFragment.OVERLAY_IMAGE_MULTI_SMALL + CategoryType.OTHERS -> if (isSelected) MapFragment.OVERLAY_IMAGE_OTHER_BIG else MapFragment.OVERLAY_IMAGE_OTHER_SMALL + } diff --git a/app/src/main/java/org/sopt/pingle/presentation/mapper/PinMapper.kt b/app/src/main/java/org/sopt/pingle/presentation/mapper/PinMapper.kt index f4d62b20..fa47b035 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/mapper/PinMapper.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/mapper/PinMapper.kt @@ -1,21 +1,31 @@ package org.sopt.pingle.presentation.mapper +import androidx.databinding.Observable import com.naver.maps.geometry.LatLng import com.naver.maps.map.overlay.Marker -import com.naver.maps.map.overlay.OverlayImage -import org.sopt.pingle.R import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.presentation.model.MarkerModel import org.sopt.pingle.presentation.type.CategoryType -fun PinEntity.toMarker(): Marker = Marker().apply { - position = LatLng(y, x) - isHideCollidedMarkers = true - icon = OverlayImage.fromResource( - when (category) { - CategoryType.PLAY.toString() -> R.drawable.ic_map_marker_play_small - CategoryType.STUDY.toString() -> R.drawable.ic_map_marker_study_small - CategoryType.MULTI.toString() -> R.drawable.ic_map_marker_multi_small - else -> R.drawable.ic_map_marker_other_small +fun PinEntity.toMarkerModel(): MarkerModel { + val markerModel = MarkerModel( + Marker().apply { + position = LatLng(y, x) + isHideCollidedMarkers = true + icon = CategoryType.fromString(category).toMarkerIcon(false) } ) + + markerModel.isSelected.addOnPropertyChangedCallback( + object : + Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable?, propertyId: Int) { + with(markerModel.marker) { + icon = CategoryType.fromString(category).toMarkerIcon(markerModel.isSelected.get()) + } + } + } + ) + + return markerModel } diff --git a/app/src/main/java/org/sopt/pingle/presentation/model/MarkerModel.kt b/app/src/main/java/org/sopt/pingle/presentation/model/MarkerModel.kt new file mode 100644 index 00000000..d719d116 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/model/MarkerModel.kt @@ -0,0 +1,9 @@ +package org.sopt.pingle.presentation.model + +import androidx.databinding.ObservableBoolean +import com.naver.maps.map.overlay.Marker + +data class MarkerModel( + val marker: Marker, + var isSelected: ObservableBoolean = ObservableBoolean(false) +) diff --git a/app/src/main/java/org/sopt/pingle/presentation/type/CategoryType.kt b/app/src/main/java/org/sopt/pingle/presentation/type/CategoryType.kt index 3a965793..4ad2c848 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/type/CategoryType.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/type/CategoryType.kt @@ -9,9 +9,9 @@ enum class CategoryType( @ColorRes val activatedOutLinedColor: Int, @ColorRes val backgroundChipColor: Int, @ColorRes val backgroundBadgeColor: Int, - @StringRes val categoryNameRes: Int + @StringRes val categoryNameRes: Int, + @StringRes val categoryDescriptionRes: Int // TODO 해당 부분은 UX, icon 정해지면 추가하기 - // @StringRes val categoryDescriptionRes: Int, // @DrawableRes val categoryIconRes: Int, ) { PLAY( @@ -19,27 +19,36 @@ enum class CategoryType( activatedOutLinedColor = R.color.pingle_green, backgroundChipColor = R.color.chip_green, backgroundBadgeColor = R.color.badge_green, - categoryNameRes = R.string.category_play + categoryNameRes = R.string.category_play, + categoryDescriptionRes = R.string.category_play_detail ), STUDY( textColor = R.color.pingle_orange, activatedOutLinedColor = R.color.pingle_orange, backgroundChipColor = R.color.chip_orange, backgroundBadgeColor = R.color.badge_orange, - categoryNameRes = R.string.category_study + categoryNameRes = R.string.category_study, + categoryDescriptionRes = R.string.category_study_detail ), MULTI( textColor = R.color.pingle_yellow, activatedOutLinedColor = R.color.pingle_yellow, backgroundChipColor = R.color.chip_yellow, backgroundBadgeColor = R.color.badge_yellow, - categoryNameRes = R.string.category_multi + categoryNameRes = R.string.category_multi, + categoryDescriptionRes = R.string.category_multi_detail ), - OTHER( + OTHERS( textColor = R.color.g_01, activatedOutLinedColor = R.color.g_01, backgroundChipColor = R.color.g_10, backgroundBadgeColor = R.color.g_07, - categoryNameRes = R.string.category_others - ) + categoryNameRes = R.string.category_others, + categoryDescriptionRes = R.string.category_others_detail + ); + + companion object { + fun fromString(categoryName: String) = + CategoryType.values().first() { it.name == categoryName } + } } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/mainlist/MainListFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/mainlist/MainListFragment.kt new file mode 100644 index 00000000..e97b3046 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/mainlist/MainListFragment.kt @@ -0,0 +1,23 @@ +package org.sopt.pingle.presentation.ui.main.home.mainlist + +import android.os.Bundle +import android.view.View +import org.sopt.pingle.R +import org.sopt.pingle.databinding.FragmentMainListBinding +import org.sopt.pingle.presentation.ui.main.home.map.MapFragment +import org.sopt.pingle.util.base.BindingFragment +import org.sopt.pingle.util.fragment.navigateToFragment + +class MainListFragment : BindingFragment(R.layout.fragment_main_list) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + addListeners() + } + + private fun addListeners() { + binding.fabMainListMap.setOnClickListener { + navigateToFragment() + } + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapFragment.kt index 074d46f3..2b7d9bda 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapFragment.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapFragment.kt @@ -2,7 +2,6 @@ package org.sopt.pingle.presentation.ui.main.home.map import android.Manifest import android.content.pm.PackageManager -import android.location.Location import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts @@ -26,16 +25,30 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.sopt.pingle.R import org.sopt.pingle.databinding.FragmentMapBinding -import org.sopt.pingle.presentation.mapper.toMarker import org.sopt.pingle.presentation.type.CategoryType +import org.sopt.pingle.presentation.ui.common.AllModalDialogFragment +import org.sopt.pingle.presentation.ui.main.home.mainlist.MainListFragment import org.sopt.pingle.util.base.BindingFragment +import org.sopt.pingle.util.component.OnPingleCardClickListener import org.sopt.pingle.util.component.PingleChip +import org.sopt.pingle.util.fragment.navigateToFragment +import org.sopt.pingle.util.fragment.navigateToWebView import org.sopt.pingle.util.fragment.showToast +import org.sopt.pingle.util.fragment.stringOf class MapFragment : BindingFragment(R.layout.fragment_map), OnMapReadyCallback { private val mapViewModel by viewModels() private lateinit var naverMap: NaverMap private lateinit var locationSource: FusedLocationSource + private val locationPermissionRequest = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + when { + permissions[LOCATION_PERMISSIONS[0]] == true || permissions[LOCATION_PERMISSIONS[1]] == true -> { + setLocationTrackingMode() + } + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -44,7 +57,6 @@ class MapFragment : BindingFragment(R.layout.fragment_map), initLayout() addListeners() collectData() - setLocationTrackingMode() } override fun onMapReady(naverMap: NaverMap) { @@ -56,9 +68,14 @@ class MapFragment : BindingFragment(R.layout.fragment_map), isZoomControlEnabled = false isScaleBarEnabled = false } + + setOnMapClickListener { _, _ -> + mapViewModel.clearSelectedMarkerPosition() + } } makeMarkers() + setLocationTrackingMode() } private fun initMap() { @@ -79,14 +96,33 @@ class MapFragment : BindingFragment(R.layout.fragment_map), chipMapCategoryPlay.setChipCategoryType(CategoryType.PLAY) chipMapCategoryStudy.setChipCategoryType(CategoryType.STUDY) chipMapCategoryMulti.setChipCategoryType(CategoryType.MULTI) - chipMapCategoryOthers.setChipCategoryType(CategoryType.OTHER) + chipMapCategoryOthers.setChipCategoryType(CategoryType.OTHERS) + cardMap.listener = object : OnPingleCardClickListener { + override fun onPingleCardChatBtnClickListener() { + startActivity(navigateToWebView(mapViewModel.dummyPingle.chatLink)) + } + + override fun onPingleCardParticipateBtnClickListener() { + when (mapViewModel.dummyPingle.isParticipating) { + true -> showMapCancelModalDialogFragment() + false -> showMapModalDialogFragment() + } + } + } } } private fun addListeners() { binding.fabMapHere.setOnClickListener { if (::locationSource.isInitialized) { - locationSource.lastLocation?.let { location -> moveMapCamera(location) } + locationSource.lastLocation?.let { location -> + moveMapCamera( + LatLng( + location.latitude, + location.longitude + ) + ) + } } } @@ -96,6 +132,10 @@ class MapFragment : BindingFragment(R.layout.fragment_map), ?.let { group.findViewById(it).categoryType } ) } + + binding.fabMapList.setOnClickListener { + navigateToFragment() + } } private fun collectData() { @@ -103,6 +143,17 @@ class MapFragment : BindingFragment(R.layout.fragment_map), // TODO 서버 통신 구현 시 삭제 예정 showToast(it?.name ?: "null") }.launchIn(lifecycleScope) + + mapViewModel.selectedMarkerPosition.flowWithLifecycle(lifecycle) + .onEach { selectedMarkerPosition -> + (selectedMarkerPosition == MapViewModel.DEFAULT_SELECTED_MARKER_POSITION).run { + with(binding) { + fabMapHere.visibility = if (this@run) View.VISIBLE else View.INVISIBLE + fabMapList.visibility = if (this@run) View.VISIBLE else View.INVISIBLE + cardMap.visibility = if (this@run) View.INVISIBLE else View.VISIBLE + } + } + }.launchIn(lifecycleScope) } private fun setLocationTrackingMode() { @@ -114,57 +165,71 @@ class MapFragment : BindingFragment(R.layout.fragment_map), } ) { locationSource = FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE) - } else { - requestLocationPermission() - } - LocationServices.getFusedLocationProviderClient(requireContext()).lastLocation.addOnSuccessListener { location -> - if (::naverMap.isInitialized) { - with(naverMap) { - locationSource = this@MapFragment.locationSource - locationTrackingMode = LocationTrackingMode.NoFollow - - locationOverlay.apply { - isVisible = true - icon = OverlayImage.fromResource(R.drawable.ic_map_location_overlay) - } - } - } - - moveMapCamera(location) - } - } + with(naverMap) { + locationSource = this@MapFragment.locationSource + locationTrackingMode = LocationTrackingMode.NoFollow - private fun requestLocationPermission() { - val locationPermissionRequest = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - when { - permissions[LOCATION_PERMISSIONS[0]] == true || permissions[LOCATION_PERMISSIONS[1]] == true -> { - setLocationTrackingMode() + locationOverlay.apply { + isVisible = true + icon = OverlayImage.fromResource(R.drawable.ic_map_location_overlay) } } + } else { + locationPermissionRequest.launch(LOCATION_PERMISSIONS) } - locationPermissionRequest.launch(LOCATION_PERMISSIONS) + LocationServices.getFusedLocationProviderClient(requireContext()).lastLocation.addOnSuccessListener { location -> + moveMapCamera(LatLng(location.latitude, location.longitude)) + } } - private fun moveMapCamera(location: Location) { + private fun moveMapCamera(latLng: LatLng) { if (::naverMap.isInitialized) { naverMap.moveCamera( - CameraUpdate.scrollTo( - LatLng( - location.latitude, - location.longitude - ) - ).animate(CameraAnimation.Linear) + CameraUpdate.scrollTo(latLng).animate(CameraAnimation.Linear) ) } } private fun makeMarkers() { - mapViewModel.dummyPinList.map { pinEntity -> - pinEntity.toMarker().map = naverMap + mapViewModel.markerList.value.mapIndexed { index, markerModel -> + markerModel.marker.apply { + map = naverMap + setOnClickListener { + with(mapViewModel) { + handleMarkerClick(index) + // TODO 마커 상세 정보 받아오는 로직 추가 + binding.cardMap.initLayout(dummyPingle) + moveMapCamera(position) + } + return@setOnClickListener true + } + } + } + } + + private fun showMapCancelModalDialogFragment() { + AllModalDialogFragment( + title = stringOf(R.string.map_cancel_modal_title), + detail = stringOf(R.string.map_cancel_modal_detail), + buttonText = stringOf(R.string.map_cancel_modal_button_text), + textButtonText = stringOf(R.string.map_cancel_modal_text_button_text), + clickBtn = { mapViewModel.cancelPingle() }, + clickTextBtn = { }, + onDialogClosed = { binding.cardMap.initLayout(mapViewModel.dummyPingle) } + ).show(childFragmentManager, MAP_CANCEL_MODAL) + } + + private fun showMapModalDialogFragment() { + with(mapViewModel.dummyPingle) { + MapModalDialogFragment( + category = CategoryType.fromString(categoryName = category), + name = name, + ownerName = ownerName, + clickBtn = { mapViewModel.joinPingle() }, + onDialogClosed = { binding.cardMap.initLayout(mapViewModel.dummyPingle) } + ).show(childFragmentManager, MAP_MODAL) } } @@ -175,5 +240,20 @@ class MapFragment : BindingFragment(R.layout.fragment_map), Manifest.permission.ACCESS_COARSE_LOCATION ) private const val SINGLE_SELECTION = 0 + private const val MAP_CANCEL_MODAL = "mapCancelModal" + private const val MAP_MODAL = "mapModal" + + val OVERLAY_IMAGE_PLAY_SMALL = + OverlayImage.fromResource(R.drawable.ic_map_marker_play_small) + val OVERLAY_IMAGE_STUDY_SMALL = + OverlayImage.fromResource(R.drawable.ic_map_marker_study_small) + val OVERLAY_IMAGE_MULTI_SMALL = + OverlayImage.fromResource(R.drawable.ic_map_marker_multi_small) + val OVERLAY_IMAGE_OTHER_SMALL = + OverlayImage.fromResource(R.drawable.ic_map_marker_other_small) + val OVERLAY_IMAGE_PLAY_BIG = OverlayImage.fromResource(R.drawable.ic_map_marker_play_big) + val OVERLAY_IMAGE_STUDY_BIG = OverlayImage.fromResource(R.drawable.ic_map_marker_study_big) + val OVERLAY_IMAGE_MULTI_BIG = OverlayImage.fromResource(R.drawable.ic_map_marker_multi_big) + val OVERLAY_IMAGE_OTHER_BIG = OverlayImage.fromResource(R.drawable.ic_map_marker_other_big) } } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapModalDialogFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapModalDialogFragment.kt index b2a628a2..490ab2a8 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapModalDialogFragment.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapModalDialogFragment.kt @@ -1,5 +1,6 @@ package org.sopt.pingle.presentation.ui.main.home.map +import android.content.DialogInterface import android.os.Bundle import android.view.View import org.sopt.pingle.R @@ -12,7 +13,8 @@ class MapModalDialogFragment( private val category: CategoryType, private val name: String, private val ownerName: String, - private val clickBtn: () -> Unit + private val clickBtn: () -> Unit, + private val onDialogClosed: () -> Unit = {} ) : BindingDialogFragment(R.layout.dialog_map_modal) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -22,6 +24,11 @@ class MapModalDialogFragment( addListeners() } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDialogClosed.invoke() + } + private fun initLayout() { with(binding) { badgeMapModalPingleInfoCategory.setBadgeCategoryType(category) diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapViewModel.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapViewModel.kt index d4a9947d..cd4d2ae5 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapViewModel.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapViewModel.kt @@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.domain.model.PingleEntity +import org.sopt.pingle.presentation.mapper.toMarkerModel +import org.sopt.pingle.presentation.model.MarkerModel import org.sopt.pingle.presentation.type.CategoryType class MapViewModel() : ViewModel() { @@ -38,10 +41,100 @@ class MapViewModel() : ViewModel() { ) ) + var dummyPingle = PingleEntity( + id = 1, + category = "PLAY", + name = "핑글핑글핑글 ~", + ownerName = "배지현", + location = "길음역", + date = "2023-01-06", + startAt = "10:30:00", + endAt = "22:34:00", + maxParticipants = 5, + curParticipants = 4, + isParticipating = false, + chatLink = "https://github.com/TeamPINGLE/PINGLE-ANDROID" + ) + private val _category = MutableStateFlow(null) val category get() = _category.asStateFlow() + private var _markerList = MutableStateFlow>(emptyList()) + val markerList get() = _markerList.asStateFlow() + + private var _selectedMarkerPosition = MutableStateFlow(DEFAULT_SELECTED_MARKER_POSITION) + val selectedMarkerPosition = _selectedMarkerPosition.asStateFlow() + + init { + setMarkerList() + } + fun setCategory(category: CategoryType?) { _category.value = category } + + fun setMarkerList() { + _markerList.value = dummyPinList.map { pinEntity -> + pinEntity.toMarkerModel() + } + } + + fun handleMarkerClick(position: Int) { + setMarkerIsSelected(position) + if (_selectedMarkerPosition.value != DEFAULT_SELECTED_MARKER_POSITION) { + setMarkerIsSelected( + _selectedMarkerPosition.value + ) + } + _selectedMarkerPosition.value = position + } + + fun clearSelectedMarkerPosition() { + if (_selectedMarkerPosition.value != DEFAULT_SELECTED_MARKER_POSITION) { + setMarkerIsSelected(_selectedMarkerPosition.value) + _selectedMarkerPosition.value = DEFAULT_SELECTED_MARKER_POSITION + } + } + + private fun setMarkerIsSelected(position: Int) { + markerList.value[position].isSelected.set(!markerList.value[position].isSelected.get()) + } + + fun cancelPingle() { + dummyPingle = PingleEntity( + id = 1, + category = "PLAY", + name = "핑글핑글핑글 ~", + ownerName = "배지현", + location = "길음역", + date = "2023-01-06", + startAt = "10:30:00", + endAt = "22:34:00", + maxParticipants = 5, + curParticipants = 4, + isParticipating = false, + chatLink = "https://github.com/TeamPINGLE/PINGLE-ANDROID" + ) + } + + fun joinPingle() { + dummyPingle = PingleEntity( + id = 1, + category = "PLAY", + name = "핑글핑글핑글 ~", + ownerName = "배지현", + location = "길음역", + date = "2023-01-06", + startAt = "10:30:00", + endAt = "22:34:00", + maxParticipants = 5, + curParticipants = 5, + isParticipating = true, + chatLink = "https://github.com/TeamPINGLE/PINGLE-ANDROID" + ) + } + + companion object { + const val DEFAULT_SELECTED_MARKER_POSITION = -1 + } } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanActivity.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanActivity.kt index 4c39ccf1..632a060e 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanActivity.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanActivity.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.sopt.pingle.R import org.sopt.pingle.databinding.ActivityPlanBinding +import org.sopt.pingle.presentation.ui.main.plan.planlocation.PlanLocationFragment import org.sopt.pingle.util.base.BindingActivity import org.sopt.pingle.util.component.AllModalDialogFragment @@ -38,8 +39,10 @@ class PlanActivity : BindingActivity(R.layout.activity_plan // TODO 차후에 나머지 개최 프로세스 fragment 추가 fragmentList = ArrayList() fragmentList.apply { + add(PlanCategoryFragment()) add(PlanTitleFragment()) add(PlanDateTimeFragment()) + add(PlanLocationFragment()) add(PlanOpenChattingFragment()) } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanCategoryFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanCategoryFragment.kt new file mode 100644 index 00000000..9a1f7685 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanCategoryFragment.kt @@ -0,0 +1,43 @@ +package org.sopt.pingle.presentation.ui.main.plan + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import org.sopt.pingle.R +import org.sopt.pingle.databinding.FragmentPlanCategoryBinding +import org.sopt.pingle.presentation.type.CategoryType +import org.sopt.pingle.util.base.BindingFragment + +class PlanCategoryFragment : + BindingFragment(R.layout.fragment_plan_category) { + private val viewModel by viewModels() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.planViewModel = viewModel + binding.lifecycleOwner = this + + initLayout() + addListeners() + } + + private fun initLayout() { + } + + private fun addListeners() { + with(binding) { + includePlanCategoryTypePlay.root.setOnClickListener { + viewModel.setSelectedCategory(CategoryType.PLAY) + } + includePlanCategoryTypeStudy.root.setOnClickListener { + viewModel.setSelectedCategory(CategoryType.STUDY) + } + includePlanCategoryTypeMulti.root.setOnClickListener { + viewModel.setSelectedCategory(CategoryType.MULTI) + } + includePlanCategoryTypeOthers.root.setOnClickListener { + viewModel.setSelectedCategory(CategoryType.OTHERS) + } + } + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanDateTimeFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanDateTimeFragment.kt index 92b5023e..36066007 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanDateTimeFragment.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanDateTimeFragment.kt @@ -5,10 +5,11 @@ import android.view.View import androidx.fragment.app.activityViewModels import java.text.SimpleDateFormat import java.time.LocalDate +import java.util.Locale import org.sopt.pingle.R import org.sopt.pingle.databinding.FragmentPlanDateTimeBinding import org.sopt.pingle.util.base.BindingFragment -import timber.log.Timber +import org.sopt.pingle.util.component.CustomSnackbar class PlanDateTimeFragment : BindingFragment(R.layout.fragment_plan_date_time) { @@ -57,6 +58,11 @@ class PlanDateTimeFragment : if (selectedLocalDate != null) { if (selectedLocalDate.before(todayLocalDate)) { // TODO 에러 스낵바 노출 + CustomSnackbar.makeSnackbar( + binding.root, + getString(R.string.plan_future_date_snackber), + 126 + ) } else { binding.includePlanTextWithTitleDate.tvText.text = String.format( "%d년 %d월 %d일", @@ -69,16 +75,37 @@ class PlanDateTimeFragment : } } - // TODO 시간 포맷 나오면 수정 예정 - // TODO {} 수정 예정 - private fun onTimeDialogFragmentClosed(time: String) { + private fun onTimeDialogFragmentClosed(meridiem: String, hour: Int, minute: Int) { + val time12Hour = String.format("%02d:%02d %s", hour, minute, meridiem) when (planViewModel.selectedTimeType.value) { START_TIME -> { - Timber.d(START_TIME) + binding.includePlanTextWithTitleStartTime.tvText.text = + convert24HFormatHours(time12Hour, false) + planViewModel.setStartTime(convert24HFormatHours(time12Hour, true)) } END_TIME -> { - Timber.d(END_TIME) + binding.includePlanTextWithTitleEndTime.tvText.text = + convert24HFormatHours(time12Hour, false) + planViewModel.setEndTime(convert24HFormatHours(time12Hour, true)) + } + } + } + + private fun convert24HFormatHours(time12Hour: String, isWithSecond: Boolean): String { + val inputFormat = SimpleDateFormat("hh:mm a", Locale.US) + val outputFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + val outputFormatWithSecond = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + + val date = inputFormat.parse(time12Hour) + + return when (isWithSecond) { + false -> { + outputFormat.format(date) + } + + true -> { + outputFormatWithSecond.format(date) } } } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanTimeDialogFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanTimeDialogFragment.kt index df7831af..44509993 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanTimeDialogFragment.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanTimeDialogFragment.kt @@ -9,7 +9,7 @@ import org.sopt.pingle.presentation.type.MeridiemType import org.sopt.pingle.util.base.BindingBottomSheetDialogFragment class PlanTimeDialogFragment( - private val onDialogClosed: (String) -> Unit + private val onDialogClosed: (meridiem: String, hour: Int, minute: Int) -> Unit ) : BindingBottomSheetDialogFragment(R.layout.dialog_time_picker) { override fun onCreate(savedInstanceState: Bundle?) { @@ -57,17 +57,13 @@ class PlanTimeDialogFragment( private fun addListeners() { binding.tvTimePickerDone.setOnClickListener { - onDialogClosed("aaa") - // 시간 설정 후 editText에 띄우기 -// with(binding) { -// val timeFormat = -// String.format("%02d:%02d:00", npTimePickerHour.value, npTimePickerMinute.value) -// onDialogClosed(timeFormat) -// Log.d( -// "aaa", -// String.format("%02d:%02d:00", npTimePickerHour.value, npTimePickerMinute.value) -// ) -// } + with(binding) { + onDialogClosed( + if (npTimePickerMeridiem.value == 0) MeridiemType.AM.name else MeridiemType.PM.name, + npTimePickerHour.value, + npTimePickerMinute.value + ) + } dismiss() } } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanViewModel.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanViewModel.kt index 3425402d..3d48808d 100644 --- a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanViewModel.kt +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/PlanViewModel.kt @@ -6,31 +6,68 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import org.sopt.pingle.domain.model.PlanLocationEntity +import org.sopt.pingle.presentation.type.CategoryType import org.sopt.pingle.presentation.type.PlanType +import org.sopt.pingle.util.combineAll class PlanViewModel : ViewModel() { private val _currentPage = MutableStateFlow(FIRST_PAGE_POSITION) val currentPage get() = _currentPage.asStateFlow() val planTitle = MutableStateFlow("") - private val _planDate = MutableStateFlow(null) + private val _planDate = MutableStateFlow("") val planDate get() = _planDate.asStateFlow() + private val _startTime = MutableStateFlow("") + val startTime get() = _startTime.asStateFlow() + private val _endTime = MutableStateFlow("") + val endTime get() = _endTime.asStateFlow() private val _selectedTimeType = MutableStateFlow(null) + val selectedTimeType get() = _selectedTimeType.asStateFlow() val planOpenChattingLink = MutableStateFlow("") // TODO Type에 맞게 수정(현재는 내가 맡은 부분 테스트를 위해 postion 값을 조정 및 임의 지정 해놓음 - val isPlanBtnEnabled: StateFlow = - combine( - currentPage, - planTitle, - planOpenChattingLink - ) { currentPage, planTitle, planOpenChattingLink -> + val isPlanBtn = + listOf(currentPage, planTitle, planDate, startTime, endTime, planOpenChattingLink) + .combineAll() + + // TODO 수정 예정, 테스트를 위해 position값 임의 설정 + val isPlanBtnEnabled: StateFlow = listOf( + currentPage, + planTitle, + planDate, + startTime, + endTime, + planOpenChattingLink + ).combineAll() + .map { values -> + val currentPage = values[0] as Int + val planTitle = values[1] as String + val planDate = values[2] as String + val startTime = values[3] as String + val endTime = values[4] as String + val planOpenChattingLink = values[5] as String + (currentPage == PlanType.TITLE.position - 1 && planTitle.isNotBlank()) || - currentPage == 1 || + (currentPage == 1 && planDate.isNotBlank() && startTime.isNotBlank() && endTime.isNotBlank()) || (currentPage == 2 && planOpenChattingLink.isNotBlank()) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + // val isPlanBtnEnabled = MutableStateFlow(true) + + private val _selectedLocation = MutableStateFlow(null) + val selectedLocation get() = _selectedLocation.asStateFlow() + + // TODO 뷰 연결 시 버튼 활성/비활성화 로직 isPlanBtnEnabled에 추가 + private val _selectedCategory = MutableStateFlow(null) + val selectedCategory get() = _selectedCategory.asStateFlow() + + fun setSelectedCategory(categoryType: CategoryType) { + _selectedCategory.value = categoryType + } fun setCurrentPage(position: Int) { _currentPage.value = position @@ -40,11 +77,91 @@ class PlanViewModel : ViewModel() { _planDate.value = planDate } + fun setStartTime(time: String) { + _startTime.value = time + } + + fun setEndTime(time: String) { + _endTime.value = time + } + fun setSelectedTimeType(timeType: String) { _selectedTimeType.value = timeType } + private fun setPlanLocation(position: Int) { + _selectedLocation.value = mockPlanLocationList[position] + // _selectedLocation.value = planLocationList[position] + } + companion object { const val FIRST_PAGE_POSITION = 0 } + + private val _planLocationList = MutableStateFlow>(emptyList()) + private val planLocationList get() = _planLocationList.asStateFlow() + + private var oldPosition = -1 + fun updatePlanLocationList(position: Int) { + if (oldPosition == -1 && oldPosition != position) { + setIsSelected(true, position) + setPlanLocation(position) + } else if (oldPosition == position) { + setIsSelected(false, oldPosition) + } else { + setIsSelected(true, position) + setPlanLocation(position) + setIsSelected(false, oldPosition) + } + oldPosition = position + } + + fun checkIsNull(): Boolean { + return mockPlanLocationList.isEmpty() + // TODO return planLocationList.value.isEmpty() + } + + private fun setIsSelected(value: Boolean, position: Int) { + mockPlanLocationList[position].isSelected.set(value) + // TODO 서버에서 받아올 리스트에 저장.. planLocationList.value[position].isSelected.set(value) + } + + val mockPlanLocationList = listOf( + PlanLocationEntity( + location = "하얀집", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집2호점", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집3호점", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집 싫어싫어싫어", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집 좋아좋아좋아", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집웅시러", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ) + ) } diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationAdapter.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationAdapter.kt new file mode 100644 index 00000000..4be7d14d --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationAdapter.kt @@ -0,0 +1,39 @@ +package org.sopt.pingle.presentation.ui.main.plan.planlocation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.sopt.pingle.databinding.ItemPlanLocationBinding +import org.sopt.pingle.domain.model.PlanLocationEntity + +class PlanLocationAdapter( + private val setOldItem: (Int) -> Unit +) : + ListAdapter( + PlanLocationDiffCallback() + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlanLocationViewHolder = + PlanLocationViewHolder( + ItemPlanLocationBinding.inflate(LayoutInflater.from(parent.context), parent, false), + setOldItem + ) + + override fun onBindViewHolder(holder: PlanLocationViewHolder, position: Int) { + holder.onBind(getItem(position)) + } +} + +class PlanLocationViewHolder( + private val binding: ItemPlanLocationBinding, + private val setOldItem: (Int) -> Unit +) : RecyclerView.ViewHolder(binding.root) { + fun onBind(item: PlanLocationEntity) { + binding.planLocationItem = item + + binding.root.setOnClickListener { + setOldItem(bindingAdapterPosition) + } + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDiffCallback.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDiffCallback.kt new file mode 100644 index 00000000..4da9a87a --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDiffCallback.kt @@ -0,0 +1,20 @@ +package org.sopt.pingle.presentation.ui.main.plan.planlocation + +import androidx.recyclerview.widget.DiffUtil +import org.sopt.pingle.domain.model.PlanLocationEntity + +class PlanLocationDiffCallback : DiffUtil.ItemCallback() { + + // Referential equality를 갖는지 판정 + override fun areItemsTheSame(oldItem: PlanLocationEntity, newItem: PlanLocationEntity): Boolean { + return oldItem === newItem + } + + // Structural equality를 갖는지 판정 + override fun areContentsTheSame( + oldItem: PlanLocationEntity, + newItem: PlanLocationEntity + ): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDivider.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDivider.kt new file mode 100644 index 00000000..cfe6823f --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationDivider.kt @@ -0,0 +1,48 @@ +package org.sopt.pingle.presentation.ui.main.plan.planlocation + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +class PlanLocationDivider( + private val dividerHeight: Int, + private val dividerColor: Int = Color.TRANSPARENT +) : RecyclerView.ItemDecoration() { + private val paint = Paint() + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + planLocationDivider(c, parent, color = dividerColor) + } + + private fun planLocationDivider(c: Canvas, parent: RecyclerView, color: Int) { + paint.color = color + + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + val param = child.layoutParams as RecyclerView.LayoutParams + + val dividerTop = child.bottom + param.bottomMargin + val dividerBottom = dividerTop + dividerHeight + + c.drawRect( + child.left.toFloat(), + dividerTop.toFloat(), + child.right.toFloat(), + dividerBottom.toFloat(), + paint + ) + } + } + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + outRect.bottom = dividerHeight + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationFragment.kt new file mode 100644 index 00000000..69cffe23 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationFragment.kt @@ -0,0 +1,77 @@ +package org.sopt.pingle.presentation.ui.main.plan.planlocation + +import android.os.Bundle +import android.view.KeyEvent +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import org.sopt.pingle.R +import org.sopt.pingle.databinding.FragmentPlanLocationBinding +import org.sopt.pingle.presentation.ui.main.plan.PlanViewModel +import org.sopt.pingle.util.base.BindingFragment +import org.sopt.pingle.util.context.hideKeyboard + +class PlanLocationFragment : + BindingFragment(R.layout.fragment_plan_location) { + private val planLocationViewModel by viewModels() + private val planLocationAdapter: PlanLocationAdapter by lazy { + PlanLocationAdapter(::deleteOldPosition) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initLayout() + addListeners() + } + + private fun initLayout() { + binding.rvPlanLocationList.apply { + this.layoutManager = LinearLayoutManager(context) + adapter = planLocationAdapter + /*addItemDecoration( + PlanLocationDivider(1, R.color.g_09), + )*/ + } + planLocationAdapter.submitList(planLocationViewModel.mockPlanLocationList) + } + + private fun addListeners() { + binding.ivPlanLocationSearchBtn.setOnClickListener { + checkListExist() + } + + val searchListener = binding.etPlanLocationSearch + searchListener.setOnKeyListener( + View.OnKeyListener { _, keyCode, event -> + if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP) { + checkListExist() + requireActivity().hideKeyboard(searchListener) + return@OnKeyListener true + } + false + } + ) + } + + private fun deleteOldPosition(position: Int) { + planLocationViewModel.updatePlanLocationList(position) + } + + private fun checkListExist() = if (planLocationViewModel.checkIsNull()) { + with(binding) { + rvPlanLocationList.visibility = View.INVISIBLE + layoutPlanLocationEmpty.visibility = View.VISIBLE + } + } else { + with(binding) { + rvPlanLocationList.visibility = View.VISIBLE + layoutPlanLocationEmpty.visibility = View.INVISIBLE + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding.rvPlanLocationList.adapter = null + } +} diff --git a/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationViewModel.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationViewModel.kt new file mode 100644 index 00000000..00b62c2e --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/presentation/ui/main/plan/planlocation/PlanLocationViewModel.kt @@ -0,0 +1,73 @@ +package org.sopt.pingle.presentation.ui.main.plan.planlocation + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.sopt.pingle.domain.model.PlanLocationEntity + +class PlanLocationViewModel : ViewModel() { + private val _planLocationList = MutableStateFlow>(emptyList()) + private val planLocationList get() = _planLocationList.asStateFlow() + + private var oldPosition = -1 + fun updatePlanLocationList(position: Int) { + if (oldPosition == -1 && oldPosition != position) { + setIsSelected(true, position) + } else if (oldPosition == position) { + setIsSelected(false, oldPosition) + } else { + setIsSelected(true, position) + setIsSelected(false, oldPosition) + } + oldPosition = position + } + + fun checkIsNull(): Boolean { + return mockPlanLocationList.isEmpty() + // TODO return planLocationList.value.isEmpty() + } + + private fun setIsSelected(value: Boolean, position: Int) { + mockPlanLocationList[position].isSelected.set(value) + // TODO 서버에서 받아올 리스트에 저장.. planLocationList.value[position].isSelected.set(value) + } + + val mockPlanLocationList = listOf( + PlanLocationEntity( + location = "하얀집", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집2호점", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집3호점", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집 싫어싫어싫어", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집 좋아좋아좋아", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ), + PlanLocationEntity( + location = "하얀집웅시러", + address = "서울 중구 퇴계로6길 12", + x = 123.5, + y = 56.7 + ) + ) +} diff --git a/app/src/main/java/org/sopt/pingle/util/CombineAll.kt b/app/src/main/java/org/sopt/pingle/util/CombineAll.kt new file mode 100644 index 00000000..1d334fe6 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/util/CombineAll.kt @@ -0,0 +1,14 @@ +package org.sopt.pingle.util + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flowOf + +inline fun List>.combineAll(): Flow> { + return when (size) { + 0 -> flowOf(emptyList()) + else -> combineTransform(this) { flows -> + emit(flows.toList()) + } + } +} diff --git a/app/src/main/java/org/sopt/pingle/util/base/BindingAdapter.kt b/app/src/main/java/org/sopt/pingle/util/base/BindingAdapter.kt index 1ee2e426..ddf57765 100644 --- a/app/src/main/java/org/sopt/pingle/util/base/BindingAdapter.kt +++ b/app/src/main/java/org/sopt/pingle/util/base/BindingAdapter.kt @@ -1,6 +1,8 @@ package org.sopt.pingle.util.base +import android.view.View import android.widget.ImageView +import android.widget.TextView import androidx.core.content.ContextCompat import androidx.databinding.BindingAdapter @@ -9,3 +11,27 @@ fun setImageResource(imageView: ImageView, resId: Int) { val drawable = ContextCompat.getDrawable(imageView.context, resId) imageView.setImageDrawable(drawable) } + +@BindingAdapter("color") +fun setTextColor(textView: TextView, resId: Int) { + val colorRes = ContextCompat.getColor(textView.context, resId) + textView.setTextColor(colorRes) +} + +@BindingAdapter("selection") +fun setSelected(view: View, isSelected: Boolean) { + view.isSelected = isSelected +} + +@BindingAdapter("visibility") +fun View.setVisibility(isVisible: Boolean?) { + if (isVisible == null) { + return + } + this.visibility = + if (isVisible) { + View.VISIBLE + } else { + View.INVISIBLE + } +} diff --git a/app/src/main/java/org/sopt/pingle/util/component/AllModalDialogFragment.kt b/app/src/main/java/org/sopt/pingle/util/component/AllModalDialogFragment.kt index 85ebcc3b..64eacc08 100644 --- a/app/src/main/java/org/sopt/pingle/util/component/AllModalDialogFragment.kt +++ b/app/src/main/java/org/sopt/pingle/util/component/AllModalDialogFragment.kt @@ -1,5 +1,6 @@ package org.sopt.pingle.util.component +import android.content.DialogInterface import android.os.Bundle import android.view.View import org.sopt.pingle.R @@ -12,7 +13,8 @@ class AllModalDialogFragment( private val buttonText: String, private val textButtonText: String, private val clickBtn: () -> Unit, - private val clickTextBtn: () -> Unit + private val clickTextBtn: () -> Unit, + private val onDialogClosed: () -> Unit = {} ) : BindingDialogFragment(R.layout.dialog_all_modal) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -21,6 +23,11 @@ class AllModalDialogFragment( addListeners() } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + onDialogClosed() + } + private fun initLayout() { with(binding) { tvAllModalTitle.text = title diff --git a/app/src/main/java/org/sopt/pingle/util/component/PingleCard.kt b/app/src/main/java/org/sopt/pingle/util/component/PingleCard.kt new file mode 100644 index 00000000..72d640e0 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/util/component/PingleCard.kt @@ -0,0 +1,132 @@ +package org.sopt.pingle.util.component + +import android.annotation.SuppressLint +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.text.style.TextAppearanceSpan +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import org.sopt.pingle.R +import org.sopt.pingle.databinding.CardPingleBinding +import org.sopt.pingle.domain.model.PingleEntity +import org.sopt.pingle.presentation.type.CategoryType +import org.sopt.pingle.util.view.colorOf +import org.sopt.pingle.util.view.stringOf + +@SuppressLint("CustomViewStyleable") +class PingleCard @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + private val binding: CardPingleBinding + var listener: OnPingleCardClickListener? = null + + init { + binding = CardPingleBinding.inflate(LayoutInflater.from(context), this, true) + + addListeners() + } + + private fun addListeners() { + binding.btnCardBottomMapChat.setOnClickListener { + listener?.onPingleCardChatBtnClickListener() + } + + binding.btnCardBottomMapParticipate.setOnClickListener { + listener?.onPingleCardParticipateBtnClickListener() + } + } + + fun initLayout(pingleEntity: PingleEntity) { + val category: CategoryType = CategoryType.fromString(pingleEntity.category) + + with(binding) { + badgeCardTopInfo.setBadgeCategoryType(category) + tvCardTopInfoName.text = pingleEntity.name + tvCardTopInfoName.setTextColor(colorOf(category.textColor)) + tvCardTopInfoOwnerName.text = pingleEntity.ownerName + tvCardBottomCalenderDetail.text = pingleEntity.convertToCalenderDetail() + tvCardBottomMapDetail.text = pingleEntity.location + btnCardBottomMapChat.isEnabled = pingleEntity.isParticipating + btnCardBottomMapParticipate.text = when (pingleEntity.isParticipating) { + true -> stringOf(R.string.map_card_cancel) + false -> stringOf(R.string.map_card_participate) + } + btnCardBottomMapParticipate.isEnabled = + pingleEntity.isParticipating || !pingleEntity.isCompleted() + + if (pingleEntity.isCompleted()) { + with(tvCardTopInfoParticipantDetail) { + text = stringOf(R.string.map_card_completed) + setTextAppearance(R.style.TextAppearance_Pingle_Sub_Semi_16) + } + } else { + with(tvCardTopInfoParticipantDetail) { + val participantDetail = context.getString( + R.string.map_card_participant_detail, + pingleEntity.curParticipants, + pingleEntity.maxParticipants + ) + text = SpannableString(participantDetail).apply { + setSpan( + ForegroundColorSpan( + colorOf(category.textColor) + ), + CUR_PARTICIPANTS_START, + pingleEntity.curParticipants.toString().length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + setSpan( + TextAppearanceSpan( + context, + R.style.TextAppearance_Pingle_Title_Semi_30 + ), + CUR_PARTICIPANTS_START, + pingleEntity.curParticipants.toString().length + 1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + setSpan( + TextAppearanceSpan( + context, + R.style.TextAppearance_Pingle_Title_Semi_20 + ), + pingleEntity.curParticipants.toString().length + 1, + participantDetail.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + } + } + } + + companion object { + const val CUR_PARTICIPANTS_START = 0 + } +} + +interface OnPingleCardClickListener { + fun onPingleCardChatBtnClickListener() + fun onPingleCardParticipateBtnClickListener() +} + +fun PingleEntity.isCompleted() = maxParticipants == curParticipants + +fun PingleEntity.convertToCalenderDetail(): String { + val localDate = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE) + val startTime = LocalTime.parse(startAt, DateTimeFormatter.ISO_LOCAL_TIME) + val endTime = LocalTime.parse(endAt, DateTimeFormatter.ISO_LOCAL_TIME) + + return buildString { + append("${localDate.year}년 ${localDate.monthValue}월 ${localDate.dayOfMonth}일\n") + append("${startTime.format(DateTimeFormatter.ofPattern("HH:mm"))} ~ ") + append("${endTime.format(DateTimeFormatter.ofPattern("HH:mm"))}") + } +} diff --git a/app/src/main/java/org/sopt/pingle/util/fragment/FragmentExt.kt b/app/src/main/java/org/sopt/pingle/util/fragment/FragmentExt.kt index c07deab7..2f0a9173 100644 --- a/app/src/main/java/org/sopt/pingle/util/fragment/FragmentExt.kt +++ b/app/src/main/java/org/sopt/pingle/util/fragment/FragmentExt.kt @@ -7,6 +7,9 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.fragment.app.replace +import org.sopt.pingle.R import org.sopt.pingle.presentation.ui.common.WebViewActivity fun Fragment.showToast(message: String, isShort: Boolean = true) { @@ -25,3 +28,8 @@ fun Fragment.navigateToWebView(link: String) = Intent(requireContext(), WebViewActivity::class.java).apply { putExtra(WebViewActivity.WEB_VIEW_LINK, link) } +inline fun Fragment.navigateToFragment() { + parentFragmentManager.commit { + replace(R.id.fcv_main_all_navi, T::class.java.canonicalName) + } +} diff --git a/app/src/main/res/color/selector_pingle_btn_m.xml b/app/src/main/res/color/selector_pingle_btn_m.xml new file mode 100644 index 00000000..df8f2c15 --- /dev/null +++ b/app/src/main/res/color/selector_pingle_btn_m.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_pingle_btn_s.xml b/app/src/main/res/color/selector_pingle_btn_s.xml new file mode 100644 index 00000000..7b6fae3d --- /dev/null +++ b/app/src/main/res/color/selector_pingle_btn_s.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_disabled_plan_location_category.xml b/app/src/main/res/drawable/background_disabled_plan_location_category.xml new file mode 100644 index 00000000..d09bf503 --- /dev/null +++ b/app/src/main/res/drawable/background_disabled_plan_location_category.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_enabled_plan_location_category.xml b/app/src/main/res/drawable/background_enabled_plan_location_category.xml new file mode 100644 index 00000000..0867e829 --- /dev/null +++ b/app/src/main/res/drawable/background_enabled_plan_location_category.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_map_marker_multi_big.xml b/app/src/main/res/drawable/ic_map_marker_multi_big.xml new file mode 100644 index 00000000..d151a5c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_multi_big.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_marker_other_big.xml b/app/src/main/res/drawable/ic_map_marker_other_big.xml new file mode 100644 index 00000000..1a36084c --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_other_big.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_marker_play_big.xml b/app/src/main/res/drawable/ic_map_marker_play_big.xml new file mode 100644 index 00000000..b3cb1a65 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_play_big.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_map_marker_study_big.xml b/app/src/main/res/drawable/ic_map_marker_study_big.xml new file mode 100644 index 00000000..a39fb0f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_map_marker_study_big.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_item_plan_location_category.xml b/app/src/main/res/drawable/selector_item_plan_location_category.xml new file mode 100644 index 00000000..90445a7b --- /dev/null +++ b/app/src/main/res/drawable/selector_item_plan_location_category.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_oval_stroke_1.xml b/app/src/main/res/drawable/shape_oval_stroke_1.xml new file mode 100644 index 00000000..4b79d3aa --- /dev/null +++ b/app/src/main/res/drawable/shape_oval_stroke_1.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/card_pingle.xml b/app/src/main/res/layout/card_pingle.xml new file mode 100644 index 00000000..dee84ad1 --- /dev/null +++ b/app/src/main/res/layout/card_pingle.xml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_commend.xml b/app/src/main/res/layout/fragment_commend.xml index 449af822..34218043 100644 --- a/app/src/main/res/layout/fragment_commend.xml +++ b/app/src/main/res/layout/fragment_commend.xml @@ -13,15 +13,39 @@ tools:context=".presentation.ui.main.commend.CommendFragment"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main_list.xml b/app/src/main/res/layout/fragment_main_list.xml new file mode 100644 index 00000000..de0de493 --- /dev/null +++ b/app/src/main/res/layout/fragment_main_list.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 1a378bc8..04d62eec 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -85,5 +85,15 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_my_pingle.xml b/app/src/main/res/layout/fragment_my_pingle.xml index 3072f597..7c6c9de3 100644 --- a/app/src/main/res/layout/fragment_my_pingle.xml +++ b/app/src/main/res/layout/fragment_my_pingle.xml @@ -13,15 +13,39 @@ tools:context=".presentation.ui.main.mypingle.MyPingleFragment"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plan_category.xml b/app/src/main/res/layout/fragment_plan_category.xml new file mode 100644 index 00000000..0bda598b --- /dev/null +++ b/app/src/main/res/layout/fragment_plan_category.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_plan_location.xml b/app/src/main/res/layout/fragment_plan_location.xml new file mode 100644 index 00000000..17461ddc --- /dev/null +++ b/app/src/main/res/layout/fragment_plan_location.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_plan_location.xml b/app/src/main/res/layout/item_plan_location.xml new file mode 100644 index 00000000..3b196cf9 --- /dev/null +++ b/app/src/main/res/layout/item_plan_location.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_plan_category_type.xml b/app/src/main/res/layout/view_plan_category_type.xml new file mode 100644 index 00000000..9faa35c8 --- /dev/null +++ b/app/src/main/res/layout/view_plan_category_type.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7febdf1c..3df7c155 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -39,6 +39,7 @@ 42dp 42dp 10dp + 95dp 50dp @@ -50,4 +51,8 @@ 24dp 34dp 24dp + + + 24dp + 81dp \ 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 463ce7e8..249d91ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,10 @@ STUDY MULTI OTHERS + 노는게 제일 좋아! + 열공, 열작업 할 사람 모여라! + 놀 땐 놀고, 일할 땐 일하자! + 다른 활동이 하고 싶다면? @@ -36,6 +40,8 @@ 나가기 다음으로 핑글 개최하기 + 오전 + 오후 핑글러들과\n언제 만날까요? 핑글 날짜 만날 날짜를 선택해주세요 @@ -45,8 +51,11 @@ 한줄 소개 개최할 핑글을\n소개해주세요! ex. 강남역에서 각잡고 공부할 사람? - 오전 - 오후 + 미래 날짜를 선택해주세요 + 링크를 입력해주세요 + 채팅방 링크 + + 잠깐! 나가실건가요? 지금까지 입력한 정보는 전부 사라져요 이어서 작성하기 @@ -63,6 +72,36 @@ 이 핑글에 참여할까요? - 링크를 입력해주세요 - 채팅방 링크 + + + 참여자 + %d/%d + 일시 + 장소 + 대화하기 + 참여하기 + 취소하기 + 모집완료 + + + 참여를 취소하시겠어요? + 취소한 모임은 언제든 다시 신청할 수 있어요 + 취소하기 + 돌아가기 + + + 핑글러들과\n어디서 만날까요? + 핑글이 열릴 장소 이름을 검색해보세요 + 다음으로 + 나중에 만드시겠어요? + 나가기 + 검색 결과가 없어요 + 다른 장소로 검색해보세요 + + + 개최할 핑글을\n선택해주세요 + + + 아직 공사중! + 아직 구현중인 기능이에요\n조금만 기다려주세요 \ No newline at end of file diff --git a/app/src/main/res/values/text_appearance.xml b/app/src/main/res/values/text_appearance.xml index 22734dff..c2d353f3 100644 --- a/app/src/main/res/values/text_appearance.xml +++ b/app/src/main/res/values/text_appearance.xml @@ -30,7 +30,7 @@ @font/suit_semibold - + + +