Skip to content

Commit d7f079e

Browse files
authored
feat - 내정보페이지 구현 (#85)
* fix: CustomColor.kt에서 black 수정 * feat: 메인화면 공통 헤더 컴포넌트 생성 * feat: 마이페이지 아름답게 구현
1 parent a356c45 commit d7f079e

File tree

5 files changed

+458
-9
lines changed

5 files changed

+458
-9
lines changed

app/src/main/java/com/apptive/japkor/data/local/DataStoreManager.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import androidx.datastore.preferences.preferencesDataStore
1010
import com.apptive.japkor.data.model.UserInfoResponse
1111
import com.google.gson.Gson
1212
import kotlinx.coroutines.flow.map
13+
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.combine
15+
import com.apptive.japkor.utils.required_info.RequiredInfoReverseMapper
1316

1417
private val Context.dataStore by preferencesDataStore(name = "user_prefs")
1518

@@ -225,4 +228,47 @@ class DataStoreManager(private val context: Context) {
225228
prefs[key] = value
226229
}
227230
}
231+
232+
// My Profile UI Model and Flow
233+
data class MyProfileUiModel(
234+
val name: String,
235+
val residenceArea: String,
236+
val smokingStatus: String,
237+
val drinkingFrequency: String,
238+
val education: String,
239+
val religion: String,
240+
val thumbnailImageUrl: String,
241+
)
242+
243+
fun getMyProfileUiModel(): Flow<MyProfileUiModel> {
244+
val nameFlow = getUserName()
245+
val residenceFlow = context.dataStore.data.map { it[KEY_RESIDENCE_AREA] ?: "" }
246+
val smokingFlow = context.dataStore.data.map { it[KEY_SMOKING_STATUS] ?: "" }
247+
val drinkingFlow = context.dataStore.data.map { it[KEY_DRINKING_FREQUENCY] ?: "" }
248+
val educationFlow = context.dataStore.data.map { it[KEY_EDUCATION] ?: "" }
249+
val religionFlow = context.dataStore.data.map { it[KEY_RELIGION] ?: "" }
250+
val thumbFlow = context.dataStore.data.map { it[KEY_THUMBNAIL_IMAGE_URL] ?: "" }
251+
252+
return combine(
253+
nameFlow, residenceFlow, smokingFlow, drinkingFlow, educationFlow, religionFlow, thumbFlow
254+
) { values: Array<String> ->
255+
val name = values[0]
256+
val residence = values[1]
257+
val smokingCode = values[2]
258+
val drinkingCode = values[3]
259+
val educationCode = values[4]
260+
val religionCode = values[5]
261+
val thumb = values[6]
262+
263+
MyProfileUiModel(
264+
name = name,
265+
residenceArea = residence,
266+
smokingStatus = RequiredInfoReverseMapper.smoking(smokingCode),
267+
drinkingFrequency = RequiredInfoReverseMapper.drinking(drinkingCode),
268+
education = RequiredInfoReverseMapper.education(educationCode),
269+
religion = RequiredInfoReverseMapper.religion(religionCode),
270+
thumbnailImageUrl = thumb
271+
)
272+
}
273+
}
228274
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.apptive.japkor.ui.components
2+
3+
import androidx.compose.foundation.layout.*
4+
import androidx.compose.material3.*
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.Alignment
7+
import androidx.compose.ui.Modifier
8+
import com.apptive.japkor.ui.theme.CustomColor
9+
import androidx.compose.ui.res.painterResource
10+
import androidx.compose.ui.unit.dp
11+
import com.apptive.japkor.R
12+
13+
@Composable
14+
fun CommonHeader(
15+
title: String,
16+
modifier: Modifier = Modifier,
17+
showBackButton: Boolean = true,
18+
onBack: (() -> Unit)? = null,
19+
) {
20+
Column(modifier = modifier.fillMaxWidth()) {
21+
Box(
22+
modifier = Modifier
23+
.fillMaxWidth()
24+
.height(56.dp),
25+
contentAlignment = Alignment.Center
26+
) {
27+
Text(
28+
text = title,
29+
style = MaterialTheme.typography.titleMedium
30+
)
31+
32+
// 왼쪽 영역: 버튼이 없어도 자리(48dp) 유지해서 제목이 안 흔들림
33+
Box(
34+
modifier = Modifier
35+
.align(Alignment.CenterStart)
36+
.size(48.dp),
37+
contentAlignment = Alignment.Center
38+
) {
39+
if (showBackButton && onBack != null) {
40+
IconButton(onClick = onBack) {
41+
Icon(
42+
painter = painterResource(id = R.drawable.ic_back),
43+
contentDescription = "back",
44+
modifier = Modifier.size(24.dp),
45+
)
46+
}
47+
}
48+
}
49+
}
50+
51+
HorizontalDivider(
52+
modifier = Modifier.padding(horizontal = 16.dp),
53+
color = CustomColor.gray300,
54+
thickness = 1.dp
55+
)
56+
57+
}
58+
}
Lines changed: 248 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,264 @@
11
package com.apptive.japkor.ui.main.mypage
22

