Skip to content

Conversation

@seungjunGong
Copy link
Collaborator

@seungjunGong seungjunGong commented Dec 31, 2025

Related issue 🛠

Work Description ✏️

  • navigation 세팅
  • 네트워크 세팅
  • 유틸, 확장 함수 추가

Screenshot 📸

Screen_recording_20251231_183227.mp4

Uncompleted Tasks 😅

  • N/A

To Reviewers 📢

질문 or 반박 환영입니다. 전체적인 구조에 대해서 봐주시면 좋을거 같습니다.
유틸 함수들 필요할만한거 추가해서 kdoc 작성해놓았는데 활용하시면 됩니다 !!

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 사용자 목록을 표시하는 홈 화면 추가
    • 탭 기반 하단 네비게이션 바 구현
    • URL에서 이미지를 로드하는 기능 추가
    • 데이트 라이트 모드 적용
  • 개선 사항

    • 로깅 시스템 강화
    • 프로젝트 구조 및 테마 체계 개선
    • 네트워크 요청 인프라 구축

✏️ Tip: You can customize this high-level summary in your review settings.

@seungjunGong seungjunGong added the ➕ ADD 부수적인 코드 추가 및 라이브러리 추가, 새로운 파일 생성 label Dec 31, 2025
@seungjunGong
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Dec 31, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

안내

Walkthrough

프로젝트의 초기 설정을 구성합니다. 클린 아키텍처 기반의 계층 구조를 수립하고, Hilt 의존성 주입, Retrofit 네트워킹, DataStore 기반 토큰 관리, Jetpack Compose UI, 그리고 타입-안전 네비게이션을 통합합니다.

Changes

