Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
623bd57
fix: ProfileSmallKeywordChip 패딩 수정
nahy-512 Jan 23, 2026
9980028
feat: 타인 프로필 뒤로가기 헤더 추가
nahy-512 Jan 23, 2026
d5901f5
feat: ProfileScreen 프리뷰 추가
nahy-512 Jan 23, 2026
0ad85ed
style: 로딩 인디케이터 색상 변경
nahy-512 Jan 23, 2026
8ee50c3
fix: profileNavGraph navigateUp 함수 추가
nahy-512 Jan 23, 2026
2a97ae3
refactor: 프로필 화면 프로필 및 작품/컬렉션 데이터 비동기 로딩
nahy-512 Jan 23, 2026
b2c9154
feat: 회원탈퇴 이스터에그 추가
nahy-512 Jan 23, 2026
808f8ea
refactor: 회원탈퇴 후 앱 재시작
nahy-512 Jan 23, 2026
75afa24
refactor: ContextExt를 통한 앱 재시작 로직 수정
nahy-512 Jan 23, 2026
2ad8a60
refactor: 프로필 화면 ProfileTopSection 로딩 범위에서 제외
nahy-512 Jan 23, 2026
872635d
refactor: 프로필 로딩바 위치 수정
nahy-512 Jan 23, 2026
398e1e4
style: 디자인 QA 반영
nahy-512 Jan 23, 2026
48dc5e4
feat: 회원탈퇴 플래그 추가
nahy-512 Jan 23, 2026
7947b31
Merge remote-tracking branch 'origin/develop' into fix/#183-profile-qa
nahy-512 Jan 23, 2026
4489afb
fix: 코드리뷰 내용 반영
nahy-512 Jan 23, 2026
3d981ed
Merge remote-tracking branch 'origin/develop' into fix/#183-profile-qa
nahy-512 Jan 23, 2026
86d8557
fix: Ott 바텀시트 표시, CollectionInputTextField 수정
nahy-512 Jan 23, 2026
94b1084
fix: 생성 컬렉션 표시
nahy-512 Jan 23, 2026
d5ad75d
fix: BasicTextField 텍스트 포커싱 영역 수정
nahy-512 Jan 23, 2026
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
14 changes: 14 additions & 0 deletions app/src/main/java/com/flint/core/common/extension/ContextExt.kt
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
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.flint.core.designsystem.theme.FlintTheme

@Composable
fun FlintLoadingIndicator() {
Expand All @@ -18,6 +19,8 @@ fun FlintLoadingIndicator() {
.background(Color.Transparent),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
CircularProgressIndicator(
color = FlintTheme.colors.primary400
)
}
}
5 changes: 5 additions & 0 deletions app/src/main/java/com/flint/data/api/AuthApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import com.flint.data.dto.auth.request.SignupRequestDto
import com.flint.data.dto.auth.request.SocialVerifyRequestDto
import com.flint.data.dto.auth.response.SignupResponseDto
import com.flint.data.dto.auth.response.SocialVerifyResponseDto
import com.flint.data.dto.auth.response.WithdrawResponseDto
import com.flint.data.dto.base.BaseResponse
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.POST

