From a1e5f6b7041350f8a199afe2f5ebcf9b43862677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Fri, 2 Feb 2024 20:41:54 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EA=B2=BD=EC=A1=B0=EC=82=AC=EB=B9=84?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=20=EA=B5=AC=ED=98=84=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat]: 아이콘 추가/변경 * [Feat]: Main History ui 구현 * [Refactor]: TypingTextField 설정 값 추가 * [Feat]: 그룹 아이템을 리스트로 보여줄 수 있는 그룹 아이템 컴포넌트를 생성 * [Feat]: 현재 뷰 선택 타입 기능 적용 * [Feat]: History 관련 초기 기능 구현 * [Feat]: History detail ui 구현 * [Refactor]: 재사용 가능하도록 chip 수정 * [Feat]: HistoryRelationItem CardView로 제작, GridLayout 연결하여 구현 * [Feat]: History 관련 model, UseCase 생성 * [Feat]: History Graph 생성 * [Feat]: Usecase 기능 수정 * [Feat]: History 기능 구현 * [Feat]: History main ui 연결, 기능 구현 * [Feat]: History detail 기능 구현 * [Fix]: HistoryScreen 제거 * [Fix]: HistoryScreen 제거 * [Style]: 코드 포맷 수정 --- .../domain/model/history/HistoryInfo.kt | 7 + .../history/GetHistoryGroupListUseCase.kt | 501 ++++++++++++++++++ .../usecase/history/GetHistoryInfoUseCase.kt | 19 + .../presentation/common/view/chip/ChipItem.kt | 166 ++++++ .../presentation/common/view/chip/ChipType.kt | 7 + .../view/chip/GroupChipListComponent.kt | 84 +++ .../common/view/textfield/TypingTextField.kt | 20 +- .../ui/main/home/history/HistoryConstant.kt | 5 + .../ui/main/home/history/HistoryScreen.kt | 33 ++ .../ui/main/home/history/HistoryViewModel.kt | 61 +++ .../home/history/main/HistoryMainConstant.kt | 7 + .../home/history/main/HistoryMainEvent.kt | 10 + .../home/history/main/HistoryMainIntent.kt | 3 + .../home/history/main/HistoryMainModel.kt | 10 + .../home/history/main/HistoryMainScreen.kt | 207 ++++++++ .../home/history/main/HistoryMainState.kt | 6 + .../history/main/detail/HistoryDetailEvent.kt | 10 + .../main/detail/HistoryDetailIntent.kt | 3 + .../history/main/detail/HistoryDetailModel.kt | 12 + .../main/detail/HistoryDetailScreen.kt | 413 +++++++++++++++ .../history/main/detail/HistoryDetailState.kt | 6 + .../main/detail/HistoryDetailViewModel.kt | 71 +++ .../main/detail/item/HistoryRelationItem.kt | 160 ++++++ .../main/detail/type/HistorySortedType.kt | 8 + .../main/detail/type/HistoryViewType.kt | 9 + .../src/main/res/drawable/ic_notification.xml | 10 +- .../main/res/drawable/ic_notification_on.xml | 15 + .../src/main/res/drawable/ic_search.xml | 21 +- 28 files changed, 1864 insertions(+), 20 deletions(-) create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/history/HistoryInfo.kt create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryGroupListUseCase.kt create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryInfoUseCase.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipItem.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipType.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/GroupChipListComponent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryConstant.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryViewModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainConstant.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainEvent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainIntent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainState.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailEvent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailIntent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailState.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailViewModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/item/HistoryRelationItem.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistorySortedType.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistoryViewType.kt create mode 100644 presentation/src/main/res/drawable/ic_notification_on.xml diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/history/HistoryInfo.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/history/HistoryInfo.kt new file mode 100644 index 00000000..f70cef04 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/history/HistoryInfo.kt @@ -0,0 +1,7 @@ +package ac.dnd.bookkeeping.android.domain.model.history + +data class HistoryInfo( + val unWrittenCount: Int, + val totalHeartCount: Int, + val unReadAlarm: Boolean +) diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryGroupListUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryGroupListUseCase.kt new file mode 100644 index 00000000..e11ad619 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryGroupListUseCase.kt @@ -0,0 +1,501 @@ +package ac.dnd.bookkeeping.android.domain.usecase.history + +import ac.dnd.bookkeeping.android.domain.model.event.Group +import ac.dnd.bookkeeping.android.domain.model.event.Relation +import ac.dnd.bookkeeping.android.domain.model.event.RelationGroup +import javax.inject.Inject + +class GetHistoryGroupListUseCase @Inject constructor( + +) { + suspend operator fun invoke( + historyType: String + ): Result> { + return Result.success( + when (historyType) { + "take" -> { + listOf( + Group( + 1, + "전체-받은 마음", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 1000000 + ), + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 100000, + takeMoney = 0 + ), + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 100000, + takeMoney = 23 + ), + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 12312223, + takeMoney = 12 + ), + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 4345346 + ), + ) + ), + Group( + 2, + "친구", + listOf( + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 100000, + takeMoney = 0 + ), + ) + ), + Group( + 3, + "가족", + listOf( + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 1000000, + takeMoney = 23 + ), + ) + ), + Group( + 4, + "지인", + listOf( + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 12231223, + takeMoney = 12 + ), + ) + ), + Group( + 5, + "직장", + listOf( + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 4345346 + ), + ) + ), + Group( + 6, + "사촌", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 1000000 + ), + ) + ) + ) + } + + "give" -> { + listOf( + Group( + 1, + "전체-준마음", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 1000000 + ), + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 100000, + takeMoney = 0 + ), + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 100000, + takeMoney = 23 + ), + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 12231223, + takeMoney = 12 + ), + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 0 + ), + ) + ), + Group( + 2, + "친구", + listOf( + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 10000, + takeMoney = 0 + ), + ) + ), + Group( + 3, + "가족", + listOf( + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 100000, + takeMoney = 23 + ), + ) + ), + Group( + 4, + "지인", + listOf( + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 1231223, + takeMoney = 12 + ), + ) + ), + Group( + 5, + "직장", + listOf( + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 4345346 + ), + ) + ), + Group( + 6, + "사촌", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 10000000 + ), + ) + ) + ) + } + + else -> { + listOf( + Group( + 1, + "전체-전체", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 10000000 + ), + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 1000000, + takeMoney = 0 + ), + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 10000000, + takeMoney = 23 + ), + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 123121223, + takeMoney = 12 + ), + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 43534346 + ), + ) + ), + Group( + 2, + "친구", + listOf( + Relation( + id = 5, + name = "김경민", + group = RelationGroup( + id = 4, + name = "친구", + ), + giveMoney = 1000000, + takeMoney = 0 + ), + ) + ), + Group( + 3, + "가족", + listOf( + Relation( + id = 0, + name = "김진우", + group = RelationGroup( + id = 0, + name = "가족", + ), + giveMoney = 10000000, + takeMoney = 23 + ), + ) + ), + Group( + 4, + "지인", + listOf( + Relation( + id = 1, + name = "박예리나", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 0, + takeMoney = 0 + ), + Relation( + id = 2, + name = "이다빈", + group = RelationGroup( + id = 1, + name = "지인", + ), + giveMoney = 1231223, + takeMoney = 12 + ), + ) + ), + Group( + 5, + "직장", + listOf( + Relation( + id = 3, + name = "장성혁", + group = RelationGroup( + id = 2, + name = "직장", + ), + giveMoney = 1000, + takeMoney = 43534546 + ), + ) + ), + Group( + 6, + "사촌", + listOf( + Relation( + id = 4, + name = "서지원", + group = RelationGroup( + id = 3, + name = "사촌", + ), + giveMoney = 20, + takeMoney = 1000000 + ), + ) + ) + ) + } + } + ) + } +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryInfoUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryInfoUseCase.kt new file mode 100644 index 00000000..57ffea57 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/history/GetHistoryInfoUseCase.kt @@ -0,0 +1,19 @@ +package ac.dnd.bookkeeping.android.domain.usecase.history + +import ac.dnd.bookkeeping.android.domain.model.history.HistoryInfo +import javax.inject.Inject + +class GetHistoryInfoUseCase @Inject constructor( + +) { + suspend operator fun invoke(): Result { + // TODO fix when api update + return Result.success( + HistoryInfo( + unWrittenCount = 5, + totalHeartCount = 32, + unReadAlarm = false + ) + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipItem.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipItem.kt new file mode 100644 index 00000000..31ecf612 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipItem.kt @@ -0,0 +1,166 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.chip + +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray400 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary4 +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun ChipItem( + chipType: ChipType = ChipType.LESS_BORDER, + currentSelectedId: Long, + chipId: Long, + chipText: String, + chipCount: Int = 0, + onSelectChip: (chipId: Long) -> Unit +) { + val isSelected = currentSelectedId == chipId + val backgroundColor = animateColorAsState( + targetValue = when (chipType) { + ChipType.MAIN -> if (isSelected) Gray700 else Gray000 + else -> if (isSelected) Primary1 else Gray000 + }, + label = "background" + ) + val textColor = animateColorAsState( + targetValue = when (chipType) { + ChipType.MAIN -> if (isSelected) Gray000 else Gray600 + else -> if (isSelected) Primary4 else Gray600 + }, + label = "textColor" + ) + val borderColor = animateColorAsState( + targetValue = when (chipType) { + ChipType.BORDER -> if (isSelected) Primary4 else Gray400 + else -> Color.Transparent + }, + label = "borderColor" + ) + + Row( + modifier = Modifier + .background( + color = backgroundColor.value, + shape = RoundedCornerShape(100.dp) + ) + .border( + color = borderColor.value, + width = 1.dp, + shape = RoundedCornerShape(100.dp) + ) + .padding( + horizontal = 14.dp, + vertical = 6.5.dp + ) + .clickable { + onSelectChip(chipId) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = chipText, + style = Body1.merge( + color = textColor.value + ) + ) + if (chipCount > 0) { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = chipCount.toString(), + style = Body1.merge( + color = textColor.value + ) + ) + } + } +} + +@Preview(backgroundColor = 0xFFF6F6F7, showBackground = true) +@Composable +fun ChipsType1Preview(){ + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)){ + ChipItem( + chipType = ChipType.LESS_BORDER, + currentSelectedId = 0, + chipId = 0, + chipText = "가족", + chipCount = 5, + onSelectChip = {} + ) + ChipItem( + chipType = ChipType.LESS_BORDER, + currentSelectedId = 0, + chipId = 1, + chipText = "친구", + chipCount = 0, + onSelectChip = {} + ) + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +fun ChipsType2Preview(){ + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)){ + ChipItem( + chipType = ChipType.BORDER, + currentSelectedId = 0, + chipId = 0, + chipText = "가족", + chipCount = 5, + onSelectChip = {} + ) + ChipItem( + chipType = ChipType.BORDER, + currentSelectedId = 0, + chipId = 1, + chipText = "친구", + chipCount = 0, + onSelectChip = {} + ) + } +} + +@Preview(backgroundColor = 0xFFF6F6F7, showBackground = true) +@Composable +fun ChipsType3Preview(){ + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)){ + ChipItem( + chipType = ChipType.MAIN, + currentSelectedId = 0, + chipId = 0, + chipText = "가족", + chipCount = 5, + onSelectChip = {} + ) + ChipItem( + chipType = ChipType.MAIN, + currentSelectedId = 0, + chipId = 1, + chipText = "친구", + chipCount = 0, + onSelectChip = {} + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipType.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipType.kt new file mode 100644 index 00000000..119ebdfa --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/ChipType.kt @@ -0,0 +1,7 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.chip + +enum class ChipType { + MAIN, + BORDER, + LESS_BORDER +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/GroupChipListComponent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/GroupChipListComponent.kt new file mode 100644 index 00000000..392a73c6 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/chip/GroupChipListComponent.kt @@ -0,0 +1,84 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.chip + +import ac.dnd.bookkeeping.android.domain.model.event.Group +import ac.dnd.bookkeeping.android.domain.model.event.Relation +import ac.dnd.bookkeeping.android.domain.model.event.RelationGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + + +@Composable +fun GroupChipListComponent( + chipType: ChipType = ChipType.LESS_BORDER, + currentSelectedId: Long, + onSelectChip: (Group) -> Unit, + groups : List +) { + LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + items(groups) { group -> + ChipItem( + chipType = chipType, + currentSelectedId = currentSelectedId, + chipId = group.id, + chipText = group.name, + chipCount = group.relations.size, + onSelectChip = { + onSelectChip(group) + } + ) + } + } +} + +@Composable +@Preview(backgroundColor = 0xFFFFFFFF) +fun GroupChipPreview() { + GroupChipListComponent( + chipType = ChipType.MAIN, + currentSelectedId = 0, + onSelectChip = {}, + groups = listOf( + Group( + 0, + "전체", + relations = listOf( + Relation( + id = 3679, + name = "Jerome Pitts", + group = RelationGroup( + id = 6599, + name = "Andrea Serrano", + ), + giveMoney = 4190, + takeMoney = 4010 + ) + ) + ), + Group( + 1, + "친구", + relations = listOf( + Relation( + id = 3679, + name = "Jerome Pitts", + group = RelationGroup( + id = 6599, + name = "Andrea Serrano", + ), + giveMoney = 4190, + takeMoney = 4010 + ) + ) + ), + Group( + 2, + "가족", + listOf() + ) + ) + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt index 6055c0b6..14d62a03 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt @@ -61,6 +61,12 @@ fun TypingTextField( maxTextLength: Int = 100, keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), visualTransformation: VisualTransformation = VisualTransformation.None, + backgroundColor: Color = Color.White, + basicBorderColor: Color = Gray400, + contentPadding: PaddingValues = PaddingValues( + vertical = 13.5.dp, + horizontal = 16.dp + ), leadingIconContent: (@Composable () -> Unit)? = null, trailingIconContent: (@Composable () -> Unit)? = null, errorMessageContent: (@Composable () -> Unit) = { }, @@ -73,7 +79,7 @@ fun TypingTextField( TypingTextFieldType.LongSentence -> false } - val currentColor = if (isError) Negative else if (isTextFieldFocused) Primary3 else Gray400 + val currentColor = if (isError) Negative else if (isTextFieldFocused) Primary3 else basicBorderColor val currentColorState = animateColorAsState( targetValue = currentColor, label = "color state" @@ -83,7 +89,7 @@ fun TypingTextField( Column( modifier = modifier .background( - color = Color.White, + color = backgroundColor, shape = Shapes.medium ) .border( @@ -107,7 +113,6 @@ fun TypingTextField( singleLine = isSingleLine, minLines = if (isSingleLine) 1 else 3, keyboardOptions = keyboardOptions, - cursorBrush = SolidColor(value = currentColorState.value), interactionSource = interactionSource, ) { textField -> @@ -126,12 +131,8 @@ fun TypingTextField( }, leadingIcon = leadingIconContent, trailingIcon = trailingIconContent, - contentPadding = PaddingValues( - vertical = 13.5.dp, - horizontal = 16.dp - ) + contentPadding = contentPadding ) - } if (textType == TypingTextFieldType.LongSentence) { @@ -153,8 +154,7 @@ fun TypingTextField( if (textType == TypingTextFieldType.Basic) { Spacer(Modifier.height(Space8)) Box( - modifier = Modifier - .alpha(if (isError) 1f else 0f) + modifier = Modifier.alpha(if (isError) 1f else 0f) ) { errorMessageContent() } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryConstant.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryConstant.kt new file mode 100644 index 00000000..dce54412 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryConstant.kt @@ -0,0 +1,5 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history + +object HistoryConstant { + const val ROUTE = "/history" +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryScreen.kt new file mode 100644 index 00000000..e86a3c50 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryScreen.kt @@ -0,0 +1,33 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history + +import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainModel +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainScreen +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle + +@Composable +fun HistoryScreen( + appState: ApplicationState, + viewModel: HistoryViewModel = hiltViewModel() +) { + + val model: HistoryMainModel = Unit.let { + val state by viewModel.state.collectAsStateWithLifecycle() + val historyInfo by viewModel.historyInfo.collectAsStateWithLifecycle() + HistoryMainModel( + state = state, + historyInfo = historyInfo + ) + } + + HistoryMainScreen( + appState = appState, + model = model, + event = viewModel.event, + intent = viewModel::onIntent, + handler = viewModel.handler + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryViewModel.kt new file mode 100644 index 00000000..cb8d6c77 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/HistoryViewModel.kt @@ -0,0 +1,61 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history + +import ac.dnd.bookkeeping.android.domain.model.error.ServerException +import ac.dnd.bookkeeping.android.domain.model.history.HistoryInfo +import ac.dnd.bookkeeping.android.domain.usecase.history.GetHistoryInfoUseCase +import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.asEventFlow +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainEvent +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainIntent +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainState +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class HistoryViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val getHistoryInfoUseCase: GetHistoryInfoUseCase +) : BaseViewModel() { + + private val _state: MutableStateFlow = MutableStateFlow(HistoryMainState.Init) + val state: StateFlow = _state.asStateFlow() + + private val _event: MutableEventFlow = MutableEventFlow() + val event: EventFlow = _event.asEventFlow() + + private val _historyInfo: MutableStateFlow = + MutableStateFlow(HistoryInfo(0, 0, false)) + val historyInfo: StateFlow = _historyInfo.asStateFlow() + + init { + launch { + _state.value = HistoryMainState.Loading + getHistoryInfoUseCase() + .onSuccess { + _historyInfo.value = it + } + .onFailure { exception -> + when (exception) { + is ServerException -> _event.emit( + HistoryMainEvent.GetHistoryInfoMain.Failure( + exception + ) + ) + + else -> _event.emit(HistoryMainEvent.GetHistoryInfoMain.Error(exception)) + } + } + _state.value = HistoryMainState.Init + } + } + + fun onIntent(intent: HistoryMainIntent) { + + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainConstant.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainConstant.kt new file mode 100644 index 00000000..bec4bd8a --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainConstant.kt @@ -0,0 +1,7 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.HistoryConstant + +object HistoryMainConstant { + const val ROUTE: String = "${HistoryConstant.ROUTE}/main" +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainEvent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainEvent.kt new file mode 100644 index 00000000..0c0d462b --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainEvent.kt @@ -0,0 +1,10 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +import ac.dnd.bookkeeping.android.domain.model.error.ServerException + +sealed interface HistoryMainEvent { + sealed interface GetHistoryInfoMain : HistoryMainEvent { + data class Failure(val exception: ServerException) : GetHistoryInfoMain + data class Error(val exception: Throwable) : GetHistoryInfoMain + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainIntent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainIntent.kt new file mode 100644 index 00000000..e41a8723 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainIntent.kt @@ -0,0 +1,3 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +sealed interface HistoryMainIntent diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainModel.kt new file mode 100644 index 00000000..e26ea45e --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainModel.kt @@ -0,0 +1,10 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +import ac.dnd.bookkeeping.android.domain.model.history.HistoryInfo +import androidx.compose.runtime.Immutable + +@Immutable +data class HistoryMainModel( + val state: HistoryMainState, + val historyInfo: HistoryInfo +) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainScreen.kt new file mode 100644 index 00000000..c32bc3ab --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainScreen.kt @@ -0,0 +1,207 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +import ac.dnd.bookkeeping.android.domain.model.history.HistoryInfo +import ac.dnd.bookkeeping.android.presentation.R +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray100 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray400 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline0 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline3 +import ac.dnd.bookkeeping.android.presentation.common.util.LaunchedEffectWithLifecycle +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve +import ac.dnd.bookkeeping.android.presentation.common.util.expansion.addFocusCleaner +import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.HistoryDetailScreen +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type.HistoryViewType +import ac.dnd.bookkeeping.android.presentation.ui.main.rememberApplicationState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun HistoryMainScreen( + appState: ApplicationState, + model: HistoryMainModel, + event: EventFlow, + intent: (HistoryMainIntent) -> Unit, + handler: CoroutineExceptionHandler +) { + val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() + val pages = listOf("전체", "받은 마음", "보낸 마음") + val pagerState = rememberPagerState( + pageCount = { 3 } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .addFocusCleaner(focusManager) + .background(Gray100) + ) { + Box( + modifier = Modifier + .height(56.dp) + .fillMaxWidth() + .background(Gray000) + .padding(horizontal = 20.dp) + ) { + Text( + text = "MUR", + style = Headline0.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier.align(Alignment.CenterStart) + ) + Image( + painter = getAlarmImage(model.historyInfo.unReadAlarm), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterEnd) + .clickable { + //TODO go alarm view + } + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .background(Gray000) + ) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .height(1.dp) + .fillMaxWidth() + .background(Gray400) + ) + TabRow( + selectedTabIndex = pagerState.currentPage, + backgroundColor = Color.Transparent, + modifier = Modifier + .background(Color.Transparent) + .padding(horizontal = 20.dp), + divider = { + Box( + modifier = Modifier + .height(1.dp) + .fillMaxWidth() + .background(Gray400) + ) + } + ) { + pages.forEachIndexed { index, pageText -> + Tab( + selected = index == pagerState.currentPage, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { + Text( + text = pageText, + style = Headline3.merge( + color = if (index == pagerState.currentPage) Gray700 else Gray400, + fontWeight = FontWeight.SemiBold + ) + ) + } + ) + } + } + } + + HorizontalPager( + state = pagerState, + userScrollEnabled = false + ) { pageIndex -> + when (pageIndex) { + 0 -> { + HistoryDetailScreen( + viewType = HistoryViewType.TOTAL, + mainModel = model + ) + } + + 1 -> { + HistoryDetailScreen( + viewType = HistoryViewType.TAKE, + mainModel = model + ) + } + + 2 -> { + HistoryDetailScreen( + viewType = HistoryViewType.GIVE, + mainModel = model + ) + } + } + } + } + + LaunchedEffectWithLifecycle(event, handler) { + event.eventObserve { event -> + when (event) { + is HistoryMainEvent.GetHistoryInfoMain -> { + + } + } + } + } +} + +@Composable +fun getAlarmImage(alarmOnState: Boolean): Painter { + return painterResource(if (alarmOnState) R.drawable.ic_notification_on else R.drawable.ic_notification) +} + +@Composable +@Preview +fun HistoryScreenPreview() { + HistoryMainScreen( + appState = rememberApplicationState(), + model = HistoryMainModel( + state = HistoryMainState.Init, + historyInfo = HistoryInfo( + unReadAlarm = true, + totalHeartCount = 30, + unWrittenCount = 5 + ) + ), + event = MutableEventFlow(), + intent = {}, + handler = CoroutineExceptionHandler { _, _ -> } + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainState.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainState.kt new file mode 100644 index 00000000..75d9189a --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/HistoryMainState.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main + +sealed interface HistoryMainState { + data object Init : HistoryMainState + data object Loading : HistoryMainState +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailEvent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailEvent.kt new file mode 100644 index 00000000..23ad39f5 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailEvent.kt @@ -0,0 +1,10 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +import ac.dnd.bookkeeping.android.domain.model.error.ServerException + +sealed interface HistoryDetailEvent { + sealed interface GetHistoryRelationList : HistoryDetailEvent { + data class Failure(val exception: ServerException) : GetHistoryRelationList + data class Error(val exception: Throwable) : GetHistoryRelationList + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailIntent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailIntent.kt new file mode 100644 index 00000000..0cb904ac --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailIntent.kt @@ -0,0 +1,3 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +sealed interface HistoryDetailIntent diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailModel.kt new file mode 100644 index 00000000..5cd9e20a --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailModel.kt @@ -0,0 +1,12 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +import ac.dnd.bookkeeping.android.domain.model.event.Group +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type.HistoryViewType +import androidx.compose.runtime.Immutable + +@Immutable +data class HistoryDetailModel( + val state: HistoryDetailState, + val viewType: HistoryViewType, + val historyGroups: List, +) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailScreen.kt new file mode 100644 index 00000000..5f5829b2 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailScreen.kt @@ -0,0 +1,413 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +import ac.dnd.bookkeeping.android.domain.model.history.HistoryInfo +import ac.dnd.bookkeeping.android.presentation.R +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Body2 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray100 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray200 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray300 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray800 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline2 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary4 +import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes +import ac.dnd.bookkeeping.android.presentation.common.theme.Space12 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space16 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space20 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space24 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space4 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space8 +import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipType +import ac.dnd.bookkeeping.android.presentation.common.view.chip.GroupChipListComponent +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextField +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextFieldType +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainModel +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.HistoryMainState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.item.HistoryRelationItem +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type.HistorySortedType +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type.HistoryViewType +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +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 +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle + +@SuppressLint("InvalidColorHexValue") +@Composable +fun HistoryDetailScreen( + viewType: HistoryViewType, + mainModel: HistoryMainModel, + viewModel: HistoryDetailViewModel = hiltViewModel() +) { + val model: HistoryDetailModel = Unit.let { + val state by viewModel.state.collectAsStateWithLifecycle() + val groups by when (viewType) { + HistoryViewType.TOTAL -> viewModel.totalGroups.collectAsStateWithLifecycle() + HistoryViewType.TAKE -> viewModel.takeGroups.collectAsStateWithLifecycle() + HistoryViewType.GIVE -> viewModel.giveGroups.collectAsStateWithLifecycle() + } + + HistoryDetailModel( + state = state, + viewType = viewType, + historyGroups = groups, + ) + } + + var searchText by remember { mutableStateOf("") } + var selectedGroupId by remember { + mutableLongStateOf( + model.historyGroups.firstOrNull()?.id ?: 0 + ) + } + var isDropDownMenuExpanded by remember { mutableStateOf(false) } + var viewSortType by remember { mutableStateOf(HistorySortedType.LATEST) } + + Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = Gray000, + shape = RoundedCornerShape( + bottomStart = Space12, + bottomEnd = Space12 + ) + ) + .padding( + start = 20.dp, + end = 20.dp, + top = 28.dp, + bottom = 18.dp + ) + ) { + Text( + text = buildAnnotatedString { + append("총 ") + withStyle( + SpanStyle(color = Primary4) + ) { + append("${mainModel.historyInfo.totalHeartCount}번") + } + append("의 마음을\n주고 받았어요 ") + }, + style = Headline2.merge( + color = Gray800, + fontWeight = FontWeight.SemiBold + ) + ) + Spacer(modifier = Modifier.height(24.dp)) + TypingTextField( + textType = TypingTextFieldType.Basic, + text = searchText, + onValueChange = { + searchText = it + }, + modifier = Modifier.height(39.dp), + contentPadding = PaddingValues( + start = if (searchText.isEmpty()) 0.dp else 12.dp, + end = 20.dp, + ), + basicBorderColor = Gray100, + backgroundColor = Gray100, + hintText = "이름을 입력하세요.", + leadingIconContent = if (searchText.isEmpty()) { + { + Image( + painter = painterResource(R.drawable.ic_search), + contentDescription = null, + modifier = Modifier.size(Space16) + ) + } + } else null, + trailingIconContent = if (searchText.isNotEmpty()) { + { + Image( + painter = painterResource(R.drawable.ic_close_circle), + contentDescription = null, + modifier = Modifier + .size(Space20) + .clickable { + searchText = "" + } + ) + } + } else null + ) + if (mainModel.historyInfo.unWrittenCount > 0) { + Box( + modifier = Modifier + .border( + width = 1.dp, + color = Gray300, + shape = Shapes.medium, + ) + .fillMaxWidth() + .padding( + horizontal = Space20, + vertical = Space16 + ) + + ) { + Image( + painter = painterResource(R.drawable.ic_close_circle), + contentDescription = null, + modifier = Modifier.align(Alignment.TopEnd) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + //TODO into icon + Box( + modifier = Modifier + .clip(CircleShape) + .background(color = Gray200) + .size(40.dp) + ) + Spacer(modifier = Modifier.width(Space8)) + Column { + Text( + text = buildAnnotatedString { + append("지난 일정 ") + withStyle( + SpanStyle(color = Gray800) + ) { + append("${mainModel.historyInfo.unWrittenCount}개") + } + }, + style = Body1.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "한번에 기록하기", + style = Body2.merge( + color = Gray600, + fontWeight = FontWeight.SemiBold + ) + ) + Image( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + modifier = Modifier + .width(16.dp) + .height(18.dp) + ) + } + } + } + } + } + } + Spacer(modifier = Modifier.height(2.dp)) + Box( + modifier = Modifier.padding( + vertical = 16.dp, + horizontal = 20.dp + ) + ) { + GroupChipListComponent( + chipType = ChipType.MAIN, + currentSelectedId = selectedGroupId, + groups = model.historyGroups, + onSelectChip = { group -> + selectedGroupId = group.id + } + ) + } + Box( + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + Row( + modifier = Modifier + .padding( + horizontal = 6.dp, + vertical = 3.5.dp + ) + .clickable { + isDropDownMenuExpanded = true + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = viewSortType.typeName, + style = Body1.merge( + color = Gray700, + fontWeight = FontWeight.Normal + ) + ) + Spacer(modifier = Modifier.width(2.dp)) + Image( + painter = painterResource(R.drawable.ic_chevron_down), + contentDescription = null, + modifier = Modifier.size(Space16) + ) + DropdownMenu( + modifier = Modifier + .wrapContentHeight() + .background( + color = Gray000, + shape = Shapes.medium + ), + expanded = isDropDownMenuExpanded, + onDismissRequest = { isDropDownMenuExpanded = false } + ) { + Column(verticalArrangement = Arrangement.Center) { + HistorySortedType.entries.forEachIndexed { index, type -> + DropdownMenuItem( + onClick = {}, + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .width(116.dp) + .height(40.dp) + ) { + Row( + modifier = Modifier + .clickable { + viewSortType = type + isDropDownMenuExpanded = false + } + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (viewSortType == type) { + Image( + painter = painterResource(R.drawable.ic_check_line), + contentDescription = null, + colorFilter = ColorFilter.tint(Primary4), + modifier = Modifier.size(Space24) + ) + } else { + Box( + modifier = Modifier.size(Space24) + .background(Color.White) + ) + } + Spacer(Modifier.width(Space4)) + Text( + text = type.typeName, + style = Body1.merge( + color = Gray700, + fontWeight = FontWeight.Normal + ) + ) + } + } + if (index != HistorySortedType.entries.lastIndex) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .padding(top = 0.5.dp) + .background(color = Gray200) + ) + } + } + } + } + } + } + + model.historyGroups.find { it.id == selectedGroupId }?.let { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(Space16), + verticalArrangement = Arrangement.spacedBy(Space16), + contentPadding = PaddingValues( + horizontal = 20.dp, + vertical = 16.dp + ) + ) { + items(it.relations) { relation -> + HistoryRelationItem( + relation, + onSelectCard = { + + } + ) + } + } + } ?: EmptyRelationView() + } +} + +@Composable +fun EmptyRelationView() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.weight(107.66f)) + Text( + text = "아직 경조사비 내역이 입력되지 않았어요.", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = Body1.merge( + color = Gray600, + fontWeight = FontWeight.SemiBold + ) + ) + Spacer(modifier = Modifier.weight(188.34f)) + } +} + + +@Preview(showBackground = true) +@Composable +fun HistoryDetailPreview() { + HistoryDetailScreen( + HistoryViewType.TOTAL, + mainModel = HistoryMainModel( + state = HistoryMainState.Init, + historyInfo = HistoryInfo( + unReadAlarm = true, + totalHeartCount = 30, + unWrittenCount = 5 + ) + ), + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailState.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailState.kt new file mode 100644 index 00000000..e0a26eda --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailState.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +sealed interface HistoryDetailState { + data object Init : HistoryDetailState + data object Loading : HistoryDetailState +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailViewModel.kt new file mode 100644 index 00000000..a6ad64dd --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/HistoryDetailViewModel.kt @@ -0,0 +1,71 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail + +import ac.dnd.bookkeeping.android.domain.model.error.ServerException +import ac.dnd.bookkeeping.android.domain.model.event.Group +import ac.dnd.bookkeeping.android.domain.usecase.history.GetHistoryGroupListUseCase +import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.asEventFlow +import ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type.HistoryViewType +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class HistoryDetailViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val getHistoryGroupListUseCase: GetHistoryGroupListUseCase +) : BaseViewModel() { + + private val _state: MutableStateFlow = + MutableStateFlow(HistoryDetailState.Init) + val state: StateFlow = _state.asStateFlow() + + private val _event: MutableEventFlow = MutableEventFlow() + val event: EventFlow = _event.asEventFlow() + + private val _totalGroups: MutableStateFlow> = MutableStateFlow(emptyList()) + val totalGroups: StateFlow> = _totalGroups.asStateFlow() + + private val _takeGroups: MutableStateFlow> = MutableStateFlow(emptyList()) + val takeGroups: StateFlow> = _takeGroups.asStateFlow() + + private val _giveGroups: MutableStateFlow> = MutableStateFlow(emptyList()) + val giveGroups: StateFlow> = _giveGroups.asStateFlow() + + init { + HistoryViewType.entries.forEach { + loadHistoryData(it) + } + } + + private fun loadHistoryData(historyViewType: HistoryViewType) { + launch { + _state.value = HistoryDetailState.Loading + getHistoryGroupListUseCase(historyViewType.typeName) + .onSuccess { + when (historyViewType) { + HistoryViewType.TOTAL -> _totalGroups.value = it + HistoryViewType.TAKE -> _takeGroups.value = it + HistoryViewType.GIVE -> _giveGroups.value = it + } + } + .onFailure { exception -> + when (exception) { + is ServerException -> { + _event.emit(HistoryDetailEvent.GetHistoryRelationList.Failure(exception)) + } + + else -> { + _event.emit(HistoryDetailEvent.GetHistoryRelationList.Error(exception)) + } + } + } + _state.value = HistoryDetailState.Init + } + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/item/HistoryRelationItem.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/item/HistoryRelationItem.kt new file mode 100644 index 00000000..96c4138a --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/item/HistoryRelationItem.kt @@ -0,0 +1,160 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.item + +import ac.dnd.bookkeeping.android.domain.model.event.Relation +import ac.dnd.bookkeeping.android.domain.model.event.RelationGroup +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Body2 +import ac.dnd.bookkeeping.android.presentation.common.theme.Caption1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray200 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline3 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary4 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary5 +import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes +import ac.dnd.bookkeeping.android.presentation.common.theme.Space12 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space8 +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 java.text.DecimalFormat + +@Composable +fun HistoryRelationItem( + relation: Relation, + onSelectCard: (Relation) -> Unit +) { + Card( + shape = Shapes.medium, + backgroundColor = Gray000, + modifier = Modifier.clickable { + onSelectCard(relation) + } + ) { + Column( + modifier = Modifier.padding(Space12) + ) { + Text( + text = relation.name, + style = Headline3.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ), + lineHeight = 24.sp, + textAlign = TextAlign.Left + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = relation.group.name, + style = Caption1.merge( + color = Gray600, + fontWeight = FontWeight.Normal + ) + ) + Spacer(modifier = Modifier.height(9.5.dp)) + Divider( + modifier = Modifier.height(1.dp), + color = Gray200 + ) + Spacer(modifier = Modifier.height(20.5.dp)) + + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = "받음", + style = Body1.merge( + color = Gray600, + fontWeight = FontWeight.Normal + ), + modifier = Modifier.align(Alignment.CenterStart) + ) + Row( + modifier = Modifier.align(Alignment.CenterEnd), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = DecimalFormat("#,###").format(relation.takeMoney), + style = Body1.merge( + color = Primary5, + fontWeight = FontWeight.SemiBold + ) + ) + Text( + text = "원", + style = Body2.merge( + color = Primary4, + fontWeight = FontWeight.SemiBold + ), + textAlign = TextAlign.Left + ) + } + } + Spacer(modifier = Modifier.height(Space8)) + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = "보냄", + style = Body1.merge( + color = Gray600, + fontWeight = FontWeight.Normal + ), + modifier = Modifier.align(Alignment.CenterStart) + ) + Row( + modifier = Modifier.align(Alignment.CenterEnd), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "-${DecimalFormat("#,###").format(relation.giveMoney)}", + style = Body1.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ), + ) + Text( + text = "원", + style = Body2.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ), + textAlign = TextAlign.Left + ) + } + } + Spacer(modifier = Modifier.height(Space8)) + } + } +} + +@Preview +@Composable +fun HeartItemPreview() { + HistoryRelationItem( + relation = Relation( + id = 8446, + name = "Harlan Yang", + group = RelationGroup( + id = 2337, + name = "Angelia McBride", + ), + giveMoney = 8327, + takeMoney = 4954 + ), + onSelectCard = {} + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistorySortedType.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistorySortedType.kt new file mode 100644 index 00000000..e96ce59d --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistorySortedType.kt @@ -0,0 +1,8 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type + +enum class HistorySortedType( + val typeName: String +) { + LATEST("최신순"), + INTIMACY("친밀도순") +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistoryViewType.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistoryViewType.kt new file mode 100644 index 00000000..437bb418 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/history/main/detail/type/HistoryViewType.kt @@ -0,0 +1,9 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.history.main.detail.type + +enum class HistoryViewType( + val typeName: String +) { + TOTAL("total"), + TAKE("take"), + GIVE("give") +} diff --git a/presentation/src/main/res/drawable/ic_notification.xml b/presentation/src/main/res/drawable/ic_notification.xml index a224bdbd..f90dd0c2 100644 --- a/presentation/src/main/res/drawable/ic_notification.xml +++ b/presentation/src/main/res/drawable/ic_notification.xml @@ -1,9 +1,9 @@ - + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/presentation/src/main/res/drawable/ic_notification_on.xml b/presentation/src/main/res/drawable/ic_notification_on.xml new file mode 100644 index 00000000..11eaf092 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_notification_on.xml @@ -0,0 +1,15 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_search.xml b/presentation/src/main/res/drawable/ic_search.xml index 5b97583e..6d3b16da 100644 --- a/presentation/src/main/res/drawable/ic_search.xml +++ b/presentation/src/main/res/drawable/ic_search.xml @@ -1,9 +1,20 @@ - + android:viewportWidth="24" + android:viewportHeight="24"> + +