3-
import androidx.compose.foundation.layout.Box
4-
import androidx.compose.foundation.layout.fillMaxSize
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.rememberScrollState
7+
import androidx.compose.foundation.shape.CircleShape
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.foundation.verticalScroll
10+
import androidx.compose.material3.HorizontalDivider
11+
import androidx.compose.material3.Surface
512
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.collectAsState
14+
import androidx.compose.runtime.getValue
615
import androidx.compose.ui.Alignment
716
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.draw.clip
18+
import androidx.compose.ui.draw.drawBehind
19+
import androidx.compose.ui.geometry.CornerRadius
20+
import androidx.compose.ui.graphics.Color
21+
import androidx.compose.ui.graphics.PathEffect
22+
import androidx.compose.ui.graphics.drawscope.Stroke
23+
import androidx.compose.ui.layout.ContentScale
24+
import androidx.compose.ui.platform.LocalContext
25+
import androidx.compose.ui.unit.Dp
26+
import androidx.compose.ui.unit.dp
27+
import coil.compose.rememberAsyncImagePainter
28+
import com.apptive.japkor.data.local.DataStoreManager
29+
import com.apptive.japkor.data.local.DataStoreManager.MyProfileUiModel
30+
import com.apptive.japkor.ui.components.CommonHeader
831
import com.apptive.japkor.ui.components.CustomText
932
import com.apptive.japkor.ui.components.CustomTextType
1033
import com.apptive.japkor.ui.theme.CustomColor
1134

