Skip to content

Commit 124fa23

Browse files
committed
Feat: 회원탈퇴 화면 구현
1 parent 3b1c1bd commit 124fa23

2 files changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package com.threegap.bitnagil.presentation.withdrawal
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.statusBarsPadding
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.foundation.text.BasicTextField
16+
import androidx.compose.material3.Text
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.focus.FocusRequester
23+
import androidx.compose.ui.focus.focusRequester
24+
import androidx.compose.ui.focus.onFocusChanged
25+
import androidx.compose.ui.platform.LocalFocusManager
26+
import androidx.compose.ui.tooling.preview.Preview
27+
import androidx.compose.ui.unit.dp
28+
import androidx.hilt.navigation.compose.hiltViewModel
29+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
30+
import com.threegap.bitnagil.designsystem.BitnagilTheme
31+
import com.threegap.bitnagil.designsystem.R
32+
import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon
33+
import com.threegap.bitnagil.designsystem.component.atom.BitnagilSelectButton
34+
import com.threegap.bitnagil.designsystem.component.atom.BitnagilSelectButtonColor
35+
import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton
36+
import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar
37+
import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple
38+
import com.threegap.bitnagil.presentation.common.flow.collectAsEffect
39+
import com.threegap.bitnagil.presentation.withdrawal.component.WithdrawalConfirmDialog
40+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalIntent
41+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalReason
42+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalSideEffect
43+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalState
44+
45+
@Composable
46+
fun WithdrawalScreenContainer(
47+
navigateToBack: () -> Unit,
48+
navigateToLogin: () -> Unit,
49+
viewModel: WithdrawalViewModel = hiltViewModel(),
50+
) {
51+
val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
52+
53+
viewModel.sideEffectFlow.collectAsEffect { sideEffect ->
54+
when (sideEffect) {
55+
is WithdrawalSideEffect.NavigateToBack -> navigateToBack()
56+
is WithdrawalSideEffect.NavigateToLogin -> navigateToLogin()
57+
}
58+
}
59+
60+
if (uiState.showSuccessDialog) {
61+
WithdrawalConfirmDialog(
62+
onConfirm = { viewModel.sendIntent(WithdrawalIntent.OnSuccessDialogConfirm) },
63+
)
64+
}
65+
66+
WithdrawalScreen(
67+
uiState = uiState,
68+
onTermsToggle = { viewModel.sendIntent(WithdrawalIntent.OnTermsToggle) },
69+
onReasonSelect = { viewModel.sendIntent(WithdrawalIntent.OnReasonSelected(it)) },
70+
onCustomReasonChanged = { viewModel.sendIntent(WithdrawalIntent.OnCustomReasonChanged(it)) },
71+
onBackClick = { viewModel.sendIntent(WithdrawalIntent.OnBackClick) },
72+
onWithdrawalClick = viewModel::withdrawal,
73+
)
74+
}
75+
76+
@Composable
77+
private fun WithdrawalScreen(
78+
uiState: WithdrawalState,
79+
onTermsToggle: () -> Unit,
80+
onReasonSelect: (WithdrawalReason?) -> Unit,
81+
onCustomReasonChanged: (String) -> Unit,
82+
onBackClick: () -> Unit,
83+
onWithdrawalClick: () -> Unit,
84+
modifier: Modifier = Modifier,
85+
) {
86+
val focusManager = LocalFocusManager.current
87+
val focusRequester = remember { FocusRequester() }
88+
89+
Column(
90+
modifier = modifier
91+
.fillMaxSize()
92+
.background(BitnagilTheme.colors.white)
93+
.statusBarsPadding(),
94+
) {
95+
BitnagilTopBar(
96+
title = "탈퇴하기",
97+
showBackButton = true,
98+
onBackClick = onBackClick,
99+
)
100+
101+
Column(
102+
modifier = Modifier.padding(horizontal = 16.dp),
103+
) {
104+
Spacer(modifier = Modifier.height(46.dp))
105+
106+
Text(
107+
text = "정말 탈퇴하시겠어요?",
108+
color = BitnagilTheme.colors.coolGray10,
109+
style = BitnagilTheme.typography.title3SemiBold,
110+
modifier = Modifier.padding(bottom = 5.dp),
111+
)
112+
113+
Text(
114+
text = "탈퇴하면 보관 중인 데이터와 서비스 이용 내역이\n모두 삭제되고, 다시 가입해도 복구되지 않아요.",
115+
color = BitnagilTheme.colors.coolGray50,
116+
style = BitnagilTheme.typography.body1Medium,
117+
)
118+
119+
Spacer(modifier = Modifier.height(26.dp))
120+
121+
Row(
122+
modifier = Modifier
123+
.fillMaxWidth()
124+
.clickableWithoutRipple { onTermsToggle() },
125+
verticalAlignment = Alignment.CenterVertically,
126+
horizontalArrangement = Arrangement.spacedBy(10.dp),
127+
) {
128+
BitnagilIcon(
129+
id = if (uiState.isTermsChecked) R.drawable.ic_check_circle else R.drawable.ic_check_default,
130+
tint = null,
131+
)
132+
133+
Text(
134+
text = "유의사항을 확인했어요.",
135+
color = BitnagilTheme.colors.coolGray40,
136+
style = BitnagilTheme.typography.body2Medium,
137+
)
138+
}
139+
}
140+
141+
if (uiState.isTermsChecked) {
142+
Spacer(modifier = Modifier.height(48.dp))
143+
144+
Column(
145+
modifier = Modifier.padding(horizontal = 16.dp),
146+
) {
147+
Text(
148+
text = "탈퇴 사유를 알려주실 수 있나요?",
149+
color = BitnagilTheme.colors.coolGray10,
150+
style = BitnagilTheme.typography.title3SemiBold,
151+
modifier = Modifier.padding(bottom = 16.dp),
152+
)
153+
154+
WithdrawalReason.entries.forEach { reason ->
155+
BitnagilSelectButton(
156+
title = reason.displayText,
157+
selected = uiState.selectedReason == reason,
158+
onClick = {
159+
onReasonSelect(reason)
160+
focusManager.clearFocus()
161+
},
162+
titleTextStyle = BitnagilTheme.typography.body1Medium,
163+
colors = BitnagilSelectButtonColor.withdrawal(),
164+
modifier = Modifier.padding(bottom = 12.dp),
165+
)
166+
}
167+
168+
BasicTextField(
169+
value = uiState.customReasonText,
170+
onValueChange = onCustomReasonChanged,
171+
textStyle = BitnagilTheme.typography.subtitle1Medium.copy(
172+
color = BitnagilTheme.colors.coolGray10,
173+
),
174+
modifier = Modifier
175+
.focusRequester(focusRequester)
176+
.onFocusChanged { focusState ->
177+
if (focusState.isFocused) {
178+
onReasonSelect(null)
179+
}
180+
},
181+
decorationBox = { innerTextField ->
182+
Box(
183+
modifier = Modifier
184+
.fillMaxWidth()
185+
.background(
186+
color = BitnagilTheme.colors.coolGray99,
187+
shape = RoundedCornerShape(12.dp),
188+
)
189+
.height(112.dp)
190+
.padding(vertical = 14.dp, horizontal = 20.dp),
191+
) {
192+
if (uiState.customReasonText.isEmpty()) {
193+
Text(
194+
text = "기타사항(직접 입력)",
195+
color = BitnagilTheme.colors.coolGray80,
196+
style = BitnagilTheme.typography.subtitle1Medium,
197+
)
198+
}
199+
innerTextField()
200+
}
201+
},
202+
)
203+
}
204+
}
205+
206+
Spacer(modifier = Modifier.weight(1f))
207+
208+
BitnagilTextButton(
209+
text = "탈퇴하기",
210+
onClick = onWithdrawalClick,
211+
enabled = uiState.isWithdrawalEnabled,
212+
modifier = Modifier
213+
.fillMaxWidth()
214+
.padding(horizontal = 16.dp, vertical = 14.dp),
215+
)
216+
}
217+
}
218+
219+
@Preview
220+
@Composable
221+
private fun WithdrawalScreenPreview() {
222+
WithdrawalScreen(
223+
uiState = WithdrawalState(
224+
isTermsChecked = true,
225+
),
226+
onTermsToggle = {},
227+
onReasonSelect = {},
228+
onCustomReasonChanged = {},
229+
onBackClick = {},
230+
onWithdrawalClick = {},
231+
)
232+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.threegap.bitnagil.presentation.withdrawal
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import androidx.lifecycle.viewModelScope
5+
import com.threegap.bitnagil.domain.auth.usecase.WithdrawalUseCase
6+
import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel
7+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalIntent
8+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalSideEffect
9+
import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalState
10+
import dagger.hilt.android.lifecycle.HiltViewModel
11+
import kotlinx.coroutines.launch
12+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
13+
import javax.inject.Inject
14+
15+
@HiltViewModel
16+
class WithdrawalViewModel @Inject constructor(
17+
savedStateHandle: SavedStateHandle,
18+
private val withdrawalUseCase: WithdrawalUseCase,
19+
) : MviViewModel<WithdrawalState, WithdrawalSideEffect, WithdrawalIntent>(
20+
savedStateHandle = savedStateHandle,
21+
initState = WithdrawalState(),
22+
) {
23+
override suspend fun SimpleSyntax<WithdrawalState, WithdrawalSideEffect>.reduceState(
24+
intent: WithdrawalIntent,
25+
state: WithdrawalState,
26+
): WithdrawalState? {
27+
val newState = when (intent) {
28+
is WithdrawalIntent.UpdateLoading -> state.copy(isLoading = intent.isLoading)
29+
is WithdrawalIntent.OnTermsToggle -> state.copy(isTermsChecked = !state.isTermsChecked)
30+
is WithdrawalIntent.ShowSuccessDialog -> state.copy(showSuccessDialog = true)
31+
32+
is WithdrawalIntent.OnCustomReasonChanged -> {
33+
state.copy(customReasonText = intent.text)
34+
}
35+
36+
is WithdrawalIntent.OnReasonSelected -> {
37+
state.copy(
38+
selectedReason = intent.reason,
39+
customReasonText = "",
40+
)
41+
}
42+
43+
is WithdrawalIntent.OnBackClick -> {
44+
sendSideEffect(WithdrawalSideEffect.NavigateToBack)
45+
null
46+
}
47+
48+
is WithdrawalIntent.OnSuccessDialogConfirm -> {
49+
sendSideEffect(WithdrawalSideEffect.NavigateToLogin)
50+
null
51+
}
52+
}
53+
54+
return newState
55+
}
56+
57+
fun withdrawal() {
58+
if (container.stateFlow.value.isLoading) return
59+
sendIntent(WithdrawalIntent.UpdateLoading(true))
60+
viewModelScope.launch {
61+
withdrawalUseCase().fold(
62+
onSuccess = {
63+
sendIntent(WithdrawalIntent.UpdateLoading(false))
64+
sendIntent(WithdrawalIntent.ShowSuccessDialog)
65+
},
66+
onFailure = {
67+
sendIntent(WithdrawalIntent.UpdateLoading(false))
68+
},
69+
)
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)