diff --git a/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSource.kt b/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSource.kt index fd49b9e2..d2470b87 100644 --- a/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSource.kt +++ b/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSource.kt @@ -1,6 +1,7 @@ package com.moive.app.data.voting.remote.datasource import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.voting.remote.dto.PlaceVoteRequest import com.moive.app.data.voting.remote.dto.RecommendedAreaListResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceDetailResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceListResponse @@ -24,4 +25,9 @@ interface VotingRemoteDataSource { meetingId: Long, recommendedPlaceId: Long, ): BaseResponse + + suspend fun postPlaceVotes( + meetingId: Long, + request: PlaceVoteRequest, + ): BaseResponse } diff --git a/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSourceImpl.kt b/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSourceImpl.kt index 739292fc..57996f4e 100644 --- a/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/moive/app/data/voting/remote/datasource/VotingRemoteDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.moive.app.data.voting.remote.datasource import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.voting.remote.dto.PlaceVoteRequest import com.moive.app.data.voting.remote.dto.RecommendedAreaListResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceDetailResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceListResponse @@ -33,4 +34,10 @@ class VotingRemoteDataSourceImpl @Inject constructor( recommendedPlaceId: Long, ): BaseResponse = votingService.getRecommendedPlaceRoute(meetingId, recommendedPlaceId) + + override suspend fun postPlaceVotes( + meetingId: Long, + request: PlaceVoteRequest, + ): BaseResponse = + votingService.postPlaceVotes(meetingId, request) } diff --git a/app/src/main/java/com/moive/app/data/voting/remote/dto/PlaceVoteRequest.kt b/app/src/main/java/com/moive/app/data/voting/remote/dto/PlaceVoteRequest.kt new file mode 100644 index 00000000..02547059 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/voting/remote/dto/PlaceVoteRequest.kt @@ -0,0 +1,10 @@ +package com.moive.app.data.voting.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PlaceVoteRequest( + @SerialName("recommendedPlaceIds") + val recommendedPlaceIds: List, +) diff --git a/app/src/main/java/com/moive/app/data/voting/remote/service/VotingService.kt b/app/src/main/java/com/moive/app/data/voting/remote/service/VotingService.kt index 39ec6a17..91e26664 100644 --- a/app/src/main/java/com/moive/app/data/voting/remote/service/VotingService.kt +++ b/app/src/main/java/com/moive/app/data/voting/remote/service/VotingService.kt @@ -1,11 +1,14 @@ package com.moive.app.data.voting.remote.service import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.voting.remote.dto.PlaceVoteRequest import com.moive.app.data.voting.remote.dto.RecommendedAreaListResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceDetailResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceListResponse import com.moive.app.data.voting.remote.dto.RecommendedPlaceRouteResponse +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path interface VotingService { @@ -33,4 +36,10 @@ interface VotingService { @Path("meetingId") meetingId: Long, @Path("recommendedPlaceId") recommendedPlaceId: Long, ): BaseResponse + + @POST("meetings/{meetingId}/place-votes") + suspend fun postPlaceVotes( + @Path("meetingId") meetingId: Long, + @Body request: PlaceVoteRequest, + ): BaseResponse } diff --git a/app/src/main/java/com/moive/app/data/voting/repository/VotingRepository.kt b/app/src/main/java/com/moive/app/data/voting/repository/VotingRepository.kt index fdc1dde6..8caec5ae 100644 --- a/app/src/main/java/com/moive/app/data/voting/repository/VotingRepository.kt +++ b/app/src/main/java/com/moive/app/data/voting/repository/VotingRepository.kt @@ -24,4 +24,9 @@ interface VotingRepository { recommendedPlaceId: Long, current: PlaceDetailModel, ): Result + + suspend fun postPlaceVotes( + meetingId: Long, + recommendedPlaceIds: List, + ): Result } diff --git a/app/src/main/java/com/moive/app/data/voting/repository/VotingRepositoryImpl.kt b/app/src/main/java/com/moive/app/data/voting/repository/VotingRepositoryImpl.kt index 4a41faeb..d6b60b45 100644 --- a/app/src/main/java/com/moive/app/data/voting/repository/VotingRepositoryImpl.kt +++ b/app/src/main/java/com/moive/app/data/voting/repository/VotingRepositoryImpl.kt @@ -2,12 +2,14 @@ package com.moive.app.data.voting.repository import com.moive.app.core.utils.suspendRunCatching import com.moive.app.data.common.dto.checkData +import com.moive.app.data.common.dto.checkSuccess import com.moive.app.data.voting.mapper.applyTo import com.moive.app.data.voting.mapper.toModel import com.moive.app.data.voting.model.PlaceDetailModel import com.moive.app.data.voting.model.PlaceRecommendedPlaceCardItemModel import com.moive.app.data.voting.model.RegionPinModel import com.moive.app.data.voting.remote.datasource.VotingRemoteDataSource +import com.moive.app.data.voting.remote.dto.PlaceVoteRequest import javax.inject.Inject class VotingRepositoryImpl @Inject constructor( @@ -55,4 +57,13 @@ class VotingRepositoryImpl @Inject constructor( .checkData() .applyTo(current) } + + override suspend fun postPlaceVotes( + meetingId: Long, + recommendedPlaceIds: List, + ): Result = + suspendRunCatching { + votingRemoteDataSource.postPlaceVotes(meetingId, PlaceVoteRequest(recommendedPlaceIds)) + .checkSuccess() + } } diff --git a/app/src/main/java/com/moive/app/data/votingstatus/di/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/di/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusDataSourceModule.kt b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusDataSourceModule.kt new file mode 100644 index 00000000..6ac28c8d --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusDataSourceModule.kt @@ -0,0 +1,20 @@ +package com.moive.app.data.votingstatus.di + +import com.moive.app.data.votingstatus.remote.datasource.VoteStatusRemoteDataSource +import com.moive.app.data.votingstatus.remote.datasource.VoteStatusRemoteDataSourceImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class VoteStatusDataSourceModule { + + @Binds + @Singleton + abstract fun bindVoteStatusRemoteDataSource( + voteStatusRemoteDataSourceImpl: VoteStatusRemoteDataSourceImpl, + ): VoteStatusRemoteDataSource +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusRepositoryModule.kt b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusRepositoryModule.kt new file mode 100644 index 00000000..62054bf4 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusRepositoryModule.kt @@ -0,0 +1,20 @@ +package com.moive.app.data.votingstatus.di + +import com.moive.app.data.votingstatus.repository.VoteStatusRepository +import com.moive.app.data.votingstatus.repository.VoteStatusRepositoryImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class VoteStatusRepositoryModule { + + @Binds + @Singleton + abstract fun bindVoteStatusRepository( + voteStatusRepositoryImpl: VoteStatusRepositoryImpl, + ): VoteStatusRepository +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusServiceModule.kt b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusServiceModule.kt new file mode 100644 index 00000000..90649a7e --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/di/VoteStatusServiceModule.kt @@ -0,0 +1,22 @@ +package com.moive.app.data.votingstatus.di + +import com.moive.app.core.network.di.Auth +import com.moive.app.data.votingstatus.remote.service.VoteStatusService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.create +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object VoteStatusServiceModule { + + @Provides + @Singleton + fun provideVoteStatusService( + @Auth retrofit: Retrofit, + ): VoteStatusService = retrofit.create() +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/mapper/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/mapper/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/mapper/VoteStatusMapper.kt b/app/src/main/java/com/moive/app/data/votingstatus/mapper/VoteStatusMapper.kt new file mode 100644 index 00000000..1175030e --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/mapper/VoteStatusMapper.kt @@ -0,0 +1,40 @@ +package com.moive.app.data.votingstatus.mapper + +import com.moive.app.data.votingstatus.model.PlaceVoteCandidateModel +import com.moive.app.data.votingstatus.model.PlaceVoteResultModel +import com.moive.app.data.votingstatus.model.ScheduleVoteCandidateModel +import com.moive.app.data.votingstatus.model.ScheduleVoteResultModel +import com.moive.app.data.votingstatus.remote.dto.PlaceVoteCandidateItem +import com.moive.app.data.votingstatus.remote.dto.PlaceVoteResultResponse +import com.moive.app.data.votingstatus.remote.dto.ScheduleVoteCandidateItem +import com.moive.app.data.votingstatus.remote.dto.ScheduleVoteResultResponse + +fun ScheduleVoteResultResponse.toModel(): ScheduleVoteResultModel = + ScheduleVoteResultModel( + isVoteSkipped = isVoteSkipped, + totalVoterCount = totalVoterCnt, + candidates = candidates.map { it.toModel() }, + ) + +fun ScheduleVoteCandidateItem.toModel(): ScheduleVoteCandidateModel = + ScheduleVoteCandidateModel( + meetingDate = meetingDate, + meetingTime = meetingTime, + voterCount = voterCnt, + isVotedByMe = isVotedByMe, + ) + +fun PlaceVoteResultResponse.toModel(): PlaceVoteResultModel = + PlaceVoteResultModel( + isFinished = isFinished, + totalVoterCount = totalVoterCnt, + candidates = candidates.map { it.toModel() }, + ) + +fun PlaceVoteCandidateItem.toModel(): PlaceVoteCandidateModel = + PlaceVoteCandidateModel( + id = placeId, + placeName = placeName, + voterCount = voterCnt, + isVotedByMe = isVotedByMe, + ) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteCandidateModel.kt b/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteCandidateModel.kt deleted file mode 100644 index 690cbfc6..00000000 --- a/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteCandidateModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.moive.app.data.votingstatus.model - -data class PlaceVoteCandidateModel( - val id: Long, - val placeName: String, - val voterCount: Int, - val isVotedByMe: Boolean, -) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteResultModel.kt b/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteResultModel.kt new file mode 100644 index 00000000..727e31b2 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/model/PlaceVoteResultModel.kt @@ -0,0 +1,14 @@ +package com.moive.app.data.votingstatus.model + +data class PlaceVoteResultModel( + val isFinished: Boolean, + val totalVoterCount: Int, + val candidates: List, +) + +data class PlaceVoteCandidateModel( + val id: Long, + val placeName: String?, + val voterCount: Int, + val isVotedByMe: Boolean, +) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteCandidateModel.kt b/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteCandidateModel.kt deleted file mode 100644 index de056627..00000000 --- a/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteCandidateModel.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.moive.app.data.votingstatus.model - -data class ScheduleVoteCandidateModel( - val meetingDate: String, - val meetingTime: String, - val voterCount: Int?, - val isVotedByMe: Boolean, -) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteResultModel.kt b/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteResultModel.kt new file mode 100644 index 00000000..eb25704b --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/model/ScheduleVoteResultModel.kt @@ -0,0 +1,18 @@ +package com.moive.app.data.votingstatus.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class ScheduleVoteResultModel( + val isVoteSkipped: Boolean, + val totalVoterCount: Int?, + val candidates: List, +) + +@Immutable +data class ScheduleVoteCandidateModel( + val meetingDate: String, + val meetingTime: String, + val voterCount: Int?, + val isVotedByMe: Boolean, +) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSource.kt b/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSource.kt new file mode 100644 index 00000000..bc650c57 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSource.kt @@ -0,0 +1,11 @@ +package com.moive.app.data.votingstatus.remote.datasource + +import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.votingstatus.remote.dto.PlaceVoteResultResponse +import com.moive.app.data.votingstatus.remote.dto.ScheduleVoteResultResponse + +interface VoteStatusRemoteDataSource { + suspend fun getScheduleVoteResult(meetingId: Long): BaseResponse + + suspend fun getPlaceVoteResult(meetingId: Long): BaseResponse +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSourceImpl.kt b/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSourceImpl.kt new file mode 100644 index 00000000..097de56b --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/remote/datasource/VoteStatusRemoteDataSourceImpl.kt @@ -0,0 +1,18 @@ +package com.moive.app.data.votingstatus.remote.datasource + +import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.votingstatus.remote.dto.PlaceVoteResultResponse +import com.moive.app.data.votingstatus.remote.dto.ScheduleVoteResultResponse +import com.moive.app.data.votingstatus.remote.service.VoteStatusService +import javax.inject.Inject + +class VoteStatusRemoteDataSourceImpl @Inject constructor( + private val voteStatusService: VoteStatusService, +) : VoteStatusRemoteDataSource { + + override suspend fun getScheduleVoteResult(meetingId: Long): BaseResponse = + voteStatusService.getScheduleVoteResult(meetingId) + + override suspend fun getPlaceVoteResult(meetingId: Long): BaseResponse = + voteStatusService.getPlaceVoteResult(meetingId) +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/PlaceVoteResultResponse.kt b/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/PlaceVoteResultResponse.kt new file mode 100644 index 00000000..4654e220 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/PlaceVoteResultResponse.kt @@ -0,0 +1,26 @@ +package com.moive.app.data.votingstatus.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PlaceVoteResultResponse( + @SerialName("isFinished") + val isFinished: Boolean, + @SerialName("totalVoterCnt") + val totalVoterCnt: Int, + @SerialName("candidates") + val candidates: List, +) + +@Serializable +data class PlaceVoteCandidateItem( + @SerialName("placeId") + val placeId: Long, + @SerialName("placeName") + val placeName: String?, + @SerialName("voterCnt") + val voterCnt: Int, + @SerialName("isVotedByMe") + val isVotedByMe: Boolean, +) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/ScheduleVoteResultResponse.kt b/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/ScheduleVoteResultResponse.kt new file mode 100644 index 00000000..3cb50581 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/remote/dto/ScheduleVoteResultResponse.kt @@ -0,0 +1,26 @@ +package com.moive.app.data.votingstatus.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ScheduleVoteResultResponse( + @SerialName("isVoteSkipped") + val isVoteSkipped: Boolean, + @SerialName("totalVoterCnt") + val totalVoterCnt: Int?, + @SerialName("candidates") + val candidates: List, +) + +@Serializable +data class ScheduleVoteCandidateItem( + @SerialName("meetingDate") + val meetingDate: String, + @SerialName("meetingTime") + val meetingTime: String, + @SerialName("voterCnt") + val voterCnt: Int?, + @SerialName("isVotedByMe") + val isVotedByMe: Boolean, +) diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/service/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/remote/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/remote/service/VoteStatusService.kt b/app/src/main/java/com/moive/app/data/votingstatus/remote/service/VoteStatusService.kt new file mode 100644 index 00000000..8b782b9d --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/remote/service/VoteStatusService.kt @@ -0,0 +1,19 @@ +package com.moive.app.data.votingstatus.remote.service + +import com.moive.app.data.common.dto.BaseResponse +import com.moive.app.data.votingstatus.remote.dto.PlaceVoteResultResponse +import com.moive.app.data.votingstatus.remote.dto.ScheduleVoteResultResponse +import retrofit2.http.GET +import retrofit2.http.Path + +interface VoteStatusService { + @GET("meetings/{meetingId}/date-votes/result") + suspend fun getScheduleVoteResult( + @Path("meetingId") meetingId: Long, + ): BaseResponse + + @GET("meetings/{meetingId}/place-votes/result") + suspend fun getPlaceVoteResult( + @Path("meetingId") meetingId: Long, + ): BaseResponse +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/repository/.gitkeep b/app/src/main/java/com/moive/app/data/votingstatus/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepository.kt b/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepository.kt new file mode 100644 index 00000000..c332e031 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepository.kt @@ -0,0 +1,10 @@ +package com.moive.app.data.votingstatus.repository + +import com.moive.app.data.votingstatus.model.PlaceVoteResultModel +import com.moive.app.data.votingstatus.model.ScheduleVoteResultModel + +interface VoteStatusRepository { + suspend fun getScheduleVoteResult(meetingId: Long): Result + + suspend fun getPlaceVoteResult(meetingId: Long): Result +} diff --git a/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepositoryImpl.kt b/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepositoryImpl.kt new file mode 100644 index 00000000..89af4a69 --- /dev/null +++ b/app/src/main/java/com/moive/app/data/votingstatus/repository/VoteStatusRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.moive.app.data.votingstatus.repository + +import com.moive.app.core.utils.suspendRunCatching +import com.moive.app.data.common.dto.checkData +import com.moive.app.data.votingstatus.mapper.toModel +import com.moive.app.data.votingstatus.model.PlaceVoteResultModel +import com.moive.app.data.votingstatus.model.ScheduleVoteResultModel +import com.moive.app.data.votingstatus.remote.datasource.VoteStatusRemoteDataSource +import javax.inject.Inject + +class VoteStatusRepositoryImpl @Inject constructor( + private val voteStatusRemoteDataSource: VoteStatusRemoteDataSource, +) : VoteStatusRepository { + + override suspend fun getScheduleVoteResult(meetingId: Long): Result = + suspendRunCatching { + voteStatusRemoteDataSource.getScheduleVoteResult(meetingId) + .checkData() + .toModel() + } + + override suspend fun getPlaceVoteResult(meetingId: Long): Result = + suspendRunCatching { + voteStatusRemoteDataSource.getPlaceVoteResult(meetingId) + .checkData() + .toModel() + } +} diff --git a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusContract.kt b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusContract.kt index 220dcc1e..ba9fab0a 100644 --- a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusContract.kt +++ b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusContract.kt @@ -13,6 +13,7 @@ interface VoteStatusContract { @Immutable data class State( val step: Step = Step.LIST, + val scheduleVoteResultUiState: ScheduleVoteResultUiState = ScheduleVoteResultUiState.Idle, val isScheduleVoteSkipped: Boolean = false, val scheduleTotalVoterCount: Int? = 5, val scheduleCandidates: ImmutableList = persistentListOf( @@ -20,47 +21,44 @@ interface VoteStatusContract { ScheduleVoteCandidateModel(meetingDate = "2026-09-08", meetingTime = "18:00", voterCount = 3, isVotedByMe = true), ScheduleVoteCandidateModel(meetingDate = "2026-09-10", meetingTime = "18:00", voterCount = 3, isVotedByMe = false), ), + val placeVoteResultUiState: PlaceVoteResultUiState = PlaceVoteResultUiState.Idle, + val isPlaceVoteFinished: Boolean = false, val placeTotalVoterCount: Int = 4, val placeCandidates: ImmutableList = persistentListOf( PlaceVoteCandidateModel(id = 1L, placeName = "OOO 맛집", voterCount = 4, isVotedByMe = true), PlaceVoteCandidateModel(id = 2L, placeName = "XXX 카페", voterCount = 3, isVotedByMe = true), PlaceVoteCandidateModel(id = 3L, placeName = "ΔΔΔ 이자카야", voterCount = 2, isVotedByMe = false), ), + val placeRouteUiState: PlaceRouteUiState = PlaceRouteUiState.Idle, val currentPlaceId: Long? = null, val currentPlaceDetail: PlaceDetailModel = PlaceDetailModel( id = 1L, - userName = "모이브", - placeName = "장소명(상호명)", - category = "카페", - address = "서울시 강남구 워시기워시기 123", - areaName = "신논현동", - totalMemberCount = 7, - matchMemberCount = 4, + userName = "", + placeName = "", + category = "", + address = "", + areaName = "", + totalMemberCount = 0, + matchMemberCount = 0, imageList = emptyList(), startPinLatLang = PlaceDetailPinLatLang( - latitude = 37.5044, - longitude = 127.0246, + latitude = 0.0, + longitude = 0.0, ), endPinLatLang = PlaceDetailPinLatLang( - latitude = 37.5089, - longitude = 127.0632, + latitude = 0.0, + longitude = 0.0, ), routeLatLang = PlaceDetailRouteLatLang( - latitude = listOf( - 37.5044, 37.5054217, 37.505525, 37.5049788, 37.50665, - 37.5083212, 37.507775, 37.5078783, 37.5089, - ), - longitude = listOf( - 127.0246, 127.0291954, 127.03425, 127.0396293, 127.0439, - 127.0481707, 127.05355, 127.0586046, 127.0632, - ), + latitude = listOf(0.0, 0.0), + longitude = listOf(0.0, 0.0), ), - totalTravelMinutes = 34, - avgTravelMinutes = 36, - walkMinutes = 10, - busMinutes = 15, - subwayMinutes = 9, - travelFare = 1_650, + totalTravelMinutes = 0, + avgTravelMinutes = 0, + walkMinutes = 0, + busMinutes = 0, + subwayMinutes = 0, + travelFare = 0, ), ) { val scheduleTopVoterCount: Int @@ -78,3 +76,30 @@ interface VoteStatusContract { DETAIL; } } + +sealed interface ScheduleVoteResultUiState { + data object Idle : ScheduleVoteResultUiState + data object Loading : ScheduleVoteResultUiState + data object Success : ScheduleVoteResultUiState + data class Failure( + val msg: String, + ) : ScheduleVoteResultUiState +} + +sealed interface PlaceVoteResultUiState { + data object Idle : PlaceVoteResultUiState + data object Loading : PlaceVoteResultUiState + data object Success : PlaceVoteResultUiState + data class Failure( + val msg: String, + ) : PlaceVoteResultUiState +} + +sealed interface PlaceRouteUiState { + data object Idle : PlaceRouteUiState + data object Loading : PlaceRouteUiState + data object Success : PlaceRouteUiState + data class Failure( + val msg: String, + ) : PlaceRouteUiState +} diff --git a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusScreen.kt b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusScreen.kt index a7ed6026..089c2ce4 100644 --- a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusScreen.kt +++ b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusScreen.kt @@ -19,7 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.moive.app.core.designsystem.component.topbar.MoiveSubTitleTopBar import com.moive.app.core.designsystem.theme.MoiveTheme import com.moive.app.core.designsystem.theme.MoiveTheme.colors -import com.moive.app.core.extensions.openKakaoMapRoute +import com.moive.app.core.extensions.openUrl import com.moive.app.data.votingstatus.model.ScheduleVoteCandidateModel import com.moive.app.presentation.common.component.placedetail.PlaceDetailContent import com.moive.app.presentation.votestatus.VoteStatusContract.Step @@ -48,14 +48,11 @@ fun VoteStatusRoute( onPlaceItemClick = viewModel::onPlaceItemClick, onDetailBackClick = viewModel::backToList, onKakaoMapClick = { - val place = uiState.currentPlaceDetail - val opened = context.openKakaoMapRoute( - startLatitude = place.startPinLatLang.latitude, - startLongitude = place.startPinLatLang.longitude, - endLatitude = place.endPinLatLang.latitude, - endLongitude = place.endPinLatLang.longitude, - ) - viewModel.onKakaoMapRouteOpened(opened) + val landingUrl = uiState.currentPlaceDetail.landingUrl + if (landingUrl.isNotBlank()) { + val opened = context.openUrl(landingUrl) + viewModel.onKakaoMapRouteOpened(opened) + } }, modifier = modifier, ) diff --git a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusViewModel.kt b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusViewModel.kt index 36401e30..cf9e5321 100644 --- a/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusViewModel.kt +++ b/app/src/main/java/com/moive/app/presentation/votestatus/VoteStatusViewModel.kt @@ -1,24 +1,158 @@ package com.moive.app.presentation.votestatus +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import com.moive.app.data.voting.model.PlaceDetailPinLatLang +import com.moive.app.data.voting.model.PlaceDetailRouteLatLang +import com.moive.app.data.voting.repository.VotingRepository +import com.moive.app.data.votingstatus.repository.VoteStatusRepository import com.moive.app.presentation.votestatus.VoteStatusContract.Step +import com.moive.app.presentation.votestatus.navigation.VoteStatus import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @HiltViewModel class VoteStatusViewModel @Inject constructor( - + savedStateHandle: SavedStateHandle, + private val voteStatusRepository: VoteStatusRepository, + private val votingRepository: VotingRepository, ) : ViewModel() { + private val meetingId: Long = savedStateHandle.toRoute().meetingId + private val _uiState = MutableStateFlow(VoteStatusContract.State()) val uiState = _uiState.asStateFlow() + private var placeRouteJob: Job? = null + + init { + getScheduleVoteResult() + getPlaceVoteResult() + } + + private fun getScheduleVoteResult() = viewModelScope.launch { + _uiState.update { it.copy(scheduleVoteResultUiState = ScheduleVoteResultUiState.Loading) } + + voteStatusRepository.getScheduleVoteResult(meetingId) + .onSuccess { result -> + _uiState.update { + it.copy( + scheduleVoteResultUiState = ScheduleVoteResultUiState.Success, + isScheduleVoteSkipped = result.isVoteSkipped, + scheduleTotalVoterCount = result.totalVoterCount, + scheduleCandidates = result.candidates.toImmutableList(), + ) + } + } + .onFailure { error -> + Timber.tag(TAG).e(error, SCHEDULE_VOTE_RESULT_FAILURE_MESSAGE) + _uiState.update { + it.copy( + scheduleVoteResultUiState = ScheduleVoteResultUiState.Failure( + error.message ?: UNKNOWN_ERROR_MESSAGE, + ), + ) + } + } + } + + private fun getPlaceVoteResult() = viewModelScope.launch { + _uiState.update { it.copy(placeVoteResultUiState = PlaceVoteResultUiState.Loading) } + + voteStatusRepository.getPlaceVoteResult(meetingId) + .onSuccess { result -> + _uiState.update { + it.copy( + placeVoteResultUiState = PlaceVoteResultUiState.Success, + isPlaceVoteFinished = result.isFinished, + placeTotalVoterCount = result.totalVoterCount, + placeCandidates = result.candidates.toImmutableList(), + ) + } + } + .onFailure { error -> + Timber.tag(TAG).e(error, PLACE_VOTE_RESULT_FAILURE_MESSAGE) + _uiState.update { + it.copy( + placeVoteResultUiState = PlaceVoteResultUiState.Failure( + error.message ?: UNKNOWN_ERROR_MESSAGE, + ), + ) + } + } + } + fun onPlaceItemClick(placeId: Long) { - _uiState.update { it.copy(step = Step.DETAIL, currentPlaceId = placeId) } + placeRouteJob?.cancel() + + val candidateName = _uiState.value.placeCandidates.firstOrNull { it.id == placeId }?.placeName + + _uiState.update { + it.copy( + step = Step.DETAIL, + currentPlaceId = placeId, + currentPlaceDetail = it.currentPlaceDetail.copy( + id = placeId, + placeName = candidateName ?: UNKNOWN_PLACE_NAME, + userName = "", + startPinLatLang = PlaceDetailPinLatLang(latitude = 0.0, longitude = 0.0), + endPinLatLang = PlaceDetailPinLatLang(latitude = 0.0, longitude = 0.0), + routeLatLang = PlaceDetailRouteLatLang(latitude = listOf(0.0, 0.0), longitude = listOf(0.0, 0.0)), + totalTravelMinutes = 0, + avgTravelMinutes = 0, + walkMinutes = 0, + busMinutes = 0, + subwayMinutes = 0, + travelFare = 0, + landingUrl = "", + ), + ) + } + + placeRouteJob = viewModelScope.launch { + fetchPlaceRoute(placeId) + } + } + + private suspend fun fetchPlaceRoute(placeId: Long) { + _uiState.update { it.copy(placeRouteUiState = PlaceRouteUiState.Loading) } + + votingRepository.getRecommendedPlaceRoute( + meetingId = meetingId, + recommendedPlaceId = placeId, + current = _uiState.value.currentPlaceDetail, + ) + .onSuccess { updated -> + if (_uiState.value.currentPlaceId != placeId) return@onSuccess + + _uiState.update { + it.copy( + placeRouteUiState = PlaceRouteUiState.Success, + currentPlaceDetail = updated, + ) + } + } + .onFailure { error -> + if (_uiState.value.currentPlaceId != placeId) return@onFailure + + Timber.tag(TAG).e(error, PLACE_ROUTE_FAILURE_MESSAGE) + _uiState.update { + it.copy( + placeRouteUiState = PlaceRouteUiState.Failure( + error.message ?: UNKNOWN_ERROR_MESSAGE, + ), + ) + } + } } fun backToList() { @@ -27,6 +161,16 @@ class VoteStatusViewModel @Inject constructor( fun onKakaoMapRouteOpened(opened: Boolean) { if (opened) return - Timber.tag("VoteStatus").e("카카오 맵을 열 수 없습니다.") + Timber.tag(TAG).e(KAKAO_MAP_ERROR) + } + + companion object { + private const val TAG = "VoteStatus" + private const val KAKAO_MAP_ERROR = "카카오 맵을 열 수 없습니다." + private const val SCHEDULE_VOTE_RESULT_FAILURE_MESSAGE = "일정 투표 현황 조회에 실패했습니다." + private const val PLACE_VOTE_RESULT_FAILURE_MESSAGE = "장소 투표 현황 조회에 실패했습니다." + private const val PLACE_ROUTE_FAILURE_MESSAGE = "이동 경로 조회에 실패했습니다." + private const val UNKNOWN_ERROR_MESSAGE = "알 수 없는 에러가 발생했습니다." + private const val UNKNOWN_PLACE_NAME = "알 수 없는 장소" } } diff --git a/app/src/main/java/com/moive/app/presentation/votestatus/component/PlaceVoteSection.kt b/app/src/main/java/com/moive/app/presentation/votestatus/component/PlaceVoteSection.kt index f75c73c7..2c794424 100644 --- a/app/src/main/java/com/moive/app/presentation/votestatus/component/PlaceVoteSection.kt +++ b/app/src/main/java/com/moive/app/presentation/votestatus/component/PlaceVoteSection.kt @@ -38,7 +38,7 @@ fun PlaceVoteSection( ) { top3.forEach { candidate -> VoteOptionRow( - text = candidate.placeName, + text = candidate.placeName ?: "알 수 없는 장소", voteRatio = voteRatio(candidate.voterCount, totalVoterCount), isTopVote = candidate.voterCount == topVoterCount, isVotedByMe = candidate.isVotedByMe, diff --git a/app/src/main/java/com/moive/app/presentation/votestatus/navigation/VoteStatusNavigation.kt b/app/src/main/java/com/moive/app/presentation/votestatus/navigation/VoteStatusNavigation.kt index fa4e313a..ba23c7a2 100644 --- a/app/src/main/java/com/moive/app/presentation/votestatus/navigation/VoteStatusNavigation.kt +++ b/app/src/main/java/com/moive/app/presentation/votestatus/navigation/VoteStatusNavigation.kt @@ -11,8 +11,9 @@ import com.moive.app.presentation.votestatus.VoteStatusRoute import kotlinx.serialization.Serializable fun NavController.navigateToVoteStatus( + meetingId: Long, navOptions: NavOptions? = null -) = navigate(VoteStatus, navOptions) +) = navigate(VoteStatus(meetingId), navOptions) fun NavGraphBuilder.voteStatusGraph( navController: NavController, @@ -27,4 +28,6 @@ fun NavGraphBuilder.voteStatusGraph( } @Serializable -data object VoteStatus : Route +data class VoteStatus( + val meetingId: Long +) : Route diff --git a/app/src/main/java/com/moive/app/presentation/voting/VotingContract.kt b/app/src/main/java/com/moive/app/presentation/voting/VotingContract.kt index 90c4d318..3ab79d4e 100644 --- a/app/src/main/java/com/moive/app/presentation/voting/VotingContract.kt +++ b/app/src/main/java/com/moive/app/presentation/voting/VotingContract.kt @@ -19,6 +19,7 @@ interface VotingContract { val recommendedPlaceUiState: RecommendedPlaceUiState = RecommendedPlaceUiState.Idle, val recommendedPlaceDetailUiState: RecommendedPlaceDetailUiState = RecommendedPlaceDetailUiState.Idle, val recommendedPlaceRouteUiState: RecommendedPlaceRouteUiState = RecommendedPlaceRouteUiState.Idle, + val placeVoteUiState: PlaceVoteUiState = PlaceVoteUiState.Idle, val isPlaceListVisible: Boolean = false, val selectedRegionId: Long? = null, val selectedRegionName: String? = null, @@ -61,6 +62,10 @@ interface VotingContract { RECOMMENDATION, DETAIL; } + + sealed class SideEffect { + data class NavigateToVoteStatus(val meetingId: Long) : SideEffect() + } } sealed interface RecommendedAreaUiState { @@ -98,3 +103,12 @@ sealed interface RecommendedPlaceRouteUiState { val msg: String, ) : RecommendedPlaceRouteUiState } + +sealed interface PlaceVoteUiState { + data object Idle : PlaceVoteUiState + data object Loading : PlaceVoteUiState + data object Success : PlaceVoteUiState + data class Failure( + val msg: String, + ) : PlaceVoteUiState +} diff --git a/app/src/main/java/com/moive/app/presentation/voting/VotingScreen.kt b/app/src/main/java/com/moive/app/presentation/voting/VotingScreen.kt index 891d55fe..1372edfe 100644 --- a/app/src/main/java/com/moive/app/presentation/voting/VotingScreen.kt +++ b/app/src/main/java/com/moive/app/presentation/voting/VotingScreen.kt @@ -5,12 +5,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.repeatOnLifecycle import com.moive.app.core.designsystem.theme.MoiveTheme import com.moive.app.core.extensions.openUrl import com.moive.app.data.voting.model.RegionPinModel @@ -23,17 +27,28 @@ import kotlinx.collections.immutable.persistentSetOf fun VotingRoute( innerPadding: PaddingValues, navigateBack: () -> Unit, - navigateToVoteStatus: () -> Unit, + navigateToVoteStatus: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: VotingViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current BackHandler(enabled = uiState.step != Step.RECOMMENDATION) { viewModel.backToPlaceList() } + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.sideEffect.collect { sideEffect -> + when (sideEffect) { + is VotingContract.SideEffect.NavigateToVoteStatus -> navigateToVoteStatus(sideEffect.meetingId) + } + } + } + } + VotingScreen( innerPadding = innerPadding, uiState = uiState, @@ -52,7 +67,7 @@ fun VotingRoute( } }, onSelectButtonClick = viewModel::onSelectButtonClick, - onCompleteButtonClick = navigateToVoteStatus, + onCompleteButtonClick = viewModel::onCompleteButtonClick, modifier = modifier, ) } @@ -90,6 +105,7 @@ private fun VotingScreen( onResetClick = onResetClick, onBackClick = onBackClick, onCompleteButtonClick = onCompleteButtonClick, + isCompleteButtonLoading = uiState.placeVoteUiState is PlaceVoteUiState.Loading, ) if (uiState.step == Step.DETAIL) { diff --git a/app/src/main/java/com/moive/app/presentation/voting/VotingViewModel.kt b/app/src/main/java/com/moive/app/presentation/voting/VotingViewModel.kt index 53de08a6..d7c8587f 100644 --- a/app/src/main/java/com/moive/app/presentation/voting/VotingViewModel.kt +++ b/app/src/main/java/com/moive/app/presentation/voting/VotingViewModel.kt @@ -13,8 +13,10 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -31,8 +33,12 @@ class VotingViewModel @Inject constructor( private val _uiState = MutableStateFlow(VotingContract.State()) val uiState = _uiState.asStateFlow() + private val _sideEffect = Channel(Channel.BUFFERED) + val sideEffect = _sideEffect.receiveAsFlow() + private var recommendedPlacesJob: Job? = null private var placeDetailJob: Job? = null + private var placeVoteJob: Job? = null init { getRecommendedAreas() @@ -226,6 +232,33 @@ class VotingViewModel @Inject constructor( Timber.tag(TAG).e(KAKAO_MAP_ERROR) } + fun onCompleteButtonClick() { + if (placeVoteJob?.isActive == true) return + + val recommendedPlaceIds = _uiState.value.selectedPlaceList.toList() + if (recommendedPlaceIds.isEmpty()) return + + placeVoteJob = viewModelScope.launch { + _uiState.update { it.copy(placeVoteUiState = PlaceVoteUiState.Loading) } + + votingRepository.postPlaceVotes(meetingId, recommendedPlaceIds) + .onSuccess { + _uiState.update { it.copy(placeVoteUiState = PlaceVoteUiState.Success) } + _sideEffect.send(VotingContract.SideEffect.NavigateToVoteStatus(meetingId)) + } + .onFailure { error -> + Timber.tag(TAG).e(error, PLACE_VOTE_FAILURE_MESSAGE) + _uiState.update { + it.copy( + placeVoteUiState = PlaceVoteUiState.Failure( + error.message ?: UNKNOWN_ERROR_MESSAGE, + ), + ) + } + } + } + } + companion object { private const val TAG = "Voting" private const val KAKAO_MAP_ERROR = "카카오 맵을 열 수 없습니다." @@ -233,6 +266,7 @@ class VotingViewModel @Inject constructor( private const val RECOMMENDED_PLACE_FAILURE_MESSAGE = "추천 장소 조회에 실패했습니다." private const val RECOMMENDED_PLACE_DETAIL_FAILURE_MESSAGE = "추천 장소 상세 조회에 실패했습니다." private const val RECOMMENDED_PLACE_ROUTE_FAILURE_MESSAGE = "이동 경로 조회에 실패했습니다." + private const val PLACE_VOTE_FAILURE_MESSAGE = "장소 투표에 실패했습니다." private const val UNKNOWN_ERROR_MESSAGE = "알 수 없는 에러가 발생했습니다." } } diff --git a/app/src/main/java/com/moive/app/presentation/voting/component/PlaceListContent.kt b/app/src/main/java/com/moive/app/presentation/voting/component/PlaceListContent.kt index 36e615e4..e9288a40 100644 --- a/app/src/main/java/com/moive/app/presentation/voting/component/PlaceListContent.kt +++ b/app/src/main/java/com/moive/app/presentation/voting/component/PlaceListContent.kt @@ -66,6 +66,7 @@ fun PlaceListContent( onBackClick: () -> Unit, onCompleteButtonClick: () -> Unit, modifier: Modifier = Modifier, + isCompleteButtonLoading: Boolean = false, ) { var kakaoMapState by remember { mutableStateOf(null) } var regionPinEntries by remember { mutableStateOf>>(emptyList()) } @@ -176,6 +177,7 @@ fun PlaceListContent( size = MoiveButtonSize.LARGE, onClick = onCompleteButtonClick, enabled = selectedPlaceIds.isNotEmpty(), + isLoading = isCompleteButtonLoading, modifier = Modifier.weight(1f), ) } diff --git a/app/src/main/java/com/moive/app/presentation/voting/navigation/VotingNavigation.kt b/app/src/main/java/com/moive/app/presentation/voting/navigation/VotingNavigation.kt index 087db98e..fc2fb975 100644 --- a/app/src/main/java/com/moive/app/presentation/voting/navigation/VotingNavigation.kt +++ b/app/src/main/java/com/moive/app/presentation/voting/navigation/VotingNavigation.kt @@ -25,8 +25,9 @@ fun NavGraphBuilder.votingGraph( composable { VotingRoute( navigateBack = navController.safePopBackStack(), - navigateToVoteStatus = { + navigateToVoteStatus = { meetingId -> navController.navigateToVoteStatus( + meetingId = meetingId, navOptions = navOptions { popUpTo { inclusive = false @@ -40,4 +41,6 @@ fun NavGraphBuilder.votingGraph( } @Serializable -data class Voting(val meetingId: Long) : Route +data class Voting( + val meetingId: Long +) : Route