Cohort / File(s) 변경 사항 요약
빌드 구성 및 버전 카탈로그
build.gradle.kts, app/build.gradle.kts, gradle/libs.versions.toml
Kotlin 2.3.0, Java 17로 업그레이드. Hilt, KSP, serialization, Parcelize 플러그인 추가. 의존성을 번들로 재정리하고 Compose BOM, Retrofit, Coil, Timber, Lottie 통합. BuildConfig에 BASE_URL 동적 로드 기능 추가.
애플리케이션 클래스 및 매니페스트
app/src/main/AndroidManifest.xml, app/src/main/java/com/smashing/app/SmashingApplication.kt
커스텀 SmashingApplication 클래스 도입. Hilt @HiltAndroidApp 애너테이션 적용. onCreate에서 Timber 로깅 및 day mode 강제 설정. 인터넷 권한 추가, cleartext 트래픽 허용, MainActivity 경로를 .presentation.main.MainActivity로 업데이트.
네비게이션 인프라
app/src/main/java/com/smashing/app/core/common/navigation/*, app/src/main/java/com/smashing/app/presentation/main/MainTab.kt, app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt
Route, MainTabRoute 기본 인터페이스 추가. MainTab 열거형으로 HOME, DUMMY 탭 정의. MainAppState 클래스로 탭 네비게이션 및 하단 바 가시성 관리. 타입-안전 직렬화 기반 라우팅.
상태 관리 및 공통 유틸리티
app/src/main/java/com/smashing/app/core/common/state/UiState.kt, app/src/main/java/com/smashing/app/core/util/SuspendRunCatching.kt, app/src/main/java/com/smashing/app/core/extension/*
밀폐형 UiState (Idle, Loading, Success, Failure)로 비동기 상태 모델링. suspendRunCatching로 안전한 예외 처리. Context.openUrl, Flow.stateInWhileSubscribed, LazyListState.onBottomReached, String.checkLength 등의 확장 함수 추가.
디자인 시스템
app/src/main/java/com/smashing/app/core/designsystem/*, app/src/main/res/drawable/ic_fake_red.xml, app/src/main/res/values/strings.xml
테마 파일을 com.smashing.app.ui.theme에서 com.smashing.app.core.designsystem.theme으로 재구성. UrlImage 컴포넌트로 Coil 기반 원격 이미지 로딩 구현. "더미", "홈" 문자열 리소스 추가.
네트워킹 및 DI 설정
app/src/main/java/com/smashing/app/core/network/*, app/src/main/java/com/smashing/app/core/local/datastore/*
Retrofit, OkHttp, Kotlinx Json 설정. JSON 기반 HTTP 로깅 인터셉터. DataStore Preferences 기반 토큰 저장소 (TokenDataStore 한정자). 모든 네트워크 및 로컬 저장소 DI 바인딩.
데이터 계층: 모델 및 원격 API
app/src/main/java/com/smashing/app/data/model/DummyUser.kt, app/src/main/java/com/smashing/app/data/remote/*
GetUserListResponse, UserData, Support 데이터 클래스. @Serializable 애너테이션과 @SerialName 필드 매핑. DummyService Retrofit 인터페이스 (/api/users GET 엔드포인트). DummyDataSource 인터페이스 및 DummyDataSourceImpl 구현. DummyMapper로 응답을 DummyUser로 변환.
데이터 계층: 로컬 저장소 및 저장소
app/src/main/java/com/smashing/app/data/local/datasource/*, app/src/main/java/com/smashing/app/data/repository/*
LocalTokenDataSource 인터페이스 및 DataStore 기반 구현. 접근/갱신 토큰 관리. DummyRepository 인터페이스 및 suspendRunCatching 기반 Result 래핑 구현.
데이터 계층: DI 모듈
app/src/main/java/com/smashing/app/data/di/*
AuthDataSourceModule, DummyDataSourceModule, DummyRepositoryModule, DummyServiceModule로 모든 데이터 계층 바인딩 정의. 싱글톤 범위 지정.
프레젠테이션 계층: 메인 화면
app/src/main/java/com/smashing/app/presentation/main/*
MainActivity (Edge-to-Edge, Hilt @AndroidEntryPoint). MainScreen 스캐폴드와 하단 네비게이션. MainBottomBar 컴포넌트로 탭 표시. 네비게이션 상태 및 애니메이션 제어.
프레젠테이션 계층: 홈 화면
app/src/main/java/com/smashing/app/presentation/home/*, app/src/main/java/com/smashing/app/presentation/dummy/HomeNavigation.kt
HomeContract 상태 정의 (dummyUsersLoadState: UiState<ImmutableList>). HomeViewModel (fetchDummyUsers 메서드로 저장소 위임). HomeScreen 컴포지션 (UiState 분기 처리). DummyUserItem 컴포넌트 (UrlImage, 이름, 이메일). HomeNavigation 및 DummyNavigation 라우팅 헬퍼.
제거된 파일
app/src/main/java/com/smashing/app/MainActivity.kt
기존 보일러플레이트 MainActivity, Greeting, GreetingPreview 컴포저블 삭제.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 프로젝트 기초 설정 작업의 주요 변경사항을 명확하게 요약하고 있으며, 이슈 번호를 포함하여 작업 추적성이 좋습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고 있으나, 스크린샷 링크는 작동하지 않으며 기술적 세부사항이 다소 부족합니다.
Linked Issues check ✅ Passed PR에서 구현한 기초 설정(네비게이션, 네트워크, 유틸 함수)이 이슈 #1의 기술 스택 기반 초기 세팅 요구사항을 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 기초 설정 요구사항 범위 내에 있으며, 의도한 범위를 벗어난 변경은 없습니다.

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 9

🧹 Nitpick comments (21)
app/src/main/java/com/smashing/app/core/extension/StringExt.kt (2)

13-13: 함수 이름을 더 명확하게 개선하는 것을 고려하세요.

checkLength()는 검증(validation)을 수행하는 것처럼 보이지만, 실제로는 문자 개수를 반환합니다. 더 명확한 이름을 고려해보세요:

  • countGraphemes(): 그래핌 클러스터 개수를 센다는 의미
  • graphemeLength(): 그래핌 기반 길이를 반환
  • perceptualLength(): 사용자가 인식하는 길이

이렇게 하면 API의 의도가 더 명확해집니다.


13-24: checkLength() 함수는 현재 코드베이스에서 사용되지 않고 있습니다.

구현 로직은 정확합니다. BreakIterator.getCharacterInstance()를 사용한 그래핌 클러스터 카운팅이 올바르게 구현되어 있으며, 이모지와 결합 문자를 정확히 처리합니다.

다만 검색 결과 이 유틸리티 함수는 현재 코드베이스의 어디에서도 호출되지 않고 있습니다. 미사용 코드는 제거하거나 실제 사용 사례를 추가해야 합니다.

app/src/main/java/com/smashing/app/data/local/datasource/impl/TokenDatasourceImpl.kt (1)

45-48: Companion 객체 이름 중복 제거 권장

companion object Companion에서 Companion은 기본 이름이므로 명시할 필요가 없습니다. companion object만 사용하면 더 간결합니다.

🔎 제안하는 수정
-    companion object Companion {
+    companion object {
         private val ACCESS_TOKEN = stringPreferencesKey("ACCESS_TOKEN")
         private val REFRESH_TOKEN = stringPreferencesKey("REFRESH_TOKEN")
     }
app/src/main/java/com/smashing/app/core/extension/ModifierExt.kt (1)

16-22: 리플 제거 로직이 올바르게 구현되었습니다.

indication = nullMutableInteractionSource를 사용하여 리플 효과를 제거하는 구현이 정확합니다. KDoc도 명확하게 작성되었습니다.

composed를 사용하면 매 리컴포지션마다 새로운 Modifier가 할당되는데, 다음과 같이 최적화를 고려할 수 있습니다:

🔎 성능 최적화를 위한 대안
-fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed {
-    clickable(
+fun Modifier.noRippleClickable(onClick: () -> Unit): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "noRippleClickable"
+        properties["onClick"] = onClick
+    }
+) {
+    this.clickable(
         indication = null,
         interactionSource = remember { MutableInteractionSource() },
         onClick = onClick,
     )
 }

또는 싱글톤 MutableInteractionSource를 모듈 레벨에서 공유하여 composed 없이 사용할 수도 있지만, 이는 동시 인터랙션 추적에 영향을 줄 수 있어 현재 구현이 더 안전합니다.

app/src/main/java/com/smashing/app/core/network/RetrofitExt.kt (1)

3-5: JSON 형식 감지 로직이 구현되었습니다.

네트워크 로깅을 위한 JSON 형식 감지 함수가 추가되었습니다. 현재 구현은 시작/종료 문자만 확인하므로 실제 JSON 구조를 검증하지는 않습니다.

로깅 목적으로는 충분하지만, 향후 더 정확한 감지가 필요한 경우 다음을 고려할 수 있습니다:

🔎 개선 제안
fun String?.isJsonObject(): Boolean {
    if (this == null) return false
    val trimmed = this.trim()
    return trimmed.startsWith("{") && trimmed.endsWith("}") && trimmed.length >= 2
}

fun String?.isJsonArray(): Boolean {
    if (this == null) return false
    val trimmed = this.trim()
    return trimmed.startsWith("[") && trimmed.endsWith("]") && trimmed.length >= 2
}

또는 실제 JSON 파싱을 시도하여 유효성을 확인할 수도 있지만, 로깅 성능에 영향을 줄 수 있어 현재 구현이 실용적입니다.

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

4-4: TODO 주석이 있습니다.

dummy 문자열 리소스에 "삭제 예정" TODO 주석이 있습니다. 이 항목을 추적하고 적절한 시점에 제거하는 것을 잊지 마세요.

이슈를 생성하여 추적하거나 제거 작업을 도와드릴까요?

app/src/main/java/com/smashing/app/core/extension/ContextExt.kt (1)

13-17: URL 검증 및 에러 로깅 추가를 권장합니다.

현재 구현은 다음 문제가 있습니다:

  • URL 검증 없이 Intent를 생성하므로, 사용자 입력이 전달될 경우 보안 문제가 될 수 있습니다
  • 실패 시 로깅이 없어 디버깅이 어렵습니다
🔎 URL 검증 및 로깅 추가 제안
+import android.util.Log
+
 /**
  * 주어진 URL을 기본 브라우저로 엽니다.
  *
  * @param url URL 문자열
  * @return 성공 시 true, 실패 시 false
  */
 fun Context.openUrl(url: String): Boolean {
     return runCatching {
+        val uri = url.toUri()
+        if (uri.scheme !in listOf("http", "https")) {
+            Log.w("ContextExt", "Invalid URL scheme: ${uri.scheme}")
+            return false
+        }
-        startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
+        startActivity(Intent(Intent.ACTION_VIEW, uri))
+    }.onFailure { e ->
+        Log.e("ContextExt", "Failed to open URL: $url", e)
     }.isSuccess
 }
app/src/main/java/com/smashing/app/presentation/home/component/DummyItem.kt (2)

44-53: 테마 색상 사용을 권장합니다.

Color.Gray를 직접 사용하면 다크 모드나 커스텀 테마에서 일관성이 떨어질 수 있습니다.

🔎 MaterialTheme 색상 사용 제안
+import androidx.compose.material3.MaterialTheme
+
             Text(
                 text = email,
                 fontSize = 10.sp,
-                color = Color.Gray,
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
             )

