diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7e99dc8..0a32bb0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,6 +34,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { jvmTarget = "11" @@ -105,4 +106,5 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") } \ 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 1869db2..9c2ad82 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.PiggyBankEntriesRequestDto 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 @@ -11,8 +12,10 @@ 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.MainAccountDetailResponseDto 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.PiggyBankEntriesResponseDto 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 @@ -76,4 +79,17 @@ interface PiggyBankApiService { suspend fun setEsgCategory( @Path("categoryId") categoryId: Int ): BaseResponse + + @GET("accounts/transactions") + suspend fun getTransactions( + @Query("accountNo") accountNo: String, + @Query("cursor") cursor: String? = null, + @Query("size") size: Int = 20, + @Query("sort") sort: String = "DESC" + ): BaseResponse + + @POST("piggybank/entries") + suspend fun getPiggyBankEntries( + @Body body: PiggyBankEntriesRequestDto + ): BaseResponse } diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankEntriesRequestDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankEntriesRequestDto.kt new file mode 100644 index 0000000..c8cd68a --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/request/PiggyBankEntriesRequestDto.kt @@ -0,0 +1,10 @@ +package com.ssafy.tiggle.data.model.piggybank.request + +data class PiggyBankEntriesRequestDto( + val type: String? = null, // CHANGE, DUTCHPAY + val cursor: String? = null, // 커서 기반 페이징 + val size: Int? = null, // 페이지 사이즈 + val from: String? = null, // 조회 시작일 (yyyy-MM-dd) + val to: String? = null, // 조회 종료일 (yyyy-MM-dd) + val sortKey: String? = null // 정렬 기준 키 +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountDetailResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountDetailResponseDto.kt new file mode 100644 index 0000000..a55f8fd --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/MainAccountDetailResponseDto.kt @@ -0,0 +1,40 @@ +package com.ssafy.tiggle.data.model.piggybank.response + +import com.ssafy.tiggle.domain.entity.piggybank.DomainTransaction +import com.ssafy.tiggle.domain.entity.piggybank.MainAccountDetail + +data class MainAccountDetailResponseDto( + val transactions: List, + val nextCursor: String?, + val hasNext: Boolean, + val size: Int +) + +data class Transaction( + val transactionId: String, + val transactionDate: String, + val transactionTime: String, + val transactionType: String, + val description: String, + val amount: Int, + val balanceAfter: Int +) + +fun MainAccountDetailResponseDto.toDomain(): MainAccountDetail = + MainAccountDetail( + transactions = transactions.map { it.toDomain() }, + nextCursor = nextCursor, + hasNext = hasNext, + size = size + ) + +fun Transaction.toDomain(): DomainTransaction = + DomainTransaction( + transactionId = transactionId, + transactionDate = transactionDate, + transactionTime = transactionTime, + transactionType = transactionType, + description = description, + amount = amount, + balanceAfter = balanceAfter + ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankEntriesResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankEntriesResponseDto.kt new file mode 100644 index 0000000..bbeac1c --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/piggybank/response/PiggyBankEntriesResponseDto.kt @@ -0,0 +1,35 @@ +package com.ssafy.tiggle.data.model.piggybank.response + +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry + +data class PiggyBankEntriesResponseDto( + val items: List, + val nextCursor: String?, + val size: Int, + val hasNext: Boolean +) { + +} + +data class PiggyBankEntryItem( + val id: String, + val type: String, + val amount: Long, + val occurredAt: String, + val title: String +) + +fun PiggyBankEntriesResponseDto.toDomain(): List = + items.map { + it.toDomain() + } + + +fun PiggyBankEntryItem.toDomain(): PiggyBankEntry = + PiggyBankEntry( + id = id, + type = type, + amount = amount, + occurredAt = occurredAt, + title = title + ) \ 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 915dcb1..89b13c9 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 @@ -1,19 +1,24 @@ package com.ssafy.tiggle.data.repository import com.ssafy.tiggle.data.datasource.remote.PiggyBankApiService +import com.ssafy.tiggle.data.model.BaseResponse import com.ssafy.tiggle.data.model.piggybank.request.CreatePiggyBankRequestDto +import com.ssafy.tiggle.data.model.piggybank.request.PiggyBankEntriesRequestDto 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 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.PiggyBankEntriesResponseDto 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.MainAccountDetail import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry import com.ssafy.tiggle.domain.repository.PiggyBankRepository import javax.inject.Inject import javax.inject.Singleton @@ -224,4 +229,57 @@ class PiggyBankRepositoryImpl @Inject constructor( } } + override suspend fun getTransactions( + accountNo: String, + cursor: String? + ): Result { + return try { + // size, sort 기본값은 스웨거 기본(20, DESC)에 맞춤 + val response = piggyBankApiService.getTransactions( + accountNo = accountNo, + cursor = cursor, + size = 20, + sort = "DESC" + ) + + 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 getPiggyBankEntries( + type: String, + cursor: String?, + size: Int?, + from: String?, + to: String?, + sortKey: String? + ): Result> { + return try { + val request = PiggyBankEntriesRequestDto( + type = type, + cursor = cursor, + size = size, + from = from, + to = to, + sortKey = sortKey + ) + val response: BaseResponse = + piggyBankApiService.getPiggyBankEntries(request) + 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/MainAccountDetail.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/MainAccountDetail.kt new file mode 100644 index 0000000..d1d725c --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/MainAccountDetail.kt @@ -0,0 +1,18 @@ +package com.ssafy.tiggle.domain.entity.piggybank + +data class MainAccountDetail( + val transactions: List = emptyList(), + val nextCursor: String? = null, + val hasNext: Boolean = false, + val size: Int = 0 +) + +data class DomainTransaction( + val transactionId: String = "", + val transactionDate: String = "", + val transactionTime: String = "", + val transactionType: String = "", + val description: String = "", + val amount: Int = 0, + val balanceAfter: Int = 0 +) diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankEntry.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankEntry.kt new file mode 100644 index 0000000..e51d2f8 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/piggybank/PiggyBankEntry.kt @@ -0,0 +1,9 @@ +package com.ssafy.tiggle.domain.entity.piggybank + +data class PiggyBankEntry( + val id: String = "", + val type: String = "", + val amount: Long = 0L, + val occurredAt: String = "", + val title: String = "" +) 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 c5a4b15..6be3067 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 @@ -3,8 +3,10 @@ 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.MainAccountDetail import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry interface PiggyBankRepository { suspend fun getAccountHolder(accountNo: String): Result @@ -34,4 +36,18 @@ interface PiggyBankRepository { ): Result suspend fun setEsgCategory(categoryId: Int): Result + suspend fun getTransactions( + accountNo: String, + cursor: String? = null + ): Result + + suspend fun getPiggyBankEntries( + type: String, + cursor: String? = null, + size: Int? = null, + from: String? = null, + to: String? = null, + sortKey: String? = null + ): Result> + } \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountDetailUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountDetailUseCase.kt new file mode 100644 index 0000000..13c4e75 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetMainAccountDetailUseCase.kt @@ -0,0 +1,16 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.MainAccountDetail +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class GetMainAccountDetailUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke( + accountNo: String, + cursor: String? = null + ): Result { + return repository.getTransactions(accountNo, cursor) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankEntryUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankEntryUseCase.kt new file mode 100644 index 0000000..0a4a482 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/piggybank/GetPiggyBankEntryUseCase.kt @@ -0,0 +1,27 @@ +package com.ssafy.tiggle.domain.usecase.piggybank + +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry +import com.ssafy.tiggle.domain.repository.PiggyBankRepository +import javax.inject.Inject + +class GetPiggyBankEntryUseCase @Inject constructor( + private val repository: PiggyBankRepository +) { + suspend operator fun invoke( + type: String, + cursor: String? = null, + size: Int? = null, + from: String? = null, + to: String? = null, + sortKey: String? = null + ): Result> { + return repository.getPiggyBankEntries( + type = type, + cursor = cursor, + size = size, + from = from, + to = to, + sortKey = sortKey + ) + } +} 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 6324800..1f87c76 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 @@ -13,5 +13,7 @@ data class PiggyBankUseCases @Inject constructor( val getMainAccountUseCase: GetMainAccountUseCase, val getPiggyBankAccountUseCase: GetPiggyBankAccountUseCase, val setPiggyBankSettingUseCase: SetPiggyBankSettingUseCase, - val setEsgCategoryUseCase: SetEsgCategoryUseCase + val setEsgCategoryUseCase: SetEsgCategoryUseCase, + val getMainAccountDetailUseCase: GetMainAccountDetailUseCase, + val getPiggyBankEntryUseCase: GetPiggyBankEntryUseCase ) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt index e673c15..e5d32fd 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt @@ -3,7 +3,6 @@ package com.ssafy.tiggle.presentation.navigation import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -18,7 +17,10 @@ import com.ssafy.tiggle.presentation.ui.donation.DonationHistoryScreen import com.ssafy.tiggle.presentation.ui.donation.DonationStatusScreen import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayScreen import com.ssafy.tiggle.presentation.ui.growth.GrowthScreen +import com.ssafy.tiggle.presentation.ui.piggybank.MainAccountDetailScreen +import com.ssafy.tiggle.presentation.ui.piggybank.OpenAccountMode import com.ssafy.tiggle.presentation.ui.piggybank.OpenAccountScreen +import com.ssafy.tiggle.presentation.ui.piggybank.PiggyBankDetailRoute import com.ssafy.tiggle.presentation.ui.piggybank.PiggyBankScreen import com.ssafy.tiggle.presentation.ui.piggybank.RegisterAccountScreen import com.ssafy.tiggle.presentation.ui.shorts.ShortsScreen @@ -92,31 +94,39 @@ fun NavigationGraph() { is BottomScreen.PiggyBank -> NavEntry(key) { PiggyBankScreen( onOpenAccountClick = { - navBackStack.add(Screen.OpenAccount) + navBackStack.add(Screen.OpenAccount()) }, onRegisterAccountClick = { - navBackStack.add(Screen.RegisterAccount) + navBackStack.add(Screen.RegisterAccount(isEdit = false)) }, onStartDutchPayClick = { navBackStack.add(Screen.CreateDutchPay) }, - onBackClick = { - navBackStack.removeLastOrNull() - } + onAccountClick = { accountNo -> + navBackStack.add(Screen.MainAccountDetail(accountNo)) + }, + onShowPiggyBankDetailClick = { + navBackStack.add(Screen.PiggyBankDetail) + }, + onEditLinkedAccountClick = { + navBackStack.add(Screen.RegisterAccount(isEdit = true)) + } ) } is Screen.OpenAccount -> NavEntry(key) { OpenAccountScreen( + mode = key.mode, onBackClick = { navBackStack.removeLastOrNull() }, onFinish = { navBackStack.removeLastOrNull() - } + }, ) } is Screen.RegisterAccount -> NavEntry(key) { RegisterAccountScreen( + isEdit = key.isEdit, onBackClick = { navBackStack.removeLastOrNull() }, onFinish = { navBackStack.removeLastOrNull() @@ -131,12 +141,25 @@ fun NavigationGraph() { ) } + is Screen.MainAccountDetail -> NavEntry(key) { + MainAccountDetailScreen( + accountNo = key.accountNo, + ) + } + is Screen.DonationHistory -> NavEntry(key) { DonationHistoryScreen( onBackClick = { navBackStack.removeLastOrNull() } ) } + is Screen.PiggyBankDetail -> NavEntry(key) { + PiggyBankDetailRoute( + onBackClick = { navBackStack.removeLastOrNull() }, + onMore = { navBackStack.add(Screen.OpenAccount(OpenAccountMode.SIMPLE)) } + ) + } + is Screen.DonationStatus -> NavEntry(key) { DonationStatusScreen( onBackClick = { navBackStack.removeLastOrNull() } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt index b456fbb..3b88a94 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt @@ -1,6 +1,7 @@ package com.ssafy.tiggle.presentation.navigation import androidx.navigation3.runtime.NavKey +import com.ssafy.tiggle.presentation.ui.piggybank.OpenAccountMode import kotlinx.serialization.Serializable /** @@ -28,17 +29,24 @@ sealed interface Screen : NavKey { object SignUp : Screen @Serializable - object OpenAccount : Screen + data class OpenAccount(val mode: OpenAccountMode = OpenAccountMode.FULL) : Screen @Serializable - object RegisterAccount : Screen + data class RegisterAccount(val isEdit: Boolean = false) : Screen @Serializable object CreateDutchPay : Screen + @Serializable + data class MainAccountDetail(val accountNo: String) : Screen + + @Serializable + object PiggyBankDetail : Screen + @Serializable object DonationHistory : Screen @Serializable object DonationStatus : Screen } + diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleHeader.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleHeader.kt index 6dadee6..1ec3490 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleHeader.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleHeader.kt @@ -1,6 +1,8 @@ package com.ssafy.tiggle.presentation.ui.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -24,33 +26,38 @@ import androidx.compose.ui.unit.sp fun TiggleHeader( title: String? = null, showBackButton: Boolean = true, - onBackClick: () -> Unit = {} + onBackClick: () -> Unit = {}, + actions: (@Composable RowScope.() -> Unit)? = null ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - if (showBackButton) { - IconButton(onClick = onBackClick) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "뒤로가기", - tint = Color.Black - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (showBackButton) { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "뒤로가기", + tint = Color.Black + ) + } } - } - title?.let { - Text( - text = it, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = Color.Black, - modifier = Modifier.padding(start = if (showBackButton) 8.dp else 16.dp) - ) + title?.let { + Text( + text = it, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.padding(start = if (showBackButton) 8.dp else 16.dp) + ) + } } + Row{actions?.invoke(this)} } } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleScreenLayout.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleScreenLayout.kt index e791914..f441f2c 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleScreenLayout.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/components/TiggleScreenLayout.kt @@ -3,6 +3,8 @@ package com.ssafy.tiggle.presentation.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -30,7 +32,9 @@ fun TiggleScreenLayout( showBackButton: Boolean = true, onBackClick: () -> Unit = {}, showLogo: Boolean = false, + topActions: (@Composable RowScope.() -> Unit)? = null, bottomButton: @Composable (() -> Unit)? = null, + contentPadding: PaddingValues = PaddingValues(horizontal = 32.dp), enableScroll: Boolean = true, content: @Composable () -> Unit ) { @@ -42,57 +46,58 @@ fun TiggleScreenLayout( .background(Color.White) .imePadding() // imePadding이 키보드 높이만큼 패딩을 자동으로 추가해줍니다. ) { - // 헤더 - if (showBackButton || title != null) { - TiggleHeader( - title = title, - showBackButton = showBackButton, - onBackClick = onBackClick - ) - } + Column( + modifier = Modifier.fillMaxSize() + ) { + // 헤더 + if (showBackButton || title != null) { + TiggleHeader( + title = title, + showBackButton = showBackButton, + onBackClick = onBackClick, + actions = topActions + ) + } - // 메인 콘텐츠 - if (enableScroll) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 32.dp) - .verticalScroll(scrollState) - ) { - if (showLogo) { - TiggleLogo() + // 메인 콘텐츠 + if (enableScroll) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 32.dp) + .verticalScroll(scrollState) + ) { + if (showLogo) { + TiggleLogo() + } + content() } - content() - } - } else { - // LazyColumn 등을 사용하는 화면을 위한 레이아웃 - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = 32.dp) - ) { - if (showLogo) { - Column { + } else { + // 스크롤이 필요 없는 화면을 위한 레이아웃 + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(contentPadding) + ) { + if (showLogo) { TiggleLogo() - content() } - } else { content() } } - } - // 하단 버튼 (선택적) - bottomButton?.let { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp) - .padding(bottom = 32.dp) - ) { - it() + // 하단 버튼 (선택적) + bottomButton?.let { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .padding(bottom = 32.dp) + ) { + it() + } } } } @@ -130,6 +135,7 @@ private fun TiggleScreenLayoutPreview() { } } + @Preview(showBackground = true) @Composable private fun TiggleScreenLayoutWithLogoPreview() { @@ -150,4 +156,5 @@ private fun TiggleScreenLayoutWithLogoPreview() { fontSize = 16.sp ) } + } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayScreen.kt index 7c33d9c..81d26bf 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayScreen.kt @@ -13,11 +13,9 @@ 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.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -76,6 +74,11 @@ fun CreateDutchPayScreen( onBackClick = { if (uiState.step == CreateDutchPayStep.PICK_USERS) onBackClick() else viewModel.goPrev() }, + enableScroll = when (uiState.step) { + CreateDutchPayStep.PICK_USERS -> false + CreateDutchPayStep.INPUT_AMOUNT -> true + CreateDutchPayStep.COMPLETE -> true + }, bottomButton = { TiggleButton( text = when (uiState.step) { @@ -161,19 +164,13 @@ fun DutchPayPickUsersContent( selectedUserIds: Set, onToggleUser: (Long) -> Unit ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(top = 12.dp) - ) { - Text("함께 결제할 유저를 선택하세요", style = AppTypography.bodyLarge) - Spacer(Modifier.height(12.dp)) - UserPicker( - users = users, - selectedUserIds = selectedUserIds, - onToggleUser = onToggleUser - ) - } + Text("함께 결제할 유저를 선택하세요", style = AppTypography.bodyLarge) + Spacer(Modifier.height(12.dp)) + UserPicker( + users = users, + selectedUserIds = selectedUserIds, + onToggleUser = onToggleUser + ) } @Composable @@ -192,7 +189,6 @@ fun DutchPayInputAmountContent( Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .padding(top = 12.dp) ) { // 선택한 친구 섹션 @@ -340,7 +336,6 @@ fun DutchPayCompleteContent( Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/MainAccountDetailScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/MainAccountDetailScreen.kt new file mode 100644 index 0000000..6303e25 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/MainAccountDetailScreen.kt @@ -0,0 +1,171 @@ +package com.ssafy.tiggle.presentation.ui.piggybank + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ssafy.tiggle.domain.entity.piggybank.DomainTransaction +import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue + +@Composable +fun MainAccountDetailScreen( + accountNo: String, + viewModel: PiggyBankViewModel = hiltViewModel(), + onBackClick: () -> Unit = {} +) { + val state by viewModel.uiState.collectAsState() + + // 첫 진입 시 데이터 불러오기 + LaunchedEffect(Unit) { + viewModel.loadTransactions(accountNo) + } + + TiggleScreenLayout( + showBackButton = true, + title = "오늘 모인 티끌", + onBackClick = onBackClick, + contentPadding = PaddingValues(0.dp), + enableScroll = false + ) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { + // 상단 잔액 영역 + Box( + modifier = Modifier + .fillMaxWidth() + .background(TiggleBlue) // 파란색 + .padding(0.dp, 70.dp), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "${formatAmount(state.mainAccountDetail.transactions.firstOrNull()?.balanceAfter?.toLong() ?: 0)}원", + color = Color.White, + fontSize = 28.sp, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(10.dp)) + Text( + text = "잔액", + color = Color.White.copy(alpha = 0.8f), + fontSize = 20.sp + ) + } + } + + Spacer(Modifier.height(20.dp)) + + // 거래 내역 리스트 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(20.dp, 0.dp), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(state.mainAccountDetail.transactions) { tx -> + TransactionItem(tx) + } + } + } + } +} + +@Composable +fun TransactionItem(tx: DomainTransaction) { + val borderColor = Color(0xFFE0E0E0) // 캡처 느낌의 연한 회색 테두리 + val timeTextColor = Color(0xFF9AA3AD) // 연한 회색 텍스트 + val amountColor = if (tx.transactionType == "출금") Color(0xFFD32F2F) else TiggleBlue + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, borderColor) + ) { + Column(modifier = Modifier.padding(vertical = 10.dp, horizontal = 15.dp)) { + + // 상단 행: 날짜 + 이름 / 금액 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + // 날짜 + 거래처명 + Column { + Text( + text = formatMonthDay(tx.transactionDate), // "8.20" 형태 + fontSize = 10.sp, + color = timeTextColor + ) + Spacer(Modifier.height(4.dp)) + Text( + text = tx.description, + fontSize = 15.sp, + fontWeight = FontWeight.ExtraBold, + color = Color(0xFF1C1F23) + ) + } + + // 금액 (+/-) + Text( + text = buildString { + append(if (tx.transactionType == "출금") "- " else "+ ") + append("${formatAmount(tx.amount.toLong())}원") + }, + fontSize = 18.sp, + fontWeight = FontWeight.ExtraBold, + color = amountColor + ) + } + + Spacer(Modifier.height(18.dp)) + + // 하단 행: 시간 / 잔액 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = tx.transactionTime, // 예: "08:30" + fontSize = 10.sp, + color = timeTextColor + ) + Text( + text = "${formatAmount(tx.balanceAfter.toLong())}원", + fontSize = 15.sp, + color = timeTextColor + ) + } + } + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountScreen.kt index 81cfe4e..a92614a 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -60,8 +61,14 @@ fun OpenAccountScreen( modifier: Modifier = Modifier, viewModel: OpenAccountViewModel = hiltViewModel(), onBackClick: () -> Unit = {}, - onFinish: () -> Unit = {} + mode: OpenAccountMode = OpenAccountMode.FULL, + onFinish: () -> Unit = {}, ) { + + LaunchedEffect(mode) { + viewModel.setMode(mode) + } + val uiState by viewModel.uiState.collectAsState() // 공통 Back 핸들러: 첫 단계면 pop, 아니면 단계-뒤로 @@ -79,7 +86,13 @@ fun OpenAccountScreen( onBackClick = handleTopBack, onTargetDonationAmountChange = viewModel::updateTargetDonationAmount, onPiggyBankNameChange = viewModel::updatePiggyBankName, - onNextClick = { viewModel.goToNextStep() } + onNextClick = { + if (mode == OpenAccountMode.SIMPLE) { + viewModel.modifyPiggyBankInfo() + } else { + viewModel.goToNextStep() + } + } ) } @@ -114,6 +127,7 @@ fun OpenAccountScreen( OpenAccountStep.SUCCESS -> { SuccessScreen( uiState = uiState, + mode = mode, onFinish = onFinish, ) } @@ -132,6 +146,7 @@ fun AccountInfoInputScreen( TiggleScreenLayout( showBackButton = true, onBackClick = onBackClick, + enableScroll = true, bottomButton = { val nextEnabled = uiState.piggyBankAccount.targetDonationAmount.toString() @@ -198,8 +213,11 @@ fun AccountInfoInputScreen( Text("기부 목표 금액", style = AppTypography.bodyLarge) Spacer(Modifier.height(8.dp)) + val target = uiState.piggyBankAccount.targetDonationAmount + val displayTarget = if (target > 0) target.toString() else "1000" + QuickAmountRow( - selected = uiState.piggyBankAccount.targetDonationAmount.toString(), + selected =displayTarget, onSelect = onTargetDonationAmountChange ) @@ -207,7 +225,7 @@ fun AccountInfoInputScreen( // 금액 직접 입력 OutlinedTextField( - value = uiState.piggyBankAccount.targetDonationAmount.toString(), + value = displayTarget, onValueChange = onTargetDonationAmountChange, placeholder = { Text("기부하고 싶은 금액을 입력하세요") }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -343,6 +361,7 @@ private fun TermsAgreementScreen( TiggleScreenLayout( showBackButton = true, onBackClick = onBackClick, + enableScroll = true, bottomButton = { TiggleButton( text = "동의하고 계속", @@ -473,6 +492,7 @@ fun CertificateScreen( TiggleScreenLayout( showBackButton = true, onBackClick = onBackClick, + enableScroll = true, bottomButton = { TiggleButton( text = "인증하기", @@ -590,6 +610,7 @@ fun CodeScreen( TiggleScreenLayout( showBackButton = true, onBackClick = onBackClick, + enableScroll = true, bottomButton = { TiggleButton( text = "인증 완료", @@ -786,10 +807,12 @@ private fun OtpCodeInput( @Composable private fun SuccessScreen( uiState: OpenAccountState, + mode: OpenAccountMode, onFinish: () -> Unit, ) { TiggleScreenLayout( showBackButton = false, + enableScroll = true, bottomButton = { TiggleButton( text = "확인", @@ -828,14 +851,20 @@ private fun SuccessScreen( modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text( - text = "티끌 계좌 개설 완료!", + text = if (mode == OpenAccountMode.SIMPLE) + "티끌 계좌 수정 완료!" + else + "티끌 계좌 개설 완료!", color = Color.Black, fontSize = 22.sp, style = AppTypography.headlineLarge, ) Spacer(Modifier.height(6.dp)) Text( - text = "티끌 저금통 계좌가 성공적으로 개설되었습니다.\n 이제 티끌을 모아 저금통을 채워보세요!", + text = if (mode == OpenAccountMode.SIMPLE) + "티끌 저금통 계좌가 성공적으로 개설되었습니다.\n 이제 티끌을 모아 저금통을 채워보세요!" + else + "티끌 저금통 계좌가 성공적으로 수정되었습니다.\n 이제 티끌을 모아 저금통을 채워보세요!", color = TiggleGrayText, fontSize = 13.sp, style = AppTypography.bodySmall, @@ -986,7 +1015,8 @@ fun Preview_CodeScreen_Error() { fun Preview_SuccessScreen() { SuccessScreen( uiState = OpenAccountState(openAccountStep = OpenAccountStep.SUCCESS), - onFinish = {} + onFinish = {}, + mode = OpenAccountMode.SIMPLE ) } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountState.kt index b70f2bb..9199fb4 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountState.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountState.kt @@ -11,7 +11,8 @@ data class OpenAccountState( // 약관 동의 val termsData: TermsData = TermsData(), -) + + ) /** * 약관 동의 데이터 @@ -31,3 +32,4 @@ data class TermsData( get() = serviceTerms && privacyPolicy && financeTerms && marketingOptional } +enum class OpenAccountMode { FULL, SIMPLE } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountViewModel.kt index a517ebe..1fff968 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/OpenAccountViewModel.kt @@ -357,4 +357,52 @@ class OpenAccountViewModel @Inject constructor( } } + private var mode: OpenAccountMode = OpenAccountMode.FULL + fun setMode(m: OpenAccountMode) { + mode = m + } + + // 기존 goToNextStep는 그대로 두고, + // INFO에서만 분기하는 전용 진입점 추가 + fun nextFromInfo() { + if (mode == OpenAccountMode.SIMPLE) { + //바로 SUCCESS + _uiState.update { it.copy(openAccountStep = OpenAccountStep.SUCCESS) } + return + } + //기존 단계 진행 + goToNextStep() + } + + fun modifyPiggyBankInfo() { + val name = _uiState.value.piggyBankAccount.piggyBankName + val targetAmount = _uiState.value.piggyBankAccount.targetDonationAmount + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + val result = useCases.setPiggyBankSettingUseCase( + name = name, + targetAmount = targetAmount + ) + + result.onSuccess { updated -> + _uiState.update { + it.copy( + isLoading = false, + openAccountStep = OpenAccountStep.SUCCESS, + errorMessage = null + ) + } + }.onFailure { e -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = e.message ?: "저금통 정보 수정에 실패했습니다." + ) + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailRoute.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailRoute.kt new file mode 100644 index 0000000..29f3c64 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailRoute.kt @@ -0,0 +1,37 @@ +package com.ssafy.tiggle.presentation.ui.piggybank + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel + +// PiggyBankDetailsRoute.kt +@Composable +fun PiggyBankDetailRoute( + onBackClick: () -> Unit, + viewModel: PiggyBankViewModel = hiltViewModel(), + initialTab: PiggyTab? = PiggyTab.SpareChange, + onMore: () -> Unit +) { + val state by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + // 최초 진입 시 한 번만 로드 + viewModel.loadAllPiggyEntries(size = 20) + initialTab?.let { viewModel.setSelectedTab(it) } // 뷰모델에 이 함수만 추가해주면 됨 + } + + PiggyBankDetailsScreen( + uiState = state, + onBack = onBackClick, + onTabChange = { tab -> + viewModel.setSelectedTab(tab) + when (tab) { + PiggyTab.SpareChange -> viewModel.reloadEntriesByType("CHANGE") + PiggyTab.DutchPay -> viewModel.reloadEntriesByType("DUTCHPAY") + } + }, + onMore = onMore + ) +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailScreen.kt new file mode 100644 index 0000000..6542dcc --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/PiggyBankDetailScreen.kt @@ -0,0 +1,470 @@ +package com.ssafy.tiggle.presentation.ui.piggybank + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.PaddingValues +import androidx.compose.foundation.layout.Row +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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.ssafy.tiggle.R +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry +import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout +import com.ssafy.tiggle.presentation.ui.theme.AppTypography +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlueLight +import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayText +import com.ssafy.tiggle.presentation.ui.theme.TiggleSkyBlue +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +// --------------------------- +// Top-level Screen +// --------------------------- + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PiggyBankDetailsScreen( + uiState: PiggyBankState, + onBack: () -> Unit = {}, + onMore: () -> Unit = {}, + onTabChange: (PiggyTab) -> Unit = {}, + onItemClick: (PiggyBankEntry) -> Unit = {} +) { + TiggleScreenLayout( + showBackButton = true, + title = "오늘 모인 티끌", + onBackClick = onBack, + contentPadding = PaddingValues(0.dp), + enableScroll = false, + topActions = { + Row(horizontalArrangement = Arrangement.End) { + Image( + painter = painterResource(id = R.drawable.linked_card_option), + contentDescription = "저금통 정보 수정", + modifier = Modifier.size(25.dp).clickable { onMore() } + ) + } + } + ) { + Column(Modifier.fillMaxSize()) { + + val today = remember { LocalDate.now() } + SummaryHeader( + amount = uiState.piggyBank.currentAmount, + date = today, + savedCount = uiState.piggyBank.savingCount, + donationCount = uiState.piggyBank.donationCount, + donationAmount = uiState.piggyBank.donationTotalAmount + ) + + Spacer(Modifier.height(8.dp)) + + PiggyTabBar( + selected = uiState.selectedTab, + onSelected = onTabChange + ) + + Spacer(Modifier.height(12.dp)) + + // --- 하단 고정 InfoTipCard + 상단 스크롤 리스트 --- + when (uiState.selectedTab) { + PiggyTab.SpareChange -> { + SectionTitle(text = "주간 자투리 적립") + Spacer(Modifier.height(10.dp)) + + Box(Modifier.fillMaxSize()) { + // 카드 높이만큼 하단 여백(패딩) 확보: 100~120dp 권장 + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 108.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 0.dp) + ) { + items(uiState.changeList, key = { it.id }) { item -> + EntryItemCard(item = item, onClick = { onItemClick(item) }) + } + } + + InfoTipCard( + title = "자투리 적립 방식", + message = "매주 월요일에 내 계좌 잔액의 천원 미만 자투리 금액이 자동으로 저금통에 적립됩니다.", + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 20.dp, vertical = 20.dp) + ) + } + } + + PiggyTab.DutchPay -> { + SectionTitle(text = "더치페이 잔돈 적립") + Spacer(Modifier.height(10.dp)) + + Box(Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 108.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = PaddingValues(horizontal = 20.dp, vertical = 0.dp) + ) { + items(uiState.dutchpayList, key = { it.id }) { item -> + EntryItemCard(item = item, onClick = { onItemClick(item) }) + } + } + + InfoTipCard( + title = "더치페이 적립 방식", + message = "더치페이할 때 ‘내가 더 낼게요’를 선택하면 자투리 금액이 저금통에 적립됩니다.", + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(horizontal = 20.dp, vertical = 20.dp) + ) + } + } + } + + Spacer(Modifier.height(18.dp)) + } + } +} + +// --------------------------- +// Components +// --------------------------- + +@Composable +private fun SummaryHeader( + amount: Long, + date: LocalDate, + savedCount: Int, + donationCount: Int, + donationAmount: Long +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(TiggleBlue) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "${formatAmount(amount)}원", + fontSize = 44.sp, + color = Color.White, + style = AppTypography.headlineLarge, + fontWeight = FontWeight.ExtraBold + ) + Spacer(Modifier.height(4.dp)) + Text( + date.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일")), + color = Color.White.copy(alpha = 0.95f) + ) + Spacer(Modifier.height(25.dp)) + StatPillRow(savedCount, donationCount, donationAmount) + } + } +} + +@Composable +private fun StatPillRow(savedCount: Int, donationCount: Int, donationAmount: Long) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(Color.White.copy(alpha = 0.16f)), + horizontalArrangement = Arrangement.spacedBy(13.dp, Alignment.CenterHorizontally) + ) { + StatPill(top = "${savedCount}회", bottom = "적립 횟수") + StatPill(top = "${donationCount}회", bottom = "기부 횟수") + StatPill(top = formatAmount(donationAmount), bottom = "기부 금액") + } +} + +@Composable +private fun StatPill(top: String, bottom: String) { + Column( + Modifier.padding(horizontal = 20.dp, vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + top, + color = Color.White, + fontWeight = FontWeight.SemiBold, + style = AppTypography.bodyLarge + ) + Spacer(Modifier.height(4.dp)) + Text(bottom, color = Color.White.copy(alpha = 0.95f), fontSize = 12.sp) + } +} + +@Composable +private fun PiggyTabBar(selected: PiggyTab, onSelected: (PiggyTab) -> Unit) { + val items = PiggyTab.values() + TabRow( + selectedTabIndex = items.indexOf(selected), + containerColor = Color.Transparent, + contentColor = TiggleBlue, + indicator = { tabPositions -> + TabRowDefaults.SecondaryIndicator( + Modifier.tabIndicatorOffset(tabPositions[items.indexOf(selected)]), + height = 3.dp, + color = TiggleBlue + ) + } + ) { + items.forEach { tab -> + Tab( + selected = tab == selected, + onClick = { onSelected(tab) }, + text = { Text(text = tab.label) }, + ) + } + } +} + +@Composable +private fun SectionTitle(text: String) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(20.dp)) { + Box( + modifier = Modifier + .width(4.dp) + .height(20.dp) + .background(TiggleBlue, RoundedCornerShape(2.dp)) + ) + Spacer(Modifier.width(8.dp)) + Text(text, fontWeight = FontWeight.SemiBold, fontSize = 16.sp) + } +} + +@Composable +private fun EntryItemCard(item: PiggyBankEntry, onClick: () -> Unit) { + val border = MaterialTheme.colorScheme.outline.copy(alpha = 0.25f) + Card( + shape = RoundedCornerShape(16.dp), + border = BorderStroke(1.dp, border), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 아이콘 매핑 + val icon = when (item.type.lowercase()) { + "자투리", "spare", "weekly", "change" -> R.drawable.coin_icon + "더치페이", "dutch", "dutchpay" -> R.drawable.dutchpay_icon + else -> null + } + + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + if (icon != null) Image( + painter = painterResource(icon), + contentDescription = null + ) + } + + Spacer(Modifier.width(12.dp)) + + Column(Modifier.weight(1f)) { + Text( + item.title, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.height(2.dp)) + Text( + item.occurredAt, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 12.sp + ) + } + + Spacer(Modifier.width(8.dp)) + + Column(horizontalAlignment = Alignment.End) { + Text( + text = "+ ${formatAmount(item.amount)}", + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + Spacer(Modifier.height(6.dp)) + Text( + text = if (item.type.lowercase() in listOf( + "자투리", + "spare", + "weekly", + "change" + ) + ) "주간 적립" else "더치페이", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 11.sp + ) + } + } + } +} + +@Composable +private fun InfoTipCard( + title: String, + message: String, + modifier: Modifier = Modifier +) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TiggleSkyBlue), + modifier = modifier.fillMaxWidth(), + ) { + Row(Modifier.padding(16.dp), verticalAlignment = Alignment.Top) { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape), + contentAlignment = Alignment.Center + ) { Text(text = "\uD83D\uDCA1") } + + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Text( + title, + color = TiggleBlueLight, + fontWeight = FontWeight.SemiBold, + style = AppTypography.bodyMedium + ) + Spacer(Modifier.height(6.dp)) + Text( + text = message, + color = TiggleGrayText, + lineHeight = 18.sp, + style = AppTypography.bodyMedium + ) + } + } + } +} + +// --------------------------- +// Preview +// --------------------------- + +private fun sampleEntriesSpare() = listOf( + PiggyBankEntry( + id = "1", + type = "CHANGE", + amount = 600, + occurredAt = "8월 18일 · 계좌 잔액 자투리", + title = "8월의 4번째 자투리 적립" + ), + PiggyBankEntry( + id = "2", + type = "CHANGE", + amount = 300, + occurredAt = "8월 11일 · 계좌 잔액 자투리", + title = "8월의 3번째 자투리 적립" + ) +) + +private fun sampleEntriesDutch() = listOf( + PiggyBankEntry( + id = "3", type = "DUTCHPAY", amount = 50, occurredAt = "오후 10:30 · 4명 참여", title = "치킨 더치페이" + ), + PiggyBankEntry( + id = "4", + type = "DUTCHPAY", + amount = 300, + occurredAt = "오후 11:30 · 2명 참여(전우)", + title = "택시 더치페이" + ) +) + +@Preview(showBackground = true, widthDp = 360, name = "자투리 탭") +@Composable +fun PreviewPiggyBankDetails_Spare() { + val state = PiggyBankState( + piggyBank = PiggyBank( + currentAmount = 847, + savingCount = 5, + donationCount = 3, + donationTotalAmount = 500 + ), + selectedTab = PiggyTab.SpareChange, + changeList = sampleEntriesSpare(), + dutchpayList = sampleEntriesDutch() + ) + + MaterialTheme { + PiggyBankDetailsScreen(uiState = state) + } +} + +@Preview(showBackground = true, widthDp = 360, name = "더치페이 탭") +@Composable +fun PreviewPiggyBankDetails_Dutch() { + val state = PiggyBankState( + piggyBank = PiggyBank( + currentAmount = 847, + savingCount = 5, + donationCount = 3, + donationTotalAmount = 500 + ), + selectedTab = PiggyTab.DutchPay, + changeList = sampleEntriesSpare(), + dutchpayList = sampleEntriesDutch() + ) + + MaterialTheme { + PiggyBankDetailsScreen(uiState = state) + } +} 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 70fd8ac..1fa69e7 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 @@ -8,9 +8,9 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row 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 @@ -24,6 +24,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -42,6 +44,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import com.ssafy.tiggle.R import com.ssafy.tiggle.core.utils.Formatter import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout @@ -58,112 +63,134 @@ fun PiggyBankScreen( onOpenAccountClick: () -> Unit = {}, onRegisterAccountClick: () -> Unit = {}, onStartDutchPayClick: () -> Unit = {}, - onBackClick: () -> Unit = {}, + onAccountClick: (String) -> Unit = {}, + onShowPiggyBankDetailClick: () -> Unit = {}, + onEditLinkedAccountClick: () -> Unit = {}, viewModel: PiggyBankViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + val lifecycleOwner = LocalLifecycleOwner.current + + // 최초 1회 로드 + LaunchedEffect(Unit) { + viewModel.setPiggyBankAccount() + viewModel.setMainAccount() + } + + //다시 화면으로 돌아왔을 때(ON_RESUME) 갱신 + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.setPiggyBankAccount() + viewModel.setMainAccount() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } TiggleScreenLayout( showBackButton = false, - showLogo = false + showLogo = false, + enableScroll = true, + contentPadding = PaddingValues(horizontal = 12.dp) ) { - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 12.dp) - ) { - Spacer(Modifier.height(30.dp)) - Text( - text = "티끌 저금통", - color = Color.Black, - fontSize = 22.sp, - style = AppTypography.headlineLarge - ) - Spacer(Modifier.height(6.dp)) - Text( - text = "작은 돈도 모으면 큰 힘이 됩니다", - color = TiggleGrayText, - fontSize = 13.sp, - style = AppTypography.bodySmall - ) - Spacer(Modifier.height(50.dp)) - - //계좌 존재 여부에 따라 - if (uiState.hasPiggyBank) { - TodaySavingBanner( - uiState = uiState - ) - } else { - DottedActionCard( - title = "티끌 저금통 개설", - desc = "계좌를 개설해\n티끌 저금통을 채워보세요!", - onClick = onOpenAccountClick - ) - } - Spacer(Modifier.height(10.dp)) - if (uiState.hasLinkedAccount) { - AccountCard( - uiState = uiState - ) - } else { - DottedActionCard( - title = "내 계좌 등록", - desc = "나의 계좌를 등록하면\n티끌 저금통에 잔돈이 자동으로 기부됩니다.", - onClick = onRegisterAccountClick - ) - } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(30.dp)) + Text( + text = "티끌 저금통", + color = Color.Black, + fontSize = 22.sp, + style = AppTypography.headlineLarge + ) + Spacer(Modifier.height(6.dp)) + Text( + text = "작은 돈도 모으면 큰 힘이 됩니다", + color = TiggleGrayText, + fontSize = 13.sp, + style = AppTypography.bodySmall + ) - if (uiState.hasPiggyBank) { - DutchButtonsRow( - onStatus = {}, - onStart = onStartDutchPayClick - ) - } - Spacer(Modifier.height(25.dp)) + Spacer(Modifier.height(50.dp)) + Spacer(Modifier.height(50.dp)) - // 스위치 섹션 - TiggleSwitchRow( - title = "저금통 자동 기부", - subtitle = "일정 금액의 티끌이 쌓이면 기부 단체에 자동으로 기부됩니다.", - checked = uiState.piggyBank.autoDonation, - onCheckedChange = viewModel::onToggleAutoDonation + if (uiState.hasPiggyBank) { + TodaySavingBanner(uiState = uiState, onClick = onShowPiggyBankDetailClick) + } else { + DottedActionCard( + title = "티끌 저금통 개설", + desc = "계좌를 개설해\n티끌 저금통을 채워보세요!", + onClick = onOpenAccountClick ) + } - HorizontalDivider( - color = TiggleGray, - thickness = 0.5.dp, - modifier = Modifier.padding(10.dp) - ) + Spacer(Modifier.height(10.dp)) - // 스위치 섹션 2 - TiggleSwitchRow( - title = "잔돈 자동 저금", - subtitle = "매일 자정에 1,000원 미만 잔돈을 자동으로 저금합니다.", - checked = uiState.piggyBank.autoSaving, - onCheckedChange = viewModel::onToggleAutoSaving + if (uiState.hasLinkedAccount) { + AccountCard( + uiState = uiState, onClick = { accountNo -> + onAccountClick(accountNo) + }, + onEditClick = { onEditLinkedAccountClick() }) + } else { + DottedActionCard( + title = "내 계좌 등록", + desc = "나의 계좌를 등록하면\n티끌 저금통에 잔돈이 자동으로 기부됩니다.", + onClick = onRegisterAccountClick ) + } - 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, - modifier = Modifier.padding(10.dp) + Spacer(Modifier.height(16.dp)) + + if (uiState.hasPiggyBank) { + DutchButtonsRow( + onStatus = {}, + onStart = onStartDutchPayClick ) + } + + Spacer(Modifier.height(20.dp)) + + TiggleSwitchRow( + title = "저금통 자동 기부", + subtitle = "일정 금액의 티끌이 쌓이면 \n기부 단체에 자동으로 기부됩니다.", + checked = uiState.piggyBank.autoDonation, + onCheckedChange = viewModel::onToggleAutoDonation + ) - Spacer(Modifier.height(24.dp)) + HorizontalDivider( + color = TiggleGray, + thickness = 0.5.dp, + modifier = Modifier.padding(10.dp) + ) + + TiggleSwitchRow( + title = "잔돈 자동 저금", + subtitle = "매일 자정에 1,000원 미만 잔돈을 자동으로 저금합니다.", + checked = uiState.piggyBank.autoSaving, + onCheckedChange = viewModel::onToggleAutoSaving + ) + + HorizontalDivider( + color = TiggleGray, + thickness = 0.5.dp, + modifier = Modifier.padding(10.dp) + ) + + Spacer(Modifier.height(24.dp)) + + if (uiState.showEsgCategorySheet) { + EsgCategoryBottomSheet( + show = uiState.showEsgCategorySheet, + selectedId = uiState.piggyBank.esgCategory?.id, + onPick = viewModel::onPickEsgCategory, + onConfirm = viewModel::onConfirmAutoDonation, + onDismiss = viewModel::onDismissEsgSheet + ) } } } + @Composable private fun DottedActionCard( title: String, @@ -248,7 +275,7 @@ private fun PlusIcon(color: Color) { } @Composable -private fun TodaySavingBanner(uiState: PiggyBankState) { +private fun TodaySavingBanner(uiState: PiggyBankState, onClick: () -> Unit) { val radius = 18.dp Box( modifier = Modifier @@ -265,6 +292,7 @@ private fun TodaySavingBanner(uiState: PiggyBankState) { ) ) .padding(horizontal = 20.dp, vertical = 18.dp) + .clickable { onClick() } ) { Row(verticalAlignment = Alignment.CenterVertically) { Column(Modifier.weight(1f)) { @@ -305,7 +333,11 @@ private fun TodaySavingBanner(uiState: PiggyBankState) { } @Composable -private fun AccountCard(uiState: PiggyBankState) { +private fun AccountCard( + uiState: PiggyBankState, + onClick: (String) -> Unit, + onEditClick: () -> Unit +) { val radius = 14.dp Column( modifier = Modifier @@ -315,6 +347,7 @@ private fun AccountCard(uiState: PiggyBankState) { .background(Color.White) .border(1.dp, Color(0x11000000), RoundedCornerShape(radius)) .padding(16.dp) + .clickable { onClick(uiState.mainAccount.accountNo) } ) { Row(verticalAlignment = Alignment.CenterVertically) { Box( @@ -345,16 +378,18 @@ private fun AccountCard(uiState: PiggyBankState) { Image( painter = painterResource(id = R.drawable.linked_card_option), contentDescription = "옵션 버튼", - Modifier.size(20.dp) + Modifier + .size(20.dp) + .clickable { onEditClick() } ) } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(17.dp)) Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End) { Text("잔액", color = TiggleGrayText, style = AppTypography.bodySmall) Spacer(Modifier.height(5.dp)) Text( - "${uiState.mainAccount.balance}원", + "${formatAmount(uiState.mainAccount.balance.toLong())}원", color = Color.Black, fontSize = 22.sp, fontWeight = FontWeight.SemiBold 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 1621d58..ac5d9a1 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,8 +1,10 @@ package com.ssafy.tiggle.presentation.ui.piggybank import com.ssafy.tiggle.domain.entity.piggybank.MainAccount +import com.ssafy.tiggle.domain.entity.piggybank.MainAccountDetail import com.ssafy.tiggle.domain.entity.piggybank.PiggyBank import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankAccount +import com.ssafy.tiggle.domain.entity.piggybank.PiggyBankEntry data class PiggyBankState( val piggyBankAccount: PiggyBankAccount = PiggyBankAccount(), @@ -17,7 +19,17 @@ data class PiggyBankState( val showEsgCategorySheet: Boolean = false, val tempSelectedCategoryId: Int? = null, // 시트에서 임시 선택 + //주계좌 상세보기 + val mainAccountDetail: MainAccountDetail = MainAccountDetail(), + + //저금통 내역 상세보기 + val changeList: List = emptyList(), + val dutchpayList: List = emptyList(), + val selectedTab: PiggyTab = PiggyTab.SpareChange, + val isLoading: Boolean = false, // 전체 에러 메시지 val errorMessage: String? = null -) \ No newline at end of file +) + +enum class PiggyTab(val label: String) { SpareChange("자투리"), DutchPay("더치페이") } 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 efc8a22..ce9f6de 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 @@ -5,12 +5,14 @@ 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.async 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 java.text.DecimalFormat import javax.inject.Inject @HiltViewModel @@ -251,4 +253,107 @@ class PiggyBankViewModel @Inject constructor( } } -} \ No newline at end of file + fun loadTransactions(accountNo: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + useCases.getMainAccountDetailUseCase(accountNo, null) + .onSuccess { detail -> + _uiState.update { it.copy(mainAccountDetail = detail, isLoading = false) } + } + .onFailure { e -> + _uiState.update { it.copy(errorMessage = e.message, isLoading = false) } + } + } + } + + fun loadAllPiggyEntries( + changeCursor: String? = null, + dutchCursor: String? = null, + size: Int? = 20, + sortKey: String? = null + ) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + // 병렬 호출 + val changeDeferred = async { + useCases.getPiggyBankEntryUseCase( + type = "CHANGE", + cursor = changeCursor, + size = size, + sortKey = sortKey + ) + } + val dutchDeferred = async { + useCases.getPiggyBankEntryUseCase( + type = "DUTCHPAY", + cursor = dutchCursor, + size = size, + sortKey = sortKey + ) + } + + val changeRes = changeDeferred.await() + val dutchRes = dutchDeferred.await() + + val changeList = changeRes.getOrElse { emptyList() } + val dutchList = dutchRes.getOrElse { emptyList() } + + val error = changeRes.exceptionOrNull()?.message + ?: dutchRes.exceptionOrNull()?.message + + _uiState.update { + it.copy( + changeList = changeList, + dutchpayList = dutchList, + isLoading = false, + errorMessage = error + ) + } + } + } + + fun reloadEntriesByType( + type: String, + cursor: String? = null, + size: Int? = 20, + sortKey: String? = null + ) { + viewModelScope.launch { + val result = useCases.getPiggyBankEntryUseCase(type, cursor, size, null, null, sortKey) + result.onSuccess { list -> + _uiState.update { s -> + when (type) { + "CHANGE" -> s.copy(changeList = list) + "DUTCHPAY" -> s.copy(dutchpayList = list) + else -> s + } + } + }.onFailure { e -> + _uiState.update { it.copy(errorMessage = e.message) } + } + } + } + + fun setSelectedTab(tab: PiggyTab) { + _uiState.update { it.copy(selectedTab = tab) } + } +} + +fun formatAmount(amount: Long): String { + val df = DecimalFormat("#,###") + return df.format(amount) +} + +/** "YYYY-MM-DD" → "M.D" 로 포맷 (예: "2025-08-20" → "8.20") */ +fun formatMonthDay(date: String): String { + // date가 "YYYY-MM-DD" 라고 가정 + return try { + val mm = date.substring(5, 7).trimStart('0') + val dd = date.substring(8, 10).trimStart('0') + "$mm.$dd" + } catch (_: Exception) { + date // 실패 시 원문 출력 + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/RegisterAccountScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/RegisterAccountScreen.kt index 0fa180c..d27df37 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/RegisterAccountScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/piggybank/RegisterAccountScreen.kt @@ -15,10 +15,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults.outlinedButtonBorder import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -58,6 +56,7 @@ fun RegisterAccountScreen( modifier: Modifier = Modifier, viewModel: RegisterAccountViewModel = hiltViewModel(), onBackClick: () -> Unit = {}, + isEdit: Boolean = false, onFinish: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsState() @@ -70,6 +69,7 @@ fun RegisterAccountScreen( viewModel.goToPreviousStep() } } + val title = if (isEdit) "계좌 수정" else "계좌 등록" when (uiState.registerAccountStep) { RegisterAccountStep.ACCOUNT -> { @@ -78,7 +78,8 @@ fun RegisterAccountScreen( onBackClick = handleTopBack, onAccountChange = viewModel::updateAccountNum, onConfirmClick = { viewModel.fetchAccountHolder() }, - onDismissError = viewModel::clearError + onDismissError = viewModel::clearError, + title = title ) } @@ -86,7 +87,8 @@ fun RegisterAccountScreen( AccountInputSuccessScreen( uiState = uiState, onBackClick = handleTopBack, - onStartVerification = { viewModel.requestOneWon() } + onStartVerification = { viewModel.requestOneWon() }, + title = title ) } @@ -95,7 +97,8 @@ fun RegisterAccountScreen( SendCodeScreen( uiState = uiState, onBackClick = handleTopBack, - onNextClick = { viewModel.goToNextStep() } + onNextClick = { viewModel.goToNextStep() }, + title = title ) } @@ -105,7 +108,8 @@ fun RegisterAccountScreen( onCodeChange = viewModel::updateCode, onBackClick = handleTopBack, onResendClick = { viewModel.resendOneWon() }, - onNextClick = { viewModel.confirmCodeAndRegisterPrimary() } + onNextClick = { viewModel.confirmCodeAndRegisterPrimary() }, + title = title ) } @@ -126,12 +130,14 @@ fun AccountInputScreen( onBackClick: () -> Unit, onAccountChange: (String) -> Unit, onConfirmClick: () -> Unit, - onDismissError: () -> Unit + onDismissError: () -> Unit, + title: String ) { TiggleScreenLayout( showBackButton = true, - title = "계좌 등록", + title = title, onBackClick = onBackClick, + enableScroll = true, bottomButton = { val keyboard = LocalSoftwareKeyboardController.current val buttonEnabled = @@ -222,11 +228,13 @@ fun AccountInputSuccessScreen( uiState: RegisterAccountState, onBackClick: () -> Unit, onStartVerification: () -> Unit, + title: String ) { TiggleScreenLayout( showBackButton = true, - title = "계좌 등록", + title = title, onBackClick = onBackClick, + enableScroll = true, bottomButton = { TiggleButton( text = if (uiState.isLoading) "요청 중..." else "1원 인증 시작", @@ -345,13 +353,14 @@ fun SendCodeScreen( uiState: RegisterAccountState, onBackClick: () -> Unit, onNextClick: () -> Unit, + title: String ) { // 하단 버튼 영역 만큼의 여유 (필요에 따라 조정: 80~96dp 권장) val bottomBarPadding = 96.dp TiggleScreenLayout( showBackButton = true, - title = "계좌 등록", + title = title, onBackClick = onBackClick, bottomButton = { TiggleButton( @@ -365,10 +374,7 @@ fun SendCodeScreen( Column( modifier = Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 20.dp) - // 하단 고정 버튼과 겹치지 않도록 여유 공간 확보 - .padding(bottom = bottomBarPadding), + .padding(horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -477,6 +483,7 @@ fun CertificationScreen( onCodeChange: (String) -> Unit, onResendClick: () -> Unit, onNextClick: () -> Unit, + title: String ) { val code = uiState.registerAccount.code val error = uiState.registerAccount.codeError @@ -484,7 +491,7 @@ fun CertificationScreen( TiggleScreenLayout( showBackButton = true, - title = "계좌 등록", + title = title, onBackClick = onBackClick, bottomButton = { val enabled = uiState.registerAccount.code.length == 4 && @@ -498,9 +505,7 @@ fun CertificationScreen( ) { Column( - modifier = Modifier - .padding(20.dp) - .verticalScroll(rememberScrollState()), + modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -679,9 +684,7 @@ fun RegisterSuccessScreen( } ) { Column( - modifier = Modifier - .padding(20.dp) - .verticalScroll(rememberScrollState()), + modifier = Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -773,7 +776,8 @@ fun AccountInputPreview() { onBackClick = {}, onAccountChange = {}, onConfirmClick = {}, - onDismissError = {} + onDismissError = {}, + title = "" ) } @@ -790,7 +794,8 @@ fun AccountInputSuccessPreview() { ) ), onBackClick = {}, - onStartVerification = {} + onStartVerification = {}, + title = "" ) } @@ -803,7 +808,8 @@ fun PreviewOneWonTransferScreen() { registerAccount = RegisterAccount(accountNum = "1234567890") ), onBackClick = {}, - onNextClick = {} + onNextClick = {}, + title = "" ) } @@ -823,7 +829,8 @@ fun PreviewCertificationScreen_Success() { onBackClick = {}, onCodeChange = {}, onResendClick = {}, - onNextClick = {} + onNextClick = {}, + title = "" ) } diff --git a/app/src/main/res/drawable/coin_icon.png b/app/src/main/res/drawable/coin_icon.png new file mode 100644 index 0000000..7440acc Binary files /dev/null and b/app/src/main/res/drawable/coin_icon.png differ diff --git a/app/src/main/res/drawable/dutchpay_icon.png b/app/src/main/res/drawable/dutchpay_icon.png new file mode 100644 index 0000000..fc188c2 Binary files /dev/null and b/app/src/main/res/drawable/dutchpay_icon.png differ