Skip to content

Commit af8c94d

Browse files
committed
refactor: 운동 장소 등록 화면 Compose 전환 #525
- ExercisePlaceRegisterScreen Composable 신규 (Stateless + Preview) - UndabangTopBar - PlaceInfoSection (마커 아이콘 + 장소명 + 주소) - 장소 이름 입력 (UndabangTextField Outlined) - NoticeSection (경고 아이콘 + 안내 문구) - PrimaryButton (등록 완료) - ExercisePlaceRegisterFragment BindingFragment → Fragment + ComposeView + applyAppTheme - onCreate에서 ViewModel 초기화 (한 번만 실행) - ExercisePlaceRegisterViewModel LiveData → StateFlow + 결과는 SharedFlow (1회성 이벤트) - fragment_exercise_place_register.xml 삭제, nav graph tools:layout 정리 - ExercisePlaceViewModelTest, ExercisePlaceRegisterViewModelTest SharedFlow 패턴(turbine test)으로 갱신
1 parent ca19d3b commit af8c94d

7 files changed

Lines changed: 312 additions & 205 deletions

File tree

feature/matching/src/main/java/com/project200/feature/matching/place/ExercisePlaceRegisterFragment.kt

Lines changed: 69 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,100 @@
11
package com.project200.feature.matching.place
22

3+
import android.os.Bundle
4+
import android.view.LayoutInflater
35
import android.view.View
6+
import android.view.ViewGroup
47
import android.widget.Toast
5-
import androidx.core.widget.doAfterTextChanged
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.ui.platform.ComposeView
10+
import androidx.fragment.app.Fragment
611
import androidx.fragment.app.viewModels
12+
import androidx.lifecycle.Lifecycle
13+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
14+
import androidx.lifecycle.lifecycleScope
15+
import androidx.lifecycle.repeatOnLifecycle
716
import androidx.navigation.fragment.findNavController
817
import androidx.navigation.fragment.navArgs
918
import com.project200.domain.model.BaseResult
10-
import com.project200.presentation.base.BindingFragment
19+
import com.project200.presentation.compose.applyAppTheme
1120
import com.project200.undabang.feature.matching.R
12-
import com.project200.undabang.feature.matching.databinding.FragmentExercisePlaceRegisterBinding
1321
import dagger.hilt.android.AndroidEntryPoint
22+
import kotlinx.coroutines.launch
1423