31-36: 접근성 개선을 고려해주세요.

프로필 이미지에 contentDescription을 추가하면 스크린 리더 사용자에게 도움이 됩니다.

🔎 contentDescription 추가 제안
         UrlImage(
             url = profileImage,
+            contentDescription = "$firstName $lastName의 프로필 이미지",
             modifier = Modifier
                 .weight(1f)
                 .aspectRatio(1f),
         )
app/src/main/java/com/smashing/app/presentation/main/component/MainBottomBar.kt (1)

73-96: isSelected 파라미터 활용 필요

isSelected 파라미터가 전달되지만 시각적 구분에 활용되지 않고 있습니다. TODO 주석에 따라 디자인 적용 시 선택 상태에 따른 아이콘/텍스트 색상 변경이 필요합니다.

app/src/main/java/com/smashing/app/core/designsystem/component/UrlImage.kt (2)

42-48: 로딩 및 에러 상태 처리 추가 권장

AsyncImageplaceholder, error, fallback 파라미터가 설정되지 않아 이미지 로딩 중이거나 실패 시 빈 공간으로 표시됩니다. 사용자 경험 개선을 위해 상태별 이미지 표시를 권장합니다.

🔎 수정 제안
     } else {
         AsyncImage(
             model = url,
             contentDescription = contentDescription,
             contentScale = contentScale,
             modifier = modifier,
+            placeholder = painterResource(ic_fake_red),
+            error = painterResource(ic_fake_red),
         )
     }

painterResource import 추가 필요:

import androidx.compose.ui.res.painterResource

51-58: Preview 함수 접근 제한자 추가 권장

Preview 함수는 외부에서 사용되지 않으므로 private으로 선언하는 것이 좋습니다.

🔎 수정 제안
 @Preview
 @Composable
