diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f965d7b9..40aae96a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,7 +81,7 @@ tools:ignore="LockedOrientationActivity" /> > + suspend fun getPinListWithoutFiltering( + teamId: Long, + category: String? + ): BaseResponse> + + suspend fun getPingleList(teamId: Long, pinId: Long): BaseResponse> } diff --git a/app/src/main/java/org/sopt/pingle/data/datasourceimpl/remote/MapRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/pingle/data/datasourceimpl/remote/MapRemoteDataSourceImpl.kt index 90a81912..fd7e6661 100644 --- a/app/src/main/java/org/sopt/pingle/data/datasourceimpl/remote/MapRemoteDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/pingle/data/datasourceimpl/remote/MapRemoteDataSourceImpl.kt @@ -3,6 +3,7 @@ package org.sopt.pingle.data.datasourceimpl.remote import javax.inject.Inject import org.sopt.pingle.data.datasource.remote.MapRemoteDataSource import org.sopt.pingle.data.model.remote.response.ResponsePinListDto +import org.sopt.pingle.data.model.remote.response.ResponsePingleListDto import org.sopt.pingle.data.service.MapService import org.sopt.pingle.util.base.BaseResponse @@ -14,4 +15,10 @@ class MapRemoteDataSourceImpl @Inject constructor( category: String? ): BaseResponse> = mapService.getPinListWithoutFiltering(teamId = teamId, category = category) + + override suspend fun getPingleList( + teamId: Long, + pinId: Long + ): BaseResponse> = + mapService.getPingleList(teamId = teamId, pinId = pinId) } diff --git a/app/src/main/java/org/sopt/pingle/data/model/remote/response/ResponsePingleListDto.kt b/app/src/main/java/org/sopt/pingle/data/model/remote/response/ResponsePingleListDto.kt new file mode 100644 index 00000000..37302508 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/data/model/remote/response/ResponsePingleListDto.kt @@ -0,0 +1,48 @@ +package org.sopt.pingle.data.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.sopt.pingle.domain.model.PingleEntity + +@Serializable +data class ResponsePingleListDto( + @SerialName("id") + val id: Long, + @SerialName("category") + val category: String, + @SerialName("name") + val name: String, + @SerialName("ownerName") + val ownerName: String, + @SerialName("location") + val location: String, + @SerialName("date") + val date: String, + @SerialName("startAt") + val startAt: String, + @SerialName("endAt") + val endAt: String, + @SerialName("maxParticipants") + val maxParticipants: Int, + @SerialName("curParticipants") + val curParticipants: Int, + @SerialName("isParticipating") + val isParticipating: Boolean, + @SerialName("chatLink") + val chatLink: String +) { + fun toPingleEntity() = PingleEntity( + id = this.id, + category = this.category, + name = this.name, + ownerName = this.ownerName, + location = this.location, + date = this.date, + startAt = this.startAt, + endAt = this.endAt, + maxParticipants = this.maxParticipants, + curParticipants = this.curParticipants, + isParticipating = this.isParticipating, + chatLink = this.chatLink + ) +} diff --git a/app/src/main/java/org/sopt/pingle/data/repository/MapRepositoryImpl.kt b/app/src/main/java/org/sopt/pingle/data/repository/MapRepositoryImpl.kt index 1ab94186..62bc0e3b 100644 --- a/app/src/main/java/org/sopt/pingle/data/repository/MapRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/pingle/data/repository/MapRepositoryImpl.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import org.sopt.pingle.data.datasource.remote.MapRemoteDataSource import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.domain.model.PingleEntity import org.sopt.pingle.domain.repository.MapRepository class MapRepositoryImpl @Inject constructor( @@ -24,4 +25,16 @@ class MapRepositoryImpl @Inject constructor( } emit(result.getOrThrow()) } + + override suspend fun getPingleList(teamId: Long, pinId: Long): Flow> = flow { + val result = runCatching { + mapDataSource.getPingleList( + teamId = teamId, + pinId = pinId + ).data.map { pingle -> + pingle.toPingleEntity() + } + } + emit(result.getOrThrow()) + } } diff --git a/app/src/main/java/org/sopt/pingle/data/service/MapService.kt b/app/src/main/java/org/sopt/pingle/data/service/MapService.kt index c3301035..22f10901 100644 --- a/app/src/main/java/org/sopt/pingle/data/service/MapService.kt +++ b/app/src/main/java/org/sopt/pingle/data/service/MapService.kt @@ -1,6 +1,7 @@ package org.sopt.pingle.data.service import org.sopt.pingle.data.model.remote.response.ResponsePinListDto +import org.sopt.pingle.data.model.remote.response.ResponsePingleListDto import org.sopt.pingle.util.base.BaseResponse import retrofit2.http.GET import retrofit2.http.Path @@ -13,11 +14,19 @@ interface MapService { @Query(CATEGORY) category: String? ): BaseResponse> + @GET("$VERSION/$TEAMS/{$TEAM_ID}/$PINS/{$PIN_ID}/$MEETINGS") + suspend fun getPingleList( + @Path("$TEAM_ID") teamId: Long, + @Path("$PIN_ID") pinId: Long + ): BaseResponse> + companion object { const val VERSION = "v1" const val TEAMS = "teams" - const val PINS = "pins" const val TEAM_ID = "teamId" + const val PINS = "pins" + const val PIN_ID = "pinId" const val CATEGORY = "category" + const val MEETINGS = "meetings" } } diff --git a/app/src/main/java/org/sopt/pingle/di/UseCaseModule.kt b/app/src/main/java/org/sopt/pingle/di/UseCaseModule.kt index 0c360644..2ae06366 100644 --- a/app/src/main/java/org/sopt/pingle/di/UseCaseModule.kt +++ b/app/src/main/java/org/sopt/pingle/di/UseCaseModule.kt @@ -9,6 +9,7 @@ import org.sopt.pingle.domain.repository.DummyRepository import org.sopt.pingle.domain.repository.MapRepository import org.sopt.pingle.domain.usecase.GetDummyUserListUseCase import org.sopt.pingle.domain.usecase.GetPinListWithoutFilteringUseCase +import org.sopt.pingle.domain.usecase.GetPingleListUseCase import org.sopt.pingle.domain.usecase.SetDummyDataUseCase @Module @@ -28,4 +29,9 @@ class UseCaseModule { @Singleton fun providesGetPinListWithoutFilteringUseCase(mapRepository: MapRepository): GetPinListWithoutFilteringUseCase = GetPinListWithoutFilteringUseCase(mapRepository = mapRepository) + + @Provides + @Singleton + fun providesGetPingleListUseCase(mapRepository: MapRepository): GetPingleListUseCase = + GetPingleListUseCase(mapRepository = mapRepository) } diff --git a/app/src/main/java/org/sopt/pingle/domain/repository/MapRepository.kt b/app/src/main/java/org/sopt/pingle/domain/repository/MapRepository.kt index 69497652..f589e83f 100644 --- a/app/src/main/java/org/sopt/pingle/domain/repository/MapRepository.kt +++ b/app/src/main/java/org/sopt/pingle/domain/repository/MapRepository.kt @@ -2,7 +2,9 @@ package org.sopt.pingle.domain.repository import kotlinx.coroutines.flow.Flow import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.domain.model.PingleEntity interface MapRepository { suspend fun getPinListWithoutFiltering(teamId: Long, category: String?): Flow> + suspend fun getPingleList(teamId: Long, pinId: Long): Flow> } diff --git a/app/src/main/java/org/sopt/pingle/domain/usecase/GetPingleListUseCase.kt b/app/src/main/java/org/sopt/pingle/domain/usecase/GetPingleListUseCase.kt new file mode 100644 index 00000000..3b556634 --- /dev/null +++ b/app/src/main/java/org/sopt/pingle/domain/usecase/GetPingleListUseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.pingle.domain.usecase + +import kotlinx.coroutines.flow.Flow +import org.sopt.pingle.domain.model.PingleEntity +import org.sopt.pingle.domain.repository.MapRepository + +class GetPingleListUseCase( + private val mapRepository: MapRepository +) { + suspend operator fun invoke(teamId: Long, pinId: Long): Flow> = + mapRepository.getPingleList(teamId = teamId, pinId = pinId) +} 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 ce5d8577..8d786c7b 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 @@ -3,7 +3,11 @@ package org.sopt.pingle.presentation.mapper import androidx.databinding.Observable import com.naver.maps.geometry.LatLng import com.naver.maps.map.overlay.Marker +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.domain.model.PingleEntity import org.sopt.pingle.presentation.model.MarkerModel import org.sopt.pingle.presentation.type.CategoryType @@ -29,3 +33,17 @@ fun PinEntity.toMarkerModel(): MarkerModel { return markerModel } + +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/presentation/ui/main/home/map/MapFragment.kt b/app/src/main/java/org/sopt/pingle/presentation/ui/main/home/map/MapFragment.kt index 773aadb5..3f531150 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 @@ -26,14 +26,15 @@ 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.model.MarkerModel +import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.presentation.mapper.toMarkerModel import org.sopt.pingle.presentation.type.CategoryType import org.sopt.pingle.presentation.ui.main.home.mainlist.MainListFragment import org.sopt.pingle.util.base.BindingFragment import org.sopt.pingle.util.component.AllModalDialogFragment -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.stringOf import org.sopt.pingle.util.view.UiState @@ -70,6 +71,7 @@ class MapFragment : BindingFragment(R.layout.fragment_map), with(uiSettings) { isZoomControlEnabled = false isScaleBarEnabled = false + isCompassEnabled = false } setOnMapClickListener { _, _ -> @@ -100,15 +102,6 @@ class MapFragment : BindingFragment(R.layout.fragment_map), chipMapCategoryStudy.setChipCategoryType(CategoryType.STUDY) chipMapCategoryMulti.setChipCategoryType(CategoryType.MULTI) chipMapCategoryOthers.setChipCategoryType(CategoryType.OTHERS) - cardMap.listener = object : OnPingleCardClickListener { - override fun onPingleCardChatBtnClickListener() { - // TODO 선택된 마커로 웹뷰 연결 - } - - override fun onPingleCardParticipateBtnClickListener() { - // TODO 선택된 마커 참여 현황 여부에 따른 모달 로직 구현 - } - } } } @@ -143,11 +136,18 @@ class MapFragment : BindingFragment(R.layout.fragment_map), mapViewModel.getPinListWithoutFilter() }.launchIn(lifecycleScope) - mapViewModel.markerListState.flowWithLifecycle(lifecycle).onEach { uiState -> + mapViewModel.pinEntityListState.flowWithLifecycle(lifecycle).onEach { uiState -> when (uiState) { is UiState.Success -> { if (::naverMap.isInitialized) { makeMarkers(uiState.data) + with(binding) { + fabMapHere.visibility = View.VISIBLE + fabMapList.visibility = View.VISIBLE + cardMap.visibility = View.INVISIBLE + } + + mapViewModel.clearSelectedMarkerPosition() } } @@ -165,6 +165,21 @@ class MapFragment : BindingFragment(R.layout.fragment_map), } } }.launchIn(lifecycleScope) + + mapViewModel.pingleListState.flowWithLifecycle(lifecycle).onEach { uiState -> + when (uiState) { + is UiState.Success -> { + with(binding.cardMap) { + initLayout(uiState.data[SINGLE_SELECTION]) + setOnChatButtonClick { + startActivity(navigateToWebView(uiState.data[SINGLE_SELECTION].chatLink)) + } + } + } + + else -> Unit + } + }.launchIn(lifecycleScope) } private fun setLocationTrackingMode() { @@ -203,17 +218,21 @@ class MapFragment : BindingFragment(R.layout.fragment_map), } } - private fun makeMarkers(markerList: List) { + private fun makeMarkers(pinEntityList: List) { mapViewModel.clearMarkerList() - markerList.mapIndexed { _, markerModel -> - markerModel.marker.apply { - mapViewModel.addMarkerList(this) - map = naverMap - setOnClickListener { - // TODO 마커 클릭 이벤트 수정 - return@setOnClickListener true + pinEntityList.mapIndexed { index, pinEntity -> + pinEntity.toMarkerModel().apply { + this.marker.apply { + map = naverMap + setOnClickListener { + mapViewModel.updateMarkerModelListSelectedValue(index) + mapViewModel.getPingleList(pinEntity.id) + moveMapCamera(position) + return@setOnClickListener true + } } + mapViewModel.addMarkerList(this) } } } 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 8daead31..f08c65cd 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 @@ -2,83 +2,115 @@ package org.sopt.pingle.presentation.ui.main.home.map import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.naver.maps.map.overlay.Marker import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.sopt.pingle.domain.model.PinEntity +import org.sopt.pingle.domain.model.PingleEntity import org.sopt.pingle.domain.usecase.GetPinListWithoutFilteringUseCase -import org.sopt.pingle.presentation.mapper.toMarkerModel +import org.sopt.pingle.domain.usecase.GetPingleListUseCase import org.sopt.pingle.presentation.model.MarkerModel import org.sopt.pingle.presentation.type.CategoryType import org.sopt.pingle.util.view.UiState @HiltViewModel class MapViewModel @Inject constructor( - private val getPinListWithoutFilteringUseCase: GetPinListWithoutFilteringUseCase + private val getPinListWithoutFilteringUseCase: GetPinListWithoutFilteringUseCase, + private val getPingleListUseCase: GetPingleListUseCase ) : ViewModel() { private val _category = MutableStateFlow(null) val category get() = _category.asStateFlow() - private val _markerListState = MutableStateFlow>>(UiState.Empty) - val markerListState get() = _markerListState.asStateFlow() + private val _pinEntityListState = MutableStateFlow>>(UiState.Empty) + val pinEntityListState get() = _pinEntityListState.asStateFlow() - private val _markerList = mutableListOf() + private val _markerModelList = mutableListOf() private var _selectedMarkerPosition = MutableStateFlow(DEFAULT_SELECTED_MARKER_POSITION) - val selectedMarkerPosition = _selectedMarkerPosition.asStateFlow() + val selectedMarkerPosition get() = _selectedMarkerPosition.asStateFlow() - private fun setMarkerIsSelected(position: Int) { - // TODO 마커 선택값 재설정 - } + private val _pingleListState = MutableSharedFlow>>() + val pingleListState get() = _pingleListState.asSharedFlow() fun setCategory(category: CategoryType?) { _category.value = category } + private fun setMarkerModelIsSelected(position: Int) { + _markerModelList[position].isSelected.set(!_markerModelList[position].isSelected.get()) + } + fun clearMarkerList() { - _markerList.forEach { it.map = null } - _markerList.clear() + _markerModelList.forEach { it.marker.map = null } + _markerModelList.clear() } - fun addMarkerList(marker: Marker) { - _markerList.add(marker) + fun addMarkerList(markerEntity: MarkerModel) { + _markerModelList.add(markerEntity) } - fun handleMarkerClick(position: Int) { - setMarkerIsSelected(position) - if (_selectedMarkerPosition.value != DEFAULT_SELECTED_MARKER_POSITION) { - setMarkerIsSelected( - _selectedMarkerPosition.value - ) + private fun getMarkerModelModelSelected(position: Int) = + _markerModelList[position].isSelected.get() + + fun updateMarkerModelListSelectedValue(position: Int) { + when (_selectedMarkerPosition.value) { + DEFAULT_SELECTED_MARKER_POSITION -> setMarkerModelIsSelected(position) + position -> Unit + else -> { + if (getMarkerModelModelSelected(_selectedMarkerPosition.value)) { + setMarkerModelIsSelected( + _selectedMarkerPosition.value + ) + } + if (!getMarkerModelModelSelected(position)) setMarkerModelIsSelected(position) + } } _selectedMarkerPosition.value = position } fun clearSelectedMarkerPosition() { if (_selectedMarkerPosition.value != DEFAULT_SELECTED_MARKER_POSITION) { - setMarkerIsSelected(_selectedMarkerPosition.value) + if (getMarkerModelModelSelected(_selectedMarkerPosition.value)) { + setMarkerModelIsSelected( + _selectedMarkerPosition.value + ) + } _selectedMarkerPosition.value = DEFAULT_SELECTED_MARKER_POSITION } } fun getPinListWithoutFilter() { viewModelScope.launch { - _markerListState.value = UiState.Loading + _pinEntityListState.value = UiState.Loading runCatching { getPinListWithoutFilteringUseCase( teamId = TEAM_ID, category = category.value?.name ).collect() { pinList -> - _markerListState.value = UiState.Success( - pinList.map { pinEntity -> - pinEntity.toMarkerModel() - } - ) + _pinEntityListState.value = UiState.Success(pinList) } }.onFailure { exception: Throwable -> - _markerListState.value = UiState.Error(exception.message) + _pinEntityListState.value = UiState.Error(exception.message) + } + } + } + + fun getPingleList(pinId: Long) { + viewModelScope.launch { + _pingleListState.emit(UiState.Loading) + runCatching { + getPingleListUseCase( + teamId = TEAM_ID, + pinId = pinId + ).collect() { pingleList -> + _pingleListState.emit(UiState.Success(pingleList)) + } + }.onFailure { exception -> + _pingleListState.emit(UiState.Error(exception.message)) } } } 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 index 2718b517..22f58cdf 100644 --- a/app/src/main/java/org/sopt/pingle/util/component/PingleCard.kt +++ b/app/src/main/java/org/sopt/pingle/util/component/PingleCard.kt @@ -7,12 +7,11 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View 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.mapper.convertToCalenderDetail +import org.sopt.pingle.presentation.mapper.isCompleted import org.sopt.pingle.presentation.type.CategoryType import org.sopt.pingle.presentation.ui.participant.ParticipantActivity import org.sopt.pingle.util.view.colorOf @@ -25,7 +24,8 @@ class PingleCard @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { private val binding: CardPingleBinding - var listener: OnPingleCardClickListener? = null + private var onChatButtonClick: () -> Unit = {} + private var onParticipateButtonClick: () -> Unit = {} init { binding = CardPingleBinding.inflate(LayoutInflater.from(context), this, true) @@ -35,11 +35,11 @@ class PingleCard @JvmOverloads constructor( private fun addListeners() { binding.btnCardBottomMapChat.setOnClickListener { - listener?.onPingleCardChatBtnClickListener() + onChatButtonClick() } binding.btnCardBottomMapParticipate.setOnClickListener { - listener?.onPingleCardParticipateBtnClickListener() + onParticipateButtonClick() } binding.layoutCardTopParticipationStatus.setOnClickListener { @@ -85,23 +85,12 @@ class PingleCard @JvmOverloads constructor( } } } -} - -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) + fun setOnChatButtonClick(chatButtonClickListener: () -> Unit) { + onChatButtonClick = chatButtonClickListener + } - return buildString { - append("${localDate.year}년 ${localDate.monthValue}월 ${localDate.dayOfMonth}일\n") - append("${startTime.format(DateTimeFormatter.ofPattern("HH:mm"))} ~ ") - append("${endTime.format(DateTimeFormatter.ofPattern("HH:mm"))}") + fun setOnParticipateButtonClick(participateButtonClickListener: () -> Unit) { + onParticipateButtonClick = participateButtonClickListener } } diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml index 04d62eec..11a8230b 100644 --- a/app/src/main/res/layout/fragment_map.xml +++ b/app/src/main/res/layout/fragment_map.xml @@ -88,6 +88,7 @@ - 추천 + 랭킹 개최 마이핑글 더보기