-
Notifications
You must be signed in to change notification settings - Fork 0
[Fix/#183] 프로필 QA 이슈 수정 #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
623bd57
9980028
d5901f5
0ad85ed
8ee50c3
2a97ae3
b2c9154
808f8ea
75afa24
2ad8a60
872635d
398e1e4
48dc5e4
7947b31
4489afb
3d981ed
86d8557
94b1084
d5ad75d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.flint.core.common.extension | ||
|
|
||
| import android.app.Activity | ||
| import android.content.Context | ||
| import android.content.ContextWrapper | ||
|
|
||
| fun Context.findActivity(): Activity { | ||
| var context = this | ||
| while (context is ContextWrapper) { | ||
| if (context is Activity) return context | ||
| context = context.baseContext | ||
| } | ||
| throw IllegalStateException("no activity") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.flint.data.dto.auth.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class WithdrawResponseDto( | ||
| @SerialName("status") | ||
| val status: Int, | ||
| @SerialName("message") | ||
| val message: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,4 +38,10 @@ class AuthRepository @Inject constructor( | |
| result | ||
| } | ||
|
|
||
| suspend fun withdraw(): Result<Unit> = | ||
| suspendRunCatching { | ||
| api.withdraw() | ||
| preferencesManager.removeString(ACCESS_TOKEN) | ||
| preferencesManager.clearAll() | ||
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.flint.presentation.profile | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.PaddingValues | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
|
|
@@ -17,20 +18,30 @@ import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.platform.LocalContext | ||
| import androidx.compose.ui.platform.LocalUriHandler | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.tooling.preview.PreviewParameter | ||
| import androidx.compose.ui.tooling.preview.PreviewParameterProvider | ||
| import androidx.compose.ui.unit.dp | ||
| import androidx.hilt.navigation.compose.hiltViewModel | ||
| import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
| import com.flint.core.common.extension.findActivity | ||
| import com.flint.core.common.util.UiState | ||
| import com.flint.core.designsystem.component.bottomsheet.OttListBottomSheet | ||
| import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator | ||
| import com.flint.core.designsystem.component.listView.CollectionSection | ||
| import com.flint.core.designsystem.component.listView.SavedContentsSection | ||
| import com.flint.core.designsystem.component.topappbar.FlintBackTopAppbar | ||
| import com.flint.core.designsystem.theme.FlintTheme | ||
| import com.flint.core.designsystem.theme.FlintTheme.colors | ||
| import com.flint.core.navigation.model.CollectionListRouteType | ||
| import com.flint.domain.model.collection.CollectionListModel | ||
| import com.flint.domain.model.content.BookmarkedContentListModel | ||
| import com.flint.domain.model.ott.OttListModel | ||
| import com.flint.domain.model.user.KeywordListModel | ||
| import com.flint.domain.model.user.UserProfileResponseModel | ||
| import com.flint.presentation.MainActivity | ||
| import com.flint.presentation.profile.component.ProfileKeywordSection | ||
| import com.flint.presentation.profile.component.ProfileTopSection | ||
| import com.flint.presentation.profile.sideeffect.ProfileSideEffect | ||
|
|
@@ -40,13 +51,15 @@ import com.flint.presentation.profile.uistate.ProfileUiState | |
| @Composable | ||
| fun ProfileRoute( | ||
| paddingValues: PaddingValues, | ||
| navigateUp: () -> Unit, | ||
| navigateToCollectionList: (routeType: CollectionListRouteType, userId: String?) -> Unit, | ||
| navigateToSavedContentList: () -> Unit, // TODO: 스프린트에서 구현 | ||
| navigateToCollectionDetail: (collectionId: String) -> Unit, | ||
| viewModel: ProfileViewModel = hiltViewModel(), | ||
| ) { | ||
| val uiState by viewModel.uiState.collectAsStateWithLifecycle() | ||
| val uriHandler = LocalUriHandler.current | ||
| val context = LocalContext.current | ||
|
|
||
| var showOttListBottomSheet by remember { mutableStateOf(false) } | ||
| var ottListModel by remember { mutableStateOf(OttListModel()) } | ||
|
|
@@ -63,6 +76,9 @@ fun ProfileRoute( | |
| ottListModel = sideEffect.ottListModel | ||
| showOttListBottomSheet = true | ||
| } | ||
| is ProfileSideEffect.WithdrawSuccess -> { | ||
| (context.findActivity() as? MainActivity)?.restartApplication() | ||
| } | ||
|
Comment on lines
+88
to
+95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 안전한 캐스트 실패 시 사용자 피드백 누락 가능성
🐛 권장 수정안 is ProfileSideEffect.WithdrawSuccess -> {
- (context.findActivity() as? MainActivity)?.restartApplication()
+ val activity = context.findActivity() as? MainActivity
+ if (activity != null) {
+ activity.restartApplication()
+ } else {
+ // Fallback: 최소한 앱을 종료하거나 에러 로그 기록
+ android.util.Log.e("ProfileRoute", "MainActivity cast failed during withdrawal")
+ }
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
@@ -76,6 +92,7 @@ fun ProfileRoute( | |
| ProfileScreen( | ||
| modifier = Modifier.padding(paddingValues), | ||
| uiState = state.data, | ||
| onBackClick = navigateUp, | ||
| onCollectionItemClick = navigateToCollectionDetail, | ||
| onContentItemClick = { contentId -> | ||
| viewModel.getOttListPerContent(contentId) | ||
|
|
@@ -92,6 +109,7 @@ fun ProfileRoute( | |
| state.data.userId | ||
| ) | ||
| }, | ||
| onEasterEggWithdraw = viewModel::easterEggWithdraw, | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -115,97 +133,142 @@ private fun ProfileScreen( | |
| uiState: ProfileUiState, | ||
| modifier: Modifier = Modifier, | ||
| onRefreshClick: () -> Unit = {}, | ||
| onBackClick: () -> Unit = {}, | ||
| onCollectionItemClick: (collectionId: String) -> Unit, | ||
| onContentItemClick: (contentId: String) -> Unit = {}, // TODO: 바텀시트 띄우기 | ||
| onContentMoreClick: () -> Unit = {}, | ||
| onCreatedCollectionMoreClick: () -> Unit, | ||
| onSavedCollectionMoreClick: () -> Unit | ||
| onSavedCollectionMoreClick: () -> Unit, | ||
| onEasterEggWithdraw: () -> Unit = {}, | ||
| ) { | ||
| val userName = uiState.profile.nickname | ||
|
|
||
| LazyColumn( | ||
| overscrollEffect = null, | ||
| contentPadding = PaddingValues(bottom = 70.dp), | ||
| modifier = | ||
| modifier | ||
| .background(colors.background) | ||
| .fillMaxSize(), | ||
| Box( | ||
| modifier = modifier | ||
| .background(colors.background) | ||
| .fillMaxSize(), | ||
| ) { | ||
| item { | ||
| with(uiState.profile) { | ||
| ProfileTopSection( | ||
| userName = nickname, | ||
| profileUrl = profileImageUrl.orEmpty(), | ||
| isFliner = isFliner, | ||
| LazyColumn( | ||
| overscrollEffect = null, | ||
| contentPadding = PaddingValues(bottom = 70.dp), | ||
| ) { | ||
| item { | ||
| with(uiState.profile) { | ||
| ProfileTopSection( | ||
| userName = nickname, | ||
| profileUrl = profileImageUrl.orEmpty(), | ||
| isFliner = isFliner, | ||
| onEasterEggWithdraw = onEasterEggWithdraw, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| item { | ||
| Spacer(Modifier.height(20.dp)) | ||
|
|
||
| ProfileKeywordSection( | ||
| nickname = uiState.profile.nickname, | ||
| keywordList = uiState.keywords, | ||
| onRefreshClick = onRefreshClick, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| item { | ||
| Spacer(Modifier.height(20.dp)) | ||
| item { | ||
| if (uiState.createCollections.collections.isNotEmpty()) { | ||
| Spacer(Modifier.height(48.dp)) | ||
|
|
||
| ProfileKeywordSection( | ||
| nickname = uiState.profile.nickname, | ||
| keywordList = uiState.keywords, | ||
| onRefreshClick = onRefreshClick, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| ) | ||
| } | ||
| CollectionSection( | ||
| title = "${userName}님의 컬렉션", | ||
| description = "${userName}님이 생성한 컬렉션이에요", | ||
| onItemClick = onCollectionItemClick, | ||
| isAllVisible = true, | ||
| onAllClick = onCreatedCollectionMoreClick, | ||
| collectionListModel = uiState.createCollections, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| item { | ||
| if (uiState.createCollections.collections.isNotEmpty()) { | ||
| Spacer(Modifier.height(48.dp)) | ||
| item { | ||
| if (uiState.savedCollections.collections.isNotEmpty()) { | ||
| Spacer(Modifier.height(48.dp)) | ||
|
|
||
| CollectionSection( | ||
| title = "${userName}님의 컬렉션", | ||
| description = "${userName}님이 생성한 컬렉션이에요", | ||
| onItemClick = onCollectionItemClick, | ||
| isAllVisible = true, | ||
| onAllClick = onCreatedCollectionMoreClick, | ||
| collectionListModel = uiState.createCollections, | ||
| ) | ||
| CollectionSection( | ||
| title = "저장한 컬렉션", | ||
| description = "${userName}님이 저장한 컬렉션이에요", | ||
| onItemClick = onCollectionItemClick, | ||
| isAllVisible = true, | ||
| onAllClick = onSavedCollectionMoreClick, | ||
| collectionListModel = uiState.savedCollections, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| item { | ||
| if (uiState.savedCollections.collections.isNotEmpty()) { | ||
| item { | ||
| Spacer(Modifier.height(48.dp)) | ||
|
|
||
| CollectionSection( | ||
| title = "저장한 컬렉션", | ||
| description = "${userName}님이 저장한 컬렉션이에요", | ||
| onItemClick = onCollectionItemClick, | ||
| isAllVisible = true, | ||
| onAllClick = onSavedCollectionMoreClick, | ||
| collectionListModel = uiState.savedCollections, | ||
| SavedContentsSection( | ||
| title = "저장한 작품", | ||
| description = "${userName}님이 저장한 작품이에요", | ||
| contentModelList = uiState.savedContents, | ||
| onItemClick = onContentItemClick, | ||
| isAllVisible = false, | ||
| onAllClick = {}, | ||
| ) | ||
| } | ||
|
||
| } | ||
|
|
||
| item { | ||
| Spacer(Modifier.height(48.dp)) | ||
|
|
||
| SavedContentsSection( | ||
| title = "저장한 작품", | ||
| description = "${userName}님이 저장한 작품이에요", | ||
| contentModelList = uiState.savedContents, | ||
| onItemClick = onContentItemClick, | ||
| isAllVisible = false, | ||
| onAllClick = {}, | ||
| if (uiState.userId != null) { | ||
| FlintBackTopAppbar( | ||
| onClick = onBackClick, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Preview(showBackground = true) | ||
| @Composable | ||
| private fun ProfileScreenPreview() { | ||
| private fun ProfileScreenPreview( | ||
| @PreviewParameter(ProfileUiStatePreviewParameterProvider::class) uiState: ProfileUiState, | ||
| ) { | ||
| FlintTheme { | ||
| ProfileScreen( | ||
| uiState = ProfileUiState.Fake, | ||
| uiState = uiState, | ||
| onCollectionItemClick = {}, | ||
| onCreatedCollectionMoreClick = {}, | ||
| onSavedCollectionMoreClick = {} | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private class ProfileUiStatePreviewParameterProvider : PreviewParameterProvider<ProfileUiState> { | ||
| override val values: Sequence<ProfileUiState> = sequenceOf( | ||
| // 내 프로필 | ||
| ProfileUiState( | ||
| userId = null, | ||
| profile = UserProfileResponseModel( | ||
| id = "", | ||
| nickname = "닉네임", | ||
| profileImageUrl = "", | ||
| isFliner = false, | ||
| ), | ||
| keywords = KeywordListModel.FakeList1, | ||
| createCollections = CollectionListModel.FakeList, | ||
| savedCollections = CollectionListModel.FakeList, | ||
| savedContents = BookmarkedContentListModel.FakeList, | ||
| ), | ||
| // 다른 사용자 프로필 | ||
| ProfileUiState( | ||
| userId = "1", | ||
| profile = UserProfileResponseModel( | ||
| id = "", | ||
| nickname = "닉네임", | ||
| profileImageUrl = "", | ||
| isFliner = true, | ||
| ), | ||
| keywords = KeywordListModel.FakeList3, | ||
| createCollections = CollectionListModel(), | ||
| savedCollections = CollectionListModel(), | ||
| savedContents = BookmarkedContentListModel.FakeList, | ||
| ), | ||
| ) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: imflint/Flint-Android
Length of output: 1116
🏁 Script executed:
Repository: imflint/Flint-Android
Length of output: 420
🏁 Script executed:
Repository: imflint/Flint-Android
Length of output: 8866
withdraw()메서드의 응답 타입 래핑 불일치 수정 필요withdraw()함수는WithdrawResponseDto를 직접 반환하지만, 다른 모든 API 메서드(signup,socialVerify포함)와 전체 코드베이스의 패턴에서는BaseResponse<T>래핑을 사용합니다.WithdrawResponseDto는status와message필드만 포함하는 응답 엔벨로프 구조이므로, 표준 API 응답 패턴에 맞게BaseResponse<WithdrawResponseDto>또는BaseResponse<Unit>로 수정해야 합니다.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
서버에서 data가 안와서 BaseResponse를 쓸 수 없음;
data를 nullable로 두기보다는 그냥 따로 관리하는 게 나을듯. 서버한테 지금 말하기 힘든 상황이니까 나중에 고칠게
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nahy-512, 이해했습니다! 백엔드 API가 표준 응답 형식을 따르지 않는 상황이라면 현재 방식이 합리적입니다.나중에 백엔드 팀과 조율이 가능할 때 통일된 응답 형식으로 수정하시면 좋을 것 같습니다. 필요하시다면 이 기술 부채를 추적하기 위한 이슈를 생성해드릴 수도 있습니다.
✏️ Learnings added