Skip to content

Conversation

@stopstone
Copy link
Contributor

@stopstone stopstone commented Sep 19, 2025

작업 내용

  • MY 클릭 시 마이페이지 이동
  • 서비스 이용에 필요한 약관 및 설명을 webView로 구성하였습니다.
  • DataStore 초기화로 로그아웃, 회원 탈퇴는 api요청으로 동작합니다.
  • 탈퇴사유에서 텍스트필드는 NearOutlinedTextField 를 사용합니다.
  • 기타인 경우만 사유를 입력할 수 있습니다.

확인 방법

  • feature/my-profile에서 MyProfile~, Withdraw~ 확인 부탁드립니다!
  • NearOutlinedTextField 를 새로 구성하였습니다. ui/components에 위치하였습니다.
  • 웹뷰 스크린 WebViewFrame은 ui/components로 구성하였습니다.

참고 사항

  • 텍스트필드를 포커스별 border를 조정, 에러 메시지 설정, 글자수 설정 등으로 변경사항이 많을 것 같다
    새롭게 구성하였습니다!
  • 웹뷰 구성으로 local.properties에 약관 링크를 넣어야합니다! 디스코드 참고 부탁드려요!
  • 회원 탈퇴시 다이얼로그로 확인하지 않고 바로 탈퇴를 하고 있습니다! 다이얼로그 디자인이 나오면 수정하겠습니다

관련 이슈

- ui/components의 NearBasicButton을 기반하여 만들었습니다.

- 현재 페이지가 마지막 페이지인지에 따라 버튼 텍스트를 "로그인/회원가입" 또는 "다음"으로 표시합니다.

- 버튼 클릭 시 `onNextClick` 콜백을 호출합니다.
- 현재 페이지의 컬러를 BLUE01_5AA2E9로 설정
- 이외 컬리는 GRAY03_EBEBEB로 설정합니다.
- 각각의 페이지의 구성요소를 담당하는 data class입니다.
- 추후 이미지를 추가하여 온보딩 페이지에서 이미지를 그리는 역할을 합니다.
- 각 컴포넌트를 조합하여 온보딩 화면을 구현하였습니다.

- createAnnotatedText() 함수를 통해 \n 이후 텍스트를 강조합니다.

- stringResource를 통해 페이지 별 타이틀을 string.xml에서 가져옵니다.
OnboardingScreen의 UI 레이아웃을 수정하여 상태바와 네비게이션 바 영역을 고려하도록 변경했습니다.
`systemBarsPadding()` 대신 `WindowInsets`를 사용하여 직접 패딩을 계산하고 적용했습니다.
온보딩 완료 상태를 저장하고 조회하는 `OnboardingPreferences` 클래스를 추가했습니다.
이를 통해 앱 최초 실행 시 온보딩 화면 표시 여부를 결정할 수 있습니다.
- 온보딩 버튼에 로딩 상태를 추가했습니다.
- 로딩 중일 때는 버튼이 비활성화됩니다.
- OnboardingViewModel에 UiState 및 Effect 추가
- OnboardingScreen에서 UiState 관찰 및 Effect 처리 로직 구현
- 온보딩 완료 시 로딩 상태 표시 및 에러 처리 추가
- 온보딩 완료 여부 저장 및 조회 기능을 위한 `OnBoardingRepository` 인터페이스와 `OnBoardingRepositoryImpl` 구현체를 추가했습니다.
- `OnboardingViewModel`이 `OnboardingPreferences` 대신 `OnBoardingRepository`를 사용하도록 수정했습니다.
- `RepositoryModule`에 `OnBoardingRepository` 관련 의존성 주입 설정을 추가했습니다.
- `title` 필드를 `String`에서 `@StringRes` 어노테이션을 사용하는 `Int` 타입의 `titleResId`로 변경했습니다.
- 이에 따라 `OnboardingScreen`에서 `title`을 참조하는 부분을 `stringResource`를 사용하도록 수정했습니다.
- ProviderType에 따라 텍스트와 아이콘을 가진다.
- values/strings.xml에서 중복된 온보딩 관련 문자열 리소스를 삭제했습니다.
@gemini-code-assist
Copy link

Summary of Changes

