Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.teamsolply.solply.designsystem.component.dropdown

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.teamsolply.solply.designsystem.R
import com.teamsolply.solply.designsystem.theme.SolplyTheme
import com.teamsolply.solply.ui.extension.customClickable

@Composable
fun SolplyBasicDropDown(
defaultLabel: String,
onClickDropIcon: () -> Unit,
modifier: Modifier = Modifier,
isDropped: Boolean = false,
content: @Composable ColumnScope.() -> Unit,
) {
Column(
modifier = modifier
.clip(
RoundedCornerShape(20.dp)
)
.border(
width = 1.dp,
color = SolplyTheme.colors.gray300,
shape = RoundedCornerShape(20.dp)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (isDropped) {
Modifier.background(
color = SolplyTheme.colors.gray100
)
} else {
Modifier.background(
color = SolplyTheme.colors.white
)
}
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = defaultLabel,
color = SolplyTheme.colors.gray900,
style = SolplyTheme.typography.body16M,
modifier = Modifier.padding(start = 20.dp)
)
Icon(
painter = painterResource(R.drawable.ic_arrow_down),
contentDescription = "",
modifier = Modifier
.padding(end = 20.dp, top = 14.dp, bottom = 14.dp)
.height(24.dp)
.width(24.dp)
.scale(
scaleX = 1f,
scaleY = if (isDropped) -1f else 1f
)
.customClickable(
rippleEnabled = false,
onClick = onClickDropIcon
)
)
Comment on lines +73 to +88
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

드롭다운 토글 아이콘에 접근성 라벨이 없습니다.
아이콘에 contentDescription=""로 설정된 상태에서 클릭 동작을 부여하면 스크린리더가 “레이블 없는 버튼”으로 인식해 조작 의도를 전달하지 못합니다. 최소한 기본 라벨을 읽어 주도록 설정해 주세요.

             Icon(
                 painter = painterResource(R.drawable.ic_arrow_down),
-                contentDescription = "",
+                contentDescription = defaultLabel,
                 modifier = Modifier
                     .padding(end = 20.dp, top = 14.dp, bottom = 14.dp)
                     .height(24.dp)
                     .width(24.dp)

Committable suggestion skipped: line range outside the PR's diff.

}
HorizontalDivider(color = SolplyTheme.colors.gray300)
AnimatedVisibility(
visible = isDropped,
enter = slideInVertically(),
exit = slideOutVertically()
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
content()
}
}
}
}
13 changes: 13 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_arrow_down.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M8.286,10.552L12.143,14.49L16,10.552"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#242424"
android:strokeLineCap="round"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.teamsolply.solply.course.navigation.navigateCourse
import com.teamsolply.solply.main.splash.Splash
import com.teamsolply.solply.maps.navigation.navigateMaps
import com.teamsolply.solply.mypage.navigation.navigateMypage
import com.teamsolply.solply.mypage.profile.navigation.navigateProfile
import com.teamsolply.solply.oauth.navigation.navigateOauth
import com.teamsolply.solply.onboarding.navigation.navigateOnBoarding
import com.teamsolply.solply.place.navigation.navigatePlace
Expand Down Expand Up @@ -137,6 +138,22 @@ internal class MainNavigator(
)
}

fun navigateToMypage(
navOptions: NavOptions
) {
navController.navigateMypage(
navOptions = navOptions
)
}

fun navigateToProfile(
navOptions: NavOptions
) {
navController.navigateProfile(
navOptions = navOptions
)
}