15-
@AndroidEntryPoint // Hilt 어노테이션 추가
16-
class ExercisePlaceRegisterFragment : BindingFragment<FragmentExercisePlaceRegisterBinding> (R.layout.fragment_exercise_place_register) {
24+
@AndroidEntryPoint
25+
class ExercisePlaceRegisterFragment : Fragment() {
1726
private val viewModel: ExercisePlaceRegisterViewModel by viewModels()
1827
private val args: ExercisePlaceRegisterFragmentArgs by navArgs()
1928

20-
override fun getViewBinding(view: View): FragmentExercisePlaceRegisterBinding {
21-
return FragmentExercisePlaceRegisterBinding.bind(view)
22-
}
23-
24-
override fun setupViews() {
25-
binding.baseToolbar.apply {
26-
setTitle(getString(R.string.exercise_place_register_title))
27-
showBackButton(true) { findNavController().navigateUp() }
28-
}
29-
binding.placeAddressTv.text = args.address
30-
29+
override fun onCreate(savedInstanceState: Bundle?) {
30+
super.onCreate(savedInstanceState)
3131
viewModel.initializePlaceInfo(
3232
id = args.placeId,
3333
placeName = args.name,
3434
placeAddress = args.address,
3535
latitude = args.latitude.toDouble(),
3636
longitude = args.longitude.toDouble(),
3737
)
38-
binding.placeNameEt.setText(args.name)
39-
setupListeners()
4038
}
4139

42-
private fun setupListeners() {
43-
binding.placeNameEt.doAfterTextChanged { text ->
44-
viewModel.onPlaceNameChanged(text.toString())
45-
}
46-
47-
binding.registerBtn.setOnClickListener {
48-
if (binding.placeNameEt.text.toString().isEmpty()) {
49-
Toast.makeText(requireContext(), R.string.empty_place_name, Toast.LENGTH_SHORT).show()
40+
override fun onCreateView(
41+
inflater: LayoutInflater,
42+
container: ViewGroup?,
43+
savedInstanceState: Bundle?,
44+
): View =
45+
ComposeView(requireContext()).apply {
46+
applyAppTheme {
47+
val placeNameInput by viewModel.customPlaceName.collectAsStateWithLifecycle()
48+
val placeAddress by viewModel.placeAddress.collectAsStateWithLifecycle()
49+
ExercisePlaceRegisterScreen(
50+
placeName = if (placeNameInput.isBlank()) getString(R.string.place_name_hint) else placeNameInput,
51+
placeAddress = placeAddress,
52+
placeNameInput = placeNameInput,
53+
onPlaceNameChange = viewModel::onPlaceNameChanged,
54+
onRegisterClick = {
55+
if (placeNameInput.isBlank()) {
56+
Toast.makeText(requireContext(), R.string.empty_place_name, Toast.LENGTH_SHORT).show()
57+
}
58+
viewModel.confirmExercisePlace()
59+
},
60+
onBackClick = { findNavController().navigateUp() },
61+
)
5062
}
51-
viewModel.confirmExercisePlace()
5263
}
53-
}
5464

55-
override fun setupObservers() {
56-
viewModel.customPlaceName.observe(viewLifecycleOwner) { name ->
57-
binding.placeNameTv.text = if (name.isBlank()) getString(R.string.place_name_hint) else name
58-
}
59-
60-
viewModel.registrationResult.observe(viewLifecycleOwner) { result ->
61-
when (result) {
62-
is BaseResult.Success -> {
63-
Toast.makeText(requireContext(), getString(R.string.success_register_place), Toast.LENGTH_SHORT).show()
64-
findNavController().navigate(R.id.action_complete_registration_and_go_to_place_list)
65-
}
66-
is BaseResult.Error -> {
67-
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
68-
}
69-
}
70-
}
65+
override fun onViewCreated(
66+
view: View,
67+
savedInstanceState: Bundle?,
68+
) {
69+
super.onViewCreated(view, savedInstanceState)
7170

72-
viewModel.editResult.observe(viewLifecycleOwner) { result ->
73-
when (result) {
74-
is BaseResult.Success -> {
75-
Toast.makeText(requireContext(), getString(R.string.success_edit_place), Toast.LENGTH_SHORT).show()
76-
findNavController().navigate(R.id.action_complete_registration_and_go_to_place_list)
71+
viewLifecycleOwner.lifecycleScope.launch {
72+
repeatOnLifecycle(Lifecycle.State.STARTED) {
73+
launch {
74+
viewModel.registrationResult.collect { result ->
75+
when (result) {
76+
is BaseResult.Success -> {
77+
Toast.makeText(requireContext(), getString(R.string.success_register_place), Toast.LENGTH_SHORT).show()
78+
findNavController().navigate(R.id.action_complete_registration_and_go_to_place_list)
79+
}
80+
is BaseResult.Error -> {
81+
Toast.makeText(requireContext(), result.message, Toast.LENGTH_SHORT).show()
82+
}
83+
}
84+
}
7785
}
78-
is BaseResult.Error -> {
79-
Toast.makeText(requireContext(), R.string.error_fail_to_register_place, Toast.LENGTH_SHORT).show()
86+
launch {
87+
viewModel.editResult.collect { result ->
88+
when (result) {
89+
is BaseResult.Success -> {
90+
Toast.makeText(requireContext(), getString(R.string.success_edit_place), Toast.LENGTH_SHORT).show()
91+
findNavController().navigate(R.id.action_complete_registration_and_go_to_place_list)
92+
}
93+
is BaseResult.Error -> {
94+
Toast.makeText(requireContext(), R.string.error_fail_to_register_place, Toast.LENGTH_SHORT).show()
95+
}
96+
}
97+
}
8098
}
8199
}
82100
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package com.project200.feature.matching.place
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
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.width
14+
import androidx.compose.material3.MaterialTheme
15+
import androidx.compose.material3.Text
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.mutableStateOf
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.setValue
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.res.painterResource
24+
import androidx.compose.ui.res.stringResource
25+
import androidx.compose.ui.tooling.preview.Preview
26+
import androidx.compose.ui.unit.dp
27+
import androidx.compose.ui.unit.sp
28+
import com.project200.presentation.compose.components.button.PrimaryButton
29+
import com.project200.presentation.compose.components.input.TextFieldVariant
30+
import com.project200.presentation.compose.components.input.UndabangTextField
31+
import com.project200.presentation.compose.components.layout.UndabangTopBar
32+
import com.project200.presentation.compose.theme.AppTheme
33+
import com.project200.presentation.compose.theme.ColorBlack
34+
import com.project200.presentation.compose.theme.ColorGray200
35+
import com.project200.presentation.compose.theme.ColorWhite300
36+
import com.project200.presentation.compose.theme.contentBold
37+
import com.project200.presentation.compose.theme.contentRegular
38+
import com.project200.undabang.feature.matching.R
39+
40+
@Composable
41+
fun ExercisePlaceRegisterScreen(
42+
placeName: String,
43+
placeAddress: String,
44+
placeNameInput: String,
45+
onPlaceNameChange: (String) -> Unit,
46+
onRegisterClick: () -> Unit,
47+
onBackClick: () -> Unit,
48+
modifier: Modifier = Modifier,
49+
) {
50+
Column(
51+
modifier =
52+
modifier
53+
.fillMaxSize()
54+
.background(ColorWhite300),
55+
) {
56+
UndabangTopBar(
57+
title = stringResource(R.string.exercise_place_register_title),
58+
onNavigationClick = onBackClick,
59+
)
60+
61+
PlaceInfoSection(
62+
placeName = placeName,
63+
placeAddress = placeAddress,
64+
)
65+
66+
Spacer(Modifier.height(30.dp))
67+
68+
Text(
69+
text = stringResource(R.string.place_name_title),
70+
style = MaterialTheme.typography.contentBold,
71+
color = ColorBlack,
72+
modifier = Modifier.padding(horizontal = 20.dp),
73+
)
74+
75+
Spacer(Modifier.height(4.dp))
76+
77+
UndabangTextField(
78+
value = placeNameInput,
79+
onValueChange = onPlaceNameChange,
80+
hint = stringResource(R.string.place_name_hint),
81+
variant = TextFieldVariant.Outlined,
82+
modifier =
83+
Modifier
84+
.fillMaxWidth()
85+
.padding(horizontal = 20.dp)
86+
.height(50.dp),
87+
)
88+
89+
Spacer(Modifier.height(30.dp))
90+
91+
NoticeSection()
92+
93+
Spacer(Modifier.weight(1f))
94+
95+
PrimaryButton(
96+
text = stringResource(R.string.exercise_place_register_complete),
97+
onClick = onRegisterClick,
98+
modifier =
99+
Modifier
100+
.fillMaxWidth()
101+
.padding(horizontal = 20.dp),
102+
)
103+
104+
Spacer(Modifier.height(40.dp))
105+
}
106+
}
107+
108+
@Composable
109+
private fun PlaceInfoSection(
110+
placeName: String,
111+
placeAddress: String,
112+
modifier: Modifier = Modifier,
113+
) {
114+
Row(
115+
modifier =
116+
modifier
117+
.fillMaxWidth()
118+
.height(100.dp)
119+
.padding(horizontal = 20.dp),
120+
verticalAlignment = Alignment.CenterVertically,
121+
) {
122+
Image(
123+
painter = painterResource(R.drawable.ic_member_marker),
124+
contentDescription = null,
125+
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(ColorBlack),
126+
)
127+
Spacer(Modifier.width(12.dp))
128+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
129+
Text(
130+
text = placeName,
131+
style = MaterialTheme.typography.contentBold,
132+
color = ColorBlack,
133+
)
134+
Text(
135+
text = placeAddress,
136+
style = MaterialTheme.typography.contentRegular,
137+
color = ColorBlack,
138+
)
139+
}
140+
}
141+
}
142+
143+
@Composable
144+
private fun NoticeSection(modifier: Modifier = Modifier) {
145+
Column(
146+
modifier = modifier.padding(horizontal = 20.dp),
147+
) {
148+
Row(verticalAlignment = Alignment.CenterVertically) {
149+
Image(
150+
painter = painterResource(R.drawable.ic_warning),
151+
contentDescription = null,
152+
)
153+
Spacer(Modifier.width(6.dp))
154+
Text(
155+
text = stringResource(R.string.place_name_notice_title),
156+
color = ColorGray200,
157+
fontSize = 12.sp,
158+
style = MaterialTheme.typography.contentBold,
159+
)
160+
}
161+
Spacer(Modifier.height(12.dp))
162+
Text(
163+
text = stringResource(R.string.place_name_notice_content),
164+
color = ColorGray200,
165+
fontSize = 12.sp,
166+
)
167+
}
168+
}
169+
170+
@Preview(showBackground = true, heightDp = 720)
171+
@Composable
172+
private fun ExercisePlaceRegisterScreenPreview() {
173+
AppTheme {
174+
var name by remember { mutableStateOf("운다방 체육관") }
175+
ExercisePlaceRegisterScreen(
176+
placeName = "체육관",
177+
placeAddress = "서울특별시 강남구 테헤란로 123",
178+
placeNameInput = name,
179+
onPlaceNameChange = { name = it },
180+
onRegisterClick = {},
181+
onBackClick = {},
182+
)
183+
}
184+
}

0 commit comments

Comments
 (0)