1235
@Composable
13-
fun MypageScreen(modifier: Modifier = Modifier) {
14-
Box(
15-
modifier = modifier.fillMaxSize(),
16-
contentAlignment = Alignment.Center
36+
fun MypageScreen(
37+
modifier: Modifier = Modifier,
38+
showBackButton: Boolean = true,
39+
onBack: (() -> Unit)? = null,
40+
) {
41+
val context = LocalContext.current
42+
val dataStoreManager = DataStoreManager(context)
43+
44+
val profile by dataStoreManager.getMyProfileUiModel()
45+
.collectAsState(initial = null)
46+
47+
Column(
48+
modifier = modifier
49+
.fillMaxSize()
50+
.background(Color.White)
51+
) {
52+
CommonHeader(
53+
title = "마이페이지",
54+
showBackButton = showBackButton,
55+
onBack = onBack
56+
)
57+
58+
Column(
59+
modifier = Modifier
60+
.fillMaxWidth()
61+
.verticalScroll(rememberScrollState())
62+
.padding(horizontal = 16.dp)
63+
) {
64+
Spacer(Modifier.height(16.dp))
65+
66+
MyProfileCard(profile = profile)
67+
68+
Spacer(Modifier.height(24.dp))
69+
}
70+
}
71+
}
72+
73+
@Composable
74+
private fun MyProfileCard(profile: MyProfileUiModel?) {
75+
Column(
76+
modifier = Modifier
77+
.fillMaxWidth()
78+
.dashedBorder(
79+
color = Color(0xFFD7D7D7),
80+
strokeWidth = 1.dp,
81+
cornerRadius = 14.dp
82+
)
83+
.padding(16.dp)
84+
) {
85+
// My Profile pill
86+
Box(
87+
modifier = Modifier
88+
.fillMaxWidth(),
89+
contentAlignment = Alignment.Center
90+
) {
91+
Surface(
92+
shape = RoundedCornerShape(999.dp),
93+
color = Color.White,
94+
shadowElevation = 0.dp,
95+
tonalElevation = 0.dp
96+
) {
97+
Box(
98+
modifier = Modifier
99+
.background(Color.White, RoundedCornerShape(999.dp))
100+
.drawBehind { }
101+
.padding(horizontal = 14.dp, vertical = 6.dp)
102+
) {
103+
CustomText(
104+
text = "My Profile",
105+
type = CustomTextType.body,
106+
color = CustomColor.gray400
107+
)
108+
}
109+
}
110+
}
111+
112+
Spacer(Modifier.height(12.dp))
113+
114+
Row(verticalAlignment = Alignment.CenterVertically) {
115+
//ProfileImageWithBadge(imageUrl = profile?.thumbnailImageUrl) // 돋보기 있는 버전
116+
ProfileImage(imageUrl = profile?.thumbnailImageUrl)
117+
118+
Spacer(Modifier.width(12.dp))
119+
120+
Column {
121+
CustomText(
122+
text = profile?.name?.ifBlank { "" } ?: "",
123+
type = CustomTextType.headline,
124+
color = CustomColor.black
125+
)
126+
}
127+
}
128+
129+
Spacer(Modifier.height(14.dp))
130+
HorizontalDivider(color = CustomColor.gray100)
131+
Spacer(Modifier.height(10.dp))
132+
133+
InfoRow(label = "지역", value = profile?.residenceArea ?: "")
134+
InfoRow(label = "흡연", value = profile?.smokingStatus ?: "")
135+
InfoRow(label = "음주", value = profile?.drinkingFrequency ?: "")
136+
InfoRow(label = "학력", value = profile?.education ?: "")
137+
InfoRow(
138+
label = "종교",
139+
value = profile?.religion ?: ""
140+
)
141+
142+
Spacer(Modifier.height(12.dp))
143+
Box(
144+
modifier = Modifier
145+
.fillMaxWidth()
146+
.padding(
147+
horizontal = 13.dp,
148+
vertical = 12.dp
149+
)
150+
) {
151+
CustomText(
152+
text = "이미 심사 통과한 프로필은 수정이 어려워요.",
153+
type = CustomTextType.body,
154+
color = CustomColor.gray300
155+
)
156+
}
157+
}
158+
}
159+
160+
@Composable
161+
private fun ProfileImage(imageUrl: String?) {
162+
val painter = rememberAsyncImagePainter(model = imageUrl)
163+
164+
Image(
165+
painter = painter,
166+
contentDescription = "profile",
167+
contentScale = ContentScale.Crop,
168+
modifier = Modifier
169+
.size(64.dp)
170+
.clip(CircleShape) // ✅ 원형
171+
.background(Color(0xFFEDEDED)) // 로딩/빈값 배경
172+
)
173+
}
174+
175+
//@Composable
176+
//private fun ProfileImageWithBadge(imageUrl: String?) {
177+
// Box(modifier = Modifier.size(70.dp)) {
178+
// val painter = rememberAsyncImagePainter(model = imageUrl)
179+
//
180+
// Image(
181+
// painter = painter,
182+
// contentDescription = "profile",
183+
// contentScale = ContentScale.Crop,
184+
// modifier = Modifier
185+
// .size(64.dp)
186+
// .background(Color(0xFFEDEDED), CircleShape)
187+
// )
188+
//
189+
// Box(
190+
// modifier = Modifier
191+
// .size(24.dp)
192+
// .align(Alignment.BottomEnd)
193+
// .background(Color(0xFF7C8055), CircleShape),
194+
// contentAlignment = Alignment.Center
195+
// ) {
196+
// Icon(
197+
// imageVector = Icons.Default.Search,
198+
// contentDescription = "search",
199+
// tint = Color.White,
200+
// modifier = Modifier.size(16.dp)
201+
// )
202+
// }
203+
// }
204+
//}
205+
206+
@Composable
207+
private fun InfoRow(label: String, value: String) {
208+
Row(
209+
modifier = Modifier
210+
.fillMaxWidth()
211+
.padding(
212+
horizontal = 13.dp,
213+
vertical = 17.dp
214+
),
215+
verticalAlignment = Alignment.CenterVertically
17216
) {
18217
CustomText(
19-
text = "내정보 페이지입니다.",
218+
text = label,
20219
type = CustomTextType.body,
21-
color = CustomColor.gray400
220+
color = CustomColor.gray300,
221+
)
222+
223+
Spacer(modifier = Modifier.weight(1f))
224+
225+
CustomText(
226+
text = value,
227+
type = CustomTextType.title,
228+
color = CustomColor.black
22229
)
23230
}
231+
HorizontalDivider(
232+
modifier = Modifier
233+
.padding(
234+
horizontal = 13.dp,
235+
vertical = 0.dp
236+
),
237+
color = CustomColor.gray100,
238+
thickness = 1.dp
239+
)
240+
}
241+
242+
/**
243+
* ✅ 점선 테두리: "여기서만 쓸 거라" 파일 내부에 private로 둠
244+
*/
245+
private fun Modifier.dashedBorder(
246+
color: Color,
247+
strokeWidth: Dp,
248+
cornerRadius: Dp,
249+
on: Dp = 6.dp,
250+
off: Dp = 6.dp,
251+
) = this.drawBehind {
252+
val stroke = Stroke(
253+
width = strokeWidth.toPx(),
254+
pathEffect = PathEffect.dashPathEffect(
255+
floatArrayOf(on.toPx(), off.toPx()),
256+
0f
257+
)
258+
)
259+
drawRoundRect(
260+
color = color,
261+
style = stroke,
262+
cornerRadius = CornerRadius(cornerRadius.toPx())
263+
)
24264
}

app/src/main/java/com/apptive/japkor/ui/theme/CustomColor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ object CustomColor {
1717
val gray400 = Color(0xFF6B7280)
1818

1919
val white = Color(0xFFFFFFFF)
20-
val black = Color(0xFF000000)
20+
val black = Color(0xFF3C3C3C)
2121

2222
}
2323

0 commit comments

Comments
 (0)