fun navigateToFavoriteTown(navOptions: NavOptions = navOptions {}) {
navController.navigateFavoriteTown(navOptions)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ import com.teamsolply.solply.main.model.SolplySnackBarData
import com.teamsolply.solply.main.splash.splashNavGraph
import com.teamsolply.solply.maps.navigation.mapsNavGraph
import com.teamsolply.solply.model.SnackBarType
import com.teamsolply.solply.mypage.navigation.Mypage
import com.teamsolply.solply.mypage.navigation.mypageNavGraph
import com.teamsolply.solply.mypage.profile.navigation.Profile
import com.teamsolply.solply.mypage.profile.navigation.profileNavGraph
import com.teamsolply.solply.oauth.navigation.oauthNavGraph
import com.teamsolply.solply.onboarding.navigation.onBoardingNavGraph
import com.teamsolply.solply.place.navigation.placeNavGraph
Expand Down Expand Up @@ -333,7 +336,22 @@ internal fun MainScreen(
)
mypageNavGraph(
paddingValues = innerPadding,
navigateToBack = navigator::navigateToBack
navigateToBack = navigator::navigateToBack,
navigateToProfile = {
val navOptions = navOptions { }
navigator.navigateToProfile(navOptions)
}
)
profileNavGraph(
paddingValues = innerPadding,
navigateToBack = navigator::navigateToBack,
navigateToMypage = {
val navOptions = navOptions {
popUpTo<Mypage> { inclusive = true }
launchSingleTop = true
}
navigator.navigateToMypage(navOptions = navOptions)
}
)
favoriteTownNavGraph(
paddingValues = innerPadding,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package com.teamsolply.solply.mypage

import com.teamsolply.solply.mypage.model.DropDownPersonaItem
import com.teamsolply.solply.ui.base.SideEffect
import com.teamsolply.solply.ui.base.UiIntent
import com.teamsolply.solply.ui.base.UiState
import kotlinx.collections.immutable.persistentListOf

data class MypageState(
val town: String = "연희동"
val town: String = "연희동",
val nickname: String = "",
) : UiState

sealed interface MypageIntent : UiIntent
sealed interface MypageIntent : UiIntent {

sealed interface MypageSideEffect : SideEffect
}
Comment on lines +14 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

detekt 경고의 원인이 된 빈 인터페이스 본문 제거 필요

sealed interface MypageIntent : UiIntent {} 형태로 바뀌면서 detekt.empty-blocks.EmptyClassBlock 경고가 발생하고, 파이프라인에서 실패할 가능성이 높습니다. 아직 구현할 멤버가 없다면 기존처럼 본문 없이 선언하거나 TODO를 추가해 주세요.

-sealed interface MypageIntent : UiIntent {
-
-}
+sealed interface MypageIntent : UiIntent

As per static analysis hints

🧰 Tools
🪛 detekt (1.23.8)

[warning] 14-16: The class or object MypageIntent is empty.

(detekt.empty-blocks.EmptyClassBlock)

🤖 Prompt for AI Agents
In feature/mypage/src/main/java/com/teamsolply/solply/mypage/MypageContract.kt
around lines 14-16, the sealed interface is declared with an empty block "sealed
interface MypageIntent : UiIntent { }" which triggers
detekt.empty-blocks.EmptyClassBlock; remove the empty braces so it reads "sealed
interface MypageIntent : UiIntent" or, if you intend to add members later,
replace the empty block with a single TODO comment (e.g. "// TODO: add intents")
to satisfy static analysis.


sealed interface MypageSideEffect : SideEffect {
data object NavigateToBack : MypageSideEffect
data object NavigateToProfile : MypageSideEffect
data object NavigateToMypage : MypageSideEffect
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,20 @@ import com.teamsolply.solply.ui.extension.customClickable
fun MypageRoute(
paddingValues: PaddingValues,
navigateToBack: () -> Unit,
navigateToProfile: () -> Unit,
viewModel: MypageViewModel = hiltViewModel()
) {
MypageScreen(
onBackButtonClick = navigateToBack,
onProfileEditClick = navigateToProfile,
modifier = Modifier.padding(paddingValues),
onBackButtonClick = navigateToBack
)
}

@Composable
fun MypageScreen(
onBackButtonClick: () -> Unit,
onProfileEditClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
Expand Down Expand Up @@ -85,7 +88,12 @@ fun MypageScreen(
style = SolplyTheme.typography.display20Sb
)
Row(
modifier = Modifier.padding(top = 12.dp)
modifier = Modifier
.padding(top = 12.dp)
.customClickable(
rippleEnabled = false,
onClick = onProfileEditClick
)
) {
Comment on lines +91 to 97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

프로필 수정 탭 영역이 Material 최소 터치 타깃(48dp)을 만족하지 않습니다.

Row가 fillMaxWidth() 없이 텍스트 크기만큼만 확장되어 터치 영역이 매우 좁습니다. 접근성 측면에서 최소 48dp 정사각형 이상을 보장해야 하므로 가로폭과 패딩을 넓혀 주세요.

-        Row(
-            modifier = Modifier
-                .padding(top = 12.dp)
-                .customClickable(
-                    rippleEnabled = false,
-                    onClick = onProfileEditClick
-                )
-        ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(top = 12.dp)
+                .customClickable(
+                    rippleEnabled = false,
+                    onClick = onProfileEditClick
+                )
+                .padding(horizontal = 20.dp, vertical = 12.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
modifier = Modifier
.padding(top = 12.dp)
.customClickable(
rippleEnabled = false,
onClick = onProfileEditClick
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.customClickable(
rippleEnabled = false,
onClick = onProfileEditClick
)
.padding(horizontal = 20.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// ... existing content ...
}
🤖 Prompt for AI Agents
In feature/mypage/src/main/java/com/teamsolply/solply/mypage/MypageScreen.kt
around lines 91–97, the profile-edit Row only sizes to its text and its touch
target is smaller than the Material minimum (48dp); change the Modifier to
expand the tappable area by adding fillMaxWidth() and increasing horizontal
padding, and enforce a minimum height (e.g., heightIn(min = 48.dp) or
sizeIn(minHeight = 48.dp)) so the customClickable covers a 48dp-high touch
target across the full width; ensure the Modifier order applies
fillMaxWidth()/padding()/heightIn() before customClickable so the whole area is
clickable.

Text(
text = "프로필 수정",
Expand Down Expand Up @@ -172,7 +180,8 @@ fun MypageScreen(
private fun MypageScreenPreview() {
SolplyTheme {
MypageScreen(
onBackButtonClick = {}
onBackButtonClick = {},
onProfileEditClick = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.teamsolply.solply.mypage.component

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.teamsolply.solply.designsystem.component.dropdown.SolplyBasicDropDown
import com.teamsolply.solply.designsystem.theme.SolplyTheme
import com.teamsolply.solply.mypage.model.DropDownPersonaItem
import com.teamsolply.solply.ui.extension.customClickable
import kotlinx.collections.immutable.persistentListOf

@Composable
fun SolplyPersonaDropDown(
placeholder: String,
onClickItem: (Int) -> Unit,
onClickDropIcon: () -> Unit,
dropDownContents: List<DropDownPersonaItem>,
selectedIndex: Int,
modifier: Modifier = Modifier,
isDropped: Boolean = false,
isSelected: Boolean = false
) {
SolplyBasicDropDown(
defaultLabel = if (isSelected) dropDownContents.get(selectedIndex).label else placeholder,
onClickDropIcon = onClickDropIcon,
isDropped = isDropped,
modifier = modifier
) {
dropDownContents.forEachIndexed { index, item ->
Comment on lines +39 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

선택 인덱스 검증 없이 get을 호출하면 크래시가 납니다.

selectedIndex가 리스트 범위를 벗어난 상태(예: 서버 갱신으로 리스트가 짧아진 경우)에서 isSelected가 true면 get이 바로 IndexOutOfBoundsException을 던집니다. 안전하게 getOrNull을 사용해 플레이스홀더로 대체하는 방식을 권장합니다.

-        defaultLabel = if (isSelected) dropDownContents.get(selectedIndex).label else placeholder,
+        defaultLabel = if (isSelected) {
+            dropDownContents.getOrNull(selectedIndex)?.label ?: placeholder
+        } else {
+            placeholder
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaultLabel = if (isSelected) dropDownContents.get(selectedIndex).label else placeholder,
onClickDropIcon = onClickDropIcon,
isDropped = isDropped,
modifier = modifier
) {
dropDownContents.forEachIndexed { index, item ->
defaultLabel = if (isSelected) {
dropDownContents.getOrNull(selectedIndex)?.label ?: placeholder
} else {
placeholder
},
onClickDropIcon = onClickDropIcon,
isDropped = isDropped,
modifier = modifier
) {
dropDownContents.forEachIndexed { index, item ->
🤖 Prompt for AI Agents
In
feature/mypage/src/main/java/com/teamsolply/solply/mypage/component/SolplyPersonaDropDown.kt
around lines 39–44, calling dropDownContents.get(selectedIndex) without
validating selectedIndex can throw IndexOutOfBoundsException when the list has
shrunk; replace the unsafe get with a safe lookup and fallback to the
placeholder (e.g. use dropDownContents.getOrNull(selectedIndex)?.label ?:
placeholder) and/or ensure isSelected is only true when selectedIndex is within
bounds so the defaultLabel resolves safely to the selected item's label or the
placeholder.

if (index != selectedIndex) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
color = SolplyTheme.colors.white
)
.customClickable(
rippleEnabled = false,
onClick = { onClickItem(index) }
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
Text(
text = item.label,
color = SolplyTheme.colors.gray900,
style = SolplyTheme.typography.body16M,
modifier = Modifier.padding(
start = 20.dp,
top = 14.dp,
bottom = 14.dp
)
)
}
HorizontalDivider(color = SolplyTheme.colors.gray300)
}
}
}
}

@Preview
@Composable
private fun SolplyPersonaDropDownPreview() {
var isDropped by remember { mutableStateOf(false) }
var isSelected by remember { mutableStateOf(false) }
var selectedIndex by remember { mutableIntStateOf(-1) }
SolplyTheme {
Column(
modifier = Modifier
.fillMaxWidth()
.background(SolplyTheme.colors.white)
) {
SolplyPersonaDropDown(
placeholder = "조용한 공간에 오래 머물고 싶어요",
onClickItem = {},
onClickDropIcon = {},
dropDownContents = persistentListOf(
DropDownPersonaItem(
"이곳저곳 둘러보고 싶어요"
),
DropDownPersonaItem(
"취향이 담긴 곳을 찾고 싶어요"
),
DropDownPersonaItem(
"자연을 감상하며 쉬고 싶어요"
),
DropDownPersonaItem(
"조용한 공간에 오래 머물고 싶어요"
),
),
isDropped = false,
selectedIndex = 0,
isSelected = false
)
SolplyPersonaDropDown(
isDropped = isDropped,
placeholder = "선택해주세요",
onClickItem = {
selectedIndex = it
isSelected = true
},
onClickDropIcon = { isDropped = !isDropped },
dropDownContents = persistentListOf(
DropDownPersonaItem(
"이곳저곳 둘러보고 싶어요"
),
DropDownPersonaItem(
"취향이 담긴 곳을 찾고 싶어요"
),
DropDownPersonaItem(
"자연을 감상하며 쉬고 싶어요"
),
DropDownPersonaItem(
"조용한 공간에 오래 머물고 싶어요"
),
),
selectedIndex = selectedIndex,
isSelected = isSelected
)
}
}
}
Loading