Skip to content

[Feat] users 남은 api 연동#99

Merged
rbqks529 merged 24 commits intoTHIP-TextHip:developfrom
JJUYAAA:feat/#73-users_api
Aug 18, 2025
Merged

[Feat] users 남은 api 연동#99
rbqks529 merged 24 commits intoTHIP-TextHip:developfrom
JJUYAAA:feat/#73-users_api

Conversation

@JJUYAAA
Copy link
Member

@JJUYAAA JJUYAAA commented Aug 18, 2025

➕ 이슈 링크


🔎 작업 내용

  • 프로필 편집
  • 사용자 검색 조회
  • 회원가입
  • 내 팔로잉 최신 피드 작성 순 -> 수정

회원가입같은 경우는 아직 소셜로그인 코드가 머지되지 않아 헤더로 보내야하는 임시토큰을 넣을 수 없는 상황이었습니다.
따라서 임의의 존재하지 않는 임시토큰 값을 넣어 "유효하지 않은 토큰입니다" 라는 에러를 받아오는 것까지 확인한 상태입니다. 참고해주세요..!

image

📸 스크린샷


😢 해결하지 못한 과제

  • 프로필 편집에서 텍스트필드에 오류 메세지를 서버에서 보내주는 메시지를 파싱하여 띄우려고 하는데, 오류가 뜨지 않고 로그로만 나오는 문제를 아직 해결하지 못한 상태입니다. 해당 부분 금방 해결해보도록 하겠습니다,


📢 리뷰어들에게

  • 참고해야 할 사항들을 적어주세요

Summary by CodeRabbit

  • New Features
    • 고객센터 화면 추가 및 마이페이지에서 이동 지원
    • 회원가입 플로우 도입(닉네임 확인→장르 선택→완료), 성공 시 피드로 이동
    • 사람 검색 개선: 실시간/최종 검색, 디바운스, 최근검색 관리, 검색 결과 표시
  • Refactor
    • 프로필 편집·사람 검색 화면을 MVVM으로 전환, 저장 성공 토스트 및 상태 반영
    • 여러 마이페이지 하위화면에 뒤로가기 동작 연결
  • Chores
    • 데이터 저장소 의존성 추가 및 문자열 리소스 보강 (오류 메시지·고객센터·프로필 완료 안내)

JJUYAAA added 21 commits August 17, 2025 20:24
@coderabbitai
Copy link

coderabbitai bot commented Aug 18, 2025

Walkthrough

회원가입·검색·프로필 수정 등 User 도메인 API 연동과 관련된 모델/서비스/레포지토리/뷰모델/컴포저블이 추가·수정되었고, DataStore 기반 TokenManager와 MyPage 고객센터 화면 및 네비게이션이 도입되었습니다. (≤50단어)

Changes

