diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/badge/BadgeColor.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/badge/BadgeColor.kt index 1e8a723d..4c6916b9 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/badge/BadgeColor.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/badge/BadgeColor.kt @@ -1,8 +1,10 @@ package com.susu.core.designsystem.component.badge +import android.util.Log import androidx.compose.ui.graphics.Color import com.susu.core.designsystem.theme.Gray10 import com.susu.core.designsystem.theme.Gray70 +import java.lang.IllegalArgumentException enum class BadgeColor( val backgroundColor: Color, @@ -36,4 +38,14 @@ enum class BadgeColor( backgroundColor = com.susu.core.designsystem.theme.Red60, textColor = Gray10, ), + ; + + companion object { + fun safeValueOf(value: String) = try { + valueOf(value) + } catch (e: IllegalArgumentException) { + Log.e("SafeValueOfError", "Invalid value provided", e) + Gray40 + } + } } diff --git a/core/model/src/main/java/com/susu/core/model/Category.kt b/core/model/src/main/java/com/susu/core/model/Category.kt index a10be68f..8919864d 100644 --- a/core/model/src/main/java/com/susu/core/model/Category.kt +++ b/core/model/src/main/java/com/susu/core/model/Category.kt @@ -10,4 +10,5 @@ data class Category( val seq: Int = 0, val name: String = "", val customCategory: String? = null, + val style: String = "", ) diff --git a/core/model/src/main/java/com/susu/core/model/Envelope.kt b/core/model/src/main/java/com/susu/core/model/Envelope.kt new file mode 100644 index 00000000..a84cb3bc --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/Envelope.kt @@ -0,0 +1,15 @@ +package com.susu.core.model + +import java.time.LocalDateTime + +data class Envelope( + val id: Long, + val uid: Long, + val type: String, + val friend: Friend, + val amount: Long, + val gift: String? = null, + val memo: String? = null, + val hasVisited: Boolean? = null, + val handedOverAt: LocalDateTime? = null, +) diff --git a/core/model/src/main/java/com/susu/core/model/EnvelopeStatics.kt b/core/model/src/main/java/com/susu/core/model/EnvelopeStatics.kt new file mode 100644 index 00000000..f123fb2e --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/EnvelopeStatics.kt @@ -0,0 +1,8 @@ +package com.susu.core.model + +data class EnvelopeStatics( + val friend: Friend = Friend(), + val receivedAmounts: Int = 0, + val sentAmounts: Int = 0, + val totalAmounts: Int = 0, +) diff --git a/core/model/src/main/java/com/susu/core/model/Friend.kt b/core/model/src/main/java/com/susu/core/model/Friend.kt new file mode 100644 index 00000000..9c924f02 --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/Friend.kt @@ -0,0 +1,10 @@ +package com.susu.core.model + +data class Friend( + val id: Long = 0, + val uid: Long = 0, + val name: String = "", + val phoneNumber: String = "", + val createdAt: String = "", + val modifiedAt: String = "", +) diff --git a/core/model/src/main/java/com/susu/core/model/FriendSearch.kt b/core/model/src/main/java/com/susu/core/model/FriendSearch.kt new file mode 100644 index 00000000..9be3f87f --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/FriendSearch.kt @@ -0,0 +1,14 @@ +package com.susu.core.model + +import java.time.LocalDateTime + +data class FriendSearch( + val friend: Friend, + val relationship: Relationship, + val recentEnvelope: RecentEnvelope? = null, +) + +data class RecentEnvelope( + val category: String, + val handedOverAt: LocalDateTime, +) diff --git a/core/model/src/main/java/com/susu/core/model/Ledger.kt b/core/model/src/main/java/com/susu/core/model/Ledger.kt index 5bd7b56b..d03acf55 100644 --- a/core/model/src/main/java/com/susu/core/model/Ledger.kt +++ b/core/model/src/main/java/com/susu/core/model/Ledger.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable @Stable @Serializable data class Ledger( - val id: Int = -1, + val id: Long = -1, val title: String = "", val description: String = "", val startAt: LocalDateTime = java.time.LocalDateTime.now().toKotlinLocalDateTime(), diff --git a/core/model/src/main/java/com/susu/core/model/Relationship.kt b/core/model/src/main/java/com/susu/core/model/Relationship.kt new file mode 100644 index 00000000..b4d8589b --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/Relationship.kt @@ -0,0 +1,10 @@ +package com.susu.core.model + +import androidx.compose.runtime.Stable + +@Stable +data class Relationship( + val id: Int = -1, + val relation: String = "", + val customRelation: String? = null, +) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 8b3390ab..6232672f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -11,4 +11,5 @@ android { dependencies { implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.immutable) } diff --git a/core/ui/src/main/java/com/susu/core/ui/Consts.kt b/core/ui/src/main/java/com/susu/core/ui/Consts.kt index bdfaa654..bdb9c1db 100644 --- a/core/ui/src/main/java/com/susu/core/ui/Consts.kt +++ b/core/ui/src/main/java/com/susu/core/ui/Consts.kt @@ -5,7 +5,9 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import kotlinx.collections.immutable.persistentListOf +// TODO REMOVE val alignList @Composable get() = listOf( @@ -15,6 +17,8 @@ val alignList stringResource(id = R.string.word_align_low_amount), ) +val moneyList = persistentListOf(10_000, 30_000, 50_000, 100_000, 500_000) + const val USER_NAME_MAX_LENGTH = 10 val nameRegex = Regex("[a-zA-Z가-힣]{0,10}") diff --git a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt index daf25d15..c3232c44 100644 --- a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt @@ -1,7 +1,9 @@ package com.susu.data.data.di import com.susu.data.data.repository.CategoryConfigRepositoryImpl +import com.susu.data.data.repository.EnvelopesRepositoryImpl import com.susu.data.data.repository.ExcelRepositoryImpl +import com.susu.data.data.repository.FriendRepositoryImpl import com.susu.data.data.repository.LedgerRecentSearchRepositoryImpl import com.susu.data.data.repository.LedgerRepositoryImpl import com.susu.data.data.repository.LoginRepositoryImpl @@ -12,7 +14,9 @@ import com.susu.data.data.repository.TokenRepositoryImpl import com.susu.data.data.repository.UserRepositoryImpl import com.susu.data.data.repository.VoteRepositoryImpl import com.susu.domain.repository.CategoryConfigRepository +import com.susu.domain.repository.EnvelopesRepository import com.susu.domain.repository.ExcelRepository +import com.susu.domain.repository.FriendRepository import com.susu.domain.repository.LedgerRecentSearchRepository import com.susu.domain.repository.LedgerRepository import com.susu.domain.repository.LoginRepository @@ -76,6 +80,16 @@ abstract class RepositoryModule { excelRepositoryImpl: ExcelRepositoryImpl, ): ExcelRepository + @Binds + abstract fun bindEnvelopesRepository( + envelopesRepositoryImpl: EnvelopesRepositoryImpl, + ): EnvelopesRepository + + @Binds + abstract fun bindFriendRepository( + friendRepositoryImpl: FriendRepositoryImpl, + ): FriendRepository + @Binds abstract fun bindStatisticsRepository( statisticsRepositoryImpl: StatisticsRepositoryImpl, diff --git a/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt new file mode 100644 index 00000000..943628e8 --- /dev/null +++ b/data/src/main/java/com/susu/data/data/repository/EnvelopesRepositoryImpl.kt @@ -0,0 +1,68 @@ +package com.susu.data.data.repository + +import com.susu.core.model.Envelope +import com.susu.core.model.EnvelopeStatics +import com.susu.core.model.Relationship +import com.susu.data.remote.api.EnvelopesService +import com.susu.data.remote.model.request.CategoryRequest +import com.susu.data.remote.model.request.EnvelopeRequest +import com.susu.data.remote.model.response.toModel +import com.susu.domain.repository.EnvelopesRepository +import javax.inject.Inject + +class EnvelopesRepositoryImpl @Inject constructor( + private val envelopesService: EnvelopesService, +) : EnvelopesRepository { + override suspend fun getEnvelopesList( + friendIds: List?, + fromTotalAmounts: Int?, + toTotalAmounts: Int?, + page: Int?, + size: Int?, + sort: String?, + ): List = envelopesService.getEnvelopesList( + friendIds = friendIds, + fromTotalAmounts = fromTotalAmounts, + toTotalMounts = toTotalAmounts, + page = page, + size = size, + sort = sort, + ).getOrThrow().toModel() + + override suspend fun getRelationShipConfigList(): List = envelopesService + .getRelationShipConfigList() + .getOrThrow() + .toModel() + + override suspend fun createEnvelope( + type: String, + friendId: Long, + ledgerId: Long?, + amount: Long, + gift: String?, + memo: String?, + hasVisited: Boolean?, + handedOverAt: kotlinx.datetime.LocalDateTime, + categoryId: Long?, + customCategory: String?, + ): Envelope = envelopesService.createEnvelope( + envelopeRequest = EnvelopeRequest( + type = type, + friendId = friendId, + ledgerId = ledgerId, + amount = amount, + gift = gift, + memo = memo, + hasVisited = hasVisited, + handedOverAt = handedOverAt, + category = if (categoryId != null) { + CategoryRequest( + id = categoryId, + customCategory = customCategory, + ) + } else { + null + }, + ), + ).getOrThrow().toModel() +} diff --git a/data/src/main/java/com/susu/data/data/repository/FriendRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/FriendRepositoryImpl.kt new file mode 100644 index 00000000..5111a7cd --- /dev/null +++ b/data/src/main/java/com/susu/data/data/repository/FriendRepositoryImpl.kt @@ -0,0 +1,30 @@ +package com.susu.data.data.repository + +import com.susu.core.model.FriendSearch +import com.susu.data.remote.api.FriendService +import com.susu.data.remote.model.request.FriendRequest +import com.susu.data.remote.model.response.toModel +import com.susu.domain.repository.FriendRepository +import javax.inject.Inject + +class FriendRepositoryImpl @Inject constructor( + private val friendService: FriendService, +) : FriendRepository { + override suspend fun createFriend( + name: String, + phoneNumber: String?, + relationshipId: Int, + customRelation: String?, + ): Long = friendService.createFriend( + FriendRequest( + name = name, + phoneNumber = phoneNumber, + relationshipId = relationshipId, + customRelation = customRelation, + ), + ).getOrThrow().id + + override suspend fun searchFriend(name: String): List = friendService.searchFriend( + name = name, + ).getOrThrow().data.map { it.toModel() } +} diff --git a/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt index 3093801a..626cd855 100644 --- a/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt @@ -37,7 +37,7 @@ class LedgerRepositoryImpl @Inject constructor( ledgerRequest = ledger.toData(), ).getOrThrow().toModel() - override suspend fun deleteLedger(id: Int) = ledgerService.deleteLedgerList( + override suspend fun deleteLedger(id: Long) = ledgerService.deleteLedgerList( listOf(id), ).getOrThrow() diff --git a/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt b/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt index 0c84f2dd..c623c65a 100644 --- a/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt +++ b/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt @@ -10,16 +10,19 @@ data class CategoryConfigEntity( val id: Int, val seq: Int, val name: String, + val style: String, ) internal fun CategoryConfigEntity.toModel() = Category( id = id, seq = seq, name = name, + style = style, ) internal fun Category.toEntity() = CategoryConfigEntity( id = id, seq = seq, name = name, + style = style, ) diff --git a/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt b/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt new file mode 100644 index 00000000..947e1c33 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/api/EnvelopesService.kt @@ -0,0 +1,31 @@ +package com.susu.data.remote.api + +import com.susu.data.remote.model.request.EnvelopeRequest +import com.susu.data.remote.model.response.EnvelopeResponse +import com.susu.data.remote.model.response.EnvelopesListResponse +import com.susu.data.remote.model.response.RelationShipListResponse +import com.susu.data.remote.retrofit.ApiResult +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface EnvelopesService { + @GET("envelopes/friend-statistics") + suspend fun getEnvelopesList( + @Query("friendIds") friendIds: List?, + @Query("fromTotalAmounts") fromTotalAmounts: Int?, + @Query("toTotalAmounts") toTotalMounts: Int?, + @Query("page") page: Int?, + @Query("size") size: Int?, + @Query("sort") sort: String?, + ): ApiResult + + @GET("envelopes/configs/create-envelopes") + suspend fun getRelationShipConfigList(): ApiResult + + @POST("envelopes") + suspend fun createEnvelope( + @Body envelopeRequest: EnvelopeRequest, + ): ApiResult +} diff --git a/data/src/main/java/com/susu/data/remote/api/FriendService.kt b/data/src/main/java/com/susu/data/remote/api/FriendService.kt new file mode 100644 index 00000000..9274a7b4 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/api/FriendService.kt @@ -0,0 +1,23 @@ +package com.susu.data.remote.api + +import com.susu.data.remote.model.request.FriendRequest +import com.susu.data.remote.model.response.FriendResponse +import com.susu.data.remote.model.response.FriendSearchListResponse +import com.susu.data.remote.retrofit.ApiResult +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface FriendService { + + @POST("friends") + suspend fun createFriend( + @Body friendRequest: FriendRequest, + ): ApiResult + + @GET("friends") + suspend fun searchFriend( + @Query("name") name: String, + ): ApiResult +} diff --git a/data/src/main/java/com/susu/data/remote/api/LedgerService.kt b/data/src/main/java/com/susu/data/remote/api/LedgerService.kt index a880d175..e8fdef66 100644 --- a/data/src/main/java/com/susu/data/remote/api/LedgerService.kt +++ b/data/src/main/java/com/susu/data/remote/api/LedgerService.kt @@ -27,7 +27,7 @@ interface LedgerService { @PATCH("ledgers/{id}") suspend fun editLedger( - @Path("id") id: Int, + @Path("id") id: Long, @Body ledgerRequest: LedgerRequest, ): ApiResult @@ -37,7 +37,7 @@ interface LedgerService { ): ApiResult @DELETE("ledgers") - suspend fun deleteLedgerList(@Query("ids") idList: List): ApiResult + suspend fun deleteLedgerList(@Query("ids") idList: List): ApiResult @GET("ledgers/configs/create-ledger") suspend fun getCreateLedgerConfig(): ApiResult diff --git a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt index 7c6452dd..60ea9578 100644 --- a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt +++ b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt @@ -2,6 +2,8 @@ package com.susu.data.remote.di import com.susu.data.remote.api.AuthService import com.susu.data.remote.api.CategoryService +import com.susu.data.remote.api.EnvelopesService +import com.susu.data.remote.api.FriendService import com.susu.data.remote.api.LedgerService import com.susu.data.remote.api.SignUpService import com.susu.data.remote.api.StatisticsService @@ -22,7 +24,7 @@ object ApiServiceModule { @Singleton @Provides - fun provideUserService(retrofit: Retrofit): AuthService { + fun provideAuthService(retrofit: Retrofit): AuthService { return retrofit.create(AuthService::class.java) } @@ -56,12 +58,24 @@ object ApiServiceModule { return retrofit.create(CategoryService::class.java) } + @Singleton + @Provides + fun providesEnvelopeService(retrofit: Retrofit): EnvelopesService { + return retrofit.create(EnvelopesService::class.java) + } + @Singleton @Provides fun providesUserService(retrofit: Retrofit): UserService { return retrofit.create(UserService::class.java) } + @Singleton + @Provides + fun providesFriendService(retrofit: Retrofit): FriendService { + return retrofit.create(FriendService::class.java) + } + @Singleton @Provides fun providesStatisticsService(retrofit: Retrofit): StatisticsService { diff --git a/data/src/main/java/com/susu/data/remote/model/request/EnvelopeRequest.kt b/data/src/main/java/com/susu/data/remote/model/request/EnvelopeRequest.kt new file mode 100644 index 00000000..45d33e5d --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/request/EnvelopeRequest.kt @@ -0,0 +1,23 @@ +package com.susu.data.remote.model.request + +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class EnvelopeRequest( + val type: String, + val friendId: Long, + val ledgerId: Long? = null, + val amount: Long, + val gift: String? = null, + val memo: String? = null, + val hasVisited: Boolean? = null, + val handedOverAt: LocalDateTime? = null, + val category: CategoryRequest? = null, +) + +@Serializable +data class CategoryRequest( + val id: Long, + val customCategory: String? = null, +) diff --git a/data/src/main/java/com/susu/data/remote/model/request/FriendRequest.kt b/data/src/main/java/com/susu/data/remote/model/request/FriendRequest.kt new file mode 100644 index 00000000..b69afebc --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/request/FriendRequest.kt @@ -0,0 +1,11 @@ +package com.susu.data.remote.model.request + +import kotlinx.serialization.Serializable + +@Serializable +data class FriendRequest( + val name: String, + val phoneNumber: String? = null, + val relationshipId: Int, + val customRelation: String? = null, +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt index cc701b65..2ca0d38d 100644 --- a/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt +++ b/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt @@ -8,10 +8,12 @@ data class CategoryConfigResponse( val id: Int, val seq: Int, val name: String, + val style: String, ) internal fun CategoryConfigResponse.toModel() = Category( id = id, seq = seq, name = name, + style = style, ) diff --git a/data/src/main/java/com/susu/data/remote/model/response/EnvelopeResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/EnvelopeResponse.kt new file mode 100644 index 00000000..76ec62f8 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/EnvelopeResponse.kt @@ -0,0 +1,31 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.Envelope +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class EnvelopeResponse( + val id: Long, + val uid: Long, + val type: String, + val friend: FriendInfo, + val amount: Long, + val gift: String? = null, + val memo: String? = null, + val hasVisited: Boolean? = null, + val handedOverAt: LocalDateTime? = null, +) + +internal fun EnvelopeResponse.toModel() = Envelope( + id = id, + uid = uid, + type = type, + friend = friend.toModel(), + amount = amount, + gift = gift, + memo = memo, + hasVisited = hasVisited, + handedOverAt = handedOverAt?.toJavaLocalDateTime(), +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/EnvelopesListResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/EnvelopesListResponse.kt new file mode 100644 index 00000000..ac76868f --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/EnvelopesListResponse.kt @@ -0,0 +1,26 @@ +package com.susu.data.remote.model.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EnvelopesListResponse( + @SerialName("data") + val envelopesList: List, + val page: Int, + val size: Int, + val sort: Sort, + val totalCount: Int, + val totalPage: Int, +) + +@Serializable +data class Sort( + val empty: Boolean, + val sorted: Boolean, + val unsorted: Boolean, +) + +internal fun EnvelopesListResponse.toModel() = this.envelopesList.map { envelopes -> + envelopes.toModel() +} diff --git a/data/src/main/java/com/susu/data/remote/model/response/EnvelopesResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/EnvelopesResponse.kt new file mode 100644 index 00000000..f64572c6 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/EnvelopesResponse.kt @@ -0,0 +1,39 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.EnvelopeStatics +import com.susu.core.model.Friend +import kotlinx.serialization.Serializable + +@Serializable +data class EnvelopesResponse( + val friend: FriendInfo, + val receivedAmounts: Int, + val sentAmounts: Int, + val totalAmounts: Int, +) + +@Serializable +data class FriendInfo( + val id: Long, + val uid: Long = 0, + val createdAt: String = "", + val modifiedAt: String = "", + val name: String, + val phoneNumber: String = "", +) + +internal fun FriendInfo.toModel() = Friend( + id = id, + uid = uid, + name = name, + phoneNumber = phoneNumber, + createdAt = createdAt, + modifiedAt = modifiedAt, +) + +internal fun EnvelopesResponse.toModel() = EnvelopeStatics( + friend = friend.toModel(), + receivedAmounts = receivedAmounts, + sentAmounts = sentAmounts, + totalAmounts = totalAmounts, +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/FriendResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/FriendResponse.kt new file mode 100644 index 00000000..68ee47e6 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/FriendResponse.kt @@ -0,0 +1,6 @@ +package com.susu.data.remote.model.response + +import kotlinx.serialization.Serializable + +@Serializable +data class FriendResponse(val id: Long) diff --git a/data/src/main/java/com/susu/data/remote/model/response/FriendSearchResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/FriendSearchResponse.kt new file mode 100644 index 00000000..21c3ca84 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/FriendSearchResponse.kt @@ -0,0 +1,57 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.FriendSearch +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class FriendSearchListResponse( + val data: List, +) + +@Serializable +data class FriendSearchResponse( + val friend: Friend, + val relationship: Relationship, + val recentEnvelope: RecentEnvelope? = null, +) + +@Serializable +data class Friend( + val id: Long, + val name: String, + val phoneNumber: String? = null, +) + +@Serializable +data class Relationship( + val id: Int, + val relation: String, + val customRelation: String? = null, +) + +@Serializable +data class RecentEnvelope( + val category: String, + val handedOverAt: LocalDateTime, +) + +internal fun FriendSearchResponse.toModel() = FriendSearch( + friend = com.susu.core.model.Friend( + id = friend.id, + name = friend.name, + phoneNumber = friend.phoneNumber ?: "", + ), + relationship = com.susu.core.model.Relationship( + id = relationship.id, + relation = relationship.relation, + customRelation = relationship.customRelation, + ), + recentEnvelope = recentEnvelope?.toModel(), +) + +internal fun RecentEnvelope.toModel() = com.susu.core.model.RecentEnvelope( + category = category, + handedOverAt = handedOverAt.toJavaLocalDateTime(), +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt index cf7d0450..e3a372fb 100644 --- a/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt +++ b/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt @@ -17,7 +17,7 @@ data class LedgerResponse( @Serializable data class LedgerInfo( - val id: Int, + val id: Long, val title: String, val description: String = "", val startAt: LocalDateTime, @@ -30,6 +30,7 @@ data class CategoryInfo( val seq: Int, val category: String, val customCategory: String? = null, + val style: String, ) internal fun LedgerResponse.toModel() = Ledger( @@ -48,4 +49,5 @@ internal fun CategoryInfo.toModel() = Category( seq = seq, name = category, customCategory = customCategory, + style = style, ) diff --git a/data/src/main/java/com/susu/data/remote/model/response/RelationShipListResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/RelationShipListResponse.kt new file mode 100644 index 00000000..3b73868c --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/RelationShipListResponse.kt @@ -0,0 +1,22 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.Relationship +import kotlinx.serialization.Serializable + +@Serializable +data class RelationShipListResponse( + val relationships: List = emptyList(), +) + +@Serializable +data class RelationConfigShipResponse( + val id: Int, + val relation: String, +) + +internal fun RelationShipListResponse.toModel() = relationships.map { it.toModel() } + +internal fun RelationConfigShipResponse.toModel() = Relationship( + id = id, + relation = relation, +) diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index ae1c9f8d..f4faa7d9 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -9,4 +9,6 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.hilt.core) + + implementation(libs.kotlinx.datetime) } diff --git a/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt b/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt new file mode 100644 index 00000000..1a82425b --- /dev/null +++ b/domain/src/main/java/com/susu/domain/repository/EnvelopesRepository.kt @@ -0,0 +1,32 @@ +package com.susu.domain.repository + +import com.susu.core.model.Envelope +import com.susu.core.model.EnvelopeStatics +import com.susu.core.model.Relationship +import kotlinx.datetime.LocalDateTime + +interface EnvelopesRepository { + suspend fun getEnvelopesList( + friendIds: List?, + fromTotalAmounts: Int?, + toTotalAmounts: Int?, + page: Int?, + size: Int?, + sort: String?, + ): List + + suspend fun getRelationShipConfigList(): List + + suspend fun createEnvelope( + type: String, + friendId: Long, + ledgerId: Long? = null, + amount: Long, + gift: String? = null, + memo: String? = null, + hasVisited: Boolean? = null, + handedOverAt: LocalDateTime, + categoryId: Long? = null, + customCategory: String? = null, + ): Envelope +} diff --git a/domain/src/main/java/com/susu/domain/repository/FriendRepository.kt b/domain/src/main/java/com/susu/domain/repository/FriendRepository.kt new file mode 100644 index 00000000..3093f9f0 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/repository/FriendRepository.kt @@ -0,0 +1,16 @@ +package com.susu.domain.repository + +import com.susu.core.model.FriendSearch + +interface FriendRepository { + suspend fun createFriend( + name: String, + phoneNumber: String? = null, + relationshipId: Int, + customRelation: String? = null, + ): Long + + suspend fun searchFriend( + name: String, + ): List +} diff --git a/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt b/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt index eca4dec3..f2e6cdd5 100644 --- a/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt +++ b/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt @@ -22,7 +22,7 @@ interface LedgerRepository { ): Ledger suspend fun deleteLedger( - id: Int, + id: Long, ) suspend fun getCreateLedgerConfig(): List diff --git a/domain/src/main/java/com/susu/domain/usecase/envelope/CreateReceivedEnvelopeUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/envelope/CreateReceivedEnvelopeUseCase.kt new file mode 100644 index 00000000..eabd92a2 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/envelope/CreateReceivedEnvelopeUseCase.kt @@ -0,0 +1,48 @@ +package com.susu.domain.usecase.envelope + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.EnvelopesRepository +import com.susu.domain.repository.FriendRepository +import kotlinx.datetime.LocalDateTime +import javax.inject.Inject + +class CreateReceivedEnvelopeUseCase @Inject constructor( + private val friendRepository: FriendRepository, + private val envelopesRepository: EnvelopesRepository, +) { + suspend operator fun invoke(param: Param) = runCatchingIgnoreCancelled { + with(param) { + val friendId = friendId ?: friendRepository.createFriend( + name = friendName!!, + phoneNumber = phoneNumber, + relationshipId = relationshipId!!, + customRelation = customRelation, + ) + + envelopesRepository.createEnvelope( + type = "RECEIVED", + friendId = friendId, + ledgerId = ledgerId, + amount = amount, + gift = gift, + memo = memo, + hasVisited = hasVisited, + handedOverAt = handedOverAt, + ) + } + } + + data class Param( + val friendId: Long? = null, + val friendName: String? = null, + val phoneNumber: String? = null, + val relationshipId: Int? = null, + val customRelation: String? = null, + val ledgerId: Long, + val amount: Long, + val gift: String? = null, + val memo: String? = null, + val handedOverAt: LocalDateTime, + val hasVisited: Boolean? = null, + ) +} diff --git a/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt new file mode 100644 index 00000000..ae47170b --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/envelope/GetEnvelopesListUseCase.kt @@ -0,0 +1,31 @@ +package com.susu.domain.usecase.envelope + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.EnvelopesRepository +import javax.inject.Inject + +class GetEnvelopesListUseCase @Inject constructor( + private val envelopesRepository: EnvelopesRepository, +) { + suspend operator fun invoke(param: Param) = runCatchingIgnoreCancelled { + with(param) { + envelopesRepository.getEnvelopesList( + friendIds = friendIds, + fromTotalAmounts = fromTotalAmounts, + toTotalAmounts = toTotalAmounts, + page = page, + size = size, + sort = sort, + ) + } + } + + data class Param( + val friendIds: List? = emptyList(), + val fromTotalAmounts: Int? = null, + val toTotalAmounts: Int? = null, + val page: Int? = null, + val size: Int? = null, + val sort: String? = null, + ) +} diff --git a/domain/src/main/java/com/susu/domain/usecase/envelope/GetRelationShipConfigListUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/envelope/GetRelationShipConfigListUseCase.kt new file mode 100644 index 00000000..bba412b1 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/envelope/GetRelationShipConfigListUseCase.kt @@ -0,0 +1,13 @@ +package com.susu.domain.usecase.envelope + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.EnvelopesRepository +import javax.inject.Inject + +class GetRelationShipConfigListUseCase @Inject constructor( + private val envelopesRepository: EnvelopesRepository, +) { + suspend operator fun invoke() = runCatchingIgnoreCancelled { + envelopesRepository.getRelationShipConfigList() + } +} diff --git a/domain/src/main/java/com/susu/domain/usecase/friend/SearchFriendUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/friend/SearchFriendUseCase.kt new file mode 100644 index 00000000..e8ba6943 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/friend/SearchFriendUseCase.kt @@ -0,0 +1,15 @@ +package com.susu.domain.usecase.friend + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.FriendRepository +import javax.inject.Inject + +class SearchFriendUseCase @Inject constructor( + private val friendRepository: FriendRepository, +) { + suspend operator fun invoke(name: String) = runCatchingIgnoreCancelled { + friendRepository.searchFriend( + name = name, + ) + } +} diff --git a/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt index 43d143ef..10495fec 100644 --- a/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt +++ b/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt @@ -7,7 +7,7 @@ import javax.inject.Inject class DeleteLedgerUseCase @Inject constructor( private val ledgerRepository: LedgerRepository, ) { - suspend operator fun invoke(id: Int) = runCatchingIgnoreCancelled { + suspend operator fun invoke(id: Long) = runCatchingIgnoreCancelled { ledgerRepository.deleteLedger(id) } } diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt index 88c86fb6..c9dac278 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt @@ -155,12 +155,12 @@ internal class MainNavigator( navController.navigateMyPageSocial() } - fun navigateMyPagePrivacyPolicy() { - navController.navigateMyPagePrivacyPolicy() + fun navigateReceivedEnvelopeAdd(categoryName: String, ledgerId: Long) { + navController.navigateReceivedEnvelopeAdd(categoryName, ledgerId) } - fun navigateReceivedEnvelopeAdd() { - navController.navigateReceivedEnvelopeAdd() + fun navigateMyPagePrivacyPolicy() { + navController.navigateMyPagePrivacyPolicy() } fun navigateReceivedEnvelopeDetail() { diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddContract.kt new file mode 100644 index 00000000..e4ac460e --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddContract.kt @@ -0,0 +1,40 @@ +package com.susu.feature.received.envelopeadd + +import androidx.annotation.StringRes +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import com.susu.feature.received.R + +data class ReceivedEnvelopeAddState( + val currentStep: EnvelopeAddStep = EnvelopeAddStep.MONEY, + val buttonEnabled: Boolean = false, + val lastPage: Boolean = false, + val isLoading: Boolean = false, +) : UiState { + val buttonResId = if (lastPage) { + com.susu.core.ui.R.string.word_done + } else { + com.susu.core.ui.R.string.word_next + } + + val progress = if (lastPage) EnvelopeAddStep.entries.size else currentStep.ordinal + 1 +} + +enum class EnvelopeAddStep( + @StringRes val stringResId: Int? = null, +) { + MONEY, + NAME, + RELATIONSHIP, + DATE, + MORE, + VISITED(R.string.envelop_add_step_visited), + PRESENT(R.string.envelop_add_step_present), + PHONE(R.string.envelop_add_step_phone), + MEMO(R.string.envelop_add_step_memo), +} + +sealed interface ReceivedEnvelopeAddSideEffect : SideEffect { + data object PopBackStack : ReceivedEnvelopeAddSideEffect + data class HandleException(val throwable: Throwable, val retry: () -> Unit) : ReceivedEnvelopeAddSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddScreen.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddScreen.kt index 865a75dc..6900eaf4 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddScreen.kt @@ -1,5 +1,6 @@ package com.susu.feature.received.envelopeadd +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -15,74 +16,92 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.appbar.SusuProgressAppBar import com.susu.core.designsystem.component.appbar.icon.BackIcon import com.susu.core.designsystem.component.button.FilledButtonColor import com.susu.core.designsystem.component.button.MediumButtonStyle import com.susu.core.designsystem.component.button.SusuFilledButton import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Relationship +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.core.ui.extension.susuDefaultAnimatedContentTransitionSpec -import com.susu.feature.received.R -import com.susu.feature.received.envelopeadd.content.MemoContent -import com.susu.feature.received.envelopeadd.content.MoneyContent -import com.susu.feature.received.envelopeadd.content.MoreContent -import com.susu.feature.received.envelopeadd.content.NameContent -import com.susu.feature.received.envelopeadd.content.PhoneContent -import com.susu.feature.received.envelopeadd.content.PresentContent -import com.susu.feature.received.envelopeadd.content.RelationshipContent -import com.susu.feature.received.envelopeadd.content.VisitedContent - -enum class EnvelopeAddStep { - MONEY, - NAME, - RELATIONSHIP, - MORE, - VISITED, - PRESENT, - PHONE, - MEMO, -} +import com.susu.feature.received.envelopeadd.content.date.DateContentRoute +import com.susu.feature.received.envelopeadd.content.memo.MemoContentRoute +import com.susu.feature.received.envelopeadd.content.money.MoneyContentRoute +import com.susu.feature.received.envelopeadd.content.more.MoreContentRoute +import com.susu.feature.received.envelopeadd.content.name.NameContentRoute +import com.susu.feature.received.envelopeadd.content.phone.PhoneContentRoute +import com.susu.feature.received.envelopeadd.content.present.PresentContentRoute +import com.susu.feature.received.envelopeadd.content.relationship.RelationShipContentRoute +import com.susu.feature.received.envelopeadd.content.visited.VisitedContentRoute +import java.time.LocalDateTime @Composable fun ReceivedEnvelopeAddRoute( + viewModel: ReceivedEnvelopeAddViewModel = hiltViewModel(), popBackStack: () -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, ) { - var currentStep by remember { mutableStateOf(EnvelopeAddStep.MONEY) } + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is ReceivedEnvelopeAddSideEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) + ReceivedEnvelopeAddSideEffect.PopBackStack -> popBackStack() + } + } + + var friendName by remember { + mutableStateOf("") + } + + BackHandler { + viewModel.goToPrevStep() + } ReceivedEnvelopeAddScreen( - currentStep = currentStep, - onClickBack = popBackStack, - onClickNext = { - // TODO: 수정 필요 (MORE 이후 분리 필요) - currentStep = when (currentStep) { - EnvelopeAddStep.MONEY -> EnvelopeAddStep.NAME - EnvelopeAddStep.NAME -> EnvelopeAddStep.RELATIONSHIP - EnvelopeAddStep.RELATIONSHIP -> EnvelopeAddStep.MORE - EnvelopeAddStep.MORE -> EnvelopeAddStep.VISITED - EnvelopeAddStep.VISITED -> EnvelopeAddStep.PRESENT - EnvelopeAddStep.PRESENT -> EnvelopeAddStep.PHONE - EnvelopeAddStep.PHONE -> EnvelopeAddStep.MEMO - else -> EnvelopeAddStep.MEMO - } + uiState = uiState, + onClickBack = viewModel::goToPrevStep, + onClickNext = viewModel::goToNextStep, + updateParentMoney = viewModel::updateMoney, + updateParentName = { name -> + viewModel.updateName(name) + friendName = name }, + updateParentFriendId = viewModel::updateFriendId, + updateParentSelectedRelationShip = viewModel::updateSelectedRelationShip, + updateParentMoreStep = viewModel::updateMoreStep, + updateParentDate = viewModel::updateDate, + categoryName = viewModel.categoryName, + updateParentVisited = viewModel::updateHasVisited, + updateParentPresent = viewModel::updatePresent, + friendName = friendName, + updateParentPhoneNumber = viewModel::updatePhoneNumber, + updateParentMemo = viewModel::updateMemo, ) } @Composable fun ReceivedEnvelopeAddScreen( - modifier: Modifier = Modifier, - currentStep: EnvelopeAddStep = EnvelopeAddStep.MONEY, + uiState: ReceivedEnvelopeAddState = ReceivedEnvelopeAddState(), onClickBack: () -> Unit = {}, onClickNext: () -> Unit = {}, + updateParentMoney: (Long) -> Unit = {}, + updateParentName: (String) -> Unit = {}, + updateParentFriendId: (Long?) -> Unit = {}, + updateParentSelectedRelationShip: (Relationship?) -> Unit = {}, + updateParentDate: (LocalDateTime?) -> Unit = {}, + updateParentMoreStep: (List) -> Unit = {}, + categoryName: String = "", + updateParentVisited: (Boolean?) -> Unit = {}, + updateParentPresent: (String?) -> Unit = {}, + friendName: String = "", + updateParentPhoneNumber: (String?) -> Unit = {}, + updateParentMemo: (String?) -> Unit = {}, ) { - // TODO: 수정 필요 - val relationshipList = listOf("친구", "가족", "친척", "동료", "직접 입력") - val friendList = listOf("김철수", "국영수", "신짱구", "홍길동") - val moreList = listOf("방문여부", "선물", "메모", "보낸 이의 연락처") - val visitedList = listOf("예", "아니요") - Column( - modifier = modifier + modifier = Modifier .background(SusuTheme.colorScheme.background15) .fillMaxSize(), ) { @@ -92,13 +111,13 @@ fun ReceivedEnvelopeAddScreen( onClick = onClickBack, ) }, - currentStep = currentStep.ordinal + 1, + currentStep = uiState.progress, entireStep = EnvelopeAddStep.entries.size, ) AnimatedContent( - modifier = modifier.weight(1f), - targetState = currentStep, - label = "SentEnvelopeAddScreen", + modifier = Modifier.weight(1f), + targetState = uiState.currentStep, + label = "ReceivedEnvelopeAddScreen", transitionSpec = { susuDefaultAnimatedContentTransitionSpec( leftDirectionCondition = targetState.ordinal > initialState.ordinal, @@ -106,26 +125,50 @@ fun ReceivedEnvelopeAddScreen( }, ) { targetState -> when (targetState) { - EnvelopeAddStep.MONEY -> MoneyContent() - EnvelopeAddStep.NAME -> NameContent(friendList = friendList) - EnvelopeAddStep.RELATIONSHIP -> RelationshipContent(relationshipList = relationshipList) - EnvelopeAddStep.MORE -> MoreContent(moreList = moreList) - EnvelopeAddStep.VISITED -> VisitedContent( - event = "결혼식", - visitedList = visitedList, + EnvelopeAddStep.MONEY -> MoneyContentRoute( + updateParentMoney = updateParentMoney, + ) + + EnvelopeAddStep.NAME -> NameContentRoute( + updateParentName = updateParentName, + updateParentFriendId = updateParentFriendId, + ) + EnvelopeAddStep.RELATIONSHIP -> RelationShipContentRoute( + updateParentSelectedRelation = updateParentSelectedRelationShip, + ) + EnvelopeAddStep.DATE -> DateContentRoute( + friendName = friendName, + updateParentDate = updateParentDate, + ) + EnvelopeAddStep.MORE -> MoreContentRoute( + updateParentMoreStep = updateParentMoreStep, + ) + EnvelopeAddStep.VISITED -> VisitedContentRoute( + categoryName = categoryName, + updateParentVisited = updateParentVisited, + ) + + EnvelopeAddStep.PRESENT -> PresentContentRoute( + updateParentPresent = updateParentPresent, + ) + EnvelopeAddStep.PHONE -> PhoneContentRoute( + friendName = friendName, + updateParentPhone = updateParentPhoneNumber, + ) + EnvelopeAddStep.MEMO -> MemoContentRoute( + updateParentMemo = updateParentMemo, ) - EnvelopeAddStep.PRESENT -> PresentContent() - EnvelopeAddStep.PHONE -> PhoneContent(name = "김철수") - EnvelopeAddStep.MEMO -> MemoContent() } } SusuFilledButton( color = FilledButtonColor.Black, style = MediumButtonStyle.height60, shape = RectangleShape, - text = stringResource(id = com.susu.core.ui.R.string.word_next), + text = stringResource(id = uiState.buttonResId), onClick = onClickNext, - modifier = modifier + isClickable = uiState.buttonEnabled, + isActive = uiState.buttonEnabled, + modifier = Modifier .fillMaxWidth() .imePadding(), ) diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddViewModel.kt new file mode 100644 index 00000000..54016c8a --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/ReceivedEnvelopeAddViewModel.kt @@ -0,0 +1,208 @@ +package com.susu.feature.received.envelopeadd + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Relationship +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.envelope.CreateReceivedEnvelopeUseCase +import com.susu.feature.received.navigation.ReceivedRoute +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import kotlinx.datetime.toKotlinLocalDateTime +import java.time.LocalDateTime +import javax.inject.Inject + +@HiltViewModel +class ReceivedEnvelopeAddViewModel @Inject constructor( + private val createReceivedEnvelopeUseCase: CreateReceivedEnvelopeUseCase, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + ReceivedEnvelopeAddState(), +) { + val categoryName = savedStateHandle.get(ReceivedRoute.CATEGORY_ARGUMENT_NAME)!! + private val ledgerId = savedStateHandle.get(ReceivedRoute.LEDGER_ID_ARGUMENT_NAME)!! + + private var money: Long = 0 + private var name: String = "" + private var friendId: Long? = null + private var relationShip: Relationship? = null + private var date: LocalDateTime? = null + private var moreStep: List = emptyList() + private var hasVisited: Boolean? = null + private var present: String? = null + private var phoneNumber: String? = null + private var memo: String? = null + + private val skipRelationshipStep + get() = friendId != null + + private fun createEnvelope() = viewModelScope.launch { + createReceivedEnvelopeUseCase( + param = CreateReceivedEnvelopeUseCase.Param( + friendId = friendId, + friendName = name, + phoneNumber = phoneNumber, + relationshipId = relationShip?.id, + customRelation = relationShip?.customRelation, + ledgerId = ledgerId.toLong(), + amount = money, + gift = present, + memo = memo, + handedOverAt = date!!.toKotlinLocalDateTime(), + hasVisited = hasVisited, + ), + ).onSuccess { + // TODO PopBackStackWithEnvelope로 변경 필요, 또한 Envelope에 friendName 추가 필요 + postSideEffect(ReceivedEnvelopeAddSideEffect.PopBackStack) + }.onFailure { + postSideEffect(ReceivedEnvelopeAddSideEffect.HandleException(it, ::createEnvelope)) + } + } + + fun goToPrevStep() = intent { + val prevStep = when (currentStep) { + EnvelopeAddStep.MONEY -> { + postSideEffect(ReceivedEnvelopeAddSideEffect.PopBackStack) + EnvelopeAddStep.MONEY + } + + EnvelopeAddStep.NAME -> EnvelopeAddStep.MONEY + EnvelopeAddStep.RELATIONSHIP -> EnvelopeAddStep.NAME + EnvelopeAddStep.DATE -> { + if (skipRelationshipStep) { + EnvelopeAddStep.NAME + } else { + EnvelopeAddStep.RELATIONSHIP + } + } + + EnvelopeAddStep.MORE -> EnvelopeAddStep.DATE + else -> goToPrevStepInMore(currentStep) + } + + copy( + currentStep = prevStep, + lastPage = false, + ) + } + + private fun goToPrevStepInMore(currentStep: EnvelopeAddStep): EnvelopeAddStep { + if (moreStep.isEmpty()) { + return EnvelopeAddStep.MORE + } + + val prevStepIndex = run { + val currentStepIndex = moreStep.indexOf(currentStep) + if (currentStepIndex == -1) EnvelopeAddStep.MORE.ordinal else currentStepIndex - 1 + } + + return moreStep.getOrNull(prevStepIndex) ?: EnvelopeAddStep.MORE + } + + fun goToNextStep() = intent { + val nextStep = when (currentStep) { + EnvelopeAddStep.MONEY -> EnvelopeAddStep.NAME + EnvelopeAddStep.NAME -> { + if (skipRelationshipStep) { + EnvelopeAddStep.DATE + } else { + EnvelopeAddStep.RELATIONSHIP + } + } + + EnvelopeAddStep.RELATIONSHIP -> EnvelopeAddStep.DATE + EnvelopeAddStep.DATE -> EnvelopeAddStep.MORE + else -> goToNextStepInMore(currentStep) + } + + if (nextStep == null) { + createEnvelope() + return@intent this + } + + copy( + currentStep = nextStep, + lastPage = nextStep == moreStep.lastOrNull(), + ) + } + + private fun goToNextStepInMore(currentStep: EnvelopeAddStep): EnvelopeAddStep? { + if (moreStep.isEmpty() || currentState.lastPage) { + return null + } + + val nextStepIndex = when (currentStep) { + EnvelopeAddStep.MORE -> 0 + else -> { + val currentStepIndex = moreStep.indexOf(currentStep) + if (currentStepIndex == -1) null else currentStepIndex + 1 + } + } ?: return null + + return moreStep.getOrNull(nextStepIndex) + } + + fun updateMoney(money: Long) = intent { + this@ReceivedEnvelopeAddViewModel.money = money + copy( + buttonEnabled = money > 0, + ) + } + + fun updateName(name: String) = intent { + this@ReceivedEnvelopeAddViewModel.name = name + copy( + buttonEnabled = name.isNotEmpty(), + ) + } + + fun updateFriendId(friendId: Long?) { + this.friendId = friendId + } + + fun updateSelectedRelationShip(relationShip: Relationship?) = intent { + this@ReceivedEnvelopeAddViewModel.relationShip = relationShip + copy( + buttonEnabled = relationShip != null, + ) + } + + fun updateMoreStep(moreStep: List) { + this@ReceivedEnvelopeAddViewModel.moreStep = moreStep + } + + fun updateHasVisited(hasVisited: Boolean?) = intent { + this@ReceivedEnvelopeAddViewModel.hasVisited = hasVisited + copy( + buttonEnabled = hasVisited != null, + ) + } + + fun updatePresent(present: String?) = intent { + this@ReceivedEnvelopeAddViewModel.present = present + copy( + buttonEnabled = !present.isNullOrEmpty(), + ) + } + + fun updatePhoneNumber(phoneNumber: String?) = intent { + this@ReceivedEnvelopeAddViewModel.phoneNumber = phoneNumber + copy( + buttonEnabled = !phoneNumber.isNullOrEmpty(), + ) + } + + fun updateMemo(memo: String?) = intent { + this@ReceivedEnvelopeAddViewModel.memo = memo + copy( + buttonEnabled = !memo.isNullOrEmpty(), + ) + } + + fun updateDate(date: LocalDateTime?) = intent { + this@ReceivedEnvelopeAddViewModel.date = date + copy( + buttonEnabled = date != null, + ) + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/NameContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/NameContent.kt deleted file mode 100644 index 970f876d..00000000 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/NameContent.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.susu.feature.received.envelopeadd.content - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.susu.core.designsystem.component.textfield.SusuBasicTextField -import com.susu.core.designsystem.theme.Gray100 -import com.susu.core.designsystem.theme.Gray40 -import com.susu.core.designsystem.theme.SusuTheme -import com.susu.feature.received.R -import com.susu.feature.received.envelopeadd.content.component.FriendListItem - -@Composable -fun NameContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), - friendList: List = emptyList(), -) { - var name by remember { mutableStateOf("") } - - Column( - modifier = modifier - .fillMaxSize() - .padding(padding), - ) { - Text( - text = stringResource(R.string.name_content_title), - style = SusuTheme.typography.title_m, - color = Gray100, - ) - Spacer( - modifier = modifier - .size(SusuTheme.spacing.spacing_m), - ) - SusuBasicTextField( - text = name, - onTextChange = { name = it }, - placeholder = stringResource(R.string.name_content_placeholder), - placeholderColor = Gray40, - modifier = modifier.fillMaxWidth(), - ) - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_xl)) - - if (friendList.isNotEmpty()) { - // TODO: 친구 목록 서버 연동 - LazyColumn { - items(friendList) { friend -> - FriendListItem(friend) - } - } - } - } -} - -@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) -@Composable -fun NameContentPreview() { - SusuTheme { - val friendList = listOf("김철수", "국영수", "가나다") - - NameContent(friendList = friendList) - } -} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/RelationshipContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/RelationshipContent.kt deleted file mode 100644 index 3bea7a52..00000000 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/RelationshipContent.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.susu.feature.received.envelopeadd.content - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.susu.core.designsystem.component.button.FilledButtonColor -import com.susu.core.designsystem.component.button.GhostButtonColor -import com.susu.core.designsystem.component.button.MediumButtonStyle -import com.susu.core.designsystem.component.button.SusuFilledButton -import com.susu.core.designsystem.component.button.SusuGhostButton -import com.susu.core.designsystem.theme.Gray100 -import com.susu.core.designsystem.theme.SusuTheme -import com.susu.feature.received.R - -@Composable -fun RelationshipContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), - relationshipList: List, -) { - val scrollState = rememberScrollState() - var selectedItem by remember { mutableStateOf(-1) } - - Column( - modifier = modifier - .fillMaxSize() - .padding(padding) - .verticalScroll(scrollState), - ) { - Text( - text = stringResource(R.string.relationship_content_title), - style = SusuTheme.typography.title_m, - color = Gray100, - ) - Spacer( - modifier = modifier - .size(SusuTheme.spacing.spacing_xxl), - ) - Column( - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - ) { - relationshipList.forEachIndexed { index, relationship -> - if (selectedItem == index) { - SusuFilledButton( - color = FilledButtonColor.Orange, - style = MediumButtonStyle.height60, - text = relationship, - onClick = { - selectedItem = index - }, - modifier = modifier.fillMaxWidth(), - ) - } else { - SusuGhostButton( - color = GhostButtonColor.Black, - style = MediumButtonStyle.height60, - text = relationship, - onClick = { - selectedItem = index - }, - modifier = modifier.fillMaxWidth(), - ) - } - } - } - } -} - -@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) -@Composable -fun RelationshipContentPreview() { - val relationshipList = mutableListOf("친구", "가족", "친척", "동료", "직접 입력") - - SusuTheme { - RelationshipContent(relationshipList = relationshipList) - } -} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/VisitedContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/VisitedContent.kt deleted file mode 100644 index f5e7de51..00000000 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/VisitedContent.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.susu.feature.received.envelopeadd.content - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.susu.core.designsystem.component.button.FilledButtonColor -import com.susu.core.designsystem.component.button.GhostButtonColor -import com.susu.core.designsystem.component.button.MediumButtonStyle -import com.susu.core.designsystem.component.button.SusuFilledButton -import com.susu.core.designsystem.component.button.SusuGhostButton -import com.susu.core.designsystem.theme.Gray60 -import com.susu.core.designsystem.theme.SusuTheme -import com.susu.core.ui.util.AnnotatedText -import com.susu.feature.received.R - -@Composable -fun VisitedContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), - event: String, - visitedList: List, -) { - var selectedItem by remember { mutableStateOf(-1) } - - Column( - modifier = modifier - .fillMaxSize() - .padding(padding), - ) { - AnnotatedText( - originalText = stringResource(R.string.visited_content_title, event), - originalTextStyle = SusuTheme.typography.title_m, - targetTextList = listOf(stringResource(R.string.visited_content_title_highlight, event)), - spanStyle = SusuTheme.typography.title_m.copy(Gray60).toSpanStyle(), - ) - Spacer( - modifier = modifier - .size(SusuTheme.spacing.spacing_xxl), - ) - Column( - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - ) { - visitedList.forEachIndexed { index, answer -> - if (selectedItem == index) { - SusuFilledButton( - color = FilledButtonColor.Orange, - style = MediumButtonStyle.height60, - text = answer, - onClick = { - selectedItem = index - }, - modifier = modifier.fillMaxWidth(), - ) - } else { - SusuGhostButton( - color = GhostButtonColor.Black, - style = MediumButtonStyle.height60, - text = answer, - onClick = { - selectedItem = index - }, - modifier = modifier.fillMaxWidth(), - ) - } - } - } - } -} - -@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) -@Composable -fun VisitedContentPreview() { - val visitedList = mutableListOf("예", "아니요") - - SusuTheme { - VisitedContent( - event = "결혼식", - visitedList = visitedList, - ) - } -} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/component/FriendListItem.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/component/FriendListItem.kt index 759a3213..0482afbe 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/component/FriendListItem.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/component/FriendListItem.kt @@ -14,15 +14,22 @@ import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.Gray60 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.susuClickable +import com.susu.core.ui.util.to_yyyy_dot_MM_dot_dd +import java.time.LocalDateTime @Composable fun FriendListItem( - friend: String, - modifier: Modifier = Modifier, + name: String, + relationship: String, + category: String?, + visitedAt: LocalDateTime?, + onClick: () -> Unit, ) { Row( - modifier = modifier + modifier = Modifier .fillMaxWidth() + .susuClickable(onClick = onClick) .padding( vertical = SusuTheme.spacing.spacing_s, ), @@ -30,22 +37,22 @@ fun FriendListItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = friend, + text = name, style = SusuTheme.typography.title_xs, color = Gray100, ) Text( - text = "친구", + text = relationship, style = SusuTheme.typography.title_xs, color = Gray60, ) Text( - text = "결혼식", + text = category ?: "", style = SusuTheme.typography.text_xs, color = Gray40, ) Text( - text = "2022.01.11", + text = visitedAt?.to_yyyy_dot_MM_dot_dd() ?: "", style = SusuTheme.typography.text_xs, color = Gray40, ) @@ -55,13 +62,15 @@ fun FriendListItem( @Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) @Composable fun FriendListItemPreview() { - val friendList = listOf("김철수", "국영수", "가나다") - SusuTheme { Column { - for (friend in friendList) { - FriendListItem(friend = friend) - } + FriendListItem( + name = "", + relationship = "", + category = null, + visitedAt = null, + onClick = {}, + ) } } } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContent.kt new file mode 100644 index 00000000..d162711f --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContent.kt @@ -0,0 +1,108 @@ +package com.susu.feature.received.envelopeadd.content.date + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.susu.core.designsystem.component.bottomsheet.datepicker.SusuDatePickerBottomSheet +import com.susu.core.designsystem.theme.Gray60 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.core.ui.util.AnnotatedText +import com.susu.core.ui.util.currentDate +import com.susu.feature.received.R +import com.susu.feature.received.ledgeradd.content.date.component.SelectDateRow +import java.time.LocalDateTime + +@Composable +fun DateContentRoute( + viewModel: DateViewModel = hiltViewModel(), + friendName: String, + updateParentDate: (LocalDateTime?) -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is DateSideEffect.UpdateParentDate -> updateParentDate(sideEffect.date) + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.updateName(friendName) + updateParentDate(uiState.date) + } + + DateContent( + uiState = uiState, + onDateItemSelected = viewModel::updateDate, + onClickDateText = viewModel::showDateBottomSheet, + onDismissDateBottomSheet = { year, month, day -> + viewModel.updateDate(year, month, day) + viewModel.hideDateBottomSheet() + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateContent( + uiState: DateState = DateState(), + onDateItemSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, + onClickDateText: () -> Unit = {}, + onDismissDateBottomSheet: (Int, Int, Int) -> Unit = { _, _, _ -> }, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), + ) { + AnnotatedText( + originalText = stringResource(R.string.envelope_date_content_title, uiState.name), + originalTextStyle = SusuTheme.typography.title_m, + targetTextList = listOf(stringResource(R.string.envelope_date_content_title_highlight, uiState.name)), + spanStyle = SusuTheme.typography.title_m.copy(Gray60).toSpanStyle(), + ) + Spacer( + modifier = Modifier + .size(SusuTheme.spacing.spacing_m), + ) + SelectDateRow( + year = uiState.date?.year, + month = uiState.date?.monthValue, + day = uiState.date?.dayOfMonth, + onClick = onClickDateText, + ) + } + + if (uiState.showDateBottomSheet) { + SusuDatePickerBottomSheet( + initialYear = uiState.date?.year ?: currentDate.year, + initialMonth = uiState.date?.monthValue ?: currentDate.monthValue, + initialDay = uiState.date?.dayOfMonth ?: currentDate.dayOfMonth, + maximumContainerHeight = 346.dp, + onDismissRequest = onDismissDateBottomSheet, + onItemSelected = onDateItemSelected, + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun DateContentPreview() { + SusuTheme { + DateContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContract.kt new file mode 100644 index 00000000..12e4b68b --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateContract.kt @@ -0,0 +1,15 @@ +package com.susu.feature.received.envelopeadd.content.date + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import java.time.LocalDateTime + +data class DateState( + val name: String = "", + val date: LocalDateTime? = null, + val showDateBottomSheet: Boolean = false, +) : UiState + +sealed interface DateSideEffect : SideEffect { + data class UpdateParentDate(val date: LocalDateTime?) : DateSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateViewModel.kt new file mode 100644 index 00000000..99fcbbe4 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/date/DateViewModel.kt @@ -0,0 +1,24 @@ +package com.susu.feature.received.envelopeadd.content.date + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDateTime +import javax.inject.Inject + +@HiltViewModel +class DateViewModel @Inject constructor() : BaseViewModel( + DateState(), +) { + fun updateName(name: String) = intent { copy(name = name) } + + fun updateDate(year: Int, month: Int, day: Int) = intent { + val toUpdateDate = LocalDateTime.of(year, month, day, 0, 0) + postSideEffect(DateSideEffect.UpdateParentDate(toUpdateDate)) + copy( + date = toUpdateDate, + ) + } + + fun showDateBottomSheet() = intent { copy(showDateBottomSheet = true) } + fun hideDateBottomSheet() = intent { copy(showDateBottomSheet = false) } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MemoContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoContent.kt similarity index 52% rename from feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MemoContent.kt rename to feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoContent.kt index a64a3c4c..964ecadb 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MemoContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoContent.kt @@ -1,7 +1,6 @@ -package com.susu.feature.received.envelopeadd.content +package com.susu.feature.received.envelopeadd.content.memo import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -9,33 +8,55 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.textfield.SusuBasicTextField import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.received.R @Composable -fun MemoContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), +fun MemoContentRoute( + viewModel: MemoViewModel = hiltViewModel(), + updateParentMemo: (String?) -> Unit, ) { - var memo by remember { mutableStateOf("") } + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is MemoSideEffect.UpdateParentMemo -> updateParentMemo(sideEffect.memo) + } + } + LaunchedEffect(key1 = Unit) { + viewModel.updateMemo(uiState.memo) + } + + MemoContent( + uiState = uiState, + onTextChangeMemo = viewModel::updateMemo, + ) +} + +@Composable +fun MemoContent( + uiState: MemoState = MemoState(), + onTextChangeMemo: (String) -> Unit = {}, +) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .padding(padding), + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), ) { Text( text = stringResource(R.string.memo_content_title), @@ -43,17 +64,17 @@ fun MemoContent( color = Gray100, ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_m), ) SusuBasicTextField( - text = memo, - onTextChange = { memo = it }, + text = uiState.memo, + onTextChange = onTextChangeMemo, placeholder = stringResource(R.string.memo_content_placeholder), placeholderColor = Gray40, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_xl)) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xl)) } } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoState.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoState.kt new file mode 100644 index 00000000..b9cc3215 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoState.kt @@ -0,0 +1,12 @@ +package com.susu.feature.received.envelopeadd.content.memo + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +data class MemoState( + val memo: String = "", +) : UiState + +sealed interface MemoSideEffect : SideEffect { + data class UpdateParentMemo(val memo: String?) : MemoSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoViewModel.kt new file mode 100644 index 00000000..a2e012e9 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/memo/MemoViewModel.kt @@ -0,0 +1,15 @@ +package com.susu.feature.received.envelopeadd.content.memo + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MemoViewModel @Inject constructor() : BaseViewModel( + MemoState(), +) { + fun updateMemo(memo: String?) = intent { + postSideEffect(MemoSideEffect.UpdateParentMemo(memo)) + copy(memo = memo ?: "") + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoneyContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContent.kt similarity index 63% rename from feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoneyContent.kt rename to feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContent.kt index 9637b13b..2e90910c 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoneyContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContent.kt @@ -1,54 +1,71 @@ -package com.susu.feature.received.envelopeadd.content +package com.susu.feature.received.envelopeadd.content.money import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.button.FilledButtonColor import com.susu.core.designsystem.component.button.SmallButtonStyle import com.susu.core.designsystem.component.button.SusuFilledButton import com.susu.core.designsystem.component.textfield.SusuPriceTextField import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.core.ui.extension.toMoneyFormat +import com.susu.core.ui.moneyList import com.susu.feature.received.R -@OptIn(ExperimentalLayoutApi::class) @Composable -fun MoneyContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), +fun MoneyContentRoute( + viewModel: MoneyViewModel = hiltViewModel(), + updateParentMoney: (Long) -> Unit, ) { - val moneyList = listOf( - 10000, - 30000, - 50000, - 100000, - 500000, + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is MoneySideEffect.UpdateParentMoney -> updateParentMoney(sideEffect.money) + } + } + + LaunchedEffect(key1 = Unit) { + updateParentMoney(uiState.money.toLongOrNull() ?: 0) + } + + MoneyContent( + uiState = uiState, + onTextChangeMoney = viewModel::updateMoney, + onClickMoneyButton = viewModel::addMoney, ) - var clickedMoney by remember { mutableStateOf("") } +} +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun MoneyContent( + uiState: MoneyState = MoneyState(), + onTextChangeMoney: (String) -> Unit = {}, + onClickMoneyButton: (Int) -> Unit = {}, +) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .padding(padding), + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), ) { Text( text = stringResource(R.string.money_content_title), @@ -56,16 +73,16 @@ fun MoneyContent( color = Gray100, ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_m), ) SusuPriceTextField( - text = clickedMoney, - onTextChange = { clickedMoney = it }, + text = uiState.money, + onTextChange = onTextChangeMoney, placeholder = stringResource(R.string.money_content_placeholder), ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_xxl), ) FlowRow( @@ -78,7 +95,7 @@ fun MoneyContent( style = SmallButtonStyle.height32, text = stringResource(id = com.susu.core.ui.R.string.money_unit_format, money.toMoneyFormat()), onClick = { - clickedMoney = money.toString() + onClickMoneyButton(money) }, ) } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContract.kt new file mode 100644 index 00000000..d1ae6828 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyContract.kt @@ -0,0 +1,12 @@ +package com.susu.feature.received.envelopeadd.content.money + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +data class MoneyState( + val money: String = "", +) : UiState + +sealed interface MoneySideEffect : SideEffect { + data class UpdateParentMoney(val money: Long) : MoneySideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyViewModel.kt new file mode 100644 index 00000000..3061286d --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/money/MoneyViewModel.kt @@ -0,0 +1,24 @@ +package com.susu.feature.received.envelopeadd.content.money + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MoneyViewModel @Inject constructor() : BaseViewModel( + MoneyState(), +) { + fun updateMoney(money: String) = intent { + postSideEffect(MoneySideEffect.UpdateParentMoney(money.toLongOrNull() ?: 0)) + copy(money = money) + } + + fun addMoney(money: Int) = intent { + val currentMoney = this.money.toLongOrNull() ?: 0 + val addedMoney = money + currentMoney + postSideEffect(MoneySideEffect.UpdateParentMoney(addedMoney)) + copy( + money = addedMoney.toString(), + ) + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoreContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContent.kt similarity index 62% rename from feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoreContent.kt rename to feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContent.kt index 92cbddc0..83dedfee 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/MoreContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContent.kt @@ -1,8 +1,7 @@ -package com.susu.feature.received.envelopeadd.content +package com.susu.feature.received.envelopeadd.content.more import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -13,12 +12,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.button.FilledButtonColor import com.susu.core.designsystem.component.button.GhostButtonColor import com.susu.core.designsystem.component.button.MediumButtonStyle @@ -27,24 +26,42 @@ import com.susu.core.designsystem.component.button.SusuGhostButton import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray70 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.received.R +import com.susu.feature.received.envelopeadd.EnvelopeAddStep + +@Composable +fun MoreContentRoute( + viewModel: MoreViewModel = hiltViewModel(), + updateParentMoreStep: (List) -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is MoreSideEffect.UpdateParentMoreStep -> updateParentMoreStep(sideEffect.moreStep) + } + } + + MoreContent( + uiState = uiState, + onClickStepButton = viewModel::toggleStep, + ) +} @Composable fun MoreContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), - moreList: List, + uiState: MoreState = MoreState(), + onClickStepButton: (EnvelopeAddStep) -> Unit = {}, ) { val scrollState = rememberScrollState() - var selectedItem by remember { mutableStateOf(-1) } Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .padding(padding) + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ) .verticalScroll(scrollState), ) { Text( @@ -53,7 +70,7 @@ fun MoreContent( color = Gray100, ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_xxxxs), ) Text( @@ -62,32 +79,33 @@ fun MoreContent( color = Gray70, ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_xxl), ) Column( verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { - moreList.forEachIndexed { index, category -> - if (selectedItem == index) { + moreStep.forEach { step -> + if (step in uiState.selectedMoreStop) { SusuFilledButton( color = FilledButtonColor.Orange, style = MediumButtonStyle.height60, - text = category, + text = stringResource(id = step.stringResId!!), onClick = { - selectedItem = index + onClickStepButton(step) }, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) } else { SusuGhostButton( color = GhostButtonColor.Black, style = MediumButtonStyle.height60, - text = category, + text = stringResource(id = step.stringResId!!), + rippleEnabled = false, onClick = { - selectedItem = index + onClickStepButton(step) }, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) } } @@ -98,9 +116,7 @@ fun MoreContent( @Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) @Composable fun MoreContentPreview() { - val moreList = mutableListOf("방문여부", "선물", "메모", "보낸 이의 연락처") - SusuTheme { - MoreContent(moreList = moreList) + MoreContent() } } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContract.kt new file mode 100644 index 00000000..a2e04e09 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreContract.kt @@ -0,0 +1,22 @@ +package com.susu.feature.received.envelopeadd.content.more + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import com.susu.feature.received.envelopeadd.EnvelopeAddStep +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class MoreState( + val selectedMoreStop: PersistentList = persistentListOf(), +) : UiState + +val moreStep = persistentListOf( + EnvelopeAddStep.VISITED, + EnvelopeAddStep.PRESENT, + EnvelopeAddStep.MEMO, + EnvelopeAddStep.PHONE, +) + +sealed interface MoreSideEffect : SideEffect { + data class UpdateParentMoreStep(val moreStep: List) : MoreSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreViewModel.kt new file mode 100644 index 00000000..9d8a4139 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/more/MoreViewModel.kt @@ -0,0 +1,25 @@ +package com.susu.feature.received.envelopeadd.content.more + +import com.susu.core.ui.base.BaseViewModel +import com.susu.feature.received.envelopeadd.EnvelopeAddStep +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import javax.inject.Inject + +@HiltViewModel +class MoreViewModel @Inject constructor() : BaseViewModel( + MoreState(), +) { + fun toggleStep(envelopeAddStep: EnvelopeAddStep) = intent { + val newStep = if (envelopeAddStep in selectedMoreStop) { + selectedMoreStop.minus(envelopeAddStep) + } else { + selectedMoreStop.plus(envelopeAddStep) + } + + postSideEffect(MoreSideEffect.UpdateParentMoreStep(newStep.sortedBy { it.ordinal })) + copy( + selectedMoreStop = newStep.toPersistentList(), + ) + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContent.kt new file mode 100644 index 00000000..3a9adfc2 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContent.kt @@ -0,0 +1,135 @@ +package com.susu.feature.received.envelopeadd.content.name + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.susu.core.designsystem.component.textfield.SusuBasicTextField +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.FriendSearch +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.feature.received.R +import com.susu.feature.received.envelopeadd.content.component.FriendListItem +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce + +@OptIn(FlowPreview::class) +@Composable +fun NameContentRoute( + viewModel: NameViewModel = hiltViewModel(), + updateParentName: (String) -> Unit, + updateParentFriendId: (Long?) -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is NameSideEffect.UpdateParentName -> updateParentName(sideEffect.name) + is NameSideEffect.UpdateParentFriendId -> updateParentFriendId(sideEffect.friendId) + NameSideEffect.FocusClear -> focusManager.clearFocus() + } + } + + LaunchedEffect(key1 = Unit) { + updateParentName(uiState.name) + } + + LaunchedEffect(key1 = uiState.name) { + snapshotFlow { uiState.name } + .debounce(100L) + .collect(viewModel::getFriendList) + } + + NameContent( + uiState = uiState, + focusRequester = focusRequester, + onTextChangeName = viewModel::updateName, + onClickFriendItem = viewModel::selectFriend, + ) +} + +@Composable +fun NameContent( + uiState: NameState = NameState(), + focusRequester: FocusRequester = remember { FocusRequester() }, + onTextChangeName: (String) -> Unit = {}, + onClickFriendItem: (FriendSearch) -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), + ) { + Text( + text = stringResource(R.string.name_content_title), + style = SusuTheme.typography.title_m, + color = Gray100, + ) + Spacer( + modifier = Modifier + .size(SusuTheme.spacing.spacing_m), + ) + SusuBasicTextField( + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + text = uiState.name, + onTextChange = onTextChangeName, + placeholder = stringResource(R.string.name_content_placeholder), + placeholderColor = Gray40, + ) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xl)) + + if (uiState.friendList.isNotEmpty() && uiState.isSelectedFriend.not()) { + LazyColumn( + modifier = Modifier.height(208.dp), + ) { + items( + items = uiState.friendList, + key = { it.friend.id }, + ) { + FriendListItem( + name = it.friend.name, + relationship = it.relationship.customRelation ?: it.relationship.relation, + category = it.recentEnvelope?.category, + visitedAt = it.recentEnvelope?.handedOverAt, + onClick = { onClickFriendItem(it) }, + ) + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun NameContentPreview() { + SusuTheme { + NameContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContract.kt new file mode 100644 index 00000000..ec2f4108 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameContract.kt @@ -0,0 +1,19 @@ +package com.susu.feature.received.envelopeadd.content.name + +import com.susu.core.model.FriendSearch +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class NameState( + val name: String = "", + val isSelectedFriend: Boolean = false, + val friendList: PersistentList = persistentListOf(), +) : UiState + +sealed interface NameSideEffect : SideEffect { + data class UpdateParentName(val name: String) : NameSideEffect + data class UpdateParentFriendId(val friendId: Long?) : NameSideEffect + data object FocusClear : NameSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameViewModel.kt new file mode 100644 index 00000000..73b726c1 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/name/NameViewModel.kt @@ -0,0 +1,56 @@ +package com.susu.feature.received.envelopeadd.content.name + +import androidx.lifecycle.viewModelScope +import com.susu.core.model.FriendSearch +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.friend.SearchFriendUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NameViewModel @Inject constructor( + private val searchFriendUseCase: SearchFriendUseCase, +) : BaseViewModel( + NameState(), +) { + fun updateName(name: String) = intent { + postSideEffect( + NameSideEffect.UpdateParentName(name), + NameSideEffect.UpdateParentFriendId(null), + ) + copy( + name = name, + friendList = if (name.isEmpty()) persistentListOf() else friendList, + isSelectedFriend = false, + ) + } + + fun selectFriend(friend: FriendSearch) = intent { + postSideEffect( + NameSideEffect.FocusClear, + NameSideEffect.UpdateParentName(friend.friend.name), + NameSideEffect.UpdateParentFriendId(friend.friend.id), + ) + copy( + name = friend.friend.name, + isSelectedFriend = true, + ) + } + + fun getFriendList(search: String) = viewModelScope.launch { + if (search.isEmpty()) return@launch + if (currentState.isSelectedFriend) return@launch + + searchFriendUseCase(name = search) + .onSuccess { + intent { + copy( + friendList = it.toPersistentList(), + ) + } + } + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PhoneContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContent.kt similarity index 53% rename from feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PhoneContent.kt rename to feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContent.kt index 191d349d..92632116 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PhoneContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContent.kt @@ -1,7 +1,6 @@ -package com.susu.feature.received.envelopeadd.content +package com.susu.feature.received.envelopeadd.content.phone import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -9,56 +8,77 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.textfield.SusuBasicTextField import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.Gray60 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.core.ui.util.AnnotatedText import com.susu.feature.received.R @Composable -fun PhoneContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), - name: String, +fun PhoneContentRoute( + viewModel: PhoneViewModel = hiltViewModel(), + friendName: String, + updateParentPhone: (String?) -> Unit, ) { - var phoneNumber by remember { mutableStateOf("") } + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is PhoneSideEffect.UpdateParentPhone -> updateParentPhone(sideEffect.phone) + } + } + LaunchedEffect(key1 = Unit) { + viewModel.updateName(friendName) + viewModel.updatePhone(uiState.phone) + } + + PhoneContent( + uiState = uiState, + onTextChangePhone = viewModel::updatePhone, + ) +} + +@Composable +fun PhoneContent( + uiState: PhoneState = PhoneState(), + onTextChangePhone: (String) -> Unit = {}, +) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .padding(padding), + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), ) { AnnotatedText( - originalText = stringResource(R.string.phone_content_title, name), + originalText = stringResource(R.string.phone_content_title, uiState.name), originalTextStyle = SusuTheme.typography.title_m, - targetTextList = listOf(stringResource(R.string.phone_content_title_highlight, name)), + targetTextList = listOf(stringResource(R.string.phone_content_title_highlight, uiState.name)), spanStyle = SusuTheme.typography.title_m.copy(Gray60).toSpanStyle(), ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_m), ) SusuBasicTextField( - text = phoneNumber, - onTextChange = { phoneNumber = it }, + text = uiState.phone, + onTextChange = onTextChangePhone, placeholder = stringResource(R.string.phone_content_placeholder), placeholderColor = Gray40, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), ) - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_xl)) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xl)) } } @@ -66,6 +86,6 @@ fun PhoneContent( @Composable fun PhoneContentPreview() { SusuTheme { - PhoneContent(name = "김철수") + PhoneContent() } } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContract.kt new file mode 100644 index 00000000..e3062997 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneContract.kt @@ -0,0 +1,13 @@ +package com.susu.feature.received.envelopeadd.content.phone + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +data class PhoneState( + val phone: String = "", + val name: String = "", +) : UiState + +sealed interface PhoneSideEffect : SideEffect { + data class UpdateParentPhone(val phone: String?) : PhoneSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneViewModel.kt new file mode 100644 index 00000000..d262e12f --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/phone/PhoneViewModel.kt @@ -0,0 +1,16 @@ +package com.susu.feature.received.envelopeadd.content.phone + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class PhoneViewModel @Inject constructor() : BaseViewModel( + PhoneState(), +) { + fun updateName(name: String) = intent { copy(name = name) } + fun updatePhone(phone: String?) = intent { + postSideEffect(PhoneSideEffect.UpdateParentPhone(phone)) + copy(phone = phone ?: "") + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PresentContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContent.kt similarity index 52% rename from feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PresentContent.kt rename to feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContent.kt index b18e7cf3..a1830e60 100644 --- a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/PresentContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContent.kt @@ -1,7 +1,6 @@ -package com.susu.feature.received.envelopeadd.content +package com.susu.feature.received.envelopeadd.content.present import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -9,33 +8,55 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.textfield.SusuBasicTextField import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.received.R @Composable -fun PresentContent( - modifier: Modifier = Modifier, - padding: PaddingValues = PaddingValues( - horizontal = SusuTheme.spacing.spacing_m, - vertical = SusuTheme.spacing.spacing_xl, - ), +fun PresentContentRoute( + viewModel: PresentViewModel = hiltViewModel(), + updateParentPresent: (String?) -> Unit, ) { - var sentPresent by remember { mutableStateOf("") } + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is PresentSideEffect.UpdateParentPresent -> updateParentPresent(sideEffect.present) + } + } + LaunchedEffect(key1 = Unit) { + viewModel.updatePresent(uiState.present) + } + + PresentContent( + uiState = uiState, + onTextChangeName = viewModel::updatePresent, + ) +} + +@Composable +fun PresentContent( + uiState: PresentState = PresentState(), + onTextChangeName: (String) -> Unit = {}, +) { Column( - modifier = modifier + modifier = Modifier .fillMaxSize() - .padding(padding), + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), ) { Text( text = stringResource(R.string.present_content_title), @@ -43,17 +64,17 @@ fun PresentContent( color = Gray100, ) Spacer( - modifier = modifier + modifier = Modifier .size(SusuTheme.spacing.spacing_m), ) SusuBasicTextField( - text = sentPresent, - onTextChange = { sentPresent = it }, + text = uiState.present, + onTextChange = onTextChangeName, placeholder = stringResource(R.string.present_content_placeholder), placeholderColor = Gray40, - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) - Spacer(modifier = modifier.size(SusuTheme.spacing.spacing_xl)) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xl)) } } diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContract.kt new file mode 100644 index 00000000..62b092e5 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentContract.kt @@ -0,0 +1,12 @@ +package com.susu.feature.received.envelopeadd.content.present + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +data class PresentState( + val present: String = "", +) : UiState + +sealed interface PresentSideEffect : SideEffect { + data class UpdateParentPresent(val present: String?) : PresentSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentViewModel.kt new file mode 100644 index 00000000..828e65be --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/present/PresentViewModel.kt @@ -0,0 +1,15 @@ +package com.susu.feature.received.envelopeadd.content.present + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class PresentViewModel @Inject constructor() : BaseViewModel( + PresentState(), +) { + fun updatePresent(present: String?) = intent { + postSideEffect(PresentSideEffect.UpdateParentPresent(present)) + copy(present = present ?: "") + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContent.kt new file mode 100644 index 00000000..0d05d788 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContent.kt @@ -0,0 +1,179 @@ +package com.susu.feature.received.envelopeadd.content.relationship + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.GhostButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.component.button.SusuGhostButton +import com.susu.core.designsystem.component.textfieldbutton.SusuTextFieldFillMaxButton +import com.susu.core.designsystem.component.textfieldbutton.TextFieldButtonColor +import com.susu.core.designsystem.component.textfieldbutton.style.MediumTextFieldButtonStyle +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Relationship +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.feature.received.R +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.launch + +@Composable +fun RelationShipContentRoute( + viewModel: RelationShipViewModel = hiltViewModel(), + updateParentSelectedRelation: (Relationship?) -> Unit = {}, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + val focusRequester = remember { FocusRequester() } + val scope = rememberCoroutineScope() + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is RelationShipSideEffect.UpdateParentSelectedRelationShip -> { + updateParentSelectedRelation(sideEffect.relationShip) + } + + RelationShipSideEffect.FocusCustomRelationShip -> scope.launch { + awaitFrame() + focusRequester.requestFocus() + } + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.getRelationShipConfig() + } + + LaunchedEffect( + key1 = uiState.selectedRelationship, + key2 = uiState.isSavedCustomRelationShip, + ) { + snapshotFlow { uiState.selectedRelationship } + .collect { + viewModel.updateParentSelectedRelationShip() + } + } + + RelationShipContent( + uiState = uiState, + focusRequester = focusRequester, + onClickRelationShipButton = viewModel::selectRelationShip, + onClickCustomRelationShipButton = viewModel::showCustomRelationShipTextField, + onClickCustomRelationShipTextFieldCloseIcon = viewModel::hideCustomRelationShipTextField, + onClickCustomRelationShipTextField = viewModel::selectCustomRelationShip, + onClickCustomRelationShipTextFieldClearIcon = { viewModel.updateCustomRelationShipText("") }, + onTextChangeCustomRelationShipTextField = viewModel::updateCustomRelationShipText, + onClickTextFieldInnerButton = viewModel::toggleTextFieldSaved, + ) +} + +@Composable +fun RelationShipContent( + uiState: RelationShipState = RelationShipState(), + focusRequester: FocusRequester = remember { FocusRequester() }, + onClickRelationShipButton: (Relationship) -> Unit = {}, + onClickCustomRelationShipButton: () -> Unit = {}, + onClickCustomRelationShipTextFieldCloseIcon: () -> Unit = {}, + onClickCustomRelationShipTextField: () -> Unit = {}, + onClickCustomRelationShipTextFieldClearIcon: () -> Unit = {}, + onTextChangeCustomRelationShipTextField: (String) -> Unit = {}, + onClickTextFieldInnerButton: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ) + .verticalScroll(scrollState), + ) { + Text( + text = stringResource(R.string.relationship_content_title), + style = SusuTheme.typography.title_m, + color = Gray100, + ) + Spacer( + modifier = Modifier + .size(SusuTheme.spacing.spacing_xxl), + ) + Column( + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + uiState.relationshipConfig.forEach { relationship -> + if (relationship == uiState.selectedRelationship) { + SusuFilledButton( + color = FilledButtonColor.Orange, + style = MediumButtonStyle.height60, + text = relationship.relation, + modifier = Modifier.fillMaxWidth(), + ) + } else { + SusuGhostButton( + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = relationship.relation, + onClick = { + onClickRelationShipButton(relationship) + }, + rippleEnabled = false, + modifier = Modifier.fillMaxWidth(), + ) + } + } + if (uiState.showTextFieldButton) { + SusuTextFieldFillMaxButton( + color = if (uiState.isCustomRelationShipSelected) TextFieldButtonColor.Orange else TextFieldButtonColor.Black, + text = uiState.customRelationship.customRelation ?: "", + onTextChange = onTextChangeCustomRelationShipTextField, + focusRequester = focusRequester, + style = MediumTextFieldButtonStyle.height60, + isSaved = uiState.isSavedCustomRelationShip, + isFocused = uiState.customRelationship == uiState.selectedRelationship, + placeholder = stringResource(com.susu.core.ui.R.string.word_input_placeholder), + onClickCloseIcon = onClickCustomRelationShipTextFieldCloseIcon, + onClickClearIcon = onClickCustomRelationShipTextFieldClearIcon, + onClickButton = { onClickCustomRelationShipTextField() }, + onClickFilledButton = onClickTextFieldInnerButton, + ) + } else { + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(com.susu.core.ui.R.string.word_input_yourself), + rippleEnabled = false, + onClick = onClickCustomRelationShipButton, + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun RelationshipContentPreview() { + SusuTheme { + RelationShipContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContract.kt new file mode 100644 index 00000000..6cad6733 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipContract.kt @@ -0,0 +1,22 @@ +package com.susu.feature.received.envelopeadd.content.relationship + +import com.susu.core.model.Relationship +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class RelationShipState( + val selectedRelationship: Relationship? = null, + val relationshipConfig: PersistentList = persistentListOf(), + val customRelationship: Relationship = Relationship(), + val showTextFieldButton: Boolean = false, + val isSavedCustomRelationShip: Boolean = false, +) : UiState { + val isCustomRelationShipSelected = customRelationship == selectedRelationship +} + +sealed interface RelationShipSideEffect : SideEffect { + data object FocusCustomRelationShip : RelationShipSideEffect + data class UpdateParentSelectedRelationShip(val relationShip: Relationship?) : RelationShipSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipViewModel.kt new file mode 100644 index 00000000..e1e3daaf --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/relationship/RelationShipViewModel.kt @@ -0,0 +1,86 @@ +package com.susu.feature.received.envelopeadd.content.relationship + +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Relationship +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.envelope.GetRelationShipConfigListUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RelationShipViewModel @Inject constructor( + private val getRelationShipConfigUseCase: GetRelationShipConfigListUseCase, +) : BaseViewModel( + RelationShipState(), +) { + private val parentSelectedRelationShip + get() = with(currentState) { + if (selectedRelationship == customRelationship && + ( + customRelationship.customRelation.isNullOrEmpty() || + isSavedCustomRelationShip.not() + ) + ) { + null + } else { + selectedRelationship + } + } + + fun getRelationShipConfig() = viewModelScope.launch { + if (currentState.relationshipConfig.isNotEmpty()) return@launch + + getRelationShipConfigUseCase() + .onSuccess { + intent { + copy( + relationshipConfig = it.dropLast(1).toPersistentList(), + customRelationship = it.last(), + ) + } + } + .onFailure { } + } + + fun selectRelationShip(relationShip: Relationship) = intent { copy(selectedRelationship = relationShip) } + + fun selectCustomRelationShip() = intent { + postSideEffect(RelationShipSideEffect.FocusCustomRelationShip) + copy(selectedRelationship = customRelationship) + } + + fun updateCustomRelationShipText(text: String) = intent { + copy( + selectedRelationship = customRelationship.copy(customRelation = text), + customRelationship = customRelationship.copy(customRelation = text), + ) + } + + fun showCustomRelationShipTextField() = intent { + copy( + showTextFieldButton = true, + selectedRelationship = customRelationship, + ) + } + + fun hideCustomRelationShipTextField() = intent { + copy( + isSavedCustomRelationShip = false, + showTextFieldButton = false, + selectedRelationship = if (isCustomRelationShipSelected) null else selectedRelationship, + customRelationship = customRelationship.copy(customRelation = ""), + ) + } + + fun toggleTextFieldSaved() = intent { + copy( + isSavedCustomRelationShip = !isSavedCustomRelationShip, + ) + } + + fun updateParentSelectedRelationShip(relationShip: Relationship? = parentSelectedRelationShip) = postSideEffect( + RelationShipSideEffect.UpdateParentSelectedRelationShip(relationShip), + ) +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContent.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContent.kt new file mode 100644 index 00000000..399425c1 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContent.kt @@ -0,0 +1,125 @@ +package com.susu.feature.received.envelopeadd.content.visited + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.GhostButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.component.button.SusuGhostButton +import com.susu.core.designsystem.theme.Gray60 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.core.ui.util.AnnotatedText +import com.susu.feature.received.R + +@Composable +fun VisitedContentRoute( + viewModel: VisitedViewModel = hiltViewModel(), + categoryName: String, + updateParentVisited: (Boolean?) -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is VisitedSideEffect.UpdateParentVisited -> updateParentVisited(sideEffect.visited) + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.updateCategoryName(categoryName) + viewModel.updateVisited(uiState.visited) + } + + VisitedContent( + uiState = uiState, + onClickVisitedButton = { viewModel.updateVisited(true) }, + onClickNotVisitedButton = { viewModel.updateVisited(false) }, + ) +} + +@Composable +fun VisitedContent( + uiState: VisitedState = VisitedState(), + onClickVisitedButton: () -> Unit = {}, + onClickNotVisitedButton: () -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xl, + ), + ) { + AnnotatedText( + originalText = stringResource(R.string.visited_content_title, uiState.categoryName), + originalTextStyle = SusuTheme.typography.title_m, + targetTextList = listOf(stringResource(R.string.visited_content_title_highlight, uiState.categoryName)), + spanStyle = SusuTheme.typography.title_m.copy(Gray60).toSpanStyle(), + ) + Spacer( + modifier = Modifier + .size(SusuTheme.spacing.spacing_xxl), + ) + Column( + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + if (uiState.visited == true) { + SusuFilledButton( + color = FilledButtonColor.Orange, + style = MediumButtonStyle.height60, + text = stringResource(id = com.susu.core.ui.R.string.word_yes), + modifier = Modifier.fillMaxWidth(), + ) + } else { + SusuGhostButton( + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(id = com.susu.core.ui.R.string.word_yes), + onClick = onClickVisitedButton, + modifier = Modifier.fillMaxWidth(), + ) + } + + if (uiState.visited == false) { + SusuFilledButton( + color = FilledButtonColor.Orange, + style = MediumButtonStyle.height60, + text = stringResource(id = com.susu.core.ui.R.string.word_no), + modifier = Modifier.fillMaxWidth(), + ) + } else { + SusuGhostButton( + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(id = com.susu.core.ui.R.string.word_no), + onClick = onClickNotVisitedButton, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun VisitedContentPreview() { + SusuTheme { + VisitedContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContract.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContract.kt new file mode 100644 index 00000000..c5b1788c --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedContract.kt @@ -0,0 +1,13 @@ +package com.susu.feature.received.envelopeadd.content.visited + +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +data class VisitedState( + val categoryName: String = "", + val visited: Boolean? = null, +) : UiState + +sealed interface VisitedSideEffect : SideEffect { + data class UpdateParentVisited(val visited: Boolean?) : VisitedSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedViewModel.kt new file mode 100644 index 00000000..cd7bec9e --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/envelopeadd/content/visited/VisitedViewModel.kt @@ -0,0 +1,19 @@ +package com.susu.feature.received.envelopeadd.content.visited + +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class VisitedViewModel @Inject constructor() : BaseViewModel( + VisitedState(), +) { + fun updateCategoryName(name: String) = intent { + copy(categoryName = name) + } + + fun updateVisited(visited: Boolean?) = intent { + postSideEffect(VisitedSideEffect.UpdateParentVisited(visited)) + copy(visited = visited) + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddViewModel.kt index 200fa96b..3e795b65 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddViewModel.kt @@ -19,10 +19,8 @@ class LedgerAddViewModel @Inject constructor( ) : BaseViewModel( LedgerAddState(), ) { - var selectedCategory: Category? = null - private set - var name: String = "" - private set + private var selectedCategory: Category? = null + private var name: String = "" private var startAt: LocalDateTime? = null private var endAt: LocalDateTime? = null diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/date/DateContent.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/date/DateContent.kt index b5be883f..e997f823 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/date/DateContent.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/date/DateContent.kt @@ -57,10 +57,16 @@ fun DateContentRoute( uiState = uiState, onStartDateItemSelected = viewModel::updateStartDate, onClickStartDateText = viewModel::showStartDateBottomSheet, - onDismissStartDateBottomSheet = viewModel::hideStartDateBottomSheet, + onDismissStartDateBottomSheet = { year, month, day -> + viewModel.updateStartDate(year, month, day) + viewModel.hideStartDateBottomSheet() + }, onEndDateItemSelected = viewModel::updateEndDate, onClickEndDateText = viewModel::showEndDateBottomSheet, - onDismissEndDateBottomSheet = viewModel::hideEndDateBottomSheet, + onDismissEndDateBottomSheet = { year, month, day -> + viewModel.updateEndDate(year, month, day) + viewModel.hideEndDateBottomSheet() + }, onClickSetDateButton = viewModel::toggleShowOnlyStartAt, ) } @@ -71,10 +77,10 @@ fun DateContent( uiState: DateState = DateState(), onStartDateItemSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, onClickStartDateText: () -> Unit = {}, - onDismissStartDateBottomSheet: () -> Unit = {}, + onDismissStartDateBottomSheet: (Int, Int, Int) -> Unit = { _, _, _ -> }, onEndDateItemSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, onClickEndDateText: () -> Unit = {}, - onDismissEndDateBottomSheet: () -> Unit = {}, + onDismissEndDateBottomSheet: (Int, Int, Int) -> Unit = { _, _, _ -> }, onClickSetDateButton: () -> Unit = {}, ) { Column( @@ -146,7 +152,7 @@ fun DateContent( initialCriteriaDay = uiState.endAt?.dayOfMonth, afterDate = false, maximumContainerHeight = 346.dp, - onDismissRequest = { _, _, _ -> onDismissStartDateBottomSheet() }, + onDismissRequest = onDismissStartDateBottomSheet, onItemSelected = onStartDateItemSelected, ) } @@ -161,7 +167,7 @@ fun DateContent( initialCriteriaDay = uiState.startAt?.dayOfMonth ?: minDate.dayOfMonth, afterDate = true, maximumContainerHeight = 346.dp, - onDismissRequest = { _, _, _ -> onDismissEndDateBottomSheet() }, + onDismissRequest = onDismissEndDateBottomSheet, onItemSelected = onEndDateItemSelected, ) } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt index 4bfd6851..c8802a15 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt @@ -14,11 +14,11 @@ data class LedgerDetailState( ) : UiState sealed interface LedgerDetailSideEffect : SideEffect { - data object NavigateEnvelopeAdd : LedgerDetailSideEffect + data class NavigateEnvelopeAdd(val categoryName: String, val ledgerId: Long) : LedgerDetailSideEffect data object NavigateEnvelopeDetail : LedgerDetailSideEffect data class NavigateLedgerEdit(val ledger: Ledger) : LedgerDetailSideEffect data class PopBackStackWithLedger(val ledger: String) : LedgerDetailSideEffect - data class PopBackStackWithDeleteLedgerId(val ledgerId: Int) : LedgerDetailSideEffect + data class PopBackStackWithDeleteLedgerId(val ledgerId: Long) : LedgerDetailSideEffect data class ShowDeleteDialog(val onConfirmRequest: () -> Unit) : LedgerDetailSideEffect data object ShowDeleteSuccessSnackbar : LedgerDetailSideEffect data class ShowSnackbar(val msg: String) : LedgerDetailSideEffect diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt index d6dcd1ba..d61f09a9 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt @@ -52,10 +52,10 @@ fun LedgerDetailRoute( viewModel: LedgerDetailViewModel = hiltViewModel(), ledger: String?, navigateLedgerEdit: (Ledger) -> Unit, - navigateEnvelopAdd: () -> Unit, + navigateEnvelopAdd: (String, Long) -> Unit, navigateEnvelopeDetail: () -> Unit, popBackStackWithLedger: (String) -> Unit, - popBackStackWithDeleteLedgerId: (Int) -> Unit, + popBackStackWithDeleteLedgerId: (Long) -> Unit, onShowSnackbar: (SnackbarToken) -> Unit, onShowDialog: (DialogToken) -> Unit, handleException: (Throwable, () -> Unit) -> Unit, @@ -89,7 +89,7 @@ fun LedgerDetailRoute( is LedgerDetailSideEffect.PopBackStackWithDeleteLedgerId -> popBackStackWithDeleteLedgerId(sideEffect.ledgerId) is LedgerDetailSideEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) is LedgerDetailSideEffect.ShowSnackbar -> onShowSnackbar(SnackbarToken(message = sideEffect.msg)) - LedgerDetailSideEffect.NavigateEnvelopeAdd -> navigateEnvelopAdd() + is LedgerDetailSideEffect.NavigateEnvelopeAdd -> navigateEnvelopAdd(sideEffect.categoryName, sideEffect.ledgerId) LedgerDetailSideEffect.NavigateEnvelopeDetail -> navigateEnvelopeDetail() } } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt index 38b72ab2..9613c872 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt @@ -81,6 +81,8 @@ class LedgerDetailViewModel @Inject constructor( } } - fun navigateEnvelopeAdd() = postSideEffect(LedgerDetailSideEffect.NavigateEnvelopeAdd) + fun navigateEnvelopeAdd() = postSideEffect( + LedgerDetailSideEffect.NavigateEnvelopeAdd(ledger.category.customCategory ?: ledger.category.name, ledger.id), + ) fun navigateEnvelopeDetail() = postSideEffect(LedgerDetailSideEffect.NavigateEnvelopeDetail) } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt index 40a7a23f..30f3486c 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt @@ -28,7 +28,7 @@ class LedgerEditViewModel @Inject constructor( LedgerEditState(), ) { private val argument = savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME)!! - private var ledgerId = 0 + private var ledgerId = 0L private val toEditLedger get() = with(currentState) { Ledger( diff --git a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt index 5419250b..c8fa48da 100644 --- a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt +++ b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt @@ -47,8 +47,8 @@ fun NavController.navigateLedgerAdd() { navigate(ReceivedRoute.ledgerAddRoute) } -fun NavController.navigateReceivedEnvelopeAdd() { - navigate(ReceivedRoute.envelopeAddRoute) +fun NavController.navigateReceivedEnvelopeAdd(categoryName: String, ledgerId: Long) { + navigate(ReceivedRoute.envelopeAddRoute(categoryName, ledgerId.toString())) } fun NavController.navigateReceivedEnvelopeDetail() { @@ -59,18 +59,19 @@ fun NavController.navigateReceivedEnvelopeEdit() { navigate(ReceivedRoute.envelopeEditRoute) } +@Suppress("detekt:LongMethod") fun NavGraphBuilder.receivedNavGraph( padding: PaddingValues, navigateLedgerDetail: (Ledger) -> Unit, popBackStack: () -> Unit, popBackStackWithLedger: (String) -> Unit, - popBackStackWithDeleteLedgerId: (Int) -> Unit, + popBackStackWithDeleteLedgerId: (Long) -> Unit, popBackStackWithFilter: (String) -> Unit, navigateLedgerSearch: () -> Unit, navigateLedgerEdit: (Ledger) -> Unit, navigateLedgerFilter: (FilterArgument) -> Unit, navigateLedgerAdd: () -> Unit, - navigateEnvelopAdd: () -> Unit, + navigateEnvelopAdd: (String, Long) -> Unit, navigateEnvelopeDetail: () -> Unit, navigateEnvelopeEdit: () -> Unit, onShowSnackbar: (SnackbarToken) -> Unit, @@ -79,8 +80,9 @@ fun NavGraphBuilder.receivedNavGraph( ) { composable(route = ReceivedRoute.route) { navBackStackEntry -> val ledger = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME) - val toDeleteLedgerId = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ID_ARGUMENT_NAME) ?: -1 + val toDeleteLedgerId = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ID_ARGUMENT_NAME) ?: -1 val filter = navBackStackEntry.savedStateHandle.get(ReceivedRoute.FILTER_ARGUMENT_NAME) + navBackStackEntry.savedStateHandle.set(ReceivedRoute.FILTER_ARGUMENT_NAME, null) ReceivedRoute( ledger = ledger, toDeleteLedgerId = toDeleteLedgerId, @@ -151,10 +153,19 @@ fun NavGraphBuilder.receivedNavGraph( } composable( - route = ReceivedRoute.envelopeAddRoute, + route = ReceivedRoute.envelopeAddRoute( + categoryName = "{${ReceivedRoute.CATEGORY_ARGUMENT_NAME}}", + ledgerId = "{${ReceivedRoute.LEDGER_ID_ARGUMENT_NAME}}", + ), + arguments = listOf( + navArgument(ReceivedRoute.CATEGORY_ARGUMENT_NAME) { + type = NavType.StringType + }, + ), ) { ReceivedEnvelopeAddRoute( popBackStack = popBackStack, + handleException = handleException, ) } @@ -178,6 +189,8 @@ object ReceivedRoute { const val route = "received" const val LEDGER_ARGUMENT_NAME = "ledger" const val LEDGER_ID_ARGUMENT_NAME = "ledger-id" + const val CATEGORY_ARGUMENT_NAME = "category-name" + const val FILTER_ARGUMENT_NAME = "filter" fun ledgerDetailRoute(ledger: String) = "ledger-detail/$ledger" fun ledgerEditRoute(ledger: String) = "ledger-edit/$ledger" @@ -186,7 +199,7 @@ object ReceivedRoute { const val ledgerAddRoute = "ledger-add" // TODO 파라미터 넘기는 방식으로 수정해야함. - const val envelopeAddRoute = "envelope-add" + fun envelopeAddRoute(categoryName: String, ledgerId: String) = "envelope-add/$categoryName/$ledgerId" const val envelopeDetailRoute = "envelope-detail" // TODO 파라미터 넘기는 방식으로 수정해야함. const val envelopeEditRoute = "envelope-edit" // TODO 파라미터 넘기는 방식으로 수정해야함. } diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt index a53099a9..9346b05c 100644 --- a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt @@ -70,7 +70,7 @@ import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState fun ReceivedRoute( viewModel: ReceivedViewModel = hiltViewModel(), ledger: String?, - toDeleteLedgerId: Int, + toDeleteLedgerId: Long, filter: String?, padding: PaddingValues, navigateLedgerDetail: (Ledger) -> Unit, @@ -257,6 +257,7 @@ fun ReceiveScreen( title = ledger.title, money = ledger.totalAmounts, count = ledger.totalCounts, + style = ledger.category.style, onClick = { onClickLedgerCard(ledger) }, ) } @@ -271,6 +272,7 @@ fun ReceiveScreen( title = ledger.title, money = ledger.totalAmounts, count = ledger.totalCounts, + style = ledger.category.style, onClick = { onClickLedgerCard(ledger) }, ) } diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt index b6918f27..bf3a54e2 100644 --- a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt @@ -118,7 +118,7 @@ class ReceivedViewModel @Inject constructor( } } - fun updateLedgerIfNeed(ledger: String?, toDeleteLedgerId: Int) { + fun updateLedgerIfNeed(ledger: String?, toDeleteLedgerId: Long) { val toUpdateLedger = ledger?.let { Json.decodeFromUri(ledger) } ?: Ledger() @@ -128,7 +128,12 @@ class ReceivedViewModel @Inject constructor( .map { if (it.id == toUpdateLedger.id) toUpdateLedger else it } .toPersistentList() - intent { copy(ledgerList = newList) } + intent { + copy( + ledgerList = newList, + showEmptyLedger = newList.isEmpty(), + ) + } } fun addLedgerIfNeed(ledger: String?) { @@ -143,6 +148,7 @@ class ReceivedViewModel @Inject constructor( ledgerList = currentState .ledgerList .add(0, toAddLedger), + showEmptyLedger = false, ) } } diff --git a/feature/received/src/main/java/com/susu/feature/received/received/component/LedgerCard.kt b/feature/received/src/main/java/com/susu/feature/received/received/component/LedgerCard.kt index 6af323d1..e56e732f 100644 --- a/feature/received/src/main/java/com/susu/feature/received/received/component/LedgerCard.kt +++ b/feature/received/src/main/java/com/susu/feature/received/received/component/LedgerCard.kt @@ -24,12 +24,15 @@ import com.susu.core.ui.extension.toMoneyFormat @Composable fun LedgerCard( modifier: Modifier = Modifier, - ledgerType: String, // TODO LedgerType에 따라 Badger 색상 변경 필요 + ledgerType: String, + style: String, title: String, money: Int, count: Int, onClick: () -> Unit = {}, ) { + val badgeColor = BadgeColor.safeValueOf(style) + Column( modifier = modifier .aspectRatio(1f) @@ -38,7 +41,7 @@ fun LedgerCard( .padding(SusuTheme.spacing.spacing_m), ) { SusuBadge( - color = BadgeColor.Orange60, + color = badgeColor, text = ledgerType, padding = BadgeStyle.smallBadge, ) @@ -75,8 +78,9 @@ fun LedgerCardPreview() { LedgerCard( ledgerType = "결혼식", title = "나의 결혼식", - money = 4335000, + style = "", count = 164, + money = 10000, ) } } diff --git a/feature/received/src/main/res/values/strings.xml b/feature/received/src/main/res/values/strings.xml index 9af12036..9500ba31 100644 --- a/feature/received/src/main/res/values/strings.xml +++ b/feature/received/src/main/res/values/strings.xml @@ -53,6 +53,12 @@ 한끼 식사 01012345678 입력해주세요 + 방문여부 + 선물 + 메모 + 보낸 이의 연락처 + %s님에게 언제 받았나요 + %s님에게 종료일 추가 시작일만 지정 diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt new file mode 100644 index 00000000..c93f4654 --- /dev/null +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentContract.kt @@ -0,0 +1,19 @@ +package com.susu.feature.sent + +import com.susu.core.model.EnvelopeStatics +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class SentState( + val isLoading: Boolean = false, + val envelopesList: PersistentList = persistentListOf(), + val showEmptyEnvelopes: Boolean = false, +) : UiState + +sealed interface SentSideEffect : SideEffect { + data object NavigateEnvelopeDetail : SentSideEffect + data object NavigateEnvelopeAdd : SentSideEffect + data object NavigateEnvelopeSearch : SentSideEffect +} diff --git a/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt b/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt new file mode 100644 index 00000000..965509ce --- /dev/null +++ b/feature/sent/src/main/java/com/susu/feature/sent/SentViewModel.kt @@ -0,0 +1,23 @@ +package com.susu.feature.sent + +import androidx.lifecycle.viewModelScope +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.envelope.GetEnvelopesListUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SentViewModel @Inject constructor( + @Suppress("detekt:UnusedPrivateProperty") + private val getEnvelopesListUseCase: GetEnvelopesListUseCase, +) : BaseViewModel( + SentState(), +) { + @Suppress("detekt:UnusedPrivateProperty") + private var page = 0 + + fun getEnvelopesList() = viewModelScope.launch { + // TODO: 리스트 불러오기 + } +}