interface AuthApi {
Expand All @@ -18,4 +20,7 @@ interface AuthApi {
suspend fun socialVerify(
@Body requestDto: SocialVerifyRequestDto,
): BaseResponse<SocialVerifyResponseDto>

@DELETE("/api/v1/auth/withdraw")
suspend fun withdraw(): WithdrawResponseDto
Comment on lines +24 to +25
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 23, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the AuthApi.kt file to see all methods and their return types
fd "AuthApi.kt" --type f --exec cat -n {}

Repository: imflint/Flint-Android

Length of output: 1116


🏁 Script executed:

# Also search for WithdrawResponseDto to understand its structure
fd "WithdrawResponseDto" --type f --exec cat -n {}

Repository: imflint/Flint-Android

Length of output: 420


🏁 Script executed:

# Search for BaseResponse usage patterns in API files
rg "BaseResponse|@DELETE|@POST|@GET" app/src/main/java/com/flint/data/api/ -A1 --type kotlin

Repository: imflint/Flint-Android

Length of output: 8866


withdraw() 메서드의 응답 타입 래핑 불일치 수정 필요

withdraw() 함수는 WithdrawResponseDto를 직접 반환하지만, 다른 모든 API 메서드(signup, socialVerify 포함)와 전체 코드베이스의 패턴에서는 BaseResponse<T> 래핑을 사용합니다. WithdrawResponseDtostatusmessage 필드만 포함하는 응답 엔벨로프 구조이므로, 표준 API 응답 패턴에 맞게 BaseResponse<WithdrawResponseDto> 또는 BaseResponse<Unit>로 수정해야 합니다.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/data/api/AuthApi.kt` around lines 24 - 25, The
withdraw() API method currently returns WithdrawResponseDto directly but must
follow the project's standard response envelope; change its return type to
BaseResponse<WithdrawResponseDto> (or BaseResponse<Unit> if the body contains no
data) to match other methods like signup and socialVerify, update the
AuthApi.withdraw() signature and any related call sites/imports to use
BaseResponse, and ensure WithdrawResponseDto is used only as the inner generic
payload type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

서버에서 data가 안와서 BaseResponse를 쓸 수 없음;
data를 nullable로 두기보다는 그냥 따로 관리하는 게 나을듯. 서버한테 지금 말하기 힘든 상황이니까 나중에 고칠게

Copy link
Contributor

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
Learnt from: nahy-512
Repo: imflint/Flint-Android PR: 189
File: app/src/main/java/com/flint/data/api/AuthApi.kt:24-25
Timestamp: 2026-01-23T08:53:45.699Z
Learning: In `app/src/main/java/com/flint/data/api/AuthApi.kt`, the `withdraw()` endpoint returns `WithdrawResponseDto` directly instead of `BaseResponse<WithdrawResponseDto>` because the backend API does not follow the standard response envelope pattern for this specific endpoint. This is a known exception planned for future alignment with backend team.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}
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
Expand Up @@ -38,4 +38,10 @@ class AuthRepository @Inject constructor(
result
}

suspend fun withdraw(): Result<Unit> =
suspendRunCatching {
api.withdraw()
preferencesManager.removeString(ACCESS_TOKEN)
preferencesManager.clearAll()
Copy link
Contributor

Choose a reason for hiding this comment

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

p3: clearAll만으로 충분하지 않을까요?

}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/flint/presentation/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class MainActivity : ComponentActivity() {
}
}

private fun restartApplication() {
fun restartApplication() {
val packageManager: PackageManager = this.packageManager
val intent = packageManager.getLaunchIntentForPackage(this.packageName)
val componentName = intent!!.component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ fun MainNavHost(

profileNavGraph(
paddingValues = paddingValues,
navigateUp = navigator::navigateUp,
navigateToCollectionList = navigator::navigateToCollectionList,
navigateToSavedContentList = navigator::navigateToSavedContent,
navigateToCollectionDetail = navigator::navigateToCollectionDetail,
Expand Down
179 changes: 121 additions & 58 deletions app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt
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
Expand All @@ -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
Expand All @@ -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()) }
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

안전한 캐스트 실패 시 사용자 피드백 누락 가능성

as? MainActivity가 실패할 경우 restartApplication()이 호출되지 않아 사용자가 회원탈퇴 후 불일치 상태에 놓일 수 있습니다. 캐스트 실패 시 최소한 로깅하거나 대체 동작을 수행하는 것이 좋습니다.

🐛 권장 수정안
 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
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt` around
lines 79 - 81, Handle the case where (context.findActivity() as? MainActivity)
returns null in the ProfileSideEffect.WithdrawSuccess branch: instead of
silently doing nothing, detect the null, log the failure (include context or
class info) and perform a safe fallback such as showing a user-facing message
(e.g., Toast/snackbar) or navigating to the auth/login flow so the app state
isn't left inconsistent; ensure you invoke MainActivity.restartApplication()
only when the cast succeeds and otherwise run the fallback and logging.

}
}
}
Expand All @@ -76,6 +92,7 @@ fun ProfileRoute(
ProfileScreen(
modifier = Modifier.padding(paddingValues),
uiState = state.data,
onBackClick = navigateUp,
onCollectionItemClick = navigateToCollectionDetail,
onContentItemClick = { contentId ->
viewModel.getOttListPerContent(contentId)
Expand All @@ -92,6 +109,7 @@ fun ProfileRoute(
state.data.userId
)
},
onEasterEggWithdraw = viewModel::easterEggWithdraw,
)
}

