Skip to content

Commit 1707627

Browse files
authored
Merge pull request #65 from YAPP-Github/feature/home
[Fix/#64] 관리 화면 복수 그룹 관리 기능 및 UI 적용
2 parents d685d0c + 09e9a44 commit 1707627

File tree

11 files changed

+280
-75
lines changed

11 files changed

+280
-75
lines changed

app/src/main/java/com/yapp/breake/di/UseCaseModule.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.yapp.breake.domain.usecaseImpl.DeleteAccountUseCaseImpl
1010
import com.yapp.breake.domain.usecase.DeleteGroupUseCase
1111
import com.yapp.breake.domain.usecase.FindAppGroupUseCase
1212
import com.yapp.breake.domain.usecase.GetNicknameUseCase
13+
import com.yapp.breake.domain.usecase.GrantNewGroupIdUseCase
1314
import com.yapp.breake.domain.usecase.LoginUseCase
1415
import com.yapp.breake.domain.usecase.LogoutUseCase
1516
import com.yapp.breake.domain.usecaseImpl.LogoutUseCaseImpl
@@ -25,6 +26,7 @@ import com.yapp.breake.domain.usecaseImpl.CreateNewGroupUseCaseImpl
2526
import com.yapp.breake.domain.usecaseImpl.DeleteGroupUseCaseImpl
2627
import com.yapp.breake.domain.usecaseImpl.FindAppGroupUsecaseImpl
2728
import com.yapp.breake.domain.usecaseImpl.GetNicknameUseCaseImpl
29+
import com.yapp.breake.domain.usecaseImpl.GrantNewGroupIdUseCaseImpl
2830
import com.yapp.breake.domain.usecaseImpl.LoginUseCaseImpl
2931
import com.yapp.breake.domain.usecaseImpl.SetAlarmUsecaseImpl
3032
import com.yapp.breake.domain.usecaseImpl.SetBlockingAlarmUseCaseImpl
@@ -113,4 +115,9 @@ internal abstract class UseCaseModule {
113115
abstract fun bindDeleteGroupUseCase(
114116
deleteGroupUseCase: DeleteGroupUseCaseImpl,
115117
): DeleteGroupUseCase
118+
119+
@Binds
120+
abstract fun bindGrantNewGroupIdUseCase(
121+
grantNewGroupIdUseCase: GrantNewGroupIdUseCaseImpl,
122+
): GrantNewGroupIdUseCase
116123
}

core/database/src/main/java/com/yapp/breake/core/database/dao/AppGroupDao.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ interface AppGroupDao {
1717
@Insert(onConflict = OnConflictStrategy.REPLACE)
1818
suspend fun insertAppGroup(groupEntity: GroupEntity)
1919

20+
@Query(
21+
"""
22+
SELECT CASE
23+
WHEN NOT EXISTS(SELECT 1 FROM `group_table` WHERE groupId = 1) THEN 1
24+
ELSE (
25+
SELECT MIN(t1.groupId + 1)
26+
FROM `group_table` t1
27+
LEFT JOIN `group_table` t2 ON t1.groupId + 1 = t2.groupId
28+
WHERE t2.groupId IS NULL
29+
)
30+
END
31+
""",
32+
)
33+
suspend fun getAvailableMinGroupId(): Long
34+
2035
@Transaction
2136
@Query("SELECT * FROM `group_table`")
2237
fun observeAppGroup(): Flow<List<AppGroupEntity>>

data/src/main/java/com/yapp/breake/data/repositoryImpl/AppGroupRepositoryImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ class AppGroupRepositoryImpl @Inject constructor(
2222
)
2323
}
2424

25+
override suspend fun getAvailableMinGroupId(): Long =
26+
appGroupDao.getAvailableMinGroupId()
27+
2528
override suspend fun deleteAppGroupByGroupId(groupId: Long) {
2629
appGroupDao.deleteAppGroupById(groupId)
2730
}

domain/src/main/java/com/yapp/breake/domain/repository/AppGroupRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface AppGroupRepository {
99

1010
suspend fun insertAppGroup(appGroup: AppGroup)
1111

12+
suspend fun getAvailableMinGroupId(): Long
13+
1214
suspend fun deleteAppGroupByGroupId(groupId: Long)
1315

1416
fun observeAppGroup(): Flow<List<AppGroup>>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.yapp.breake.domain.usecase
2+
3+
interface GrantNewGroupIdUseCase {
4+
suspend operator fun invoke(
5+
onError: suspend (Throwable) -> Unit,
6+
): Long
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.yapp.breake.domain.usecaseImpl
2+
3+
import com.yapp.breake.domain.repository.AppGroupRepository
4+
import com.yapp.breake.domain.usecase.GrantNewGroupIdUseCase
5+
import javax.inject.Inject
6+
7+
class GrantNewGroupIdUseCaseImpl @Inject constructor(
8+
private val appGroupRepository: AppGroupRepository,
9+
) : GrantNewGroupIdUseCase {
10+
override suspend fun invoke(onError: suspend (Throwable) -> Unit): Long {
11+
return appGroupRepository.getAvailableMinGroupId()
12+
}
13+
}

presentation/home/src/main/java/com/yapp/breake/presentation/home/HomeScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ private fun HomeContent(
111111
onEditClick = {
112112
onShowEditScreen(it.id)
113113
},
114+
onAddClick = onShowAddScreen,
114115
)
115116
}
116117

presentation/home/src/main/java/com/yapp/breake/presentation/home/component/AppGroupList.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ internal fun AppGroupList(
3737
}
3838

3939
@Composable
40-
private fun AppGroupItem(
40+
internal fun AppGroupItem(
4141
appGroup: AppGroup,
4242
onEditClick: () -> Unit,
4343
modifier: Modifier = Modifier,
Lines changed: 184 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,215 @@
11
package com.yapp.breake.presentation.home.screen
22

3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.animateContentSize
5+
import androidx.compose.animation.core.LinearEasing
6+
import androidx.compose.animation.core.LinearOutSlowInEasing
7+
import androidx.compose.animation.core.animateDpAsState
8+
import androidx.compose.animation.core.animateFloatAsState
9+
import androidx.compose.animation.core.tween
10+
import androidx.compose.animation.fadeIn
11+
import androidx.compose.animation.fadeOut
12+
import androidx.compose.foundation.Image
13+
import androidx.compose.foundation.LocalOverscrollFactory
14+
import androidx.compose.foundation.background
315
import androidx.compose.foundation.layout.Column
416
import androidx.compose.foundation.layout.Row
517
import androidx.compose.foundation.layout.fillMaxSize
618
import androidx.compose.foundation.layout.fillMaxWidth
719
import androidx.compose.foundation.layout.padding
8-
import androidx.compose.foundation.layout.statusBarsPadding
20+
import androidx.compose.foundation.lazy.LazyColumn
21+
import androidx.compose.foundation.lazy.itemsIndexed
22+
import androidx.compose.foundation.lazy.rememberLazyListState
23+
import androidx.compose.material.icons.Icons
24+
import androidx.compose.material.icons.filled.Add
25+
import androidx.compose.material3.ExperimentalMaterial3Api
26+
import androidx.compose.material3.Icon
27+
import androidx.compose.material3.IconButton
928
import androidx.compose.material3.MaterialTheme
29+
import androidx.compose.material3.Scaffold
1030
import androidx.compose.material3.Text
31+
import androidx.compose.material3.TopAppBar
32+
import androidx.compose.material3.TopAppBarDefaults
1133
import androidx.compose.runtime.Composable
34+
import androidx.compose.runtime.CompositionLocalProvider
35+
import androidx.compose.runtime.derivedStateOf
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.remember
1238
import androidx.compose.ui.Alignment
1339
import androidx.compose.ui.Modifier
40+
import androidx.compose.ui.layout.ContentScale
41+
import androidx.compose.ui.res.painterResource
1442
import androidx.compose.ui.res.stringResource
43+
import androidx.compose.ui.text.style.TextAlign
1544
import androidx.compose.ui.tooling.preview.Preview
1645
import androidx.compose.ui.unit.dp
1746
import com.yapp.breake.core.designsystem.component.HorizontalSpacer
1847
import com.yapp.breake.core.designsystem.component.VerticalSpacer
1948
import com.yapp.breake.core.designsystem.theme.BrakeTheme
49+
import com.yapp.breake.core.designsystem.theme.Gray100
2050
import com.yapp.breake.core.designsystem.theme.Gray200
51+
import com.yapp.breake.core.designsystem.theme.Gray900
2152
import com.yapp.breake.core.model.app.AppGroup
2253
import com.yapp.breake.presentation.home.R
23-
import com.yapp.breake.presentation.home.component.AppGroupList
24-
import com.yapp.breake.presentation.home.component.ImageTextBox
54+
import com.yapp.breake.presentation.home.component.AppGroupItem
2555

56+
@OptIn(ExperimentalMaterial3Api::class)
2657
@Composable
2758
internal fun ListScreen(
2859
appGroups: List<AppGroup>,
2960
onEditClick: (AppGroup) -> Unit,
61+
onAddClick: () -> Unit,
3062
) {
31-
Column(
32-
modifier = Modifier.fillMaxSize(),
33-
horizontalAlignment = Alignment.CenterHorizontally,
34-
) {
35-
ImageTextBox(
36-
imageRes = R.drawable.img_home_list,
37-
text = stringResource(R.string.list_screen_description),
38-
modifier = Modifier.statusBarsPadding(),
39-
)
40-
VerticalSpacer(30.dp)
41-
Row(
42-
verticalAlignment = Alignment.Bottom,
43-
modifier = Modifier
44-
.fillMaxWidth()
45-
.padding(horizontal = 24.dp),
46-
) {
47-
Text(
48-
text = stringResource(R.string.group),
49-
style = BrakeTheme.typography.subtitle22SB,
50-
color = MaterialTheme.colorScheme.onSurface,
51-
)
52-
HorizontalSpacer(1f)
53-
Text(
54-
text = stringResource(R.string.group_count_format, appGroups.size),
55-
style = BrakeTheme.typography.body12M,
56-
color = Gray200,
57-
)
63+
val listState = rememberLazyListState()
64+
val showTitle by remember {
65+
derivedStateOf {
66+
listState.firstVisibleItemIndex > 0
5867
}
59-
VerticalSpacer(16.dp)
60-
AppGroupList(
61-
appGroups = appGroups,
62-
onEditClick = onEditClick,
63-
modifier = Modifier
64-
.fillMaxWidth()
65-
.padding(horizontal = 16.dp),
66-
)
6768
}
69+
val alpha by animateFloatAsState(
70+
targetValue = if (showTitle) 1f else 0f,
71+
animationSpec = tween(20, easing = LinearEasing),
72+
label = "appbarAlpha",
73+
)
74+
val container = Gray900.copy(alpha = alpha)
75+
val headerKey = "groupsHeader"
76+
77+
Scaffold(
78+
topBar = {
79+
TopAppBar(
80+
title = {
81+
AnimatedVisibility(
82+
visible = showTitle,
83+
modifier = Modifier.fillMaxWidth(),
84+
enter = fadeIn(),
85+
exit = fadeOut(),
86+
) {
87+
Text(
88+
text = stringResource(R.string.list_screen_title),
89+
style = BrakeTheme.typography.subtitle16SB,
90+
color = Gray100,
91+
textAlign = TextAlign.Center,
92+
)
93+
}
94+
},
95+
modifier = Modifier.animateContentSize(),
96+
navigationIcon = {
97+
// 공간만 차지하는 네비게이션 아이콘
98+
HorizontalSpacer(48.dp)
99+
},
100+
actions = {
101+
IconButton(onClick = onAddClick) {
102+
Icon(
103+
imageVector = Icons.Default.Add,
104+
contentDescription = stringResource(R.string.add_button_content_description),
105+
)
106+
}
107+
},
108+
colors = TopAppBarDefaults.topAppBarColors(
109+
containerColor = container,
110+
scrolledContainerColor = container,
111+
navigationIconContentColor = Gray100,
112+
titleContentColor = Gray100,
113+
actionIconContentColor = Gray100,
114+
),
115+
)
116+
},
117+
content = { innerPadding ->
118+
CompositionLocalProvider(LocalOverscrollFactory provides null) {
119+
LazyColumn(
120+
modifier = Modifier
121+
.fillMaxSize()
122+
.padding(bottom = 16.dp),
123+
state = listState,
124+
horizontalAlignment = Alignment.CenterHorizontally,
125+
) {
126+
item {
127+
Column(
128+
modifier = Modifier
129+
.fillMaxWidth()
130+
.padding(horizontal = 16.dp),
131+
horizontalAlignment = Alignment.CenterHorizontally,
132+
) {
133+
Image(
134+
painter = painterResource(id = R.drawable.img_home_list),
135+
contentDescription = null,
136+
modifier = Modifier.fillMaxWidth(),
137+
contentScale = ContentScale.FillWidth,
138+
)
139+
VerticalSpacer(12.dp)
140+
Text(
141+
text = stringResource(R.string.list_screen_description),
142+
style = BrakeTheme.typography.subtitle22SB,
143+
color = Gray100,
144+
textAlign = TextAlign.Center,
145+
)
146+
}
147+
}
148+
149+
stickyHeader(key = headerKey) {
150+
val isPinned by remember {
151+
derivedStateOf {
152+
val info = listState.layoutInfo.visibleItemsInfo
153+
.firstOrNull { it.key == headerKey }
154+
info?.offset == 0
155+
}
156+
}
157+
158+
// 핀일 때만 AppBar 높이 적용, 아니면 0
159+
val topInset = innerPadding.calculateTopPadding()
160+
val spacer by animateDpAsState(
161+
targetValue = if (isPinned) topInset else 36.dp,
162+
animationSpec = tween(500, easing = LinearOutSlowInEasing),
163+
label = "HeaderTopInset",
164+
)
165+
Column(
166+
modifier = Modifier.background(container),
167+
) {
168+
VerticalSpacer(spacer)
169+
Row(
170+
verticalAlignment = Alignment.Bottom,
171+
modifier = Modifier
172+
.fillMaxWidth()
173+
.padding(top = 4.dp, bottom = 16.dp)
174+
.padding(horizontal = 24.dp),
175+
) {
176+
Text(
177+
text = stringResource(R.string.group),
178+
style = BrakeTheme.typography.subtitle22SB,
179+
color = MaterialTheme.colorScheme.onSurface,
180+
)
181+
HorizontalSpacer(1f)
182+
Text(
183+
text = stringResource(
184+
R.string.group_count_format,
185+
appGroups.size,
186+
),
187+
style = BrakeTheme.typography.body12M,
188+
color = Gray200,
189+
)
190+
}
191+
}
192+
}
193+
194+
itemsIndexed(
195+
appGroups,
196+
key = { _, appGroup -> appGroup.id },
197+
) { index, appGroup ->
198+
AppGroupItem(
199+
appGroup = appGroup,
200+
onEditClick = { onEditClick(appGroup) },
201+
modifier = Modifier
202+
.fillMaxWidth()
203+
.padding(horizontal = 16.dp),
204+
)
205+
if (index != appGroups.lastIndex) {
206+
VerticalSpacer(12.dp)
207+
}
208+
}
209+
}
210+
}
211+
},
212+
)
68213
}
69214

70215
@Preview
@@ -74,6 +219,7 @@ private fun ListScreenPreview() {
74219
ListScreen(
75220
appGroups = listOf(AppGroup.sample),
76221
onEditClick = { /* TODO: Handle app group click */ },
222+
onAddClick = { /* Handle add click */ },
77223
)
78224
}
79225
}

0 commit comments

Comments
 (0)