diff --git a/app/src/main/java/com/ssafy/tiggle/core/utils/Formatter.kt b/app/src/main/java/com/ssafy/tiggle/core/utils/Formatter.kt index ed2a398..48b998f 100644 --- a/app/src/main/java/com/ssafy/tiggle/core/utils/Formatter.kt +++ b/app/src/main/java/com/ssafy/tiggle/core/utils/Formatter.kt @@ -24,6 +24,19 @@ object Formatter { } } + // 날짜만 포맷팅: "2025-08-26T01:25:21" -> "2025.08.26" + @SuppressLint("NewApi") + fun formatDate(isoDateTime: String): String { + return try { + val dateTime = LocalDateTime.parse(isoDateTime) + val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + dateTime.format(formatter) + } catch (e: DateTimeParseException) { + // 파싱 실패 시 원본 문자열 반환 + isoDateTime + } + } + // 간단한 날짜 포맷팅: "2025-08-26T01:25:21" -> "8월 26일" @SuppressLint("NewApi") fun formatDateOnly(isoDateTime: String): String { diff --git a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DutchPayApiService.kt b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DutchPayApiService.kt index c5099ba..71e120e 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DutchPayApiService.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DutchPayApiService.kt @@ -2,13 +2,18 @@ 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.dutchpay.request.DutchPayPaymentRequestDto import com.ssafy.tiggle.data.model.dutchpay.request.DutchPayRequestDto import com.ssafy.tiggle.data.model.dutchpay.response.DutchPayRequestDetailResponseDto +import com.ssafy.tiggle.data.model.dutchpay.response.DutchPaySummaryResponseDto +import com.ssafy.tiggle.data.model.dutchpay.response.DutchPayListResponseDto +import com.ssafy.tiggle.data.model.dutchpay.response.DutchPayDetailResponseDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface DutchPayApiService { @POST("/api/dutchpay/requests") @@ -20,4 +25,24 @@ interface DutchPayApiService { suspend fun getDutchPayRequestDetail( @Path("id") dutchPayId: Long ): Response> + + @GET("/api/dutchpay/requests/{id}") + suspend fun getDutchPayDetail( + @Path("id") dutchPayId: Long + ): Response> + + @POST("/api/dutchpay/requests/{dutchpayId}/pay") + suspend fun payDutchPay( + @Path("dutchpayId") dutchPayId: Long, + @Body request: DutchPayPaymentRequestDto + ): Response> + + @GET("/api/dutchpay/requests/summary") + suspend fun getDutchPaySummary(): Response> + + @GET("/api/dutchpay/requests/list") + suspend fun getDutchPayList( + @Query("tab") tab: String, + @Query("cursor") cursor: String? = null + ): Response> } diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/request/DutchPayPaymentRequestDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/request/DutchPayPaymentRequestDto.kt new file mode 100644 index 0000000..72b8607 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/request/DutchPayPaymentRequestDto.kt @@ -0,0 +1,5 @@ +package com.ssafy.tiggle.data.model.dutchpay.request + +data class DutchPayPaymentRequestDto( + val payMore: Boolean +) diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayDetailResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayDetailResponseDto.kt new file mode 100644 index 0000000..4383686 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayDetailResponseDto.kt @@ -0,0 +1,31 @@ +package com.ssafy.tiggle.data.model.dutchpay.response + +import kotlinx.serialization.Serializable + +@Serializable +data class DutchPayDetailResponseDto( + val id: Long, + val title: String, + val message: String, + val totalAmount: Int, + val status: String, + val creator: CreatorDto, + val shares: List, + val roundedPerPerson: Int?, + val payMore: Boolean, + val createdAt: String +) + +@Serializable +data class CreatorDto( + val id: Long, + val name: String +) + +@Serializable +data class ShareDto( + val userId: Long, + val name: String, + val amount: Int, + val status: String +) diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayListResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayListResponseDto.kt new file mode 100644 index 0000000..bf0f6ff --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPayListResponseDto.kt @@ -0,0 +1,24 @@ +package com.ssafy.tiggle.data.model.dutchpay.response + +import kotlinx.serialization.Serializable + +@Serializable +data class DutchPayListResponseDto( + val items: List, + val nextCursor: String?, + val hasNext: Boolean +) + +@Serializable +data class DutchPayItemDto( + val dutchpayId: Long, + val title: String, + val myAmount: Int, + val totalAmount: Int, + val participantCount: Int, + val paidCount: Int, + val requestedAt: String, + val isCreator: Boolean, + val creatorName: String, + val tiggleAmount: Int +) diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPaySummaryResponseDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPaySummaryResponseDto.kt new file mode 100644 index 0000000..d7c4622 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/dutchpay/response/DutchPaySummaryResponseDto.kt @@ -0,0 +1,10 @@ +package com.ssafy.tiggle.data.model.dutchpay.response + +import kotlinx.serialization.Serializable + +@Serializable +data class DutchPaySummaryResponseDto( + val totalTransferredAmount: Int, + val transferCount: Int, + val participatedCount: Int +) diff --git a/app/src/main/java/com/ssafy/tiggle/data/repository/DutchPayRepositoryImpl.kt b/app/src/main/java/com/ssafy/tiggle/data/repository/DutchPayRepositoryImpl.kt index 074f678..2b678fb 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/repository/DutchPayRepositoryImpl.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/repository/DutchPayRepositoryImpl.kt @@ -2,8 +2,15 @@ package com.ssafy.tiggle.data.repository import com.ssafy.tiggle.data.datasource.remote.DutchPayApiService import com.ssafy.tiggle.data.model.dutchpay.request.DutchPayRequestDto +import com.ssafy.tiggle.data.model.dutchpay.request.DutchPayPaymentRequestDto import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequest import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequestDetail +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPaySummary +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayList +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayItem +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayDetail +import com.ssafy.tiggle.domain.entity.dutchpay.Creator +import com.ssafy.tiggle.domain.entity.dutchpay.Share import com.ssafy.tiggle.domain.repository.DutchPayRepository import javax.inject.Inject import javax.inject.Singleton @@ -67,4 +74,123 @@ class DutchPayRepositoryImpl @Inject constructor( Result.failure(e) } } + + override suspend fun getDutchPayDetail(dutchPayId: Long): Result { + return try { + val response = dutchPayApiService.getDutchPayDetail(dutchPayId) + + if (response.isSuccessful) { + val responseData = response.body()?.data + if (responseData != null) { + val detail = DutchPayDetail( + id = responseData.id, + title = responseData.title, + message = responseData.message, + totalAmount = responseData.totalAmount, + status = responseData.status, + creator = Creator( + id = responseData.creator.id, + name = responseData.creator.name + ), + shares = responseData.shares.map { shareDto -> + Share( + userId = shareDto.userId, + name = shareDto.name, + amount = shareDto.amount, + status = shareDto.status + ) + }, + roundedPerPerson = responseData.roundedPerPerson, + payMore = responseData.payMore, + createdAt = responseData.createdAt + ) + Result.success(detail) + } else { + Result.failure(Exception("응답 데이터가 없습니다")) + } + } else { + Result.failure(Exception("더치페이 상세 조회 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun payDutchPay(dutchPayId: Long, payMore: Boolean): Result { + return try { + val requestDto = DutchPayPaymentRequestDto(payMore = payMore) + val response = dutchPayApiService.payDutchPay(dutchPayId, requestDto) + + if (response.isSuccessful) { + Result.success(Unit) + } else { + Result.failure(Exception("송금 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getDutchPaySummary(): Result { + return try { + val response = dutchPayApiService.getDutchPaySummary() + + if (response.isSuccessful) { + val responseData = response.body()?.data + if (responseData != null) { + val summary = DutchPaySummary( + totalTransferredAmount = responseData.totalTransferredAmount, + transferCount = responseData.transferCount, + participatedCount = responseData.participatedCount + ) + Result.success(summary) + } else { + Result.failure(Exception("응답 데이터가 없습니다")) + } + } else { + Result.failure(Exception("더치페이 현황 조회 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getDutchPayList(tab: String, cursor: String?): Result { + return try { + val response = dutchPayApiService.getDutchPayList(tab, cursor) + + if (response.isSuccessful) { + val responseData = response.body()?.data + if (responseData != null) { + val items = responseData.items.map { itemDto -> + DutchPayItem( + dutchpayId = itemDto.dutchpayId, + title = itemDto.title, + myAmount = itemDto.myAmount, + totalAmount = itemDto.totalAmount, + participantCount = itemDto.participantCount, + paidCount = itemDto.paidCount, + requestedAt = itemDto.requestedAt, + isCreator = itemDto.isCreator, + creatorName = itemDto.creatorName, + tiggleAmount = itemDto.tiggleAmount + ) + } + + val list = DutchPayList( + items = items, + nextCursor = responseData.nextCursor, + hasNext = responseData.hasNext + ) + Result.success(list) + } else { + Result.failure(Exception("응답 데이터가 없습니다")) + } + } else { + Result.failure(Exception("더치페이 내역 조회 실패: ${response.code()}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayDetail.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayDetail.kt new file mode 100644 index 0000000..84e96af --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayDetail.kt @@ -0,0 +1,26 @@ +package com.ssafy.tiggle.domain.entity.dutchpay + +data class DutchPayDetail( + val id: Long, + val title: String, + val message: String, + val totalAmount: Int, + val status: String, + val creator: Creator, + val shares: List, + val roundedPerPerson: Int?, + val payMore: Boolean, + val createdAt: String +) + +data class Creator( + val id: Long, + val name: String +) + +data class Share( + val userId: Long, + val name: String, + val amount: Int, + val status: String +) diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayItem.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayItem.kt new file mode 100644 index 0000000..5571b6a --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayItem.kt @@ -0,0 +1,14 @@ +package com.ssafy.tiggle.domain.entity.dutchpay + +data class DutchPayItem( + val dutchpayId: Long, + val title: String, + val myAmount: Int, + val totalAmount: Int, + val participantCount: Int, + val paidCount: Int, + val requestedAt: String, + val isCreator: Boolean, + val creatorName: String, + val tiggleAmount: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayList.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayList.kt new file mode 100644 index 0000000..fbcc256 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPayList.kt @@ -0,0 +1,8 @@ +package com.ssafy.tiggle.domain.entity.dutchpay + +data class DutchPayList( + val items: List, + val nextCursor: String?, + val hasNext: Boolean +) + diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPaySummary.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPaySummary.kt new file mode 100644 index 0000000..16d0827 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/dutchpay/DutchPaySummary.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.entity.dutchpay + +data class DutchPaySummary( + val totalTransferredAmount: Int, + val transferCount: Int, + val participatedCount: Int +) diff --git a/app/src/main/java/com/ssafy/tiggle/domain/repository/DutchPayRepository.kt b/app/src/main/java/com/ssafy/tiggle/domain/repository/DutchPayRepository.kt index 62a0987..77bc4be 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/repository/DutchPayRepository.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/repository/DutchPayRepository.kt @@ -2,8 +2,15 @@ package com.ssafy.tiggle.domain.repository import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequestDetail import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequest +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPaySummary +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayList +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayDetail interface DutchPayRepository { suspend fun createDutchPayRequest(request: DutchPayRequest): Result suspend fun getDutchPayRequestDetail(dutchPayId: Long): Result + suspend fun getDutchPayDetail(dutchPayId: Long): Result + suspend fun payDutchPay(dutchPayId: Long, payMore: Boolean): Result + suspend fun getDutchPaySummary(): Result + suspend fun getDutchPayList(tab: String, cursor: String? = null): Result } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPayListUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPayListUseCase.kt new file mode 100644 index 0000000..3783592 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPayListUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.dutchpay + +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayList +import com.ssafy.tiggle.domain.repository.DutchPayRepository +import javax.inject.Inject + +class GetDutchPayListUseCase @Inject constructor( + private val dutchPayRepository: DutchPayRepository +) { + suspend operator fun invoke(tab: String, cursor: String? = null): Result { + return dutchPayRepository.getDutchPayList(tab, cursor) + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPaySummaryUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPaySummaryUseCase.kt new file mode 100644 index 0000000..8c72507 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/GetDutchPaySummaryUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.dutchpay + +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPaySummary +import com.ssafy.tiggle.domain.repository.DutchPayRepository +import javax.inject.Inject + +class GetDutchPaySummaryUseCase @Inject constructor( + private val dutchPayRepository: DutchPayRepository +) { + suspend operator fun invoke(): Result { + return dutchPayRepository.getDutchPaySummary() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/PayDutchPayUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/PayDutchPayUseCase.kt new file mode 100644 index 0000000..3570f0f --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/dutchpay/PayDutchPayUseCase.kt @@ -0,0 +1,12 @@ +package com.ssafy.tiggle.domain.usecase.dutchpay + +import com.ssafy.tiggle.domain.repository.DutchPayRepository +import javax.inject.Inject + +class PayDutchPayUseCase @Inject constructor( + private val dutchPayRepository: DutchPayRepository +) { + suspend operator fun invoke(dutchPayId: Long, payMore: Boolean): Result { + return dutchPayRepository.payDutchPay(dutchPayId, payMore) + } +} 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 e5f8b34..5bece68 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 @@ -20,6 +20,8 @@ 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.dutchpay.DutchpayRecieveScreen +import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayStatusScreen +import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayDetailScreen import com.ssafy.tiggle.presentation.ui.growth.GrowthScreen import com.ssafy.tiggle.presentation.ui.piggybank.MainAccountDetailScreen import com.ssafy.tiggle.presentation.ui.piggybank.OpenAccountMode @@ -140,6 +142,9 @@ fun NavigationGraph( onStartDutchPayClick = { navBackStack.add(Screen.CreateDutchPay) }, + onDutchPayStatusClick = { + navBackStack.add(Screen.DutchPayStatus) + }, onAccountClick = { accountNo -> navBackStack.add(Screen.MainAccountDetail(accountNo)) }, @@ -179,7 +184,15 @@ fun NavigationGraph( is Screen.DutchpayRecieve -> NavEntry(key) { // key에서 dutchPayId를 직접 꺼내서 화면에 전달합니다. - DutchpayRecieveScreen(dutchPayId = key.dutchPayId) + DutchpayRecieveScreen( + dutchPayId = key.dutchPayId, + onBackClick = { navBackStack.removeLastOrNull() }, + onPaymentClick = { + // 송금 성공 후 piggybank 화면으로 이동 + navBackStack.clear() + navBackStack.add(BottomScreen.PiggyBank) + } + ) } is Screen.MainAccountDetail -> NavEntry(key) { @@ -207,6 +220,22 @@ fun NavigationGraph( ) } + is Screen.DutchPayStatus -> NavEntry(key) { + DutchPayStatusScreen( + onBackClick = { navBackStack.removeLastOrNull() }, + onItemClick = { dutchPayId -> + navBackStack.add(Screen.DutchPayDetail(dutchPayId)) + } + ) + } + + is Screen.DutchPayDetail -> NavEntry(key) { + DutchPayDetailScreen( + dutchPayId = key.dutchPayId, + onBackClick = { navBackStack.removeLastOrNull() } + ) + } + else -> throw IllegalArgumentException("Unknown route: $key") } 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 caadc2e..ccd2739 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 @@ -51,5 +51,11 @@ sealed interface Screen : NavKey { @Serializable data class DutchpayRecieve(val dutchPayId: Long) : Screen + + @Serializable + object DutchPayStatus : Screen + + @Serializable + data class DutchPayDetail(val dutchPayId: Long) : Screen } 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 81d26bf..149a550 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 @@ -1,5 +1,7 @@ package com.ssafy.tiggle.presentation.ui.dutchpay +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -13,6 +15,7 @@ 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.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions @@ -27,6 +30,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,6 +57,9 @@ import com.ssafy.tiggle.presentation.ui.components.UserPicker import com.ssafy.tiggle.presentation.ui.theme.AppTypography import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayLight +import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayState +import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayStep +import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayViewModel import kotlin.math.ceil @Composable @@ -94,9 +103,9 @@ fun CreateDutchPayScreen( } }, enabled = when (uiState.step) { - CreateDutchPayStep.PICK_USERS -> uiState.selectedUserIds.isNotEmpty() - CreateDutchPayStep.INPUT_AMOUNT -> uiState.amountText.isNotBlank() && uiState.title.isNotBlank() - CreateDutchPayStep.COMPLETE -> true + CreateDutchPayStep.PICK_USERS -> uiState.selectedUserIds.isNotEmpty() && !uiState.isLoading + CreateDutchPayStep.INPUT_AMOUNT -> uiState.amountText.isNotBlank() && uiState.title.isNotBlank() && !uiState.isLoading + CreateDutchPayStep.COMPLETE -> !uiState.isLoading }, isLoading = uiState.isLoading, variant = TiggleButtonVariant.Primary @@ -218,6 +227,50 @@ fun DutchPayInputAmountContent( checked = payMore, onCheckedChange = onPayMoreChange ) + + // 티끌 적립 정보 표시 (payMore가 true일 때만) + if (payMore) { + val total = amountText.toLongOrNull() ?: 0L + val participantCount = if (selectedCount > 0) selectedCount + 1 else 0 + if (total > 0 && participantCount > 0) { + val perHead = total.toDouble() / participantCount + val myAmount = roundUpToHundreds(perHead) + val tiggleAmount = myAmount - perHead.toLong() + + if (tiggleAmount > 0) { + Spacer(Modifier.height(12.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F8FF)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🐷", + fontSize = 20.sp + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "티끌 적립", + style = AppTypography.bodyLarge.copy(fontWeight = FontWeight.Bold), + color = Color(0xFF1B6BFF), + modifier = Modifier.weight(1f) + ) + AnimatedNumberCounter( + targetValue = tiggleAmount + ) + } + } + } + } + } + Spacer(Modifier.height(16.dp)) TiggleTextField( value = title, @@ -269,6 +322,25 @@ private fun SelectedUserBadge(name: String) { } } +@Composable +private fun AnimatedNumberCounter( + targetValue: Long, + modifier: Modifier = Modifier +) { + val animatedValue by animateIntAsState( + targetValue = targetValue.toInt(), + animationSpec = tween(durationMillis = 1000), + label = "number_animation" + ) + + Text( + text = "+ ${Formatter.formatCurrency(animatedValue.toLong())}", + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = TiggleBlue, + modifier = modifier + ) +} + @Composable private fun DutchPaySummary(totalAmount: Long, participantCount: Int, payMore: Boolean) { // 1인당 금액 계산 @@ -303,6 +375,30 @@ private fun DutchPaySummary(totalAmount: Long, participantCount: Int, payMore: B HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) Spacer(Modifier.height(8.dp)) SummaryRow(label = "내 결제 금액", value = Formatter.formatCurrency(myAmount)) + + // 티끌 적립 정보 표시 (payMore가 true일 때만) + if (payMore) { + val tiggleAmount = myAmount - perHead.toLong() + if (tiggleAmount > 0) { + Spacer(Modifier.height(8.dp)) + HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color) + Spacer(Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🐷 티끌 적립", + style = AppTypography.bodySmall, + color = TiggleBlue + ) + AnimatedNumberCounter( + targetValue = tiggleAmount + ) + } + } + } Spacer(Modifier.height(8.dp)) } } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayViewModel.kt index 92d4f15..595f4d4 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/CreateDutchPayViewModel.kt @@ -3,8 +3,8 @@ package com.ssafy.tiggle.presentation.ui.dutchpay import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequest -import com.ssafy.tiggle.domain.usecase.dutchpay.GetAllUsersUseCase import com.ssafy.tiggle.domain.usecase.dutchpay.CreateDutchPayRequestUseCase +import com.ssafy.tiggle.domain.usecase.dutchpay.GetAllUsersUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -62,19 +62,25 @@ class CreateDutchPayViewModel @Inject constructor( } fun goNext() { - _uiState.update { current -> - when (current.step) { - CreateDutchPayStep.PICK_USERS -> { - current.copy(step = CreateDutchPayStep.INPUT_AMOUNT) - } + val currentState = _uiState.value + if (currentState.isLoading) { + return // 중복 실행 방지 + } - CreateDutchPayStep.INPUT_AMOUNT -> { - // 더치페이 요청 API 호출 - createDutchPayRequest() - current - } + // when 문을 update 블록 밖으로 꺼냅니다. + when (currentState.step) { + CreateDutchPayStep.PICK_USERS -> { + // 상태 변경만 필요하므로 update 블록 사용 + _uiState.update { it.copy(step = CreateDutchPayStep.INPUT_AMOUNT) } + } + + CreateDutchPayStep.INPUT_AMOUNT -> { + // API 요청 함수를 직접 호출 + createDutchPayRequest() + } - CreateDutchPayStep.COMPLETE -> current + CreateDutchPayStep.COMPLETE -> { + // 이 로직은 UI에서 onFinish 콜백으로 처리되므로 ViewModel에서는 할 일 없음 } } } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailScreen.kt new file mode 100644 index 0000000..6517ca4 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailScreen.kt @@ -0,0 +1,373 @@ +package com.ssafy.tiggle.presentation.ui.dutchpay + +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.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.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider + +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ssafy.tiggle.core.utils.Formatter +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayDetail +import com.ssafy.tiggle.domain.entity.dutchpay.Creator +import com.ssafy.tiggle.domain.entity.dutchpay.Share +import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout +import com.ssafy.tiggle.presentation.ui.theme.AppTypography +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue + +@Composable +fun DutchPayDetailScreen( + dutchPayId: Long, + onBackClick: () -> Unit, + viewModel: DutchPayDetailViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(dutchPayId) { + viewModel.loadDutchPayDetail(dutchPayId) + } + + TiggleScreenLayout( + showBackButton = true, + title = "더치페이 현황", + onBackClick = onBackClick, + enableScroll = true + ) { + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = TiggleBlue) + } + } + + uiState.error != null -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = uiState.error!!, + color = Color.Red, + fontSize = 16.sp + ) + } + } + + uiState.dutchPayDetail != null -> { + DutchPayDetailContent(detail = uiState.dutchPayDetail!!) + } + } + } +} + +@Composable +private fun DutchPayDetailContent(detail: DutchPayDetail) { + var selectedTab by remember { mutableStateOf("PENDING") } // 기본값은 정산 미완료 + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // 요약 카드 + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "${detail.shares.size}명 참여", + style = AppTypography.bodyMedium, + color = Color(0xFF666666) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = Formatter.formatCurrency(detail.totalAmount.toLong()), + style = AppTypography.headlineLarge.copy( + fontSize = 32.sp, + fontWeight = FontWeight.Bold + ), + color = Color.Black + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "요청일 ${Formatter.formatDate(detail.createdAt)}", + style = AppTypography.bodySmall, + color = Color(0xFF999999) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 상태 표시 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + val completedCount = detail.shares.count { it.status == "PAID" } + val pendingCount = detail.shares.count { it.status == "PENDING" } + + StatusItem( + count = completedCount, + label = "정산 완료", + backgroundColor = Color(0xFFE8F5E8) + ) + + StatusItem( + count = pendingCount, + label = "정산 미완료", + backgroundColor = Color(0xFFF0F0F0) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 탭 (선택 가능) + Row( + modifier = Modifier + .fillMaxWidth() + .background(Color(0xFFF5F5F5), RoundedCornerShape(8.dp)) + .padding(4.dp) + ) { + // 정산 완료 탭 + Box( + modifier = Modifier + .weight(1f) + .background( + color = if (selectedTab == "PAID") Color.White else Color.Transparent, + shape = RoundedCornerShape(6.dp) + ) + .clickable { selectedTab = "PAID" } + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + val completedCount = detail.shares.count { it.status == "PAID" } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "${completedCount}명", + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = if (selectedTab == "PAID") TiggleBlue else Color(0xFF666666) + ) + Text( + text = "정산 완료", + style = AppTypography.bodySmall, + color = if (selectedTab == "PAID") TiggleBlue else Color(0xFF999999) + ) + } + } + + // 정산 미완료 탭 + Box( + modifier = Modifier + .weight(1f) + .background( + color = if (selectedTab == "PENDING") Color.White else Color.Transparent, + shape = RoundedCornerShape(6.dp) + ) + .clickable { selectedTab = "PENDING" } + .padding(vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + val pendingCount = detail.shares.count { it.status == "PENDING" } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "${pendingCount}명", + style = AppTypography.bodyMedium.copy( + fontWeight = FontWeight.Medium, + color = if (selectedTab == "PENDING") TiggleBlue else Color(0xFF666666) + ) + ) + Text( + text = "정산 미완료", + style = AppTypography.bodySmall, + color = if (selectedTab == "PENDING") TiggleBlue else Color(0xFF999999) + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 참여자 목록 (선택된 탭에 따라 표시) + val selectedShares = detail.shares.filter { it.status == selectedTab } + + if (selectedShares.isNotEmpty()) { + selectedShares.forEach { share -> + ParticipantItem( + name = share.name, + amount = share.amount.toLong(), + status = share.status + ) + if (share != selectedShares.last()) { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color(0xFFE0E0E0) + ) + } + } + } else { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = if (selectedTab == "PAID") "정산 완료인 참여자가 없습니다" else "정산 미완료인 참여자가 없습니다", + style = AppTypography.bodyMedium, + color = Color(0xFF999999) + ) + } + } + } +} + +@Composable +private fun StatusItem( + count: Int, + label: String, + backgroundColor: Color +) { + Card( + modifier = Modifier + .width(120.dp) + .height(60.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = backgroundColor), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "${count}명", + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Bold) + ) + Text( + text = label, + style = AppTypography.bodySmall, + color = Color(0xFF666666) + ) + } + } +} + +@Composable +private fun ParticipantItem( + name: String, + amount: Long, + status: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = name, + style = AppTypography.bodyMedium, + color = Color.Black + ) + + // 상태 표시 + Box( + modifier = Modifier + .background( + color = if (status == "PAID") Color(0xFFE8F5E8) else Color(0xFFFFF3E0), + shape = RoundedCornerShape(4.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = if (status == "PAID") "완료" else "대기", + style = AppTypography.bodySmall, + color = if (status == "PAID") Color(0xFF2E7D32) else Color(0xFFF57C00) + ) + } + } + + Text( + text = Formatter.formatCurrency(amount), + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Medium), + color = Color.Black + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun DutchPayDetailScreenPreview() { + val sampleDetail = DutchPayDetail( + id = 2L, + title = "치킨 회식", + message = "치킨/맥주 더치페이 부탁!", + totalAmount = 11111, + status = "REQUESTED", + creator = Creator(id = 1L, name = "김테스트"), + shares = listOf( + Share(userId = 1L, name = "김테스트", amount = 3703, status = "PAID"), + Share(userId = 2L, name = "박테스트", amount = 3704, status = "PAID"), + Share(userId = 10L, name = "jiwon", amount = 3704, status = "PENDING") + ), + roundedPerPerson = null, + payMore = false, + createdAt = "2025-08-28T12:46:16" + ) + + DutchPayDetailContent(detail = sampleDetail) +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailViewModel.kt new file mode 100644 index 0000000..1b5a946 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayDetailViewModel.kt @@ -0,0 +1,48 @@ +package com.ssafy.tiggle.presentation.ui.dutchpay + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayDetail +import com.ssafy.tiggle.domain.repository.DutchPayRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DutchPayDetailViewModel @Inject constructor( + private val dutchPayRepository: DutchPayRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(DutchPayDetailUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadDutchPayDetail(dutchPayId: Long) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + + dutchPayRepository.getDutchPayDetail(dutchPayId) + .onSuccess { detail -> + _uiState.value = _uiState.value.copy( + isLoading = false, + dutchPayDetail = detail, + error = null + ) + } + .onFailure { exception -> + _uiState.value = _uiState.value.copy( + isLoading = false, + error = exception.message ?: "더치페이 상세 정보를 불러오는데 실패했습니다." + ) + } + } + } +} + +data class DutchPayDetailUiState( + val isLoading: Boolean = false, + val dutchPayDetail: DutchPayDetail? = null, + val error: String? = null +) diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailUiState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailUiState.kt index 4f40fc1..7c8311e 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailUiState.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailUiState.kt @@ -5,5 +5,6 @@ import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayRequestDetail data class DutchPayRequestDetailUiState( val isLoading: Boolean = false, val dutchPayDetail: DutchPayRequestDetail? = null, - val errorMessage: String? = null + val errorMessage: String? = null, + val isPaymentSuccess: Boolean = false ) diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailViewModel.kt index caa97de..bf39239 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailViewModel.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayRequestDetailViewModel.kt @@ -3,6 +3,7 @@ package com.ssafy.tiggle.presentation.ui.dutchpay import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ssafy.tiggle.domain.usecase.dutchpay.GetDutchPayRequestDetailUseCase +import com.ssafy.tiggle.domain.usecase.dutchpay.PayDutchPayUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -13,7 +14,8 @@ import javax.inject.Inject @HiltViewModel class DutchPayRequestDetailViewModel @Inject constructor( - private val getDutchPayRequestDetailUseCase: GetDutchPayRequestDetailUseCase + private val getDutchPayRequestDetailUseCase: GetDutchPayRequestDetailUseCase, + private val payDutchPayUseCase: PayDutchPayUseCase ) : ViewModel() { private val _uiState = MutableStateFlow(DutchPayRequestDetailUiState()) @@ -44,7 +46,36 @@ class DutchPayRequestDetailViewModel @Inject constructor( } } + fun payDutchPay(dutchPayId: Long, payMore: Boolean) { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + viewModelScope.launch { + payDutchPayUseCase(dutchPayId, payMore) + .onSuccess { + _uiState.update { + it.copy( + isLoading = false, + isPaymentSuccess = true, + errorMessage = null + ) + } + } + .onFailure { exception -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = exception.message ?: "송금에 실패했습니다." + ) + } + } + } + } + fun clearErrorMessage() { _uiState.update { it.copy(errorMessage = null) } } + + fun clearPaymentSuccess() { + _uiState.update { it.copy(isPaymentSuccess = false) } + } } diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusScreen.kt new file mode 100644 index 0000000..6648638 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusScreen.kt @@ -0,0 +1,573 @@ +package com.ssafy.tiggle.presentation.ui.dutchpay + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.tween +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.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.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ssafy.tiggle.core.utils.Formatter +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayItem +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPaySummary +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 kotlinx.coroutines.flow.distinctUntilChanged + +@Composable +fun DutchPayStatusScreen( + onBackClick: () -> Unit, + onItemClick: (Long) -> Unit = {}, + viewModel: DutchPayStatusViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + TiggleScreenLayout( + showBackButton = true, + title = "더치페이 현황", + onBackClick = onBackClick, + enableScroll = false + ) { + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = TiggleBlue) + } + } + + uiState.error != null -> { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = uiState.error!!, + color = Color.Red, + fontSize = 16.sp + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { /* TODO: 재시도 로직 구현 */ }) { + Text("다시 시도") + } + } + } + + uiState.summary != null -> { + DutchPayStatusContent( + summary = uiState.summary!!, + selectedTabIndex = uiState.selectedTabIndex, + inProgressItems = uiState.inProgressItems, + completedItems = uiState.completedItems, + hasNextInProgress = uiState.hasNextInProgress, + hasNextCompleted = uiState.hasNextCompleted, + isLoadingMore = uiState.isLoadingMore, + onTabSelected = viewModel::onTabSelected, + onLoadMore = viewModel::loadMoreItems, + onItemClick = onItemClick + ) + } + } + } +} + +// DutchPayItemCard는 변경사항 없습니다. +@Composable +private fun AnimatedNumberCounter( + targetValue: Int, + modifier: Modifier = Modifier +) { + val animatedValue by animateIntAsState( + targetValue = targetValue, + animationSpec = tween(durationMillis = 1000), + label = "number_animation" + ) + + Text( + text = "+ ${Formatter.formatCurrency(animatedValue.toLong())}", + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = Color(0xFF1B6BFF), + modifier = modifier + ) +} + +@Composable +private fun DutchPayItemCard( + item: DutchPayItem, + isCompletedTab: Boolean = false, + onCardClick: (Long) -> Unit = {} +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { onCardClick(item.dutchpayId) }, + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(40.dp) + .background( + color = TiggleBlue.copy(alpha = 0.1f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Text(text = "💰", fontSize = 20.sp) + } + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = item.title, + style = AppTypography.bodyLarge.copy(fontWeight = FontWeight.Bold), + modifier = Modifier.weight(1f) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .background(color = Color(0xFFF5F5F5), shape = RoundedCornerShape(8.dp)) + .padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = if (isCompletedTab) "내가 낸 금액" else "내가 낼 금액", + style = AppTypography.bodySmall, + color = Color(0xFF666666) + ) + Text( + text = "총 ${Formatter.formatCurrency(item.totalAmount.toLong())}", + style = AppTypography.bodySmall, + color = Color(0xFF999999) + ) + } + Column(horizontalAlignment = Alignment.End) { + Text( + text = Formatter.formatCurrency(item.myAmount.toLong()), + style = AppTypography.bodyLarge.copy( + fontWeight = FontWeight.Bold, + color = TiggleBlue + ) + ) + Text( + text = "${item.participantCount}명 참여", + style = AppTypography.bodySmall, + color = Color(0xFF666666) + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "참여 현황", style = AppTypography.bodySmall, color = Color(0xFF666666)) + Text( + text = "${item.paidCount}/${item.participantCount}명 참여", + style = AppTypography.bodySmall, + color = Color(0xFF666666) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + AnimatedProgressBar( + progress = if (item.participantCount > 0) item.paidCount.toFloat() / item.participantCount else 0f + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = if (item.isCreator) "나의 요청" else "${item.creatorName}님의 요청", + style = AppTypography.bodySmall, + color = Color(0xFF999999) + ) + Text( + text = Formatter.formatDateTime(item.requestedAt), + style = AppTypography.bodySmall, + color = Color(0xFF999999) + ) + } + + // 티끌 적립 정보 표시 (tiggleAmount가 0보다 클 때만) + if (item.tiggleAmount > 0) { + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = Color(0xFFF0F8FF), + shape = RoundedCornerShape(12.dp) + ) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🐷", + fontSize = 16.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "티끌 적립", + style = AppTypography.bodyMedium, + color = Color(0xFF1B6BFF), + modifier = Modifier.weight(1f) + ) + AnimatedNumberCounter( + targetValue = item.tiggleAmount + ) + } + } + } + } +} + + +@Composable +private fun DutchPayStatusContent( + summary: DutchPaySummary, + selectedTabIndex: Int, + inProgressItems: List, + completedItems: List, + hasNextInProgress: Boolean, + hasNextCompleted: Boolean, + isLoadingMore: Boolean, + onTabSelected: (Int) -> Unit, + onLoadMore: () -> Unit, + onItemClick: (Long) -> Unit +) { + val listState = rememberLazyListState() + + LaunchedEffect(listState, selectedTabIndex) { // selectedTabIndex가 바뀔 때도 다시 실행되도록 + snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index } + .distinctUntilChanged() + .collect { lastIndex -> + if (lastIndex == null) return@collect + + val currentItems = if (selectedTabIndex == 0) inProgressItems else completedItems + val hasNext = if (selectedTabIndex == 0) hasNextInProgress else hasNextCompleted + + // 마지막 아이템 근처에 도달했고, 다음 페이지가 있고, 로딩중이 아닐 때 호출 + if (lastIndex >= currentItems.size - 2 && hasNext && !isLoadingMore) { + onLoadMore() + } + } + } + + Column(modifier = Modifier.fillMaxSize()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(200.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TiggleBlue) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + // ✨ 수정된 부분: .toLong()을 추가하여 타입 에러 해결 + text = Formatter.formatCurrency(summary.totalTransferredAmount.toLong()), + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "더치페이로 모은 총 티끌", + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.8f) + ) + Spacer(modifier = Modifier.height(20.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + StatisticItem(value = "${summary.transferCount}회", label = "티끌 적립 횟수") + StatisticItem(value = "${summary.participatedCount}회", label = "더치페이 횟수") + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = Modifier.fillMaxWidth() + ) { + Tab( + selected = selectedTabIndex == 0, + onClick = { onTabSelected(0) }, + text = { Text("진행중 (${inProgressItems.size})") } + ) + Tab( + selected = selectedTabIndex == 1, + onClick = { onTabSelected(1) }, + text = { Text("완료 기록 (${completedItems.size})") } + ) + } + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp) + ) { + val currentItems = if (selectedTabIndex == 0) inProgressItems else completedItems + + if (currentItems.isEmpty() && !isLoadingMore) { + item { + Box( + modifier = Modifier + .fillParentMaxHeight(0.5f) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text(text = "더치페이 기록이 없습니다", fontSize = 16.sp, color = Color.Gray) + } + } + } else { + items(currentItems, key = { it.dutchpayId }) { item -> + DutchPayItemCard( + item = item, + isCompletedTab = selectedTabIndex == 1, + onCardClick = onItemClick + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + if (isLoadingMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), color = TiggleBlue) + } + } + } + } + } + } +} + +// StatisticItem, AnimatedProgressBar는 변경사항 없습니다. +@Composable +private fun StatisticItem( + value: String, + label: String +) { + Card( + modifier = Modifier + .width(120.dp) + .height(60.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = Color.White.copy(alpha = 0.2f) + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = value, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + Text( + text = label, + fontSize = 12.sp, + color = Color.White.copy(alpha = 0.8f) + ) + } + } +} + +@Composable +private fun AnimatedProgressBar(progress: Float) { + var isVisible by remember { mutableStateOf(false) } + val animatedProgress by animateFloatAsState( + targetValue = if (isVisible) progress else 0f, + animationSpec = tween(durationMillis = 1500), + label = "progress" + ) + + LaunchedEffect(Unit) { + isVisible = true + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .background( + color = Color(0xFFE0E0E0), + shape = RoundedCornerShape(4.dp) + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth(animatedProgress) + .height(8.dp) + .clip(RoundedCornerShape(4.dp)) + .background( + brush = Brush.horizontalGradient( + colors = listOf(Color.White, TiggleBlue) + ) + ) + ) + } +} + +// ✨ 수정된 부분: Preview가 새 파라미터에 맞게 값을 전달하도록 변경 +@Preview(showBackground = true) +@Composable +private fun DutchPayStatusScreenPreview() { + val sampleSummary = DutchPaySummary( + totalTransferredAmount = 1200, + transferCount = 3, + participatedCount = 4 + ) + + val sampleItems = listOf( + DutchPayItem( + dutchpayId = 1L, + title = "어제 저녁 먹은거 정산", + myAmount = 17000, + totalAmount = 50000, + participantCount = 3, + paidCount = 1, + requestedAt = "2025-08-20T12:00:00Z", + isCreator = true, + creatorName = "나", + tiggleAmount = 334 + ), + DutchPayItem( + dutchpayId = 2L, + title = "택시팟", + myAmount = 16300, + totalAmount = 50000, + participantCount = 4, + paidCount = 3, + requestedAt = "2025-08-20T10:30:00Z", + isCreator = false, + creatorName = "홍길동", + tiggleAmount = 0 + ) + ) + + // Preview를 위한 샘플 상태 객체 + val sampleUiState = DutchPayStatusUiState( + isLoading = false, + summary = sampleSummary, + inProgressItems = sampleItems, + completedItems = emptyList(), + selectedTabIndex = 0 + ) + + // DutchPayStatusContent 호출 시, uiState 객체 대신 개별 값들을 전달 + DutchPayStatusContent( + summary = sampleUiState.summary!!, + selectedTabIndex = sampleUiState.selectedTabIndex, + inProgressItems = sampleUiState.inProgressItems, + completedItems = sampleUiState.completedItems, + hasNextInProgress = true, // preview용 임시값 + hasNextCompleted = false, // preview용 임시값 + isLoadingMore = false, // preview용 임시값 + onTabSelected = {}, + onLoadMore = {}, + onItemClick = {} + ) +} + +@Preview(showBackground = true) +@Composable +private fun DutchPayItemCardPreview() { + val sampleItem = DutchPayItem( + dutchpayId = 1L, + title = "어제 저녁 먹은거 정산", + myAmount = 17000, + totalAmount = 50000, + participantCount = 3, + paidCount = 1, + requestedAt = "2025-08-20T12:00:00Z", + isCreator = true, + creatorName = "나", + tiggleAmount = 334 + ) + + DutchPayItemCard( + item = sampleItem, + isCompletedTab = false, + onCardClick = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusUiState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusUiState.kt new file mode 100644 index 0000000..58b3bd0 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusUiState.kt @@ -0,0 +1,18 @@ +package com.ssafy.tiggle.presentation.ui.dutchpay + +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPayItem +import com.ssafy.tiggle.domain.entity.dutchpay.DutchPaySummary + +data class DutchPayStatusUiState( + val isLoading: Boolean = true, // 초기 전체 화면 로딩 + val isLoadingMore: Boolean = false, // 리스트 하단 더보기 로딩 + val error: String? = null, + val summary: DutchPaySummary? = null, + val selectedTabIndex: Int = 0, + val inProgressItems: List = emptyList(), + val completedItems: List = emptyList(), + val inProgressCursor: String? = null, + val completedCursor: String? = null, + val hasNextInProgress: Boolean = false, + val hasNextCompleted: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusViewModel.kt new file mode 100644 index 0000000..f212a7b --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchPayStatusViewModel.kt @@ -0,0 +1,136 @@ +package com.ssafy.tiggle.presentation.ui.dutchpay + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ssafy.tiggle.domain.usecase.dutchpay.GetDutchPaySummaryUseCase +import com.ssafy.tiggle.domain.usecase.dutchpay.GetDutchPayListUseCase +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 javax.inject.Inject + +@HiltViewModel +class DutchPayStatusViewModel @Inject constructor( + private val getDutchPaySummaryUseCase: GetDutchPaySummaryUseCase, + private val getDutchPayListUseCase: GetDutchPayListUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(DutchPayStatusUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadInitialData() + } + + private fun loadInitialData() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, error = null) } + + val summaryDeferred = async { getDutchPaySummaryUseCase() } + val inProgressListDeferred = async { getDutchPayListUseCase(tab = "IN_PROGRESS", cursor = null) } + val completedListDeferred = async { getDutchPayListUseCase(tab = "COMPLETED", cursor = null) } + + val summaryResult = summaryDeferred.await() + val inProgressListResult = inProgressListDeferred.await() + val completedListResult = completedListDeferred.await() + + _uiState.update { currentState -> + var newState = currentState.copy(isLoading = false) + summaryResult.onSuccess { summary -> + newState = newState.copy(summary = summary) + }.onFailure { exception -> + newState = newState.copy(error = exception.message ?: "요약 정보 로딩 실패") + } + inProgressListResult.onSuccess { list -> + newState = newState.copy( + inProgressItems = list.items, + inProgressCursor = list.nextCursor, + hasNextInProgress = list.hasNext + ) + }.onFailure { exception -> + newState = newState.copy(error = exception.message ?: "진행중 내역 로딩 실패") + } + completedListResult.onSuccess { list -> + newState = newState.copy( + completedItems = list.items, + completedCursor = list.nextCursor, + hasNextCompleted = list.hasNext + ) + }.onFailure { exception -> + newState = newState.copy(error = exception.message ?: "완료 내역 로딩 실패") + } + newState + } + } + } + + // 새로고침 또는 탭 선택 시 호출 + fun loadDutchPayList(isRefresh: Boolean = true) { + viewModelScope.launch { + // isRefresh는 새로고침, isRefresh가 아니면 더보기 로딩으로 간주 + _uiState.update { + if (isRefresh) it.copy(isLoading = true, error = null) + else it.copy(isLoadingMore = true, error = null) + } + + val currentState = _uiState.value + val tab = if (currentState.selectedTabIndex == 0) "IN_PROGRESS" else "COMPLETED" + val cursor = if (isRefresh) null else { + if (currentState.selectedTabIndex == 0) currentState.inProgressCursor else currentState.completedCursor + } + + getDutchPayListUseCase(tab, cursor).fold( + onSuccess = { list -> + _uiState.update { state -> + val newItems = if (isRefresh) list.items else { + if (state.selectedTabIndex == 0) state.inProgressItems + list.items + else state.completedItems + list.items + } + + state.copy( + isLoading = false, + isLoadingMore = false, + inProgressItems = if (state.selectedTabIndex == 0) newItems else state.inProgressItems, + completedItems = if (state.selectedTabIndex == 1) newItems else state.completedItems, + inProgressCursor = if (state.selectedTabIndex == 0) list.nextCursor else state.inProgressCursor, + completedCursor = if (state.selectedTabIndex == 1) list.nextCursor else state.completedCursor, + hasNextInProgress = if (state.selectedTabIndex == 0) list.hasNext else state.hasNextInProgress, + hasNextCompleted = if (state.selectedTabIndex == 1) list.hasNext else state.hasNextCompleted + ) + } + }, + onFailure = { exception -> + _uiState.update { + it.copy( + isLoading = false, + isLoadingMore = false, + error = exception.message ?: "더치페이 내역을 불러오는데 실패했습니다." + ) + } + } + ) + } + } + + fun onTabSelected(tabIndex: Int) { + if (_uiState.value.selectedTabIndex != tabIndex) { + _uiState.update { it.copy(selectedTabIndex = tabIndex) } + // 탭을 선택하면 항상 새로고침 + loadDutchPayList(isRefresh = true) + } + } + + fun loadMoreItems() { + val currentState = _uiState.value + val hasNext = if (currentState.selectedTabIndex == 0) currentState.hasNextInProgress else currentState.hasNextCompleted + + // 초기 로딩 중이거나, 더보기 로딩 중이 아닐 때만 호출 + if (hasNext && !currentState.isLoading && !currentState.isLoadingMore) { + loadDutchPayList(isRefresh = false) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchpayRecieveScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchpayRecieveScreen.kt index 034a903..fd3fc58 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchpayRecieveScreen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/dutchpay/DutchpayRecieveScreen.kt @@ -1,5 +1,7 @@ package com.ssafy.tiggle.presentation.ui.dutchpay +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,6 +11,7 @@ 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.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card @@ -41,6 +44,7 @@ import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout import com.ssafy.tiggle.presentation.ui.components.TiggleSwitchRow import com.ssafy.tiggle.presentation.ui.theme.AppTypography import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue +import com.ssafy.tiggle.presentation.ui.dutchpay.DutchPayRequestDetailViewModel @Composable fun DutchpayRecieveScreen( @@ -50,11 +54,27 @@ fun DutchpayRecieveScreen( viewModel: DutchPayRequestDetailViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() + var payMoreEnabled by remember { mutableStateOf(false) } LaunchedEffect(dutchPayId) { viewModel.loadDutchPayDetail(dutchPayId) } + // payMore 상태를 detail에서 초기화 + LaunchedEffect(uiState.dutchPayDetail) { + uiState.dutchPayDetail?.let { detail -> + payMoreEnabled = detail.payMoreDefault + } + } + + // 송금 성공 시 처리 + LaunchedEffect(uiState.isPaymentSuccess) { + if (uiState.isPaymentSuccess) { + viewModel.clearPaymentSuccess() + onPaymentClick() + } + } + TiggleScreenLayout( onBackClick = onBackClick, bottomButton = { @@ -62,9 +82,9 @@ fun DutchpayRecieveScreen( if (!detail.isCreator) { TiggleButton( text = "송금하기", - onClick = onPaymentClick, + onClick = { viewModel.payDutchPay(dutchPayId, payMoreEnabled) }, enabled = !uiState.isLoading, - isLoading = false, + isLoading = uiState.isLoading, variant = TiggleButtonVariant.Primary ) } @@ -84,7 +104,9 @@ fun DutchpayRecieveScreen( else -> { uiState.dutchPayDetail?.let { detail -> DutchPayPaymentContent( - detail = detail + detail = detail, + payMoreEnabled = payMoreEnabled, + onPayMoreChanged = { payMoreEnabled = it } ) } } @@ -105,7 +127,7 @@ fun DutchpayRecieveScreen( TextButton( onClick = { viewModel.clearErrorMessage() } ) { - Text("송금하기") + Text("확인") } } ) @@ -113,11 +135,30 @@ fun DutchpayRecieveScreen( } @Composable -private fun DutchPayPaymentContent( - detail: DutchPayRequestDetail +private fun AnimatedNumberCounter( + targetValue: Long, + modifier: Modifier = Modifier ) { - var payMoreEnabled by remember { mutableStateOf(detail.payMoreDefault) } + val animatedValue by animateIntAsState( + targetValue = targetValue.toInt(), + animationSpec = tween(durationMillis = 1000), + label = "number_animation" + ) + + Text( + text = "+ ${Formatter.formatCurrency(animatedValue.toLong())}", + style = AppTypography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = TiggleBlue, + modifier = modifier + ) +} +@Composable +private fun DutchPayPaymentContent( + detail: DutchPayRequestDetail, + payMoreEnabled: Boolean, + onPayMoreChanged: (Boolean) -> Unit +) { // 내가 낼 금액 계산 val myPaymentAmount = if (payMoreEnabled) { detail.originalAmount + detail.tiggleAmount @@ -240,23 +281,65 @@ private fun DutchPayPaymentContent( if (payMoreEnabled && detail.tiggleAmount > 0) { Spacer(modifier = Modifier.height(8.dp)) - DetailRow( - label = "티끌", - value = "+${Formatter.formatCurrency(detail.tiggleAmount)}", - valueColor = TiggleBlue - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "티끌", + style = AppTypography.bodyMedium, + color = Color.Gray + ) + AnimatedNumberCounter( + targetValue = detail.tiggleAmount + ) + } } } } Spacer(modifier = Modifier.height(20.dp)) + // 티끌 적립 정보 표시 (payMoreEnabled이고 tiggleAmount가 0보다 클 때만) + if (payMoreEnabled && detail.tiggleAmount > 0) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F8FF)), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "🐷", + fontSize = 20.sp + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "티끌 적립", + style = AppTypography.bodyLarge.copy(fontWeight = FontWeight.Bold), + color = Color(0xFF1B6BFF), + modifier = Modifier.weight(1f) + ) + AnimatedNumberCounter( + targetValue = detail.tiggleAmount + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + } + // 돈 더내고 잔돈 기부하기 스위치 TiggleSwitchRow( title = "돈 더내고 잔돈 기부하기", subtitle = "자투리 금액을 티끌 저금통에 적립", checked = payMoreEnabled, - onCheckedChange = { payMoreEnabled = it } + onCheckedChange = onPayMoreChanged ) Spacer(modifier = Modifier.height(40.dp)) @@ -322,7 +405,7 @@ private fun PreviewDutchPayPayment() { ) } ) { - DutchPayPaymentContent(detail = sampleDetail) + DutchPayPaymentContent(detail = sampleDetail, payMoreEnabled = true, onPayMoreChanged = {}) } } @@ -357,6 +440,6 @@ private fun PreviewNoTiggleDutchPayPayment() { ) } ) { - DutchPayPaymentContent(detail = sampleDetail) + DutchPayPaymentContent(detail = sampleDetail, payMoreEnabled = false, onPayMoreChanged = {}) } } 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 1fa69e7..6469f79 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 @@ -63,6 +63,7 @@ fun PiggyBankScreen( onOpenAccountClick: () -> Unit = {}, onRegisterAccountClick: () -> Unit = {}, onStartDutchPayClick: () -> Unit = {}, + onDutchPayStatusClick: () -> Unit = {}, onAccountClick: (String) -> Unit = {}, onShowPiggyBankDetailClick: () -> Unit = {}, onEditLinkedAccountClick: () -> Unit = {}, @@ -143,7 +144,7 @@ fun PiggyBankScreen( if (uiState.hasPiggyBank) { DutchButtonsRow( - onStatus = {}, + onStatus = onDutchPayStatusClick, onStart = onStartDutchPayClick ) }