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
-
+
+
+