Hello @stopstone, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 사용자 계정 관리 기능을 강화하기 위해 마이 프로필 화면과 회원 탈퇴 기능을 도입합니다. 사용자는 자신의 프로필 정보를 확인하고, 서비스 약관을 열람하며, 필요에 따라 로그아웃하거나 계정을 안전하게 탈퇴할 수 있습니다. 이는 사용자 경험을 개선하고 애플리케이션의 전반적인 계정 관리 시스템을 완성하는 데 기여합니다.

Highlights

  • 마이 프로필 및 회원 탈퇴 기능 추가: 사용자 계정 정보를 확인하고, 로그아웃 및 회원 탈퇴를 처리할 수 있는 새로운 화면이 추가되었습니다.
  • 약관 및 정책 WebView 연동: 서비스 이용 약관, 개인정보 수집 및 이용 동의, 개인정보 처리 방침 등의 내용을 WebView를 통해 확인할 수 있도록 구현되었습니다.
  • 회원 관련 API 및 Repository 구현: 회원 정보 조회 및 회원 탈퇴를 위한 새로운 API 서비스(MemberApiService)와 데이터 계층(MemberRepository, MemberRepositoryImpl)이 추가되었습니다.
  • 커스텀 텍스트 필드(NearOutlinedTextField) 도입: 회원 탈퇴 시 '기타' 사유를 입력할 수 있도록 글자 수 제한, 에러 메시지 표시 등의 기능을 갖춘 커스텀 텍스트 필드가 새로 구성되었습니다.
  • Glide Compose 라이브러리 추가: 이미지 로딩을 위해 Glide Compose 라이브러리가 프로젝트에 통합되었습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이 PR은 마이 프로필 조회 및 회원 탈퇴 기능을 추가하는군요. 전반적으로 기능 구현이 잘 이루어졌습니다. 몇 가지 개선점을 제안합니다. build.gradle.kts 파일의 중복 코드를 제거하여 가독성을 높일 수 있습니다. MemberRepositoryImpl에서 인증 토큰이 하드코딩된 부분은 심각한 보안 문제가 될 수 있으므로 반드시 수정이 필요합니다. 또한, ViewModel과 UI 간의 에러 처리 방식, 네비게이션 인자 전달 방식, 일부 컴포저블의 구현을 더 일관되고 효율적으로 개선할 수 있는 부분을 찾아 리뷰 코멘트를 남겼습니다. 코드 제안을 참고하여 더 안정적이고 유지보수하기 좋은 코드를 만들어주시길 바랍니다.