Cohort / File(s) Summary
Build & Version Catalog
app/build.gradle.kts, gradle/libs.versions.toml
DataStore Preferences 라이브러리 추가 및 버전 카탈로그 등록.
Token Storage & DI
app/src/main/java/.../data/manager/TokenManager.kt, app/src/main/java/.../MainActivity.kt
Preferences DataStore 기반 TokenManager 추가(싱글톤, Hilt 주입) 및 MainActivity에 @Inject 프로퍼티 추가(미사용).
User Request Models
app/src/main/java/.../data/model/users/request/ProfileUpdateRequest.kt, .../SignupRequest.kt
프로필 수정·회원가입 요청 DTO(@serializable) 추가.
User Response Models
app/src/main/java/.../data/model/users/response/SignupResponse.kt, .../UserSearchResponse.kt, .../MyFollowingsResponse.kt, .../OthersFollowersResponse.kt
회원가입·검색 응답 모델 추가, 일부 필드명 변경 및 @SerializedName 매핑 추가.
Service & Repository
app/src/main/java/.../data/service/UserService.kt, .../data/repository/UserRepository.kt
updateProfile, signup(Authorization 헤더), searchUsers 엔드포인트 및 Repository 메서드 추가; UserRepository에 TokenManager 주입.
Search MVVM & Mapper
app/src/main/java/.../ui/feed/screen/SearchPeopleScreen.kt, .../viewmodel/SearchPeopleViewModel.kt, .../ui/feed/mock/MySubscriptionData.kt
SearchPeopleScreen을 MVVM으로 전환(디바운스·최종검색·최근검색), SearchPeopleViewModel 추가 및 UserItem→MySubscriptionData 매퍼 추가.
Feed ViewModel
app/src/main/java/.../ui/feed/viewmodel/FeedViewModel.kt
fetchRecentWriters() 공개 메서드 추가 및 init에서 호출하도록 변경.
MyPage UI & VM
app/src/main/java/.../ui/mypage/screen/*.kt, .../viewmodel/MyPageEditViewModel.kt, .../viewmodel/MyPageViewModel.kt
고객센터 화면 추가, 여러 MyPage 서브스크린에 onNavigateBack 콜백 연결, EditProfile MVVM 도입(뷰모델·UI 상태), MyPageViewModel의 자동 초기 로드 제거(명시 호출 필요).
Navigation (MyPage & Signup)
app/src/main/java/.../ui/navigator/routes/MyPageRoutes.kt, .../extensions/MyPageNavigationExtensions.kt, .../navigations/MyPageNavigation.kt, .../navigations/signupNavigation.kt
MyPage에 CustomerService 라우트/네비게이션 추가, 서브화면 뒤로가기 핸들링 적용, signup_flow 네비게이션 그래프 및 ViewModel 공유 구현.
Signup Flow 리팩터링
app/src/main/java/.../ui/signin/viewmodel/SignupViewModel.kt, .../screen/SignupNicknameScreen.kt, .../SignupGenreScreen.kt
Nickname/Genre 흐름을 단일 SignupViewModel로 통합(닉네임 검증 → 칭호 선택 → 가입), 관련 UI/시퀀스 변경.
Removed Legacy VMs
app/src/main/java/.../ui/signin/viewmodel/NicknameViewModel.kt, .../SignupAliasViewModel.kt
기존 NicknameViewModel 및 SignupAliasViewModel 파일 제거.
Strings
app/src/main/res/values/strings.xml
고객센터 문구 분리 및 닉네임 에러/오류/프로필 완료 문구 추가.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Signup UI
  participant VM as SignupViewModel
  participant Repo as UserRepository
  participant API as UserService
  participant Store as TokenManager

  UI->>VM: onNicknameChange / checkNickname()
  VM->>Repo: checkNickname(nickname)
  Repo->>API: GET /users/nickname/check
  API-->>Repo: BaseResponse<...>
  Repo-->>VM: Result

  UI->>VM: fetchAliasChoices()
  VM->>Repo: getAliasChoices()
  Repo->>API: GET /users/aliases
  API-->>Repo: BaseResponse<...>
  Repo-->>VM: Result

  UI->>VM: signup()
  VM->>Store: getTempToken()
  Store-->>VM: token or null
  alt token exists
    Repo->>API: POST /users/signup (Authorization)
    API-->>Repo: BaseResponse<SignupResponse>
    Repo-->>VM: Success
  else
    VM-->>UI: error (temp token missing)
  end
Loading
sequenceDiagram
  participant UI as Search UI
  participant VM as SearchPeopleViewModel
  participant Repo as UserRepository
  participant API as UserService

  UI->>VM: onSearchTextChanged(text)
  VM->>VM: debounce 500ms
  VM->>Repo: searchUsers(keyword, isFinalized=false)
  Repo->>API: GET /users?keyword&isFinalized=false
  API-->>Repo: BaseResponse<UserSearchResponse>
  Repo-->>VM: Result -> update uiState.searchResults

  UI->>VM: onFinalSearch(query)
  VM->>Repo: searchUsers(keyword, isFinalized=true)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
프로필 편집, 마이페이지 유저정보, 팔로잉 관련(집합) (#73)
사용자 검색 조회 (#73)
회원가입, 닉네임 중복 검증, 칭호선택 (#73)
팔로잉 상태 변경 / 특정유저 팔로잉 리스트 (세부 항목) (#73) UserRepository/Service에 팔로우 토글 관련 변경은 보이지 않음; 관련 API 호출 추가 여부 불명.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
고객센터 화면 및 네비게이션 추가 (app/src/main/java/.../ui/mypage/screen/MypageCustomerServiceScreen.kt; .../routes/MyPageRoutes.kt; .../extensions/MyPageNavigationExtensions.kt; .../navigations/MyPageNavigation.kt) 링크된 이슈는 User API 연동 중심인데, 고객센터 UI/라우팅 추가는 API 연동 목표와 직접 관련이 없음.
Saved/Notification/Leave 화면의 onNavigateBack 시그니처 변경 (app/src/main/java/.../MypageSaveScreen.kt; .../MypageNotificationEditScreen.kt; .../MypageLeavethipScreen.kt) 기능상은 UI 네비게이션 연결로 범위 외 변경이며, API 연동 요구사항과 무관함.

Possibly related issues

Possibly related PRs

Suggested labels

✅ OK merge

Suggested reviewers

  • rbqks529

Poem

깡충 깡충 커밋을 건너뛰며
토큰을 주머니에 살포시 품고,
닉네임은 반짝이며 검색은 살금살금—
프로필 단장하고 고객센터 길도 열었지요.
당근 하나 들고 배포길로 폴짝 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0e74ef5 and 8592b8c.

📒 Files selected for processing (3)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (5 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt
  • app/src/main/res/values/strings.xml
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🔭 Outside diff range comments (1)
app/build.gradle.kts (1)

79-79: Compose Foundation 중복/버전 충돌 위험: BOM 사용 중에 고정 버전(1.5.0) 명시 제거 권장.

이미 BOM과 라이브러리 alias(libs.androidx.foundation)를 사용 중인데, 같은 모듈을 직접 버전으로 한 번 더 추가하면 버전 미스매치/해결 불일치가 생길 수 있습니다. 아래 라인 제거를 권장합니다.

-    implementation("androidx.compose.foundation:foundation:1.5.0")
🧹 Nitpick comments (38)
app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt (4)

8-8: FollowerList 네이밍이 의미와 맞지 않음 — 단수 엔티티명으로 정리 제안

FollowerList는 “목록”처럼 들리지만 실제로는 단일 팔로워 아이템입니다. 도메인 용어 혼동을 줄이기 위해 Follower 또는 FollowerItem 같은 단수형 네이밍을 권장합니다.

 data class OthersFollowersResponse(
-    @SerializedName("followers") val followers: List<FollowerList>,
+    @SerializedName("followers") val followers: List<Follower>,
     ...
 )

-@Serializable
-data class FollowerList(
+@Serializable
+data class Follower(
     ...
 )

주의: 파일/타입명을 변경하면 사용처 전반(리포지토리, UI 모델 매핑 등)도 함께 수정이 필요합니다.

Also applies to: 15-22


10-11: 중복된 페이징 신호(nextCursor, isLast) — 일관성 규칙 합의 권장

서버가 nextCursor와 isLast를 함께 내려주면, 두 값이 불일치할 경우 어떤 값을 신뢰할지 규칙이 필요합니다. 예: nextCursor가 null이어도 isLast=false로 내려오는 등의 엣지 케이스.

권장:

  • 저장소(Repository) 레이어에서 우선순위를 명시(isLast를 신뢰하고 nextCursor는 보조로 사용 등).
  • 혹은 한 값만 사용하도록 내부 규약 문서화.

테스트로 불일치 케이스를 최소 1건 커버하면 회귀 방지에 도움이 됩니다.


6-12: 간단 KDoc 추가 제안

응답 모델은 사용처가 넓어 도메인 의미(예: totalFollowerCount가 전체 팔로워 수인지, 현재 페이지 기준인지)를 짧게 KDoc으로 남겨두면 유지보수에 유리합니다.


1-22: kotlinx.serialization 어노테이션 통일 및 불필요한 Gson 의존성 제거
네트워크 모듈에서 이미 retrofit2-kotlinx-serialization-converter(json.asConverterFactory(…))를 사용하고 있어 @SerializedName(Gson)은 런타임에 적용되지 않습니다. 모델 클래스에서 com.google.gson.annotations.SerializedName을 제거하고 kotlinx.serialization.SerialName으로 통일하거나, 필드명 기반 매핑(어노테이션 없이)으로 유지해주세요. 또한, gradle/libs.versions.tomlconverter-gson 의존성도 함께 정리하는 것을 권장합니다.

  • 검토 대상
    • app/src/main/java/com/texthip/thip/data/model/@SerializedName 사용 파일
    • gradle/libs.versions.toml: converter-gson 항목
    • 참고: app/src/main/java/com/texthip/thip/data/di/NetworkModule.kt (.addConverterFactory(json.asConverterFactory(...)) 사용 중)

  • 예시 (OthersFollowersResponse.kt)

-package com.texthip.thip.data.model.users.response
-
-import com.google.gson.annotations.SerializedName
-import kotlinx.serialization.Serializable
+package com.texthip.thip.data.model.users.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable

 @Serializable
 data class OthersFollowersResponse(
-    @SerializedName("followers") val followers: List<FollowerList>,
+    @SerialName("followers")     val followers: List<FollowerList>,
     @SerialName("totalFollowerCount") val totalFollowerCount: Int,
     @SerialName("nextCursor")          val nextCursor: String?,
     @SerialName("isLast")              val isLast: Boolean
 )
app/src/main/java/com/texthip/thip/data/model/users/request/ProfileUpdateRequest.kt (1)

5-9: nullable 필드 전송 방식 확인 필요(null 전송 vs. 미포함).

nickname이 null일 때 서버가 "null 값을 명시적으로 전송"하는지, "필드를 생략"하기를 기대하는지 확인해주세요. kotlinx.serialization의 Json 설정(explicitNulls)에 따라 동작이 달라집니다. 서버가 미포함을 기대한다면 Retrofit의 Kotlinx Json 인스턴스를 explicitNulls = false로 설정해야 합니다.

예시(네트워크 모듈에서):

val json = Json {
  ignoreUnknownKeys = true
  explicitNulls = false // null 필드는 직렬화하지 않음
}
app/src/main/res/values/strings.xml (1)

137-143: 오류 메시지 추가 좋습니다. 소소한 문장부호/일관성 제안.

  • 일부 토스트/알림 문구는 종결 부호가 있고(예: 프로필 성공), 일부는 없습니다. 동일 맥락의 메시지라면 종결 부호 유무를 통일하면 좋습니다.

예시(원칙 정한 뒤 일괄 적용):

  • 토스트/스낵바: 마침표 생략
  • 다이얼로그 본문/긴 문장: 마침표 사용
    필요 시 일괄 변경 PR 생성 도와드릴게요.
app/build.gradle.kts (2)

86-90: Retrofit 컨버터 2종(Gson + Kotlinx) 동시 사용 주의 — 한 가지로 표준화 또는 순서 명시.

모델에서 Gson 주석과 Kotlinx @serializable를 혼합 사용 중입니다. 실제 파싱은 Retrofit 빌더에서 등록 순서에 좌우됩니다. 팀 표준(예: Kotlinx 우선)을 정하고, 불필요한 컨버터/주석을 제거하는 것을 권장합니다.

Retrofit 구성 예:

Retrofit.Builder()
  .baseUrl(BASE_URL)
  .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) // kotlinx 우선
  // .addConverterFactory(GsonConverterFactory.create()) // 필요 없다면 제거
  .build()

12-14: BASE_URL 미설정 시 빌드 타임에 실패하도록 방어 로직 추가 제안.

현재 local.properties에 BASE_URL이 없으면 문자열 "null"이 주입될 가능성이 있습니다. 빌드 시점에 명확히 실패시키는 편이 안전합니다.

다음과 같이 개선 가능합니다:

 val properties = Properties().apply {
-    load(project.rootProject.file("local.properties").inputStream())
+    val file = project.rootProject.file("local.properties")
+    if (file.exists()) load(file.inputStream())
 }
 ...
-        buildConfigField("String", "BASE_URL", "\"${properties["BASE_URL"]}\"")
+        val baseUrl = properties["BASE_URL"] as? String
+            ?: throw GradleException("local.properties에 BASE_URL이 정의되어 있지 않습니다.")
+        buildConfigField("String", "BASE_URL", "\"$baseUrl\"")

Also applies to: 29-29

app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt (1)

29-31: 고객센터 네비게이션 추가 LGTM — 중복 쌓임 방지 옵션 고려

동일 화면으로의 중복 네비게이션을 방지하고 상태 복원을 활용하려면 NavOptions를 함께 지정하는 것을 권장합니다.

아래와 같이 옵션을 추가해 보세요:

 fun NavHostController.navigateToCustomerService() {
-    navigate(MyPageRoutes.CustomerService)
+    navigate(MyPageRoutes.CustomerService) {
+        launchSingleTop = true
+        restoreState = true
+    }
 }
app/src/main/java/com/texthip/thip/MainActivity.kt (1)

21-23: 필드 주입이 현재 미사용 — 불필요하면 제거하거나 경고 억제

tokenManager가 현재 사용되지 않아 Lint 경고 및 추후 유지보수 시 혼동의 여지가 있습니다. 당장 필요 없다면 제거하거나, 임시로 유지해야 한다면 경고 억제를 권장합니다.

옵션 A) 제거:

-    @Inject
-    lateinit var tokenManager: TokenManager

옵션 B) 경고 억제:

     @Inject
+    @Suppress("unused")
     lateinit var tokenManager: TokenManager
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt (1)

35-37: onNavigateBack에 기본값 제공으로 호출 간소화 제안

다른 호출부(프리뷰 포함)에서 보일러플레이트를 줄이려면 기본값을 두는 것이 편합니다.

 fun NotificationScreen(
-    onNavigateBack: () -> Unit
+    onNavigateBack: () -> Unit = {}
 )
app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (1)

26-26: 잔존 recentWriters 식별자 없음 확인 — @SerialName 어노테이션 병행 지정 권장

코드베이스 전반에서 기존 recentWriters 참조는 더 이상 발견되지 않았습니다. 아래와 같이 Gson의 @SerializedName과 Kotlin Serialization의 @SerialName을 동시에 지정하면 직렬화 어노테이션 정합성을 높일 수 있습니다.

  • 대상:
    app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (26행)
    적용 예시(diff):
    - @SerializedName("myFollowingUsers") val myFollowingUsers: List
    + @SerialName("myFollowingUsers")
    + @SerializedName("myFollowingUsers") val myFollowingUsers: List

  • 파일 상단에 import 추가:
    import kotlinx.serialization.SerialName

  • 선택적 리네임 제안:
    현재 RecentWriterList 타입명이 실제 의미와 다소 불일치할 수 있으므로, FollowingUserSummary 등으로 이름을 검토해 보시기 바랍니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (6)

60-62: 초기 페치 트리거는 적절합니다만, 네이밍/중복 호출 방지 개선 제안

  • LaunchedEffect(Unit)으로 최초 컴포지션 시점에 페치하는 접근은 적절합니다.
  • 다만 UI 상태 필드가 myFollowingUsers인데 메서드명이 fetchRecentWriters()로 남아 있어 의미가 혼재됩니다. 도메인 용어에 맞춰 메서드명을 fetchMyFollowings() 등으로 정리하면 가독성과 유지보수성이 높아집니다.
  • 또한 화면 재진입/재구성 시 불필요한 재호출을 막으려면, 이미 데이터가 로드된 경우는 스킵하는 간단한 가드도 고려할 수 있습니다.

70-119: 사용되지 않는 더미 데이터 블록 제거

val mySubscriptions = listOf(...)는 이 파일 내 어디에서도 사용되지 않습니다. 유지하면 혼동을 유발하고 리소스를 낭비합니다. 제거를 권장합니다.

아래처럼 전체 블록을 제거하세요:

-    val mySubscriptions = listOf(
-        MySubscriptionData(
-            profileImageUrl = "https://example.com/image1.jpg",
-            nickname = "abcabcabcabc",
-            role = "문학가",
-            roleColor = colors.SocialScience
-        ),
-        ...
-    )

120-120: 중복된 상태 수집 제거

feedUiState를 이미 collectAsState() 하고 있는데, 동일한 uiState를 다시 subscriptionUiState로 수집하고 사용하지 않습니다. 불필요하므로 제거하세요.

-    val subscriptionUiState by viewModel.uiState.collectAsState()

220-229: 구독 바 매핑: 중복 제거를 위해 확장 함수 활용 제안

PR 내에 UserItem.toMySubscriptionData() 확장 함수가 추가된 것으로 보입니다. 여기서 수동 매핑 대신 확장 함수를 사용하면 중복 로직이 사라지고 필드 매핑(예: role/roleColor 등)도 통일됩니다.

-                        val subscriptionsForBar = feedUiState.myFollowingUsers.map { user ->
-                            MySubscriptionData(
-                                profileImageUrl = user.profileImageUrl,
-                                nickname = user.nickname,
-                                role = "",
-                                roleColor = colors.White,
-                                subscriberCount = 0,
-                                isSubscribed = true
-                            )
-                        }
+                        val subscriptionsForBar = feedUiState.myFollowingUsers.map { it.toMySubscriptionData() }

참고: 필요 시 import com.texthip.thip.ui.feed.mock.toMySubscriptionData를 추가하세요.


208-213: Divider 표시 조건의 기준 리스트 정합성

itemsIndexed의 데이터 소스는 feedStateList인데, Divider 조건은 feeds.lastIndex를 기준으로 비교하고 있습니다. 현재는 길이가 동일하더라도, 이후 feedStateList가 동적으로 변할 가능성을 고려하면 기준을 동일하게 맞추는 편이 안전합니다.

-                            if (index != feeds.lastIndex) {
+                            if (index != feedStateList.lastIndex) {

Also applies to: 255-260


237-237: 불필요한 로컬 변수 제거

val profileImage = ...는 선언만 하고 사용되지 않습니다. 제거하세요.

-                        val profileImage = feed.userProfileImage?.let { painterResource(it) }
app/src/main/java/com/texthip/thip/ui/feed/mock/MySubscriptionData.kt (1)

1-1: ‘mock’ 패키지 위치 재고려 제안

생산 코드에서 실제로 사용하는 매퍼(확장 함수)가 ui.feed.mock 패키지에 위치해 있어 혼동 소지가 있습니다. 재사용성과 의도를 위해 ui.feed.model 또는 ui.common.mapper 같은 위치로의 이동을 고려해 주세요.

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageCustomerServiceScreen.kt (3)

37-41: 내부 Column은 weight로 남은 영역을 채우는 것이 Compose 권장 패턴

Column 내부에서 또 다른 Column에 fillMaxSize()를 주면 부모 Column의 측정 제약과 맞지 않아 의도대로 동작하지 않을 수 있습니다. 남은 영역을 차지하며 중앙 정렬하려면 weight(1f) + fillMaxWidth() 조합을 권장합니다.

아래처럼 수정해 주세요:

-        Column(
-            modifier = Modifier
-                .fillMaxSize(),
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .weight(1f),
             verticalArrangement = Arrangement.Center,
             horizontalAlignment = Alignment.CenterHorizontally
         ) {

3-8: Spacer에 padding만 주는 대신 고정 높이 사용

Spacer는 크기 지정 없이 padding만 주면 의도한 간격이 보장되지 않습니다. height(8.dp)로 변경하세요. 함께 height import 추가가 필요합니다.

+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-            Spacer(modifier = Modifier.padding(top = 8.dp))
+            Spacer(modifier = Modifier.height(8.dp))

Also applies to: 47-47


42-46: 고객센터 이메일을 클릭 시 메일 앱으로 연결 (선택사항)

접근성과 편의성을 위해 이메일을 눌렀을 때 메일 앱으로 이동하도록 처리하는 것을 권장합니다.

아래 변경이 필요합니다:

  • import 및 uriHandler 추가
  • 이메일 Text에 clickable 부여
+import androidx.compose.foundation.clickable
+import androidx.compose.ui.platform.LocalUriHandler

함수 내부(라인 25 직후 등 적절한 위치)에 추가:

val uriHandler = LocalUriHandler.current

이메일 Text 수정:

-            Text(
-                text = stringResource(R.string.customer_center_email),
-                style = typography.smalltitle_sb600_s18_h24,
-                color = colors.White
-            )
+            Text(
+                text = stringResource(R.string.customer_center_email),
+                style = typography.smalltitle_sb600_s18_h24,
+                color = colors.White,
+                modifier = Modifier.clickable {
+                    uriHandler.openUri("mailto:${stringResource(R.string.customer_center_email)}")
+                }
+            )
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (1)

31-31: 새 요청 시작 시 이전 에러 메시지 초기화 제안

이전 실패 메시지가 다음 로딩 상태에도 남아 UI에 잔존할 수 있습니다. 로딩 시작 시 errorMessage를 null로 초기화하는 것이 안전합니다.

-            _uiState.update { it.copy(isLoading = true) }
+            _uiState.update { it.copy(isLoading = true, errorMessage = null) }
app/src/main/java/com/texthip/thip/ui/navigator/navigations/signupNavigation.kt (1)

15-22: 하드코딩된 경로 문자열 상수화로 오타·유지보수 리스크 감소

route와 destination 문자열(“signup_flow”, “signup_nickname”, “signup_genre”)을 상수로 묶어두면 안전합니다.

아래 상수를 추가하고 사용처를 교체해 주세요.

상수 정의(파일 상단 import 아래에 추가):

private object SignupRoutes {
    const val Root = "signup_flow"
    const val Nickname = "signup_nickname"
    const val Genre = "signup_genre"
}

사용처 변경:

-    navigation(
-        startDestination = "signup_nickname",
-        route = "signup_flow"
-    ) {
-        composable("signup_nickname") { navBackStackEntry ->
+    navigation(
+        startDestination = SignupRoutes.Nickname,
+        route = SignupRoutes.Root
+    ) {
+        composable(SignupRoutes.Nickname) { navBackStackEntry ->
             val parentEntry = remember(navBackStackEntry) {
-                navController.getBackStackEntry("signup_flow")
+                navController.getBackStackEntry(SignupRoutes.Root)
             }
             val viewModel: SignupViewModel = hiltViewModel(parentEntry)

             SignupNicknameScreen(
                 viewModel = viewModel,
                 onNavigateToGenre = {
-                    navController.navigate("signup_genre")
+                    navController.navigate(SignupRoutes.Genre)
                 }
             )
         }

-        composable("signup_genre"){ navBackStackEntry ->
+        composable(SignupRoutes.Genre){ navBackStackEntry ->
             val parentEntry = remember(navBackStackEntry) {
-                navController.getBackStackEntry("signup_flow")
+                navController.getBackStackEntry(SignupRoutes.Root)
             }
             val viewModel: SignupViewModel = hiltViewModel(parentEntry)

             SignupGenreScreen(
                 viewModel = viewModel,
                 onSignupSuccess = {
                     navController.navigate(MainTabRoutes.Feed) {
-                        popUpTo("signup_flow") { inclusive = true }
+                        popUpTo(SignupRoutes.Root) { inclusive = true }
                     }
                 }
             )
         }

Also applies to: 19-23, 28-29, 33-37, 42-44

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (2)

51-53: 초기 데이터 로드 LaunchedEffect 도입 — 적절

첫 진입 시 마이페이지 정보를 로드하도록 보장되어 UX 측면에서 좋습니다. 재진입 시 갱신 정책이 필요하다면 ViewModel init에서 트리거하거나, 화면 가시성 기준으로 repeatOnLifecycle을 고려해볼 수 있습니다.


17-18: Lifecycle-aware 수집으로 변경 제안 (collectAsStateWithLifecycle)

다른 화면(Signup*)에서는 collectAsStateWithLifecycle을 사용하고 있어, 이 화면도 동일하게 맞추면 리소스 관리와 일관성에 도움이 됩니다.

아래처럼 변경을 제안드립니다:

- import androidx.compose.runtime.collectAsState
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
- val uiState by viewModel.uiState.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Also applies to: 50-50

app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (1)

45-49: 에러 Toast는 상태 대신 이벤트 스트림 사용 권장

동일 문자열을 다시 발행해도 Composable 측에서 토스트가 다시 뜨지 않을 수 있고(동일 key), 재입장 시 중복 재생 리스크가 있습니다. SharedFlow/Channel 기반 단발 이벤트로 전환을 권장합니다.

원하시면 ViewModel/Composable 이벤트 전환 패치를 준비해드릴게요. 예시:

ViewModel 예시

class SignupViewModel : ViewModel() {
    private val _errorEvents = MutableSharedFlow<String>(extraBufferCapacity = 1)
    val errorEvents: SharedFlow<String> = _errorEvents

    fun emitError(message: String) {
        _errorEvents.tryEmit(message)
    }
}

Composable 예시

LaunchedEffect(Unit) {
    viewModel.errorEvents.collect { message ->
        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
    }
}
app/src/main/java/com/texthip/thip/data/service/UserService.kt (1)

64-69: Authorization 헤더 파라미터 네이밍 개선 제안

서비스 계층에서는 헤더의 의미를 일반화하여 두는 편이 유지보수에 유리합니다. tempToken 대신 authorization으로 명명하면 호출부와 역할이 더 명확해집니다(현재 호출부는 이미 "Bearer ..." 접두사를 붙여 전달 중).

아래처럼 변경을 제안합니다(호출부의 인자명 지정이 없다면 바이너리 호환성 문제는 없습니다):

- suspend fun signup(
-     @Header("Authorization") tempToken: String,
+ suspend fun signup(
+     @Header("Authorization") authorization: String,
      @Body request: SignupRequest
  ): BaseResponse<SignupResponse>
app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (2)

89-100: 임시 토큰 누락 시 메시지 생성의 계층 분리 제안

Repository에서 사용자 문구(한글 문자열)를 생성하면 다국어/표현 변경 시 수정 범위가 커집니다. 도메인 예외를 던지고 UI 계층에서 문자열 리소스로 매핑하는 방식을 권장합니다.

아래처럼 전환을 제안드립니다:

- if (tempToken.isNullOrBlank()) {
-     return Result.failure(Exception("임시 토큰이 없습니다. 로그인을 다시 시도해주세요."))
- }
+ if (tempToken.isNullOrBlank()) {
+     return Result.failure(MissingTempTokenException())
+ }

Repository 외부(예: common/errors 패키지)에 예외 타입 추가:

class MissingTempTokenException : IllegalStateException("Temp token is missing")

UI(ViewModel)에서 MissingTempTokenException을 잡아 사용자 문구를 stringResource로 매핑하세요.


102-109: 검색 size 파라미터 노출 제안

서비스는 기본 30이지만, 호출부에서 사이즈 조절이 필요할 수 있습니다. Repository 메서드에 size를 옵션으로 노출하면 확장성이 좋아집니다.

아래처럼 변경을 제안합니다:

- suspend fun searchUsers(
-     keyword: String,
-     isFinalized: Boolean
- ): Result<UserSearchResponse?> = runCatching {
-     userService.searchUsers(isFinalized = isFinalized, keyword = keyword)
+ suspend fun searchUsers(
+     keyword: String,
+     isFinalized: Boolean,
+     size: Int = 30
+ ): Result<UserSearchResponse?> = runCatching {
+     userService.searchUsers(isFinalized = isFinalized, keyword = keyword, size = size)
         .handleBaseResponse()
         .getOrThrow()
 }
app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (3)

36-41: DataStore 읽기 시 IOException 캐치 없이 first() 호출 → 앱 크래시 가능성

DataStore 권장 패턴대로 data 플로우에 catch를 추가해 빈 Preferences를 emit하도록 해주세요. 일시적 I/O 문제에서 안전합니다.

적용 제안(diff):

-    suspend fun getTempToken(): String? {
-        return context.dataStore.data.map { prefs ->
-            prefs[TEMP_TOKEN_KEY]
-        }.first() // Flow에서 첫 번째 값을 한번만 읽어옴
-    }
+    suspend fun getTempToken(): String? {
+        return context.dataStore.data
+            .catch { emit(emptyPreferences()) }
+            .map { prefs -> prefs[TEMP_TOKEN_KEY] }
+            .first()
+    }

필요 import:

import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.catch

51-56: AccessToken Flow에도 예외 처리 추가 권장

getAccessToken() 역시 동일한 I/O 예외 가능성이 있으니 catch를 추가해 주세요.

적용 제안(diff):

-    fun getAccessToken(): kotlinx.coroutines.flow.Flow<String?> {
-        return context.dataStore.data.map { prefs ->
-            prefs[ACCESS_TOKEN_KEY]
-        }
-    }
+    fun getAccessToken(): kotlinx.coroutines.flow.Flow<String?> {
+        return context.dataStore.data
+            .catch { emit(emptyPreferences()) }
+            .map { prefs -> prefs[ACCESS_TOKEN_KEY] }
+    }

필요 import(이미 위 코멘트에 포함): emptyPreferences, catch


51-56: Refresh Token 조회 API 부재로 대칭성/재사용성 저하

Access/Refresh 동시 저장(saveAccessTokens)은 있지만 Refresh 토큰을 읽는 API가 없어 사용처에서 우회 구현될 여지가 있습니다. 대칭 API를 추가해 주세요.

참고 구현(추가 코드):

// TokenManager 내에 추가
fun getRefreshToken(): kotlinx.coroutines.flow.Flow<String?> {
    return context.dataStore.data
        .catch { emit(emptyPreferences()) }
        .map { prefs -> prefs[REFRESH_TOKEN_KEY] }
}
app/src/main/java/com/texthip/thip/ui/feed/screen/SearchPeopleScreen.kt (1)

88-139: 로딩/에러 UI 미노출

uiState.isLoading, uiState.errorMessage에 대한 피드백이 없어 검색 시 무반응처럼 보일 수 있습니다. 상단에 얇은 로딩 인디케이터 또는 전체 오버레이를 추가하고, 에러는 간단한 안내 텍스트/토스트로 표시하는 것을 권장합니다.

간단 예시(추가 코드):

if (uiState.isLoading) {
    androidx.compose.material3.LinearProgressIndicator(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 20.dp)
    )
}
uiState.errorMessage?.let { msg ->
    Spacer(Modifier.height(8.dp))
    Text(
        text = msg,
        color = colors.NeonGreen,
        style = typography.menu_m500_s14_h24,
        modifier = Modifier.padding(horizontal = 20.dp)
    )
}
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/SearchPeopleViewModel.kt (4)

18-25: 최근 검색어 초기값 더미 제거 및 영속화 제안

recentSearches에 하드코딩된 "메롱" 값은 프로덕션에 노출될 수 있습니다. DataStore/Room 등으로 최대 10개를 영속화해 초기 로딩하도록 전환을 권장합니다. 필요 시 스펙 제안/구현 도와드릴게요.


73-76: mock 패키지 의존 최소화

toMySubscriptionData가 ui.feed.mock 아래에 있는 점이 약간 어색합니다. 실제 화면에서 사용하는 매퍼/모델이라면 ui.model(or presentation.model)로 이동해 mock 명칭 의존을 제거하면 패키지 의미가 더 명확해집니다.


37-53: 디바운스 처리 방향은 적절합니다 (추가 개선 아이디어)

현재 Job+delay로 디바운스 구현이 명확합니다. 향후 입력을 Flow로 바꾸면 debounce + distinctUntilChanged + flatMapLatest로 간결하게 취소/동시성까지 일관 관리할 수 있습니다. 현 단계에서는 선택 사항입니다.


85-97: 최근 검색어 dedup/최대 개수 제한 로직 적절합니다

  • 새 키워드를 맨 앞에 두고 distinct().take(10)으로 중복 제거/개수 제한하는 방식 합리적입니다.
  • 추후 대소문자/공백 트리밍 규칙을 정의하면 UX가 더 좋아질 수 있습니다.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e3438dc and a2884ff.

📒 Files selected for processing (35)
  • app/build.gradle.kts (1 hunks)
  • app/src/main/java/com/texthip/thip/MainActivity.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/request/ProfileUpdateRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/request/SignupRequest.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/data/service/UserService.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/mock/MySubscriptionData.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/screen/SearchPeopleScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/feed/viewmodel/SearchPeopleViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageCustomerServiceScreen.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt (3 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt (4 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (5 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageEditViewModel.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt (0 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/navigations/signupNavigation.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt (1 hunks)
  • app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (2 hunks)
  • app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt (0 hunks)
  • app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt (0 hunks)
  • app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt (1 hunks)
  • app/src/main/res/values/strings.xml (2 hunks)
  • gradle/libs.versions.toml (2 hunks)
💤 Files with no reviewable changes (3)
  • app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt
  • app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt
  • app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt
🧰 Additional context used
🧬 Code Graph Analysis (6)
app/src/main/java/com/texthip/thip/ui/feed/mock/MySubscriptionData.kt (1)
app/src/main/java/com/texthip/thip/utils/color/HexToColor.kt (1)
  • hexToColor (5-12)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageCustomerServiceScreen.kt (2)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (1)
  • DefaultTopAppBar (22-65)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/signupNavigation.kt (2)
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (1)
  • SignupNicknameScreen (31-58)
app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (1)
  • SignupGenreScreen (32-60)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (5)
app/src/main/java/com/texthip/thip/ui/common/topappbar/InputTopAppBar.kt (1)
  • InputTopAppBar (25-69)
app/src/main/java/com/texthip/thip/ui/common/forms/WarningTextField.kt (1)
  • WarningTextField (35-121)
app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt (1)
  • RoleCard (34-91)
app/src/main/java/com/texthip/thip/ui/common/modal/ToastWithDate.kt (1)
  • ToastWithDate (24-60)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (5)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt (1)
  • EditProfileScreen (46-70)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt (1)
  • SavedScreen (46-120)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt (1)
  • NotificationScreen (34-114)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt (1)
  • DeleteAccountScreen (41-169)
app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageCustomerServiceScreen.kt (1)
  • MypageCustomerServiceScreen (21-61)
app/src/main/java/com/texthip/thip/ui/feed/screen/SearchPeopleScreen.kt (6)
app/src/main/java/com/texthip/thip/ui/common/topappbar/DefaultTopAppBar.kt (1)
  • DefaultTopAppBar (22-65)
app/src/main/java/com/texthip/thip/ui/common/forms/SearchBookTextField.kt (1)
  • SearchBookTextField (38-112)
app/src/main/java/com/texthip/thip/ui/feed/component/LiveSearchPeopleResult.kt (1)
  • SearchPeopleResult (21-50)
app/src/main/java/com/texthip/thip/ui/feed/component/SearchPeopleEmptyResult.kt (1)
  • SearchPeopleEmptyResult (16-32)
app/src/main/java/com/texthip/thip/ui/feed/component/PeopleRecentSearch.kt (1)
  • PeopleRecentSearch (27-63)
app/src/main/java/com/texthip/thip/ui/theme/Theme.kt (1)
  • ThipTheme (41-69)
🔇 Additional comments (35)
app/src/main/java/com/texthip/thip/data/model/users/response/OthersFollowersResponse.kt (1)

16-21: API의 null 가능성 점검 필요(aliasName, aliasColor) — 런타임 NPE 리스크

aliasName/aliasColor가 서버에서 간헐적으로 null/생략될 수 있다면:

  • Gson 사용 시: Kotlin non-null(String) 필드에 null이 주입되어 런타임에서 NPE가 날 수 있습니다.
  • kotlinx.serialization 사용 시: 디코딩 단계에서 실패할 수 있습니다.

API 계약이 확실히 “항상 값 존재”가 아니라면 nullable로 선언하거나 상위 레이어에서 안전하게 기본값을 부여하는 쪽이 안전합니다.

참고 변경안(nullable로 완화):

-    @SerializedName("aliasName") val aliasName: String,
-    @SerializedName("aliasColor") val aliasColor: String,
+    @SerializedName("aliasName") val aliasName: String?,
+    @SerializedName("aliasColor") val aliasColor: String?,

kotlinx.serialization로 전환 시에는 @SerialName으로 동일하게 변경하면 됩니다.

app/src/main/java/com/texthip/thip/data/model/users/request/ProfileUpdateRequest.kt (1)

5-9: 모델 자체는 깔끔합니다.

요청 DTO 구조가 명확하고, 불필요한 주석/로직이 없어 유지보수에 용이합니다.

app/src/main/java/com/texthip/thip/data/model/users/request/SignupRequest.kt (1)

5-9: 요청 DTO 정의 적절.

필수 필드만 포함한 심플한 모델로 충분합니다. 서버 필드명이 동일하다면 별도의 @SerialName 없이도 문제 없습니다.

app/src/main/res/values/strings.xml (1)

126-127: 문장 분리 변경 적절.

2줄을 각각의 문자열 리소스로 분리하여 UI에서 줄바꿈/레이아웃 제어가 쉬워졌습니다. 연결 사용 시 접근성(읽기 순서)도 명확해집니다.

app/build.gradle.kts (1)

71-71: Datastore 의존성 추가 OK.

안드로이드X Datastore Preferences 추가가 TokenManager 도입과 일치합니다.

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt (2)

73-73: 뒤로가기 콜백 연결 적절합니다

TopAppBar의 좌측 클릭에 onNavigateBack을 직접 연결한 점 좋습니다. 내비게이션 일관성이 좋아졌습니다.


119-121: 프리뷰 시그니처 업데이트 적절합니다

프리뷰가 새로운 파라미터 시그니처를 반영하도록 수정된 점 좋습니다.

app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt (1)

12-12: 새 Route 추가 LGTM

@Serializable data object CustomerService 추가로 타입-세이프 내비게이션 확장이 자연스럽습니다. 확장 함수 및 내비게이션 그래프와의 연결도 일관됩니다.

app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt (1)

16-16: LaunchedEffect import 추가 문제 없음

컴포저블 내 초기 사이드이펙트를 위한 LaunchedEffect 사용을 위해 필요한 import입니다. 이상 없습니다.

gradle/libs.versions.toml (1)

62-62: datastore-preferences 의존성 alias 추가 LGTM

TokenManager의 Preferences DataStore 사용과 일치합니다. 문제 없습니다.

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt (3)

42-44: 뒤로가기 콜백 파라미터 도입 LGTM

내비게이션 패턴 일관성 관점에서 좋은 변경입니다.


56-56: 탑 앱바 좌측 클릭 핸들러 연결 LGTM

onNavigateBack 위임으로 사용자 기대와 맞는 동작을 합니다.


174-176: 프리뷰 업데이트 LGTM

새 시그니처에 맞춘 더미 콜백 적용이 적절합니다.

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt (4)

48-51: 뒤로가기 콜백 파라미터 추가 LGTM

내비게이션 그래프와의 결합도가 낮아지고 재사용성이 좋아집니다.


65-65: 탑 앱바 좌측 클릭 핸들러 연결 LGTM

사용자 기대에 맞는 뒤로가기 동작을 제공합니다.


128-129: 프리뷰 업데이트 LGTM

새 시그니처에 맞춰 더미 콜백 전달이 적절합니다.


140-141: 프리뷰(빈 데이터) 업데이트 LGTM

일관된 시그니처 적용 확인했습니다.

app/src/main/java/com/texthip/thip/ui/feed/mock/MySubscriptionData.kt (1)

17-26: 사용자 → 구독 UI 데이터 매핑 잘 추가됨 (LGTM)

  • 필드 매핑이 명확하고, 잘못된 색상 문자열에 대한 hexToColor의 예외 처리도 커버되어 안정적입니다.
  • API에 구독 여부가 없어서 isSubscribed=false로 고정한 것도 현재 제약을 고려하면 합리적입니다. 향후 API에 구독 여부가 추가되면 해당 필드로 치환하면 되겠습니다.
app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt (2)

16-16: recentWriters → myFollowingUsers로의 상태 전환 및 외부 트리거화(LGTM)

  • 상태 키 변경 의도가 명확하고, 화면에서 LaunchedEffect로 호출하도록 가시화한 점 👍
  • 성공/실패 모두 로딩 종료 처리도 적절합니다.

Also applies to: 29-29, 37-38


29-45: fetchRecentWriters() 호출 보장 확인 완료
FeedScreen.kt(61행)의 LaunchedEffect(Unit) 블록에서만 fetchRecentWriters()를 호출하고 있으며, 프로젝트 내 다른 호출 지점은 없습니다.

app/src/main/java/com/texthip/thip/ui/navigator/navigations/signupNavigation.kt (1)

14-24: NavGraph 범위 Hilt ViewModel 공유 패턴 적절 (LGTM)

  • signup_flow 단위로 hiltViewModel(parentEntry) 공유한 점과 popUpTo로 스택 정리한 점 모두 적절합니다.

Also applies to: 33-39, 39-46

app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt (1)

10-10: MyPage 고객센터 라우팅 추가 연동 적절 (LGTM)

  • MyPageScreen에서 onCustomerService 콜백을 통해 navigateToCustomerService()로 자연스럽게 연결되고,
  • CustomerService 라우트에서 MypageCustomerServiceScreen을 렌더링하며 뒤로가기 popBackStack도 일관됩니다.

Also applies to: 13-13, 29-31, 54-57

app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt (1)

46-49: 고객센터 네비게이션 콜백 배선 — LGTM

  • 파라미터 추가 → Content 전달 → 버튼 onClick 연결 → 프리뷰 반영까지 일관성 있게 처리되었습니다.

Also applies to: 59-64, 72-72, 166-167, 255-256

app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt (3)

33-35: 공용 SignupViewModel 주입 및 네비게이션 콜백 — LGTM

단일 ViewModel로 플로우를 공유하는 설계가 자연스럽고, 네비게이션 콜백도 명확합니다.


38-44: 장면 전환 원샷 처리 — LGTM

navigate flag를 소모(onNavigatedToGenre)하는 호출이 포함되어 재실행을 방지하는 구조가 적절합니다.


56-57: 경고 메시지 리소스 전달 — LGTM

null 처리와 stringResource 변환 시점이 안전하게 작성되었습니다.

app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt (3)

39-41: 별칭(장르/역할) 목록 초기 로드 — LGTM

첫 구성에서 fetchAliasChoices() 트리거로 데이터 준비가 명확합니다.


57-59: 액션 바인딩과 중복 호출 방지 상태 — LGTM

onCardSelected, onNextClick 메서드 레퍼런스 연결과 isLoading을 활용한 Button 비활성화로 중복 트리거 방지가 잘 되어 있습니다.


139-142: 프리뷰 상태 구성 — LGTM

미리보기에서 선택 인덱스와 카드 목록이 직관적으로 표현되어 UI 확인이 수월합니다.

app/src/main/java/com/texthip/thip/data/service/UserService.kt (2)

59-63: PATCH users: updateProfile 엔드포인트 정의 — LGTM

서버가 본문 없는 성공 응답을 주는 경우(BaseResponse)에 맞춰 선언된 것으로 보이며 문제 없습니다.


70-76: GET users 검색 API — LGTM

기본 size=30 지정과 쿼리 파라미터 구성이 명확합니다.

app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt (2)

24-26: TokenManager DI 추가 — LGTM

회원가입 플로우에서 임시 토큰 의존성을 Repository 수준으로 명확히 둔 점이 좋습니다.


83-88: 프로필 수정 API 위임 — LGTM

기존 패턴(handleBaseResponse → getOrThrow)과 일관되며 예외/성공 경로 처리가 명확합니다.

app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageEditViewModel.kt (1)

104-114: 확인 완료: null 값 미포함 설정 적용되어 있습니다
NetworkModule.provideJson()에서 explicitNulls = false로 설정되어 있어 nickname = null일 경우 JSON에 해당 필드가 전송되지 않습니다. 의도한 대로 동작하므로 추가 조치가 필요 없습니다.

app/src/main/java/com/texthip/thip/ui/feed/viewmodel/SearchPeopleViewModel.kt (1)

32-34: StateFlow 캡슐화 패턴 깔끔합니다

MutableStateFlow를 private로 감추고 asStateFlow로 노출한 점 좋습니다. 이후 테스트/미들웨어 추가 시에도 유연합니다.

Comment on lines +6 to +10
@Serializable
data class SignupResponse(
@SerializedName("accessToken") val accessToken: String,
@SerializedName("userId") val userId: Long
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

직렬화 어노테이션 혼용 — 한 가지로 통일 권장

@Serializable(kotlinx)와 @SerializedName(Gson)을 동시에 사용하는 것은 혼동을 야기합니다. 현재 Retrofit 컨버터 구성이 어떤지에 따라 아래 둘 중 하나로 통일하세요.

  • 옵션 A) Gson만 사용 시: @Serializable 제거, Gson만 유지
-package com.texthip.thip.data.model.users.response
-
-import com.google.gson.annotations.SerializedName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class SignupResponse(
-    @SerializedName("accessToken") val accessToken: String,
-    @SerializedName("userId") val userId: Long
-)
+package com.texthip.thip.data.model.users.response
+
+import com.google.gson.annotations.SerializedName
+
+data class SignupResponse(
+    @SerializedName("accessToken") val accessToken: String,
+    @SerializedName("userId") val userId: Long
+)
  • 옵션 B) Kotlinx Serialization만 사용 시: @SerialName으로 전환, Gson 제거
-package com.texthip.thip.data.model.users.response
-
-import com.google.gson.annotations.SerializedName
-import kotlinx.serialization.Serializable
-
-@Serializable
-data class SignupResponse(
-    @SerializedName("accessToken") val accessToken: String,
-    @SerializedName("userId") val userId: Long
-)
+package com.texthip.thip.data.model.users.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SignupResponse(
+    @SerialName("accessToken") val accessToken: String,
+    @SerialName("userId") val userId: Long
+)

Retrofit 컨버터 구성 실사용 여부를 아래 스크립트로 확인해 주세요. 결과에 따라 A/B 중 하나를 적용하면 좋습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== addConverterFactory 사용 위치 ==="
rg -n -C3 --hidden 'addConverterFactory'

echo
echo "=== Gson 컨버터 사용 여부 ==="
rg -n -C3 --hidden 'GsonConverterFactory'

echo
echo "=== Kotlinx 컨버터 사용 여부 ==="
rg -n -C3 --hidden 'kotlinx\.serialization|KotlinxSerializationConverter'

echo
echo "=== 모델 어노테이션 사용 분포 ==="
rg -n --hidden '@Serializable|@SerializedName' app/src/main/java/com/texthip/thip/data/model -C2

Length of output: 943


Kotlinx Serialization 컨버터 사용 중 — @SerialName으로 통일 필요

네트워크 모듈에서 json.asConverterFactory(…)를 통해 GsonConverterFactory 없이 KotlinxSerializationConverter만 설정되어 있으므로, 해당 모델에서 Gson 어노테이션(@SerializedName)을 제거하고 Kotlinx 어노테이션(@SerialName)으로 전환하세요.

적용 위치:

  • app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt
@@ app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt
-import com.google.gson.annotations.SerializedName
-import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerialName

 @Serializable
 data class SignupResponse(
-    @SerializedName("accessToken") val accessToken: String,
-    @SerializedName("userId") val userId: Long
+    @SerialName("accessToken") val accessToken: String,
+    @SerialName("userId") val userId: Long
 )
📝 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
@Serializable
data class SignupResponse(
@SerializedName("accessToken") val accessToken: String,
@SerializedName("userId") val userId: Long
)
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
@Serializable
data class SignupResponse(
@SerialName("accessToken") val accessToken: String,
@SerialName("userId") val userId: Long
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/users/response/SignupResponse.kt
around lines 6 to 10, replace the Gson annotation usages with Kotlinx
Serialization ones: remove @SerializedName and annotate properties with
@SerialName("accessToken") and @SerialName("userId"); ensure the file imports
kotlinx.serialization.SerialName and retains @Serializable on the data class so
the kotlinx serialization converter can map JSON fields correctly.

Comment on lines +3 to +4
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Gson 주석과 kotlinx.serialization 혼용 — 한 가지 직렬화 전략으로 통일 권장.

현재 @serializable(kotlinx)와 @SerializedName(Gson)을 혼용 중입니다. Retrofit에서 GsonConverterFactory와 KotlinxSerializationConverterFactory를 함께 쓰면, 등록 순서에 따라 서로 다른 파서가 선택되어 예기치 않은 동작/버그가 발생할 수 있습니다. 해당 응답 모델을 Kotlinx로 사용할 계획이라면 @SerialName으로 통일하고 Gson 주석과 import를 제거해주세요. 반대로 Gson을 쓸 계획이면 @serializable를 제거하세요.

권장 변경(Kotlinx로 통일 시):

-import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 @Serializable
 data class UserItem(
-    @SerializedName("userId") val userId: Int,
-    @SerializedName("nickname") val nickname: String,
-    @SerializedName("profileImageUrl") val profileImageUrl: String?,
-    @SerializedName("aliasName") val aliasName: String,
-    @SerializedName("aliasColor") val aliasColor: String,
-    @SerializedName("followerCount") val followerCount: Int
+    @SerialName("userId") val userId: Long,
+    @SerialName("nickname") val nickname: String,
+    @SerialName("profileImageUrl") val profileImageUrl: String?,
+    @SerialName("aliasName") val aliasName: String,
+    @SerialName("aliasColor") val aliasColor: String,
+    @SerialName("followerCount") val followerCount: Int
 )

Retrofit 구성 시 ConverterFactory 순서도 확인이 필요합니다(Gson 제거 또는 Kotlinx 우선 등록).

📝 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
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
// app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserItem(
@SerialName("userId") val userId: Long,
@SerialName("nickname") val nickname: String,
@SerialName("profileImageUrl") val profileImageUrl: String?,
@SerialName("aliasName") val aliasName: String,
@SerialName("aliasColor") val aliasColor: String,
@SerialName("followerCount") val followerCount: Int
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
around lines 3-4, the file mixes Gson (@SerializedName) and
kotlinx.serialization (@Serializable) annotations causing ambiguous parser
selection; standardize to kotlinx.serialization by removing the Gson import and
any @SerializedName usages, replace them with @SerialName where needed, keep
@Serializable on the data class, and then verify Retrofit converter registration
removes GsonConverterFactory or registers the kotlinx converter with higher
priority.

Comment on lines +12 to +18
@SerializedName("userId") val userId: Int,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String?,
@SerializedName("aliasName") val aliasName: String,
@SerializedName("aliasColor") val aliasColor: String,
@SerializedName("followerCount") val followerCount: Int
) No newline at end of file
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🛠️ Refactor suggestion

🧩 Analysis chain

userId 타입 불일치(Int vs Long) — 도메인 전반 Long으로 일원화 권장.

SignupResponse에서 userId: Long으로 정의된 반면, 여기서는 Int입니다. 사용자 식별자는 장기적으로 Int 범위를 초과할 가능성이 있고, 타입 혼재는 변환/비교 시 미묘한 버그를 유발합니다. 전역적으로 Long으로 통일하는 것을 권장합니다.

다음 스크립트로 userId 타입 불일치 지점을 점검할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# userId 타입 선언 위치와 사용처 탐색
rg -n -C2 -P 'data\s+class\s+\w+\s*\(.*\buserId:\s*(Int|Long)' app | sed 's/^/DECL: /'
rg -n -C2 -P '\buserId\s*:\s*(Int|Long)\b' app | sed 's/^/TYPE: /'
rg -n -C2 -P '\buserId\b' app/src/main/java | sed 's/^/USE: /'

Length of output: 44260


userId 타입 불일치(Int vs Long) — 도메인 전반 Long으로 통일 필요

현재 아래 모델 및 DTO에서 userIdInt로 정의되어 있지만, 다른 곳은 모두 Long을 사용하고 있어 타입 혼재로 인한 변환·비교 버그 위험과 향후 32비트 범위 초과 가능성이 있습니다. 전역적으로 Long으로 일원화해 주세요.

수정이 필요한 주요 파일:

  • app/src/main/java/com/texthip/thip/data/model/users/response/UserSearchResponse.kt
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsUsersResponse.kt
  • app/src/main/java/com/texthip/thip/data/model/rooms/response/RoomsPostsResponse.kt

(필요 시 테스트·모킹용 클래스에서도 userId: IntuserId: Long으로 변경)

예시:

 data class UserItem(
-    @SerializedName("userId") val userId: Int,
+    @SerializedName("userId") val userId: Long,
     @SerializedName("nickname") val nickname: String,
     ...
 )

— 변경 후 앱 전반의 사용자 식별자 타입이 일관된지, 호출부·뷰모델에서 Long으로 사용되는지 확인해 주세요.

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

Comment on lines +58 to +75
@Composable
fun SearchPeopleContent(
uiState: SearchPeopleUiState,
onSearchTextChanged: (String) -> Unit,
onFinalSearch: (String) -> Unit,
onRecentSearchClick: (String) -> Unit,
onRecentSearchRemove: (String) -> Unit

) {

Box(
modifier = modifier.fillMaxSize()
Column(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.fillMaxSize()
) {
DefaultTopAppBar(
title = stringResource(R.string.search_user),
onLeftClick = {},
)
Column(
modifier = Modifier
.fillMaxSize()
) {
Spacer(modifier = Modifier.height(16.dp))

SearchBookTextField(
DefaultTopAppBar(
title = stringResource(R.string.search_user),
onLeftClick = {},
)
Spacer(modifier = Modifier.height(16.dp))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

상단 백 아이콘이 동작하지 않음(onLeftClick = {})

사용자가 뒤로가기를 눌러도 아무 동작이 없어 UX 결함입니다. 시스템 back dispatcher를 연결해 즉시 해결 가능합니다.

적용 제안(diff):

 @Composable
 fun SearchPeopleContent(
     uiState: SearchPeopleUiState,
     onSearchTextChanged: (String) -> Unit,
     onFinalSearch: (String) -> Unit,
     onRecentSearchClick: (String) -> Unit,
     onRecentSearchRemove: (String) -> Unit
 ) {
+    val backDispatcher = androidx.activity.compose.LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
@@
-        DefaultTopAppBar(
+        DefaultTopAppBar(
             title = stringResource(R.string.search_user),
-            onLeftClick = {},
+            onLeftClick = { backDispatcher?.onBackPressed() },
         )

권장 import:

import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/screen/SearchPeopleScreen.kt
around lines 58 to 75, the top-app-bar left/back icon handler is a no-op
(onLeftClick = {}) so the back button doesn't navigate; fix by wiring the
onLeftClick to the system back dispatcher: obtain the dispatcher via
LocalOnBackPressedDispatcherOwner.current and invoke its
onBackPressedDispatcher.onBackPressed() inside onLeftClick, and add the
recommended import (import
androidx.activity.compose.LocalOnBackPressedDispatcherOwner) at the top of the
file.

Comment on lines +45 to +49
searchJob = viewModelScope.launch {
delay(500L)
searchUsers(keyword = text, isFinalized = false)
}
} else {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

검색 취소/동시 호출 레이스로 인해 오래된 결과가 최신 결과를 덮어쓸 수 있음 — searchUsers를 suspend로 바꾸고 단일 Job으로 관리하세요

현재 searchUsers 내부에서 viewModelScope.launch를 다시 생성하고 있어(이중 launch), 외부에서 관리하는 searchJob 취소가 네트워크 호출에 전달되지 않습니다. 그 결과:

  • 빠른 타이핑/연속 검색 시 이전 호출이 취소되지 않고 동시 실행됨
  • 오래된 응답이 나중에 도착해 최신 쿼리의 결과를 덮어쓰는 레이스 발생
  • isLoading 토글이 응답 순서에 따라 틀어질 수 있음

해결책:

  • searchUsers를 suspend로 변경해 호출한 코루틴(searchJob)의 취소가 전파되도록 합니다.
  • onFinalSearch도 searchJob에서 실행해 최종 검색 역시 동일 취소/순서 보장이 되도록 통일합니다.
  • 요청 시작 시 errorMessage를 null로 초기화하고, 응답 반영 시 현재 쿼리와 일치할 때만 상태를 갱신(스테일 드롭)합니다.

아래 패치를 제안합니다.

@@
-    // 키보드의 '검색' 버튼이나 아이콘을 눌렀을 때 호출
-    fun onFinalSearch(query: String) {
-        searchJob?.cancel()
-        _uiState.update { it.copy(isSearched = true) }
-
-        if (query.isNotBlank()) {
-            addRecentSearch(query)
-            searchUsers(keyword = query, isFinalized = true)
-        } else {
-            _uiState.update { it.copy(searchResults = emptyList()) }
-        }
-    }
+    // 키보드의 '검색' 버튼이나 아이콘을 눌렀을 때 호출
+    fun onFinalSearch(query: String) {
+        searchJob?.cancel()
+        searchJob = viewModelScope.launch {
+            _uiState.update { it.copy(isSearched = true, searchText = query) }
+            if (query.isNotBlank()) {
+                addRecentSearch(query)
+                searchUsers(keyword = query, isFinalized = true)
+            } else {
+                _uiState.update { it.copy(searchResults = emptyList()) }
+            }
+        }
+    }
@@
-    // 실제 API를 호출하는 private 함수
-    private fun searchUsers(keyword: String, isFinalized: Boolean) {
-        viewModelScope.launch {
-            _uiState.update { it.copy(isLoading = true) }
-            userRepository.searchUsers(keyword, isFinalized)
-                .onSuccess { response ->
-                    val userList =
-                        response?.userList?.map { it.toMySubscriptionData() } ?: emptyList()
-                    _uiState.update { it.copy(isLoading = false, searchResults = userList) }
-                }
-                .onFailure { exception ->
-                    _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) }
-                }
-        }
-    }
+    // 실제 API를 호출하는 private suspend 함수 (호출자의 Job 취소가 전파됨)
+    private suspend fun searchUsers(keyword: String, isFinalized: Boolean) {
+        _uiState.update { it.copy(isLoading = true, errorMessage = null) }
+        userRepository.searchUsers(keyword, isFinalized)
+            .onSuccess { response ->
+                val userList = response?.userList?.map { it.toMySubscriptionData() } ?: emptyList()
+                // 중간 검색(debounce) 동안의 스테일 응답은 드롭
+                val shouldApply = isFinalized || keyword == uiState.value.searchText
+                if (shouldApply) {
+                    _uiState.update { it.copy(isLoading = false, searchResults = userList) }
+                }
+            }
+            .onFailure { exception ->
+                val shouldApply = isFinalized || keyword == uiState.value.searchText
+                if (shouldApply) {
+                    _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) }
+                }
+            }
+    }

이렇게 하면 단일 searchJob으로 모든 검색을 관리하므로, 취소 전파/순서 보장이 되고 UI가 과거 응답으로 오염되지 않습니다.

Also applies to: 56-66, 68-82

🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/feed/viewmodel/SearchPeopleViewModel.kt
around lines 45-49 (also apply same changes to 56-66 and 68-82), replace the
current pattern that launches a nested coroutine inside searchUsers with a
suspend searchUsers so cancellation propagates: make searchUsers a suspend
function and remove its internal viewModelScope.launch; ensure you cancel any
existing searchJob before assigning a new viewModelScope.launch for the
debounced search (so only one Job runs), run onFinalSearch inside that same
searchJob, set errorMessage = null at the start of each request, and when
applying results only update state if the current query equals the keyword
passed (drop stale responses); wrap the network call in try/catch/finally to set
isLoading properly and rethrow/capture cancellation so isLoading is consistent.

Comment on lines +111 to +114
val request = ProfileUpdateRequest(
nickname = nicknameToSend,
aliasName = selectedRole.genre
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

프로필 업데이트 요청 시 aliasName 잘못 전송 가능성

위 매핑 정정 이후에는 aliasName=selectedRole.role이 맞습니다. 현재는 genre를 사용하고 있습니다.

적용 제안(diff):

-            val request = ProfileUpdateRequest(
-                nickname = nicknameToSend,
-                aliasName = selectedRole.genre
-            )
+            val request = ProfileUpdateRequest(
+                nickname = nicknameToSend,
+                aliasName = selectedRole.role
+            )
📝 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
val request = ProfileUpdateRequest(
nickname = nicknameToSend,
aliasName = selectedRole.genre
)
val request = ProfileUpdateRequest(
nickname = nicknameToSend,
aliasName = selectedRole.role
)
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageEditViewModel.kt
around lines 111 to 114, the ProfileUpdateRequest currently uses aliasName =
selectedRole.genre but should send aliasName = selectedRole.role; update that
field mapping so aliasName uses selectedRole.role when constructing the
ProfileUpdateRequest.

Comment on lines +42 to +47
LaunchedEffect(uiState.isSignupSuccess) {
if (uiState.isSignupSuccess) {
onSignupSuccess()
}
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

isSignupSuccess 플래그 미소모 시 재네비게이션 가능성

LaunchedEffect는 key 값(true) 상태에서 화면 재입장 시 다시 실행될 수 있습니다. Nickname 화면과 동일하게 성공 이벤트를 소모(reset)하는 호출이 필요합니다(예: onSignupSuccessConsumed()).

아래처럼 후처리를 추가하는 것을 제안합니다(해당 ViewModel 메서드 구현 필요):

 LaunchedEffect(uiState.isSignupSuccess) {
     if (uiState.isSignupSuccess) {
         onSignupSuccess()
+        viewModel.onSignupSuccessConsumed()
     }
 }
📝 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
LaunchedEffect(uiState.isSignupSuccess) {
if (uiState.isSignupSuccess) {
onSignupSuccess()
}
}
LaunchedEffect(uiState.isSignupSuccess) {
if (uiState.isSignupSuccess) {
onSignupSuccess()
viewModel.onSignupSuccessConsumed()
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt
around lines 42–47, the LaunchedEffect reacts to uiState.isSignupSuccess but
does not consume/reset the success flag, which can cause re-navigation when the
composable re-enters; after calling onSignupSuccess(), call a ViewModel consumer
method (e.g., onSignupSuccessConsumed()) to reset the flag in the ViewModel, and
implement that method to clear the success state (or emit a one-shot event) so
the LaunchedEffect does not retrigger on recomposition or navigation back.

Comment on lines +69 to +79
fun fetchAliasChoices() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
userRepository.getAliasChoices()
.onSuccess { response ->
val roleCards = response?.aliasChoices?.map { RoleItem(it.aliasName, it.categoryName, it.imageUrl, it.aliasColor) } ?: emptyList()
_uiState.update { it.copy(isLoading = false, roleCards = roleCards) }
}
.onFailure { exception -> _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } }
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

RoleItem 매핑 필드가 뒤바뀜(카테고리/칭호 혼동) → UI 라벨 반전

RoleCard의 파라미터 의미는 genre=카테고리명, role=칭호(별칭)입니다. 현재 aliasChoices 매핑이 aliasName→genre, categoryName→role로 반대로 들어가 UI 표기가 뒤집힙니다.

적용 제안(diff):

-                    val roleCards = response?.aliasChoices?.map { RoleItem(it.aliasName, it.categoryName, it.imageUrl, it.aliasColor) } ?: emptyList()
+                    val roleCards = response?.aliasChoices
+                        ?.map { RoleItem(it.categoryName, it.aliasName, it.imageUrl, it.aliasColor) }
+                        ?: emptyList()
📝 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
fun fetchAliasChoices() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
userRepository.getAliasChoices()
.onSuccess { response ->
val roleCards = response?.aliasChoices?.map { RoleItem(it.aliasName, it.categoryName, it.imageUrl, it.aliasColor) } ?: emptyList()
_uiState.update { it.copy(isLoading = false, roleCards = roleCards) }
}
.onFailure { exception -> _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } }
}
}
fun fetchAliasChoices() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
userRepository.getAliasChoices()
.onSuccess { response ->
val roleCards = response?.aliasChoices
?.map { RoleItem(it.categoryName, it.aliasName, it.imageUrl, it.aliasColor) }
?: emptyList()
_uiState.update { it.copy(isLoading = false, roleCards = roleCards) }
}
.onFailure { exception ->
_uiState.update { it.copy(isLoading = false, errorMessage = exception.message) }
}
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt
around lines 69–79, the mapping of aliasChoices into RoleItem is reversed
(aliasName and categoryName are swapped), causing UI labels to be inverted;
change the mapping so RoleItem receives genre = categoryName and role =
aliasName (keep imageUrl and aliasColor as-is), i.e. construct
RoleItem(categoryName, aliasName, imageUrl, aliasColor) and update the _uiState
assignment accordingly.

Comment on lines +95 to +99
val request = SignupRequest(
nickname = currentState.nickname,
aliasName = selectedRole.genre
)

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

회원가입 요청 시 aliasName 필드에 잘못된 값 사용 가능성

위 매핑 수정 이후에는 요청에 aliasName=selectedRole.role을 사용해야 실제 서버가 기대하는 "칭호(별칭)"가 전송됩니다. 현재는 genre를 사용하고 있어 카테고리명이 전송될 수 있습니다.

적용 제안(diff):

-            val request = SignupRequest(
-                nickname = currentState.nickname,
-                aliasName = selectedRole.genre
-            )
+            val request = SignupRequest(
+                nickname = currentState.nickname,
+                aliasName = selectedRole.role
+            )
📝 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
val request = SignupRequest(
nickname = currentState.nickname,
aliasName = selectedRole.genre
)
val request = SignupRequest(
nickname = currentState.nickname,
aliasName = selectedRole.role
)
🤖 Prompt for AI Agents
In app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupViewModel.kt
around lines 95 to 99, the SignupRequest currently assigns aliasName =
selectedRole.genre which can send a category name instead of the server-expected
title/alias; change the mapping to aliasName = selectedRole.role so the correct
"칭호(별칭)" value is sent in the request and update any related variable names or
tests to reflect this corrected field.

retrofit = "3.0.0"
retrofitKotlinSerializationConverter = "1.0.0"
androidxComposeNavigation = "2.8.2"
datastorePreferences = "1.1.7"
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DataStore 버전 유효성 확인 필요

datastorePreferences = "1.1.7"은 비교적 이례적으로 높은 마이너 버전입니다. 실제 배포 버전인지 확인해 주세요. 존재하지 않는 버전을 지정하면 동기화/빌드가 실패합니다.

다음 스크립트를 실행해 Maven Central에 배포된 버전 목록에서 1.1.7 존재 여부를 확인해 주세요:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# androidx.datastore:datastore-preferences 버전 목록 조회
curl -s 'https://search.maven.org/solrsearch/select?q=g:androidx.datastore+AND+a:datastore-preferences&rows=200&wt=json' \
| jq -r '.response.docs[].v' | sort -V

# 특정 버전 존재 여부 확인
TARGET="1.1.7"
if curl -sfI "https://repo1.maven.org/maven2/androidx/datastore/datastore-preferences/${TARGET}/datastore-preferences-${TARGET}.pom" >/dev/null; then
  echo "FOUND: $TARGET"
else
  echo "NOT FOUND: $TARGET"
fi

Length of output: 361


datastore-preferences 버전 존재하지 않음 – 유효한 버전으로 수정 필요

Maven Central에 1.1.7 버전이 존재하지 않아 현재 빌드/동기화가 실패합니다. 실제 배포된 최신 버전을 확인한 뒤 gradle/libs.versions.toml의 해당 항목을 올바른 버전으로 수정해주세요.

  • File: gradle/libs.versions.toml, Line 28
    - datastorePreferences = "1.1.7"
    + datastorePreferences = "<실제_배포된_버전>"
📝 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
datastorePreferences = "1.1.7"
datastorePreferences = "<실제_배포된_버전>"
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 28, the datastorePreferences entry is
set to the non-existent version "1.1.7"; check Maven Central (or the official
artifact repo) for the actual published latest version of
androidx.datastore:datastore-preferences (or matching artifact used in the
project) and update the value on line 28 to that valid version string, then
re-sync the Gradle project to verify the build succeeds.

@rbqks529 rbqks529 merged commit fb73fa4 into THIP-TextHip:develop Aug 18, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API] User 도메인 API 연결 [(THIP2025-229)]

3 participants