diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 74dd639..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/PiggyBankApiService.kt b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/PiggyBankApiService.kt index cdb77cf..1869db2 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/PiggyBankApiService.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/PiggyBankApiService.kt @@ -3,6 +3,7 @@ package com.ssafy.tiggle.data.datasource.remote import com.ssafy.tiggle.data.model.BaseResponse import com.ssafy.tiggle.data.model.EmptyResponse import com.ssafy.tiggle.data.model.piggybank.request.CreatePiggyBankRequestDto +import com.ssafy.tiggle.data.model.piggybank.request.PiggyBankSettingRequestDto import com.ssafy.tiggle.data.model.piggybank.request.PrimaryAccountRequestDto import com.ssafy.tiggle.data.model.piggybank.request.SendSMSRequestDto import com.ssafy.tiggle.data.model.piggybank.request.VerificationCheckRequestDto @@ -10,12 +11,18 @@ import com.ssafy.tiggle.data.model.piggybank.request.VerificationRequestDto import com.ssafy.tiggle.data.model.piggybank.request.VerifySMSRequestDto import com.ssafy.tiggle.data.model.piggybank.response.AccountHolderResponseDto import com.ssafy.tiggle.data.model.piggybank.response.CreatePiggyBankResponseDto +import com.ssafy.tiggle.data.model.piggybank.response.MainAccountResponseDto +import com.ssafy.tiggle.data.model.piggybank.response.PiggyBankAccountResponseDto +import com.ssafy.tiggle.data.model.piggybank.response.PiggyBankSettingResponseDto import com.ssafy.tiggle.data.model.piggybank.response.VerificationCheckResponseDto import com.ssafy.tiggle.data.model.piggybank.response.VerifySMSResponseDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.PATCH import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path import retrofit2.http.Query interface PiggyBankApiService { @@ -53,4 +60,20 @@ interface PiggyBankApiService { suspend fun verifySMS( @Body body: VerifySMSRequestDto ): BaseResponse + + @GET("accounts/primary") + suspend fun getMainAccount(): BaseResponse + + @GET("piggybank/summary") + suspend fun getPiggyBankAccount(): BaseResponse + + @PATCH("piggybank/settings") + suspend fun setPiggyBankSetting( + @Body body: PiggyBankSettingRequestDto + ): BaseResponse + + @PUT("piggybank/category/{categoryId}") + suspend fun setEsgCategory( + @Path("categoryId") categoryId: Int + ): BaseResponse } diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankSettingRequestDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankSettingRequestDto.kt new file mode 100644 index 0000000..20ef894 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankSettingRequestDto.kt @@ -0,0 +1,9 @@ +package com.ssafy.tiggle.data.model.piggybank.request + +data class PiggyBankSettingRequestDto( + val name: String? = null, + val targetAmount: Long? = null, + val autoDonation: Boolean? = null, + val autoSaving: Boolean? = null, + val esgCategoryId: Int? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountResponseDto.kt new file mode 100644 index 0000000..ec4cb78 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountResponseDto.kt @@ -0,0 +1,17 @@ +package com.ssafy.tiggle.data.model.piggybank.response + +import com.ssafy.tiggle.domain.entity.piggybank.AccountHolder +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount + +data class MainAccountResponseDto( + val accountName:String="", + val accountNo:String="", + val balance:String="" +) + +fun MainAccountResponseDto.toDomain(): MainAccount = + MainAccount( + accountName = accountName, + accountNo= accountNo, + balance=balance + ) diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankAccountResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankAccountResponseDto.kt new file mode 100644 index 0000000..6dc7ff1 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankAccountResponseDto.kt @@ -0,0 +1,16 @@ +package com.ssafy.tiggle.data.model.piggybank.response + +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount + +data class PiggyBankAccountResponseDto ( + val name:String="", + val currentAmount:Long=0L, + val lastWeekSavedAmount:Long=0L +) + +fun PiggyBankAccountResponseDto.toDomain():PiggyBankAccount= + PiggyBankAccount( + name=name, + currentAmount=currentAmount, + lastWeekSavedAmount=lastWeekSavedAmount + ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankSettingResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankSettingResponseDto.kt new file mode 100644 index 0000000..80eb140 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankSettingResponseDto.kt @@ -0,0 +1,46 @@ +package com.ssafy.tiggle.data.model.piggybank.response + +import com.ssafy.tiggle.domain.entity.piggybank.EsgCategory +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank + +data class PiggyBankSettingResponseDto( + val id: Int, + val name: String, + val currentAmount: Long, + val targetAmount: Long, + val savingCount: Int, + val donationCount: Int, + val donationTotalAmount: Long, + val autoDonation: Boolean, + val autoSaving: Boolean, + val esgCategory: EsgCategoryDto +) + +data class EsgCategoryDto( + val id: Int, + val name: String, + val description: String, + val characterName: String +) + +fun PiggyBankSettingResponseDto.toDomain(): PiggyBank = + PiggyBank( + id = id, + name = name, + currentAmount = currentAmount, + targetAmount = targetAmount, + savingCount = savingCount, + donationCount = donationCount, + donationTotalAmount = donationTotalAmount, + autoDonation = autoDonation, + autoSaving = autoSaving, + esgCategory = esgCategory.toDomain() + ) + +fun EsgCategoryDto.toDomain(): EsgCategory = + EsgCategory( + id = id, + name = name, + description = description, + characterName = characterName + ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/repository/PiggyBankRepositoryImpl.kt b/app/src/main/java/com/ssafy/tiggle/data/repository/PiggyBankRepositoryImpl.kt index 850ac65..915dcb1 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/repository/PiggyBankRepositoryImpl.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/repository/PiggyBankRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.ssafy.tiggle.data.repository import com.ssafy.tiggle.data.datasource.remote.PiggyBankApiService import com.ssafy.tiggle.data.model.piggybank.request.CreatePiggyBankRequestDto +import com.ssafy.tiggle.data.model.piggybank.request.PiggyBankSettingRequestDto import com.ssafy.tiggle.data.model.piggybank.request.PrimaryAccountRequestDto import com.ssafy.tiggle.data.model.piggybank.request.SendSMSRequestDto import com.ssafy.tiggle.data.model.piggybank.request.VerificationCheckRequestDto @@ -10,6 +11,9 @@ import com.ssafy.tiggle.data.model.piggybank.request.VerifySMSRequestDto import com.ssafy.tiggle.data.model.piggybank.response.VerifySMSResponseDto import com.ssafy.tiggle.data.model.piggybank.response.toDomain import com.ssafy.tiggle.domain.entity.piggybank.AccountHolder +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount import com.ssafy.tiggle.domain.repository.PiggyBankRepository import javax.inject.Inject import javax.inject.Singleton @@ -147,4 +151,77 @@ class PiggyBankRepositoryImpl @Inject constructor( } } + override suspend fun getMainAccount(): Result { + return try { + val response = piggyBankApiService.getMainAccount() + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "주계좌 정보를 불러오기에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getPiggyBankAccount(): Result { + return try { + val response = piggyBankApiService.getPiggyBankAccount() + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "저금통 계좌 정보를 불러오기에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun setPiggyBankSetting( + name: String?, + targetAmount: Long?, + autoDonation: Boolean?, + autoSaving: Boolean?, + esgCategory: Int? + ): Result { + return try { + val response = piggyBankApiService.setPiggyBankSetting( + PiggyBankSettingRequestDto( + name, + targetAmount, + autoDonation, + autoSaving, + esgCategory + ) + ) + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "저금통 설정에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + + } + + override suspend fun setEsgCategory(categoryId: Int): Result { + return try { + val response = piggyBankApiService.setEsgCategory( + categoryId = categoryId + ) + + if (response.result && response.data != null) { + Result.success(response.data.toDomain()) + } else { + Result.failure(Exception(response.message ?: "카테고리 설정에 실패했습니다.")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/MainAccount.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/MainAccount.kt new file mode 100644 index 0000000..100fd76 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/MainAccount.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.entity.piggybank + +data class MainAccount ( + val accountName:String="", + val accountNo:String="", + val balance:String="" +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBank.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBank.kt new file mode 100644 index 0000000..fed055f --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBank.kt @@ -0,0 +1,21 @@ +package com.ssafy.tiggle.domain.entity.piggybank + +data class PiggyBank( + val id: Int = 0, + val name: String = "", + val currentAmount: Long = 0L, + val targetAmount: Long = 0L, + val savingCount: Int = 0, + val donationCount: Int = 0, + val donationTotalAmount: Long = 0L, + val autoDonation: Boolean = false, + val autoSaving: Boolean = false, + val esgCategory: EsgCategory? = null, +) + +data class EsgCategory( + val id: Int = 0, + val name: String = "", + val description: String = "", + val characterName: String = "" +) diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankAccount.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankAccount.kt new file mode 100644 index 0000000..7990474 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankAccount.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.entity.piggybank + +data class PiggyBankAccount ( + val name:String="", + val currentAmount:Long=0L, + val lastWeekSavedAmount:Long=0L +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/repository/PiggyBankRepository.kt b/app/src/main/java/com/ssafy/tiggle/domain/repository/PiggyBankRepository.kt index fe33336..c5a4b15 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/repository/PiggyBankRepository.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/repository/PiggyBankRepository.kt @@ -2,6 +2,9 @@ package com.ssafy.tiggle.domain.repository import com.ssafy.tiggle.data.model.piggybank.response.VerifySMSResponseDto import com.ssafy.tiggle.domain.entity.piggybank.AccountHolder +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount interface PiggyBankRepository { suspend fun getAccountHolder(accountNo: String): Result @@ -14,5 +17,21 @@ interface PiggyBankRepository { suspend fun createPiggyBank(name: String, targetAmount: Long, esgCategoryId: Int): Result suspend fun sendSMS(phone: String, purpose: String): Result - suspend fun verifySMS(phone: String, code: String, purpose: String): Result + suspend fun verifySMS( + phone: String, + code: String, + purpose: String + ): Result + + suspend fun getMainAccount(): Result + suspend fun getPiggyBankAccount(): Result + suspend fun setPiggyBankSetting( + name: String?, + targetAmount: Long?, + autoDonation: Boolean?, + autoSaving: Boolean?, + esgCategory: Int? + ): Result + + suspend fun setEsgCategory(categoryId: Int): Result } \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountUseCase.kt new file mode 100644 index 0000000..f93ff3d --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountUseCase.kt @@ -0,0 +1,14 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.AccountHolder +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class GetMainAccountUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke():Result { + return repository.getMainAccount() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankAccountUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankAccountUseCase.kt new file mode 100644 index 0000000..3dde78c --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankAccountUseCase.kt @@ -0,0 +1,14 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class GetPiggyBankAccountUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke():Result { + return repository.getPiggyBankAccount() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/PiggyBankUseCases.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/PiggyBankUseCases.kt index aa9bc46..6324800 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/PiggyBankUseCases.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/PiggyBankUseCases.kt @@ -9,5 +9,9 @@ data class PiggyBankUseCases @Inject constructor( val registerPrimaryAccountUseCase: RegisterPrimaryAccountUseCase, val createPiggyBankUseCase: CreatePiggyBankUseCase, val sendSMSUseCase: SendSMSUseCase, - val verifySMSUseCase: VerifySMSUseCase + val verifySMSUseCase: VerifySMSUseCase, + val getMainAccountUseCase: GetMainAccountUseCase, + val getPiggyBankAccountUseCase: GetPiggyBankAccountUseCase, + val setPiggyBankSettingUseCase: SetPiggyBankSettingUseCase, + val setEsgCategoryUseCase: SetEsgCategoryUseCase ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetEsgCategoryUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetEsgCategoryUseCase.kt new file mode 100644 index 0000000..c1dd322 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetEsgCategoryUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class SetEsgCategoryUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke(categoryId: Int): Result { + return repository.setEsgCategory(categoryId) + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetPiggyBankSettingUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetPiggyBankSettingUseCase.kt new file mode 100644 index 0000000..1abb8ba --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/SetPiggyBankSettingUseCase.kt @@ -0,0 +1,25 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class SetPiggyBankSettingUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke( + name: String? = null, + targetAmount: Long? = null, + autoDonation: Boolean? = null, + autoSaving: Boolean? = null, + esgCategory: Int? = null + ): Result { + return repository.setPiggyBankSetting( + name, + targetAmount, + autoDonation, + autoSaving, + esgCategory + ) + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/EsgCategoryBottomSheet.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/EsgCategoryBottomSheet.kt new file mode 100644 index 0000000..ba06a22 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/EsgCategoryBottomSheet.kt @@ -0,0 +1,143 @@ +package com.ssafy.tiggle.presentation.ui.piggybank + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.ssafy.tiggle.R +import com.ssafy.tiggle.presentation.ui.theme.AppTypography +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue +import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayText + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EsgCategoryBottomSheet( + show: Boolean, + selectedId: Int?, + onPick: (Int) -> Unit, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + if (!show) return + + ModalBottomSheet( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + containerColor = Color.White + ) { + Column(Modifier.padding(horizontal = 20.dp, vertical = 16.dp)) { + Text("자동 기부 분야 설정", style = AppTypography.headlineLarge, fontSize = 18.sp) + Spacer(Modifier.height(6.dp)) + Text( + "자동 기부 설정 시 기부할 분야를 선택해주세요.", + style = AppTypography.bodySmall, + color = TiggleGrayText + ) + + Spacer(Modifier.height(16.dp)) + + // 간단한 3옵션 선택 (Planet / People / Prosperity) + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + EsgChoiceChip( + "Planet", + id = 1, + selected = selectedId == 1, + imageUrl = R.drawable.icon_planet, + onClick = { onPick(1) }) + EsgChoiceChip( + "People", + id = 2, + selected = selectedId == 2, + imageUrl = R.drawable.icon_people, + onClick = { onPick(2) }) + EsgChoiceChip( + "Prosperity", + id = 3, + selected = selectedId == 3, + imageUrl = R.drawable.icon_prosperity, + onClick = { onPick(3) }) + } + } + + Spacer(Modifier.height(20.dp)) + + Button( + onClick = onConfirm, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = TiggleBlue, + contentColor = Color.White + ), + enabled = selectedId != null + ) { Text("확인", style = AppTypography.bodyLarge) } + + Spacer(Modifier.height(12.dp)) + } + } +} + +@Composable +private fun EsgChoiceChip( + label: String, + id: Int, + selected: Boolean, + imageUrl: Int, + onClick: () -> Unit +) { + val border = if (selected) TiggleBlue else TiggleGrayText + val CHIP_WIDTH = 110.dp + val CHIP_HEIGHT = 100.dp + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .size(width = CHIP_WIDTH, height = CHIP_HEIGHT) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .border(1.dp, border, RoundedCornerShape(12.dp)) + .clickable(onClick = onClick) + ) { + Image( + painter = painterResource(id = imageUrl), + contentDescription = "아이콘", + modifier = Modifier.size(25.dp) + ) + Text( + label, + color = TiggleGrayText, + style = AppTypography.bodyMedium, + fontSize = 15.sp, + textAlign = TextAlign.Center + ) + } +} + diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankScreen.kt index f3ab88c..70fd8ac 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -42,7 +41,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.hilt.navigation.compose.hiltViewModel import com.ssafy.tiggle.R import com.ssafy.tiggle.core.utils.Formatter import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout @@ -60,7 +59,7 @@ fun PiggyBankScreen( onRegisterAccountClick: () -> Unit = {}, onStartDutchPayClick: () -> Unit = {}, onBackClick: () -> Unit = {}, - viewModel: PiggyBankViewModel = viewModel() + viewModel: PiggyBankViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -87,14 +86,12 @@ fun PiggyBankScreen( fontSize = 13.sp, style = AppTypography.bodySmall ) - Spacer(Modifier.height(30.dp)) + Spacer(Modifier.height(50.dp)) //계좌 존재 여부에 따라 if (uiState.hasPiggyBank) { TodaySavingBanner( - amount = uiState.todaySaving, - lastWeek = uiState.lastWeekRemainder, - rounded = uiState.lastWeekRounded + uiState = uiState ) } else { DottedActionCard( @@ -106,10 +103,7 @@ fun PiggyBankScreen( Spacer(Modifier.height(10.dp)) if (uiState.hasLinkedAccount) { AccountCard( - bank = uiState.accountBank.orEmpty(), - name = uiState.accountName.orEmpty(), - number = uiState.accountNumberMasked.orEmpty(), - balance = uiState.accountBalance ?: 0 + uiState = uiState ) } else { DottedActionCard( @@ -126,14 +120,14 @@ fun PiggyBankScreen( onStart = onStartDutchPayClick ) } - Spacer(Modifier.height(18.dp)) + Spacer(Modifier.height(25.dp)) // 스위치 섹션 TiggleSwitchRow( - title = "잔돈 자동 기부", - subtitle = "매일 잔정에 1,000원 미만 잔돈을 자동으로 기부합니다", - checked = uiState.changeLeftoverDonate, - onCheckedChange = viewModel::setChangeLeftoverDonate + title = "저금통 자동 기부", + subtitle = "일정 금액의 티끌이 쌓이면 기부 단체에 자동으로 기부됩니다.", + checked = uiState.piggyBank.autoDonation, + onCheckedChange = viewModel::onToggleAutoDonation ) HorizontalDivider( @@ -144,12 +138,21 @@ fun PiggyBankScreen( // 스위치 섹션 2 TiggleSwitchRow( - title = "목표 달성 자동 기부", - subtitle = "더치페이 할 때 남는 자투리 금액을 기부할 수 있습니다", - checked = uiState.achieveGoalDonate, - onCheckedChange = viewModel::setAchieveGoalDonate + title = "잔돈 자동 저금", + subtitle = "매일 자정에 1,000원 미만 잔돈을 자동으로 저금합니다.", + checked = uiState.piggyBank.autoSaving, + onCheckedChange = viewModel::onToggleAutoSaving ) + if (uiState.showEsgCategorySheet) { + EsgCategoryBottomSheet( // <- 네가 만든 컴포넌트 이름 + show = uiState.showEsgCategorySheet, + selectedId = uiState.piggyBank.esgCategory?.id, + onPick = viewModel::onPickEsgCategory, // 카테고리 탭 + onConfirm = viewModel::onConfirmAutoDonation, // 확인 버튼 + onDismiss = viewModel::onDismissEsgSheet // 바깥 터치/뒤로 + ) + } HorizontalDivider( color = TiggleGray, thickness = 0.5.dp, @@ -245,11 +248,12 @@ private fun PlusIcon(color: Color) { } @Composable -private fun TodaySavingBanner(amount: Int, lastWeek: Int, rounded: Int) { +private fun TodaySavingBanner(uiState: PiggyBankState) { val radius = 18.dp Box( modifier = Modifier .fillMaxWidth() + .height(130.dp) .clip(RoundedCornerShape(radius)) .background( brush = Brush.horizontalGradient( @@ -264,17 +268,23 @@ private fun TodaySavingBanner(amount: Int, lastWeek: Int, rounded: Int) { ) { Row(verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { - Text("오늘 모인 띠끌", color = Color(0xCCFFFFFF), style = AppTypography.bodySmall) + Text( + text = uiState.piggyBankAccount.name, + color = Color(0xCCFFFFFF), + style = AppTypography.bodySmall + ) Spacer(Modifier.height(6.dp)) Text( - "${amount}원", + "${uiState.piggyBankAccount.currentAmount}원", color = Color.White, fontSize = 34.sp, fontWeight = FontWeight.ExtraBold ) - Spacer(Modifier.height(6.dp)) + Spacer(Modifier.height(10.dp)) Text( - "지난주 잔액 ${Formatter.formatCurrency(lastWeek.toLong())} → ${Formatter.formatCurrency(rounded.toLong())}", + "+ 지난주에 ${ + Formatter.formatCurrency(uiState.piggyBankAccount.lastWeekSavedAmount) + }이 저금 됐어요", color = Color(0xE6FFFFFF), style = AppTypography.bodySmall ) @@ -295,11 +305,12 @@ private fun TodaySavingBanner(amount: Int, lastWeek: Int, rounded: Int) { } @Composable -private fun AccountCard(bank: String, name: String, number: String, balance: Int) { +private fun AccountCard(uiState: PiggyBankState) { val radius = 14.dp Column( modifier = Modifier .fillMaxWidth() + .height(130.dp) .clip(RoundedCornerShape(radius)) .background(Color.White) .border(1.dp, Color(0x11000000), RoundedCornerShape(radius)) @@ -318,13 +329,17 @@ private fun AccountCard(bank: String, name: String, number: String, balance: Int Spacer(Modifier.width(10.dp)) Column(Modifier.weight(1f)) { Text( - "$name", + "${uiState.mainAccount.accountName}", color = Color.Black, style = AppTypography.bodyLarge, fontSize = 15.sp ) Spacer(Modifier.height(2.dp)) - Text("$bank $number", color = TiggleGrayText, style = AppTypography.bodySmall) + Text( + "신한 ${uiState.mainAccount.accountNo}", + color = TiggleGrayText, + style = AppTypography.bodySmall + ) } Box() { Image( @@ -335,10 +350,11 @@ private fun AccountCard(bank: String, name: String, number: String, balance: Int } } Spacer(Modifier.height(16.dp)) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) { Text("잔액", color = TiggleGrayText, style = AppTypography.bodySmall) + Spacer(Modifier.height(5.dp)) Text( - Formatter.formatCurrency(balance.toLong()), + "${uiState.mainAccount.balance}원", color = Color.Black, fontSize = 22.sp, fontWeight = FontWeight.SemiBold @@ -349,6 +365,7 @@ private fun AccountCard(bank: String, name: String, number: String, balance: Int @Composable private fun DutchButtonsRow(onStatus: () -> Unit, onStart: () -> Unit) { + Spacer(Modifier.height(20.dp)) Row(Modifier.fillMaxWidth()) { OutlinedButton( onClick = onStatus, diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankState.kt index 1637153..1621d58 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankState.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankState.kt @@ -1,23 +1,23 @@ package com.ssafy.tiggle.presentation.ui.piggybank +import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount + data class PiggyBankState( - val isLoading: Boolean = false, + val piggyBankAccount: PiggyBankAccount = PiggyBankAccount(), + val mainAccount: MainAccount = MainAccount(), + val piggyBank: PiggyBank = PiggyBank(), //계좌 여부 val hasPiggyBank: Boolean = false, val hasLinkedAccount: Boolean = false, - //배너/ 계좌 카드 데이터 (임시로 만들었음 수정 예정) - val todaySaving: Int = 0, // 오늘 모인 띠끌 - val lastWeekRemainder: Int = 0, // 지난주 잔액 예: 15847 - val lastWeekRounded: Int = 0, // 지난주 목표 반올림 금액 예: 15000 - val accountBank: String? = null, // 예: "신한" - val accountName: String? = null, // 예: "쏠편한 입출금통장(저축예금)" - val accountNumberMasked: String? = null, // 예: "123-456-789000" - val accountBalance: Int? = null, // 예: 100000 - // 토글 - val changeLeftoverDonate: Boolean = false, - val achieveGoalDonate: Boolean = false, + //바텀 시트 + val showEsgCategorySheet: Boolean = false, + val tempSelectedCategoryId: Int? = null, // 시트에서 임시 선택 + + val isLoading: Boolean = false, // 전체 에러 메시지 - val generalError: String? = null + val errorMessage: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankViewModel.kt index 5c9e393..efc8a22 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankViewModel.kt @@ -1,44 +1,254 @@ package com.ssafy.tiggle.presentation.ui.piggybank import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ssafy.tiggle.domain.entity.piggybank.EsgCategory +import com.ssafy.tiggle.domain.usecase.piggybank.PiggyBankUseCases +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import retrofit2.HttpException +import javax.inject.Inject -class PiggyBankViewModel : ViewModel() { - private val _uiState = MutableStateFlow( - PiggyBankState( - hasPiggyBank = true, - hasLinkedAccount = true, - todaySaving = 847, - lastWeekRemainder = 15847, - lastWeekRounded = 15000, - accountBank = "신한", - accountName = "쏠편한 입출금통장(저축예금)", - accountNumberMasked = "123-456-789000", - accountBalance = 100_000, - changeLeftoverDonate = true, - achieveGoalDonate = true - - - ) - ) +@HiltViewModel +class PiggyBankViewModel @Inject constructor( + val useCases: PiggyBankUseCases +) : ViewModel() { + private val _uiState = MutableStateFlow(PiggyBankState()) val uiState: StateFlow = _uiState.asStateFlow() - fun setChangeLeftoverDonate(value: Boolean) { - _uiState.update { it.copy(changeLeftoverDonate = value) } + init { + setPiggyBankAccount() + setMainAccount() + loadPiggyBankSettings() } - fun setAchieveGoalDonate(value: Boolean) { - _uiState.update { it.copy(achieveGoalDonate = value) } + //저금통 계좌 확인 + fun setPiggyBankAccount() { + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + val result = useCases.getPiggyBankAccountUseCase() + + result + .onSuccess { account -> + _uiState.update { + it.copy( + isLoading = false, + piggyBankAccount = account, + hasPiggyBank = true + ) + } + } + .onFailure { e -> + val isNotFound = (e is HttpException && e.code() == 404) + _uiState.update { + it.copy( + isLoading = false, + hasPiggyBank = !isNotFound, + errorMessage = if (isNotFound) null + else e.message ?: "저금통 계좌 가져오기에 실패했습니다." + ) + } + } + } } - fun setHasPiggyBank(v: Boolean) { - _uiState.update { it.copy(hasPiggyBank = v) } + //주계좌 확인 + fun setMainAccount() { + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + val result = useCases.getMainAccountUseCase() + + result + .onSuccess { account -> + _uiState.update { + it.copy( + isLoading = false, + mainAccount = account, + hasLinkedAccount = true + ) + } + } + .onFailure { e -> + val isNotFound = (e is HttpException && e.code() == 404) + _uiState.update { + it.copy( + isLoading = false, + hasLinkedAccount = !isNotFound, + errorMessage = if (isNotFound) null + else e.message ?: "주계좌 가져오기에 실패했습니다." + ) + } + } + } } - fun setHasLinkedAccount(v: Boolean) { - _uiState.update { it.copy(hasLinkedAccount = v) } + + fun onToggleAutoDonation(checked: Boolean) { + if (checked) { + // UI만 켜고 시트 띄움 (서버 호출은 확인 시에) + _uiState.update { + it.copy( + piggyBank = it.piggyBank.copy(autoDonation = true), + showEsgCategorySheet = true, + errorMessage = null + ) + } + } else { + // 바로 서버 반영 (카테고리는 그대로) + val before = _uiState.value.piggyBank + _uiState.update { it.copy(piggyBank = it.piggyBank.copy(autoDonation = false)) } + + viewModelScope.launch { + val result = useCases.setPiggyBankSettingUseCase( + autoDonation = false + ) + result.onSuccess { updated -> + _uiState.update { it.copy(piggyBank = updated) } + }.onFailure { e -> + // 롤백 + _uiState.update { + it.copy( + piggyBank = before, + errorMessage = e.message ?: "자동 기부 해제 실패" + ) + } + } + } + } + } + + /** 바텀시트에서 카테고리 버튼 눌렀을 때: PiggyBank 안의 id만 바꿔둠(임시) */ + fun onPickEsgCategory(categoryId: Int) { + _uiState.update { + val cur = it.piggyBank + it.copy( + piggyBank = cur.copy( + esgCategory = (cur.esgCategory ?: EsgCategory()).copy(id = categoryId) + ) + ) + } } + + fun onConfirmAutoDonation() { + val pb = _uiState.value.piggyBank + val categoryId = pb.esgCategory?.id + if (categoryId == null) { + _uiState.update { it.copy(errorMessage = "카테고리를 선택해주세요.") } + return + } + + val before = pb + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + viewModelScope.launch { + // 1) 카테고리 먼저 확정 + val r1 = useCases.setEsgCategoryUseCase(categoryId) + r1.onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + // 토글 롤백 + piggyBank = before.copy(autoDonation = false), + showEsgCategorySheet = false, + errorMessage = e.message ?: "카테고리 설정 실패" + ) + } + return@launch + } + + // 2) 자동 기부 ON (서버에 켜기) + val r2 = useCases.setPiggyBankSettingUseCase(autoDonation = true) + r2.onSuccess { updated -> + _uiState.update { + it.copy( + isLoading = false, + piggyBank = updated, + showEsgCategorySheet = false + ) + } + }.onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + piggyBank = before.copy(autoDonation = false), // 실패 시 OFF로 되돌림 + showEsgCategorySheet = false, + errorMessage = e.message ?: "자동 기부 설정 실패" + ) + } + } + } + } + + /** 바텀시트 닫기(취소): 서버 호출 없이 토글만 다시 꺼짐 */ + fun onDismissEsgSheet() { + _uiState.update { + it.copy( + showEsgCategorySheet = false, + piggyBank = it.piggyBank.copy(autoDonation = false) + ) + } + } + + fun onToggleAutoSaving(checked: Boolean) { + + val before = _uiState.value.piggyBank.autoSaving + // 낙관적 업데이트 + _uiState.update { it.copy(piggyBank = it.piggyBank.copy(autoSaving = checked)) } + + viewModelScope.launch { + val result = useCases.setPiggyBankSettingUseCase(autoSaving = checked) + result + .onSuccess { updated -> + _uiState.update { it.copy(piggyBank = updated) } // 서버 값으로 동기화 + } + .onFailure { e -> + _uiState.update { + it.copy( + piggyBank = it.piggyBank.copy(autoSaving = before), + errorMessage = e.message ?: "자동 저금 설정 변경 실패" + ) + } + } + } + } + + fun loadPiggyBankSettings() { + viewModelScope.launch { + // 초기 진입 시 로딩 인디케이터를 꼭 보여주고 싶지 않다면 isLoading은 빼도 됨 + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + val result = useCases.setPiggyBankSettingUseCase() // ← 프로젝트에 맞는 이름으로 교체 + result + .onSuccess { settings -> + _uiState.update { + it.copy( + isLoading = false, + piggyBank = settings, + hasPiggyBank = true + ) + } + } + .onFailure { e -> + val isNotFound = (e is HttpException && e.code() == 404) + _uiState.update { + it.copy( + isLoading = false, + // 404면 설정이 아직 없다는 뜻 → 토글은 false로 유지, hasPiggyBank는 false로 + hasPiggyBank = if (isNotFound) false else it.hasPiggyBank, + errorMessage = if (isNotFound) null + else e.message ?: "저금통 설정을 불러오지 못했습니다." + ) + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_people.png b/app/src/main/res/drawable/icon_people.png new file mode 100644 index 0000000..742fc3c Binary files /dev/null and b/app/src/main/res/drawable/icon_people.png differ diff --git a/app/src/main/res/drawable/icon_planet.png b/app/src/main/res/drawable/icon_planet.png new file mode 100644 index 0000000..9196bd2 Binary files /dev/null and b/app/src/main/res/drawable/icon_planet.png differ diff --git a/app/src/main/res/drawable/icon_prosperity.png b/app/src/main/res/drawable/icon_prosperity.png new file mode 100644 index 0000000..6709325 Binary files /dev/null and b/app/src/main/res/drawable/icon_prosperity.png differ