-fun UrlImagePreview() {
+private fun UrlImagePreview() {
     UrlImage(
app/src/main/java/com/smashing/app/presentation/main/MainTab.kt (1)

46-48: contains 함수 간소화 가능

mapany를 호출하는 대신 직접 any를 사용하면 중간 컬렉션 생성을 피할 수 있습니다.

🔎 수정 제안
         fun contains(predicate: (Route) -> Boolean): Boolean {
-            return entries.map { it.route }.any { predicate(it) }
+            return entries.any { predicate(it.route) }
         }
app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt (1)

80-85: DUMMY1, DUMMY2 탭 네비게이션 미구현

DUMMY1DUMMY2 탭 클릭 시 아무 동작도 하지 않습니다. 사용자가 해당 탭을 클릭해도 반응이 없어 혼란을 줄 수 있습니다. TODO 주석에 따라 추후 구현 예정이라면 해당 탭을 임시로 숨기거나, 최소한 로그를 남기는 것을 권장합니다.

네비게이션 함수 구현이나 해당 탭 비활성화 처리를 도와드릴까요?

app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt (2)

32-36: 프로덕션에서 prettyPrint 비활성화 권장

prettyPrint = true는 JSON 직렬화 시 추가 오버헤드를 발생시킵니다. 디버그 빌드에서만 활성화하는 것을 권장합니다.

🔎 수정 제안
     @Provides
     @Singleton
     fun provideJson(): Json = Json {
         encodeDefaults = true
         ignoreUnknownKeys = true
-        prettyPrint = true
+        prettyPrint = BuildConfig.DEBUG
     }

65-71: OkHttpClient 타임아웃 설정 누락

타임아웃 설정이 없으면 네트워크 지연이나 장애 시 무한 대기가 발생할 수 있습니다. connectTimeout, readTimeout, writeTimeout 설정을 권장합니다.

🔎 수정 제안
+import java.util.concurrent.TimeUnit
+
     @Provides
     @Singleton
     fun provideOkHttpClient(
         loggingInterceptor: Interceptor,
     ): OkHttpClient = OkHttpClient.Builder()
         .addInterceptor(loggingInterceptor)
+        .connectTimeout(30, TimeUnit.SECONDS)
+        .readTimeout(30, TimeUnit.SECONDS)
+        .writeTimeout(30, TimeUnit.SECONDS)
         .build()
gradle/libs.versions.toml (1)

36-36: 오타 수정: "Thrid party" → "Third party"

🔎 수정 제안
-# Thrid party
+# Third party
app/src/main/java/com/smashing/app/data/mapper/DummyMapper.kt (1)

6-12: 매퍼 함수가 잘 구성되어 있습니다.

null-safe 처리를 orEmpty() 및 기본값으로 적절히 처리하고 있습니다.

다만, 실제 프로덕션 코드로 전환 시 id가 null인 경우를 0으로 처리하는 것이 비즈니스 로직에 적합한지 검토가 필요할 수 있습니다. 현재 Dummy 구현 단계에서는 문제없습니다.

app/src/main/java/com/smashing/app/presentation/main/MainScreen.kt (1)

51-58: 화면 전환 애니메이션 의도 확인

모든 화면 전환 애니메이션이 None으로 설정되어 있습니다. 이것이 의도된 디자인인지 확인하시기 바랍니다. 향후 부드러운 사용자 경험을 위해 전환 애니메이션 추가를 고려할 수 있습니다.

app/src/main/java/com/smashing/app/presentation/home/HomeScreen.kt (1)

44-63: 빈 상태 화면 구현 필요

UiState의 모든 분기를 올바르게 처리하고 있습니다. 다만 Idle, Failure, Loading 상태에 대한 UI 구현이 아직 되어있지 않습니다. 향후 사용자 경험 향상을 위해 이들 상태에 대한 화면 구현을 추가하시기 바랍니다.

app/src/main/java/com/smashing/app/presentation/home/HomeViewModel.kt (1)

24-34: 빈 리스트 상태 처리 재검토 제안

데이터 로딩 로직이 전반적으로 잘 구현되어 있습니다. 다만 Lines 26-30에서 빈 리스트를 UiState.Idle로 처리하는 것은 재고해볼 여지가 있습니다.

현재 로직:

  • 빈 리스트 → Idle (데이터 로딩 전 상태와 동일)
  • 데이터 있음 → Success

이렇게 하면 "아직 로딩하지 않음"과 "로딩했지만 결과가 비어있음"을 구분할 수 없습니다. Success(emptyList)로 처리하면 의미가 더 명확해집니다.

🔎 제안 수정
 fun fetchDummyUsers() = viewModelScope.launch {
+    updateDummyUiState(UiState.Loading)
     dummyRepository.fetchDummyUserList(page = 1).onSuccess { userList ->
-        if (userList.isNotEmpty()) {
-            updateDummyUiState(UiState.Success(userList.toImmutableList()))
-        } else {
-            updateDummyUiState(UiState.Idle)
-        }
+        updateDummyUiState(UiState.Success(userList.toImmutableList()))
     }.onFailure {
         updateDummyUiState(UiState.Failure(it.message ?: "Unknown Error"))
     }
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7faa5dd and 90021f5.

📒 Files selected for processing (57)
  • .github/auto_assign.yml
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/smashing/app/MainActivity.kt
  • app/src/main/java/com/smashing/app/SmashingApplication.kt
  • app/src/main/java/com/smashing/app/core/common/constant/.gitkeep
  • app/src/main/java/com/smashing/app/core/common/navigation/MainTabRoute.kt
  • app/src/main/java/com/smashing/app/core/common/navigation/Route.kt
  • app/src/main/java/com/smashing/app/core/common/state/UiState.kt
  • app/src/main/java/com/smashing/app/core/common/type/.gitkeep
  • app/src/main/java/com/smashing/app/core/designsystem/component/UrlImage.kt
  • app/src/main/java/com/smashing/app/core/designsystem/theme/Color.kt
  • app/src/main/java/com/smashing/app/core/designsystem/theme/Theme.kt
  • app/src/main/java/com/smashing/app/core/designsystem/theme/Type.kt
  • app/src/main/java/com/smashing/app/core/extension/ContextExt.kt
  • app/src/main/java/com/smashing/app/core/extension/FlowExt.kt
  • app/src/main/java/com/smashing/app/core/extension/LazyListStateExt.kt
  • app/src/main/java/com/smashing/app/core/extension/ModifierExt.kt
  • app/src/main/java/com/smashing/app/core/extension/StringExt.kt
  • app/src/main/java/com/smashing/app/core/local/datastore/di/DataStoreModule.kt
  • app/src/main/java/com/smashing/app/core/local/datastore/di/DataStoreQualifier.kt
  • app/src/main/java/com/smashing/app/core/network/RetrofitExt.kt
  • app/src/main/java/com/smashing/app/core/network/di/NetworkModule.kt
  • app/src/main/java/com/smashing/app/core/util/SuspendRunCatching.kt
  • app/src/main/java/com/smashing/app/data/di/auth/AuthDataSourceModule.kt
  • app/src/main/java/com/smashing/app/data/di/dummy/DummyDataSourceModule.kt
  • app/src/main/java/com/smashing/app/data/di/dummy/DummyRepositoryModule.kt
  • app/src/main/java/com/smashing/app/data/di/dummy/DummyServiceModule.kt
  • app/src/main/java/com/smashing/app/data/local/datasource/api/TokenDatasource.kt
  • app/src/main/java/com/smashing/app/data/local/datasource/impl/TokenDatasourceImpl.kt
  • app/src/main/java/com/smashing/app/data/mapper/DummyMapper.kt
  • app/src/main/java/com/smashing/app/data/model/DummyUser.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/api/DummyDataSource.kt
  • app/src/main/java/com/smashing/app/data/remote/datasource/impl/DummyDataSourceImpl.kt
  • app/src/main/java/com/smashing/app/data/remote/dto/GetUserListResponse.kt
  • app/src/main/java/com/smashing/app/data/remote/service/DummyService.kt
  • app/src/main/java/com/smashing/app/data/repository/api/DummyRepository.kt
  • app/src/main/java/com/smashing/app/data/repository/impl/DummyRepositoryImpl.kt
  • app/src/main/java/com/smashing/app/domain/mapper/.gitkeep
  • app/src/main/java/com/smashing/app/domain/model/.gitkeep
  • app/src/main/java/com/smashing/app/domain/usecase/.gitkeep
  • app/src/main/java/com/smashing/app/presentation/dummy/HomeNavigation.kt
  • app/src/main/java/com/smashing/app/presentation/home/HomeContract.kt
  • app/src/main/java/com/smashing/app/presentation/home/HomeScreen.kt
  • app/src/main/java/com/smashing/app/presentation/home/HomeViewModel.kt
  • app/src/main/java/com/smashing/app/presentation/home/component/DummyItem.kt
  • app/src/main/java/com/smashing/app/presentation/home/navigation/HomeNavigation.kt
  • app/src/main/java/com/smashing/app/presentation/home/type/.gitkeep
  • app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainScreen.kt
  • app/src/main/java/com/smashing/app/presentation/main/MainTab.kt
  • app/src/main/java/com/smashing/app/presentation/main/component/MainBottomBar.kt
  • app/src/main/res/drawable/ic_fake_red.xml
  • app/src/main/res/values/strings.xml
  • build.gradle.kts
  • gradle/libs.versions.toml
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/smashing/app/MainActivity.kt
🧰 Additional context used
🧬 Code graph analysis (7)
app/src/main/java/com/smashing/app/presentation/dummy/HomeNavigation.kt (1)
app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt (1)
  • navigate (68-86)
app/src/main/java/com/smashing/app/presentation/home/navigation/HomeNavigation.kt (2)
app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt (1)
  • navigate (68-86)
app/src/main/java/com/smashing/app/presentation/home/HomeScreen.kt (1)
  • HomeRoute (20-34)
app/src/main/java/com/smashing/app/presentation/home/component/DummyItem.kt (1)
app/src/main/java/com/smashing/app/core/designsystem/component/UrlImage.kt (1)
  • UrlImage (27-49)
app/src/main/java/com/smashing/app/presentation/home/HomeScreen.kt (1)
app/src/main/java/com/smashing/app/presentation/home/component/DummyItem.kt (1)
  • DummyUserItem (19-56)
app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt (3)
app/src/main/java/com/smashing/app/core/designsystem/theme/Theme.kt (1)
  • SmashingAndroidTheme (35-57)
app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt (1)
  • rememberMainAppState (93-99)
app/src/main/java/com/smashing/app/presentation/main/MainScreen.kt (1)
  • MainScreen (19-44)
app/src/main/java/com/smashing/app/presentation/main/MainScreen.kt (3)
app/src/main/java/com/smashing/app/presentation/main/component/MainBottomBar.kt (1)
  • MainBottomBar (32-71)
app/src/main/java/com/smashing/app/presentation/home/navigation/HomeNavigation.kt (1)
  • homeGraph (18-26)
app/src/main/java/com/smashing/app/presentation/dummy/HomeNavigation.kt (1)
  • dummyGraph (22-38)
app/src/main/java/com/smashing/app/data/repository/impl/DummyRepositoryImpl.kt (1)
app/src/main/java/com/smashing/app/core/util/SuspendRunCatching.kt (1)
  • suspendRunCatching (18-29)
🔇 Additional comments (44)
app/src/main/res/drawable/ic_fake_red.xml (1)

1-9: 정상적으로 구현된 드로어블 리소스입니다.

벡터 드로어블 정의가 올바르고, 40×40 dp 빨간색 정사각형 아이콘이 명확하게 지정되어 있습니다. XML 구문도 정확하고 뷰포트 크기가 치수와 일치합니다.

.github/auto_assign.yml (1)

5-5: 이 구성은 유효합니다.

addAssignees: author는 kentaro-m/auto-assign-action에서 공식적으로 지원하는 유효한 값입니다. 이 설정을 사용하면 PR 작성자가 자동으로 assignee로 추가됩니다.

app/src/main/java/com/smashing/app/core/extension/StringExt.kt (1)

5-12: KDoc 문서화가 잘 되어 있습니다.

사용자가 인식하는 문자 개수를 반환한다는 설명이 명확합니다. 이모지와 결합 문자 처리에 대한 설명도 포함되어 있어 좋습니다.

app/src/main/java/com/smashing/app/core/local/datastore/di/DataStoreQualifier.kt (1)

1-7: LGTM!

Dagger Hilt qualifier 어노테이션이 올바르게 정의되었습니다. @QualifierBINARY retention을 사용한 표준 패턴입니다.

app/src/main/java/com/smashing/app/core/local/datastore/di/DataStoreModule.kt (1)

13-25: LGTM!

DataStore 모듈이 올바르게 구성되었습니다. preferencesDataStore delegate를 사용한 표준 패턴이며, @TokenDataStore qualifier를 통해 DI 그래프에서 구분할 수 있습니다.

app/src/main/java/com/smashing/app/data/di/auth/AuthDataSourceModule.kt (1)

11-20: 모듈 구조는 올바르나, 구현체의 생성자 어노테이션 확인 필요

@Binds 패턴을 사용한 표준 DI 바인딩입니다. 다만 @Binds는 구현체가 @Inject 생성자를 가져야 Dagger가 인스턴스를 생성할 수 있습니다. LocalTokenDataSourceImpl의 생성자에 @Inject 어노테이션이 누락된 것으로 보입니다 (별도 코멘트 참조).

app/src/main/java/com/smashing/app/data/local/datasource/api/TokenDatasource.kt (1)

3-9: LGTM!

토큰 관리를 위한 명확한 인터페이스 계약입니다. 모든 메서드가 DataStore의 비동기 특성에 맞게 suspend 함수로 정의되어 있습니다.

app/src/main/java/com/smashing/app/data/local/datasource/impl/TokenDatasourceImpl.kt (3)

16-24: LGTM!

토큰 읽기 로직이 올바르게 구현되었습니다. DataStore의 data Flow를 map하여 값을 추출하고 firstOrNull()로 현재 값을 가져오는 표준 패턴입니다.


26-36: LGTM!

토큰 저장 로직이 올바르게 구현되었습니다. dataStore.edit를 사용하여 Preferences를 안전하게 업데이트하는 표준 패턴입니다.


38-43: LGTM!

토큰 삭제 로직이 올바르게 구현되었습니다. remove()를 사용하여 두 토큰을 모두 제거합니다.

app/src/main/java/com/smashing/app/core/extension/FlowExt.kt (1)

9-22: 훌륭한 KDoc 문서화입니다!

문서가 매우 상세하며 다음 내용을 잘 설명하고 있습니다:

  • WhileSubscribed 전략의 동작 방식
  • 기본 timeout 값의 근거 (Configuration change 대응)
  • 모든 파라미터와 반환값에 대한 명확한 설명

개발자가 이 함수의 목적과 사용법을 쉽게 이해할 수 있습니다.

app/src/main/java/com/smashing/app/core/common/navigation/Route.kt (1)

1-3: 네비게이션 계층 구조의 기반이 잘 설정되었습니다.

마커 인터페이스 패턴을 사용한 타입-안전 네비게이션 구조가 적절합니다. MainTabRoute 및 향후 다른 라우트 타입들의 베이스로 활용될 수 있습니다.

app/src/main/java/com/smashing/app/core/common/navigation/MainTabRoute.kt (1)

1-3: 메인 탭 라우트 타입이 올바르게 정의되었습니다.

Route를 확장하여 메인 탭 전용 네비게이션 타입을 분리한 구조가 적절합니다. HomeDummy 네비게이션에서 일관되게 사용될 수 있습니다.

app/src/main/java/com/smashing/app/core/designsystem/theme/Color.kt (1)

1-1: 패키지 구조 개선이 적절합니다.

테마 관련 파일을 core.designsystem.theme 패키지로 이동하여 디자인 시스템 모듈화가 잘 이루어졌습니다. 컬러 값의 기능적 변경은 없습니다.

Also applies to: 11-11

app/src/main/java/com/smashing/app/core/designsystem/theme/Type.kt (1)

1-1: 타이포그래피 패키지 재구성이 올바릅니다.

core.designsystem.theme 패키지로의 이동이 다른 테마 파일들과 일관성 있게 적용되었습니다. Material 3 Typography 설정에 변경사항이 없습니다.

Also applies to: 34-34

app/src/main/java/com/smashing/app/data/model/DummyUser.kt (1)

1-9: 데이터 모델이 명확하게 정의되었습니다.

불변(immutable) 속성들로 구성된 깔끔한 데이터 클래스입니다. 프로퍼티 타입이 적절하며, 데이터/도메인/프레젠테이션 레이어에서 일관되게 사용될 수 있는 구조입니다.

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

3-6: 문자열 리소스가 올바르게 추가되었습니다.

메인 탭에 사용될 한글 문자열 리소스(, 더미)가 적절하게 정의되었습니다. 주석을 통한 섹션 구분도 명확합니다.

build.gradle.kts (1)

6-9: LGTM! 플러그인 구성이 적절합니다.

루트 레벨에서 apply false로 플러그인을 선언하는 것은 멀티모듈 프로젝트의 표준 패턴입니다. 서브모듈에서 필요에 따라 적용할 수 있습니다.

app/src/main/java/com/smashing/app/core/designsystem/theme/Theme.kt (1)

1-1: LGTM! 패키지 재구성이 적절합니다.

디자인 시스템을 core 패키지로 이동하는 것은 클린 아키텍처 관점에서 좋은 접근입니다.

app/src/main/java/com/smashing/app/core/util/SuspendRunCatching.kt (1)

18-29: LGTM! 코루틴 취소 시맨틱을 올바르게 처리합니다.

CancellationException을 재throw하고 ensureActive()를 호출하는 구현이 정확합니다. TimeoutCancellationException을 실패로 처리하는 것도 적절한 설계 결정입니다.

app/src/main/java/com/smashing/app/presentation/dummy/HomeNavigation.kt (1)

18-41: LGTM! 네비게이션 구성이 표준 패턴을 따릅니다.

타입-안전 네비게이션을 사용하고 있으며, 더미 화면의 플레이스홀더 구현이 적절합니다.

app/src/main/java/com/smashing/app/core/common/state/UiState.kt (1)

3-15: LGTM! UI 상태 관리 패턴이 적절합니다.

Sealed interface를 사용한 타입-안전 상태 관리가 잘 구현되어 있습니다. Success의 공변성(covariance) 처리도 정확합니다.

app/src/main/java/com/smashing/app/presentation/home/navigation/HomeNavigation.kt (1)

14-29: LGTM! 네비게이션 구조가 일관되고 명확합니다.

타입-안전 네비게이션을 활용하고 있으며, innerPaddingHomeRoute에 올바르게 전달하고 있습니다.

app/src/main/java/com/smashing/app/presentation/main/MainTab.kt (1)

24-39: 동일한 route를 사용하는 탭들에 대한 주의

DUMMY, DUMMY1, DUMMY2가 모두 동일한 Dummy route를 사용합니다. find 함수 호출 시 항상 첫 번째 매칭 탭(DUMMY)만 반환되어 의도치 않은 동작이 발생할 수 있습니다. TODO 주석에 따라 추후 변경 예정이라면 현재 상태에서는 문제없습니다.

app/src/main/java/com/smashing/app/presentation/main/MainAppState.kt (1)

93-99: LGTM!

rememberMainAppState 함수가 Compose의 상태 홀더 패턴을 올바르게 따르고 있습니다. navControllercoroutineScope를 키로 사용하여 적절히 remember 처리되어 있습니다.

gradle/libs.versions.toml (1)

108-144: LGTM!

번들 정의가 잘 구성되어 있습니다. androidx-core, compose, network, hilt, debug, test 번들로 의존성을 그룹화하여 관리하기 좋습니다.

app/src/main/java/com/smashing/app/data/repository/api/DummyRepository.kt (1)

5-7: 잘 구성된 레포지토리 인터페이스입니다.

Result 타입을 사용하여 성공/실패를 명시적으로 처리하는 점이 좋습니다. 클린 아키텍처 패턴을 잘 따르고 있습니다.

app/src/main/java/com/smashing/app/data/di/dummy/DummyServiceModule.kt (1)

12-18: 표준 Hilt DI 패턴을 잘 따르고 있습니다.

Retrofit 서비스 제공을 위한 모듈 구성이 깔끔하고 적절합니다. @Singleton 스코프도 올바르게 적용되었습니다.

app/src/main/java/com/smashing/app/data/remote/datasource/api/DummyDataSource.kt (1)

5-7: 깔끔한 데이터소스 인터페이스입니다.

레이어 분리가 명확하고 suspend 함수를 사용한 비동기 처리가 적절합니다.

app/src/main/java/com/smashing/app/presentation/main/MainActivity.kt (1)

10-25: Compose 기반 액티비티 구성이 훌륭합니다.

  • @AndroidEntryPoint로 Hilt 통합
  • enableEdgeToEdge()로 최신 UI 스타일 적용
  • 상태 관리(rememberMainAppState)와 화면 구성이 적절하게 분리되어 있습니다

현대적인 Android 개발 패턴을 잘 따르고 있습니다.

app/src/main/java/com/smashing/app/data/remote/datasource/impl/DummyDataSourceImpl.kt (1)

8-14: 데이터소스 구현이 적절합니다.

서비스 계층으로의 단순 위임이 명확하며, 에러 처리는 상위 레이어(repository)에서 suspendRunCatching으로 처리되는 구조가 적절합니다.

app/src/main/java/com/smashing/app/presentation/home/HomeContract.kt (1)

8-14: 우수한 상태 관리 패턴입니다.

  • @ImmutableImmutableList 사용으로 불변성 보장
  • UiState 타입으로 로딩/성공/실패 상태를 명확히 표현
  • MVI 아키텍처 패턴을 잘 따르고 있습니다

Compose의 재구성 최적화에도 유리한 구조입니다.

app/src/main/java/com/smashing/app/data/di/dummy/DummyRepositoryModule.kt (1)

11-19: Hilt 바인딩 모듈이 올바르게 구성되었습니다.

@Binds를 사용한 인터페이스-구현체 바인딩이 효율적이고 적절합니다. DI 아키텍처가 깔끔하게 설계되었습니다.

app/src/main/java/com/smashing/app/data/di/dummy/DummyDataSourceModule.kt (1)

11-19: LGTM!

Hilt 바인딩 모듈이 올바르게 구성되어 있습니다. @Binds를 사용한 추상 메서드 패턴과 @Singleton 스코프 적용이 적절합니다.

app/src/main/java/com/smashing/app/presentation/main/MainScreen.kt (1)

19-44: LGTM!

MainScreen 구조가 잘 설계되어 있습니다. collectAsStateWithLifecycle을 사용한 라이프사이클 인식 상태 수집과 Scaffold 구성이 적절합니다.

app/src/main/java/com/smashing/app/data/repository/impl/DummyRepositoryImpl.kt (1)

10-18: LGTM!

리포지토리 구현이 깔끔합니다. suspendRunCatching을 활용한 에러 핸들링과 null-safe 매핑 처리가 적절합니다.

app/src/main/java/com/smashing/app/SmashingApplication.kt (1)

8-24: 다크 모드 지원 의도 확인

애플리케이션 초기화가 올바르게 구성되어 있습니다. 다만 Line 22에서 MODE_NIGHT_NO로 설정하여 다크 모드를 비활성화하고 있습니다. 이것이 의도된 정책인지 확인하시기 바랍니다. 최신 안드로이드 가이드라인에서는 다크 모드 지원을 권장합니다.

app/src/main/java/com/smashing/app/presentation/home/HomeScreen.kt (2)

20-34: LGTM!

HomeRoute 구성이 우수합니다. Hilt를 통한 ViewModel 주입, collectAsStateWithLifecycle을 사용한 상태 수집, 그리고 LaunchedEffect(Unit)을 활용한 초기 데이터 페치가 모두 Compose 베스트 프랙티스를 따르고 있습니다.


67-86: LGTM!

LazyColumn에서 key 파라미터를 사용하여 리컴포지션 최적화를 적용한 것이 좋습니다. 리스트 아이템 렌더링이 적절하게 구성되어 있습니다.

app/src/main/AndroidManifest.xml (1)

3-6: LGTM!

INTERNET 권한과 커스텀 Application 클래스 설정이 올바르게 구성되어 있습니다.

app/src/main/java/com/smashing/app/presentation/home/HomeViewModel.kt (1)

17-23: LGTM!

ViewModel 구조가 올바르게 설계되어 있습니다. Hilt를 통한 의존성 주입과 StateFlow를 활용한 상태 관리가 적절합니다.

app/src/main/java/com/smashing/app/data/remote/service/DummyService.kt (1)

8-14: 테스트용 더미 서비스이므로 코드 수정이 불필요합니다.

DummyService는 reqres.in의 공개 테스트 API를 위한 더미 서비스입니다. "reqres-free-v1"은 reqres.in의 무료/데모 계층을 위한 공식 문서화된 API 키이며, 공개된 테스트 API 키이므로 보안 문제가 아닙니다. 서비스 이름과 dummy 모듈 경로는 이것이 테스트 목적 코드임을 명확히 나타냅니다.

Likely an incorrect or invalid review comment.

app/src/main/java/com/smashing/app/data/remote/dto/GetUserListResponse.kt (2)

15-22: UserData 구조가 올바르게 정의되었습니다.

중첩 데이터 클래스 구조가 적절하며, API 필드명(avatar)을 의미론적으로 더 명확한 Kotlin 프로퍼티명(profileImage)으로 매핑한 것은 좋은 선택입니다.


6-14: DTO 구조가 적절하게 설계되었습니다.

API 응답 역직렬화를 위한 구조가 올바르게 구성되어 있습니다:

  • 모든 필드에 @SerialName 어노테이션을 사용하여 API 계약 안정성을 확보했습니다.
  • 외부 API 응답의 불확실성을 고려하여 모든 필드를 nullable로 처리한 것은 적절한 방어 코딩입니다.
  • DummyMapper.kt에서 Elvis 연산자(?:)와 orEmpty()를 사용하여 null 케이스를 안전하게 처리하고 있습니다.


implementation(libs.timber)
implementation(libs.lottie.compose)
ksp(libs.kotlin.metadata.jvm)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for kotlin-metadata usage in gradle files
rg "kotlin-metadata" --type=gradle -n

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Check the full build.gradle.kts file around line 92 for context
head -100 app/build.gradle.kts | tail -20

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Search for ksp declarations and their usage
rg "ksp\(" --type=gradle -B2 -A2

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 222


🏁 Script executed:

#!/bin/bash
# Find gradle files
find . -name "*.gradle.kts" -o -name "*.gradle" -o -name "*.toml" | head -20

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Search for kotlin-metadata in all files without type filter
rg "kotlin-metadata" -n

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Check the full app/build.gradle.kts to understand the dependency structure
wc -l app/build.gradle.kts

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Read the full app/build.gradle.kts file
cat -n app/build.gradle.kts

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 3220


🏁 Script executed:

#!/bin/bash
# Search for kotlin.metadata usage in source code
rg "kotlin.metadata" -n --type kotlin

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Check if there are any KSP plugins or configurations
rg "plugin.*ksp\|id.*ksp" -n --type kotlin

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check gradle/libs.versions.toml for more context on the dependency
cat gradle/libs.versions.toml

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 5307


🏁 Script executed:

#!/bin/bash
# Search for any Hilt or other annotation processor that might require kotlin-metadata-jvm
rg "Hilt\|kapt\|ksp" app/build.gradle.kts -B3 -A3

Repository: TEAM-SMASHING/SMASHING-ANDROID

Length of output: 56


kotlin-metadata-jvm을 KSP 프로세서로 사용하는 것은 잘못된 구성입니다

kotlin-metadata-jvm은 Kotlin 메타데이터에 대한 런타임 리플렉션을 위한 라이브러리로, KSP 애노테이션 프로세서가 아닙니다. 이 의존성이 필요하면 implementation(libs.kotlin.metadata.jvm)으로 변경하거나, 사용하지 않는다면 제거해야 합니다. 현재 Hilt만이 유일한 KSP 프로세서입니다.

🤖 Prompt for AI Agents
In app/build.gradle.kts around line 92, the dependency
ksp(libs.kotlin.metadata.jvm) is incorrect because kotlin-metadata-jvm is not a
KSP processor; replace that ksp(...) entry with
implementation(libs.kotlin.metadata.jvm) if you need the library at runtime, or
remove the dependency entirely if unused, and ensure only actual KSP processors
(e.g., Hilt) remain under ksp.

Comment on lines 32 to 42
val loading by rememberUpdatedState(isLoading)

val shouldLoadMore by remember {
derivedStateOf {
val total = layoutInfo.totalItemsCount
if (total == 0 || isLoading) return@derivedStateOf false

val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible + threshold >= total - 1
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

loading 변수 미사용 - 캡처된 값 불일치 가능성

rememberUpdatedStateloading 변수를 생성했지만, derivedStateOf 블록에서 isLoading 파라미터를 직접 참조하고 있습니다. 이로 인해 loading 변수가 사용되지 않고, 상태 업데이트가 올바르게 반영되지 않을 수 있습니다.

🔎 수정 제안
     val shouldLoadMore by remember {
         derivedStateOf {
             val total = layoutInfo.totalItemsCount
-            if (total == 0 || isLoading) return@derivedStateOf false
+            if (total == 0 || loading) return@derivedStateOf false

             val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
             lastVisible + threshold >= total - 1
         }
     }
📝 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 loading by rememberUpdatedState(isLoading)
val shouldLoadMore by remember {
derivedStateOf {
val total = layoutInfo.totalItemsCount
if (total == 0 || isLoading) return@derivedStateOf false
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible + threshold >= total - 1
}
}
val loading by rememberUpdatedState(isLoading)
val shouldLoadMore by remember {
derivedStateOf {
val total = layoutInfo.totalItemsCount
if (total == 0 || loading) return@derivedStateOf false
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible + threshold >= total - 1
}
}
🤖 Prompt for AI Agents
In app/src/main/java/com/smashing/app/core/extension/LazyListStateExt.kt around
lines 32 to 42, the code creates a rememberUpdatedState called loading but still
reads isLoading directly inside derivedStateOf, which can cause stale capture
issues; replace references to isLoading inside the derivedStateOf block with the
loading value provided by rememberUpdatedState (or remove rememberUpdatedState
and consistently use isLoading), ensuring the derivedStateOf reads the stable,
up-to-date value so state updates propagate correctly.

Comment on lines 12 to 14
class LocalTokenDataSourceImpl(
@TokenDataStore private val dataStore: DataStore<Preferences>,
) : LocalTokenDataSource {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

생성자에 @Inject 어노테이션 누락

AuthDataSourceModule에서 @Binds를 사용하여 이 구현체를 바인딩하는데, @Binds는 구현 클래스가 @Inject 어노테이션이 있는 생성자를 가져야 Dagger가 인스턴스를 생성할 수 있습니다. 현재 생성자에 @Inject가 없어 컴파일 오류가 발생하거나 런타임에 DI가 실패할 수 있습니다.

🔎 생성자에 @Inject 추가
+import javax.inject.Inject
+
-class LocalTokenDataSourceImpl(
+class LocalTokenDataSourceImpl @Inject constructor(
     @TokenDataStore private val dataStore: DataStore<Preferences>,
 ) : LocalTokenDataSource {
📝 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
class LocalTokenDataSourceImpl(
@TokenDataStore private val dataStore: DataStore<Preferences>,
) : LocalTokenDataSource {
import javax.inject.Inject
class LocalTokenDataSourceImpl @Inject constructor(
@TokenDataStore private val dataStore: DataStore<Preferences>,
) : LocalTokenDataSource {
🤖 Prompt for AI Agents
In
app/src/main/java/com/smashing/app/data/local/datasource/impl/TokenDatasourceImpl.kt
around lines 12 to 14, the constructor is missing the @Inject annotation
required for Dagger when using @Binds; add @Inject to the primary constructor
(e.g., annotate the constructor that receives @TokenDataStore
DataStore<Preferences>) and import the correct Inject type (javax.inject.Inject
or jakarta.inject.Inject consistent with the project), then rebuild to ensure
Dagger can construct and bind this implementation.

Copy link
Collaborator

@oilbeaneda oilbeaneda left a comment

Choose a reason for hiding this comment

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

리드님 넘 수고하셨습니다 🤩아자스!!!!!

import androidx.compose.ui.composed

/**
* 리플 효과 없이 클릭 가능하게 만드는 Modifier
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 리플효과 없이 클릭 or 있게 클릭 여부는 개발 단계에서 디자이너 분들이 정해주시는지 아니면 프론트에서 ux를 고려해서 정하는지 궁금합니다!

Copy link
Collaborator

@vahkjsdf vahkjsdf left a comment

Choose a reason for hiding this comment

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

수고 많으셨습니다!ㅎㅎ 앱잼 화이팅👍👍

Comment on lines 24 to 25
fun fetchDummyUsers() = viewModelScope.launch {
dummyRepository.fetchDummyUserList(page = 1).onSuccess { userList ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

loading 상태를 추가하려면

Suggested change
fun fetchDummyUsers() = viewModelScope.launch {
dummyRepository.fetchDummyUserList(page = 1).onSuccess { userList ->
fun fetchDummyUsers() = viewModelScope.launch {
updateDummyUiState(UiState.Loading)
dummyRepository.fetchDummyUserList(page = 1).onSuccess { userList ->

여기에 이런식으로 쓰는게 맞을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네 맞습니다 ㅎ.. Loading 부분 추가 되어야하네용

* @param timeout 구독 해제 후 업스트림 취소까지의 대기 시간 (기본: 5000ms)
* @return Hot StateFlow
*/
fun <T> Flow<T>.stateInWhileSubscribed(
Copy link
Collaborator

Choose a reason for hiding this comment

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

상태 값 연산은 cold Flow로 처리하고 최종 값을 hot Flow로 변환해서 상태를 공유할 수 있도록 하되 SharingStarted.WhileSubscribed를 통해 구독자가 없을 때만 업스트림 collect를 중단하는 걸로 이해했는데.. 이게 맞을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

state in 같은 경우는 collect(구독자) 가 없는 경우에 원본(업스트림) flow 에 대해서 cancel 을 수행한다고 보시면 됩니다 !

만약, ui 에서 collect 하고 있는 경우에 화면회전이 일어나면 collect 는 제거 되어서 일반적인 경우라면 collect 가 취소되었기에 다시 flow 를 수행하겠지만, state in 에서 time out을 사용하면 flow 를 처음부터 다시 시작하지 않고 5초 이내로 flow 를 유지합니다. 이 5초의 의미는 메인스레드가 block 할 수 있는 최대 시간이 5초이기에 해당 시간으로 설정했습니다.

++어제 이야기 하던 걸 마저 하자면
cold flow는 collect 호출마다 새로 실행됩니다. 따라서, 그대로 사용하게 될 경우 각각의 collect 를 수행하는 곳에서 중복 실행이 됩니다. 예를 들어 api 호출 하는 로직을 다른곳에서 동시에 collect 하고 있다한다면 api 호출은 동시에 두군데에서 일어나게 되는 것이죠.

이와 반대로 hot flow 는 하나의 flow 를 가지고 여러 곳에서 collect 하고 공유하기 때문에 api 호출은 한번만 일어나고 각각의 emit 으로 받은 상태값을 사용해 Ui 를 업데이트 합니다.

다음 링크들을 참고하면 더 좋을거 같아요.
android stateIn
stateIn 관련 설명글

Copy link
Collaborator

@ShinHyeongcheol ShinHyeongcheol left a comment

Choose a reason for hiding this comment

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

기본 세팅 수고하셨습니다!!
중간에 이해한게 맞는지 코멘트 하나 남겼어요.

Comment on lines +21 to +22
private val _uiState = MutableStateFlow(HomeContract.State())
val uiState = _uiState.asStateFlow()
Copy link
Collaborator

Choose a reason for hiding this comment

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

공통 UiState나 Contract를 사용하지 않아서 이 부분 질문드리는데,
SideEffect와 같은 경우에는 HomeContract에 sealed class Effect{}를 만들고
private val _effect = MutableSharedFlow<HomeContract.Effect>처럼 사용하면 되는걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

네 저번 1차 회의 떄 말씀 드렸던 패턴을 사용하는데요!

아래 링크 노션에 적혀있는 부분을 봐주시면 더 좋을거 같습니다 !
노션 링크

@seungjunGong seungjunGong merged commit c0012f3 into develop Jan 2, 2026
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 5, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

➕ ADD 부수적인 코드 추가 및 라이브러리 추가, 새로운 파일 생성

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ADD] 초기 세팅

5 participants