flow {
emit(
runCatching {
val response = memberApiService.getMyInfo("")

Choose a reason for hiding this comment

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

critical

API 호출 시 Authorization 헤더에 빈 문자열 ""이 하드코딩되어 있습니다. 이는 심각한 보안 취약점이 될 수 있습니다. 실제 프로덕션 환경에서는 반드시 유효한 인증 토큰을 사용하도록 수정해야 합니다. DataStore나 다른 보안 저장소에서 토큰을 가져와 사용하는 것을 권장합니다. 45번째 줄의 withdraw 메소드에서도 동일한 문제가 발견되었습니다.

val selectedReason: WithdrawReason? = null,
val otherReasonText: String = "",
val isLoading: Boolean = false,
val errorMessage: String = "",

Choose a reason for hiding this comment

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

high

WithdrawUiStateerrorMessage 필드에 탈퇴 실패 또는 로그아웃 실패 시 에러 메시지가 할당되지만, WithdrawScreen에서 이 메시지를 사용자에게 보여주는 UI 로직이 구현되어 있지 않습니다. 사용자에게 중요한 오류 상황을 알릴 수 없으므로, SnackbarText 컴포저블 등을 사용하여 에러 메시지를 표시하는 로직을 추가해야 합니다.

Comment on lines +37 to +39
buildConfigField("String", "SERVICE_AGREED_TERMS_URL", getProperty("SERVICE_AGREED_TERMS_URL"))
buildConfigField("String", "PERSONAL_INFO_TERMS_URL", getProperty("PERSONAL_INFO_TERMS_URL"))
buildConfigField("String", "PRIVACY_POLICY_TERMS_URL", getProperty("PRIVACY_POLICY_TERMS_URL"))

Choose a reason for hiding this comment

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

medium

releasedebug 빌드 유형 모두에서 동일한 buildConfigField가 반복적으로 선언되고 있습니다. 코드를 더 간결하게 유지하고 중복을 줄이기 위해 이 필드들을 defaultConfig 블록으로 옮기는 것을 고려해 보세요. NEAR_URL과 같이 빌드 유형에 따라 다른 값을 가져야 하는 경우에만 각 buildType에 남겨두는 것이 좋습니다.

data class MyProfileUiState(
val isLoading: Boolean = false,
val memberInfo: MyProfileInfo,
val error: String? = null,

Choose a reason for hiding this comment

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

medium

MyProfileUiState 데이터 클래스에 error: String? 필드가 정의되어 있지만, ViewModel 내에서 실제 에러 메시지를 할당하는 부분이 없습니다. 에러는 _errorEvent 채널을 통해 별도로 처리되고 있습니다. 사용되지 않는 필드는 혼란을 줄 수 있으므로 제거하는 것이 좋습니다.

Suggested change
val error: String? = null,
val error: String? = null, // 이 줄을 삭제하세요

Comment on lines 65 to 72
val title = backStackEntry.arguments?.getString("title") ?: ""
val url = backStackEntry.arguments?.getString("url") ?: ""

WebViewFrame(
onNavigateBack = onNavigateBack,
title = title,
url = url,
)

Choose a reason for hiding this comment

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

medium

composable<RouteWebView>에서 backStackEntry.arguments를 통해 수동으로 인자를 추출하고 있습니다. Navigation Compose의 타입-세이프(type-safe)한 접근 방식을 일관되게 사용하기 위해 backStackEntry.toRoute<RouteWebView>()를 사용하여 라우트 객체를 직접 가져오는 것을 권장합니다. 이렇게 하면 코드가 더 간결해지고 타입 안정성도 높아집니다.

        val route = backStackEntry.toRoute<RouteWebView>()

        WebViewFrame(
            onNavigateBack = onNavigateBack,
            title = route.title,
            url = route.url,
        )

Comment on lines 28 to 46
AndroidView(
modifier =
modifier
.fillMaxSize(),
factory = { context ->
WebView(context).apply {
webViewClient = WebViewClient()
settings.apply {
domStorageEnabled = true
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
loadWithOverviewMode = true
useWideViewPort = true
builtInZoomControls = true
displayZoomControls = false
}
loadUrl(url)
}
},
)

Choose a reason for hiding this comment

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

medium

현재 WebView에서 뒤로가기 동작이 WebView의 히스토리 스택을 고려하지 않고 바로 화면을 닫습니다. 사용자가 WebView 내에서 다른 페이지로 이동했을 경우, 앱의 뒤로가기 버튼을 누르면 이전 페이지로 돌아가는 것이 더 자연스러운 사용자 경험일 수 있습니다. BackHandler를 사용하여 WebView가 뒤로 갈 수 있을 때는 webView.goBack()을 호출하고, 그렇지 않을 때만 onNavigateBack()을 호출하도록 구현하는 것을 고려해보세요.

container = {
OutlinedTextFieldDefaults.Container(
enabled = enabled,
isError = false,

Choose a reason for hiding this comment

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

medium

OutlinedTextFieldDefaults.ContainerisError 파라미터에 false가 하드코딩되어 있습니다. 이로 인해 isError 상태일 때 Material3에서 기본으로 제공하는 시각적 피드백(예: 테두리 색상 변경)이 적용되지 않을 수 있습니다. NearOutlinedTextFieldisError 파라미터를 여기에 전달하여 일관된 에러 상태 표시를 보장하는 것이 좋습니다.

Suggested change
isError = false,
isError = isError,

Comment on lines 37 to 47
.apply(
when (contentScale) {
ContentScale.Crop -> RequestOptions.centerCropTransform()
ContentScale.Fit -> RequestOptions.fitCenterTransform()
ContentScale.FillBounds -> RequestOptions.centerInsideTransform()
ContentScale.FillHeight -> RequestOptions.centerInsideTransform()
ContentScale.FillWidth -> RequestOptions.centerInsideTransform()
ContentScale.Inside -> RequestOptions.centerInsideTransform()
else -> RequestOptions.centerCropTransform()
},
)

Choose a reason for hiding this comment

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

medium

GlideImagecontentScale 파라미터는 이미지의 스케일링을 이미 처리해줍니다. 현재 코드의 apply 블록에서 RequestOptions를 사용하여 contentScale에 따라 수동으로 변환을 적용하는 부분은 중복으로 보입니다. GlideImage의 기능을 활용하여 코드를 더 간결하게 만들 수 있습니다. 이 apply 블록을 제거해도 동일하게 동작할 것입니다.

stopstone and others added 10 commits September 19, 2025 16:39
- `MemberApiService`의 `getMyInfo`, `withdraw` 함수에서 Authorization 헤더 파라미터 삭제
- `MemberRepositoryImpl`에서 `getMyInfo`, `withdraw` 호출 시 Authorization 헤더 전달 로직 삭제
- 기존에 arguments에서 직접 값을 가져오던 방식에서 `toRoute<RouteWebView>()`를 사용하도록 변경했습니다.
- WebViewFrame 컴포저블에 BackHandler를 추가하여 WebView 내에서 뒤로가기 기능을 구현했습니다.
- WebView의 canGoBack 상태를 추적하여 뒤로가기 가능 여부를 결정합니다.
- 페이지 로드 완료 시 및 WebView 업데이트 시 canGoBack 상태를 업데이트합니다.
- GlideImage의 contentScale 파라미터가 이미 존재하므로 중복되는 RequestOptions의 contentScale 관련 코드를 삭제했습니다.
- `OutlinedTextFieldDefaults.Container`의 `isError` 파라미터에 `isError` 상태를 직접 전달하도록 수정하여, 텍스트 필드의 에러 상태가 올바르게 반영되도록 했습니다.
- WithdrawViewModel에서 errorMessage를 직접 관리하는 대신 WithdrawUiEvent.ShowError 이벤트를 발생시켜 UI에 에러를 전달하도록 수정했습니다.
- WithdrawScreen에서 ShowError 이벤트를 수신하여 스낵바를 통해 에러 메시지를 표시하도록 변경했습니다.
- WithdrawUiState에서 errorMessage 필드를 제거했습니다.
- NavigationMyProfile의 WithdrawRoute에 onShowErrorSnackBar 파라미터를 추가하여 에러 발생 시 스낵바를 표시할 수 있도록 했습니다.
Copy link
Contributor

@rhkrwngud445 rhkrwngud445 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!
큰 작업인데, 꼼꼼하게 작업 주셨군요 감사합니다😀
사실 디스코드에 웹뷰 링크가 안보여서 구동은 못해봤습니다😂 코드 위주로 확인하여 리뷰드립니다!
궁금한 점 위주로 코멘트 남겼습니다. 확인 부탁드려요!

color = NearTheme.colors.BLACK_1A1A1A,
)

NearSwitch { }
Copy link
Contributor

Choose a reason for hiding this comment

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

람다 내부가 비워져있는데, 아무 역할 없는 것 맞을까요?🧐

Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 코드에서는 스위치 배치만 하고 동작은 하지 않습니다..
TODO를 작성해두는 편이 좋았겠네요..!

알람에 대해 주형님께서 담당해주시면 그 부분에 맞춰서 통일하면 좋을 것 같아 두었었어요
지금 생각해보니 먼저 해두는 것도 좋았을 것 같습니다 🥲
알람 여부에 동의에 대해 구성하고 상태 반영 및 동작하도록 수정 예정입니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

알람 스위치였군요😅
이 부분은 알람 기능 구성시에 제가 함께 적용하도록 하겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

감사합니다!!

modifier: Modifier = Modifier,
) {
if (!uri.isNullOrEmpty()) {
GlideImage(
Copy link
Contributor

Choose a reason for hiding this comment

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

오 Glide를 써주셨군요! 저희 Coil 사용하기로 했던 것 같은데, Glide 사용주신 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

앗..... 논의 전에 작성한 코드였는데 Coil을 사용하기로 한 것을 잊고 있었습니다..!
해당 pr에서 수정후 올리겠습니다

if (response.isSuccessful) {
response.body() ?: throw Exception("회원 정보가 null입니다.")
} else {
val errorMessage =
Copy link

Choose a reason for hiding this comment

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

@stopstone // 코드 잘 보았습니다! 지석님이 작성하신 코드 중에 404, 401, etc를 switch하는 구문이 자주 보이는데, 이걸 한 곳에 묶어놓는건 어떨까요? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아 좋은 의견 감사합니다!
며칠 전 고민하던 내용이였는데 저 메시지들로 처리가 안되는 현상이 있어서 그 문제 해결할 때 리팩토링 해보겠습니다 :)

Copy link
Contributor

@rhkrwngud445 rhkrwngud445 left a comment

Choose a reason for hiding this comment

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

코멘트에 대한 추가 답변 드립니다!

- 기존 Glide를 사용하던 ImageLoader를 Coil을 사용하도록 수정했습니다.
- `AsyncImage`를 사용하여 이미지를 비동기적으로 로드하고, `ImageRequest.Builder`를 통해 crossfade 효과를 적용했습니다.
- crossfade는 논의 후붎필요하면 제거하겠습니다!
- 관련 의존성을 `libs.versions.toml` 및 `app/build.gradle.kts`에서 Glide에서 Coil로 변경하였습니다.
- `MemberApiService`의 `getMyInfo` 함수의 반환 타입을 `Response<MemberInfo>`에서 `MemberInfo`로 변경했습니다.
- `withdraw` 함수의 반환 타입을 `Response<Unit>`에서 `Unit`으로 변경했습니다.
- `apiCallFlow` 함수를 추가하여 Repository에서 API 호출 시 예외 처리를 단순화했습니다.
- apiCallFlow를 활용하면 emit을 생략할 수 있습니다.
- 불필요한 runCatching을 제거하였습니다.
- `apiCallFlow` 함수를 추가하여 Repository에서 API 호출 시 예외 처리를 단순화했습니다.
- `MemberRepositoryImpl`의 `getMyInfo` 및 `withdraw` 함수에 `apiCallFlow`를 적용하여 중복 코드를 제거하고 가독성을 향상했습니다.
- 회원 탈퇴 API 호출 시 `catch`를 사용하여 에러를 처리하도록 수정했습니다.
- `MemberInfoEntity` 및 `WithdrawRequestEntity`를 추가하여 Data Layer의 API 요청/응답 모델을 정의했습니다.
- 기존 Model Layer의 `MemberInfo`, `WithdrawRequest`를 Data Layer와 Presentation Layer 사이의 중간 모델로 사용하도록 수정했습니다.
- `MemberMapper`를 개편하여 각 Layer 간 모델 변환 로직을 명확히 분리했습니다.
    - Data Layer Entity <-> Model Layer 변환 함수 추가
    - Model Layer <-> UI Layer 변환 함수명 변경 (`toMyProfileInfo` -> `toMyProfileInfoUIModel`)
- `MemberRepositoryImpl`에서 API 호출 시 Data Layer Entity를 사용하도록 수정했습니다.
- `MyProfileViewModel`에서 UI 모델명 변경사항을 반영했습니다. (`MyProfileInfo` -> `MyProfileInfoUIModel`)
- `WithdrawRequest` 모델을 `model/member` 패키지에서 `network/request` 패키지로 이동했습니다.
- `MemberRepository`의 `withdraw` 함수 시그니처를 변경했습니다.
    - 기존: `fun withdraw(request: WithdrawRequest): Flow<Unit>`
    - 변경: `fun withdraw(reason: WithdrawReason, customReason: String? = null): Flow<Unit>`
- 머지 충돌로 인하여 중복된 NavGraph를 제거합니다.
# Conflicts:
#	Near/app/src/main/java/com/alarmy/near/data/di/RepositoryModule.kt
#	Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt
- `NearNavHost`의 시작 화면을 결정하는 `isLoggedIn` 파라미터를 제거하고, `startDestination` 파라미터로 통합하여 외부에서 시작 화면을 직접 지정하도록 변경했습니다.
@stopstone stopstone merged commit b70979a into dev Sep 23, 2025
1 check passed
@stopstone stopstone deleted the feat/my-profile branch October 6, 2025 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 마이페이지

4 participants