Expand All @@ -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 = {},
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "ProfileScreen.kt" --type f

Repository: imflint/Flint-Android

Length of output: 209


🏁 Script executed:

wc -l "app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt"

Repository: imflint/Flint-Android

Length of output: 134


🏁 Script executed:

cat -n "app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt" | head -230 | tail -100

Repository: imflint/Flint-Android

Length of output: 4126


🏁 Script executed:

rg -n "fun SavedContentsSection" --type=kotlin -B2 -A20

Repository: imflint/Flint-Android

Length of output: 2737


🏁 Script executed:

wc -l "app/src/main/java/com/flint/core/designsystem/component/listView/SavedContentsSection.kt"

Repository: imflint/Flint-Android

Length of output: 157


🏁 Script executed:

cat -n "app/src/main/java/com/flint/core/designsystem/component/listView/SavedContentsSection.kt"

Repository: imflint/Flint-Android

Length of output: 5000


SavedContentsSection에 빈 상태 조건부 렌더링 추가 필요

createCollectionssavedCollections 섹션은 리스트가 비어있을 때 렌더링을 건너뛰지만, SavedContentsSection은 항상 렌더링됩니다. SavedContentsSection 컴포넌트는 빈 상태를 내부적으로 처리하지 않으므로, 내용이 없을 때 헤더와 설명만 표시되는 빈 섹션이 나타납니다. 다른 섹션과의 일관성을 위해 if (uiState.savedContents.contents.isNotEmpty()) 조건을 추가하세요.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt` around
lines 207 - 218, SavedContentsSection is always rendered causing an empty header
when there are no saved items; wrap the item { ... SavedContentsSection(...) }
block with a conditional that checks uiState.savedContents.contents.isNotEmpty()
so it only renders when there are saved contents, matching the behavior used for
createCollections and savedCollections.

Comment on lines +183 to +251
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

UiState.Error 처리 누락

else 분기(line 233)가 비어 있어 sectionDataError 상태일 때 사용자에게 아무런 피드백이 표시되지 않습니다. 에러 상태에서 재시도 버튼이나 에러 메시지를 표시하는 것이 UX 측면에서 바람직합니다.

🐛 권장 수정안
-                else -> {}
+                is UiState.Error -> {
+                    item {
+                        // 에러 UI 표시 (예: 재시도 버튼)
+                        // ErrorSection(onRetryClick = onRefreshClick)
+                    }
+                }
+
+                else -> {}
🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt` around
lines 181 - 234, The when branch handling uiState.sectionData in
ProfileScreen.kt currently leaves the else branch empty so UiState.Error shows
nothing; replace the else with an explicit is UiState.Error branch that renders
an error state item (e.g., an error message and a retry button) and wire the
retry button to the existing onRefreshClick (or a new onRetry callback if
preferred) so users see feedback and can retry when sectionData is Error; locate
the when block around ProfileKeywordSection / CollectionSection /
SavedContentsSection and add the UiState.Error UI there.

}

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,
),
)
}
Loading